객체 직렬화(Serialization)

청포도봉봉이·2023년 10월 16일
1

Spring

목록 보기
26/35
post-thumbnail

개요

SpringBoot JPA에서 API 테스트 코드를 짜던 중에

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

위와 같은 에러가 났다.

내가 짜던 소스는 Post Entity와 Reply Entity를 이용하여 Post List API의 Response에서 Post와 Reply List를 같이 뿌려주고 싶었다.

[
    {
      "id": 1,
      "post_id": 1,
      "email": "a@a.com",
      "nickname": "aUser",
      "contents": "comment update1 !",
      "createdDate": "2023-09-03T20:03:40.515621",
      "updatedDate": "2023-09-03T20:10:55.459202",
      "replyList": [
        {
          "reply_id": 1,
          "email": "a@a.com",
          "nickname": "aUser",
          "contents": "reply modify test 2",
          "createdDate": "2023-10-03T15:36:50.400402",
          "updatedDate": "2023-10-06T20:52:57.450132"
        },
        {
          "reply_id": 2,
          "email": "a@a.com",
          "nickname": "aUser",
          "contents": "create reply comment second!",
          "createdDate": "2023-10-04T22:37:17.601544",
          "updatedDate": "2023-10-04T22:37:17.601544"
        }
      ]
    }
]

API를 만들던 중 Entity 자체를 반환 타입으로 사용하려고 했는데 직렬화 에러가 났다.

직렬화는 뭐고 왜 에러가 나는지 정리해 보았다.




직렬화, 역직렬화

직렬화(Serialization)란, 객체의 상태를 바이트 스트림으로 변환하는 과정을 의미한다. 이는 네트워크로 전송하거나 파일에 저장하기 위해 사용된다. 반대로, 직렬화된 바이트 스트림을 다시 객체로 복원하는 것을 역직렬화(Deserialization)라고 한다.




문제 파악 및 해결 방법


Comment Entity

package study.till.back.entity;

import lombok.*;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Setter
public class Comment extends BaseTimeEntity {

    @Id
    @GeneratedValue
    @Column(name = "comment_id")
    private long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "email")
    private Member member;

    private String contents;

    @Builder.Default
    @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Reply> replyList = new ArrayList<>();

    public void updateComment(String contents) {
        this.contents = contents;
    }
}

Reply Entity

package study.till.back.entity;

import lombok.*;

import javax.persistence.*;

@Entity
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Reply extends BaseTimeEntity {

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

    private String contents;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "comment_id")
    private Comment parentComment;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "email")
    private Member member;
}

InvalidDefinitionException

com.fasterxml.jackson.databind.exc.InvalidDefinitionException은 Jackson 라이브러리가 Java 객체를 JSON으로 변환하려고 할 때, 클래스 정의에 문제가 있어서 발생하는 예외다.

JPA 엔티티를 직접 반환하려고 하면, InvalidDefinitionException이 발생할 수 있다. 이는 JPA 엔티티가 지연 로딩(Lazy Loading) 전략을 사용하여 연관된 객체를 로드하는 경우에 특히 그럴 수 있는데 이 때문에 Jackson이 해당 프록시 객체를 올바르게 직렬화하지 못한다.

해당 에러는 Hibernate 프록시 객체를 직렬화하려고 시도할 때 발생한다. Hibernate는 Lazy Loading 전략을 사용하여 데이터베이스에서 필요한 데이터만 가져오기 위해 프록시 객체를 생성합니다. 이런 프록시 객체는 실제 엔티티 클래스의 하위 클래스로 동적으로 생성되며, ByteBuddyInterceptor라는 Hibernate 내부 클래스 인스턴스를 포함한다.

그런데 ByteBuddyInterceptor는 직렬화할 수 있는 형태가 아니므로 위와 같은 에러가 발생한다.

해결 방법 중 하나로 DTO(Data Transfer Object) 패턴을 사용하여 필요한 데이터만 선택적으로 제공하는 것이다.


Entity를 그대로 반환할 때 생기는 문제점

  1. Lazy Initialization Exception: Post 엔티티에는 Member라는 연관 관계 필드가 있고, 이 필드는 LAZY 로딩 전략을 사용하고 있다. 즉, Post 객체가 실제로 사용될 때까지 해당 Member 객체의 로딩을 지연시키는 것이다. 그러나 API 요청 처리가 끝나고 세션이 닫혀버리면, 이 후에 Member 객체를 로딩할 수 없게 됩니다. 따라서 직렬화 과정에서 해당 필드에 접근하려 할 때 "could not initialize proxy - no Session" 같은 Lazy Initialization Exception이 발생한다.

  2. Infinite Recursion (StackOverflowError): 만약 Member 엔티티와 Post 엔티티 사이에 양방향 연관 관계가 있다면 무한 순환 참조 문제가 발생할 수 있다. 예를 들어, 각각의 'post'들이 'member'를 참조하고 있는데 반대로 'member'도 자신의 'posts' 목록을 가지고 있다면, JSON 변환 시 무한 순환 참조로 인해 StackOverflowError가 발생한다.


DTO 생성

package study.till.back.dto.comment;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import study.till.back.dto.reply.ReplyDTO;
import study.till.back.entity.Comment;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class FindCommentResponse {
    private Long id;
    private Long post_id;
    private String email;
    private String nickname;
    private String contents;
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;
    private List<ReplyDTO> replyList = new ArrayList<>();

    public static FindCommentResponse fromEntity(Comment comment) {
        return FindCommentResponse.builder()
                .id(comment.getId())
                .post_id(comment.getPost().getId())
                .email(comment.getMember().getEmail())
                .nickname(comment.getMember().getNickname())
                .contents(comment.getContents())
                .createdDate(comment.getCreatedDate())
                .updatedDate(comment.getUpdatedDate())
                .replyList(ReplyDTO.fromEntities(comment.getReplyList()))
                .build();
    }
}

반환 타입을 DTO(Data Transfer Object)로 변경하면 엔티티의 직렬화 문제를 해결할 수 있다.

DTO는 계층 간 데이터 전송을 위해 설계된 객체로, 필요한 데이터만 담고 있는 Plain Old Java Object (POJO)이다. 이는 JPA 엔티티와 같은 비즈니스 로직이나 관계를 가지지 않으므로 직렬화에 안전하다.


CommentService

@Service
@RequiredArgsConstructor
public class CommentService {

    private final MemberRepository memberRepository;
    private final PostRepository postRepository;
    private final CommentRepositroy commentRepositroy;

    public ResponseEntity<List<FindCommentResponse>> findComments() {
        List<Comment> comments = commentRepositroy.findAll();

        List<FindCommentResponse> findCommentResponses = comments.stream()
                .map(FindCommentResponse::fromEntity)
                .collect(Collectors.toList());

        return ResponseEntity.ok(findCommentResponses);
    }
}

위와 같이 DTO를 반환 타입으로 지정 후 변환해주어 클라이언트에게 필요한 정보만 전달되며, 직렬화 문제도 회피할 수 있다.

DTO 패턴은 API 디자인에서 일반적으로 사용되는 패턴 중 하나이며, 보안과 성능 면에서 좀 더 안전하고 효율적인 방식이다.

profile
서버 백엔드 개발자

0개의 댓글