모르지 않다는 것은 아는것과 다르다.

객체지향

리플렉션을 활용한 동적 프락시 구현

채마스 2022. 3. 10. 23:56

기존 코드

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