api를 설계할 때 중요한 것은 리소스와 행위를 분리하는 것이다. 따라서 URI는 리소스만 식별해야 하고, 그에 대한 처리는 메서드로 구분한다.

 

주요 HTTP 메서드 종류

1. GET

  • 리소스 조회
  • 서버에 전달하고 싶은 데이터는 쿼리를 통해서 전달.
    • ex) GET /search?q=hello&hl=ko
  • 메시지 바디를 통해 전달 가능. 하지만 지원하지 않는 곳이 많아서 권장하지 않음.

2. POST

  • 요청 데이터 처리
    • 주로 신규 리소스 등록, 프로세스 처리에 사용.
    • 응답값이 없기도 함.
    • 포괄적으로 정의되어 있기 때문에, 다른 메서드를 사용하기 애매하다 싶으면 사용해도 됨.
    • PATCH, PUT을 지원하지 않는 경우에도 사용 가능.
  • GET과 달리, 메시지 바디를 통해 요청 데이터 전달

3. PUT

  • 리소스가 있으면 대체, 리소스가 없으면 생성.
    • 요청 시 필드를 누락하면, null로 저장되는 것을 주의.
  • 클라이언트가 리소스 위치를 알고 URI를 지정
    • ex) PUT /members/100

4. PATCH

  • 리소스 부분 변경
  • PUT과 달리, 요청 시 필드를 누락하더라도 해당 필드의 값이 변경되지 않음.

5. DELETE

  • 리소스 제거

 

HTTP 메서드의 속성

1. 안전

  • 몇 번을 호출해도 리소스 변경이 없는 것을 의미한다.
  • GET, HEAD 등

2. 멱등

  • 몇 번을 호출해도 결과가 동일한 것을 의미한다.
    • 안전과 달리 변경이 발생할 수 있다.
  • 멱등 메서드
    • GET : 몇 번을 조회하든 변경이 없고 같은 결과가 조회된다.
    • PUT : 리소스를 대체하므로, 같은 요청을 여러번 해도 결과는 같다.
    • DELETE : 리소스를 삭제하므로, 같은 요청을 여러번 해도 삭제된 상태이다.
    • POST : 멱등이 아니다. 리소스를 등록하는 요청의 경우, 요청할 때마다 새로운 리소스 생성.
    • PATCH : 멱등이 아니다. 나이를 1씩 더하는 요청의 경우, 요청할 때마다 지속적으로 값이 증가.

3. 캐시 가능

  • 응답 결과를 캐시해서 사용해도 되는지에 대한 여부를 의미한다.
  • POST, PATCH도 캐시 가능하지만, 실제로는 GET, HEAD 정도만 사용.
    • GET은 URL만 캐시 키로 고려하면 되지만, POST, PATCH는 본문 내용까지 고려해야 하기 때문에 구현이 쉽지 않음.

 

1. 애그리거트 트랜잭션

그림 8.1

운영자는 고객의 배송 상태를 배송 시작으로 변경하고, 고객은 배송지를 변경하는 것이 동시에 발생하는 상황이다. 이때 요구 사항이 `배송이 시작되면 배송지 변경 불가`라면, 애그리거트의 일관성이 깨질 수가 있다.

 

이러한 문제를 해결하기 위해서는 트랜잭션 처리가 적용되어야 한다. 대표적인 처리 방식으로는 비관적 락(선점 잠금)과 낙관적 락(비선점 잠금)이 있다.

 

2. 비관적 락

그림 8.2

스레드 2는 스레드 1이 잠금을 해제한 뒤에 애그리거트에 접근할 수 있다. 이처럼 한 스레드의 애그리거트 사용이 끝날 때까지 다른 스레드의 접근을 막는 방식을, 비관적 락이라고 한다.

보통 DBMS의 for update와 같은 쿼리를 사용해서 행단위 잠금을 건다. JPA를 사용하면 보다 쉽게 구현할 수 있다.

 

애그리거트를 동시에 수정할 수 없도록 막기 때문에(상호 배제) 갱신 손실 문제가 발생하지 않는다. 다만 순환 대기, 비선점, 점유 대기를 만족하는 경우에는 데드락이 발생할 수 있는 점을 주의해야 한다.

이에 대한 해결 방법에는 최대 자원 대기 시간을 설정하는 방법이 있다. 이는 힌트를 사용하여 쉽게 구현할 수 있다. 그런데  DBMS에 따라 1) 쿼리별로 대기 시간을 지정하거나 2) 커넥션 단위로만 지정하는 경우로 나뉘니, 사전에 꼭 확인하자

 

3. 낙관적 락

그림 8.3

그림 8.2에서는 운영자와 고객이 변경을 동시에 수행하는 상황이었다. 하지만 그림 8.3에서는 운영자가 사전에 조회한 정보를 바탕으로 변경이 따로 발생하는 상황이다. 이는 비관적 락으로 해결할 수 없지만, 낙관적 락으로는 해결할 수 있다.

 

낙관적 락은 동시에 접근하는 것을 막는 방식이 아니라, 변경한 데이터를 실제 DBMS에 반영하는 시점메 변경 가능 여부를 확인하는 방식이다. 변경 가능 여부는 버전 관리 방식을 통해 구현할 수 있다.

 

그림 8.4

좀 더 원리를 살펴보자면, 사전에 조회한 애그리거트의 버전 값이 새로 조회한 버전 값과 같은 경우에만 데이터를 수정할 수 있다. 그리고 수정에 성공하면 버전 값을 1 증가시키는 방식이다.

 

강제 버전 증가

애그리거트는 여러 엔티티와 밸류로 구성되어 있다. 이때 루트 엔티티가 아닌 다른 엔티티의 값이 변경되더라도, 애그리거트 관점에서는 버전이 달라져야 하는 게 올바르다. 이 역시 JPA는 관련 기능을 지원한다.

 

4. 오프라인 비관적 락

사실 비관적 락을 사용해서 그림 8.3을 해결하는 방법이 있다.

그림 8.5

첫 번째 트랜잭션에서 오프라인 락을 걸고 마지막 트랜잭션에서 락을 해제하면 동시성 문제를 해소할 수 있다. 이러한 방식을 오프라인 비관적 락이라고 부른다.

 

락을 걸 때는 애그리거트에 lockId를 저장하고, 이후에 lockId를 사용하여 락을 해제하는 방식으로 구현할 수 있다. 사용자가 락을 걸기만 하고 해제하지 않을 수도 있으므로, 잠금 유효 시간을 가져야 함을 유의하자.

 

5. 비관적 락과 낙관적 락 

그러면 비관적 락과 낙관적 락을 언제 사용해야 할까? 결론부터 말하자면 낙관적 락은 많은 충돌이 예상되지 않을 때(낙관적인 상황) 사용하고, 비관적 락은 잦은 충돌이 예상될 때(비관적인 상황) 사용하면 된다.

 

갱신 충돌이 자주 발생하지 않는 경우에는 낙관적 락이 성능이 더 좋다. 비관적 락은 다른 트랜잭션이 접근하지 못하도록 락을 걸기 때문에 동시성이 떨어지기 때문이다. 하지만 반대되는 상황에서는 그렇지 않다.

충돌이 잦을수록 트랜잭션이 중단되어 롤백할 가능성이 높아진다. 하지만 롤백은 보류 중인 모든 변경 사항을 되돌려야 하므로 비용이 많이 드는 작업이다.

 

이러한 이유로 충돌이 자주 발생하는 경우에는 비관적 락이 더 적합할 수 있다.

 

참고자료

https://stackoverflow.com/questions/129329/optimistic-vs-pessimistic-locking

1. 여러 애그리거트가 필요한 기능

도메인 영역의 코드를 작성하다 보면, 한 애그리거트로 기능을 구현할 수 없을 때가 있다. 대표적인 예가 상품, 주문, 할인 쿠폰, 회원 애그리거트들이 관여하는 결제 금액 계산 로직이다.

 

이 상황에서는 어떤 애그리거트가 주체인지 쉽게 판단할 수 없다. 어찌어찌 한 도메인 안에 욱여 넣더라도, 자신의 책임 범위를 넘어서는 기능을 구현하기 때문에 다음의 단점이 존재한다. 1) 코드가 길어지고 2) 외부에 대한 의존이 높아지게 되며 3) 코드가 복잡하여 수정이 어렵다. 4) 게다가 애그리거트 범위를 넘어서는 도메인 개념이 애그리거트에 숨어들어 명시적으로 드러나지 않게 된다.

 

이에 대한 해결 방법으로는 별도 서비스로 구현하는 것이다.

 

2. 도메인 서비스

도메인 서비스는 도메인 영역에 위치한 도메인 로직을 표현할 때 사용한다. 주로 여러 애그리거트가 필요한 계산이나 외부 시스템 연동이 필요한 경우에 사용한다.

 

계산 로직과 도메인 서비스

결제 금액 계산처럼 한 애그리거트에 넣기 애매한 도메인 기능은, 도메인 서비스를 이용해서 도메인 개념을 명시적으로 드러낼 수 있다.

 

응용 서비스 vs 도메인 서비스

응용 서비스는 도메인 로직 없이, 그저 표현 영역과 도메인 영역을 연결하는 창구 역할을 수행한다. 따라서 응용 서비스는 애그리거트나 도메인 서비스의 로직을 실행하기만 할 뿐이다.

반면 도메인 서비스는 애그리거트의 상태를 변경하거나 상태 값을 계산하는 도메인 로직이 존재한다. 트랜잭션 처리와 같은 로직은 응용 로직이므로 응용 서비스에서 처리한다.

 

도메인 서비스 vs 애그리거트가 가진 기능들

애그리거트는 도메인 로직 뿐만 아니라 관련된 필드를 가지고 있다. 반면 도메인 서비스는 상태를 가지지 않고 기능만 가진다. 애그리거트의 상태값이 필요하다면, 응용 서비스의 도움을 받아 메서드 파라미터를 통해 주입받을 수 있다.

 

외부 시스템 연동과 도메인 서비스

전에 우리는 JPA 레포지터리 인터페이스는 도메인 영역에 포함시키고, 구현 클래스는 인프라 영역에 포함시켰다. 또한, 도메인 로직 관점에서 인터페이스를 작성했었다. 이와 비슷하다.

 

외부 시스템이나 타 도메인과의 연동은 도메인 서비스 포함시킬 수 있다. 가령 구현 부분이 HTTP 호출로 이루어져 있더라도 상관없다. 도메인 로직 관점에서 인터페이스를 작성한 뒤 도메인 서비스로 포함시키고, 구현 클래스를 인프라 영역으로 보내면 그만이다.

+ Recent posts