반응형

마이크로서비스 아키텍처(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;
}
}응답 메시지 처리 과정
- 요청 처리: 클라이언트가 특정 호텔의 정보를 요청하면, getHotelById 메소드가 호출됩니다
- 비즈니스 로직 실행: hotelService.getHotelById(id) 메소드를 통해 호텔 정보를 조회합니다
- DTO 생성: 조회된 호텔 정보를 사용하여 HotelMapper를 통해 HotelDTO 객체를 생성합니다
- 응답 반환: 생성된 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의 안정성, 보안성, 유지보수성을 크게 향상시킬 수 있습니다.
반응형
'IT기술 > MSA (with. springboot)' 카테고리의 다른 글
| REST API 완벽 가이드: MSA 환경에서의 효율적인 서비스 간 통신 설계 (0) | 2025.07.09 |
|---|---|
| Spring Web MVC 프레임워크 완벽 가이드: MSA 환경에서의 웹 애플리케이션 구축 (0) | 2025.07.08 |
| Spring Bean, Java Bean, DTO, VO와 불변 클래스 설계 완벽 가이드: MSA 환경에서의 객체 관리 전략 (0) | 2025.07.07 |
| Spring @Primary와 @Lazy 어노테이션 완벽 가이드: MSA 환경에서의 효율적인 빈 관리 (0) | 2025.07.06 |
| Spring Bean 생명주기와 BeanPostProcessor 완벽 가이드: MSA 환경에서의 효율적인 빈 관리 (0) | 2025.07.05 |