제육's 휘발성 코딩
Published 2021. 7. 31. 04:51
[Spring] 싱글톤 이란? 🔷 Spring/basic
반응형

싱글톤 패턴

  • 클래스 인스턴스가 1개만 생성되는 것을 보장하는 디자인 패턴
  • 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
    • 즉, private 생성자를 통해 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.
public class SingletonService {

  //1. 자기 자신을 static영역으로 가지면 객체가 하나만 존재한다.
  private static final SingletonService instance = new SingletonService();

  //2. public 으로 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회되도록 한다.
  public static SingletonService getInstance() {
      return instance;
  }

  //3. 외부에서 new 연산자로 객체 생성하는 것을 막는다.
  private SingletonService(){}

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

}

싱글톤이 필요한 상황

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 SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        // 1. 조회 : 호출 할 때마다 객체 생성
        MemberService memberService1 = appConfig.memberService();

        // 2. 조회 : 호출 할 때마다 객체 생성
        MemberService memberService2 = appConfig.memberService();

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

}
  • 객체를 생성할 때마다 새로운 객체가 출력된다.

sec4캡처1

  • 다음 그림을 보면, 요청이 올때마다 DI 컨테이너가 객체를 새로 생성한다.
  • 또한 이 객체가 DI 관계가 있다면 연관된 객체마저도 새로 생성됨으로 메모리 낭비가 심해진다.해결 방안은 객체를 1번만 생성하고, 공유하도록 설계 해야 한다. 이 방식이 바로 싱글톤 패턴이다.

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드가 많이 들어간다.
    • logic() 메서드 하나 쓰려고하는데 여러 코드가 들어감.
  • 의존관계상 클라이언트가 구체 클래스에 의존하게됨. DIP를 위반하나, 별도의 설정 클래스를 사용하면 해결은 가능하다.
  • 객체를 지정해서 가져오기 때문에 유연하게 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 유연성이 떨어지게 되고(DI 적용이 어려워짐), 안티패턴으로 불리기도 한다.
  • 동시성 문제가 발생할 수 있다. 즉, Thread에 safe하지 않다.

스프링 컨테이너

  • 스프링 컨테이너는 자동으로 싱글톤 패턴으로 객체를 생성 및 관리(싱글톤 레지스트리)한다.
    • 성능 향상
    • 빈 이름(메서드 이름), 빈 객체로 저장 (EX : memberService, MemberService@x01)
  • 기존의 싱글톤 패턴의 문제점을 모두 해결해준다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
  AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

  MemberService memberService1 = ac.getBean("memberService", MemberService.class);
  MemberService memberService2 = ac.getBean("memberService", MemberService.class);

  // 참조 값이 같은 것을 확인
  System.out.println("memberService1 = " + memberService1);
  System.out.println("memberService2 = " + memberService2);

  assertThat(memberService1).isSameAs(memberService2);
}
  • 클라이언트의 요청이 올때마다, 이미 만들어진 객체를 재사용 할 수 있다.
  • 기존에 싱글톤패턴의 설정 없이 스프링 컨테이너(AnnotationConfigApplicationContext) 사용만으로 싱글톤 패턴 적용이 가능하다.
  • 요청할 때마다 새로운 객체를 생성해서 반환하는 방법도 제공한다.
    • Bean 스코프 (거의 쓰지 않음), httpSession 등 특정 경우에만 사용 한다.

싱글톤 패턴 주의점

  • 하나의 같은 객체의 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • 무상태(stateless)로 설계해야 한다.
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 큰 장애가 발생할 수있다.
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;
    }
}
  • 상태를 유지하는 클래스 생성
class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //Thread A : A 사용자 10000원 주문
        statefulService1.order("userA", 10000);
        //Thread B : B 사용자 20000원 주문
        statefulService2.order("userA", 20000);

        //사용자 A가 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("A : price = " + price);
    }

    static class TestConfig {

        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}
  • A 사용자가 주문 후 조회하는 사이에 B사용자가 주문 시 A 사용자의 주문 조회 결과가 B사용자의 주문 조회로 출력 된다. (10000원이 나와야하는데 20000원이 나오는 문제 발생)
  • 이런 장애는 찾기도 어렵기 때문에 정말 중요하다.
  • 공유 필드는 정말 조심해야한다. 스프링 빈은 항상 무상태(stateless)로 설계하자.
public class StatefulService {

//    private int price; //상태 유지 필드

    public int order(String name, int price) {
        System.out.println("name = " + name + "price = " +price);
//        this.price = price; //여기가 문제!
        return price;
    }

}
  • 공유 필드를 없애고 파라미터로 입력 받아 해당 금액을 리턴하도록 설계한다.
class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //Thread A : A 사용자 10000원 주문
        int userAPrice = statefulService1.order("userA", 10000);
        //Thread B : B 사용자 20000원 주문
        int userBPrice = statefulService2.order("userB", 20000);

        //사용자 A가 주문 금액 조회
//        int price = statefulService1.getPrice();
        System.out.println("A : price = " + userAPrice);

    }

    static class TestConfig {

        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}
  • 무상태(stateless) 설계를 하면 정상적으로 사용자 A의 금액이 조회 된다.

@Configuratin 과 싱글톤

@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();
 }
}
  • memberService의 빈과 orderService의 빈은 MemoryMemberRepository()를 생성한다.각 다른 2개의 객체가 생성되면서 싱글톤이 깨지는 것처럼 보이지만 깨지지 않는다.
//test 용도 
public MemberRepository getMemberRepository() {
    return memberRepository;
}
@Test
void configurationTest() {
  AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

  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("memberService -> memberRepository1 = " + memberRepository1);
  System.out.println("orderService -> memberRepository2 = " + memberRepository2);
  System.out.println("memberRepository = " + memberRepository);
  Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
  Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
  • 각 클래스의 get 메서드 생성후 테스트 해본결과 같은 객체로 결과가 나온다.
  • new 연산자로 따로 실행되었지만 결과가 같다.

@Configuration 과 바이트코드의 조작

  • 스프링 컨테이너는 싱글톤 레지스트리로, 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.
  • 하지만, 자바 코드까지는 관리 해줄 수가 없다.
@Test
void configurationDeep() {
  AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
  AppConfig bean = ac.getBean(AppConfig.class);

  System.out.println("bean = " + bean.getClass());
}
  • bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$58fbf5a5 이라는 결과가 나온다.
  • 순수한 클래스라면 class hello.core.AppConfig 가 출력되어야 한다.
  • CGLIB가 붙으면 스프링이 바이트코드 조작 라이브러리르 사용해서 AppConfig 클래스를 상속 받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.
  • 바이트코드 조작으로 싱글톤이 보장되도록 해준다.
@Bean
public MemberRepository memberRepository() {

 if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
     return 스프링 컨테이너에서 찾아서 반환;
 } else { //스프링 컨테이너에 없으면
   기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
   return 반환
 }
}
  • CGLIB 예상 코드 (실제로는 더 복잡하다.)
  • 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링빈이 없으면 생성해서 스프링 빈으로 등록하고
  • 반환하는 코드가 동적으로 만들어진다.
  • AppConfig@CGLIB는 AppConfig의 자식이므로 AppConfig로 조회가 가능하다.
  • 해당 메커니즘은 AOP 에서도 동일하게 적용된다.
  • @Configuration 없이 @Bean 만 적용한다면 CGLIB가 보장되지 않는다.
    • 즉, 새로운 객체를 생성해서 싱글톤을 보장하지 못하게 된다.
    • 스프링 설정 정보에는 반드시 @Configuration을 설정하자.
  • 자동 주입 @Autowired를 사용하면 @Configuration 없어도 적용할 수 있다.

 


본 포스팅은 인프런 김영한님 강의(스프링 핵심원리 - 기본편)를 토대로 정리한 내용입니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^