LostCatBox

(항해) 5주차 서버 동시성 설계 및 테스트

Word count: 2.3kReading time: 14 min
2025/04/22 3 Share

5주차 공개 Q/A

결제가 외부 API를 이용할 경우 트랜잭션 전략

  • 주문 -> 포인트 사용 -> 결제 방식으로 진행해야한다.

    • 결제가 외부 API면 취소가 안된다. -> 따라서 결제를 맨 나중에 해야함
  • 내부 DB 트랜잭션 롤백은 쉽지만, 외부 API를 사용하는경우, 보상로직이 돌아야한다.

    보상로직

    ApplicationEvent를 만들고, EventListener가 받아서 처리한다.

낙관락, 비관락

낙관락

  • 재시도 2 ~3번 시 반드시 성공가능한 케이스에서만 도입하자
    • 내부적인 재시도 처리를 한다면, 성공할 확률이 매우 높아야한다.
  • application단에서 락을 관리하며, 테이블의 version 필드 등을 이용하여 구현된다.
  • 예시
    • 맥북 상시 10%할인 -> 사용자가 몰리지 않음. -> 낙관적락으로 구현해도 크게 문제되지않음 -> 재고가 겹칠 타이밍이 많이나오지 않음
    • 포인트 충전은 낙관락 + 재시도로만 접근하면 큰일 난다.
      • “같은 요청” 으로 포인트 충전을 여러번 실제 요청했는지 구별도 해야한다.
      • 멱등키

비관락

  • 어떻게든 “연산” 을 성공시켜야 하는 것!
  • db 의 lock 기능을 이용하여, db connection을 소모한다.
  • 단점:
    • 롱-트랜잭션의 데드락 발생 가능성이 높아짐
    • 락을 걸고 해제하는게 잦아서 성능 문제가 생길 수있다.
  • 예시
    • 맥북 50% 할인 -> 사용자가 몰림. -> 재시도의 수행 등을 고려했을때, 비관적락이 훨씬 결과적으로 db부하가 적다.

비관적락, 낙관적락 테스트시 고려사항

  • 성능테스트 ->Time Space 가 같이 존재
    • 시간적인 성능 측정
      • api latency P50, P95, P99
      • 재처리가언제 완료되는가
    • 공간적 측정
      • Blocking i/o의 발생
      • idle thread의 개수
      • connection 점유 비율 수준
  • 정합성 검증 테스트
  • 부하테스트 도구 사용하기 + 힙덤프 등등

filter는 로깅, interceptor 는 인증 이외에 e-commerce에 적용할만한 예시

  • Filter
    • Trace /Span (분산 추적) - 쓰레드 아이디 추적 요청 아이디 추적
    • Multi-Tenancy - (다국어 처리 등등) - 접근한 국가가 어디냐에 따라 번역 응답이 달라짐. filter에서 번역만함
      • filter가 사전을 알고잇고 번역책임함
    • A/B Test
    • RateLimitter
    • CORS 제어
  • Interceptor
    • 회원 정보, 회원 등급 핸들링 -> ThreadLocal에 씌우고, 비즈니스에서 활용
      • 유저를 쓰레드 로컬에 넣어놓는다.
    • 레퍼럴 체크 (친구 추천 코드 검증 등등)
    • 기간 한정이벤트용 API일 경우 , 검증

도메인 엔티티 - JPA 엔티티 분리에 대한 견해

  • 실무에서 JPA를 사용할 때 N+1 문제를 방지하기 위해 @OneToMany는 지양하는 것으로 알고 있습니다. @ManyToOne도 결국 1번의 쿼리가 더 발생하게 되는데 … 이런 이슈를 차단 하기 위해, 엔티티 간에는 식별자(ID)만 저장해두고, 필요한 경우 QueryDSL을 활용해 명시적으로 조인하는 방식이 더 낫지 않을까 생각해봤습니다. 이런 접근이 괜찮은 방식일까요? (도메인 엔티티 - JPA 엔티티 분리하였습니다.)

  • JPA의 컨셉 및 목적

    1
    2
    3
    4
    5
    6
    7
    Order ( 주문 )
    OrderItem ( 주문 항목 )
    User ( 사용자 )

    Order -> User 를 항상 알아야 함..
    id 만 들고 있으면... 매번 따로 쿼리를 날려야 함..
    근데 그게 싫어... 그래서 연관관계로 풀면 같이 가져온다는 컨셉에 맞아..
  • 그래서 @OneToMany = 거의 안씀..

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @OneToMany = 거의 안씀..
    연관관계 = 최소화하여 쓴다. <- 사이드 이펙트가 큼.

    현업에서의 문제는 연관관계로 풀었을 때 지옥이 되는 경우가 많음..

    브랜드 : 상품

    Brand {
    @OneToMany
    List<Product> -> 10만개?
    }

    Product {
    @ManyToOne
    Brand
    }

    실제로 대부분의 조회 -> Projection 으로 한다...
  • JPA 도메인 모델 적합한 케이스

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    ---
    JPA 이 자식.. 너는 그냥 테이블 매퍼야..
    ---> 장점... 테이블의 명세가 도메인에 간섭하는 걸 아예 없앤다..
    이렇게 쓸려면...
    도메인 모델은 객체간의 관계로 나타내야함.
    ---


    엔티티는 Id 만 그냥 컬럼처럼 알고 있어야 함..

    그리고 내가 주문 내놔 했을 떄,,
    알아서 infra 구현체에서 조립해서 올린다...

    연관관계 기반의 JPA 도메인 모델을 쓰는 경우 -> "명시적으로 수정 라이프사이클" 이 같은애들을
    대장격인 도메인 모델 = 어그리거트 루트 ( e.g. 주문 , 결제 )
  • JPA 도메인 모델 부적합한 케이스

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    (조회) 내가 좋아한 웹툰 내놔.
    -> JPA 도메인 모델 ? 부적합. 왜? 연관관계는 페이지네이션 수용 못함
    user_webtoon_like { user_id, webtoon_id }
    webtoon { id, .. }

    @Entity
    UserLikedWebtoonList {
    User
    List<Webtoon> //100만개면 터짐
    }

    (수정) 내가 좋아한 웹툰에 "패션왕" 추가해.

    (수정) 내 프로필 이미지 수정해.
  • JPA-entity와 도메인 모델 분리시. infra 구현체 참조

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // Webtoon 객체 구성에는 user를 의존한다. user의 AUTHOR, user의 DESIGNER
    // 그렇다면 webtoon은 도메인 모델로 돌아다니고, JPA-Entity로 전환하는 로직을 구현해야한다.
    Webtoon {
    Author; <- user { type = AUTHOR }
    Designer; <- user { type = DESIGNER }
    ReviewPoint;
    }


    save(webtoon)를 infra에서 구현한다면
    --->
    webtoonJpaRepository.save(WebtoonEntity.from(webtoon);
    userJpaRepository.save(UserEntity.from(webtoon.author));
    userJpaRepository.save(UserEntity.from(webtoon.designer));

분산락의 목적

  • 추후 분산락을 사용하게 된다면 비관적 락을 모두 제거 해도 괜찮을지 궁금합니다.
  • 분산락 ttl(time to live) 설정 이슈 등으로 예상치 못한 상황이 생길 수 있을것 같은데 실무에서는 어떻게 처리하시는지 궁금합니다.
  • 완벽히 대체할 수 없기 때문에 안된다.
    • DB Lock은 완벽하게 Transaction에 대해서 완벽한 롤백이가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Redis, Zookeeper, .. DB NamedLock.. 다양한 방식이 있음.
"원자성"을 완전하게 보장하기 어렵다.
잠금 해제가 실패한다거나... TTL 이 지나기 전에 작업이 완료가 안된다거나..

왜? 연산 주체는 DB 니까..

DB Lock = 연산 주체가 락을 가지고 있음. ( 안정성이 높음 )
DB Lock 과 시스템적 분리 / 분산락 등을 활용한다면 ( 안정성이 낮음 )

현업에서 사용 할 때는...

1차 방어선 = 분산락 사용하는 상황
2차 방어선으로 DB Lock 도 둔다. ( 일반적으로 DB Lock 만 뒀을 떄보다 성능 하락이 적음 )


분산락 사용하는 이유 자체가 하나의 트랜잭션을 보호하기 위함이 아니다.

DB 트랜잭션이라는 개념을 초월한 자물쇠를 만드는 게 목적.

예를 들면, 동시에 결제 API 를 못쏘게 한다던가..

1번 결제에 대해서 배송지 수정과 환불을 동시에 못하게 한다던가..

실무에서는 성능, db 부하 문제로 비관적락을 많이 안쓴다? 경합이 있다고 판단할때, Redis 사용?

  • 어느 정도 경합이 있을때, redis 분산락을 사용하느냐?
    • 분산락을 “지양”하자
    • redis는 분산락에 특화되어있지만, 디버깅이 어렵다 그래서
    • 시스템 설계로 많이 푼다
      • 메시지 큐 같은 것을 이용해서 해결시도
  • Redis는 개수기로써 많이 사용된다.
    • 남은 쿠폰 개수, 좋아 개수 등등 DB로 연산했을떄, 잠금 경합이 되게 많다.
    • redis보안으로, SQL DB를 영속화를 따로 해줘서, redis날아가도 복구가능하도록함

mysql이 mvcc기반이여서 상품 재고 차감시, 쓰기 락을 걸더라도 상품 조회에 대한 지연이 없어보이는데 상품 정보와 상품 재고 테이블 분리하면 좋은 이유가있을까?

  • 어차피 쓰기락있든 말든 상품 조회가능한데? ->mvcc니까
  • 확장성, 운영 안정성 같은 면에서 충분히 이점이있다.
    • 상품 정보 = 변경이 자주일어나는가? X
    • 재고 정보 = 변경이 자주일어나는가 ? 0
    • 재고 때문에 상품 정보를 수정할 때 기다려야 한다면 굳이?
    • 비즈니스를 제공할떄, (파트너사) “재고 충전” 기능을 제공함
    • 상품 정보 수정 기능에서 재고 충전을 넣진않음. -> 분리 필요해짐
    • 상품 서비스와 재고 서비스를 MSA로 가르기도함
    • [물류 시스템] <-> [운영 시스템] <-> [카탈로그(상품) 시스템]
    • 보통 상품수가 많고, 실시간으로 많은 트래픽이 있을때, 재고 테이블 분리함
    • 아니면 선착순 쿠폰 같이 “개수”가 예민한 애들은 재고 테이블 분리함

어떤 락은 활용하는가 실무에서

  • 분산락은 개수기로 사용됨
  • 비관적락은 DB에서 동작하도록함
  • kafka로 이벤트 기반으로 한번에 하나만 처리되게끔함 -> 동시성 문제를 메시지 큐로 한번에 한번만 접근하여 해결

롱-트랜잭션이 비관적락일때, 안좋은이유

  • 성능적 이슈를 많이 발생

  • 낙관락과 비관락 충돌이 많냐 적냐 차이 -> 사용 사례로 판단해야한다.

    • “내가 어떻게 동시성 문제를 처리할 건지”에 따라 다른다
    • 내가 어떤 락을 쓰느냐? 동시성 이슈는 1개의 빵을 N명이 먹고싶어하는 상황
      1. 락을 안쓰고 해결한다 = 시스템 설계
      2. 1명만 먹고, 나머지 다 울어도됨 = 낙관적 락 (실패 시 재시도 X), 점유도 하지않아
      3. “내꺼”라서 동시에 접근하는 게 나바껭 없을때, 2~3번 재시도 하면 성공 가능(자기 포인트 사용+ 적립) -> 낙관적 락
      4. 어떻게든 “내 연산”을 성공시켜야함 -> 원자성 보장 -> 비관적락(DB Lock)
      5. 무조건 “내 유산”이 지켜져야 함 -> 정합성 보장 -> 비관적락 (DB Lock)

현업에서 낙관적 락 사용시 주의사항(flush)

  • 낙관적 락을 적용한 동시성 테스트 케이스에서 ObjectOptimisticLockingFailureException 가 발생하면 catch 해서 예외를 커스텀해주고 싶은데 catch가 안 잡혀서 삽질 중…

    • entityManager.flush 시점에서 예외가 터지는 게 아닐까 추정됩니다.
  • 락을 걸어야 할 만한 케이스들은 알겠는데, 낙관적 락을 적용해야 할지, 비관적 락을 적용해야 할지 너무 헷갈립니다.

  • RetryTemplate 등의 Retry 로직이 꼭 필요할지 고민이 됩니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    현업에서 낙관적 락을 사용할 때는 명시적 flush 를 해줍니다.
    JPA 를 사용하고 있고, 트랜잭션 범위 내에서 해당 수정이 일어남.

    그런데, 트랜잭션 범위 내에서 낙관적 락을 적용한 수정 로직이 "마지막" 이라는 보장이 없음.

    JPA 특성상 지연 쿼리를 발생시킴.
    -> 트랜잭션 커밋직전 시점에 flush 를 하게되고, 그때가서 예외가 터짐.

    try {
    payment.complete()
    xxRepository.flush()
    }

동일한 자원에 대해 각각의 비즈니스 로직에서 서로 다른 락 전략 적용 시 발생할 수 있는 문제가 궁금했습니다.

  • 같은 자원에 대해서는 같은 락을 거는것이 관리 용이함

  • 낙관락이 디비 리소스를 아낄 수 있어 일부라도 적용하는 것이 자원관리 측면에서 괜찮다고 생각했지만, 비관락 로직이 commit되기 전까지 낙관락 로직도 update 쿼리를 치기위해 대기하므로 해당 리소스에 비관락 로직이 동시에 1개라도 수행된다면 리소스 사용은 비슷하다. 따라서 이점이 적거나 거의 없다. 그리고 유지보수 복잡도가 올라가므로 하나만 사용하기로 했다.

1
2
3
비관적 락 .. -> 같은 자원에 대해서는 같은 락을 거는것이 관리 용이함
PESSIMISTIC_WRITE ( x-lock ) = 사생활 보호 필름
PESSIMISTIC_READ ( s-lock ) = 일반 보호 필름
  • 전제
    • 포인트 충전의 경우 유사시 재시도하도록 처리해도 괜찮다.
    • 차감의 경우 주문과 관련돼 실패하면 안된다.
  • 차감시에만 배타락을 걸고 충전시에는 걸지 않는다.
  • Version 어노테이션이 달려있으면 비관락이든 아니든 version관리가 진행된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
차감(비관락)이 먼저 조회할 경우

차감
select where id = 1 and version = 1 for update

충전
select where id =1 and version = 1 (MVCC여서 가능)

충전
update set version = 2 where id = 1 and version = 1
시도하려하지만 차감이 점유중이므로 디비 내부에서 블로킹

차감
update set version = 2 where id = 1 and version = 1
완료

충전
update set version = 2 where id = 1 and version = 1
(업데이트 된 row 0)
exception 발생 및 재시도후 정상 적용

논블로킹 기반 로직에 블로킹을 섞을 경우에 스레드 점유에 대한 병목이 일어나는 것처럼
공유자원에 대한 병목현상이 생긴다.
  • 낙관락 비관락의 기준이 맞는지 아직도 애매모호하다

    • 충돌이 적다? 많다?

      • 처음엔 단순히 이벤트 쿠폰, 기한한정 상품 재고등에 대한 순간적 트래픽의 존재 여부라고 생각

      • 그렇다면 순간적 트래픽이 발생하지 않는 API에 관련된 리소스는 모두 낙관락인가

      • 저 말이 의미하는 게 무엇인지 고민

        • 충돌이 많을 때 낙관락을 건다면?
          • 많은요청이 계속해서 최대횟수까지 재시도하기 때문에 조회요청으로 디비 네트워크 부하 (타 멘토링 청강)
          • 여러 사용자가 계속 접근하는 경우 사용자는 예외가 발생해 재요청을 비교적 많이 해야함.
        • 충돌이 적을 때 비관락을 건다면?
          • 불필요한 디비 부하로 병목이 걸려 좋지 않은 선택
        1
        2
        3
        4
        5
        **(어떻게든 연산을 성공시켜야하는 경우에)** 충돌이 많을 때 "비관락" 충돌이 적을 때 "낙관락"

        동시성 문제
        - 어떻게든 성공시켜야하는 연산 -> 비관적락 -> DB가 줄세움. 계속 나머지들은 기다림
        - FAST-FAIL 이 가능한 연산 -> 낙관적락 -> DB에 경쟁적으로 요청함. 실패도 빨리나옴
    • 결론

      • 비관락
        • 자원 낭비가 있더라도 사용자에게 예외를 던지지 않고 수행시키고 싶을 때
        • 여러 사용자 혹은 API가 동시에 접근해 여러번 수정할 수 있는 자원
          • ex) 주문생성시 재고처리, 선착순 쿠폰 발급 (발급시 재고가 -1된다. 콘서트 좌석이나 멘토링 예약 일정처럼 각각 1개만 적용되지 않는다. 따라서 0에 도달하기 전까지는 점유하지 못했다는 이유로 예외를 던져 빠르게 실패해서는 안된다.)
      • 낙관락
        • 예외를 던지지 않고 수행시키고 싶은데, 접근 지점이 적을 때 (한 자원에 대한 사용자 수, 사용 API)
        • 충돌 시 예외를 던지고 싶을 때 빠르게 실패시켜도될때 (잠시 후 다시 시도해주세요, 요청에 실패했습니다.)
          • ex) 포인트 충전
          • 포인트 사용(주문 결제)
          • 사용자에게 발급된 쿠폰 사용(주문 생성)

퍼사드의 한 메소드에서 낙관락 비관락 로직이 섞여있을 때 되도록 예외 가능성이 높은 낙관락을 먼저 수행하려고(early return 처럼..)합니다. 괜찮은 생각일까요?

1
2
3
4
5
6
7
8
9
10
fun createOrder() {
// 재고확인 및 선차감 (비관락)
// 내 쿠폰 사용처리 (낙관락)
// 주문 생성
// 임의의 낙관락
}
일 때, 낙관락에서 예외 발생률이 높으므로 다른 로직의 결과에 의존성이 없는 경우
임의로 해당 로직을 우선 수행해도 괜찮은지 궁금합니다.

flush 안하면 말짱 도루묵이다.. JPA 특성상 지연 쿼리이므로
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


//아래 부분은 비관락이 걸리는게 매우적다
Tx {
...
..
....
...
...
..
비관락
처리
}

//아래 부분은 비관락이 걸리는게 매우길다
Tx {
비관락
..
..
.
.
.
.
처리
}
  • (14팀 질문과 완전 중복) MySQL이 MVCC이기 때문에 stock테이블을 분리하지 않고 변경을 위해 배타락을 걸더라도 읽기에 대한 병목은 없어보이는데 분리하면 좋은 이유가 궁금합니다.

  • 최대한 가설을 세워 동시성 테스트케이스를 작성하시는지, 락을 섣불리 거는 것보다는 관찰에 의해 발견되면 테스트케이스로 작성하시는지 궁금합니다. 만약 가설을 세우신다면 가설을 세우는 노하우가 있을까요?

    • 저는 그냥 가설을 세워 동시성 테스트케이스를 작성했었다..
      "이제는" 경험치 기반으로 작성한다.
      ---- 잘 작성하려고 한다.
      
      동시성 이슈가 발생할 수 있는 거를 식별할 수 있는 가설..,
      
      데이터 변경 케이스 -> 동시성 이슈가 발생할까?
      상태 기반 로직 -> 다 의심함
          - if ( status == .. ) 근데 누군가는 status 를 바꿈..
          - if (강의.capacity > 강의.applyCount) .. 
      
      타임라인 ( 순서 ) 가 중요한 로직 -> 의심함
          - 선착순
          - 최근 요청만 반영한다고 하면... 업데이트 collision 일어날 수 있음.저는 그냥 가설을 세워 동시성 테스트케이스를 작성했었다..
      "이제는" 경험치 기반으로 작성한다.
      ---- 잘 작성하려고 한다.
      
      동시성 이슈가 발생할 수 있는 거를 식별할 수 있는 가설..,
      
      데이터 변경 케이스 -> 동시성 이슈가 발생할까?
      상태 기반 로직 -> 다 의심함
          - if ( status == .. ) 근데 누군가는 status 를 바꿈..
          - if (강의.capacity > 강의.applyCount) .. 
      
      타임라인 ( 순서 ) 가 중요한 로직 -> 의심함
          - 선착순
          - 최근 요청만 반영한다고 하면... 업데이트 collision 일어날 수 있음.
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10



      - 낙관적락에서 재시도를 한다는거 자체가 우선 성공시키고 보겠다는 행위 인거 같은데 재시도를 하기보다는 비관락을 선택하는게 나은 선택일 수 있는지 궁금합니다.

      - 1번 retry 정도까지는 낙관락 쓴다.

      - 이번 과제에서 꼭 하면 좋은 게 있다면 무엇이 있을까요?

      -
      동시성 - 어떤 문제를 식별/가정 - 어떻게 동시성 이슈를 해결할거ㅓ고 - 그렇기에 어떤 방식을 적용할건지 완성 - 기능적으로 다 동작하는가 ( 필터 / 인터셉터 별로 안 중요하다 ) 하나더 DB Lock 기반 조회나 findXXwithLock 반드시 붙이기.POST_FIX붙여라
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      - 재고가 충분하지 않더라도 실적을 위해 우선 주문을 받아두고 실제보다 적은 게 (0보다 작을 때) 어드민에서 포착되면 배송지연이 될 수 있다는 알림 팝업 정도만 띄워두고 주문은 계속 받을 수도 있을 것 같습니다. 이렇게 했을 때 실제로 재고의 가시성은 커머스에 입점한 판매자에게만 있는데 시스템에서 정확하게 관리되어야 하나요? 실제 커머스의 정책에서 정확하게 관리돼야한다면 그 이유가 궁금합니다.

      -

      - 동시성 테스트를 통해 충돌이 많은 상황이라면 비관적 락이 낙관적 락보다 빠르다는 것을 도출했습니다. 롤백과 재시도가 없어 그렇다고 판단했는데 맞나요? 그리고 밑의 표처럼 적용하여도 문제가 없을까요?

      - [Step 9](https://www.notion.so/Step-9-1dc97b3441a180d28ed1ec75694a30fd?pvs=21) [충돌비율에 따른 낙관락 비관락 성능에 대한 동시성 테스트]

      -
      100명이 동시에 -;.. 전부 작업은 성공시켜야 함 UPDATE 쿼리 개수 비관적 락 = 100개 낙관적 락 = 1+..+100개 = n(n+1)/2 = 5050
    서비스 락 방식 이유
    CouponService.publish Pessimistic Lock 남은 수량이 핵심이고, 실패 가능성을 0으로 보장해야 하므로 재시도 없이 바로 비관적 락 적용
    OrderService.create Pessimistic Lock 재고는 충돌 확률이 높고 중요하므로 비관적 락 적용
    PointService.charge Optimistic Lock (retry=5) 충돌 확률이 낮아 낙관적 락 적용
    PointService.use Optimistic Lock (retry=5) 충돌 확률이 낮아 낙관적 락 적용
  • 낙관 락의 재시도 횟수는 어느 정도가 적당할까요? (쿠폰과 포인트 각각 어떤 기준으로 지정할 수 있을지)

이번주차에 어디에 집중하냐?

  • 동시성
    • 어떤 동시성 문제에 대해 식별했는지
    • 어떤 이유에서 어떤 Lock 을 적용했는지 완성
  • 필터/인터셉터… 이것저것 쑤셔박기..
    • 필터 : HTTP Message 로깅
    • 인터셉터 : 유저 조회해서 들여보내기..
  • 내부 자세히 안 볼 거고.
  • API 가 다 정상적으로 동작하는지 ( 기능적 요구사항 )
  • 비기능적 요구사항 ( 성능, … ) => 안봄
CATALOG
  1. 1. 5주차 공개 Q/A
  2. 2. 이번주차에 어디에 집중하냐?