발단: 기존 서비스 레이어에서 타 도메인의 기능을 참조하는 것을 분리하고 싶었다.
문제 상황: 팀원 모두가 정확한 이해없이 레이어를 분리하다보니, 이전과 다를 게 없는 의미없는 구조가 되었다.
해결 과정: 각 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이 발생하였다.
해결 과정 :
기존에는 하나의 서비스 메서드에서 락을 획득한 후 트랜잭션이 시작되었기 때문에 Spring의 프록시 기반 트랜잭션 관리 방식에서 실제 트랜잭션이 적용되지 않은 상태로 로직이 실행되고 있었다.
이를 해결하기 위해 다음과 같은 방식으로 구조를 변경했다:
분산 락과 트랜잭션을 명확히 분리
락 획득은 트랜잭션 외부에서 처리하고, 트랜잭션이 필요한 로직은 별도의 서비스 메서드로 위임