어댑터 패턴 완벽 정리: Spring Boot 외부 API 연동을 분리하는 방법
도입부
외부 API 호출을 Service에 바로 넣으면 공급자의 DTO와 상태 코드가 비즈니스 로직까지 퍼집니다. 외부 필드가 바뀌거나 업체를 교체할 때 주문 서비스도 수정해야 합니다.
어댑터 패턴은 맞지 않는 인터페이스를 연결합니다. 애플리케이션은 내부 인터페이스만 알고, 어댑터가 외부 요청·응답·예외를 변환합니다.
어댑터 패턴이란?
어댑터 패턴에는 세 역할이 있습니다.
- Target: 우리 애플리케이션이 사용하려는 인터페이스
- Adaptee: 인터페이스가 맞지 않는 외부 SDK나 API
- Adapter: Target을 구현하고 외부 호출과 변환을 담당하는 객체
핵심은 주문 서비스가 특정 결제사 클래스와 상태 문자열을 모르게 하는 것입니다.
내부 인터페이스부터 정의한다
외부 API가 아니라 우리 서비스가 필요한 기능을 기준으로 인터페이스를 만듭니다.
public interface PaymentGateway {
PaymentResult pay(PaymentCommand command);
}
public record PaymentCommand(Long orderId, long amount) {
}
public record PaymentResult(
String paymentId,
PaymentStatus status
) {
}
public enum PaymentStatus {
SUCCEEDED, FAILED
}
특정 업체 이름보다 PaymentGateway처럼 비즈니스 역할로 이름을 정합니다.
외부 API용 어댑터 구현
외부 결제사의 형식을 아는 코드는 어댑터 한 곳에 모읍니다.
@Component
public class SamplePayAdapter implements PaymentGateway {
private final RestClient restClient;
public SamplePayAdapter(RestClient.Builder builder) {
this.restClient =
builder.baseUrl("https://api.sample-pay.example").build();
}
@Override
public PaymentResult pay(PaymentCommand command) {
SamplePayRequest request = new SamplePayRequest(
command.orderId().toString(), command.amount()
);
try {
SamplePayResponse response = restClient.post()
.uri("/v1/payments")
.body(request)
.retrieve()
.body(SamplePayResponse.class);
PaymentStatus status = "APPROVED".equals(response.status())
? PaymentStatus.SUCCEEDED
: PaymentStatus.FAILED;
return new PaymentResult(response.transactionId(), status);
} catch (RestClientException e) {
throw new PaymentGatewayException("외부 결제 요청 실패", e);
}
}
}
SamplePayRequest와 SamplePayResponse 같은 외부 DTO도 인프라 계층에 둡니다. API 키는 환경 변수나 비밀 저장소에서 주입하고, 타임아웃·인증·공급자별 오류 변환도 어댑터 경계에서 처리합니다.
주문 서비스는 내부 언어만 사용한다
PaymentResult result = paymentGateway.pay(
new PaymentCommand(order.getId(), order.getTotalAmount())
);
if (result.status() == PaymentStatus.FAILED) {
throw new PaymentFailedException(order.getId());
}
order.completePayment(result.paymentId());
생성자 주입을 사용하면 다른 결제사나 테스트용 구현체로 쉽게 교체할 수 있습니다. 여러 결제 방식을 선택한다면 전략 패턴으로 if-else 걷어내기의 Map 주입을 함께 사용할 수 있습니다.
테스트가 단순해진다
비즈니스 테스트에는 가짜 PaymentGateway를 주입하고, 어댑터 통합 테스트에서 URL·헤더·JSON·오류 매핑만 검증합니다.
비슷한 패턴과 차이
| 패턴 | 핵심 목적 | 외부 API 예시 |
|---|---|---|
| Adapter | 인터페이스 변환 | 외부 응답을 내부 결과로 변환 |
| Facade | 복잡한 기능을 단순화 | 결제·영수증 호출을 하나로 묶음 |
| Proxy | 같은 인터페이스의 호출 통제 | 권한 검사, 캐시, 로깅 |
| Strategy | 여러 구현 중 하나를 선택 | 카드·포인트 결제 선택 |
프록시는 같은 인터페이스의 호출을 통제하고 어댑터는 다른 인터페이스를 연결합니다. 자세한 구조는 프록시 패턴 완벽 이해에서 확인할 수 있습니다. 결제 후 이메일·포인트 적립 분리는 옵저버 패턴과 Spring ApplicationEvent의 이벤트 방식이 적합합니다.
실무 적용 방식
application에는 PaymentGateway, domain에는 PaymentStatus, infrastructure/samplepay에는 어댑터와 외부 DTO를 둡니다. 내부 계층은 외부 DTO를 import하지 않고, infrastructure가 내부 인터페이스를 구현합니다.
상태를 바꾸는 요청은 멱등성 키를 적용한 뒤 재시도 여부를 결정해야 합니다.
자주 하는 실수
- 외부 DTO를 Controller 응답이나 엔티티로 그대로 사용합니다.
- 내부 인터페이스를 외부 SDK와 똑같이 복제합니다.
- HTTP 상태나 SDK 예외를 비즈니스 계층까지 그대로 던집니다.
- 타임아웃과 멱등성 없이 재시도부터 적용합니다.
핵심 요약
- 어댑터는 서로 다른 인터페이스 사이에서 요청·응답·예외를 변환합니다.
- 비즈니스 계층은 내부 인터페이스에만 의존해야 합니다.
- 외부 DTO와 상태 코드는 어댑터 안에 가둡니다.
- 생성자 주입으로 실제 어댑터와 가짜 구현체를 교체할 수 있습니다.
- Adapter는 변환, Strategy는 선택, Proxy는 통제가 핵심입니다.
마무리
외부 시스템의 언어는 어댑터에서 끝내고 비즈니스 로직은 내부 모델로만 말하게 해야 공급자 변경과 장애 테스트에 강한 구조를 유지할 수 있습니다.
최종 확인: 2026-07-28
참고 자료: Spring Framework REST Clients 공식 문서, Spring Framework 의존성 주입 공식 문서
'Programming > Design Pattern' 카테고리의 다른 글
| 빌더 패턴 vs Lombok @Builder, 언제 직접 구현할까 (0) | 2026.08.07 |
|---|---|
| 옵저버 패턴, Spring ApplicationEvent로 실무에 써먹기 (0) | 2026.07.23 |
| 전략 패턴으로 if-else 걷어내기 (0) | 2026.07.16 |
| 📌Java static vs Singleton 완벽 비교: 언제 어떤 걸 써야 할까? (1) | 2025.07.31 |
| ✅ PRG(Post-Redirect-Get) 패턴 – 새로고침 중복방지와 공유 가능한 웹 설계의 핵심 (1) | 2022.10.31 |