- 그냥 mysql 에 요청 데이터 insert 하고 자기 데이터 쌓은 거 기준 전에 쌓인 데이터들을 다 sum 을 매번 해가지고 전체 금액 계산하면 될 것 같다는 의견 있엇음. 좋은 것 같음. (그러나 아래 개선 사항 쪽에 내용 추가)
- 투자 history 를 현재 바로 mysql 에 넣음. 근데 바로 mysql 에 데이터 넣는다면 데이터 몇 천만개 됐을 때 insert 시 index 계산하는데 시간이 많이 걸릴 것 같음.
요구사항을 통해 파악한 도메인
- 투자 상품
- 사용자의 투자 상품 및 투자 금액
개발 전 요구사항에 대해 생각해야 넘어가야 할 사항들
- 투자 상품 모집기간 내에 상품만 응답 필요
- 사용자의 투자 금액 취소는 고려 X
- 모집 투자 시간 체크하도록 함.
- 사용자가 투자하기 API 호출 시 금액 관련 동시성 체크하기
- sold out 처리 -> 모집 금액보다 더 많이 투자되어선 안됨
- 동일 사용자의 중복 요청 막기 (혹시 모르는 retry 로 인한 중복)
- 남은 모집 금액보다 사용자가 더 많은 금액으로 투자 요청 시 에러 처리하도록 함
- (투자 요청 금액 - 남은 모집 금액) 으로 의도하지 않게 자동 투자가 되는 것을 막음
- 남은 모집금액을 실시간으로 계속 보여줘야 하는 상황
- 특정 투자상품에 N번 투자 가능 여부 -> 요구사항에는 관련 내용 없으나 가능하다고 판단
- 나의 투자상품 조회 시 특정 상품에 N번 투자 시 분리해서 보여주도록 함
- 투자상품 수정에 대해서는 고려 X
- 오픈될 때 다수의 고객이 동시에 투자하기에 특정 시간에 대부분의 요청 쏠리는 상황인거 인지 필요
- 투자하기 요청에 대한 상황에서 redis 장애 시 rdb 로의 요청 전환은 고려하지 않음.
- 보통 redis 장애 시 rdb 로 전환되도록 해야 하나, 특정 시간에 트래픽이 몰리는 상황에서 rdb 로 전환되더라도 굉장히 많은 동싱 요청에 대한 부분이 해결되지 않기에 redis 장애 시 투자하기 api 는 막도록 함.
- 투자 히스토리 조회 시 데이터들은 비정규화로 가지고 있도록 함
- 정규화보다 비정규화로 가지고 있는게 더 이득이라 생각됨. 매우 많은 사람이 몰리는 중 모집 가격 변경은 불가능.
-
redis
- single thread 기반인 inmemory db 를 사용하여 특정 상황에 트래픽이 몰리게 되며 생기는 금액에 대한 동시성 문제 해결.
- 특정 시간에 많이 호출되는
남은 모집금액, 상품, 총 투자자 수
를 캐싱하여 응답속도 개선- 상품 데이터 캐싱 시
남은 모집금액
,총 투자자 수
는 실제 계산되고 있는남은 모집금액
,총 투자자 수
데이터를 1초 동안 캐싱하도록 함 - 추후 redis 트래픽으로 인해 read, write 분리 가능하도록 투자 요청 시에는 비즈니스 로직에서 바라보는 실제
남은 모집금액
,총 투자자 수
과 상품 리스트에서 조회되는남은 모집금액
,총 투자자 수
를 분리함
- 상품 데이터 캐싱 시
-
mysql
- 투자 모집금액이 채워졌을 때 redis 에서 rdb 에 남은 모집금액을 싱크.
- 많은 데이터가 쌓여있는 mysql 에 초당 N개를 insert 하면 무척 속도가 느릴 것이기긴 한데 현재 구현 시간 상 rdb 에 저장하도록 구현.
- 전체 투자 상품 데이터
- 특정 투자 상품에 대한 남은 금액
- 특정 투자 상품에 대한 투자자 수
- redis 에서 해당 상품의 투자모집 상태 변경
- rdb 에 투자한 사용자 및 금액 싱크 및 투자모집상태 변경
- kotlin
- spring boot
- mybatis
- jpa 로 구현할 수 있으나 native query 가 현 상황에 이득이라 판단됨
- redis
- redisson
- 분산락을 실제로 구현하지 하지 않으려고 내부적으로 pubsub 을 이용한 분산락 기능을 제공해주는 redisson 클라이언트를 이용
- redisson
decrby 명령어 이용해 구현하려고 했으나, 아래 상황이 존재할 가능성 있음.
- 모집금액 5만원
- A 사용자, B 사용자, C 사용자가 동시에 각각 2만원, 4만원, 1만원 투자 요청
- 세명이 동시에 validation 로직 통과
- A 사용자 금액을 남은 투자금액에 대해 decrby 결과 -> 3만원
- B 사용자 금액을 남은 투자금액에 대해 decrby 결과 -> -1만원
- C 사용자 금액을 남은 투자금액에 대해 decrby 결과 -> -2만원
- B 사용자 금액에 대해 decrby 금액이 마이너스임. exception 처리 및 금액 롤백 처리 -> 최종 -2만원 + 4만원 = 2만원
- C 사용자 금액에 대해 decrby 금액이 마이너스임. exception 처리 및 금액 롤백 처리 -> 최종 2만원 + 1만원 = 3만원
- C 사용자는 정상적으로 처리가 되야 하는 상황이었으나 B 사용자의 요청을 롤백 전에 처리가 들어와 exception 처리됨.
그래서 분산 락을 이용하여 (금액 차감 + 롤백) 의 과정을 락을 검. 이 때 redisson 을 이용하여 lock 선점에 대해 pubsub 구조를 가져가 선점에 대한 redis 요청 최소화 시킴. (금액 차감 + 롤백) 의 과정에 대한 redis 요청은 최소 요청 1번에서 최대 2번 이기에 매우 빠른 속도로 이루어질 것으로 보여서 속도 면에선 문제 없을 것으로 보임.
전체적인 프로젝트 구조는 port and adapter 패턴을 따름. 추후, 외부 구현체(infrastructure)인 redis, mybatis 등과 같은 기술들을 변경하더라도 내부 구현체에는 영향이 없도록 설계
- application: 비즈니스 로직에 대한 driven, driving port 를 정의하여 비즈니스 로직을 구현
- driven: 외부 구현체를 위한 port
- driving: 내부 구현체를 외부에서 사용하기 위한 port
- common: 전체적으로 사용 가능한 util 등이 존재
- config
- controller: rest api 컨트롤러가 존재하며 실질적으로 adapter 역할을 하고, 내부 driving port 를 호출
- domain: 외부 구현체에 대해 의존하지 않고 서비스에 대한 도메인을 가지는 pojo 클래스가 존재
- exceptions
- infrastructure: 외부 구현체가 존재. application 패키지 내에 driven port 를 구현함
- adapter: driven port 의 구현체가 존재
- entity: 외부 구현체에 의존하는 entity 들이 존재
- mapper: 외부 구현체(mybatis)에 대한 mapper 가 존재