기존 코드
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);
}
}
}
- 위의 코드는 데코레이션 패턴을 통해서 관심사를 적절히 분리한 코드이다.
- 하지만 위의 코드는 몇가지 문제점을 가지고 있다. -> 만약 프락시가 MovieReader 가 아닌 다른 객체에 적용하려면? -> CachingMovieReader 와 같은 프락시를 그 객체에 맞게 다시 구성해야한다. -> 코드의 중복이 발생할 가능성이 크다.
- 위의 문제를 해결하기 위해서 자바에서는 동적 프락시 라는 기술이 있다.
- 동적 프락시는 리플렉션 기술을 기반으로 구현할 수 있다.
리플렉션이란?
- 자바 기본 플랫폼에 내장된 API 로써, 동적으로 프락시를 생성할 수 있는 기능을 제공한다.
@Test
void reflectionTest() throws Exception {
// Without reflection
Duck duck = new Duck();
duck.quack();
// With reflection
Class<?> duckClass = Class.forName("moviebuddy.ReflectionTests$Duck");
Object duckObject = duckClass.getDeclaredConstructor().newInstance();
Method quack = duckObject.getClass().getDeclaredMethod("quack", new Class<?>[0]);
quack.invoke(duckObject);
}
static class Duck {
void quack() {
System.out.println("꽥꽥!");
}
}
리플렉션 기술을 이용한 동적 프락시 적용
@Test
void useDynamicProxy() throws Exception {
CsvMovieReader movieReader = new CsvMovieReader();
movieReader.setResourceLoader(new DefaultResourceLoader());
movieReader.setMetadata("movie_metadata.csv");
movieReader.afterPropertiesSet();
ClassLoader classLoader = JdkDynamicProxyTests.class.getClassLoader();
Class<?>[] interfaces = new Class[] { MovieReader.class };
InvocationHandler handler = new PerformanceInvocationHandler(movieReader);
MovieReader proxy = (MovieReader) Proxy.newProxyInstance(classLoader, interfaces, handler);
proxy.loadMovies();
proxy.loadMovies();
}
static class PerformanceInvocationHandler implements InvocationHandler {
final Logger log = LoggerFactory.getLogger(getClass());
final Object target;
PerformanceInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
long elapsed = System.currentTimeMillis() - start;
log.info("Executing {} method finished in {} ms", method.getName(), elapsed);
return result;
}
}
- 위와 같이 classLoader, interfaces, handler 를 newProxyInstance 메소드의 생성인자로 넘겨준다.
- PerformanceInvocationHandler 를 통해서 하고자 하는 작업을 구현한다.
- 로그를 기록하는 기능을 구현한다.
- 이렇게 되면 proxy.loadMovies() 가 호출되는 시점에 PerformanceInvocationHandler 클래스의 invoke 메소드가 호출된다.
- invoke 메소드의 첫번째 인자인 proxy 는 proxy.loadMovies()의 proxy가 넘어가고, 두번째 인자인 method 는 proxy.loadMovies() 의 loadMovies() 가 넘어가고, 마지막 인자인 args 은 proxy.loadMovies() 의 매개변수가 넘어갈 것이다. 현재는 매개변수를 넘기지 않고 있기때문에 빈 배열이 넘어간다.
- 이렇게 세개의 인자를 넘겨 받으면 method.invoke(target, args) 가 실행 되는데, 여기서 target 은 PerformanceInvocationHandler(movieReader) 에서 대상객체로 넘겨준 movieReader 가 된다.
- 이렇게 동적 프락시를 사용하면 프락시를 위한 병도의 클래스를 작성하지 않아도 된다.
- 하지만, 위에서 보듯 코드가 복잡하고 다루기 어렵다는 단점이 있다. 또한 대상 객체에 부가기능을 부여하기 위해서는 부가기능 객체를 만들고, 대상 객체에 의존 관계 주입을 해줘야 한다.
- 이러한 문제점은 AOP 를 적용함으로써 프락시 좀더 깔끔하게 다룰 수 있다.
REFERENCES
- 박용권님의 스프링러너 스프링 아카데미
'객체지향' 카테고리의 다른 글
구조패턴 (디자인 패턴) (0) | 2022.05.15 |
---|---|
클래스 다이어그램 (0) | 2022.04.23 |
데코레이터 패턴 활용해서 관심사 분리하기 (0) | 2022.03.10 |
상속 보다는 합성을 사용하라! (0) | 2022.03.10 |
역할과 책임분리 (0) | 2022.03.10 |