자바로 구현하는 결제 엔진 핵심 설계

결제 도메인 모델링

전자상거래 시스템의 핵심인 결제 모듈을 자바로 구현해본다. 실무에서 자주 마주치는 다양한 결제 수단 통합, 잔액 검증, 예외 상황 처리 등을 객체지향 설계 원칙에 따라 구성한다.

거래 기록 엔티티

결제의 대상이 되는 거래 정보를 담는 클래스다. 불변성을 위해 필드는 final로 선언하고, 빌더 패턴을 적용해 유연한 객체 생성을 지원한다.

public final class Transaction {
    private final String txId;
    private final String itemTitle;
    private final BigDecimal price;

    private Transaction(Builder builder) {
        this.txId = builder.txId;
        this.itemTitle = builder.itemTitle;
        this.price = builder.price;
    }

    public static Builder builder() {
        return new Builder();
    }

    public String getTxId() { return txId; }
    public String getItemTitle() { return itemTitle; }
    public BigDecimal getPrice() { return price; }

    public static class Builder {
        private String txId;
        private String itemTitle;
        private BigDecimal price;

        public Builder txId(String val) {
            this.txId = val;
            return this;
        }

        public Builder itemTitle(String val) {
            this.itemTitle = val;
            return this;
        }

        public Builder price(BigDecimal val) {
            this.price = val;
            return this;
        }

        public Transaction build() {
            return new Transaction(this);
        }
    }
}

결제 수단 추상화

다양한 결제 수단을 동일한 규약으로 처리하기 위해 함수형 인터페이스를 정의한다. 실제 결제 로직은 각 구현체에서 담당한다.

@FunctionalInterface
public interface PaymentProcessor {
    PaymentResult process(Transaction transaction) throws PaymentDeclinedException;
}

카카오페이 구현체

모바일 간편결제를 시뮬레이션하는 구현체다. 외부 연동 부분은 로그 출력으로 대체하되, 잔액 부족 시 명시적 예외를 발생시킨다.

public class KakaoPayGateway implements PaymentProcessor {
    private final BigDecimal accountLimit;

    public KakaoPayGateway(BigDecimal initialLimit) {
        this.accountLimit = initialLimit;
    }

    @Override
    public PaymentResult process(Transaction tx) throws PaymentDeclinedException {
        if (tx.getPrice().compareTo(accountLimit) > 0) {
            throw new PaymentDeclinedException(
                "한도 초과: 현재 잔액 " + accountLimit + "원, 요청 금액 " + tx.getPrice() + "원"
            );
        }
        
        System.out.printf("[카카오페이] %s 상품 %s원 결제 승인%n", 
            tx.getItemTitle(), tx.getPrice());
        
        return new PaymentResult(tx.getTxId(), Instant.now(), PaymentStatus.APPROVED);
    }
}

토스페이 구현체

또 다른 간편결제 수단으로, 할인 정책을 추가해 차별화한다.

public class TossPayGateway implements PaymentProcessor {
    private static final BigDecimal DISCOUNT_THRESHOLD = new BigDecimal("50000");
    private static final BigDecimal DISCOUNT_RATE = new BigDecimal("0.05");

    @Override
    public PaymentResult process(Transaction tx) throws PaymentDeclinedException {
        BigDecimal finalAmount = applyDiscount(tx.getPrice());
        
        System.out.printf("[토스페이] %s → 할인 적용 후 %s원 결제 처리%n",
            tx.getItemTitle(), finalAmount);
        
        return new PaymentResult(tx.getTxId(), Instant.now(), PaymentStatus.APPROVED);
    }

    private BigDecimal applyDiscount(BigDecimal original) {
        if (original.compareTo(DISCOUNT_THRESHOLD) >= 0) {
            return original.multiply(BigDecimal.ONE.subtract(DISCOUNT_RATE))
                        .setScale(0, RoundingMode.HALF_UP);
        }
        return original;
    }
}

커스텀 예외 계층

결제 거절 상황을 명확히 표현하는 검사 예외다. 호출자가 반드시 처리하도록 강제한다.

public class PaymentDeclinedException extends Exception {
    public PaymentDeclinedException(String reason) {
        super(reason);
    }
}

결제 결과 값 객체

public record PaymentResult(
    String transactionRef,
    Instant processedAt,
    PaymentStatus status
) {}
public enum PaymentStatus {
    PENDING, APPROVED, REJECTED, REFUNDED
}

실행 흐름 검증

클라이언트 코드에서 동적으로 결제 수단을 선택하고, 예외 상황을 우아하게 처리한다.

public class CheckoutService {
    public static void main(String[] args) {
        Transaction order = Transaction.builder()
            .txId("TX-" + UUID.randomUUID())
            .itemTitle("스프링 부트 완벽 가이드")
            .price(new BigDecimal("55000"))
            .build();

        PaymentProcessor selectedMethod = resolvePaymentMethod(args);

        try {
            PaymentResult result = selectedMethod.process(order);
            System.out.println("✅ 결제 완료: " + result);
        } catch (PaymentDeclinedException ex) {
            System.err.println("❌ 결제 실패: " + ex.getMessage());
            notifyCustomerSupport(order, ex);
        }
    }

    private static PaymentProcessor resolvePaymentMethod(String[] args) {
        if (args.length > 0 && "TOSS".equalsIgnoreCase(args[0])) {
            return new TossPayGateway();
        }
        return new KakaoPayGateway(new BigDecimal("100000"));
    }

    private static void notifyCustomerSupport(Transaction failed, Throwable cause) {
        System.out.printf("[알림] 주문 %s 결제 오류: %s%n", failed.getTxId(), cause.getMessage());
    }
}

실행 시 커맨드라인 인자로 TOSS를 전달하면 토스페이, 그 외에는 카카오페이가 선택된다. 금액이 카카오페이 한도를 초과하면 PaymentDeclinedException이 발생하며, 고객 지원 알림 로직이 실행된다.

태그: java Payment Gateway BigDecimal Builder Pattern Functional Interface

6월 29일 04:25에 게시됨