개발놀이터

싱글톤과 @Configuration 본문

Spring/Spring

싱글톤과 @Configuration

마늘냄새폴폴 2021. 12. 27. 17:10

이 포스팅은 인프런 김영한 님의 스프링 핵심 원리 기본 편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요

 

싱글톤 패턴이란 무엇일까?

 

싱글톤 패턴이란 클래스의 인스턴스가 딱 한개만 생성되는 것을 보장하는 디자인 패턴이다. 인스턴스가 딱 한개만 생성되지 않는다면 고객이 요청할 때마다 인스턴스를 새로 만들어야한다. 인스턴스를 계속 새로 만들면 메모리 낭비가 심하기 때문에 인스턴스는 하나만 만드는것이 좋다.

 

public class SingletonService {
	//1. static 영역에 객체를 딱 1개만 생성해둔다.
	private static final SingletonService instance = new SingletonService();
	//2. public으로 열어서 객체 인스터스가 필요하면 이 static 메서드를 통해서만 조회하도록
	허용한다.
	public static SingletonService getInstance() {
		return instance;
	}
	//3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
	private SingletonService() {
	}
	public void logic() {
		System.out.println("싱글톤 객체 로직 호출");
	}
}

 

테스트코드로 정말 인스턴스가 딱 한개만 생성되는지 확인해보자

@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
	//private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
	//new SingletonService();
	//1. 조회: 호출할 때 마다 같은 객체를 반환
	SingletonService singletonService1 = SingletonService.getInstance();
	//2. 조회: 호출할 때 마다 같은 객체를 반환
	SingletonService singletonService2 = SingletonService.getInstance();
	//참조값이 같은 것을 확인
	System.out.println("singletonService1 = " + singletonService1);
	System.out.println("singletonService2 = " + singletonService2);
	// singletonService1 == singletonService2
	assertThat(singletonService1).isSameAs(singletonService2);
	singletonService1.logic();
}

테스트를 돌려보면 정말 인스턴스가 하나만 생성되는 것을 확인할 수 있다. 

 

하지만 이렇게 좋아보이는 싱글톤 패턴에도 문제점이 엄청 많다.

 

1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.

2. 의존관계상 클라이언트가 구체 클래스에 의존한다. ( SingletoinService.getInstance() )

3. 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.

4. 테스트하기가 어렵다.

5. 내부 속성을 변경하거나 초기화하기 어렵다.

6. private 생성자로 자식 클래스를 만들기 어렵다.

7. 결론적으로 유연성이 떨어진다.

8. 때문에 안티패턴으로 불리기도 한다.

 

 

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다. 어떻게 가능한지 살펴보자

 

우선 그전에 AppConfig를 다시 살펴보자

@Configuration
public class AppConfig {

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

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

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

    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

여기서 MemberRepository를 잘 보면 빈으로 등록할 때 MemoryMemberRepository가 호출되고 MemberServiceImpl이 호출될 때 한번 더 호출되고 OrderServiceImpl 까지 호출된다면 세번 호출된다. 세번 호출되니까 싱글톤이 깨지는거 아닌가? 테스트로 확인해보자

 

public class SingletonTest {

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("세번 호출되는지 확인하는 테스트")
    void singletonTest() {
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemoryMemberRepository memberRepository = ac.getBean("memberRepository", MemoryMemberRepository.class);

        System.out.println("memberService = " + memberService.getMemberRepository());
        System.out.println("orderService = " + orderService.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);
    }
}

다 같은 인스턴스가 참조되는 것을 확인할 수 있다. 이게 어떻게 된일일까?

 

비밀은 바로  @Configuration에 있다. 

 

AnnotationConfigApplicationContext구현체에서 클래스정보를 넘기면 해당 클래스도 스프링빈으로 등록되는데 이 AppConfig의 값이 뭐가 나오나 찍어보자

 

@Test
void configurationDeep() {
	ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
	//AppConfig도 스프링 빈으로 등록된다.
	AppConfig bean = ac.getBean(AppConfig.class);
	System.out.println("bean = " + bean.getClass());
	//출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}

원래 내가 원했던 값은 class hello.core.AppConfig까지인데 뒤에 이상한게 많이 붙어있다. 우린 여기서 CGLIB에 주목하자 바로 비밀이 여기에 있기 때문이다.

 

이것은 바로 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 클래스를 만들고 그 다른 클래스를 스프링 빈으로 등록한 것이다. 

 

CGLIB의 코드는 훨씬 복잡하겠지만 예상되는 코드는 다음과 같다.

@Bean
public MemberRepository memberRepository() {
	if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면) {
    	return 스프링 컨테이너에서 찾아서 반환
    }
    else {
    	기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
        return 반환
    }
}

 

CGLIB를 통해 우리는 안전하게 싱글톤을 사용할 수 있다. 앞서 말한 싱글톤패턴의 문제점을 다 해결하면서 싱글톤을 보장해주는 것이다. 그렇다면 @Configuration을 빼면 어떻게 될까? 곧바로 싱글톤이 깨지는 것을 볼 수 있다. 때문에 설정정보를 가진 곳에는 @Configuration을 붙여주자

 

다음 포스팅은 컴포넌트스캔과 자동의존관계주입에 대해서 포스팅할 예정이다. 

'Spring > Spring' 카테고리의 다른 글

@Autowired  (0) 2021.12.28
컴포넌트 스캔과 자동 의존 관계 주입  (0) 2021.12.27
스프링을 사용해서 DI 구현하기  (0) 2021.12.26
순수한 자바로 DI 구현하기  (0) 2021.12.21
객체지향 설계 5원칙 SOLID  (0) 2021.12.20