- Java 버전: 21
- Spring Boot 버전: 3.3.1
- 데이터베이스:
- H2 (개발 환경)
- MySQL (운영 환경)
- ORM: Spring Data JPA, QueryDSL
- API 문서화: SpringDoc OpenAPI
- 캐싱: Caffeine
- 모니터링: Spring Boot Actuator, Prometheus
- JPA와 QueryDSL을 이용한 데이터 접근
- Swagger를 이용한 API 문서화
- Caffeine을 이용한 캐싱 구현
- Actuator와 Prometheus를 이용한 애플리케이션 모니터링
지연쿼리 분석 및 캐싱전략
- 캐싱은 조회에서 주로 발생합니다.
- 그래서 User, Product, Order 조회시 캐싱을 적용했습니다(1차적 접근).
- 조회가아닌 업데이트 메서드에는 CacheEvict를 적용했습니다.
- 캐싱전략은 Cacheable을 사용했습니다. 제공된 파일들을 바탕으로 각 시나리오에서 발생하는 쿼리와 대량 트래픽 발생 시 지연이 발생할 수 있는 조회 쿼리를 분석해보겠습니다.
- ProductRepositoryImpl.getPopularProducts():
분석 결과:
- 복잡도: 높음 (조인, 그룹화, 정렬, 서브쿼리 포함)
- 예상 데이터 볼륨: 주문 테이블 100만 건, 상품 테이블 10만 건 가정
- 실행 계획 분석: 주문 테이블 full scan, 상품 테이블과의 해시 조인, 그룹화 및 정렬 작업
- 예상 성능 이슈: 데이터 증가에 따른 응답 시간 증가, 메모리 사용량 증가
- 개선 방안:
- 주문 테이블에 (orderDate, orderStatus) 복합 인덱스 추가
- 인기 상품 집계 결과를 별도 테이블로 관리하고 배치 작업으로 갱신
- OrderRepositoryImpl.getFinishedOrderWithDays():
분석 결과:
- 복잡도: 중간 (날짜 범위 조회, 조인 포함)
- 예상 데이터 볼륨: 주문 테이블 100만 건 가정
- 실행 계획 분석: 주문 테이블 범위 스캔, 사용자 테이블과의 조인
- 예상 성능 이슈: 조회 기간이 길어질수록 처리 시간 증가
- 개선 방안:
- (orderDate, orderStatus) 복합 인덱스 추가
- 페이징 처리 도입
- 주문 상태별 파티셔닝 고려
- UserRepositoryImpl.getAll():
분석 결과:
- 복잡도: 낮음 (단순 전체 조회)
- 예상 데이터 볼륨: 사용자 테이블 100만 건 가정
- 실행 계획 분석: 사용자 테이블 full scan
- 예상 성능 이슈: 데이터 증가에 따른 응답 시간 및 메모리 사용량 선형 증가
- 개선 방안:
- 사용 목적에 따라 필요한 컬럼만 조회하도록 최적화
- 읽기 전용 레플리카 활용 고려
- UserRepositoryImpl.getCouponByUser():
분석 결과:
- 복잡도: 중간 (조인 포함)
- 예상 데이터 볼륨: 사용자당 평균 10개의 쿠폰 가정
- 실행 계획 분석: 사용자 테이블과 쿠폰 테이블 조인, 인덱스 활용 가능
- 예상 성능 이슈: 사용자의 쿠폰 수가 많아질 경우 성능 저하 가능성
- 개선 방안:
- (userId, couponId) 복합 인덱스 추가
- 자주 조회되는 사용자-쿠폰 정보 캐싱
- 쿠폰 정보를 별도 테이블로 분리하여 관리 고려
- OrderRepositoryImpl.getOrders():
분석 결과:
- 복잡도: 중간 (조인 포함, IN 절 사용)
- 예상 데이터 볼륨: 주문 테이블 100만 건 가정
- 실행 계획 분석: 주문 ID에 대한 인덱스 스캔, 사용자 테이블과의 조인
- 예상 성능 이슈: 조회하는 주문 ID 수가 많아질 경우 성능 저하 가능성
- 개선 방안:
- 조회 가능한 주문 ID 수 제한 설정
- 대량 조회 시 페이징 처리 도입
- 주문 상세 정보 캐싱 고려
성능 최적화
- DB : Mysql 8.4
- 주요 성능향상이 야기되는 쿼리를 산정 후, 인덱스 사용 과 사용하지 않는것을 분석함으로 효율적인 데이터 판단.
- 주문데이터 1만건, 사용자데이터 2만건, 상품데이터 2만건, 쿠폰데이터 1만건씩 생성 인입.
- Order 엔티티에 대한 인덱싱 성능 검증
OrderStatus 에 대한 인덱스 검증 결과
type:
적용 전: ALL 적용 후: ref
테이블 전체 스캔(ALL)에서 인덱스를 사용한 참조(ref)로 변경되었음을 의미합니다.
possible_keys와 key:
적용 전: null 적용 후: idx_order_status
인덱스 적용 후 사용 가능한 키와 실제 사용된 키가 idx_order_status로 변경되었습니다.
key_len:
적용 전: null적용 후: 1023
인덱스 키의 길이가 지정되었습니다.
ref:
적용 전: null 적용 후: const
인덱스를 사용하여 상수 값과 비교하고 있음을 나타냅니다.
rows:
적용 전: 9492 적용 후: 2357
검사해야 할 예상 행의 수가 크게 감소했습니다.
filtered:
적용 전: 10.0 적용 후: 100.0
필터링된 행의 비율이 증가했습니다.
Extra:
적용 전: Using where 적용 후: null
WHERE 조건을 사용한 추가 필터링이 필요 없어졌습니다.
- User 엔티티에 대한 인덱싱 성능 검증
UserName 에 대한 인덱스 검증 결과
type:
적용 전: ALL 적용 후: ref
테이블 전체 스캔(ALL)에서 인덱스를 사용한 참조(ref)로 변경되었음을 의미합니다.
possible_keys와 key:
적용 전: null 적용 후: idx_username
인덱스 적용 후 사용 가능한 키와 실제 사용된 키가 idx_username으로 변경되었습니다.
key_len:
적용 전: null 적용 후: 1023
인덱스 키의 길이가 지정되었습니다.
ref:
적용 전: null 적용 후: const
인덱스를 사용하여 상수 값과 비교하고 있음을 나타냅니다.
rows:
적용 전: 19878 적용 후: 1
검사해야 할 예상 행의 수가 크게 감소했습니다.
filtered:
적용 전: 10.0 적용 후: 100.0
필터링된 행의 비율이 증가했습니다.
Extra:
적용 전: Using where 적용 후: null
WHERE 조건을 사용한 추가 필터링이 필요 없어졌습니다.
- Product 엔티티에 대한 인덱싱 성능 검증
Product의 name 에 대한 인덱스 검증 결과
1. type:
- 적용 전: ALL 적용 후: ref
인덱스 적용 후, 테이블 전체 스캔(ALL)에서 인덱스를 사용한 참조(ref)로 변경되었습니다.
2. possible_keys와 key:
- 적용 전: null 적용 후: idx_product_name
인덱스 적용 후, 사용 가능한 키와 실제 사용된 키가 idx_product_name으로 변경되었습니다.
3. key_len:
- 적용 전: null 적용 후: 1023
인덱스 키의 길이가 지정되었습니다. 이는 VARCHAR 필드의 최대 길이를 나타냅니다.
4. ref:
- 적용 전: null 적용 후: const
인덱스를 사용하여 상수 값과 비교하고 있음을 나타냅니다.
5. rows:
- 적용 전: 19418 적용 후: 1
검사해야 할 예상 행의 수가 크게 감소했습니다.
6. filtered:
- 적용 전: 10.0 적용 후: 100.0
필터링된 행의 비율이 증가했습니다.
Extra:
- 적용 전: Using where 적용 후: null
WHERE 조건을 사용한 추가 필터링이 필요 없어졌습니다.
설계 문서 작성:
a. 아키텍처 개요:
현재 Interface, Application, Domain, Infra 의 4계층 구조이며,
컨트롤러에서 요청받은 행위에대해서 Command는 변경을 요청하고, Query는 조회를 요청합니다.
컨트롤러에서 받은 요청에 대해서 동시성이 발생하는것이 예상되는 부분에 대해서 락을 획득한뒤에 트랜잭션이 시작하는 로직이 적용되어있습니다.
주문서비스는 사용자 및 상품에의해서 조합되므로 퍼사드 계층에 존재하고, 엔티티에대한 변경이나 조회가 일어나는 부분은 도메인에 존재합니다.
최근에 도메인에서 JPA기술을 알아야할 필요성이 없어 보여 도메인에서 리포지터리에서 어떤 정보를 불러올것인지 명세하고,
구현체와 JPA 인터페이스는 인프라 계층으로 이관했습니다.
b. 서비스 분리 계획:
현재 주문 서비스 자체는 상품서비스와 사용자 서비스에 의해서 조합되고있습니다.
주문서비스의 경우 트랜잭션이 주문 결제시에 주문확인 -> 재고 차감 -> 포인트 차감 -> 결제완료 -> 외부플랫폼 전송이므로
외부 서비스가 추가될수록 트랜잭션의 점유시간이 길어지고, 부분실패에 대한 보상 트랜잭션이 어려워집니다.
이에 따라서 주문서비스에서 여러 서비스를 조합 하는 경우에 대해서 서비스를 분리하고,
이벤트 기반으로 처리하는 방식을 고려하고 있습니다.
주요 이벤트 목록
1. 주문 결제
2. 주문 취소
1. 상품 재고 증가
2. 상품 재고 감소
1. 사용자 포인트 증가
2. 사용자 포인트 감소
이벤트 발행 및 구독 흐름
f. 분산 트랜잭션 처리:
이벤트 기반 비동기 트랜잭션
g. 확장성 및 성능:
부하 분산 전략
캐싱 전략
부하 테스트
- CPU: 4G
- 메모리: 8G
- 주문 결제 API
- 주문 생성 API
- 주문 조회 API
- 시스템의 최대 처리 용량 파악
- 응답 시간 및 처리량 측정
- 병목 지점 식별
- 200TPS 부하에 대한 안전성 보장
- 부하 테스트
- 내구성 테스트
- 스트레스 테스트
- 최고 부하 테스트 (스파이크 테스트)
-
초기 실행 시 데드락 발생
- 원인: 'User' 도메인 변경에 동시성 문제
- 해결: 락 적용 후 재시도
-
하이버네이트 관련 예외 발생
- IllegalStateException 및 NullPointerException 발생
- 원인: 하이버네이트의 스레드 안전성 문제
-
데드락 해결을 위한 설계 변경
- 주문->유저 참조 방식 변경
- 컨트롤러 스펙변경을 통해 유저번호, 주문번호를 통한 변경 방식 도입
-
서버 스펙 업그레이드
- 2CPU 4G에서 4CPU 8G로 변경
-
도커 컴포즈 도입
- 단일 도커파일 사용에서 도커 컴포즈로 전환
- 평균: 10.4ms
- 중앙값: 7.48ms
- 최소: 990μs
- 최대: 757.78ms
- 90번째 백분위수: 20.42ms
- 95번째 백분위수: 26.15ms
- 총 처리 요청: 458,616
- 초당 요청 처리량: 141.457939/s
- http_req_failed: 0.00%
- 최대 가상 사용자 수: 550명
- 테스트 실행 시간: 54분 2.1초
- 총 반복 횟수: 114,654회
-
성능 목표 달성:
- 목표했던 200 TPS에 근접한 141 TPS를 달성했습니다.
-
시스템 안정성:
- 오류율 0%
- 평균 응답 시간 10.4ms
-
확장성:
- 최대 550명의 동시 사용자를 처리할 수 있었습니다.
-
예상 일일 사용자 수:
- 약 30만~35만 명의 일일 활성 사용자(DAU)를 안정적으로 지원할 수 있을 것으로 추정됩니다.
- 이는 사용자당 평균 30개의 요청을 한다고 가정했을 때의 보수적인 추정치입니다.
- 테스트 시나리오 200TPS 이상으로 요청
-
성능 목표 달성:
- 목표했던 200 TPS를 크게 초과하여 580.496062 TPS 달성
-
시스템 안정성:
- 오류율 0%
- 평균 응답 시간 6.44ms
-
확장성:
- 최대 300명의 동시 사용자를 처리할 수 있었습니다.
- 이는 실제 운영 환경에서 대규모 트래픽을 효과적으로 감당할 수 있음을 의미합니다.
-
예상 일일 사용자 수:
- 약 125만 명의 일일 활성 사용자(DAU)를 안정적으로 지원할 수 있을 것으로 추정됩니다.
- 이는 사용자당 평균 50개의 요청을 한다고 가정했을 때의 계산 결과입니다.
-
추가 성능 지표:
- 데이터 처리량: 수신 125 kB/s, 송신 89 kB/s
- 95번째 백분위수 응답 시간: 8.36ms
가상 장애대응
현재 테스트상 장애가 발생하지 않으므로, 가상 장애대응 문서를 작성합니다.
- 모니터링 시스템 구축: Prometheus와 Grafana를 활용하여 실시간 시스템 모니터링 구축
- 알림 설정: CPU 사용률, 메모리 사용량, 응답 시간 등 주요 지표에 대한 임계값 설정 및 알림 구성
- 내부 공지: Slack 채널을 통한 개발팀 및 운영팀 즉시 알림
- 외부 공지: 사용자 대상 공지 페이지 및 푸시 알림 시스템 구축
- 공지 템플릿: 장애 유형별 표준화된 공지 템플릿 준비
- 에스컬레이션 프로세스: 장애 심각도에 따른 단계별 보고 체계 수립
- 1단계: 당직 엔지니어 또는 운영팀
- 2단계: 서비스 담당 개발팀
- 3단계: 경영진 또는 상급 조직
- 에스컬레이션 기준: 시간기반, 영향도 기반, 복잡도 기반
- 비상 연락망: 주요 담당자 및 의사결정권자 연락처 목록 관리
- 상황 공유 플랫폼: Jira 또는 전용 대시보드를 통한 실시간 상황 공유
- 롤백 절차:
- 이전 안정 버전으로의 신속한 롤백 프로세스 문서화
- 데이터베이스 롤백 절차 및 데이터 정합성 확인 방법 정립
- 핫픽스 배포:
- CI/CD 파이프라인을 통한 신속한 핫픽스 배포 체계 구축
- 코드 리뷰 및 테스트 간소화 프로세스 마련
- 사후 분석 회의(Post-mortem): 장애 원인, 대응 과정, 개선점 논의
- 재발 방지 대책: 기술적/프로세스적 개선 사항 도출 및 이행 계획 수립
- 문서화: 장애 보고서 작성 및 지식베이스 구축
- KPI 설정: MTTR(평균 복구 시간), MTBF(평균 장애 간격) 등 주요 지표 선정\
- MTTR: 장애 발생부터 복구까지 걸리는 평균 시간
- MTBF: 장애 발생 간격의 평균 시간
- 트렌드 분석: 장애 유형, 빈도, 영향도 등에 대한 정기적인 분석 실시
- 성과 측정: 장애 대응 프로세스 개선에 따른 효과 측정 및 보고
- 정기적인 모의 훈련: 분기별 장애 대응 시뮬레이션 실시
- 역할 교육: 팀원별 역할 및 책임에 대한 정기적인 교육 진행
- 대내외 소통 채널: 고객 지원센터, 소셜 미디어, 이메일 등 다양한 채널 활용
- 투명성 확보: 장애 상황 및 복구 진행 상황에 대한 투명한 공개 원칙 수립
- 장애 개요
- 장애 발생 일시: 2023년 5월 15일 14:30 KST
- 장애 종료 일시: 2023년 5월 15일 16:45 KST
- 장애 지속 시간: 2시간 15분
- 영향 범위: 전체 사용자의 결제 서비스 불가
- 장애 유형: 데이터베이스 연결 오류
- 장애 탐지
- 탐지 방법: Grafana 모니터링 알림
- 최초 탐지 시간: 2023년 5월 15일 14:32 KST
- 탐지 내용: 데이터베이스 연결 시도 실패 로그 급증
- 서킷브레이커: Reslience4J를 통한 서킷브레이커 동작 확인
- 슬로우 쿼리: my.cnf 활성화를 통해 Slow Query Log를 통한 쿼리 성능 저하 확인
- 장애 전파
- 내부 공유: Slack #장애대응 채널에 즉시 공유
- 고객 공지: 웹사이트 및 모바일 앱에 장애 안내 배너 게시
- 대응 조치
- 14:35 - 데이터베이스 서버 상태 확인
- 14:40 - 데이터베이스 연결 풀 재설정 시도
- 15:00 - 데이터베이스 서버 재시작
- 15:30 - 백업 데이터베이스로 전환
- 16:30 - 서비스 정상화 확인
- 원인 분석
- 주 데이터베이스 서버의 메모리 누수로 인한 연결 처리 불가
- 연결 풀 관리 로직의 버그로 인한 연결 해제 실패
- 재발 방지 대책
- 데이터베이스 서버 메모리 모니터링 강화
- 연결 풀 관리 로직 개선 및 테스트 강화
- 자동 장애 복구 시스템 구축 계획 수립
- 후속 조치
- 고객 대상 사과문 발송 및 보상 정책 수립
- 개발팀 대상 장애 원인 및 대응 과정 공유 세션 진행
- 운영 프로세스 개선을 위한 태스크포스 구성
- 교훈 및 개선점
- 백업 시스템으로의 전환 시간 단축 필요
- 주요 시스템 컴포넌트에 대한 정기적인 스트레스 테스트 도입
- 장애 상황 시뮬레이션 훈련 정례화
이벤트 기반 아키텍처
저번주, 이벤트 기반 아키텍처를 준비하며, 기존 서비스와 이벤트 기반으로 전환하면서 발생했던 체크포인트들을 중점으로
정리했습니다.
기존 이커머스 서비스는 모놀리스 아키텍처로 구성되어 있었습니다.
이커머스 프로젝트는 다음과 같이 발전했었습니다.
1주차 서비스 시나리오를 선택하면서, 시나리오 요구사항 분석을 하면서 작업계획을 세웠습니다.
2주차 API 설계를 하면서 API 명세를 작성하고 그에따라 비즈니스 요구사항을 구현했었습니다.
-> 이때 시퀀스 다이어그램 , 플로우차트, 유스케이스에 대한 이해를 마쳤습니다.
3주차 인바운드 및 아웃바운드에 대해서 필터 및 인터셉터를 고려하며, 부가로직에 대해서 고려했습니다.
-----------------------------------------
4주차 서비스에 발생할수있는 도메인의 변화를 고려하며, 락과 트랜잭션을 통해 처리를 개선했습니다.
5주차 캐싱을 통해 조회 I/O 에대한 성능개선을 하며, 캐시의 유효성을 검증했습니다.
6주차 서비스의 성능을 개선하기위해 Mysql의 Explain 기능을 통해 인덱스 유효 여부를 검증했습니다.
패키지 구조는 전반적으로 신경을 잘 쓰지않았었습니다. 단지 도메인은 지켜져야할 최후의 보루라고 생각했었습니다.
하지만 테스트를 함에있어서 JPArepository를 직접적으로 의존하면서 기술 명세에 대한 의존성이 서비스에 노출되는 문제가 있었고,
서비스에서 필요한 기능에 대한 명세를 가짐으로서 의존성을 분리할수있었고, 이게 나중에 DIP 원칙이었다는 사실을 알게되었습니다.
또 항해플러스를 하면서 가볍게 봤었던 유비쿼터스 랭귀지에 대해서 고민하지 않은 상태로 커맨드패턴을 사용해 개발을 진행했었습니다.
4L에 대해서도 정형화되어있다 판단해 어느정도 반감을 가지고있었고, 직접 쪼개고 붙이면서 알아가기로 결정했습니다.
메서드에 락을 붙이고 트랜잭션을 적용함으로서 도메인의 변화에 대해서 동시성을 제어할수있었지만, 다른 메서드에 비해 이질감이 느껴졌었습니다.
한 클래스에 이질적인 메서드가 많을수록 클래스의 책임이 커지고, 전혀 객체지향적인 코드가 아니라는 느낌이 들었습니다.
그러고 다시 4L 을 보고나서 외부 의존성이나 다른 도메인들의 조합이 일어나는것들을 어플리케이션 계층으로 올렸습니다.
유스케이스를 사용해서 조합했던것들이 결국에는 서비스 코드들의 결합도를 낮추기위해서였던것들과, 과한 어플리케이션의 비대를 방지하는데 도움이 되었습니다.
어플리케이션 계층의 특징상 다른 도메인들과의 조합은 기존 도메인의 조회와 변경의 조합이라는 점을 알게되어,
기존 서비스를 전부 도메인별로 조회와 변경부분을 나눠보았습니다.
진행하며 오히려 도메인의 변경에 대해서 오히려 조회와 변경이 분리된다고해서 도메인의 행위가 딱, 맞아떨어지지않는다는 점이 특이했습니다.
또 멘토링을 하면서 배웠던것이긴 했지만, 4L로 올리고나니 도메인객체를 바로 반환하는것이 특이하게 느껴져 DTO 객체, Info 객체를 만들었습니다.
이로인해 도메인의 변경에 대해서는 도메인 객체를 변경하고, 조회에 대해서는 DTO 객체를 변경하면서 도메인의 변경에 대한 영향을 최소화할수있었습니다.
물론 JPA 엔티티가 도메인이라고 생각하지만, JPA 엔티티는 컬렉션 객체에 있어서 제약을 많이 느껴,
JPA 엔티티를 단지 엔티티와 읽기 모델, 그리고 쓰기모델로 나뉘어서 사용하면서, JPA 엔티티의 제약을 최소화할수있었습니다.
하지만 실질적으로 읽기모델은 거의 사용되지 않으면서, 읽기모델은 단순 DTO와 차이가 없다는 것을 알게되었고,
이로인해 어떻게보면 도메인 객체 자체가 데이터베이스 엔티티 와 매핑된 JPA 엔티티에대한 쓰기모델이라고 느꼈습니다.
이로인해 크게 보면, 어플리케이션은 도메인과 도메인을 변경하는 작업 두가지로 나눠지고 변경하는 작업은 도메인을 확인하기전까지는,
독립적이라고 생각하게되었습니다.
발제 내용상으로 트랜잭션이 외부영향을 많이 안받을수록, 또 변경이 일어나는 부분을 최소화할수록 좋다는 격언을 들어 트랜잭션의
독립성을 어떻게 보장해줘야할까 생각이들어 이벤트를 고민하기 시작했습니다.
이벤트기반 아키텍처의 주요 개념
이벤트: 시스템내에서 주요한 변화나 사건을 의미합니다.
이벤트 생산자, 이벤트 소비자: 이벤트를 생성하고 소비하는 시스템입니다.
이벤트 브로커: 이벤트를 중개하는 시스템입니다.
비동기 통신: 서비스간 직접적인 의존성을 낮춥니다.
이벤트 기반 아키텍처는 마이크로서비스와 같은가?
- 이벤트 기반 아키텍처와 밀접한 관련이 있지만, 마이크로서비스는 꼭 이벤트기반 아키텍처를 따르지 않아도 됩니다.
- 이른바 상호작용의 방법의 차이죠. 마이크로서비스는 서비스간의 상호작용을 HTTP, REST API를 통해 직접적으로 통신합니다.
- 이벤트 기반 아키텍처는 서비스간의 상호작용을 이벤트를 통해 간접적으로 통신합니다.
도메인 주도 설계의 도메인 이벤트: 비즈니스 도메인내에서의 주요한 변화, 단일 바운디드 컨텍스트의 변화입니다.
이벤트 기반 아키텍처의 이벤트: 시스템 전반의 통신과 상호작용을 위한 이벤트입니다.
바운디드 컨텍스트 : 시스템간의 논리적 경계를 가진 컨텍스트입니다.
도메인과 시스템의 관계는 어떻게 되는가?
- 시스템: 상호작용하는 요소들의 집합입니다.
- 도메인: 어플리케이션이 대상으로 하는 주제영역입니다.
- 도메인과 시스템의 관계: 도메인은 비즈니스 관점에서 접근하고, 시스템은 기술적인 관점에서 접근합니다.
1. 기존 도메인에서의 에서의 주요한 상태변화들을 식별해야합니다.
2. 상태변화를 담당하는 비즈니스 로직을 직접적인 실행에서 이벤트를 발행하고, 이벤트를 수신하는 방식으로 변경해야합니다.
3. 이벤트를 발행하고 수신하는 방식으로 변경할때, 서비스 특성에 맞는 메시지브로커를 선택해야합니다.
1,2,3 을 진행하면서 어플리케이션 계층에서 상당히 고민해볼법한 문제가 하나 있습니다.
어플리케이션에서는 특정 도메인의 조회를 통해 변경을 이벤트를 발행하는데, 조회도 이벤트로 발행해야할까요?
조회를 이벤트로 발행했을때의
장점: 모든 요청을 이벤트로 처리할수있어, 도메인의 변경에 대한 의존성을 최소화할수있습니다.
단점: 비동기 처리로인해 데이터의 일관성을 유지하기가어려워서, 최종일관성(eventual consistency)에 문제가 발생할 수있습니다.
그러면 최종일관성은 무엇일까요?
- 이벤트의 순서 또는 시간에 따라 데이터의 일관성이 유지되지 않지만 최종적으로는 일관성이 유지되는것을 말합니다.
- 이벤트 기반 아키텍처에서는 최종일관성을 유지하기 위해 이벤트의 순서를 보장하는 방법을 고민해야합니다.
이벤트의 순서를 보장하는 방법은 무엇일까요?
1. 타임스탬프를 활용하기
2. 이벤트 버전을 활용하기
3. 상태머신을 활용하기
4. 이벤트소싱 패턴을 활용하기
이정도면 이벤트 기반 아키텍처로의 전환을 위한 기본적인 고민들을 해볼수있을것같습니다.
추가적으로 고려해야할것들은 무엇이있을까요?
1. 이벤트 스토리지: 이벤트를 저장함으로서 시점을 기록하고, 이벤트의 스냅샷을 생성할수 있습니다.
2. 이벤트 라우팅 및 필터링: 후술할 메시지 브로커와 같이 기술하겠습니다.
3. 오류 처리및 재시도 메커니즘
3-1. 데드레터큐
- 데드레터큐는 메시지 브로커에 전달되지 못한 메시지를 보관하는 큐입니다. 재시도 정책과 함께 사용되어 처리되지 않은 메시지를 저장할수있습니다.
3-2. 재시도 메커니즘
- 재시도 메커니즘은 메시지를 처리하는 과정에서 오류가 발생했을때, 메시지를 다시 처리할수있는 방법을 제공합니다.
3-2-1. 지수 백오프
- 지수 백오프는 재시도 횟수를 지수적으로 증가시키는 방법입니다. 재시도 횟수가 증가할수록 재시도 간격이 증가합니다.
3-2-2. 랜덤 지터
- 랜덤 지터는 재시도 간격을 랜덤하게 설정하는 방법입니다. 재시도 간격을 랜덤하게 설정함으로서, 재시도 메시지의 충돌을 방지할수있습니다.
더도말고 덜도말고, 이벤트 기반 아키텍처에서는 이벤트 발행의 원자성을 보장하는것이 중요합니다.
트랜잭셔널 아웃박스 패턴은 이벤트 발행을 비동기로 트랜잭션과 분리하는 패턴입니다.
메시지브로커의 일시적인 장애를 판단해서 이벤트 발행을 실행해서 메시지의 멱등성을 보장합니다.
기본적인 트랜잭셔널 아웃박스 패턴과 일반적인 이벤트 발행의 순서의 차이는 다음과 같습니다.
1. 일반적인 패턴
- 이벤트 발행을 요청합니다.
- 이벤트 발행이 성공하면, 트랜잭션을 커밋합니다.
2. 트랜잭셔널 아웃박스 패턴
- 이벤트 발행을 아웃박스에 저장합니다.
- 아웃박스에서는 비동기로 이벤트 발행을 실행합니다.
- 이벤트 발행이 실패하면, 재시도 로직을 통해 이벤트 발행을 재시도합니다.
- 성공한 이벤트 발행은 아웃박스에서 삭제합니다.
트랜잭셔널 아웃박스 패턴을 적용할때 고려해야할것은 무엇일까요?
1. 이벤트 발행의 원자성을 보장해야합니다.
- 재시도 로직이 필요합니다.
2. 아웃박스 테이블은 장기보관을 목적으로 하지않습니다.
- 메시지브로커를 위한 단기적인 메시지 큐의 역할을합니다.
반대로 이벤트 소싱 패턴은 이벤트를 저장함으로서 시점을 기록하고, 이벤트의 스냅샷을 생성할수있습니다.
이벤트를 순서를 통해 기록해서 발행한다는점에서 트랜잭셔널 아웃박스와의 모습은 굉장히 비슷합니다.
하지만 이벤트 소싱은 이벤트 전부를 이벤트 스토어에 기록함으로서 시점을 기록하고, 이벤트의 스냅샷을 생성하는게 주 목적입니다.
그러한 복원능력을 주요 쟁점으로 삼는걸 '재생성(Replay)' 이라고 부르기에, 재생성에 대한 간단한 이해도 필요합니다.
이벤트 소싱의 과정
1. 이벤트 발행을 요청합니다.
2. 이벤트 발행 요청을 이벤트 스토어에 저장합니다.
3. 이벤트 스토어에서 이벤트를 읽어와서 이벤트를 발행합니다.
4. 다만 완료된 이벤트 스토어는 삭제하지않고, 재생성을 위해 보관합니다.
재생성이란?
- 저장된 이벤트를 시간 순서대로 실행하여 특정 시점의 상태를 온전하게 복원하는것을 의미합니다.
그렇기에 이벤트 소싱 패턴은 트랜잭셔널 아웃박스 패턴과 다르게, 이벤트를 전반적으로 재생성이 필요할 경우에 구현됩니다.
재생성이 필요한 경우: 금융감사시스템, 버전관리 시스템
재생성이 필요하지 않은 경우: 실시간 스트리밍 서비스
사가(Saga)와 2PC(Two-Phase Commit)은 분산 트랜잭션 처리의 일관성을 유지하기 위한 패턴입니다.
사가와 2PC의 특징, 그리고 과정
1. 사가
- 사가는 분산 트랜잭션을 여러개의 로컬 트랜잭션으로 분리합니다.
- 사가는 각 로컬 트랜잭션을 순차적으로 실행하며, 각 로컬 트랜잭션의 결과를 이벤트로 발행합니다.
사가 - 코레오그래피
1. 트랜잭션 시작을 위한 이벤트 발행
2. 서비스가 로컬 트랜잭션을 실행하고 결과 이벤트를 발행
3. 다음 서비스가 이벤트를 수신하고 로컬 트랜잭션을 실행하고 결과 이벤트를 발행
4. 마지막 서비스가 이벤트를 수신하고 로컬 트랜잭션을 실행하고 트랜잭션 마무리
* 코레오그래피의 경우 각 서비스 내부에서 상태머신을 사용합니다.
사가 - 오케스트레이션
1. 사가 오케스트레이터가 트랜잭션 시작
2. 사가 오케스트레이터가 각 서비스에 로컬 트랜잭션을 실행하라는 명령
3. 각 서비스가 로컬 트랜잭션을 실행하고 결과 이벤트를 오케스트레이터에 전달
4. 사가 오케스트레이터가 모든 서비스의 결과를 확인하고 트랜잭션 마무리
* 오케스트레이션의 경우 사가 오케스트레이터에서 상태머신을 사용합니다.
2. 2PC
- 2PC는 분산 트랜잭션을 전체적으로 관리합니다.
- 2PC는 모든 로컬 트랜잭션을 준비상태로 변경하고, 모든 로컬 트랜잭션의 준비상태를 확인한 후, 커밋 또는 롤백을 결정합니다.
2PC의 과정
1. 코디네이터가 트랜잭션을 시작합니다.
2. 코디네이커가 모든 참여자에게 트랜잭션을 준비하라는 명령을 전달합니다.
3. 모든 참여자가 준비를 완료하면, 코디네이터가 커밋 또는 롤백을 결정합니다.
사가와 2PC의 차이점
사가는 각 로컬 트랜잭션이 커밋해서 단계별로 진행되는 것에 반해, 2PC는 한 참여자라도 아니오라고 응답하면 블로킹됩니다.
그렇기에 사가에 비해 2PC는 강한 일관성을 띄지만 참여자 수가 늘어날수록 가용성이 떨어집니다.
결국 방식은 다르지만 이 두개의 패턴은 모두 최종일관성을 위한 전략들입니다.
상태 머신은 시스템의 상태와 상태 간 전이를 모델링하는 추상적인 기계입니다.
주요 구성 요소:
1. 상태(State): 시스템이 특정 시점에 있을 수 있는 조건이나 상황
2. 전이(Transition): 한 상태에서 다른 상태로의 변화
3. 이벤트(Event): 전이를 트리거하는 외부 자극이나 조건
4. 액션(Action): 전이 중에 실행되는 동작
상태 머신의 특징:
- 현재 상태를 명확히 정의합니다.
- 이벤트에 따라 상태 간 전이 규칙을 정의합니다.
- 복잡한 시스템 동작을 단순화하여 모델링할 수 있습니다.
전이와 전파
전이: 한 상태에서 다른 상태로 변화하는것을 의미합니다.
전파: 상태가 한 지점에서 다른 지점으로 이동되는것을 의미합니다.
결국 상태머신이던 도메인의 변화로 일어난 이벤트던 시스템 내부의 상태가 전이하고,
다른 시스템으로 전파되는것을 이해하는것이 중요하다고 생각합니다.
메시지 브로커는 애플리케이션, 시스템 및 서비스 간에 메시지를 중개합니다.
주요 기능:
1. 메시지 라우팅: 발신자로부터 수신자로 메시지를 전달합니다.
2. 메시지 변환: 필요에 따라 메시지 형식을 변환합니다.
3. 메시지 저장: 수신자가 처리할 준비가 될 때까지 메시지를 저장합니다.
4. 신뢰성 있는 전달: 메시지 손실을 방지하고 최소 한 번 전달을 보장합니다.
이벤트 기반 아키텍처에서의 역할:
- 서비스 간 느슨한 결합 제공
- 비동기 통신 지원
- 시스템 확장성 향상
- 부하 분산 및 피크 처리






