보상 트랜잭션으로 분산 환경에서도 안전하게 환전하기
양방향 무료 환전! MSA 환경에서 서로 데이터베이스도 다르게 보고 있을 때 트랜잭션을 어떻게 지켜내야할까?
분산 트랜잭션
- 2PC
- Commit 가능 여부를 질의 (Voting)
- DB가 가능하다고 응답, 한 개라고 불가능하다고 응답한다면 이전 작업들을 모두 롤백한다.
- 강한 일관성, 낮은 가용성, 낮은 확장성
- 사가 패턴
- 각 서비스의 작은 트랜잭션들을 실행하면서 진행
- 특정 단계에서 실패하면 보상 트랜잭션 실행
- 높은 가용성, 높은 확장성, 중간 상태가 노출되고 보상 트랜잭션 구현을 필요로 한다.
- 코레오그래피 사가
- 중앙제어자 없이 메세지 브로커를 통해 이벤트를 교환하는 방식
- 단일장애지점이 없고 각 서비스가 느슨하게 결합됨
- 하지만 현재 진행중인 트랜잭션의 상태를 추적하거나 디버깅하기 어렵다.
- 오케스트레이션 사가
- 중간제어자가 직접 서비스에게 트랜잭션과 보상 트랜잭션을 명령하는 방식
- 중간제어자가 단일장애지점이 되고 모든 서비스들이 결합된다.
- 하지만 현재 진행중인 상태를 추적하기가 쉽다.
- 예외 핸들링
- 정상적인 실패 : 잔액 부족, 계좌해지, 거래제한
- 비정상적인 실패 : 서버에러, 타임아웃
- 배지 재처리
- 환전 지연 이벤트를 발행하지도 못하고 서버가 죽은 경우 배치를 통해 ‘출금 취소’ → ‘환전 실패’ 처리
- 결과적 일관성
- 출금 취소에 실패한다면 CDL에 적재하여 지정한 횟수만큼 재시도 한다.
- 그래도 실패한다면 개발자가 직접 개입하여 DL 서버를 통해 다시 메시지를 발행한다.
- DB: State Path
- 이벤트 조율을 오케스트레이터로 관리하기 때문에 매 상태를 확인하여 중간에 멈춘 환전을 알림으로 보낸다.
- 트랜잭셔널 메세징
- 입금 실패로 인한 환전 실패 처리와 출금 취소 메세지 발행은 항상 같이 이루어져야 한다!
- 즉, 로컬 트랜잭션 커밋과 메세지 발행이 원자적으로 이루어져야 한다.
- 트랜잭셔널 아웃박스 패턴으로 해결할 수 있지만, 토스는 프로듀서 데드 레터(PDL)로 해결한다.
- 출금취소 메시지 발행에 실패한다면 PDL에 메시지를 발행한다. DL서버가 PDL을 컨슘하여 브로커로 다시 메세지를 발행한다.
사가패턴에서는 중간 상태가 노출되기 때문에 출금부터 진행해야 한다.
입금,출금은 HTTP로 동기처리로 진행하기 때문에 타임아웃 구현이 필요하다.
만약 출금은 성공했지만 입금에 실패한 경우 출금 취소로 처리한다.
상대 계좌 서버나 네트워크 문제로 출금 결과 확인에 실패한다면 어떻게 처리해야 하나?
카프카 메세지를 지연 발송하여 상대 계좌 서버나 네트워크 문제가 회복되는 시간을 벌 수 있다. (30초 → 1분 ..)
트랜잭셔널 아웃박스 패턴과 PDL에 대한 질문
Q. 내부적으로 메시징 발행 재처리가 DL 중심으로 잡혀 있는지?
A. 토스뱅크는 모든 토픽에 대하여 기본적으로 PDL이 적용되어 있기에 따라서 추가적인 아웃박스 패턴 등을 적용하지 않았다.
Q. DL 서버에서 메시지 발행에 실패하면 어떻게 되는지?
A. DL 서버의 PDL 메시지 처리도 컨슈머로 동작하므로, CDL을 이용하여 재처리할 수 있다.
Q. PDL이 아웃박스 패턴보다 나은 점
A. 모든 서비스에서 일괄 적용하기 쉽다는 장점이 있다. 아웃박스 패턴은 서비스 변경과 아웃박스 데이터 저장을 트랜잭션으로 묶어 처리해야 하므로, 서비스 DB 내에 아웃박스 테이블이 존재해야한다.
토스뱅크에서는 수백개의 MSA 서버가 독립된 스키마(db)를 바라보고 있고, 이 스키마들은 수십개의 분리된 물리 서버 위에 그룹핑되어 존재한다.
또, 사용하는 DB 역시 Oracle, MySQL, Mongo 등으로 다양하기에, 모든 MSA에 일괄적으로 아웃박스 패턴을 적용하려면, DB종류, 물리서버, 스키마마다 아웃박스 테이블들을 만들어줘야하고, 테이블에서 메시지를 발행하는 어플리케이션도 각각 작성해야 한다. 반면 PDL은 모든 서버에서 동일한 메시지 브로커를 바라볼 수 있기 때문에, 라이브러리 형태로 제공되어 일괄 적용하는데 편리한 점이 있다.
Q. PDL 메시지 브로커도 고가용성이 보장되는지, 그렇다면 불필요한 인프라 비용이 생기는 것은 아닌지?
A. PDL 메시지 브로커로 서버에서 로그를 남길때 사용하는 로그 Kafka 클러스터를 사용하고 있다. 즉 고가용성을 위한 세팅이 적용되어 있으며 평상시에도 항상 사용하고 있기 때문에, PDL 메시지가 발행되지 않더라도 메시지 브로커에 문제가 생기면 그 즉시 알게 된다.
생각할 점
- 데이터 일관성을 아웃박스 패턴으로 해결할 수 있지만, 이 방법의 단점을 이해하고 PDL을 고려할 수 있음
선물하기 시스템의 상품 재고는 어떻게 관리되어질까?
- 총 4가지 시스템
- 서비스는 상품의 속성을 정의하고, 관리하는
상품시스템
- 상품 엔티티 : 상품명, 상품 이미지 등 보여지는 요소
- 판매상품 엔티티 : 상품이 어떤식 (판매기간)으로 판매될지를 결정
- 가격정책 엔티티 : 어떻게 판매될지 결정된 판매상품을 어떠한 가격에 얼만큼 팔지 (원금액, 할인금액, 인당재고, 총재고)
- 정의된 상품을 어느 카테고리에 매핑시켜 노출시킬지를 결정하는
전시시스템
- 상품을 상품권화 시키기 위해 고객님의 구매가 이루어질 수 있도록 하는
구매시스템
- 상품권을 음식주문시 사용할 수 있도록 하는
상품권 시스템
- 서비스는 상품의 속성을 정의하고, 관리하는
- 요구사항
- 상품의 권종별로 전체 재고수량과 인당 재고수량이 관리되어야 한다.
- 상품의 권종은 전체 재고량을 초과하여 판매되면 안된다.
- 판매가 한번 시작된 상품의 경우에는 재고량 수정이 가능하나 최초 설정된 재고량 이상을 설정할 수 없어야 한다.
- 상품권은 한 개씩 구매한다.
- 설계
- 전체 재고량의 경우 RDB에 저장하여 관리하고, 트랜잭션이 일어나는 재고사용량의 관리는 연산속도가 빠른 in-memory DB를 사용한다.
- 연산처리는 단일 스레드에서 처리하는 Redis를 이용하여 동시성 이슈를 해결한다.
- 레디스의 데이터 유실이 일어날 수 있으므로, 재고 사용량 데이터를 RDB에 싱크한다.
- 구매번호는 유니크한 값이고, Redis의 Set 자료구조는 중복을 허용하지 않기때문에 구매번호를 Set에 저장할 경우 SCARD 오퍼레이션을 통해 손쉽게 사용량을 가져올 수 있다.
- 재고량 증가 혹은 감소시점에 (삽입만 발생하는) 재고량 히스토리 정보를 누적한다.
구현
`{상품번호}:{권종}:stock:{타입}`
# 전체 재고 관리
S0630000RU:5000:stock:total
# 개인별 재고 관리
S0630000RU:5000:stock:{회원번호}
- S0630000RU: 상품번호 (오늘도 수고했어 상품)
- 5000: 권종 (5000원권)
- stock: 재고 구분자
- total: 전체 재고 / 201209320003: 회원번호
ADD 구매번호
재고 사용량 증가
구매 과정에 필요한 타 시스템의 API와 동기 방식으로 진행
이 플로우는 동기화된 메소드로 실행됨 (배민이지만 상품권 구매는 빈번하지 않다고 판단한듯)
- 상품 시스템 플로우
트랜잭션 시작
- 구매가 가능한 상태인지 (왜 트랜잭션 열고 유효성 검증하지?)
- 총 재고수량 & 인당 제한수량 확인
- 가능한 경우 Redis에 구매번호 ADD
- 총 재고수량 & 인당 재고 사용량 증가를 트랜잭션으로 묶음
- 가능한 경우 RDB에 재고 히스토리 테이블 INSERT
- 구매가 가능한 상태인지 (왜 트랜잭션 열고 유효성 검증하지?)
트랜잭션 커밋
- 재고를 차감한 이후에 구매가 실패한 경우
- 재고사용량을 차감시키라는 이벤트를
재고사용량 감소 큐
에 발행 트랜잭션 시작
- 레디스에서 전체 재고, 인당 재고 사용량을 구매번호로 제거
- 재고 히스토리 테이블에 재고량 감소
- 메세지 ack 처리
트랜잭션 커밋
- 재고사용량을 차감시키라는 이벤트를
생각할 점
- 재고 관리가 빡빡하지 않은 것 같음
- 상품권을 한 개씩 구매함
- 레디스 재고 사용량 변경에 대한 동시성 문제를 메소드 동기화로 해결했음
- 외부 서비스 연동을 동기로 처리함
- 만약
- 상품을 한 번에 여러 개 구매 가능하고
- 재고 변경에 대한 동시성 문제를 분산 환경에 대응해야 하고
- 외부 서비스 연동을 비동기로 처리해야 할만큼 요청이 많이 몰린다면?
- 재고를 확인하는 시점과 실제로 차감하는 시점 사이에 시간 차이가 존재하기 때문에, 두 요청이 거의 동시에 들어와서 같은 재고 상태를 확인한 후, 각각 재고를 차감하는 상황
- 조회와 업데이트가 분리된 두 개의 독립적인 Redis 명령어로 이루어짐
- Synchronized가 JVM 레벨에서만 동작하여 Redis 레벨의 원자성은 보장하지 못함
- Redis 업데이트는 성공했지만 RDB 트랜잭션이 실패하여 롤백되는 경우
- Redis의 MULTI/EXEC와 Spring의 @Transactional은 완전히 독립적인 트랜잭션 시스템이므로, RDB 예외 발생 시 Redis 트랜잭션은 롤백되지 않음
- EXEC를 실행하는 시점에 큐잉되어있던 명령어들 모두 실행
- 분산 환경에서 Synchronized가 의미가 없음
프롬프트 엔지니어링 논문
구글, 화제의 프롬프트 엔지니어링 논문 공개에서 소개하는 프롬프트 엔지니어링 논문을 읽어보았다.
- 해피 케이스, 에지 케이스 예제를 제공하여 견고한 출력을 생성할 수 있다.
- 시스템 프롬프트 : 언어 모델에 대한 전반적인 컨텍스트와 목적을 설정한다. (모델이 수행해야 하는 작업에 대한 '큰 그림'을 정의)
- 특정 요구 사항을 충족하는 출력을 생성하는데 유용하다. (코드 스니펫 생성 등)
- 문맥 프롬프트 : 현재 상황이나 세부 정보를 제공하여 질문의 뉘앙스를 이해하고 그에 따른 답변을 맞춤화하는데 도움된다.
- 역할 프롬프트 : 특정 캐릭터 또는 신원을 할당하여 관련 지식 및 핼동과 일치하는 응답을 생성하는데 도움된다.
- 스텝백 프롬프트 : 당면한 문제와 관련된 일반적인 문제를 먼저 고려하도록 유도한 다음, 그 일반적인 질문에 대한 답을 특정 작업에 대한 후속 프롬프트에 제공함으로써 성능을 향상시키는 기법이다.
- Step 1: 추상화 : "이 문제에 적용되는 일반적인 원칙/개념/배경지식은 무엇인가?"
- Step 2: 구체적 문제 해결 : "위에서 도출한 원칙을 바탕으로 원래 질문에 답해줘."
- 생각의 사슬 (CoT) 프롬프트 : 중간 추론 단계를 생성하여 추론 능력을 향상시키는 기법이다. 수학적 과제 같은 경우 단계별로 논리적인 내용을 예제로 제공하는 것이 좋다.
- Self-consistency 프롬프트
- ReAct (reason & act) 프롬프트 : 인간의 실제 작동 방식을 모방한다. 추론과 행동을 사고-행동 루프로 결합하는 방식으로 작동하기에 문제에 대해 추론하고 행동 계획을 생성한다. 그리고 작업을 수행하고 결과를 관찰한다. 해결책에 도달할 때 까지 반복한다.
핵심은
- 직관적인 단어를 사용하고 필요한 정보만 제공하라.
- 출력에 대해 구체적으로 설명해라.
- 제약 조건을 설정하는 것은 유용하긴 하지만 제약 조건이 충돌할 수도 있고 잠재력을 제한시킬 수 있기 때문에 유의해야 한다.
도메인 주도 설계 첫걸음
현재 회사에서 PM, 기획자없이 혼자 도메인 전문가와 긴밀하게 협업하면서 바이크 계약 어드민을 거의 새로 만드는 수준으로 개발하고 있다.
요구하는 기술 수준이 높은 작업은 아니였지만 계약이라는 도메인 자체가 처음이기도 하고 기존에 구현되어있던 계약 어드민에 대한 데이터 구조, 아키텍처를 스스로 파악해가면서 개발하기란 쉽지 않았다.
문득 이 상황에 도메인주도설계가 도움이 되지 않을까?라는 생각에 읽어보았다.
기술적인 내용도 좋았지만 협업하는 자세와 방법에 대해서 인사이트를 얻을 수 있어 좋았다.
이 책을 통해 도메인주도설계가 기술적인 문제를 해결하기 보다는 프로젝트를 성공시키기 위한 방법이라는 것을 깨달았다.
"우리가 해결하고자 하는 문제가 무엇인지 합의하기 전에 해결책을 얘기하는 것은 의미가 없다."
"또한 해결책에 대해 합의하기 전에 어떻게 구현하는지 얘기하는 것도 의미가 없다."
무엇?
과왜?
라는 질문에 대한 정답을 찾는 전략적 설계와어떻게?
라는 방법에 대한 정답을 찾는 전술적 설계- 스테이크 홀더(도메인 전문가 등)와 효과적인 협업과 소통을 하는 방법으로 먼저 용어집을 만들자!
- 도메인에서 핵심, 일반, 하위 도메인을 구분할 수 있어야 하며, 어떤 것에 집중할지 결정해야한다!
- 바운디드 컨텍스트는 MSA가 될 수 없지만, MSA가 바운디드 컨텍스트일 수는 있다.
- 애그리게이트는 작업 단위 기준으로 구분되며, 최대한 좁게 설계되어야 한다.
- 비즈니스 로직 패턴에 만능은 없다!. 트랜잭션 스크립트, 액티브 레코드, 도메인 모델, 이벤트 소싱 순으로 적절한 복잡도에 적절한 패턴을 선택해야 한다.
MSA와 EDA, 이벤트 소싱에 대해서도 더 설명하는데 아직 경험이 부족하여 와닿지는 않은 것 같다.
이 책을 읽고 흥미가 생긴 지점은 비즈니스 로직 복잡도를 도메인 모델로 해결하는 방법이다.
용어집을 만들어 바운디드 컨텍스트를 보호하고, 더 작은 단위인 유즈케이스로 애그리게이트 루트를 만들어 작업 단위와 유즈케이스를 뚜렷하게 경계지어서 복잡도를 낮출 수 있는 방법에 대해 궁금해졌다.
애그리게이트는 외부 기술에 의존하지 않고 순수한 도메인 영억으로 만들어야 하는지? 그렇다면 애그리게이트를 생성할 때 필요한 데이터를 모두 조회해서 다 주입해줘야 하는지? 애그리게이트는 빈이 될 수 없는지?과 같은 의문이 생겼다.
다음 책으로 실제 구현하는 방법에 대해 학습해보고 실무에 스스로 적용해보아야겠다.
개발자를 위한 레디스
사내 스터디를 진행하면서 읽어보았다. 내용이 무겁지 않고 글자가 커서 1주일에 한 번씩, 2회만에 완독했다.
- 레디스를 유용하게 사용하는 사례, 적절한 상황
- 레디스 자료구조
- 쓰기 전략
- 키 삭제 방식
- 메모리 관리와 maxmemory-policy 설정
- 캐시 스탬피드를 완화하는 방법
- 레디스의 pub/sub :
fire-and-forget
패턴 - 레디스의 stream : 카프카와 같은 팬아웃 데이터 분산 처리 가능, 소비자 그룹의 장점이 있음
- 복제 방식
- 클러스터 등
레디스 입문서로 적절하다고 생각한다.
길 위의 뇌
우연하게 유산소 운동이 뇌에 미치는 놀라운 효과영상을 접하면서 책을 읽게 되었다.
평소 러닝에 관심이 많기에 러닝에 대한 장점은 대충 알고 있었다. 심페지구력이과 근지구력이 좋아지고 뇌의 노화를 낮추고 뇌 가소성을 높힌다라는 것들 말이다.
하지만 책으로 더 확실하게 알아본다면 나의 러닝 습관에 좋은 영향을 줄 수 있지 않을까 하고 읽어보았다.
저자는 재활의학과에서 뇌신경질환을 겪는 환자들을 진료하기에 운동이 신체 건강과 뇌 건강에 끼치는 영향에 대해 잘 알고 있다.
그렇기에 이 건강을 지키거나 얻기 위해서는 가만히 노력없이 얻을 수 없다고 설명한다. 전문가가 이렇게 설명하니 꾸준히 달리는 입장에서 기분이 좋았다.
러닝이 명상같다고 느껴 머리가 복잡하거나 불안할 때 늦은 시간이라도 그냥 뛰러 나가 내 발소리를 들으면서 생각 정리를 한 적이 많다.
부정적 감정을 없애고 머리에 불필요한 찌꺼기들을 지우기에도 좋다고 생각했다.
저자도 "운동하러 갔다올게"
를 "생각하러 갔다올게"
라고 말할 수 있을 정도로 고요함을 느낀다고 하기에 동질감을 느꼈다.
이 책은 단순히 '운동하면 건강이 좋아져요~'라는 이야기를 하기보다는 운동을 대하는 생각을 바꿔주기 위해 노력하고 있다고 느껴졌다.
몸과 마음의 힘을 기르기 위해 운동을 통해 컴포트존을 벗어나 스트레스를 관리하는 것을 추천한다.
스트레스라고 하면 다들 부정적으로 여기지만, 그렇다고 스트레스 진공 상태에서 지내는 것이 몸과 마음에 좋은 것만은 아니다.
중요한 것은 내가 그 스트레스를 충분히 처리하고 대처할 수 있는가다.
'좋은 스트레스'는 힘들고 도전적이지만 긍정적인 결과를 얻게하는 스트레스다.
결과가 좋지 않았어도 좋은 스트레스가 될 수 있는데, 성장하는 경험이 되었을 때다.
'견딜 만한 스트레스'는 결과는 부정적일지라도 이를 잘 처리할 수 있는 스트레스다.
'나쁜 스트레스'는 신체, 행동, 생각에 부정적인 영향을 미치는 스트레스다.
결국 좋은 스트레스, 나쁜 스트레스를 결정하는 것은 스트레스 그 자체가 아니라 스트레스를 처리하는 나의 능력치에 달렸다.
돈의 심리학
부제는 "당신은 왜 부자가 되지 못했는가"
이다. 책의 제목만 보면 속물들이 읽을 법한 책으로 보일 수 있다.
인문학적 소양은 제쳐두고 돈을 벌기에 혈안이 된 사람들이 좋아할 내용일 것처럼 보이지만, 읽는 동안 총,균,쇠
, 불안
, 프레임
같은 책들이 떠오를 만큼 의외로 깊이 있는 통찰과 여러 교훈을 제공한다.
- 투자에 대한 성향과 선호도는 개인의 지능이나 교육과 무관하다. 사람의 성향은 그들이 언제, 어디서 태어나고 어떤 경험을 했는지에 따라 결정된다.
- 성공에서 행운의 역할을 인정하라. 개인의 노력만으로 모든 결과가 이루어진다는 믿음은 착각이다.
- 스스로 느끼는 만족의 기준을 이해하고 현실과 타협하라. 무조건 더 많은 것을 추구하기보다는 자신에게 진정한 만족감을 주는 것이 무엇인지 알아야 한다.
- 다른 사람에게 잘 보이려 소비하지 마라. 부는 당신이 소유한 것이 아니라 보이지 않게 축적된 자산이다.
- 어마어마한 성공은 어마어마한 힘에서 오지 않는다. 꾸준히 시간을 들이고 인내하는 과정이 성공의 핵심이다.
- 내 시간을 내 뜻대로 사용할 수 있는 자유가 돈이 주는 가장 큰 가치다.
- 부의 진정한 척도는 소득이 아니라 지출과 만족의 균형이다. 연간 5천 달러를 벌면서 4천 달러에 만족하는 사람은, 연간 1만 달러를 벌지만 1만1천 달러에 만족하는 사람보다 부자다.
이 책은 단기간에 돈을 벌 수 있는 비법을 알려주는 책이 아니다. 그런 목적이라면 이 책을 읽지 않는 것이 좋다.
그러나 돈을 대하는 자세와 철학, 그리고 돈이 줄 수 있는 진정한 가치를 배우고 싶다면 강력히 추천한다. 책은 단순한 투자 전략을 넘어 삶의 방식과 가치관을 돌아보게 한다.
특히 돈을 통해 얻을 수 있는 가장 중요한 것이 "자유"임을 강조하며, 물질적 풍요가 아닌 내면적 만족을 중시한다는 점이 인상 깊다.
살다 보면 자신이 내린 선택으로 부와 가난이 결정된다고 생각하기가 쉽다.
...
나는 네가 열심히 노력하는 것의 가치와 그 보상을 믿었으면 좋겠다.
그러나 모든 성공이 노력의 결실도 아니고, 모든 가난이 게으름의 결과도 아님을 깨닫기를 바란다.
...
돈이 주는 가장 큰 배당금은 네 시간을 마음대로 할 수 있는 능력이다.
네가 원할 때, 원하는 일을, 원하는 곳에서, 원하는 사람과 함께, 원하는 만큼 오래할 수 있다는 사실은 그 어떤 고가의 물건이 주는 기쁨보다 더 크고 더 지속적인 행복을 준다.
네가 모은 한 푼, 한 푼은 모두 남들 손에 맡겨질 수 있었던 네 미래 한 조각을 소유하는 것과 같단다.
- '나의 아이들에게 보내는 금융 조언' 중