[Spring] 연관관계 매핑 @ManyToOne 어노테이션 1

류재성's avatar
Mar 18, 2024
[Spring] 연관관계 매핑 @ManyToOne 어노테이션 1
 
💡
@ManyToOne 어노테이션은 JPA에서 엔티티 간의 관계를 매핑할 때 사용하는 애너테이션 중 하나이다. 이 애너테이션은 "다대일"(Many-to-One) 관계를 나타내는데 사용된다. 즉, 하나의 엔티티가 다른 엔티티와 다대일 관계에 있다는 것을 선언할 때 @ManyToOne 애너테이션을 사용한다.
 
User Entity
@NoArgsConstructor // 빈생성자가 필요 @Entity @Data @Table(name = "user_tb") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id ; @Column(unique = true) private String username ; private String password; private String email; @CreationTimestamp // persistance centext 에 전달될 때 자동으로 주입됨. private Timestamp createdAt; @Builder //빌더 패턴,엔티티에는 다 걸기 public User(int id, String username, String password, String email, Timestamp createdAt) { this.id = id; this.username = username; this.password = password; this.email = email; this.createdAt = createdAt; } }
 
Board Entity
@NoArgsConstructor // 빈생성자가 필요 @Entity @Data @Table(name = "board_tb") public class Board { @Id @GeneratedValue (strategy = GenerationType.IDENTITY) private Integer id; private String title; private String content; //@JoinColumn(name="user_id") 변수명을 직접 지정 가능 @ManyToOne(fetch = FetchType.LAZY) private User user ; // 변수명이 user. user_id 를 만들어줌 @CreationTimestamp // persistance centext 에 전달될 때 자동으로 주입됨. private Timestamp createdAt; @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; } }
 
💡
User 엔티티와 Board 엔티티가 있다. Board 엔티티 필드에 User 객체가 포함되어 있다. 이 두 엔티티의 매핑을 도와주는게 @ManyToOne 어노테이션이다.
 

1. Fetch 속성

 
@ManyToOne 애너테이션에서 fetch 속성은 연관된 엔티티를 어떻게 로딩할지 결정하는 옵션이다.
 
  • Eager
  • Lazy
  • Lazy Loarding
  • default_batch_fetch_size
 

2. Eager 속성

 
notion image
 
 
💡
fetch 의 기본 속성은 Eager 로 정해져있다. Eager는 연관된 엔티티의 데이터를 처음 엔티티를 로딩할 때 함께 로딩하
 
BoardRepository
public Board findById(int id){ Board board = em.find(Board.class,id); return board; }
 
 
JUnit 테스트
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.context.annotation.Import; @Import(BoardReposiroty.class) @DataJpaTest public class BoardRepositoryTest { @Autowired private BoardReposiroty boardReposiroty; @Test public void findById_test(){ int id = 1 ; boardReposiroty.findById(1); } }
 
notion image
 
💡
실행시 Board 엔티티와 User 엔티티가 자동으로 조인된다. 매우 편리한 방식이지만 불필요한 데이터까지 로딩되어 성능 저하를 일으킬 수 있다.
 
 

2. Lazy 속성

 
notion image
 
 
fetch 속성을 Lazy 로 변경 후 동일한 JUnit 테스트를 실행한다.
 
JUnit 테스트
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.context.annotation.Import; @Import(BoardReposiroty.class) @DataJpaTest public class BoardRepositoryTest { @Autowired private BoardReposiroty boardReposiroty; @Test public void findById_test(){ int id = 1 ; boardReposiroty.findById(1); } }
 
notion image
 
 
💡
JOIN이 발생하지 않고 Board 엔티티만 조회가 된다.
 

3.Lazy Loading

 
💡
Lazy 속성에선 Board 엔티티와 User 엔티티가 자동으로 조인되지 않기 때문에 만약 User 엔티티의 id를 제외한 다른 데이터는 null 값이 예상된다.
 
@Test public void findById_test(){ int id = 1 ; System.out.println("start-1"); Board board = boardReposiroty.findById(1); System.out.println("start-2"); System.out.println(board.getUser().getId()); }
 
notion image
 
Lazy 속성을 사용하면 Board 엔티티를 조회했을 때 User 엔티티의 id를 조회할 수 있다.
 
이번에는 User 엔티티의 PK(id) 를 제외한 다른 데이터를 조회해본다.
 
@Test public void findById_test(){ int id = 1 ; Board board = boardReposiroty.findById(id); System.out.println("start-2"); System.out.println(board.getUser().getId()); System.out.println("start-3"); System.out.println(board.getUser().getUsername()); } }
 
 
notion image
 
User 엔티티의 username을 조회했을 때 null 값을 예상했지만, User 엔티티의 필터가 조회되는 시점에 쿼리가 한 번 더 전송되면서 데이터가 조회가 된다.
 
💡
연관된 객체가 Lazy 상태일 때, 연관된 객체의 pk가 아닌 필드에 접근할 때 한번 더 조회 쿼리가 늦게 발동된다. Lazy 속성 역시 불필요한 데이터를 조회해 성능 저하가 발생할 수 있다.
 

4. 어떤 전략을 사용해야 할까?

 
💡
fetch 의 속성은 Lazy 를 사용, 조인이 필요하다면 직접 JPQL 을 사용하자.
 
BoardRepository
public Board findByIdJoinUser(int id){ //join fetch 를 하면 b 를 조회했을 때 user의 객체도 같이 조회된다. Query query = em.createQuery("select b from Board b join fetch b.user u where b.id =:id",Board.class); query.setParameter("id",id); Board board = (Board) query.getSingleResult(); return board; }
 
Board 엔티티와 User 엔티티가 조인이 필요할 때 조인 쿼리를 직접 작성한다.
 
💡
JPQL 을 사용해 쿼리를 작성할 때 join fetch 를 사용해야 조인된 전체 필드가 조회된다. 만약 fetch 가 없다면 드라이빙 테이블의 필드만 select 절에 조회된다.
 
JUnit 테스트/ fetch 미사용
notion image
 
 
JUnit 테스트/ fetch 사용
notion image
 
 
BoardController
@GetMapping("/board/{id}") public String detail(@PathVariable Integer id,HttpServletRequest request) { // int 를 쓰면 값이 없으면 0, Integer 를 넣으면 값이 없을 때 null 값이 들어옴. Board board = boardReposiroty.findByIdJoinUser(id); request.setAttribute("board",board); return "board/detail"; }
 
Board/detail.mustache
<div class="container p-5"> <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <a href="/board/{{board.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/board/{{board.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> <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>
 
💡
Board 엔티티의 User 오브젝트에 username이 있기 때문에 {{board.user.username}} 를 적는다.
 
notion image
Share article

{CODE-RYU};