🥕
TIL
  • [TIL] Studying tech / computer science knowledge
  • KeyMap
  • 알고리즘
    • 복잡도 계산 ( Computational Complexity )
    • DisjointSet-unionFind
    • Bellman-ford Algorithm
    • Dijkstra's Algorithm
    • DP ( Dynamic Programming , 동적 계획법 )
    • 플로이드-워셜 알고리즘 (Floyd-Warshall algorithm)
    • Kruskal's Algorithm
    • 최장 증가 수열 (Longes Increasing Subsequence)
    • Prim's Algorithm
    • 정렬
    • 시간복잡도 와 공간복잡도 ( Time Complexity & Space Complexity )
    • Topological Sort (위상 정렬)
  • 책 읽고난 후 요약
    • 프로그래밍 대회에서 배우는 알고리즘 문제해결 전략
    • cleancode
    • 도메인 주도 설계로 시작하는 마이크로서비스 개발
    • 오브젝트
  • CDC
    • debzium
    • kafka
  • 개발 상식
    • asciidoctor
    • 컴파일러
    • ELK 스택
    • 엔디안
    • git
    • Gitmoji
    • 테스트 종류
    • 라이브러리와 프레임워크
    • 정규 표현식
    • REST API
    • 동기와 비동기 / Blocking과 NonBlocking
    • Transaction Script와 Domain Model
    • 디자인 패턴
      • 행동 패턴
      • 객체 생성 패턴
        • 추상 팩토리 패턴
        • 빌더 패턴
        • 팩토리 메서드 패턴
        • [생성 패턴] 프로토 타입 (Prototype Parttern)
        • 싱글톤
      • 구조 패턴
        • 어댑터 패턴
        • 브릿지 패턴
        • 컴포짓(Composite) 패턴
        • 데코레이터
        • 프록시
    • refactoring
      • 중복 코드
      • 전역 데이터
      • 긴 함수
      • 긴 매개변수 목록
      • 가변 데이터
      • 이해하기 힘든 이름
  • 자료구조
    • AVL Tree
    • Splay Tree
    • aaTree
    • array-list
    • 자료구조 시간/공간 복잡도
    • 그래프
    • 힙
    • Red Black Tree
    • stack-queue
    • 트리 ( Tree )
  • DevOps
    • MSA
    • Kubernetes
      • AccessingAPI
      • controller
      • dashboard
      • kubernetes
      • object
      • pod
      • service
      • volume
  • Java
    • 어노테이션
    • 제어문
    • 데이터 타입
    • Enum
    • jvm
    • 연산자
    • thread
    • Java8
      • CompletableFuture
      • Date/Time
      • 어노테이션과 메타스페이스
      • 인터페이스
      • 람다식
      • Optional
      • 스트림
  • JavaScript
    • moduleProject
    • webpack-babel
    • 코어 자바스크립트
      • array
      • 함수 바인딩
      • 데코레이터와 포워딩
      • Class
      • 비교 연산자
      • Date 내장 객체
      • destructuring-assignment
      • function
      • 함수의 prototype 프로퍼티
      • 가비지 컬렉션 ( Garbage Collection )
      • JSON (JavaScript Object Notation)
      • map-set
      • 내장 프로토타입
      • new연산자와 생성자 함수
      • 객체
      • Object.keys, values, entries
      • 옵셔널 체이닝 '?.'
      • 프로퍼티 플래그
      • 프로퍼티 종류
      • 프로토 타입
      • 호출 스케줄링 ( scheduling a call )
      • scope
      • this
      • type-conversions
      • type
      • 함수의 자료형
      • var_let_const
  • Linux
    • 기본 명령어
    • 파일 종류
    • 리눅스
  • 네트워크
    • 응용 계층 ( Application Layer )
    • 오류 검출과 오류 정정
    • Http
    • Http Header
    • 컴퓨터 네트워크란
    • 네트워크 계층
    • 네트워크 제어 영역
    • 전송 계층 ( Transport Layer )
  • PHP
    • Facade
    • composer
    • scopeResolutionOperator
    • Laravel
      • SocialProvider
      • architecture
      • blade
      • controller
      • db
      • dbArchitecture
      • debug
      • eloquent
      • email
      • event
      • exceptionHandling
      • middleware
      • model
      • modelFactory
      • pagingLoading
      • queryBuilder
      • route
      • scout
      • seeding
      • tntsearch
      • validate
      • view
  • React
    • Next.js
    • React 란?
  • Spring
    • Controller
    • 요청이 들어왔을때 스프링이 처리하는 방법 ( 내부구조 )
    • ConfigurationProperties
    • Entity / DTO / VO
    • Maven
    • Repository와 DAO
    • 스프링 빈
    • Spring Framework
    • MVC 패턴
    • 도메인 입력값 검증
    • Spring Cloud
      • Spring Cloud
      • Eureka
    • Spring Data
      • JPA
      • JPA 어노테이션
      • 엔티티 비교
      • 복합 키와 식별 관계 매핑
      • JPA 예외처리
      • 객체지향 쿼리
      • EntityManagerFactory와 EntityManager
      • JPA 최적화
      • 프록시와 연관관계 맵핑
      • 연관관계
      • 상속관계 맵핑
      • 트랜잭션 범위와 영속성 컨텍스트
      • 데이터 타입
      • MySQL 연결
      • Pageable
    • Spring Project들과 library
      • Custom Serialize
      • Elasticsearch Index API
      • Spring HATEOAS
      • lombok (롬복)
      • Model Mapper
      • Object Mapper
      • Representation Model
      • Spring REST Docs
      • Spring Boot
    • Spring Security
      • Spring Security
      • Authentication
      • Authentication Filter
      • Authorization Filter
      • Filter Chain
      • SecurityContext
      • Spring OAuth2.0
    • Spring Test
      • AssertJ
      • Junit5
      • JunitParams
      • Mock Object
  • DataBase
    • ALIAS
    • CONCAT
    • CTE
    • Group By
    • HAVING
    • IFNULL
    • 인덱스
    • JOIN
    • ORDER BY
    • ROLLUP
    • SELECT
    • SELECT DISTINCT
    • SQL
    • WHERE
  • Web 상식
    • OAuth
    • WAS
    • HTTP통신 기반 인증
    • 브라우저
    • CSR 과 SSR
    • HTTPS
    • Web
Powered by GitBook
On this page
  • 목적
  • 구현 방법
  • 싱글톤이 깨지는 경우
  • 1. Reflection
  • 2. Serialize / DeSerialize
  1. 개발 상식
  2. 디자인 패턴
  3. 객체 생성 패턴

싱글톤

목적

  1. 인스턴스를 오직 한개만 만들기 위함

  2. 한개만 만든 객체에 대해 글로벌하게 접근이 가능해야함

구현 방법

  • Eager Initialization (사전 초기화) : 클래스 로딩시에 인스턴스를 생성하는 방법 ( 클래스 내부에서 인스턴스를 생성)

    인스턴스를 실제로 사용하지 않는다몀 불필요한 연산과 메모리 낭비

    public class Settings{
        private static final Settings INSTANCE = new Settings();
    
        private Settings(){}
    
        public static Settings getInstance() {
            return INSTANCE;
        }
    }
    
    //byte code
    private final static Lstudy/Settings; INSTANCE
    
    // access flags 0x2
    private <init>()V
    L0
        LINENUMBER 7 L0
        ALOAD 0
        INVOKESPECIAL java/lang/Object.<init> ()V
        RETURN
    L1
        LOCALVARIABLE this Lstudy/Settings; L0 L1 0
        MAXSTACK = 1
        MAXLOCALS = 1
    
    // access flags 0x9
    public static getInstance()Lstudy/Settings;
    L0
        LINENUMBER 10 L0
        GETSTATIC study/Settings.INSTANCE : Lstudy/Settings;
        ARETURN
        MAXSTACK = 1
        MAXLOCALS = 0
    
    // access flags 0x8
    static <clinit>()V
    L0
        LINENUMBER 5 L0
        NEW study/Settings
        DUP
        INVOKESPECIAL study/Settings.<init> ()V
        PUTSTATIC study/Settings.INSTANCE : Lstudy/Settings;
        RETURN
        MAXSTACK = 2
        MAXLOCALS = 0
  • Lazy Initialization (지연 초기화) : 인스턴스를 실제로 사용할 시점에서 인스턴스를 생성하는 방법으로 인스턴스를 실제로 사용하지 않는다면 메모리와 연산량을 아낄 수 있으나 이중 객체 생성 문제 발생할 가능성이 높다.

    public class Settings{
        private static Settings instance;
    
        private Settings(){}
    
        public static Settings getInstance() {
            if(instance == null){
                instance = new Settings();
            }
            return instance;
        }
    }
    //
    public class study/Settings {
    
        // compiled from: Settings.java
    
        // access flags 0xA
        private static Lstudy/Settings; instance
    
        // access flags 0x2
        private <init>()V
        L0
            LINENUMBER 7 L0
            ALOAD 0
            INVOKESPECIAL java/lang/Object.<init> ()V
            RETURN
        L1
            LOCALVARIABLE this Lstudy/Settings; L0 L1 0
            MAXSTACK = 1
            MAXLOCALS = 1
    
        // access flags 0x9
        public static getInstance()Lstudy/Settings;
        L0
            LINENUMBER 10 L0
            GETSTATIC study/Settings.instance : Lstudy/Settings;
            IFNONNULL L1
        L2
            LINENUMBER 11 L2
            NEW study/Settings
            DUP
            INVOKESPECIAL study/Settings.<init> ()V
            PUTSTATIC study/Settings.instance : Lstudy/Settings;
        L1
            LINENUMBER 13 L1
        FRAME SAME
            GETSTATIC study/Settings.instance : Lstudy/Settings;
            ARETURN
            MAXSTACK = 2
            MAXLOCALS = 0
        }

    여러개의 스레드가 getInstance를 거의 동시에 접근하여 객체를 얻는 상황이 발생한다고 한다면, instance가 null 인지 검사하는 부분에서 조건이 걸리지 않아 여러개의 객체를 생성하는 문제가 발생할 수 있다. 이를 해결하기 위해서는 동시에 접근을 하지못하도록 lock을 걸어야 하기 때문에 synchronized 키워드를 사용하면 확정으로 한개만 생성할 수 있다.

    public class Settings{
        private static Settings instance;
    
        private Settings(){}
    
        public static synchronized Settings getInstance() {
            if(instance == null){
                instance = new Settings();
            }
            return instance;
        }
    }
    
    //byte code
    public class study/Settings {
        // compiled from: Settings.java
    
        // access flags 0xA
        private static Lstudy/Settings; instance
    
        // access flags 0x2
        private <init>()V
        L0
            LINENUMBER 7 L0
            ALOAD 0
            INVOKESPECIAL java/lang/Object.<init> ()V
            RETURN
        L1
            LOCALVARIABLE this Lstudy/Settings; L0 L1 0
            MAXSTACK = 1
            MAXLOCALS = 1
    
        // access flags 0x29
        public static synchronized getInstance()Lstudy/Settings;
        L0
            LINENUMBER 10 L0
            GETSTATIC study/Settings.instance : Lstudy/Settings;
            IFNONNULL L1
        L2
            LINENUMBER 11 L2
            NEW study/Settings
            DUP
            INVOKESPECIAL study/Settings.<init> ()V
            PUTSTATIC study/Settings.instance : Lstudy/Settings;
        L1
            LINENUMBER 13 L1
        FRAME SAME
            GETSTATIC study/Settings.instance : Lstudy/Settings;
            ARETURN
            MAXSTACK = 2
            MAXLOCALS = 0
        }

    하지만 lock을 걸고 회수하는 것도 모두 자원을 소비하는 것이기 때문에 성능상 불이익이 있을 수 있다.

    • 최적화

      • Double Check Locking : 스레드를 학부생때 배운내용으로 동기화 과정에서 lock의 범위는 최소한으로 하는 것이 좋다고 배웠었다. lock의 범위가 넓어질 수록 락이 걸릴 필요가 없는 경우도 포함되어 스레드가 대기하는 부분이 많아지고 그만큼 비용이 들기 때문이다.

        public class Settings{
            private static volatile Settings instance;
        
            private Settings(){}
        
            public static Settings getInstance() {
                if(instance == null){
                    synchronized ( Settings.class ){
                        if(instance == null){
                            instance = new Settings();
                        }
                    }
                }
                return instance;
            }
        }
        
        //byte code
        public class study/Settings {
            // compiled from: Settings.java
        
            // access flags 0x4A
            private static volatile Lstudy/Settings; instance
        
            // access flags 0x2
            private <init>()V
            L0
                LINENUMBER 7 L0
                ALOAD 0
                INVOKESPECIAL java/lang/Object.<init> ()V
                RETURN
            L1
                LOCALVARIABLE this Lstudy/Settings; L0 L1 0
                MAXSTACK = 1
                MAXLOCALS = 1
        
            // access flags 0x9
            public static getInstance()Lstudy/Settings;
                TRYCATCHBLOCK L0 L1 L2 null
                TRYCATCHBLOCK L2 L3 L2 null
            L4
                LINENUMBER 10 L4
                GETSTATIC study/Settings.instance : Lstudy/Settings;
                IFNONNULL L5
            L6
                LINENUMBER 11 L6
                LDC Lstudy/Settings;.class
                DUP
                ASTORE 0
                MONITORENTER
            L0
                LINENUMBER 12 L0
                GETSTATIC study/Settings.instance : Lstudy/Settings;
                IFNONNULL L7
            L8
                LINENUMBER 13 L8
                NEW study/Settings
                DUP
                INVOKESPECIAL study/Settings.<init> ()V
                PUTSTATIC study/Settings.instance : Lstudy/Settings;
            L7
                LINENUMBER 15 L7
            FRAME APPEND [java/lang/Object]
                ALOAD 0
                MONITOREXIT
            L1
                GOTO L5
            L2
            FRAME SAME1 java/lang/Throwable
                ASTORE 1
                ALOAD 0
                MONITOREXIT
            L3
                ALOAD 1
                ATHROW
            L5
                LINENUMBER 17 L5
            FRAME CHOP 1
                GETSTATIC study/Settings.instance : Lstudy/Settings;
                ARETURN
                MAXSTACK = 2
                MAXLOCALS = 2
            }

        위처럼 instance가 생성되었는지를 두번 체크함으로써 lock의 범위를 줄여 필요없는 동기화 과정을 줄일 수 있다.

        Settings를 LDC하는 것을 볼 수 있는데 이는 런타임 상수풀에 값을 push(LDC)하는 작업이다. 후에 모니터객체와 값을 비교하기위해 클래스를 상수값으로 푸시하는 것을 볼 수 있다.

        이때 instance를 volatile keyword를 사용하지 않을 경우, double-checked locking에서 문제가 발생한다. 두 개의 thread T1, T2가 있다고 가정해보자. T1이 instance를 생성한 후에 synchronized block를 벗어나고 T2가 synchronized block에 들어가서 null 체크를 하는 시점에서 working memory와 main memory 사이의 동기화가 이뤄져있지 않을 수 있다. 즉, T1이 생성한 instance가 main memory에 존재하지 않거나 main memory에는 있지만 T2의 working memory에는 없을 경우 T2 또한 instance를 생성하게 된다.

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

        동기화 블럭을 사용시 괄호 안의 객체를 전달받고 있으며 이객체를 통해 동기화를 진행한다. 이를 모니터 객체라고 부른다. 그래서 여러 스레드에서 이 모니터 객체에 접근하고 이를 통해 동기화를 수행하기 때문에 컴파일타임에 새로 변수에 할당하는 것을 볼 수 있고 바이트 코드를 봐도 DUP을 통해 스택의 최상위에 있는 값(여기서는 Settings.class)를 복제하여 ASTORE하는 것을 볼 수 있다.

      • static inner 클래스 : static inner 클래스를 활용하면 위의 double check locking처럼 getInstance가 호출되는 타이밍에 inner cl

            public class Settings{
            private static Settings instance;
        
            private Settings(){}
        
            private static class SettingsHolder {
                private static final Settings INSTANCE = new Settings();
            }
        
            public static synchronized Settings getInstance() {
                return SettingsHolder.INSTANCE;
            }
        }
        
        //bytecode
        public class study/Settings {
        
            // compiled from: Settings4.java
            NESTMEMBER study/Settings$SettingsHolder
            // access flags 0xA
            private static INNERCLASS study/Settings$SettingsHolder study/Settings SettingsHolder
        
            // access flags 0x2
            private <init>()V
            L0
                LINENUMBER 8 L0
                ALOAD 0
                INVOKESPECIAL java/lang/Object.<init> ()V
                RETURN
            L1
                LOCALVARIABLE this Lstudy/Settings; L0 L1 0
                MAXSTACK = 1
                MAXLOCALS = 1
        
            // access flags 0x9
            public static getInstance()Lstudy/Settings;
            L0
                LINENUMBER 15 L0
                GETSTATIC study/Settings$SettingsHolder.INSTANCE : Lstudy/Settings;
                ARETURN
                MAXSTACK = 1
                MAXLOCALS = 0
        }

        이를 Initialization-on-demand holder idiom라고 하마며 요청시 초기화하여 lazy-load를 하는 패턴. JVM이 Settings 클래스를 load하여 초기화할때 중첩(내부) 클래스는 최초 실행 시점에 load한다는 점을 이용한 것.

+) 자바의 동기화

  1. 암묵적인 동기화 : synchronized keyword

    JVM의 BytecodeInterpreter에서 InterpreterRuntime의 MONITORENTER / MONITOREXIT 메서드 를 호출하여 구현.

    명시적인 동기화와 비교하면 암묵적인 동기화는 biased lock이라는 기법을 사용하여 경쟁상태가 아닌 스레드의 성능 향상 효과가 있다. 한마디로 동일 스레드가 연속적으로 cirtical section에 접근하는 경우에 실제 lock이 아닌 흉내만 내는 lock을 통해 atomic operation을 수행하지 않아 수행속도를 향상 시킬 수 있다.

    • 메서드 동기화 : BYTECODE Interpreter가 메서드를 수행하는 시저메 동기화 여부를 판별

    • 블럭 동기화 : 컴파일러에 의해 MONITORENTER / MONITOREXIT 로 변환

  2. 명시적인 동기화 : concurrent.locks.lock

    HOTSPOT에서 바로 NATIVE INTRINSIC FUNCTION 호출

싱글톤이 깨지는 경우

객체를 사용하는쪽에서 이상하게 사용하면 깨질 수 있다.

1. Reflection

public class Study {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Settings settings = Settings.getInstance();

        Constructor<Settings> constructor = Settings.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Settings settings1 = constructor.newInstance();

        System.out.println(settings == settings1); //false
    }
}

생성자를 private로 막아놔도 리플레션을 통해 접근을 하게 되면 싱글톤이 깨질 수 있다.

2. Serialize / DeSerialize

public class Study {
    public static void main(String[] args) throws IOException,ClassNotFoundException {
        Settings settings = Settings.getInstance();
        Settings settings1 = null;

        try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))){
            out.writeObject(settings);
        }

        try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))){
            settings1 = (Settings) in.readObject();
        }

        System.out.println(settings == settings1);      //false
    }
}

Serializable을 구현한 객체라면 Stream으로 직렬화/역직렬화 할 수 있는데 이때 역직렬화 하는 과정에서 기본이 new 키워드를 통해 객체를 생성하기 때문에 싱글톤이 깨질 수 있다.

public class Settings implements Serializable {
    private Settings() { }

    private static class SettingsHolder {
        private static final Settings INSTANCE = new Settings();
    }

    public static Settings getInstance(){
        return SettingsHolder.INSTANCE;
    }
    
    @Serial
    protected Object readResolve() {
        return getInstance();
    }
}

물론, 위와 같이 readResolve 메서드를 구현하면 역직렬화의 방법을 재정의 하여 싱글톤을 유지 할 수 있다.

https://01010011.blog/2017/01/20/java-synchronization-internal/

Previous[생성 패턴] 프로토 타입 (Prototype Parttern)Next구조 패턴

Last updated 3 years ago