티스토리 뷰

Spring 기반 애플리케이션에서 DTO(Data Transfer Object)와 Entity 간 변환은 대부분의 API 요청과 응답에서 필수적으로 수행되는 작업이다. 이 과정에서 비효율적으로 구현되면 코드 중복, 유지보수의 어려움, 때로는 성능 저하까지 초래할 수 있다.
이 글에서 다양한 DTO-Entity 변환 전략과 예시 및 장단점을 비교해보려고한다.
MapStruct와 ModelMapper 학습 소스코드는 깃허브에서 확인할 수 있다.
프로젝트 구성
프레임워크 : Spring boot 3.3.9
빌드 도구 : grade
Java 버전 : 17
주요 의존성 :
- Spring Data JPA
- Spring Web
- Lombok
- MapStruct
- ModelMapper
- Postgresql 14 (Docker)
DTO-Entity 변환이 필요한 이유
- 관심사 분리 : Entity 는 데이터베이스 스키마와 밀접하게 연결되어 있고, DTO는 클라이언트와의 인터페이스를 담당한다. 두 계층을 분리함으로써 각 계층의 변경이 다른 계층에 미치는 영향을 최소화 할 수 있다.
- 데이터 은닉 : 모든 엔티티 필드가 API를 통해 외부로 노출되는 것은 보안상 큰 문제이다. DTO를 사용하면 필요한 데이터만 선택적으로 노출할 수 있다.
- 유연성 : API 버전 관리나 요구사항 변경 시 데이터베이스 스키마와 독립적으로 API 응답 구조를 조정할 수 있어 유연성이 높아진다.
- 성능 최적화 : 필요한 데이터만 주고받음으로써 네트워크 트래픽을 줄이고, 특정 상황에서는 불필요한 데이터베이스 조회를 방지할 수 있다.
다양한 변환 방법
1. 수동 변환(직접 매핑)
가장 기본적인 방법으로, 직접 피드를 매핑하는 코드를 작성한다.
// 서비스 레이어에서 변환
public UserDto userToDto(User user){
if (user == null){
return null;
}
UserDto dto = new UserDto();
dto.setId(user.getId());
dto.setName(user.getName());
dto.setEmail(user.getEmail());
// 기타 필요한 필드 매핑
return dto;
}
장점 | 단점 |
- 명시적이고 이해하기 쉬움 - 외부 의존성이 없음 - 필요한 필드만 정확히 매핑할 수 있음. - 복잡한 변환 로직을 유연하게 구현할 수 있음 |
- 코드 중복 발생 - 필드 추가/변경 시 여러 곳을 수정해야함 - 코드량이 많아짐 - 오타나 실수로 인한 오류 가능싱이 높음 |
이 방식은 DTO와 Entity 간의 구조가 간단하고, 완전한 커스텀 매핑이 필요한 특수항 상황에서 유용하다.
2. 정적 변환 메서드
public class UserDto{
// 필드 및 기본 메서드 ...
// Entity -> DTO 변환
public static UserDto from(User user) {
if(user == null){
return null;
}
return UserDto.builder()
.id(user.getId())
.name(user.getName())
.email(user.getEmail())
.build();
}
// DTO -> Entity 변환
public User toEntity(){
return User.builder()
.name(this.name)
.email(this.email)
.build();
}
}
장점 | 단점 |
- 변환 로직이 관현 클래스와 함께 위치하여 응집도가 높음 - 재사용 가능하고 일관된 변환을 보장 - 빌더 패턴과 결합하여 가독성을 높일 수 있음 - 외부 의존성 없이 구현 가능 |
- 여전히 필드 추가/변경 시 수동 업데이트가 필요 - DTO와 Entity 간 강한 결합이 발생할 수 있음 - 복잡한 변호나 로직에는 제한적 - 양방향 매핑 시 순환 참조 문제가 발생 |
이 방식은 도메인 주도 설계(DDD) 접근 방식과 잘 어울린다.
코드의 응집도를 높이고 관련 로직을 한 곳에 모을 수 있다는 장점이 있다.
3. MapStruct 활용
MapStruct는 컴파일 시점에 변환 코드를 자동 생성해주는 라이브러리다.
@Mapper(componentModel = "spring")
public interface UserMapper{
UserDto toDto(User user);
User toEntity(UserDto dto);
// 특정 필드만 업데이트
@Mapping(target ="id", ignore = true)
void updateUserFromDto(UserDto dto, @MappingTarget User user);
}
장점 | 단점 |
- 컴파일 타임에 코드 생성으로 런타임 성능이 우수 - 타입 안정성 보장 - 필드 이름 불일치 처리가 용이 - 중첩 객체 자동 매핑을 지원 - IDE 지원과 에러 검출이 용이 - 반복 작업을 최소화 |
- 추가 의존성 필요 - 복잡한 매핑에 대한 러닝 커브 존재 - Lombok과 함께 사용시 설정이 복잡 - 커스텀 로직 구현은 제한적 |
MapStruct 는 타입 안정성과 성능을 중시한다면 적용해볼 법한 라이브러리이다.
컴파일 시점에 코드를 생성하므로 리플렉션 기반 라이브러리 보다 성능이 뛰어나다.
4. ModelMapper
리플렉션 기반의 라이브러리로, 런타임에 객체 간 매핑을 수행한다.
// ModelMapper 사용
ModelMapper modelMapper = new ModelMapper();
UserDto userDto = modelMapper.map(user, UserDto.Class);
장점 | 단점 |
- 사용이 매우 간단 - 코드량이 크게 감소 - 설정을 통한 유연한 매핑 가능 - 별도의 매퍼 클래스 관리가 필요하지 않음 |
- 리플렉션 사용으로 성능이 저하 - 타입 안정성이 부족 - 디버깅이 어려움 - 복잡한 매핑에 제한적 - 런타임에 에러발생 가능성 높음 |
ModelMapper는 빠른 개발과 간결한 코드를 우선시하는 경우에 적합하다.
또한 레거시 코드를 빠르게 리팩토링할 때 유용하게 사용할 수 있다.
복합 객체 매핑의 효율적 처리
실제 애플리케이션에서는 단순한 객체뿐만 아니라 중첩된 객체나 컬렉션을 포함하는 복합객체를 다루는 경우가 많다. 이러한 복잡한 상황에서는 각 변환 방식마다 장단점이 더욱 두드러진다.
중첩 객체 처리
// MapStructfh 중첩 객체 처리
@Mapper(componentModel = "spring")
public interface OrderMapper{
@Mapping(source = "custom.name", target = "customerName")
@Mapping(source = "items", target = "orderItems")
OrderDto toDto(Order order);
// 중첩 매퍼 사용
List<OrderItemDto) mapOrderItems(List<OrderItem> items);
}
MapStruct는 중첩 객체와 컬렉션 매핑을 자동으로 처리해주어 복잡한 객체 구조에서도 깔끔한 코드를 유지할 수 있다.
반면, 수동 매핑이나 ModelMapper를 사용할 경우 중첩 구조가 복잡해질수록 코드가 복잡해지거나 성능 문제가 발생할 수 있다.
더 나은 DTO-Entity 변환을 위한 패턴과 전략
1. 단방향 매핑 활용
많은 경우, 양방향 매핑이 아닌 단방향 매핑만으로 충분하다. 특히 읽기 전용 API 나 명령과 쿼리를 분리하는 CQRS 패턴에서는 단방향 매핑이 더 적합할 수 있다.
// 읽기 전용 DTO
public record UserSummaryDto(Long id, String name){
public static UserSummaryDto from(User user){
return new UserSummaryDto(user.getId(), user.getName());
}
}
2. 역할 기반 DTO 설계
하나의 Entity에 대해 여러 용도의 DTO를 만들어 상황에 맞게 사용하면 각 API 의 요구사항에 더 적합한 데이터 구조를 제공할 수 있다.
// 목록 조회용 (최소 정보)
public record UserListDto(Long id, String name){}
// 상세 조회용 (모든 정보)
public record UserDetailDto(Long id, String name, String email, Address address, List<OrderDto> orders){}
// 생성용 (필수 정보)
public record CreateUserDto(String name, String email, String password){}
3. 비지니스 로직과 매핑 분리
매핑 로직과 비지니스 로직을 명확히 분리하면 코드의 책임이 명확해지고 테스트가 용이해진다.
@Service
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper; // 매핑 담당
private final PasswordEncoder passwordEncoder; // 비지니스 로직 관련
public UserDto createUser(CreateUserDto dto){
// 비지니스 로직
String encodedPassword = passwordEncoded.encode(get.getPassword());
// 매핑
User user = userMapper.toEntity(dto);
user.setPassword(encodedPassword);
// 저장 및 결과 반환
User savedUser = userRepository.save(user);
return userMapper.toDto(savedUser);
}
}
4. 검증 로직 통합
DTO에 검증 로직을 포함시켜 데이터 무결성을 보장하고, 검증 책임을 명확히 할 수 있다.
public class UserRequestDto {
@NotBlank(message = "이름은 필수 입니다")
private String name;
@Email(message = "유효한 이메일 형식이어야 합니다")
private String email;
// 커스텀 검증 로직
public void validateBusinessRules(){
if (name != null && name.length() > 50){
throw new IllegalArgumentException("이름은 50자를 초과할 수 없습니다");
}
}
}
프로젝트 규모별 추천 전략
추천 방식 | 이유 | 주의사항 |
정적 변환 메서드 또는 ModelMapper | - 빠른 개발 속도와 간결한 코드 - 외부 의존성 최소화 - 설정 복잡성 감소 |
- 프로젝트 성장 시 리팩토링 계획 수립 필요 - 중요한 성능 병목 구간에 대한 모니터링 |
MapStruct 또는 정적 변환 메서드 + MapStruct 혼합 | - 타입 안정성 확보 - 컴파일 타입 검증으로 오류 조기 발견 - 성능과 유지보수성 간 Balance |
- 일관된 매핑 전략 문서화 - 복합 객체 매핑에 대한 패턴 확립 |
MapStruct + 커스텀 매핑 전략 | - 엄격한 타입 안정성 - 확장성과 유지보수성 - 고성능 요구사항 충족 |
- 명확한 매핑 가이드라인 수립 - DTO 버전 관리 전략 수립 - 팀 교육 및 코드 리뷰 강화 |
결론
DTO와 Entity 간의 변환은 단순해 보이지만, 애플리케이션의 복잡도와 규모가 커질수록 신경써야할 부분이 많아진다.
개인적인 의견은 MapStruct 와 정적 변환 메서드를 활용하는게 더 나은 방법인것 같다. 이유는 2가지다
- 각 엔티티별로 Mapper 인터페이스에서 변환 관련 로직을 관리
- 컴파일 시점에 에러를 통한 런타임 에러 예방
특수 케이스에 대해서는 정적 변환 메서드를 통해 처리하고, 공통적으로 사용되는 로직에서는 Mapstruct를 사용하는게 최선의 방법이지 않을까 싶다.

이상 전달 끝!
'프레임워크 > Spring & Spring boot' 카테고리의 다른 글
동시성 해결하다가 Advisory Lock 을 알게된 건에 대하여(Feat. PostgreSql) (0) | 2025.02.16 |
---|---|
2025년 2월 Spring Rest Docs 입문기 (0) | 2025.02.02 |
2024년12월 Swagger 입문기 (0) | 2024.12.22 |
기업정보 조회 API 개발 기록(2024.09.02 ~ 2024.09.30) (0) | 2024.10.06 |
[JPA] 학습테스트로 알아보는 영속과 준영속 (feat. EntityManager) (0) | 2024.07.02 |
- Total
- Today
- Yesterday
- 유데미
- springboot
- Comparator
- BufferedWriter
- 항해99
- dxdy
- 챗봇
- NLU
- 객체정렬
- 글또
- 재기동
- 코드트리
- 개발자취준
- 취업리부트코스
- Comparable
- 전자정부프레임워크
- script
- Spring
- Java
- thymeleaf
- 나만의챗봇
- BufferedReader
- 백준
- 취리코
- JWT
- 코딩테스트
- RASA
- BFS
- spring boot
- 자바
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |