Spring 개발일지

[팀프로젝트] MSA 기반 티켓팅 프로그램 - Kafka 토픽명 설정값 분리 / DB 인덱스 추가 / 테스트 코드 보완

김둘리 2026. 6. 1. 17:06

하드코딩된 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% 이상의 성능 개선을 확인했고 실제 운영 환경에서 데이터가 쌓일수록 인덱스의 효과는 더욱 커진다.