람다를 이용하여 virtual proxy pattern을 구현하는 방법을 알아봅니다.
먼저 버추얼 프록시 패턴이 뭔지를 알아야 겠죠?
하지만 이름은 몰랐더라도 이미 사용하는 코드에 녹아있을수도 있습니다.
Virtual proxy pattern
- 예제가 잘 설명된 글: http://ncanis.tistory.com/102
- 개념이 잘 설명된 글: http://egloos.zum.com/ingenuity/v/1836034
Lambda를 이용한 virtual proxy 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() 함수가 처음 호출되면 수행 순서는 아래와 같습니다.
- heavy.get()이 불린다.
- createAndCacheHeavy() 함수가 호출된다.
- if문으로 HeavyFactory의 instance인지를 확인하는 과정에서 HeavyFactory가 언급되면서 heavyIsntance가 생성됩니다.
- 따라서 heavy = new HeavyFactory()가 수행됩니다. (heavy에는 HeavyFactory 객체가 담깁니다.)
- HeavyFactory의 get()함수가 호출되면서 Heavy의 instance가 return 됩니다.
'개발이야기 > Java' 카테고리의 다른 글
Java 8 Lambda를 이용한 Builder pattern (0) | 2017.12.14 |
---|---|
Java 8 Lambda를 이용한 lazy evaluation (0) | 2017.12.13 |
Java 8 Lambda를 이용한 Execute around pattern #2 - lock 관리 (0) | 2017.12.07 |
Java 8 Lambda를 이용한 Execute around pattern #1 - resource 관리 (0) | 2017.12.05 |
Java의 동기화 Synchronized 개념 정리#2 (7) | 2017.11.20 |