⌨️ 외부 API 호출에 대한 유연한 테스트 환경 구축하기
어찌어찌 취직은 했다, 작은 SI 기업이지만 첫 발을 디뎠다는 것에 의미를 두고 싶다.
실무에 들어와서 코드를 받으니 이런 말을 많이 들었다.
그 기능은 외부 API라서 테스트가 안 돼요
실제로 일을 하면 생각보다 뜻대로 안 되는 일이 많다.
이런 간단한 불만 사항도 맘대로 수정할 수가 없다. 하물며 해결책을 알아도 말이다.
어찌 됐든 일단 문제없이 돌아가는 서비스 코드를 수정하는 것이고 아무리 SM이더라도 현실적으로는 공수 없이 무료 봉사할 일은 아니라는 의견이 전적이다. (주관은 아니지만)
개인적으로는 지속적인 관리를 위해서 할 수 있는 역량이 있다면 해결하고 가는 것을 선호하긴 한다.
1. 외부 API 호출과의 문제점
우선 실제 개발에서 외부 API 호출과의 문제점은 개인적으로는 크게 두 개를 생각해 볼 수 있다.
1. 우리 개발 속도와 달리 외부 API가 준비가 미흡한 경우
- 많지 않은 경우지만 개발하고 있는 서비스 혹은 프로젝트에 외부 API 연동이 아직 안 됐는데 테스트가 필요하거나 후속 기능을 구현해야 할 경우가 생긴다.
2. on-demand 서비스 혹은 토큰의 제한이 있어 호출 횟수의 제약이 따르는 경우
- 이 또한 많은 경우는 아니지만 위의 경우와 달리 보통 이 경우에서 더 고민이 많이 될 것이다.
왜냐면 피부로 과금이 느껴지기 때문이다..
예를 들어 S3나 RDS를 프리티어가 아닌 경우를 생각해 보자 개발할 때 수많은 요청과 다양한 테스트가 시도될 터인데 그때마다 그 모든 통신이 과금으로 들어간다면..? 끔찍하다..
나는 위와 같은 이유들로 Spring PSA를 활용하여 외부 API 의존성을 제거한 테스트 전용 클래스를 구현하는 것을 선호한다.
1.1. Spring PSA
PSA(Poratable Service Abstraction) : Spring 프레임워크에서 외부 서비스와의 통합을 추상화한 계층이다.
이를 통해 테스트하기 어려운 외부 API 호출 로직을 추상화할 수 있다.
대표적인 예로 Spring Data JPA 같은 경우가 있다
1.2. 구현 방법
1.2.1. 인터페이스 정의
외부 API 호출을 추상화한 인터페이스를 정의한다.
public interface ExternalService {
ApiResponse callApi(RequestData requestData);
}
1.2.2. PSA를 통한 구현체 선택
Configuration을 통해 PSA를 활용하여 사용할 구현체를 선택한다.
◈구현체에서 직접 프로필을 선택하지 않고 Config 작성을 하는 이유?
1.
단일 책임 원칙(Single Responsibility Principle) 위반: 구현 클래스는 단순히 해당 기능을 구현하는 역할을 수행해야 합니다. 빈 등록은 구현 클래스의 책임 범위를 벗어나므로 단일 책임 원칙을 위반하게 됩니다.
2.
의존성 역전 원칙(Dependency Inversion Principle) 위반: 구현 클래스가 빈 등록을 담당하면 의존성이 역전되지 않습니다. 즉, 클래스가 자신의 의존성을 직접 관리하므로 외부에서 주입되는 의존성에 대한 제어권이 클래스 내부에 위치하게 됩니다.
3.
테스트 용이성 저하: 구현 클래스에서 빈 등록을 하면 테스트 시에 해당 클래스를 Mock으로 대체하기가 어려워집니다. 테스트 시에는 주입된 의존성을 모의 객체로 대체하는 것이 일반적이지만, 구현 클래스에서 빈을 직접 등록하면 이러한 모의 객체 주입이 어려워집니다.
---
이러한 이유로 Config를 작성하여 사용하는 것을 권장한다.
1.2.3. 테스트용 구현체 작성
public class TestExternalServiceImpl implements ExternalService {
@Override
public ApiResponse callApi(RequestData requestData) {
// 모의 객체를 사용하여 원하는 결과 반환
return new ApiResponse("Mock response");
}
}
1.2.4. 테스트 작성
@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("test")
public class MyServiceTest {
@Autowired
private MyService myService;
@Test
public void testApiCall() {
// 외부 API 호출 테스트
ApiResponse response = myService.callExternalApi(new RequestData());
// 테스트 결과 검증
assertNotNull(response);
assertEquals("Mock response", response.getData());
}
}
1.2.5. 결론
이렇듯 PSA를 활용하여 외부 API 호출을 추상화하고 테스트가 가능한 코드를 작성할 수 있다.
실제 서버를 Run 할 때에도 기대되는 Return값을 Stub으로 구현하여 테스트 환경에서도 사용 가능한 기능 또한 구현이 가능하다.
★ Interface와 PSA의 차이
언뜻 보면 어차피 인터페이스로 구현체를 다르게 구현하는 거 아니야? 뭐가 다르길래 PSA라는 명칭까지 주어진 거야?라는 생각이 들었다.
해당 의문에 대해서는 간단한 코멘트로 답변을 받았다
PSA -> 개발자가 여러 종류의 서비스에 동일한 코드를 사용할 수 있도록 하는 추상화 계층
이를 이용하면 특정 서비스에 의존하지 않고 일관된 방식으로 여러 서비스를 다룰 수 있다.
JPA의 예를 생각해 보면 이런 느낌이다 우리 프로젝트에서 세금 계산서를 발행해 주는 업체 A를 선정하여 그쪽의 API를 받았으나 업체 B를 추가하고 싶은 경우, 로직적으로 우리가 제공해주어야 할 Reutrn값은 그대로이지만 외부 API의 변경으로 서비스의 변화는 생긴다. 그러나 이 경우 우리의 프로젝트는 어찌 되었는 PSA로 설계한 인터페이스를 주입받고 있으므로 외부 API 변경에 따른 내부 로직의 변화는 크지 않다.
Interface는 특정한 기능이나 동작을 수행하기 위한 코드를 추상화하는 데 사용된다
개념적으로 PSA를 더 넓게 그리고 조금은 더 방법적인 것으로 생각하면 될 것 같다.