관리 메뉴

제뉴어리의 모든것

[스프링 핵심 원리] 싱글톤 컨테이너 본문

Spring Boot/스프링 핵심 원리

[스프링 핵심 원리] 싱글톤 컨테이너

제뉴어리맨 2022. 8. 10. 02:26

 

싱글톤이란?

프로그래밍을 할때 사용되는 디자인 패턴 중 하나이다.

그렇다면 디자인 패턴이란?

디자인 패턴이란 기존 환경 내에서 반복적으로 일어나는 문제들을 어떻게 풀어나갈 것인가에 대한 일종의 솔루션이라고 한다.

그렇지만 쉽게 말해, 그냥 코딩 방법? 코딩 스타일? 정로 이해할 수 있다.

그리고 그 코딩스타일로 문제들을 특정 문제들을 해결할 수 있는것이다.

 

그럼 다시 돌아가서, 싱글톤 패턴이란!

정의된 클래스의 객체를 하나만 만들도록 하는 코딩 방법이다!

 

싱글톤이 아닌 클래스와 싱글톤인 클래스

 

  • 싱글톤이 아닌 클래스
public class BasicService{

    public BasicService() { 
      
    }
    public void logic() {
        System.out.println("비싱글톤 객체 로직 호출");
    }
}

기능이 logic()만 있는 그냥 일반적인 객체이다.

 

  • 싱글톤인 클래스
public class SingletonService {

    private static SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }

    private SingletonService() {

    }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}

1. 생성자를 private 으로 만들고 (외부에서 new로 객체 생성 막음)

2. 본 클래스의 static 멤버 필드로 자신을 선언하고 생성. (생성자가 private이여서 외부에서 new로써 객체를 못 만들게 막았으니 자신의 객체를 멤버로 가지고 있음)

3. 본 클래스의 객체를 얻어오는 메소드를 static으로 만듬 (new로 객체 생성은 안되고, 이미 static으로 만들어 놓은 객체가 있으니)

 

 

그렇다면 왜 갑자기 싱글톤을 얘기하는가?

회원관리 프로그램이 있다고 했을때, 만약 클라이언트에서 서버로 회원을 추가해달라는 요청이 들어왔다.

그럼 그 요청 정보를 처리 하기 위해 Service 역할을 하는 객체를 생성할것이고, Service 객체를 잘 처리를 하고 소멸이 될것이다.

그런데, 만약 동시에 수천대의 클라이언트가 회원 추가 요청을 한다면, Service 객체는 순간적으로 수천개가 생길것이다.

이런 상황은, 서버에게는 과도한 메모리 사용이며, 불필요한 과정이다.

그냥 하나의 객체만 만들어두고 사용하면 되는것이다.

그렇기 때문에 웹애플리케이션에서 사용되는 객체들은 대부분 싱글톤 상태여야 한다.

 

 

 

그렇다면 웹앱에서 쓰이는 객체들을 모두 싱글톤 패턴으로 적용해야 하나?

정답은 NO이다.

아래의 싱글톤패턴의 단점으로 인해 위의 예제 코드와 같이 모든 클래스들에게 싱글톤 패턴을 적용하는것은 비효율적이며, 객체지향적 프로그래밍도 아니다.

그렇다면 어떻게 스프링은 싱글톤 패턴을 적용시켜 주는것인가?

아래의 내용을 쭉 읽으면 정답을 찾을 수 있다.

 

 

싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다.
  • DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.

 

싱글톤 컨테이너

스프링 컨테이너의 또 다른 이름이다.

왜냐하면 스프링 컨테이너가 싱글톤을 보장해주기 때문이다.

싱글톤 컨테이너란 클래스들의 객체를 딱 1개씩만 만들어서 그 객체들을  관리해주는 녀석이다.

관리란 필요할때 의존성 주입도 해주고, 객체의 생성 소멸을 담당한다는것이다.

 

싱글톤 컨테이너의 역할

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
    - 이전에 설명한 컨테이너 생성 과정을 자세히 보자. 컨테이너는 객체를 하나만 생성해서 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
    - 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    - DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다

 

그렇다면 어떻게 싱글톤 컨테이너, 즉 스프링 컨테이너를 사용할 수 있나?

이전 포스트에 나와있는 코드들이 곧 사용방법이다.

즉, ApplicationContext 구현체들에게 설정파일을 넘겨 스프링빈으로 등록하면 그 객체들은 모두 싱글톤이 적용되어 있다.

 

싱글톤 방식의 주의점 

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • 무상태(stateless)로 설계해야 한다!
    - 특정 클라이언트에 의존적인 필드가 있으면 안된다.

    이 말의 의미는 싱글톤인 클래스에 멤버 필드가 있을 경우,
    해당 멤버 필드를 다른 클래스에서 막 바꾸거나 하는 경우가 있어선 안된다는것이다.

    - 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!

    위에 내용가 일맥상통한다.

    - 가급적 읽기만 가능해야 한다.

    싱글톤 클래스의 멤버 변수가 있다하더라도 읽기만 하자.

    - 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!

 

 

+ Assertions.assertThat().isNotSameAs와
Assertions.assertThat().isNotEqualTo의 차이

자바 문법의 == 와 equals 의 차이라고 보면 됨

isNotSameAs는 !=
isNotEqualsTo는 !equals() 의 의미

 

참조 : https://coding-factory.tistory.com/536

 

[Java] 문자열 비교하기 == , equals() 의 차이점

Java에서 int와 boolean과 같은 일반적인 데이터 타입의 비교는 ==이라는 연산자를 사용하여 비교합니다. 하지만 String처럼 Class의 값을 비교할때는 ==이 아닌 equals()라는 메소드를 사용하여 비교를 합

coding-factory.tistory.com

 

 

@Configuration과 싱글톤

지금까지 @Configuration과 @Bean, 그리고 ApplicationContext와 그 구현체를 사용하여 빈을 등록하는 방법을 사용하였다.

그 방법은 아래와 같다.

 

  • AppConfig 클래스 (스프링컨테이너의 설정파일)
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        System.out.println("AppConfig 내의 memberService() 메소드 호출됨!!!");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("AppConfig 내의 memberRepository() 메소드 호출됨!!!");
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        System.out.println("AppConfig 내의 orderService() 메소드 호출됨!!!");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        System.out.println("AppConfig 내의 discountPolicy() 메소드 호출됨!!!");
        return new RateDiscountPolicy();
    }
}

 

  • AppConfig 클래스를 설정파일로 사용하여 스프링 컨테이너를 사용하는 코드
public class MemberApp {
    public static void main(String[] args) {

//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);//스프링 컨테이너
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("find member = " + member.getName());
    }
}

위에 코드에서 여러 부분이 있지만, 

설정파일을 사용하여 스프링 컨테이너를 만들고 빈으로 등록한 아래 코드만 이해하면 된다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);//스프링 컨테이너
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

 

 

AnnotationConfigApplicationContext 객체는 어쨋든 AppConfig클래스의 내용을 읽어서 빈을 생성할텐데,

그렇다면 AppConfig 내의 어떤 메소드를 먼저 호출하든, 어쨋든 memberRepository() 메소드가 여러번 호출 된다.

그리고 그 메소드 안에는 new로 MemoryMemberRepository 객체를 생성한다.

그렇다면 MemoryMemberRepository 객체가 여러번 생성되는것일텐데 어째서 싱글톤이 지켜지는걸까??

그 해답은 바이트코드 조작에 있다!!

 

 

@Configuration과 바이트코드 조작의 마법

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그런데 스프링이 자바 코드까지 어떻게 하기는 어렵다. 저 자바 코드를 보면 분명 3번 호출되어야 하는 것이 맞다. 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다. 모든 비밀은 @Configuration 을 적용한 AppConfig 에 있다

 

위에 AppConfig가 존재하는 상태에서 아래의 테스트를 돌려보자.

 

  • test 패키지 하위에서 생성한 테스트 코드
public class AppConfigTest {

    @Test
    @DisplayName("AppConfig의 실체 확인")
    void AppConfigPrint() {

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String key : beanDefinitionNames) {

            BeanDefinition beanDefinition = ac.getBeanDefinition(key);

            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION)
            {
                Object bean = ac.getBean(key);

                System.out.println("key = " + key + " , value = " + bean);
            }
        }
    }
}

 

  • 결과 화면

주목할 건 appConfig 출력 부분이다.

key = appConfig , value = hello.core.AppConfig$$EnhancerBySpringCGLIB$$8d8b893a@57ac5227

 

다른 클래스들과 다르게 $$EnhancerBySpringCGLIB$$8d8b893a 이런게 붙어있다.

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

 

그리고 또 놀라운것!

memberRepository() 메소드가 1번만 호출됬다. 각 스프링 컨테이너가 메소드들을 호출시켜 빈 등록을 하기 위해선 

기본적인 자바 문법 흐름상 3번은 호출되어야 하는데 말이다.. 이것 또한 바이트코드 조작으로 인해 그렇다.

 

 

그 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다.(실제로는 CGLIB의 내부 기술을 사용하는데 매우 복잡하다.)

 

  • AppConfig@CGLIB 예상 코드

@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다. 덕분에 싱글톤이 보장되는 것이다.

 

+ 참고 AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다

 

@Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?

 

@Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 만약 @Bean만 적용하면 어떻게 될까?

(@Configuration이 붙어있는 클래스는 스프링컨테이너의 설정정보라는것을 나타내기에 스프링에서는 알아서 멤버 메소드들이 리턴하는 객체들을 스프링의 사용목적인 싱글톤을 적용시켜 주는것이다.)

 

위에 AppConfig 코드에서 

@Configuration만 주석처리하고 AppConfigTest 테스트를 다시 돌려보자.

아마도 AppConfig의 대한 출력이 아래와 같이 나올것이다

key = appConfig , value = hello.core.AppConfig@176b3f44

그럼 AppConfig 내부에서 선언 메소드들 또한 빈등록을 하기위해 호출이 됬을텐데, 바이트코드 조작이 이루어지지 않은 실제 AppConfig 클래스이기 때문에 결과 화면이 아래와 같다

memberRepository()가 여러번 호출되었다. 즉, 객체가 여러개 생긴것이다.

 

 

정리

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
  • memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
  • 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자