#스프링 #강의

[토비의 스프링 6] 템플릿

1jeongg 1jeongg Follow 2024년 06월 29일 · 8 mins read
Share this

템플릿

코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분(템플릿)을 자유롭게 변경되는 성질을 가진 부분(콜백)으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법

리팩토링시 기능이 변경되면 안된다. 버그가 발생하면 안됨.

템플릿

템플릿은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀
고정된 틀 안에 바꿀 수 있는 부분을 넣어서 사용하도록 만들어진 오브젝트


템플릿 메소드 패턴

템플릿 메소드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼 클래스에 두고,
바뀌는 부분을 서브 클래스의 메소드에 두는 구조로 이뤄진다.

이렇게 변경성이 높은 부분과 낮은 부분을 분리해준다.

@Component
public class WebApiExRateProvider implements ExRateProvider {

    @Override
    public BigDecimal getExRate(String currency) {
        // URI를 준비하고 예외 처리를 위한 작업을 하는 코드 - 코드의 기본 틀이므로 잘 변경되지 않음
        String url = "https://open.er-api.com/v6/latest/" + currency;
        return runApiForExRate(url);
    }
    
    // 변경되지 않는 부분
    private static BigDecimal runApiForExRate(String url) {
        URI uri;
        try {
            uri = new URI(url);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        // API를 실행하고 서버로부터 받은 응답을 가져오는 코드 - 기술 및 방법이 변경될 수 있음
        String response;
        try {
            response = executeApi(uri);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // JSON 문자열을 파싱하고 필요한 환율 정보를 추출하는 코드 - JSON 구조에 따라 정보 추출 방식 변경될 수 있음
        try {
            return extractExRate(response);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    // 변경 가능성이 높은 부분
    private static BigDecimal extractExRate(String response) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        ExRateData data = mapper.readValue(response, ExRateData.class);
        return data.rates().get("KRW");
    }

    private static String executeApi(URI uri) throws IOException {
        String response;
        HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
        try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
            response = br.lines().collect(Collectors.joining());
        }
        return response;
    }
}

콜백

콜백은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메서드를 실행시키는 것이 목적이다.
하나의 메소드를 가진 인터페이스 타입(SAM)의 오브젝트 또는 람다 프로젝트

다른 코드의 인수로서 넘겨주는 실행 가능한 코드로 즉시 실행할 수도, 나중에 실행할 수도 있다
즉, 코드가 호출은 되는데 코드를 넘겨준 곳의 뒤에서 실행된다

템플릿/콜백은 전략 패턴의 특별한 케이스: 메소드 하나만 가진 전략 인터페이스를 사용하는 전략 패턴

  • 템플릿은 전략 패턴의 컨텍스트
  • 콜백은 전략 패턴의 전략

메소드 주입이란 의존 오브젝트가 메소드 호출 시점에 파라미터로 전달되는 방식을 말한다.

작업 흐름

다음과 같이 클라이언트 - 콜백 - 템플릿에 따라 분리하고 ApiExecutor, ExRateExecutor 콜백을 생성하여 깔끔하게 바꿀 수 있다.


@Component
public class WebApiExRateProvider implements ExRateProvider {

    // <<클라이언트>>
    @Override
    public BigDecimal getExRate(String currency) {
        // URI를 준비하고 예외 처리를 위한 작업을 하는 코드 - 코드의 기본 틀이므로 잘 변경되지 않음
        String url = "https://open.er-api.com/v6/latest/" + currency;
        // <<콜백>>
        // 이후 다른 기술로 전활할 때 파라매터만 바꿔주면 됨
        // 익명 클래스를 사용해서 그 안에 body만 작성해서 사용할 수 있음
        return runApiForExRate(url, new SimpleApiExecutor(), new ERApiExRateExtractor());
    }
    // <<템플릿>>
    // 잘 변경되지 않는 부분
    private static BigDecimal runApiForExRate(String url, ApiExecutor apiExecutor, ExRateExtractor exRateExtractor) {
        /* try-catch 3개(위 코드랑 동일) */
    }
}

public interface ApiExecutor {
    String execute(URI uri) throws IOException;
}

public interface ExRateExtractor {
    BigDecimal extract(String response) throws JsonProcessingException;
}

public class ERApiExRateExtractor implements ExRateExtractor{
    @Override
    public BigDecimal extract(String response) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        ExRateData data = mapper.readValue(response, ExRateData.class);
        return data.rates().get("KRW");
    }
}

public class SimpleApiExecutor implements ApiExecutor{
    @Override
    public String execute(URI uri) throws IOException {
        String response;
        HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
        try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
            response = br.lines().collect(Collectors.joining());
        }
        return response;
    }
}

Api Template 분리

환율정보 API로부터 환율을 가져오는 기능을 제공하는 오브젝트
API 호출과 정보 추출의 기본 틀 제공
두 가지 콜백을 이용
유사한 여러 오브젝트에서 재사용 가능

@Component
public class WebApiExRateProvider implements ExRateProvider {

    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());
    }
}

디폴트 콜백과 템플릿 빈

디폴트 콜백이 없다면 기본적으로 설정하도록 하고 생성자 등을 통해서 변경할 수도 있다. 이 내용은 Config에 스프링 빈으로 등록해주면 된다.

public class ApiTemplate {

    private final ApiExecutor apiExecutor;
    private final ExRateExtractor exRateExtractor;

    public ApiTemplate() {
        this.apiExecutor = new HttpClientApiExecutor();
        this.exRateExtractor = new ErApiExtractor();
    }

    // Project Level
    public ApiTemplate(ApiExecutor apiExecutor, ExRateExtractor exRateExtractor) {
        this.apiExecutor = apiExecutor;
        this.exRateExtractor = exRateExtractor;
    }

    public BigDecimal getForExRate(String url) {
        return this.getForExRate(url, this.apiExecutor, this.exRateExtractor);
    }

    public BigDecimal getForExRate(String url, ApiExecutor apiExecutor) {
        return this.getForExRate(url, apiExecutor, this.exRateExtractor);
    }
    ...
}

스프링이 제공하는 템플릿

Rest Template: Http API 요청을 처리하는 템플릿

  • HTTP Client 라이브러리 확장: ClientHttpRequestFactory (변경 가능하게)
    • SimpleClientHttpRequest, JdkClientHttpRequest, …
  • Message Body를 변환하는 전략: HttpMessageConverter (손쉽게 변환)
public class RestTemplateExRateProvider implements ExRateProvider  {

    private final RestTemplate restTemplate;

    public RestTemplateExRateProvider(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public BigDecimal getExRate(String currency) {
        String url = "https://open.er-api.com/v6/latest/" + currency;
        return restTemplate.getForObject(url, ExRateData.class).rates().get("KRW");
    }
}

스프링의 Template

  • JdbcTemplate
  • JmsTemplate
  • TransactionTemplate
  • HibernateTemplate

MyBatis

  • SqlSessionTemplate


1jeongg
Written by 1jeongg Follow

I'm studying Android development by Kotlin and Spring by Java