여러 사용자가 하나의 금광을 동시에 채굴하는 상황을 구현하며, 데이터베이스 동시성 제어를 단계적으로 학습하고 검증하는 프로젝트입니다.
동시성은 대부분의 백엔드 서비스에서 반드시 고려해야 하는 문제입니다.
Gold Rush Lab은 여러 사용자가 하나의 자원을 동시에 수정하는 상황을 금광 채굴이라는 도메인으로 단순화하여 다음과 같은 질문을 검증합니다.
- 동시에 채굴하면 어떤 문제가 발생하는가?
- 데이터는 어떻게 깨지는가?
- 데이터베이스는 이를 어떻게 해결하는가?
- 환경과 요구사항에 따라 어떤 동시성 제어 방식이 적합한가?
프로젝트는 버전별로 기능을 확장하며 동시성 문제와 해결 방식을 직접 구현하고 비교합니다.
- Java 21
- Spring Boot 4.1.0
- Spring Web
- Spring Data JPA
- Hibernate
- Lombok
- Gradle 9.5.1
- PostgreSQL 16
- Docker
- Docker Compose
- Prometheus
- Grafana
- JUnit 5
- Spring Boot Test
- k6
v0.1
MineEntity
├── id: Long
├── remainingAmount: Long
└── createdAt: LocalDateTime
|
+-- 1:N -- UserEntity
└── 1:N -- MiningLogEntity
UserEntity
├── id: Long
├── mine: MineEntity
├── totalMinedGold: Long
├── sessionId: UUID
└── createdAt: LocalDateTime
|
└── 1:N -- MiningLogEntity
MiningLogEntity
├── id: Long
├── user: UserEntity
├── mine: MineEntity
├── amount: Long
└── createdAt: LocalDateTime
애플리케이션은 app/src/main/resources/application.yml에서 다음 환경 변수를 참조합니다.
| Environment Variable | Description |
|---|---|
POSTGRES_URL |
PostgreSQL JDBC URL |
POSTGRES_USER |
애플리케이션에서 사용할 PostgreSQL 사용자명 |
POSTGRES_PASSWORD |
PostgreSQL 비밀번호 |
POSTGRES_DB |
PostgreSQL 데이터베이스 이름 |
- 애플리케이션용 Compose 파일:
app/compose.yml - 독립 PostgreSQL 인프라 Compose 파일:
infra/db/compose.yml - 초기 DB 스키마:
infra/db/init/001-schema.sql - 애플리케이션 포트:
8080
부하 테스트를 위해 HikariCP의 최대 커넥션 풀 크기는 50으로 설정되어 있습니다.
현재 두 Compose 설정의 환경 변수 이름과 애플리케이션 datasource 설정은 완전히 통일되지 않은 상태입니다.
main 브랜치나 v* 태그를 GitHub에 푸시하면 GitHub Actions가 애플리케이션 이미지를 빌드하여 GHCR에 게시합니다.
docker pull ghcr.io/yeoooo/gold-rush-lab:latest비공개 패키지는 먼저 GitHub PAT로 로그인해야 합니다.
echo "$GHCR_TOKEN" | docker login ghcr.io -u yeoooo --password-stdin컨테이너 실행 시 데이터베이스 접속 정보를 환경 변수로 전달합니다.
docker run --rm -p 8080:8080 \
-e POSTGRES_URL=jdbc:postgresql://host.docker.internal:5432/goldrush \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=goldrush \
ghcr.io/yeoooo/gold-rush-lab:latest애플리케이션과 PostgreSQL을 함께 실행하려면 app 디렉터리에서 container 프로필을 활성화합니다.
cd app
POSTGRES_DB=goldrush \
POSTGRES_USER=goldrush \
POSTGRES_PASSWORD=change-me \
docker compose --profile container up -d애플리케이션은 PostgreSQL의 health check가 통과된 후 시작됩니다.
| Environment Variable | Default | Description |
|---|---|---|
APP_IMAGE_TAG |
v0.1 |
실행할 애플리케이션 이미지 태그 |
APP_PORT |
8080 |
호스트에 노출할 애플리케이션 포트 |
k6를 사용하여 여러 사용자가 하나의 광산을 동시에 채굴하는 Hot Spot 시나리오를 실행할 수 있습니다.
cp infra/load-test/.env.example infra/load-test/.env
./infra/load-test/run.sh hotspot| Environment Variable | Default | Description |
|---|---|---|
BASE_URL |
http://localhost:8080/api |
테스트 대상 주소 |
MINE_AMOUNT |
100000 |
생성할 광산의 초기 잔량 |
USER_COUNT |
100 |
동시 접속 사용자 및 VU 수 |
ITERATIONS |
100 |
사용자 한 명당 요청 횟수 |
HOTSPOT_MAX_DURATION |
1m |
Hot Spot 시나리오 최대 실행 시간 |
자세한 실행 방법과 시나리오 설명은 infra/load-test/README.md를 참고합니다.
부하 테스트 후 infra/db/assets/verification.sql을 사용하여 광산의 초기 잔량, 실제 채굴량, 채굴 후 잔량 및 광산별 채굴 로그를 확인할 수 있습니다.
검증 파일 앞부분에는 테이블 초기화와 시퀀스 재설정 쿼리가 포함되어 있으므로 실행 범위를 확인해야 합니다.
아무런 동시성 제어 없이 기본 채굴 시스템을 구현합니다.
- Spring Boot, PostgreSQL, VM 환경 구성
- 기반 코드 작성
- 기본 채굴 API
- Integration Test
- 동시성 정합성 테스트
- 부하 테스트
- 핫스팟 스트레스 테스트
- Lost Update 분석
- TPS / 응답 시간 측정
DB Lock을 이용하여 동시성 문제를 해결합니다.
- Optimistic Lock
- Pessimistic Lock
- 동일한 벤치마크 수행
- Lost Update 제거 확인
- v0.1과 성능 비교
- Lock 충돌률 분석
API 서버를 여러 대로 확장합니다.
- Multiple API Instance
- Load Balancer
- 동일한 벤치마크 수행
- 처리량(TPS) 비교
- DB Lock의 확장성 분석
Redis 기반 분산 락을 적용합니다.
- Redis
- Distributed Lock
- 동일한 벤치마크 수행
- DB Lock 대비 성능 비교
- 락 대기 시간 분석
Kafka 기반의 비동기 처리 구조로 확장합니다.
- Kafka
- Event-Driven Architecture
- Consumer
- 동일한 벤치마크 수행
- 처리량 및 지연 시간 비교
- 최종 성능 분석
모든 버전은 동일한 테스트 시나리오를 기준으로 정합성과 성능을 비교합니다.
모든 요청을 하나의 금광으로 집중시켜 시스템의 처리 한계와 락 경합을 측정합니다.
- 동시 사용자: 단계적으로 증가
- 대상: 동일한 금광
- 시스템 포화 시점까지 수행
- 최대 처리량(TPS)
- 평균 응답 시간
- P95 / P99 지연 시간
- 락 대기 시간
- 타임아웃 발생 횟수
- 데드락 발생 여부
Gold-Rush-Lab
├── app
│ ├── build.gradle
│ ├── compose.yml
│ └── src
│ ├── main
│ │ ├── java/io/devyeoooo/Gold_Rush_Lab
│ │ │ ├── comm
│ │ │ │ └── BaseEntity.java
│ │ │ ├── mine/repository
│ │ │ │ ├── entity/MineEntity.java
│ │ │ │ ├── MineRepository.java
│ │ │ │ ├── MineJpaRepository.java
│ │ │ │ └── MineJpaAdapter.java
│ │ │ ├── user/repository
│ │ │ │ ├── entity/UserEntity.java
│ │ │ │ ├── UserRepository.java
│ │ │ │ ├── UserJpaRepository.java
│ │ │ │ └── UserJpaAdapter.java
│ │ │ ├── mining_log/repository
│ │ │ │ ├── entity/MiningLogEntity.java
│ │ │ │ ├── MiningLogRepository.java
│ │ │ │ ├── MiningLogJpaRepository.java
│ │ │ │ └── MiningLogJpaAdapter.java
│ │ │ └── GoldRushLabApplication.java
│ │ └── resources/application.yml
│ └── test/java/io/devyeoooo/Gold_Rush_Lab
│ └── GoldRushLabApplicationTests.java
└── infra
├── db
│ ├── assets/verification.sql
│ ├── compose.yml
│ └── init/001-schema.sql
├── load-test
├── .env.example
├── run.sh
└── scenarios/hotspot.js
└── monitoring
├── grafana/dashboards/gold-rush-lab.json
└── prometheus/prometheus.yml