본문 바로가기
1분 테크

싱글톤(Singleton) 패턴

by 1223v 2024. 5. 31.

싱글톤이란?


  • 단 하나의 유일한 객체를 만들기 위한 코드 패턴
  • 메모리 절약을 위해, 인스턴스가 필요할 때 똑같은 인스턴스를 새로 만들지 않고 기존의 인스턴스를 가져와 활용하는 기법. → 전역변수를 만들어 이용하는 이유는 똑같은 데이터를 메서드마다 지역변수로 선언해서 사용하면 무의미하고 낭비이기 때문에 전역에서 한번만 데이터를 선언하고 가져와 사용하면 효율적
  • 보통 해당 객체가 리소스를 많이 차지하는 역할을 하는 무거운 클래스일 때 사용

ex) 데이터베이스 연결 모듈에서 데이터베이스에 접속하는 작업(I/O 바운드)은 그 자체로 무거운 작업에 속하며, 한번만 객체를 생성하고 돌려쓰면 되지 굳이 여러번 생성하여 메모리를 낭비할 이유가 없음

, 디스크 연결, 네트워크 통신, DBCP 커넥션풀, 스레드 풀, 캐시, 로그기록 객체 등.

 

 

싱글톤 패턴 구현 원리

 

싱글톤으로 이용할 클래스를 외부에서 마구잡이로 new 생성자를 통해 인스턴스화 하는 것을 제한하기 위해 클래스 생성자 메서드에 private 키워드를 붙여주면 된다.

위 그림에서 볼 수 있듯이 getInstance() 라는 메서드에 생성자 초기화를 해주어, 만약 클라이언트가 싱글톤 클래스를 생성해서 사용하려면, getInstance() 라는 매서드를 실행을 통해 instance 필드 변수가 null일 경우 초기화를 진행하고, null이 아닐경우 이미 생성된 객체를 반환하는 식으로 구성

정적 메소드로 getInstance()를 통해 객체를 불러와 변수에 저장하고 이를 출력해보면 같은 주소 출력.

  • 즉, 객체 하나만 생성하고 여러 변수를 불러와도 돌려쓰기 한 것
public class Main {
	public static void main(String[] args) {
		
		// Singleton.getInstance()를 통해 싱글톤 객체를 각기 변수마다 받아와도 똑같은 객체 주소를 가리킴
		Singleton i1 = Singleton.getInstance();
		Singleton i2 = Singleton.getInstance();
		Singleton i3 = Singleton.getInstance();
		
		System.out.println(i1.toString()); // Singleton@1b6d3586
		System.out.println(i2.toString()); // Singleton@1b6d3586
		System.out.println(i3.toString()); // Singleton@1b6d3586
		
		System.out.println(i1 == i2); // true
	}
	
}

 

 

싱글톤 구현 기법

ver.JAVA

  1. Eager Initialization
    • 한번만 미리 만들어두는, 가장 직관적이면서도 심플한 기법
    • static final이라 멀티쓰레드 환경에서도 안전함
    • 그러나 static 멤버는 당장 객체를 사용하지 않더라도 메모리에 적재하기 때문에 만일 리소스가 큰 객체의 경우, 자원 낭비가 발생
class Singleton {

// 싱글톤 클래스 객체를 담을 인스턴스 변수
private static final Singleton INSTANCE = new Singleton();

// 생성자를 private로 선언 (외부에서 new 사용 x)
private Singleton(){}

public static Singleton getInstance(){
	return INSTANCE;
	}
}

 

 

2. Static block initialization

  • statice block을 이용해 예외를 잡을 수 있음
  • 그러나 여전히 static의 특성으로 사용하지 않는데도 공간 차지
  • static block: 클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 실행하되는 블록
class Singleton {

//싱글톤 클래스 개체를 담을 인스턴스 변수
private static Singleton instance;

// 생성자를 private로 선언
private Singleton(){}

//static 블록을 이용해 예외 처리
static {
	try{
			instance = new Singleton();
		} catch (Exception e){
			throw new RuntimeException("싱글톤 객체 생성 오류");
		}
	}

	public static Singleton getInstance() {
			return instance;
	}
}

 

 

3. Lazy initialization

  • 객체 생성에 대한 관리를 내부적으로 처리
  • 메서드를 호출했을 때 인스턴스 변수의 null 유무에 따라 초기화하거나 있는 걸 반환하는 기법
  • 위의 미사용 고정 메모리 차지의 한계점 극복
  • 스레드 세이프를 하지 않는 치명적 단점을 가짐
class Singleton {
    // 싱글톤 클래스 객체를 담을 인스턴스 변수
    private static Singleton instance;

    // 생성자를 private로 선언 (외부에서 new 사용 X)
    private Singleton() {}
	
    // 외부에서 정적 메서드를 호출하면 그제서야 초기화 진행 (lazy)
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // 오직 1개의 객체만 생성
        }
        return instance;
    }
}

 

    • 대부분의 자료에서 보통 위의 코드를 싱글톤 패턴의 정석이라고 하지만, 자바는 멀티 쓰레드 언어이기에 스레드 세이프하지 않다는 점.
    • 각 스레드는 자신의 실행 단위를 기억하면서 코드를 위에서 아래로 읽어감 따라서 동시성 문제 발생멀티 스레드 환경에서의 치명적 단점
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//싱글톤 객체
class Singleton {
		private static Singleton instance;
		
		private Singleton() {}
		
		public static Singleton getInstance() {
			if (instance == null) {
				instance = new Singleton(); // 오직 1개의 객체만 생성
			}
			return instance;
		}
	}
	
public class Main {
	public static void main(String[] args) {
		//1. 싱글톤 객체를 담을 배열
		Singleton[] singleton = new Singleton[10];
		
		//2. 스레드 풀 생성
		ExecutorService service = Executors.newCachedThreadPool();
		
		//3. 반복문을 통해 10개의 스레드가 동시에 인스턴스 생성
		for(int i = 0; i < 10; i++) {
			final int num = i;
			service.submit(() -> {
				singleton[num] = Singleton.getInstance();
			});
		}
		
		//4. 종료
		service.shutdown();
		
		
		//5. 싱글톤 객체 주소 출력
		for(Singleton s : singleton) {
			System.out.println(s.toString());
		}
	}
}

여러개의 쓰레드를 생성하고 싱글톤 클래스르르 get하여 정말로 이 객체가 유일한 객체인지 해시코드롤 판단하는 코드

 

 

4. Thread safe initialization

  • synchronized 키워드를 통해 메서드에 스레드들을 하나하나씩 접근하게 하도록 설정
  • 하지만 여러개의 모듈들이 매번 객체를 가져올 떄, synchronized 메서드를 매번 호출하여 동기화 처리 작업에 overhead 발생 → 성능 하락
class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // synchronized 메서드
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 

 

5. Double-Checked Locking

  • 매번 synchronized 동기화를 실행하는 것이 문제라면 최초 초기화할 때만 적용하고 이미 만들어진 인스턴스를 반환 할때는 사용하지 않도록 하는 기법
  • 이때 인스턴스 필드에 volatile 키워드를 붙여 주어야 I/O 불일치 문제를 해결할 수 있다.
  • 그러나 volatile 키워드를 이용하기 위해선 JVM 1.5 이상이어야 되고, JVM 에 대한 심층적 이해가 필요해서 여전히 스레드 세이프 하지 않는 경우가 발생하기 때문에 사용 지양
class Singleton {
	private static volatile Singleton instance; // volatile 키워드 사용
	
	private Singleton() {}
	
	public static Singleton getInstance() {
		if (instance == null) {
			// 메서드에 동기화 거는게 아닌, Singleton 클래스 자체를 동기화 걸어버림
			synchronized (Singleton.class) {
				if(instance == null) {
					instance = new Singleton(); // 최초 초기화만 동기화 작업이 일어나서 리소스 낭비 최소화
				}
			}
		}
		return instance; // 최초 초기화가 되면 앞으로 생성된 인스턴스만 반환
	}

 

 

 

6. Bill Pugh Solution(권장)

  • 멀티스레드 환경에서 안전하고 LAZY.Loading도 가능한 완벽한 싱글톤 기법
  • 클래스 안에 내부 클래스(holder)를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법(스레드 세이프함)
  • static 메소드에서는 static 멤버만을 호출할 수 있기 때문에 내부 클래스를 static으로 설정
  • 역직렬화와 같은 것으로 클라이언트가 임의로 싱글톤 파괴 가능
class Singleton {
	private Singleton() {
	
	//static 내부 클래스를 이용
	// Holder로 만들어, 클래스가 메모리에 로드되지 않고, getInstance 메서드가 호출되어야  로드됨
	private static class SingleInstanceHolder {
		private static final Singleton INSTANCE = new Singleton();
	}
	
	public static Singleton getInstance() {
		return SingleInstanceHolder.INSTANCE;
	}
}	
  • 우선 내부클래스를 static으로 선언하였기 때문에 싱글톤 클래스가 초기화되어도 SingleInstanceHolder 내부 클래스는 메모리에 로드되지 않음
  • 어떠한 모듈에서 getInstance() 메서드를 호출할 때, SingleInstanceHolder내부 클래스의 static 멤버를 가져와 리턴하게 되는데, 이때 내부 클래스가 한번만 초기화되면서 싱글톤 객체를 최초로 생성 및 리턴하게 된다.
  • 마지막으로 final로 지정함으로써 다시 값이 할당되지 않도록 방지한다.

 

7. Enum 이용

  • enum은 애초에 멤버를 만들때 private로 만들고 한번만 초기화하기 때문에 thread safe 함.
  • enum내에서 상수 뿐만 아니라, 변수나 메서드를 선언해 사용가능하기 때문에 이를 이용해 싱글톤 클래스처럼 응용 가능
  • 클라이언트의 Reflection을 통한 공격에도 안전
  • 만약 싱글톤 클래스를 멀티톤으로 마이그레이션 해야할 떄 처음부터 코드를 다시 짜야 되는 단점 존재
  • 클래스 상속이 필요할때, enum외의 클래스 상속은 불가능
enum SingletonEnum {
	INSTANCE;
	
	private final Client dbClient;
	
	SingletonEnum() {
		dbClient = Database.getClient();
	}
	
	public static SingletonEnum getInstance() {
		return INSTANCE;
	}
	
	public Client getClient() {
		return dbClient;
	}	
}

public class Main {
	public static void main(String[] args) {
		SingletonEnum singleton = SingletonEnum.getInstance();
		single.getClient();
	}
}

 

 

 

ver. React

리액트 함수형 컴포넌트에서 싱글톤 패턴을 사용하는 방법은 크게 두 가지

첫 번째는 간단한 싱글톤 클래스를 사용하고 이를 함수형 컴포넌트에서 사용하는 방법

두 번째는 Context API를 이용하는 방법.

 

싱글톤 클래스 정의

// ConfigSingleton.js

class ConfigSingleton {
  constructor() {
    if (!ConfigSingleton.instance) {
      this.config = {}; // 설정 데이터를 저장하는 객체
      ConfigSingleton.instance = this; // ConfigSingleton 인스턴스를 저장
    }
    return ConfigSingleton.instance; // 이미 존재하는 인스턴스를 반환
  }

  setConfig(key, value) {
    this.config[key] = value; // 설정 데이터 저장
  }

  getConfig(key) {
    return this.config[key]; // 설정 데이터 반환
  }
}

const configInstance = new ConfigSingleton();
Object.freeze(configInstance); // 인스턴스를 동결하여 수정할 수 없도록 함

export default configInstance;

 

함수형 컴포넌트에서 사용하기

// App.js
import React, { useEffect, useState } from 'react';
import configInstance from './ConfigSingleton'; // 싱글톤 인스턴스 가져오기

// 설정을 업데이트하는 컴포넌트
const ConfigUpdater = () => {
  useEffect(() => {
    configInstance.setConfig('apiUrl', 'https://api.example.com'); // 설정 값을 업데이트
  }, []);

  return <div>Config Updated</div>;
};

// 설정을 사용하는 컴포넌트
const ConfigViewer = () => {
  const [apiUrl, setApiUrl] = useState('');

  useEffect(() => {
    const url = configInstance.getConfig('apiUrl'); // 설정 값을 가져옴
    setApiUrl(url);
  }, []);

  return <div>API URL: {apiUrl}</div>;
};

// App 컴포넌트
const App = () => {
  return (
    <div>
      <h1>Singleton Example in a Real World Scenario</h1>
      <ConfigUpdater />
      <ConfigViewer />
    </div>
  );
};

export default App;

 

 

2. 리액트 Context API와 싱글톤 사용하기

컨텍스트 및 프로바이더 정의

// ConfigContext.js
import React, { createContext, useContext, useState } from 'react';

// ConfigContext 생성
const ConfigContext = createContext();

// ConfigProvider 컴포넌트
const ConfigProvider = ({ children }) => {
  const [config, setConfig] = useState({});

  const setConfigValue = (key, value) => {
    setConfig((prevConfig) => ({ ...prevConfig, [key]: value }));
  };

  const getConfigValue = (key) => {
    return config[key];
  };

  return (
    <ConfigContext.Provider value={{ setConfigValue, getConfigValue }}>
      {children}
    </ConfigContext.Provider>
  );
};

// ConfigContext를 사용하는 커스텀 훅
const useConfig = () => {
  return useContext(ConfigContext);
};

export { ConfigProvider, useConfig };

 

함수형 컴포넌트에서 사용하기

// App.js
import React, { useEffect } from 'react';
import { ConfigProvider, useConfig } from './ConfigContext';

// 설정을 업데이트하는 컴포넌트
const ConfigUpdater = () => {
  const { setConfigValue } = useConfig();

  useEffect(() => {
    setConfigValue('apiUrl', 'https://api.example.com'); // 설정 값을 업데이트
  }, [setConfigValue]);

  return <div>Config Updated</div>;
};

// 설정을 사용하는 컴포넌트
const ConfigViewer = () => {
  const { getConfigValue } = useConfig();
  const apiUrl = getConfigValue('apiUrl'); // 설정 값을 가져옴

  return <div>API URL: {apiUrl}</div>;
};

// App 컴포넌트
const App = () => {
  return (
    <ConfigProvider>
      <h1>Singleton Example with Context API</h1>
      <ConfigUpdater />
      <ConfigViewer />
    </ConfigProvider>
  );
};

export default App;

 

위의 두 가지 방법을 사용하면 리액트 함수형 컴포넌트에서도 싱글톤 패턴을 효과적으로 구현할 수 있다. 첫 번째 방법은 간단한 클래스 기반의 싱글톤을 사용하는 것이고, 두 번째 방법은 Context API를 활용하여 글로벌 상태를 관리하는 것이다.

하지만 자바와 다르게 자바스크립트는 싱글스레드 기반으로 작동된다.

 

그렇다면, 동기화에서 문제가 발생하지 않을까?

전혀 아니다.

 

동기화 문제

1. 비동기 작업 주의

비동기 작업(예: fetch, setTimeout, Promise 등)은 이벤트 루프를 통해 처리되므로, 이러한 작업을 할 때 데이터 상태가 변경될 수 있다. 싱글톤 인스턴스에 데이터를 설정하거나 가져올 때 비동기 작업이 동시에 발생하는 경우를 조심해야 한다.

2. 상태 동기화

리액트의 상태는 컴포넌트별로 관리되며, 상태 업데이트는 비동기로 처리된다. 따라서 싱글톤 패턴을 사용할 때, 상태가 제대로 동기화되지 않으면 의도치 않은 결과가 발생할 수 있다. 이는 주로 비동기 작업이나 여러 컴포넌트에서 동일한 싱글톤 인스턴스를 사용할 때 발생할 수 있다.

3. Context API와 싱글톤

Context API를 사용하면 전역 상태 관리를 더 안전하고 일관되게 할 수 있다. 이를 통해 상태를 관리하면 비동기 작업과 관련된 문제를 어느 정도 해결할 수 있다.

 

개선된 코드

// ConfigSingleton.js

class ConfigSingleton {
  constructor() {
    if (!ConfigSingleton.instance) {
      this.config = {}; // 설정 데이터를 저장하는 객체
      ConfigSingleton.instance = this; // ConfigSingleton 인스턴스를 저장
    }
    return ConfigSingleton.instance; // 이미 존재하는 인스턴스를 반환
  }

  async setConfig(key, value) {
    // 비동기 작업을 시뮬레이션
    await new Promise((resolve) => setTimeout(resolve, 100));
    this.config[key] = value; // 설정 데이터 저장
  }

  getConfig(key) {
    return this.config[key]; // 설정 데이터 반환
  }
}

const configInstance = new ConfigSingleton();
Object.freeze(configInstance); // 인스턴스를 동결하여 수정할 수 없도록 함

export default configInstance;

 

// App.js
import React, { useEffect, useState } from 'react';
import configInstance from './ConfigSingleton'; // 싱글톤 인스턴스 가져오기

// 설정을 업데이트하는 컴포넌트
const ConfigUpdater = () => {
  useEffect(() => {
    const updateConfig = async () => {
      await configInstance.setConfig('apiUrl', 'https://api.example.com'); // 설정 값을 업데이트
    };

    updateConfig();
  }, []);

  return <div>Config Updated</div>;
};

// 설정을 사용하는 컴포넌트
const ConfigViewer = () => {
  const [apiUrl, setApiUrl] = useState('');

  useEffect(() => {
    const url = configInstance.getConfig('apiUrl'); // 설정 값을 가져옴
    setApiUrl(url);
  }, []);

  return <div>API URL: {apiUrl}</div>;
};

// App 컴포넌트
const App = () => {
  return (
    <div>
      <h1>Singleton Example in a Real World Scenario</h1>
      <ConfigUpdater />
      <ConfigViewer />
    </div>
  );
};

export default App;

 

위 예제에서는 비동기 작업이 포함된 설정 업데이트를 구현했다. 비동기 작업이 완료된 후 설정 값이 저장된다. 이렇게 하면 비동기 작업이 완료되기 전에 설정 값을 가져오는 문제를 방지할 수 있다.

리액트에서 싱글톤 패턴을 사용할 때 멀티스레딩에 대한 걱정은 하지 않아도 되지만, 비동기 작업과 상태 동기화에 주의해야 한다. Context API를 사용하는 것이 이러한 문제를 해결하는 데 도움이 될 수 있다.

 

 

싱글톤 단점

  1. TDD 단위 테스트 힘듬 : TDD할떄 주로 단위 테스트를 진행하는데, 단위 테스트가 서로 독립적이어야 하며, 테스트를 어떤 순서로든 실행할 수 있어야 한다. 하지만 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 독립적인 인스턴스를 만들기 어렵다.
  2. 모듈간의 의존성이 높아짐 : 싱글톤 패턴의 대부분은 인터페이스가 아닌 클래스의 객체를 미리 생성해 놓고 정적 메서드를 이용하는 방식을 사용하기 때문에, 클래스 사이에 강한 의존성과 높은 결합이 생기게 된다. 의존성이 높다는 것은 하나의 모듈을 수정함으로 그 모듈을 참조하는 다른 모듈들도 수정이 필요하게 된다는 것이다. 이는 의존성 주입을 통해 모듈간의 결합을 조금 더 느슨하게 만들어 해결할 수 있다. 의존성 주입이란, 클래스가 필요한 객체를 클래스 외부에서 생성하여 클래스 내부로 파라미터를 통해 내부로 주입하는 것이다.

 

 

참고 문헌.

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%8B%B1%EA%B8%80%ED%86%A4Singleton-%ED%8C%A8%ED%84%B4-%EA%BC%BC%EA%BC%BC%ED%95%98%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

 

💠 싱글톤(Singleton) 패턴 - 꼼꼼하게 알아보자

Singleton Pattern 싱글톤 패턴은 디자인 패턴들 중에서 가장 개념적으로 간단한 패턴이다. 하지만 간단한 만큼 이 패턴에 대해 코드만 던져주고 끝내버리는 경우가 있어, 어디에 쓰이는지 어떠한 문

inpa.tistory.com

https://taemham.github.io/posts/CS_DesignPattern/

 

[CS] 디자인패턴이란? Part 01

디자인 패턴이란?

taemham.github.io

 

'1분 테크' 카테고리의 다른 글

Enum  (0) 2024.06.25
전략패턴(Strategy) 패턴  (0) 2024.06.10
정규화(Normalization)  (0) 2024.03.31

댓글