728x90

📌 API 요청이 들어오면 처리하는 과정에 대한 고찰

고찰의 시작: BaseEntity를 어느 패키지에 놓을지에 대한 고찰에서 시작되었다. ( 관련 PR )

보통 API를 만들 때, API 요청을 받고 Controller를 지나서 Service를 지나 Repository를 거치고 또 다시 Service로 가고 Controller로 나가는 구조이다. 여기서 요청할 때 처리하는 과정을 어떻게 처리할지에 따라 모든 설계가 달라진다. 해당 이슈는 SKKA 프로젝트에서 BaseEntity를 어느 패키지에 놓을지에 대한 고찰에서 나온 이슈이다. 어떻게 요청들을 처리할지 파헤쳐보자.




image




보통 (1)로도 충분하고, 나 또한 계속 1번으로 해 온 것같다. 간단하게 말하면 (1)의 구조로 하면 확장에 용이하지 않다. 그러면 (2)와 같이 세부적으로 나누면 무엇이 좋을지 한번 알아보자.



(CustomerWebRequestV1, CustomerWebResponseV1)


커스터머를 생성할 수 있는 API를 만든다고 생각하자. 그러면 클라이언트가 커스터머를 생성할 수 있는 파라메터를 줘 보자.

고객 객체는 다음과 같다.

 

class Customer { name, age, address }

 

POST /customers { name: “홍길동”, age: 30, address: “서울시 성동구” }


이 정보가 있으면 API에서 받아서 Customer 객체를 받아서 저장을 할 것이다.

그러면 Response로 (밑을 보자)

CustomerWebResponseV1 { id: 1 }

을 줄것이다.

 

이 Response는 결과가 도출 된 값인데 이 값은 어떻게 이용될까?


→ 사용자가 파라메터를 채워서 POST /customers API를 날리고 Response를 받아가는 것인데, 나중에 Customer를 식별할 수 있는 “id: 1” 정보를 클라이언트가 알아야 하는 정보인 것이다.


Response를 설계하는 방법은 방법론이 많다.

 

CustomerWebRequestV1와 CustomerWebResponseV1는 고객과의 한 약속이기 때문에 불변이다. 그러나 고객이 API를 통해 서버로 데이터를 보내면 서버가 데이터를 받고 그 데이터를 어떻게 안에서 대처하든 고객은 상관쓰지 않기 때문에 서버 개발자 마음대로 내부 로직을 처리한다. 이렇게 고객과의 약속은 절대 바뀌면 안된다. 신뢰성의 문제다. 다시 말해 내부 로직이 바뀌어서 사이드 이팩트로 API 호출 파라메터를 바꾸는 고객간의 신뢰를 저버리는 일이 발생하지 않게 하기 위해 CustomerWebRequestV1와 CustomerWebResponseV1는 불변이어야 하고 위와 같이 요청 프로세스를 (2)과 같이 나눈다.

 

이렇게 되면 프론트앤드와의 협업에도 좋아진다. Controller의 파라메터의 수정에 의해 갑자기 파라메터가 바뀔 일이 없어지기 때문에 한번 정해놓은 약속을 어길 일이 없어지기 때문이다.



(CustomerRequest, CustomerResponse)

 

이 단계에서는 고객 객체의 파라메터에서 age를 받지 않고 tel 을 받는다. 그러면 다음과 같은 파라메터를 가지면 되는 것이다.

class Customer { name, address, tel }

이 구조는 크게 다음과 같은 두 장점이 있다.

 

1. 유동성

결론적으로 (2)과 같은 구조로 하면, API에서 분리되어 Controller에서만 신경쓸 수 있는 부분이다. 그러니까 서버쪽으로 데이터를 받고 난 다음에는 개발자의 자유로 유동적으로 파라메터를 조정할 수 있는 것이다.

 

2. 통일성

CustomerWebRequestV1 → CustomerRequest 이런 식으로가 아닌 이 단계를 Controller로 통일한다고 가정할 때,

또 다른 API가 나오면 CustomerWebRequestV2, CustomerWebRequestV3, … 이런식으로 늘어날 것인데, 이러면

 

register(CustomerWebRequestV2 customer)
register(CustomerWebRequestV3 customer)

 

이런식으로 병렬적으로 늘어나는 단점이 있다.

 

반면, CustomerWebRequestV1 → CustomerRequest 이런 식으로 하면

 

register(CustomerRequest customer)
register(CustomerRequest customer)

 

이렇게 Service로 들어올 수 있는 데이터가 통일될 수 있고 로직도 통일될 수 있는 엄청난 장점이 있다.



(Customer, CustomerEntity)

 

Customer와 CustomerEntity의 차이는 무엇일까?

→ JPA를 사용하면 테이블과 그 테이블에 해당하는 객체를 매핑을 해야한다. CustomerEntity는 그 역할을 한다. 반면 Customer는 DB와 상관없는 객체의 역할을 한다.

이렇게 나누는 이유는 무엇일까?

(2) 구조는 전체적으로 다음과 같은 구조가 된다. (Customer 도메인에 한해서)

 

*이 프로젝트에서 헥사고날 아키텍처를 사용하기로 하였다.

adaptor
├── inbound (primary adapter)
│   ├── api
│   ├── CustomerController
│   ├── CustomerWebRequestV1   --> 기본적 spring validation @NotNull, @NotEmpty, @Digit, @JsonProperty("")
│   ├── CustomerWebResponseV1
├── outbound (secondary adapter)
│   ├── jpa
│   ├── BaseEntity (package-private)
│   ├── CustomerEntity (package-private)
│   ├── CustomerJpaReposity (implements JpaRepository<>) (package-private)
│   └── CustomerRepositoryAdapter (implements CustomerRepository) (package-private) @Bean
│
application
├── CustomerService
├── CustomerRequest
└── CustomerResponse
│
domain
├── Customer
└── CustomerRepository

이러한 구조는 어떠한 장점이 있을까?

이 구조의 장점을 알기 위해 코드를 살펴보자

class CustomerService {
    @Autowire
    private CustomerRepository repository; // DI (CustomerRepositoryAdapter)
    public CustomerResponse save(CustomerRequest request) {
       repository.save(request.toCustomer());
    }
}


// 저장소가 뭐가 될진몰라 근데 도메인을 저장해..
interface CustomerRepository {
    public Customer save(Customer customer);
}



import CustomerEntity;

class CustomerRepositoryAdapter implements CustomerRepository {

  private CustomerJpaReposity jpaRepository;

    @override
    public Customer save(Customer customer) {
       CustomerEntity entity = customer.toCustomerEntity();
       CustomerEntity entity = jpaRepository.save(entity);
       return entity.toCustomer();
    }
}

이와 같은 OCP에 위반되지 않는 코드가 탄생한다. CustomerService는 무엇으로 어디에 저장할지는 몰라도 그냥 일단 “저장”을 한다.

그리고 CustomerRepository가 PORT 역할을 하고 JPA, MyBatis, Redis 등등의 어뎁터들이 포트에 끼워지며 저장을 하는 OCP를 위반하지 않는 코드가 된다. 그리고 CustomerRepositoryAdapter에서 로직을 살펴보면 JPA와 테이블이 매핑되어있는 객체로 변환(Customer → CustomerEntity)하고 데이터베이스에 저장하는 방식이다. 그리고 다시 Customer 객체로 변환하여 반환하는 구조이다. CustomerRepositoryAdapter 와 통신하는 주체 자체는 Customer 클래스이다.

이렇게 하면 CustomerEntity 객체가 CustomerJpaReposity와 CustomerRepositoryAdapter 안에서만 사용하기 때문에 밖에 나가지 않아서 구분이 확실해진다. 즉 각 역할별로 모듈화가 잘된다. 모듈화가 잘 되면 Service에서는 Service에만 집중할 수 있게 되어 논리적인 코드를 짤 수 있게 된다. 역할의 분리가 잘 된다.

그리고 이 구조는 의존성 방향에 장점이 있다.

image

 

즉 의존성 방향이 이렇게 되면서 application은 adaptor의 상세 구현을 모르는 구조가 된다.

'난중(개발)일기' 카테고리의 다른 글

홈서버 만들기-2. 포트포워딩  (0) 2024.03.19
final 클래스 모킹  (0) 2023.07.12

+ Recent posts