마치 자신이 클라이언트가 사용하려는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아 주는 갳게를 프락시라고 부른다.
프락시를 통해서 최종적으로 요청을 위임받아 처리하는 실제 객체를 타깃 오브젝트 즉, 대상 객체라고 부른다.
프락시의 특징은 대상 객체와 같은 인터페이스를 구현했다는 것과 프락시가 대상 객체를 제어할 수 있는 위치에 있다는 것이다.
프락시는 사용 목적에 따라서 두 가지로 구분할 수 있다.
첫 번째는 대상 객체에 부가 기능을 부여해 주는 목적이다. -> 이는 디자인 패턴에서, 데코레이터 패턴이라고 부른다.
두 번재는 클라이언트가 대상 객체에 접근하는 방법을 제어하는 목저이다. -> 이는 디자인 패턴에서, 프락시 패턴이라고 부른다.
이번 글에서는 캐싱이라는 부가 기능을 MovieReader 객체에 부여하는 것이 목적이기 때문에 데코레이터 패턴으로 해결 가능하다.
기존 코드
public class CsvMovieReader implements MovieReader {
private final CacheManager cacheManager;
public CsvMovieReader(CacheManager cacheManager) {
this.cacheManager = Objects.requireNonNull(cacheManager);
}
@Override
public List<Movie> loadMovies() {
Cache cache = cacheManager.getCache(getClass().getName());
/**
* 부가 기능 : 캐시에 저장된 데이터가 있다면, 즉시 반환
*/
List<Movie> movies = cache.get("csv.movies", List.class);
if (Objects.nonNull(movies) && movies.size() > 0){
return movies;
}
/**
* 핵심 기능!!
*/
try {
final InputStream content = getMetadataResource().getInputStream();
return new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))
.lines()
.skip(1)
.collect(Collectors.toList());
} catch(IOException error) {
throw new ApplicationException("failed to load movies data.", error);
}
/**
* 부가 기능 :획득한 데이터를 캐시에 저장하고, 반환한다.
*/
cache.put("csv.movies", movies);
return movies;
}
}
캐시 부가 기능이 핵심 기능을 가진 CsvMovieReader 에 침투해있다.
그렇기 때문에, 내부 복잡도가 오르고, 관심사가 섞이는 문제가 있다.
이를 해결하기 위해서는 캐시를 꺼내고 저장하는 기능을 외부로 빼내야 한다.
데코레이터 패턴을 활용해서 위의 문제를 해결할 수 있다.
데코레이터 패턴 활용해서 관심사 분리하기
public class CachingMovieReader implements MovieReader {
static final String CACHE_NAME = CachingMovieReader.class.getName();
static final String CACHE_KEY_MOVIES = "movies";
private final CacheManager cacheManager;
private final MovieReader target;
public CachingMovieReader(CacheManager cacheManager, MovieReader target) {
this.cacheManager = Objects.requireNonNull(cacheManager);
this.target = Objects.requireNonNull(target);
}
@Override
public List<Movie> loadMovies() {
// 캐시된 데이터가 있으면 즉시 반환 처리
Cache cache = cacheManager.getCache(CACHE_NAME);
List<Movie> movies = cache.get(CACHE_KEY_MOVIES, List.class);
if (Objects.nonNull(movies)) {
return movies;
}
movies = target.loadMovies();
// 읽어온 데이터를 캐시 처리하고 반환
cache.put(CACHE_KEY_MOVIES, movies);
return movies;
}
}
위와 같이 캐시와 관련된 기능을 따로 분리해서 CachingMovieReader 를 구현한다.
public class CsvMovieReader implements MovieReader {
@Override
public List<Movie> loadMovies() {
try {
final InputStream content = getMetadataResource().getInputStream();
return new BufferedReader(new InputStreamReader(content, StandardCharsets.UTF_8))
.lines()
.skip(1)
.collect(Collectors.toList());
} catch(IOException error) {
throw new ApplicationException("failed to load movies data.", error);
}
}
}
캐시 기능을 따로 분리했기 때문에 위와 같이 캐시랑 관련된 코드를 제거할 수 있다.
테스트
@Test
void caching() {
CacheManager cacheManager = new ConcurrentMapCacheManager();
MovieReader target = new DummyMovieReader();
MovieReader movieReader = new CachingMovieReader(cacheManager, target);
Cache cache = cacheManager.getCache(CachingMovieReader.CACHE_NAME);
assertNull(cache.get(CachingMovieReader.CACHE_KEY_MOVIES));
List<Movie> movies = movieReader.loadMovies();
assertNotNull(cache.get(CachingMovieReader.CACHE_KEY_MOVIES));
assertSame(movieReader.loadMovies(), movies);
}
class DummyMovieReader implements MovieReader {
@Override
public List<Movie> loadMovies() {
return new ArrayList<>();
}
}
먼저 테스트를 위한 CacheManager, MovieReader 객체를 생성해 준다.
위의 코드를 실행하면 assertNull 메소드에서는 아직 loadMovies 메소드가 호출되기 전이기 때문에 당연히 null 이 반환되는것이 맞다.
이후에 loadMovies 메소드가 호출 되었기 때문에 더 이상 null 이 호출되지 않는다.
@Primary
@Bean
public MovieReader cachingMovieReader(CacheManager cacheManager, MovieReader target) {
return new CachingMovieReader(cacheManager, target);
}
위와 같이 CacheManager, MovieReader 를 설정해서 빈으로 등록한다.
@Primary 애노테이션을 통해서 만약 같은 타입의 빈이 있다면, 해당 빈을 우선적으로 사용하도록 설정할 수 있다.
붙이지 않는다면, 위의 예시에서 MovieReader 의 구현체가 CsvMovieReader, CachingMovieReader 두개 이므로 오류가 발생한다.
이렇듯 데코레이터 패턴에서는 프락시가 여러개 일수 있기때문에 순서를 지정하는 것이 중요하다.
하지만 프락시의 단점도 존재한다. -> 만약 프락시가 MovieReader 가 아닌 다른 객체에 적용하려면? -> 프락시를 그 객체에 맞게 다시 구성해야한다. -> 코드의 중복이 발생할 가능성이 크다.