2026 Dev Log
[Springboot] entity <-> DTO 변환 방법
by Dev.후크리
2026. 4. 30.
변환 방식 요약
| 방식 |
변환 책임 |
핵심 코드 |
추천 |
| 1. Dto 내부 메서드 |
Dto |
dto.toEntity() / ResponseDto.from(entity) |
소~중형 ✅ |
| 2. Entity 내부 메서드 |
Entity |
entity.toDto() / Item.from(dto) |
거의 안 씀 ❌ |
| 3. Service 직접 변환 |
Service |
빌더/생성자 직접 호출 |
소형 단순용 |
| 4. Mapper 클래스 (수동) |
Mapper |
mapper.toEntity(dto) |
중~대형 ✅ |
| 5. MapStruct (자동생성) |
컴파일 자동 |
@Mapper 인터페이스만 선언 |
대형 강추 ✅ |
| 6. ModelMapper (리플렉션) |
런타임 자동 |
modelMapper.map(dto, Item.class) |
느려서 비추 |
package com.example.demo.dto;
// ================================================================
// Entity ↔ DTO 변환 방식 총정리
// ================================================================
//
// [공통 Entity 가정]
//
// @Entity @Getter @NoArgsConstructor
// public class Item {
// @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
// private Long id;
// private String name;
// private Integer price;
// private String description;
//
// @Builder
// public Item(String name, Integer price, String description) { ... }
//
// public void update(String name, Integer price, String description) { ... }
// }
//
// ================================================================
// ================================================================
// 방식 1. Dto 안에 변환 메서드를 두는 방식
// ================================================================
//
// 가장 흔한 방식. Dto가 변환 책임을 직접 가짐.
// Service에서는 dto.toEntity() / ResponseDto.from(entity) 한 줄로 해결.
// ================================================================
// ── 1-A. RequestDto (JSON 입력 → Entity 변환) ──────────────────
@Getter
@NoArgsConstructor
public class ItemRequestDto {
private String name;
private Integer price;
private String description;
// [Entity → DTO 방향 없음] RequestDto는 입력 전용이므로 toEntity()만 존재
/**
* toEntity() : Dto → Entity 변환
* Service에서: Item item = requestDto.toEntity();
*/
public Item toEntity() {
return Item.builder()
.name(this.name)
.price(this.price)
.description(this.description)
.build();
}
}
// ── 1-B. ResponseDto (Entity → Dto 변환) ──────────────────────
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ItemResponseDto {
private Long id;
private String name;
private Integer price;
// description은 응답에서 제외하고 싶어서 필드 자체를 선언하지 않음
/**
* 정적 팩토리 메서드 from() : Entity → Dto 변환
* Service에서: return ItemResponseDto.from(item);
*
* 왜 생성자 대신 정적 팩토리 메서드?
* - 메서드 이름(from)으로 의도가 명확해짐
* - 내부 구현을 숨길 수 있어 유지보수에 유리
*/
public static ItemResponseDto from(Item item) {
return new ItemResponseDto(
item.getId(),
item.getName(),
item.getPrice()
);
}
}
// ================================================================
// 방식 2. Entity 안에 변환 메서드를 두는 방식
// ================================================================
//
// Entity가 변환 책임을 가짐.
// 도메인 중심 설계(DDD)에서 사용하기도 하지만,
// Entity가 Dto를 알아야 하므로 의존 방향이 역전됨 → 잘 안 씀.
// ================================================================
@Entity @Getter @NoArgsConstructor
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer price;
private String description;
/**
* Entity → Dto 변환 메서드를 Entity 안에 두는 방식
* Service에서: return item.toResponseDto();
*
* 단점: Entity가 Dto 클래스에 의존하게 됨 (레이어 의존성 역전)
* Dto가 변경되면 Entity도 수정해야 함 → 잘 사용하지 않음
*/
public ItemResponseDto toResponseDto() {
return new ItemResponseDto(this.id, this.name, this.price);
}
/**
* Dto → Entity 변환 메서드를 Entity 안에 두는 방식 (정적 메서드)
* Service에서: Item item = Item.from(requestDto);
*/
public static Item from(ItemRequestDto dto) {
return Item.builder()
.name(dto.getName())
.price(dto.getPrice())
.description(dto.getDescription())
.build();
}
}
// ================================================================
// 방식 3. Service에서 직접 변환 (변환 전담 코드 없음)
// ================================================================
//
// 별도 메서드 없이 Service 로직 안에서 바로 빌더/생성자로 변환.
// 작은 프로젝트에서 단순하게 쓸 때 적합.
// 변환 코드가 Service 여기저기 흩어지면 중복이 생기는 단점.
// ================================================================
@Service
@RequiredArgsConstructor
public class ItemServiceDirectConvert {
private final ItemRepository itemRepository;
public ItemResponseDto createItem(ItemRequestDto requestDto) {
// [Dto → Entity] 서비스에서 빌더로 직접 변환
Item item = Item.builder()
.name(requestDto.getName())
.price(requestDto.getPrice())
.description(requestDto.getDescription())
.build();
Item savedItem = itemRepository.save(item);
// [Entity → Dto] 서비스에서 생성자로 직접 변환
return new ItemResponseDto(
savedItem.getId(),
savedItem.getName(),
savedItem.getPrice()
);
}
}
// ================================================================
// 방식 4. Mapper 클래스를 별도로 만드는 방식
// ================================================================
//
// 변환 로직만 담당하는 전용 클래스를 분리.
// 변환 코드가 한 곳에 모이므로 재사용성, 유지보수성이 높음.
// 중대형 프로젝트에서 권장.
// ================================================================
@Component // 스프링 빈으로 등록해서 Service에서 주입받아 사용
public class ItemMapper {
/**
* RequestDto → Entity
*/
public Item toEntity(ItemRequestDto dto) {
return Item.builder()
.name(dto.getName())
.price(dto.getPrice())
.description(dto.getDescription())
.build();
}
/**
* Entity → ResponseDto
*/
public ItemResponseDto toResponseDto(Item item) {
return new ItemResponseDto(
item.getId(),
item.getName(),
item.getPrice()
);
}
/**
* List<Entity> → List<ResponseDto>
*/
public List<ItemResponseDto> toResponseDtoList(List<Item> items) {
return items.stream()
.map(this::toResponseDto)
.collect(Collectors.toList());
}
}
// Mapper를 사용하는 Service
@Service
@RequiredArgsConstructor
public class ItemServiceWithMapper {
private final ItemRepository itemRepository;
private final ItemMapper itemMapper; // Mapper 주입
public ItemResponseDto createItem(ItemRequestDto requestDto) {
Item item = itemMapper.toEntity(requestDto); // Dto → Entity
Item savedItem = itemRepository.save(item);
return itemMapper.toResponseDto(savedItem); // Entity → Dto
}
public List<ItemResponseDto> getAllItems() {
List<Item> items = itemRepository.findAll();
return itemMapper.toResponseDtoList(items); // List 변환
}
}
// ================================================================
// 방식 5. MapStruct 라이브러리 사용 (자동 생성)
// ================================================================
//
// build.gradle 의존성 추가 필요:
// implementation 'org.mapstruct:mapstruct:1.5.5.Final'
// annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
//
// @Mapper 인터페이스만 선언하면 컴파일 타임에 구현체를 자동 생성.
// 필드명이 같으면 자동 매핑, 다르면 @Mapping으로 지정.
// 대형 프로젝트에서 변환 코드 반복을 없애는 데 많이 사용.
// ================================================================
@Mapper(componentModel = "spring") // 스프링 빈으로 자동 등록
public interface ItemMapStructMapper {
/**
* RequestDto → Entity
* id 필드는 DB가 생성하므로 매핑에서 제외
*/
@Mapping(target = "id", ignore = true)
Item toEntity(ItemRequestDto dto);
/**
* Entity → ResponseDto
* 필드명이 같으면 자동 매핑
* 이름이 다를 경우: @Mapping(source = "itemName", target = "name")
*/
ItemResponseDto toResponseDto(Item item);
/**
* List<Entity> → List<ResponseDto> (위 toResponseDto를 자동으로 반복 적용)
*/
List<ItemResponseDto> toResponseDtoList(List<Item> items);
}
// MapStruct Mapper를 사용하는 Service
@Service
@RequiredArgsConstructor
public class ItemServiceWithMapStruct {
private final ItemRepository itemRepository;
private final ItemMapStructMapper mapper; // 컴파일 타임에 자동 생성된 구현체가 주입됨
public ItemResponseDto createItem(ItemRequestDto requestDto) {
Item item = mapper.toEntity(requestDto);
Item savedItem = itemRepository.save(item);
return mapper.toResponseDto(savedItem);
}
public List<ItemResponseDto> getAllItems() {
return mapper.toResponseDtoList(itemRepository.findAll());
}
}
// ================================================================
// 방식 6. ModelMapper 라이브러리 사용 (리플렉션 기반)
// ================================================================
//
// build.gradle 의존성:
// implementation 'org.modelmapper:modelmapper:3.1.1'
//
// 리플렉션으로 필드명을 자동으로 매핑.
// 설정이 간단하지만 MapStruct보다 느리고,
// 컴파일 타임 오류 감지가 안 되는 단점.
// ================================================================
@Configuration
public class ModelMapperConfig {
@Bean
public ModelMapper modelMapper() {
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); // 엄격 매핑
return mapper;
}
}
@Service
@RequiredArgsConstructor
public class ItemServiceWithModelMapper {
private final ItemRepository itemRepository;
private final ModelMapper modelMapper; // 빈 주입
public ItemResponseDto createItem(ItemRequestDto requestDto) {
// [Dto → Entity] 리플렉션으로 필드명 기준 자동 매핑
Item item = modelMapper.map(requestDto, Item.class);
Item savedItem = itemRepository.save(item);
// [Entity → Dto] 마찬가지로 자동 매핑
return modelMapper.map(savedItem, ItemResponseDto.class);
}
}
// ================================================================
// 방식 비교 요약
// ================================================================
//
// 방식 변환 책임 특징 추천 규모
// ─────────────────────────────────────────────────────────────────────
// 1. Dto 내부 메서드 Dto 가장 흔한 패턴. 심플. 소~중형
// toEntity() Service 한 줄 처리
// from(entity)
//
// 2. Entity 내부 Entity Entity가 Dto에 의존하게 거의 안 씀
// 메서드 됨 → 레이어 역전 위험
//
// 3. Service 직접 Service 코드 중복 발생 가능 소형/단순
// 변환 변환 흩어짐
//
// 4. Mapper 클래스 Mapper 변환 로직 한 곳에 집중 중~대형
// (수동) 재사용성 높음
//
// 5. MapStruct 자동생성 컴파일 타임 검증, 빠름 중~대형 권장
// (라이브러리) 코드 자동 생성
//
// 6. ModelMapper 자동생성 설정 간단, 런타임 매핑 소형
// (라이브러리) 리플렉션 → 느림, 오류 감지 약함
//
// ================================================================