Framework/Spring

싱글톤 패턴(Singleton Pattern)

  • -
반응형

디자인패턴을 공부한 경험이 있다면(혹은 그렇지 않아도) 싱글톤 패턴에 대해 들어본 적이 있을 것입니다.

오늘은 싱글톤 패턴에 대해 공부하고 정리해봤습니다.


싱글톤 패턴

싱글톤 패턴은 클래스의 인스턴스를 하나만 생성하고 사용하는 형태이다.

대표적인 예로는 커넥션풀이 있다.

 

싱글톤 패턴을 어떤 경우에 사용하고 왜 사용하는지 알아보자.

 

클라이언트에서 요청이 들어올 때 하나의 인스턴스가 생성 되는 프로그램이 있다.

....  10번의 요청이 들어왔을 때 10개의 인스턴스가 생성되었다.

이 프로그램은 이대로 사용을 해도 괜찮을까?

 

위와 같은 상황에서 만약 100번, 1000번 그 이상의 요청이 들어온다면 어떻게 될까?

 

GC(Garbage Collector)가 사용중이지 않은 인스턴스를 정리해준다고 해도 서버에 많은 부하가 가고 메모리가 많이 소모될 것으로 예상된다.

 

싱글톤 패턴은 이러한 경우에 사용한다.

 

인스턴스가 여러군데에서 사용되지만 해당 인스턴스가 한번만 생성되면 되는 경우에 유용한 디자인 패턴이다.

그렇기 때문에 생성자가 여러 차례 호출되어도 최초 생성한 객체를 반환하여 서버 부하를 방지하고 메모리 낭비를 방지할 수 있다.

 

아래는 (싱글톤 패턴이 적용되기 전)프린터 연결과 관련된 예제를 만들어보았다.

프린터를 호출하여 연결하는 것Printer 클래스를 인스턴스화 하는 것이라고 가정해보자.

 

사용자가 문서 출력을 위해 프린터에 연결을 시도했다.

얼마 후 다시 프린터에 연결을 시도했을 때, 새로운 프린터와 연결이 되었다. (??)

 

Printer.java

package spring.chap1.singleton;

public class Printer {
        
    public Printer() {
            System.out.println("## 프린터 생성 ##");
        }    
}

 

User.java

package spring.chap1.singleton;

public class User {

        /**
         * @param args
         */
        public static void main(String[] args) {
                Printer pt = new Printer();
                System.out.println("프린터 연결 시도 >>> " + pt.hashCode());
                System.out.println("1분 후........... ");
                pt = new Printer();
                System.out.println("프린터 연결 시도 >>> " + pt.hashCode());
        }
}

 

hashCode() 메소드를 이용하여 객체의 해시 코드(객체를 식별할 수 있는 정수값을 의미)를 출력해보았다.

 

 

두 해쉬코드가 서로 상이한 것을 보아 기존의 프린터가 아닌 다른 프린터와 연결이 달리 되었음을 알 수 있다.

이런 경우 프린터 인스턴스는 하나 이상을 필요로 하지 않는다.

이미 프린터와 연결이 되어있는데 굳이 다른 프린터에 연결을 하려는 것은 비효율적인 일이기 때문이다.

그럼 어떻게 하면 프린터를 호출할 때 하나만의 인스턴스를 유지할 수 있을까?

 

여기에 일반적으로 잘 알려진 Lazy Instantiation라고 불리는 싱글톤 패턴을 적용해보았다.

 

PrinterLazy.java

package spring.chap1.singleton;

public class PrinterLazy {
        private static PrinterLazy pl = null;
        
        private PrinterLazy(){
            System.out.println("## 프린터 생성 ##");
        }
        
        public static PrinterLazy getInstance(){
            if(pl == null){
                pl = new PrinterLazy();
            }       
            return pl;
        }
}

생성자를 private 으로 선언하기 때문에 접근이 불가능하게 된다. 따라서 외부에서 new를 통해 인스턴스 생성이 불가능하게 되었다. 대신 public static으로 선언된 getInstance() 메소드를 호출해서 인스턴스가 null일 경우 새로운 객체를 생성하여 인스턴스를 받아온다.

 

User.java

package spring.chap1.singleton;

public class User {

        /**
         * @param args
         */
        public static void main(String[] args) {
                PrinterLazy pl = PrinterLazy.getInstance();
                System.out.println("프린터 연결 시도 >>> " + pl.hashCode());
                System.out.println("1분 후........... ");
                pl = PrinterLazy.getInstance();
                System.out.println("프린터 연결 시도 >>> " + pl.hashCode());
        }
}

 

실행을 해보면 동일한 프린터 연결이 된 것을 확인할 수 있다.

 

 

하지만 이 코드에는 문제점이 있다. 

 

멀티 스레드 환경에서 Printer 인스턴스의 사용이 병렬적으로 발생한다면 인스턴스가 여러번 생성되는 경우가 발생할 수 있다. 이 말을 이해하는데 시간이 좀 걸렸다.

(일반적으로 프로그램을 실행하게 되면 하나의 프로세스가 생성이 되며, 프로세스내에는 실제 작업을 수행하는 스레드가 존재한다. 모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행한다. 이 때, 하나의 프로세스 내에 두 개 이상의 스레드가 동시에 작업을 수행하는 것을 멀티 스레드라고 한다)

 

아래 그림을 보면 스레드1, 2, 3이 Lazy Instantiation가 적용된 Printer 클래스의 인스턴스를 생성하고 있다.

 

 

 

Printer 인스턴스가 생성되지 않았을 때 스레드 1이 getInstance() 메소드를 호출해 인스턴스를 생성하려고 한다.

이 때, 스레드1의 인스턴스가 생성되기 직전 스레드 2가 마찬가지로 getInstance() 메소드를 호출해 인스턴스의 미생성으로 인해 인스턴스를 생성하는 일이 벌어졌다. 

이렇게 되면 싱글톤 패턴의 핵심이 지켜지지 못한다. 인스턴스가 2개 이상이 될 수 있는 것이다. 

 

이 문제를 멀티 스레드 환경에 안전하지 못하다고 하는데 이를 해결하기 위한 방법들이 있다.

 


1.동기화(synchronized)

Printer.java 

package spring.chap1.singleton;

public class Printer {
    private static Printer pt = null;    

    private Printer(){
        System.out.println("## 프린터 생성 ##");
    }        

    public static synchronized Printer getInstance(){
        if(pt == null) {            
            pt = new Printer();
        }
        return pt;
    }
}

 

 

getInstance() 메소드를 동기화시킴으로써 한 스레드의 사용이 끝나기 전까지 다른 스레드는 기다리게 되었다. 따라서 getInstance() 메소드가 동시에 호출될 일이 없어진다. 하지만 해당 메소드가 실행되는 경우 동기화를 위해 스레드의 Lock을 거는 행위로 인해 프로그램의 성능이 저하된다.

 

2.Eager initialization

Printer.java

package spring.chap1.singleton;

public class Printer {
    private static Printer pt = new Printer();
    
    private Printer(){
        System.out.println("## 프린터 생성 ##");
    }        
    
    public static synchronized Printer getInstance(){
        return pt;
    }
}

이 방법은 인스턴스의 생성 유무와 관계없이 초기에 인스턴스를 생성하는 방식이다. 

이 방법 역시 멀티 스레드 환경의 문제점을 해결할 수 있다. 하지만 초기 인스턴스 생성으로 인해 프로그램의 시작과 종료까지 객체가 메모리에 상주하게 된다. 때문에 사용유무에 따라 비효율적인 문제가 있다.

 

3.DCL(Double-Checking Locking) + volatile

Printer.java

package spring.chap1.singleton;

public class Printer {
    private volatile static Printer pt = null;

    private Printer(){
        System.out.println("## 프린터 생성 ##");
    }        
    
    public static Printer getInstance(){
        if(pt == null) {
            synchronized (Printer.class) {
                if(pt == null){
                    pt = new Printer();
                }
            }
        }
        return pt;
    }
}

DCL은 인스턴스를 체크하여 인스턴스가 null일 경우에만 동기화를 한다. 즉, 최초 인스턴스 생성시에만 동기화 블럭에 진입하게 되면서 이후로는 동기화되지 않는다. 하지만 volatile(자바 변수를 메인 메모리에 저장하겠다는 명시)을 사용하지 않게 되면 멀티 스레드 환경에서 각 스레드마다 기억하는 변수의 값이 다를 수 있어(왜냐하면 각 스레드마다 동일한 메모리를 공유하지 않기 때문) 인스턴스의 동기화가 보장되지 않는다.

 

volatile를 사용하게 되면 멀티 스레드 환경에서 인스턴스의 동기화를 보장받을 수 있기 때문에 volatile이 적용된 DCL을 사용하는 것이 좋다.
참고로 volatile 키워드는 자바 5 버전이상에서만 사용가능하다.

+피드백은 언제나 환영입니다 :)

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.