DTO ↔ Entity 변환에 대한 고민과 MapStruct

2025. 3. 16. 18:54Back-End/Spring-Boot

반응형

DTO <-> Entity 변환에 대한 고민과 MapStruct

스프링 부트를 사용하면서 DTO와 Entity의 책임을 분리하다 보면, 변환 과정에서 코드가 점점 복잡해진다.

처음에는 get/set을 직접 사용해서 변환했지만, 필드가 많아질수록 코드가 길어지고 유지보수가 어려워졌다.

이 고민 끝에 MapStruct를 도입해 보았다.


MapStruct는 DTO ↔ Entity 변환을 자동화해주면서도 성능이 뛰어나고 유지보수가 쉬운 라이브러리다.
이 글에서는 DTO와 Entity 변환에 대한 고민을 정리하고, MapStruct를 어떻게 활용할 수 있는지 살펴보려고 한다.

 

1. DTO ↔ Entity 변환이 복잡해지는 이유

JPA를 사용하면서 DTO와 Entity를 분리하는 이유는 명확하다.

  • Entity는 데이터베이스와 직접 연관되는 객체
  • DTO는 클라이언트와 데이터를 주고받기 위한 객체

이렇게 책임을 분리하면 유지보수성이 좋아지지만, 두 객체 간 변환 과정이 필수적으로 필요해진다.
처음에는 간단한 get/set으로 변환을 처리했다.

public User toEntity(UserRequestDto dto) {
    User user = new User();
    user.setName(dto.getName());
    user.setEmail(dto.getEmail());
    user.setAge(dto.getAge());
    return user;
}
public UserResponseDto toDto(User user) {
    UserResponseDto dto = new UserResponseDto();
    dto.setName(user.getName());
    dto.setEmail(user.getEmail());
    dto.setAge(user.getAge());
    return dto;
}

처음에는 이 방법이 나쁘지 않았다.
하지만 필드가 많아질수록 변환 코드가 점점 길어졌고, DTO와 Entity가 변경될 때마다 변환 코드도 직접 수정해야 하는 문제가 생겼다.

빌더 패턴을 활용하면 조금 깔끔해지긴 했지만, 여전히 수동 변환이 필요했다.

public User toEntity(UserRequestDto dto) {
    return User.builder()
            .name(dto.getName())
            .email(dto.getEmail())
            .age(dto.getAge())
            .build();
}

결국 DTO와 Entity 변환을 자동화할 방법이 필요했다.

 

2. MapStruct 도입

MapStruct는 컴파일 타임에 변환 코드를 자동 생성하는 라이브러리다.
get/set을 일일이 작성할 필요 없이, 인터페이스만 정의하면 자동으로 변환 코드를 만들어준다.

1) MapStruct 의존성 추가 (pom.xml)

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.5.Final</version>
    <scope>provided</scope>
</dependency>

<!-- Lombok 사용 시 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.26</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok-mapstruct-binding</artifactId>
    <version>0.2.0</version>
    <scope>provided</scope>
</dependency>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <source>17</source>
        <target>17</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.5.5.Final</version>
            </path>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok-mapstruct-binding</artifactId>
                <version>0.2.0</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

위 설정을 추가하면, 컴파일할 때 자동으로 변환 코드가 생성된다.

3. MapStruct 사용법

1) Mapper 인터페이스 작성

@Mapper(componentModel = "spring")
public interface UserMapper {
    // UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

    // DTO -> Entity 변환
    User toEntity(UserRequestDto dto);

    // Entity -> DTO 변환
    UserResponseDto toDto(User user);
}

이렇게 인터페이스만 작성하면 MapStruct가 변환 코드를 자동으로 생성해준다.
필드명이 동일하면 get/set을 사용하지 않아도 자동으로 매핑된다.

 

2) DTO와 Entity 예시

@Getter @Setter
public class UserRequestDto {
    private String name;
    private String email;
    private int age;
}
@Entity
@Getter @Setter
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private int age;
}
@Getter @Setter
public class UserResponseDto {
    private String name;
    private String email;
    private int age;
}

MapStruct는 위와 같이 필드명이 같은 경우 자동으로 변환해준다.

 

4. MapStruct 내부 변환 코드 확인

MapStruct는 컴파일할 때 변환 코드를 자동 생성한다.
빌드 후 target/generated-sources/annotations 폴더를 보면, 변환 코드가 자동 생성된 걸 확인할 수 있다.

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-03-16T12:34:56"
)
public class UserMapperImpl implements UserMapper {

    @Override
    public User toEntity(UserRequestDto dto) {
        if (dto == null) {
            return null;
        }
        User user = new User();
        user.setName(dto.getName());
        user.setEmail(dto.getEmail());
        user.setAge(dto.getAge());
        return user;
    }

    @Override
    public UserResponseDto toDto(User user) {
        if (user == null) {
            return null;
        }
        UserResponseDto dto = new UserResponseDto();
        dto.setName(user.getName());
        dto.setEmail(user.getEmail());
        dto.setAge(user.getAge());
        return dto;
    }
}

결국 MapStruct가 내부적으로 get/set을 자동 생성해주는 방식이라는 걸 알 수 있다.
덕분에 개발자가 직접 변환 코드를 작성할 필요 없이, 자동으로 DTO ↔ Entity 변환이 가능해진다.

 

5. DTO에는 없고, Entity에만 있는 필드는?

DTO에는 없지만, Entity에는 있는 필드는 기본적으로 변환되지 않는다.
이럴 때는 @Mapping을 사용해서 수동 매핑할 수 있다.

1) 기본값 설정

@Mapper(componentModel = "spring")
public interface UserMapper {
    @Mapping(target = "createdAt", expression = "java(java.time.LocalDateTime.now())")
    User toEntity(UserRequestDto dto);
}

createdAt 필드는 DTO에는 없지만, 변환될 때 자동으로 현재 시간을 넣어줄 수 있다.

2) 특정 필드 무시

@Mapper(componentModel = "spring")
public interface UserMapper {
    @Mapping(target = "id", ignore = true)
    User toEntity(UserRequestDto dto);
}

id는 DB에서 자동 생성되므로, 변환 시 무시하도록 설정할 수 있다.

 

6. 결론

DTO와 Entity 변환을 하다 보면 코드가 점점 복잡해지고 유지보수가 어려워진다.
get/set을 직접 사용하는 방식은 한두 개의 필드일 때는 괜찮지만, 필드가 많아질수록 비효율적이다.

MapStruct를 사용하면 변환 로직을 자동으로 생성하면서도, 성능이 뛰어나고 유지보수가 쉬워진다.
덕분에 DTO ↔ Entity 변환이 간편해지고, 코드도 깔끔해진다.

만약 DTO와 Entity 변환이 복잡해진다면, MapStruct를 도입해보는 것을 추천한다.
궁극적으로는 코드의 가독성과 유지보수를 개선하는 것이 목표이기 때문이다.

반응형