Spring Boot에서 엔티티를 DTO로 변환하는 다양한 방법

차윤하·2025년 5월 13일
0

Spring Boot로 API를 개발하다 보면, 프론트엔드로 데이터를 보낼 때 보통 엔티티(Entity)를 직접 반환하지 않고, 별도의 DTO(Data Transfer Object)를 만들어서 전달한다. 이번 글에서는 엔티티를 DTO로 변환하는 이유와 다양한 방법, 각각의 방식이 어떤 장단점 및 특징을 가지고 있는지 정리한다.


왜 굳이 DTO를 만들어서 보내야 할까?

처음에는 "그냥 엔티티 그대로 반환하면 되지 않을까?"라고 생각할 수 있다. 하지만 실제로는 다음과 같은 이유 때문에 DTO를 만들어 사용하는 것이 좋다.

1. 보안 문제 방지

엔티티에는 사용자 비밀번호, 내부 데이터 등 민감한 정보가 들어 있을 수 있는데, 이 때 엔티티를 그대로 응답으로 내보내면 정보들이 함께 노출될 수 있다.

2. 프론트엔드에 맞는 데이터 형태 제공

  • 프론트에서는 꼭 모든 필드가 필요하지 않다. DTO를 통해 필요한 필드만 추려서 보낼 수 있다.
  • 응답 형식을 프론트와 협의한 대로 정확히 맞춰줄 수 있다.

3. 유지보수 유리

엔티티 구조가 변경되어도 DTO를 통해 응답 구조를 따로 관리할 수 있다.


엔티티를 DTO로 바꾸는 3가지 방법

List 형태의 엔티티를 DTO로 변환하는 과정이다.

1. 생성자 사용

List<AttachmentDTO> attachmentDTOs = attachments.stream()
    .map(a -> new AttachmentDTO(
        a.getName(),
        a.getChangedName(),
        a.getSize(),
        a.getExtension()
    )).toList();

장점

  • 간단하고 빠르게 작성할 수 있다.

단점

  • 파라미터 순서를 지켜야 하고, 필드가 많아질수록 가독성이 떨어지고 실수할 수 있다.
  • 어떤 값이 어떤 필드에 들어가는지 직관적으로 보이지 않는다.

2. 빌더 패턴 사용

@Builder
public class AttachmentDTO {

    private Long attachmentId;
    private String name;
    private String changedName;
    private LocalDateTime createdAt;
    private Long size;
    private String extension;

먼저, 해당 DTO 클래스에 @Builder 어노테이션을 추가해야 한다.

List<AttachmentDTO> attachmentDTOs = attachments.stream()
    .map(a -> AttachmentDTO.builder()
        .name(a.getName())
        .changedName(a.getChangedName())
        .size(a.getSize())
        .extension(a.getExtension())
        .build()
    ).toList();

장점

  • 명확하게 어떤 값이 어떤 필드에 들어가는지 확인할 수 있어 가독성이 좋다.
  • 선택적으로 필드를 설정할 수 있어서 유연하다.

단점

  • 코드가 약간 길어진다.

3. 정적 팩토리 메소드 사용

public static AttachmentDTO convertDTO(Attachment attachment) {
    return new AttachmentDTO(
        attachment.getName(),
        attachment.getChangedName(),
        attachment.getSize(),
        attachment.getExtension()
    );
}

DTO 클래스 내부에 정적 메소드로 작성하여 사용한다. 엔티티에서 DTO로 변환하는 메소드라는 뜻으로 다음과 같이 convertDTO라는 이름으로 작성했다.

// 사용
List<AttachmentDTO> attachmentDTOs = attachments.stream()
    .map(AttachmentDTO::convertDTO)
    .toList();

장점

  • AttachmentDTO.convertDTO()처럼 객체를 생성하지 않고 클래스 이름으로 직접 호출할 수 있어서 직관적이다.

  • 변환 로직을 DTO 클래스 내부에 두어 관련 책임을 부여할 수 있다.

  • 명확한 이름을 붙여서 어떤 역할인지 표현할 수 있다.
    * 빌더를 사용하는 방식도 @Builder를 통해 정적 메소드를 주입한 것이다. 즉, AttachmentDTO.builder()도 정적 팩토리 메소드처럼 클래스 이름으로 객체를 생성하는 방식이다.

  • 로직이 바뀌더라도 메소드 내부만 수정하면 돼서 유지보수에 좋다.

단점

  • 변환 메소드를 작성해야 하므로 약간의 반복 작업이 필요하다.
  • 클래스명.메소드() 형태로 전역으로 호출할 수 있기 때문에, 남용하면 설계가 복잡해질 수 있다.

+ OneToMany 연관 관계에서의 사용

Post <-> List(Attachment) 연관 관계이고,
Post → PostDetailDTO 변환

public static PostDetailDTO convertDTO(Post post, List<AttachmentDTO> attachmentDTOs) {
    return new PostDetailDTO(
        post.getPostId(),
        post.getTitle(),
        post.getContent(),
        post.getCreatedAt(),
        attachmentDTOs
    );
}
  • 복합적인 DTO를 만들 때는 여러 값을 함께 받아서 변환하는 메소드를 만들어 사용한다.

정리

API 응답을 깔끔하고 안정적으로 만들기 위해서는 DTO 설계와 변환 방식에 신경 써야 한다. 단순히 동작만 되면 끝이 아니라, 나중에 유지보수나 확장할 때 얼마나 편할지를 생각하고 코드를 짜보자.

정적 팩토리 메소드는 처음에는 익숙하지 않을 수 있지만, 점점 큰 프로젝트일수록 의도를 명확히 전달할 수 있고 변환 로직 변경이 필요할 때도 해당 메서드만 수정하면 되어 코드의 가독성과 유지보수성을 높일 수 있다.

profile
풀스택 개발자가 되고 싶은 꿈나무

0개의 댓글