본문 바로가기
CS 먹고 레벨업~

혼자야?

by 봄설날 2025. 9. 17.

어 아직 싱글이야

싱글톤이야

싱글톤 패턴은 디자인 패턴 중에서 개념적으로 가장 간단한 패턴이다. 그렇기 때문에 글만 읽고 넘어가기 쉬운데 이번 기회를 통해 자세히 알아보자.

 

핵심 개념만 정리하자면 싱글톤 패턴은 ‘하나의 클래스에 하나의 인스턴스만 가지는 패턴’ 이다.

 

 

단 하나의 유일한 객체만을 만들어 객체의 유일성을 보장하고 싶을 때 사용한다. 때문에 메모리가 절약된다는 장점이 있다. 만약 인스턴스가 필요한 상황이라면 똑같은 인스턴스를 새로 만들지 않고 기존의 인스턴스를 가져와 활용한다.

언제써요?

싱글톤 패턴은 만들어져 있는 것을 가져와서 활용하는 방식이기 때문에 한 번 객체를 만드는데 드는 비용이 큰 객체를 여러번 사용해야 하는 상황에서 적합하다.

 

대표적인 예시가 데이터베이스 연결이다.

데이터베이스는 접속하는 데에 큰 비용이 발생한다. 때문에 객체가 필요할 때 마다 데이터베이스를 연결하고 객체를 생성해서 가져오는 행위는 굉장히 비효율적이다. 이런 상황에서 싱글톤 패턴을 사용해 객체를 한 번만 생성하고 돌려쓸 수 있는 것이다.

또한 캐시, 로그 기록, 애플리케이션의 환경설정을 관리하는 객체처럼 전역적인 상태 관리가 필요할 때 이용된다. 이러한 객체들은 잘 변하지 않아 새로 만들어서 사용할 일이 없기 때문이다.

어떻게 써요?

싱글톤 패턴의 개념과 활용 예시를 통해 어떤 디자인 패턴인지 파악했다. 이제 어떻게 싱글톤 패턴을 구현할 수 있을지 알아보자.

구현 원리

Java에서 싱글톤 패턴은 생성자 메서드에 private 접근 제어자를 붙여 사용한다. private을 사용하여 싱글톤으로 사용하고 싶은 클래스를 외부에서 new 생성자를 통해 생성하는 것을 막는다. 그렇게 생성된 단 하나의 인스턴스는 static 변수에 저장한다. 그리고 인스턴스 접근을 위한 public static 메서드를 제공하면 된다.

 

getInstance() 라는 메서드에 생성자 초기화를 해주어 클라이언트가 싱글톤 클래스를 생성해서 사용하려면 getInstance() 라는 메서드 실행을 통해 instance 필드 변수가 null 일경우 초기화를 진행하고 null이 아닐경우 이미 생성된 객체를 반환하는 식으로 구성하면 된다.

 

예제를 하나 살펴보자.

예제
실행

getInstance() 를 통해 객체를 불러와 변수에 저장하고 이를 출력해보면 똑같은 객체 주소를 가지고 있는 걸 볼 수 있다.

즉, 객체 하나만 생성하고 여러 변수에 불러와도 돌려쓰기를 한 것이다.

 

개념과 장점을 나열하다 보니 여기까지만 보면 싱글톤 패턴이 정말 좋은 디자인 패턴으로 보인다. 하지만 역시 얻는 이점이 있으면 문제점도 있다.

그러나 싱글톤 패턴은 정말 좋은 패턴일까?

먼저 싱글톤 패턴은 객체 지향 설계의 5대 원칙인 SOLID 원칙에 위반되는 사례가 나타난다는 점이다.

 

1. 싱글톤 클래스는 인스턴스를 생성하고 관리하는 책임과 비지니스 로직을 수행하는 책임 두가지의 책임을 가지기 때문에 단일 책임 원칙을 위배한다.

 

2. 싱글톤 인스턴스는 혼자 자원을 독점하게 되고 다른 클래스들간의 결합도가 높아져 확장 및 교체가 어렵다. 이는 개방-폐쇄 원칙을 위배한다.

 

3. 의존 관계 상 클라이언트가 인터페이스가 아닌 구체 클래스에 의존하게 되어 의존 역전 원칙도 위배한다.

따라서 싱글톤 인스턴스를 너무 많이 사용하면 잘못된 디자인 패턴이 되는 것이다.

 

4. 더 결정적인 단점은 위와 같은 문제들로 인해 단위 테스트가 어렵다는 점이다.

 

단위 테스트는 서로 독립적이어야 하며 테스트의 순서에 상관 없이 실행이 가능해야 한다. 하지만 싱클톤 인스턴스는 자원을 공유하기 때문에 테스트를 수행할 때 서로 영향을 미치고 테스트가 문제없이 수행되려면 항상 인스턴스 상태를 초기화시켜주어야 한다.

보통 테스트를 진행할 때 Mock 객체를 생성하는 방식을 많이 사용하는데 이는 상속에 의존하기 때문에 싱글톤 코드는 Mock 객체를 이용한 테스트도 어렵다.

 

결과적으로 싱글톤 패턴은 유연성이 많이 떨어지고 객체 지향 프로그래밍과는 조금 먼 패턴이다.

그럼 문제점을 고쳐서 쓰면 되잖아요

이러한 싱글톤 패턴의 문제점을 해결할 수 있는 방법이 의존성 주입이다.

 

실용적이지만 모듈 간의 결합이 강한 싱글톤 패턴에서 모듈간의 결합을 조금 느슨하게 만들어줄 수 있는 방법이다. 객체를 내가 직접 만들어서 쓰는 게 아니라 외부에서 만들어서 넣어주자! 라고 생각하면 편하다.

 

의존성 주입은 보통 제어의 역전이라는 개념을 통해 구현된다. 객체의 생성, 관리, 소멸 등 모든 제어권이 개발자에서 외부 컨테이너로 넘어가면서 외부 컨테이너에서 만들어진 객체를 클래스로 가져오는 것이다.

 

결과적으로 제어의 역전을 통한 의존성 주입을 통해 모듈 간의 결합을 느슨하게 만들 수 있는 것이다. 실제 객체 대신 Mock 객체를 쉽게 주입할 수 있어 단위 테스트가 편리해진다.

돌고 돌아 스프링

싱글톤 패턴이 가지는 장점이 좋다는 것을 이제 알았다. 단점도 명확하지만 해결 방법이 있다는 것도 알았다. 그리고 좋은 의존성 주입이라는 개념을 가장 잘 활용하고 있는 프레임워크가 스프링 프레임워크다.

 

스프링의 핵심인 DI 컨테이너는 개발자가 작성한 자바 클래스들을 알아서 관리하며 필요한 곳에 알아서 주입해 준다. 이때 알고 가야할 아주 중요한 사실이 있다.

"스프링 컨테이너는 기본적으로 모든 빈을 싱글톤으로 관리한다."

 

스프링 빈은 스프링 DI 컨테이너가 생성하고 관리하는 모든 객체를 의미한다.

 

하지만 스프링에서 싱글톤으로 관리한다는 의미와 싱글톤 '패턴'은 근본적으로 다르다.

즉, 스프링은 싱글톤 패턴의 장점만 사용하고 단점은 의존성 주입으로 보완하는 것이다.

 

스프링 프레임워크를 사용하면 개발자는 싱글톤을 만들기 위해 private 생성자나 .getInstacne() 와 같은 코드를 작성할 필요가 없다. 그냥 클래스를 만들고 @Service, @Repository 와 같은 어노테이션만 붙여주면 끝난다.

@Service
public class MemberService {

    // 나는 MemberRepository가 필요하다.. 싱글인지 혼자왔는지는 관심없다...
    private final MemberRepository memberRepository;

    // 스프링 컨테이너가 알아서 싱글톤으로 관리되는 MemberRepository 빈을 주입해준다.
    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    // ... 비즈니스 로직
}

객체 관리는스프링 컨테이너가 담당하며 싱글톤의 유일성을 보장해준다.

사용하는 쪽에서도 싱글톤이든 아니든 신경 쓸 필요 없이 그저 생성자나 필드를 통해 필요한 객체를 주입받아 사용하면 된다.

 

싱글톤 패턴의 장점은 명확하고 그 장점만을 골라 사용하는게 스프링 프레임워크의 큰 특징이다

 

우리 모두 스프링 열심히 해봐요~!

Q&A

1. 자바에서 객체가 생성되면 어디에 위치하는가?! -> 자바에서 new 키워드를 통해 생선된 객체는 Heap 메모리 영역에 위치!!

   그렇다면 private static fianl 을 이용해 클래스의 인스턴스를 생성하면 어디에 위치하는가?

new ClassicSingleton() = 객체 본체
new 키워드로 생성된 객체의 실체는 힙영역에 위치한다.

instance = 참조 변수
static 키워드가 붙어있기 때문에 일반 변수처럼 스택에 위치하지 않는다. static 변수는 클래스 자체에 소속되므로 클래스 정보와 함께 메소드 영역에 위치한다.

 

2. 위 예시에서 이른 초기화를 사용하고 있는데 그 때의 장단점은? 그렇다면 지연 초기화를 하면 생길 수 있는 문제점은?

private static final ClassicSingleton instance = new ClassicSingleton();
클래스가 메모리에 로드될 때 바로 실행되는 부분
getInstance() 메서드가 호출되든 안되든 JVM이 ClassicSingleton 클래스를 읽어들일 때 instance가 즉시 생성된다.

장점: 코드가 간단하고 멀티스레드 환경에서도 동기화 문제 없이 안전하게 사용할 수 있다.
단점: getInstance()를 한 번도 호출하지 않더라도 인스턴스가 무조건 생성되므로 아주 약간의 리소스 낭비가 발생

 

만약 지연 초기화를 사용한다면~

-> getInstance() 메서드가 처음으로 호출될 때 인스턴스를 생성

public static ClassicSingleton getInstance() {
    if (instance == null) { // 호출 시점에 인스턴스가 없으면
        instance = new ClassicSingleton(); // 그 때 생성한다.
    }
    return instance;
}

인스턴스가 필요 없는 경우에는 생성하지 않아 리소스를 아낄 수 있지만 멀티스레드 환경에서 동기화 문제(Thread-Safety)가 발생할 수 있다.

 

2-1. Thread-Safety 문제란?

여러 스레드가 하나의 공유된 데이터에 동시에 접근하여 조작할 때 예상치 못한 잘못된 결과가 발생하는 것
이는 각 스레드의 작업이 원자적으로 실행되지 않고 서로 간섭하기 때문에 발생!!

대표적으로 Race Condition과 임계 영역과 동기화 문제가 있다.

 

2-2. 싱글톤 구현 방식은?

  • Eager Initialization
  • Static block initialization
  • Lazy initialization
  • Thread safe initialization
  • Double-Checked Locking
  • Bill Pugh Solution
  • Enum 이용
    총 7가지 방식이 있으며 모두 싱글톤을 지향하지만 각각 장단점이 존재한다. 각 순서마다 1번 부터 조금씩 단점을 보완하는 식으로 흘러간다고 생각하면 된다!!

자세한 내용은 다음에 따로 다뤄보도록 하자 ^^~

 

출처 : https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%8B%B1%EA%B8%80%ED%86%A4Singleton-%ED%8C%A8%ED%84%B4-%EA%BC%BC%EA%BC%BC%ED%95%98%EA%B2%8C-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

 

3. 스프링이 아닌 상황이라면 싱글톤과 의존성 주입을 어떻게 관리하는가?

  • 개발자가 직접 구현
    싱글톤 : 정적 내부 클래스와 같은 디자인 패턴을 개발자가 직접 코드로 구현한다. 스레드 안전성의 책임도 개발자에게 있다.
    의존성 주입 : 프레임워크가 없다면 수동 의존성 주입을 사용한다. 애플리케이션의 시작점에서 모든 객체를 직접 생성하고 필요한 곳에 생성자를 통해 수동으로 주입한다.
  • 다른 프레임 워크 사용!
    싱글톤 : 스프링처럼 프레임워크가 싱글톤을 관리하는 것이 아니라 각기 다른 방식으로 싱글톤을 관리해준다~
    의존성 주입 : 대부분의 프레임워크는 의존성 주입을 제공한다고 한다 !!