1️⃣ 문제 상황
게시글 수정 기능을 구현한 뒤, 이상한 현상을 발견했다.
❗ 증상
- 수정 버튼 클릭 → “수정에 실패했습니다” 알림
- 하지만 새로고침하면?
- ✅ DB에는 수정 내용이 반영되어 있음
👉 수정은 성공했는데, 화면에는 실패로 보이는 상황
2️⃣ 에러 로그 확인
서버 로그에서 가장 중요한 메시지는 이 문장이었다.
InvalidDefinitionException:
No serializer found for class
org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor
처음 보면 굉장히 intimidating 하지만,
의미만 풀면 아주 단순한 문제다.
3️⃣ 에러 원인 분석
한 줄 요약
지연 로딩(LAZY) 상태의 엔티티를 그대로 JSON으로 반환하려다 Jackson이 폭발했다.
무슨 일이 벌어졌을까? (순서대로)
- 사용자가 수정 버튼 클릭
- 서버에서 update 로직 실행
- ✅ DB 업데이트 성공
- 컨트롤러에서 수정된 Article 엔티티를 그대로 반환
- Jackson이 JSON 변환 시도
- Article.author가 지연 로딩 프록시 객체
- Jackson: “이 가짜 객체(ByteBuddyProxy)를 JSON으로 어떻게 바꾸라는 거야?”
- 💥 500 Internal Server Error 발생
왜 author가 문제일까?
@ManyToOne(fetch = FetchType.LAZY)
private User author;
- author는 실제 User가 아니라 프록시 객체
- Jackson은 이 프록시를 직렬화할 방법을 모름
- 그 결과 InvalidDefinitionException 발생
4️⃣ 그래서 왜 DB는 바뀌었을까?
이 부분이 가장 헷갈리는 포인트다.
실제 작업 순서
- Hibernate가 DB에 update 쿼리 실행
- 트랜잭션 내부에서 변경 사항 반영
- 응답을 만들다가 JSON 변환 단계에서 실패
- 서버는 500 에러 반환
- 프론트엔드는 실패로 인식
👉 즉,
“일은 다 끝냈는데, 결과 보고서 작성하다가 넘어진 상황”
그래서 화면에는 실패가 뜨지만, DB에는 이미 반영되어 있었던 것.
5️⃣ 문제의 근본 원인
컨트롤러에서 엔티티를 직접 반환하고 있었다.
❌ 나쁜 예 ❌
@PutMapping("/api/articles/{id}")
public ResponseEntity<Article> updateArticle(
@PathVariable long id,
@RequestBody UpdateArticleRequest request) {
Article updatedArticle = blogService.update(id, request);
// 🚨 엔티티 직접 반환
return ResponseEntity.ok(updatedArticle);
}
6️⃣ 해결 방법
✅ 정석적인 해결책: DTO를 반환하자
엔티티는 DB용 객체
API 응답은 DTO(Data Transfer Object)
✔ 수정된 컨트롤러 코드
@PutMapping("/api/articles/{id}")
public ResponseEntity<ArticleResponse> updateArticle(
@PathVariable long id,
@RequestBody UpdateArticleRequest request) {
Article updatedArticle = blogService.update(id, request);
return ResponseEntity.ok(new ArticleResponse(updatedArticle));
}
// 또는 수정 성공 여부만 전달
// return ResponseEntity.ok().build();
7️⃣ DTO 예시
ArticleResponse
@Getter
public class ArticleResponse {
private final String title;
private final String content;
private final String author;
public ArticleResponse(Article article) {
this.title = article.getTitle();
this.content = article.getContent();
this.author = article.getAuthor().getNickname();
}
}
ArticleListViewResponse
@Getter
public class ArticleListViewResponse {
private final long id;
private final String title;
private final String content;
private final String author;
public ArticleListViewResponse(Article article) {
this.id = article.getId();
this.title = article.getTitle();
this.content = article.getContent();
this.author = article.getAuthor().getNickname();
}
}
8️⃣ 왜 DTO를 써야 할까?
① Jackson 직렬화 안정성
- DTO는 순수 데이터(String, 숫자) 만 포함
- 프록시, 지연 로딩, Hibernate 내부 구현 ❌
② 보안
- 엔티티를 그대로 반환하면:
- 비밀번호
- 내부 필드
- 연관 객체
가 의도치 않게 노출될 위험
- DTO는 보여주고 싶은 것만 선택
③ 순환 참조 방지
- 엔티티 간 양방향 연관관계
- 그대로 반환 시 무한 루프
- DTO는 이런 문제를 구조적으로 차단
9️⃣ 한 줄 요약
“엔티티를 API 응답으로 바로 던지는 순간, 언젠가는 반드시 터진다.”
→ 항상 DTO에 담아서 보내자.
'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
| 👥💡 Querydsl 페이징: fetchResults()가 비권장(Deprecated)된 이유와 실무 정석 패턴 (0) | 2026.02.14 |
|---|---|
| 👥🚨 수정시간 및 수정하기 작성자 권한 변경 (0) | 2026.02.14 |
| 👥💡 N+1 리팩토링 과정 (1) | 2026.02.07 |
| 👥🚨 로컬로 실행 후 배포한 서버에도 DB가 적용되는 이유 (0) | 2026.02.07 |
| 👥🚨 배포 후 로컬 실행 시 OAuth2 승인 오류 (0) | 2026.02.07 |