본문 바로가기

Java/spring

[Spring/JPA] LazyLoading으로 인한 ByteBuddyInterceptor 문제해결(@EntityGraph, JPQL, DTO로 필요한 데이터만 로드)

들어가며

✍️ 본 글은 LazyLoading으로 인한 ByteBuddyInterceptor 문제해결 과정을 담고 있습니다.
아래 코드와 @EntityGraph(attributePaths = "member") 설정은 Hibernate Lazy 로딩 문제를 해결하였습니다. 

 


✏️ 데이터 모델 요약

✅ 테이블 관계 요약

  • admin_responses ↔ member_questions: 1:1 관계 (OneToOne)
    : admin_responses.questions_sequence_id는 member_questions.questions_sequence_id를 참조합니다.
  • member_questions ↔ members: N:1 관계 (ManyToOne)
    : member_questions.member_id는 members.member_id를 참조합니다.

✅ 요구사항

  • Admin은 모든 답변을, Member는 자신이 작성한 문의에 대한 답변만 확인 가능합니다.
  • 특정 답변(questions_sequence_id)에 대해 해당 질문을 작성한 사용자(member)를 확인해야 하는 상황입니다.

✏️ 문제 발생 원인

  • MemberQuestion 엔티티의 member 속성은 기본적으로 Lazy로 로드됩니다.
  • findByMember_MemberIdAndQuestionsSequenceId() 메서드를 호출하면 MemberQuestion과 member 간의 관계에서 member 데이터가 Lazy 상태로 남아있습니다.
  • 이후 컨트롤러나 서비스 계층에서 member 속성에 접근할 경우, Hibernate는 프록시를 초기화하려 하지만 연관 데이터가 로드되지 않아 ByteBuddyInterceptor 예외 발생합니다.

 

✅ ByteBuddyInterceptor

ℹ️ 정의
- Hibernate 프록시 객체에 대해 동적 바이트 코드 생성 담당합니다.
- 프록시 객체가 초기화되지 않은 상태에서 이를 잘못 호출하면 다음과 같은 에러 발생 가능합니다.
  • 발생 가능한 에러
    • 프록시 초기화 실패: 연관 엔티티를 가져오려고 시도했으나, DB에서 해당 데이터를 조회하지 않습니다.
    • 잘못된 접근: 호출된 속성이 연관 엔티티지만 Lazy Loading 설정으로 인해 로드되지 않은 상태에서 접근합니다.
    • 정리해 보자면 연관된 데이터가 초기화되지 않았음에도 즉시 접근하려고 할 때 발생합니다.
  • 주로 프록시 객체가 초기화되지 않은 상태에서 특정 속성에 접근하려고 할 때 발생합니다.
  • 결합된 Lazy Loading 문제
    : 특정 답변(questions_sequence_id)에 연결된 질문(member_questions)과 그 질문을 작성한 사용자(members.member_id)에 접근하려 할 때, 지연 로딩된 프록시 객체가 초기화되지 않아 ByteBuddyInterceptor 에러가 발생합니다.

✏️@EntityGraph

  • 정의
    • JPA에서 제공하는 에너테이션
    • 특정 엔티티를 조회할 때 Lazy Loading 대신 Eager Loading 방식으로 연관된 데이터를 명시적으로 가져오도록 설정합니다.
    • N+1 문제 방지, 연관 데이터를 즉시 가져와야 할 때 유용합니다.
  • 주요 역할
    • Lazy Loading 회피: 필요할 때만 Eager Loading을 적용할 수 있어 효율적입니다.
    • JOIN 쿼리 최적화: Hibernate가 내부적으로 JOIN을 사용해 연관 데이터를 한 번에 가져옵니다.

 

EntityGraph로 에러를 해결할 수 있었던 이유는?

  • @EntityGraph(attributePaths = “member”)
    • questionsSequenceId와 연관된 member 데이터를 즉시 로딩하도록 설정합니다.
    • 즉, Lazy Loading 대신 → Eager Loading으로 연관 엔티티를 가져와 ByteBuddyInterceptor 예외가 발생하지 않습니다.
  • 동작 쿼리 변화
    : 적용 후엔 JOIN을 통해 member_questions와 members 데이터를 한 번에 가져옵니다.
    EntityGraph
    적용 전
    SELECT * FROM member_questions WHERE member_id = ? AND questions_sequence_id = ?;
    SELECT * FROM members WHERE id = ?;
    EntityGraph
    적용 후
    SELECT mq.*, m.*
    FROM member_questions mq
    JOIN members m ON mq.member_id = m.member_id
    WHERE mq.member_id = ? AND mq.questions_sequence_id = ?;
  • 적용 전후 로딩 방식 비교
    로딩 방식 ByteBuddyInterceptor 발생 여부 이유
    Lazy Loading 발생 member 데이터가 필요할 때 초기화되지 않아 프록시 객체 문제 발생
    Eager Loading 발생하지 않음 member 데이터를 즉시 로드하여 프록시 객체 문제 해결

 


✏️ EntityGraph 주의할 점

✅ Lazy vs. Eager 로딩 전략의 균형

  • @EntityGraph를 과도하게 사용하면 모든 연관 데이터를 한 번에 가져오려는 쿼리로 인해 성능 저하 발생 가능합니다.
  • 따라서 꼭 필요한 경우에만 @EntityGraph를 사용하는 것이 바람직합니다.

 

✅ EntityGraph 외 해결방안

  • JPQL - Fetch Join
    : 쿼리를 직접 작성하여 필요 조건을 좀 더 세밀하게 제어할 수 있습니다.
@Query("SELECT mq FROM MemberQuestion mq JOIN FETCH mq.member WHERE mq.member.memberId = :memberId AND mq.questionsSequenceId = :questionsSequenceId")
Optional<MemberQuestion> findWithMemberByMemberIdAndQuestionsSequenceId(@Param("memberId") Long memberId, @Param("questionsSequenceId") Long questionsSequenceId);

 

  • DTO로 필요한 데이터만 로드
    : Entity 대신 DTO를 사용해 필요한 데이터만 조회하고 전달합니다.
    • 불필요한 엔티티 전체 데이터를 로드하지 않으므로 성능 최적화 가능합니다.
    • 다만, 엔티티 변경 시 DTO와 쿼리도 함께 수정해야 하므로 유지보수의 부담이 있습니다.
// JPQL 쿼리에서 select new 문법을 사용해 특정 필드만 조회 -> 조회 결과를 DTO에 매핑해 반환
// DTO 정의
public class MemberQuestionDTO {
    private Long questionsSequenceId;
    private String memberName;

    public MemberQuestionDTO(Long questionsSequenceId, String memberName) {
        this.questionsSequenceId = questionsSequenceId;
        this.memberName = memberName;
    }
}

// Repository 메서드
@Query("SELECT new com.example.MemberQuestionDTO(mq.questionsSequenceId, m.name) FROM MemberQuestion mq JOIN mq.member m WHERE m.memberId = :memberId AND mq.questionsSequenceId = :questionsSequenceId")
Optional<MemberQuestionDTO> findMemberQuestionDTO(@Param("memberId") Long memberId, @Param("questionsSequenceId") Long questionsSequenceId);

 


✏️ 정리하면

  • ByteBuddyInterceptor: 프록시 객체의 동작 바이트 코드 생성을 담당, 프록시 객체 초기화에 실패하면 연관 데이터가 DB에서 조회되지 않아 문제가 발생할 수 있습니다.
  • @EntityGrap:는 JPA에서 제공하는 에너테이션으로 Eager Loading 방식으로 연관된 데이터를 가져옵니다. (SQL: JOIN을 통해 데이터를 한 번에 조회)
  • Eager Loading의 단점: 모든 연관 데이터를 한 번에 가져오기 때문에 성능 저하로 이어질 수 있습니다.
  • EntityGraph 외 해결방안: JPQL - Fetch Join, DTO를 사용해 필요한 데이터만 선택해 로드하는 방식이 있습니다.
반응형