[우아한테크코스] 7월 19일 TIL

2 minute read

[JPA] 양방향 매핑

회원 -> 팀: 다대일 단방향 매핑
팀 <-> 회원: 양방향 연관관계

회원에서 팀에 접근할 수 있고, 팀에서도 회원에 접근할 수 있는 것이 양방향 연관관계이다.
JPA에서는 Collection, Set, Map 모두 사용 가능하다.

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
엔티티를 단방향 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리할 수 있다.
하지만 양방향 매핑이 되면 서로 참조하므로 연관관계를 두 군데서 관리해줘야 한다.
이 때 정하는게 연관관계의 관리자로 두 엔티티 중 하나가 외래키를 관리하도록 하는 것이다.

mappedBy라는 속성을 통해 주인이 누구인지 지정해줄 수 있다.
주인은 mappedBy를 사용하지 않고 지정받기만 한다.
주인은 외래키를 가지고 있다.
대체로 일대다에서 다가 주인이 된다.

양방향에서는 두 객체에 모두 값을 저장해줘야한다는 것을 잊으면 안된다.
이 때 각 객체에서 메서드를 호출하기 보다는 하나의 메서드에서 해결하도록 하는 것이 좋다.
연관관계가 수정될 때도 기존 관계를 제거하는 코드를 추가해야한다.

양방향 연관관계의 장점은 반대로 객체를 탐색할 수 있다는 것 뿐이다.
단방향 매핑만으로도 연관관계 매핑은 완료된 것이다.
복잡한 양방향 매핑은 필요할 때 적용해도 된다.

[JPA] n+1 문제

메서드마다 @Transactional을 붙여줌으로써 하나의 영속성 컨텍스트를 생성한다.
보통 트랜잭션은 서비스 레이어까지 관리되므로 컨트롤러, 뷰 레이어에서 엔티티는 준영속 계층이 된다.
준영속 상태에서는 지연 로딩과 변경 감지가 이뤄지지 않는다.
이 때, 글로벌 페치 전략 수정, JPQL 페치 조인, 강제 초기화하거나, OSIV를 사용해 항상 영속 상태로 만들어 해결할 수 있다.

  • 글로벌 페치 전략
    지연 로딩을 즉시 로딩으로 수정한다.
    엔티티에 있는 fetchType을 수정하면 어플리케이션 전체에서 사용되므로 글로벌 페치 전략이라고 한다.
    하지만 호출하지 않을 때도 사용하지 않는 엔티티를 로딩한다는 단점과 n+1 문제가 발생한다는 단점이 있다.

  • n+1 문제
    JPA가 JPQL을 분석할 때는 글로벌 페치에 관심 없이 JPQL을 사용한다.
    따라서 만약 order 엔티티가 10개면 member 조회도 10번 실행해야한다. 쓸데 없이! 성능에 극악이다.

  • JPQL 페치 조인
    이는 JPQL만 페치 조인을 사용해 해결할 수 있다.
    하지만 이 과정에서 레파지토리 메서드가 증가하고 데이터 접근 계층을 침범한다는 단점이 있다.

  • 강제 초기화
    FACADE 계층을 추가해 서비스와 컨트롤러 사이에 논리적인 의존성을 분리할 수 있다.
    뷰를 위한 프록시 초기화를 담당하는 것이다.
    컨트롤러 계층에 필요한 프록시 객체를 강제로 초기화한다.
    하지만 잘 쓰려면 이 계층에 모든 조회 메서드가 존재해야 한다.

  • OSIV
    영속성 컨텍스트를 뷰까지 열어둔다. 이를 통해 뷰에서도 지연로딩을 사용할 수 있게 된다.
    하지만 컨트롤러에서 데이터를 바꿔버리는 것은 곤란하니까 엔티티를 인터페이스로 제공, 엔티티 래핑, dto 반환 등의 방법을 통해 수정을 막을 수 있다.

  • Spring OSIV
    필터나 인터셉터에서 적용할 수 있다.
    1. 클라이언트의 요청이 들어오면 필터나 인터셉터에서 영속성 컨텍스트 생성
    2. 서비스에서 시작할 때 영속성 컨텍스트와 함께 트랜잭션 시작
    3. 서비스 끝나면 트랜잭션 커밋하고 영속성 컨텍스트 플러시, 영속성 컨텍스트 종료되지 않음
    4. 컨트롤러와 뷰까지 유지된 영속성 컨텍스트로 엔티티는 계쏙 영속 상태
    5. 또 필터나 인터셉터로 요청 들어오면 이전 영속성 컨텍스트 종료, 플러시 호출하지 않음
      결국 엔티티 수정은 트랜잭션이 있는 계층에서만 동작한다.
      하지만 같은 영속성 컨텍스트가 공유될 수 있다.
      너무 엄격한 계층을 지킬 필요가 있나? 생각해볼 필요가 있다.
  • 즉시로딩과 n+1
    JPQL로 실행하면 JPA는 그냥 sql를 생성해서 실행한다.
    하지만 즉시로딩이 걸려있어 관련 로직을 그대로 또 쓸데없이 수행해야한다.
    실행한 sql 결과만큼 추가로 sql 실행한다.
    OneToOne, ManyToOne의 기본은 즉시로딩이다.

  • 지연로딩과 n+1
    지연로딩을 사용하면 db에서 회원만 조회될 수 있다.
    하지만 모든 회원에 대한 주문을 조회하고자 할 때 n+1 문제가 발생한다.
    주로 지연로딩을 사용하도록 하자.
    OneToMany, ManyToMany의 기본은 지연로딩이다.

  • 페치조인을 사용한 n+1 해결

  • 하이버네이트의 @BatchSize 사용
    연관된 엔티티 조회 시 지정한 size만큼만 조회한다.
    @BatchSize(size = 5)