싱글톤(Singleton) 패턴

2023. 5. 9. 17:06CS/디자인 패턴

728x90

2023.05.09 - [디자인 패턴] - 소프트웨어 위기를 극복한 디자인 패턴의 탄생과 발전

 

소프트웨어 위기를 극복한 디자인 패턴의 탄생과 발전

먼저 디자인 패턴의 정의부터 바로 알아보겠습니다! 디자인 패턴은 S/W 설계에서 자주 발생하는 문제들을 해결하기 위해 사용되는 재사용 가능한 해결책이다. 아마 이 글을 읽으시는 분들 중 디

xpmxf4.tistory.com

해당 포스팅을 읽기 전 위 글을 읽고 오시는 것을 추천드려요!


위 글에서 디자인 패턴이 무엇인지, 그 필요성에 대해 알게 됐다면

이제는 본격적으로 디자인 패턴 즉 소프트웨어 솔루션들에 각 종류에 대해 알아보겠습니다!

 

디자인 패턴은 크게 3 가지 유형으로 나눠집니다!

      1. 생성 패턴
        객체 생성에 관련된 패턴으로, 객체를 생성하는 방법 시점 결정하는 도움이 되는 방법론입니다.
        생성 패턴은 객체의 생성 과정을 캡슐화하여, 객체 생성을 단순화하고 유연성 높입니다.
      2. 구조 패턴
        클래스와 객체를 조합하여 구조 만드는 패턴입니다.
        구조 패턴은 구성 요소들 사이의 관계를 정의하고, 유연한 구조를 만들어 코드의 재사용성을 높입니다.
      3. 행동 패턴
        객체 간의 커뮤니케이션과 책임
        분배 관련된 패턴입니다.
        행동 패턴은 객체들이 함께 작업하는 방법 정의하고, 객체 간의 상호작용을 최적화하여
        명확하고 유연한 코드를 작성할 있게 돕습니다.

이게 뭔 개소x야

저 설명들만 본다면 위 짤과 같이 말을 하고 있는

독자분들의 무수한 야유가 당연하다고 생각합니다.

 

지금은 저 3가지 패턴들에 대해 "이렇게 3가지가 있나 보구나~" 하고 넘기시고,

각각의 패턴들에 대해 배울 때 해당 패턴의 유형을 함께 보며 

왜 그 유형에 속하는 지 생각해보면 충분합니다!

싱글톤(Singleton) 패턴

먼저 싱글톤 패턴에 대해 간단히 알아보겠습니다!

유형

생성 패턴

정의

하나의 클래스에 오직 하나의 인스턴스만 존재하도록 하는 패턴.

장점

  1. 전역 인스턴스 제어
    전역적으로 접근 가능하고 제어가 가능하다.
  2. 메모리 효율
    인스턴스를 한번만 생성하는 패턴이기에, 메모리의 사용률이 내려간다!
  3. 리소스 공유
    싱글톤 인스턴스로 다양한 리소스를 공유하거나 상태를 관리하게 된다.

단점

  1. 전역 상태 사용 -> 클래스 간 결합도, 의존성 발생때문에 테스트 어려움
  2. 동시성 문제 가능성
    동시성 문제의 가능성 때문에, 이를 고려한 추가적인 솔루션을 생각해야 한다.

예시 1 - 데이터베이스와 동시성 문제)

여기까지 잠깐 한숨 돌릴 까요?

(숨쉬기 운동 사진)

이 타이밍에 쉰 이유는 위에 알 수 없는 얘기들이 잔뜩 있었기 때문입니다 ㅎㅎ

 

위 장단점들을 제대로 이해하기 위해선 예시를 통해 봐야

이해가 제대로 가기 때문이죠.

그럼 본격적 설명 드가자~

싱글턴 패턴의 리소스 공유 라는 장점은

여러 객체가 동일한 리소스에 접근하거나 조작할 필요가 있을 때 유용합니다.

또한 리소스 사용의 일관성 유지, 중앙 집중형 관리가 가능해지기 때문이죠.

 

싱글톤 패턴을 사용하는 단적인 예시가 바로 데이터베이스 연결입니다.

 

여러 객체가 동일한 데이터베이스에 접근해야 하는데, 

데이트베이스 연결은 리소스를 많이 사용하므로

한정된 수의 연결만 생성하는 것이 훨씬 자원을 효율적으로 사용할 수 있습니다.

 

이런 경우 싱글턴 패턴을 사용하여 데이터베이스 연결 관리자를 구현하게 되죠.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseConnectionManager {
    private static DatabaseConnectionManager instance;
    private Connection connection;

    private DatabaseConnectionManager() {
        // 비어있는 생성자를 private로 선언하여 외부에서 인스턴스 생성을 방지
    }

    public static DatabaseConnectionManager getInstance() {
        if (instance == null) {
            instance = new DatabaseConnectionManager();
        }
        return instance;
    }

    public Connection getConnection() {
        return connection;
    }

    public void connect(String url, String username, String password) throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = DriverManager.getConnection(url, username, password);
        }
    }

    public void disconnect() throws SQLException {
        if (connection != null && !connection.isClosed()) {
            connection.close();
        }
    }
}

위 코드가 바로 싱글톤 패턴을 사용한 DB 연결 관리자를 구현한 형태입니다.

 

하지만 위 코드는 단점 중 하나가 고려가 되어 있지 않는 코드입니다.

바로 동시성 문제 입니다.

 

위에서도 얘기했다시피 싱글톤 패턴의 문제점은

동시성 문제를 야기할 수 있다 나왔었죠!

위 코드에서는 여러 객체가 마음대로 getInstance(), connect(), disconnect() 를 사용할 수 있습니다.

즉, 멀티 스레드 환경에서 여러 스레드가 동시에 DB 에 접근하는 경우가 발생하죠.

 

그래서 자바에서는 synchronized 키워드를 사용해

예시에서의 싱글턴 인스턴스인 DB 에 한 번에 하나의 스레드만 메서드를 실행할 수 있게 해야 합니다.

 

그러면 다음과 같이 수정할 수 있습니다.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DatabaseConnectionManager {
    private static DatabaseConnectionManager instance;
    private Connection connection;

    private DatabaseConnectionManager() {
        // 비어있는 생성자를 private로 선언하여 외부에서 인스턴스 생성을 방지
    }

	// 동기화
    public static synchronized DatabaseConnectionManager getInstance() {
        if (instance == null) {
            instance = new DatabaseConnectionManager();
        }
        return instance;
    }

    public Connection getConnection() {
        return connection;
    }

	// 동기화
    public synchronized void connect(String url, String username, String password) throws SQLException {
        if (connection == null || connection.isClosed()) {
            connection = DriverManager.getConnection(url, username, password);
        }
    }

	// 동기화
    public synchronized void disconnect() throws SQLException {
        if (connection != null && !connection.isClosed()) {
            connection.close();
        }
    }
}

 

예시2 - 전역 상태와 DI )

위에서 나온 싱글톤 패턴의 단점 중 하나는

바로 클래스간 의존성 발생 및 결합도 증가입니다.

 

싱글턴 패턴의 DatabaseConnectionManager 사용하는 UserService 클래스가 있다고 가정해보겠습니다.

// Main.java
public class Main {
    public static void main(String[] args) {
        // UserService 생성 시, 생성자에 주입하지 않음
        UserService userService = new UserService();
        userService.performSomeDatabaseOperation();
    }
}

// UserService.java
public class UserService {
    private final DatabaseConnectionManager connectionManager;

    public UserService() {
        // 직접 싱글턴 인스턴스를 가져옴
        this.connectionManager = DatabaseConnectionManager.getInstance();
    }

    public void performSomeDatabaseOperation() {
        // connectionManager를 사용한 데이터베이스 작업
    }
}

현재 UserService 클래스는 내부에서 직접 DatabaseConnectionManager 의 싱글톤 인스턴스를 호출하고 있습니다.

 

하지만 우리가 이렇게 DB 의 연결을 해야 하는 모듈을 테스트 할 때는

실제 데이터베이스 인스턴스에 연결하기 보단, 가짜 데이터베이스에 연결해 테스트를 해야 하는데

위 코드의 경우는 이게 어렵죠.

즉, 싱글턴 인스턴스를 직접 가져오게 되어 두 클래스간의 결합도가 높아져 테스트를 하기 어려운 코드입니다.

 

테스트는 개발에 있어서 정말 중요한 부분이기도 하기 때문에,

싱글톤 패턴의 리소스 효율이라는 장점을 가져가면서도 테스트를 진행해야 하는 방법이 필요하게 됩니다.

 

DI (Dependency Injection)

그래서 필요한 방법이 바로 DI 입니다.

의존성 주입(dependency injection)은 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다.

"의존성"은 예를 들어 서비스로 사용할 수 있는 객체이다.
클라이언트가 어떤 서비스를 사용할 것인지 지정하는 대신, 클라이언트에게 무슨 서비스를 사용할 것인지를 말해주는 것이다.
[출처] : https://ko.wikipedia.org/wiki/%EC%9D%98%EC%A1%B4%EC%84%B1_%EC%A3%BC%EC%9E%85

즉, 우리가 봤던 예시에 빗대어 설명한다면

데이터베이스 싱글톤 인스턴스를 외부에서 주입하는 객체를 만들어

클래스 간의 의존성을 낮추는 방법입니다!

 

// Main.java
public class Main {
    public static void main(String[] args) {
        // 싱글턴 인스턴스를 생성자에 주입하여 의존성을 관리
        DatabaseConnectionManager connectionManager = DatabaseConnectionManager.getInstance();
        UserService userService = new UserService(connectionManager);
        userService.performSomeDatabaseOperation();
    }
}

// UserService.java
public class UserService {
    private final DatabaseConnectionManager connectionManager;

    // 생성자를 통해 의존성 주입
    public UserService(DatabaseConnectionManager connectionManager) {
        this.connectionManager = connectionManager;
    }

    public void performSomeDatabaseOperation() {
        // connectionManager를 사용한 데이터베이스 작업
    }
}

 

요약

자세하게 설명하려다 보니 글이 길어졌네요! 다음 3가지로 싱글톤 패턴을 기억해주세요!

  1. 싱글턴 패턴은 객체를 하나만 생성하여 전역적으로 사용하고자 할 때 적용되는 디자인 패턴입니다.
  2. 싱글턴 패턴의 장점은 리소스 공유와 인스턴스 생성 횟수의 제한 등이 있으나,
    전역 상태로 인한 결합도 증가와 테스트 어려움 등의 단점이 있습니다.
  3. 의존성 주입(Dependency Injection, DI)을 사용하면 싱글턴 패턴의 단점을 해결할 수 있으며, 객체 간의 결합도를 낮추고 유연성 및 테스트 용이성을 향상시킬 수 있습니다.
728x90