본문 바로가기
Spring/스프링 핵심 원리 - 기본편

5-1. 싱글톤 컨테이너 + static method

by hk27 2022. 1. 6.
@Configuration은 왜 사용하는 것일까요?

 

안녕하세요. 오늘은 싱글톤 컨테이너에 대해서 알아보겠습니다.

싱글톤 컨테이너를 학습하고, @Configuration 사용 이유를 이해하는 것이 오늘의 목표입니다.

 

스프링 컨테이너는 싱글톤 컨테이너인데요. 

먼저 싱글톤 패턴이 무엇인지 알아봅시다. 

 

싱글톤 패턴

싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴입니다.

객체의 인스턴스가 2개 이상 생성되지 못하도록 막습니다.

싱글톤 패턴을 구현하는 방법에는 여러 가지가 있는데, 여기서는 객체를 미리 생성해두는 방법을 활용해 알아봅시다.

 

객체의 constructor를 public으로 만들면, 어디서든 객체를 만들 수 있겠죠.

그러나 객체를 한 개만 두고 싶으므로  누구나 만들지 못하도록 생성자를 private으로 둡니다. 

private 생성자라니! 낯선 방식인데요, 객체를 public 생성자로 만드는 대신 static 영역에 하나의 객체만을 두고 사용합니다.

 

예제 코드를 봅시다. 

public class SingletonService {
    // static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    // 객체 인스턴스가 필요할 때는 static 메서드를 통해서만 조회한다.
    public static SingletonService getInstance(){
        return instance;
    }
    //private 생성자를 이용한다.
    private SingletonService(){}
}

객체 인스턴스를 클래스의 static 영역에 만들어두었습니다.

참고로 static final은 모든 영역에서 고정된 값으로 사용하는 '상수'의 의미를 가집니다[2]. 

이번 예제 코드에서는 instance가 변하지 않을 것이기 때문에 final 키워드를 쓰신 것 같습니다. 

 

싱글톤 객체 사용 예시는 아래와 같습니다.

SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();

singletonService1과 2는 같은 객체입니다. 

 

싱글톤 패턴은, 객체를 한 개만 형성하면 되므로 메모리를 절약할 수 있다는 장점이 있습니다.

1000명의 user가 객체를 불러도, 한 개만 형성하면 됩니다.

그런데 위와 같은 방법으로 싱글톤 패턴을 적용할 때는 문제점이 있습니다.

클라이언트 코드가 구체클래스에 의존해야 하는데요.

singletonService.getInstance()와 같은 방식으로 객체를 불러야 하므로 DIP, OCP를 지키지 못합니다.

또한 싱글톤 패턴 구현 코드가 길고, 따라서 테스트하기가 어렵습니다.

생성자가 private이기 때문에 자식을 만들기도 어렵습니다. 

따라서 싱글톤 패턴은 안티 패턴(실제 많이 사용되는 패턴이지만 비효율적이거나 비생산적인 패턴)으로 불리기도 합니다[3]. 

 

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 싱글톤으로 객체를 1개씩 생성하여 관리합니다. 

지금까지 학습한 스프링 빈은 모두 싱글톤으로 관리됩니다.

스프링 컨테이너가 객체를 싱글톤으로 생성하고 관리하므로, 스프링 컨테이너는 싱글톤 컨테이너입니다.

싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 부릅니다.

 

스프링 컨테이너는 빈 저장소에 객체를 미리 저장해놓고 관리합니다. 1개만 등록해놓고 사용하는 것이죠.

앞서 스프링 컨테이너는 AppConfig를 활용해서 DIP, OCP를 지키는 것을 확인하였습니다. 

또한 저희가 싱글톤 패턴으로 코드를 작성하지 않아도 getBean으로 불러와 테스트할 수 있어서 용이하고,

private이 아닌 public 생성자를 활용하지만, 유저의 응답을 받아 사용할 때는 getBean으로 싱글톤 빈을 사용하므로 private 생성자도 사용하지 않을 수 있습니다. 

 

요약하자면, 스프링 컨테이너는 객체를 미리 싱글톤으로 만들어놓고 필요할 때(고객의 요청이 올 때) 객체를 공유합니다.

아래 사진과 같은 방식입니다. 

 

@Configuration과 싱글톤

https://passionate.tistory.com/11에서 스프링 컨테이너를 학습하면서, 스프링 컨테이너 ApplicationContext는 아래 코드로 생성할 수 있음을 배웠습니다. 

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

여기서 설정 파일로 AppConfig.class를 넘겨줍니다.

그리고 AppConfig클래스에서는 클래스 정의 윗줄에 @Configuration을 추가하고, 각 함수 위에 @Bean을 추가합니다. @Configuration이 있으면 스프링은 구성정보 클래스임을 인지하고, 메소드 위에 @Bean이 있으면 스프링 컨테이너에 스프링 빈으로 등록됩니다.

그런데 왜 스프링 컨테이너를 만들 때 AppConfig.class를 이미 명시하였는데 AppConfig에서도 @Configuration로 설정 정보임을 명시해야 하는지 의문점이 듭니다.

이유는 바로 싱글톤을 보장하기 위함입니다. 

 

AppConfig 코드를 봅시다.

@Configuration
public class AppConfig { 
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository() ,discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

스프링에 빈이 4개가 등록되어야 하죠. memberService, orderService, memberRepository, discountPolicy 4개가요.

memberRespository 함수에 중점을 두고 생각을 해봅시다.

이 함수는 몇 번 불릴까요?

 

3번 불려야 할 것처럼 보입니다. 

1) 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository() 호출
2) memberService() 로직에서 memberRepository() 호출
3) orderService() 로직에서 memberRepository() 호출

 

그렇다면 new를 하므로, 객체가 3개가 생성되어야 합니다.

그러나 객체는 1개만 생성됩니다.

빈에 등록된 memberRepository 객체, memberService와 orderService의 memberRepository 객체는 모두 같은 객체입니다.

분명 new를 3번 하는 것 같은데! 그렇다면 위의 3개가 다른 객체일 것입니다.

사실은 new를 3번 하지 않습니다.

 

@ Configuration과 AppConfig@CGLIB

스프링 빈을 등록할 때 사용하는 클래스는 정확히 말하면 'AppConfig'가 아니라 'AppConfig@CGLIB'입니다.

CGLIB는 코드를 생성하는 라이브러리입니다. AppConfig를 바탕으로 싱글톤을 보장할 수 있도록 다른 클래스를 만듭니다. 

AppConfig@CGLIB 코드에는 저희가 작성한 AppConfig 코드에 더해서 이미 스프링 컨테이너에 객체가 등록되어있는지를 확인하고, 그렇다면 스프링 컨테이너에서 찾아서 반환하는 코드가 더해져 있을 것입니다.

이렇게 AppConfig@CGLIB가 적용되게 하는 것이 @Configuration입니다.

@Configuration 어노테이션이 없다면, 그냥 AppConfig 클래스가 등록되고, new가 여러 번 불리니 싱글톤이 보장되지 않습니다.

따라서 싱글톤을 보장하여 메모리 낭비를 막기 위해서 설정 정보 파일에는 항상 @Configuration을 붙여줘야 합니다. 

 

Further study

static method(정적 메소드)

싱글톤 패턴을 설명하며 싱글톤으로 관리하는 클래스에서는 static method만을 활용해서 getInstance를 조회한다고 말씀드렸습니다. 왜 static method를 활용해야 하는지 궁금증이 생겨서 알아보았습니다. 

아래 코드를 보면 static method로 getInstance를 정의하고 있고, 외부 코드에서 SingletonService.getInstance();로 함수를 이용하고 있습니다.

public class SingletonService {
    // static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    // 객체 인스턴스가 필요할 때는 static 메서드를 통해서만 조회한다.
    public static SingletonService getInstance(){
        return instance;
    }
    //private 생성자를 이용한다.
    private SingletonService(){}
}
// 외부
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();

 

어떤 메소드가 인스턴스가 생성되지 않았더라도, 호출할 것이라면 static method를 사용해야 합니다[4]. 

따라서 이 경우에는 반드시 static method를 사용해야 합니다.

 

일반적으로는 클래스의 함수를 부를 때, instance.method()로 함수를 사용합니다. 그러나 static 함수는 Class.method()로 이용할 수 있습니다. 예제 코드를 함께 봅시다.  

class Test {
	Test () {}
	static void m1 () {}
	void m2 () {}
}

Test.m1() // O
Test.m2() // X
Test test = new Test()
test.m1() // 가능하나 권장X
test.m2() // O

// 출처: https:mygumi.tistory.com/253

 

static method인 m1은 Test 클래스에 바로 접근하여 메소드를 호출할 수 있습니다. static method가 아닌 m2는 객체를 만들어야만 호출할 수 있습니다. 

static 키워드를 사용하면 클래스가 메모리에 올라올 때, 메소드도 메모리에 올라오기 때문에 객체를 만들지 않고 Class.method()로 접근하여 함수를 사용할 수 있습니다[5]. 

따라서 싱글톤 객체는 외부에서 생성할 수 없기 때문에, 싱글톤 클래스를 외부에서 활용하려면 static 메소드를 사용해야 합니다. 

 

 

인프런  '스프링 핵심 원리 - 기본편' 강의를 듣고 공부하며 정리한 자료입니다. 

잘못된 부분은 피드백 주시면 감사하겠습니다. 

글 읽어주셔서 감사합니다 :-)

 

참고자료

[1] 스프링 핵심 원리 - 기본편, 섹션 5. 싱글톤 컨테이너 https://www.inflearn.com/course/스프링-핵심-원리-기본편

[2] https://gobae.tistory.com/3

[3] https://ko.wikipedia.org/wiki/%EC%95%88%ED%8B%B0%ED%8C%A8%ED%84%B4

[4] https://mygumi.tistory.com/253

[5] https://mangkyu.tistory.com/47

 

 

 

 

댓글