커피주문 TechSpec
커피주문 TechSpec
커피 주문 시스템 기술 명세서 (Tech Spec)
1. 개요
본 문서는 ‘커피 주문 시스템’ 백엔드 서버 개발을 위한 기술적인 설계와 구현 방향을 정의합니다. 다수의 사용자와 트래픽을 안정적으로 처리할 수 있도록 확장성과 데이터 정합성을 핵심 가치로 삼아 아키텍처를 설계합니다.
2. 기술 스택
- 언어: Java 11
- 프레임워크: Spring Boot 2.7.x
- 데이터베이스:
- RDBMS: PostgreSQL (사용자, 메뉴, 주문 등 핵심 데이터 영구 저장)
- In-Memory DB: Redis (인기 메뉴 순위 집계, 포인트 잔액 캐싱 등 성능 최적화)
- 빌드 도구: Gradle
- API 문서화: Springdoc-openapi (Swagger UI)
- 테스트: JUnit 5, Mockito
3. 시스템 아키텍처
- 기본 구조: Layered Architecture (Controller - Service - Repository)
- Controller: API 엔드포인트 정의, HTTP 요청/응답 처리
- Service: 비즈니스 로직 처리, 트랜잭션 관리
- Repository: 데이터베이스 접근
- 인프라: 다수의 서버와 인스턴스 환경을 고려하여 Load Balancer 하단에 애플리케이션 서버를 배치하는 구조를 가정합니다. 이를 통해 수평 확장이 용이하며 무중단 배포가 가능합니다.
4. 데이터베이스 스키마 (PostgreSQL)
a. users - 사용자 정보 및 포인트 잔액
1
2
3
4
5
6
7
CREATE TABLE users (
user_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name VARCHAR(255) NOT NULL,
point_balance BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
b. menus - 커피 메뉴 정보
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE TABLE menus (
menu_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name VARCHAR(255) NOT NULL UNIQUE,
price BIGINT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);```
**c. orders - 주문 내역**
```sql
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id BIGINT NOT NULL REFERENCES users(user_id),
menu_id BIGINT NOT NULL REFERENCES menus(menu_id),
order_price BIGINT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
created_at
필드를 통해 최근 7일간의 주문 데이터를 필터링합니다.
d. point_charge_history - 포인트 충전 내역
1
2
3
4
5
6
CREATE TABLE point_charge_history (
history_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id BIGINT NOT NULL REFERENCES users(user_id),
amount BIGINT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
5. API 명세
Base URL: /api/v1
a. 커피 메뉴 목록 조회 API
- Endpoint:
GET /menus
- Description: 주문 가능한 모든 커피 메뉴 목록을 조회합니다.
- Success Response (200 OK):
1 2 3 4
[ {"menuId": 1, "name": "아메리카노", "price": 4500}, {"menuId": 2, "name": "카페라떼", "price": 5000} ]
b. 포인트 충전 API
- Endpoint:
POST /points/charge
- Description: 특정 사용자의 포인트를 충전합니다. (1원 = 1P)
- Request Body:
1
{"userId": 1, "amount": 10000}
- Success Response (200 OK):
1
{"userId": 1, "currentBalance": 15000}
- Error Response:
404 NOT FOUND
: 사용자를 찾을 수 없을 때
c. 커피 주문/결제 API
- Endpoint:
POST /orders
- Description: 사용자가 메뉴를 선택하여 포인트로 주문 및 결제합니다.
- Request Body:
1
{"userId": 1, "menuId": 2}
- Success Response (200 OK):
json { "orderId": 101, "userId": 1, "menuId": 2, "menuName": "카페라떼", "paymentAmount": 5000, "remainingPoints": 10000 }
* Error Response:400 BAD REQUEST
: 포인트가 부족할 때404 NOT FOUND
: 사용자 또는 메뉴를 찾을 수 없을 때
d. 인기 메뉴 목록 조회 API
- Endpoint:
GET /menus/popular
- Description: 최근 7일간 가장 많이 판매된 상위 3개 메뉴를 조회합니다.
- Success Response (200 OK):
1 2 3 4 5
[ {"menuId": 1, "name": "아메리카노", "orderCount": 150}, {"menuId": 3, "name": "바닐라라떼", "orderCount": 120}, {"menuId": 2, "name": "카페라떼", "orderCount": 95} ]
6. 주요 구현 계획
a. 동시성 제어 (Concurrency Control)
users
테이블의 특정 row에 Pessimistic Lock(비관적 락)을 적용합니다.PointService
또는OrderService
에서 사용자의 포인트를 조회하고 변경하는 로직 전체를@Transactional
로 묶습니다.- Repository(JPA)에서 사용자를 조회할 때
LockModeType.PESSIMISTIC_WRITE
옵션을 사용하여 트랜잭션이 끝날 때까지 다른 트랜잭션의 접근을 막습니다. 이를 통해 ‘조회 시점’과 ‘수정 시점’ 사이의 데이터 변경을 원천 차단하여 데이터 일관성을 보장합니다.
b. 인기 메뉴 집계 (Popular Menu Aggregation)
- Redis의 Sorted Set을 활용하여 실시간 순위를 집계합니다.
- 주문 발생 시:
OrderService
에서 주문이 성공적으로 완료되면, Redis Sorted Set에 해당menu_id
의 점수(score)를 1 증가시킵니다 (ZINCRBY
명령어).- Key:
popular_menus
- Member:
menu_id
- Score: 주문 횟수
- Key:
- 인기 메뉴 조회 시: Redis의
ZREVRANGE
명령어를 사용하여 점수가 높은 순으로 상위 3개의menu_id
와 score(주문 횟수)를 즉시 조회합니다.
- 주문 발생 시:
- 기간(7일) 처리:
- (방법 1: 스케줄링): 매일 자정에
@Scheduled
를 이용하여 7일이 지난 주문 데이터를 기반으로 Redis의 순위 정보를 재계산(Re-build)합니다. 이 방법은 집계 시 약간의 부하가 있지만, 조회 성능이 매우 빠릅니다. - (방법 2: Key 분리): Redis Key를 날짜별로 관리(
popular_menus:YYYY-MM-DD
)하고, 조회 시 최근 7일 치 Key들을ZUNIONSTORE
로 합산하여 결과를 조회합니다. 이 방법은 더 정확하지만 조회가 상대적으로 복잡합니다. 프로젝트 초기에는 방법 1을 권장합니다.
- (방법 1: 스케줄링): 매일 자정에
c. 외부 시스템 연동 (Mock API 호출)
- 비동기(Asynchronous) 호출로 처리합니다.
- Spring의
@Async
어노테이션을 활용하여 별도의 스레드에서 Mock API를 호출합니다. 이를 통해 주문 API는 외부 시스템의 응답을 기다리지 않고 즉시 사용자에게 응답을 반환할 수 있어 응답 시간이 단축되고 시스템 간의 결합도가 낮아집니다.
7. 테스트
- Unit Test: 각 Service와 Controller의 비즈니스 로직을 Mockito를 이용해 의존성을 격리하여 테스트합니다.
- Integration Test:
@SpringBootTest
를 사용하여 API 엔드포인트부터 데이터베이스까지 전체 흐름을 테스트합니다. Testcontainers를 활용하여 실제 PostgreSQL, Redis와 동일한 환경에서 테스트하여 신뢰도를 높입니다. - Concurrency Test:
ExecutorService
와CountDownLatch
를 사용하여 포인트 충전/차감 API에 동시에 여러 요청을 보내는 테스트 케이스를 작성하고, Pessimistic Lock이 정상적으로 동작하여 데이터 정합성이 깨지지 않는지 검증합니다.
This post is licensed under CC BY 4.0 by the author.