BackEnd/JPA

[JPA] Lazy 로딩으로 인한 JSON 반환 오류 (No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer)

샤아이인 2022. 9. 7.

 

1. 문제의 상황

Issue 와 Comment 는 1:N의 관계이다. 따라서 Comment의 코드를 보면 다음과 같이 Lazy 로딩이 걸려있다.

 

▶ Comment

@Getter
@Entity
@EqualsAndHashCode(of = "id")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment extends BaseTimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;
    
    // 생략...
}

 

문제는 내가 Issue에 Comment를 추가 한 후, CommentDto를 통해 해당 정보를 반환하려 할 때 발생한다.

우선 해당 CommentDto는 다음과 같다.

 

▶ CommentDto

@Getter
@RequiredArgsConstructor
public class CommentDto {

    private final Long id;
    private final User user;
    private final String description;
    private final String avatarUrl;
    private final LocalDateTime createdDateTime;

    public static CommentDto of(Comment comment) {
        return new CommentDto(comment.getId(), comment.getUser(), comment.getDescription(), comment.getUser().getAvatarUrl(), comment.getCreatedDateTime());
    }
}

뒤에서 해결하겠지만, 위 코드는 User 도메인을 직접 반환한다는 문제가 도사리고 있다.

Entity를 직접 반환하는 것 은 좋지 못하다, DTO로 바꿔서 반환시켜야 한다.

 

Issue에 Comment를 추가하고 DTO로 변환하여 반환하는 코드는 다음과 같다.

 

▶ CommentService

public class CommentService {

    public CommentDto add(Long issueId, String comment, String userEmail) {
        // 생략...

        Comment newComment = Comment.builder()
                .description(comment)
                .issue(findIssue)
                .user(findUser)
                .build();

        findIssue.addComment(newComment);
        Issue savedIssue = issueRepository.save(findIssue);
        return CommentDto.of(savedIssue.getLastComment());
    }
}

CommentDto.of()의 인자로 넘어가는 comment에는 User가 초기화되있지 않다.

FetchType이 Lazy라 실제 User가 아닌, Proxy(User$HibernateProxy$wEvbAcCi["hibernateLazyInitializer"])가 담겨있다.

 

따라서, 해당 Proxy를 Serializer 시키려 하니 문제가 발생한 것 이다.

 

2. 해결 방법

2-1) application.properties에 spring.jackson.serialization.fail-on-empty-beans=false 추가하기

다음과 같이 설정에 추가해주자.

spring.jackson.serialization.fail-on-empty-beans=false

다만 이 방식은 문제의 근본을 해결한 방식이 아니다.

위 결과처럼 반환이 되기는 하는데, 우리가 축가한 적 없는 hibernateLazyInitializer 라는 필드가 생기게 된다.

 

2-2) Dto에서 초기화 해주기

개인적으로 Dto를 통해 초기화 시키는 방식을 선호한다.

우선 다음과 같이 UserResponseDto를 만들어보자!

 

▶ UserResponseDto

@Getter
@NoArgsConstructor
public class UserResponseDto {

    private String userName;
    private String avatarUrl;

    private UserResponseDto(String userName, String avatarUrl) {
        this.userName = userName;
        this.avatarUrl = avatarUrl;
    }

    public static UserResponseDto of(User user) {
        return new UserResponseDto(user.getUserName(), user.getAvatarUrl());
    }
}

이를 사용하여 CommentDto를 변경해보자.

 

▶ CommentDto

@Getter
@RequiredArgsConstructor
public class CommentDto {

    private final Long id;
    private final UserResponseDto userResponseDto;
    private final String description;
    private final String avatarUrl;
    private final LocalDateTime createdDateTime;

    public static CommentDto of(Comment comment) {
        return new CommentDto(comment.getId(),
                UserResponseDto.of(comment.getUser()),
                comment.getDescription(),
                comment.getUser().getAvatarUrl(),
                comment.getCreatedDateTime()
        );
    }
}

dto 내부에서 User를 변환하면서 초기화 시키고 있다. 따라서 정상적으로 반환할 수 있게되었다.

결과는 다음과 같이 나온다.

또한 이전과 달리 User Entity를 직접 반환하여 모든 정보를 공개하는 것 이 아니라, 내가 원하는 User의 정보만 반환하도록 되었다.

댓글