카테고리 없음

싱글톤 방식이란?(volatile 그리고 Synchronized)

kingmusung 2024. 2. 21. 03:02

https://kingmusung.tistory.com/39

 

접근지정자와 Static 그리고 캡슐화에 대해

싱글톤 방식에 대해 공부를 하다가 문득 접근지정자, 그리고 static의 개념에 대해 기억이 희미해져 다시 정리를 해보려고 합니다! 순서는 접근지정자, static 의 정의 및 사용 예시들을 나열 후 캡

kingmusung.tistory.com

싱글톤

 

싱글톤은 디자인 패턴중 하나이고. 어떤 클레스가 최대 하나의 인스턴스만 가지도록 보장하는 방식 입니다.

 

해당 클래스의 인스턴스를 여러 곳에서 생성하지 않고, 오직 한 곳에서만 생성하고 이를 전역적으로 접근할 수 있도록 합니다.

 

 

생성자는 private으로 선언:

외부에서 인스턴스를 직접 생성하지 못하도록 생성자를 private으로 선언합니다. 

 

접근지정자를 공부하면서 아래와 같은 예시가 있었다.

 

package mypackage;

public class Parent {
    protected int protectedVar = 10;
    int defaultVar = 20;
    private int privateVar = 30;
    

     protected int getDefaultVar(){
        return this.defaultVar;
    }

    protected int getProtectedVar(){
        return this.protectedVar;
    }

    protected int getPrivateVar(){
         return this.privateVar;
    }
}

 

 

package mypackage;

    public class Child extends Parent {
        public void accessParentVars() {
              Parent parent = new Parent();
              Parent parent2 = new Parent();
            System.out.println(parent.getDefaultVar());
            System.out.println(parent2.getDefaultVar());


        }
        }
Parent parent = new Parent();
Parent parent2 = new Parent();

 

 

위와같은 무분별한 객체 생성을 막기위해 생성자를 private형태로 만들겠다.

 

package mypackage;

public class Parent {
    protected int protectedVar = 10;
    int defaultVar = 20;
    private int privateVar = 30;
    
    private Parent(){}


     protected int getDefaultVar(){
        return this.defaultVar;
    }

    protected int getProtectedVar(){
        return this.protectedVar;
    }

    protected int getPrivateVar(){
         return this.privateVar;
    }
}

 

private Parent(){}

 

생성자를 private 로 만들면서 기존에 객체를 생성하던 코드들에 모두 에러가 생겼다.

package mypackage;

    public class Child extends Parent {
        public void accessParentVars() {
              Parent parent = new Parent();
              Parent parent2 = new Parent(); // 생성자 생성 X
              }

 


정적(static) 메서드를 통한 인스턴스 반환:

싱글톤 클래스 내부에서 유일한 인스턴스에 접근할 수 있는 정적(static) 메서드를 제공합니다. 이 메서드를 통해 인스턴스를 반환하며, 이 인스턴스가 없는 경우에는 새로운 인스턴스를 생성합니다. 

 

package mypackage;

public class Parent {
    protected int protectedVar = 10;
    int defaultVar = 20;
    private int privateVar = 30;

    private static Parent instance = new Parent();

    public static Parent getInstance() {
        return instance;
    }
    private Parent(){}
}

 

private static Parent instance = new Parent();

public static Parent getInstance() {
    return instance;
}

 

이렇게 static으로 선언을 해버리면 자바가 실행될때 instance를 미리 만들놓는다.

필요에 따라 getInstance()메서드를 사용해서 미리 만들어진 instance를 받아서 사용하면 된다.

 

package mypackage;

    public class Child extends Parent{

    public static void main(String[] args) {
    Parent parent = Parent.getInstance();
        System.out.println(parent.protectedVar);
        }

}

 

이런식으로 !

 

하지만 여기서 문제! 부모의 생성자가 private로 선언이 되어 있을 시 상속을 받을 수 있을까요 없을까요?

 

-> 정답은 상속을 못받는다 입니다.

 

package mypackage;

    public class Child{

    public static void main(String[] args) {
    Parent parent = Parent.getInstance();
        System.out.println(parent.protectedVar);
        }

}

 

이렇게 getInstance() 메서드로 객체를 주입받은 후 사용을 하면 동작이 잘 됩니다.


Lazy Initialization (지연 초기화): 인스턴스가 필요한 시점에 생성되도록 하여 자원을 절약할 수 있습니다. 즉, 처음으로 해당 인스턴스가 요청될 때까지는 인스턴스를 생성하지 않습니다. 

Instance Holder

public class Parent {
    private Parent() {}

    private static class ParentHolder {
        private static final Parent INSTANCE = new Parent();
    }

    public static Parent getInstance() {
        return ParentHolder.INSTANCE;
    }
}

 

private static 내부 클래스를 정의하여 내부 클래스의 정적 필드로 싱글톤 객체를 생성하고, 이 객체에 대한 참조를 반환하는 방식입니다.

 

 

DCL(Double Checking Locking) 을 사용한 지연 초기화

 

public class Parent {
    protected int protectedVar = 10;
    int defaultVar = 20;
    private int privateVar = 30;

    private static volatile Parent instance;

    private Parent() {}

    public static Parent getInstance() {
        if (instance == null) {
            synchronized (Parent.class) {
                if (instance == null) {
                    instance = new Parent();
                }
            }
        }
        return instance;
    }
    }

 

private static Parent instance;

public static Parent getInstance() {
    if (instance == null) {
        synchronized (Parent.class) {
            if (instance == null) {
                instance = new Parent();
            }
        }
    }
    return instance;
}

 

조건문을 이용 후 DCL 방식을 적용을 하면 최초로 객체에 대해 생성요청이 들어오면 객체를 만든 후 반환을 해주고 기존에 객체가 있다면 기존에 있는 객체를 반환을 해준다.

 

여기서 또 바뀐점을 보자면 volatile, synchronized 라는 익숙하지 않은 용어들이 있을것이다.

(궁금해서 찾아보고 찾아보니 글이 삼천포로 빠지는거 같다 ..)


잠깐 삼천포로 빠집니다

 

Volatile

 

해당 변수의 값을 메인 메모리에 즉시 반영하고, CPU 캐시를 통해 변수를 읽거나 쓸 때 캐시된 값을 사용하지 않고 항상 메인 메모리의 값을 참조하도록 보장합니다. 이를 통해 변수의 가시성과 일관성을 보장하며, 다중 스레드 환경에서 변수의 동기화를 지원합니다.

 

일반적으로 자바 프로그램은 메모리에 데이터를 저장할 때 메인 메모리(RAM)와 CPU의 캐시 메모리(Cache)를 사용합니다. 

메인 메모리(Memory): 메인 메모리는 프로그램이 실행되는 동안 사용되는 모든 데이터를 저장하는 곳입니다. 메인 메모리는 프로세서(CPU)가 직접 액세스할 수 있으며, 모든 스레드에 공유되는 중앙 저장소 역할을 합니다. 

 

CPU의 캐시 메모리(Cache Memory): 캐시 메모리는 CPU와 메인 메모리 간의 속도 차이를 줄이기 위해 사용됩니다. CPU는 프로그램에서 사용하는 데이터를 캐시 메모리에 복사하여 더 빠른 속도로 액세스할 수 있습니다. 캐시 메모리에 저장된 데이터는 CPU의 코어(core)마다 별도로 유지되며, 코어 간에는 데이터를 공유할 수 없습니다. 일반적으로 자바에서 변수는 캐시 메모리에 저장되며, CPU는 변수에 대한 읽기 및 쓰기 작업을 캐시 메모리에서 수행합니다. 이 때문에 하나의 스레드에서 변수를 수정하면 수정된 값은 캐시 메모리에 반영되지만, 다른 스레드에서는 이 변경 사항을 즉시 알 수 없습니다. 대신에, 변경된 값이 메인 메모리에 반영된 후에 다른 스레드에게 노출됩니다. 하지만 volatile 키워드가 사용된 변수는 메인 메모리에 직접 저장되며, CPU 캐시를 거치지 않습니다. 따라서 volatile 변수의 변경 사항은 즉시 메인 메모리에 반영되어 다른 스레드에게 즉시 알려지며, 메인 메모리에서 변수 값을 읽거나 쓸 때 CPU 캐시의 영향을 받지 않습니다. 이것이 volatile 키워드가 메인 메모리에 즉시 반영된다는 의미입니다.

 

이러한 이유로 volatile를 사용을 하는데, 여러 쓰레드(객체 생성요청)가 동시에 들어오면 동시성문제 혹은 참조를 잘못해서 아래와 같은 상황이 발생 할 수도 있습니다.

 

스레드 A가 getInstance() 메서드를 호출하여 instance가 null임을 확인합니다. 

스레드 B도 동일하게 getInstance() 메서드를 호출하여 instance가 null임을 확인합니다.

 스레드 A는 synchronized 블록으로 진입하여 instance를 생성하고 초기화합니다. 

스레드 A가 instance를 반환하기 전에 스레드 B가 synchronized 블록으로 진입합니다. 스레드 B도 instance가 null임을 확인하고 instance를 생성하고 초기화합니다. 

스레드 B가 instance를 반환하고 synchronized 블록을 빠져나갑니다.

 스레드 A가 synchronized 블록을 빠져나가고 이미 생성된 instance를 반환합니다. 이렇게 되면 두 개의 서로 다른 객체가 생성되어 반환될 수 있습니다

 

Synchronized

synchronized 키워드는 자바에서 다중 스레드 환경에서 동기화를 제공하는 데 사용됩니다. synchronized 키워드를 사용하면 특정 블록 또는 메서드에 대해 한 번에 하나의 스레드만 접근할 수 있도록 제어할 수 있습니다.

 

synchronized를 사용하지 않으면 다중 스레드 환경에서 안전하지 않을 수 있습니다. 예를 들어 다음과 같은 상황이 발생할 수 있습니다:

 

스레드 A가 getInstance() 메서드를 호출하여 instance가 null임을 확인합니다. 

스레드 B도 동일하게 getInstance() 메서드를 호출하여 instance가 null임을 확인합니다.

스레드 A와 스레드 B 모두 instance가 null임을 확인하고 if (instance == null) 문에 진입합니다.

스레드 A와 스레드 B가 동시에 synchronized 블록에 진입하여 instance를 생성하고 초기화합니다.

스레드 A가 instance를 생성하고 반환하기 전에 스레드 B가 이미 생성된 instance를 덮어쓰는 작업을 수행합니다.

스레드 A가 instance를 반환하고 synchronized 블록을 빠져나갑니다.

 

이러한 상황에서는 instance 변수가 덮어쓰여서 다른 스레드에게 반환되는 것을 확인할 수 있습니다. 따라서 두 개의 서로 다른 객체가 생성되어 반환될 수 있습니다.

 

멀티쓰레드 환경에서 동기처리를 할때 각각의 쓰임세를 파악하고 적절하게 사용하는게 좋을거 같다. 아직 경험이 많이 없어 이론으로 나마 정리를 해본다 ㅠㅠ

 


싱글톤 패턴 문제점

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 강의 - 인프런

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢

www.inflearn.com

 

예시는 단순한 자바 코드로 들었지만.

 

싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.

의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반한다.

클라이언트가 구체 클래스에 의존한다 -> OCP 원칙을 위반할 가능성이 높다.

내부 속성을 변경하거나 초기화 하기 어렵다.

private 생성자로 자식 클래스를 만들기 어렵다.

결론적으로 유연성이 떨어진다. 

                                                                                         (김영한 선생님의 말씀)