[DevOps] 스프링 서버 모놀리식 배포 - DTO의 사용 이유, Controller, Service layer 작성
본격적으로 작성하기 전에 할 말
이번 포스트에서는 Controller, Service layer를 작성할 예정입니다.
간단한 Controller, Service layer를 작성할 예정이기 때문에 따로 테스트 코드를 작성하지는 않겠습니다. 저희는 코드를 어떻게 짜느냐가 메인이 아니라, 어떻게 스프링 서버를 배포시키는지에 집중하기로 했기 때문이에요!
DTO가 뭔지는 알고 사용해보죠
일단 DTO를 구성하기 이전에, 왜 DTO라는 객체를 사용해야하는지 부터 알아봅시다.
저희는 흔한 말로, 서버 어플리케이션을 구성할 때 엔티티를 직접 반환해서는 안되고, 반드시 DTO를 사용해서 반환하여라 라는 말을 들어봤을겁니다. 그러나, 왜 DTO를 사용하는지에 대해서는 모르는 경우가 많을겁니다. 사실 저도 이 글을 쓰기 전에는 그 이유를 몰랐습니다.
DTO를 사용하는 이유에 대해서는 유명한 서적 클린코드의 저자인 마틴 파울러가 잘 설명을 해주었습니다.
위의 글을 번역하면 내용은 이렇습니다.
- Entity를 직접 호출하는데 비용이 많이 들어갑니다. 이유는 다음과 같습니다.
- call 자체에 비용이 많이 들어갑니다. 그리고 그 call의 수는 증가할 수 있습니다.
- call을 줄이기 위해서는 parameter를 많이 전달하는 방법이 있는데, 이는 절대 clean하지 못합니다.
- Java같은 언어는 메서드마다 return 값이 하나만 가능하므로, 가끔 Entity를 직접 호출하는 방식으로는 로직을 구성하는데 어려움이 존재할 수도 있습니다.
- 단일 call을 통해서 여러개의 remote call을 하기 위해서는 직렬화 알고리즘(serialization algorithm)이 필요할수도 있습니다. 그리고 이는 DTO에 두면 매우 효율적입니다.
- 직렬화 알고리즘을 Domain layer 외부에 둠으로써 아키텍쳐를 clean하게 가져갈 수 있다는 이점이 있습니다.
그 외에 제 경험상, 그리고 제가 찾아보았던 자료들을 토대로 몇 가지 이유를 첨부하자면 다음의 이유도 존재합니다.
- 서버 어플리케이션의 아키텍처를 최대한 효율적으로 가져가기 위해서는 Domain layer를 분리할 필요가 있습니다. (흔히, DDD(Domain Driven Development)라고 부르는 것이죠) 그런데 Domain layer는 직렬화 알고리즘을 기억해서는 안되기 때문에 직렬화 알고리즘을 DTO에 두어야하는 것도 이유가 됩니다.
- 하나의 Entity에 대해서 response의 형태는 다양하게 존재할 수도 있고, 그리고 response에 있어서 Entity에서 감춰야할 정보가 있거나, 혹은 확장시켜야할 정보가 분명히 존재합니다. 따라서 하나의 Entity에 대해서 response DTO를 여러개 두는게 여러가지 이점을 가집니다.
이제 왜 DTO를 사용해야하는지 살펴보았으니, 본격적으로 구현을 해봅시다!
DTO를 구현해봅시다. 여기서는 코드만 보여드릴게요
MenuRequestDto.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MenuRequestDto {
private String name;
@JsonProperty("shop_id")
private Long shopId;
}
MenuResponseDto.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MenuResponseDto {
private Long id;
private String name;
@JsonProperty("shop_name")
private String shopName;
}
ShopRequestDto.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ShopRequestDto {
private String name;
private String address;
public static Shop dtoToEntity(ShopRequestDto requestDto) {
return Shop.builder()
.name(requestDto.getName())
.address(requestDto.getAddress())
.build();
}
}
ShopResponseDto.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ShopResponseDto {
private Long id;
private String name;
private String address;
}
Service layer를 구성해봅시다
일단 MenuService를 먼저 살펴보겠습니다
@Service
@RequiredArgsConstructor
public class MenuService {
private final MenuRepository menuRepository;
private final ShopRepository shopRepository;
public ResponseEntity<SingleResult<MenuResponseDto>> read(Long id) {
return menuRepository.findById(id).map(menu -> ResponseEntity.ok(
ResultProvider.getSingleResult(entityToResponseDto(menu)))
).orElseThrow(() -> new EntityNotFoundException("Entity not found!")
);
}
public ResponseEntity<CommonResult> create(MenuRequestDto requestDto) {
// request가 들어오지 않은 경우
if(requestDto == null)
throw new RuntimeException();
Menu requestMenu = requestDtoToEntity(requestDto);
Menu savedMenu = menuRepository.save(requestMenu);
return ResponseEntity.ok(ResultProvider.getSuccessResult());
}
// entity -> responseDto
private MenuResponseDto entityToResponseDto(Menu menu) {
return MenuResponseDto.builder()
.id(menu.getId())
.name(menu.getName())
.shopName(menu.getShop().getName())
.build();
}
// requestDto -> entity
// 무조건 service logic 내부에서만 사용할 메소드이기 때문에 여기서 예외처리를 해주면 되는 것임.
private Menu requestDtoToEntity(MenuRequestDto requestDto) {
Optional<Shop> foundShop = shopRepository.findById(requestDto.getShopId());
return foundShop.map(shop -> Menu.builder()
.name(requestDto.getName())
.shop(shop)
.build()
).orElseThrow(() -> new EntityNotFoundException("Entity not found!")
);
}
}
우선 필요한 repository들을 의존성 주입을 받습니다.
저희는 service layer에는 create, read만 작성하겠습니다.
read의 경우에는 파라미터로 pathVariable로 id만을 받습니다. 그리고 반환값으로는, SingleResult에 DTO를 실어서 보낼 예정입니다.
우선 id를 바탕으로 menu를 하나 찾아냅니다. 이 때 findById는 Optional 필드이기 때문에 null인 경우 EntityNotFoundException을 발생시켜서 예외를 던짐과 동시에 transaction을 rollback 시켜버립니다.
그리고 menu를 찾아낸 결과가 null이 아닌 경우에는 entity 형태로 존재하기 때문에 entity를 dto로 변환시켜서 SingleResult에 실어서 반환해주면 됩니다.
create의 경우에는 파라미터로 requestDto를 받아냅니다. requestDto가 전달이 되지 않는 경우에는 에외를 던져줍니다.
null이 아닌 경우에도 requestDto가 유효하지 않아도 예외를 발생시켜야하지 않느냐 라는 질문을 던질수도 있습니다. 이거는 service layer에서 구현을 하는 것이 아니라, DTO에서 validation을 걸어서 검증을 해주는겁니다. Service layer는 본연의 비지니스 로직에만 집중을 하셔야합니다. 물론 저는 귀찮아서 validation을 걸지 않았습니다.
그리고 모든 케이스를 통과하면 dto를 entity로 변환시켜서 저장하고 CommonResult를 반환해주면 됩니다.
그리고 ShopService의 경우도 구현이 위와 비슷합니다. 코드만 제시해드리고 Controller로 넘어가겠습니다.
@Service
@RequiredArgsConstructor
public class ShopService {
private final ShopRepository shopRepository;
// GET Method
public ResponseEntity<SingleResult<ShopResponseDto>> read(Long id) {
return shopRepository.findById(id).map(shop ->
ResponseEntity.ok(ResultProvider.getSingleResult(entityToResponseDto(shop)))
).orElseThrow(() -> new EntityNotFoundException("Entity not found!"));
}
// Post Method
public ResponseEntity<CommonResult> create(ShopRequestDto requestDto) {
Shop requestShop = ShopRequestDto.dtoToEntity(requestDto);
Shop savedShop = shopRepository.save(requestShop);
return ResponseEntity.ok(ResultProvider.getSuccessResult());
}
// 모든 가게 목록을 뽑아오는 메소드
public ResponseEntity<MultipleResult<ShopResponseDto>> searchAllShops() {
List<Shop> allShops = shopRepository.findAll();
if(allShops.size() == 0)
throw new RuntimeException("가게가 존재하지 않아요!");
List<ShopResponseDto> responseList = allShops.stream().map(shop -> entityToResponseDto(shop)
).collect(Collectors.toList()
);
return ResponseEntity.ok(ResultProvider.getMultipleResult(responseList)
);
}
private ShopResponseDto entityToResponseDto(Shop shop) {
return ShopResponseDto.builder()
.id(shop.getId())
.name(shop.getName())
.address(shop.getAddress())
.build();
}
}
Controller를 살펴봅시다. 간단하니까 코드만 남길게요
ShopController.java
@RequestMapping("/api/shop")
@RestController
@RequiredArgsConstructor
public class ShopController {
private final ShopService shopService;
@GetMapping("/{id}")
public ResponseEntity<SingleResult<ShopResponseDto>> read(@PathVariable Long id) {
return shopService.read(id);
}
// Post Method
@PostMapping("")
public ResponseEntity<CommonResult> create(@RequestBody ShopRequestDto requestDto) {
return shopService.create(requestDto);
}
@GetMapping("/all-list")
public ResponseEntity<MultipleResult<ShopResponseDto>> searchAllShops() {
return shopService.searchAllShops();
}
// For Health Check
@GetMapping("/check")
public String healthCheck() {
return "Health check!!";
}
}
MenuController.java
@RestController
@RequestMapping("/api/menu")
@RequiredArgsConstructor
public class MenuController {
private final MenuService menuService;
@GetMapping("/{id}")
public ResponseEntity<SingleResult<MenuResponseDto>> read(@PathVariable Long id) {
return menuService.read(id);
}
@PostMapping("")
public ResponseEntity<CommonResult> create(@RequestBody MenuRequestDto requestDto) {
return menuService.create(requestDto);
}
}
ShopController에서 특이한 메소드가 하나 있는데, healthCheck() 메소드입니다. 이 메소드는 추후에 load balancing하는데 있어서 Health check를 하기 위해서 둔 메소드입니다.
다음 포스트에서는, 지금까지 작성한 스프링 어플리케이션을 어떻게 ec2에 배포하는지를 다뤄볼 예정입니다.
다음 포스트에서 뵙겠습니다. 감사합니다.
참고문헌
- DTO를 사용하는 이유는 무엇인가?
https://martinfowler.com/eaaCatalog/dataTransferObject.html
Author And Source
이 문제에 관하여([DevOps] 스프링 서버 모놀리식 배포 - DTO의 사용 이유, Controller, Service layer 작성), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@18k7102dy/devops-mono-4저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)