[Spring Data JPA Tutorial] 10. LazyInitializationException 해결하기 2. @OneToMany

2022. 4. 15. 16:55Spring/Spring Data JPA Tutorial

반응형
  1. 사전작업
    1. Entity 수정
      1. User
      2. Store
    2. api 테스트
  2. LazyInitializationException 해결
    1. @JsonIgnore
    2. Transactional 내부에서 연관관계 미리 조회 권장
    3. Entity 설정에서 FetchType.EAGER로 설정
    4. EntityGraph
    5. fetch join
  3. N+1 문제 해결
    1. Transactional 내부에서 연관관계 미리 조회
      1. Service
      2. batch size 미적용시
      3. batch size 적용
        1. @BatchSize
        2. default_batch_fetch_size 권장
        3. query
    2. Entity 설정에서 FetchType.EAGER로 설정
    3. EntityGraph
    4. fetch join

1. 사전작업

이전 포스팅에 이어서 이번 시간에는 1:N 연관관계 필드값을 가지는 엔티티 조회에서 발생될 수 있는 LazyInitializationException을 해결하는 방법을 알아볼 것입니다.


1.1. Entity 수정

1.1.1. User

User 엔티티가 필드로 갖는 stores를 이용하여 1:N 연관관계에 발생되는 에러현상을 살펴볼 것이기 설정되어있기때문에, 이전시간에 설정했던 @JsonIgnore를 제거합니다.

@NoArgsConstructor
@DynamicInsert @DynamicUpdate
@Getter
@Entity
@Table(name = "user", indexes = {@Index(name = "UK_USER_EMAIL", columnList = "email", unique = true)})
@Where(clause = "active = true")
public class User implements Serializable {

	...

	@OneToMany
	@JoinColumn(name = "user_id")
	@Setter private List<Store> stores = new ArrayList<>();

}

1.2. Store

서로 순환참조되는 에러를 방지하기 위해 user 필드값에 @JsonIgnore 설정을 추가합니다.

관련 에러: HttpMessageNotWritableException: Could not write JSON: Infinite recursion

@NoArgsConstructor
@DynamicInsert
@DynamicUpdate
@Getter
@Entity
public class Store {

    ...

    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", foreignKey = @ForeignKey(name = "FK_USER_STORE"))
    private User user;

}

1.2. UserRepository 수정

기존의 UserRepository는 Repository를 상속받고 있었습니다.
JPA에서 자주 이용되는 api를 정의해둔 JpaRepository를

public interface UserRepository extends JpaRepository<User, Long> {
  ...
}

기존에 정의했었던 Optional<User> findById(Long id), User save(User user), void delete(User user); 는 JpaRepository에서 이미 구현하고 있기 때문에 제거합니다.


1.3. api 테스트

엔티티를 위와 같이 수정한 후 1번 user를 조회해보면 아래와 같은 에러가 발생됩니다.

02-3

swagger 화면


response를 내보내기 전에 controller에서 열어본 user 객체의 모습입니다. stores를 읽어들이는 부분에서 LazyInitializationException 이 발생되어 있습니다.

02-2

2022-04-14 15:50:33.811  WARN 15648 --- [nio-8989-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException:
 Could not write JSON: failed to lazily initialize a collection of role: me.jiniworld.demo.domain.entity.User.stores, could not initialize proxy - no Session;
  nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role:
   me.jiniworld.demo.domain.entity.User.stores, could not initialize proxy - no Session (through reference chain: java.util.HashMap["user"]->me.jiniworld.demo.domain.entity.User["stores"])]

2. LazyInitializationException 해결

2.1. @JsonIgnore

@OneToMany 연관관계 설정되어있는 stores 필드값에 @JsonIgnore를 설정합니다.

@NoArgsConstructor
@DynamicInsert @DynamicUpdate
@Getter
@Entity
@Table(name = "user", indexes = {@Index(name = "UK_USER_EMAIL", columnList = "email", unique = true)})
@Where(clause = "active = true")
public class User implements Serializable {
	...

	@JsonIgnore
	@OneToMany
	@JoinColumn(name = "user_id")
	@Setter private List<Store> stores = new ArrayList<>();

}

02-4

그러면, api가 에러없이 동작됩니다.
그러나 response에 stores 정보가 포함되지 않기 때문에 우리가 원하는 결과가 아닙니다.

로그상에도 user를 조회하는 쿼리만 발생했습니다.

Hibernate:
    select
        user0_.id as id1_1_0_,
        user0_.active as active2_1_0_,
        user0_.birth_date as birth_da3_1_0_,
        user0_.created_at as created_4_1_0_,
        user0_.email as email5_1_0_,
        user0_.name as name6_1_0_,
        user0_.password as password7_1_0_,
        user0_.phone_number as phone_nu8_1_0_,
        user0_.sex as sex9_1_0_,
        user0_.type as type10_1_0_,
        user0_.updated_at as updated11_1_0_
    from
        user user0_
    where
        user0_.id=?
        and (
            user0_.active = 1
        )

2.2. Transactional 내부에서 연관관계 미리 조회 권장

@Transactional 설정된 UserService 내의 메서드에서, stores 정보를 미리 LOAD합니다.

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class UserService {

	private final UserRepository userRepository;

	public User select(Long id) {
		User user = userRepository.findById(id).orElse(null);
		user.getStores().stream().forEach(store -> store.getName());
		return user;
	}
}

영속상태에서 미리 연관관계 정보를 로드해두면, LAZY 관련 에러가 발생되지 않습니다.

api를 실행해봅시다.
이번에는 우리가 원하던 stores 정보도 함께 포함되어 response가 출력되었습니다.

02-5


user를 조회하는 쿼리와 user_id를 이용하여 stores를 조회하는 쿼리가 발생했습니다.

Hibernate:
    select
        user0_.id as id1_1_0_,
        user0_.active as active2_1_0_,
        user0_.birth_date as birth_da3_1_0_,
        user0_.created_at as created_4_1_0_,
        user0_.email as email5_1_0_,
        user0_.name as name6_1_0_,
        user0_.password as password7_1_0_,
        user0_.phone_number as phone_nu8_1_0_,
        user0_.sex as sex9_1_0_,
        user0_.type as type10_1_0_,
        user0_.updated_at as updated11_1_0_
    from
        user user0_
    where
        user0_.id=?
        and (
            user0_.active = 1
        )
Hibernate:
    select
        stores0_.user_id as user_id4_0_0_,
        stores0_.id as id1_0_0_,
        stores0_.id as id1_0_1_,
        stores0_.industry as industry2_0_1_,
        stores0_.name as name3_0_1_,
        stores0_.user_id as user_id4_0_1_
    from
        store stores0_
    where
        stores0_.user_id=?

아래는 쿼리를 직접 조회한 결과 화면입니다.

02-7

02-8


2.3. Entity 설정에서 FetchType.EAGER로 설정 비권장

@Entity
@Table(name = "user", indexes = {@Index(name = "UK_USER_EMAIL", columnList = "email", unique = true)})
@Where(clause = "active = true")
public class User implements Serializable {

	...

	@OneToMany(fetch = FetchType.EAGER)
	@JoinColumn(name = "user_id")
	@Setter private List<Store> stores = new ArrayList<>();

}

EAGER 방식으로 조회할 경우, left outer join을 이용하여 조회됩니다.

이용된 쿼리를 보면 1개의 쿼리만 실행된것을 확인할 수 있는데, @OneToMany 연관관계에서는 EAGER방식으로 데이터를 읽어들이는 것을 권장하지 않습니다.

Hibernate:
    select
        user0_.id as id1_1_0_,
        user0_.active as active2_1_0_,
        user0_.birth_date as birth_da3_1_0_,
        user0_.created_at as created_4_1_0_,
        user0_.email as email5_1_0_,
        user0_.name as name6_1_0_,
        user0_.password as password7_1_0_,
        user0_.phone_number as phone_nu8_1_0_,
        user0_.sex as sex9_1_0_,
        user0_.type as type10_1_0_,
        user0_.updated_at as updated11_1_0_,
        stores1_.user_id as user_id4_0_1_,
        stores1_.id as id1_0_1_,
        stores1_.id as id1_0_2_,
        stores1_.industry as industry2_0_2_,
        stores1_.name as name3_0_2_,
        stores1_.user_id as user_id4_0_2_
    from
        user user0_
    left outer join
        store stores1_
            on user0_.id=stores1_.user_id
    where
        user0_.id=?
        and (
            user0_.active = 1
        )

위의 쿼리를 직접 실행시켜보면 이유를 알 수 있습니다.
User 하나는 여러개의 Store를 가지고 있을 수 있고, 이것을 JPA를 이용하여 조회할 경우 기준이 되는 엔티티인 User 정보가 중복되게 됩니다.

02-6


left outer join 하기 때문에 stores값이 없어도 user 정보를 읽을 수 있습니다.
3번 사용자를 조회한 결과는 아래와 같습니다.

03-2


2.4. EntityGraph 비권장

이전시간에 배웠듯, EntityGraph 역시, EAGER 방식으로 연관관계를 가져옵니다.

2.3 와 똑같이 left outer join으로 데이터를 읽어오기 때문에, LazyInitializationException 에러는 해결할 수 있으나, 기준이 되는 User 엔티티 정보를 중복적으로 가져오기 때문에 비권장하는 방식입니다.

다만, EntityGraph는 Distinct 설정은 가능하다는 장점이 있습니다.

2.4.1. Repository

public interface UserRepository extends Repository<User, Long> {

	@EntityGraph(attributePaths = "stores")
	Optional<User> findDistinctWithStoresById(Long id);

}

2.4.2. Service

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class UserService {

	private final UserRepository userRepository;

	public User select(Long id) {
		User user = userRepository.findDistinctWithStoresById(id).orElse(null);
		return user;
	}

}

2.4.3. query

Hibernate:
    select
        distinct user0_.id as id1_1_0_,
        stores1_.id as id1_0_1_,
        user0_.active as active2_1_0_,
        user0_.birth_date as birth_da3_1_0_,
        user0_.created_at as created_4_1_0_,
        user0_.email as email5_1_0_,
        user0_.name as name6_1_0_,
        user0_.password as password7_1_0_,
        user0_.phone_number as phone_nu8_1_0_,
        user0_.sex as sex9_1_0_,
        user0_.type as type10_1_0_,
        user0_.updated_at as updated11_1_0_,
        stores1_.industry as industry2_0_1_,
        stores1_.name as name3_0_1_,
        stores1_.user_id as user_id4_0_1_,
        stores1_.user_id as user_id4_0_0__,
        stores1_.id as id1_0_0__
    from
        user user0_
    left outer join
        store stores1_
            on user0_.id=stores1_.user_id
    where
        (
            user0_.active = 1
        )
        and user0_.id=?

2.5. fetch join 비권장

fetch join은 EntityGraph와 join방식이 다른것 말고는 유사합니다.
미리 연관관계를 읽어오기 때문에 LazyInitializationException 에러를 방지할 수 있으나, EntityGraph와 마찬가지로 기준이 되는 User 엔티티 정보를 중복적으로 가져옵니다.

또, join 방식이 inner join이기 때문에 연관관계 설정된 stores값이 없을 경우, user 도 조회되지 않는다는 점에서 EntityGraph와 차이가 있습니다.

2.5.1. Repository

public interface UserRepository extends Repository<User, Long> {

  @Query("SELECT DISTINCT u FROM User u join fetch u.stores WHERE u.id = ?1")
	Optional<User> findDistinctWithStoresById(Long id);

}
Hibernate:
    select
        distinct user0_.id as id1_1_0_,
        stores1_.id as id1_0_1_,
        user0_.active as active2_1_0_,
        user0_.birth_date as birth_da3_1_0_,
        user0_.created_at as created_4_1_0_,
        user0_.email as email5_1_0_,
        user0_.name as name6_1_0_,
        user0_.password as password7_1_0_,
        user0_.phone_number as phone_nu8_1_0_,
        user0_.sex as sex9_1_0_,
        user0_.type as type10_1_0_,
        user0_.updated_at as updated11_1_0_,
        stores1_.industry as industry2_0_1_,
        stores1_.name as name3_0_1_,
        stores1_.user_id as user_id4_0_1_,
        stores1_.user_id as user_id4_0_0__,
        stores1_.id as id1_0_0__
    from
        user user0_
    inner join
        store stores1_
            on user0_.id=stores1_.user_id
    where
        (
            user0_.active = 1
        )
        and user0_.id=?

1번 사용자에 대해서는 2.4 와 똑같이 응답이 나오지만,
3번 사용자는 stores값이 없어서 user 정보를 읽을 수 없습니다.

03-1


3. N+1 문제 해결

user를 모두 조회하는 GET /api/users 라는 api가 있다고 해볼때,
2.2 ~ 2.5 에서 살펴보았던 방식을 user 모두 조회에 적용해서, N + 1 문제에 대해 알아보도록 합시다.

3.1. Transactional 내부에서 연관관계 미리 조회

user의 stores를 forEach를 이용하여 @Transactional이 끝나기 전에 미리 조회하여, Controller에서 LazyInitializationException 에러가 발생되지 않도록 합니다.

3.1.1. Service

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class UserService {

	private final UserRepository userRepository;

	public List<User> selectAll() {
		List<User> users = userRepository.findAll();
		users.stream()
				.forEach(user -> user.getStores().stream()
						.filter(store -> store != null)
						.forEach(store -> store.getName()));
		return users;
	}
}

3.1.2. batch size 미적용시

@OneToMany 관계에서 N+1 문제를 개선하기 위해 필수적으로 이용해야할 설정이 batch size 입니다.

batch size를 설정하지 않았을때, 위의 서비스로 정의된 api를 실행했을 경우 아래와 같이 쿼리가 실행됩니다.

Hibernate:
    select
        user0_.id as id1_1_,
        user0_.active as active2_1_,
        user0_.birth_date as birth_da3_1_,
        user0_.created_at as created_4_1_,
        user0_.email as email5_1_,
        user0_.name as name6_1_,
        user0_.password as password7_1_,
        user0_.phone_number as phone_nu8_1_,
        user0_.sex as sex9_1_,
        user0_.type as type10_1_,
        user0_.updated_at as updated11_1_
    from
        user user0_
    where
        (
            user0_.active = 1
        )
Hibernate:
    select
        stores0_.user_id as user_id4_0_0_,
        stores0_.id as id1_0_0_,
        stores0_.id as id1_0_1_,
        stores0_.industry as industry2_0_1_,
        stores0_.name as name3_0_1_,
        stores0_.user_id as user_id4_0_1_
    from
        store stores0_
    where
        stores0_.user_id=?
Hibernate:
    select
        stores0_.user_id as user_id4_0_0_,
        stores0_.id as id1_0_0_,
        stores0_.id as id1_0_1_,
        stores0_.industry as industry2_0_1_,
        stores0_.name as name3_0_1_,
        stores0_.user_id as user_id4_0_1_
    from
        store stores0_
    where
        stores0_.user_id=?
Hibernate:
    select
        stores0_.user_id as user_id4_0_0_,
        stores0_.id as id1_0_0_,
        stores0_.id as id1_0_1_,
        stores0_.industry as industry2_0_1_,
        stores0_.name as name3_0_1_,
        stores0_.user_id as user_id4_0_1_
    from
        store stores0_
    where
        stores0_.user_id=?

조회하고자하는 엔티티인 user를 조회하는 쿼리 1번 + 조회해야할 연관관계(stores)를 user의 count만큼 조회하는 쿼리 3번 → N + 1 문제 발생!!!

3.1.3. batch size 적용시 권장

batch size를 설정할 경우, 1:N 연관관계를 갖는 필드값을 batch size만큼 한번에 IN절을 이용하여 조회할 수 있습니다.
즉, batch size를 설정하면, 1:N 연관관계에서 발생되는 N+1 문제를 개선할 수 있습니다.

batch size를 설정할 수 있는 방법은 2가지가 있습니다.

  1. 각 엔티티별 필드값에 직접 정의
  2. 프로퍼티를 이용하여 1:N 연관관계 필드값에 기본 batch size 정의

3.1.3.1. @BatchSize

@OneToMany 연관관계 필드값에 직접 @BatchSize를 설정합니다.

@NoArgsConstructor
@DynamicInsert @DynamicUpdate
@Getter
@Entity
@Table(name = "user", indexes = {@Index(name = "UK_USER_EMAIL", columnList = "email", unique = true)})
@Where(clause = "active = true")
public class User implements Serializable {

	...

	@OneToMany(fetch = FetchType.EAGER)
	@BatchSize(size = 100)
	@JoinColumn(name = "user_id")
	@Setter private List<Store> stores = new ArrayList<>();

}

3.1.3.2. default_batch_fetch_size

application.yml에 기본 batch size를 지정합니다.

application.yml에 기본 batch size를 설정하고, Entity에 따라 batch size를 더 키우고 싶으면 직접 설정하는 것을 권장합니다.

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

size는 100~1000 사이의 수를 지정는 것을 권장하며, size가 클수록 API 성능은 좋아지나, DB과부화가 걸릴 수 있습니다.


3.1.3.3. query

아래는 BatchSize를 적용한 후 users 전체 조회시 실행된 쿼리입니다.
N+1 문제가 개선되었습니다.

Hibernate:
    select
        user0_.id as id1_1_,
        user0_.active as active2_1_,
        user0_.birth_date as birth_da3_1_,
        user0_.created_at as created_4_1_,
        user0_.email as email5_1_,
        user0_.name as name6_1_,
        user0_.password as password7_1_,
        user0_.phone_number as phone_nu8_1_,
        user0_.sex as sex9_1_,
        user0_.type as type10_1_,
        user0_.updated_at as updated11_1_
    from
        user user0_
    where
        (
            user0_.active = 1
        )
Hibernate:
    select
        stores0_.user_id as user_id4_0_1_,
        stores0_.id as id1_0_1_,
        stores0_.id as id1_0_0_,
        stores0_.industry as industry2_0_0_,
        stores0_.name as name3_0_0_,
        stores0_.user_id as user_id4_0_0_
    from
        store stores0_
    where
        stores0_.user_id in (
            ?, ?, ?
        )

만일 user의 수가 302개일 경우, BatchSize가 100이므로, IN절을 이용한 store 조회는 총 4번 일어납니다.


3.2. Entity 설정에서 FetchType.EAGER로 설정 비권장

3.2.1. Entity

Entity에 설정하는 것은 2.3 과 똑같이, Entity의 stores 필드값만 수정해주면 됩니다.

@NoArgsConstructor
@DynamicInsert @DynamicUpdate
@Getter
@Entity
@Table(name = "user", indexes = {@Index(name = "UK_USER_EMAIL", columnList = "email", unique = true)})
@Where(clause = "active = true")
public class User implements Serializable {

	...

	@OneToMany(fetch = FetchType.EAGER)
	@JoinColumn(name = "user_id")
	@Setter private List<Store> stores = new ArrayList<>();

}

3.2.2. Service

Service 메서드에 forEach를 이용하여 stores를 조회하는 코드도 필요하지 않습니다.

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class UserService {

	private final UserRepository userRepository;

	public List<User> selectAll() {
		List<User> users = userRepository.findAll();
		return users;
	}
}

3.2.3. batch size 미적용시

EAGER 방식이니, user조회할때에 left outer join을 하여 1번의 쿼리만 실행될거라고 예상하겠지만, 예상과 달리 3.1 처럼 N + 1 문제 가 발생되었습니다.

Hibernate:
    select
        user0_.id as id1_1_,
        user0_.active as active2_1_,
        user0_.birth_date as birth_da3_1_,
        user0_.created_at as created_4_1_,
        user0_.email as email5_1_,
        user0_.name as name6_1_,
        user0_.password as password7_1_,
        user0_.phone_number as phone_nu8_1_,
        user0_.sex as sex9_1_,
        user0_.type as type10_1_,
        user0_.updated_at as updated11_1_
    from
        user user0_
    where
        (
            user0_.active = 1
        )
Hibernate:
    select
        stores0_.user_id as user_id4_0_0_,
        stores0_.id as id1_0_0_,
        stores0_.id as id1_0_1_,
        stores0_.industry as industry2_0_1_,
        stores0_.name as name3_0_1_,
        stores0_.user_id as user_id4_0_1_
    from
        store stores0_
    where
        stores0_.user_id=?
Hibernate:
    select
        stores0_.user_id as user_id4_0_0_,
        stores0_.id as id1_0_0_,
        stores0_.id as id1_0_1_,
        stores0_.industry as industry2_0_1_,
        stores0_.name as name3_0_1_,
        stores0_.user_id as user_id4_0_1_
    from
        store stores0_
    where
        stores0_.user_id=?
Hibernate:
    select
        stores0_.user_id as user_id4_0_0_,
        stores0_.id as id1_0_0_,
        stores0_.id as id1_0_1_,
        stores0_.industry as industry2_0_1_,
        stores0_.name as name3_0_1_,
        stores0_.user_id as user_id4_0_1_
    from
        store stores0_
    where
        stores0_.user_id=?

3.2.4. batch size 적용

batch size를 적용할 경우 3.1.3 에서와 마찬가지로 N+1 문제를 개선됩니다. 다만, left outer join은 이뤄지지 않고, BatchSize에 맞춰 IN절로 stores를 조회합니다.

batch size를 설정한 후, api를 실행해보니 아래와 같이 쿼리가 개선된 것을 확인해 볼 수 있습니다.

Hibernate:
    select
        user0_.id as id1_1_,
        user0_.active as active2_1_,
        user0_.birth_date as birth_da3_1_,
        user0_.created_at as created_4_1_,
        user0_.email as email5_1_,
        user0_.name as name6_1_,
        user0_.password as password7_1_,
        user0_.phone_number as phone_nu8_1_,
        user0_.sex as sex9_1_,
        user0_.type as type10_1_,
        user0_.updated_at as updated11_1_
    from
        user user0_
    where
        (
            user0_.active = 1
        )
Hibernate:
    select
        stores0_.user_id as user_id4_0_1_,
        stores0_.id as id1_0_1_,
        stores0_.id as id1_0_0_,
        stores0_.industry as industry2_0_0_,
        stores0_.name as name3_0_0_,
        stores0_.user_id as user_id4_0_0_
    from
        store stores0_
    where
        stores0_.user_id in (
            ?, ?, ?
        )

user 단건조회가 아닌 경우 위와같이 user와 store가 각각 조회되기 때문에 코드상으로는 문제가 없지만, 단건 조회할때에는 EAGER 로 설정되어있기 때문에 left outer join으로 데이터를 가져와서 데이터가 중복되게 됩니다.
그 외에도, Entity상에 fetchType을 EAGER로 정의하는것은 비권장 사항입니다. 위의 코드는 실행 결과를 보기 위함이고 LAZY로 정의하고 다른 방법을 이용하여 LazyInitializationException 에러를 해결하기를 권장합니다.


3.3. EntityGraph 비권장

Entity에 설정했던 fetchType.EAGER 설정은 제거하고, @EntityGraph를 이용하여 stores 정보를 가져와봅니다.

3.3.1. Service

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class UserService {

	private final UserRepository userRepository;

	public List<User> selectAll() {
		List<User> users = userRepository.findDistinctWithStoresBy();
		return users;
	}

}

3.3.2. Repository

public interface UserRepository extends JpaRepository<User, Long> {

	@EntityGraph(attributePaths = "stores")
	List<User> findDistinctWithStoresBy();

}

3.3.3. query

EntityGraph를 이용하니, 우리가 원하던 대로 left outer join을 이용하여 1번의 쿼리만 이용되었습니다.

Hibernate:
    select
        distinct user0_.id as id1_1_0_,
        stores1_.id as id1_0_1_,
        user0_.active as active2_1_0_,
        user0_.birth_date as birth_da3_1_0_,
        user0_.created_at as created_4_1_0_,
        user0_.email as email5_1_0_,
        user0_.name as name6_1_0_,
        user0_.password as password7_1_0_,
        user0_.phone_number as phone_nu8_1_0_,
        user0_.sex as sex9_1_0_,
        user0_.type as type10_1_0_,
        user0_.updated_at as updated11_1_0_,
        stores1_.industry as industry2_0_1_,
        stores1_.name as name3_0_1_,
        stores1_.user_id as user_id4_0_1_,
        stores1_.user_id as user_id4_0_0__,
        stores1_.id as id1_0_0__
    from
        user user0_
    left outer join
        store stores1_
            on user0_.id=stores1_.user_id
    where
        (
            user0_.active = 1
        )

left outer join을 이용해서 user를 조회해기 때문에 stores가 없는 3번 사용자 코코도 조회됩니다.

{
  "result": "SUCCESS",
  "user": [
    {
      "id": 1,
      "type": "BASIC",
      "email": "jini@jiniworld.me",
      "name": "지니",
      "sex": "F",
      "birthDate": "19920201",
      "phoneNumber": "01011112222",
      "active": true,
      "createdAt": "2022-04-11T07:59:07.148+00:00",
      "updatedAt": null,
      "stores": [
        {
          "id": 1,
          "name": "우리의 시간",
          "industry": "카페"
        },
        {
          "id": 2,
          "name": "에머이",
          "industry": "베트남음식"
        }
      ]
    },
    {
      "id": 2,
      "type": "OWNER",
      "email": "2han@appleboxy.xyz",
      "name": "이한",
      "sex": "M",
      "birthDate": "19880702",
      "phoneNumber": "01088887777",
      "active": true,
      "createdAt": "2022-04-13T03:21:30.542+00:00",
      "updatedAt": null,
      "stores": [
        {
          "id": 3,
          "name": "한잔의 추억",
          "industry": "술집"
        }
      ]
    },
    {
      "id": 3,
      "type": "OWNER",
      "email": "coco@jiniworld.me",
      "name": "코코",
      "sex": "F",
      "birthDate": "19980117",
      "phoneNumber": "01055557741",
      "active": true,
      "createdAt": "2022-04-13T07:14:01.098+00:00",
      "updatedAt": null,
      "stores": []
    }
  ]
}

03-3

EntityGraph를 이용하여 users 를 조회하는 것은 쿼리는 1개만 실행되기 때문에 효율적으로 보일수도 있으나, left outer join 방식을 이용하기 때문에 위와같이 중복되는 데이터를 초래합니다.
EntityGraph를 이용하는 것 보다는 영속상태일 때 직접 stores 데이터를 LOAD하는 것을 권장합니다. (fetch join 역시 마찬가지)


3.4. fetch join 비권장

3.4.1. Repository

public interface UserRepository extends JpaRepository<User, Long> {

	@Query("SELECT DISTINCT u FROM User u join fetch u.stores")
	List<User> findDistinctWithStoresBy();

}

3.4.2. query

fetch join으로 조회했을 때에도 inner join을 이용하여 1번의 쿼리만 이용됩니다.

Hibernate:
    select
        distinct user0_.id as id1_1_0_,
        stores1_.id as id1_0_1_,
        user0_.active as active2_1_0_,
        user0_.birth_date as birth_da3_1_0_,
        user0_.created_at as created_4_1_0_,
        user0_.email as email5_1_0_,
        user0_.name as name6_1_0_,
        user0_.password as password7_1_0_,
        user0_.phone_number as phone_nu8_1_0_,
        user0_.sex as sex9_1_0_,
        user0_.type as type10_1_0_,
        user0_.updated_at as updated11_1_0_,
        stores1_.industry as industry2_0_1_,
        stores1_.name as name3_0_1_,
        stores1_.user_id as user_id4_0_1_,
        stores1_.user_id as user_id4_0_0__,
        stores1_.id as id1_0_0__
    from
        user user0_
    inner join
        store stores1_
            on user0_.id=stores1_.user_id
    where
        (
            user0_.active = 1
        )

단, inner join으로 user정보를 가져오기 때문에 stores가 없는 3번 사용자인 코코는 response에 포함되지 않습니다.

{
  "result": "SUCCESS",
  "user": [
    {
      "id": 1,
      "type": "BASIC",
      "email": "jini@jiniworld.me",
      "name": "지니",
      "sex": "F",
      "birthDate": "19920201",
      "phoneNumber": "01011112222",
      "active": true,
      "createdAt": "2022-04-11T07:59:07.148+00:00",
      "updatedAt": null,
      "stores": [
        {
          "id": 1,
          "name": "우리의 시간",
          "industry": "카페"
        },
        {
          "id": 2,
          "name": "에머이",
          "industry": "베트남음식"
        }
      ]
    },
    {
      "id": 2,
      "type": "OWNER",
      "email": "2han@appleboxy.xyz",
      "name": "이한",
      "sex": "M",
      "birthDate": "19880702",
      "phoneNumber": "01088887777",
      "active": true,
      "createdAt": "2022-04-13T03:21:30.542+00:00",
      "updatedAt": null,
      "stores": [
        {
          "id": 3,
          "name": "한잔의 추억",
          "industry": "술집"
        }
      ]
    }
  ]
}

++

  • How to fix N + 1 problems in Spring Boot
  • How to set default batch size in Spring Boot

GitHub에서 demo 프로젝트(v1.0.6)를 다운받아 볼 수 있습니다.

728x90
반응형