Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- 카카오
- 코딩테스트
- Kotlin
- Java
- 자바
- 리눅스마스터 1급 정리
- 코테
- map
- java 백준 1차원 배열
- JavaScript
- 프로그래머스
- Linux
- 반복문
- 고잉버스
- 월간코드챌린지
- 리눅스
- GoingBus
- toCharArray
- 백준 java
- 개발자 회고록
- 리눅스마스터1급
- 명령어
- 연습문제
- 백준 javascript
- 자바스크립트 코딩의 기술
- 리눅스마스터 3과목
- 문자열
- 스프링 빈
- Memoir
- 스프링 컨테이너
Archives
- Today
- Total
hoon's bLog
Spring gradle project 스프링 핵심 원리 기본편 | 싱글톤 컨테이너 Singleton Container @Configuration 본문
IT/Spring
Spring gradle project 스프링 핵심 원리 기본편 | 싱글톤 컨테이너 Singleton Container @Configuration
개발한기발자 2024. 5. 20. 10:19반응형
본 포스팅은 인프런에 있는 인터넷 강좌인,
김영한 강사님의 스프링 핵심 원리 기본편을 공부하며,
개인적으로 공부하고, 정리하는 용도로 포스팅을 해보겠다.
Spring gradle project 환경설정 및 회원 가입 서비스 예제 만들기
Spring gradle project 주문/할인 도메인 설계
Spring gradle project 객체 지향 원리 적용
Spring gradle project AppConfig 리팩토링 OCP 위반 해결 및 중복 제거
Spring gradle project 좋은 객체 지향 설계 5가지 원칙 적용 및 스프링 전환
Spring gradle project 스프링 컨테이너와 스프링 빈
Spring gradle project BeanFactory, ApplicationContext 이해 및 차이
Spring gradle project XML 설정 사용, BeanDefinition, 스프링 빈 설정 메타 정보
Spring gradle project 웹 애플리케이션과 싱글톤, WebApplication and Singleton
싱글톤 컨테이너, @Configuration과 싱글톤
싱글톤 방식의 주의점
- 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안 된다.
- 무상태(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 price;
}
}
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);
// Thread A: A 사용자 10000원 주문
statefulService1.order("userA", 10000);
// Thread B: B 사용자 20000원 주문
statefulService2.order("userB", 20000);
// Thread A: 사용자 A 주문 금액 조회
int price = statefulService1.getPrice();
// Thread A: 사용자 A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);
// Assertion
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
- ApplicationContext를 사용하여 TestConfig 설정 클래스에서 정의된 빈을 로드하고, AnnotationConfigApplicationContext는 Java 기반의 설정 클래스에서 빈을 로드하는 데 사용된다.
- statefulService1과 statefulService2는 같은 싱글턴 인스턴스를 참조하고 있다.
여기서 두 개의 다른 스레드가 같은 서비스 인스턴스에 접근해서 order 메서드를 호출해서, statefulService1은 사용자 A의 주문을 10000원에 처리하고, 동시에 statefulService2는 사용자 B의 주문을 20000원에 처리한다. - 주문 금액을 저장하는 변수가 서비스 내부에 상태로 관리되기 때문에, 사용자 A가 주문 금액을 조회할 때 사용자 B의 주문 금액이 출력되는데, 이것이 바로 싱글턴 서비스가 상태 정보를 갖게 되면 생길 수 있는 문제점이다.
- 따라서, 싱글턴 빈은 상태를 가지지 않는 게 좋고, 상태가 필요하면 각 요청이나 사용자별로 독립적인 빈 인스턴스를 생성하는 방식을 고려해야 한다.
@Configuration과 싱글톤
@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();
}
...
}
- memberService 빈을 만드는 코드를 보면 memberRepository()를 호출하면서, new MemoryMemberRepository()를 호출
- orderService 빈을 만드는 코드도 동일하게 memberRepository()를 호출하면서 new MemoryMemberRepository()를 호출
- 결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것처럼 보인다.
- 스프링 컨테이너를 이용해 이 문제를 해결해 보자!
검증을 위한 코드 추가
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
package hello.core.singleton;
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);
}
}
- getMemberRepository() 메서드를 호출하여 각 서비스 내의 MemberRepository 인스턴스를 출력해 확인하고, 직접 memberRepository 빈도 가져와서 출력
- MemberRepository가 실제로 컨테이너로부터 직접 가져온 memberRepository 빈과 동일한 인스턴스인지 확인해서, 모두 true면, 스프링이 성공적으로 싱글톤을 보장하고 있는 것이고, 설정 클래스가 제대로 작동하고 있는 것이다.
AppConfig에 호출 로그 남김
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
//1번
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
//1번
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository"); return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
// 출력
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.orderService
}
- 스프링 컨테이너가 각각 @Bean을 호출해서 스프링 빈을 생성한다
- 그래서 memberRepository()는 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository(), memberService() 로직에서 호출, orderService() 로직에서 호출 총 3번을 하는 것 같지만, 결과는 모두 1번만 호출한다.
@Configuration과 바이트코드 조작의 마법
- 스프링 컨테이너는 싱글톤 레지스트리다.
- 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.
- 위 코드를 보면 분명 3번 호출되어야 하는 것이 맞다.
- 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
@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도 스프링 빈이 된다.
- 그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다.
- 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다!
- @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어지기 때문에, 싱글톤이 보장되는 것이다.
@Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 될까?
- AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된 것을 확인할 수 있다.
// 출력
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.orderService
// call AppConfig.memberRepository
// call AppConfig.memberRepository
- 출력 결과를 통해서 MemberRepository가 총 3번 호출된 것을 알 수 있다.
- 1번은 @Bean에 의해 스프링 컨테이너에 등록하기 위해서이고, 2번은 각각 memberRepository()를 호출하면서 발생한 코드다.
정리
- @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
- memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
- 스프링 설정 정보는 항상 @Configuration을 사용하자.
끝.
728x90
반응형