SEB_BE_41

10.24 (서비스 계층)

Greatshine 2022. 10. 24. 23:54

 

 

https://www.quora.com/What-is-a-three-tiered-client-server-architecture

 

서비스 계층(Application layer)은 웹 애플리케이션의 비즈니스 요구 사항을 처리하는 핵심 계층이다.

 

API계층에서 전달받은 클라이언트의 요청 데이터를 서비스 계층이 받으면

그 데이터를 정해진 로직에 맞춰서 가공한 후,

API계층에게 다시 전달해주면서 API계층이 응답 데이터를 클라이언트에게 제공되게 된다. 

(데이터 액세스 계층을 제외하고 생각한 부분)

 


도메인 Entity 클래스

 

서비스 계층에서 데이터 액세스 계층과 연동하면서

비즈니스 로직을 처리하기 위해 필요한 데이터를 담는 역할을 하는 클래스이다.

(현재 학습에서는 데이터 액세스 계층과의 연동은 하지 않고 있다)

 

 

Mapper 클래스

 
 

DTO 클래스는 API계층에서 요청 데이터를 전달받고, 응답 데이터를 전송하는 것이 주목적이고,

Entity 클래스는 서비스 계층에서 데이터 액세스 계층과 연동하여

비즈니스 로직의 결과로 생성된 데이터를 다루는 것이 주목적이다.

따라서 사용되는 목적이 다르기 때문에 이를 분리해줘야 하고, 데이터를 처리하는 과정에서

DTO가 Entity로, Entity가 DTO로 변환되어야 하는 순간이 있다.

 

이를 처리해 줄 클래스가 바로 Mapper 클래스이다.

 


실제 예제

 

DTO 클래스는 따로 게시하지 않고, 컨트롤러와 Entity클래스,  Service클래스, Mapper 클래스만

작성하여 복습하도록 할 것이다.

보기 편하기 위해서 package와 import도 생략하도록 하겠다.

 

 

Entity 클래스 생성

 

1
2
3
4
5
6
7
8
9
10
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Coffee {
    private long coffeeId;
    private String korName;
    private String engName;
    private int price;
}
cs

 

@NoArgsConsturctor = 파라미터가 없는 기본 생성자를 자동으로 생성

@AllArgsConstructor = 모든 변수를 파라미터로 갖는 생성자를 자동으로 생성

 


Service 클래스

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Service
public class CoffeeService {
 
    public Coffee createCoffee(Coffee coffee) {
        return coffee;
    }
 
    public Coffee updateCoffee(Coffee coffee) {
        return coffee;
    }
 
    public Coffee findCoffee(long coffeeId) {
        return new Coffee(coffeeId, "아메리카노""Americano"2500);
    }
 
    public List<Coffee> findCoffees() {
        return List.of(
             new Coffee(1L, "아메리카노""Americano"2500),
             new Coffee(2L, "카라멜 라떼""Caramel Latte"5000));
    }
 
    public void deleteCoffee() {
 
    }
}
cs

 

@Service 애너테이션을 추가함으로써 MemberService 클래스는 Spring Bean으로 자동으로 등록된다.

(@Service 애너테이션 안에 @Component 포함)

 

※ 생성자가 하나 이상일 경우 DI를 적용하기 위한 생성자에 반드시 @Autowired를 붙여야 한다.

 

현재 데이터 액세스 계층과 연결되어 있는 것이 아니기 때문에 findCoffee(get)에는 stub 데이터를 제공한다.

(stub데이터 = 사전 정의된 데이터를 보유하고 테스트 중에 호출에 응답하는 데 사용하는 객체)

 


Mapper 클래스

 

1
2
3
4
5
6
7
8
@Mapper(componentModel = "spring")
public interface CoffeeMapper {
 
    Coffee coffeePostDtoToCoffee(CoffeePostDto coffeePostDto);
    Coffee coffeePatchDtoToCoffee(CoffeePatchDto coffeePatchDto);
    CoffeeResponseDto coffeeToCoffeeResponseDto(Coffee coffee);
 
}
cs

 

MapSturct

DTO 클래스처럼 Java Bean 규약을 지키는 객체들 간의 변환 기능을 제공하는 Mapper 구현 클래스를 자동으로

생성해주는 코드 자동 생성기이다.

@Mapper 인터페이스를 추가함으로써 인터페이스를 MapStruct의 매퍼 인터페이스로 정의할 수 있다.

@Mapper에 (componentModel = "spring")을 지정해주면 Spring의 Bean으로 등록된다.

 

인터페이스 내부에서 상황에 맞게 DTO를 Entity로, Entity를 DTO로 변환시키고 있다.

 

+ 구현 클래스

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Component
public class CoffeeMapperImpl implements CoffeeMapper {
    public CoffeeMapperImpl() {
    }
 
    public Coffee coffeePostDtoToCoffee(CoffeePostDto coffeePostDto) {
        if (coffeePostDto == null) {
            return null;
        } else {
            Coffee coffee = new Coffee();
            coffee.setKorName(coffeePostDto.getKorName());
            coffee.setEngName(coffeePostDto.getEngName());
            coffee.setPrice(coffeePostDto.getPrice());
            return coffee;
        }
    }
 
    public Coffee coffeePatchDtoToCoffee(CoffeePatchDto coffeePatchDto) {
        if (coffeePatchDto == null) {
            return null;
        } else {
            Coffee coffee = new Coffee();
            coffee.setCoffeeId(coffeePatchDto.getCoffeeId());
            coffee.setKorName(coffeePatchDto.getKorName());
            coffee.setEngName(coffeePatchDto.getEngName());
            coffee.setPrice(coffeePatchDto.getPrice());
            return coffee;
        }
    }
 
    public CoffeeResponseDto coffeeToCoffeeResponseDto(Coffee coffee) {
        if (coffee == null) {
            return null;
        } else {
            long coffeeId = 0L;
            String korName = null;
            String engName = null;
            int price = false;
            coffeeId = coffee.getCoffeeId();
            korName = coffee.getKorName();
            engName = coffee.getEngName();
            int price = coffee.getPrice();
            CoffeeResponseDto coffeeResponseDto = new CoffeeResponseDto(coffeeId, korName, engName, price);
            return coffeeResponseDto;
        }
    }
}
 
cs

 

MapStruct가 자동으로 생성해준 구현 클래스이다.

MapStruct가 없었으면 우리가 다 적어줬어야 할 코드이다.

이렇게 하나하나 Mapper 클래스를 정의해줄 필요가 없다는 점,

그로 인해서 많은 시간을 절약할 수 있고 에러 발생 측면에서도 큰 이점을 준다.

 


Controller

 

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/v5/coffees")
@Validated
public class CoffeeController {
 
    private final CoffeeService coffeeService;
    private final CoffeeMapper coffeeMapper;
 
    public CoffeeController(CoffeeService coffeeService, CoffeeMapper coffeeMapper) {
        this.coffeeService = coffeeService;
        this.coffeeMapper = coffeeMapper;
    }
cs

 

DI(의존성 주입)을 통해 CoffeeService와 CoffeeMapper의 객체를 받아오고 있다.

 

1
2
3
4
5
6
@PostMapping
    public ResponseEntity postCoffee(@Valid @RequestBody CoffeePostDto coffeePostDto) {
        Coffee coffee = coffeeService.createCoffee(coffeeMapper.coffeePostDtoToCoffee(coffeePostDto));
 
        return new ResponseEntity<>(coffeeMapper.coffeeToCoffeeResponseDto(coffee), HttpStatus.CREATED);
    }
cs

 

상당히 복잡해 보이고, 나도 헷갈렸지만 페어 분과의 학습을 통해 뚜렷하게 알게 되었다.

기억해야 될 것은 딱 두 가지이다.

 

DTO 객체는 클라이언트에게 요청 데이터를 받고, 응답 데이터를 주는 역할만 한다.

Entity 객체(Coffee 객체)는 DTO 객체가 받아온 데이터를 비즈니스 로직(CoffeeService)에서 처리하기 위해 존재한다.

 

이것을 기억하면서 코드를 살펴보면

CoffeePostDto 객체가 postCoffee에 들어오면, CoffeeMapper 클래스의 coffeePostDtoToCoffee를 통해

CoffeePostDto 객체가 Coffee 객체(Entity 객체)로 바뀐다.

이후 데이터가 담긴 Coffee 객체는 CoffeeService 클래스의 createCoffee (비즈니스 로직)을 거쳐 데이터가 가공된다.

가공된 데이터는 Entity 객체 즉, Coffee 객체에 담겨 있는데 이를 다시

CoffeeMapper 클래스의 coffeeToCoffeeResponseDto를 통해 CoffeePostDto 객체로 전환되면서

결국 응답 데이터는 DTO로 전달하게 되는 것이다.

(스스로 이해하기 위해 적었다.)

 


 

머릿속으로 정리한 것은

API 계층은 요청과 응답을 주목적으로 갖고 있는 계층으로 이 데이터를 다루기 위해 DTO 클래스가 존재하는 것이고,

Service 계층은 API 계층에서 전달 받은 데이터를 비즈니스 로직을 통해 가공하여 다시 API 계층으로 돌려주는 데,

이때 비즈니스 로직에서 API 계층에게 직접적으로 데이터를 받는 것이 아닌 Entity 클래스를 거쳐서 받는다 라는 것이다.

 

+ DTO 클래스와 Entity 클래스의 역할 분리가 필요한 이유

 

계층별 관심사의 분리

위에서 정리한 내용에 덧 붙여서,

하나의 클래스나 메서드 내에 여러 개의 기능들을 구현하고 잇는 것은 객체 지향 코드 관점에서도 리팩터링 대상이 된다.

 

코드 구성의 단순화

DTO 클래스에서 사용하는 유효성 검사 애너테이션이 Entity 클래스에서 사용된다면 JPA(데이터 액세스 기술)에서

사용하는 애너테이션과 뒤섞일 수 있으므로 혼란을 야기할 수 있다.

 

REST API 스펙의 독립성 확보

데이터 액세스 계층에서 전달받은 데이터로 채워진 Entity 클래스를 그대로 응답으로 전달하면 원치 않은 데이터까지

보내게 될 수 있다.

DTO 클래스를 사용하면 원하는 정보만 제공할 수 있게 된다.