이전에 졸업작품으로 진행했던 Ignis 프로젝트에서 결제 기능을 구현한 경험이 있었다. 당시 포트원(PortOne)을 활용하여 결제 연동을 완료했지만, 결제 시스템의 내부 동작 원리와 전체적인 흐름에 대해 더 깊이 이해하고 싶다는 생각이 들었다.
최근 대부분의 웹 서비스는 온라인 결제 기능을 필수적으로 포함하고 있으며, 결제 과정은 사용자 인증, 결제 승인, 결제 상태 업데이트, 정산 등 여러 단계로 구성된다. 이러한 결제 시스템은 실제로 PG(Payment Gateway) 업체를 통해 복잡한 연동 과정을 거쳐 동작하며, 내부 구조가 공개되지 않는 경우가 많아 학습자가 전반적인 흐름을 이해하기 어렵다.
이에 따라 본 보고서는 결제 프로세스의 핵심 작동 방식과 데이터 흐름을 직접 체험하고 이해하기 위해, 결제 기능을 단순화한 학습용 토이 프로젝트 PayFlow를 조사 및 구현 대상으로 선정하였다. 본 조사는 PayFlow 프로젝트를 통해 결제 모델의 구성 요소, 백엔드 구조, 상태 관리 방식 등을 분석하는 것을 목표로 한다.
본 조사의 목적은 다음과 같다.
- 실제 결제 시스템이 갖추어야 하는 기본 기능과 구조를 이해한다.
- 결제 생성 → PG 승인 → 상태 조회 과정의 흐름을 직접 구성하여 백엔드 동작 원리를 분석한다.
- 간단한 Payment 데이터 모델을 설계함으로써 상태 기반(State-based) 데이터 처리 개념을 학습한다.
- Spring Boot 기반의 결제 처리 시스템 전체 구조를 조사하고 구현하여 실무적인 감각을 기른다.
- 포트원(PortOne) V2 Checkout SDK 실제 연동을 통해 PG 연동 경험을 확보한다.
PayFlow는 Spring Boot 3.2.2 + Java 17 기반의 간단한 결제 처리 시스템으로, 사용자가 결제 정보를 입력하면 서버에서 결제 데이터를 생성하고 포트원(PortOne) V2 Checkout SDK를 통해 실제 결제 승인을 처리한다. 이후 데이터베이스에 해당 내역을 저장하고, 결제 상세 페이지에서 결과를 조회할 수 있도록 한다.
본 프로젝트는 "결제 요청 → PG 승인 처리 → 상태 변경" 과정을 중심으로 구성하였으며, 실제 포트원 V2 API를 통한 실결제 연동을 구현하였다.
| 구분 | 기술 |
|---|---|
| Backend | Spring Boot 3.2.2, Java 17 |
| ORM | Spring Data JPA, Hibernate |
| Database | MySQL 8.x |
| Template Engine | Thymeleaf |
| Build Tool | Gradle |
| PG 연동 | 포트원 V2 Checkout SDK |
| 보안 | Spring Security Crypto (BCrypt) |
결제 시스템 구현 시 활용할 수 있는 다양한 PG사들이 존재한다. 각 PG사별 특징을 조사하였다.
| PG사 | 특징 | 연동 방식 |
|---|---|---|
| 포트원(PortOne) | 여러 PG사를 통합 연동할 수 있는 결제 대행 서비스. 하나의 API로 다양한 PG사 연동 가능. Checkout V2 SDK 제공 | REST API + JavaScript SDK |
| 토스페이먼츠 | 토스 계열 PG사. 깔끔한 API 문서와 개발자 친화적 환경 제공. 결제위젯(Payment Widget) 방식 지원 | REST API + JavaScript SDK |
| NHN KCP | 오래된 PG사 중 하나로 안정적인 서비스 제공. 다양한 결제 수단 지원 | REST API + Server-to-Server |
| 이니시스(INICIS) | KG이니시스. 국내 점유율 1위 PG사. 대형 쇼핑몰에서 많이 사용 | REST API + JavaScript SDK |
| 다날 | 휴대폰 결제에 강점. 소액결제 서비스 전문 | REST API |
| 카카오페이 | 간편결제 서비스. 카카오톡 앱 연동. 빠른 결제 경험 제공 | REST API + Redirect |
| 네이버페이 | 네이버 간편결제. 네이버 쇼핑과 연계 시 유리 | REST API + JavaScript SDK |
본 프로젝트에서 **포트원(PortOne)**을 선택한 이유는 다음과 같다:
- 통합 연동: 하나의 API로 토스페이먼츠, 이니시스, KCP 등 다양한 PG사를 연동할 수 있음
- V2 Checkout SDK: 최신 JavaScript SDK로 간편하게 결제창 호출 가능
- 테스트 환경: 별도의 사업자등록 없이 테스트 결제 진행 가능
- 문서화: 개발자 친화적인 API 문서 및 예제 코드 제공
참고: 토스페이먼츠는 본 프로젝트에서 직접 사용하지 않았지만, 향후 비교 연동을 위해 관심 있는 PG사로 조사하였다. 토스페이먼츠는 Payment Widget 방식을 제공하여 프론트엔드에서 간편하게 결제창을 구현할 수 있다는 특징이 있다.
flowchart LR
subgraph Direct["직접 연동 방식"]
A[토스페이먼츠] --> B[각 PG사별 개별 연동]
C[이니시스] --> B
D[KCP] --> B
end
subgraph Aggregator["통합 연동 방식"]
E[포트원] --> F[단일 API로 통합 관리]
F --> G[토스페이먼츠]
F --> H[이니시스]
F --> I[KCP]
end
flowchart TB
subgraph Client["👤 클라이언트"]
A[사용자 요청]
end
subgraph Controller["① Controller 계층"]
B[PaymentController - 페이지 렌더링]
C[PaymentRestController - API 수신]
end
subgraph BO["② BO 계층 (Business Object)"]
E[PaymentBO - 결제 생성/상태 업데이트]
end
subgraph Client_Layer["③ PG Client 계층"]
F[PgClient Interface]
G[PortOneClient]
end
subgraph Repository["④ Repository 계층"]
I[PaymentRepository]
J[UserRepository]
end
subgraph DB["⑤ DB 계층 (MySQL)"]
K[(payflow Database)]
end
subgraph PG["⑥ PG사"]
L[포트원 V2 API]
M[이니시스 실결제]
end
A --> B
A --> C
B --> E
C --> E
E --> F
F --> G
G --> L
L --> M
E --> I
E --> J
I --> K
J --> K
| 계층 | 역할 | 주요 클래스 |
|---|---|---|
| ① Controller 계층 | 페이지 렌더링 및 API 요청 처리 | PaymentController, PaymentRestController |
| ② BO 계층 | 비즈니스 로직 담당 | PaymentBO |
| ③ PG Client 계층 | PG사 연동 추상화 | PgClient, PortOneClient |
| ④ Repository 계층 | 데이터 영속성 담당 | PaymentRepository, UserRepository |
| ⑤ DB 계층 | 데이터 저장 | MySQL (payflow DB) |
| ⑥ PG사 | 실제 결제 처리 | 포트원 → 이니시스 |
포트원 V2 Checkout SDK를 사용한 실제 결제 흐름은 다음과 같다:
sequenceDiagram
participant Client as 👤 클라이언트
participant Server as 🖥️ PayFlow 서버
participant PortOne as 💳 포트원 V2 SDK
participant PG as 🏦 PG사 (이니시스)
Client->>Server: 1. 결제 생성 요청 (POST /api/pay/create)
Note over Server: userId, amount, method
Server->>Server: 2. Payment 엔티티 생성<br/>(status: ready, orderId 생성)
Server->>Client: 3. orderId 반환 및 리다이렉트
Client->>Server: 4. 결제 요청 페이지 (GET /pay/request/{orderId})
Server->>Server: 5. 포트원 파라미터 준비<br/>(storeId, channelKey, amount 등)
Server->>Client: 6. 포트원 V2 SDK 파라미터 전달
Client->>PortOne: 7. PortOne.requestPayment() 호출
Note over Client,PortOne: 결제창 표시
PortOne->>PG: 8. 결제 승인 요청
PG->>PortOne: 9. 결제 승인 결과
PortOne->>Client: 10. paymentId 반환 (성공 시)
Client->>Server: 11. 결제 성공 콜백 (GET /pay/success)
Note over Server: paymentId, orderId
Server->>Server: 12. 결제 상태 업데이트<br/>(status: paid, pg_tid 저장)
Server->>Client: 13. 결제 완료 페이지 리다이렉트
상세 단계 설명:
-
결제 생성 요청: 사용자가 금액과 결제 수단을 선택하여
POST /api/pay/create로 결제 생성 요청- 세션에서
userId자동 추출 amount,method파라미터 수신
- 세션에서
-
Payment 엔티티 생성: 서버에서
ORD-{timestamp}형식의 주문번호 생성- 초기 상태:
ready createdAt,updatedAt자동 설정
- 초기 상태:
-
결제 요청 페이지:
GET /pay/request/{orderId}로 결제창 호출 페이지 렌더링- 포트원 V2 SDK 파라미터 준비:
storeId: 포트원 스토어 IDchannelKey: 포트원 채널 키 (V2 필수)orderId,amount,orderNamepayMethod: CARD 또는 VBANKcustomer: 구매자 정보 (fullName, phoneNumber, email)successUrl,failUrl: 콜백 URL
- 포트원 V2 SDK 파라미터 준비:
-
결제창 호출: 프론트엔드에서
PortOne.requestPayment()함수 실행- 포트원 V2 SDK가 결제창을 자동으로 표시
- 사용자가 결제 수단 선택 및 인증 진행
-
PG사 결제 처리: 포트원을 통해 실제 PG사(이니시스)로 결제 승인 요청
- 포트원이 내부적으로 이니시스 API 호출
- 카드사 승인 처리
-
결제 결과 처리:
- 성공 시:
paymentId를 받아successUrl로 리다이렉트 - 실패 시: 에러 메시지와 함께
failUrl로 리다이렉트
- 성공 시:
-
서버 콜백 처리:
GET /pay/success또는GET /pay/fail엔드포인트 호출- 성공:
PaymentBO.updatePaymentSuccess()호출orderId로 Payment 조회status를paid로 변경pg_tid에paymentId저장pg_response에 "SUCCESS" 저장
- 실패:
PaymentBO.updatePaymentFail()호출status를failed로 변경pg_response에 실패 사유 저장
- 성공:
-
결제 완료 페이지: 최종 결과를 사용자에게 표시
- 클라이언트 사이드 결제: 서버에서 별도의 승인 API 호출 없이 클라이언트에서 직접 결제 처리
- 자동 리다이렉트: 결제 완료 후 자동으로
successUrl또는failUrl로 이동 - 간편한 연동: 복잡한 서버-서버 통신 없이 JavaScript SDK만으로 구현 가능
- 통합 PG 관리: 하나의 채널 키로 여러 PG사를 자동 선택
erDiagram
users ||--o{ payments : "has"
payments ||--o{ payment_logs : "logs"
payments ||--o{ webhook_callback : "receives"
payments ||--o{ refunds : "may have"
users {
BIGINT id PK
VARCHAR name
VARCHAR email
DATETIME created_at
}
payments {
BIGINT id PK
BIGINT user_id FK
VARCHAR order_id UK
INT amount
VARCHAR status
VARCHAR method
DATETIME created_at
DATETIME updated_at
}
payment_logs {
BIGINT id PK
BIGINT payment_id FK
VARCHAR log_type
TEXT message
DATETIME created_at
}
webhook_callback {
BIGINT id PK
BIGINT payment_id FK
TEXT pg_data
DATETIME created_at
}
refunds {
BIGINT id PK
BIGINT payment_id FK
INT refund_amount
VARCHAR reason
DATETIME created_at
}
| 컬럼명 | 타입 | 설명 |
|---|---|---|
id |
BIGINT | 기본키(PK), AUTO_INCREMENT |
name |
VARCHAR(50) | 사용자 이름 |
email |
VARCHAR(100) | 사용자 이메일 |
created_at |
DATETIME | 생성일 (기본값: CURRENT_TIMESTAMP) |
| 컬럼명 | 타입 | 설명 |
|---|---|---|
id |
BIGINT | 기본키(PK), AUTO_INCREMENT |
user_id |
BIGINT | 사용자 식별자 (FK → users.id) |
order_id |
VARCHAR(100) | 주문 식별자 (UNIQUE) - ORD-{timestamp} 형식 |
amount |
INT | 결제 금액 (NOT NULL) |
status |
VARCHAR(20) | 결제 상태 (기본값: 'ready') |
method |
VARCHAR(20) | 결제 방식 (CARD/VBANK 등) |
created_at |
DATETIME | 생성일 |
updated_at |
DATETIME | 수정일 (ON UPDATE 자동 갱신) |
결제 상태(status) 값:
| PayFlow | 설명 |
|---|---|
ready |
결제 대기 |
paid |
결제 완료 |
failed |
결제 실패 |
참고: 엔티티 클래스(
Payment.java)에는pg_tid,pg_response컬럼이 정의되어 있으나, 초기 DB 스키마에는 포함되지 않았다. 이는 JPA의ddl-auto: update설정에 의해 자동으로 추가되거나, 추후 마이그레이션을 통해 추가될 예정이다.
| 컬럼명 | 타입 | 설명 |
|---|---|---|
id |
BIGINT | 기본키(PK), AUTO_INCREMENT |
payment_id |
BIGINT | 결제 식별자 (FK → payments.id) |
log_type |
VARCHAR(20) | 로그 유형 (REQUEST, RESPONSE, ERROR 등) |
message |
TEXT | 로그 메시지 (PG 응답 데이터 포함) |
created_at |
DATETIME | 생성일 |
결제 과정의 모든 로그를 기록하여 디버깅 및 감사(Audit) 목적으로 사용할 수 있다.
| 컬럼명 | 타입 | 설명 |
|---|---|---|
id |
BIGINT | 기본키(PK), AUTO_INCREMENT |
payment_id |
BIGINT | 결제 식별자 (FK → payments.id) |
pg_data |
TEXT | PG사로부터 받은 Webhook 콜백 데이터 (JSON) |
created_at |
DATETIME | 생성일 |
포트원의 경우 가상계좌 입금 확인, 결제 취소 등의 이벤트를 Webhook으로 전달받을 수 있다. 본 프로젝트에서는 아직 구현하지 않았으나, 향후 확장을 위해 테이블을 미리 설계하였다.
| 컬럼명 | 타입 | 설명 |
|---|---|---|
id |
BIGINT | 기본키(PK), AUTO_INCREMENT |
payment_id |
BIGINT | 결제 식별자 (FK → payments.id) |
refund_amount |
INT | 환불 금액 (NOT NULL) |
reason |
VARCHAR(255) | 환불 사유 |
created_at |
DATETIME | 생성일 |
포트원 환불 API 호출 후 결과를 저장한다. 본 프로젝트에서는 아직 구현하지 않았으나, 향후 확장을 위해 테이블을 미리 설계하였다.
com.payflow
├── PayFlowApplication.java # Spring Boot 메인 클래스
├── controller
│ └── WelcomeController.java # 메인 페이지 컨트롤러
├── payment
│ ├── PaymentController.java # 결제 페이지 렌더링
│ ├── PaymentRestController.java # 결제 REST API
│ ├── bo
│ │ └── PaymentBO.java # 결제 비즈니스 로직
│ ├── client
│ │ ├── PgClient.java # PG 클라이언트 인터페이스
│ │ ├── PortOneClient.java # 포트원 V2 구현체
│ │ └── PgResponse.java # PG 응답 DTO
│ ├── domain
│ │ └── Payment.java # 결제 엔티티
│ └── repository
│ └── PaymentRepository.java # 결제 JPA Repository
└── user
├── UserController.java # 사용자 컨트롤러
├── bo
│ └── UserBO.java # 사용자 비즈니스 로직
├── domain
│ └── User.java # 사용자 엔티티
└── repository
└── UserRepository.java # 사용자 JPA Repository
flowchart LR
subgraph Presentation["Presentation Layer"]
A[PaymentController]
B[PaymentRestController]
end
subgraph Business["Business Layer"]
C[PaymentBO]
D[PgClient Interface]
end
subgraph Data["Data Access Layer"]
E[PaymentRepository]
end
subgraph External["External - PG사"]
F[포트원 V2 API]
end
A --> C
B --> C
C --> D
C --> E
D --> F
| 구성 요소 | 역할 |
|---|---|
| PaymentController | 결제 생성/요청/결과 페이지 렌더링, Thymeleaf 뷰 반환 |
| PaymentRestController | REST API 엔드포인트 제공, JSON 응답 (/api/pay/create) |
| PaymentBO | 핵심 비즈니스 로직 담당, 결제 생성/상태 업데이트/목록 조회 |
| PgClient | PG사 연동 인터페이스 (Strategy Pattern) |
| PortOneClient | 포트원 V2 구현체 (Checkout V2는 클라이언트 사이드에서 처리되므로 인터페이스만 구현) |
| PaymentRepository | JPA를 통한 결제 데이터 CRUD |
# PG사 설정
pg:
type: portOneClient
portone:
store-id: store-xxx
channel-key: channel-key-xxx
api-url: https://api.portone.io
success-url: http://localhost/pay/success
fail-url: http://localhost/pay/fail프론트엔드에서 포트원 V2 SDK를 사용하여 결제창을 호출한다:
<!-- PortOne Checkout V2 SDK -->
<script src="https://cdn.portone.io/v2/browser-sdk.js"></script>
<script>
const response = await PortOne.requestPayment({
storeId: "store-xxx",
channelKey: "channel-key-xxx",
paymentId: "PAY-" + Date.now(),
orderName: "PayFlow 결제",
totalAmount: 10000,
payMethod: "CARD",
currency: "KRW",
customer: {
fullName: "테스트사용자",
phoneNumber: "01012345678",
email: "test@example.com"
},
redirectUrl: "http://localhost/pay/success"
});
</script>| PayFlow | 포트원 V2 |
|---|---|
card |
CARD |
vbank |
VBANK |
- 클라이언트 사이드 결제: 서버에서 별도의 승인 API 호출 없이 클라이언트에서 직접 결제 처리
- 자동 리다이렉트: 결제 완료 후 자동으로
successUrl또는failUrl로 이동 - 간편한 연동: 복잡한 서버-서버 통신 없이 JavaScript SDK만으로 구현 가능
- 통합 PG 관리: 하나의 채널 키로 여러 PG사를 자동 선택
장점:
- 구현이 간단하고 빠름
- 서버 부하 감소 (클라이언트에서 직접 처리)
- 사용자 경험 향상 (빠른 응답 속도)
단점:
- 서버에서 결제 검증이 어려움
- 클라이언트 조작 가능성 (추가 검증 로직 필요)
- Webhook을 통한 서버 검증 권장
본 프로젝트 조사를 통해 다음과 같은 결과를 도출하였다.
- 결제 프로세스가 단일 요청이 아닌 여러 단계의 상태 전이를 통해 동작함을 이해하였다.
ready → paid/failed와 같은 상태 모델링이 결제 시스템의 핵심이라는 점을 확인하였다.- PgClient 인터페이스를 통해 다양한 PG사를 추상화하여 확장 가능한 구조로 설계할 수 있음을 확인하였다.
- 포트원 V2 Checkout SDK를 활용하여 실제 PG사(이니시스) 연동을 성공적으로 구현하였다.
- 다양한 PG사(토스페이먼츠, KCP, 이니시스, 카카오페이 등)의 특징과 연동 방식을 조사하여 비교할 수 있었다.
- 포트원 V2의 클라이언트 사이드 결제 방식을 통해 서버 부하를 줄이고 사용자 경험을 개선할 수 있음을 확인하였다.
PayFlow 프로젝트 조사를 통해 결제 시스템의 기본 원리를 이해하는 데 큰 도움이 되었으며, 특히 상태 관리 기반의 데이터 처리 방식에 대한 이해가 높아졌다.
이전 졸업작품 Ignis 프로젝트에서는 결제 기능을 단순히 연동하는 수준에서 그쳤다면, 이번 PayFlow 프로젝트를 통해 결제 시스템의 전체 구조와 흐름을 직접 설계하고 구현해봄으로써 더 깊은 이해를 얻을 수 있었다.
또한 Spring Boot의 계층 구조와 역할 분담을 체험함으로써, 실무적인 개발 과정에서 백엔드 로직을 설계하고 구현하는 능력을 강화할 수 있었다.
포트원 V2 Checkout SDK를 사용하면서, 기존의 서버-서버 통신 방식과 달리 클라이언트 사이드에서 직접 결제를 처리하는 방식의 장단점을 경험할 수 있었다. 이는 향후 다른 PG사 연동 시에도 유용한 경험이 될 것이다.
- 결제 취소(Refund) 기능 구현
- Webhook 콜백 검증 로직 추가
- 결제 로그 테이블 활용 (payment_logs)
- 다른 PG사(토스페이먼츠) 직접 연동 비교 구현
- 예외 처리 강화 및 에러 핸들링 고도화
pg_tid,pg_response컬럼을 DB 스키마에 추가
본 조사는 결제 시스템 개발의 기초를 다지는 좋은 출발점이 되었으며, 이를 바탕으로 더 높은 수준의 웹 서비스 개발 역량을 갖추는 데 기반이 될 것이다.