커피주문 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.