DTO (Data Transfer Object)와 Entity 간 변환

o_z·2024년 2월 3일

Spring이랑 백엔드 공부를 하고 팀 프로젝트에서 개발을 시작했다. 내 역할을 시작하기 전에 이것저것 사람들이 개발한 프로젝트의 구성을 몇 개 참고했는데 'DTO'라는게 자꾸 등장했다. DTO 전용 패키지를 빼놓던데 자주 보이던 만큼 알아야할 것 같아서 공부했다.


DTO (Data Transfer Object)

DTO는 계층 간 데이터 전달만을 위해 존재하는 객체로, 유저와 서비스 간 분리 가능하게 만드는 역할이다.

Entity와 비슷한 구조를 가졌음에도 DTO를 굳이 분리해두는 이유는 Entity는 DB와 맞닿아 있는 핵심 객체로, 예민하게 관리되어야 하기 때문이다. 또한, Controller와 Service 계층 간 강한 의존을 피하게 하기 위해서도 사용된다.

내 프로젝트를 예시로 들자면, 유저로부터 일정 생성을 위한 request 전용 DTO를 백엔드로 받아오면 해당 DTO를 일정 Entity로 변환해서 DB에 저장하게 된다. 이 때 저 중간자 역할이 DTO다.

거의 모든 API에 대해 DTO를 구성하는 것이 서비스와 유저 간 분리를 위해서는 합리적인 구조이다. 그래서 일단 나는 일정의 생성/수정/조회에서 필요한 request DTO 또는 response DTO를 최대한 빼내려고 구성했다.
모든 DTO는 java의 record를 사용해서 간단하게 구축해놨다. (record는 다른 게시글에)


DTO와 Entity 간 변환

DTO를 막상 만들어놓으니 Entity와 DTO를 어떻게 변환하여 사용 할 지도 고민이다. 일단 알아 본 대표적인 방법은 아래와 같다.

  1. DTO 내부 메서드로 값 전달하기
  2. ModelMapper 라이브러리 사용하기

내 프로젝트에서는 Schedule가 DB와 맞닿아 있는 Entity고, ScheduleRequestDTO가 일정 생성을 위한 request DTO로서 존재한다.


1. DTO 내부 메서드로 값 전달하기

DTO 클래스 내에 DTO를 Entity로 변환하는 메서드를 아래와 같이 추가하는 방법이 있다.

// DTO -> Entity 변환
public Schedule toEntity(){
		return Schedule.builder()
			.title(title)
			.locate(locate)
			.startDate(startDate)
			.endDate(endDate)
			.memo(memo)
			.build();
}

// Entity -> DTO 변환
public ScheduleRequestDTO toDTO(Schedule schedule){
		return ScheduleRequestDTO.builder()
			.title(schedule.getTitle())
			.locate(schedule.getLocate())
			.startDate(schedule.getStartDate())
			.endDate(schedule.getEndDate())
			.memo(schedule.getMemo())
			.build();
	}

좀 복잡해보이긴 하지만 그냥 builder 사용해서 새로운 객체로서 만들어주는 메서드이다.


2. ModelMapper 라이브러리 사용하기

이 라이브러리를 사용하려면 DTO와 Entity 간 필드가 동일해야 한다는 조건이 있다 (아니면 에러 발생함). build.gradle에 라이브러리를 import 해주고 config를 별도로 생성해줘야 한다.

// Entity -> DTO 변환
Schedule schedule = modelMapper.map(scheduleRequestDTO, Schedule.class);

// DTO -> Entity 변환
ScheduleRequestDTO scheduleDTO = modelMapper.map(schedule, ScheduleDTO.class);

간편하긴 정말 간편해보인다!

ModelMapper는 장단점이 아래와 같다.

  • 장점
  1. 코드가 간결
  2. 일반적으로 필드 변경 사항에 대한 고려가 필요하지 않음
  3. Lombok 라이브러리와 충돌 없이 같이 사용 가능
  • 단점
  1. 컴파일러에 의해 최적화 되지 않음
  2. 최초 작업 시 캐싱 되지 않음
  3. 다른 방식들보다 오버헤드가 많음 -> 성능이 좋지 않음
  4. 바이트 코드 생성 방식을 사용하므로 문제 원인 발견과 디버깅이 어려움
    (특정 필드의 변경으로 인한 매핑 누락 문제 발생 시, 발견 어려움)
  5. Setter를 열어놔야 함

내가 하는 프로젝트에서의 DTO는 일단 1번 방법으로 구축했다.

public record CreateScheduleRequestDTO(@NotNull(message = "일정 제목은 필수 입력 값입니다.") String title,
									   @NotNull(message = "일정 국가는 필수 입력 값입니다.") Country country,
									   @NotNull(message = "일정 지역은 필수 입력 값입니다.") City city,
									   @NotNull(message = "시작 날짜는 필수 입력 값입니다.") LocalDate startDate,
									   @NotNull(message = "끝 날짜는 필수 입력 값입니다.") LocalDate endDate,
									   String memo
						  ){

	public Schedule toEntity(User user, LocateRepository locateRepository){
		Locate locate = locateRepository.findByCountryAndCity(country, city);

		return Schedule.builder()
			.user(user)
			.title(title)
			.locate(locate)
			.startDate(startDate)
			.endDate(endDate)
			.readOnlyFlag(false)
			.memo(memo)
			.build();
	}
}

ModelMapper는 장단점을 더 찾아보고 나중에 리팩토링 해봐야겠다.

profile
트러블슈팅과 구현기를 위주로 기록합니다-

0개의 댓글