자바생
Published 2022. 3. 17. 18:12
Singleton Pattern Design Pattern
728x90

기본 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해야함

예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
    • 해당 방법을 제일 선호한다고 한다(구현 쉽다)

예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

싱글턴 패턴을 왜 사용할까요?

  1. 메모리 측면
    1. 객체 생성은 새로운 요청이 있을 때마다 생성되지 않고 한 번만 생성
  2. 데이터 공유 쉽다
    1. static으로 선언되있어 전역으로 사용되는 인스턴스이다. 따라서 데이터 공유가 쉽다.
  3. 객체 생성 횟수를 한 개로만 제한하여 소켓이나 DB 연결과 같은 리소스에 대한 접근 제어가 가능해진다.
  4. 다중 스레드 및 데이터베이스 응용 프로그램이 캐싱, 로깅, 스레드 풀링, 구성 설정(configuration settings) 등을 위해 singleton을 사용한다.
    1. 라이센스가 있고, 데이터베이스 연결이 하나만 있거나 JDBC 드라이버가 멀티스레딩을 허용하지 않는 경우 singleton 클래스가 나타나 한 번에 하나의 연결 또는 단일 스레드만 연결에 엑세스할 수 있도록 한다.

💡 3, 4번 내용이 이해하기 어렵다

 

일반 클래스와 싱글톤 클래스의 차이는 뭘까요?

  • 일반 클래스는 애플리케이션의 라이프사이클이 끝날 때 사라짐
  • 싱글톤은 애플리케이션이 완료되도 사라지지 않는다.
  • 일반 클래스는 생성자를 통해 객체를 생성
  • 싱글톤은 getInsatnce() 방법을 사용함

Singleton을 구현했을 때 어떤 disadvantage가 있을까요?

  1. 전역 상태
    1. Singleton은 어디에서든지 누구나 접근할 수 있으므로 아무 객체가 자유롭게 접근하여 수정하고 데이터를 공유할 수 있는 상태가 된다.
  2. 테스트 하기 어렵
    1. singleton instance는 자원을 공유하고 있기 때문에 테스트를 할 때마다 인스턴스의 상태를 초기화시켜줘야함. 그렇지 않으면 전역적으로 상태를 공유하고 있기 때문에 테스트 수행 못함
      1. 예를 들어 repository에 접근하고 있는 싱글톤이 있는데, 테스트하기 위해서는 해당 repository를 초기화해줘야한다. 그렇지 않으면 원래의 repo가 유출당할 수 있기 때문이다.
    2. 구현 클래스에 의존하게 되므로 DIP를 위반하게 된다. 또한, OCP 도 위반할 가능성이 높다

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

싱글톤의 문제점

spring에서의 singleton

singleton 속성을 깨뜨릴 수 있는 concept 3가지 & overcome하는 방법

Enum

singleton을 사용하는 이유

Enum singleton의 장점

Dependency hiding

728x90

'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
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그