프락시와 데코레이터 패턴
- 마치 자신이 클라이언트가 사용하려는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아 주는 갳게를 프락시라고 부른다.
- 프락시를 통해서 최종적으로 요청을 위임받아 처리하는 실제 객체를 타깃 오브젝트 즉, 대상 객체라고 부른다.
- 프락시의 특징은 대상 객체와 같은 인터페이스를 구현했다는 것과 프락시가 대상 객체를 제어할 수 있는 위치에 있다는 것이다.
- 프락시는 사용 목적에 따라서 두 가지로 구분할 수 있다.
- 첫 번째는 대상 객체에 부가 기능을 부여해 주는 목적이다. -> 이는 디자인 패턴에서, 데코레이터 패턴이라고 부른다.
- 두 번재는 클라이언트가 대상 객체에 접근하는 방법을 제어하는 목저이다. -> 이는 디자인 패턴에서, 프락시 패턴이라고 부른다.
- 이번 글에서는 캐싱이라는 부가 기능을 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 가 아닌 다른 객체에 적용하려면? -> 프락시를 그 객체에 맞게 다시 구성해야한다. -> 코드의 중복이 발생할 가능성이 크다.
- 보통 위의 문제를 해결하기 위해 AOP 기술을 이용한다.
REFERENCES
- 박용권님의 스프링러너 스프링 아카데미
'객체지향' 카테고리의 다른 글
클래스 다이어그램 (0) | 2022.04.23 |
---|---|
리플렉션을 활용한 동적 프락시 구현 (0) | 2022.03.10 |
상속 보다는 합성을 사용하라! (0) | 2022.03.10 |
역할과 책임분리 (0) | 2022.03.10 |
디자인 패턴에 함수형 프로그래밍 적용하기 (0) | 2022.03.06 |