
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