대용량 트래픽 경험해보기(13/14) - MSA 적용기
이전 여행 플래너 서비스는 인프라 구축 및 부하에 대한 대응 구성을 통해 대용량 트래픽 처리를 진행했다.
초기 구조는 모놀로식 아크텍처를 목적으로 벡엔드 개발을 진행하면서 앞으로 서비스의 구조를 마이크로 서비스 아키텍처를 기반으로 진행해보려한다.
DDD(Domain-Driven Design : 도메인 주도 설계)
MSA 설계 방법론으로 도메인 주도 설계
가 있다. 도메인 주도 설계라는 것을 알게되면서 처음에는 어떻게 도메인을 설계해야지?
에 대해 생각해보았을 때 사용자 서비스와, 관리자용 서비스를 나누는 것과 같은 것일까?
라는 생각이 들었다.
주요 용어
- Domain - 유사한 업무 영역 또는 비즈니스 영역 (가장 상위 레벨의 개념)
- Aggregate - 하나 이상의 도메인(앤티티, 상태 값 객체 등)의 그룹화한 묶음
- Domain과 비슷하지만 다른 개념
- Aggregate의 접근은 Aggregate Root에서 가능
- Aggregate Root - Aggregate에 포함된 도메인 중 Aggregate를 대표하는 도메인
주요 단계
- 유비쿼터스 언어(Ubiquitous Language) - 개발자와 도메인 전문가가 공통으로 사용하는 언어
- 도메인 모델을 설명하는 데 사용되며, 팀 내 의사소통의 혼란을 방지하는 데 중요한 역할
- 유비쿼터스 언어를 통해 개발팀과 비즈니스 팀이 동일한 이해를 공유
- 이벤트 스토밍(Event Storming) - DDD의 원칙을 실질적으로 적용할 수 있는 실천법
- DDD의 핵심 개념 중 하나인 도메인 이벤트(Domain Event)를 중심으로 도메인을 시각화하고, 이를 통해 복잡한 시스템을 설계하는 방법
DDD 세부 설계
- 전략적 설계(Strategic Design)
- 모델과 바운디드 컨텍스트를 나누는 설계이며 협업을 위한 유비쿼터스 언어의 정리도 포함된다.
- 개념을 코드로 구현하기 전에 모델들 사이의 상호작용을 고려하여 인터페이스 등을 정의하거나 전체적인 흐름을 파악하고 경계를 나누며 여러 문제를 명확히 이해할 수 있도록 하는 과정이다.
- 전술적 설계(Tactical Design)
- 세부 아키텍처, 코드의 구현과 관련된 부분이며 전략적 설계(Strategic Design)에서 나뉜 경계와 모델 등을 유지보수성과 확장성을 고려하여 코드를 구성하는 등의 작업과정이다.
- 데이터 흐름 등을 고려한 세부적인 설계를 수행하고, 구현 과정에서 발생할 수 있는 다양한 문제들을 해결하고 코드의 유연성과 유지보수성을 보장하는 과정이다.
전략적 설계(Strategic Design)
기존 여행 플래너 서비스에서 서비스 확장을 생각하면서 숙소 조회, 예약, 결제
에 대한 전략적 설계를 유비쿼터스 언어 정의 → 이벤트 스토밍
순 진행했다.
유비쿼터스 언어(Ubiquitous Language) 정의
도메인 용어 정의
- 숙소 : 고객이 예약 가능한 항목
- 예약 : 숙소를 사용하기 위해 생성된 요청
- 가용성 : 고객이 숙소를 예약 가능성에 대한 여부
- 예약 취소 : 고객이 예약한 숙소를 취소하는 행위
- 결제 : 예약에 대한 금전적인 거래
- 결제 생성 : 결제 요청에 대해 생성된 이력
- 결제 승인 : 생성된 결제가 정상적으로 최종 완료된 상태
- 결제 취소 : 예약 취소 또는 서비스 문제로 인해 사용자에게 금액을 반환하는 절차
- 알림 : 고객에게 예약과 결제 정보를 전달하는 수단
- 알림 발송 : 고객에게 예약과 결제 정보를 전달하는 과정
- 예약자 : 숙소를 예약하는 서비스 사용자
- 시스템 : 예약/결제/알림 과정을 관리하는 내부 플랫폼
이벤트 스토밍(Event Storming)
DDD를 시작하기 위해서 가장먼저 이벤트 스토밍을 진행했다. 기존 여행 플래너 서비스에서 서비스 확장을 생각하면서 숙소 조회 및 예약
에 대해 진행하려한다.
이벤트 스토밍 주요사항
각 포스트잇은 , 커맨드, 정책, 애그리거트, 시스템, 정보, 핫스팟(Issue), 액터 순으로 지정
- 도메인 이벤트(Event) : 발생한 사건
- 커맨드(Command) : 도메인 이벤트를 트리거하는 명령
- 정책(Policy) : 이벤트 조건에 따라 진행되는 결정,
이벤트
가 발생할 때,커맨드
를 실행 - 애그리거트(Aggregate) : 도메인 이벤트와 커맨드가 처리하는 데이터, 상태가 변경되는 데이터
- 시스템(System) : 도메인 이벤트가 호출하거나 관계가 있는 레거시 또는 외부 시스템
- 정보(ReadModel) : 액터에게 제공되는 데이터, 결정을 내리는데 영향을 주는 정보
- 핫스팟(Issue) : 의문사항, 결정하기 힘든 사항
- 액터(Actor) : 개인 또는 조직의 역할
도메인 이벤트 정의
- 이벤트를 유사 비즈니스 흐름으로 나누고 발생 순서를 고려하여 배치한다.
- 비즈니스 흐름에서 발생한 이벤트에 초첨을 맞춰서 결정한다.
정책 도출
- 이벤트의 비즈니스 흐름에서 이벤트 이후에 진행되어야하는 조건을 결정한다.
커맨드, 액터, 외부 시스템 도출
- 커멘더는 액터와 이벤트 사이에 도출, 개발자 입장에서 구현해야할 API를 구분 가능하다.
- 서비스 사용자를 예약자로 구분하며, 액터는 예약자와 시스템으로 도출한다.
- 외부 시스템은 숙소, 결제, 알림 시스템 이용한다.
문장으로 액터, 커맨드, 이벤트, 정책, 시스템 검토
예약자(액터)
는숙소검색(커맨드)
하면숙소조회됨(이벤트)
가 발생하고 이어서 외부 시스템숙소(시스템)
이 실행된다.예약자(액터)
는방조회(커맨드)
하면방조회됨(이벤트)
가 발생하고 이어서 외부 시스템숙소(시스템)
이 실행된다.예약자(액터)
는예약(커맨드)
하면예약요청됨(이벤트)
가 발생하고,시스템(액터)
는예약생성(정책)
에 의해예약생성(커맨드)
하면예약생성됨(이벤트)
가 발생에 이어서 외부 시스템숙소(시스템)
이 실행되며, 다음으로시스템(액터)
는결제생성요청(정책)
에 의해결제생성(커맨드)
하면결제생성됨(이벤트)
에 이어서결제(시스템)
이 실행된다.시스템(액터)
는예약승인(정책)
에 의해예약승인(커맨드)
하면예약승인됨(이벤트)
발생에 이어서숙소(시스템)
이 실행되며, 다음으로시스템(액터)
는결제승인요청(정책)
에 의해결제승인(커맨드)
하면결제승인됨(이벤트)
가 발생하고 이어서결제(시스템)
이 실행되고 마지막으로시스템(액터)
는알림발송요청(정책)
에 의해알림발송요청(커맨드)
하면알림발송됨(이벤트)
가 발생에 이어서알림(시스템)
이 실행된다.예약자(액터)
는예약조회(커맨드)
하면예약조회됨(이벤트)
가 발생한다.예약자(액터)
는예약취소(커맨드)
하면예약취소요청됨(이벤트)
가 발생하고시스템(액터)
는취소가능확인(정책)
에 의해취소가능확인(커맨드)
하면취소가능확인됨(이벤트)
발생 후시스템(액터)
는예약취소(정책)
에 의해예약취소(커맨드)
하면예약취소됨(이벤트)
발생하고 이어서숙소(시스템)
이 실행되고시스템(액터)
는결제취소요청(정책)
에 의해결제취소(커맨드)
하면결제취소됨(이벤트)
가 발생하고 이어서결제(시스템)
이 실행되며 마지막으로시스템(액터)
는알림발송요청(정책)
에 의해알림발송요청(커맨드)
하면알림발송됨(이벤트)
가 발생에 이어서알림(시스템)
이 실행된다.
애그리거트 도출
- 애그리거트는 가장 작은 도메인 모델의 모듈 단위이다.
- 커맨드와 도메인 이벤트가 영향을 주는 데이터 요소이다.
- 개발자의 입장에서 보면, 도메인의 실체 개념을 표현하는 객체(엔티티)로 구현하게 될 대상이다.
- 커맨드와 도메인 이벤트 사이에 배치한다.
바운디드 컨텍스트(Bounded Context)
- 지금까지 도출한 결과를 하나의 바운디드 컨텍스트로 묶는다.
- 생성된 바운디드 컨텍스트들은 각각의 마이크로 서비스가 될 가능성이 있다.
컨택스트 매핑(Context Mapping)
- 구성요소들 간의 관계를 설정하는 단계이다.
- 고려하지 못했던 부분을 확인해야하며 전체 시스템을 검토한다.
전술적 설계(Tactical Design) 전 고려사항
- 숙소 시스템은 숙박시설별 객실과리시스템 혹은 PMS(Property Management System)을 사용하여 CMS(Channel Manage System)을 이용한다고 한다. CSM 시스템 사용에 대한 협업을 할 수 없기에 유사한 CSM 시스템을 별도로 개발을 해야한다.
- 결제 시스템(PG)의 경우 모든 PG 시스템을 연동하여 구현하기에 제한적인 부분이 있다. Toss API를 이용하여 간단한 결제 과정을 이용을 목적으로 진행하지만, Toss Core API 사용에 상점관리자 설정이 불가능하다는 점에서 결제 처리 API 응답 객체를 기반으로 도메인 설계를 진행한다.
- 기존의 여행 플래너 서비스에서 회원과 여행 플래너 각각의 도메인으로 분리한다.
- MSA 과정에서 기존 Nginx가 아닌 Spring Cloud Gateway를 이용해 클라이언트와 서비스간에 Gateway를 구현한다.
전술적 설계(Tactical Design)
MSA 설계 초안
MSA 설계 시 서비스가 적을 경우 각 서비스를 직접적으로 호출하여 사용해도 무방하다고 생각한다. 하지만, 지속적인 서비스가 생겨나고 트래픽이 많이 발생할 때 각 서비스를 직접적으로 호출하기에 관리가 복잡해지는 측면에서 Gateway 서버를 추가하였다.
API Gateway는 기존 Nginx과 비교했을 때 Gateway 기능에 최적
이며 필터링을 통한 인증처리
를 위해 Spring Cloud Gateway
를 이용하려 한다.
서비스(도메인)별 기능
도메인 | 기능 |
---|---|
인증(API Gateway) | 토큰 생성 |
토큰 및 회원 정보 저장 | |
토큰 재발급 | |
회원 | 회원가입 |
로그인 | |
회원조회 | |
예약 | 숙소 조회 |
방 조회 | |
예약 요청 | |
예약 승인 | |
예약 취소 | |
결제 | 결제 정보 생성 |
결제 승인 처리 | |
결제 취소 | |
결제 정보 조회 | |
알림 | 예약 알림 발송 |
결제 알림 발송 | |
여행 플래너 알림 발송 |
Spring Cloud Gateway
Spring Cloud Gateway의 주요 3가지 요소로 Route
, Predicate
, Filter
가 있으며, 클라이언트의 요청이 각 컴포넌트를 거쳐 서비스에 요청 및 응답이 진행된다.
- Route : Route는 고유 ID, URI, Predicate, Filter로 구성되어 있으며, Route를 통해 요청된 URI의 조건이 Predicate를 통과하여 매핑된 해당 경로로 매칭된다.
- Predicate : 주어진 요청이 주어진 조건을 충족하는지 검증하는 요소이며, 만약 Predicate에 매칭되지 않을 경우 자체적으로 404(Not Found)를 응답한다.
- Filter : 요청이나 응답에 대한 전/후 처리를 담당하며, Proxy Filter는 프록시 요청이 처리될 떄 수행되는 필터이다.
- Gateway Filter
- 요청 / 응답
- AddRequestHeader / AddResponseHeader : 요청 / 응답 헤더를 추가
- AddRequestParameter : 요청 시 파라미터를 추가
- RemoveRequestHeader / RemoveResponseHeader : 요청 / 응답 헤더를 삭제
- RemoveRequestParameter : 요청 시 파라미터를 삭제
- DedupeResponseHeader : 이름이 공백인 헤더 이름 목록을 포함할 수 있음
- MapRequestHeader : 새롭게 명명된 헤더를 만들고 기존에 들어온 요청에 명명된 헤더에서 값을 추출하여 넣음
- PrefixPath : 모든 요청의 경로에 접두사를 붙일 수 있음
- RewritePath : 특정 경로를 새 경로로 변환
- RewriteLocationResponseHeader : 응답 헤더의 값 중 Location을 수정
- RewriteResponseHeader : 응답 헤더 값을 수정
- RedirectTo : 300번대 HTTP 상태값에 대해 특정 URI로 리다이렉션할 수 있도록 설정
- SaveSession : Spring Session 사용 시 유용하며 전달된 호출을 만들기 전에 세션 상태가 저장되었는지 확인
- SecureHeaders : 각종 보안 관련 헤더를 추가
- SetPath : 요청 경로를 특정 경로로 변경, 템플릿 세그먼트 허용
- SetRequestHeader / SetResponseHeader : 요청 / 응답 헤더를 지정된 이름으로 대체함
- SetStatus : 특정 HTTP 상태로 설정
- StripPrefix : 요청에서 특정 수만큼 경로를 제거함
- Retry : 재시도해야 하는 횟수, 상태, 메서드, 예외, 백오프 등을 설정
- RequestSize : 요청 크기가 허용 가능한 한도보다 큰지 체크
- SetRequestHost : 기존 호스트 헤더를 지정된 값으로 대체할 수 있음
- 예외
- CircuitBreaker : Spring Cloud Circuit Breaker 연동, 게이트웨이 경로를 Circuit Breaker로 래핑함
- FallbackHeaders : FallbackHeaders에 전달된 요청의 헤더에 Circuit Breaker 실행 예외 세부 정보를 추가할 수 있음
- 네트워크
- PreserveHostHeader : HTTP 클라이언트가 결정한 호스트 헤더가 아닌 원래 호스트 헤더를 보내야하는지 결정하기 위해 라우팅 필터가 검사하는 요청 속성을 설정
- 부하 처리
- RequestRateLimiter : RateLimiter를 통해 현재 요청을 진행할 수 있는지 확인, 진행할 수 없을 경우 HTTP 429 - Too Many Requests 상태가 반환됨
- 기본 필터 : 모든 경로에 적용하기 위한 필터를 default-filters에 묶어서 지정
- 요청 / 응답
- Global Filter
- LoadBalancerClientFilter : URL에 lb 체계가 있는 경우 Spring Cloud를 사용하여 LoadBalancerClient 이름을 실제 호스트 및 포트로 확인하고 동일한 속성의 URI를 바꿈
- ReactiveLoadBalancerClientFilter
- WebClientHttpRoutingFilter
- NettyWriteResponseFilter
- RouteToRequestUrlFilter
- GatewayMetricFilter : Spring Boot Actuator와 연결, spring.cloud.gateway.metrics.enabled 속성 설정 시 작동됨. 다음 지표가 Actuator에 추가됨
- routeId : 경로 ID
- routeUri : API가 라우팅 되는 URI
- outcome
- status
- httpStatusCode
- httpMethod
- Gateway Filter
인증 API Gateway (GlobalFilter)
로그인 기능을 제외한 모든 요청의 경우 토큰이 필요하며 서비스 이용에 회원ID
가 필요하여 GlobalFilter
에서 공통으로 처리하였다.
인증 API의 경우 헤더의 토큰을 유효성 검증을 거치게 된다. 만약, 토큰이 만료된 경우 재발급 필터를 통해 토큰을 생성한다.
요청 헤더의 토큰 유효성 검증 → 토큰 만료 여부 확인 → Subject 추출 → Redis 회원 정보 확인 → RefreshToken으로 액세스 토큰 재생성 → 응답 헤더에 신규 토큰 발급
Route 설정
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
spring:
cloud:
gateway:
routes:
- id: PLACE-SERVICE # 숙소 조회 서비스
uri: http://localhost:8082
predicates:
- Path=/place/**
- Method=GET
filters:
- PrefixPath=/v1
- name: PrefixLoginTokenFilter # 토큰 유효성 검증 필터
- name: MemberParameterFilter # 회원 유효성 검증 파라미터 필터
- name: MemberResponseFilter # 회원 유효성 검증 응답처리 필터
- name: PostfixLoginTokenFilter # 토큰 발급 및 저장 필터
- id: TRAVELS-SERVICE # 여행 정보 서비스
uri: http://localhost:8080
predicates:
- Path=/travels/**
- Method=GET,POST,DELETE
filters:
- PrefixPath=/v2
- id: TRAVEL-REGIONS-SERVICE # 여행 지역 서비스
uri: http://localhost:8080
predicates:
- Path=/travel-regions/**
- Method=GET,POST,DELETE
filters:
- PrefixPath=/v2
- id: TRAVEL-PLANS-SERVICE # 여행 계획 서비스
uri: http://localhost:8080
predicates:
- Path=/travel-plans/**
- Method=GET,POST,DELETE
filters:
- PrefixPath=/v2
- id: RESERVATION-SERVICE # 예약 서비스
uri: http://localhost:8082
predicates:
- Path=/reservation/**
- Method=GET,POST,DELETE
filters:
- PrefixPath=/v1
애플리케이션 설정을 통해 위와 같이 Route를 구성할 수 있지만, 개인적으로 위와 같은 구성 방식의 경우 눈에 들어오지 않다는 생각이 있다. 이에, 클래스파일을 통해 직접적으로 구성하는 방식으로 변경했다.
회원 라우트
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
@Configuration
class MemberRouteConfig(
private val prefixLoginTokenFilter: PrefixLoginTokenFilter,
private val memberParameterFilter: MemberParameterFilter,
private val memberResponseFilter: MemberResponseFilter,
private val postfixLoginTokenFilter: PostfixLoginTokenFilter
) {
companion object {
const val URI = "http://localhost:8081"
const val PREFIX_PATH = "/v1"
}
@Bean
fun memberGatewayRoute(builder: RouteLocatorBuilder): RouteLocator {
return builder.routes()
.route("member-service") { r ->
r.path(LOGIN_PATH)
.and().method("POST")
.filters { f ->
f.prefixPath(PREFIX_PATH)
.filter(memberParameterFilter, 0)
.filter(prefixLoginTokenFilter, 1)
.filter(memberResponseFilter, 2)
.filter(postfixLoginTokenFilter, 3)
}
.uri(URI)
}
.build()
}
}
숙소 라우트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
class PlaceRouteConfig {
companion object {
const val URI = "http://localhost:8082"
const val PREFIX_PATH = "/v1"
}
@Bean
fun placeGatewayRoute(builder: RouteLocatorBuilder): RouteLocator {
return builder.routes()
.route("places-service") { r ->
r.path("/place/**")
.and().method("GET")
.filters { f ->
f.prefixPath(PREFIX_PATH)
}
.uri(URI)
}
.build()
}
}
여행 정보/지역/계획 라우트
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
@Configuration
class TravelPlannerRouteConfig {
companion object {
const val URI = "http://localhost:8080"
const val PREFIX_PATH = "/v2"
}
@Bean
fun travelPlannerGatewayRoute(builder: RouteLocatorBuilder): RouteLocator {
return builder.routes()
.route("travels-service") { r ->
r.path("/travels/**")
.and().method("GET","POST","DELETE")
.filters { f ->
f.prefixPath(PREFIX_PATH)
}
.uri(URI)
}
.route("travel-regions-service") { r ->
r.path("/travel-regions/**")
.and().method("GET","POST")
.filters { f ->
f.prefixPath(PREFIX_PATH)
}
.uri(URI)
}
.route("travel-plans-service") { r ->
r.path("/travel-plans/{travelId}/plans")
.and().method("GET","POST","DELETE")
.filters { f ->
f.prefixPath(PREFIX_PATH)
}
.uri(URI)
}
.build()
}
}
예약 라우트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
class ReservationRouteConfig {
companion object {
const val URI = "http://localhost:8082"
const val PREFIX_PATH = "/v1"
}
@Bean
fun reservationGatewayRoute(builder: RouteLocatorBuilder): RouteLocator {
return builder.routes()
.route("reservations-service") { r ->
r.path("/reservation/**")
.and().method("GET","POST","DELETE")
.filters { f ->
f.prefixPath(PREFIX_PATH)
}
.uri(URI)
}
.build()
}
}
임시 CMS(Channel Management System) 구현
CMS의 경우 협업 과정을 통해 시스템을 연동하여 구현해야한다. 이 과정이 현재 불가능하기에 아래와 같은 API가 있다는 생각으로 임시 CMS를 구현했으며 크롤링으로 약 3000건의 숙박 정보 저장했습니다.
숙소 조회
숙소 단건 조회
숙소 예약 요청
숙소 예약 승인
숙소 예약 취소
숙소 결제 완료
숙소 정보 크롤링
- Selenium 설치
- 크롬 버전 확인 후 크롬 드라이버 설치
https://googlechromelabs.github.io/chrome-for-testing/
- 크롬 드라이버 실행 실패시 아래 명령어 실행
1
$ xattr -d com.apple.quarantine chromedriver
- 크롬 버전 확인 후 크롬 드라이버 설치
chromedriver 환경 변수 설정
1 2
export CHROME_DRIVER_PATH=/usr/local/bin export PATH=${PATH}:$CHROME_DRIVER_PATH
chromedriver 버전 확인
1
$ chromedriver —version
1
2. 숙소 시스템 구현 : 채널관리시스템(CMS)과 관련된 시스템 사용의 경우 별도 협업이나 비용 지불을 통해 가능하기에 크롤링으로 큰 숙소 정보만 가져오고 나머지 객실은 랜덤으로 생성
객실 정보의 경우 봇 차단으로 랜덤으로 숙소별 10건씩 생성 -. 랜덤으로 예약가능 및 예약마감 설정으로 운영되고 있듯이 배치 프로그램 개발
트러블 슈팅
데이터 일관성 및 성능 이슈
위 3개 서비스를 구현하면서 각 서비스간의 데이터 일관성에 많은 초점을 맞춰야 했다고 생각했다. 처음 구현을 시작하면서 FeignClient를 통해 외부 서비스를 호출하는 방법을 선택했다.
FeignClient는 RestTemplate, WebClient을 이용하는 것에 비해 인터페이스만 정의하여 간결하다는 이점이 있다. 하지만, 하나의 외부 서비스까지는 유용하다고 생각하나 2개 이상의 경우 데이터 일관성 이슈가 발생했으며 로직은 아래와 같다.
사용자 예약 요청 → 예약 정보 생성 → CMS 예약 처리 → 임시 결제 정보 생성 → 클라이언트 결제 요청 완료 → FE에서 예약 완료 요청 → 예약 완료 → CMS 예약 승인 → 결제 승인 완료
데이터의 일관성 이슈가 발생할 수 있는 로직은 결제 승인 처리와 CMS 예약 완료이다.
CMS 예약 승인 과정에서도 내부 오류가 발생할 때 CMS에 예약 취소 혹은 철회 요청을 해야하고 결제 승인 처리 과정에서 결제 서비스 혹은 내부 서버 오류가 발생했을 때에도 CMS 예약 취소 혹은 철회 요청을 해야한다. 이 경우 CMS 예약 취소 혹은 철회에 대한 중보 코드가 발생하게 되며 내부 서비스에서 오류가 발생하는 시점에서 CMS의 API를 호출해야하는 이슈가 있었다.
게다가, 하나의 비즈니스 로직에서 위 API를 모두 구현한다면 Letency로 인해 목표한 응답시간을 만족하지 못할 가능성이 크다.
해결방안
이러한 데이터 일관성 문제와 성능 이슈를 해결하기 위해 결제 승인 처리 과정에 Apache Kafka
를 이용하였다.
예약 서비스 Producer / 결제 서비스 Consumer - 결제 승인 요청 및 처리
예약 서비스에서 결제 승인 요청 Producer 메세지 발송 → 결제 서비스 Consumer에서 결제 승인 처리 → 결제 서비스 Producer로 응답 결과 메세지 발송 → Offset Commit
결제 서비스 Producer / 예약 서비스 Consumer - 결제 승인 처리 응답
예약 서비스 Consumer에서 결제 승인 결과 응답 → 결과에 따라 CMS 예약 승인 처리 혹은 예약 취소(철회) 로직 진행 → Offset Commit
Kafka를 이용한 과정에서 중요하게 생각했던 부분은 Offset Commit
이다. 결제 서비스에서 결제 승인 처리 이후 Producer 응답 메세지까지 모두 정상 처리되어야 Offset Commit
을 한다. 만약, 결제 승인 혹은 메세지 발송 과정에서 오류가 발생하여 Commit이 되지 않더라도 Consumer는 Commit되지 않는 메세지를 다시 읽어 처리하게 된다.
예약 서비스 Consumer도 마찬가지이다. 결제 서비스에서 발송한 메세지가 Commit이 되지 않을 경우 특정 기준으로 Commit되지 않은 메세지를 다시 읽어 관련 프로세스를 진행하게 된다.
이렇게 Kafka를 이용해 결제 서비스에서의 처리 결과를 통해 CMS 예약 승인 혹은 예약 취소 프로세스도 정상적으로 이루어져 데이터의 일관성 이슈를 해결할 수 있었다.