1. 결제 프로세스
우리가 인터넷을 통해 옷을 구매하는 과정(결제 프로세스)은 다음과 같습니다.
- 구매자가 구입할 옷에 대한 정보와 금액을 판매자에게 전달
- 판매자는 전달받은 금액을 PG사에게 결제해줄 것을 요청
- PG사는 요청받은 정보를 은행사에게 다시 결제 요청
- 은행사는 요청받은 금액을 구매자의 계좌에서 출금 후 PG사로 전달
- PG사는 판매자에게 금액을 전달 (일정량의 수수료를 제외)
- 판매자는 금액 확인 후, 구매자에게 옷을 배송
💡 PG (Payment Gateway)
- 구매자와 판매자 사이에서의 이뤄지는 결제를 안전하게 할 수 있도록 대행해주는 역할을 담당
- 대표적인 PG사로는 KG 이니시스, NHN, KCP, LGU+ 등이 있으며,
- 모바일 환경으로는 KG 모빌리언스, 다날, 카카오Pay 등이 있습니다.
결제 연동 시스템을 직접 구현한다면 위와 같은 과정을 거쳐야 하는데, 최소 2주 이상은 온전하게 결제 연동에 매달려야 구현이 가능하다.
거기에 더해 각 PG사는 결제를 위해 사용하게 되는 모듈이 서로 다르다. 때문에, PG사를 변경해야 하는 상황이 다가온다면, 해당 PG사에 맞춰서 결제 연동 시스템을 완전히 새롭게 구축해야 한다.
이러한 단점을 극복하고자 결제 솔루션이 등장했다. 사용하고자 하는 결제 솔루션사의 결제 API를 사용하면, 복잡한 과정을 보다 간결하게 수행할 수 있죠.
각 PG사에 맞는 결제 연동 시스템은 결제 솔루션에서 구현해 두었다. 우리는 해당 기능을 사용하기 위한 API만 활용하면 되는거죠. 결제 솔루션의 예로는 iamport(PortOne), 부트페이 등이 존재한다
1.1 결제솔루션( 아임포트(포트원), 부트페이 ) 이해
아임포트(I’mport) / 포트원(PortOne) 는 개발환경과 상관없이, 원하는 PG사와의 결제시스템을 연결시켜주는 결제 API 서비스입니다. (결제 솔루션)
아임포트가 포트원 서비스로 이름을 변경함. 그래서 포트원으로 부름
포트원이 해주는 역할
- PG 결제창 호출
- 결제 결과 수신
- 결제 조회/검증(REST-API)
결제 API를 완성시키는 과정에는 많은 시간이 소요된다. 거기에 더해 결제는 민감한 부분이다. 때문에 기능 개발이 완료 되었더라도 실제 정산을 받기 위해 적합한 서비스인지 PG사, 카드사의 검수 과정을 통과해야 한다. 이 모든 과정을 처리하는데 적어도 2~3달의 시간이 필요하다. 따라서, 결제솔루션을 사용한다고 해도 결제는 절대 쉬운 일이 아니다
2. Transaction - Isolation
2.1 트랜잭션의 속성(ACID)에 대한 이해
ACID 는 Transaction을 정의하는 4가지 속성을 가리키는 약어 입니다.
A(Atomicity): 원자성 →- 안전성 보장을 위해 가져야 할 성질 중의 하나로,
- 트랜잭션과 관련된 작업들이 부분적으로 실행되다가 중단되지 않는 것을 보장하는 능력.
- 모두 성공할 것 아니면 모두 실패하게 만드는 것(DB의 오염을 막기 위함).
- 안전성 보장을 위해 가져야 할 성질 중의 하나로,
C(Consistency): 일관성 →- 트랜잭션이 실행을 성공적으로 완료하면 언제나 일관성 있는 데이터베이스 상태로 유지하는 것.
- 똑같은 쿼리를 조회할 때마다 동일한 결과값이 나타나야하는 것.
- 수정과 삭제로 인해 결과값이 달라지는 것은 당연함.
I(Isolation): 격리성 →- 트랜잭션을 수행 시 다른 트랜잭션의 연산 작업이 끼어들지 못하도록 보장하는 것.
- A 사람의 요청을 처리하는 동안 B사람의 요청은 잠시 기다리는 것
D(Durability): 지속성 →- 성공적으로 수행된 트랜잭션에 대한 로그가 남아야하는 성질로 런타임 오류나 시스템 오류가 발생하더라도,
- 해당 기록은 영구적이어야 하는 것.
- 성공하여 commit이 되었으면 서버를 다시 켜도 그 데이터는 그대로 유지가 되어야 되는 것.
- 성공적으로 수행된 트랜잭션에 대한 로그가 남아야하는 성질로 런타임 오류나 시스템 오류가 발생하더라도,
2.2 Isolation-level
Isolation-level 이란 Transaction의 격리 수준이라고 합니다. 즉, 동시에 여러 트랜잭션이 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것입니다.
Isolation은 총 4단계를 걸칩니다. 아래 단계로 내려갈수록 안전해지는 반면 성능이 느려집니다.
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
3. Isolation-level에 따른 문제점
| Isolation-Level | Dirty-Read | Non-Repeatable-Read | Phantom-Read |
|---|---|---|---|
| READ UNCOMMITTED | O | O | O |
| READ COMMITTED | X | O | O |
| REPEATABLE READ | X | X | O |
| SERIALIZABLE | X | X | X |
3.1 READ UNCOMMITTED
- 각 트랜잭션에서의 변경 내용이 COMMIT이나 ROLLBACK 여부에 상관 없이 다른 트랜잭션에서 값을 읽을 수 있다.
- 즉, commit 되지 않는 데이터들을 조회할 수 있으나,
- 정합성에 문제가 많은 격리 수준이기 때문에 사용하지 않는 것을 권장
- Commit이 되지 않는 상태지만 Update된 값을 다른 트랜잭션에서 읽을 수 있음
💡 Dirty Read
- READ UNCOMMITTED는 문제는 DIRTY READ 현상이 발생 되는것
- 즉 트랜잭션이 작업이 완료되지 않았는데도 다른 트랜잭션에서 볼 수 있게 되는 현상을 의미
3.2 READ COMMITTED
- RDB(관계형 데이터베이스)에서 대부분 기본적으로 사용되고 있는 격리 수준
- Dirty Read와 같은 현상은 발생하지 않는다.
- 실제 테이블 값을 가져오는 것이 아니라 Undo 영역에 백업된 레코드에서 값을 가져온다.
💡 NON REPEATABLE READ
- 한 트랜잭션 내에서 같은 쿼리를 두번 수행할 때,
- 그 사이에 다른 트랜잭션이 값을 수정 또는 삭제함으로써 두 쿼리가 상이하게 나타나는 현상
- 즉, 동일한 쿼리를 요청하면 매번 동일한 결과값이 나타나야 한다.
- 하지만, 동일한 결과값이 나타나지 않을 때 나타나는 현상
3.3 REPEATABLE READ
MySQL에서는 트랜잭션마다 트랜잭션 ID를 부여하여 트랜잭션 ID보다 작은 트랜잭션 번호에서 변경한 것만 읽는다. Undo 공간에 백업해두고 실제 레코드 값을 변경한다.
- 백업된 데이터는 불필요하다고 판단하는 시점에 주기적으로 삭제합니다.
- Undo에 백업된 레코드가 많아지면 MySQL 서버의 처리 성능이 떨어질 수 있습니다.
이러한 변경방식은 MVCC(Multi Version Concurrency Control)라고 부름
💡 PHANTOM READ
- PHANTOM READ란, 종종 다른 트랜잭션에서 수행한 변경 작업에 의해
- 레코드가 보였다가 안 보였다가 하는 현상을 말함
REPEATABLE READ를 default isolation 단계로 사용하는 MySQL에서는 PHANTOM READ가 발생하지 않도록 알아서 처리해줌
3.4 SERIALIZABLE
철수 3,000원, 훈이 2,000원, 영희 5,000원이 존재한다고 가정
- 철수와 훈이가 가진 돈을 영희에게 모두 준다고 하면,
- 영희의 돈은 10,000원이 되야 한다.
하지만 컴퓨터의 입장에서는 철수가 영희에게 3,000원을 주는 동시에, 훈이 또한 영희에게 2,000원을 주게 된다면 컴퓨터는 어떻게 받아들일까요?
- 영희의 돈(5,000원)을 조회
- 철수가 주는 3,000원을 영희의 돈으로 update
- 영희의 돈(5,000원)을 조회
- update가 완료되었습니다.
- 훈이가 주는 2,000원을 영희의 돈으로 update
- update가 완료
- 최종적으로 영희가 가진 돈은 7,000원이 된다.
즉, 위와 같은 상황은 철수가 준 돈을 update 완료 하기 전에 훈이가 돈을 넘겨줌으로써, 데이터의 오염이 일어나게 된 것이다.
그렇기 때문에 철수가 돈을 주고 update가 완료될 때 까지는 영희 데이터에 훈이는 접근할 수 없도록 lock을 걸어줘야 한다.
4. 데드락
포인트 충전 API와 쿠폰 교환 API가 있다고 가정해보면,
- 2개의 API 모두 현재 포인트가 얼마인지 조회하는 API를 사용한다.
- 2개의 API 모두 조회 시점에 lock을 걸게 된다.
- 각각의 API는 하나의 Transaction으로 묶여 있기 때문에,
- 포함한 모든 로직이 완료되어야 commit이 이뤄지고 lock이 해제된다.
- 만약 두 API들이 lock을 건 테이블의 lock이 해제되길 기자리며 무한히 기다리는 상황을
- lock이 죽었다고 표현하여,
Dead-Lock(데드락)이라고 부른다. - 따라서 Dead-Lock 이 발생하지 않게 API 설계를 해야합니다.
- lock이 죽었다고 표현하여,
- lock을 사용할 때는 모든 API 내에서,
- 동일한 순서로 테이블을 조회하도록 작성하는 것이 일반적인 원칙!