PaymentService처럼JdbcTemplate (스프링 6에는 좀 더 모던한 JdbcClient가 추가되었다.)RestTemplate (최근에는 RestClient가 추가되어 더 모던한 API 스타일을 작성할 수 있게 되었다.)TransactionTemplatepublic class WebApiExRateProvider extends ExRateProvider {
@Override
BigDecimal getExRate(String currency) throws IOException {
// 네트워크 호출을 통해 환율을 가져오는 로직
// ...
return exRate;
}
}
WebApiExRateProvider는 네트워크 통신을 수행한다.IOEXception이 체크 예외로 발생할 수 있기에 기존에는 위처럼 예외를 메서드 밖으로 던졌다.ExRateProvider 인터페이스만 본다면 네트워크 통신을 해야하니 IOException 난다는 사실을 모른다.
IOException이 필요 없을 수 있다.WebApiExRateProvider는 IOException을 적절히 처리해야 한다.
IOException이 아닌)ExRateProvider 외부에선 try/catch나 throws를 선언하지 않아도 된다.
PaymentService 쪽의 관심사에선 분리하는 거에 의미가 있다.public class WebApiExRateProvider extends ExRateProvider {
@Override
BigDecimal getExRate(String currency) {
String url = "https://open.er-api.com/v6/latest/" + currency;
URI uri;
uri = // 1. URI를 준비하고 예외 처리를 위한 작업을 하는 코드
try {
// 2. API를 실행하고 서버로부터 받은 응답을 가져오는 코드
} catch (IOException e) {
throw new RuntimeException(e)
}
try {
// 3. JSON 문자열을 파싱하고 필요한 환율 정보를 추출하는 코드
} catch (JsonProcessingException e) {
throw new RuntimeException(e)
}
return exRate;
}
}
public class WebApiExRateProvider extends ExRateProvider {
@Override
BigDecimal getExRate(String currency) {
String url = "https://open.er-api.com/v6/latest/" + currency;
URI uri;
uri = // 1. URI를 준비하고 예외 처리를 위한 작업을 하는 코드
String response;
try {
response = executeApi(uri);
} catch (IOException e) {
throw new RuntimeException(e)
}
try {
return extractExRate(response)
} catch (JsonProcessingException e) {
throw new RuntimeException(e)
}
}
private BigDecimal extractExRate(String response) throws JsonProcessingException {
return // 3. JSON 문자열을 파싱하고 필요한 환율 정보를 추출하는 코드
}
private String executeApi(URI uri) throws IOException {
return // 2. API를 실행하고 서버로부터 받은 응답을 가져오는 코드
}
}
getExRate 메서드 내부의 코드는 2, 3번이 변화해도 바뀌지 않게 되었다.
url 변수로 URI를 생성하는 부분부터 변하지 않는 부분public class WebApiExRateProvider extends ExRateProvider {
@Override
BigDecimal getExRate(String currency) {
String url = "https://open.er-api.com/v6/latest/" + currency;
return runApiForExRate(url);
}
private BigDecimal runApiForExRate(String url) {
// 변하지 않는 코드
}
private BigDecimal extractExRate(String response) throws JsonProcessingException {
return // 3. JSON 문자열을 파싱하고 필요한 환율 정보를 추출하는 코드
}
private String executeApi(URI uri) throws IOException {
return // 2. API를 실행하고 서버로부터 받은 응답을 가져오는 코드
}
}
runApiForExRate는 변하지 않는, 탬플릿이 되었다.WebApiExRateProvider 클래스가 바뀌게 된다.excuteApi 메서드를 인터페이스로 분리public interface ApiExecutor {
String execute(URI uri) throws IOException;
}
public class SimpleApiExecutor implements ApiExecutor {
@Override
public String execute(URI uri) throws IOException {
// ...
}
}
ApiExecutor를 전달해보자.
public class WebApiExRateProvider extends ExRateProvider {
@Override
public BigDecimal getExRate(String currency) {
String url = "https://open.er-api.com/v6/latest/" + currency;
return runApiForExRate(url, new SimpleApiExecutor());
}
private BigDecimal runApiForExRate(String url, ApiExecutor apiExecutor) {
// ...
String response;
try {
response = apiExecutor.execute(uri);
} catch (IOException e) {
throw new RuntimeException(e)
}
// ...
}
//...
}
extractExRate 메소드도 똑같이 인터페이스로 만들고 구현체를 메소드 주입으로 전달해보자.public class WebApiExRateProvider extends ExRateProvider {
@Override
public BigDecimal getExRate(String currency) {
String url = "https://open.er-api.com/v6/latest/" + currency;
return runApiForExRate(url, new SimpleApiExecutor(), new ErApiExRateExtractor());
}
private BigDecimal runApiForExRate(String url, ApiExecutor apiExecutor, ExRateExtractor extractor) {
// 변하지 않는 탬플릿에서 변하는 콜백을 전달 받아 호출
}
}
ApiTemplate이라 정의해보자.
public class ApiTemplate {
public BigDecimal getExRate(String url, ApiExecutor apiExecutor, ExRateExtractor extractor) {
URI uri;
try {
uri = new URI(url);
} catch(URISyntaxException e) {
throw new RuntimeException(e);
}
String response;
try {
response = apiExecutor.execute(uri);
} catch (IOException e) {
throw new RuntimeException(e)
}
try {
return extractor.extract(response)
} catch (JsonProcessingException e) {
throw new RuntimeException(e)
}
}
}
WebApiExRateProvider에서 ApiTemplate을 사용해보자.public class WebApiExRateProvider extends ExRateProvider {
private final ApiTemplate apiTemplate = new ApiTemplate();
@Override
public BigDecimal getExRate(String currency) {
String url = "https://open.er-api.com/v6/latest/" + currency;
return apiTemplate.getExRate(url, new SimpleApiExecutor(), new ErApiExRateExtractor());
}
}
WebApiExRateProvider에서 구현체를 명시적으로 바꾸는 수밖에 없다.public class WebApiExRateProvider extends ExRateProvider {
private final ApiTemplate apiTemplate = new ApiTemplate();
@Override
public BigDecimal getExRate(String currency) {
String url = "https://open.er-api.com/v6/latest/" + currency;
return apiTemplate.getExRate(url, new HttpClientApiExecutor(), new ErApiExRateExtractor());
}
}
public class ApiTemplate {
private final ApiExecutor apiExecutor;
private final ExRateExtractor exRateExtractor;
public ApiTemplate() {
this.apiExecutor = new HttpClientApiExecutor();
this.exRateExtractor = new ErApiExtractor();
}
public BigDecimal getExRate(String url) {
// ...
}
public BigDecimal getExRate(String url, ApiExecutor apiExecutor, ExRateExtractor extractor) {
// ...
}
}
url만 전달해서 탬플릿을 사용할 수 있다.public class WebApiExRateProvider extends ExRateProvider {
private final ApiTemplate apiTemplate = new ApiTemplate();
@Override
public BigDecimal getExRate(String currency) {
String url = "https://open.er-api.com/v6/latest/" + currency;
return apiTemplate.getExRate(url);
}
}
ApiTemplate을 스프링 빈 주입 받도록 설정해보자@Configuration
public class ObjectFactory {
// ...
@Bean
public ExRateProvider exRateProvider() {
return new WebApiExRateProvider(apiTemplate());
}
@Bean
public ApiTemplate apiTemplate() {
return new ApiTemplate();
}
}
public class WebApiExRateProvider extends ExRateProvider {
private final ApiTemplate apiTemplate;
public WebApiExRateProvider(ApiTemplate apiTemplate) {
this.apiTemplate = apiTemplate;
}
@Override
public BigDecimal getExRate(String currency) {
String url = "https://open.er-api.com/v6/latest/" + currency;
return apiTemplate.getExRate(url);
}
}
public class ApiTemplate {
private final ApiExecutor apiExecutor;
private final ExRateExtractor exRateExtractor;
public ApiTemplate() {
this.apiExecutor = new HttpClientApiExecutor();
this.exRateExtractor = new ErApiExtractor();
}
public ApiTemplate(ApiExecutor apiExecutor, ExRateExtractor extractor) {
this.apiExecutor = apiExecutor;
this.exRateExtractor = extractor;
}
// ...
}