싱글 톤 패턴

💡 학습 목표
    1. 싱글톤 패턴에 대해서 알아보고 직접 코드를 작성할 수 있다.
    2. 싱글톤 패턴을 언제 어떻게 활용할 수 있는지 말할 수 있다.

0. 사전기반 지식

  • 클래스와 객체: 자바에서 클래스는 객체를 생성하기 위한 틀이다. 일반적으로 하나의 클래스에서 여러 객체를 생성할 수 있지만, 싱글톤 패턴은 단 하나의 객체만을 보장한다.
  • 생성자(Constructor): 객체가 생성될 때 호출되는 메서드로, 싱글톤 패턴에서는 생성자를 외부에서 호출하지 못하도록 제한한다.
  • 정적 변수와 메서드: 싱글톤 패턴에서 주로 사용되는 정적(static) 멤버에 대한 이해가 필요하다.

1. 싱글톤 패턴 개념

싱글톤 패턴은 특정 클래스의 인스턴스를 하나만 생성하고, 이를 전역적으로 접근할 수 있도록 하는 디자인 패턴이다. 주요 목적은 시스템 전체에서 공통된 리소스를 공유하거나, 객체 생성을 제한하여 메모리 낭비를 줄이기 위함이다.

package singleton;

// 1. 정적 변수를 선언한다. 
// 2. private 생성자를 선언한다. 
// 3. 외부에서 접근할 수 있는 public 정적 메서드를 선언한다. 
public class SingleTon {

    // 1. 유일한 인스턴스를 저장할 변수를 선언한다. (private, static)
    private static SingleTon instance; 

    // 2. 외부에서 객체를 생성 못 하도록 private 생성자를 선언한다. 
    private SingleTon() {}

    // 3. 외부에서 인스턴스 주소를 반환 받을 수 있는 메서드를 선언한다. 
    // 심화 : 멀티 스레스 환경에서 안정하게 싱글톤 패턴을 구현 하기 위해서는 
    public static synchronized SingleTon getInstance() { // 동기화 처리 
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }

}
package singleton;

public class MainTest {

    public static void main(String[] args) {
        // 싱글톤 객체를 불러 와보자. !! 
        SingleTon systemSingleTon1 = SingleTon.getInstance();
        SingleTon systemSingleTon2 = SingleTon.getInstance();

        if (systemSingleTon1 == systemSingleTon2) {
            System.out.println("같은 객체를 바라 보고 있습니다.");
        }

    }
}

2. 활용 예시

  • 로깅 시스템: 여러 클래스에서 동일한 로깅 인스턴스를 사용하여 로그를 기록한다.
  • 설정 관리(Configuration): 시스템의 설정 값을 저장하고, 이를 전역적으로 사용해야 하는 경우 싱글톤을 활용한다

도전 과제 : Connection 객체를 반환하는 클래스를 설계 한다.
(단 멀티 스레드환경에 안전한 객체를 생성할 수 있도록 코드 작성)

public class DatabaseConnection {
    // 데이터베이스 연결 코드
    // 실제 데이터베이스 연결 객체 (예: JDBC Connection)
    private Connection connection;
    // 데이터베이스 URL, 사용자명, 비밀번호와 같은 설정
    // JDBC 드라이버 로드
    // Class.forName("com.mysql.cj.jdbc.Driver");
 
}
package singleton;

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

public class DatabaseConnection {
   // 데이터베이스 연결 코드
   // 실제 데이터베이스 연결 객체 (예: JDBC Connection)
   private static DatabaseConnection instance;
   private Connection connection;
   // 데이터베이스 URL, 사용자명, 비밀번호와 같은 설정
   // JDBC 드라이버 로드
   // Class.forName("com.mysql.cj.jdbc.Driver");

   private static final String URL = "~~~";
   private static final String USERNAME = "root";
   private static final String PASSWORD = "asd123";

   static {
      try {
         Class.forName("com.mysql.cj.jdbc.Driver");
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }
   }

   public void connect() {
      try {
         connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
      } catch (SQLException e) {
         e.printStackTrace();
      }
   }

   public void disconnect() {
      if (connection != null) {
         try {
            connection.close();
         } catch (SQLException e) {
            e.printStackTrace();
         }
      }
   }

   public Connection getConnection() {
      if (connection == null) {
         connect();
      }
      return connection;
   }

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

}

3. 핵심 요약

  • 싱글톤 패턴은 특정 클래스의 인스턴스를 하나만 유지하는 디자인 패턴이다.
  • 주로 공통된 리소스를 관리하거나, 전역 상태를 관리하는 데 사용된다.
  • 장점: 메모리 절약, 전역적으로 동일한 인스턴스 사용
  • 단점: 전역 상태로 인한 의존성 증가, 멀티스레드 환경에서의 동기화 이슈
(도전 과제) - 풀링 기법에 대해 알아 보자 (! 싱글톤 패턴 개념이 아님)

 

객체 10개만 미리 생성해 두기

 

객체 풀링(Object Pooling)

자원을 효율적으로 관리하기 위한 디자인 패턴 중 하나이다. 주로 객체 생성 비용이 크거나, 제한된 자원을 사용할 때 객체의 재사용을 통해 성능을 향상시키는 데 사용된다. 객체 풀링의 핵심 개념은 여러 개의 객체를 미리 생성해 두고, 필요할 때 그 객체들을 재사용하는 것이다.

package singleton;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class CustomConnection {
   int num;
   Connection connection;

   public CustomConnection(int num, Connection connection) {
      this.num = num;
      this.connection = connection;
   }
}

public class DatabaseConnectionPool {

   private static DatabaseConnectionPool instance;

   private static final int POOL_SIZE = 10;
   private BlockingQueue<CustomConnection> connectionPool;

   private static final String URL = "jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Seoul";
   private static final String USERNAME = "root";
   private static final String PASSWORD = "asd123";

   static {
      try {
         Class.forName("com.mysql.cj.jdbc.Driver");
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }
   }

   private DatabaseConnectionPool() {
      connectionPool = new ArrayBlockingQueue<>(POOL_SIZE);
      for (int i = 0; i < POOL_SIZE; i++) {
         try {
            Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            connectionPool.add(new CustomConnection(i + 1, connection));
         } catch (SQLException e) {
            e.printStackTrace();
         }
      }
   }

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

   // 커넥션 풀에서 커넥션을 가져옴
   public CustomConnection getConnection(int threadNumber) {
      try {
         if (connectionPool.isEmpty()) {
            System.out.println(threadNumber + "번 쓰레드가 커넥션 대기 중<<<<<<<<<<");
         }
         CustomConnection customConnection = connectionPool.take(); // 큐에서 커넥션 가져옴 (없으면 대기)
         return customConnection;
      } catch (InterruptedException e) {
         e.printStackTrace();
         return null;
      }
   }

   // 사용한 커넥션을 다시 풀에 반환
   public void releaseConnection(CustomConnection customConnection) {
      if (customConnection != null) {
         try {
            connectionPool.put(customConnection); // 큐에 커넥션 반환
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }

   // 모든 커넥션을 종료
   public void shutdown() {
      for (CustomConnection customConnection : connectionPool) {
         if (customConnection != null) {
            try {
               customConnection.connection.close();
            } catch (SQLException e) {
               e.printStackTrace();
            }
         }
      }
   }

   public static void main(String[] args) {
      DatabaseConnectionPool pool = DatabaseConnectionPool.getInstance();
      for (int i = 1; i <= 20; i++) {
         int threadNumber = i;
         new Thread(() -> {
            while (true) {
               CustomConnection connection = pool.getConnection(threadNumber);
               System.out.println(threadNumber + "번 쓰레드가 " + connection.num + "번 커넥션 가져옴!!-------------------------------------------------");
               try {
                  Thread.sleep(300);
               } catch (InterruptedException e) {
                  e.printStackTrace();
               }
               pool.releaseConnection(connection);
               System.out.println(threadNumber + "번 쓰레드가 " + connection.num + "번 커넥션 반환!!");
            }
         }).start();
      }
   }
}

디자인 패턴 목차로 돌아가기

 

'Java > 디자인 패턴' 카테고리의 다른 글

팩토리 패턴  (0) 2024.09.26
빌더 패턴이란?  (6) 2024.09.26
콜백 메서드 만들어 보기 (1)  (0) 2024.09.25
S.O.L.I.D 원칙  (1) 2024.09.25
템플릿 메서드 패턴  (0) 2024.04.30