출처 : 김영한님의 스프링 핵심 원리 - 기본편 인프런 강의
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
웹 어플리케이션과 싱글톤
스프링은 대부분 웹 애플리케이션을 개발하는 데 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다. 따라서 스프링 컨테이너에 등록된 스프링 빈 객체에 동시다발적으로 많은 요청이 몰리게 될텐데 그럴때마다 객체를 생성해서 요청에 대한 응답을 한다면 메모리 낭비와 더불어 매우 비효율적인 설계가 될 것이다.
스프링 컨테이너가 아닌 순수 자바 코드로 설계된 애플리케이션을 먼저 살펴보면 기존의 순수 DI 컨테이너인 AppConfig는 클라이언트가 요청할 때마다 new로 객체를 생성해서 반환한다. 이렇게 되면 JVM 메모리에 계속해서 객체가 생성/삭제되고 요청이 많이지면 많아질수록 매우 비효율적으로 메모리 공간을 사용하게 되는 문제가 생긴다.
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);
//memberService1 != memberService2
assertThat(memberService1).isNotSameAs(memberService2);
}
}
해결방안은 해당 객체가 딱 1개만 생성되고, 동일한 요청에 동일한 객체가 공유되도록 설계하는 싱클돈 패턴을 적용해야 한다.
싱글톤 패턴
클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴으로 한 자바 서버 안에서 객체 인스턴스가 하나만 생성되도록 보장하는 원리이다. 이를 위해 private 생성자를 이용해 외부에서 임의로 new 키워드로 생성하지 못하도록 막아야 한다. 오로지 최초의 생성자를 통해서만 1번 생성되는 것이다.
이 싱글톤 패턴으로 작성한 SingletonService를 테스트 코드로 정말 한번만 생성되고 여러번 요청, 조회해도 동일한 인스턴스를 반환하는 지 확인한다.
같은 인스턴스가 호출되는 것을 볼 수 있다.
그러나 싱글톤 패턴은 수 많은 문제점들을 가지고 있다.
- 많은 코드
- 구체클래스.getInstance() 방식으로 꺼내야 해서 의존관계상 클라이언트가 구체 클래스에 의존해서 DIP원칙과 OCP원칙을 위반하게 된다.
- 테스트하기 어렵다.
- 내부 속성 변경, 초기화 어려움
- private 생성자로 자식 클래스 만들기 어려움
- 유연성이 떨어져 안티패턴으로 불리기도 한다.
싱글톤 컨테이너
- 스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서 객체 인스턴스를 싱글톤으로 관리한다.
- 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 싱글톤 컨테이너 역할을 하는데 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
- 싱글톤의 모든 단점을 없애고 객체를 싱글톤으로 유지한다.
- 지저분한 코드 제거 가능
- DIP, OCP, 테스트, private 생성자로부터 자유롭게 개발 가능
스프링 컨테이너의 싱글톤 속성을 테스트해보자
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//1. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
//2. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
//참조값이 같은 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberService1 == memberService2
assertThat(memberService1).isSameAs(memberService2);
}
스프링 컨테이너를 이용하면 고객의 요청이 올 때마다 이미 만들어진 동일한 객체를 공유해서 반환한다.
싱글톤 방식의 주의점
싱글톤을 사용한다면 여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안되고 무상태(stateless)로 설계해야 한다.
무상태(stateless) 설계
- 특정 클라이언트에 의존적인 필드 X
- 특정 클라이언트가 값을 변경할 수 있는 필드 X
- 읽기 모드만 제공 (수정 X)
- 필드 대신 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용
- 스프링 빈의 필드에 공유 값을 설정하게 되면 큰 장애 발생 가능
상태(stateful) 설계의 문제점
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;
}
}
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();
}
}
}
StatefulService의 price 필드는 공유되는 필드인데 지금 특정 클라이언트가 값을 변경하고 있다. 사용자 A가 주문하려고 조회하는 사이에 사용자 B의 주문이 처리된 것이다. 이렇게 되면 B가 필드 값을 바꿔서 A가 원하는 값을 얻을 수 없다.
실무에서 이런 실수가 종종 나오는데 정말 큰 장애를 불러 일으킨다. 싱글톤을 사용할 때에는 절대 절대 공유 필드가 존재하면 안되고 무상태(stateless)로 설계해야 한다!!
무상태(stateless) 설계
먼저 공유 필드를 없애고 파라미터로 들어온 price를 바로 return해준다.
package hello.core.singleton;
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;
}
// 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);
// ThreadA: A 사용자 10000원 주문
int userAPrice = statefulService1.order("userA", 10000);
// ThreadB: A가 주문하고 조회하려는 사이 B 사용자 20000원 주문
int userBPrice = statefulService2.order("userB", 20000);
// ThreadA: 사용자A 주문 금액 조회
// int price = statefulService1.getPrice(); // 기대값: 10000
System.out.println("price = " + userAPrice); // 출력값: 10000
System.out.println("price = " + userBPrice); // 출력값: 20000
// Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
이렇게 되면 각각 원하는 값을 얻을 수 있게 된다.
@Configuration과 싱글톤
스프링 컨테이너는 객체를 싱글톤으로 관리한다고 했는데 이전에 작성한 AppConfig를 보면 의아한 점이 있다.
@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();
}
...
}
@Bean memberService -> new MemoryMemberRepository();
@Bean orderService -> new MemoryMemberRepository();
@Bean MemberRepository -> new MemoryMemberRepository();
이러면 MemoryMemberRepository를 3번 호출해서 싱글톤이 깨지는 거 아닌가?
그렇게 보이지만 스프링 컨테이너는 이러한 문제도 알아서 해결하고 싱글톤을 유지해준다.
이를 직접 테스트하기 위해 MemberServiceImpl과 OrderServiceImpl에 테스트용 코드를 추가한다.
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
출력 결과를 보면 호출도 제대로 3번 되고 싱글톤도 잘 유지되는 것을 볼 수 있다.
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
memberService -> memberRepository1 = hello.core.member.MemoryMemberRepository@13e547a9
orderService -> memberRepository2 = hello.core.member.MemoryMemberRepository@13e547a9
memberRepository = hello.core.member.MemoryMemberRepository@13e547a9
Process finished with exit code 0
@Configuration과 바이트코드 조작의 마법
이 모든 비밀은 @Configuration을 적용한 AppConfig에 있다.
@Test
void configurationDeep() {
// AnnotationConfigApplicationContext의 파라미터로 AppConfig.class를 넘겨줄 때 자동으로 스프링 빈 등록해줌
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
당연히 순수한 클래스라면 hello.core.AppConfig가 출력되어야 한다. 그런데 예상과는 다르게 출력 결과에는 xxxCGLB가 붙으면서 상당히 복잡한 결과물이 나온다.
이것은 내가 만든 클래스가 아닌 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록했기 때문이다.
그 임의의 클래스가 바로 싱글톤이 보장되도록 해주는 것이다.
AppConfig@CGLIB의 예상 로직은 아마 스프링 빈에 이미 등록되어 있다면 찾아서 반환하고 없으면 기존 로직을 호출해서 생성+등록을 할 것이다.
AppConfig@CGLIB는 AppConfig의 자식 타입이므로 AppConfig 타입으로 조회 가능
그렇다면 @Configuration을 적용하지 않고 @Bean만 적용한다면 어떻게 될까?
AppConfig의 @Configuration을 삭제해준 후 테스트를 실행해보자
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
bean = class hello.core.AppConfig
순수한 AppConfig가 스프링 빈에 등록된 것을 볼 수 있다. 하지만 호출을 보면 MemberRepository가 총 3번 호출된 것을 알 수 있다.
이 상태로 싱글톤이 유지되고 있는지 확인하기 위해 아까 해본 조회 테스트를 해보면 다음과 같이 나온다.
다 다른 인스턴스를 받게 되는 것을 통해 싱글톤이 깨졌다는 사실을 알 수 있다. 스프링 빈이 아닌 그저 new로 새로 만들어서 주입받았다는 뜻이다.
물론 @Bean이 붙으면 스프링 빈으로 등록은 되지만 싱글톤이 보장되지는 않는다. 따라서 스프링 설정 정보는 항상 @Configuration을 사용해 싱글톤을 보장하도록 설계하자
'Spring > Spring 핵심 원리 - 기본' 카테고리의 다른 글
[Spring] 의존관계 자동 주입 (0) | 2022.02.25 |
---|---|
[Spring] 컴포넌트 스캔 (0) | 2022.02.23 |
[Spring] 스프링 컨테이너와 스프링 빈 (0) | 2022.02.22 |
[Spring] 객체 지향 원리 적용 (0) | 2022.02.22 |
[Spring] 순수 자바 코드로 설계하기 (0) | 2022.02.21 |