👨‍💻 싱글턴(Singleton) 패턴이란?

하나의 객체 인스턴스만 생성해서 애플리케이션 전체에서 접근이 가능하게 하여
불필요한 자원 소모를 줄이는 디자인 패턴

어떠한 클래스가 있을 때 우리는 new 라는 키워드를 통해
해당 클래스의 인스턴스 객체를 만들게 된다.
하지만 필요할때마다 새롭게 인스턴스를 만들게 되면
불필요한 자원 소모량이 많아지게 된다.

예를들어, 달력을 출력해주는 인스턴스가 필요해서
어떠한 로직에서 달력 클래스 인스턴스를 생성하고 한번 쓰고 더이상 사용하지 않는다면,
또 다른 로직에서 달력 클래스 인스턴스를 새롭게 생성하고 또 한번 쓰고 더이상 사용하지 않는다면,

그냥 그렇게 자원을 불필요하게 소모하는 것이다.

실제로 이것을 막기 위해 싱글턴 패턴을 구현한 방법이 있다.
위의 예시에서 java의 Calendar 클래스는
생성자를 private으로 지정해놓는다.

그렇게 되면 인스턴스 객체를 아예 하나도 생성하지 못하는게 아닐까?
인스턴스가 생성된 적이 있다면 기존의 인스턴스를 반환하도록
아래와같이 구현하면 된다.

public class Calendar {
	private static Calendar calendarInstance; // 단 하나의 인스턴스가 될 멤버
    
    private Calendar() {} // private 지정자로 선언된 생성자
    
    public static Calendar getInstance() {
    	if (calendarInstance == null) { // 인스턴스가 없을 경우
			this.calendarInstance = new Calendar(); // 생성(private은 클래스 내부에서만 접근할 수 있다)
        }
        return this.calendarInstance; // 인스턴스를 리턴
    }
}

위 방식은 고전적으로 싱글턴 패턴을 구현한 방법이다.
사용 예시를 보자!

public static void main(String[] args) {
	Calendar singletonCalendar = Calendar.getInstance();
    singletonCalendar.메소드()...
}

해당 클래스의 getInstance()라는 static method를 통해서 하나의 인스턴스만으로
사용이 가능하다.

하지만 이 방법은 약간(2%)정도 부족하다.
자바의 JVM은 멀티스레드 환경이 있기 때문이다.

❓ 멀티 스레드와 멀티 프로세스에 대해서는 아래에 추가 설명을 하도록 하겠다.

만약 Calendar클래스의 인스턴스를 받아오는 함수가 1번쓰레드, 2번쓰레드에서
거의 동시에 시작됐다고 하면,

위와 같이, 인스턴스가 두번 생성되는 일이 벌어질 수 있다.
이를 해결하기 위해 사용하는 방법이 바로 synchronized 제어자이다.

public class Calendar {
	private static Calendar calendarInstance; // 단 하나의 인스턴스가 될 멤버
    
    private Calendar() {} // private 지정자로 선언된 생성자
    
    public static synchronized Calendar getInstance() {
    	if (calendarInstance == null) { // 인스턴스가 없을 경우
			this.calendarInstance = new Calendar(); // 생성(private은 클래스 내부에서만 접근할 수 있다)
        }
        return this.calendarInstance; // 인스턴스를 리턴
    }
}

synchronized 제어자를 사용하면 한 스레드에서 해당 메소드 사용을 끝내기 전까지
다른 스레드에서 해당 메소드를 실행하지 않게 되는데
이또한 메소드가 끝날때까지 기다려야 하는 시간적인 문제가 있다.

이러한 방법은 Lazy Initialization(게으른 초기화) 라고도 불리며,
말그대로 엄청 늦다.

위의 예시에서 꼭 동기화가 필요한 부분은 아래 뿐이기 때문에
if (calendarInstance == null) { // 인스턴스가 없을 경우
위 부분을 제외한 나머지 부분에서 오버헤드가 발생할 수 밖에 없다.

조금 더 효율적으로 멀티스레딩을 고려한 싱글턴 패턴을 구현하기 위해서는
몇가지 방법이 존재한다.

  1. 클래스 로딩시 static 멤버에 초기화시키는 방법
public class Calendar {
	private static Calendar calendar = new Calendar();
    
    public static Calendar getInstance() {
    	return this.calendar;
    }
}

static(정적) 멤버와 메소드는 클래스 로딩시 한번만 로딩되기 때문에
이를 이용해서 딱 한번만 인스턴스를 생성할 수 있게 된다.

  1. DCL(Double-Checked-Locking)을 사용하여 인스턴스가 생성되지 않았을 때(처음에 한번만) 동기화 하는 방법
public class Calendar {
	private volatile static Calendar calendar;
    
    private Calendar() {}
    
    public static Calendar getInstance() {
    	if (this.calendar == null) {
        	synchronized (Calendar.class) { // 처음에만 동기화
            	if (this.calendar == null) {
                	calendar = new Calendar();
                }
            }
        }
    }
}

자세히 보면 멤버 변수에 volatile 키워드가 있다.
volatile 키워드는 read, write 가 sync문제가 있을 경우에 write를 우선으로 수행해서 가장 최신의 값을
저장한 뒤에 read에서의 오류가 없게끔 하는 방법이고, non-volatile은 그 반대이다.

정리하자면, 먼저 인스턴스가 생성된 적이 없을 때(처음에만) 동기화 해준다.
이때, 2개의 쓰레드에서 거의 동시에
1. 하나는 인스턴스를 생성하려고 하고
2. 하나는 읽으려고 할 때,
인스턴스가 생성돼서 저장되기 직전에 다른 스레드에서 생성된 줄 착각하고 읽으려 한다면
오류가 발생한다.
따라서, volatile 키워드를 이용해서 write를 먼저 수행하여 가장 최신의 값이 저장되고
read를 수행하여 그 최신의 값을 읽게 함으로써 오류가 없게 만들어주는 것이다.

이에 대한 참고 글들을 첨부한다.

첨부
volatile, Double-Checked-Locking



🔎 싱글턴 패턴의 장단점


장점

  1. 객체 생성 시간을 절약하여 객체 로딩 시간을 줄여 성능이 좋아진다.
  2. 무분별한 인스턴스 생성을 막기 때문에 메모리를 절약할 수 있다.

단점

  1. 테스트, 디버깅이 어렵다.
  2. 싱글톤 패턴을 위한 구현 코드가 많아진다.
  3. 구체 클래스에 의존하는 싱글턴 패턴이기 때문에 DIP를 위반한다.
  4. 하나의 인스턴스가 전역적으로 여러 곳에서 결합도가 높아지게 되면 OCP를 위반할 수 있다.



🔎 의존성 주입(Dependency Injection)


의존성 주입이란 필요한 객체를 외부로부터 주입받아서
해당 객체와의 결합도를 낮추는 것을 말한다.

대표적으로 Spring Framework에서는 이렇게 주입시켜주어야 할 객체들을 관리해주어야 하는데,
이때 필요한 것이 바로 Singleton Pattern 이다.
주입시켜주어야 할 객체를 하나의 인스턴스로만 관리하여 일관성을 유지하는 것이다.

추가로, Spring에서는 이렇게 주입시키기 위해 필요한 객체들을 Bean이라고 부르고,
IoC 컨테이너에서 이 Bean들(싱글톤객체들)을 관리하면서, 주입을 시켜주게 된다.



🔎 멀티 스레드 vs 멀티 프로세스


앞서, 언급한 멀티 스레드와 많이 혼동되는 것이 바로 멀티 프로세스이다.
프로세서(Processor)는 CPU 즉 프로세스를 실행하는 주체이고,
프로세스란 프로세서가 명령을 실행을 위해 메모리에 적재되어 사용되는 프로그램(코드)이다.
여기서 스레드가 프로세스 내에서 실제로 작업을 수행하는 주체이다.

따라서, 멀티 스레드는 한 프로세스(프로그램)를 수행하는 주체(스레드)가 여러개인 것이고
멀티 프로세스는 실행할 프로세스(프로그램들)이 여러개가 동시에 병렬적으로 수행되는 것을 말한다.



profile
백엔드를 사랑하는 초보 개발자

0개의 댓글