회고록(31) - 비즈니스 Layer, @Service, Mapper, Entity
1. 데일리 일기
5초룰이 생각보다 효과가 좋다. 내 뇌속의 생각이 틀림을 인정해서 인지, 좀 더 원하는 행동력을 가지게 된 것 같다. 내 생각을 의심하면서 사용해보자.
2. 오늘 배운 내용.
1. API계층과 Service 계층 연동하기
API 계층은 Controller로 사용되며 요청 URI의 엔드포인트로써 여러 계층의 Model과 View 사이에서 요청에 맞는 데이터를 얻어오고 얻어온 데이터를 응답메시지로 만드는 작업을 한다. 그러므로 Service 계층과 연동할 필요가 있다.
Service 계층은 비즈니스 로직을 수행하는 계층으로 @Service 를 적용한 클래스로 작성한다. 데이터 액세스 계층에 존재하는 DB에서 얻어온 데이터를 CRUD에 맞는 메서드를 수행시켜(원하는 비즈니스 로직을 수행하여) 결과 데이터를 API 계층으로 보내준다.
이 둘은 스프링 컨테이너의 DI를 통해 연동 될 수 있습니다. 바로 엔드포인트인 Controller에서 Spring 컨테이너에 존재하는 Service Bean을 생성자 주입으로 받아 해당 Service객체의 메서드를 통해 비즈니스 로직을 수행할 수 있게 됩니다.
@RestController
@RequestMapping("/v3/members")
@Validated
public class MemberController {
private final MemberService memberService;
// (1) MemberController의 변경 포인트
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
...
...
}
DI를 통한 API계층과 Service 계층 연동시의 문제점
이전에 API 계층은 코드의 간결성, 유효성 검증의 분리, 한번에 여러 필드를 객체로 제공하기 위해 DTO를 사용했습니다.
Service 계층 또한 비즈니스 로직과 DB연동의 역할에 관심을 집중하기위해 엔티티라는 객체를 사용합니다.
문제는 DI만 이용해서 Service 로직의 결과인 데이터(엔티티) 얻어 올수 있지만, 이 것만으로는 아래의 문제들을 해결 하지 못합니다.
- Service 계층의 메서드를 호출하기위해 요청 DTO를 엔티티로 바꾸어주는 작업이 필요합니다.
- 결과로 받아온 엔티티 객체(Service 에서 사용하는 객체)를 응답메세지에 사용하게 됩니다.
- 여기서도 만약 DTO로 응답하고 싶다면 엔티티를 DTO로 바꾸어주는 작업이 필요합니다.
이는 OOP의 클래스는 단일 책임과 역할을 가져야한다는 SRP 법칙을 지키지 않는다고 볼 수 있습니다. 그러므로 우리는 엔티티와 DTO를 양쪽으로 서로 변환해주는 존재가 필요합니다.
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
// (2)
Member member = new Member();
member.setEmail(memberDto.getEmail());
member.setName(memberDto.getName());
member.setPhone(memberDto.getPhone());
// (3)
Member response = memberService.createMember(member);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
2. Mapper의 등장
이전 OOP의 SRP을 준수하도록 하기위해서 요청 DTO를 엔티티로, 엔티티를 응답 DTO로 바꾸어줄 필요가 있습니다.
이를 Java로 Mapper 클래스를 만들어 줄수도 있습니다. 그러나 이 방식은 여러 Controller에 존재하는 DTO와 엔티티들이 생성될때마다(= 도메인 업무가 늘어날때 마다) 수동으로 메서드를 선언하고 이를 변환하는 로직을 구현해야 합니다.
그리하여 DTO와 엔티티 클래스의 Getter, Setter, 생성자등을 이용하여 매핑 구현 로직을 자동으로 생성해주는 Library Mapstruct가 탄생하게 되었습니다.
Mapstruct 라이브러리
DTO와 엔티티 클래스의 Getter, Setter, 생성자등을 이용하여 매핑 구현 로직을 Build 시 자동으로 생성해주는 라이브러리 입니다.
해당 라이브러리는 Mapper의 메서드를 선언하는 interface와 @Mapping 을 이용합니다.
package com.codestates.member.mapstruct.mapper;
import com.codestates.member.dto.MemberPatchDto;
import com.codestates.member.dto.MemberPostDto;
import com.codestates.member.dto.MemberResponseDto;
import com.codestates.member.entity.Member;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring") // (1)
public interface MemberMapper {
Member memberPostDtoToMember(MemberPostDto memberPostDto);
Member memberPatchDtoToMember(MemberPatchDto memberPatchDto);
MemberResponseDto memberToMemberResponseDto(Member member);
}
위와 같이 메서드 시그니처를 선언하면 매개변수와 메서드 반환형에 있는 Entitiy와 DTO의 Getter, Setter, 생성자를 이용하여 엔티티를 DTO로 DTO를 엔티티로 변환 해줍니다.
@RestController
@RequestMapping("/v5/coffees")
@Validated
public class CoffeeController {
private final CoffeeService coffeeService;
private final CoffeeMapper mapper;
// Service Layer와 Mapper는 Controller에 DI 하여 연동시켜준다.
public CoffeeController(CoffeeService coffeeService, CoffeeMapper coffeeMapper) {
this.coffeeService = coffeeService;
this.mapper = coffeeMapper;
}
@PostMapping
public ResponseEntity postCoffee(@Valid @RequestBody CoffeePostDto coffeePostDto) {
// Mapper에서 요청 DTO -> 엔티티로 처리 후 비즈니스 로직 수행
Coffee coffee = coffeeService.createCoffee(mapper.coffeePostDtoToCoffee(coffeePostDto));
// Mapper에서 엔티티 -> 응답 DTO로 DTO를 응답 바디로 변환
return new ResponseEntity<>(mapper.coffeeToCoffeeResponseDto(coffee), HttpStatus.CREATED);
}
...
요약
- DI를 통해 API 계층과 비즈니스 계층을 연동 시킬수 있었다.
- 두 계층에서 사용되는 클래스 (DTO와 엔티티)의 역할과 관심사를 분리해 주고 위해 Mapper가 사용된다.
- Mapper를 더 편하게 구현하도록 MapStruct라이브러리가 제공된다.
- 이를 통해 아래의 장점을 얻을 수 있다.
- 계층별 관심사의 분리
- DTO 클래스는 요청 데이터를 전달 받고 응답데이터를 보내는데 관심사를 집중
- 엔티티는 데이터 액세스 계층과 연동하여 비즈니스 로직의 결과로 생성된 데이터를 다루는데 관심사를 집중
- 코드 구성의 단순화
- DTO를 통한 유효성 검증 분리
- REST API 스펙의 독립성 확보
- 엔티티의 제공하고 싶지않은 부분을 제외한 일부분의 데이터만 DTO로 변환하여 제공가능
- 계층별 관심사의 분리