본문 바로가기
백엔드 공부

[Spring] JPA에서 OneToOne 관계 N+1 문제 (Feat : Lazy Loading)

by CSEGR 2025. 5. 16.
728x90

회사에서 프로젝트를 하면서 발견한 JPA OneToOne 관계의 N+1 문제이다! 

 

OneToOne 관계에 놓인 Entity A와 B가 있다고 가정하자.

Entity A를 조회했을 때, DB 조회를 위한 한 번의 쿼리가 아닌 B에 대한 조회까지 총 두 번의 쿼리가 나가는 문제점을 발견하였다.

 

코드는 공개할 수 없어서, Entity인 User와 UserProfile를 예시로 들어보겠다.

 

  • User: 회원 기본 정보 (아이디, 이메일, 비밀번호 등)
  • UserProfile: 회원의 상세 프로필 정보 (주소, 생년월일, 취미 등)

→ 모든 유저는 하나의 프로필을 반드시 가지고 있고, 반대로 프로필은 반드시 특정 유저에 속해야 함.
→ 따라서 1:1 관계, 그리고 현실적으로 양방향으로 참조할 필요가 있음.

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String email;

    @OneToOne(mappedBy = "user", 
    fetch = FetchType.LAZY, 
    cascade = CascadeType.ALL, optional = false)
    private UserProfile profile;

    public void setProfile(UserProfile profile) {
        this.profile = profile;
        profile.setUser(this);
    }
}
@Entity
public class UserProfile {
    @Id
    private Long id; // User와 같은 ID 사용

    private String address;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}

 

즉, User를 조회할 때, UserProfile 까지 조회하는 쿼리가 생성되었다.

UserProfile에 Lazy Loading을 설정해도, 실제 실행 시 지연 로딩(Lazy Loading)이 동작하지 않고 즉시 로딩(Eager Loading)처럼 작동된다.

 

 

Lazy Loading vs Eager Loading

 

❯ Lazy (지연 로딩)

  • 프록시 객체로 감싸서 초기에는 데이터 로딩 안 함
  • 처음 접근하는 시점에 SELECT 쿼리 실행

- Lazy Loading시 쿼리 로그

select
    user0_.id as id1_0_0_,
    user0_.email as email2_0_0_,
    user0_.profile_id as profile_3_0_0_
from
    user user0_
where
    user0_.id=1;

 


❯ Eager (즉시 로딩)

  • 연관 객체를 즉시 로딩하기 때문에, User를 불러올 때 자동으로 JOIN 쿼리가 실행되어 UserProfile도 조회.

 

- Eager Loading시 쿼리 로그

select
    user0_.id as id1_0_0_,
    user0_.email as email2_0_0_,
    user0_.profile_id as profile_3_0_0_,
    userprofil1_.id as id1_1_1_,
    userprofil1_.address as address2_1_1_
from
    user user0_
left outer join
    user_profile userprofil1_
        on user0_.profile_id=userprofil1_.id
where
    user0_.id=1;

 

✔️ JPA에서 @OneToOne(fetch = LAZY)가 실제로 Lazy로 동작하기 위한 조건

Hibernate는 @OneToOne(fetch = FetchType.LAZY)를 명시하더라도, 내부적으로 프록시 객체를 생성할 수 없는 상황에서는 강제로 EAGER처럼 동작시킨다. 따라서, 아래의 발동 조건을 만족해야지만 Lazy Loading 동작이 가능하다.

 

1. optional = false : 참조 대상이 반드시 존재해야 함

@OneToOne(fetch = FetchType.LAZY, optional = false)
 
  • Hibernate는 Lazy Loading을 위해 프록시 객체를 사용한다. optional = false로 지정하면, 프록시 객체를 생성해도 절대 null이 아니므로 안전하게 지연 로딩이 가능하다고 판단하게 된다. 반대로 optional = true인 경우에는 프록시가 실제로 존재하지 않을 수도 있어 Hibernate가 즉시 로딩(EAGER)로 대체 해버린다. 
 

2. 단방향 관계여야 함 (@OneToOne 단방향)

3. @PrimaryKeyJoinColumn이 없어야 함 (PK 공유 관계 금지)

  • @PrimaryKeyJoinColumn은 부모-자식 엔티티가 PK를 공유하는 경우에 사용된다. 이 구조에서는 JPA가 내부적으로 프록시 객체 생성을 지원하지 않는다.

 

728x90