하드코딩된 Kafka 토픽명을 설정값으로 분리하고 DB 인덱스 추가 및 테스트 코드를 보완했다.
1. Kafka 토픽명 하드코딩 문제
기존에는 Kafka 토픽명이 코드에 하드코딩되어 있었다.
// PaymentCommandService.java
Events.publish(..., "payment.completed", ...);
Events.publish(..., "payment.failed", ...);
// PaymentKafkaConsumer.java
@KafkaListener(topics = "booking.refund.request", groupId = "payment-service")
토픽명이 변경되면 코드를 직접 수정하고 재배포해야 했다.
2. config-repo yml로 분리
payment-service.yml 공통 파일에 토픽명을 모아서 관리했다.
kafka:
topics:
payment-completed: payment.completed
payment-failed: payment.failed
payment-refund-completed: payment.refund.completed
booking-expired: booking.expired
booking-cancel-requested: booking.cancel.requested
booking-compensation: booking.payment.compensation
코드에서는 @Value로 주입받아서 사용한다.
// PaymentCommandService.java
@Value("${kafka.topics.payment-completed}")
private String paymentCompletedTopic;
@Value("${kafka.topics.payment-failed}")
private String paymentFailedTopic;
@Value("${kafka.topics.payment-refund-completed}")
private String paymentRefundCompletedTopic;
// 사용
Events.publish(..., paymentCompletedTopic, ...);
// PaymentKafkaConsumer.java
@KafkaListener(topics = "${kafka.topics.booking-expired}", groupId = "payment-service")
@KafkaListener(topics = "${kafka.topics.booking-cancel-requested}", groupId = "payment-service")
@KafkaListener(topics = "${kafka.topics.booking-compensation}", groupId = "payment-service")
토픽명이 변경되어도 config-repo yml만 수정하면 코드 변경 없이 반영된다.
3. DB 인덱스 추가
인덱스가 필요한 이유
DB 인덱스가 없으면 원하는 데이터를 찾기 위해 테이블 전체를 스캔한다.
인덱스 없음 → Full Scan (Seq Scan)
→ 테이블의 모든 행을 하나하나 확인
→ 데이터가 많을수록 느려짐
인덱스 있음 → Index Scan
→ 인덱스로 바로 원하는 행 위치 파악
→ 데이터가 많아도 빠름
책에서 원하는 내용을 찾을 때 목차(인덱스)가 없으면 처음부터 끝까지 읽어야 하고 목차가 있으면 바로 해당 페이지로 이동할 수 있는 것과 같다.
추가할 인덱스 선정
orderId → UNIQUE 제약으로 이미 인덱스 자동 생성 ✅
bookingId → UNIQUE 제약으로 이미 인덱스 자동 생성 ✅
추가로 필요한 것들
user_id → findAllByUserId() 자주 사용
status + requested_at → 스케줄러에서 만료 결제 조회
payment_id (history) → Payment JOIN 시 사용
PostgreSQL은 FK에 자동으로 인덱스를 생성하지 않아서 payment_id에 명시적으로 추가했다.
V3__add_payment_index.sql 마이그레이션 스크립트를 추가했다.
CREATE INDEX idx_payment_user_id
ON p_payment (user_id);
CREATE INDEX idx_payment_status_requested_at
ON p_payment (status, requested_at);
CREATE INDEX idx_payments_history_payment_id
ON p_payments_history (payment_id);
4. 인덱스 성능 측정
10만 건의 데이터를 기준으로 인덱스 추가 전/후 쿼리 속도를 EXPLAIN ANALYZE로 측정했다.
EXPLAIN ANALYZE는 PostgreSQL에서 쿼리 실행 계획과 실제 실행 시간을 보여주는 명령어다.
user_id 인덱스
EXPLAIN ANALYZE
SELECT * FROM p_payment WHERE user_id = '...';
인덱스 없을 때
Seq Scan on p_payment
Rows Removed by Filter: 100000
Execution Time: 15.957 ms
테이블 전체 10만 건을 스캔하고 조건에 맞는 1건을 찾았다.
인덱스 있을 때
Index Scan using idx_payment_user_id on p_payment
Index Searches: 1
Execution Time: 0.107 ms
인덱스로 바로 해당 행을 찾았다.
15.957ms → 0.107ms
약 99% 감소 (149배 향상)
status + requested_at 복합 인덱스
결제 만료 스케줄러에서 자주 실행되는 쿼리다.
EXPLAIN ANALYZE
SELECT * FROM p_payment
WHERE status = 'PENDING'
AND requested_at < NOW();
인덱스 없을 때
Seq Scan on p_payment
Rows Removed by Filter: 100001
Execution Time: 17.298 ms
인덱스 있을 때
Index Scan using idx_payment_status_requested_at on p_payment
Index Searches: 1
Execution Time: 0.076 ms
17.298ms → 0.076ms
약 99.6% 감소 (227배 향상)
복합 인덱스는 status와 requested_at 두 컬럼을 함께 인덱싱해서 두 조건을 동시에 만족하는 행을 빠르게 찾을 수 있다.
5. 성능 측정 결과 요약
쿼리인덱스 전인덱스 후개선율
| user_id 조회 | 15.957ms | 0.107ms | 99% 감소 (149배) |
| status + requested_at 조회 | 17.298ms | 0.076ms | 99.6% 감소 (227배) |
데이터가 많아질수록 인덱스의 효과는 더 커진다. 특히 결제 만료 스케줄러처럼 주기적으로 실행되는 배치 쿼리에 인덱스를 추가하면 서비스 전체 성능에 큰 영향을 준다.
6. 테스트 코드 보완
FINAL_FAILED 상태 추가 이후 테스트가 부족했다.
PaymentTest.java에 테스트 케이스를 추가했다.
@Test
@DisplayName("최종 실패 시 FINAL_FAILED 상태")
void final_fail_payment_status_final_failed() {
Payment payment = createPayment();
payment.fail(-1); // 이미 만료
payment.finalFail();
assertThat(payment.getStatus()).isEqualTo(PaymentStatus.FINAL_FAILED);
}
@Test
@DisplayName("FINAL_FAILED 상태에서 confirm 시 예외")
void confirm_final_failed_payment_throws_exception() {
Payment payment = createPayment();
payment.fail(-1);
payment.finalFail();
assertThatThrownBy(() -> payment.confirm("paymentKey", LocalDateTime.now()))
.isInstanceOf(PaymentException.class);
}
@Test
@DisplayName("선점 시간 만료 전에는 finalFail 호출 시 예외")
void final_fail_before_expired_throws_exception() {
Payment payment = createPayment();
payment.fail(300); // 아직 만료 안 됨
assertThatThrownBy(() -> payment.finalFail())
.isInstanceOf(PaymentException.class);
}
마무리
하드코딩은 언제나 기술 부채가 된다.
Kafka 토픽명처럼 변경 가능성이 있는 값은 설정값으로 관리해야 코드 변경 없이 유연하게 대응할 수 있다.
DB 인덱스는 처음부터 설계하는 것이 좋지만 지금처럼 Flyway 마이그레이션으로 추가하는 것도 좋은 방법이다.
10만 건 기준으로 99% 이상의 성능 개선을 확인했고 실제 운영 환경에서 데이터가 쌓일수록 인덱스의 효과는 더욱 커진다.
'Spring 개발일지' 카테고리의 다른 글
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - 모니터링 연동 (Prometheus + Zipkin) (0) | 2026.06.02 |
|---|---|
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - 사가 패턴 / 보상 트랜잭션 / DLQ 구현 (0) | 2026.05.29 |
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - 환불 완료 이벤트 발행 (0) | 2026.05.28 |
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - Dockerfile 및 CI workflow 작성 (0) | 2026.05.27 |
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - 결제 만료 스케줄러 구현 (0) | 2026.05.26 |