DTO๋ ์ด๋ค Layer์ ํฌํจ๋์ด์ผ ํ๋๊ฐ.
by JiwonDev์ต๊ทผ์ Dto Mapper์ ๊ด๋ จ๋ ๊ธ์ ์ฐ๋ฉฐ, ์๊ฐ๋ณด๋ค ๋ง์ ํ๋ก์ ํธ์์ ์๋์ ๊ฐ์ ์ฝ๋๋ฅผ ๋ง์ด ์ฌ์ฉํ๋ค๋ ๊ฑธ ์์๋ค.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/")
public class ItemApiController {
private final ItemFacade itemFacade; // package.application.ItemFacade
private final ItemDtoMapper mapper;
@GetMapping("/register")
public CommonResponse register(@RequestBody @Valid ItemDto.RegisterItemRequest request) {
var itemCommand = mapper.toCommand(request); // package.domain.ItemCommand
var itemInfo = itemFacade.register(itemCommand); // // package.domain.ItemInfo
var response = mapper.toResponse(itemInfo);
return CommonResponse.success(response); // package.presentation.ItemResponse
}
}
์ค์ ์คํ๋ง ๊ฐ์ด๋๋ก ์ ๋ช ํ baeldung.com ์ ์์ ์ฝ๋๋ ์ด๋ฌํ๋ค.
@RestController
class PostRestController {
@Autowired
private ModelMapper modelMapper;
@GetMapping
public List<PostDto> getPosts(...) {
//...
List<Post> posts = postService.getPostsList(page, size, sortDir, sort);
return posts.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public PostDto createPost(@RequestBody PostDto postDto) {
Post post = convertToEntity(postDto);
Post postCreated = postService.createPost(post));
return convertToDto(postCreated);
}
@GetMapping(value = "/{id}")
public PostDto getPost(@PathVariable("id") Long id) {
return convertToDto(postService.getPostById(id));
}
@PutMapping(value = "/{id}")
@ResponseStatus(HttpStatus.OK)
public void updatePost(@PathVariable("id") Long id, @RequestBody PostDto postDto) {
if(!Objects.equals(id, postDto.getId())){
throw new IllegalArgumentException("IDs don't match");
}
Post post = convertToEntity(postDto);
postService.updatePost(post);
}
}
DtoMapper์ ํจ๊ป ์์ ๊ฐ์ ์ฝ๋๋ฅผ ์ฌ์ฉํ๋ฉด ๋ง์ ์ฅ์ ์ด ์๋ค.
- Application(Service) ๋ ์ด์ด๋ Request, Response์ ๋ํด ๋ชฐ๋ผ๋ ๋๋ค.
- Presentation(Controller) ๋ ์ด์ด๋ง ์ ์ถ๋ ฅ์ ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์, ์ ์ฐ์ฑ์ด ๋ฐ์ด๋๊ณ ์ฝ๋๊ฐ ๊น๋ํด์ง๋ค.
๐ญ ํ์ง๋ง ์ฐ๋ฆฌ ํ๋ก์ ํธ์์ ์ฌ์ฉํ ์ ์์ด์
ํ์ฌ ํ๋ก์ ํธ์์๋ ์๋์ ๊ฐ์ ํจํค์ง ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๊ณ ์๋ค.
๋๋ฉ์ธ์ ์ฌ์ฉํ๋ ค๋ฉด ๋ฐ๋์ ์ดํ๋ฆฌ์ผ์ด์ ๋ ์ด์ด๋ฅผ ๊ฑฐ์ณ์ผํ๋ค. ์ง์ ์ ์ ํ๋๋ก ๋ง๋ค๊ณ , ์์กด์ฑ ๊ด๋ฆฌํ๊ธฐ ์ํจ์ด๋ค.
์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. ์ปจํธ๋กค๋ฌ์์๋ Domain Dto (ItemInfo, ItemDto ๋ฑ)์ ์ฌ์ฉํ๊ฑฐ๋ import ํ ์ ์๋ค.
- Dto๋ ๋๋ฉ์ธ ๋ ์ด์ด์ ์กด์ฌํ๊ธฐ ๋๋ฌธ์, ์ปจํธ๋กค๋ฌ์์ ๊บผ๋ด์ ์ฌ์ฉํ ์ ์๋ค.
@GetMapping("/register")
public CommonResponse register(@RequestBody @Valid ItemDto.RegisterItemRequest request) {
var itemCommand = mapper.toCommand(request); // package.domain.ItemCommand
var itemInfo = itemFacade.register(itemCommand); // // package.domain.ItemInfo
var response = mapper.toResponse(itemInfo);
return CommonResponse.success(response); // package.presentation.ItemResponse
}
๐ : ๊ทธ๋ผ [๋๋ฉ์ธ ์์ DTO]๋ฅผ ์ฌ์ฉํ์ง ์์ผ๋ฉด ๋์์์? ๋ค๋ฅธ ๊ณ์ธต์๋ง ๋ฐ๋ก ๋ง๋ค์ฃ ?
๋ง๋ ๋ง์ด๋ค. '๋๋ฉ์ธ DTO'๋ฅผ ์์ ์ฌ์ฉํ์ง ์๊ณ , ์๋์ฒ๋ผ ๋๋ฉ์ธ ์๋น์ค์ ํ๋ํ๋ ํ๋๋ฅผ ๊น์ ๋ฃ์ผ๋ฉด ๋๋ค.
// Application ๋ ์ด์ด - AccountFacade ์ฝ๋
@Transactional
public RegisterAccountResponse register(RegisterAccountCommand request) {
var entity = accountService.register( // ๋๋ registerAccountProcessor.register(..)
request.getUserId(),
request.getEmail(),
request.getPassword());
return new RegisterAccountResponse(entity.getUserId(), entity.getEmail());
}
์ค์ ๋ก ๋ฐ ๋ฒ๋ ผ - ๋๋ฉ์ธ ์ฃผ๋ ์ค๊ณ ํต์ฌ (IDDD) ์ํ์ฝ๋๋ฅผ ๋ณด๋ฉด, ์์ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ํ๋๋ฅผ ํ๋ํ๋ ๋ฃ๊ณ ์๋ค.
ํ์ง๋ง ์ด ๋ฐฉ๋ฒ์ผ๋ก ํ๊ณ๊ฐ ์๋ค. ์๋์ ๊ฐ์ด ํ๋๊ฐ ์์ฒญ ๋ง๋ค๋ฉด? ๊ณผ์ฅํ๊ฒ ์๋๋ผ, ์ค์ ๋ก ์ ๋ฐ DTO๊ฐ ๋ง์ด ์ฌ์ฉ๋๋ค.
@Getter
@Setter
@ToString
public static class RegisterOrderRequest {
private String orderToken;
private Long userId;
private String payMethod;
private Long totalAmount;
private String orderedAt;
private String status;
private String statusDescription;
private String receiverName;
private String receiverPhone;
private String receiverZipcode;
private String receiverAddress1;
private String receiverAddress2;
private String etcMessage;
private List<RegisterOrderItem> orderItemList;
}
์ด๋ฐ ๊ฒฝ์ฐ, ๋๋ฉ์ธ ๋ฉ์๋์ ํ๋ผ๋ฉํ๋ 10๊ฐ, 20๊ฐ๊ฐ ๋ ๊ฒ์ด๋ค.
์ด ์ฝ๋๋ ํํ ๋งํ๋ ํด๋ฆฐ์ฝ๋์๋ ๊ฑฐ๋ฆฌ๊ฐ ๋งค์ฐ ๋ฉ๋ค. ๋๋ฉ์ธ Dto(OrderInfo, OrderDto)์ ํฌ์ฅํด์ ์ ๋ฌํด์ผํ๋ค.
public register(Long userId, Long totalAmount, String orderToken,
String payMethod, String orderedAt, String status, ...)// ์ด๋ ๊ฒ 20๊ฐ์ ํ๋ผ๋ฉํ๋ฅผ ๋ฐ์๊ฑด๊ฐ?
๐ญ Request, Response๋ ์ด๋ ๋ ์ด์ด์ ๋ฌ์ผ ํ๋๊ฐ.
๐ : ?? ๋น์ฐํ ์ปจํธ๋กค๋ฌ๊ฐ ์๋ ํ๋ ์ ํ ์ด์ ๋ ์ด์ด๊ฒ ์ฃ !
๋๋ ์ฒ์์ ๊ทธ๋ ๊ฒ ์๊ฐํ๋ค. ํ์ง๋ง ์ปจํธ๋กค๋ฌ์ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ฝ๋๋ฅผ ๋ณด์.
@PostMapping("/signup")
public ApiResponse<RegisterAccountResponse> register(
@Valid @RequestBody RegisterAccountCommand request) {
return ApiResponse.success(accountFacade.register(request));
}
// Application ๋ ์ด์ด - AccountFacade ์ฝ๋
@Transactional
public RegisterAccountResponse register(RegisterAccountCommand request) {
var entity = accountService.register( // ๋๋ registerAccountProcessor.register(..)
request.getUserId(),
request.getEmail(),
request.getPassword());
return new RegisterAccountResponse(entity.getUserId(), entity.getEmail());
}
Request๊ฐ ํ๋ ์ ํ ์ด์ ์ ์กด์ฌํ๋ ๊ฒฝ์ฐ, App ๋ ์ด์ด๊ฐ Presentation ๋ ์ด์ด๋ฅผ ์์กดํ๊ฒ ๋๋ค.
Application์ Presentation์ ์ ํ์๊ฐ ์ ํ์๋ค. ๋๋ฉ์ธ ์์ ์ ์์ํ๊ณ , ์ธ๋ถ ์ฑ์ ํธ์ถํ๋ ์ญํ ๋ง ๊ฐ์ง๋ค.
์ข ๋ ์ฝ๊ฒ ๋น์ ํด๋ณด์๋ฉด Application์ TV ๋ฆฌ๋ชจ์ปจ๊ณผ ๊ฐ์ ์ญํ ์ด๋ค.
๋ฆฌ๋ชจ์ปจ์ ๋ง๋๋๋ฐ ์ด๋ค ์ฌ๋์ด ์ฌ์ฉํ ์ง๋ฅผ ์ ํํ๊ฒ ์์์ผํ ๊น? ์๋๋ค. ์๋น์(Presentation)๊ฐ ๋ฆฌ๋ชจ์ปจ์ ์ฌ์ฉํ๋ ๊ฑฐ์ง, ๋ฆฌ๋ชจ์ปจ์ด ์๋น์๋ฅผ ์ ํ์๋ ์ ํ ์๋ค.
๊ทธ๋ผ ๊ฐ์ฅ ๊น๋ํ ๋ฐฉ๋ฒ์ ๊ฐ ๊ณ์ธต๋ง๋ค DTO๋ก ๋๋ฐฐํด๋๋ ๊ฒ์ด๋ค.
DomainDto โก AppDto โก ControllerResponse ์ด๋ ๊ฒ 3๊ฐ์ Dto๋ฅผ ์ฌ์ฉํ๋ฉด, ์์กด์ฑ์ด ๊ผฌ์ด์ง ์๊ณ ์ฒ๋ฆฌํ ์ ์๋ค.
์ฐ๋ฆฌ๋ ํด๋ฆฐ์ฝ๋, ํด๋ฆฐ์ํคํ ์ฒ๋ฑ์ ๊ณต๋ถํ๋ฉด์ ๋ณํ์ ๋ฐฉํฅ, ์์กด์ฑ์ ํต์ ํ๋ ๊ฒ์ ์ค์ํจ์ ์๊ณ ์๋ค.
- application์ ๋๋ฉ์ธ์ ์บก์ํํ๊ณ , ์ฑ ์ง์ ์ ์ ํ๋๋ก ๋ง๋ค์ด์ค๋ค.
- SRP ์์น. ์ปจํธ๋กค๋ฌ๊ฐ ๋ณ๊ฒฝ๋๋ค ํด์ ๋๋ฉ์ธ์ ์ํฅ์ด ์์ผ๋ฉด ์๋๋ค. ์ด๋ ๋ฐ๋๋ ๋ง์ฐฌ๊ฐ์ง
- application์ ์ปจํธ๋กค๋ฌ Request์ ์์กด์ฑ์ด ์๊ธฐ๋ฉด ์๋๋ค. ์ด๋ Http Request๋ผ๋ ํ์ ๊ธฐ์ ์ ์ฑ์ด ๋ฌถ์ด๊ฒ๋๋ค.
์ด ๊ทธ๋ฆผ์ฒ๋ผ ํ๋ฉด '์ด๋ก ์ผ๋ก๋ ๊ฐ์ฅ ๊น๋ํ ์ฝ๋'๊ฐ ๋์จ๋ค.
ํ์ง๋ง ๋๋ฉ์ธ ๋ชจ๋ธ๊ณผ ๋๊ฐ์ DTO๋ฅผ 3๊ฐ์ฉ ๋ง๋ค์ด์ ๋ณํํ๋ค๋ณด๋ฉด, ์ด ๋ ธ๊ฐ๋ค๋ฅผ ๊ณ์ํ๋๊ฒ ๋ง๋ ์์ฌ์ด ๋ ๋ค.
๐ : ์์กด์ฑ์..์ด๋ ๊ฒ ๊น์ง ๊ด๋ฆฌํ ํ์๋ ์์ง์์๊น? DTO์ ๋๋ ๊ทธ๋ฅ ์ฐ์
์ด์ง ๋ป์ง์ด๊ธดํ์ง๋ง.. DTO๋ฅผ ์ปจํธ๋กค๋ฌ์์ ์จ๊ธฐ๋ ค๊ณ ์๋์ ๊ฐ์ ์ฝ๋๋ฅผ ์ฌ์ฉํ ๊น ๊ณ ๋ฏผํ ์ ๋ ์๋ค.
mapper.toResponse( MemberFacade.create ( mapper.toCommand( request ) ) );
@GetMapping("/register")
public CommonResponse register(@RequestBody @Valid CreateMemberRequest request) {
// var itemCommand = mapper.toCommand(request);
// var itemInfo = itemFacade.register(itemCommand);
// var response = mapper.toResponse(itemInfo);
// ํ์ค๋ก ์์ฑํด์ DTO ์จ๊ฒจ๋ฒ๋ฆฌ๊ธฐ (mapper๋ง Presentation์ ์์กด์ฑ์ด ์์ผ๋ฉฐ, ์ด๋ Application์ ๋๋ค.)
var response = mapper.toResponse(MemberFacade.create(mapper.toCommand(request)));
return CommonResponse.success(response);
}
๐ญ ์๋ ๊ทธ๋์ ๊ฒฐ๋ก ์ด ๋ญ์์
์ ํด์ง ๋ต์ด ์๋ค๋๊ฒ ๊ฒฐ๋ก ์ด๋ค. DTO๋ฅผ ์ด๋ป๊ฒ ์ฌ์ฉํ๋๊ฐ๋ ์ ๋ต์ด ์๋ค.
๋ชจ๋ ๊ณณ์ ์๋ฒฝํ ์ํํ์ ์๋ค.
์์๋ ์ด์ด(Controller)์์ ํ์๋ ์ด์ด(App, Entity)๋ฅผ ์ฐธ์กฐํ๋ ๊ฑด ๋น์ฅ ํฐ ๋ฌธ์ ๊ฐ ๋์ง๋ ์๋๋ค.
์ฌ์ฉํ๋ ๋ชฉ์ ์ ๋ง๊ฒ ์ ์ ํ ํํํด์ ์ฐ๋ฉด ๋๋ค. ์ ๋ต์ ์๋ค.
- 'DTO๋ฅผ ์ด๋ป๊ฒ ์ฐ๋'๋ก ๊ฐ๋ฉด ์์ํ ํด๊ฒฐ๋์ง ์๋๋ค.
- '์ DTO๋ฅผ ์ฌ์ฉํ๋๊ฐ?' ์ ์ด์ ์ ๋ง์ถฐ์ ๋์๊ฒ ๋ง๋ ๋ฐฉ๋ฒ์ ์ฐพ์์ผํ๋ค.
๐ญ ๊ธ์ด์ด๋ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์?
๋๋ฉ์ธ ๋ฉ์๋๋ ๊น๋ํ๊ฒ ์ ์งํ๊ณ , Application ์ฝ๋๋ฅผ ์กฐ๊ธ ๋๋ฝ๊ฒ(?) ์ฌ์ฉํ๋ค.
ํ๋๋ฅผ ๊น์ ๋ฃ๊ณ ์๊ณ [๋๋ฉ์ธ Dto]๋ ๊ผญ ํ์ํ ๊ฒฝ์ฐ์๋ง Application์์ ๋ณํํด์ ์ฌ์ฉํ๋ค.
- Request(Command) -> [๋๋ฉ์ธ Dto] -> ๋๋ฉ์ธ ์๋น์ค ์ ๋ฌ
- ๋๋ฉ์ธ ์๋น์ค -> [๋๋ฉ์ธ Dto] -> Response
๊ทธ๋ฆฌ๊ณ Request, Response๋ฅผ Application ๋ ์ด์ด์ ๋ฌ์ Presentation์ ์์กด์ฑ์ ์์ด๋ค.
@Transactional
public void write(Long memberId, WriteArticleCommand command) {
writeArticleProcessor.write(memberId,
command.getTitle(), command.getContent(),
command.getImage(), command.getTags());
}
@Transactional(readOnly = true)
public List<ArticlePreviewResponse> findAll(Long articleId, int pageSize) {
var domainDto = articleReader.findAll(articleId, pageSize);
// ์ถํ return mapper.toResponse(domainDto); ๋ก ๋ณ๊ฒฝ.
return domainDto.stream()
.map(dto -> new ArticlePreviewResponse(
dto.getId(), dto.getAuthorId(), dto.getTitle(),
dto.getNickname(), dto.getImage(), dto.getCreatedAt(),
dto.getModifiedAt(), dto.getStatus())
).collect(Collectors.toList());
}
๋์ค์ Application์ Mapstruct๊ฐ์ ๋งคํผ๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๋๊ฐ ๊น๋ํด์ง ๊ฒ๊ฐ๋ค.
์ฌ์ค ์ ์ผ ์ฌ์ฉํ๊ณ ์ถ์ ๋ฐฉ๋ฒ์ ์ฒ์์ ๋งํ๋ ์ฝ๋์ด๊ธดํ๋ฐ, ์ฌ์ค ๋๋ ์ ๋ชจ๋ฅด๊ฒ ๋ค.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/")
public class ItemApiController {
private final ItemFacade itemFacade; // package.application.ItemFacade
private final ItemDtoMapper mapper;
@GetMapping("/register")
public CommonResponse register(@RequestBody @Valid ItemDto.RegisterItemRequest request) {
var itemCommand = mapper.toCommand(request); // package.domain.ItemCommand
var itemInfo = itemFacade.register(itemCommand); // // package.domain.ItemInfo
var response = mapper.toResponse(itemInfo);
return CommonResponse.success(response); // package.presentation.ItemResponse
}
}
DTO ์ฌ์ฉ๋ฒ์ ์ฐ์ฐํ์ง ๋ง๊ณ , ํ๋ก์ ํธ๋ฅผ ๊ณ์ ๋ง๋ค๋ฉด์ ๋ฌธ์ ๊ฐ ์๊ธฐ๊ณ ๋ฆฌํฉํ ๋ง ํด์ผํ ์์ ์ด ์์ ๋ ํ๋์ฉ ๋ณ๊ฒฝํ ์๊ฐ์ด๋ค.
๊ฒฐ๊ตญ DTO์ ์ฌ์ฉ๋ฐฉ๋ฒ๋ ๊ธฐ์ ์ ์ง์ฐฉํ์ง๋ง๊ณ , ๋ฌธ์ ๋ฅผ ๋ณด๋ผ๋ ๊ฒ๊ณผ ๋์ผํ ๋งฅ๋ฝ์ธ ๋ฏ ํ๋ค.
๐ญ ๋ ํผ๋ฐ์ค
tecoble - DTO์ ์ฌ์ฉ ๋ฒ์์ ๋ํ์ฌ
๋๋ฉ์ธ ์ํฐํฐ๋ฅผ DTO์ ํฌ์ํ๋ ๋ ๋์ ๋ฐฉ๋ฒ
DDD - which layer DTO should be implemented
Should service layer accept a DTO or a custom request object from the controller?
'๐ฑBackend > Java' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
Mapstruct(DTO Mapper ๋ผ์ด๋ธ๋ฌ๋ฆฌ) (0) | 2022.01.20 |
---|---|
๋ JPA๋ฅผ ์ ๋๋ก ์๊ณ ์ฐ๋ ๊ฒ์ธ๊ฐ (์ ๋ฆฌ์ค..) (0) | 2021.11.17 |
ํ ์คํธ ๋ชจํน Mockito ๋ผ์ด๋ธ๋ฌ๋ฆฌ (0) | 2021.09.07 |
[์ฝ๋๋ถ์๋๊ตฌ]# 3 SonarLint, SonarQube (0) | 2021.08.21 |
[์ฝ๋๋ถ์๋๊ตฌ]#2 Jacoco ์ ์ฉํ๊ธฐ (0) | 2021.08.21 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
JiwonDev
JiwonDev