3~4주차 - web 구조 & 동시성 처리에 대한 나의 생각

ChoRong0824·2025년 2월 7일
0

Web

목록 보기
29/51
post-thumbnail

mvc 패턴에서 controller는 역할이 무수히 많았다.

  1. 요청에 대한 처리
  2. 예외처리
  3. View Template 응답 or Data 응답
  4. 비지니스 로직 처리
  5. DB 상호작용

Layered Architecture 구조

이 부분이 전체적인 로직을 이해하기에 좋은 아키텍쳐인 것 같다.

  • 클라이언트의 요청을 받는 역할을 수행한다.
  • 요청에 대한 처리를 Service Layer에 전달한다.
  • Service에서 처리 완료된 결과를 클라이언트에 응답한다.
  • 사용하는 Annotation : @Controller, @RestController
  1. Service
  2. Repository
  3. DTO(Data Transfer Object)

DBMS의 주요 기능

  1. 데이터 정의
  2. 데이터 관리
  3. 데이터 보안
  4. 트랜잭션 관리
  • DBMS는 여러 사용자가 동시에 데이터에 접근할 때, 데이터의 일관성을 유지하기 위한 트랜잭션 관리 기능을 제공한다.

  • ACID 속성 보장

    • Atomicity: 트랜잭션의 모든 작업이 성공적으로 완료되거나, 실패 시 모든 작업이 롤백
    • Consistency: 트랜잭션이 데이터베이스를 일관된 상태로 유지
    • Isolation: 동시에 실행되는 트랜잭션 간의 영향을 최소화
    • Durability: 트랜잭션이 완료된 후 데이터의 변경 사항은 영구적으로 저장
  1. 백업 및 복구
  2. 동시성 제어

다수의 사용자가 동시에 데이터베이스에 접근하더라도 데이터 일관성이 유지되도록 동시성 제어를 제공한다. 이를 통해 충돌이나 데이터 불일치를 방지할 수 있다.
dbms의 동시성제어에서 책 재고가 1개 남았을때, 서점에선 그냥 먼저 가져가는 사람이 우선권이 있다.
하지만 온라인의 경우는 결제하거나 결제창을 먼저 띄운 사람에게 있다.
이런 경우 어떻게 처리하는게 좋은 것일까?
만약, 먼저 결제하는 사람이 우선권을 가져서, 둘 다 결제창을 띄울 수 있다면? 결제를 하는데 client마다 인터넷 속도가 다른 것도 우리가 고려를 해야하는 것인가? 동시에 똑같이 15시 17분 4초에 결제가 되었다면?
이런 경우는 또 누가 우선권이 있는 것일까 ?

이는 온라인 서점에서 동시성 제어를 처리하는 방식은 여러 가지가 있지만, 기본적으로 "결제 우선권을 어떻게 부여할 것인가" 와 "경합 상황을 어떻게 해결할 것인가" 가 핵심이 됩니다.


1. 일반적인 동시성 문제

고객 A와 고객 B가 동시에 결제 페이지에 들어감.
재고가 1개 남아 있음.
두 고객이 거의 동시에 결제 시도를 함.
인터넷 속도 차이 또는 서버 부하에 따라 결제 요청이 도착하는 순서가 다를 수 있음.
이러한 경우를 해결하기 위해 락(Lock), 트랜잭션 격리 수준, 큐잉 시스템, 선점 예약 등의 기법이 사용될 수 있습니다.

2. 해결 방법

(1) 선점 예약 (Optimistic Locking)

결제 창을 먼저 띄운 사람에게 우선권을 주는 방법
고객이 결제 창을 띄울 때, 해당 아이템을 예약(Reserve) 상태로 변경.
일정 시간(예: 5분) 내에 결제하지 않으면 예약이 해제되고 다른 고객이 구매 가능.
장점: 빠른 속도를 보장하면서도 충돌을 줄일 수 있음.
단점: 시간이 지나면 다른 고객이 구매 가능하므로 UX가 나쁠 수 있음.
구현 방식 (예시)

UPDATE books 
SET reserved_by = 'user_A'
WHERE book_id = 1 AND reserved_by IS NULL;

이렇게 하면 최초로 reserved_by를 설정한 사용자만 해당 아이템을 결제할 수 있음.

(2) 선착순 결제 성공 기준 (First-Paid Wins)

결제 요청을 먼저 완료한 사람이 최종적으로 구매 권리를 가짐.
결제 요청 시 UPDATE를 통해 재고를 감소시키고, 성공한 첫 번째 요청만 유효하게 처리.
트랜잭션을 사용해 동시에 들어온 요청을 제어.
구현 방식 (예시)

UPDATE books
SET stock = stock - 1
WHERE book_id = 1 AND stock > 0;

위 쿼리는 stock > 0 조건을 사용해 첫 번째 결제 요청만 성공하도록 만듦.
트랜잭션이 충돌하는 경우, 실패한 요청은 롤백해야 함.
실패한 고객에게는 "품절" 메시지를 반환.

(3) Pessimistic Lock (비관적 락)

한 명의 사용자만 재고 정보를 업데이트할 수 있도록 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 사용자가 트랜잭션을 끝내기 전까지 다른 사용자는 대기해야 하므로 성능이 낮아질 수 있음.
하지만 데이터 일관성이 가장 강하게 보장됨.

(4) 대기열 기반 FIFO (Queue-Based Processing)

결제 요청이 들어오면 메시지 큐(Kafka, RabbitMQ 등) 를 이용해 순차적으로 처리.
들어온 순서대로 큐에 저장하고, 하나씩 꺼내면서 결제를 진행.
장점

  • 결제 요청이 많아도 서버 부하를 분산시킬 수 있음.
  • 주문 순서를 정확히 보장할 수 있음.
    단점
  • 사용자 입장에서는 결제 처리가 바로 되지 않고 기다려야 할 수도 있음.

3. 동시에 결제가 되었을 때, 누가 우선권을 가지는 것일까?

결제 요청이 완전히 동시에 (예: 15:17:04 초에) 들어오는 경우, DB 트랜잭션에서 먼저 성공한 사람이 우선권을 가짐.
즉, 트랜잭션 충돌이 발생하면, 먼저 커밋된 쪽이 승리하게 되고, 나머지는 롤백됨.

📌 결제 완료 시점 기준으로 우선권을 부여하는 것이 일반적

결제 요청이 거의 동시에 발생해도, DB는 결국 단 하나의 트랜잭션만 먼저 처리하게 됨.
인터넷 속도가 다르더라도 DB 기준으로 먼저 결제 승인된 사람이 최종 승자.

4. 결론 및 추천 방식

(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 기준으로 저장하고 비교하면 해결됩니다.

데이터베이스에 UTC로 저장하고 변환

  • 서버는 모든 결제 시간을 UTC로 저장하고, 사용자에게는 현지 시간으로 변환해서 보여줌.
  • SQL에서 TIMESTAMP WITH TIME ZONE을 사용하면 자동 변환 가능.

Mysql

CREATE TABLE payments (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    amount DECIMAL(10,2),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
  • created_at을 TIMESTAMP로 설정하면 UTC로 자동 저장됨.

PA(Java)에서 UTC 저장

@Column(name = "created_at", columnDefinition = "TIMESTAMP")
@CreationTimestamp
private Instant createdAt;

Java에서는 Instant를 사용하면 UTC로 저장됨.


결제 우선순위 결정 방법

  1. DB에 저장된 UTC 타임스탬프 기준으로 비교
SELECT * FROM payments ORDER BY created_at ASC LIMIT 1;

가장 먼저 결제된 기록을 찾음.

  1. 동일한 시간에 결제되었다면?
  • 결제 ID (Auto-Increment)가 더 작은 쪽이 우선.
  • 혹은 결제 승인 코드(승인번호) 기준으로 비교.

📌 2. 시간 동기화 문제 해결 (NTP 사용)

서버마다 시간이 다를 경우 결제 순서를 정확하게 판단할 수 없음. 이를 해결하기 위해 NTP(Network Time Protocol) 동기화를 설정해야 함.

서버 시간 동기화 (Linux)

sudo apt update
sudo apt install ntp
sudo timedatectl set-timezone UTC
sudo systemctl restart ntp
  • 모든 서버를 UTC 기준으로 동기화해야 함.
  • 만약 여러 서버가 분산되어 있다면, NTP 서버를 직접 운영할 수도 있음.

📌 3. 결제 경합이 발생하는 경우

만약 두 사람이 같은 시간(UTC 기준) 에 결제했다면?

  • 승인번호(Order ID) 기준으로 정렬 (DB에서 Auto-Increment 적용)
  • 트랜잭션 커밋 순서로 결정
  • Queue를 이용해 순차 처리 (FIFO 방식)

결론

🔹 한국에서 2월 7일 오후 9시 vs 미국에서 2월 7일 오후 8시 59분

// UTC로 변환하면
🇰🇷 한국 (KST) 21:00 → UTC 12:00
🇺🇸 미국 동부 (EST) 20:59 → UTC 01:59

미국에서 8시 59분에 결제한 사람이 더 먼저 결제한 것.

즉, 결제 시스템에서는 다음을 적용해야 함

  1. 모든 결제 시간을 UTC로 저장하고 비교.
  2. NTP를 통해 서버 시간을 동기화하여 정확한 기록 유지.
  3. 동일한 시간이라면 ID 순서, 승인번호, 트랜잭션 커밋 순서로 우선권 결정.
  4. 유저에게는 현지 시간(한국, 미국 등)으로 변환하여 보여줌.

데이터 무결성 ?

  • 데이터의 정확성, 일관성, 완전성을 유지하는 것
    1. 정확성: 데이터가 올바르고 오류 없이 저장되는 것을 의미한다.ex) 숫자로 저장되어야 하는 값이 문자로 저장되지 않도록 하는 것.
    2. 일관성: 데이터가 서로 모순되지 않고 조화를 이루는 상태를 유지하는 것을 의미한다.ex) 외래 키 제약 조건을 통해 두 테이블 간의 관계가 일치하는 것을 보장하는 것.
    3. 완전성: 필요한 모든 데이터가 빠짐없이 저장되고 관리되는 것을 의미한다.ex) 필수 입력 항목이 비어 있지 않도록 하는 것.
    • 즉, 데이터가 입력, 저장, 전송, 처리되는 동안 변경되거나 손상되지 않도록 보장하는 개념

외래키 이해하기

1. customers 테이블

  • 고객 정보를 저장하는 테이블
CREATE TABLE customers (
    customer_id INT PRIMARY KEY,  -- 고객 고유 번호
    name VARCHAR(50) NOT NULL     -- 고객 이름
);
  • customer_id: 각 고객을 고유하게 식별하는 PRIMARY KEY
  • name: 고객의 이름을 저장하는 열

2. orders 테이블

  • 주문 정보를 저장하는 테이블
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를 참조하는 외래 키
);
  • order_id: 각 주문을 고유하게 식별하는 PRIMARY KEY
  • order_date: 주문 날짜를 저장하는 열
  • customer_id: customers 테이블의 customer_id를 참조하는 FOREIGN KEY
  • FOREIGN KEY 동작 예시
    1. 데이터 추가:
      INSERT INTO customers (customer_id, name) VALUES (1, 'Alice');
      INSERT INTO customers (customer_id, name) VALUES (2, 'Bob');
  • 고객 Alice와 Bob을 customers 테이블에 추가
    1. 주문 추가

      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);
      • Alice(고객 ID 1)와 Bob(고객 ID 2)의 주문을 orders 테이블에 추가
      • orders 테이블의 customer_id 열은 FOREIGN KEY로서, 반드시 customers 테이블의 customer_id와 일치해야 한다. 그렇지 않으면 참조 무결성 위반 오류가 발생한다.
    • 존재하지 않는 customer_id를 가진 주문을 추가하려고 하면 오류가 발생하여, 잘못된 데이터가 저장되지 않도록 방지할 수 있다.
    • 주문 테이블에서 참조하고 있는 customer_id가 있다면 해당하는 customers 의 row를 삭제할 수 없다.

Join

  1. INNER JOIN
    • 두 테이블에서 공통된 값을 가지고 있는 행만 반환한다.
      • ON 절의 조건이 일치하는 결과
    • MySQL에서는 JOIN, INNER JOIN, CROSS JOIN이 모두 같은 의미이다.
    • 교집합
  2. LEFT JOIN
    • 왼쪽 테이블의 모든 행과 오른쪽 테이블의 일치하는 행을 반환한다.
      • ON 절의 조건 중 첫번째 테이블인 왼쪽(기준)의 데이터를 모두 가져온다.
    • 오른쪽 테이블에 일치하는 데이터가 없으면 NULL로 반환한다.
    • JOIN을 여러번 사용하는 경우 LEFT JOIN으로 시작했다면 이후 JOIN도 LEFT JOIN으로 해야한다.
    • 부분 집합(왼쪽 테이블)
  3. RIGHT JOIN
    • 오른쪽 테이블의 모든 행과 왼쪽 테이블의 일치하는 행을 반환한다.
      • ON 절의 조건 중 두번째 테이블인 오른쪽(기준)의 데이터를 모두 가져온다.
    • 왼쪽 테이블에 일치하는 데이터가 없으면 NULL로 반환한다.
    • 부분 집합(오른쪽 테이블)
  4. OUTER JOIN
    • 두 테이블에서 공통된 값을 가지지 않는 행도 포함해서 반환한다.
      - 합집합
    • LEFT OUTTER JOIN, RIGHT OUTTER JOIN, FULL OUTTER JOIN(잘 사용하지 않음) 이 있다.
    • 대부분의 DB는 FULL OUTTER JOIN을 지원하지 않고 UNION을 사용하도록 한다.
      • UNION을 사용하면 자동으로 중복을 제거(DISTICT)해준다.

profile
백엔드를 지향하며, 컴퓨터공학과를 졸업한 취준생입니다. 많이 부족하지만 열심히 노력해서 실력을 갈고 닦겠습니다. 부족하고 틀린 부분이 있을 수도 있지만 이쁘게 봐주시면 감사하겠습니다. 틀린 부분은 댓글 남겨주시면 제가 따로 학습 및 자료를 찾아봐서 제 것으로 만들도록 하겠습니다. 귀중한 시간 방문해주셔서 감사합니다.

0개의 댓글