IT기술/MSA (with. springboot)

Spring Boot DTO 패턴 완벽 가이드: MSA 환경에서의 효율적인 데이터 전송 객체 활용

후스파 2025. 7. 7. 12:46
반응형

마이크로서비스 아키텍처(MSA)에서 REST API를 통해 클라이언트에 데이터를 전달할 때, 데이터 전송 객체(Data Transfer Object, DTO)를 사용하는 것이 일반적입니다. DTO는 서버에서 클라이언트로 전송되는 데이터를 구조화하고, 특정 형식으로 응답 메시지를 처리하는 데 도움을 줍니다.
이번 포스트에서는 DTO의 개념과 사용 방법, 그리고 응답 메시지 처리 과정에 대해 알아보겠습니다.


DTO와 VO의 차이

DTO (Data Transfer Object)

  • 정의: DTO는 데이터 전송 객체로, 클라이언트와 서버 간에 데이터를 전송하기 위해 사용되는 객체입니다. DTO는 일반적으로 API 응답의 구조를 정의하며, 필요한 데이터 필드만 포함합니다
  • 특징:
    • 주로 API 응답 메시지에 사용되며, 불필요한 데이터 노출을 방지합니다
    • 데이터 전송에 최적화되어 있어, 클라이언트가 필요한 데이터만 쉽게 파악할 수 있도록 돕습니다
    • 비즈니스 로직을 포함하지 않으며, 단순히 데이터 구조를 정의합니다

VO (Value Object)

  • 정의: VO는 값 객체로, 특정한 값의 의미를 가지며 불변성을 갖는 객체입니다. VO는 비즈니스 로직을 포함할 수 있습니다
  • 특징:
    • 비즈니스 도메인을 표현하는 데 사용되며, 상태를 표현합니다
    • 불변성을 유지하여, 데이터의 일관성을 보장합니다
    • 주로 내부 로직에서 사용되며, 클라이언트와의 데이터 전송에는 사용되지 않습니다

요약

  • DTO는 데이터 전송에 최적화된 객체로, API의 응답 메시지로 사용됩니다
  • VO는 비즈니스 로직을 포함할 수 있는 불변 객체로, 내부 데이터 모델을 표현합니다
목적데이터 전송값 표현
가변성가변불변
비즈니스 로직없음포함 가능
사용 위치API 계층도메인 계층
생명주기요청-응답 동안애플리케이션 전반

DTO 클래스 구현

아래는 호텔 정보를 전달하기 위한 DTO 클래스의 예제입니다.

HotelDTO 클래스

package com.example.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import java.time.LocalDateTime;

public class HotelDTO {
    @NotNull
    private Long id;

    @NotBlank
    private String name;

    @NotBlank
    private String location;

    private String description;

    @DecimalMin("0.0")
    @DecimalMax("5.0")
    private Double rating;

    @JsonProperty("room_count")
    private Integer roomCount;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createdAt;

    // 기본 생성자
    public HotelDTO() {}

    // 필수 필드 생성자
    public HotelDTO(Long id, String name, String location) {
        this.id = id;
        this.name = name;
        this.location = location;
    }

    // 전체 필드 생성자
    public HotelDTO(Long id, String name, String location, String description, 
                   Double rating, Integer roomCount, LocalDateTime createdAt) {
        this.id = id;
        this.name = name;
        this.location = location;
        this.description = description;
        this.rating = rating;
        this.roomCount = roomCount;
        this.createdAt = createdAt;
    }

    // Getter 및 Setter
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getLocation() { return location; }
    public void setLocation(String location) { this.location = location; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    public Double getRating() { return rating; }
    public void setRating(Double rating) { this.rating = rating; }

    public Integer getRoomCount() { return roomCount; }
    public void setRoomCount(Integer roomCount) { this.roomCount = roomCount; }

    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

요청용 DTO 클래스

package com.example.dto;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;

public class HotelCreateRequestDTO {

    @NotBlank(message = "호텔 이름은 필수입니다")
    private String name;

    @NotBlank(message = "호텔 위치는 필수입니다")
    private String location;

    private String description;

    @DecimalMin(value = "0.0", message = "평점은 0.0 이상이어야 합니다")
    @DecimalMax(value = "5.0", message = "평점은 5.0 이하여야 합니다")
    private Double rating;

    private Integer roomCount;

    // 생성자, getter, setter...
    public HotelCreateRequestDTO() {}

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getLocation() { return location; }
    public void setLocation(String location) { this.location = location; }

    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    public Double getRating() { return rating; }
    public void setRating(Double rating) { this.rating = rating; }

    public Integer getRoomCount() { return roomCount; }
    public void setRoomCount(Integer roomCount) { this.roomCount = roomCount; }
}

REST API 응답 메시지 처리

이제 HotelController 클래스를 수정하여, DTO를 사용하여 응답 메시지를 처리하는 방법을 살펴보겠습니다.

HotelController 수정

package com.example.controller;

import com.example.dto.HotelDTO;
import com.example.dto.HotelCreateRequestDTO;
import com.example.model.Hotel;
import com.example.service.HotelService;
import com.example.mapper.HotelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/v1/hotels")
@CrossOrigin(origins = "*")
public class HotelController {

    @Autowired
    private HotelService hotelService;

    @Autowired
    private HotelMapper hotelMapper;

    // 특정 호텔 정보 조회
    @GetMapping("/{id}")
    public ResponseEntity getHotelById(@PathVariable Long id) {
        Optional hotel = hotelService.getHotelById(id);

        return hotel.map(h -> {
            HotelDTO hotelDTO = hotelMapper.toDTO(h);
            return ResponseEntity.ok(hotelDTO);
        }).orElse(ResponseEntity.notFound().build());
    }

    // 모든 호텔 정보 조회 (DTO 사용)
    @GetMapping
    public ResponseEntity> getAllHotels(
            @RequestParam(required = false) String location,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {

        List hotels = hotelService.getAllHotels(location, page, size);
        List hotelDTOs = hotelMapper.toDTOList(hotels);

        return ResponseEntity.ok(hotelDTOs);
    }

    // 호텔 생성
    @PostMapping
    public ResponseEntity createHotel(
            @Valid @RequestBody HotelCreateRequestDTO requestDTO) {

        Hotel hotel = hotelMapper.toEntity(requestDTO);
        Hotel savedHotel = hotelService.addHotel(hotel);
        HotelDTO responseDTO = hotelMapper.toDTO(savedHotel);

        return ResponseEntity.status(HttpStatus.CREATED).body(responseDTO);
    }

    // 호텔 정보 수정
    @PutMapping("/{id}")
    public ResponseEntity updateHotel(
            @PathVariable Long id,
            @Valid @RequestBody HotelCreateRequestDTO requestDTO) {

        try {
            Hotel hotel = hotelMapper.toEntity(requestDTO);
            Hotel updatedHotel = hotelService.updateHotel(id, hotel);
            HotelDTO responseDTO = hotelMapper.toDTO(updatedHotel);

            return ResponseEntity.ok(responseDTO);
        } catch (RuntimeException e) {
            return ResponseEntity.notFound().build();
        }
    }

    // 호텔 삭제
    @DeleteMapping("/{id}")
    public ResponseEntity deleteHotel(@PathVariable Long id) {
        try {
            boolean deleted = hotelService.deleteHotel(id);
            return deleted ? ResponseEntity.noContent().build() 
                          : ResponseEntity.notFound().build();
        } catch (RuntimeException e) {
            return ResponseEntity.notFound().build();
        }
    }
}

HotelMapper 클래스

package com.example.mapper;

import com.example.dto.HotelDTO;
import com.example.dto.HotelCreateRequestDTO;
import com.example.model.Hotel;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@Component
public class HotelMapper {

    // Entity to DTO
    public HotelDTO toDTO(Hotel hotel) {
        if (hotel == null) {
            return null;
        }

        return new HotelDTO(
            hotel.getId(),
            hotel.getName(),
            hotel.getLocation(),
            hotel.getDescription(),
            hotel.getRating(),
            hotel.getRoomCount(),
            hotel.getCreatedAt()
        );
    }

    // Entity List to DTO List
    public List toDTOList(List hotels) {
        return hotels.stream()
                .map(this::toDTO)
                .collect(Collectors.toList());
    }

    // Request DTO to Entity
    public Hotel toEntity(HotelCreateRequestDTO requestDTO) {
        if (requestDTO == null) {
            return null;
        }

        Hotel hotel = new Hotel();
        hotel.setName(requestDTO.getName());
        hotel.setLocation(requestDTO.getLocation());
        hotel.setDescription(requestDTO.getDescription());
        hotel.setRating(requestDTO.getRating());
        hotel.setRoomCount(requestDTO.getRoomCount());
        hotel.setCreatedAt(LocalDateTime.now());

        return hotel;
    }
}

응답 메시지 처리 과정

  1. 요청 처리: 클라이언트가 특정 호텔의 정보를 요청하면, getHotelById 메소드가 호출됩니다
  2. 비즈니스 로직 실행: hotelService.getHotelById(id) 메소드를 통해 호텔 정보를 조회합니다
  3. DTO 생성: 조회된 호텔 정보를 사용하여 HotelMapper를 통해 HotelDTO 객체를 생성합니다
  4. 응답 반환: 생성된 DTO 객체를 클라이언트에게 JSON 형식으로 반환합니다

MapStruct를 활용한 고급 매핑

의존성 추가


    org.mapstruct
    mapstruct
    1.5.5.Final


    org.mapstruct
    mapstruct-processor
    1.5.5.Final
    provided

MapStruct 매퍼 인터페이스

package com.example.mapper;

import com.example.dto.HotelDTO;
import com.example.dto.HotelCreateRequestDTO;
import com.example.model.Hotel;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

import java.util.List;

@Mapper(componentModel = "spring")
public interface HotelMapperInterface {

    HotelMapperInterface INSTANCE = Mappers.getMapper(HotelMapperInterface.class);

    // Entity to DTO
    @Mapping(source = "roomCount", target = "roomCount")
    HotelDTO toDTO(Hotel hotel);

    // DTO List 변환
    List toDTOList(List hotels);

    // Request DTO to Entity
    @Mapping(target = "id", ignore = true)
    @Mapping(target = "createdAt", expression = "java(java.time.LocalDateTime.now())")
    Hotel toEntity(HotelCreateRequestDTO requestDTO);
}

결론

DTO를 사용하여 REST API의 응답 메시지를 처리하는 것은 클라이언트에게 필요한 데이터만을 전송하고, 불필요한 정보의 노출을 방지하는 데 도움이 됩니다. DTO는 데이터 전송을 최적화하여 클라이언트와의 통신을 효율적으로 관리할 수 있게 해줍니다.
핵심 포인트:

  • DTO와 VO의 명확한 역할 구분을 통한 적절한 객체 설계
  • 요청용 DTO와 응답용 DTO 분리로 API 계약 명확화
  • 매퍼 패턴 활용을 통한 Entity-DTO 간 변환 로직 분리
  • Validation 어노테이션 활용으로 데이터 검증 강화
  • MapStruct 등 매핑 라이브러리를 통한 보일러플레이트 코드 최소화

이러한 구조를 통해 MSA 환경에서 REST API를 설계하고 구현하는 데 유용한 패턴을 제공합니다. DTO 패턴을 올바르게 활용하면 API의 안정성, 보안성, 유지보수성을 크게 향상시킬 수 있습니다.

반응형