싱글톤 컨테이너

강한친구·2022년 4월 6일
0

Spring

목록 보기
4/27

웹 어플리케이션과 싱글톤

대부분의 스프링 어플리케이션은 웹 어플리케이션이다. 그 밖에것도 가능하다.
웹 어플리케이션에서는 보통 여러 고객이 동시에 요청을 한다.

예를 들어, A B C 고객이 동시에 memberService를 요청한다고 하자. 그러면 DI컨테이너 (AppConfig)는 new memberService를 생성해서 계속 반환해준다.

즉, 3회 신청시 3회 반납이 오는것이다.

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class SingletoneTest {

    @Test
    @DisplayName("Pure DI Container")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        //1. call
        MemberService memberService1 = appConfig.memberService();
        //2. call2
        MemberService memberService2 = appConfig.memberService();

        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService1).isNotSameAs(memberService2);

    }
}

이렇게 콜 할때 마다 객체가 여러개 생기는걸 볼 수 있다. 만약 리퀘가 오만번오면 과연 감당할 수 있을까?
안된다.

따라서 하나만 만들고 공유하는 방식을 사용하는것이 좋다.

싱글톤

  1. 스태틱 영역에 인스턴스를 하나 생성한다.
  2. 이 객체 인스턴스가 필요하면 오직 하나의 getInstance메서드로만 조회 가능하게 하고 이 메소드는 항상 같은 인스턴스를 반환한다.

3. private 생성자를 만들어서 혹시라도 외부에서

public static void main(String[] args) {
        SingletonService ss = new SingletonService();
    }

를 써서 호출하는것을 막아야한다.

package hello.core.singleton;

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }

    private SingletonService() {

    }
@Test
    @DisplayName("Singleton Test")
    void SingletonService() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        Assertions.assertThat(singletonService1).isSameAs(singletonService2);
        Assertions.assertThat(singletonService1).isInstanceOf(SingletonService.class);

이렇게 테스트를 해보면 싱글톤은 하나만 된다는것을 알 수 있다.

이 밖에 싱글톤 구현방법은 자세한 내용은 이 글을 참고하자.

스프링은 알아서 싱글톤을 적용해준다!

싱글톤의 문제

  1. 싱글톤을 구현하려면 코드를 많이 써야한다
  2. DIP를 위반하고 유연성이 떨어진다
  3. 테스트하기도 어렵고 자식클래스 만들기도 어렵다
  4. 단점이 너무 많아서 안티패턴 소리도 듣는다.

하지만 스프링에서는 단점은 전부 제거하고 객체관리만 싱글톤으로 해준다

싱글톤 컨테이너

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

위에 퓨어 컨테이너 코드에서 스프링 컨테이너 코드로 바꾸면, 이렇게 알아서 잘 바꿔준다.

테스트 돌려보면, 알아서 똑같은 객체를 잡아주는것을 알 수 있다.

주의점

싱골톤은 무상태로 설계되어야한다.

  • 특정 클라이언트에 의존적인 필드가 존재하면 안되고
  • 특정 클라이언트에 의해 필드값이 수정되면 안되고
  • 가급적 읽기만 해야하고
  • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, 쓰레드로컬등을 사용해야 한다.

빈의 필드에 공유값이 들어가면 장애가 발생할 수 있다!

package hello.core.singleton;

public class StatefulService {

    private int price;

    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        this.price = price;
    }

    public int getPrice() {
        return price;
    }
}

이런 코드가 있을 때, private int price는 필드에 값을 저장한다. 그리고 특정 클라이언트에 의해 값이 변경되는 문제가 생긴다.

이러면 멀티 쓰레드에서 각 필드에 접근하다보면 값이 덮어씌어지는것처럼, 문제가 생기게 된다.

package hello.core.singleton;

public class StatefulService {

    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        return price;
    }

    public int getPrice() {
        return price;
    }
}

이를 방지하려면, 필드에 값을 저장하지 말고, 받은 price를 바로 돌려주는 방식으로 처리해야한다.

Appconfig의 비밀

Appconfig에는 여러코드가 있는데 아래 예시를 보자

@Bean memberService -> new MemoryMemberRepository 
@Bean orderService -> memberRepository() --> new MemoryMemberRepository --> ???

memberSerivce를 호출하면 memberRepository가 호출되고 이는 MemoryMemberRepository를 호출한다.

OrderService를 호출하면 이는 memberRepository()를 호출하는데 이렇게 하면 new MemoryMemberRepository가 또 생성된다. 이게 싱글톤이 될까?

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberRepository = " + memberRepository);
        System.out.println("memberRepository1 = " + memberRepository1);
        System.out.println("memberRepository2 = " + memberRepository2);

이렇게 각각 호출하고 비교하는 코드를 작성해보면, 이게 다 같은 인스턴스를 공유하고 있는것을 알 수 있다.

하지만 AppConfig에서는 분명 new가 두번 부르니깐 다른 인스턴스를 써야만 할것같다.

실제로 검증을 해보면, memberRepository 메소드가 1회만 호출된것을 알 수 있다.

이는 스프링의 싱글톤 보장기능이다.

Config와 바이트조작

실제로 클래스명을 출력해보면 xxxCGLIB가 붙는 이상한 클래스가 나온다.

이는 스프링이 내가 만든 Appconfig를 상속받는 다른 클래스를 하나 만든 것이다. 그리고 이 다른 클래스를 컨테이너에 등록하는것이다.

이 방식을 통해 싱글톤을 보장한다.

만약 이미 등록이 되어있는거면 거기서 찾아서 꺼내온다는 의미이다.

CGLIB은 자식이라 부모조회시 자식이 끌려나오는 원리에 의해 조회된다.

@Configuration

이게 빠지면, 위에서 말한 CGLIB이 적용되지 않는다. 내가 만든 클래스 그 자체가 Bean으로 등록이 되는것이다.
대신, 이 방법은 싱글톤을 보장하지 않는다.

즉, 스프링 컨테이너에 등록이 되지 않는것이다.

물론 이 방법 말고도 DI주입기법중 하나인 AutoWired를 쓰면 해결될 수 도 있다.

결론

@Configuration을 빼먹지말자...
스프링이 해주는데 굳이 안쓸 이유가?

0개의 댓글