1. 싱글턴 패턴의 기본 개념
- 오직 하나의 인스턴스만을 생성하고, 그 인스턴스에 전역적으로 접근할 수 있도록 보장하는 디자인 패턴
❗ 시스템에서 특정 클래스의 인스턴스가 하나만 존재해야 하는 경우에 사용한다.
2. 싱글턴 패턴 구현 (JAVA)
- 미리 생성 유무에 따라서 게으른 초기화(Lazy Initialization)와 이른 초기화(Eager Initialization)로 나뉜다.
1. 게으른 초기화(Lazy Initialization) : 필요할 때까지 인스턴스를 생성하지 않는 방식
- 장점 : 사용하지 않을 때는 메모리에 할당 X
- 단점 : 인스턴스를 요청할 때마다 새로 생성하는 지연이 있을 수 있음
public class Singleton {
private static Singleton instance; // 정적 변수로 유일한 인스턴스를 저장
private Singleton() {} // private 생성자
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 최초 호출 시 인스턴스 생성
}
return instance;
}
}
2. 이른 초기화(Eager Initialization) : 클래스 로딩 시 미리 인스턴스를 생성하는 방식
- 장점 : 애플리케이션이 시작될 때 미리 생성 -> 인스턴스를 요청할 때마다 새로 생성하는 지연이 없음
- 단점 : 사용하지 않더라도 메모리에 항상 할당
public class Singleton {
private static final Singleton instance = new Singleton(); // 미리 인스턴스 생성
private Singleton() {} // private 생성자
public static Singleton getInstance() {
return instance; // 미리 생성된 인스턴스 반환
}
}
3. 싱글턴 장단점
장점
- 인스턴스의 유일성 보장 : 시스템 전역에서 하나의 인스턴스를 공유 가능 -> 자원의 낭비 X.
- 전역 접근 : 어디서나 인스턴스에 접근 가능 -> 여러 컴포넌트 간 데이터를 쉽게 공유 가능.
- 메모리 절약 : 인스턴스를 하나만 생성 -> 메모리 사용량 Down.
단점
- 테스트 어려움: 싱글턴 인스턴스는 전역적으로 사용 -> 테스트할 때 격리가 어려움.
- 병목 현상: 하나의 인스턴스만 사용 -> 멀티스레드 환경에서 동기화 처리를 잘 하지 않으면 병목 현상이 발생.
- 유연성 부족: 클래스 설계 시 싱글턴으로 설정 -> 나중에 여러 인스턴스를 생성해야 하는 경우 유연성이 떨어짐.
4. 싱글턴 패턴과 다른 디자인 패턴 비교
1.1 팩토리 패턴
- 객체 생성을 클래스에 직접적으로 의존하지 않고, 팩토리(생산자) 메서드를 통해 객체를 생성하는 방식.
- 객체 생성의 책임을 팩토리 클래스에 넘김으로써 코드의 유연성을 높이고, 객체 생성 로직을 중앙에서 관리.
- 다양한 하위 클래스에 대한 인스턴스를 생성할 수 있는 유연성
- 객체 생성의 로직을 숨길 수 있음
- 유지보수와 코드 확장성이 용이
비교
- 싱글턴은 단 하나의 인스턴스만을 보장하고, 팩토리는 여러 종류의 객체를 동적으로 생성할 수 있습니다.
- 싱글턴은 전역적인 리소스 관리에 사용, 팩토리는 다양한 하위 클래스의 인스턴스 생성을 간단하게 처리할 때 사용.
1.2. 빌더 패턴
- 빌더 패턴은 복잡한 객체를 단계적으로 생성.
- 객체 생성 시 많은 매개변수가 필요한 경우, 각 단계에서 객체를 구성하는 방식을 세분화 가능.
- 이를 통해 코드의 가독성을 높이고, 객체 생성 중의 변경 가능성을 줄일 수 있음.
- 복잡한 객체를 단계별로 생성
- 각 단계에서 유연한 조립과 설정이 가능
- 필드가 많은 클래스에 유용 (특히 불변 객체 생성)
비교:
- 싱글턴은 하나의 인스턴스를 관리하는 데 초점이 있고, 빌더는 복잡한 객체를 보다 간결하게 생성하는 데 중점.
- 싱글턴은 객체가 단일성을 보장하는 데 사용되고, 빌더는 객체 생성 중에 유연성을 제공합니다.
1.3. 프로토타입 패턴
- 프로토타입 패턴은 기존 객체를 복제하여 새로운 객체를 생성하는 방식. ->특정 객체를 복제하는 것이 필요할 때 사용
- 이 방식은 객체를 생성하는 데 복잡한 초기화 작업이 필요한 경우에 유용
-> deep copy나 shallow copy를 통해 객체를 복사.- 객체를 복사하여 생성
- 새로운 객체를 직접 생성하지 않고 기존 객체를 복제하여 사용
- 성능 최적화에 유리
비교:
- 싱글턴은 인스턴스를 하나만 생성하고 공유, 프로토타입은 기존 객체를 복제하여 새로운 객체를 생성.
- 싱글턴은 메모리 절약에 초점, 프로토타입은 객체 복사에 따른 성능 최적화와 복잡한 객체의 효율적인 생성에 중점.
1.4. 의존성 주입 (Dependency Injection)
- 클래스 내부에서 필요한 객체를 직접 생성하는 대신, 외부에서 필요한 객체를 전달받는 방식.
- 객체들 간의 결합도를 낮추고, 테스트와 확장성을 용이하게 만듬.
- 객체 간의 의존성을 낮추고, 유지보수성을 높임
- 객체 생성과 관리가 외부 컨테이너에서 이루어짐
- 유닛 테스트에서 쉽게 모킹(Mock)할 수 있음
비교:
- 싱글턴은 전역적으로 하나의 인스턴스만 관리, DI는 객체의 생성과 사용을 분리하여 더 유연한 구조를 제공.
- 싱글턴은 전역적 접근에 유리, DI는 결합도를 낮추고 테스트와 확장성을 높이는 데 유리.
1.5. 퍼사드 패턴 (Facade Pattern)
- 퍼사드 패턴(Facade Pattern)은 복잡한 시스템이나 서브시스템을 단순한 인터페이스로 감싸 사용자가 복잡성을 느끼지 않고 쉽게 사용 가능하게 함.
- 즉, 복잡한 로직이나 서브시스템이 내부에서 작동, 외부에는 단순화된 인터페이스를 제공.
- 복잡한 서브시스템을 단순화된 인터페이스로 감싸서 제공
- 시스템 간의 의존성을 줄이고, 유지보수성을 높임
- 사용자는 복잡한 내부 로직을 알 필요 없이 간단하게 접근 가능
비교:
- 싱글턴은 객체의 유일성을 보장, 퍼사드는 복잡한 시스템을 간단하게 제공하는 데 초점.
- 싱글턴은 객체 관리에 관한 문제를 해결, 퍼사드는 사용자의 편리성을 제공하기 위해 복잡한 시스템을 감춤.
5. 테스트 및 유지보수
5.1. 싱글턴 테스트 전략
- 전역 상태를 유지 -> 유닛 테스트에서 객체의 상태를 독립적으로 유지하는 것이 어렵다는 점
5.1.1. 싱글턴 인스턴스 초기화
- 테스트 환경에서 인스턴스를 초기화하거나, 재설정할 수 있는 방법을 제공해야 합니다.
- 싱글턴 인스턴스가 테스트 간에 공유되지 않도록, 필요한 경우 인스턴스를 초기화할 수 있어야 합니다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
// 테스트에서만 사용할 수 있는 reset 메서드 추가
public static void resetInstance() {
instance = null;
}
}
5.1.2. Mocking을 활용한 테스트
- 의존성을 주입하는 방식으로 싱글턴 인스턴스를 Mocking하여, 테스트가 독립적으로 수행되도록 만들 수 있습니다.
- 이를 위해 Mockito 같은 라이브러리를 사용.
- ex) 싱글턴 객체의 메서드를 Mock으로 대체하여, 상태에 의존하지 않는 유닛 테스트를 진행.
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
public class SingletonTest {
@Test
public void testSingletonMethod() {
Singleton singleton = mock(Singleton.class);
when(singleton.someMethod()).thenReturn("Mocked Result");
assertEquals("Mocked Result", singleton.someMethod());
}
}
c. 의존성 주입 (Dependency Injection)
싱글턴 패턴을 사용하더라도, 의존성 주입(DI)을 통해 싱글턴 객체를 테스트할 수 있습니다. 의존성 주입을 활용하면 싱글턴 객체의 인스턴스를 외부에서 주입할 수 있기 때문에, 테스트 시 다른 구현체나 목(Mock) 객체를 사용할 수 있습니다.
5.2. 싱글턴 유지보수
5.1.1. 싱글턴 확장성 문제
- 앱이 커지거나 복잡해질수록, 전역 상태를 관리하는 싱글턴 클래스는 여러 모듈 간 의존성을 일으킬 수 있습니다.
- 이는 코드 수정 시 많은 모듈에 영향을 미치므로 유지보수가 어렵습니다.
해결 방법:
- 의존성 주입을 통해 싱글턴 객체의 결합도를 줄이고, 모듈화된 코드베이스를 유지할 수 있습니다.
- 인터페이스 기반의 설계로 구현 세부 사항을 감추고, 객체 간 결합도를 낮춥니다.
public interface Service {
void perform();
}
public class SingletonService implements Service {
private static SingletonService instance;
private SingletonService() {}
public static SingletonService getInstance() {
if (instance == null) {
instance = new SingletonService();
}
return instance;
}
@Override
public void perform() {
// 서비스 로직
}
}
5.2.2. 리소스 관리 문제
- 싱글턴 인스턴스는 애플리케이션의 생명 주기 동안 메모리에 상주할 수 있습니다.
-> 데이터베이스 연결, 파일 핸들링 등 리소스 관리가 필요한 경우, 적절한 시점에 자원을 해제하지 않으면 메모리 누수 또는 리소스 고갈 문제가 발생.
해결 방법:
- 자원 해제 메서드를 추가하고, 애플리케이션 종료 시 자원을 명시적으로 해제해야 합니다.
- **try-with-resources**나 finally 블록을 사용하여 자원 관리를 철저히 합니다.
public class SingletonService {
private static SingletonService instance;
private Connection dbConnection;
private SingletonService() {
dbConnection = // Initialize DB connection;
}
public static SingletonService getInstance() {
if (instance == null) {
instance = new SingletonService();
}
return instance;
}
// 자원 해제 메서드
public void close() {
if (dbConnection != null) {
dbConnection.close();
}
}
}
c. 멀티스레드 환경에서의 안전성 유지
- 싱글턴 인스턴스를 멀티스레드 환경에서 안전하게 사용하는 것이 매우 중요합니다.
- 멀티스레드 환경에서 여러 스레드가 동시에 인스턴스를 생성하려고 하면 여러 개의 인스턴스가 생성될 수 있습니다.
해결 방법:
- 이른 초기화나 **DCL(Double-Checked Locking)**을 사용하여 스레드 안전성을 확보할 수 있습니다.
- volatile 키워드를 사용하여 인스턴스 변수에 대한 스레드 간의 가시성을 보장합니다.
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
6. 싱글턴 패턴 사용 시점
싱글턴 패턴을 사용하는 시점은 특정 객체의 인스턴스를 하나로만 유지해야 할 때입니다. 이 패턴을 사용하면 애플리케이션 전반에서 동일한 인스턴스를 공유하며 사용할 수 있게 해줍니다. 다음은 싱글턴 패턴을 사용하는 적절한 시점과 그 이유입니다.
6.1. 리소스를 공유해야 할 때
데이터베이스 연결, 설정 파일, 로그 기록기 등 리소스가 많이 소모되는 객체를 하나만 생성하고 모든 부분에서 공유해야 할 때 싱글턴 패턴이 적합합니다. 이러한 리소스들은 중복 생성되면 성능에 문제가 생기거나, 리소스 관리가 어려워질 수 있습니다.
- 예시: 데이터베이스 연결 풀, 파일 시스템 접근, 네트워크 소켓, 설정 파일 로딩 등.
6.2. 전역 상태를 관리해야 할 때
애플리케이션 전체에서 동일한 상태를 공유해야 하는 경우에 싱글턴 패턴을 사용할 수 있습니다. 이렇게 하면 객체 상태가 전역적으로 공유되며 일관성을 유지할 수 있습니다.
- 예시: 애플리케이션 전역 설정, 구성 정보, 캐싱 메커니즘 등.
6.3. 상호 배타적 리소스 접근이 필요할 때
멀티스레드 환경에서 동기화된 리소스 접근이 필요할 때 싱글턴 패턴을 활용할 수 있습니다. 예를 들어, 파일 쓰기나 로그 기록을 여러 스레드가 동시에 수행할 경우 문제가 발생할 수 있는데, 싱글턴 패턴을 사용하면 이러한 리소스를 안전하게 관리할 수 있습니다.
- 예시: 로깅 시스템, 스레드 풀, 캐싱 시스템.
6.4. 애플리케이션의 라이프사이클과 일치하는 객체가 필요할 때
특정 객체는 애플리케이션 시작 시점에 생성되어, 애플리케이션이 종료될 때까지 존재해야 하는 경우가 있습니다. 이러한 경우 싱글턴 패턴을 사용해 해당 객체를 애플리케이션 전반에 걸쳐 유지할 수 있습니다.
- 예시: 애플리케이션 전체에서 관리해야 하는 이벤트 관리자나 서비스 레이어 객체.
6.5. 어디서나 접근이 필요한 객체
애플리케이션에서 하나의 객체가 여러 모듈, 클래스 또는 서비스에서 필요할 때 싱글턴 패턴을 사용하면 어디서나 동일한 인스턴스에 쉽게 접근할 수 있습니다. 이로 인해 클래스 간의 결합도를 줄이고 코드의 재사용성을 높일 수 있습니다.
- 예시: 중앙 관리 객체, 서비스 레지스트리, 팩토리 클래스 등.
'CS 스터디' 카테고리의 다른 글
CS스터디용 이미지1 (0) | 2024.10.31 |
---|---|
TCP와 UDP의 차이점 (0) | 2024.04.15 |
웹 사이트 접속하는 과정 (0) | 2024.04.15 |