공부해봅시당
[디자인 패턴] 생성 패턴 - 싱글톤 패턴 본문
싱글톤이란?
클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근(엑세스) 지점을 제공하는 생성 디자인 패턴
문제
싱글톤 패턴은 한 번에 두 가지의 문제를 동시에 해결함으로써 단일 책임 원칙을 위반함
단일 책임 원칙(Single Responsibility Principle)
객체는 단 하나의 책임만 가져야 한다는 원칙
여기서 '책임' 이라는 의미는 하나의 '기능 담당'이라는 의미
즉, 하나의 클래스는 하나의 기능 담당하여 하나의 책임을 수행하는데 집중되어야 있어야 한다는 의미
1. 클래스에 인스턴스가 하나만 있도록 함
- 사람들이 클래스에 있는 인스턴스 수를 제어하려는 가장 일반적인 이유
- 공유 리소스(ex. 데이터베이스 또는 파일) 일부에 대한 접근을 제어하기 위함
- 예를 들어 객체를 이미 생성했지만, 잠시 후 새 객체를 생성하기로 했다고 가정
- 그러면 새 객체를 생성하는 대신 이미 만든 객체를 받게 됨
- 생성자 호출은 특성상 반드시 새 객체를 반환해야 하므로 위 행동은 일반 생성자로 구현할 수 없음
2. 해당 인스턴스에 대한 전역 접근 지점을 제공함
- 필수 객체들을 저장하기 위해 전역 변수들을 정의했다고 가정
- 이 변수들을 사용하면 편리할 수는 있음
- 하지만 잠재적으로 모든 코드가 해당 변수의 내용을 덮어쓸 수도 있고, 그로 인해 앱에 오류가 발생해 충돌할 수 있으므로 안전한 방법은 아님
- 전역 변수와 마찬가지로 싱글톤 패턴을 사용하면 프로그램의 모든 곳에서부터 일부 객체에 접근 가능
- 하지만 이 패턴은 다른 코드가 해당 인스턴스를 덮어쓰지 못하도록 보호하기도 함
- 첫 번째 문제를 해결하는 코드가 프로그램 전체에 흩어져 있을 수 있음
- 특히, 코드의 나머지 부분이 이미 첫 번째 문제를 해결하는 코드에 의존하고 있다면, 이 코드를 한 클래스 내에 두는 것이 훨씬 좋음
최근에는 싱글톤 패턴이 워낙 대중화되어 있어 위 문제 중 한 가지만 해결하더라도 그것을 싱글톤이라고 부를 수 있음
해결책
싱글톤의 모든 구현은 공통적으로 다음의 두 단계를 가짐
- 다른 객체들이 싱글톤 클래스와 함께 new 연산자를 사용하지 못하도록 디폴트 생성자를 private 설정
- 생성자 역할을 하는 static 생성 메소드 만들기
- 내부적으로 이 메소드는 객체를 만들기 위해 private 생성자 호출 후 객체를 static 필드에 저장함
- 이 메소드에 대한 그 다음 호출들은 모두 캐쉬된 객체를 반환함
코드가 싱글톤 클래스에 접근할 수 있는 경우, 이 코드는 싱글톤의 static 메소드 호출가능
해당 메소드가 호출될 때마다 항상 같은 객체 호출될 수 있도록 함
실제상황 적용
- 싱글톤 패턴의 예시를 '정부'로 진행함
- 국가는 하나의 공식 정부만 가질 수 있음
- 'x의 정부'라는 명칭은 정부를 구성하는 개인들의 신원과 관계없이 정부 책임자들의 그룹을 식별하는 글로벌 접근 지점
구조
- 싱글톤 클래스는 정적 메소드 getInstance 선언
- 이 메소드는 자체 클래스의 같은 인스턴스 반환
- 싱글톤의 생성자는 항상 클라이언트 코드에서부터 숨겨져야 함
- getInstance 메소드를 호출하는 것이 Singleton 객체를 가져올 수 있는 유일한 방법이어야 함
의사코드
// 데이터베이스 클래스는 클라이언트들이 프로그램 전체에서 데이터베이스 연결의 같은
// 인스턴스에 접근할 수 있도록 해주는 `getInstance`(인스턴스 가져오기) 메서드를
// 정의합니다.
class Database is
// 싱글턴 인스턴스를 저장하기 위한 필드는 정적으로 선언되어야 합니다.
private static field instance: Database
// 싱글턴의 생성자는 `new` 연산자를 사용한 직접 생성 호출들을 방지하기 위해
// 항상 비공개여야 합니다.
private constructor Database() is
// 데이터베이스 서버에 대한 실제 연결과 같은 일부 초기화 코드.
// 싱글턴 인스턴스로의 접근을 제어하는 정적 메서드.
public static method getInstance() is
if (Database.instance == null) then
acquireThreadLock() and then
// 이 스레드가 잠금 해제를 기다리는 동안 인스턴스가 다른
// 스레드에 의해 초기화되지 않았는지 확인하세요.
if (Database.instance == null) then
Database.instance = new Database()
return Database.instance
// 마지막으로 모든 싱글턴은 해당 로직의 인스턴스에서 실행할 수 있는 비즈니스
// 로직을 정의해야 합니다.
public method query(sql) is
// 예를 들어 앱의 모든 데이터베이스 쿼리들은 이 메서드를 거칩니다. 따라서
// 여기에 스로틀링 또는 캐싱 논리를 배치할 수 있습니다.
// …
class Application is
method main() is
Database foo = Database.getInstance()
foo.query("SELECT ...")
// …
Database bar = Database.getInstance()
bar.query("SELECT ...")
// 변수 `bar`는 변수 `foo`와 같은 객체를 포함할 것입니다.
적용
- 싱글톤 패턴은 프로그램 클래스의 모든 클라이언트가 사용할 수 있는 단일 인스턴스만 있어야 할 때 사용하는 것 추천
- 예: 프로그램의 다른 부분들에서 공유되는 단일 데이터베이스 객체 등
- 특별 생성 메소드를 제외하고는 클래스의 객체들을 생성할 수 있는 모든 다른 수단들을 비활성화함
- 이 메소드는 새 객체를 생성하거나 객체가 이미 생성되었으면 기존 객체를 반환함
- 싱글톤 패턴은 전역 변수들을 더 엄격하게 제어해야 할 때 사용 추천
- 전역 변수들과 달리 싱글톤 패턴은 클래스의 인스턴스가 하나만 있도록 보장
- 캐시된 인스턴스는 싱글톤 클래스 자체를 제외하고는 그 어떤 것과도 대체 불가
- 참고로 이 제한은 언제든 조정할 수 있고, 원하는 수만큼의 싱글턴 인스턴스 생성을 허용할 수 있음
- 그러기 위해서 변경해야 하는 코드의 유일한 부분은 getInstance 메소드 뿐임
구현 방법
- 싱글턴 인스턴스의 저장을 위해 클래스에 private static 필드 추가
- 싱글턴 인스턴스를 가져오기 위한 공개된 public 생성 메소드 선언
- static 메소드 내에서 '지연된 초기화' 구현
- 그러면 첫 번째 호출에서 새 객체를 만든 후 그 객체를 정적 필드에 넣을 것
- 이 메소드는 모든 후속 호출들에서 항상 해당 인스턴스를 반환해야 함
- 클래스의 생성자를 private으로 만들어야 함. 그러면 클래스의 static 메소드는 여전히 생성자를 호출할 수 있지만 다른 객체들은 호출할 수 없을 것
- 클라이언트 코드를 살펴보며 싱글톤 생성자에 대한 모든 직접 호출들을 싱글턴의 정적 생성 메서드에 대한 호출로 변경
장단점
장점
- 클래스가 하나의 인스턴트만 갖는다는 것을 확신할 수 있음
- 이 인스턴스에 대한 전역 접근 지점을 얻음
- 싱글턴 객체는 처음 요청될 때만 초기화됨
단점
- 단일 책임 원칙을 위반. 이 패턴은 한 번에 두 가지의 문제를 동시에 해결
- 잘못된 디자인(프로그램의 컴포넌트들이 서로에 대해 너무 많이 알고 있는 경우 등)을 알아차리기 힘들게 함
- 다중 스레드 환경에서 여러 스레드가 싱글턴 객체를 여러 번 생성하지 않도록 처리 필요
- 싱글턴의 클라이언트 코드를 유닛 테스트하기 어려울 수 있음
- 많은 테스트 프레임워크들이 모의 객체들을 생성할 때 상속에 의존하기 때문
- 싱글턴 클래스의 생성자는 비공개이고 대부분 언어에서 정적 메서드를 오버라이딩하는 것이 불가능하므로 싱글턴의 한계를 극복할 수 있는 창의적인 방법을 생각함
- 아니면 그냥 테스트를 작성하지 말거나 싱글턴 패턴을 사용하지 않아야 함
다른 패턴들과의 관계
- 대부분의 경우 하나의 퍼사드 객체만 있어도 충분하므로 퍼사드 패턴의 클래스는 종종 싱글턴으로 변환될 수 있음
- 만약 객체들의 공유된 상태들을 단 하나의 플라이웨이트 객체로 줄일 수 있다면 플라이웨이트는 싱글턴과 유사해질 수 있음. 그러나 이 패턴들에는 두 가지 근본적인 차이점이 있음.
- 싱글턴
- 인스턴스가 하나만 있어야 함
- 변할 수 있음 (mutable)
- 플라이웨이트 클래스
- 여러 고유한 상태를 가진 여러 인스턴스를 포함할 수 있음
- 변할 수 없음 (immutable)
- 싱글턴
- 추상 팩토리들, 빌더들 및 프로토타입들은 모두 싱글턴으로 구현할 수 있음
코드 예시 - Java
Java 코드에서의 싱글톤 패턴은 감소 추세
그럼에도 자바 코어 라이브러리에는 많은 싱글톤 사용 사례 존재
기본 싱글톤(단일 스레드)
Singletion.java: 싱글톤
package refactoring_guru.singleton.example.non_thread_safe;
public final class Singleton {
private static Singleton instance;
public String value;
private Singleton(String value) {
// The following code emulates slow initialization.
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
this.value = value;
}
public static Singleton getInstance(String value) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}
DemoSingleThread.java: 클라이언트 코드
package refactoring_guru.singleton.example.non_thread_safe;
public class DemoSingleThread {
public static void main(String[] args) {
System.out.println("If you see the same value, then singleton was reused (yay!)" + "\n" +
"If you see different values, then 2 singletons were created (booo!!)" + "\n\n" +
"RESULT:" + "\n");
Singleton singleton = Singleton.getInstance("FOO");
Singleton anotherSingleton = Singleton.getInstance("BAR");
System.out.println(singleton.value);
System.out.println(anotherSingleton.value);
}
}
OutputDemoSingleThread.txt: 실행 결과
If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)
RESULT:
FOO
FOO
기본 싱글톤(멀티 스레드)
같은 클래스는 다중 스레드 환경에서 잘못 작동함.
여러 스레드가 생성 메소드를 동시에 호출할 수 있으며, 싱글톤 클래스의 여러 인스턴스를 가져올 수 있기 때문
Singleton.java: 싱글톤
package refactoring_guru.singleton.example.non_thread_safe;
public final class Singleton {
private static Singleton instance;
public String value;
private Singleton(String value) {
// The following code emulates slow initialization.
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
this.value = value;
}
public static Singleton getInstance(String value) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}
DemoMultiThread.java: 클라이언트 코드
package refactoring_guru.singleton.example.non_thread_safe;
public class DemoMultiThread {
public static void main(String[] args) {
System.out.println("If you see the same value, then singleton was reused (yay!)" + "\n" +
"If you see different values, then 2 singletons were created (booo!!)" + "\n\n" +
"RESULT:" + "\n");
Thread threadFoo = new Thread(new ThreadFoo());
Thread threadBar = new Thread(new ThreadBar());
threadFoo.start();
threadBar.start();
}
static class ThreadFoo implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance("FOO");
System.out.println(singleton.value);
}
}
static class ThreadBar implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance("BAR");
System.out.println(singleton.value);
}
}
}
OutputDemoMultiThread.txt: 실행결과
If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)
RESULT:
FOO
BAR
지연 로딩이 있는 스레드로부터 안전한 싱글톤
위 문제를 해결하려면 싱글톤 객체를 처음 생성하는 동안 스레드들을 동기화해야 함
Singletion.java: 싱글톤
package refactoring_guru.singleton.example.thread_safe;
public final class Singleton {
// The field must be declared volatile so that double check lock would work
// correctly.
private static volatile Singleton instance;
public String value;
private Singleton(String value) {
this.value = value;
}
public static Singleton getInstance(String value) {
// The approach taken here is called double-checked locking (DCL). It
// exists to prevent race condition between multiple threads that may
// attempt to get singleton instance at the same time, creating separate
// instances as a result.
//
// It may seem that having the `result` variable here is completely
// pointless. There is, however, a very important caveat when
// implementing double-checked locking in Java, which is solved by
// introducing this local variable.
//
// You can read more info DCL issues in Java here:
// https://refactoring.guru/java-dcl-issue
Singleton result = instance;
if (result != null) {
return result;
}
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton(value);
}
return instance;
}
}
}
DemoMultiThread.java: 클라이언트 코드
package refactoring_guru.singleton.example.thread_safe;
public class DemoMultiThread {
public static void main(String[] args) {
System.out.println("If you see the same value, then singleton was reused (yay!)" + "\n" +
"If you see different values, then 2 singletons were created (booo!!)" + "\n\n" +
"RESULT:" + "\n");
Thread threadFoo = new Thread(new ThreadFoo());
Thread threadBar = new Thread(new ThreadBar());
threadFoo.start();
threadBar.start();
}
static class ThreadFoo implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance("FOO");
System.out.println(singleton.value);
}
}
static class ThreadBar implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance("BAR");
System.out.println(singleton.value);
}
}
}
OutputDemoMultiThread.txt: 실행결과
If you see the same value, then singleton was reused (yay!)
If you see different values, then 2 singletons were created (booo!!)
RESULT:
BAR
BAR
출처
https://refactoring.guru/ko/design-patterns/singleton
싱글턴 패턴
/ 디자인 패턴들 / 생성 패턴 싱글턴 패턴 다음 이름으로도 불립니다: Singleton 의도 싱글턴은 클래스에 인스턴스가 하나만 있도록 하면서 이 인스턴스에 대한 전역 접근(액세스) 지점을 제공하
refactoring.guru
https://refactoring.guru/ko/design-patterns/singleton/java/example
자바로 작성된 싱글턴 / 디자인 패턴들
사용 사례들: 많은 개발자는 싱클턴을 안티패턴으로 간주합니다. 그래서 자바 코드에서의 사용이 감소하고 있습니다. 그럼에도 불구하고 자바 코어 라이브러리에는 많은 싱글턴 사용사례들이
refactoring.guru
'STUDY > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] MVC와 MVP 패턴 비교 (0) | 2023.06.09 |
---|---|
[디자인 패턴] MVC 패턴 (0) | 2023.06.09 |
[디자인패턴] 행동 패턴 - 전략 패턴 (0) | 2023.06.05 |
[디자인 패턴] 생성 패턴 - 팩토리 메소드 패턴 (0) | 2023.06.05 |
[디자인패턴] 디자인 패턴이란? (0) | 2023.06.05 |