본문 바로가기
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      자동생성    설정 간단, 런타임 매핑       소형
//     (라이브러리)                  리플렉션 → 느림, 오류 감지 약함
//
// ================================================================