티스토리 뷰
목차
1. DI 컨테이너
2. 스프링 컨테이너
3. 싱글톤 컨테이너
1. DI 컨테이너
회원을 생성하고 회원 등급에 따라 할인 정책을 적용해 상품에 대한 주문을 처리하는 서비스이다.
주문 서비스의 도메인 구조가 위와 같을 때,
회원 저장 역할은 데이터베이스가 변경됨에 따라 함께 변경될 수 있고
할인 정책 역할은 회원의 등급에 따라 다른 정책을 적용하는 등 변경 가능성이 높다.
다음과 같이 주문 서비스가 회원 저장소와 할인 정책의 구현체에 의존하면 확장에 의한 변경에 대응할 수 없게 된다.
코드를 보면 주문 서비스가 회원 저장소의 구현체인 MemoryMemberRepository와 할인 정책의 구현체인 FixDiscountPolicy에 의존하고 있다.
데이터베이스가 변경되거나 할인 정책이 변경되면 비즈니스 코드인 OrderServiceImpl 클래스를 수정해 주어야 한다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
따라서 OCP와 DIP원칙에 따라 다음과 같이 역할과 구현을 분리해 주어야 한다.
역할을 추상화 하면 구현 객체를 자유롭게 조립하면서 유연하게 회원 저장소와 할인 정책을 변경할 수 있다.
회원 저장소와 할인 정책의 역할을 정의하는 인터페이스와 역할을 구현하는 클래스를 만들고 주문 서비스가 추상에 의존하도록 설정하였다.
아직은 구현 객체가 주문 서비스 내에 없기 때문에 NPE(null pointer exception)가 발생하고 애플리케이션은 정상적으로 동작하지 않는다.
다음으로 SRP원칙에 따라 구현 객체를 생성하고 연결하는 책임을 별도의 클래스가 갖도록 위임하고 주문 서비스는 해당 객체를 전달받아 사용하며 주문에 대한 기능에만 집중하도록 할 수 있다.
구성 역할을 하는 AppConfig 클래스를 만들어 사용한다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
애플리케이션을 실행하는 시점에 AppConfig에서 필요한 객체를 꺼내 전달하고
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
}
}
주문 서비스에서는 생성자를 통해 객체를 전달받아 사용한다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
AppConfig의 등장으로 객체를 생성하고 구성하는 영역과 사용 영역으로 관심사가 분리되었고 회원 저장소와 할인 정책이 변경되어도 사용 영역은 전혀 영향을 받지 않게되었다.
위와 같이 필요한 구현 객체를 생성하고 사용하며 프로그램의 제어를 스스로 하지 않고 AppConfig 처럼 외부에서 프로그램의 흐름을 제어하는 것을 제어의 역전(IoC)이라고 한다. 사용 영역에서는 추상에 의존하며 프로그램의 제어가 역전되었기 때문에 외부에서 전달되는 객체에 신경쓰지 않고 자신의 로직을 실행하는 역할만 담당하게 된다.
애플리케이션 실행 시점(런타임)에 외부에서 실제 객체를 생성하고 사용 영역에 전달해서 실제 의존관계가 연결되는 것을 의존관계 주입(DI)이라고 하며, AppConfig 처럼 DI를 담당하는 것을 DI 컨테이너라고 한다.
2. 스프링 컨테이너
@Configuration, @Bean 어노테이션으로 스프링이 AppConfig를 사용해서 컨테이너를 생성하고 DI를 하도록 설정할 수 있다.
@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
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
스프링은 @Configuration이 붙은 AppConfig를 설정정보로 사용한다.
@Bean이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
이 스프링 빈은 @Bean이 붙은 메서드의 이름을 빈의 이름으로 사용한다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
설정 정보(AppConfig)를 전달해 스프링 컨테이너를 생성하고 ApplicationContext 에서 스프링 빈을 꺼내 사용한다.
스프링 컨테이너는 BeanFactory와 ApplicationContext로 구분되어 있는데
ApplicationContext가 BeanFactory를 상속하고 있는 구조이다. 하단 설명 참고.
위의 코드에서 자바 설정 클래스를 기반으로 AnnotationConfigApplicationContext를 생성했는데 XML 기반으로 GenericXmlApplicationContext를 생성할 수 있다.
두 개 모두 인터페이스인 ApplicationContext의 구현체이다.
1) 스프링 컨테이너 생성 과정
스프링은 스프링 빈을 생성하고, 의존관계를 설정하는 단계가 나누어져 있다.
스프링 빈 생성
파라미터로 넘어온 클래스 정보를 사용해서 스프링 빈을 등록한다.
스프링 빈 이름은 메서드 이름을 사용하지만 @Bean(name="memberService2")와 같이 직접 부여할 수도 있다.
의존관계 설정
실제 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.
하지만 단순히 자바 코드를 호출하는 것과는 차이가 있다.
참고: 의존관계 자동 주입, 싱글톤 컨테이너
2) 스프링 빈 조회
(1) 모든 빈 조회 : ac.getBeanDefinitionNames()
스프링 컨테이너에 등록된 모든 빈
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name=" + beanDefinitionName + " object=" + bean);
}
애플리케이션 빈
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
//Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
//Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name=" + beanDefinitionName + " object=" + bean);
}
}
(2) 스프링 빈 조회 : ac.getBean(빈이름, 타입)
빈 이름으로 조회
MemberService memberService = ac.getBean("memberService", MemberService.class);
타입만으로 조회
MemberService memberService = ac.getBean(MemberService.class);
이름과 타입으로 조회
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
타입으로 모든 빈 조회
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value = " + beansOfType.get(key));
}
상속 관계 스프링 빈 조회
부모 타입으로 조회하면 모든 자식 타입도 함꼐 조회된다.
Object 타입으로 조회하면 모든 빈이 조회된다.
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value=" + beansOfType.get(key));
}
예외
조회 대상 스프링 빈이 없는 경우 : NoSuchBeanDefinitionException
조회 대상 스프링 빈이 둘 이상인 경우 : NoUniqueBeanDefinitionException
3) BeanFactory와 ApplicationContext
BeanFactory : 빈 관리기능
가 스프링 컨테이너 최상위 인터페이스 이고, 스프링 빈을 관리하고 조회하는 역할을 담당한다.
(getBean() 등 제공)
ApplicationContext : 빈 관리기능 + 편리한 부가 기능
빈을 관리하고 검색하는 기능은 BeanFactory의 기능을 상속받아서 사용하고.
애플리케이션을 개발할 때 필요한 부가 기능을 여러 인터페이스에서 상속받아 제공한다.
MessageSource
- 메시지소스를 활용한 국제화 기능
- 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
EnvironmentCapable
- 환경변수
- 로컬, 개발, 운영등을 구분해서 처리
ApplicationEventPublisher
- 애플리케이션 이벤트
- 이벤트를 발행하고 구독하는 모델을 편리하게 지원
ResourceLoader
- 편리한 리소스 조회
- 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
ApplicationContext 구현체
스프링 컨테이너는 다양한 ApplicationContext의 구현체를 제공하기 때문에 애플리케이션의 설정정보를 유연하게 변경할 수 있다.
4) BeanDefinition
다양한 설정 파일을 통해 스프링 컨테이너가 스프링 빈을 생성할 수 있는 이유는
각각 빈에 대한 메타정보를 BeanDefinition이라는 추상화를 만들기 때문이다.
설정 형식 각각 BeanDefinitionReader를 통해 BeanDefinition을 만들고
스프링 컨테이너는 BeanDefinition에 저장된 빈에 대한 메타정보를 기반을 스프링 빈을 생성한다.
BeanDefinition 정보
- BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
- factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
- factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
- Scope: 싱글톤(기본값)
- lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
- InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
- DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
- Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리
- 역할의 빈을 사용하면 없음)
자바 설정 파일을 통해 BeanDefinition의 정보를 만들때는 외부에서 AppConfig의 메소드를 호출하는 팩토리 메소드 방식이기 때문에 BeanClassName이 비어있고 factoryBeanName, factoryMethodName이 등록되어 있다.
3. 싱글톤 컨테이너
1) 싱글톤 패턴
순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성하기 때문에 메모리 낭비가 심하다.
따라서 클래스의 인스턴스를 1개만 만들어 공유하도록 DI 객체를 구성하기 위한 클래스에 싱글톤 패턴을 적용해야 한다.
static 영역에 인스턴스를 미리 1개 만들어 올려두고 getInstance() 메서드를 통해서만 인스턴스를 제공한다.
생성자는 private으로 선언해 외부에서의 객체생성 요청을 막는다.
public class SingletonService {
//1. static 영역에 객체를 딱 1개만 생성해둔다.
private static final SingletonService instance = new SingletonService();
//2. public으로 열어서 객체 인스터스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
public static SingletonService getInstance() {
return instance;
}
//3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
// Test
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
assertThat(singletonService1).isSameAs(singletonService2);
싱글톤 패턴을 적용해서 고객의 요청에 대해 하나의 인스턴스를 공유하도록 만들었지만
다음과 같은 문제점이 있다.
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화 하기 어렵다.
- private 생성자로 자식 클래스를 만들기 어렵다.
- 결론적으로 유연성이 떨어진다.
- 안티패턴으로 불리기도 한다.
2) 싱글톤 컨테이너
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.
싱글톤 패턴을 위한 위의 예제와 같은 지저분한 코드가 들어가지 않아도 되고,
DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.
3) @Configuration과 싱글톤
@Configuration은 AppConfig와 같은 설정정보 클래스의 빈을 스프링 컨테이너에 등록할 때
CGLIB 바이트 코드 조작 라이브러리를 사용한다.
실제로 AppConfig는 컨테이너에 등록하지 않고 AppConfig를 상속받는 클래스의 빈을 등록한다.
스프링 컨테이너에서 AppConfig 타입으로 빈을 조회해 보면 클래스의 타입이 다른 것을 확인할 수 있다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
@Configuration을 사용하지 않은 경우
bean = class hello.core.AppConfig
@Configuration을 사용한 경우
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
AppConfig@CGLIB
AppConfig@CGLIB 는 AppConfig를 통해 등록되는 빈들의 싱글톤을 보장해준다.
@Bean을 통해 컨텍스트에 스프링 빈을 등록할 수 있지만 @Configuration이 없으면 스프링 빈 간의 의존 관계에서 싱글톤을 보장하지 않는다.
AppConfig를 통해 @Configuration과 싱글톤을 테스트 해본다.
AppConfig에서 MemoryMemberRepository가 생성되는 메소드는 3곳이다.
memberService(), orderService(), memberRepository()
@Configuration이 싱글톤을 보장해 주더라도 AppConfig를 통해 스프링 빈을 만들면
memberRepository()는 3번 호출되어야 한다.
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
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();
}
}
// Test
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);
@Configuration 없이 테스트해 보면 총 3번의 memberRepository()가 호출된다.
결국 new 키워드로 memberRepository를 생성하기 때문에 모두 다른 객체이며 싱글톤을 보장하지 않는다.
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
@Configuration 을 사용하면 memberRepository()가 단 한번만 호출되기 때문에 싱글톤을 보장한다.
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
위의 테스트에서 볼 수 있듯이 @Configuration은 이미 스프링 컨테이너에 등록된 빈은
새롭게 생성하지 않는다는 것을 확인할 수 있고 이 것이 가능한 건 AppConfig가 아닌
AppConfig@CGLIB를 사용하기 때문이다.
AppConfig@CGLIB 예상 코드
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환;
}
}
@Bean만 사용해도 스프링 빈으로 등록되지만 memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출하는 경우에는 싱글톤을 보장하지 않지만 @Configuration을 통해 가능하다.
4) 싱글톤 방식의 주의점
싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든,
객체 인스턴스를 하나만 생성해서 공유하기 떄문에 싱글톤 객체는 무상태로 설계해야 한다.
특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안되고 가급적 읽기만 가능하도록 해야한다.
필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, 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;
}
}
statefulService1.order("userA", 10000);
statefulService2.order("userB", 20000);
int price = statefulService1.getPrice(); //20000원 출력
StatefulService를 price라는 상태를 갖도록 설계하고 스프링 빈으로 등록하였다.
userA, userB가 싱글톤 객체를 공유하기 때문에 price 필드를 공유한다.
userA의 주문 금액이 userB에 의해 변경되는 문제가 발생하게 된다.
출처
스프링 핵심 원리 - 기본편(김영한)
'Spring' 카테고리의 다른 글
[Spring] 예외 처리 (0) | 2022.10.21 |
---|---|
[Spring] 트랜잭션 (0) | 2022.10.19 |
[Spring] 3. 스프링 빈 (0) | 2022.04.13 |
[Spring] 2. 컴포넌트 스캔과 의존관계 자동 주입 (0) | 2022.03.21 |
- Total
- Today
- Yesterday
- 학습 테스트
- 육각형 아키텍처
- 클린코드
- clean code
- kafka
- Stream
- ATDD
- 스프링 예외 추상화
- Spring Boot
- 계층형 아키텍처
- JPA
- 이벤트 스토밍
- Spring Data JPA
- java8
- H2
- http
- Git
- spring rest docs
- Ubiquitous Language
- 트랜잭셔널 아웃박스 패턴
- 마이크로서비스 패턴
- MySQL
- HTTP 헤더
- 폴링 발행기 패턴
- named query
- TDD
- Spring
- mockito
- 도메인 모델링
- 스프링 카프카 컨슈머
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |