[Java] InvalidDefinitionException 에러 - Hibernate Lazy 로딩 프록시 객체 문제

Sadie·2025년 3월 23일

Error

목록 보기
3/3

문제 상황

Springboot와 JPA 환경에서 멤버별 결과를 반환하는 코드를 구현하고 있었습니다
아래는 처음 작성했던 코드입니다

// Controller

@GetMapping("/result/{memberId}")
    public ResponseEntity<BaseResponse<List<TutorialResult>>> getTutorialResult(
            @PathVariable Long memberId
    ) {
        return ResponseEntity.ok(new BaseResponse<>(tutorialService.getTutorialResult(memberId)));
    }
// Service

public List<TutorialResult> getTutorialResult(Long memberId) {
        return tutorialRepository.findByMember_MemberId(memberId);
    }

다음과 같은 코드를 실행했을 때, InvalidDefinitionException 오류가 발생했습니다

// TurotialResult Entity

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long tutorialResultId;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "company_id", referencedColumnName = "companyId", nullable = false)
    private Company company;

    @Column(nullable = false)
    private int startMoney;

    @Column(nullable = false)
    private int endMoney;

    @Column(nullable = false)
    private LocalDateTime startDate;

    @Column
    private LocalDateTime endDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id", referencedColumnName = "memberId", nullable = false)
    private Member member;

원인은 JPA의 Lazy Loading 이었습니다

@ManyToOne(fetch = FetchType.LAZY) 또는 @OneToOne(fetch = FetchType.LAZY)를 사용하면, 연관된 엔티티는 프록시 객체로 로딩됩니다
프록시 객체는 실제 클래스가 아니기 때문에, Jackson 라이브러리는 이것을 직렬화할 수 없습니다
따라서 컨트롤러에서 엔티티를 직접 반환하는 것을 요구하는 위 코드에서 다음과 같은 에러가 발생했습니다


해결방법 (DTO를 이용)

이런 직렬화 문제를 해결하는 방법은 여러가지가 있지만, 가장 권장되는 방법은 DTO(Data Transfer Object)를 이용하는 것입니다


@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})

@JsonIgnoreProperties 같은 어노테이션을 사용해서 프록시 객체 직렬화 오류를 무시할 수도 있지만

직렬화 시 어떤 필드가 포함되고 빠질지 예측하기 어렵고, 프로젝트가 커질수록 일관성이 떨어지며, 연관 엔티티가 늘어날수록 유지보수가 어려워집니다


반면 DTO를 이용하면

  1. 프록시 직렬화 문제를 방지 (직접 필요한 데이터만 추출해서 프록시 객체가 포함되지 않음)
  2. 응답의 명확성과 안전성 확보 (클라이언트에 어떤 데이터 전달할지 명확, 민감하거나 불필요한 데이터 노출 막을 수 있음)
  3. 코드 유지보수성 향상 (추후 변경이 생겨도 도메인 로직과 응답 구조를 독립적으로 관리 가능)

다음과 같은 장점을 가지기 때문에 DTO 방식이 가장 권장됩니다


// DTO

@Getter
public class TutorialResultResponse {

    private Long tutorialResultId;

    private Long companyId;
    private String companyName;

    private int startMoney;
    private int endMoney;

    private LocalDateTime startDate;
    private LocalDateTime endDate;

    private Long memberId;
    private String memberName;

    public TutorialResultResponse(TutorialResult entity) {
        this.tutorialResultId = entity.getTutorialResultId();
        this.startMoney = entity.getStartMoney();
        this.endMoney = entity.getEndMoney();
        this.startDate = entity.getStartDate();
        this.endDate = entity.getEndDate();

        // Lazy 객체에서 필요한 값만 추출
        Company company = entity.getCompany();
        if (company != null) {
            this.companyId = company.getCompanyId();
            this.companyName = company.getCompanyName();
        }

        Member member = entity.getMember();
        if (member != null) {
            this.memberId = member.getMemberId();
            this.memberName = member.getNickname();
        }
    }
}

다음과 같은 TutorialResultResponse 이라는 새로운 엔티티를 만들어서 문제를 해결했습니다

// Controller

@GetMapping("/result/{memberId}")
     public ResponseEntity<BaseResponse<List<TutorialResultResponse>>> getTutorialResult(
             @PathVariable Long memberId
     ) {
         return ResponseEntity.ok(new BaseResponse<>(tutorialService.getTutorialResult(memberId)));
     }
// Service

public List<TutorialResultResponse> getTutorialResult(Long memberId) {
        return tutorialRepository.findByMember_MemberId(memberId)
                .stream()
                .map(TutorialResultResponse::new)
                .collect(Collectors.toList());
    }

컨트롤러와 서비스도 다음과 같이 코드를 수정하여 문제를 해결했습니다

0개의 댓글