PaymentService
처럼JdbcTemplate
(스프링 6에는 좀 더 모던한 JdbcClient
가 추가되었다.)RestTemplate
(최근에는 RestClient
가 추가되어 더 모던한 API 스타일을 작성할 수 있게 되었다.)TransactionTemplate
public 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;
}
// ...
}