오늘 JPA를 연습하기 위해 스프링 Data JPA를 JPA로 바꿔서 JPQL 쿼리를 실행해보았는데 원하는 결과는 나왔지만 그 과정이 이상했다. 나는 분명 모든 회원을 검색했는데 모든 회원 검색 쿼리 + 검색된 회원 수만큼의 쿼리가 실행된 것이다. 아직 자세히는 배우지 않았지만 전에 들었던 N + 1 문제임을 직감하고 찾아봤는데 해당 문제가 맞았다. 그런데 대부분의 경우는 OneToOne이 아닌 다른 경우에서 생기는 것 같았다. 그래서 더 찾아보니 OneToOne의 관계에서도 N + 1 문제가 생기는 이유가 있었다.
문제 발생
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private final EntityManager entityManager;
@Override
public Optional<User> findByEmail(String email) {
String jpql = "SELECT u FROM User u WHERE u.email = :email";
User user = entityManager.createQuery(jpql, User.class)
.setParameter("email", email)
.getSingleResult();
return Optional.ofNullable(user);
}
@Override
public List<User> findAll() {
String jpql = "SELECT u FROM User u";
return entityManager.createQuery(jpql, User.class).getResultList();
}
}
ㄴ UserRepositoryImpl
@SpringBootTest
@Transactional
class UserRepositoryImplTest {
@Autowired UserRepository userRepository;
@Test
@DisplayName("정상 이메일 조회")
public void findByEmail() {
// given
String email = "test@test.com";
// when
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new NoResultException("유저 없음"));
// then
Assertions.assertThat(user.getEmail()).isEqualTo(email);
}
@Test
@DisplayName("모든 회원 조회")
public void findAll() {
userRepository.findAll();
}
}
ㄴ 테스트 코드
findAll()은 findByEmail()을 테스트하다 실행되는 쿼리가 이상해서 단순히 실행되는 쿼리만 보고자 했다.




하나만 찾는 테스트는 조회 쿼리가 두 번 실행되었고, 모든 회원을 찾는 테스트는 모든 회원의 수 만큼 개별 조회 쿼리가 더 실행되었다.
원인 찾기
왜 이런 문제가 발생하는지 찾아보기로 했다.
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private UserRole userRoles;
위의 코드는 User 엔티티와 UserRole 엔티티를 객체 그래프 탐색을 위해 연결해놓은 부분이다.
문제가 발생한 쿼리를 보니 UserRole과 관련된 부분이 추가로 실행된다. 그런데 나는 User 엔티티를 불러올 때 역할이 바로 필요없기 때문에 FetchType.LAZY로 설정하여 나중에 필요할 때 불러오기로 했다. 그리고 테스트 코드에서는 역할을 필요로 하는 코드도 없었다.
원인
원인은 생각지도 못했던 곳에서 발생했다.

위는 내가 설계한 테이블 구성이다.
연관 관계를 공부할 때 배웠던 부분이 있다. 연관 관계의 주인은 외래키를 갖고 있는 엔티티이다. 그렇기 때문에 UserRole 엔티티에 JoinColumn을 설정해주었고, User에는 mappedBy로 조회할 수 있도록 설정해 놓았다.


문제는 여기서 발생한다.
UserRole에서 User를 가져오는 것은 테이블에 유저 id가 있기 때문에 User 엔티티를 검색해보지 않아도 유저가 매핑되어 있다는 것을 알 수 있다. 하지만 반대의 경우, User에서는 UserRole이 있는지 없는지 모른다. 테이블에 그런 정보는 없기 때문이다. userRole은 단지 객체 그래프 탐색을 위한 변수이지 users 테이블에 있는 정보는 아니다.
그래서 해당 정보가 없으니 UserRole에 대한 정보를 추가로 검색해서 연관 관계를 만들어 주는 것이다.

Repository에서 user에 대한 정보를 출력하도록 했다.
확인해보니 LAZY로 설정했지만 일단 프록시 객체가 아님을 알 수 있다. 그래서 User에 대한 정보를 한번에 가져와서 관계를 설정해야 한다.
해결방법
@Slf4j
@Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private final EntityManager entityManager;
@Override
public Optional<User> findByEmail(String email) {
String jpql = "SELECT u FROM User u JOIN FETCH u.userRole WHERE u.email = :email";
User user = entityManager.createQuery(jpql, User.class)
.setParameter("email", email)
.getSingleResult();
// log.info("User = {}", user);
return Optional.ofNullable(user);
}
@Override
public List<User> findAll() {
String jpql = "SELECT u FROM User u JOIN FETCH u.userRole";
return entityManager.createQuery(jpql, User.class).getResultList();
}
}
해결 방법은 여러가지가 있을 수 있지만 나는 fetch join으로 해결했다. fetch join은 연관 관계가 있는 객체를 미리 가져와서 설정해 두는 방법이다. 해당 방법을 사용하면 위의 두 테이블을 join해서 User에 UserRole 객체를 할당한 결과를 가져올 수 있다.




이제 정보를 미리 한꺼번에 가져와서 조회 쿼리를 한번만 실행하게 된다.
나는 한번에 연관 정보를 가져와도 부담이 없는 정도여서 이렇게 해결했지만 미리 모든 정보를 가져오는 방식이 부담이 되는 경우라면 다른 해결방법을 찾거나 현재 테이블 구조에 대해 생각해 볼 필요가 있다.
'공부 > JPA' 카테고리의 다른 글
| [JPA] 스프링 OSIV (Open Session In View) 사용하기 (0) | 2024.03.02 |
|---|---|
| [JPA] LazyInitializationException 해결하기 (0) | 2024.02.28 |
| [JPA] 프록시 (Proxy) (0) | 2024.02.24 |
| [JPA] 다대다 연관 관계에서 복합키 사용하기 (0) | 2024.02.21 |
| [JPA] 연관 관계 중 인조키 vs 복합키 에 대한 고민 (0) | 2024.02.20 |