싱글톤

Dayeon myeong·2022년 3월 4일
1

면접

목록 보기
9/35

Singleton pattern 이란 무엇이고, 어떤 장점과 단점이 있을까요?

Singleton pattern(싱글턴 패턴)이란 애플리케이션에서 인스턴스를 하나만 만들어 사용하기 위한 패턴이다.

java에서는 생성자를 private으로 선언해 다른 곳에서 생성하지 못하도록 만들고, getInstance() 메소드를 통해 받아서 인스턴스 하나만을 사용하도록 구현한다.

커넥션 풀, 스레드 풀 등의 경우, 인스턴스를 여러 개 만들게 되면 자원을 낭비하게 되거나 버그를 발생시킬 수 있으므로 오직 하나만 생성하고 그 인스턴스를 사용하도록 하는 것이 이 패턴의 목적이다.

장점

  • 객체 재사용이 가능해서 메모리 절약할 수 있다.

단점

  • 멀티 스레드 환경에서 동기화 문제가 발생할 수 있다. 문제 해결을 위해 syncrhonized, double checked Locking, LazyHolder 방식을 사용할 수 있다.
  • private 생성자로 인해 상속을 할 수 없다
  • static 필드와 static 메소드를 사용하기 때문에 다형성같은 객체 지향 특징이 적용되지 않는다.
  • 테스트하기 어렵다 : private 생성자로 싱글톤 객체 생성에 제한적이기 때문에 인터페이스를 구현한 싱글톤 객체가 아니라면 mock 객체 (가짜 객체)를 만들 수 없어 이를 사용해 테스트하기 어렵다.

static, final

final은 값의 재할당을 하지 못한다. final 이란 영어 뜻 최종적, 마지막이라는 의미 그대로 초기에 한 번 할당을 하게 되면 최종적인 값이 된다. 즉, 재할당을 막는다는 것이지 불변이라는 것은 아니다.

static 변수는 전역 변수로, Heap 영역이 아닌 GC 대상이 아닌 static 영역에 할당된다. 따라서 객체 생성 없이도 동일한 static 변수를 공유해서 사용할 수 있다.

static 필드를 왜 정적 필드라고 하냐면, static 영역에 할당되어 프로그램 종료시까지 남는다. 따라서 몇개의 인스턴스를 생성하든 클래스들이 모두 동일한 단 1개의 static 필드를 공유해서 갖는다. 인스턴스 별로 필드를 가지는게 아니기 때문이다.

지연 초기화 lazy initialization

지연 초기화는 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 방법이다. 값이 전혀 쓰이지 않으면 초기화도 결코 일어나지 않는다.

초기화란 객체를 선언하고 그 값을 최초로 할당하는 것이다. 즉, 최초로 값을 넣어주는 것. ex) int a = 10

대부분의 경우에는 초기화 지연보다는 일반적으로 클래스를 생성하면서 초기화 하는 것이 좋다. 초기화 비용이 크고, 내부적으로 필드 사용 빈도가 적다면 초기화 지연이 적절하다.

기본 싱글톤 패턴

public class Singleton { 
	
	private static Singleton instance; // 단 하나의 인스턴스만 사용

	private Singleton() {} // private 생성자. 외부에서 인스턴스 생성못함.

	public static Singleton getInstance() { // 단하나의 인스턴스만 사용

		if (instance == null){ //여러 스레드에서 이 곳을 동시에 실행하면 문제 발생
			instance = new Singleton(); 
		} 
	return instance; 
	} 
}

싱글톤 패턴시 사용하는 코드이다. 생성자를 private으로 만들어 클래스 외부에서는 인스턴스를 생성하지 못한다. getInstance() 메서드 사용 시점에 단하나의 인스턴스를 한다. 즉 지연 초기화한다.

문제점 : 실제로는 멀티스레드 환경에서 이 코드는 thread safe하지 않다. 인스턴스가 null인지 확인하는 부분을 여러 스레드에서 동시에 확인한다면 인스턴스를 여러 개 생성할 수 있기 때문이다. 그래서 이후에 getInstance()를 해도 동일하지 않은 인스턴스가 반환될 수 있다.

synchronized 사용

public class Singleton { 
	
	private static Singleton instance; 

	private Singleton() {} 

	public static synchronized Singleton getInstance() {  // synchronized 추가
		if (instance == null){ 
			instance = new Singleton(); 
		} 
	return instance; 
	} 
}

getInstance 메서드를 처음 사용할 때 인스턴스를 생성하기 때문에 이 역시 지연초기화다.

기본 싱글톤 패턴의 동기화 문제는 해결했지만, 성능상에 문제가 있다.

문제점 : 성능의 문제가 있다. 여러 클라이언트 요청이 있을 때 getInstance() 메서드에 static synchronized를 적용했기 때문에 Singleton 클래스 자체를 락을 걸었다. 클래스를 사용할 때 단 하나의 스레드만 사용하게 되는 것이다. 그럼 다른 스레드는 락이 풀릴 때까지 기다려야 한다.

Double checked Locking의 문제

Lazy Holder 방식 외에도 double checked locking 방식이 있다.

이펙티브 자바에서는 성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중 검사 double check 관용구를 사용하라고 한다. 이 관용구는 이미 초기화가 된 필드에 접근할 때는 동기화를 하지 않아도 된다. 즉, 초기화된 필드에 접근할 때의 동기화 비용을 없애준다. 초기화되지 않았을 때만 동기화하여 검사한다.

정적 필드에서는 LazyHolder가 Lock이 전혀 필요하지 않으니 당연히 LazyHolder가 낫지만 double checked Locking도 사용할 수 있다.

public class Singleton {
    private static DoubleCheckedSingleton instance;

    private DoubleCheckedSingleton() {}

    public DoubleCheckedSingleton getInstance() {

     if(instance == null) { //첫번째 검사는 락을 사용안함. 이미 초기화되어있다면 바로 리턴하여 동기화 비용을 없애준다.
         synchronized(Singleton.class) {//이건 위와 동일한 static synchronized라고 보면 될 듯
            if(instance == null) { //두번째 검사는 락을 사용한다. 초기화되어있지 않기 때문에.
               instance = new DoubleCheckedSingleton();
            }
         }
      }
      return instance;
    }
}

하지만 double checked 방식은 필드가 초기화된 후로는 동기화하지 않으므로 해당 필드는 반드시 volatile로 선언해서 가시성을 확보해야 한다. 특히 멀티 코어 환경에서 동작한다면 스레드 별 cpu cache와 메인 메모리 간에 동기가 이뤄지지 않을 수 있다.

예를 들어

  • 첫번째 스레드가 instance를 생성하고 synchronized를 벗어남.
  • 두번째 스레드가 synchronized 블록에 들어와서 null 체크를 하는 시점에서,
  • 첫번째 스레드에서 생성한 instance가 CPU cache가 있는 working memory에만 존재하고 main memory에는 존재하지 않을 경우
  • 또는, main memory에 존재하지만 두번째 스레드의 working memory에 존재하지 않을 경우
  • 즉, 메모리 간 동기화가 완벽히 이루어지지 않은 상태라면 두번째 스레드는 인스턴스를 또 생성하게 된다.
    따라서 volatile을 붙여야 thread safe하게 사용할 수 있다.
public class Singleton {
    private volatile static DoubleCheckedSingleton instance;

LazyHolder

public class Singleton {

    private Singleton() {}

    private static class LazyHolder() {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return LazyHolder.instance;
    }

}

성능 문제를 해결하기 위해서 이펙티브 자바에서는 정적 필드(static 필드)를 지연초기화해야한다면 지연 초기화 홀더 클래스 (lazy initialization holder class ) 관용구를 사용하라고 한다.
이 또한 getInstance가 처음 호출되는 순간 LazyHolder.instance가 읽히면서 비로소 LazyHolder 클래스 초기화를 촉발한다.

synchronized 메서드 방식에 비해 성능 문제가 어떻게 해결됬냐면
getInstance() 메서드가 instance 필드에 접근할 때 전혀 동기화를 하지 않으니 Lock을 안걸고 성능이 느려질 거리가 전혀 없다는 것이다.

다음 싱글턴 코드의 어떤 점을 개선하실 수 있습니까? (개선이 필요 없을 수도 있음 / 왜?)


class MySingleton {
  private static MySingleton instance;

  public static synchronized MySingleton getInstance() {
    if (instance == null) {
        instance = new MySingleton();
    }
    return instance;
  }
}

synchronized를 사용해서 멀티 스레드 환경에서 여러개의 인스턴스가 생성되는 것을 막았지만 lock을 거는 것이기 때문에 성능상의 문제가 있다. static synchronized는 해당 클래스 자체에 락을 거는 것이다.

  public static MySigleton getInstance() {
	synchronized(MySingleton.class) {... }
  }

따라서 클래스를 사용할 때 한 번에 단 하나의 스레드만 사용하게 되는 것이다. 그럼 다른 스레드들은 이 클래스 락이 풀릴 때까지 기다려야 한다.

public class Singleton {

    private Singleton() {}

    private static class LazyHolder() {
        private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return LazyHolder.instance;
    }

}

성능적인 개선을 개선을 해야한다면 LazyHolder 방식을 사용할 수 있다.
getInstance 메서드가 처음 호출될 때 LazyHolder.instance가 읽히면서 비로소 LazyHolder 클래스 초기화가 이뤄진다. final이기 때문에 값의 초기화 한 번 이후 재할당이 없고 static 이기 때문에 싱글톤으로 전역으로 공유해서 사용이 가능하다. 따라서 getInstance() 메서드가 instance 필드에 접근할 때 전혀 Lock을 안걸고도 동기화 문제를 해결할 수 있다.

스프링 빈 스코프 : 싱글톤, 프로토타입

  • 빈 : IoC 컨테이너에 등록된 객체

  • 싱글톤

    • 설명 : 객체를 하나만 생성
    • 장점 : 객체가 재사용
    • 단점 : 멀티스레드 이슈
  • 프로토타입

    • 설명 : 빈을 주입할 때 새로운 객체가 생성되는 것

      @Component @Scope("prototype")
      public class Proto {
      
       }
    • 장점

      • immutable
    • 단점

      • 객체가 새로 생성
  • 프로토타입의 빈이 싱글톤 타입의 빈을 참조하는 경우

    • 프로토타입은 매번 다른 인스턴스로 생성되지만, 참조하고있는 싱글톤은 매번 같은 인스턴스이므로 아무 문제가 없다
  • 싱글톤 타입의 빈이 프로토타입의 빈을 참조하는 경우

    • 싱글톤은 객체가 한번만 생성된다. 이 때 프로토타입도 같이 세팅된다. 그리고 이후에는 객체를 다시 생성하지 않으니까, 프로토타입 스코프임에도 불구하고 프로토타입 빈은 매번 생성되지 않는다.
    • 해결 방법
      • ProxyMode 설정 : 프로토타입에 대한 Proxy를 만들도록 함.
        • 싱글톤 빈은 프로토타입 빈을 직접 참조하지 않고 Proxy가 대신해서 사용되기 때문에 Proto는 매번 다른 인스턴스를 생성할 수 있게 된다

싱글턴 코드는 테스트를 어렵게 만드는 문제가 있습니다. 왜 그럴까요? 싱글턴이 좋지 않다는데 왜 스프링 프레임워크 같은 녀석들은 별다른 규칙이 없을 때 기본으로 Singleton bean 을 만들까요?

테스트하기 어렵다 : private 생성자로 싱글톤 객체 생성에 제한적이기 때문에 인터페이스를 구현한 싱글톤 객체가 아니라면 mock 객체 (가짜 객체)를 만들 수 없어 이를 사용해 테스트하기 어렵다.

스프링에서 싱글톤 빈을 사용하는 이유는 서버의 성능에 좋기 때문이다. 서버 프로그램에서 매번 요청시마다 여러 개의 객체를 생성하는 것은 서버의 성능을 저하시킨다. 하지만 싱글톤 빈을 사용하면 객체를 더이상 생성하지 않기 때문에 성능을 높일 수 있다.

스프링에서 사용하는 싱글톤은 싱글톤 패턴과는 다르다. 싱글톤 레지스토리라고 하는데 싱글톤 객체를 저장하고 관리하는 곳으로 싱글톤 패턴과는 달리 public 생성자를 가질 수 있고, 간단히 객체를 생성해서 테스트도 자유롭고 DI도 가능하다. 즉 싱글톤 패턴과는 자유롭게 객체지향적인 설계나 원칙을 적용하는데 아무런 제약이 없다.

그리고 싱글톤은 보통 멀티 스레드 환경에서의 동시성 문제가 있긴 하지만
스프링은 Service, Repository, Controller같은 경우 대부분 상태 변수가 없이 무상태성으로 클래스를 설계하여 동시성 문제를 막을 수 있다. 변경이 일어날 수 있는 정보들에 대해서는 파라미터, 지역변수, 리턴값등을 활용하면 된다.

참고

인프런 백기선 Spring 강의

토비의 스프링 1장

https://junghyungil.tistory.com/150

https://github.com/JaeYeopHan/Interview_Question_for_Beginner/tree/master/DesignPattern

https://github.com/gyoogle/tech-interview-for-developer/blob/master/Design%20Pattern/Singleton%20Pattern.md

profile
부족함을 당당히 마주하는 용기

0개의 댓글