mvc 패턴에서 controller는 역할이 무수히 많았다.
이 부분이 전체적인 로직을 이해하기에 좋은 아키텍쳐인 것 같다.
@Controller, @RestController
DBMS는 여러 사용자가 동시에 데이터에 접근할 때, 데이터의 일관성을 유지하기 위한 트랜잭션 관리 기능을 제공한다.
ACID 속성 보장
다수의 사용자가 동시에 데이터베이스에 접근하더라도 데이터 일관성이 유지되도록 동시성 제어를 제공한다. 이를 통해 충돌이나 데이터 불일치를 방지할 수 있다.
dbms의 동시성제어에서 책 재고가 1개 남았을때, 서점에선 그냥 먼저 가져가는 사람이 우선권이 있다.
하지만 온라인의 경우는 결제하거나 결제창을 먼저 띄운 사람에게 있다.
이런 경우 어떻게 처리하는게 좋은 것일까?
만약, 먼저 결제하는 사람이 우선권을 가져서, 둘 다 결제창을 띄울 수 있다면? 결제를 하는데 client마다 인터넷 속도가 다른 것도 우리가 고려를 해야하는 것인가? 동시에 똑같이 15시 17분 4초에 결제가 되었다면?
이런 경우는 또 누가 우선권이 있는 것일까 ?
이는 온라인 서점에서 동시성 제어를 처리하는 방식은 여러 가지가 있지만, 기본적으로 "결제 우선권을 어떻게 부여할 것인가" 와 "경합 상황을 어떻게 해결할 것인가" 가 핵심이 됩니다.
고객 A와 고객 B가 동시에 결제 페이지에 들어감.
재고가 1개 남아 있음.
두 고객이 거의 동시에 결제 시도를 함.
인터넷 속도 차이 또는 서버 부하에 따라 결제 요청이 도착하는 순서가 다를 수 있음.
이러한 경우를 해결하기 위해 락(Lock), 트랜잭션 격리 수준, 큐잉 시스템, 선점 예약 등의 기법이 사용될 수 있습니다.
결제 창을 먼저 띄운 사람에게 우선권을 주는 방법
고객이 결제 창을 띄울 때, 해당 아이템을 예약(Reserve) 상태로 변경.
일정 시간(예: 5분) 내에 결제하지 않으면 예약이 해제되고 다른 고객이 구매 가능.
장점: 빠른 속도를 보장하면서도 충돌을 줄일 수 있음.
단점: 시간이 지나면 다른 고객이 구매 가능하므로 UX가 나쁠 수 있음.
구현 방식 (예시)
UPDATE books
SET reserved_by = 'user_A'
WHERE book_id = 1 AND reserved_by IS NULL;
이렇게 하면 최초로 reserved_by를 설정한 사용자만 해당 아이템을 결제할 수 있음.
결제 요청을 먼저 완료한 사람이 최종적으로 구매 권리를 가짐.
결제 요청 시 UPDATE를 통해 재고를 감소시키고, 성공한 첫 번째 요청만 유효하게 처리.
트랜잭션을 사용해 동시에 들어온 요청을 제어.
구현 방식 (예시)
UPDATE books
SET stock = stock - 1
WHERE book_id = 1 AND stock > 0;
위 쿼리는 stock > 0 조건을 사용해 첫 번째 결제 요청만 성공하도록 만듦.
트랜잭션이 충돌하는 경우, 실패한 요청은 롤백해야 함.
실패한 고객에게는 "품절" 메시지를 반환.
한 명의 사용자만 재고 정보를 업데이트할 수 있도록 DB에서 락을 걸어 동시 접근을 차단.
SELECT ... FOR UPDATE 를 사용해 트랜잭션이 끝날 때까지 다른 사용자는 접근하지 못하게 함.
구현 방식 (예시)
BEGIN;
SELECT stock FROM books WHERE book_id = 1 FOR UPDATE;
-- 재고 확인 후 감소
UPDATE books SET stock = stock - 1 WHERE book_id = 1;
COMMIT;
a 사용자가 트랜잭션을 끝내기 전까지 다른 사용자는 대기해야 하므로 성능이 낮아질 수 있음.
하지만 데이터 일관성이 가장 강하게 보장됨.
결제 요청이 들어오면 메시지 큐(Kafka, RabbitMQ 등) 를 이용해 순차적으로 처리.
들어온 순서대로 큐에 저장하고, 하나씩 꺼내면서 결제를 진행.
장점
결제 요청이 완전히 동시에 (예: 15:17:04 초에) 들어오는 경우, DB 트랜잭션에서 먼저 성공한 사람이 우선권을 가짐.
즉, 트랜잭션 충돌이 발생하면, 먼저 커밋된 쪽이 승리하게 되고, 나머지는 롤백됨.
📌 결제 완료 시점 기준으로 우선권을 부여하는 것이 일반적
결제 요청이 거의 동시에 발생해도, DB는 결국 단 하나의 트랜잭션만 먼저 처리하게 됨.
인터넷 속도가 다르더라도 DB 기준으로 먼저 결제 승인된 사람이 최종 승자.
(1) UX 고려
➡ 선점 예약 방식 (Optimistic Locking) + 타이머 적용
사용자가 결제 페이지를 띄우면 5분 동안 예약.
결제를 하지 않으면 예약이 해제됨.
(2) 성능 고려
➡ FIFO 큐 기반 처리
결제 요청이 들어오면 큐에 넣고, 순차적으로 하나씩 처리.
(3) 완벽한 동시성 제어를 원한다면
➡ Pessimistic Lock + 트랜잭션 강제 적용
SELECT FOR UPDATE를 사용해 하나씩 처리하되, 성능 저하를 감수.
쿠팡, 네이버, 아마존 같은 대형 플랫폼은 Optimistic Lock + 예약 타임아웃을 주로 사용하는 것을 확인함.
한정판 제품 구매 이벤트에서는 FIFO 큐 기반으로 처리.
"결제창을 먼저 띄운 사람이 우선권을 갖도록 할 것인지, 실제 결제를 먼저 성공한 사람이 가져가도록 할 것인지"를 비즈니스 로직에 따라 선택해야 합니다.
➡ 즉각적인 결제 성공을 보장하려면 First-Paid Wins 방식이 적절.
➡ 사용자 경험을 고려하면 예약 시스템이 좋음.
➡ 트랜잭션 충돌을 줄이고 순서를 지키려면 FIFO 큐가 유리.
또한, 우리나라에서 2월7일기준 오후9시에 결제랑, 미국에서 오후8시59분 결제 했을때 누가 더 빨리 결제한 것인지 궁금하지 않나요 ?
➡ 타임존(Timezone) 문제도 동시성 제어에서 중요한 요소입니다. 특히 글로벌 서비스에서는 "어떤 시간을 기준으로 결제 순서를 판단할 것인가?"가 명확해야 합니다.
➡ 모든 결제 기록을 UTC 기준으로 저장하고 비교하면 해결됩니다.
CREATE TABLE payments (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
amount DECIMAL(10,2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
@Column(name = "created_at", columnDefinition = "TIMESTAMP")
@CreationTimestamp
private Instant createdAt;
Java에서는 Instant를 사용하면 UTC로 저장됨.
SELECT * FROM payments ORDER BY created_at ASC LIMIT 1;
가장 먼저 결제된 기록을 찾음.
서버마다 시간이 다를 경우 결제 순서를 정확하게 판단할 수 없음. 이를 해결하기 위해 NTP(Network Time Protocol) 동기화를 설정해야 함.
서버 시간 동기화 (Linux)
sudo apt update
sudo apt install ntp
sudo timedatectl set-timezone UTC
sudo systemctl restart ntp
만약 두 사람이 같은 시간(UTC 기준) 에 결제했다면?
🔹 한국에서 2월 7일 오후 9시 vs 미국에서 2월 7일 오후 8시 59분
// UTC로 변환하면
🇰🇷 한국 (KST) 21:00 → UTC 12:00
🇺🇸 미국 동부 (EST) 20:59 → UTC 01:59
미국에서 8시 59분에 결제한 사람이 더 먼저 결제한 것.
CREATE TABLE customers (
customer_id INT PRIMARY KEY, -- 고객 고유 번호
name VARCHAR(50) NOT NULL -- 고객 이름
);
CREATE TABLE orders (
order_id INT PRIMARY KEY, -- 주문 고유 번호
order_date DATE NOT NULL, -- 주문 날짜
customer_id INT, -- 고객 고유 번호 (외래 키로 설정됨)
FOREIGN KEY (customer_id)
REFERENCES customers(customer_id) -- 고객 ID를 참조하는 외래 키
);
INSERT INTO customers (customer_id, name) VALUES (1, 'Alice');
INSERT INTO customers (customer_id, name) VALUES (2, 'Bob');
주문 추가
INSERT INTO orders (order_id, order_date, customer_id) VALUES (1, '2024-08-25', 1);
INSERT INTO orders (order_id, order_date, customer_id) VALUES (2, '2024-08-26', 2);
NULL
로 반환한다.NULL
로 반환한다.