싱글톤?

기르기르·2022년 9월 26일
1
post-thumbnail

사실 가벼운 마음으로 강의에서 배웠던 DTO, DAO를 이용한 포스팅을 하려고 하였으나, Singleton은 단점이 명확해서 배운 것 외에도 새로 알게된 사실이 많았다. 그래서 그냥 내가 새로이 안 사실들을 포함해서 이론적으로 서술하려고 한다.

Singleton이란?

오직 하나의 객체만 생성하여 사용할 수 있는 인스턴스를 말한다. 클래스 전역에서 이 인스턴스를 공유하여 사용할 수 있기때문에 여러 곳에서 공통된 내용의 객체를 다수 생성해서 사용할 때 Singleton으로 만들어 사용한다.

왜 Singleton이 필요할까?

내가 배운 연락처 관리 프로그램 구현을 예로 들어 설명하자면 데이터 베이스 접근을 위해서 사용해야할 공통된 요소가 존재한다.

	private Connection con;				// DB접속
	private PreparedStatement ps;		// 쿼리문 실행
	private ResultSet rs;				// SELECT 결과
	private String sql;					// 쿼리문
	private int result;					// INSERT, UPDATE, DELETE 결과 반환

필드들과

	public Connection getConnection() throws Exception {

	Class.forName("oracle.jdbc.OracleDriver");
		
	Properties p = new Properties();
	p.load(new FileReader("db.properties"));
		String url = p.getProperty("url");
		String user = p.getProperty("user");
		String password = p.getProperty("password");
		return DriverManager.getConnection(url, user, password);
	}

oracle과 접속하기 위한 메소드

  public void close() {
        try {
           	  if(rs != null) rs.close();
              if(ps != null) ps.close();
              if(con != null) con.close();
          } catch(Exception e) {
              e.printStackTrace();
          }
      }

그리고 oracle과의 연결을 닫아주기 위한 메소드가 있다.

이 많은 코드들을 매 클래스마다 새로 작성하고 또 작성한다면 시간의 문제도 문제거니와 공통된 내용의 다중 작성으로인한 메모리 낭비 문제도 클것이다. 또, 만약 메소드 내용을 수정 할 일이 생긴다면 그 수 많은 메소드들을 일일이 찾아서 고쳐줘야한다. 그러다보면 휴먼에러가 일어날 가능성이 다분한 것이다. 그런 불필요한 낭비를 방지하고자 Singleton 패턴을 사용한다.

❕Singleton을 사용해야하는 이유 요약

1) 메모리 관리
2) 쉬운 접근성, 데이터 공유
3) 그로인한 속도 향상

Singleton은 이런 편리한 이점이 있는 반면에 단점 또한 두드러지게 나타난다.
바로 객체지향설계의 개방-폐쇄 원칙 위배, 멀티 스레드 환경에서 동시 호출되었을 때 유일성을 잃는 것 이다.
전자는 너무 많은 곳에서 사용될 경우인데 다른 클래스들과의 결합도가 높아져 수정이 어려워지고 유지 보수 비용이 커지는 단점을 이야기 한다.
후자는 두 가지의 경우를 이야기할 수 있는데
첫째는 Multi-thread 환경에서 instance가 아직 존재하지 않을 때 여러 곳에서 동시에 singleton을 호출할 경우이다.
이 경우 미처 생성되기 전에 호출 되어 미생성되었다고 인식하여 각자 새로운 instance로 생성될 가능성이 있다.
두 번째는 Multi-thread 환경에서 동시에 plusCount()를 실행할 때이다.
이 또한 실행된 pluscount가 끝마치기 전에 반복 실행이되어 count가 제대로 실행되지 않을 가능성이 있다.

❌Singleton을 사용할 때 일어나는 문제 점 요약

1) 객체지향설계의 개방-폐쇄 원칙 위배
2) 멀티 스레드 환경에서 동시 호출되었을 때

가장 좋은 예방 방법은 Singleton을 사용하지 않는 것이지만 사용되는 경우가 있기에 이를 극복하기 위한 다양한 구현 방법이 존재한다.

Singleton의 다향한 구현 방법

Singleton은 외부에서 여러 객체를 만들 수 없도록 private로 생성자를 만들고 getInstance()라는 메소드를 이용해서 외부에서 불러온다. 기본적인 형태는 이렇다.


	private static Singleton singletonA = new Singleton();
    
	private Singleton() {
	}
	
	public static Singleton getInstance() {
		return singletonA;
	}

Lazy Initialization(지연 초기화)

Singleton.getInstance() 메서드 호출 시점에 정적 필드가 아직 초기화되지 않았으면 객체를 생성하는 방법이다.

	public static Singleton getInstance() {
   	 if (singletonA == null) {  
           singletonA = new Singleton();  
       }  
		return singletonA;
	}

하지만 이 구현 법은 다중 스레드(Multi-thread)에서 2번째 문제를 일으킬 가능성이 있다.

Synchronized 키워드-동기화

다중 스레드의 동기화는 2번째 문제를 막기 위해 사용할 수 있는 방법이다. 이것은 Java에서 제공하는 키워드로 한 번에 하나의 스레드만 접근할 수 있도록 해주는 방식이다.

	public static synchronized Singleton getInstance() {
   	 if (singletonA == null) {  
           singletonA = new Singleton();  
       }  
		return singletonA;
	}

아쉽지만 이 방법도 성능 저하를 발생시키기때문에 권장하지 않는 방법이고 남발해서는 안된다.

Double-Checked Locking, volatile

위에서 일어나는 성능 저하를 막기 위해서 두 번에 걸쳐 검사를 하도록 구현되는 방식이다. SynchronizedDouble-Checked Locking 둘 다 getInstance() 메소드에서 수정되는 방식이다.

	public static Singleton getInstance() {
		if (singletonA == null) {
			synchronized (Singleton.class) {
				if (singletonA == null) {
					singletonA = new Singleton();
				}
			}
		}
		return singletonA;
	}

동기화의 오버헤드를 막아서 성능적으로 이점이 있지만 이 또한 멀티 스레드에서 객체의 초기화가 완전히 끝나기 전에 다른 스레드가 실행되어서 null이 아님을 확인하고 초기화 중인 객체 참조를 그대로 반환하는 경우가 생길 수 있다. 동기화 블록 내부에서 컴파일러가 최적화를 위해 재배열할 수 있기때문이다.
이러한 것을 방지하기 위해서 singleton 변수를 volatile로 선언해준다. volatile에 관해서는 따로 게시글을 작성할 예정이기에 여기서는 기술하지 않고 volatile선언까지 완료된 코드만 작성하면

	public class Singleton {  
      private static volatile Singleton singletonA;

      private Singleton() { }  

      public static Singleton getInstance() {
          if (singletonA == null) {
              synchronized (Singleton.class) {
                  if (singletonA == null) {
                      singletonA = new Singleton();
                  }
              }
          }
          return singletonA;
       }
	}

이러한 모습이 된다.
아주 슬프게도 이 방법 또한 문제점이 있다. 프로그램의 구조가 복잡해지고 비용이 커지며 코드가 부정확하게 작성될 가능성이 있다는 것이다. 그로인해 이번에는 JVM에게 Singleton의 초기화 문제를 떠넘기는 방식을 기술한다.

INITIALIZATION-ON-DEMAND HOLDER PATTERN(요청시 초기화)

클래스 안에 클래스(holder)를 두는 방식으로 더블 체크 락킹보다 더 단순하며 안전하다. 그리고 실제로 가장 많이 사용되는 일반적인 singleton 방식이라고 한다.

	public class Singleton {  
 
    private Singleton() { }
    
    private static final class Holder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    
    public static Singleton getInstance() {  
        return Holder.INSTANCE;  
    }
}

Holder 클래스에서 선언된 INSTANCEgetInstance()에서 실행되기 전 클래스가 로딩되면서 초기화가 되는데 static final로 선언되어 있기 때문에 단 한번만 초기화되어 호출된다. 다시 값이 할당되지 않도록 만드는 방식인 것이다.

Singleton은..💦

인강이나 학원에서 배울 때에 한 줄 요약, 혹은 이런거다 라는 느낌으로 별거 없는 내용인줄 알았는데 생각보다 무거운 이야기였다. 솔직히 배우면서 이게 어디다 쓰이는거야 싶은게 많았는데 이렇게 벨로그 작성한다고 따로 찾아보니 이렇게 활용되는구나 이런 문제가 있구나 알 수 있었다. 벨로그 쓰는게 공부가 꽤 된다는 사실에 놀랐고 Singleton을 이용해 어플 개발하신 대단한 분! 이런 능력자가 많다는 것도 놀랐다.. 세상은 넓고 나는 아직 우물 안 개구리구나 현업에 들어가서 잘 할 수 있을까 싶은데 그냥 잘 해야지 뭐.. 여튼 Singleton 공부 끝

0개의 댓글