Thumbnail

✨ 디자인 패턴이란?
 프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 ‘규약’ 형태로 만들어 놓은 것

이번 글에서는 디자인 패턴의 종류 중 하나인 싱글톤 패턴에 대해 알아보려고 한다. 싱글톤 패턴이 무엇을 의미하는지, 또 어떻게 사용하는지와 사용하는 이유나 주의할 점은 없는지 다양한 예제를 통해 알아보자.
Singleton

싱글톤 패턴

먼저 싱글톤 패턴(Singleton Pattern)의 의미를 보자면 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴이다. 즉, 생성자의 호출이 반복적으로 이뤄져도 실제로 생성되는 객체는 최초 생성된 객체를 반환 해주는 것이다. 쉽게 얘기하면 단 한 개의 물건으로 여러 사함이 함께 공유하며 사용하는 방법이다.

그렇다면 싱글톤 패턴이 필요한 이유는 뭘까?

예를 들어 사무실에 여러 명의 사람이 프린터를 사용한다고 생각해 보자. 이때 프린터를 사용하려는 사람들이 프린터를 각자 생성해서 사용하는 것은 불가능하고 1대만 존재하는 프린터를 여러 사람이 함께 공유하며 사용해야 한다. 이러한 상황은 현실에서도 존재하지만, 우리가 개발하는 프로그램 안에서도 존재할 수 있다.

프로그램 내에서 단 1개만 존재해야 하는 객체가 있으며 이를 프로그램 내부의 여러 부분에서 호출하여 사용하는 경우이다. 보통 공통된 객체를 여러 개 생성해서 사용하는 데이터베이스 연결 모듈에 싱글톤 패턴이 많이 사용한다고 한다.

이렇게 하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에 인스턴스를 생성할 때 드는 비용이 줄어드는 장점이 있다. 하지만 의존성이 높아진다는 단점도 있다. 단점과 주의할 점에 대해서는 조금 후에 살펴보기로 하고 다양한 예제들을 살펴보자!

자바스크립트의 싱글톤 패턴

우리가 자바스크립트에서 자주 쓰는 객체 리터럴 또는 new Object로 객체를 생성하게 되면 다른 어떤 객체와도 같지 않기 때문에 이 자체만으로 싱글톤 패턴을 쉽게 구현할 수 있다.

우선 가장 간단한 싱글톤 예제를 객체 리터럴을 이용해서 만들어보자.

const obj1 = {
  a: 27,
};
const obj2 = {
  a: 27,
};
console.log(obj1 === obj2);
/* false */

싱글톤 패턴은 하나의 클래스에 하나의 인스턴스만을 가지기 때문에 obj1과 obj2에 같은 내용이 들어있어도 서로 다른 인스턴스를 가지기 때문에 obj1과 obj2는 서로 다르기 때문에 두 개의 값을 비교한 결과는 false가 나온다.

class Singleton {
  constructor() {
    if (!Singleton.instance) {
      Singleton.instance = this; // 없으면 객체 생성 후 반환
    }
    return Singleton.instance; // 있으면 그냥 반환
  }
  getInstance() {
    return this.instance;
  }
}

const a = new Singleton();
const b = new Singleton();
console.log(a === b);
/* true */

위의 코드는 Singleton.instance라는 하나의 인스턴스를 가지는 Singleton 클래스를 구현한 모습이다. 이를 통해 a와 b는 하나의 인스턴스를 가지므로 두 개의 값을 비교한 결과는 true가 나온다.

하지만 이때까지 살펴본 두 예제를 보면 모든 속성이 공개되어 있다는 단점이 있다. 비공개로 만드는 게 바로 제대로 된 싱글톤 패턴이다.

즉, 싱글톤 객체를 생성하기 위해선 객체 리터럴 + 클로저의 조합이 필요하다!

자바스크립트에는 private, public, protected 와 같은 별도의 문법이 없다. 기본적으로 객체의 모든 멤버는 public(공개)로 되어 있다.
공개가 되었다는 뜻은 외부에서 해당변수의 접근이 가능하다는 것이고 별도의 문법이 존재하지 않지만 클로저(closure)를 이용하여 비공개 멤버를 구현할 수 있다.

클로저를 이용한 싱글톤 패턴에 대해 더 자세히 알고 싶다면 이곳을 참고하자!

데이터베이스 연결 모듈

앞에서 설명했듯이 싱글톤 패턴은 데이터베이스 연결 모듈에 많이 쓰인다.

const URL = 'mongodb://localhost:3000/DevLog'
const createConnection = url => ({"url" : url})
class DB {
    constructor(url) {
        if (!DB.instance) {
            DB.instance = createConnection(url) // 없으면 createConnection을 통해 객체 생성
        }
        return DB.instance // 있으면 그냥 반환
    }
    connect() {
        return this.instance
    }
}

const a = new DB(URL)
const b = new DB(URL)
console.log(a === b)
/* true */

이렇게 DB.instance라는 하나의 객체를 기반으로 a, b를 생성하는 것을 볼 수 있다. 이를 통해 데이터베이스 연결에 관한 객체 생성 비용을 아낄 수 있다는 장점이 있다.

자바의 싱글톤 패턴

마지막 예제로는 사람들이 자주 쓰는 자바로 싱글톤 패턴을 구현해 보려 한다.

class Singleton {
    // 단 1개만 존재해야 하는 객체의 인스턴스, static으로 선언
    private static Singleton instance;
    // private생성자로 외부에서 객체 생성을 막음
    private Singleton() {}
    //외부에서는 getInstance()로 instance를 반환
    public static Singleton getInstance() {
        // instance가 null일 때만 생성
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
public class HelloWorld{
    public static void main(String []args){
        Singleton a = Singleton.getInstance();
        Singleton b = Singleton.getInstance();
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());
        if (a == b){
         System.out.println(true);
        }
    }
}
/*
705927765
705927765
true
*/

위 코드는 JAVA에서 싱글톤 패턴의 가장 기본적은 구현 방법이다.

private 생성자로 외부에서 객체 생성을 막으며, getInstance()함수로만 instance에 접근이 가능하게 만들었다. 실제로 위 코드의 HelloWorld 클래스를 살펴보면 a와 b의 HashCode가 서로 같은 걸 볼 수 있다.

하지만 위의 코드는 멀티 스레드 환경에서 Thread-Safe를 보장해주지 않는다!

예를 들면 두 개의 스레드 동시에 getInstance()를 호출한 경우 if (instanc==null) 조건문에 동시에 도달하게 되어 instance를 두 번 생성할 수도 있다.

이를 해결하기 위해, Java에서 스레드 동기화를 지원하는 Synchronized를 사용할 수 있다. 하지만 Synchronized의 가장 큰 단점은 Thread-Safe를 보장하기 위해 성능 저하가 심하다. 그렇다면 동시성 문제를 해결하면서 성능 저하도 막을 수 있는 방법은 뭐가 있을까?

바로 LazyHolder를 사용하는 싱글톤 패턴이 있다. 아래의 코드를 살펴보자!

class Singleton {
    // private static inner class 인 LazyHolder
    private static class LazyHolder {
        // LazyHolder 클래스 초기화 과정에서 JVM이 Thread-Safe 하게 instance를 생성
        private static final Singleton INSTANCE = new Singleton();
    }
    // LazyGolder의 instance에 접근하여 반환
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

public class HelloWorld{
    public static void main(String []args){
        Singleton a = Singleton.getInstance();
        Singleton b = Singleton.getInstance();
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());
        if (a == b){
         System.out.println(true);
        }
    }
}
/*
705927765
705927765
true
*/

현재 JAVA에서 가장 많이 사용되는 LazyHolder를 사용하여 구현한 싱글톤 패턴이다.

JVM의 클래스 초기화 과정에서 원자성을 보장하는 원리를 이용하는 방식이다.

이러한 방법의 장점은 성능 저하가 심한 Synchronized를 사용하지 않아도 JVM 자체가 보장하는 원자성을 사용하여 Thread-Safe 하게 싱글톤 패턴을 구현할 수 있다는 점이다.

싱글톤 패턴의 단점

이제 싱글톤 패턴의 단점에 대해 알아보자.

싱글톤 패턴은 TDD(Test Driven Development)를 할 때 걸림돌이가 된다. TDD를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 하는데 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 ‘독립적인’ 인스턴스를 만들기가 어렵다.

의존성 주입

또한, 싱글톤 패턴은 사용하기가 쉽고 굉장히 실용적이지만 모듈 간의 결합도가 높아져 개방-폐쇄 원칙을 위배하게 된다. 이는 객체 지향 설계 원칙에 어긋난다.

이때 의존성 주입(DI, Dependency Injection)을 통해 모듈 간의 결합을 조금 더 느슨하게 만들어 해결할 수 있다.

✨ 의존성 주입이란?
 메인 모듈이 ‘직접’ 다른 하위 모듈에 의존성을 주지 않고, 의존성 주입자 (dependency injector)를 중간에 둬서 간접적으로 의존성을 주입하는 방식이며 ‘디커플링’ 이라고도 한다.

🔥의존성 주입의 장점

모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅이 쉽고 마이그레이션이 수월해진다.

또한 구현 시 추상화 layer를 넣고 이를 기반으로 구현체를 넣기 때문에 어플리케이션 의존성 방향이 일관된고, 어플리케이션을 쉽게 추론할 수 있으며, 모듈 간 관계가 좀 더 명확해진다.

🔥의존성 주입의 단점

모듈들이 더 분리되므로 클래스의 수가 늘어나 복잡성이 증가하고 약간의 런타임 페널티가 생길 수 있다.

🔥의존성 주입의 원칙

의존성 주입은 상위 모듈은 하위 모듈에서 어떤 것도 가져오면 안된다. 또한 둘 다 추상화에 의존해야 하며 추상화는 세부사항에 의존하지 말아야 한다.

알아두면 좋은 점

  • 싱글톤 패턴은 Spring framework에서도 많이 사용되며, 어떤 식으로 구현하는지 알아두면 도움이 된다.
    자바와 Spring에서의 싱글톤 차이점이라면, 싱글톤 객체의 생명주기가 다르다. 또한 자바에서 공유 범위는 Class loader 기준이지만, Spring에서는 ApplicationContext가 기준이 된다.


요약


  • 싱글톤 패턴은 하나의 클래스로 오직 하나의 인스턴스만 가지는 디자인패턴
  • 데이터베이스 연결 모듈에 주로 사용
  • 인스턴스가 절대적으로 한개만 존재하는 것을 보증하고 싶을 경우 사용
  • 객체 로딩 시간이 현저하게 줄어 성능이 좋아지는 장점 존재
  • TDD할 때 단점 존재
  • 개방-폐쇄 원칙 위배 (DI 활용으로 해결 가능)
⚡읽어주셔서 감사합니다!⚡
🙂틀린 부분이나 보완할 점이 있을 경우 언제든지 댓글 혹은 메일로 지적해 주시면 감사하겠습니다!🙂

Leave a comment