발단: 기존 서비스 레이어에서 타 도메인의 기능을 참조하는 것을 분리하고 싶었다.

문제 상황: 팀원 모두가 정확한 이해없이 레이어를 분리하다보니, 이전과 다를 게 없는 의미없는 구조가 되었다.

해결 과정: 각 domain service에서는 해당 도메인의 repository를 참조하고 검증과 같은 로직들을 수행하는 메서드들을 위치 시키고 application service layer에서는 각 domain service를 주입 받아 Facade Pattern이나 usecase와 유사하게 조합하는 방식으로 해결했다.

결과: 외부 레퍼지토리 의존도를 줄일 수 있었고, 각 레이어의 책임이 명확하게 개선됐다. 추가적으로 코드 가독성도 높아지고 코드의 응집도가 향상되었다.

(기범님이 발표하시면서 팀원 모두 클린코드 관점에서 메서드명이나 변수명등을 작성하는 능력을 향상시킬 수 있었고, 필요한 기능들을 요청하는 협업 경험까지 할 수 있었다.)

// Todo

// 패키지 tree

├─main
│  ├─java
│  │  └─nbc
│  │      └─ticketing
│  │          └─ticket911
│  │              ├─common
│  │              │  ├─annotation
│  │              │  ├─aop
│  │              │  ├─audit
│  │              │  ├─exception
│  │              │  │  ├─dto
│  │              │  │  └─handler
│  │              │  ├─funtional
│  │              │  └─response
│  │              ├─config
│  │              ├─domain
│  │              │  ├─auth
│  │              │  │  ├─application
│  │              │  │  ├─controller
│  │              │  │  ├─dto
│  │              │  │  │  ├─request
│  │              │  │  │  └─response
│  │              │  │  └─vo
│  │              │  ├─booking
│  │              │  │  ├─application
│  │              │  │  ├─constant
│  │              │  │  ├─controller
│  │              │  │  ├─dto
│  │              │  │  │  ├─request
│  │              │  │  │  └─response
│  │              │  │  ├─entity
│  │              │  │  ├─exception
│  │              │  │  │  └─code
│  │              │  │  ├─repository
│  │              │  │  └─service
│  │              │  ├─concert
│  │              │  │  ├─application
│  │              │  │  ├─controller
│  │              │  │  ├─dto
│  │              │  │  │  ├─request
│  │              │  │  │  └─response
│  │              │  │  ├─entity
│  │              │  │  ├─exception
│  │              │  │  │  └─code
│  │              │  │  ├─repository
│  │              │  │  └─service
│  │              │  ├─concertseat
│  │              │  │  ├─application
│  │              │  │  ├─controller
│  │              │  │  ├─dto
│  │              │  │  │  ├─request
│  │              │  │  │  └─response
│  │              │  │  ├─entity
│  │              │  │  ├─exception
│  │              │  │  │  └─code
│  │              │  │  ├─repository
│  │              │  │  └─service
│  │              │  ├─lock
│  │              │  ├─seat
│  │              │  │  ├─application
│  │              │  │  ├─controller
│  │              │  │  ├─dto
│  │              │  │  │  ├─request
│  │              │  │  │  └─response
│  │              │  │  ├─entity
│  │              │  │  ├─exception
│  │              │  │  │  └─code
│  │              │  │  ├─repository
│  │              │  │  └─service
│  │              │  ├─stage
│  │              │  │  ├─application
│  │              │  │  ├─controller
│  │              │  │  ├─dto
│  │              │  │  │  ├─request
│  │              │  │  │  └─response
│  │              │  │  ├─entity
│  │              │  │  ├─exception
│  │              │  │  │  └─code
│  │              │  │  ├─repository
│  │              │  │  ├─service
│  │              │  │  └─status
│  │              │  └─user
│  │              │      ├─application
│  │              │      ├─constant
│  │              │      ├─controller
│  │              │      ├─dto
│  │              │      │  ├─request
│  │              │      │  └─response
│  │              │      ├─entity
│  │              │      ├─exception
│  │              │      │  └─code
│  │              │      ├─repository
│  │              │      └─service
│  │              └─infrastructure
│  │                  ├─lettuce
│  │                  ├─redisson
│  │                  │  └─exception
│  │                  │      └─code
│  │                  └─security
│  │                      └─jwt
│  │                          ├─constant
│  │                          ├─exception
│  │                          │  └─code
│  │                          └─filter
│  │                              └─exception
│  │                                  └─code
│  └─resources
│      ├─static
│      └─templates
└─test
    ├─java
    │  └─nbc
    │      └─ticketing
    │          └─ticket911
    │              ├─application
    │              │  └─stage
    │              │      └─service
    │              ├─domain
    │              │  ├─auth
    │              │  │  ├─controller
    │              │  │  └─service
    │              │  ├─booking
    │              │  │  └─controller
    │              │  ├─concert
    │              │  │  └─application
    │              │  ├─lock
    │              │  └─user
    │              │      ├─controller
    │              │      └─service
    │              └─support
    │                  └─security
    └─resources

발단 : 공연 예매 시스템에서 동일 좌석에 대해 100명 이상 동시에 요청이 들어오는 동시성 제어를 JMeter를 통해 진행했다.

문제 상황 : 100건 이하의 동시 요청에서는 정상적으로 1건만 성공하고 나머지는 실패 처리되었으나, 동시 요청이 100건을 초과하면 2~3건의 예매가 동시에 성공하는 문제가 발생했다. 즉, 동일 좌석에 대해 중복 예매가 허용되는 Race Condition이 발생하였다.

해결 과정 :

  1. Lettuce : Lettuce 기반 구조에서 문제 원인을 분석한 결과, 분산 락은 정상적으로 동작하고 있었지만, 락이 적용되는 시점과 트랜잭션의 시작 시점 사이의 시간 차이로 인해 동시 접근이 허용되는 경우가 있었다.

기존에는 하나의 서비스 메서드에서 락을 획득한 후 트랜잭션이 시작되었기 때문에 Spring의 프록시 기반 트랜잭션 관리 방식에서 실제 트랜잭션이 적용되지 않은 상태로 로직이 실행되고 있었다.

이를 해결하기 위해 다음과 같은 방식으로 구조를 변경했다:

분산 락과 트랜잭션을 명확히 분리

락 획득은 트랜잭션 외부에서 처리하고, 트랜잭션이 필요한 로직은 별도의 서비스 메서드로 위임