본문 바로가기

신입 개발자 면접 기초

싱글톤 패턴(Singleton pattern)을 쓰는 이유와 문제점

싱글톤 패턴(Singleton Pattern)

싱글톤 패턴

애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고(Static) 그 메모리에 인스턴스를 만들어 사용하는 디자인패턴.

생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나고 최초 생성 이후에 호출된 생성자는 최초에 생성한 객체를 반환한다. (자바에선 생성자를 private로 선언해서 생성 불가하게 하고 getInstance()로 받아쓰기도 함)

=> 싱글톤 패턴은 단 하나의 인스턴스를 생성해 사용하는 디자인 패턴이다.

(인스턴스가 필요 할 때 똑같은 인스턴스를 만들어 내는 것이 아니라, 동일(기존) 인스턴스를 사용하게함)



싱글톤 패턴을 쓰는 이유

고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있음

또한 싱글톤으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽다.

DBCP(DataBase Connection Pool)처럼 공통된 객체를 여러개 생성해서 사용해야하는 상황에서 많이 사용.

(쓰레드풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정, 로그 기록 객체등)

안드로이드 앱 같은 경우 각 액티비티나 클래스별로 주요 클래스들을 일일이 전달하기가 번거롭기 때문에 싱글톤 클래스를 만들어 어디서나 접근하도록 설계하는 것이 편하기 때문...

+ 인스턴스가 절대적으로 한개만 존재하는 것을 보증하고 싶을 경우 사용.

+ 두 번째 이용시부터는 객체 로딩 시간이 현저하게 줄어 성능이 좋아지는 장점!




싱글톤 패턴의 문제점

싱글톤 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 "개방-폐쇄 원칙" 을 위배하게 된다. (=객체 지향 설계 원칙에 어긋남)

따라서 수정이 어려워지고 테스트하기 어려워진다.

또한 멀티쓰레드환경에서 동기화처리를 안하면 인스턴스가 두개가 생성된다든지 하는 경우가 발생할 수 있음

개발을 할때 항상 들어온 goto는 쓰면 안돼! 전역 객체는 안 좋은거야! 라는 말 처럼 꼭 필요한 경우아니면 지양해야함. // 적절히 잘 쓰면 아주 좋음, (그게 어렵지)




멀티쓰레드에서 안전한(Thread-safe) 싱글톤 클래스, 인스턴스 만드는 방법

1. Thread safe Lazy initialization (게으른 초기화)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ThreadSafeLazyInitialization{
 
    private static ThreadSafeLazyInitialization instance;
 
    private ThreadSafeLazyInitialization(){}
     
    public static synchronized ThreadSafeLazyInitialization getInstance(){
        if(instance == null){
            instance = new ThreadSafeLazyInitialization();
        }
        return instance;
    }
 
}
cs

private static으로 인스턴스 변수를 만들고 private 생성자로 외부에서 생성을 막았으며 synchronized 키워드를 사용해서 thread-safe하게 만들었다.

하지만 synchronized 특성상 비교적 큰 성능저하가 발생하므로 권장하지 않는 방법이다.


2. Thread safe lazy initialization + Double-checked locking

- 게으른 초기화의 성능저하를 완화시키는 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadSafeLazyInitialization {
 
    private volatile static ThreadSafeLazyInitialization instance;
 
    private ThreadSafeLazyInitialization(){}
     
    public static ThreadSafeLazyInitialization getInstance(){
        
        if(instance == null){
            synchronized (ThreadSafeLazyInitialization.class) {
                if(instance == null)
                    instance = new ThreadSafeLazyInitialization();
            }
 
        }
        return instance;
    }
}


getInstance()에 synchronized를 사용하는 것이 아니라 첫 번째 if문으로 인스턴스의 존재여부를 체크하고 두 번째 if문에서 다시 한번 체크할 때 동기화 시켜서 인스턴스를 생성하므로 thread-safe하면서도 처음 생성 이후에 synchonized 블럭을 타지 않기 때문에 성능저하를 완화했다.

그러나 완벽한 방법은 아니다. 


3. Initialization on demand holder idiom (holder에 의한 초기화)

클래스안에 클래스(Holder)를 두어 JVM의 Class loader 매커니즘과 Class가 로드되는 시점을 이용한 방법

1
2
3
4
5
6
7
8
9
10
11
12
public class Something {
    private Something() {
    }
 
    private static class LazyHolder {
        public static final Something INSTANCE = new Something();
    }
 
    public static Something getInstance() {
        return LazyHolder.INSTANCE;
    }
}
cs

개발자가 직접 동기화 문제에 대해 코드를 작성하고 문제를 회피하려 한다면 프로그램 구조가 그 만큼 복잡해지고 비용 문제가 생길 수 있고 특히 정확하지 못한 경우가 많다.(100%가 아닐수 있음)

그런데 이 방법은 JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용하여 싱글턴의 초기화 문제에 대한 책임을 JVM에 떠넘긴다.

holder안에 선언된 instance가 static이기 때문에 클래스 로딩시점에 한번만 호출될 것이며 final을 사용해 다시 값이 할당되지 않도록 만든 방법.

* 가장 많이 사용하고 일반적인 Singleton 클래스 사용 방법이다.


참고 사이트

http://limkydev.tistory.com/67

https://blog.seotory.com/post/2016/03/java-singleton-pattern

  • 푸르딩 2018.01.05 15:40

    마지막 홀더를 사용하는 방법은 그냥 class 안의 singleton 변수를 static final 로 선언/초기화 하는것과 뭐가 다른가요?

    • 직접 static final로 기술할 경우 Something 클래스가 로드될 때 싱글톤 변수가 로드되어서 사용하지도 않는데 메모리에 할당됩니다.
      하지만 내부클래스인 Holder클래스에 싱글톤을 할당하면 getInstance()로 접근할 때 할당되기 때문에 메모리가 낭비되지 않습니다.
      (질문 덕분에 클래스로더에 대해 공부하게 되었습니다 감사합니다.)

      public class Something {
      private Something() {
      System.out.println("Something 생성");
      }
      static {
      System.out.println("Something static load");
      }
      private static class LazyHolder {
      public static final Something INSTANCE = new Something();
      static {
      System.out.println("Holder static load");
      }
      }

      public static Something getInstance() {
      return LazyHolder.INSTANCE;
      }
      public static void main(String[] args) {
      System.out.println("메인 함수 시작");
      Something a = Something.getInstance();
      }
      }

    • 푸르딩 2018.01.08 10:30

      아... 그렇네요 감사합니다 ^^

  • Favicon of https://defacto-standard.tistory.com BlogIcon defacto standard 2018.04.28 21:02 신고

    특정 정보를 담는데, 해당 정보를 하나만 가지고 있어야 하는 경우는 어떻게 하나요?

    예를 들어서, Color 클래스가 있다고 한다면 String 값으로 Red, Blue 등을 가지고 있는 객체는 단 한개여야 하지만,
    싱글톤으로 구현 시 Color 객체는 1개밖에 못만드는데, 어떻게 해결하나요?

  • 2018.11.01 22:46

    이거.. 3번이 Lazy initialization 기법인데요..? 거꾸로 알고계신듯 합니다 ㅠ

    • 1, 2, 3번 모두 Lazy initialization 입니다.

      1,2,3번 모두 처음부터 싱글톤 변수에 초기화를 시키지 않고 있습니다.
      필요로 할 때(게으르게) getInstance() 메서드를 통해 인스턴스를 생성하죠.

      1번은 synchronized 를 이용해서 멀티 쓰레드가 getInstance() 메서드를 동시에 접근해서 2개 이상 싱글톤 객체가 생기는 것을 방지하는 것입니다.

      2번은 getInstance() 메서드 전체를 synchronized로 잡는 것은 불필요하게 lock을 잡는 것을 방지하는 방식으로 개선한 것입니다.

      3번은 synchronized 키워드로 개발자가 직접 싱글톤 변수의 중복 생성을 방지하는 것이 아니라 JVM에 위임하는 방법입니다.
      - static 키워드는 클래스가 로딩될 때 한번 만 실행된다는 것을 JVM이 보장해줍니다. 따라서 getInstance() 메서드가 최초로 호출될 때 JVM이 클래스를 로드하며 INSTANCE를 생성해줍니다.
      - final 키워드를 통해서 초기화된 이후 다시 값이 할당되는 것을 방지해서 싱글톤임을 보장합니다.

      제가 잘 못 알고 있는점 지적 부탁드립니다. 감사합니다.

  • Dog발자 2018.12.03 17:31

    싱글톤의 주요 단점이라면 lifecycle제어하기 힘들다는 점도 있다고 하네요.
    예를들어 앱에 한 군데에서만 쓰고 말 모듈을 싱글톤으로 구현했을때, 해당 싱글톤이 제대로 gc가 되지 않는다면 끝까지 메모리를 먹어버린다는 점이죠. 그래서 싱글톤을 남발하는것은 굉장히 안좋다고 합니다^^;;

    • 지식 공유에 감사드립니다.

      맞습니다. 싱글톤은 적절하게 사용하는 것이 가장 어렵죠.
      저도 때로는 그냥 new로 생성해서 사용하고 Util같은 경우는 static 메서드를 써버리기도 합니다.

  • 꿀봉 2019.02.24 15:47

    안녕하세요 정리해주신 내용 잘보았습니다.
    3번 방식도 멀티쓰레드 환경에서 thread-safe 한 것인가요???

  • Favicon of https://jokerkwu.tistory.com BlogIcon 땡초남 2019.03.17 20:41 신고

    좋은 정보 감사합니다.! 싱글톤 패턴에 대해서 단순히 synchronized만 걸어주면 되는 줄 알았는데..
    더 효율적인 방법이 있군요,.!!

  • orionjeong 2019.03.20 13:13

    안녕하세요 싱글톤 패턴에 대해서 정리해주셔서 감사합니다. 궁금한 점이 있는데요, 마지막에 나온 홀더클래스를 통해서 싱글톤 패턴을 구현하는 방식의 로드되는 시점이 궁금합니다. 제가 알기론 자바는 대부분이 클래스가 로드되면서 필요한 클래스들을 바로 로드하는 로드타임 동적로딩인걸로 알고 있습니다. 그렇다면 static final로 싱글톤 변수를 가지고 있을 때 Something 클래스가 로드되면서 메모리에 할당되는 것과 내부클래인 Holder클래스에 싱글톤변수를 가지고 있을 때의 차이가 없다고 생각하는데요, 이렇게 생각한 이유는 Something클래스를 로드하면서 내부 Holder클래스를 로드할꺼라고 생각했고 그 Holder클래스를 로드하면서 static변수인 싱글톤변수를 할당할거라고 생각해서입니다. 그래서 getInstance로 접근할때 할당된다는 것이 아직 이해가 안되는데 혹시 더 자세히 설명해주실 수 있나요?

    • 안녕하세요!
      날카로운 질문 감사합니다.
      덕분에 저도 아래와 같은 내용을 공부할 수 있게 되었습니다.
      - 클래스로더의 로딩 방법 두 가지
      1. 런타임 로딩 : 특정 클래스의 코드를 실행할 때 클래스를 로드하는 방법
      2. 로드 타임 로딩 : 런타임 로딩에 의해서 클래스(A)가 로딩될 때, 해당 클래스(A) 내부에서 참조(사용)하는 클래스(B, C, D, ...)가 있다면 그 클래스도 로드하는 방법

      위에서 말한 로딩 방법에 의해 로드 시점만 봤을 때는 말씀하신대로 차이가 없습니다.
      결국은 Something 클래스 내부의 메서드가 호출될 때 싱글톤 객체를 할당받기 때문이죠.
      차이가 나는 부분은 Something 클래스에 또 다른 static 메서드가 존재할 때 입니다.

      public class Example {
      public static final Example example = new Example();
      private Example() {
      System.err.println("example create");
      }
      public static Example getInstance() {
      System.err.println("getInstance");
      return example;
      }
      public static void commonsMethod() {
      System.err.println("commonsMethod");
      }
      //.. none-static method
      }
      위와 같이 public static final 변수를 꺼내고 또 다른 static 메서드인 commonsMethod()를 만들었습니다.
      그리고 comonsMethod()를 호출했습니다.
      그러면 아직 example이라는 싱글톤 객체를 사용하지도 않을 건데 commonsMethod가 불렸다는 이유로 static final Example example 변수를 할당합니다.
      반면에 Holder를 이용한 경우에는 또 다른 static 메서드가 호출되더라도 Something 싱글톤 객체가 할당되지 않고 오직 getInstance()가 불렸을 때만 할당됩니다.

      질문하신 의도에 맞게 답변이 잘 되었을지는 모르겠습니다...
      저도 부족한 부분은 공부해서 전달해드리는 것이라 또 다른 차이점이 있는지는 모르겠습니다.

    • orionjeong 2019.03.20 20:32

      답변 감사합니다. ㅎㅎ

      아직 이해를 하지 못했는데요.

      제가 생각했던 건 이미 Something이 로드되는 시점에 static final로 변수를 뺀 경우든, Holder클래스로 감쌌던지 static이 쓰일 수 있다는건 Somthing클래스가 로드가 된 후라고 생각하고 있습니다.

      그리고 Something이 로드가 되었다는건 그 안의 static 변수,메서드도 이미 로드되어있는 상태라고 생각하고 있습니다.

      어느쪽이든 그 static메서드를 쓸 때 쯤이면 모든 클래스 그 클래스 안까지 로드가 되어있을거라 생각해서 아직도 의아하네요 ㅠㅠ

      제가 클래스로더를 잘못알고 있어서 그런건지몰라도 아직까지는 정답을 찾지 못했습니다.

    • orionjeong 2019.03.20 20:36

      혹시 내부 static class는 class가 로드되는 시점에 로드되는게 아닌가요?

    • 네 맞습니다.
      클래스 내부에 static 클래스든 아니든 결국은 클래스로더는 내부 클래스를 쓸 때 로드됩니다.
      Something 클래스가 로드되어도 Holder는 로드되지 않습니다.

      A클래스 내부에 B클래스가 정의되어있고
      A클래스의 "메서드1"에서 B클래스가 전혀 필요하지 않다면 "메서드1"이 호출되는 순간, A클래스가 로드되어있지 않다면 A클래스만 로드할 것입니다. (B클래스는 로드 안함)
      그리고 A클래스의 "메서드2"에서 B클래스를 참조하는 상황이라면 "메서드2"가 호출될 때 그제서야 B클래스를 로드합니다.

    • orionjeong 2019.03.20 21:57

      정보 감사합니다. ㅎㅎ Holder클래스도 Static이기 때문에 클래스가 로드되면서 바로 로드되는 줄알았는데 아니었나 보네요 ~~ 혹시 이와 관련된 출처가 있을까요?? 아무리 찾아도 이와 관련된 정보는 얻을 수가 없어서요 ㅠㅠ, 없어도 괜찮습니다. 좋은 지식 공유해주셔서 감사합니다~~

    • 위의 푸르딩님의 질문에 대한 답변의 코드를 실행해보셔도 좋습니다.
      내부클래스(Holder)가 로드되는 시점에 static 블록이 실행될 것이니까요.
      출처로 확인해보셔도 좋습니다:)
      https://coderanch.com/t/573706/java/Loading-Static-class
      https://stackoverflow.com/questions/24538509/does-the-java-classloader-load-inner-classes

    • orionjeong 2019.03.21 02:02

      지식을 너무 쉽게얻으려고했네요ㅠㅠ.
      위의 소스코드만 확인해봐도 되는거였는데...

      제가 오해했던 부분은 클래스의 타입만 지정돼도 클래스가 로드되는 줄 알았는데 그게 아니네요.

      객체가 만들어지든, 그안의 static변수나 메서드가 호출될 때 즉 진짜로 코드의 실행이 필요할 때 로드가 되네요.

      지식공유해 주셔서 감사합니다. 덕분에 확실히 이해했습니다.

    • 괜찮습니다:)
      저도 덕분에 다시 한 번 공부하고 개념을 잡을 수 있어서 좋았습니다!

  • choe 2019.07.07 11:35

    제일마지막패턴의
    public static final Something INSTANCE = 이 부분이 잘 이해가안되는대요~
    왜 public으로 선언하신거에요? private해도 modifier access관련 에러는 발생하지않더라구요~
    궁금합니다!

  • dd 2020.01.27 23:09

    ThreadSafeLazyInitialization.class 이 문법이 이해가 되지 않는데요. .class 라는 문법은 무슨 의미인가요?

  • 질문있습니다 2020.06.23 11:04

    가장 마지막방법을 사용하면 클래스를 초기화 할때, 인자값을 어떻게 넘겨줄 수 있을까요???
    public static TestClass getInstance(String args) {
    // 초기화
    }

    다음과 같을때, 마지막 방법은 인자값을 안전하게 넘길 수 있는 방법이 없는 걸까요?

    • 정말 좋은 질문 감사합니다!
      말씀해주신 부분 조금 찾아봤는데요...
      클래스로더가 static 변수를 생성해주는 타이밍을 이용한 것이라 LazyHolder를 이용한 방법으로는 파라미터 전달이 어려운 것 같습니다...
      다른 방식을 이용해야할 것 같은데요, Spring에서 어떻게 파라미터를 전달하고 있는지 알아봐야겠습니다...ㅠㅠ

    • Favicon of https://vvshinevv.tistory.com BlogIcon 최홍희 2020.08.23 22:01 신고

      저도 궁금해서 자료를 찾아보니, 파라미터가 있는 것 자체가 싱글톤이 아니라는 의견이 있네요. 이유는 파라미터가 있다는 것 자체가 여러 상태가 존재한다는걸 가정한다는 의미에서 그런 말이 나온 것 같아요.

      https://datacadamia.com/code/design_pattern/singleton

    • 좋은 의견 감사합니다.
      스프링에서도 DI로 생성자 파라미터로 주입을 받지만 싱글톤 객체로 있는 경우도 있어서 그 부분을 좀 찾아보고 싶더라고요... (지금은 몸 대신 마음만 그렇지만요 ㅠㅠ)