[스프링&JPA 스터디] #5 싱글톤 컨테이너

오예찬·2023년 7월 12일
0

spring&jpa 스터디

목록 보기
5/15

싱글톤 컨테이너


싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴

싱글톤 패턴의 장점

  • 순수한 DI 컨테이너로 만든 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다. 따라서 고객 트래픽이 많아질 경우 메모리 낭비가 심하다는 단점이 있다.

  • 싱글톤 패턴은 객체를 한 개만 생성하고 공유함으로써 메모리 낭비를 막을 수 있다.

순수한 DI 컨테이너 예시


싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계 상 클라이언트가 구체 클래스에 의존한다. -> DIP 위반
  • 클라이언트가 구체 클래스에 의존하므로 OCP 원칙을 위반할 가능성이 높아진다.
  • 테스트 하기 어렵다.
  • 내부 속성을 변경하거나 초기화하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
    • 싱글톤 패턴을 위한 추가적인 코드 필요X
    • DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.


싱글톤 컨테이너 적용 후

  • 스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해 효율적으로 재사용할 수 있다.

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나마나 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.

  • 반드시 무상태(stateless)로 설계해야 한다.

    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.

상태를 유지할 경우 발생하는 문제점 예시(StatefulService)

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

상태를 유지할 경우 발생하는 문제점 예시(StatefulServiceTest)

package hello.core.singleton;

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;

public 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);
 
 		Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
 	 }
     
 	static class TestConfig {
    
 	@Bean
 	public StatefulService statefulService() {
 		return new StatefulService();
 	}
  }
}
  • 단순하게 설명하기 위해, 실제 쓰레드는 사용X
  • TreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다고 가정.
  • StatefulServiceprice필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경.
  • 사용자A의 주문 금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나옴.
  • 진짜 공유필드는 조심해야 한다! 스프링 빈은 항상 무상태(stateless)로 설계하자.

@Configuration

  • 스프링에서 Bean을 수동으로 등록하기 위해서는, 설정 class 위에 @Configuration을 추가하고, @Bean을 사용하면 된다.

  • 이때 메서드 이름으로 빈의 이름이 결정된다. 따라서 중복된 빈 이름이 존재하지 않도록 주의해야 한다.

@Configuration
public class SomeConfig {

    @Bean
    public Resource shine() {
        return new Resource();
    }

}
  • 일반적으로 위와 같이 의존성 주입을 위해서 @Configuration을 사용하게 된다.
  • 스프링 컨테이너는 @Configuration이 붙어 있는 클래스의 @Bean을 빈으로 등록한다.

@Configuration의 역할

  • Bean을 등록할 때 싱글톤이 되도록 보장해준다.
  • 스프링 컨테이너에서 Bean을 관리할 수 있게 됨.

다음과 같은 클래스가 있다.

public class MyBean {

    public MyBean() {
        System.out.println("MyBean instance created");
    }
}
public class MyBeanConsumer {

    public MyBeanConsumer(MyBean myBean) {
        System.out.println("MyBeanConsumer created");
        System.out.println("myBean.hashCode() = " + myBean.hashCode());
    }
}

AppcConfig 코드

@Configuration
public class Appconfig {

    @Bean
    public MyBean myBean() {
        return new MyBean();
    }

    @Bean
    public MyBeanConsumer myBeanConsumer() {
        return new MyBeanConsumer(myBean());
    }
}
  • 위 파일을 실행하면 "MyBean instance created"가 총 몇 번 호출될까?
*MyBean instance created
*MyBeanConsumer created
*myBean hashcode = 1647766367

MyBean instance created은 한 번만 출력될까? 원래 자바 코드대로 라면 MyBean instance created은 두 번 출력되어야 한다.

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

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다. 모든 비밀은 Configuration을 적용한 AppConfig에 있다.

@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$$bd479d70
}
  • 사실 AnnotationConfigApplicationContext에 파라미터로 넘긴 값은 스프링 빈으로 등록된다. 그래서 AppConfig도 스프링 빈이 된다.

AppConfig스프링 빈을 조회해서 클래스 정보를 출력하면 다음과 같다.

bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70

하지만 순수한 클래스라면 다음과 같이 출력되어야 한다.

class hello.core.AppConfig

그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다. 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 Appconfig 클래스를 상속박은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.

  • 그 임의의 다른 클래스가 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다.

  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.

@Configuration 을 적용하지 않고, @Bean 만 적용하면?

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

//@Configuration 삭제
public class AppConfig {
}

실행

bean = class hello.core.AppConfig
  • 결과를 보면 AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된 것을 확인할 수 있다.

*MyBean instance created
*MyBean instance created
*MyBeanConsumer created
*myBean hashcode = 1647766367
  • myBean() 메서드가 호출될 때마다 new로 새로운 MyBean을 생성하기 때문에 MyBean instance created이 2번 호출된다.
  • 당연히 인스턴스가 같은지 테스트하는 코드도 실패한다.

@Configuration 정리

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
  • 크게 고민할 것 없이 스프링 설정 정보는 항상 @Configuration을 사용하면 된다.

참고

스프링 핵심 원리 - 기본편 (김영한)
Blog-Shine

profile
안녕하세요. 반갑습니다.

0개의 댓글