이번 글에서는 디자인 패턴의 종류 중 하나인 싱글톤 패턴에 대해 다뤄볼 것입니다.
싱글톤 패턴이란?
싱글톤 패턴(Singleton pattern)은 객체의 인스턴스를 단 하나만 사용하는 디자인 패턴을 의미합니다.
애플리케이션이 시작될 때, 어떤 클래스에 대해 최초 한 번만 메모리를 할당(static)하고 해당 메모리에 인스턴스를 만들어 사용하는 패턴입니다.
생성자를 여러번 호출해도, 실제로 생성되는 객체는 하나이며 최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환시키도록 합니다.
주로 공통된 객체를 여러개 생성해서 사용해야하는 상황(예: 데이터베이스 커넥션풀, 스레드풀, 캐시, 로그 기록 객체 등)에 사용합니다.
싱글톤 패턴 구현방법
싱글톤 패턴의 기본적인 구현 방법은 다음과 같습니다.
public class Singleton {
// 단 1개만 존재해야 하는 객체의 인스턴스로 static 으로 선언
private static Singleton instance;
// private 생성자로 외부에서 객체 생성을 막아야 한다.
private Singleton() {
}
// 외부에서는 getInstance() 로 instance 를 반환
public static Singleton getInstance() {
// instance 가 null 일 때만 생성
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
싱글톤의 모든 구현은 공통적으로 다음의 두 단계를 갖습니다.
- 먼저 생성자를 private으로 선언해 외부에서 새로운 객체를 생성하지 못하도록 만듭니다.
- private static으로 Singleton 객체의 인스턴스를 선언한 다음, 객체 생성을 위한 'getInstance()' 메소드를 만드세요.
해당 메소드가 처음 실행 될때만 하나의 인스턴스가 생성되고 그 후에는 이미 생성된 인스턴스를 반환합니다.
싱글톤 패턴을 사용하는 이유
싱글톤 패턴의 이점은 다음과 같다.
1. 메모리 측면의 이점
한개의 인스턴스만을 고정 메모리 영역에 생성하기 때문에 추후 해당 객체를 접근할 때 메모리 낭비를 방지할 수 있다.
2. 속도 측면의 이점
생성된 인스턴스를 사용할 때는 이미 생성된 인스턴스를 활용하여 속도 측면에 이점이 있다.
3. 데이터 공유가 쉽다
싱글톤으로 구현한 인스턴스는 '전역'으로, 다른 여러 클래스에서 데이터를 공유하며 사용할 수 있다.
싱글톤 패턴의 문제점과 해결법
문제점1. 개방-폐쇄 원칙 위반
객체 지향 설계 원칙중에는 "개방-폐쇄 원칙(OCP, Open-Closed Principle)"이란 것이 존재합니다. OCP는 소프트웨어 엔티티(클래스, 모듈 등)는 확장에 대해 열려 있어야 하고 수정에 대해서는 닫혀 있어야 한다는 원칙을 나타냅니다. 싱글톤 패턴은 수정에 대해 폐쇄적이지만 확장에 대해서는 개방적이지 않을 수 있습니다.
만약 싱글톤 인스턴스가 혼자 너무 많은 일을 하거나, 많은 데이터를 공유시키면 다른 클래스들 간의 결합도가 높아지게 되는데, 이때 개방-폐쇄 원칙이 위배됩니다. 결합도가 높아지게 되면, 상속을 통한 확장이 어려울 수 있습니다. 새로운 기능을 추가하려면 기존 싱글톤 클래스를 수정해야 해서 유지보수가 힘들고 테스트도 원활하게 진행할 수 없는 문제점이 발생합니다.
이러한 문제를 해결하기 위해 다음과 같은 방법을 고려할 수 있습니다.
해결법1. 인터페이스 활용
인터페이스를 정의하고 해당 인터페이스를 구현하는 클래스를 만듭니다. 이를 통해 다양한 클래스가 싱글톤 대신 인터페이스를 활용할 수 있습니다.
해결법2. DI(의존성 주입)
싱글톤 객체가 의존성 주입을 받을 수 있도록 구조로 변경합니다. 이를 통해 다른 객체를 주입하여 기능을 확장하거나 변경할 수 있습니다.
해결법3. 컨테이너/팩토리 패턴
객체 생성 및 관리를 담당하는 별도의 컨테이너나 팩토리를 도입합니다. 이를 통해 객체 생성 및 관리를 중앙에서 수행하고, 확장이 용이해집니다.
해결법4. Spring Framework 사용
Spring Framework와 같은 IoC(제어 역전) 컨테이너를 사용하면 객체의 라이프사이클과 의존성 주입을 쉽게 관리할 수 있으며, 싱글톤 객체를 더 유연하게 다룰 수 있습니다. Spring에서 @Component 어노테이션은 해당 클래스를 Spring Bean으로 등록하며, Spring IoC 컨테이너가 객체의 생명주기와 의존성을 관리합니다. 이렇게 하면 MySingletonService 클래스가 싱글톤으로 생성되며, 여러 클라이언트에서 의존성 주입을 통해 사용할 수 있습니다. 이렇게 Spring을 사용하면 객체를 확장하거나 다른 구현체로 교체하기가 훨씬 쉬워집니다.
Spring을 사용하면 개방-폐쇄 원칙을 준수하고 더 모듈화된 코드를 작성할 수 있으며, 코드 변경 없이도 새로운 기능을 추가하거나 변경할 수 있습니다.
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;
// 1. 의존성 주입을 통한 싱글톤 객체 생성:
@Component
public class MySingletonService {
// 싱글톤 객체의 로직 구현
}
// 2. 클라이언트에서 의존성 주입:
public class MyClientService {
private final MySingletonService singletonService;
@Autowired
public MyClientService(MySingletonService singletonService) {
this.singletonService = singletonService;
}
// 싱글톤 객체 사용
}
문제점2. 멀티스레드 환경에서의 문제
멀티 스레드환경에서의 싱글톤 패턴을 사용하게 된다면 다음과 같은 문제가 발생할 수 있습니다.
1. 여러개의 인스턴스 생성
멀티스레드 환경에서 인스턴스가 없을 때 동시에 'getInstance()' 메서드를 실행하는 경우 각각 새로운 인스턴스를 생성할 수 있습니다.
2. 변수 값의 일관성 실패
다음과 같은 코드가 실행이 되었을 때 여러개의 thread에서 'plusCount()'를 동시에 실행을 한다면 일관되지 않은 값들이 생길 수
있습니다.
public class Singleton {
private static Singleton instance;
private static int count = 0;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public static void plusCount() {
count++;
}
}
이러한 문제를 해결하기 위해 다음과 같은 방법을 고려할 수 있습니다.
해결법1. 정적 변수선언에서 인스턴스 생성
이러한 문제는 아래와 같이 static 변수로 singleton 인스턴스를 생성하는 방법으로 해결할 수 있습니다. 아래와 같이 초기에 인스턴스를 생성하게 된다면 멀티스레드 환경에서도 다른 객체들은 'getInstance()'를 통해 하나의 인스턴스를 공유할 수 있습니다.
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
해결법2. synchronzied의 사용
아래의 코드와 같이 synchronzied를 적용하여 멀티스레드 환경에서 동시성 문제를 해결할 수 있습니다.
synchronized로 선언되었기 때문에, 한 스레드가 'getInstance()' 메서드에 접근하면 다른 스레드는 대기 상태로 들어가며, 이로써 동시에 여러 스레드가 'instance'를 생성하는 것을 방지합니다.
그러나 이러한 방식은 성능에 영향을 미칠 수 있으므로, 많은 스레드가 동시에 'getInstance()' 메서드에 접근하는 시나리오에서는 성능 문제가 발생할 수 있습니다. 따라서 더 효율적인 싱글톤 패턴 구현을 고려하는 것이 좋습니다. 예를 들어 "이중 검사 락"을 사용하거나, 초기화된 인스턴스를 사용하도록 하는 방식을 고려할 수 있습니다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronzied Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
Reference
https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/
https://gyoogle.dev/blog/design-pattern/Singleton%20Pattern.html
https://refactoring.guru/ko/design-patterns/singleton
https://velog.io/@seongwon97/싱글톤Singleton-패턴이란
결론
싱글톤 패턴은 메모리, 속도, 데이터 공유 측면에서 이점이 많이 있습니다. 앞서 말헀듯이 멀티스레드 환경에서는 동시성 문제가 발생할 수 있기에 싱글톤 패턴을 사용하고자 한다면 "해당 객체의 인스턴스가 한 개만 존재하여야 하는지?" 여부와 "사용을 하였을 때의 동시성 문제가 발생하지 않는지"를 체크하며 사용하여야 합니다.
'CS > 디자인패턴' 카테고리의 다른 글
[디자인 패턴] 빌더 패턴(Builder pattern) (0) | 2024.01.17 |
---|