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

객체지향

데코레이터 패턴 활용해서 관심사 분리하기

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

프락시와 데코레이터 패턴

  • 마치 자신이 클라이언트가 사용하려는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아 주는 갳게를 프락시라고 부른다.
  • 프락시를 통해서 최종적으로 요청을 위임받아 처리하는 실제 객체를 타깃 오브젝트 즉, 대상 객체라고 부른다.
  • 프락시의 특징은 대상 객체와 같은 인터페이스를 구현했다는 것과 프락시가 대상 객체를 제어할 수 있는 위치에 있다는 것이다.
  • 프락시는 사용 목적에 따라서 두 가지로 구분할 수 있다.
  • 첫 번째는 대상 객체에 부가 기능을 부여해 주는 목적이다. -> 이는 디자인 패턴에서, 데코레이터 패턴이라고 부른다.
  • 두 번재는 클라이언트가 대상 객체에 접근하는 방법을 제어하는 목저이다. -> 이는 디자인 패턴에서, 프락시 패턴이라고 부른다.
  • 이번 글에서는 캐싱이라는 부가 기능을 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

  • 박용권님의 스프링러너 스프링 아카데미