단순히 현재 저장하고 있는 어떤 객체의 목록을 불러오는 api를 만들고, 호출해보니 Postman에서 이런 에러가 떴다.
참고로 호출된 객체는 다음과 같다.
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
Order라는 객체 목록을 불러왔고, 이 안에는 분명 "ByteBuddyInterceptor"라는 자료형이 없는데 (다른 데에도 선언하지 않았다.)
왜 이런 타입이 맞지 않다고하는지 궁금해 알아보았다.
우선 "ByteBuddyInterceptor" 라는 클래스는 "org.hibernate.proxy.pojo.bytebuddy"라는 곳에 존재한다.
첫 번째, hibernate는 hibernate ORM 이라고도 불리는데, ORM은 "Object Relational Mapping" 의 약자이다.
즉, 이름 자체가 관계형 데이터 베이스(RDB) 테이블을 자바 객체로 매핑시킨다는 의미를 포함하고 있다고 볼 수 있는데, 여기서는 일단 "hibernate가 객체의 관계를 다룬다"라고만 알면 될 것 같다.
두 번째, proxy는 존재 이유부터 알아보니 이 문제에 대해서 이해하기 한결 쉬워졌다.
Order라는 엔티티 안에 Member 엔티티가 포함되어 있다. 그런데, 과연 Order를 조회할 때 항상 Member도 같이 조회해야 할까?
이 질문에 대한 답은 당연히 "아니다"이다.
특히 비즈니스 로직에서 그 특징이 두드러지는데, 만약 내가 유저 "suhwan"이 가진 잔액을 조사하려 "suhwan"을 조회하는데, 이 사람의 성별까지 꺼내서 알려줄 필요 없다는 것이다.
일상생활에서도 A만 물어봤는데, 이것저것 알려주면 귀찮을 때가 있는데, 비즈니스에서는 손실이 발생하므로 이 부분은 더 큰 문제가 된다.
따라서 hibernate는 이 부분에 대해 "실제 엔티티 객체"를 내부적으로 상속받아 만들어 주는데, 이를 "proxy 객체"라고 한다.
이 객체는 가짜이지만 겉모양은 실제와 같아 사용자 입장에서는 똑같이 보인다.
한 단계 깊이 내려가면 proxy 객체는 여러 가지를 포함하고 있는데, 위 상황에서는 그 안에 있는 ByteBuddyInterceptor라는 녀석이 가짜 객체 역할을 하고 있는 것이다.
그럼, 여기서 문제는 위 코드의 어느 부분에서 proxy 객체를 썼다는 것인가? 라 생각 가능한데..
이 부분은 Member 엔티티가 위 어노테이션처럼 (fetch = LAZY)로 선언되어있기 때문이다.
이 fetch = LAZY는 지연 로딩 방식이라고 하는데, 이를 위 어노테이션에 적어주게 되면, 로딩되는 시점에 해당 엔티티는 proxy 객체로 가져오게 된다. 그 후 실제로 사용하는 시점에 초기화가 된다. (DB에 쿼리가 나간다.)
따라서 결론적으로는 지연 로딩으로 인해 Order 엔티티를 조회할 때는 아직 Member 엔티티가 "ByteBuddyInterceptor" 이므로 type definition error 가 나왔다고 볼 수 있다.
해결 방법
1. 간단하게 지연 로딩을 즉시 로딩으로 바꾸는 방법이 있다. 이 방법은 LAZY -> EAGER로 바꿔주면 된다.
하지만 이 방법은 실무에서 매우 권장되지 않는 방법이라고 한다. 이유는 N + 1 문제가 발생하기 때문인데, 이 문제는 마치 고구마 하나를 캤는데 아래 고구마들이 우르르 딸려 나온다는 느낌과 같다. (여기서 1이 우리가 조회한 것이고 N이 딸려 나온 고구마들을 의미한다.)
이유는 위 proxy 존재 이유에서 설명했으므로 생략하겠다.
2. "datatype-hibernate5" 설치
hibernate5에서 이런 문제를 내부적으로 처리해주는 것이 있다고 한다. 나는 build.gradle에서 아래 문장을 추가해주었다.
//hibernate 5
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5'
또한 이를 실행하기 위해 프로그램의 메인 함수 안에 Hibernate5Module을 만들어주면 된다.
@Bean
Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
3. 사실 이런 방법을 고민하면 안된다!
지금까지 문제는 api에서 엔티티를 불러 올 때 타입이 맞지 않는 것이 문제였는데, 사실 api 응답으로 엔티티를 외부로 노출하는 것은 좋지 않다. 왜냐하면 외부로 노출되는 순간 해당 엔티티가 어디서 바뀔 지 종잡을 수 없기 때문이다.
따라서 엔티티를 그대로 돌리는 것보단, DTO(Data Transfer Object)를 만들어 필요한 것만 보내는 것이 옳은 방법이다.
또한 DTO를 만들고 보내는 과정에서 해당 데이터들을 참조하기 때문에 proxy 객체 이슈 발생 가능성을 없애는 효과도 기대할 수 있겠다.