본문 바로가기

CS 스터디

싱글톤 (SingleTon Pattern)

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. 싱글턴 장단점

장점

  1. 인스턴스의 유일성 보장 :  시스템 전역에서 하나의 인스턴스를 공유 가능 -> 자원의 낭비 X.
  2. 전역 접근 : 어디서나 인스턴스에 접근 가능 -> 여러 컴포넌트 간 데이터를 쉽게 공유 가능.
  3. 메모리 절약 : 인스턴스를 하나만 생성 -> 메모리 사용량 Down.

단점

  1. 테스트 어려움: 싱글턴 인스턴스는 전역적으로 사용 -> 테스트할 때 격리가 어려움.
  2. 병목 현상: 하나의 인스턴스만 사용 -> 멀티스레드 환경에서 동기화 처리를 잘 하지 않으면 병목 현상이 발생.
  3. 유연성 부족: 클래스 설계 시 싱글턴으로 설정 -> 나중에 여러 인스턴스를 생성해야 하는 경우 유연성이 떨어짐.

 

4. 싱글턴 패턴과 다른 디자인 패턴 비교

1.1 팩토리 패턴

  • 객체 생성을 클래스에 직접적으로 의존하지 않고, 팩토리(생산자) 메서드를 통해 객체를 생성하는 방식.
  • 객체 생성의 책임을 팩토리 클래스에 넘김으로써 코드의 유연성을 높이고, 객체 생성 로직을 중앙에서 관리.
    • 다양한 하위 클래스에 대한 인스턴스를 생성할 수 있는 유연성
    • 객체 생성의 로직을 숨길 수 있음
    • 유지보수와 코드 확장성이 용이

비교

  • 싱글턴은 단 하나의 인스턴스만을 보장하고, 팩토리는 여러 종류의 객체를 동적으로 생성할 수 있습니다.
  • 싱글턴은 전역적인 리소스 관리에 사용, 팩토리는 다양한 하위 클래스의 인스턴스 생성을 간단하게 처리할 때 사용.

1.2. 빌더 패턴

  • 빌더 패턴은 복잡한 객체를 단계적으로 생성.
  • 객체 생성 시 많은 매개변수가 필요한 경우, 각 단계에서 객체를 구성하는 방식을 세분화 가능.
  • 이를 통해 코드의 가독성을 높이고, 객체 생성 중의 변경 가능성을 줄일 수 있음.
    • 복잡한 객체를 단계별로 생성
    • 각 단계에서 유연한 조립과 설정이 가능
    • 필드가 많은 클래스에 유용 (특히 불변 객체 생성)

비교:

  • 싱글턴은 하나의 인스턴스를 관리하는 데 초점이 있고, 빌더는 복잡한 객체를 보다 간결하게 생성하는 데 중점.
  • 싱글턴은 객체가 단일성을 보장하는 데 사용되고, 빌더는 객체 생성 중에 유연성을 제공합니다.

1.3. 프로토타입 패턴

  • 프로토타입 패턴기존 객체를 복제하여 새로운 객체를 생성하는 방식. ->특정 객체를 복제하는 것이 필요할 때 사용
  • 이 방식은 객체를 생성하는 데 복잡한 초기화 작업이 필요한 경우에 유용
    -> deep copyshallow 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