인스턴스가 단 하나밖에 없는 객체
책임을 분리하고 유연성을 키우기 위해 클래스를 나누었지만
Config, 로그 기록용 클래스 등 인스턴스가 단 하나만 있어도 작동하는 경우는 꽤나 있습니다.
이 경우, 인스턴스를 2개 이상 생성할 경우 괜히 자원만 더 잡아먹을 뿐이죠.
싱글톤 패턴 구현 : private 생성자, getInstance, static 3총사
public class MySingleton {
private static MySingleton instance = null;
// 생성자를 private로 두어 기본 생성자를 못쓰게 함
private MySingleton() {
}
public static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
}
이전 디자인 패턴 포스팅이였던 정적 팩토리 메서드를 응용할 수 있을 것 같습니다.
https://uknowblog.tistory.com/456
MySingleton instance = new MySingleton(); // 기본 생성자가 private 이기 때문에 컴파일 에러 발생!
기본 생성자를 private로 둠으로써 외부에서 'new' 키워드로 인스턴스화 하는 것을 막아야 합니다.
그래야 클래스를 설계한 의도에 맞게 인스턴스를 얻을 수 있거든요.
MySingleton instance1 = MySingleton.getInstance();
기본 생성자를 막음으로써(private)
저희가 의도한 getInstance()로만 인스턴스를 얻을 수 있도록 만들어줍시다.
public static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
getInstance()에서는 하나뿐인 인스턴스가 저장되며,
null일 경우 새 인스턴스를 할당해주고,
instance를 return 해줍니다.
instance는 한 번 할당된 후에는 다시 new 키워드로 새롭게 인스턴스가 할당되지 않기 때문에
프로그램 실행 중 해당 클래스가 인스턴스화 되는 횟수는 단 한 번 입니다.
위와 같이 private, getInstance, static 이라는 세 키워드만 기억한다면 싱글톤 패턴을 쉽게 만들 수 있습니다.
public class Main {
public static void main(String[] args) {
MySingleton instance1 = MySingleton.getInstance();
MySingleton instance2 = MySingleton.getInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
}
}
=====================================================================
출력>>>
(패키지명).MySingleton@24d46ca6
(패키지명).MySingleton@24d46ca6
true
위와 같이, getInstance()로 받은 객체는 모두 같은 객체임을 알 수 있습니다.
멀티 스레드와 싱글톤
위와 같이 만든 싱글톤 클래스, 과연 멀티 스레드 환경에서도 모두 다 같은 객체임을 보증할 수 있을까요?
궁금할 때엔 직접 코딩해서 실행해보는게 최고죠.
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
MySingleton mySingleton = MySingleton.getInstance();
System.out.println(mySingleton);
}
});
thread.start();
}
}
}
위와 같이 10개의 스레드를 돌려 MySingleton 클래스의 인스턴스를 얻어봤습니다.
저희가 원한 대로 모두 다 같은 객체일까요?
그렇지 않습니다.
중간에 다른 해시코드를 가진 객체가 하나 끼어있네요.
이 처럼 멀티스레딩 환경에서는 위와 같은 방법은 모두 같은 객체임을 보증할 수 없다는 것을 알았습니다.
그럼, 해결방법이 있을까요?
synchronized 키워드 사용
public class MySingleton {
private static MySingleton instance;
private MySingleton() {
}
public synchronized static MySingleton getInstance() {
if (instance == null) {
instance = new MySingleton();
}
return instance;
}
}
synchronized 키워드를 사용하면 해당 메서드에 한 번에 하나의 스레드만 접근가능합니다.
꽤 깔끔하게 문제를 해결할 수 있으나, 불필요한 오버헤드를 증가시켜 속도 문제가 생길 수 있습니다.
처음부터 그냥 만들기
public class MySingleton {
private static MySingleton instance = new MySingleton();
private MySingleton() {
}
public static MySingleton getInstance() {
return instance;
}
}
instance가 null인지 체크 후 new로 할당을 시켜주는 것이 아니라,
그냥 처음부터 new로 할당을 시켜주는 방법입니다.
꽤나 깔끔하고 직관적인 방법이죠.
싱글톤 패턴의 사용
싱글톤은 단 하나의 인스턴스만을 가지기 때문에
단순 작업을 하는 유틸용 클래스, Config, 로깅 등 클래스를 처리하는데 효율적입니다.
싱글톤을 아주 잘 활용한 프레임워크가 있는데, 바로 자바/코틀린의 백엔드 프레임워크인 Spring 입니다.
Spring의 Bean은 스프링이 싱글톤으로써 관리하는 객체입니다.
매번 새롭게 인스턴스를 하는 것이 아닌
미리 인스턴스를 생성해놓고, 해딩 빈(클래스)가 필요한 곳곳에 의존성을 주입해줌으로써,
성능측면에서 이점을 얻을 수 있죠.
싱글톤의 단점
위 설명만 들으면, 복수의 인스턴스가 필요하지 않을 경우
되도록 싱글톤을 쓰는게 성능상 유리하다고 생각할 수 있을 것 같은데요.
어느 상황에서나 완벽한 디자인 패턴은 없듯, 싱글톤 패턴도 단점을 가지고 있으며
때때론 안티 패턴으로 분류되기도 합니다.
의존성이 높아짐
싱글톤은 하나의 인스턴스만 가집니다. 때문에 해당 클래스를 사용하는 다른 모든 모듈들이
하나의 인스턴스만을 활용합니다.
싱글톤 패턴을 변경하면, 해당 클래스를 참조하는 다른 모듈 역시 변경이 일어날 가능성이 있습니다.
또한, 기본 생성자가 private으로 되어있고, 정적 변수를 기반으로 하기 때문에 상속이 쉽지 않습니다.
데이터 공유
하나의 인스턴스만을 가지기에, 데이터 역시 모두 공유합니다.
공유라기보단, 단 하나밖에 없다는 말이 더 적합할 것 같네요.
멀티스레드 환경 등에서는 데이터의 무결성이 깨질 수도 있습니다.
테스트의 어려움
자원을 공유하므로, 각각의 테스트 마다 인스턴스를 초기화 or 롤백해줘야 될 수도 있습니다.
때문에 Mock 객체를 사용하기도 합니다.
마무리
싱글톤 패턴은 안티 패턴으로 분류될 때도 있는 만큼, 많은 단점을 가지고 있습니다.
하지만, 메모리 측면에서 얻을 수 있는 장점 역시 꽤나 큽니다.
싱글톤은 다소 특수한 상황에서 특정 용도로 사용하려고 만든 것인 만큼,
싱글톤을 적용했을 때 trade-off를 잘 고려하여 설계하는 것이 중요할 것 같네요.
부록. 코틀린의 object와 싱글톤 패턴
object Singleton {
fun say() {
println("Hello, Singleton!")
}
}
코틀린에서는 object를 통해 싱글톤을 매우 쉽게 구현할 수 있습니다.
object 키워드로 정의한 객체는 단일 인스턴스를 가지며,
멀티 스레드 환경에서도 안전합니다.
해당 싱글톤 객체의 호출은 아래와 같이 사용할 수 있습니다.
fun main() {
Singleton.say()
}
object로 선언된 싱글톤 클래스는 처음 접근 시 초기화되는 lazy 초기화 방식입니다.
즉, 해당 싱글톤 클래스를 참조한 곳이 없다면 프로그램 종료 시 까지 객체가 생성되지 않아
애플리케이션의 시작 시간을 줄일 수 있습니다.
꽤나 쉽고 간결하게 싱글톤을 활용할 수 있네요.
'CS 지식 > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] Adaptor(어댑터) 패턴 (1) | 2024.05.08 |
---|---|
[디자인 패턴] Factory(팩토리) 패턴 (0) | 2024.03.10 |
[디자인 패턴] Decorator (데코레이터) 패턴 (1) | 2023.09.12 |
[디자인 패턴] Observer(옵저버) 패턴 (0) | 2023.09.08 |
[디자인 패턴] Strategy(전략) 패턴 + Template Callback Pattern (0) | 2023.06.20 |