본문으로 바로가기

Java 8 Lambda를 이용한 virtual proxy pattern

category 개발이야기/Java 2017. 12. 12. 00:25
반응형

람다를 이용하여 virtual proxy pattern을 구현하는 방법을 알아봅니다.

먼저 버추얼 프록시 패턴이 뭔지를 알아야 겠죠?

하지만 이름은 몰랐더라도 이미 사용하는 코드에 녹아있을수도 있습니다.


Virtual proxy pattern

이 패턴은 어렵지 않은 구조이므로 간단히 개념만 설명하고자 합니다.
일단 목적은 "lazy evaluation"입니다.

생성해서 쓰기에 버거운 객체의 초기화를 뒤로 미뤄서 초기 로딩 속도를 높이는 방법입니다.
말은 거창하지만 실제로 많이 쓰이는 방법입니다.

어렵지 않고 이미 예제가 많이 있으므로 여기서 따로 설명하지는 않습니다.
따라서 잘 설명되어있는 블로그를 링크합니다.

  • 개념이 잘 설명된 글: http://egloos.zum.com/ingenuity/v/1836034

Lambda를 이용한 virtual proxy pattern

람다를 이용하여 생성을 지연시키는 방법에 대해서 알아보겠습니다.

자바에서는 보통 작성한 코드가 바로 실행되도록 코딩을 합니다. 이런 코드들은 eager code라고 합니다.
하지만 heavy한 코드를 수행하는건 loading하는 시점이 아니라 해당 함수를 호출하는 시점으로 미루고 싶을수도 있습니다.
예를 들면 화면을 보여주는데 빨리 보여줄수 있는 텍스트와 무거운 이미지가 있다면, 텍스트를 먼저 보여주면서 화면을 빠르게 띄우고, 이미지는 실제 로딩이 필요한 순간에 가져오도록 시키는거죠.

사실 lazy한 코드를 작성하기 위해서는 더 많은 노력이 들어가며 종종 버그를 만들기도 합니다.
하지만 람다를 사용하면 이런 단점들을 효과적으로 커버할 수 있습니다.

객체의 생성지연

손쉽게 사용할 수 있는 객체생성 지연 방법으로는 holder를 들수 있습니다.
heavy한 작업을 따로 분리해 내고 이 작업의 레퍼런스를 갖는 holder를 만듭니다.
holder에서는 이 heavy한 호출은 내부적으로 적절하게 호출하도록 합니다.

무슨 말인지 모르시겠다구요?
가장 쉽게는 singleton만들때 동기화 오류를 막기위해 사용하는 holder pattern이라고 생각하시면 됩니다.

가장 좋은건 역시! 예제입니다.^^
-
-

public class Heavy {
  public Heavy() { System.out.println("Heavy created"); }

  public String toString() { return "quite heavy"; }
}

일단 무거운 작업을 하는 class를 하나 만듭니다.

그리고 이를 감싸는 holder를 하나 만듭니다.

private Heavy heavy;
  
  public HolderNaive() {
    System.out.println("Holder created");
  }
  
  public Heavy getHeavy() {
    if(heavy == null) {
      heavy = new Heavy();
    }
    
    return heavy;
  }

//...


그리고 main에서는 아래와 같이 호출합니다.

public static void main(final String[] args) {
    final Holder holder = new Holder();
    System.out.println("deferring heavy creation...");
    System.out.println(holder.getHeavy());
    System.out.println(holder.getHeavy());
  }


결과는 이렇습니다.

holder created

deferring heavy creation...

Heavy created

quite heavy

quite heavy


Thread safe하게 만들려면?

만약에 getHeavy()를 여러 thread가 동시에 호출한다고 합시다.

그럼 race condition이 발생합니다.

또한 두개의 객체가 동시에 생기는 side effect이 발생할수도 있습니다.


하지만 이 문제는 간단하게 synchronized를 걸면 해결할 수 있습니다.

 public synchronized Heavy getHeavy() {
    if(heavy == null) {
      heavy = new Heavy();
    }
    
    return heavy;
  }

이런경우 thread safe하게는 되었지만 synchronized로 인해서 추가적인 오버헤드를 갖게 되었습니다.

실제로 synchronized 키워드가 필요한건 heavy가 null인경우 객체를 할당하는 한번인데, 매번 이 함수에 lock을 걸고 메모리 장벽을 넘나드는 오버헤드를 갖게 됩니다.

잘 일어나지 않는 하나의 경우 때문에 전체에 오버헤드를 주는 꼴 입니다.

따라서 객체를 생성할때 한번만 thread-safety 하게 만들고 그 이후에는 이 제약을 풀도록 하면 좋을것 같네요.


Indirection level의 추가

쉽게 initialize를 늦추기 위해서는 lambda를 이용하면 편합니다.

람다를 사용하면 정의되는 시점이 아닌 실제 호출하는 시점에서 해당 작업이 수행되기 때문입니다.

여기서는 객체를 생성하는 부분이니 () -> T의 시그니처를 갖는 supplier를 사용하면 좋겠네요.

public class Holder {
  private Supplier<Heavy> heavy = () -> createAndCacheHeavy();
  
  public Holder() {
    System.out.println("Holder created");
  }

  public Heavy getHeavy() {
    return heavy.get();
  }
  //...

  private synchronized Heavy createAndCacheHeavy() {
  // 동기화 처리는 여기서
  }

  public static void main(final String[] args) {
    final Holder holder = new Holder();
    System.out.println("deferring heavy creation...");
    System.out.println(holder.getHeavy());
    System.out.println(holder.getHeavy());
  }
}


heavy를 () ->createAndCacheHavey()란 supplier type의 함수로 바꿔줍니다.

일단 heavy란 멤버변수는 람다식이므로 get()을 호출하기 전까지는 수행이 지연됩니다.

getHeavy()에서 heavy.get()을 호출하면 그때 createAndCacheHeavy()란 함수가 수행되겠죠?

private synchronized Heavy createAndCacheHeavy() {
    class HeavyFactory implements Supplier<Heavy> {
      private final Heavy heavyInstance = new Heavy();

      public Heavy get() { return heavyInstance; }
    }

    if(!HeavyFactory.class.isInstance(heavy)) {
      heavy = new HeavyFactory();
    }
    
    return heavy.get();
  }


createAndCacheHeavy() 함수는 synchronized 라서 thread-safe합니다.

getHeavy() 함수가 처음 호출되면 수행 순서는 아래와 같습니다.

  1. heavy.get()이 불린다.
  2. createAndCacheHeavy() 함수가 호출된다.
  3. if문으로 HeavyFactory의 instance인지를 확인하는 과정에서 HeavyFactory가 언급되면서 heavyIsntance가 생성됩니다.
  4. 따라서 heavy = new HeavyFactory()가 수행됩니다. (heavy에는 HeavyFactory 객체가 담깁니다.)
  5. HeavyFactory의 get()함수가 호출되면서 Heavy의 instance가 return 됩니다.

두번째 getHeavy()를 호출하면 어떻게 될까요?
1. heavy.get()이 불립니다.
2. 단. 위 4번에서 이미 heavy 변수에는 HeavyFactory 객체를 담았습니다.
3. 따라서 HeaveFactory의 get()이 불리면서 Heavy 객체가 반환됩니다.

이렇게 구조를 만드면 처음 생성시에만 synchronized된 함수를 사용하고 그 이후에는 일반함수를 호출하는 형태가 됩니다.

사실 lazy evaluation을 하기 위해 돌아돌아 코드를 만들었습니다.
따라서 정말 큰 작업이 아니고서야 이런 수고로움이 필요한지는 많은 고민을 해야할것 같네요~
그나마 이건 람다로 만들었기 때문에 virtual proxy pattern의 경량화된 버전입니다.^^a


반응형