1. 웹 애플리케이션과 싱글톤
스프링은 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다. 대부분의 스프링 애플리케이션은 웹 애플리케이션이며 이때 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 보낸다. 우리가 작성한 일반적인 자바 코드는 클라이언트가 A라는 객체를 요청하면 요청 받는 횟수만큼 객체를 만들어야 한다. 예시 코드는 아래와 같다.
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer(){
AppConfig appConfig = new AppConfig();
// 두 개의 객체를 요청한다.
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
// 두 객체는 서로 다른 주소를 가리킨다.
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
- assertThat.isNotSameAs: 메모리상 같은 객체를 가리키는지 주소를 통해 비교한다.
- assertThat.isEqualTo: 객체가 같은 값을 가지고 있는지 비교한다.
이와 같은 컨테이너는 요청마다 객체를 생성하고 소멸하므로 메모리 낭비가 너무 심하다. 따라서 객체를 하나만 생성하고 공유하도록 설계하여 해결방안을 마련했다. 이러한 방식을 "싱글톤 패턴"이라고 부른다.
2. 싱글톤 패턴
싱클톤 패턴이란 클래스의 인스턴스가 무조건 1개만 생성되는 것을 보장하는 디자인 패턴이다. 따라서 객체 인스턴스가 2개 이상 생성되지 못하도록 막아야 한다. 이를 위해 아래 코드와 같이 private 생성자를 사용해 외부에서 new 키워드를 사용하지 못하게 막는다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
private SingletonService() {
}
public static SingletonService getInstance() {
return instance;
}
}
- static이 붙은 필드는 정적 멤버로 객체를 생성하지 않고 사용할 수 있는 필드와 메소드를 말한다. (따라서 클래스의 객체든 클래스 그 자체든 같은 값을 가지게 된다.)
- final이 붙으면 우리는 값을 설정한 이후 값 변경할 수 없다. final이 붙은 필드는 선언시 혹은 생성자에서 초기화할 수 있다.
- 만약 static과 final이 같이 붙게 되면 "상수"라고 부르며 위에서 말한 두 특징을 모두 가지게 된다.
- 만약 위에서 싱글톤 패턴으로 생성된 객체를 반환받으려면 다른 메서드에서 SingletonService.getInstance()를 사용하면 받을 수 있다.
싱글톤 패턴은 위 방법 말고도 구현 방법이 여러 개지만 위 방식이 가장 단순하고 안전하다. 싱글톤 패턴은 어쨋든 하나의 객체만 생성하여 각 요청마다 해당 객체를 재사용하므로 효율적이지만 아래와 같은 문제점을 가진다.
- 싱글톤 패턴을 구현하는 코드가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다. 따라서 DIP를 위반할 수 있다. 또한 이 때문에 OCP도 위반할 수 있다.
- 테스트가 힘들다.
- 내부 속성을 변경하거나 초기화하기 어렵다.
- private 생성자로 자식 클래스를 만들기 어렵다.
- 결론적으로 싱글톤 패턴은 유연성이 떨어지며 안티 패턴이라고도 불린다.
하지만 스프링 프레임워크는 이 싱글톤 패턴의 단점을 모두 제거하고 객체를 싱글톤으로 관리해준다.
3. 싱글톤 컨테이너
스프링 컨테이너는 싱글톤 패턴의 문제를 해결하면서 객체 인스턴스를 싱글톤으로 관리한다. 우리가 지금까지 사용한 스프링 빈들은 싱글톤으로 관리된다. 어쨋든 스프링 컨테이너는 싱글톤 컨테이너 역할을 하며 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리(Singleton Registry)라고 부른다.
- 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리해준다.
- 기존 싱글톤의 모든 단점을 해결하여 싱글톤 패턴을 위한 코드가 없어도 되고, DIP, OCP, 테스트 등의 문제도 상관없이 싱글톤을 사용할 수 있다.
이러한 스프링 컨테이너의 특징은 아래와 같은 코드로 확인할 수 있다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService ms1 = ac.getBean("memberService", MemberService.class);
MemberService ms2 = ac.getBean("memberService", MemberService.class);
Assertions.assertThat(ms1).isSameAs(ms2);
}
단, 스프링의 기본 빈 등록 방식이 싱글톤인 것이지 싱글톤 방식만 지원하는 것은 아니다. 요청마다 객체를 생성해 반환하는 기능도 제공한다.
4. 싱글톤 컨테이너의 주의점
싱글턴 패턴이든 싱글톤 컨테이너든 하나의 객체를 클라이언트가 공유하는 방식이므로 상태를 유지하게 설계하면 안된다. 즉, 무상태(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 this.price;
}
}
위의 price같이 상태를 유지하고 있는 필드가 있다면 아래와 같이 이전 사용자의 값이 사라져 원할한 서비스를 제공하지 못하는 등의 문제가 발생한다. 코드의 문제 설명은 주석을 참고하자.
class StatefulServiceTest {
@Test
@DisplayName("상태 유지의 문제")
void statefulServiceSingleton() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService ss1 = ac.getBean("statefulService", StatefulService.class);
StatefulService ss2 = ac.getBean("statefulService", StatefulService.class);
// 사용자1이 1만원 주문
ss1.order("사용자1", 10000);
// 사용자2가 2만원 주문
ss2.order("사용자2", 20000);
// 사용자1의 주문 금액 조회
int price = ss1.getPrice(); // 1만원 이어야 함.
Assertions.assertThat(price).isEqualTo(20000); // but 2만원이 됨. (마지막으로 등록한 것이 2만원 이므로)
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
5. @Configuration과 싱글톤
아래 AppConfig의 코드를 살펴보자.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
스프링은 @Bean을 보고 memberService()를 호출할 것이다. 이때 MemberServiceImpl 생성자 안의 memoryMemberRepository() 메서드를 보고 호출한다. 이후 orderService() 메서드를 보고 생성자로 빈을 생성하려고 할 때 다시 memoryMemberRepository() 메서드를 호출한다. 즉, memoryMemberRepository는 총 두 번 호출되는데 싱글톤이 깨지는 것이 아닐까? 그렇지 않다. orderService과 memberSevice에는 같은 memberRepository가 들어간다. 사실 스프링에서는 다음과 같은 과정을 거쳐 빈을 생성한다.
- memberService를 호출한다.
- memberRepository를 호출한다.
- orderService를 호출한다.
위와 같이 memberRepository는 한 번만 호출된다. 즉, 싱글톤이 보장된다.
6. @Configuration과 바이트코드 조작의 마법
스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해야 한다. 하지만 스프링이 자바 코드를 조작하긴 힘드며 위에서 봤던 예시에서 memberRepository는 3번 호출되어야 한다. 이 비밀은 @Configuration에 있다. 아래 테스트 코드를 살펴보자.
public class ConfigurationSingletonTest {
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean.getClass() = " + bean.getClass());
}
}
해당 결과는 다음과 같다.
사실 정상적인 클래스라면 결과는 아래와 같아야 한다.
- class com.example.projectEx1.AppConfig
이것은 내가 만든 클래스인 AppConfig가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AppConfig 클래스를 상속받은 임의의 다른 클래스로 만들고 해당 클래스를 스프링 빈으로 등록한 것이다. 그리고 조작된 바이트 코드는 아마 memberRepository()와 같은 메서드를 통해 스프링 빈을 만들때 스프링 컨테이너에 등록되어 있지 않으면 새로 객체를 생성하고 생성되어 있다면 스프링 컨테이너에서 찾아서 반환하도록 되어있을 것이다. 따라서 싱글톤이 보장되는 것이다.
- 위 테스트 코드에서는 빈을 조회할 때 AppConfig.class 타입으로 조회했다. 이는 AppConfig@CGLIB가 AppConfig의 자식이기 때문에 가능한 것이다.
- 만약 @Configuration을 사용하지 않는다면 기존 자바 코드처럼 동작하여 memberRepository()를 세 번 호출한다.
'BackEnd > Spring' 카테고리의 다른 글
[Spring] 의존관계 자동 주입 + lombok 설정 (0) | 2023.08.30 |
---|---|
[Spring] 컴포넌트 스캔 (0) | 2023.08.28 |
[Spring] 스프링 컨테이너와 스프링 빈 (0) | 2023.08.20 |
[Spring] IoC, DI, 컨테이너 (0) | 2023.08.20 |
[Spring] DIP 위반 문제 해결 (DI의 등장) (0) | 2023.08.20 |