개발을 하다 보면 환율 정보를 필요로 하는 기능들이 점점 많아집니다.
예를 들어, 상품 목록을 원화로 계산해서 보여줘야 한다면, 환율 정보가 필수적이죠. 그렇다면 환율 정보가 필요할 때마다 매번 웹 API를 호출해야 할까요?
매번 API를 호출하게 되면 트래픽이 많아지고 성능 문제가 발생할 수 있습니다. 만약 환율 정보가 30분 동안만 보장된다고 한다면, API를 30분에 한 번만 호출하고 그동안 캐시된 데이터를 사용하는 방법을 고려할 수 있습니다.
하지만 캐시 기능을 어떻게 추가할 수 있을까요? 이번 글에서는 데코레이터 디자인 패턴을 활용해 API 호출 코드를 수정하지 않고도 캐시 기능을 추가하는 방법을 소개하도록 하겠습니다!
WebApiExRateProvider에 캐시 기능을 추가하기
현재 WebApiExRateProvider는 환율 정보를 API로부터 받아옵니다. 하지만 캐시 기능을 추가하지 않고 그대로 API를 호출하게 되면 불필요한 요청이 많아지게 됩니다. 이 문제를 해결하기 위해 데코레이터 패턴을 적용할 수 있습니다.
데코레이터 패턴은 객체에 부가적인 기능을 동적으로 추가하는 패턴입니다. 기존 코드를 수정하지 않고 기능을 추가할 수 있기 때문에 유연하게 사용할 수 있습니다! 우리가 여기서 구현할 캐시 기능도 데코레이터 패턴을 통해 해결할 수 있습니다.
캐시 기능 추가 : 데코레이터 패턴 구현
먼저, CachedExRateProvider
라는 클래스를 만들어 환율 정보를 캐싱하는 역할을 담당하게 합니다. 이 클래스는 기존 ExRateProvider
인터페이스를 구현하면서, 내부적으로 WebApiExRateProvider
의 기능을 사용합니다.
public class CachedExRateProvider implements ExRateProvider {
private final ExRateProvider target;
public CachedExRateProvider(ExRateProvider target) {
this.target = target;
}
@Override
public BigDecimal getExRate(String currency) throws IOException {
return target.getExRate(currency);
}
}
위 코드는 기존의 WebApiExRateProvider
를 감싸는 형태입니다. 이렇게 하면 CachedExRateProvider
가 환율 정보 제공자 역할을 대신할 수 있게 됩니다.
의존성 설정 : ObjectFactory 수정
캐시 기능을 적용하려면 스프링을 사용해 객체 의존성을 설정해주어야 합니다. ObjectFactory
에서 CachedExRateProvider
를 추가하고 이를 PaymentService
에서 사용할 수 있게 구성해봅시다.
@Configuration
@ComponentScan
public class ObjectFactory {
@Bean
public PaymentService paymentService() {
return new PaymentService(cachedExRateProvider());
}
@Bean
public ExRateProvider cachedExRateProvider() {
return new CachedExRateProvider(exRateProvider());
}
@Bean
public ExRateProvider exRateProvider() {
return new WebApiExRateProvider();
}
}
ObjectFactory
는 의존성을 설정해주는 역할을 하는 클래스입니다.
이제 PaymentService
는 CachedExRateProvider
를 통해 환율 정보를 제공받고, 캐시가 없을 경우에만 실제로 API 호출을 하게 됩니다.
캐시 구현
환율 정보를 캐시하려면, 통화를 기준으로 환율 정보를 저장하고 일정 시간이 지나면 캐시를 무효화하는 방식으로 구현해야 합니다. 예를 들어, 캐시된 값이 없으면 API를 호출해 환율 정보를 가져오고, 그 값을 저장해 일정 시간 동안 사용하도록 하였습니다.
public class CachedExRateProvider implements ExRateProvider {
private final ExRateProvider target;
private BigDecimal cachedExRate;
private LocalDateTime cacheExpiryTime;
public CachedExRateProvider(ExRateProvider target) {
this.target = target;
}
@Override
public BigDecimal getExRate(String currency) throws IOException {
if (cachedExRate == null || cacheExpiryTime.isBefore(LocalDateTime.now())) {
cachedExRate = this.target.getExRate(currency);
cacheExpiryTime = LocalDateTime.now().plusSeconds(3); // 캐시 만료 시간을 3초로 설정
System.out.println("Cache Updated");
}
return cachedExRate;
}
}
위 코드는 캐시된 값이 없거나 만료되었을 때만 API 호출을 하고, 그렇지 않으면 캐시된 값을 반환합니다. 여기서 cacheExpiryTime
을 설정해 캐시가 만료되면 다시 API를 호출하도록 합니다.
테스트 코드 작성
캐시가 제대로 동작하는지 확인하기 위해 테스트를 진행해봅시다. Client 코드를 수정해 1초, 2초 간격으로 호출해 캐시가 갱신되는지 확인할 수 있습니다.
public class Client {
public static void main(String[] args) throws IOException, InterruptedException {
BeanFactory beanFactory = new AnnotationConfigApplicationContext(ObjectFactory.class);
PaymentService paymentService = beanFactory.getBean(PaymentService.class);
Payment payment1 = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println("payment1: " + payment1);
System.out.println("=================");
TimeUnit.SECONDS.sleep(1);
Payment payment2 = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println("payment2: " + payment2);
System.out.println("=================");
TimeUnit.SECONDS.sleep(2);
Payment payment3 = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println("payment3: " + payment3);
}
}
마무리 : 디자인 패턴과 캐시 도입
데코레이터 패턴을 활용해 API 호출 코드에 직접적인 변경 없이 캐시 기능을 구현할 수 있었습니다. 이렇게 스프링과 디자인 패턴을 결합하여, 유연하게 새로운 기능을 추가하는 방식을 익히면 다양한 상황에 쉽게 대응할 수 있습니다.
캐시를 적절히 활용하면 성능 향상에 도움이 되지만, 캐시 만료 시간과 업데이트 주기에 대한 세심한 설계가 필요합니다. 이번 예시에서는 3초로 설정했지만, 실제 서비스에서는 요구 사항에 맞게 설정해보세요!!
'Java' 카테고리의 다른 글
븟츠의 clone 재정의는 항상 조심하자! (0) | 2024.08.25 |
---|---|
자바 코딩 컨벤션 정리 (0) | 2024.08.18 |
만두의 Exception? (0) | 2024.08.05 |
븟츠의 JWT 소개 및 정리 (0) | 2024.08.03 |
븟츠의 try-finally 보다는 try-with-resources를 사용하자! (0) | 2024.07.28 |
개발을 하다 보면 환율 정보를 필요로 하는 기능들이 점점 많아집니다.
예를 들어, 상품 목록을 원화로 계산해서 보여줘야 한다면, 환율 정보가 필수적이죠. 그렇다면 환율 정보가 필요할 때마다 매번 웹 API를 호출해야 할까요?
매번 API를 호출하게 되면 트래픽이 많아지고 성능 문제가 발생할 수 있습니다. 만약 환율 정보가 30분 동안만 보장된다고 한다면, API를 30분에 한 번만 호출하고 그동안 캐시된 데이터를 사용하는 방법을 고려할 수 있습니다.
하지만 캐시 기능을 어떻게 추가할 수 있을까요? 이번 글에서는 데코레이터 디자인 패턴을 활용해 API 호출 코드를 수정하지 않고도 캐시 기능을 추가하는 방법을 소개하도록 하겠습니다!
WebApiExRateProvider에 캐시 기능을 추가하기
현재 WebApiExRateProvider는 환율 정보를 API로부터 받아옵니다. 하지만 캐시 기능을 추가하지 않고 그대로 API를 호출하게 되면 불필요한 요청이 많아지게 됩니다. 이 문제를 해결하기 위해 데코레이터 패턴을 적용할 수 있습니다.
데코레이터 패턴은 객체에 부가적인 기능을 동적으로 추가하는 패턴입니다. 기존 코드를 수정하지 않고 기능을 추가할 수 있기 때문에 유연하게 사용할 수 있습니다! 우리가 여기서 구현할 캐시 기능도 데코레이터 패턴을 통해 해결할 수 있습니다.
캐시 기능 추가 : 데코레이터 패턴 구현
먼저, CachedExRateProvider
라는 클래스를 만들어 환율 정보를 캐싱하는 역할을 담당하게 합니다. 이 클래스는 기존 ExRateProvider
인터페이스를 구현하면서, 내부적으로 WebApiExRateProvider
의 기능을 사용합니다.
public class CachedExRateProvider implements ExRateProvider {
private final ExRateProvider target;
public CachedExRateProvider(ExRateProvider target) {
this.target = target;
}
@Override
public BigDecimal getExRate(String currency) throws IOException {
return target.getExRate(currency);
}
}
위 코드는 기존의 WebApiExRateProvider
를 감싸는 형태입니다. 이렇게 하면 CachedExRateProvider
가 환율 정보 제공자 역할을 대신할 수 있게 됩니다.
의존성 설정 : ObjectFactory 수정
캐시 기능을 적용하려면 스프링을 사용해 객체 의존성을 설정해주어야 합니다. ObjectFactory
에서 CachedExRateProvider
를 추가하고 이를 PaymentService
에서 사용할 수 있게 구성해봅시다.
@Configuration
@ComponentScan
public class ObjectFactory {
@Bean
public PaymentService paymentService() {
return new PaymentService(cachedExRateProvider());
}
@Bean
public ExRateProvider cachedExRateProvider() {
return new CachedExRateProvider(exRateProvider());
}
@Bean
public ExRateProvider exRateProvider() {
return new WebApiExRateProvider();
}
}
ObjectFactory
는 의존성을 설정해주는 역할을 하는 클래스입니다.
이제 PaymentService
는 CachedExRateProvider
를 통해 환율 정보를 제공받고, 캐시가 없을 경우에만 실제로 API 호출을 하게 됩니다.
캐시 구현
환율 정보를 캐시하려면, 통화를 기준으로 환율 정보를 저장하고 일정 시간이 지나면 캐시를 무효화하는 방식으로 구현해야 합니다. 예를 들어, 캐시된 값이 없으면 API를 호출해 환율 정보를 가져오고, 그 값을 저장해 일정 시간 동안 사용하도록 하였습니다.
public class CachedExRateProvider implements ExRateProvider {
private final ExRateProvider target;
private BigDecimal cachedExRate;
private LocalDateTime cacheExpiryTime;
public CachedExRateProvider(ExRateProvider target) {
this.target = target;
}
@Override
public BigDecimal getExRate(String currency) throws IOException {
if (cachedExRate == null || cacheExpiryTime.isBefore(LocalDateTime.now())) {
cachedExRate = this.target.getExRate(currency);
cacheExpiryTime = LocalDateTime.now().plusSeconds(3); // 캐시 만료 시간을 3초로 설정
System.out.println("Cache Updated");
}
return cachedExRate;
}
}
위 코드는 캐시된 값이 없거나 만료되었을 때만 API 호출을 하고, 그렇지 않으면 캐시된 값을 반환합니다. 여기서 cacheExpiryTime
을 설정해 캐시가 만료되면 다시 API를 호출하도록 합니다.
테스트 코드 작성
캐시가 제대로 동작하는지 확인하기 위해 테스트를 진행해봅시다. Client 코드를 수정해 1초, 2초 간격으로 호출해 캐시가 갱신되는지 확인할 수 있습니다.
public class Client {
public static void main(String[] args) throws IOException, InterruptedException {
BeanFactory beanFactory = new AnnotationConfigApplicationContext(ObjectFactory.class);
PaymentService paymentService = beanFactory.getBean(PaymentService.class);
Payment payment1 = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println("payment1: " + payment1);
System.out.println("=================");
TimeUnit.SECONDS.sleep(1);
Payment payment2 = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println("payment2: " + payment2);
System.out.println("=================");
TimeUnit.SECONDS.sleep(2);
Payment payment3 = paymentService.prepare(100L, "USD", BigDecimal.valueOf(50.7));
System.out.println("payment3: " + payment3);
}
}
마무리 : 디자인 패턴과 캐시 도입
데코레이터 패턴을 활용해 API 호출 코드에 직접적인 변경 없이 캐시 기능을 구현할 수 있었습니다. 이렇게 스프링과 디자인 패턴을 결합하여, 유연하게 새로운 기능을 추가하는 방식을 익히면 다양한 상황에 쉽게 대응할 수 있습니다.
캐시를 적절히 활용하면 성능 향상에 도움이 되지만, 캐시 만료 시간과 업데이트 주기에 대한 세심한 설계가 필요합니다. 이번 예시에서는 3초로 설정했지만, 실제 서비스에서는 요구 사항에 맞게 설정해보세요!!
'Java' 카테고리의 다른 글
븟츠의 clone 재정의는 항상 조심하자! (0) | 2024.08.25 |
---|---|
자바 코딩 컨벤션 정리 (0) | 2024.08.18 |
만두의 Exception? (0) | 2024.08.05 |
븟츠의 JWT 소개 및 정리 (0) | 2024.08.03 |
븟츠의 try-finally 보다는 try-with-resources를 사용하자! (0) | 2024.07.28 |