관리 메뉴

JIE0025

[Design Patten] Singleton 본문

개발/Java, Kotlin

[Design Patten] Singleton

Kangjieun11 2022. 12. 7. 09:54
728x90

 

 

 

 Java 객체지향 디자인 패턴 책을 개인적으로 공부하며 정리한 내용입니다.


 

 




✅ Singleton pattern

객체의 인스턴스가 오직 1개만 생성되는 패턴

 

 

생성자

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {
        // 생성자는 외부에서 호출못하게 private 으로 지정
    }

    public static Singleton getInstance() {
        return instance;
    }

    public void say() {
        System.out.println("hi, there");
    }
}



⏺ 싱글톤 패턴을 사용하는 이유

인스턴스를 1개만 가져가면 어떤 이점이 있을까?

 

  • 메모리 측면
    • 최초 1번의 new 연산자를 통해 고정된 메모리 영역을 사용하기 때문에 추후 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있음.
    • 생성된 인스턴스를 활용함으로써 속도 측에서도 이점이 있다.
  • 데이터 공유가 쉬움
    • 싱글톤 인스턴스는 전역으로 사용됨 (static)
    • 다른 클래스의 인스턴스들이 접근해 사용할 수 있음
    • 단 여러 클래스의 인스턴스에서 싱글턴인스턴스 데이터에 동시 접근시 동시성 문제가 발생할 수 있다.



⏺ 싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하는 코드가 많이 필요함.
  • 테스트하기 어렵다.
    • 테스트가 결정적으로 격리된 환경에서 수행하기 위해 매번 인스턴스의 상태를 초기화 해주어야한다.
    • 초기화를 안해주면, 어플리케이션 전역에서 상태를 공유하기떄문에 테스트가 온전하게 사용되지 못함
  • 클라이언트가 구체 클래스에 의존하게 됨.
    • new 키워드를 직접 사용해 클래스 내부에서 객체를 생성하고 있기 때문에 SOLID원칙 중 DIP(Dependency Inversion Principle)의존성 역전 원칙을 위반하게 됨
    • DIP 의존성 역전 원칙이란 :  상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. (둘 모두 추상화에 의존해야 한다)
    •  

 

 

→ 단독으로 사용될 경우 객체지향에 위반되는 사례가 많지만, 스프링 컨테이너 같은 프레임워크의 도움을 받으면 싱글톤 패턴의 문제를 보완하면서 장점의 혜택을 누릴 수 있음.

→ 스프링 빈은 컨테이너의 도움을 받아 싱글톤으로 관리됨.






💻  실습코드 설명

printer_6_1

  • Printer : 싱글톤으로 관리될 클래스
  • User : 싱클톤객체(프린터기)를 생성하고, 사용하는 클래스
    • print() : 사용 되는 싱글톤객체의 주소 확인
  • Main : User를 생성하고, print()메소드를 출력
//1번만 인스턴스가 생성되어야 하는 프린터 class
public class Printer {
    //static변수로 선언함으로써, class 자체에 속하고, 인스턴스를 통하지 않고도 실행할 수 있음.

    private static Printer printer = null;
    private Printer() { }


    //싱글톤의 목적이 단 하나의 객체만 생성해서 어디에서든지 참조 가능하게 만드는 것이므로, static 메서드로 만들어주어야함.
    public static Printer getPrinter(){
        if(printer == null) {
            printer = new Printer();
        }
        return printer;
    }
    public void print(String str) {
        System.out.println(str);
    }
}


public class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public void print() {
        Printer printer = Printer.getPrinter(); //싱글톤 객체 생성
        printer.print(this.name + " print  ::  using " + printer.toString());
    }
}

public class Main {
    public static void main(String[] args) {
        User[] users = new User[]{
                new User("강지은"),
                new User("심규선"),
                new User("이찬혁"),
                new User("이수현"),
                new User("자우림")
        };

        for(int i=0;i<users.length; i++) {
            users[i].print();
        }
    }
}

 

문제점?

  • 다중 스레드에서 Printer class를 이용할 경우 인스턴스가 1개 이상 생성되는 경우가 발생할 수 있음
  • 경합조건 : 메모리가 같은 동일한 자원을 2개 이상의 스레드가 이용하려고 경합하는 현상..

 

printer_6_2

  • Printer : 싱글톤으로 관리될 클래스
    • getPrinter() : 스레드를 잠시 정지시키고, printer를 생성하게 해서 여러개의 쓰레드가 동시에 접근하고, 결국 싱글톤이 아닌 여러개의 인스턴스가 나타나는것을 확인할 수 있음.
  • UserThread : 싱클톤객체(프린터기)를 생성하고, 사용하는 클래스
    • print() : 사용 되는 싱글톤객체의 주소 확인
  • Client: UserThread를 생성하고, 모든 쓰레드 start()
public class Client {
    private static final int THREAD_NUM = 5;

    public static void main(String[] args) {
        UserThread[] user = new UserThread[THREAD_NUM];

        for(int i=0;i<user.length; i++) {
            user[i] = new UserThread((i+1) + "-thread");
            user[i].start();
        }
    }
}


public class Printer {
    private static Printer printer = null;

    private Printer(){}

    public synchronized static Printer getPrinter() {
        if (printer == null) {
            try {
                Thread.sleep(1);
            }
            catch (InterruptedException e) {

            }
            printer = new Printer();
        }
        return printer;
    }
    public void print(String str) {
    	System.out.println(str);
    }

}

결국 멀티스레드 환경에선 문제가 생긴다.

 

만약 프린트 클래스가 상태를 유지해야하는 경우에는?

  • 프린터 출력 횟수가 정해져있어서, counter 변수를 사용해 상태를 유지해야하는 상황이라고 가정
public class Printer {
    private static Printer printer = null;
    private int counter = 0; //(1)상태를 유지해야하는 변수 생성

    private Printer(){}

    public static Printer getPrinter() {
        if (printer == null) {
            try {
                Thread.sleep(1);
            }
            catch (InterruptedException e) {

            }
            printer = new Printer();
        }
        return printer;
    }
    public void print(String str) {
        counter++; //(2) 상태 변경
        System.out.println(str + " 현재 " +counter+"번 사용되었습니다.");
    }
}

기대한 결과와 다르게 나오는 것 확인



✅ 다중 스레드 어플리케이션에서 발생하는 문제를 해결하는 방법

 

1️⃣  정적 변수에 인스턴스를 만들어서 바로 초기화 하는 방법

private static Printer printer = new Printer();
  • 정적 변수는 객체가 생성되기 전, 클래스가 메모리에 로딩될 때 만들어져서 초기화가 한번만 실행 됨
  • 다중 스레드에서 문제가 되었던 printer == null 조건 검사문 제거

 

2️⃣ 인스턴스를 만드는 메서드에 동기화 하는 방법

public synchronized static Printer getPrinter() { 
	//생략
}
  • 다중 스레드에서 getPrinter메서드를 여러개의 스레드가 접근하는 걸 방지함.

 

 

프린트 클래스가 상태를 유지해야하는 변수 (counter)도 동기화 해줘야한다.

  • 객체는 하나만 생성되었지만. 여러개의 스레드가 counter 변수값에 동시에 접근할 수는 있음
synchronized (this) {
	counter++; 
	System.out.println(str + " 현재 " +counter+"번 사용되었습니다.");
}

 

 


 

References

'개발 > Java, Kotlin' 카테고리의 다른 글

[Java] GC(Garbage Collection) 가비지컬렉션이란?  (0) 2023.05.05
[Design Patten] Proxy  (0) 2022.12.12
[WhiteShip][13주차] I/O  (0) 2022.11.26
[Whiteship][12주차] Annotation  (0) 2022.11.20
[WhiteShip][14주차] 제네릭  (0) 2022.11.13