본문 바로가기
  • GDG on campus Ewha Tech Blog
3-2기 스터디/Spring 입문

[9주차] 5. 싱글톤 컨테이너

by 냉냐리 2022. 6. 22.

웹 애플리케이션과 싱글톤

스프링 애플리케이션 -> 대부분 웹 -> 웹 어플리케이션은 대부분 동시요청 (ex. AppConfig.java)
요청마다 객체를 만들어냄 ....

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();
        //참조값 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
    }
}


둘 다 다르다.... 호출때마다 객체 새로 생성 .. 매우 비효율적

싱글톤 패턴

객체의 인스턴스가 오직 1개만 생성되는 패턴
=> 어플리케이션 실행 시 최초 한번만 메모리를 할당하고 그 메모리에 인스턴스를 만들어서 사용하는 디자인, 객체 2개 이상 생성 못하도록 강제해야함.

public class SingletonService {
    //static 영역에 객체를 딱 1개만 생성
    private static final SingletonService instance = new SingletonService();
    //public으로 조회시 static 통해서만 조회하게 
    public static SingletonService getInstance() {
        return instance; //얘만 조회가능
    }
    //생성 막기
    private SingletonService() {
    }
    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}
  • static 영역에 객체 인스턴트 하나 미리 생성.
  • 객체 인스턴스 필요 시 getInstance 메소드 통해서만 조회 가능.
  • 생성자를 priate로 막아서 외부에서 객체 생성되는 것을 방어.
@Test
    @DisplayName("싱글톤 패턴 적용 객체 사용")
    void SingletonServiceTest() {
        SingletonService SingletonService1 = SingletonService.getInstance();

        SingletonService SingletonService2 = SingletonService.getInstance();

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

        Assertions.assertThat(singletonService1).isSameAs(singletonService2);
        singletonService1.logic();
    }

테스트 코드로 같음을 확인
//isSameAs 인스턴스 비교
-> 스프링 컨테이너는 걍 싱글톤 패턴 기본 적용 ( 똑똑이 )

하지만 .. 싱글톤 패턴도 단점이 있음

  • 코드 추가해야됨
  • 의존관계상 클라이언트가 구체 클래스에 의존하는 문제
  • DIP를 위반 //memberserviceimpl.getInstan.... 하면서
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성 up
  • 테스트 어려움
  • 내부 속성을 변경하거나 초기화 힘듬
  • private 생성자 사용으로 자식 클래스 생성 어려움
  • 결론적으로 유연성 down
  • 안티패턴? (비효율 비생산)

싱글톤 컨테이너

스프링 컨테이너는 위의 단점을 제거한 싱글톤으로 관리한다 .. -> 싱글톤 패턴 적용 없이 객체 인스턴스를 싱글톤으로 관리.(싱글톤 레지스트리)

@Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {
        ApplicationContext 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);
        
        Assertions.assertThat(memberService1).isSameAs(memberService2);
    }

싱글톤 코드 X


요청 올때마다 동일한 멤버 서비스 반환 -> 처리 속도 up (이미 만들어진 객체 공유)
//빈 스코프(요청 시 마다 새로운 객체 생성해서 반환하는 기능)도 존재하긴 함.

싱글톤 방식의 주의점

하지만 이 방식도 주의점이 있다네요 ~ ..
싱글톤 방식은 한 객체 인스턴스가 공유되는 방식이기 때문에 상태를 무상태로 설계해야한다.
무상태 ?
-> 읽기만 가능
-> 값 수정 X
-> 필드 대신에 자바에서 공유되지 않는 것들을 사용해야함
-> 의존적 필드 존재하면 X

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;
    }
}

//Ctrl+Shift+T 단축키로 테스트 생성

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

import static org.junit.jupiter.api.Assertions.*;

class StatefulServiceTest {
    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean("statefulService",
                StatefulService.class); //빈조회
        StatefulService statefulService2 = ac.getBean("statefulService",
                StatefulService.class);
        //ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000);
        //ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000);
        //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price); //사용자B의 금액 출력함
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }

}

-> 공유필드가 만드는 치명적인 문제 ,,

public int getPrice() {
        return price;
    }

얘를 지우고 테스트 코드에서 출력 코드를 조금 수정해주면 정상 출력됨

@Configuration과 싱글톤

  • memberService 빈을 만드는 코드를 보면 memberRepository() 를 호출한다. -> 이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.
  • orderService 빈을 만드는 코드도 동일하게 memberRepository() 를 호출한다. -> 이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.
    => ??? 이게 뭐야 싱글톤 ? 이라매 ?
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;


public class ConfigurationSingletonTest {
    @Test
    void configurationTest() {
        ApplicationContext 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);
        
        //모두 같은 인스턴스를 참고
        System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
        System.out.println("orderService -> memberRepository = " + orderService.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);
      

        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}

인스턴스는 모두 같은 인스턴스가 공유되어서 사용 ( 한 번만 생성 )
-> 호출은 다른데 ? -> ?? 호출 로그를 남겨봅시다.
AppConfig.java에 다음과 같은 줄 삽입
//System.out.println("call AppConfig.출력함수");


memberRepository 1번만 호출된 걸 볼 수 있음! -> 스프링 똑똑이
하지만 어떻게 ?

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

비밀의 정답은 @Configuration

@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$$208f260
    }

?? 순수 클래스와 다르게 출력되는 모습을 볼 수 있음 -> 내가 만든 class가 아니기 때문 -> 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해 다른 클래스를 임의로 제작해 등록한 것


AppConfig.java

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

AppConfig@CGLIB 예상코드 ( CGLIB는 자식타입이라 AppConfig로 조회가능)
없으면 등록하고 등록되어 있으면 꺼내서 반환하는 형식으로 싱글톤 보장.

하지만 @Configuration 하지 않고 @Bean만 하면?
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
이런 형식으로 출력된다. (각기 다른 인스턴스를 가지고 있음)
스프링 컨테이너가 관리하지 않음 .. 스프링 빈이 아니야 .. new 해준 거랑 완전 똑같음. -> 스프링 설정정보가 있을 시에는 무조건 @Configuration을 사용합시다 ^!^

@AutoWired는 뒤에서 심도있게 ,,

댓글