개발노트/Spring

[디자인 패턴] Singleton Pattern

superpil 2022. 3. 27. 19:02

시작하기

이번 프로젝트를 시작하면서 서버에서 서버(server to server)로 데이터를 주고받아 처리하는 작업이 필요했다.

특별히 인증서버를 두지않고 요청할 서버에 토큰을 받아 인증받는 정책이었다.

 

해당 서버에서 토큰을 받기 위해 계정 정보, 도메인 정보와 같은 필수 정보들을 db에서 한 번만 조회 후 변수에 담아 재사용하고 싶었다.

처음에는 단순하게 static에 담아 재사용 생각을 했는데 문득 싱글톤 패턴이 생각나서 지금 같은 경우에 적용하기 좋은지 생각하게 되었다.

예전에 가볍게 스쳐 지나가듯 공부를 해서 이번에 제대로 알아보려 한다.

 

개념

싱글톤은 이름 그대로 객체(인스턴스)를 한 개만 생성하는 패턴이다.

즉, 유일무이 하게 단 하나의 객체 생성해서 모두가 공용으로 사용한다.

 

굳이…!? 왜 한 개만 생성해서 사용할까?!

“Java언어로 배우는 디자인 패턴 입문” 도서에서는 그 이유를 아래와 같이 설명한다.

  • 지정한 클래스의 인스턴스가 절대로! 1개밖에 존재하지 않는 것을 보증하고 싶을 때
  • 인스턴스가 1개밖에 존재하지 않는 것을 프로그램 상에서 표현하고 싶을 때
  • 예를 들어 컴퓨터 자체를 표현한 클래스, 현재의 시스템 설정을 표현한 클래스 등에 적용

 

다이어그램

싱글톤 다이어그램

이름답게 싱글톤 다이어그램은 단순하게 객체 하나뿐이다.

추가로 다이어그램에 +, -, #기호는 접근제어를 의미하는데, “-” 가 붙어있는 것은 private, “+”은 public, “#”은 protection의 의미를 가지고 있다.

또한, “instance : Singleton”“getInstance() : Singleton”처럼 밑줄로 표시된 것은 static의미다.

 

기본 예제

Singleton Class

public class Singleton{

    private static Singleton singleton = new Singleton();

    private Singleton(){
        System.out.println("인스턴스를 생성했습니다");
    }

    public static Singleton getInstance(){
        return singleton;
    }

}

👉 private static Singleton singleton = new Singletion()

  1. singleton변수는 static변수로 Singleton 클래스의 인스턴스에서 초기화된다
  2. 초기화는 singleton클래스를 로드할 때 1회만 실행된다
  3. 외부에서 접근하여 값을 변경할 수 없으며 단 한 개의 객체만 생성되는 것을 보증한다.

 

👉 private Singletion() { ... }

  1. 만약, 코드 상에서 new Singleton()으로 호출할 경우 컴파일 에러가 발생
  2. 프로그래머가 실수를 해도 인스턴스가 1개만 생성되도록 보증한다
  3. 즉, 다수의 Singleton 인스턴스 생성을 막기 위해서 이다

 

👉 public static Singleton getInstance() { ... }

  1. Singleton 클래스의 유일한 인스턴스를 얻을 수 있는 방법은 getInstance()를 호출하는 방법뿐이다
  2. 즉, getInstance메서드에서 return 되는 singleton객체와 static singleton 변수에 할당된 Singletion객체는 동일하며 객체의 주소 값도 당연히 같다
  3. 최초 getInstance()가 실행될 때 Singleton객체가 초기화된다.

 

Main Class

public class Main{

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);

        Singleton obj1 = Singleton.getInstance();
        Singleton obj2 = Singleton.getInstance();

        if(obj1 == obj2){
            System.out.println("obj1과 obj2는 같은 인스턴스 입니다.");
        }
        else{
            System.out.println("obj1과 obj2는 다른 인스턴스 입니다.");
        }
    }

}

//Start
//인스턴스가 생성되었습니다.
//obj1과 obj2는 같은 인스턴스 입니다.
//End

진짜 싱글톤인지 객체가 동일한지 테스트해보자.

간단하게 Main Class에서 Singleton객체의 getInstance()를 두 번 호출하여 변수에 할당 후 if문으로 비교한다.

동일한 객체라서 "obj1과 obj2는 같은 인스턴스입니다."가 출력되는 것을 볼 수 있다.

 

Singleton의 문제점

동시성(Thread Safe) 문제

우선 동시성 문제점을 파악하기 위해 기본 예제를 수정해보자.

public class Singleton {

  private static Singleton singleton;

  private Singleton() {}

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

}

👉 if(singleton == null) singleton = new Singleton(num)

  1. 이번에는 Singleton class가 로드되는 즉시 singleton변수에 new Singleton()을 할당하지 않고 getInstance()가 호출될 때 Singleton인스턴스를 할당한다.
  2. 해당 코드도 싱글톤을 구현하기 위한 기본 예제 코드면서 동시성 문제를 가진 대표적인 코드다.
  3. 만약 스레드가 다중 스레드일 경우 동시에 getInstance()를 호출하게 되면 각각의 스레드는 singleton변수를 null값으로 인지하고 new Singleton()를 하게 된다.
  4. 결국 동일한 인스턴스가 아닌 다중 인스턴스가 생성될 가능성이 매우 커진다. 사실인지 아래에서 확인해보자.

 

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
        for(int i = 0; i < 10; i++) new Thread(new SingletonRun()).start();
    }

    static class SingletonRun implements Runnable{
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance();
            System.out.println("singleton address value : " + singleton);
        }
    }

}

👉 for(int i = 0; i < 10; i++) new Thread(new SingletonRun()).start()

  1. 스레드를 반복으로 돌려서 생성하고 start()를 호출하여 실행한다.
  2. new Thread()에는 Runnable를 구현한 SingletonRun calss를 생성하여 스레드의 동작을 주입한다.

 

👉 public void run() { ... }

  1. 실제 스레드의 동작을 정의한 곳이다.
  2. 현재 코드에서는 Singleton.getInstance()를 호출해서 System.out.println으로 singleton객체의 주소 값을 찍어본다.
  3. 단일 객체가 생성된다면 주소 값이 전부 같을 거고 2개 이상 인스턴스가 생성되면 주소 값은 2개 이상이 출력 될 것 이다.

 

싱글톤 동시성 문제 객체 주소값

주소 값이 @392d258b, @7bd51d87 두 개가 출력된다.

즉, 멀티 스레드 경우 인스턴스가 2개 이상 생성될 확률이 매우 높다.

(생각보다 2개 이상 생성이 안돼서 계속 재실행 했다능........ 😤😤)

 

동시성의 문제점을 파악했으니 이제 해결방법에 대해 알아보자.

 

동시성 해결방법 1 (동기 처리)

package com.example.demo;

public class Singleton {

  private static Singleton singleton;

  private Singleton() {}

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

}

👉 public static synchronized Singleton getInstance() { ... }

  1. getInstance()에 synchronized를 사용하여 동기로 호출하게 한다.
  2. 즉, synchronized는 멀티 스레드가 getInstance()를 호출하면 차례로 하나의 스레드만 getInstance()에 진입하게 하여 다중 인스턴스 생성을 막는다.
  3. 다만 동기로 호출되므로 성능상 이슈가 생길 수 있다.

 

동시성 해결방법 2 (미리 인스턴스 생성)

public class Singleton{

    private static Singleton singleton = new Singleton();

    private Singleton() {}

    public static Singleton getInstance(){
        return singleton;
    }

}

👉 private static Singleton singleton = new Singleton()

  1. 기본 예제에서 본 코드로써 미리 인스턴스를 생성해서 변수에 할당하는 방법이다.
  2. 아주 간단한 해결법이지만 만약 인스턴스 만드는 과정이 복잡한데 미리 만들어 놓고 사용하지 않는다면 효율성에서 낭비가 된다는 문제점이 있다.

 

동시성 해결방법 3 (class추가 생성)

public class Singleton {

  private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton SINGLETON = new Singleton();
    }

  public static Singleton getInstance() {
        return SingletonHolder.SINGLETON;
  }

}

👉 private static class SingletonHolder { ... }

  1. getInstance()를 호출하면 SingleTonHolder class가 생성이 되면서 SINGLETON에 new Singleton() 인스턴스가 할당된다.
  2. 멀티 스레드 환경에서 안전하고 동기화 처리가 없기 때문에 성능에도 문제가 발생하지 않는다.
  3. “동기 처리 해결방법”과 “미리 인스턴스 생성 해결방법”의 아쉬운 점을 한 번에 잡아내는 방법이지 않을까 싶다.

 

위 언급한 해결방법 이외에도 싱글톤 생성 방법은 많은 것 같다.

다양한 방법을 많이 아는 것도 중요하지만 싱글톤을 얼마나 시기적절하게 사용하는지가 더 중요하지 않을까 싶다.

 

개방-폐쇄 원칙 위배

  • 싱글톤으로 만든 객체의 역할이 간단한 것이 아닌 역할이 복잡한 경우라면 해당 싱글톤 객체를 사용하는 다른 객체 간의 결함도가 높아져서 객체 지향 설계 원칙(개방-폐쇄 원칙)에 어긋나게 된다.
  • 해당 싱글톤 객체를 수정할 경우 싱글톤 객체를 사용하는 곳에서 사이드 이팩트 발생 확률이 생기게 된다

 

마무리

이번 글에서 싱글톤 기본 예제, 사용 목적, 문제점에 대해 알아봤다.

비록 패턴 공부를 했다고 이번 프로젝트 사례에 싱글톤을 적용하는 게 맞다고 확신할 수 없다.

일단 적용 후 다양하게 테스트하고 최고의 선택이었는지 확인해보려 한다.

 

잘못된 내용이나 보충이 필요한 내용이 있다면 댓글 남겨 주세요!

주니어 개발자에게 큰 도움이 됩니다! 감사합니다! 😃

 

Reference

  1. http://www.yes24.com/Product/Goods/2918928 - 디자인 패턴 도서
  2. https://cjw-awdsd.tistory.com/42 - 싱글톤 동시성
  3. https://cjw-awdsd.tistory.com/42 - 싱글톤 개념