https://www.youtube.com/watch?v=DJCmvzhFVOI
위의 세미나를 유튜브로 시청한뒤 회사에서 마침 적용한 부분이 있어서 사례를 작성해 보려고 합니다.
1. 투표 기능
2. 외부 API 연동
1. 특정 날짜에 의존하지 않기 (투표 기능)
어느날 투표 기능을 할 수 있는 API를 추가해야 된다고 했을때 다음과 같은 요구 사항이 있다고 가정하겠습니다.
1. 해당 투표는 특정한 요일에만 정상 작동 되어야 한다.
그러면 다음과 같이 코드를 작성할 수 있습니다.
기존 코드
public class VoteService {
public String vote() {
LocalDateTime now = LocalDateTime.now();
if (now.getDayOfWeek() == DayOfWeek.THURSDAY) {
return "You can vote now!";
}
return "You can`t vote now!";
}
}
기존 테스트 코드
@DisplayName("투표 관련 기능")
public class VoteTest {
@Test
@DisplayName("투표 가능한 시간이면 투표한다.")
void test() {
VoteService voteService = new VoteService();
String response = voteService.vote();
assertThat(response).isEqualTo("You can vote now!");
}
}
여기서 문제는 무엇일까요?
일단 두가지 문제가 있습니다.
첫번째는 제어할 수 없는 현재 날짜에 의존하고 있다는 점이고
두번째, 이로 인해 항상 성공하는 테스트를 작성할 수 없다는 것입니다.
위의 코드는 테스트 실행일자가 목요일 당시에만 테스트를 통과할 수 있는 코드가 되므로 제어할 수 없는 것에 의존하는 코드가 되어버렸습니다.
세미나에서는 '비즈니스 로직에는 제어할 수 있는 코드로 영역을 최대한 늘리고 제어할 수 없는 코드는 외부로 밀어내야한다.'
이제 코드를 수정해보겠습니다.
변경된 코드
public class VoteService {
public String vote(LocalDateTime voteTime) {
if (voteTime.getDayOfWeek() == DayOfWeek.THURSDAY) {
return "You can vote now!";
}
return "You can`t vote now!";
}
}
위와 같은 코드로 변경되면 어떻게 될까요?
제어할 수 없는 현재 날짜를 외부(파라미터로 주입)로 밀어내면서 테스트 할 수 있는 코드로 변경 되었습니다.
여기서 외부로 밀어낸다는 개념은 현재 객체 VoteService를 사용하는 클라이언트(Controller, Test Code... 등등)로 제어할 수 없는 부분을 밀어내어 내가 제어할 수 있는 코드로 비즈니스 영역을 최대한 응집시킨다는 의미입니다.
변경된 테스트 코드
@DisplayName("투표 관련 기능")
public class VoteTest {
@Test
@DisplayName("투표 가능한 시간이면 투표한다.")
void test() {
VoteService voteService = new VoteService();
String response = voteService.vote(LocalDateTime.of(2023, 11, 9, 0, 0));
assertThat(response).isEqualTo("You can vote now!");
}
}

외부에서 투표하는 날짜를 주입시켜 항상 통과하는 테스트 코드가 되었습니다.
2. 외부 API에 의존하지 않기
요즘에는 많은 솔루션들이 있어서 API 연동을 통한 개발을 많이 하게 됩니다.
외부 API에 대한 테스트 작성 시 매번 드는 비용 + 외부 업체의 에러 + 네트워크 통신 불량 등의 이유로 실제 API를 호출하는 것은 비용도 그렇고 매번 성공한다는 보장이 없는 테스트 코드가 된다.
기존 코드
public class ExternalAPI {
private String externalApi() {
Random random = new Random();
int result = random.nextInt(10);
if (result > 5) {
return "error";
}
return "ok";
}
public String callExternalApi() {
return externalApi();
}
}
기존 테스트 코드
@DisplayName("외부 API 연동 관련 기능 테스트")
public class ExternalAPITest {
@DisplayName("외부 API 연동")
@Test
void test() {
ExternalAPI externalAPI = new ExternalAPI();
String response = externalAPI.callExternalApi();
assertThat(response).isEqualTo("ok");
}
}
여기서 랜덤값은 네트워크, 리스폰스, 비용등을 의미합니다.
위의 코드가 항상 성공을 보장할 수 있는 코드 일까요?
랜덤 값에 따라 5보다 크면 에러 작으면 성공을 반환하는 코드입니다.
제어할 수 없는 외부 API에 의존하고 있는 것을 발견합니다.
그리고 기존 코드는 private 메서드를 사용합니다. 이렇게 private 메서드를 테스트 한다는 것은 이미 클래스가 이미 너무 많은 책임을 가지고 있다는 의미를 가집니다.
변경된 코드
public class ExternalAPI {
private final ExternalAPIInterface externalApi;
public ExternalAPI(ExternalAPIInterface externalApi) {
this.externalApi = externalApi;
}
interface ExternalAPIInterface {
String externalApi();
}
static class DefaultExternalAPI implements ExternalAPIInterface {
@Override
public String externalApi() {
Random random = new Random();
int result = random.nextInt(10);
if (result > 5) {
return "error";
}
return "ok";
}
}
public String callExternalApi() {
return externalApi.externalApi();
}
}
변경된 테스트 코드
@DisplayName("외부 API 연동 관련 기능 테스트")
public class ExternalAPITest {
@DisplayName("외부 API 연동")
@Test
void test() {
String ok = "ok";
ExternalAPI.ExternalAPIInterface apiInterface = Mockito.mock(ExternalAPI.DefaultExternalAPI.class);
Mockito.when(apiInterface.externalApi()).thenReturn(ok);
ExternalAPI externalAPI = new ExternalAPI(apiInterface);
String response = externalAPI.callExternalApi();
assertThat(response).isEqualTo(ok);
}
}
변경된 코드에서는 스프링의 PSA 와 DI를 통해 제어할 수 없는 외부 환경에 대한 부분에서는 mock객체로 주입 시켜 항상
성공 할 수 있는 코드로 변경할 수 있습니다.
예제를 편하게 작성하기 위해 inner class를 사용하였습니다.
PSA(Potable Service Abstaction)
PSA란 환경의 변화와 관계없이 일관된 방식의 기술로의 접근 환경을 제공하는 추상화 구조를 말하고
PSA가 적용된 코드라면 나의 코드가 바뀌지 않고, 다른 기술로 간편하게 바꿀 수 있도록 확장성이 좋고,
기술에 특화되어 있지 않는 코드를 의미합니다.
위의 코드에서 PSA는 인터페이스로 두어 테스트 하기 어려운 코드를 테스트 하기 좋은 코드로 만들 수 있습니다.
DI(Dependency Injection)
DI란 객체를 직접 생성하는게 아닌 외부에서 생성한 후 주입 시켜주는 방식이다.
이번 인프콘도 떨어지고..실제로 가서 보고 싶다..
'Spring Boot' 카테고리의 다른 글
MongoDB 테스트하기(TestContainer 및 memory DB) (0) | 2024.01.08 |
---|---|
[GitHub Actions + Spring Boot + Nginx + Slack Notification] 무중단 배포 CI/CD 구축하기 (0) | 2023.11.22 |
QueryDSL 다중 DB 설정하기 (0) | 2023.06.27 |
[디자인패턴] Spring 에서 사용되는 프록시, 데코레이터 패턴 (0) | 2023.04.30 |
[Git] Permission denied (publickey). fatal- Could not read from remote repository (0) | 2022.10.09 |
댓글