[Java] Lazy Initialization with Memoization

구근우·2021년 9월 12일
4

Java

목록 보기
1/1
Trulli Photo by Martina Misar-Tummeltshammer on Unsplash

필자는 Java 로 Android SDK 개발을 진행하고 있다. SDK 내부 클래스들의 의존성 관리를 위해 Dependency Injection 를 사용하고 있다. 그리고 그 객체들을 Container 클래스에서 관리하고 있었다. 개발 과정에서 Container에서 관리하는 객체들이 필요할 때 생성되게(Lazy Initialization) 하고 싶었고, 그렇게 생성된 인스턴스가 계속 유지되도록(Memoization) 하고 싶었다. Java 에선 해당 기능이 기본적으로 제공되지 않고, SDK 특성상 서드파티 라이브러리 사용에는 한계가 있으니 직접 기능을 조금씩 구현해보자.

Eager Initialization & Lazy Initialization

먼저 Eager Initialization 은 객체를 바로 생성하는 것을 의미한다. 주로 생성자를 바로 사용해 객체를 생성하는 것이 Eager Initialization 이다. 이와 달리, Lazy Initialization 은 객체가 필요한 시점에 객체가 생성되도록 하는 것을 의미한다.

이 Lazy Initialization은 Java 8에 포함된 Supplier<T> 로 구현해볼 수 있다.

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

Supplier<T> 인터페이스는 T 타입의 값을 제공하는 함수 역할을 한다. Supplier<T> 함수 안에서 클래스의 생성자를 사용하면, get() 메서드가 불려야 해당 클래스의 객체가 생성된다.

Supplier<T> 를 이용한 Lazy Initialization을 Eager Initialization과 비교해보자.

import java.util.function.Supplier;

public class LazyExample {
    public static class MyObject {

        public MyObject(String name) {
            System.out.println(name + " is created!");
        }
    }

    public static void main(String[] args) {
        Supplier<MyObject> lazyObjectSupplier = () -> new MyObject("lazyObject");
        MyObject eagerObject = new MyObject("eagerObject");
        MyObject lazyObject = lazyObjectSupplier.get();
    }
}

main 함수를 실행하면 아래와 같이 출력된다.

eagerObject is created!
lazyObject is created!

즉, eagerObject는 생성자를 바로 사용했기 때문에 즉시 생성(Eager Initialization)되지만, lazyObjectSupplier<T>get() 메서드를 호출해야 그 때 생성되는 것(Lazy Initialization)을 알 수 있다.

자, 여기까지 Lazy Initialization 에 대해서 알아보았다. 그런데 Supplierget()을 호출할 때마다 새로운 인스턴스가 생성된다. 예제를 수정해서 확인해보자.

import java.util.function.Supplier;

public class LazyExample {
    public static class MyObject {

        public MyObject(String name) {
            System.out.println(name + " " + this + " is created!");
        }

        @Override
        public String toString() {
            return super.toString();
        }
    }

    public static void main(String[] args) {
        Supplier<MyObject> lazyObjectSupplier = () -> new MyObject("lazyObject");
        MyObject eagerObject = new MyObject("eagerObject");
        lazyObjectSupplier.get();
        lazyObjectSupplier.get();
        lazyObjectSupplier.get();
    }
}

main 함수의 실행 결과이다.

eagerObject LazyExample$MyObject@53d8d10a is created!
lazyObject LazyExample$MyObject@e9e54c2 is created!
lazyObject LazyExample$MyObject@65ab7765 is created!
lazyObject LazyExample$MyObject@1b28cdfa is created!

출력을 보면 lazyObject의 인스턴스들의 hashCode가 모두 다른 것을 확인할 수 있다. 따라서 Supplierget()을 호출할 때마다 새로운 인스턴스가 생성된다는 것을 알 수 있다.

Memoization

개발 중 필요한 요구 사항은 객체를 Lazy하게 초기화하고, 그렇게 생성된 하나의 인스턴스를 계속 유지하는 것이다. 클래스의 인스턴스를 하나만 유지하는 것을 memoizing 한다고 표현한다.

위에서 살펴보았듯이 Supplier<T> 로는 하나의 인스턴스를 유지할 수 없으니, 객체가 필요할 때 인스턴스가 생성되고 그 인스턴스를 계속 유지할 수 있는 새로운 클래스인 Lazy<T> 를 만들어보자.

import java.util.function.Supplier;

public class LazyExample {
    public static class MyObject {

        public MyObject(String name) {
            System.out.println(name + " " + this + " is created!");
        }

        @Override
        public String toString() {
            return super.toString();
        }
    }

    public static class Lazy<T> implements Supplier<T> {
        private final Supplier<? extends T> supplier;
        private T value;

        private Lazy(Supplier<? extends T> supplier) {
            this.supplier = supplier;
        }

        public static <T> Lazy<T> of(Supplier<? extends T> supplier) {
            return new Lazy<>(supplier);
        }

        @Override
        public T get() {
            if (value == null) {
                value = supplier.get();
            }
            return value;
        }
    }

    public static void main(String[] args) {
        Lazy<MyObject> lazyObjectSupplier = Lazy.of(() -> new MyObject("lazyObject"));
        MyObject eagerObject = new MyObject("eagerObject");
        lazyObjectSupplier.get();
        lazyObjectSupplier.get();
        lazyObjectSupplier.get();
    }
}

Lazy<T> 클래스를 위와 같이 만들어보았다. Lazy<T> 클래스는 memoization 가능한 Supplier<T> 이므로 Supplier<T>implement 하였다.
그리고 정적 팩토리 메서드인 of의 인자로 T 의 하위 타입도 받을 수 있게 하기 위해서 메서드 인자의 타입으로 Supplier<? extends T> 를 사용하였다.

main 함수 출력 결과는 아래와 같다.

eagerObject LazyExample$MyObject@e9e54c2 is created!
lazyObject LazyExample$MyObject@65ab7765 is created!

lazyObject가 필요한 타이밍에 인스턴스가 생성되었고, Lazy<T>get() 메서드를 여러번 호출해도 새로운 인스턴스가 생성되지 않는 것을 확인할 수 있다.

그러나 이 Lazy<T> 클래스는 멀티쓰레드 환경에서도 하나의 인스턴스만 생성할까?
main 함수를 수정해서 확인해보자.

import java.util.function.Supplier;

public class LazyExample {
    public static class MyObject {

        public MyObject(String name) {
            System.out.println(name + " " + this + " is created!");
        }

        @Override
        public String toString() {
            return super.toString();
        }
    }

    public static class Lazy<T> implements Supplier<T> {
        private final Supplier<? extends T> supplier;
        private T value;

        private Lazy(Supplier<? extends T> supplier) {
            this.supplier = supplier;
        }

        public static <T> Lazy<T> of(Supplier<? extends T> supplier) {
            return new Lazy<>(supplier);
        }

        @Override
        public T get() {
            if (value == null) {
                value = supplier.get();
            }
            return value;
        }
    }

    public static void main(String[] args) {
        Lazy<MyObject> lazyObjectSupplier = Lazy.of(() -> new MyObject("lazyObject"));
        MyObject eagerObject = new MyObject("eagerObject");
        
        for (int i = 0; i < 10; i++) {
            new Thread(lazyObjectSupplier::get).start();
        }
    }
}

main 함수의 출력 결과이다.

eagerObject LazyExample$MyObject@e9e54c2 is created!
lazyObject LazyExample$MyObject@18c3f653 is created!
lazyObject LazyExample$MyObject@803d777 is created!

여러 쓰레드에서 동시에 Lazy<T>get() 메서드를 호출하니 인스턴스가 두 개나 생성된 것을 확인할 수 있다.
따라서 이 클래스는 멀티쓰레드 환경에서 안전하지 않다!

Thread-safe Lazy class

그럼 Lazy<T> 클래스가 멀티쓰레드 환경에서 인스턴스를 하나만 생성할 수 있게 수정해보자.

import java.util.function.Supplier;

public class LazyExample {
    public static class MyObject {

        public MyObject(String name) {
            System.out.println(name + " " + this + " is created!");
        }

        @Override
        public String toString() {
            return super.toString();
        }
    }

    public static class Lazy<T> implements Supplier<T> {
        private Supplier<? extends T> supplier;
        private volatile T value;

        private Lazy(Supplier<? extends T> supplier) {
            this.supplier = supplier;
        }

        public static <T> Lazy<T> of(Supplier<? extends T> supplier) {
            return new Lazy<>(supplier);
        }

        @Override
        public T get() {
            T localReference = value;
            if (localReference == null) {
                synchronized (this) {
                    localReference = value;
                    if (localReference == null) {
                        value = localReference = supplier.get();
                        supplier = null;
                    }
                }
            }
            return localReference;
        }
    }

    public static void main(String[] args) {
        Lazy<MyObject> lazyObjectSupplier = Lazy.of(() -> new MyObject("lazyObject"));
        MyObject eagerObject = new MyObject("eagerObject");

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " wants to get " + lazyObjectSupplier.get());
            }).start();
        }
    }
}

Lazy<T> 의 멀티쓰레드 안정성을 위해 먼저 value 변수를 volatile 로 선언해주었다.

volatile 로 선언된 변수의 값을 읽거나 쓰는 것은 CPU 캐시에서가 아니라 메인 메모리에서만 가능하다.
그래서 멀티쓰레드 환경에서 변수의 값이 동일한 것을 보장해준다.
volatile 키워드는 한 변수를 두고 오직 한 쓰레드만 이 변수에 읽기/쓰기 작업을 하고, 다른 쓰레드들은 읽기 작업만 하는 상황에 적합하다. 즉, 인스턴스를 하나의 쓰레드에서 한 번만 생성하고(쓰기), 그렇게 생성된 인스턴스를 여러 쓰레드에서 사용하는(읽기) Lazy<T> 클래스에 적합하다는 것이다.

그리고 Double-checked locking logic 사용으로, 멀티쓰레드 상황에서의 get() 메서드 호출을 효율적으로 처리하였다.

마지막으로 supplier 변수를 살펴보면, value 변수에 값이 할당된 후에는 supplier 가 더 이상 사용되지 않는다.
따라서 메모리를 최적화하기 위해 suppliernon-final로 만들고, value 변수에 값이 할당된 후에는 suppliernull로 만들어 주었다.

그럼 main 함수의 출력 결과를 확인해보자.

eagerObject LazyExample$MyObject@e9e54c2 is created!
lazyObject LazyExample$MyObject@16682fd9 is created!
Thread-2 wants to get LazyExample$MyObject@16682fd9
Thread-3 wants to get LazyExample$MyObject@16682fd9
Thread-0 wants to get LazyExample$MyObject@16682fd9
Thread-4 wants to get LazyExample$MyObject@16682fd9
Thread-7 wants to get LazyExample$MyObject@16682fd9
Thread-1 wants to get LazyExample$MyObject@16682fd9
Thread-5 wants to get LazyExample$MyObject@16682fd9
Thread-6 wants to get LazyExample$MyObject@16682fd9
Thread-8 wants to get LazyExample$MyObject@16682fd9
Thread-9 wants to get LazyExample$MyObject@16682fd9

출력 결과를 보면 멀티쓰레드 환경에서 lazyObject의 인스턴스가 하나만 생성되고, 그 하나의 인스턴스를 여러 쓰레드에서 읽는다는 것을 확인할 수 있다.

Final Code

public class Lazy<T> implements Supplier<T> {
    private Supplier<? extends T> supplier;
    private volatile T value;

    private Lazy(Supplier<? extends T> supplier) {
        this.supplier = supplier;
    }

    public static <T> Lazy<T> of(Supplier<? extends T> supplier) {
        return new Lazy<>(supplier);
    }

    @Override
    public T get() {
        T localReference = value;
        if (localReference == null) {
            synchronized (this) {
                localReference = value;
                if (localReference == null) {
                    value = localReference = supplier.get();
                    supplier = null;
                }
            }
        }
        return localReference;
    }
}

Lazy<T> 클래스를 이용하면 객체를 필요할 때만 생성되게 할 수 있고, 그 하나의 인스턴스를 get() 메서드를 통해 멀티쓰레드 환경에서 안전하게 사용할 수 있다.
따라서 개발 요구 사항에 맞는 기능을 구현하는데 성공했다!

조만간 이 Lazy<T>를 이용해 Container 클래스에서 Dependency Injection을 관리해보는 과정을 별도로 작성하도록 하겠다.

profile
Android SDK Developer

0개의 댓글