싱글톤 패턴의 개념
싱글톤 패턴은 객체지향 프로그래밍에서 특정 클래스의 인스턴스를 하나로 유지하기 위한 디자인 패턴이다. 다량의 자원을 필요로 하는 객체의 무분별한 생성을 방지하기 위해 설계되었다.
싱글톤 패턴은 인스턴스를 하나만 유지하므로, 해당 인스턴스는 사실상 전역 변수와 같은 역할을 한다. 이 때문에 여러 클라이언트의 요청이 동시에 처리되는 경우 데이터의 무결성에 문제가 발생할 소지가 크다. 따라서 동기화를 통해 리소스 동시 접근을 제어함으로써 데이터를 보호하고, 잘못된 상태 변경의 결과가 다른 데이터로 파급되지 않도록 주의해야 한다.
싱글톤 패턴의 장점
인스턴스 제어
인스턴스가 하나만 유지되도록 제한함으로써 무분별한 객체 생성을 방지하고, 메모리를 효율적으로 관리할 수 있다.
객체 상태 일관성 유지
싱글톤 패턴은 외부의 모든 객체가 하나의 인스턴스를 사용하는 것을 전제하므로, 모든 개별 사용에 대해 객체 상태의 일관성을 유지하고 관리하기 용이하다.
전역 인스턴스 제공
싱글톤 패턴은 전역 인스턴스를 통해 다른 클래스에서 쉽게 접근할 수 있다. 따라서 하나의 인스턴스만 유지하며 원하는 작업을 수행할 수 있다.
코드 재사용성 증대
매번 인스턴스를 생성하지 않고 어디서나 전역 인스턴스를 참조할 수 있으므로, 코드의 중복을 피하고 외부에서 동일한 메서드 호출 코드를 지속적으로 재사용할 수 있다.
DI 컨테이너에서의 활용
DI(Dependency Injection) 컨테이너의 의존성 주입을 활용해 객체 간 결합도를 낮추고 요구사항 변경에 유연한 클래스를 설계할 수 있으며, 개발자는 핵심 비즈니스 로직에 집중할 수 있다.
싱글톤 패턴의 단점
객체 오용의 위험성
싱글톤 패턴은 전역 인스턴스를 유지하므로, 개별 상태 변경의 결과가 이후의 모든 참조에 영향을 미친다. 따라서 개별 인스턴스의 상태가 구분되어야 하는 경우, 싱글톤 패턴을 적용하기 어렵다. 또한 클라이언트의 예상치 못한 요청 순서에 의해 의도치 않은 상태 변경이 발생하면 해당 버그가 소프트웨어 전체로 파급될 위험성을 내포하고 있다. 이 같은 문제를 방지하기 위해 불변 객체를 사용할 수 있다. 이곳(https://hellmir.tistory.com/entry/개발지식-스터디-발표-자료빌더-패턴-팩토리-메서드-패턴)에 불변 객체에 대한 설명이 있다.
테스트 어려움
싱글톤 패턴을 사용하면 테스트가 어려워진다. 순수 함수를 유지하여 같은 입력에 대한 같은 결과가 보장되어야 테스트 설계가 용이하다. 그러나 싱글톤 패턴은 하나의 인스턴스가 외부의 다양한 영향을 받기 때문에, 모든 경우의 수를 고려한 테스트 케이스의 설계가 어렵고 다른 테스트 케이스의 영향에 대한 테스트 환경의 통제가 어렵다.
상속 어려움
싱글톤 패턴을 사용하면 인스턴스가 하나만 존재해야 하므로, 생성자를 private으로 제한하게 된다. 따라서 다른 클래스에서의 확장이 어려워진다. 이는 객체 지향 프로그래밍에서 상당히 불리한 특성이다.
객체 간 결합도 증가
싱글톤 패턴에서는 하나의 인스턴스가 외부의 모든 영향을 공유하므로, 이를 사용하는 객체 간 의존성이 높아질 가능성이 크다. 마찬가지로 객체 지향 프로그래밍에서 불리한 특성이다.
멀티 스레드 환경에서의 위험성
싱글톤 패턴을 사용하면 외부의 모든 객체가 하나의 인스턴스를 참조하므로, 특히 멀티 스레드 환경에서 동시 접근에 대한 동시성 문제가 발생할 위험성이 크다. 따라서 반드시 적절한 동기화 기법을 통해 관리되어야 한다.
대표적인 싱글톤 패턴 종류
Eager Initialization 전략
Eager Initialization 전략은 Hibernate의 Eager Fetching 전략과 유사한 개념으로서, 클래스 로딩 시점에 인스턴스를 생성하는 방식이다.
장점
- 클래스 로딩 시 인스턴스를 생성하므로, 직관적이고 구현이 간단하다.
- 최초 클래스 로딩 시점에 인스턴스가 자동으로 생성되고 해당 과정에서 동기화 상태가 유지되므로, 인스턴스 생성에 관련된 추가 동기화 작업이 필요 없다.
- 멀티 스레드 환경에서 여러 클라이언트가 동시에 접근하더라도 스레드 안전성이 보장된다.
단점
- 인스턴스를 생성할 필요가 없는 상황에서도, 정적 메서드를 호출하거나 정적 필드를 참조하기 위해 클래스를 로딩할 때 인스턴스가 자동으로 생성된다.
- 실제로 사용하지 않는 인스턴스 생성으로 인해 오버헤드(작업에 소요되는 필요 이상의 추가 자원, 시간)가 발생할 수 있다. 이 경우 추가 메모리와 인스턴스 생성, 초기화 시간을 낭비하게 된다.
코드 예시
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
public void doSomething() {
System.out.println("무언가를 합니다.");
}
}
INSTANCE 필드를 통해 클래스 로딩 시점에 자동으로 인스턴스를 생성한다. 생성자의 접근 제한자가 private이고 인스턴스 생성에는 final 필드를 사용하므로, 이후 추가적인 인스턴스를 생성할 수 없다.
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.doSomething(); // 무언가를 합니다.
}
}
Singleton 인스턴스를 생성하거나 참조할 때는 getInstance 메서드를 사용한다.
Lazy Initialization 전략
Lazy Initialization 전략은 Hibernate의 Lazy Fetching 전략과 유사한 개념으로서, 실제로 사용이 필요한 시점에 인스턴스를 생성한다.
장점
- 실제로 인스턴스를 생성할 필요가 없다면 생성하지 않는다.
- 인스턴스를 생성하지 않고 정적 메서드를 호출하거나 정적 필드만을 참조하는 경우 자원과 시간을 절약할 수 있다.
단점
- 여러 클라이언트에서 동시에 접근하는 경우 중복된 인스턴스 생성 시도로 인한 동시성 문제가 발생할 수 있다.
- 동기화 기법을 통한 추가 관리가 필요하다.
코드 예시
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void doSomething() {
System.out.println("무언가를 합니다.");
}
}
인스턴스는 자동으로 생성되지 않고, getInstance 메서드를 최초로 호출하는 클라이언트에 의해 생성된다. 이후에는 instance 필드의 값이 존재하므로 추가 인스턴스를 생성할 수 없다.
synchronized 키워드를 통해 lock 기법을 사용하여 getInstance 메서드의 동시 접근을 차단했지만, 메서드의 성능을 저하시킬 수 있다는 단점을 가지고 있다.
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.doSomething(); // 무언가를 합니다.
}
}
Initialization-on-demand holder idiom
Java에서 가장 많이 사용되는 Singleton 패턴으로서, Eager Initialization 전략과 Lazy Initialization 전략의 장점을 모두 가진다.
인스턴스 생성을 Singleton 클래스의 필드나 메서드가 아닌 내부 정적 클래스의 필드에서 담당하며, 생성된 인스턴스는 이후 해당 필드를 통해 참조된다.
장점
- Eager Initialization 전략과 Lazy Initialization 전략의 장점을 결합해, 동시성 문제를 발생시키지 않으면서도 추가 오버헤드를 방지할 수 있다.
단점
- 멀티 스레드 환경에서의 클래스 로딩을 통한 초기화 과정에서 동기화 상태를 유지할 수 있어야 하며, 내부 정적 클래스 기능을 지원해야 한다.
- 두 가지 모두 지원되는 언어에서만 사용할 수 있으며, 지원 언어의 종류가 많지 않다.
코드 예시
public class Singleton {
private Singleton() {
}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
public void doSomething() {
System.out.println("무언가를 합니다.");
}
}
Singleton 클래스 내부에 있는 Holder 클래스의 INSTANCE 필드를 통해 인스턴스를 생성한다. 따라서 Singleton 클래스 로딩 시점에 인스턴스를 미리 생성하지 않으면서도, 다수 클라이언트의 동시 접근에 대한 동시성 문제를 방지할 수 있다.
public class Main {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.doSomething(); // 무언가를 합니다.
}
}
'Development > Software Design' 카테고리의 다른 글
클래스 다이어그램 예시(배달 애플리케이션) (0) | 2023.08.08 |
---|---|
클래스 설계 시 상속 관계, 합성 관계, 위임 관계의 개념(in Java) (2) | 2023.07.23 |
클래스 다이어그램(Class Diagram) 모델링 (2) | 2023.07.10 |