KakaoPay v2 DynamoDB/KMS
KakaoPay v2 신규 구축 과정에서 DynamoDB 엔티티 설계와 KMS 키 관리를 어떻게 결정했는지를 기록한다. 데이터 저장 계층의 설계 결정이 이후 서비스 운영과 데이터 호환성에 어떤 영향을 미쳤는지, 그리고 공통 결제 라이브러리의 convention과 동기화해야 했던 이유를 남긴다.
왜 저장 구조와 암호화를 같이 봐야 했나
DynamoDB attribute naming 문제는 사소해 보이지만, 공통 라이브러리를 여러 서비스에서 공유하는 구조에서는 런타임 에러로 이어질 수 있는 문제다. 실제로 공통 모델과 테이블의 naming convention 불일치를 발견하고 해결한 과정을 기록한다.
DynamoDB 기준에서 먼저 정해야 했던 것
KakaoPay v2도 DynamoDB를 주 저장소로 사용했다. 그래서 이 단계의 핵심은 DynamoDB를 새로 도입하는 것이 아니라, 기존에 쓰고 있던 데이터 구조와 공통 라이브러리 convention 위에서 엔티티 규칙을 다시 명확히 정하는 일이었다. 동시에 KMS(Key Management Service)를 통한 암호화도 필요했다. 결제 데이터는 민감 정보를 포함하므로 저장 시 암호화가 필수이고, KMS 키의 리전 배치가 서비스 배포 리전과 맞아야 한다.
이 시기에는 DynamoDB entity convention, KMS 키 설정, 테스트 커버리지 보강이 서로 얽혀 진행되었다. 하나를 바꾸면 나머지 둘에도 영향을 주는 구조였기 때문에 저장 구조와 보안 정책을 따로 떼어 볼 수 없었다.
핵심 체크포인트
- 결제 시스템의 DynamoDB 테이블들은 단일 convention이 아니었다. 기존 테이블들은 camelCase, 신규 벤더별 transaction 테이블들은 PascalCase로 생성되어 있었다.
- 공통 결제 라이브러리에 트랜잭션 엔티티 모델을 만들어 여러 서비스에서 공유하려 했으나, 실제 서비스에 적용하자 DynamoDB 조회에서 attribute를 찾지 못하는 에러가 발생했다. 모델의 attribute naming(camelCase)과 테이블의 attribute naming(PascalCase)이 달랐기 때문이다.
- DynamoDB는 attribute name이 대소문자를 구분하므로,
transactionId와TransactionId는 완전히 다른 attribute다. 테이블은 이미 데이터가 있으므로 attribute name을 바꿀 수 없었고, 코드를 테이블에 맞추는 수밖에 없었다.
convention 불일치를 어떻게 발견하고 해결했나
공통 결제 라이브러리에 트랜잭션 엔티티 모델을 만들어 여러 벤더 서비스에서 공유하려 했다. 처음에는 Java 표준에 따라 camelCase로 모델을 작성했다.
그런데 실제 서비스에 적용하자 DynamoDB 조회에서 attribute를 찾지 못하는 에러가 발생했다. 원인을 추적하니 신규 transaction 테이블들의 attribute가 PascalCase(TransactionId, RecordType, OrderId)로 생성되어 있었고, 모델의 camelCase와 맞지 않았던 것이다.
이 코드는 구조 이해를 위한 개념 스케치이며, 실제 프로덕션 코드는 더 많은 필드와 검증 로직을 포함한다.
Before (불일치)
// camelCase 기본값 — 테이블의 PascalCase attribute와 불일치
@DynamoDbImmutable(builder = TransactionRecordDbo.TransactionRecordDboBuilder.class)
public class TransactionRecordDbo {
private String transactionId; // → "transactionId"로 조회 → 매칭 실패
private RecordType recordType;
private String orderId;
private long createdAt;
}DynamoDB는 attribute name이 대소문자를 구분한다. transactionId와 TransactionId는 완전히 다른 attribute다. 이미 데이터가 있는 테이블의 attribute name은 바꿀 수 없으므로, 코드를 테이블에 맞추는 것이 유일한 선택이었다.
결과적으로 같은 시스템 안에 두 가지 convention이 공존하게 되었다.
| 구분 | 테이블 | Attribute 네이밍 |
|---|---|---|
| 기존 서비스 | 승인, 게이트웨이, 프리뷰 등 기존 서비스 테이블 | camelCase |
| 신규 transaction 서비스 | 벤더별 transaction 테이블 (KakaoPay, Afterpay, PayPal 등) | PascalCase |
이론적으로는 하나의 convention으로 통일하는 것이 맞지만, 이미 운영 중인 테이블의 attribute name을 바꾸는 것은 전체 데이터 마이그레이션을 의미한다. 신규 테이블의 convention에 맞춰 공통 모델을 수정하고, 기존 테이블은 그대로 유지하는 것이 현실적인 선택이었다.
KMS 키는 서비스가 배포된 같은 리전에 두었다. KMS는 리전 단위로 관리되므로 저장 계층과 다른 리전에 키를 두면 지연과 운영 복잡도가 함께 늘어난다.
공통 모델을 만들어 서비스에 적용했다
공통 결제 라이브러리에 트랜잭션 엔티티 모델을 만들어 여러 벤더 서비스에서 공유하려 했다.
런타임 에러로 attribute naming 불일치를 발견했다
DynamoDB 조회 시 attribute를 찾지 못하는 에러가 발생해, 모델과 테이블 간 naming convention 차이를 추적했다.
코드를 테이블에 맞춰 수정했다
테이블의 PascalCase attribute에 맞게 모델에 명시적 @DynamoDbAttribute 매핑을 추가했다.
저장과 암호화를 함께 완성했다
데이터 구조를 고정한 뒤 동일한 운영 기준으로 KMS 암호화 정책을 붙였다.
실제 테이블 구조
AWS CloudFormation이란?
아래는 실제 구현을 그대로 옮긴 것이 아니라, 당시 구조를 설명하기 위한 개념 예시다.
Resources:
PaymentTransactionTable:
Type: AWS::DynamoDB::GlobalTable
Properties:
TableName: payment.vendor.transaction
BillingMode: PAY_PER_REQUEST
Replicas:
- Region: us-east-1
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
- Region: us-west-2
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: true
AttributeDefinitions:
- AttributeName: TransactionId
AttributeType: S
- AttributeName: RecordType
AttributeType: S
- AttributeName: OrderId
AttributeType: S
KeySchema:
- AttributeName: TransactionId
KeyType: HASH
- AttributeName: RecordType
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: OrderId-TransactionId-GSI
Projection:
ProjectionType: KEYS_ONLY
KeySchema:
- AttributeName: OrderId
KeyType: HASH
- AttributeName: TransactionId
KeyType: RANGE
TimeToLiveSpecification:
AttributeName: ExpireAt
Enabled: true
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES테이블은 us-east-1과 us-west-2에 GlobalTable로 복제되어 있다. attribute name이 모두 PascalCase(TransactionId, RecordType, OrderId)인 것을 볼 수 있다. 코드의 @DynamoDbAttribute 어노테이션은 이 테이블의 attribute name과 정확히 일치해야 한다.
저장 구조와 보안 정책을 함께 고정한 이유
데이터 모델과 암호화는 따로 결정할 수 있는 주제가 아니었다. 저장 규칙이 흔들리면 운영 일관성이 깨지고, 보안 정책이 늦어지면 배포 기준 자체가 불안정해지기 때문에 이 단계에서는 둘을 같은 설계 결정으로 묶어 다루는 편이 맞았다.
다음 글에서는 이런 설계 결정을 실제 협업과 배포 흐름에 붙이기 위해 API 문서 빌드를 어떻게 자동화했는지 이어서 본다.