티스토리 뷰

Language/Java

싱글톤(Singleton)에 대하여

DUCKBAE's 2024. 10. 20. 23:10

싱글톤이 무엇이고 메모리 관점에서 어떻게 동작하는지에 대해 알아보려고 한다.


싱글톤 패턴

클래스에서 오직 한 개의 객체만 갖도록 보장하고, 객체에 대한 전역적인 접근점을 제공하는 패턴이다.

 

싱글톤

프로그램 내에서 단 한번만 생성된 객체이다.

 

왜 그리고 언제 사용할까

싱글톤을 사용하는 목적은 하나의 객체가 반복적으로 생성되지 않도록 보장하기 위한 것이다.

즉 인스턴스가 오직 하나여야 함을 보장하고, 잘 정의된 접근점으로 모든 사용자가 접근할 수 있도록 해야할 때 사용한다.

예로 데이터베이스 연결(DBCP, Database Connection Pooling)이 있다. 데이터베이스 연결이 필요한 곳에서 매번 새로운 연결 객체를 생성한다면, 리소스 낭비가 발생하고 성능 저하가 일어날 수 있다.

객체를 사용할 때 데이터베이스를 연결하고, 사용이 필요없어지면 데이터베이스 연결을 해제하고.. 객체마다 이러한 동작이 반복될 것이다. 한 번 생성된 연결을 재 사용하게 된다면 새로운 연결을 생성하는 오버헤드를 줄일 수 있다. 따라서, 데이터베이스 연결을 효과적으로 관리하기 위해 싱글톤 패턴을 사용하는 것이 좋다.

 

하나밖에 생성할 수 없는 이유

싱글톤 객체를 하나만 생성할 수 있는 이유는 외부에서 객체를 생성할 수 없기 때문이다. 즉 외부에서 생성자에 대한 접근이 불가능하도록 선언되있다는 것이다.

싱글톤은 클래스의 인스턴스가 오직 하나임을 보장하기 때문에, 싱글톤 객체는 자신 클래스에서만 생성할 수 있도록 구현하며 외부에서 해당 객체를 사용할 수 있도록 메서드를 제공한다.

예제를 보면 쉽게 이해가 갈 것이다.

public class Singleton {

    private static Singleton mySingleton = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	return mySingleton;
    }
}

본 코드는 Singleton 클래스의 생성자를 private 으로 설정하고, 생성된 객체를 반환하는 정적 메서드를 제공하고 있다.

public class Main {
	public static void main(String[] args) {
    
    	/*
            다음과 같이 컴파일 오류가 발생한다.
            'Singleton()' has private access in 'Singleton'
        */
    	Singleton singleton = new Singleton();
        
        //생성된 객체를 반환하는 정적 메서드를 호출해야한다.
        Singleton singleton = Singleton.getInstance();
    }
}

위에 Singleton 클래스의 객체를 생성하려고 하면 컴파일 오류가 발생한다. 생성자가 private 으로 제한되어있기 때문에 객체를 생성할 수 없다는 것이다. 따라서 싱글톤 객체를 사용하려면 해당 클래스에 미리 만들어진 싱글톤 객체를 반환해주는 메서드를 호출해야한다.

 

싱글톤을 구현하는 방법

위 예제에서 싱글톤 필드를 선언시에 싱글톤 객체를 생성하였다.

만약 싱글톤 객체를 생성했는데 애플리케이션에서 사용하지 않는다고 하면 불필요한 인스턴스가 되는 것으로 메모리 낭비가 된다. 따라서 싱글톤 객체를 사용할 때 객체를 생성하도록 다음과 같이 구현할 수 있다.

public class Singleton {
    private static Singleton singleton;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

근데 만약 getInstance 메서드를 동시에 호출하는 스레드가 있다면 동시성 문제가 발생한다. 이는 동시에 해당 메서드를 호출하는 스레드들은 서로 다른 인스턴스를 반환받을 수 있다는 것이다. 둘다 singleton 객체가 null이라고 판단하여 객체를 생성하기 때문이다.

이때, 동시에 여러 스레드가 이 메서드에 접근할 수 없도록 syncronized 키워드를 사용한다. 이것은 인스턴스가 중복으로 생성되는 것을 방지할 수 있다.

public class Singleton {
    private static Singleton singleton;
    
    private Singleton() {}
    
    public static syncronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

하지만, 이 또한 인스턴스가 이미 생성된 경우에도 getInstance 메서드를 호출하는 모든 곳에서 불필요한 locking 과정을 거쳐야 하므로 성능에 영향을 줄 수가 있다.

이러한 문제를 해결하기 위해 다음과 같이 double checked locking 방식을 사용할 수 있다.

public class Singleton {
    private static volatile Singleton singleton;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (singleton == null) { (1)
            synchronized (Singleton.class) { (2)
                if (singleton == null) { (3)
                    singleton = new Singleton(); (4)
                }
            }
        }
        return singleton;
    }
}

이 방식은 singleton 객체가 null인지 두번 체크한다. 이는 첫 동시 스레드에만 locking 과정을 수행하며, 객체가 생성되었을 땐 locking 과정이 필요없이 싱글톤 객체를 반환받을 수 있다.

첫 번째로 확인할 때에는 동시에 요청온 스레드들이 코드를 수행할 수 있지만, 두 번째로 확인할 때에는 하나의 스레드만 접근할 수 있도록 구현되어있다.

 

동시에 스레드 3개가 getInstance 함수를 호출했다고 가정해본다. 3개의 스레드는 각각 1번, 2번, 3번 스레드라고 이야기 하겠다.

1. 스레드 3개는 (1)번 코드를 수행할 것이다. 3개의 스레드는 싱글톤 객체가 null인지 확인한다.

2.  syncronized 키워드 덕분에 3개 중 1번 스레드만 (2)번 코드를 수행할 것이다. 이 때, 남은 2개의 스레드는 대기 상태로 전환된다.

3. 1번 스레드는 (3)번 조건에 부합하기때문에 (4)번 코드를 통해 싱글톤 객체를 생성한다. 이 때 volatile 키워드로 인해 2, 3번 스레드는 싱글톤 객체를 즉시 참조할 수 있게된다.

4. 1번 스레드가 객체 생성을 마치고 synchronized 블록 수행을 종료하면 2,3번 스레드도 동일하게 synchronized 블록을 수행할 수 있다. (3)번 코드를 실행하는데 이미 생성된 객체가 존재하므로 (4)로직을 수행하지 않고 바로 싱글톤 객체를 반환한다.

 

싱글톤을 구현하는 방법은 여러가지가 더 있으며, 하단에 작성된 링크에서 확인할 수 있다.

 

메모리 관점에서의 싱글톤

클래스로더는 클래스 파일을 읽어서 클래스를 메모리에 로드하는 역할을 담당한다.

클래스 로더에 의해 클래스가 로드되면 method 영역에 클래스의 정적 멤버를 포함한 클래스 정보가 할당된다.

싱글톤 클래스도 동일하게 이 과정이 적용된다.

1. (메서드 영역) 클래스 로더에 의해 Singleton 클래스가 로드되면, method 영역에 Singleton 클래스의 정적 멤버들을 포함한 클래스 정보가 할당된다. 이 때, 정적 멤버인 필드와 메서드는 클래스가 메모리에 로드될 때 생성되며 클래스 로딩이 완료되면 사용할 수 있다.

 

2. (힙 영역) 싱글톤 객체 초기화를 정적 필드 선언 시 초기화하였으면 클래스 로드 시 싱글톤 객체가 heap 영역에 미리 할당된다. 하지만 정적 메서드 함수를 통해 객체를 초기화하도록 구현하였다면, 해당 함수가 첫 번째로 호출될 때 싱글톤 객체는 초기화되며 heap 영역에 할당된다. (필드 선언 시 초기화 하는 대신 메서드 호출 시 초기화 하는 방법이 좋겠다. 필드 선언 시 초기화 하였는데 이를 사용하지 않는다면 불필요하게 메모리에 할당된 것이기에.) Singleton 객체는 한 번만 생성되기 때문에 heap 영역에는 하나의 객체만 할당되고 이를 참조하는 모든 객체는 Singleton 객체를 공유하게 된다.

 

3. (스택 영역) Singleton 객체를 사용하기 위해서 new 연산자 대신 정적 메서드인 getInstance 를 호출한다. getInstance 메서드가 호출될 때마다 method 영역의 정적 메서드가 실행되고, 유일한 Singleton 객체의 참조 주소를 반환한다.


 

싱글톤은 프로그램 실행 시 한 번만 생성되는 객체이다.

애플리케이션 내에서 인스턴스를 단 하나만 생성하여 전역적으로 접근할 수 있도록 보장하기때문에 동일한 인스턴스를 사용해야할 때 유용하다.

클래스 로더에 의해 싱글톤 클래스의 정적 멤버가 method 영역에 로드되며, heap 영역에 할당 된 단 하나의 싱글톤 객체는 사용되는 모든 곳에서 공유된다.

 

다음에는 싱글톤을 double checked locking 구현할 때 사용했던 volatile 키워드에 대해서 더 자세하게 알아봐야겠다.


참조

- https://www.baeldung.com/java-singleton-double-checked-locking

- https://www.baeldung.com/java-volatile

 

'Language > Java' 카테고리의 다른 글

인터페이스에 대하여  (0) 2024.10.27
추상클래스와 인터페이스  (0) 2024.10.22
상속과 인터페이스의 다형성  (0) 2024.10.20
인스턴스 멤버와 정적 멤버  (0) 2024.10.18
final 키워드  (0) 2024.10.18
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함