-
[Java] 싱글톤(Singleton)이란?Java 2022. 1. 27. 00:01728x90
싱글톤(Singleton)이란?
전역 변수를 사용하지 않고 애플리케이션이 시작될 때 최초 한 번만 메모리에 할당되어 객체를 하나만 생성하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴
즉, 싱글톤 패턴은 단 하나의 인스턴스를 생성해 사용하는 디자인 패턴입니다.
Single이라는 단어 자체가 한 개/단일을 의미하기 때문에 "싱글톤"이라는 단어 자체로 어느 정도 객체를 하나만 생성한다고 유추할 수도 있습니다.
싱글톤을 사용하는 이유?
- 고정된 메모리 영역을 얻으면서 한 번의 new로 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있습니다.
- 전역으로 사용되는 인스턴스이기 때문에 다른 클래스의 인스턴스들이 접근하여 사용할 수 있습니다. 즉, 데이터 공유가 쉽다는 것입니다. 하지만 여러 클래스의 인스턴스에서 싱글톤 인스턴스 데이터에 동시에 접근하게 되면 동시성 문제가 발생할 수 있으니 이점을 유의해야 합니다.
여러 개의 객체가 필요 없는 경우(싱글톤을 언제 사용해야 할까?)
- 객체를 구별할 필요 없는 경우 = 수정 가능한 멤버 변수가 없고 기능만 있는 경우(이런 객체를 stateless 한 객체라고 합니다.)
- 객체를 계속 생성/삭제하는데 많은 비용이 들어 재사용이 유리한 경우
싱글톤(Singleton) 디자인 패턴
- 외부에서 생성자에 접근 금지 -> 생성자의 접근 제한자를 private으로 설정(누군가 생성할 수 없도록)
- 내부에서는 private에 접근 가능하므로 내부에서 객체 생성(내부에서 static 하게 단 1개만 생성)
- 외부에서 private member에 접근할 수 있도록 getter 생성 -> setter는 필요 없다. (해당 객체를 가져다 사용할 수 있게)
- 객체 없이 외부에서 접근할 수 있도록 getter와 변수에 static 추가
싱글톤 디자인 패턴 사용예시
싱글톤 디자인 패턴 적용 x
public class Singleton { void someMethod() { System.out.println("do something"); } } public class SingletonTest { public static void main(String[] args) { Singleton st1 = new Singleton(); Singleton st2 = new Singleton(); System.out.println(st1==st2); //출력 = false } }
main함수에서 객체를 계속 생성하게 되면 새로운 객체가 계속 생성되게 됩니다.
st1과 st2의 주소 값을 비교해보면 false(다른 값)인 것을 알 수 있습니다.
싱글톤 디자인 패턴은 객체 생성을 하나만 해야 하므로 이는 싱글톤 디자인 패턴이 아닙니다.
싱글톤 디자인 패턴 적용 o
public class Singleton { private static Singleton st = new Singleton(); private Singleton(){} static public Singleton getSingleton() { return st; } void someMethod() { System.out.println("do something"); } } public class SingletonTest { public static void main(String[] args) { Singleton st1 = Singleton.getSingleton(); Singleton st2 = Singleton.getSingleton(); System.out.println(st1==st2); //출력 = true } }
싱글톤 디자인 패턴을 적용한 코드입니다.
외부에서 생성자에 접근 금지시키기 위해 생성자의 접근 제한자를 private으로 설정합니다.
private Singleton(){}
private에 접근 가능해야 하므로 내부에서 객체 생성을 합니다.
이때 private static Singleton st = new Singleton()으로 static을 활용하여 미리 객체를 하나 생성합니다.
외부에서 private member에 접근할 수 있도록 getter 생성 -> setter는 필요 없다.
getSinlgeton() 메서드를 통해 Singleton 객체를 반환합니다.
이때 외부에서 사용해야 하므로 public 접근 제한자를 사용합니다.
또한 static 변수를 반환해야 하므로 getSingleton() 메서드 또한 static으로 선언되어야 합니다.
이후에 main에서 객체를 생성하기 위해서는 (클래스 이름. 메서드)를 사용합니다.
Singleton.getSingleton()을 통해 이미 메모리에 올라가 있는 st 객체가 반환됩니다.
여러 번 생성해도 계속 st객체가 반환되며 st1==st2의 주소 값이 같으므로 true가 출력됩니다.
싱글톤은 구현하는 다양한 방법
싱글톤을 구현하는 방법 중에는 위의 방법처럼 구현한다면 가장 간단하게 구현이 가능합니다.
하지만 만약 싱글톤 클래스를 만약에 사용하지 않더라도 인스턴스를 생성하기 때문에 자원의 낭비가 발생할 수 있습니다.
Lazy Initialization
public class Singleton { private static Singleton instance; private Singleton(){} public static Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; } }
이 방법을 사용한다면 위의 방법이 가졌던 문제점인 '사용하지 않았을 경우 인스턴스가 낭비'되는 점을 해결할 순 있습니다.
하지만 또 다른 문제가 발생하게 됩니다.
바로 multi-thread 환경에서의 동시성 문제입니다.
만약 인스턴스가 생성되지 않은 시점에서 여러 스레드가 동시에 getInstance()를 호출한다면 예상치 못하게 많은 인스턴스가 생성될 수 있습니다.
즉, 싱글톤의 특징인 1개의 인스턴스가 생성됨이 지켜지지 않을 수도 있습니다.
이것을 해결하기 위해서는 getInstance() 메서드에 synchronized를 걸어두거나 싱글턴 클래스에 synchronized를 걸어서 동시성 문제를 해결합니다.
Lazy Initialization, LazyHolder
public class Singleton { private Singleton() {} private static class InnerInstanceClass { // 클래스 로딩 시점에서 생성 private static final Singleton uniqueInstance = new Singleton(); } public static Singleton getInstance() { return InnerInstanceClass.uniqueInstance; } }
Bill Pugh가 고안한 방식으로 inner static helper class를 사용하는 방식으로 volatile이나 synchronized 키워드 없이도 동시성 문제를 해결하기 때문에 성능이 뛰어나며 현재 가장 널리 쓰이는 싱글톤 구현 방법입니다.
InnerInstanceClass는 싱글턴 클래스가 Load 될 때에도 Load 되지 않다가 getInstance()가 호출되었을 때 비로소 JVM 메모리에 로드되고, 인스턴스를 생성하게 됩니다.
여기서 저는 static 키워드를 사용하면 "JVM 메모리에 바로 로드되는 게 아녔던가?"라는 의문점을 갖게 되었고 더 조사를 하게 되었습니다.
https://junuuu.tistory.com/250
사실 위의 방법들은 모두 Java의 리플렉션을 통해서 싱글톤을 파괴할 수 있습니다.
또한 직렬화에 사용될 때 역직렬화를 한다면 같은 타입의 인스턴스가 여러 개 생길 수 있습니다.
직렬화/역직렬화에서 발생하는 문제를 막기 위해서 선언부에 transient를 추가하고 readResolve 메서드를 다음과 같이 구현해야 합니다.
private Object readResolve(){ return INSTANCE; }
역직렬화 할 때 Java에서 내부적으로 쓰는 메서드가 readResolve()라는 메서드입니다.
이때 싱글턴 인스턴스를 반환하게 되면 매번 역직렬화를 할 때마다 싱글턴 객체를 반환하기 때문에 안전해집니다.
Enum 등장
enum SingletonEnum { INSTANCE; int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } } public class SingletonEnumTest { public static void main(String[] args) { SingletonEnum singleton = SingletonEnum.INSTANCE; System.out.println(singleton.getValue()); singleton.setValue(2); System.out.println(singleton.getValue()); } }
Enum 인스턴스의 생성은 기본적으로 Thread-safe 합니다.
아주 복잡한 직렬화 상황이나, 리플렉션 공격에도 제2의 인스턴스가 생성되는 것을 막아준다는 것입니다.
Enum이 메서드와 필드를 가질 수 있음을 사용하는 방법입니다.
하지만 유일한 단점으로 Enum 말고 다른 상위 클래스를 사용할 수 없습니다. (상속 불가능)
싱글톤 패턴의 문제점
- 단일 객체를 보장하기 위해 private, static , getInstance()등의 코드를 작성해야 하기 때문에 구현하는 코드 자체가 많이 필요합니다. (구현해야 하는 코드가 많다)
- 싱글톤 인스턴스는 자원을 공유하고 있기 때문에 테스트를 격리된 환경에서 수행하려면 매번 인스턴스의 상태를 초기화시켜주어야 합니다. 그렇지 않으면 애플리케이션 전역에서 상태를 공유하기 때문에 테스트가 온전하게 수행되지 못합니다. (테스트하기 어렵다)
- 의존 관계상 클라이언트가 구체 클래스에 의존하게 됩니다. new 키워드를 직접 사용하여 클래스 안에서 객체를 생성하고 있기 때문에 SOLID원칙 중 DIP를 위반하게 되고 OCP 원칙 또한 위반할 가능성이 높습니다. (SOLID 원칙 위반)
- 싱글톤은 자신만이 객체를 생성할 수 있도록 private 생성자를 갖고 있습니다. 상속을 통해 다형성을 적용하기 위해서는 다른 기본 생성자가 필요하므로 상속이 불가능하며 객체지향의 장점을 적용할 수 없습니다. (상속 불가능)
결론
싱글톤 패턴은 한 개의 인스턴스 생성을 보증하기 때문에 얻는 장점도 있지만 위와 같은 단점도 존재합니다.
실제로 싱글톤 패턴은 안티 패턴으로 불릴 만큼 단독으로 사용한다면 객체 지향에 위반되는 사례가 많습니다.
스프링 컨테이너 같은 프레임워크의 도움을 받으면 싱글톤 패턴의 문제점들을 보완하면서 장점의 혜택을 누릴 수 있습니다.
하지만 프레임워크 도움 없이 싱글톤 패턴을 적용하고 싶다면 싱글톤 패턴의 장단점의 trade-off를 잘 고려하여 사용해야 합니다.
출처
https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/
https://mangkyu.tistory.com/153
'Java' 카테고리의 다른 글
[Java] 스트림의 생성 (0) 2022.01.31 [Java] 스트림(Stream)이란? + 특징, 등장배경 (0) 2022.01.29 [Java] 메서드 참조란? (0) 2022.01.25 [Java] Math.random() vs java.util.Random 효율성 차이점 비교 (0) 2022.01.24 [Java] java.util.function 패키지란? (0) 2022.01.22