[데브코스] Spring Boot REST API 실습 (8강) - 엔티티가 바로 노출되는 것은 좋지 않다 — PostDto 도입

zuno·2025년 12월 20일

이번 글은 데브코스 Spring Boot REST API 실습 8강 내용을 정리한 글이다.
8강에서는 엔티티를 그대로 API 응답으로 반환하지 않고,
DTO(PostDto)를 도입하는 과정을 3단계 실습으로 나누어 진행했다.

이 강의의 핵심은
DTO를 “만드는 것”보다, DTO를 잘못 설계했을 때 어떤 문제가 발생하는지
직접 경험하도록 하는 데 있다.


1️⃣ 8강의 출발점 — 왜 엔티티를 바로 노출하면 안 될까?

이전 강의(7강)까지는 컨트롤러에서 Post 엔티티를 그대로 반환했다.

return post;

이 방식의 문제점은 다음과 같다.

  • 엔티티 구조가 그대로 API 스펙이 됨
  • DB 구조 변경이 API 변경으로 직결됨
  • 프론트엔드 요구사항에 따라 엔티티가 흔들림

그래서 8강에서는
엔티티와 API 응답을 분리하기 위해 DTO를 도입한다.


2️⃣ 실습 1 — PostDto 도입 (하지만 실패)

📌 첫 번째 커밋

PostDto 클래스 도입, JSON 응답 실패

먼저 PostDto 클래스를 만들고,
컨트롤러의 반환 타입을 PostPostDto로 변경한다.

PostDto (첫 번째 버전)

public class PostDto {
    private int id;
    private LocalDateTime createdDate;
    private LocalDateTime modifiedDate;
    private String subject;
    private String body;

    public PostDto(Post post) {
        this.id = post.getId();
        this.createdDate = post.getCreateDate();
        this.modifiedDate = post.getModifyDate();
        this.subject = post.getTitle();
        this.body = post.getContent();
    }
}

컨트롤러

@GetMapping("/{id}")
public PostDto getItem(@PathVariable int id) {
    Post post = postService.findById(id).get();
    return new PostDto(post);
}

하지만 조회 요청을 보내면
JSON 응답이 실패하거나 비어 있는 문제가 발생한다.


3️⃣ 왜 실패했을까? — Jackson 직렬화의 기본 규칙

Jackson은 객체를 JSON으로 변환할 때
Java Bean 규약을 따른다.

즉, 기본적으로 다음 요소만 JSON 필드로 인식한다.

  • public getter 메서드
  • 또는 public 필드

하지만 첫 번째 PostDto

  • 모든 필드가 private
  • getter 메서드가 하나도 없음

👉 Jackson 입장에서는
“외부로 노출 가능한 데이터가 하나도 없는 객체”로 보인다.

결과적으로 Jackson은 이 객체를
Empty Bean(빈 객체) 로 판단한다.


4️⃣ 실습 2 — 임시 대응: fail-on-empty-beans=false

📌 두 번째 커밋

빈 객체라도 오류를 발생시키지 않도록 설정

spring:
  jackson:
    serialization:
      fail-on-empty-beans: false

이 설정의 의미는 다음과 같다.

  • 직렬화할 필드가 없어도
  • 에러는 발생시키지 말고
  • 빈 JSON 객체 {}로 응답

👉 에러는 사라지지만
👉 데이터는 여전히 내려오지 않는다

즉, 이는 근본적인 해결이 아니라 임시 회피에 불과하다.


5️⃣ 실습 3 — 정답: PostDto에 @Getter 적용

📌 세 번째 커밋

PostDto에 @Getter 적용하여 JSON 정상 출력

@Getter
public class PostDto {
    private int id;
    private LocalDateTime createdDate;
    private LocalDateTime modifiedDate;
    private String subject;
    private String body;

    public PostDto(Post post) {
        this.id = post.getId();
        this.createdDate = post.getCreateDate();
        this.modifiedDate = post.getModifyDate();
        this.subject = post.getTitle();
        this.body = post.getContent();
    }
}

@Getter를 적용하면 Lombok이 다음 메서드를 자동 생성한다.

getId()
getCreatedDate()
getModifiedDate()
getSubject()
getBody()

이제 Jackson은 getter를 통해 값을 읽을 수 있고,
JSON 응답도 정상적으로 생성된다.


6️⃣ 그런데 JSON 필드 순서가 이상하지 않을까?

실제 JSON 응답은 다음과 같이 출력된다.

{
  "body": "내용 1",
  "createdDate": "2025-12-21T01:47:02.82079",
  "id": 1,
  "modifiedDate": "2025-12-21T01:47:02.82079",
  "subject": "제목 1"
}

하지만 PostDto의 필드 선언 순서는 다음과 같다.

private int id;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;
private String subject;
private String body;

id가 맨 앞에 나오지 않는 이유는 무엇일까?


7️⃣ JSON 필드 순서가 달라지는 이유

Jackson은 JSON을 생성할 때
자바 필드 선언 순서를 기준으로 하지 않는다.

기본적으로 Jackson은:

  • getter 메서드를 기준으로 프로퍼티를 수집하고
  • 프로퍼티 이름을 알파벳 순으로 정렬해 출력한다

즉, 다음 이름들이

body
createdDate
id
modifiedDate
subject

사전순으로 정렬되어
현재와 같은 JSON 출력 결과가 나온다.


8️⃣ JSON 필드 순서를 제어하고 싶다면?

필요하다면 @JsonPropertyOrder를 사용해
명시적으로 순서를 지정할 수 있다.

@JsonPropertyOrder({
    "id",
    "subject",
    "body",
    "createdDate",
    "modifiedDate"
})
@Getter
public class PostDto {
    ...
}

다만 실무에서는
JSON은 키 기반 접근이므로 순서를 크게 신경 쓰지 않는 경우가 많다.


9️⃣ 8강에서 의도한 핵심 메시지

  • 엔티티는 내부 도메인 모델이다
  • API 응답은 DTO로 분리해야 한다
  • Jackson은 getter 기준으로 직렬화한다
  • 설정으로 문제를 덮기보다 구조를 올바르게 잡는 것이 중요하다

🔚 8강 정리

  • 엔티티를 그대로 반환하는 구조의 문제점을 확인했다
  • DTO를 도입했지만 getter가 없어 JSON 직렬화에 실패했다
  • fail-on-empty-beans=false는 임시 대응일 뿐이다
  • DTO에는 반드시 명확한 getter가 필요하다
  • JSON 필드 순서는 기본적으로 알파벳 순으로 정렬된다

💡 한 줄 요약

8강은 DTO를 도입하는 과정에서
Jackson 직렬화 규칙과 DTO 설계의 중요성을 단계적으로 체험하는 강의였다.

0개의 댓글