Java Singleton 알아보기

떡ol·2023년 4월 29일
0

한번 초기화하고 계속 사용하는 것이 싱클톤 패턴이라고 합니다. Android를 개발하다보면 부딪히는 멀티 쓰레드일때 Singleton이 어떻게 작동되는지 직접 테스트를 해본적이 없어서 작성하게 되었다.

Singleton vs Static class

차이SingletonStatic
생성시점사용자 요청시
하나의 인스턴스를 생성해 재사용
클래스가 로드된 후 정적 스택에 저장됨
인터페이스가능불가능
Overide가능불가능
저장위치HeapStack

생성시점

	//Singleton
    class MyCrypt1{
        private static MyCrypt1 crypt = null;
       
        private MyCrypt1() { // 외부에서 접근 못하게한다.
        }
		// MyCrypt1.getCrypt()를 하면 자동생성해주게 된다. 따라서 생성시점은 요청시
        public static MyCrypt1 getCrypt() { 
            if (crypt == null)
                crypt = new MyCrypt1();
            return crypt;
        }

    }
    
    //static
    class MyCrypt1{
        private int Num = 1;
        private static int sNum = 2;
        static void method(){
            System.out.println(Num); // 에러
            System.out.println(this.Num); // 에러
            System.out.println(sNum);
        }
    }

앞에서 설명하였든 Singleton은 초기화시점이 생성되는 시점입니다. 사용할때 맘편하게 사용하시면 됩니다. static의 경우에는 클래스 파일이 로드된 후 이기 때문에, 같은 시점에서 로드되는 static 필드값들이 아닌이상 에러가 발생합니다. this 또한 클래스 자체이므로 후에 로드되는 static 매서드에서 사용이 불가능 합니다.

인터페이스 & Override

Singleton은 인터페이스 & Override가 사용가능합니다.

우선 안되는 static부터 확인해봅시다.

public interface Service {
    static void MyCrypt(String key); // 안돼요
}

생성시점을 잘 알고계시다면 interface로 만들 수 없다는것을 예상하실겁니다. interface(먼저)와 static이 로드되는 시점이 다르니깐요.

다음으로는 Singleton입니다.

    static class MyCrypt implements Service{ //돼요
        private static MyCrypt crypt = null; 
        public String key = ""; // 테스트용으로만듦, getter따로 생성해서사용하면 됨
        private MyCrypt() { // 위 설명과 기본 구성에는 차이가 없습니다.
        }

        public static MyCrypt getCrypt() {
            if (crypt == null)
                crypt = new MyCrypt();
            return crypt;
        }

        @Override
        public void MyCryptSubMethod(String key) { 
        //단 이부분에서 interface의 내용을 상속받아 사용가능합니다.
            this.key = key;
            System.out.println("상속받은 객체를 사용 가능합니다.");
        }

    }

내부 변수와 생성 메소드가 static으로 선언되어 있는데, 정적 변수, 메소드는 인스턴스에 속하는 영역이 아니고 클래스 자체에 속해있는겁니다. 그렇기 때문에 클래스의 인스턴스를 통하지 않고도 정적 메소드를 사용할 수가 있는겁니다.

Mult Threads에서 문제점

문제의 시작

자, 이제 본론으로와서 위에 내용은 기본으로 알고 가는 이야기이고 멀티쓰레드에서 어떻게 문제가되고 어떻게 적용해야할지 알아봅시다.
문제점
멀티 스레드를 사용하는 환경에서는 위와 같은 코드는 문제가 될 수 있다. 다음과 같은 경우를 생각해보자

1. `MyCrypt`가 아직 생성되지 않았을 때 `Thread 1`이 `getCrypt()`메소드를 호출한다.
   `if`에서 `MyCrypt`가 `null`인지까지 체크를 한다.
2. `Thread 1`이 인스턴스를 생성하지 않은 상황에서 `Thread 2`가 `getCrypt()`메소드를 호출한다. 
    아직 `MyCrypt`가 `null`이 므로 `MyCrypt` 인스턴스를 생성한다.
3. `Thread 1`과 `Thread 2` 둘다 인스턴스를 생성하게 되고 결과적으로 `MyCrypt` 클래스의 인스턴스가 
    2개가 생성된다.

소스를 작성해보겠습니다.

static class MyCrypt implements Service{
        private static MyCrypt crypt = null;
        private String key = "";
        private MyCrypt() {
        }

        public static MyCrypt getCrypt() {
            if (crypt == null) { // 이부분을 바꾸시면 됩니다. 
                try {
                    Thread.sleep(8); // 저는 8ms를 줬습니다. 1하니깐 테스트가 안되네요
                } catch (InterruptedException e) {
                }
                crypt = new MyCrypt();
            }
            return crypt;
        }
		//아래동일...

    }

Thread class 와 test Code를 작성해봅시다.

    static class MyThread extends Thread {
        public MyThread(String name) {
            super(name);
        }

        @Override
        public void run() {
            MyCrypt myCrypt = MyCrypt.getCrypt();
            System.out.println(Thread.currentThread().getName()+" :: "+myCrypt.toString());
        }
    }

    @Test
    void tonTest(){
        MyThread[] threads = new MyThread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new MyThread((i + 1) + "-thread");
            threads[i].start();
        }
    }

결과입니다. 다 다른객체로 나오네요.

해결법

해결법은 두가지가 있습니다.

  • 정적 변수를 바로 초기화 한다.
  • 동기화를 사용한다.

정적 변수를 바로 초기화 한다.

이 방법은 그냥 클래서 생성시 바로 초기화하는겁니다. 당연히 클래스 로드시 이미 초기화 되었으니 문제는 없습니다.
단, 이러면 인스턴스 변수의 '사용자 요청시 하나의 인스턴스를 생성해 재사용'은 이제 의미가 없어져 버립니다.

static class MyCrypt implements Service{
        private static MyCrypt crypt = new MyCrypt(); // 이부분이 바꼈습니다.
        private String key = "";
        private MyCrypt() {
        }

        public static MyCrypt getCrypt() {
            if (crypt == null) {
                try {
                    Thread.sleep(8);
                } catch (InterruptedException e) {
                }
                crypt = new MyCrypt();
            }
            return crypt;
        }

        @Override
        public void MyCryptSubMethod(String key) {
            this.key = key;
            System.out.println("상속받은 객체를 사용 가능합니다.");
        }

    }

동기화를 사용한다.

get 부분에 synchronized를 넣어주면 됩니다.

	//class MyCrypt에서 수정해주세요
        public synchronized static MyCrypt getCrypt() {
            if (crypt == null) {
                try {
                    Thread.sleep(8);
                } catch (InterruptedException e) {
                }
                crypt = new MyCrypt();
            }
            return crypt;
        }

하지만 이또한 약간? 문제는 있습니다.
모든 쓰레드가 getCrypt()에 접근할때 동기화에 걸리게되므로 해당 메소드의 사용이 빈번할 때는 성능의 저하가 일어날 수 있다.

    static class MyCrypt implements Service{
        private static volatile MyCrypt crypt = null; // volatile 추가
        private String key = "";
        private MyCrypt() {
        }

        public synchronized static MyCrypt getCrypt() {
            if (crypt == null) {// if문 추가 추가
                synchronized(MyCrypt.class) { // synchronized 추가
                    if (crypt == null) {
                        try {
                            Thread.sleep(8);
                        } catch (InterruptedException e) {
                        }
                        crypt = new MyCrypt();
                    }
                }
            }
            return crypt;
        }
		//이하 동일...
    }

volatile을 키워드로 쓰레드가 인스턴스를 생성할때 CPU 캐시가 아닌 메인메모리에 저장되도록 강제 지정해줍니다.
null을 체크하는 if내부에 if를 한번 더 지정해주는 것을 볼 수 있는데 이는 각 스레드에서 생성한 인스턴스가 아직 메인메모리에 올라가지 않았을 때 발생하는 Race Condition 문제를 해결하기 위함입니다.
synchronized 외부에 if를 둔건 필요없는 synchronized발생을 방지하기 위함이고 내부의 ifsynchronized를 실행하는 스레드들의 동시 인스턴스 생성을 방지하기 위함이라고 생각하면 됩니다. 이를 DCL(Double Checking Looking)이라고 합니다.

마지막으로 가장 많이 사용하는 LazyHolder 방식입니다.
위에 코드보다 더깔끔해집니다.

static class MyCrypt implements Service{
        private static MyCrypt crypt = null;
        private String key = "";
        private MyCrypt() {
        }
        private static class InnerMyCryptClazz{ 
        // InnerClass를 하나 만들고 여기서 초기화합니다.
        // static class는 또다른 파일 클래스를 하나 만드는거랑 비슷한 효과가있습니다. 
        // 따라서 static이라도 스택에서 초기화가 바로되는게 아님.
            private static final MyCrypt instance = new MyCrypt();
            
        }

        public static MyCrypt getCrypt() {
            if (crypt == null) {
                try {
                    Thread.sleep(8);
                } catch (InterruptedException e) {
                }
                crypt = InnerMyCryptClazz.instance; // return 되는걸 Inner에서 가져옵니다.
            }
            return crypt;
        }
		//이하 동일...
    }

static 멤버 클래스더라도 클래스 로더가 초기화를 할 때 초기화되지 않고 getCrypt()메소드를 사용할 때 초기화가 됩니다.
동적바인딩(Dynamic Binding, 런타임시에 결정)의 특징을 이용하였기 때문에 Thread-safe합니다.

결론

Android는 라이프사이클 자체가 전부(?) Thread라 Frame간 데이터를 동기화해주기 굉장히 불편했는데, 이걸 먼저 알았다면 좋았을거 같네요... (저는 해결 못해서 전부 static, RxJava로 해결했거든요..)



참고자료들___
(참고) 자바 디자인 패턴 - 싱글턴 패턴
(참고) static, final, static final의 차이

profile
하이

0개의 댓글