
1. spring.jpa.open-in-view
- 스프링 부트에서 JPA를 사용할 때의 설정 중 하나
 
- 기본적으로 활성화 : view까지 끌고 옴
 
- HTTP 요청이 완료될 때까지 영속성 컨텍스트를 열어두는 역할
 
       영속성 컨텍스트
       엔티티 객체를 데이터베이스 테이블과 매핑해주는데 사용되는 캐시와 유사한 개념
       트랜잭션 내에서 엔티티 객체의 상태를 추, 엔티티 객체의 변경을 데이터베이스에 반영
- 활성화 상태 : 영속성 컨텍스트가 HTTP 요청의 끝까지 유지
 
      뷰 렌더링 동안에도 영속성 컨텍스트가 열려 있는 상태를 유지, 지연로딩(Lazy Loading)이 가능
       지연로딩
        JPA에서 연관된 엔티티를 실제로 사용하기 직전까지 데이터베이스에서 로딩을 지연
       성능을 향상시키고 메모리 사용을 최적화할 수 있도록 도와줌 
       발생하는 시점이 영속성 컨텍스트가 열려 있는 동안에만 가능
        영속성 컨텍스트를 종료하기 전까지 지연로딩된 엔티티를 접근하는 것이 중요
- 비활성화 : sevice가 종료될 때 끝남
 

2. 목록 보기
- 댓글 3개, board 정보(제목, 내용), user정보(username)의 관계 : N대 1대 1
 
3. 댓글 정보 추가하기 - 전략
- user는 one 관계 - > join
 
      board는 many 관계 -> 조회 1번 더하기 -> 객체 2개 -> DTO에 담기
- many 관계 -> 양방향 맵핑
 
3가지 전략
- One 관계는 조인, Many 관계는 Lazy Loading
 
- One 관계는 조인,
 
      Many 관계를 페이징해야 된다면, 직접 쿼리를 만들어서 두 번 조회
- One 관계와 Many 관계를 한번에 조인
 
- 단위 테스트하기
 
package shop.mtcoding.blog.reply;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
/* 2가지 전략
 * 1. user는 one 관계 - > join
 *    board는 many 관계 -> 조회 1번 더하기 -> 객체 2개 -> DTO에 담기
 * 2. many 관계 -> 양방향 맵핑
 * */
@DataJpaTest
public class ReplyJPARepositoryTest {
    @Autowired
    private ReplyJPARepository replyJPARepository;
    @Test
    public void findByBoardId_test() {
        // given
        int boardId = 4;
        // when
        List<Reply> replyList = replyJPARepository.findByBoardId(boardId);
        // then
        System.out.println(replyList.size());
    }
}4. 댓글로 페이징하는 방법
- 댓글 페이징 → join해서 하는 쿼리가 매우 힘듦
 
       댓글 페이징 쿼리를 따로 짜서 조회를 따로해서 DTO로 합치기
5. 양방향 매핑하기

- 컬렉션, 객체 타입은 필드화 될 수 없음
 
- 역방향의 필드를 적어서 조회할 때 담는 용도로만 사용하기!
 
       board와 user join해서 한번 담고 reply 조회해서 한번 담기
- @OneToMany : 반대 방향
 
       (mappedBy = "board") : 필드화될 수 없으니까 외래키의 주인(entity 객체)의 필드명 알려주기 
- 기본 디폴트 설정 Many To One : EAGER
 
      One To Many : Lazy
package shop.mtcoding.blog.board;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIncludeProperties;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import shop.mtcoding.blog.reply.Reply;
import shop.mtcoding.blog.user.User;
import shop.mtcoding.blog._core.utils.MyDateUtil;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
@NoArgsConstructor
@Data // 변경되는 데이터에만 setter가 필요함
@Table(name = "board_tb")
@Entity
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String title;
    private String content;
    // private String username;
    // username은 user_tb에도 있기에 외래키로 연관관계 맺어야 함
    // @JoinColumn(name = "user_id") 직접 지정할 때
    @ManyToOne(fetch = FetchType.LAZY) // ORM 할 것이기에 user 객체 필요
    private User user; // DB에 컬럼명 : user_id (변수명_PK키)
    @CreationTimestamp // PC로 인해 DB에 INSERT될 때 날짜 주입
    private Timestamp createdAt;
    @OneToMany(mappedBy = "board") // 반대방향 -> 필드화될 수 없으니까 외래키의 주인(entity 객체)의 필드명 알려주기
    private List<Reply> replies = new ArrayList<>(); // 테이블은 컬렉션이 필드화될 수 없어서 오류가 남
    @Transient // 테이블 생성이 안됨
    private boolean isOwner;
    public void update(){
    }
    @Builder
    public Board(Integer id, String title, String content, User user, Timestamp createdAt) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.user = user;
        this.createdAt = createdAt;
    }
    public String getBoardDate(){
        return MyDateUtil.timestampFormat(createdAt);
    }
}package shop.mtcoding.blog.Board;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.domain.Sort;
import shop.mtcoding.blog.board.Board;
import shop.mtcoding.blog.board.BoardJPARepository;
import shop.mtcoding.blog.user.User;
import java.util.List;
import java.util.Optional;
@DataJpaTest
public class BoardJPARepositoryTest {
    @Autowired
    private BoardJPARepository boardJPARepository;
    // findByIdJoinUser
    @Test
    public void findByIdJoinUser_test() {
        // given
        int id = 1;
        // when
        Optional<Board> board = boardJPARepository.findByIdJoinUser(id);
        // then
    }
}
package shop.mtcoding.blog.board;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface BoardJPARepository extends JpaRepository<Board, Integer> {
    @Query("select b from Board b join fetch b.user u where b.id = :id")
   Optional<Board> findByIdJoinUser(@Param("id") int id);
    @Query("select b from Board b join fetch b.user u join fetch b.replies r where b.id = :id")
    Optional<Board> findByIdJoinUserAndReply(@Param("id") int id);
}
- 단위 테스트하기
 
package shop.mtcoding.blog.Board;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.domain.Sort;
import shop.mtcoding.blog.board.Board;
import shop.mtcoding.blog.board.BoardJPARepository;
import shop.mtcoding.blog.user.User;
import java.util.List;
import java.util.Optional;
@DataJpaTest
public class BoardJPARepositoryTest {
    @Autowired
    private BoardJPARepository boardJPARepository;
       @Test
        public void findByIdJoinUserAndReplies_test() {
        // given
        int id = 4;
        // when
        Optional<Board> board = boardJPARepository.findByIdJoinUserAndReplies(id);
        // then
        System.out.println("findByIdJoinUser_test : " + board);
    }
}
- 현재 테이블 현황
 
- id(PK)는 다 index
 
User  | ㅤ  | 
userId  | username  | 
1  | ssar  | 
2  | cos  | 
3  | love  | 
Board  | ㅤ  | ㅤ  | 
boardId  | title  | content  | 
1  | 제목1  | 내용1  | 
2  | 제목2  | 내용2  | 
3  | 제목3  | 내용3  | 
4  | 제목4  | 내용4  | 
Reply  | ㅤ  | ㅤ  | ㅤ  | 
replyId  | comment  | userId  | boardId  | 
1  | 댓글1  | 4  | 1  | 
2  | 댓글2  | 4  | 1  | 
3  | 댓글3  | 4  | 2  | 
4  | 댓글4  | 3  | 2  | 
6. 조회 2번 하는 것부터 시작
- 드라이빙 : board , 드리븐 : user
 
- findByIdJoinUser 쿼리
 
@Query("select b from Board b join fetch b.user u where b.id = :id")
Optional<Board> findByIdJoinUser(@Param("id") int id);b.id  | b.title  | b.user  | u.id  | u.username  | ㅤ  | 조회수  | 
1  | 제목1  | 1  | 1  | ssar  | ㅤ  | 2  | 
2  | 제목2  | 1  | 1  | ssar  | ㅤ  | 2  | 
3  | 제목3  | 2  | 2  | cos  | ㅤ  | 2  | 
4  | 제목4  | 3  | 3  | love  | ㅤ  | 2  | 
7. 한 번에 JOIN
- join한 결과를 가지고 다시 join하는 것
 
@Query("select b from Board b join fetch b.user u left join fetch b.replies r where b.id = :id")
Optional<Board> findByIdJoinUserAndReplies(@Param("id") int id);b.id  | b.title  | b.user  | u.id  | u.username  | ㅤ  | 조회수  | 
1  | 제목1  | 1  | 1  | ssar  | ㅤ  | 2  | 
2  | 제목2  | 1  | 1  | ssar  | ㅤ  | 2  | 
3  | 제목3  | 2  | 2  | cos  | ㅤ  | 2  | 
4  | 제목4  | 3  | 3  | love  | ㅤ  | 2  | 
Reply  | ㅤ  | ㅤ  | ㅤ  | 
replyId  | comment  | userId  | boardId  | 
1  | 댓글1  | 4  | 1  | 
2  | 댓글2  | 4  | 1  | 
3  | 댓글3  | 4  | 2  | 
4  | 댓글4  | 3  | 2  | 
b.id  | b.title  | b.user  | u.id  | u.username  | ㅤ  | 조회수  | r.id  | r.comment  | r.userId  | r.boardId  | 조회수  | 
1  | 제목1  | 1  | 1  | ssar  | ㅤ  | 2  | ㅤ  | ㅤ  | ㅤ  | ㅤ  | 5  | 
2  | 제목2  | 1  | 1  | ssar  | ㅤ  | 2  | ㅤ  | ㅤ  | ㅤ  | ㅤ  | 5  | 
3  | 제목3  | 2  | 2  | cos  | ㅤ  | 2  | 4  | 댓글4  | 3  | 2  | 5  | 
4  | 제목4  | 3  | 3  | love  | ㅤ  | 2  | 1  | 댓글1  | 4  | 1  | 5  | 
4  | 제목4  | 3  | 3  | love  | ㅤ  | 2  | 2  | 댓글2  | 4  | 1  | ㅤ  | 
4  | 제목4  | 3  | 3  | love  | ㅤ  | 2  | 3  | 댓글3  | 4  | 2  | ㅤ  | 
- 결과
 
- join 28번 // 최악!
 
- 드라이빙 테이블 변경 16번
 
- where 먼저 걸고 join 12번 , 액세스 1번 // 베스트!
 
       JPA로 안됨, 네이티브 쿼리로 해야 함
@Query("select r from Reply r right join (select b from Board b join fetch b.user u left join fetch b.replies r where b.id = :id)")
Optional<Board> findByIdJoinUserAndRepliesv2(@Param("id") int id);- select 2번 12번, 액세스 2번
 
      유지 보수가 편함
{{> /layout/header}}
<div class="container p-5">
    {{#isOwner}}
        <!-- 수정삭제버튼 -->
        <div class="d-flex justify-content-end">
            <!-- Post 요청-> 해당 페이지로 이동-->
            <a href="/board/{{board.id}}/update-form" class="btn btn-warning me-1">수정</a>
            <!-- Post 요청 -> 해당 페이지 삭제 -->
            <form action="/board/{{board.id}}/delete" method="post">
                <button class="btn btn-danger">삭제</button>
            </form>
        </div>
    {{/isOwner}}
    <div class="d-flex justify-content-end">
        <b>작성자</b> : {{board.user.username}}
    </div>
    <!-- 게시글내용 -->
    <div>
        <h2><b>{{board.title}}</b></h2>
        <hr/>
        <div class="m-4 p-2">
            {{board.content}}
        </div>
    </div>
    <!-- 댓글 -->
    <div class="card mt-3">
        <!-- 댓글등록 -->
        <div class="card-body">
            <form action="/reply/save" method="post">
                <textarea class="form-control" rows="2" name="comment"></textarea>
                <div class="d-flex justify-content-end">
                    <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button>
                </div>
            </form>
        </div>
        <!-- 댓글목록 -->
        <div class="card-footer">
            <b>댓글리스트</b>
        </div>
        <div class="list-group">
            {{#board.replies}}
            <!-- 댓글아이템 -->
            <div class="list-group-item d-flex justify-content-between align-items-center">
                <div class="d-flex">
                    <div class="px-1 me-1 bg-primary text-white rounded">{{user.username}}</div> <!--lazyloarding이 일어나면서 user만 selecting 됨-->
                    <div>{{comment}}</div>
                </div>
                <form action="/reply/{{id}}/delete" method="post">
                    <button class="btn">🗑</button>
                </form>
            </div>
            {{/board.replies}}
        </div>
    </div>
</div>
{{> /layout/footer}}package shop.mtcoding.blog.board;
import ch.qos.logback.core.model.Model;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import shop.mtcoding.blog._core.errors.exception.Exception403;
import shop.mtcoding.blog._core.errors.exception.Exception404;
import shop.mtcoding.blog.user.User;
import java.lang.annotation.Native;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class BoardController {
    private final HttpSession session;
    private final BoardService boardService;
    // @Transactional 트랜잭션 시간이 너무 길어져서 service에 넣어야함
    @PostMapping("/board/{id}/update")
    public String update(@PathVariable Integer id, BoardRequest.UpdateDTO reqDTO) {
        User sessionUser = (User) session.getAttribute("sessionUser");
        boardService.update(id, sessionUser.getId(), reqDTO);
        return "redirect:/board/" + id;
    }
    @GetMapping("/board/{id}/update-form")
    public String updateForm(@PathVariable(name = "id") Integer id, HttpServletRequest request) {
        User sessionUser = (User) session.getAttribute("sessionUser");
        Board board = boardService.updateForm(id, sessionUser.getId());
        request.setAttribute("board", board);
        return "/board/update-form"; // 서버가 내부적으로 index를 요청 - 외부에서는 다이렉트 접근이 안됨
    }
    @PostMapping("/board/{id}/delete")
    public String delete(@PathVariable Integer id) {
        User sessionUser = (User) session.getAttribute("sessionUser");
        boardService.delete(id, sessionUser.getId());
        return "redirect:/";
    }
    @GetMapping("/")
    public String index(HttpServletRequest request) {
        List<Board> boardList = boardService.findAll();
        request.setAttribute("boardList", boardList);
        return "index"; // 서버가 내부적으로 index를 요청 - 외부에서는 다이렉트 접근이 안됨
    }
    @PostMapping("/board/save")
    public String save(BoardRequest.SaveDTO reqDTO) {
        User sessionUser = (User) session.getAttribute("sessionUser");
        boardService.save(reqDTO, sessionUser);
        return "redirect:/";
    }
    @GetMapping("/board/save-form")
    public String saveForm() {
        return "board/save-form";
    }
    @GetMapping("/board/{id}")
    public String detail(@PathVariable Integer id, HttpServletRequest request) {
        User sessionUser = (User) session.getAttribute("sessionUser");
        Board board = boardService.detail(id, sessionUser);
        request.setAttribute("board", board);
        System.out.println("서버 사이드 랜더링 직전에는 board와 user만 조회된 상태이다...이 로고가 찍힌 후 Lazyloarding이 이뤄짐");
        return "board/detail";
    }
}
- 커넥션을 끈지 않고 뷰 랜더링할 때 까지 커넥션을 유지함 → Lazyloarding 가능
 




Share article