ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 연관관계 엔티티 직렬화 오류와 해결 (DTO 패턴)
    Jpa 2025. 7. 7. 19:20

     

    서론

    Spring Boot로 영화 및 리뷰 관리 API를 개발하던 중, 영화 리뷰 목록을 조회하는 API에서 직렬화 오류가 발생했습니다. 해당 오류는 Review 엔티티가 Movie 엔티티와 연관관계를 맺고 있는 상황에서 발생했으며, 문제를 해결하기 위해 3가지 방법을 고려했습니다. 이 글에서는 문제의 원인과 각 해결 방법을 소개하고, 최종적으로 선택한 DTO 패턴 방식에 대해 포스팅하겠습니다. 

     

     

    문제 상황

    영화 리뷰 목록 조회 API:

    GET /movies/{movieId}/reviews?sort=rating

    정상적으로 리뷰 데이터를 받아올 것으로 예상했지만, 아래와 같은 에러가 발생했습니다.

     

    com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
    No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor ...

    해당 에러는 Review 엔티티가 @ManyToOne으로 Movie를 참조하고 있고, 지연 로딩(LAZY)으로 인해 Movie가 프록시 객체로 변환되면서 발생한 직렬화 오류입니다.

     

     

    원인 분석

    Review 엔티티 구조 일부

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "movie_id", nullable = false)
    private Movie movie;
    • Movie 필드는 JPA의 지연 로딩(LAZY) 설정으로 인해 실제 객체가 아닌 Hibernate의 프록시 객체(ByteBuddyInterceptor)로 감싸진 상태였고, Jackson은 이를 직렬화할 수 없어 예외가 발생했습니다.

     

    고려한 3가지 해결 방안

    1. @JsonIgnore 사용

    @JsonIgnore private Movie movie;
    • 장점: 간단한 방법으로 직렬화 제외.
    • 단점: 일부 API에서는 movie 정보가 필요할 수 있어 재사용이 어렵고 유연하지 않음. API 스펙을 위해 엔티티를 변경하는 것은 매우 좋지 않음.

     

    2. @JsonIgnoreProperties 사용

    @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) 
    private Movie movie;
    • 장점: LAZY 로딩 프록시 속성만 무시 가능.
    • 단점: hibernateLazyInitializer, handler 속성만 무시할 수 있어, 연관 객체 전체가 필요한 경우에는 여전히 문제가 발생할 수 있음. JPA 내부 구현에 의존하므로 유지보수에 불리함.

     

    3. DTO 패턴 사용 (선택한 방법)

    • 엔티티 대신 DTO 객체에 필요한 필드만 담아서 응답:
    @Getter
    public class ReviewDto {
        private Long id;
        private String content;
        private double rating;
        private String createdAt;
    
        public ReviewDto(Review review) {
            this.id = review.getId();
            this.content = review.getContent();
            this.rating = review.getRating();
            this.createdAt = review.getCreatedAt().toString();
        }
    }

     

    • 컨트롤러에서 DTO로 변환하여 응답:
    @GetMapping("/movies/{movieId}/reviews")
    public List<ReviewDto> getReviews(@PathVariable Long movieId,
                                      @RequestParam(defaultValue = "createdAt") String sort) {
        return reviewService.findAll(movieId, sort).stream()
            .map(ReviewDto::new)
            .toList();
    }

     

     

     

    최종 선택: DTO 방식

    선택 이유:

     

    • 응답 구조를 명확하게 분리함으로써 클라이언트에 불필요한 내부 구현 세부사항이 노출되지 않음
    • 엔티티의 순환 참조 문제를 원천 차단
    • 향후 API 변경이나 확장에 유연하게 대응 가능
    • Jackson 직렬화 문제를 해결하는 가장 표준적이고 유지보수성 높은 방식

     

     

    느낀 점

    Spring Boot에서 JPA를 활용할 때, 엔티티 그대로 리턴하는 방식은 간단해 보이지만 위험한 선택일 수 있다. 연관관계가 복잡해질수록 직렬화 문제가 쉽게 발생하고, 유지보수성도 떨어지기 때문입니다.
    앞으로는 API의 모든 요청과 응답에서 DTO를 사용하여 데이터 구조를 명확히 관리하고, 엔티티는 서비스 및 리포지토리 계층 등 비즈니스 로직 내부에서만 사용하도록 설계할 계획입니다.

     

     

     

Designed by Tistory.