JAVA

[JAVA] 싱글톤 패턴(Singleton Pattern)이란?

sagecode 2025. 5. 20. 16:21

싱글톤 패턴이란?

싱글톤 패턴(Singleton Pattern)은 디자인 패턴 중 생성 패턴으로 하나의 클래스에 오직 하나의 인스턴스만 가지도록 하는 패턴이다. 하나의 클래스를 기반으로 여러가지 인스턴스를 만들 수 있지만, 그렇게 하지 않고 단 하나의 인스턴스를 만들어 이를 기반으로 로직을 만든다.

 

<싱글톤 그림>

 

싱글톤 패턴 구현

public class Singleton {
	
    private static Singleton singletonInstance;
    
    // 생성자를 외부에서 접근하지 못하도록 private 접근제어
    private Singleton() {}
    
    public static Singleton getInstance() {
    	if (singletonInstance == null) {
        	singletonInstance = new Singleton();
        }
        
        return singletonInstance;
    }
}

 

싱글톤 패턴을 구현할 때, singletonInstance 변수와 getInstance() 메소드를 정적 변수와 메소드로 선언하는 이유는

  • 클래스 레벨에서 단 하나의 인스턴스를 유지하기 위해서
  • 힙 메모리가 아닌 클래스영역에 저장되어서 전역으로 접근할 수 있게 하기 위해서

정적 메소드로 구현하지 않았을 경우

public class NotSingleton {
    private NotSingleton() {
        
    }

    public NotSingleton getInstance() {
        return new NotSingleton(); // 새로운 인스턴스를 생성
    }

    public static void main(String[] args) {
        NotSingleton instance1 = new NotSingleton().getInstance();
        NotSingleton instance2 = new NotSingleton().getInstance();

}

인스턴스 유무를 조회할 때 마다 새로운 다른 객체를 생성해야 하므로 정적 변수와 메소드를 사용해야 하나의 인스턴스를 유지할 수 있다.

 

여러 스레드에서 접근시 문제 발생 및 해결

싱글톤 패턴의 경우 단일 객체이기 때문에, 상태값을 갖지 않는 것이 좋다. 단일 객체가 상태 값을 가지는 경우 특정 참조 변수가 상태를 변경했을 때 다른 참조 변수에도 영향을 미치기 때문이다. 멀티스레드 환경에서 싱글톤의 상태값이 존재하지 않기 때문에 객체 생성 유무 판단에서 동기화가 되지 않을 수도 있다. 

public class Singleton {
	private static Singleton singletonInstance;
    
    private Singleton() {}
    
    public static Singleton getInstance() { // 두 번째 Thread 진입 가능
    	if (singletonInstance == null) { // 첫 번째 Thread 진입
        	singletonInsatance = new Singleton();
        }
        return singletonInstance;
    }
}

 

그렇기 때문에 여러가지 방법으로 이 문제를 해결한다.

  • Synchronized 사용
public class Singleton {
    private static Singleton singletonInstance;
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
    	if (singletonInstance == null) {
        	singletonInsatance = new Singleton();
        }
        return singletonInstance;
    }
}

synchronized를 사용할 경우 하나의 스레드만 접근이 가능하다.

  • final 변수 이용
public class Singleton {
    private final static Singleton singletonInstance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return singletonInstance;
    }
}

변수를 선언하면서 동시에 인스턴스를 생성할 수 있다. 하지만 이 객체를 사용하지 않을 경우 메모리 낭비로 이어질 수 있다.

  • Bill Pugh solution 사용
public class Singleton {

    private Singleton() {
    }

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

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

싱글톤 패턴의 단점

싱글톤 패턴은 TDD(Test Driven Development)를 할 때 걸림돌이 된다. TDD를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 테스트가 서로 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 한다.

 

스프링에서 싱글톤 패턴을 어떻게 적용하고 있을까?

스프링 컨테이너는 싱글톤 객체를 직접 생성하고, 내부에서 캐싱하여 재사용한다. @Configuration, @Component, @Bean 등의 어노테이션을 활용하여 스프링 내에서 사용하고 싶은 객체들을 Bean을 통해 생명주기를 관리한다.

 

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

여기서 @Configuration을 통해 AppConfig를 스프링 설정정보로 정의한다. 이 안에 들어있는 @Bean 이 붙어있는 메소드 내의 리턴 객체들을 스프링 컨테이너에 Bean으로 등록하여 스프링에서 사용한다.

 

그럼 대체 이 Bean을 어떻게 싱글톤을 통해서 관리한다는 걸까?

스프링 내부에서 동작하는 싱글톤 레지스트리

위 그림과 같이 만약 싱글톤이 작동하지 않는다면 스프링 컨테이너에서 사용자가 서비스를 요청할 때 마다 서비스 객체가 생성 될 것이다. 이럴 경우 같은 객체를 생성하는데 너무 많은 양의 데이터가 낭비된다. 그렇기 때문에 동일한 요청을 하는 경우에는 하나의 인스턴스만 생성하는게 좋다.

 

그래서 스프링 내에서는 DefaultSingletonBeanRegistry라는 곳에서 아래 코드처럼 동작을 하며 싱글톤을 적용한다.

Object sharedInstance = singletonObjects.get(beanName);

if (sharedInstance == null) {
    sharedInstance = createBean(beanName);
    singletonObjects.put(beanName, sharedInstance);
}

return sharedInstance;

 

DefaultSingletonBeanRegistry는 AbstractBeanFactory의 부모 클래스이다. 그래서 모든 스프링 Bean의 생성과 관리는 이 클래스에서 이루어진다.

 

@Configuration 테스트 코드

 

@Configuration을 달지 않게 되면 @Bean이 적혀있는 메소드들이 일반 메소드 취급을 받게되면서 객체가 싱글톤으로 적용되지 않는다. 그래서 @Configuration을 주석처리하고 빈이 생성이 되었는지 확인해보았다.

AppConfig 클래스를 applicationContext로 지정하고 생성한 memberService 빈에서 가져온 Repository 객체와 따로 생성한 memberRepository 객체가 같은지를 비교해보는 테스트이다.

 

그 결과 두 객체의 주소값은 다르게 나왔다. 

 

@Configuration을 붙였을 때,

결론은 @Configuration을 설정하고 @Bean으로 객체들을 등록했을 때 싱글톤으로 스프링이 알아서 관리해준다는 것을 알았다.