728x90
깨달음의 발단: https://github.com/f-lab-edu/SSKA/pull/47#discussion_r1070973660

 

 

나는 지금까지 어떤 기능을 만들 때는 꼭 해당 기능 맞춤형 API 하나가 만들어져야 한다고 생각했다. 그런데 그게 아니었다.

기능이 있다고 그 기능의 API 하나를 설계 해야 하는 것이 아니라 그 로직을 설계 해야 하는 것이었다.

 

우리가 Restful 하게 API를 만드는 것은 리소스를 생성, 수정, 삭제, 조회하는 것이지 특정 기능만을 위한 api는 아니다

 

이번 SKKA 프로젝트를 하면서 "좌석 시간 변경" 기능을 만들고 API를 붙이려고 할 때, 해당 프로젝트에는 API가 총 4개가 있는데 ("좌석 예약", "좌석 이동", "좌석 이용 시간 변경", "좌석 예약 취소/퇴실") 기능별로 꼭 하나의 API를 설계 해야 한다고 생각하고 있었던 것이었다.

 

 

이렇게 하다 보니 코드의 반복이 계속 되었다. 그러다 보니 코드가 복잡해지며 서비스 로직에서 하나의 메소드에서 하나의 역할에 충실하지 못한 메소드가 나와야했다. 이것을 해소하기 위해서는 코드의 재사용이 필요했다. 예를 들어 "좌석 이동" 기능을 개발할 때 여러 로직들이 다른 API 에서 사용되는 서비스, 도메인 로직이 겹쳐, 이를 해결하기 위해  해당 기능("좌석 이동")을 개발할 때 "좌석 예약 기능"을 사용하면 풀리는 문제였다.

 

즉, 

 

이런식으로 "좌석 예약" 기능을 이용하여 다른 기능을 하는 API를 추가하면 코드의 반복을 줄일 수 있고 코드의 재사용이 가능해지며 하나의 역할에 충실할 수 있는 구조가 되었다.

728x90

REST API의 근간

과거에 다음, 한미르 같은 시절에는 WAS가 없었고 Nginx, apache 같은 Web Server 만 있었다. Web Server 는 정적인 파일을 serving 해준다. 사용자가 이미지 던져달라하면 던져주고 html 던져달라하면 던져주는 방식이다.

 

옛날에는 “im.html”을 던져달라하면 나의 개인정보가 나오는 html파일을 던져주곤 했다. 그러다 보니 그 당시 서버는 html 파일 서버였던 것이었다. 다시 말해 그 당시에는 우리가 그 html에 접근하기 위해서는 url에 “user/im.html” 를 요청하면 파일을 달라고 요청하는 것이었다. 그러면 html 웹 서버가 디렉토리에서 뒤져서 파일을 찾아서 주는 것이었다.

 

만약 그 위치에 파일이 존재하지 않으면 not found를 던져주는 것이고 url에 잘못 쳐서 “user/em.html” 을 요청하면 양식이 맞지않다고 400에러를 준다. 여기 개념에서 부터 Rest API가 나온 것이다.

 

 

REST API 설계

html파일도 정적 Resource를 호출하는 것이기 때문에 Rest API 는 어떻게 보면 Resource를 호출하는 것이다. 그러니까 우리가 Rest API 를 만들 때도 가상의 디렉토리가 있다고 생각하고 Rest API를 설계한다고 생각하면 쉽게 된다. 밑에를 예를 들면 파일 서버 안에 shops라는 폴더가 있고, 그 안에는 1번 id를 가진 애가 있고 얘는 “싸다김밥”이고, 2번 id를 가진 애가 있고 얘는 “메가커피”이다. 그러면 디렉토리 구조라고 쳤을 때 다음과 같은 구조를 가지게 된다.

 

shops
├── 1
│   ├── 싸다 김밥
└───└── 메가 커피

이런 구조에서 1번 디렉토리에 있는 정보를 들고오고 싶으면

GET + /shops/1

를 해서 들고오면 되는 것이다.

 

그리고 shops라는 디렉토리 내에 새로운 무언가를 만들려고 하면

POST + /shops

를 요청하게끔 하면 되는 것이다.

 

업데이트와 삭제도 마찬가지이다.

PUT + /shop/1

DELETE /shops/1

이런식으로 하면 된다.

 

 

REST API 행위 구분

Rest API의 특징중에 “유니폼 인터페이스”라고 있다. 유니폼이란 같은 옷을 입고 있다는 뜻이다. 이것을 제어 하는 것은 Http method(GET, POST, PUT, DELETE) 로 행위를 제어해야한다. 즉 End point는 동일한데 Http method를 가지고 행위를 다르게 한다는 뜻이다.

 

 

응용을 해보면

teams
├── 1
│   └── 법무팀
├── 2
│   └── 홍보팀
├── 3
└───└── 영업팀

이 있고

teams
└── 1
    └── memebers
           └── 홍길동

트리구조로 이렇게 "법무팀에서 일하는 홍길동 사원을 찾고싶을 때"

GET + /teams/1/members/1

요청으로 찾으면 “홍길동”이 결과로 나오는 것이다.

 

결론은 “파일” 처럼 보면 된다. 그러면 Rest API 설계가 굉장히 쉬워진다.
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