UMC-GREENY / greeny-backend

UMC GREENY project API server
3 stars 1 forks source link

OneToOne fetch lazy #23

Open dtd1614 opened 1 year ago

dtd1614 commented 1 year ago

@OneToOne 양방향 매핑시 Fetch 전략을 Lazy로 설정해도 Eager로 동작하는 경우가 있습니다.

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member extends AuditEntity {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    @OneToOne(mappedBy = "member", fetch = LAZY, cascade = ALL, orphanRemoval = true)
    private MemberGeneral memberGeneral;
    @OneToOne(mappedBy = "member", fetch = LAZY, cascade = ALL, orphanRemoval = true)
    private MemberSocial memberSocial;
    @OneToOne(mappedBy = "member", fetch = LAZY, cascade = ALL, orphanRemoval = true)
    private MemberProfile memberProfile;
    @OneToOne(mappedBy = "member", fetch = LAZY, cascade = ALL, orphanRemoval = true)
    private MemberAgreement memberAgreement;
    @Column(nullable = false)
    private String email;
    @Enumerated(EnumType.STRING) 
    @Column(nullable = false)
    private Role role;

Member 엔티티는 MemberGeneral, MemberSocial, MemberProfile, MemberAgreement와 OneToOne으로 매핑되어 있으며, Fetch 전략을 Lazy로 설정해주었습니다.

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberGeneral extends AuditEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_general_id")
    private Long id;
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @Column(nullable = false)
    private String password;
    @Column(nullable = false)
    private boolean isAuto;
}

MemberGeneral 또한 Member와 OneToOne으로 매핑되어 OneToOne 양방향 관계가 설정되었습니다. 마찬가지로 Fetch 전략을 Lazy로 설정해주었습니다. MemberSocial, MemberProfile, MemberAgreement 또한 Member와 OneToOne 양방향 관계로 매핑되어있습니다.

이제 Member를 가져왔을 때 Lazy 로딩이 잘 되는 지 테스트 해보겠습니다.

@Test
void oneToOneLazyTest(){
memberRepository.save(Member.builder().role(Role.ROLE_USER).email("user1@example.com").build());
memberRepository.findByEmail("user1@example.com");
}

Member 하나를 먼저 저장해주고, 저장한 멤버를 불러왔을 때 쿼리가 어떻게 나가는지 확인해봤습니다.

Hibernate: 
    insert 
    into
        member
        (created_at, updated_at, email, role) 
    values
        (?, ?, ?, ?)
Hibernate: 
    select
        member0_.member_id as member_i1_1_,
        member0_.created_at as created_2_1_,
        member0_.updated_at as updated_3_1_,
        member0_.email as email4_1_,
        member0_.role as role5_1_ 
    from
        member member0_ 
    where
        member0_.email=?
Hibernate: 
    select
        memberagre0_.member_agreement_id as member_a1_2_0_,
        memberagre0_.created_at as created_2_2_0_,
        memberagre0_.updated_at as updated_3_2_0_,
        memberagre0_.member_id as member_i6_2_0_,
        memberagre0_.personal_info as personal4_2_0_,
        memberagre0_.third_party as third_pa5_2_0_ 
    from
        member_agreement memberagre0_ 
    where
        memberagre0_.member_id=?
Hibernate: 
    select
        membergene0_.member_general_id as member_g1_3_0_,
        membergene0_.created_at as created_2_3_0_,
        membergene0_.updated_at as updated_3_3_0_,
        membergene0_.is_auto as is_auto4_3_0_,
        membergene0_.member_id as member_i6_3_0_,
        membergene0_.password as password5_3_0_ 
    from
        member_general membergene0_ 
    where
        membergene0_.member_id=?
Hibernate: 
    select
        memberprof0_.member_profile_id as member_p1_4_0_,
        memberprof0_.created_at as created_2_4_0_,
        memberprof0_.updated_at as updated_3_4_0_,
        memberprof0_.birth as birth4_4_0_,
        memberprof0_.member_id as member_i7_4_0_,
        memberprof0_.name as name5_4_0_,
        memberprof0_.phone as phone6_4_0_ 
    from
        member_profile memberprof0_ 
    where
        memberprof0_.member_id=?
Hibernate: 
    select
        membersoci0_.member_social_id as member_s1_5_0_,
        membersoci0_.created_at as created_2_5_0_,
        membersoci0_.updated_at as updated_3_5_0_,
        membersoci0_.member_id as member_i5_5_0_,
        membersoci0_.provider as provider4_5_0_ 
    from
        member_social membersoci0_ 
    where
        membersoci0_.member_id=?

Fetch 전략을 Lazy로 설정했음에도 Eager로 동작하는 것을 확인할 수 있습니다.

OneToOne 양방향 관계 시, Fetch 전략을 Lazy로 설정하여도 주인이 아닌 쪽, 즉 FK를 가지고 있지 않은 엔티티에서는 Lazy 로딩이 동작하지 않습니다. FK가 없으면 연관 관계 엔티티의 존재 여부를 알 수 없습니다. Lazy 로딩은 관계 엔티티를 Proxy 객체로 할당해야 하는데, FK가 없는 엔티티만 읽어서는 관계 엔티티의 존재 여부를 알 수 없으니 강제로 Eager 로딩을 수행하는 것입니다. (Proxy에 null을 할당할 수는 없습니다.) 참고 : https://jeong-pro.tistory.com/249

Member는 FK를 가지고 있지 않기 때문에 주인이 아닙니다. 테스트 코드에서 주인이 아닌 Member를 가져왔기 때문에 Lazy 로딩이 동작하지 않은 것입니다.

OneToOne 관계에서 Lazy loading 발동 조건은 다음과 같습니다. image 참고 : https://velog.io/@moonyoung/JPA-OneToOne-%EC%96%91%EB%B0%A9%ED%96%A5-%EB%A7%A4%ED%95%91%EA%B3%BC-LazyLoading

Member와 MemberGeneral의 관계를 단방향으로 설정해주어 Lazy 로딩을 수행시킬 수 있을 것으로 보입니다.

Minuooooo commented 1 year ago

현재의 문제 상황이 어떤지 확실하게 파악하고 계시고, 훌륭한 설명인 것 같습니다. 이렇게 쿼리가 남발된다면 성능이 저하될 것이고, One-To-One 양방향 연관 관계에서는 fetch = LAZY 로 설정하는 것에 제약이 따르는 것을 인지하였습니다.

Minuooooo commented 1 year ago

One-To-One 양방향 연관 관계에서 주인이 아닌 Entity를 조회하면 지연 로딩이 적용되기 어렵습니다. 연관 관계를 설정할 필요가 없다고 판단하여, 제거하였습니다.

다음은, 관계의 주인 MemberGeneral 입니다.

public class MemberGeneral extends AuditEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_general_id")
    private Long id;
    private Long memberId;
    @Column(nullable = false)
    private String password;
    @Column(nullable = false)
    private boolean isAuto;
}

이에 따라 MemberGeneralRepositoryfindByMemberId() 를 활용할 수 있습니다.

public interface MemberGeneralRepository extends JpaRepository<MemberGeneral, Long> {
    Optional<MemberGeneral> findByMemberId(Long memberId);
    boolean existsByMemberId(Long memberId);
}

보시는 바와 같이, 이제는 Member 가 아닌 memberId 를 통해서 조회하게 됩니다.

회원 관련 정보를 조회할 때, 연관 관계가 설정되어 있지 않아야 효율적이라고 판단하여 명시적으로 연관 관계를 설정하지 않았습니다. 대신, memberId 속성을 추가하여 Member 를 조회할 수 있습니다.