프로젝트/기술 면접 복습 플랫폼

JsonMappingException 에러

yoon4360 2025. 5. 3. 19:12

최근에 진행 중인 프로젝트에서 프론트엔드와의 연동 테스트 도중 예상치 못한 직렬화 오류가 발생했다. 컨트롤러에서 Qna 엔티티를 그대로 반환했을 뿐인데, Postman에서 응답을 보려고 하니 서버에서 500 에러가 터졌다.
로그를 보니 생전 처음 보는 클래스가 등장했다.

No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor

 

처음에는 Jackson 설정 문제인가 싶었지만, 알고 보니 JPA의 LAZY 로딩 + Hibernate 프록시 객체 직렬화 이슈였다. 이번 포스트에서는 그 문제를 어떻게 파악하고 해결했는지 기록해두려 한다.

 


 

문제 상황- JsonMappingException + ByteBuddy

내가 작업하던 Qna 엔티티는 User 엔티티와 연관 관계를 맺고 있었고, 다음처럼 지연 로딩 설정을 해두고 있었다.

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

 

그리고 /qna/{id} API에서 Qna 객체를 그대로 리턴하려고 했는데, 아래와 같은 직렬화 오류가 발생했다

No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor 
and no properties discovered to create BeanSerializer

 

알고 보니 user 필드는 아직 실제 User 객체가 로딩되지 않은 상태였고,

Hibernate가 대신 만든 ByteBuddy 프록시 객체가 들어 있었던 것이다.

이 객체는 Jackson이 직렬화할 수 없는 내부 필드만 가지고 있어서 예외가 터졌던 것이다.

 


 

해결 방법 고민

처음엔 단순히 FetchType.EAGER로 바꿔볼까 했지만, 이건 오히려 더 위험했다. N+1 문제의 시작이니까...

그래서 좀 더 근본적인 방법들을 검토해봤다.

방법 1: @JsonIgnoreProperties 사용 (빠른 해결)

hibernateLazyInitializer, handler 같은 내부 프록시 필드를 Jackson이 무시하도록 설정할 수 있다.

@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
@Entity
public class User {
    ...
}

 

이 방법으로 직렬화 오류는 사라졌다.

하지만 불필요한 데이터를 여전히 리턴할 수 있고, 엔티티 구조가 변경되면 API 응답도 바뀔 수 있는 위험이 있었다.

빠른 해결에는 좋지만, 장기적인 유지보수 관점에서는 적절하지 않았다.

 

방법 2: DTO 패턴 적용 (근본적인 해결)

그래서 결국, 안전하고 명확한 방식인 DTO 변환을 선택했다.

public record QnaResponse(String question, String answer) {
    public static QnaResponse from(Qna qna) {
        return new QnaResponse(qna.getQuestion(), qna.getAnswer());
    }
}

 

컨트롤러에서는 엔티티를 그대로 반환하지 않고, DTO로 변환해서 리턴했다:

@GetMapping("/qna/{id}")
public ResponseEntity<QnaResponse> getQna(@PathVariable UUID id) {
    Qna qna = qnaService.findById(id);
    return ResponseEntity.ok(QnaResponse.from(qna));
}

 

이렇게 하니 직렬화 이슈도 사라지고, API 응답 포맷도 명확하게 정의할 수 있었다.

 


 

결론

이번 문제는 단순히 직렬화 설정의 문제가 아니라,
JPA의 LAZY 로딩과 Jackson 직렬화 간의 구조적 충돌이었다.

처음에는 엔티티에 @JsonIgnoreProperties 하나 붙이면 될것이라고 생각했지만,
프로젝트가 커지고, 연관 관계가 복잡해질수록 이 방식이 얼마나 위험할 수 있는지 깨달았다.

  • 🔥 엔티티 직접 반환 → 구조 변경 시 API도 깨짐
  • 🔥 보안 측면에서도 위험 (예: 패스워드 같은 민감 필드 노출 가능)
  •  DTO를 쓰면 필요한 정보만 명확하게 선택 가능하고
  •  응답 스펙이 고정되기 때문에 API 테스트나 문서화에도 유리

개인적으로는 앞으로 어떤 엔티티든 API 응답으로 쓸 때는 무조건 DTO로 변환해서 리턴하는 습관을 들이기로 했다.
지금은 약간 귀찮을 수 있어도, 나중을 생각하면 이게 훨씬 안전하고 깔끔하다.

 

혹시 비슷한 문제로 고생 중이라면, 처음에는 @JsonIgnoreProperties로 빠르게 해결해보고,
장기적으로는 DTO 패턴으로 넘어가는 걸 추천한다.