티켓 구조대
김태익, 박기범, 이승현, 정석현
문제 정의
- 동시성 제어 부재로 인한 데이터 무결성 위험
- 특정 리소스에 대한 다중 요청의 동시 처리 시 발생하는 데이터 정합성 문제 및 신뢰도 저하
락 방식 1: LETTUCE 락
- Redis client library Lettuce
- 특징 : 비동기 지원, 연결 관리
- 장점 : 구현 용이성, 확장성, 성능
- 단점 : Pub/Sub 관련 제약, 오류 처리, 모니터링
- 고려사항 : 분산 락 구현 시 별도의 타임아웃 처리 필요, 높은 트래픽 환경에서 연결 관리 전략 수립, 장애 복구 시나리오 사전 계획 필요
락 방식 2: MYSQL 락
- JPA Pessimistic Lock (MySQL)
- SELECT ... FOR UPDATE 동시성 제어
- JPA에서는 @Lock(LockModeType.PESSIMISTIC_WRITE)
- 장점: DB 기반, 안정성, 구현 용이, 데이터 정합성
- 단점: 성능 병목, 확장성 제한, 대기 시간, 데드락
락 방식 3: REDISSON 분산 락
- Redis + Redisson 라이브러리
- 특징 : TTL, 자동 재시도, 공정성, 복구
- 장점 : 성능, 확장성, 유연성, 안정성
- 단점 : TTL 관리, 인프라, 복잡성, 네트워크
최종선택 및 이유
- Redisson 락 선택
- 이유: 완성도 높은 구현체, 검증된 성능, 자동화된 관리
- 다른 방식의 한계
- Lettuce : Pub/Sub 메시지 유실 가능성, 락 해제 시 경쟁 조건 발생, 복구 메커니즘 부재
- MySQL : 높은 DB 리소스 사용, 동시 요청 시 성능 저하, 확장성 한계
테스트 결과 비교 (1)
테스트 결과 비교 (2)
(1000건 기준 9건 중복 예매)
(1000건 기준 0건 중복 예매)
결론
- REDISSON LOCK 적용으로 데이터 정합성 확보 + 테스트 확인
- 클러스터 환경 및 대규모 요청에 최적
CI/CD 구성 개요
CI 절차
name: CI - Build and Test
on:
pull_request:
branches:
- main
- develop
jobs:
build-and-test:
runs-on: ubuntu-latest
services:
redis:
image: redis:7.0-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- name: Grant permission for Gradle
run: chmod +x ./gradlew
- name: Build Project
run: ./gradlew build -x test
- name: Run tests
run: ./gradlew clean test
env:
SPRING_REDIS_HOST: localhost
SPRING_REDIS_PORT: 6379
CD 절차
name: CD - Deploy to EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Create .env file
run: |
echo "SPRING_DATASOURCE_URL=${{ secrets.DB_URL }}" >> docker/.env
echo "SPRING_DATASOURCE_USERNAME=${{ secrets.DB_USERNAME }}" >> docker/.env
echo "SPRING_DATASOURCE_PASSWORD=${{ secrets.DB_PASSWORD }}" >> docker/.env
echo "SPRING_DATA_REDIS_HOST=${{ secrets.REDIS_HOST }}" >> docker/.env
echo "SPRING_DATA_REDIS_PORT=${{ secrets.REDIS_PORT }}" >> docker/.env
echo "SPRING_JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> docker/.env
chmod 600 docker/.env
- name: Copy files to EC2
uses: appleboy/scp-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_KEY }}
port: '22'
source: "./docker/docker-compose.yml, docker/.env"
target: "~/app"
- name: Login to DockerHub
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Build Docker image
run: docker build -t shlee054/ticket911-app:latest ./docker
- name: Push Docker image
run: docker push shlee054/ticket911-app:latest
- name: Deploy via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_KEY }}
script: |
# System update and required packages installation
sudo apt-get update -y
sudo apt-get install -y \
openjdk-17-jdk \
ca-certificates \
curl \
gnupg
# Docker repository setup and installation
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update -y
sudo apt-get install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin \
docker-compose
# Add ubuntu user to docker group and reload group settings
sudo usermod -aG docker ubuntu
# Kill existing Java Processes
pkill -f 'java -jar' || true
cd ~/app || exit 1
sudo docker compose pull
sudo docker compose down --remove-orphans || true
sudo docker compose up -d[
Dockerfile & Docker-compose.yml
version: "3.9"��services:� app:� build:� context: ..� dockerfile: docker/Dockerfile� container_name: ticket911-app� ports:� - "8080:8080"� env_file:� - .env�� prometheus:� image: prom/prometheus� container_name: ticket911-prometheus� ports:� - "9090:9090"� restart: always� volumes:� - ./prometheus/prometheus-prod.yml:/etc/prometheus/prometheus.yml�� grafana:� image: grafana/grafana� container_name: ticket911-grafana� ports:� - "3000:3000"�
FROM gradle:8.12-jdk17 AS build�WORKDIR /app��COPY build.gradle settings.gradle ./�COPY src ./src��RUN gradle bootJar --no-daemon��FROM openjdk:17-jdk-slim�WORKDIR /app��COPY --from=build /app/build/libs/ticket911-0.0.1-SNAPSHOT.jar .��EXPOSE 8080��ENTRPOINT ["java", "-jar", "ticket911-0.0.1-SNAPSHOT.jar"]
트러블슈팅 - 서비스 레이어 분리 이슈
트러블슈팅 - 동시성 제어 실패 이슈
즉, 동일 좌석에 대해 중복 예매가 허용되는 Race Condition이 발생
1000건 결과
트러블슈팅 - Lettuce 해결 과정
분산 락과 트랜잭션을 명확히 분리
락 획득은 트랜잭션 외부에서 처리하고, 트랜잭션이 필요한 로직은 별도의 서비스 메서드로 위임
별도 메서드에는 @Transactional을 직접 적용하여, Spring 트랜잭션 프록시가 제대로 동작하도록 구성
1000건 결과
트러블슈팅 - MySQL해결 과정
1000건 결과
트러블슈팅 - Redisson 해결 과정
스프링 AOP 내부에서 여러 어드바이스(예: 트랜잭션, 로깅 등)가 정해진 순서대로 적용되기 때문에 트랜잭션 커밋이 언락보다 늦게 적용되는 경우 동시성 문제가 재현될 수 있음
우선순위를 명확히 조정함으로써 AOP 기반 구조에서도 중복 예매를 완벽하게 차단
1000건 결과
THANK YOU
티켓 구조대
GitHub : https://github.com/SeungHyunLee054/Ticket-911-spring.git