[기본] 스프링의 싱글톤

kiwonkim·2021년 7월 5일
0
post-thumbnail

이전 포스팅 내용

주문에는 저장소와 할인정책이 필요한 상황. 저장소와 할인정책은 가변적이다.

  1. 주문과 저장소, 할인정책을 단순 클래스로 구현한다.

    • 한계 : 저장소와 할인정책이 변경되면 저장소와 할인정책 객체를 사용하는 주문 클래스도 뜯어고쳐야한다.
  2. 저장소와 할인정책의 부모로 인터페이스를 두고, 주문에서는 인터페이스를 참조변수로 하는 객체를 만들어 사용한다. (다형성 활용)

    • MemberRepositoy와 DiscountPolicy를 인터페이스로 만든다.
    • OrderServiceImpl에서는 인터페이스를 참조변수로 하는 인스턴스를 생성하여 비즈니스로직을 수행한다.
    • 저장소와 할인정책이 바뀌면 주문에서 저장소, 할인정책의 인스턴스 생성 시 구현체만 바꿔끼우면 된다.
    • 한계 : 저장소와 할인정책의 구현체가 바뀌면 OrderServiceImpl에서 객체 생성하는 부분도 변경되어야함.
  3. 주문의 필드에 저장소와 할인정책의 참조 변수만 두고 생성자에서 주입받도록 한다. 모든 객체를 생성해놓는 AppConfig를 만들어 놓자.

    • AppConfig에서 저장소, 할인정책 객체를 생성하고. 두 객체를 주입한 주문 객체도 생성한다.
    • 저장소, 할인정책의 구현체가 변경되면 AppConfig에서 해당 부분만 변경하면 된다.
    • 객체가 필요하면 AppConfig의 메서드로 생성하면 된다.
    • 객체를 생성 및 의존성 주입하는 AppConfig를 분리. OrderService에서는 필드에 존재하는 저장소와 할인정책을 사용하기만 하면된다.

우리는 인터페이스와 AppConfig의 도입으로 DIP를 준수할 수 있게 되었다.


의문점

인터페이스를 도입하고, 객체를 생성 및 주입하는 설정 클래스를 분리하여 주문이 저장소와 할인정책 객체를 사용하기만 하면 되도록 하여 DIP 원칙을 지켰다.

스프링은 설정 클래스를 파라미터로 받아 객체를 스프링 컨테이너 내부에 생성한다. 이를 "스프링 빈"이라 하며 스프링은 이 객체들을 관리해주며 객체 사용시 컨테이너에서 꺼내서 사용해야 한다.

그냥 Config 클래스에서 객체들을 갖다 쓰면 되는데, 왜 굳이 스프링을 도입하여 컨테이너에 객체들을 등록하고 꺼내가며 사용할까?


싱글톤

public class AppConfig {
	
    //저장소 객체 생성
    public MemberRepository memberRepository() { 	
        return new MemoryMemberRepository();
    }
	
    //할인정책 객체 생성
    public DiscountPolicy discountPolicy() { 
        return new RateDiscountPolicy();
    }
    
    //저장소와 할인정책객체 주입한 주문객체 생성
    public OrderService orderService() { 
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

}

AppConfig에서는 객체들을 생성하는 메서드들을 갖는다. 클라이언트는 여기서 객체를 요청 후 받아 사용하게 될 것이다.

여기서 문제가 생긴다.
웹은 요청이 매우 많이 일어나는 곳인데, 요청 하나마다 새로 객체를 만들어서 주면 오버헤드가 매우 심할것이다. AppConfig에서 생성하는 객체들을 싱글톤 방식으로 하여 하나의 객체를 모든 클라이언트에게 공유하도록 넘겨주면 되지않을까?

싱글톤으로 변경

public class rateDiscountPolicy implements DiscountPolicy {
    //1. static 영역에 객체 1개 생성해둠
    private static final DiscountPolicy instance 
    		= new rateDiscountPolicy();
    
    //2. public으로 객체 인스턴스 필요시 static 메서드로 생성해둔 객체 반환하도록
    public static DiscountPolicy getInstance() {
    	return instance;
    }
 	
    // 3. 생성자를 private로 막아 추가 생성 못하도록
    private rateDiscountPolicy() {
    }
}

싱글톤으로 구현하려면 객체를 생성해두고, 객체를 반환하는 메서드를 정의하고, 생성자를 private로 막아두는 번거로운 수정을 해야하고 유연성이 떨어진다.

스프링 컨테이너는 객체 인스턴스를 싱글톤으로 관리한다. 뿐만 아니라 DIP OCP 위반, private 생성자 등 유연성이 떨어지는 모든 문제를 해결하여 관리해주고 여러 편리한 기능을 제공한다.

우리는 싱글톤 사용 및 스프링이 객체를 관리함으로서 얻는 여러 이점을 활용하기 위해 스프링을 사용한다.


@Configuration과 싱글톤

우리는 Spring 컨테이너에 인스턴스를 등록하고. 이를 꺼내씀으로서 싱글톤을 실현하기로 하였다.

//빈 등록
@Configuration
public class AppConfig {
	
    @Bean
    public MemberRepository memberRepository() { 	
        return new MemoryMemberRepository();
    }
	
    @Bean
    public DiscountPolicy discountPolicy() { 
        return new RateDiscountPolicy();
    }
    
    @Bean
    public OrderService orderService() { 
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

//빈 꺼내 사용
public class test{
	
    @Test
    void configurationTest(){
      ApplicationContext ac 
              = new AnnotationConfigApplicationContext(AppConfig.class);

      Memberservice memberService = ac.getBean("memberService", MemberService.class);
    }
}

Config 클래스에 @Configuration
객체 생성 메서드에 @Bean

Config 클래스를 AnnotationConfigApplicationContext 의 파라미터로 넘기면. 해당 설정 정보의 빈을 등록한 스프링 컨테이너를 리턴하며 꺼내서 사용할 수 있다. 빈들은 모두 싱글톤 방식으로 하나의 객체씩만 생성된다.

의문이 생긴다
AppConfig를 보면 저장소와 할인정책을 빈으로 등록한다.
그 후 주문에서 의존성 주입을 위해 생성 메서드를 호출 해 저장소와 할인정책을 또 생성한다.
그러면 객체가 둘이되어 싱글톤이 깨지는게 아닌가?


스프링은 바이트조작을 활용한 CGLIB으로 이를 방지한다.
스프링은 Config 클래스를 스프링빈으로 등록하고 이를 바탕으로 다른 빈들을 등록한다. 이때 우리가 생성한 Config 클래스가 아닌 이를 상속하는 변조 Config 클래스를 빈으로 등록한다.

변조 Config 클래스는 바이트조작으로 각 Bean 생성 함수를 이미 존재하면 이를 반환하도록 변경한다. 이를 통해 싱글톤이 반드시 보장되는 것이다.

Spring은 @Configuration으로 Config 파일을 설정하고 이를 매개변수로 받아 빈들을 등록한다. Spring은 이미 빈이 존재하면 컨테이너에서 꺼내서 반환하도록 변조한 Config 자식 클래스를 스프링빈으로 등록하여. 스프링빈의 싱글톤을 보장한다.


싱글톤 주의점

싱글톤은 하나의 객체를 공유한다. 그래서 새로 객체를 생성하지 않아도 되지만 필드를 공유하므로 문제가 생길 수 있다.

public class OrderService {
	private int price;
    
    public void order(String item, int price) {
    	System.out.println("item = " + item + "price = " + price);
        this.price = price; //여기가 문제
    }
    
    public int getPrice() {
    	return price;
    }
}

위의 OrderService 객체가 싱글톤으로 공유된다 가정하자.
클라이언트A가 10000원 짜리를 주문했다. -> OrderService price가 10000이 됨.
클라이언트B가 20000원 짜리를 주문했다. -> OrderService pirce가 20000이 됨.
클라이언트A가 금액을 조회한다. -> 현재 OrderServcice price인 20000이 출력됨.

즉 클라이언트가 공유필드인 price를 변경하게 되면 A가 주문 후 B가 주문하게되면 B의 금액으로 덮어씌워지게 된다. 싱글톤은 무상태로 설계해야함에 유의해야한다.

싱글톤(스프링 빈)은 항상 상태를 유지하지 않는 무상태로 설계하자. 특히 객체의 필드는 매우 조심해야 한다.


본 글은 김영한님의 "스프링 핵심 원리 - 기본편" 강의내용 및 이해한 내용을 정리한 것입니다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

0개의 댓글