기본 User, Main 클래스
package singletonPattern;
public class User extends Thread{
public User(String name) {
super(name);
}
public void run() {
Printer printer = Printer.getPrinter();
printer.print(Thread.currentThread().getName()
+ " using " + printer.toString() + ".");
}
}
package singletonPattern;
public class Main {
private static final int NUM = 5;
public static void main(String[] args) {
User[] user = new User[NUM];
for (int i = 0; i < NUM; i++) {
user[i] = new User((i + 1) + "Thread");
user[i].start();
}
}
}
예1)
현재 회사에는 프린터가 1개밖에 존재하지 않고 10명의 직원들이 하나의 프린터만 공유해서 사용해야한다. 즉, Printer 객체를 생성하는 new Printer는 한번만 사용할 수 있다.
해결
package singletonPattern;
public class Printer {
private static final Printer printer = new Printer();
private Printer() {
}
public static Printer getInstance() {
return printer;
}
public void print(String str) {
System.out.println(str);
}
}
/*
4Thread using singletonPattern.Printer@706feaba.
3Thread using singletonPattern.Printer@706feaba.
1Thread using singletonPattern.Printer@706feaba.
2Thread using singletonPattern.Printer@706feaba.
5Thread using singletonPattern.Printer@706feaba.
*/
결과 및 문제점
- 결과
- 코드가 간단함(가독성 좋음)
- 문제점
- 클래스가 호출될 때마다 인스턴스를 생성
- 인스턴스가 필요하지 않은 상황에서도 CPU 시간을 계속 낭비
- 예외 처리가 불가능
예2)
예1)은 클래스가 호출될 때마다 인스턴스를 생성하기 때문에 CPU 낭비를 함
- Eager initialization
따라서 Lazy initialization을 이용하여 필요할 때만 인스턴스를 생성하도록 하자
해결
package singletonPattern;
public class Printer {
private static Printer printer;
private Printer() {
}
public static Printer getInstance() {
if(printer == null)
printer = new Printer();
return printer;
}
public void print(String str) {
System.out.println(str);
}
}
/*
멀티 스레딩 환경
4Thread using singletonPattern.Printer@706feaba.
5Thread using singletonPattern.Printer@79a2def8.
3Thread using singletonPattern.Printer@79a2def8.
2Thread using singletonPattern.Printer@79a2def8.
1Thread using singletonPattern.Printer@28f19255.
*/
결과 및 문제점
- 결과
- null 체크(예외 체크)함으로서 필요할 때만 인스턴스를 생성할 수 있음
- resource 낭비, CPU 시간 낭비 overcome
- 문제점
- 멀티 스레딩 환경에서 인스턴스가 여러 개 생길 수 있음
예3)
다중 스레드에선 예2) 코드에서 문제가 발생할 수 있다.
- 스레드1이 이미 if문을 실행해서 null을 확인하고 printer = new Printer() 를 접근하려고 함
- 이때 스레드2가 if문을 실행해서 null을 확인하고 객체 생성하는 코드에 접근하여 생성함
- 스레드1도 객체를 생성함
- 문제가 발생하게 됨. Printer 클래스의 인스턴스가 2개가 될 수 있음
- 스레드1과 스레드 2는 race condition
해결
package singletonPattern;
public class Printer {
private static Printer printer;
private Printer() {
}
public synchronized static Printer getInstance() {
if(printer == null)
printer = new Printer();
return printer;
}
public void print(String str) {
System.out.println(str);
}
}
/*
4Thread using singletonPattern.Printer@d1c79d.
3Thread using singletonPattern.Printer@d1c79d.
1Thread using singletonPattern.Printer@d1c79d.
2Thread using singletonPattern.Printer@d1c79d.
5Thread using singletonPattern.Printer@d1c79d.
*/
결과 및 문제점
- 결과
- 인스턴스가 한 개만 생김
- 문제점
- getInstance 메서드가 동기화 처리 되기 때문에 멀티 스레딩이지만 단일 스레딩처럼 액세스되어 성능이 저하됨
- 모든 스레드들이 wait해야함
- getInstance 메서드가 동기화 처리 되기 때문에 멀티 스레딩이지만 단일 스레딩처럼 액세스되어 성능이 저하됨
예4)
예3)에서의 성능을 개선하기 위해 double-checked locking을 사용한다.
해결
package singletonPattern;
public class Printer {
private volatile static Printer printer;
private Printer() {
}
public static Printer getInstance() {
if(printer == null) {
synchronized (Printer.class) {
if (printer == null) {
printer = new Printer();
}
}
}
return printer;
}
public void print(String str) {
System.out.println(str);
}
}
/*
3Thread using singletonPattern.Printer@7ea81ac7.
2Thread using singletonPattern.Printer@7ea81ac7.
4Thread using singletonPattern.Printer@7ea81ac7.
1Thread using singletonPattern.Printer@7ea81ac7.
5Thread using singletonPattern.Printer@7ea81ac7.
*/
결과 및 문제점
- 결과
- getInstnace를 동기화시키지 않음
- 인스턴스를 생성하는 부분을 동기화 시켜서 overhead 극복
- 캐시 일관성 문제를 방지하기 위해 volatile 키워드 사용
- 문제점
- volatile은 java 1.4 아래에선 호환되지 않음
- 코드가 읽기 어렵다
예5)
synchroinzed 키워드도 사용하지 않고, eager initialization처럼 보이지만 lazy initalization이다.
해결
package singletonPattern;
public class Printer {
private Printer() {
}
private static class InstanceHolder{
private static final Printer instance
= new Printer();
}
public static Printer getInstance() {
return InstanceHolder.instance;
}
public void print(Stringstr) {
System.out.println(str);
}
}
이른 초기화와 holder 초기화
/*
5Thread using singletonPattern.Printer@2cce0393.
1Thread using singletonPattern.Printer@2cce0393.
4Thread using singletonPattern.Printer@2cce0393.
2Thread using singletonPattern.Printer@2cce0393.
3Thread using singletonPattern.Printer@2cce0393.
*/
결과 및 문제점
- 결과
- singleton class가 load될 때, inner class는 load되지 않는다
- 객체가 항상 생성 X
- inner class는 getInstance를 호출할 때만 load됨
- eager initialzation처럼 보이지만 lazy initialization
- synchronized 키워드 사용 X
- 해당 방법을 제일 선호한다고 한다(구현 쉽다)
- singleton class가 load될 때, inner class는 load되지 않는다
예6)
Enum을 사용하여 singleton을 구현(Effective Java Book Item3)
해결
package singletonPattern;
public enum SingletonTest {
INSTANCE("Initial class Info");
private String info;
private SingletonTest(String info) {
this.info = info;
}
public SingletonTest getInstance() {
return INSTANCE;
}
//getter, setter
}
package singletonPattern;
public class Main {
private static final int NUM = 5;
public static void main(String[] args) {
SingletonTest test1 = SingletonTest.INSTANCE.getInstance();
System.out.println("test1.getInfo() = " + test1.getInfo());
SingletonTest test2 = SingletonTest.INSTANCE.getInstance();
test2.setInfo("New enum info");
System.out.println("test1.getInfo() = " + test1.getInfo());
System.out.println("test2.getInfo() = " + test2.getInfo());
}
}
/*
test1.getInfo() = Initial class Info
test1.getInfo() = New enum info
test2.getInfo() = New enum info
*/
결과 및 문제점
- ENUM공부를 하고 다시 공부해보자
- 아래의 Enum singleton의 장점 링크 참고!!
총정리
- singleton
- 동시성 이슈
- sychronized 사용
- 속도 이슈
- double-checked locking(volatile 사용)
- 장황 & 가독성 좋지 않음 & 자바 1.4 아래에선 volatile 호환 X
- static 블록 사용(Eager initialzation) || static class(Lazy initialzation)
- (여기서부턴 다음에 공부해보자)
- singleton을 파훼하는 것들
- reflection, cloning, serialzation
싱글턴 패턴을 왜 사용할까요?
- 메모리 측면
- 객체 생성은 새로운 요청이 있을 때마다 생성되지 않고 한 번만 생성
- 데이터 공유 쉽다
- static으로 선언되있어 전역으로 사용되는 인스턴스이다. 따라서 데이터 공유가 쉽다.
- 객체 생성 횟수를 한 개로만 제한하여 소켓이나 DB 연결과 같은 리소스에 대한 접근 제어가 가능해진다.
- 다중 스레드 및 데이터베이스 응용 프로그램이 캐싱, 로깅, 스레드 풀링, 구성 설정(configuration settings) 등을 위해 singleton을 사용한다.
- 라이센스가 있고, 데이터베이스 연결이 하나만 있거나 JDBC 드라이버가 멀티스레딩을 허용하지 않는 경우 singleton 클래스가 나타나 한 번에 하나의 연결 또는 단일 스레드만 연결에 엑세스할 수 있도록 한다.
💡 3, 4번 내용이 이해하기 어렵다
일반 클래스와 싱글톤 클래스의 차이는 뭘까요?
- 일반 클래스는 애플리케이션의 라이프사이클이 끝날 때 사라짐
- 싱글톤은 애플리케이션이 완료되도 사라지지 않는다.
- 일반 클래스는 생성자를 통해 객체를 생성
- 싱글톤은 getInsatnce() 방법을 사용함
Singleton을 구현했을 때 어떤 disadvantage가 있을까요?
- 전역 상태
- Singleton은 어디에서든지 누구나 접근할 수 있으므로 아무 객체가 자유롭게 접근하여 수정하고 데이터를 공유할 수 있는 상태가 된다.
- 테스트 하기 어렵
- singleton instance는 자원을 공유하고 있기 때문에 테스트를 할 때마다 인스턴스의 상태를 초기화시켜줘야함. 그렇지 않으면 전역적으로 상태를 공유하고 있기 때문에 테스트 수행 못함
- 예를 들어 repository에 접근하고 있는 싱글톤이 있는데, 테스트하기 위해서는 해당 repository를 초기화해줘야한다. 그렇지 않으면 원래의 repo가 유출당할 수 있기 때문이다.
- 구현 클래스에 의존하게 되므로 DIP를 위반하게 된다. 또한, OCP 도 위반할 가능성이 높다
- singleton instance는 자원을 공유하고 있기 때문에 테스트를 할 때마다 인스턴스의 상태를 초기화시켜줘야함. 그렇지 않으면 전역적으로 상태를 공유하고 있기 때문에 테스트 수행 못함
Singleton의 잘못된 점(?)
SRP를 위반(두 가지의 책임을 가짐)
- 하나의 인스턴스만 만듬
- DB 액세스, 고유 리소스 관리 등 클래스의 핵심 기능
singleton 판별하는 것을 클래스 자체 책임이 아니라 system이나 context에 의존
- 인스턴스 수 관리를 클래스 자체에서 추출하여 외부에서 관리하는게 이상적
- 예로 구성요소의 생명 주기를 관리하고 인스턴스화할 특정 종속성을 선택하는 의존성 주입 컨테이너가 있다.
- 이러한 방식으로 동일한 클래스를 서로 다른 라이프사이클 관리와 함께 여러 컨텍스트에서 사용할 수 있음
Testing
- 유닛 테스트가 매우 어렵
- 인스턴스가 전역 상태이기 때문에 싱글톤에 의존하는 클래스를 완전히 분리할 수 없게 된다.
- 테스트 하려고 싱글톤도 테스트해야함. 즉, 인스턴스 초기화를 계속 해야함.
- 유닛 테스트 시 클래스의 모든 종속성이 외부(생성자 or setter)에 의해 주입받아야 쉽게 함
Dependency hiding
클래스가 singleton을 호출했을 때 생성자나 메서드를 명백할게 알 수 없다.
- constructor’s or methods’s signature isn’t visible
아래의 코드를 예로 설명함
singleton instance를 얻기 위해 getInstance를 호출하면 우리는 getInstatnce내부를 알 수 없게 되므로 어떤 종속성을 가지고 있는지 알 수 없다.
이것을 dependency hiding이라 한다.
public class Printer {
private volatile static Printer printer;
private Printer() {
}
public synchronized static Printer getInstance() {
if(printer == null) {
synchronized (Printer.class) {
if (printer == null) {
printer = new Printer();
}
}
}
return printer;
}
public void print(String str) {
System.out.println(str);
}
}
아래의 예가 visible한 것
public class Printer {
private PrinterDriver printerDriver;
public Printer(PrinterDriver newPrinterDriver) {
this.printerDriver = newPrinterDriver;
}
}
Singleton with spring
싱글톤은 하나의 애플리케이션에 객체의 인스턴스가 단 한개만 존재한다.
Spring에서는 스프링 IoC 컨테이너 당 싱글톤을 하나의 개체로 제한한다.
스프링 컨테이너가 빈을 관리하면서 DI를 이용하여
싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다 → 단점 해결
의존관계 상 클라이언트가 구체 클래스에 의존한다 → 단점 해결
클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다. → 단점 해결
💡 싱글톤 방식의 주의점
- 싱글톤 객체는 상태유지(stateful)하게 설계하면 안된다!!
- 무상태(stateless)
- 특정 클라이언트에 의존적인 필드가 있으면 안댐
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다
- 가급적 읽기만 가능해야 한다
- 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야함
REFERENCES
'Design Pattern' 카테고리의 다른 글
Decorator Pattern (0) | 2022.11.06 |
---|---|
Template Callback Pattern (0) | 2022.11.01 |
Command Pattern (0) | 2022.03.15 |
State Pattern (0) | 2022.03.12 |
Strategy Pattern (0) | 2022.03.04 |