[Spring] JPA 연관관계 매핑, 최적화 적용

easyone·2025년 11월 19일

Spring

목록 보기
15/18

UMC 4주차 시니어 미션 진행합니다.

성능을 고려한 연관관계 매핑 & 최적화 적용

  • @OneToMany 컬렉션을 조회할 때 List<MemberPrefer>Set<MemberPrefer>로 변경 후 차이점 분석
  • 데이터 정합성을 고려하여 orphanRemoval = true가 필요한 곳 확인 후 적용

성능을 고려한 연관관계 매핑 & 최적화 적용

List vs Set 비교해보기

  • List - 순서 보장

    • 일반적으로 순서가 보장되어, 클라이언트에 저장 순서대로 그대로 응답할 때에 편리하고 직관적임

    • 게시글과 같이 최신순으로 저장하고 조회하는 요구사항이 일반적일 경우 List를 사용

    • List 타입 컬렉션을 2개 이상 Fetch join할 경우, 예외가 발생함 :MultipleBagFetchException 발생

      • Bag: 순서가 없으며 중복도 허용하는 컬렉션
      • Hibernatesms List를 내부적으로 Bag으로 취급하는데, 카타시안 곱 문제가 생길 수 있음 → user
      • Hibernate 내부에서 리스트를 중복 제거 없이 매핑하려 할 때, 조인 결과가 증가
    • Featch join은 꼭 필요한 연관 객체를 1개만 사용하도록 하고, 나머지는 @BatchSize를 사용하여 지연 로딩 시에 N+1문제를 완화하거나, 전역으로 batch_fetch_size를 설정하는 방법이 있음

    • 다음과 같이 해결 가능

          @BatchSize(size = 100)
          @OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
          private List<UserCategory> userCategories;
  • Set - 순서 보장 안됨

    • 순서가 보장이 안되어있으므로, 순서 구분이 무의미할 때 사용해도 된다.
    • 중복이 안되기 때문에 중복을 자동으로 방지해야 할 때 사용하면 좋다.
    • List 타입 컬렉션을 2개 이상 Fetch join할 경우, 예외가 발생함 :MultipleBagFetchException 발생한다고 했는데.. Set으로 하면 중복제거가 자동으로 되기 때문에, 해당 예외가 발생하지 않는다.
  • orphanRemoval = true

    
     @BatchSize(size = 100)
     @OneToMany(mappedBy = "user", orphanRemoval = true)
     private List<UserCategory> userCategories;

    연관관계가 끊긴 엔티티에 대해서 REMOVE 작업을 진행하고, 전파할 지에 대한 옵션

    true로 설정해두면, 연관관계가 끊어진 고아 객체를 자동으로 삭제, 부모 엔티티가 삭제되지 않아도, 연관관계만 끊으면 삭제되는 옵션이다.

    다음과 같이 연관관계를 끊으면, usercategory가 DB에서 삭제된다.

    // 연관관계 끊기
    user.getUserCategories().remove(userCategory);
  • cascade = CascadeType.REMOVE와의 차이?

    // 부모 삭제 -> 해당 user를 부모로 가지고 있는 userCategory가 모두 DELETE됨
    userRepository.delete(user); 
    
    // 연관관계만 끊을 경우에는 DB에 여전히 남아있음

    연관관계를 끊는것만으로는 삭제가 안됨, 부모 엔티티가 삭제될 때 자식도 함께 삭제된다.

  • orphanRemoval = true 사용해야 하는 곳

    • userCategory같은 중간 테이블에서 사용해야 한다. 카테고리와 user 간의 연관관계가 끊기면 삭제하도록 해야 한다. PERSIST 사용 시 자동 저장되도록 하기 때문에 일반적으로 둘 다 사용한다.

          @OneToMany(mappedBy = "user", 
                     cascade = CascadeType.PERSIST, 
                     orphanRemoval = true)
          private List<UserCategories> userCategories = new ArrayList<>();
    • 중간 테이블의 삭제 흐름

      유저 조회 → 카테고리 조회 → 선호 카테고리 삭제 
      SELECT * FROM user WHERE id = 1;
      SELECT * FROM category WHERE id = 5;
      // 삭제 호출 시, 지연로딩이 된다. 
      SELECT * FROM user_category WHERE user_id = 1;

      여기서 만약에 BatchSize를 안쓰면, user가 100명이라고 할 때 각 유저의 유저 카테고리를 조회하면..

      BatchSize 사용 안할경우

      -- 1: User 조회
      SELECT * FROM user;  -- 100-- N:UserUserCategory 조회
      SELECT * FROM user_category WHERE user_id = 1;   -- User 1
      SELECT * FROM user_category WHERE user_id = 2;   -- User 2
      SELECT * FROM user_category WHERE user_id = 3;   -- User 3
      ...
      SELECT * FROM user_category WHERE user_id = 100; -- User 100
      
      --101번의 쿼리 발생, N+1 문제..

      user 조회를 하고, 각 유저의 연관 데이터도 조회를 하게 된다.

    • @BatchSize 를 사용한다면..?

    batchsize는 n개씩 묶어서 조회를 할거다라고 명시를 해주는 어노테이션이다. 즉 batchsize가 0이라고 하면, 10명씩 묶어서 IN 쿼리를 사용해서 조회하게 된다. User를 접근한다고 할 때, 위의 쿼리처럼 하나하나씩 접근하지 않고 한번에 쿼리를 날리게 된다.

    BatchSize사용할 경우

User 1-10SELECT ... WHERE user_id IN (1,2,3,...,10)
User 11-20SELECT ... WHERE user_id IN (11,12,...,20)
profile
백엔드 개발자 지망 대학생

0개의 댓글