이카's
article thumbnail

싱글톤 패턴(Singleton)

보통 하나의 클래스를 기반으로 여러 개의 인스턴스를 만들지만, 그러지 않고 하나의 클래스에 하나의 인스턴스만 만들어 이를 기반으로 로직을 쓴다.
즉 하나의 클래스에 하나의 인스턴스만 가지는 패턴이다.

 

 

싱글톤 패턴은?

싱글톤 패턴은 생성 패턴으로 생성패턴에는 두 가지 중요한 점이 있다.

  1. 생성 패턴은 어떤 Class를 사용하는지 캡슐화를 한다.
  2. 인스턴스들의 결합, 생성에 대한 정보를 은닉한다.

즉, 생성 시점, 누가 사용하는지, 무엇이 생성되는지 등등 유연성을 가질 수 있다.

 

문제점 or 단점

싱글톤 패턴을 사용하게 되면 크게 두 가지 단점이 발생한다.

  1. 의존성이 높아진다.
  2. TDD에 걸림돌이 된다.

 

의존성이 높아진다.

의존성은 B가 변할 때, A가 변한다 라는 개념이다.
의존성이 높아진다는 것은 어떤 것을 수정하는 예시로 이해가 쉽다.

A Class(ver.1) - ABC Instance(ver.1) --> a 객체(ver.1)  
                                     --> b 객체(ver.2)

만약 A Class의 어떤 부분을 변경해야 한다면, 나머지 instance, 객체들도 변경을 해야한다는 것이다. 이를 의존성이 높다고 한다.

 

TDD 걸림돌

단위 테스트에서 서로 독립성이 필요하다.
하지만 싱글톤 패턴은 하나의 인스턴스를 가지기 때문에 독립적인 인스턴스를 만들기 어렵다. 아래 예시로 이해해 보자.

단위 테스트 실험
A -> B
B -> A
싱글톤 패턴
A Class(ver.1) - ABC Instance(ver.1) --> a 객체(ver.1)  
                                     --> b 객체(ver.2)

단위 테스트에서 순서가 바뀌어도 서로에 영향을 미치지 않고, 결과값이 일정해야 하나, 싱글톤 패턴을 사용한다면, 영향이 미칠 수가 있다.
즉, a 객체를 테스트하고, b 객체를 테스트한다면 문제가 없을 가능성이 있어도, b 객체를 테스터하고, a 객체를 테스트한다면 문제가 없을거라고 보장을 못한다.

 

장점 or 해결 or 결과

하지만 장점또한 있다.
인스턴스를 생성할 때 비용이 낮아진다. 예를들어 I/O바운드 작업을 할때, 유리하다.

I/O바운드는 네트워크, DB연결, File System 등을 칭하는 말이다. 시간이 많이 걸리는 작업이다.

비용이 낮아진다는 것은

Server => DB 사이 연결과 요청을 보낸다.

Connect => Query  
Connect => Query  
Connect => Query  
....

매번 연결과 요청을 보내면 비용이 증가할 수 밖에 없다. 하지만 싱글톤 패턴을 사용한다면,

 

 

I/O바운드 작업(Connect)을 한번만 하면된다. 즉 비용 낮출 수 있다.

 

해결

싱글톤 패턴은 의존성이 높다는 것을 위에서 말했다. 이를 해결하기 위해서는 의존성 주입 방식으로 모듈간의 결합을 느슨하게 하여 해결할 수 있다.

 

왼쪽이 의존성 주입 전 오른쪽이 의존성 주입 후
 

왼쪽이 의존성 주입 전 오른쪽이 의존성 주입 후

의존성 개입자가 간접적으로 의존성을 주입하는 방법이다. 따라서 메인에 대한 결합은 느슨해진다.

의존성 주입의 장점으로 무듈 교체가 쉬워지며, TDD에 용이하고, Migration하기에도 수월해진다. 또한 애플리케이션의 의존성 방향이 일관되고, 구조를 쉽게 추론할 수 있다.

 

싱글톤 패턴 구현 7가지

총 7가지로 패턴을 공부하고, 이를 바탕으로 싱글톤 패턴은 이런 거구나? 라고 느낌이 오면 베스트!

 

1. 단순한 메서드 호출

  • 문제점 : 원자성 결여, JAVA 멀티쓰레드

쓰레드가 두 개로 하고, Sleep을 건다. 그리고 마지막에 출력을 한다. 이런 경우 순차적으로 객체를 생성하여 a 객체 값, b 객체 값이 나올법 하지만, 아닐 수가 있다.

이를 해결하기 위해 synchronized 키워드를 사용해야 한다.

public class Singleton {
  private stgatic Singleton instace;

  private Singleton() {}

  // 문제점 발견
  public static Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
}

 

2. synchronized

  • 문제점 발견 : synchronized 키워드로 잠금을 할 수 있다. 최초 접근한 스레드가 해당 메서드 호출시 접근하지 못하도록 lock을 걸어준다. 이러한 과정에서 getInstance()를 호출할 때마다 성능저하가 생길 수 있다.

이를 해결하기 위해 정적 멤버 방식을 사용한다.

public class Singleton {
  private stgatic Singleton instace;

  private Singleton() {}

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

 

3,4. Static (정적 멤버, 정적 블록)

Static은 JVM이 클래스 로딩 때 미리 인스턴스를 이용하는 방법이다. 즉 클래스 로딩과 동시에 싱글톤 인스턴스를 만든다.

  • 문제점 : 자원 낭비
  • 싱글톤 인스턴스가 필요없는 경우* 무조건 싱글톤 인스턴스를 호출해야 하기 때문에 자원낭비가 발생

이를 해결하기 위해 정적 멤버 + Holder 방식을 사용한다.

// 정적 멤버
class Singleton {
  private static final Singleton instance = new Singleton();

  private Singleton() {}

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

// 정적 블록
class Singleton {
  private static Singleton instance = null;

  // static 블록 
  static { instance = new Singleton(); }

  private Singleton() {}

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

 

5. 정적멤버와 Lazy Holder(중첩 클래스)

보편적으로 가장 많이 사용하는 방식이다.

모듈이 필요할때만 정적 멤버로 선언하여 3, 4번의 문제점인 자원 낭비를 해결할 수 있다.

class Singleton {

  // Holder 방식
  private static class singleInstanceHolder {
    private static final Singleton INSTANCE = new Singleton();
  }
  public static Singleton getInstance() {
    return singleInstanceHolder.INSTANCE;
  }
}

@test
public class HelloWorld {
  public static void main(STring[] args) {
    Singleton a = singleton.getInstance();
    Singleton b = singleton.getInstance();
    if (a.equals(b)) {
        ...  // true
    } 
  }
}

 

6. 이중 확인 잠금(DCL)

인스턴스 생성 여부를 패턴 잠금 전에 한번, 객체 생성하기 전에 한 번 총 2번 체크하면서 인스턴스가 존재하지 않을 때만 잠금을 할 수 있다.

  • volatile : RAM 위에 / 메인 메모리 위에 / CPU 캐시 메모리라는 L3, L2, L1 캐시가 있다.
    java는 스레드가 2개 열리면 변수를 메인메모리에서 가져오는 것이 아니라 캐시메모리가 메인 메모리에서 가져온 것을 보낸다.

 

 
  • 문제점 : a스레드는 while 무한루프고, 이를 종료하기 위해서 b스레드에서 종료하를 할 수 있는 flag가 있다고 한다면, 이는 가능할까?

정답은 불가능이다. 멀티쓰레드 환경에서 변수가 공유가 되는게 아니라, flag가 공유되지 않기 때문에 계속 무한루프에 빠진다.

이를 해결하기 위해서 volatile를 변수를 사용하게 된다면 메인 메모리에서 변수값이 있으므로 공유가 되게 된다.

// 정적 멤버
class Singleton {
  private volatile Singleton instance;

  private Singleton() {}

  public static Singleton getInstance() {
    if (instance == null) {
      synchronized (Singleton.class) {
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

 

7. enum

enum는 기본적으로 스레드세이프한 점이 보장되기 때문에 이를 통해 생성할 수 있다.

public enum Singleton {
  INSTANCE;
  public void orrtCloud() {}
}

 

그렇다면 뭘 사용해?

사실 이 부분이 가장 중요하다. 위 구현 코드는 자바를 배우는 입장에서 메모리 적으로 많은 생각을 하게 해주는 것에서 이점이 있다.
자료의 의하면 5번인 Holder 방식과 7번인 enum방식을 추천한다.
이유는 Holder는 가장 많이 사용하는 방식으로 알려져 있고, 7번은 이펙티브 자바에서 조슈아가 추천하기 때문이다.

 

Referance

참고 자료
CS전공지식노트
큰돌 유튜브
준비된 개발자 블로그

반응형
profile

이카's

@Edan Cafe ☕

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!