단축키
OPTION + ENTER : static method import
OPTION + COMMAND + V :
SHIFT + COMMAND + ENTER : 자동 세미콜론
soutv : 변수값을 println
COMMAND + N : 제네레이트
OPTION + ENTER : 빨간줄 해결, 인터페이스 자동구현
SHIFT + COMMAND + T : 테스트 생성
COMMNAD + E : 히스토리 (바로 엔터치면 이전으로 돌아간다)
클릭 + OPTION + COMMAND + B : 구현 객체 확인
JUnit Test
•
given(대상 객체) - when(메서드 실행) - then(검사)
•
Assertions (assertJ.core.api) 의 assertThat
assertThat(a).isEqualTo(b); //a가 b랑 같으면 테스트 통과
assertThat(a).isInstanceOf(b); //a와 b가 같은 타입이면 테스트 통과
assertThat(a).isSameAs(b); //a와 b의 참조를 비교하여 같으면(==) 통과
assertThat(a).isNotSameAs(b); //a와 b의 참조를 비교하여 다르면(!=) 통과
Java
복사
•
Assertions (junit.jupiter.api) 의 assertThrows
assertThrow(Exception.class, () -> 실행 코드); //실행 코드에서 지정한 예외가 발생하면 테스트 통과
Java
복사
Annotation
•
@BeforEach
◦
테스트에서 메서드 위에 붙이면 각 테스트 실행 전 무조건 실행하는 메서드가 된다.
•
@Test
◦
테스트에서 로직에 사용되는 메서드 위에 붙이면 테스트할거라고 명시한다.
•
@Configuration
◦
애플리케이션의 설정정보를 갖고 있는 클래스에 붙인다.
◦
Configuration 내의 @Bean이 붙은 메서드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록한다
•
@Bean
◦
스프링 컨테이너에 등록하기 위해 메서드에 붙인다.
◦
메서드명을 스프링 빈의 이름으로 사용하며 @Bean(name = “”)으로 이름을 지정 해줄 수도 있다.
◦
등록된 스프링 빈은 ApplicationContext.getBean(”이름”, 반환클래스.class)로 찾는다.
SOLID
•
SRP (Single Responsibility Principle) - 단일 책임 원칙
◦
“한 클래스는 하나의 책임만 가져야 한다"
◦
기존 클라이언트 객체는 구현 객체 생성, 연결, 실행의 책임을 가졌다.
◦
관심사 분리를 통해 클라이언트 객체는 실행만, AppConfig이 구현 객체를 생성, 연결한다.
•
OCP (Open/Closed Principle) - 개방 폐쇄 원칙
◦
“확장에는 열려 있으나 변경에는 닫혀 있어야 한다”
◦
애플리케이션을 사용 영역, 구성 영역으로 나누고 사용 영역의 변경은 닫혀 있어야 한다.
◦
기존 클라이언트 코드는 AppConfig을 통한 관심사 분리가 되지 않았기 때문에 코드 변경이 불가피하다.
◦
AppConfig을 통해 의존관계만 바꿔서 주입하면 클라이언트 코드의 변경이 없다.
•
LSP (Liskov Substitution principle) - 리스코프 치환 원칙
◦
•
ISP (Interface Segregation Principle) - 인터페이스 분리 원칙
◦
•
DIP (Dependency inersion principle) - 의존 역전 원칙
◦
“구체화에 의존하지 말고 추상화에 의존해야 한다”
◦
기존 클라이언트 객체는 추상화 인터페이스와 구체화 구현 클래스에 함께 의존했다.
◦
클라이언트 객체를 추상화 인터페이스에만 의존하게 하고
AppConfig을 통해 구현 객체를 대신 생성하고 의존관계를 주입해줬다.
객체지향 설계법
1.
역할로 먼저 구분하고 그에 해당하는 인터페이스(추상)를 설계 ex. DiscountPolicy
2.
역할에 대한 행동을 하는 클래스(구현체)는 필요한 방식마다 설계 ex. FixDiscountPolicy, RateDiscountPolicy
→ 여기서 문제점 DIP와 OCP를 지키기 위해
OrderServiceImpl 에서 FixDiscountPolicy가 아닌 DiscountPolicy 인터페이스 그 자체를 바라보게 하면
구현된게 없기 때문에 NPE이 발생하게 된다… 그럼 어떻게 해야하느냐..?
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemberMemoryRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private DiscountPolicy discountPolicy; <- 이부분
Java
복사
OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입 해줄 것이 필요하다!!!
관심사의 분리
•
역할(인터페이스)은 협력을 하는 역할의 구현 객체가 뭔지 알 필요가 없다 ← DIP
//MemberServiceImpl의 기존 코드는 MemberMemoryRepository에 대해 알고 있다
private final MemberRepository memberRepository = new MemberMemoryRepository();
//DIP를 지키기 위해 관심사 분리를 한 코드
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
Java
복사
•
생성자를 만들어 관심사를 분리시키고 AppConfig에게 구현 객체를 생성하고 연결하는 책임을 부여한다
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemberMemoryRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemberMemoryRepository(), new FixDiscountPolicy());
}
}
Java
복사
•
이제 MemberApp(클라이언트 코드) 에서는 AppConfig을 통해 DI(의존성주입)를 받기만 하면 될 뿐이다.
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
Java
복사
•
하지만 이런 AppConfig에서도 역할이 잘 드러나게 리팩토링 해줘야 한다. ← SRP?
◦
각 구현객체를 반환하는 역할을 분리해 애플리케이션의 전체 구성을 빠르게 파악할 수 있다
◦
중복된 부분을 분리하여 수정이 용이해져 코드의 유지보수성이 증가한다
//이전 코드
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemberMemoryRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemberMemoryRepository(), new FixDiscountPolicy());
}
}
//리팩토링 후 코드
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
//MemberRepository를 적절한 구현객체로 생성해 반환해주는 역할을 분리했다
//이로써 기존에 2군데를 수정해야하던 부분이 1군데로 줄며 코드 유지보수성이 좋아졌다
private MemberRepository memberRepository() {
return new MemberMemoryRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
//DiscountPolicy를 적절한 구현객체로 생성해 반환해주는 역할을 분리했다
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
Java
복사
•
이렇게 관심사의 분리를 통해 구현객체를 쉽게 갈아 끼울 수 있다 ← OCP
//AppConfig에서 DiscountPolicy 구현객체를 반환해주는 코드
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
//return new FixDiscountPolicy(); 이렇게 한 줄만 바꿔줘도 할인정책이 변경된다
}
Java
복사
제어의 역전 IoC (Inversion of Control)
•
프로그램의 제어 흐름을 직접(구현 객체) 제어하는 것이 아니라 외부(AppConfig)에서 관리하는 것
프레임워크 vs 라이브러리
•
프레임워크 : 내가 작성한 코드를 제어하고 대신 실행하는 것 (JUnit)
•
라이브러리 : 내가 작성한 코드가 직접 제어의 흐름을 담당하는 것
정적 vs 동적 의존관계
•
정적인 클래스 의존관계
◦
import 코드만 보고 판단하는 의존관계로 실제로 어떤 객체가 주입되는지 알 수 없다
•
동적인 객체 인스턴스 의존관계
◦
애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존관계이다.
의존관계 주입 DI (Dependency Injection)
•
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서
클라이언트와 서버의 실제 의존관계가 연결되는 것
•
객체 인스턴스를 생성하고 그 참조값을 전달해서 연결된다
•
클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다
•
정적인 클래스 의존관계를 변경하지 않고 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다
IoC 컨테이너, DI 컨테이너
•
AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것
•
의존관계 주입에 초점을 맞춰 최근에는 주로 DI 컨테이너라 한다
스프링으로 전환
•
AppConfig에 @Configuration, @Bean 어노테이션을 통해 스프링 컨테이너로 만들어준다.
•
이 때, @Bean이 붙은 메서드는 ‘이름-반환값’ 쌍으로 Map형태로 ApplicationContext에 등록된다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemberMemoryRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
Java
복사
•
클라이언트 객체에서 AppConfig을 통해 DI 하던걸 ApplicationContext를 통해 DI 한다.
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
Java
복사
→ 이제 우리는 ApplicationContext라는 스프링 컨테이너와 @Bean이 붙은 스프링 빈을 알게 됐다.
스프링 컨테이너
ApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
Java
복사
•
‘ApplicationContext’는 인터페이스로 스프링 컨테이너라고 부른다.
•
ApplicationContext 인터페이스를 구현하는 여러 ‘~~ApplicationContext()’ 를 통해 각 종류의 컨테이너를 만들 수 있다 (XML, 어노테이션 등)
•
컨테이너를 생성할 땐 구성정보(AppConfig.class)를 매개변수로 넘겨줘야 한다.
•
컨테이너는 Map 형태로 빈의 이름-객체 쌍을 저장한다.
•
이 때, 빈의 이름은 기본적으로 메서드의 이름을 사용하며 @Bean(name = “”)으로 직접 부여할 수도 있다.
•
반드시 빈의 이름은 중복이 되면 안된다.
•
스프링 컨테이너는 빈을 생성하고 의존관계를 주입하는 단계가 나눠져 있다.
스프링 빈 조회 - 기본
•
ac.getBean(빈 이름, 타입)
•
ac.getBean(타입)
•
빈이 존재하지 않으면 NoSuchBeanDefinitionException 발생
//[?] 스프링 컨테이너가 아니라 테스트 클래스이므로 ApplicationContext 타입이 아니어도 되는거 같음
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 출력하기") //테스트 이름 부여
void findAllBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for(String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("beanDefinitionName = " + beanDefinitionName + " object = " + bean);
}
}
@Test
@DisplayName("애플리케이션 빈만 출력하기")
void findApplicationBean() {
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("beanDefinitionName = " + beanDefinitionName + " object = " + bean);
}
}
}
Java
복사
•
ac.getBeanDefinition() 은 빈의 정보들을 갖고 있고 빈의 이름을 매개변수로 전달해 가져올 수 있다.
•
ac.getBeanDefinitionNames() 로 빈의 정보 중 빈의 이름들을 가져올 수 있다.
•
BeanDefinition.getRole() 은 빈이 어디서 정의됐는지, 어떻게 사용되는지에 대한 값이다.
스프링 빈 조회 - 동일한 타입이 둘 이상인 경우
•
타입으로 조회 시 같은 타입의 빈이 둘 이상인 경우 NoUniqueBeanDefinitionException이 발생한다.
이 때는 getBean(”빈 이름", 타입)으로 조회
public class ApplicationContextSameBeanFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 중복 오류 발생")
void findBeanByTypeDuplicate() {
assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(MemberRepository.class));
}
@Test
@DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다")
void findBeanByName() {
MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
assertThat(memberRepository).isInstanceOf(MemberRepository.class);
}
@Test
@DisplayName("특정 타입을 모두 조회하게")
void findAllBeanByType() {
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " beansOfType.get(key) = " + beansOfType.get(key));
}
System.out.println("beansOfType = " + beansOfType);
assertThat(beansOfType.size()).isEqualTo(2);
}
//임의로 만든 Configuration
//static 키워드를 붙인 이유는 class 안에 내부 class 이기 때문에 scope을 지정 해준다는 것
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new MemberMemoryRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemberMemoryRepository();
}
}
}
Java
복사
•
ac.getBeansOfType() 으로 해당 타입의 모든 빈을 조회할 수 있다.
스프링 빈 조회 - 상속관계
•
부모 타입으로 조회하면 자식 타입도 함께 조회되는게 스프링의 대원칙이다.
즉, Object로 조회하면 모든 빈을 조회할 수 있다.
@Test
@DisplayName("특정 하위 타입으로 조회")
void findBeanBySubType() {
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("부모 타입으로 모두 조회하기")
void findAllBeanByParentType() {
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
assertThat(beansOfType.size()).isEqualTo(2);
for(String key : beansOfType.keySet()) {
System.out.println("key = " + key + " beansOfType.get(key) = " + beansOfType.get(key));
}
}
@Test
@DisplayName("부모 타입으로 모두 조회하기 - Object")
void findAllBeanByObjectType() {
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " beansOfType.get(key) = " + beansOfType.get(key));
}
}
Java
복사
BeanFactory와 ApplicationContext
•
“BeanFactory는 빈 관리기능, ApplicationContext는 빈 관리기능 + 부가기능”
•
둘 다 스프링 컨테이너지만 BeanFactory는 직접 사용할 일이 거의 없다.
•
BeanFactory
◦
스프링 컨테이너의 최상위 인터페이스이다.
◦
스프링 빈을 관리하고 조회하는 기능을 제공한다.
•
ApplicationContext
◦
BeanFactory 기능을 모두 상속받아서 제공한다.
◦
여러 부가 기능을 제공한다.
•
BeanFactory와 Application의 큰 차이점은 “부가기능"이다.
◦
MessageSource : 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력하는 국제화 기능
◦
EnvironmentCapable : 로컬, 개발, 운영 등을 구분해서 처리하는 환경변수
◦
ApplicationEventPublisher : 이벤트를 발행하고 구독하는 모델을 편리하게 지원하는 기능
◦
ResourceLoader : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회하는 기능
다양한 애플리케이션 설정 형식 지원 - 자바 코드, XML
•
스프링 컨테이너는 다양한 형식의 설정 정보를 받아 들일 수 있게 유연하게 설계되어 있다.
•
Annotation 기반 자바 코드 설정
◦
AnnotationConfigApplicationContext 클래스를 사용하여 자바 코드로 구성 정보를 넘기면 된다.
◦
new AnnotationConfigApplicationContext(AppConfig.class)
•
XML 설정
◦
GenericXmlApplicationContext 클래스를 사용하여 xml 설정 파일을 넘기면 된다.
◦
최근엔 대부분 스프링 부트를 사용하므로 잘 사용되지 않는다.
•
아래의 두 설정은 같은 내용이다.
//XML로 설정한 부분
<bean id="memberService" class="com.example.demo.member.MemberServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository" />
</bean>
<bean id="memberRepository" class="com.example.demo.member.MemoryMemberRepository"/>
//자바 코드로 설정한 부분
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
Java
복사
스프링 빈 설정 메타 정보 - BeanDefinition
•
스프링 컨테이너는 XML로 설정하든 자바 코드로 설정하든 BeanDefinition으로 만들어 읽는다.
•
해당 ApplicationContext의 BeanDefinitionReader가 AppConfig 읽기 → BeanDefinition 생성
•
@Bean, <bean> 당 각각 하나의 메타 정보가 생성된다.
•
스프링 컨테이너는 이 메타 정보를 기반으로 스프링 빈을 생성한다.
•
BeanDefinition 정보
◦
BeanClassName : 생성할 빈의 클래스명 (자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)
◦
factoryBeanName : 팩토리 역할의 빈을 사용할 경우 이름 ex.appConfig
◦
factoryMethodName : 빈을 생성할 팩토리 메서드 지정 ex.memberService
◦
Scope : 싱글톤(기본값)
◦
lazyInit : 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라 실제 빈을 사용할 때까지 최대한 생성을 지연하는지 여부
◦
InitMethodName : 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드명
◦
DestroyMethodName : 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드명
◦
Constructor arguments, Properties : 의존관계 주입에서 사용 (자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)
웹 애플리케이션과 싱글톤
•
대부분의 스프링 애플리케이션은 웹 애플리케이션이고 그것은 보통 여러 고객이 동시에 요청을 한다.
AppConfig appConfig = new AppConfig();
//1. 조회 : 호출 할 때마다 객체를 생성
MemberService memberService1 = appConfig.memberService();
//2. 조회 : 호출 할 때마다 객체를 생성
MemberService memberService2 = appConfig.memberService();
//참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
Java
복사
•
순수 DI 컨테이너인 AppConfig은 요청을 할 때마다 새로운 객체를 생성한다. → 메모리 낭비
•
객체를 1개만 생성하고 요청을 할 때마다 그 객체를 공유하게끔 하면 된다. → 싱글톤 패턴
싱글톤 패턴
•
클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
•
객체 인스턴스가 2개 이상 생성되지 못하도록 막아야 한다.
→ private 생성자를 통해 외부에서 new 키워드로 생성되지 않게 해야함
→ private static final을 통해 메모리에 올라갈 때 1회 인스턴스를 생성해 static 변수에 담아두고
static 메서드를 통해서만 객체를 반환하도록 해야함
//싱글톤 객체를 만드는 클래스
public class SingletonService {
//private을 통해 다른 곳에서 사용되는 것을 막는다.
//static을 통해 메모리에 올라감과 동시에 new SingletonService 스스로를 생성해 instance에 담아두게 한다.
//final을 통해 메모리에 올라 갔을 때 생성된 이후 변경되지 않게끔 한다.
private static final SingletonService instance = new SingletonService();
//이 메서드를 통해서만 SingletonService 호출이 가능하고 항상 같은 instance를 반환한다.
public static SingletonService getInstance() {
return instance;
}
//private 생성자를 통해 다른 곳에서 객체가 생성되는걸 막는다
private SingletonService() {}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
//싱글톤 객체를 사용한 테스트
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
assertThat(singletonService1).isSameAs(singletonService2);
}
Java
복사
싱글톤 패턴의 단점
•
싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다
•
의존관계상 클라이언트가 구체 클래스에 의존한다 (DIP 위반)
→ AppConfig에서 getInstance()를 사용하면 DIP를 지키며 싱글톤 패턴을 만들 수 있다.
•
테스트하기 어렵다
•
내부 속성을 변경하거나 초기화하기 어렵다
•
private 생성자로 자식 클래스를 만들기 어렵다
•
결론적으로 유연성이 떨어진다
•
안티패턴으로 불리기도 한다
싱글톤 컨테이너
•
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서 객체 인스턴스를 싱글톤으로 관리한다.
◦
DIP, OCP, 테스트, private 생성자로부터 자유롭게 사용할 수 있다.
•
싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
•
스프링의 기본 빈 등록 방식은 싱글톤이지만 빈 스코프를 통해 요청할 때마다 새로운 객체를 생성해서 반환할 수도 있다.
싱글톤 방식의 주의점
•
싱글톤 객체는 상태를 유지(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;
}
}
Java
복사
스프링 컨테이너에서의 싱글톤
//테스트 코드
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
//AppConfig
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
Java
복사
•
테스트 코드에서 memberService와 orderService, memberRepository를 호출해보면
memberService → memberRepository → orderService → memberRepository → memberRepository
이렇게 memberRepository가 3번 new를 통해 생성되면서 싱글톤이 지켜지지 않는 것처럼 보인다.
•
하지만 로그엔 각각 1번의 call만 찍힌걸 확인할 수 있다
@Configuration과 CGLIB
•
먼저 @Configuration이 붙은 AppConfig을 살펴보자
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean.getClass() = " + bean.getClass());
}
Java
복사
•
AC를 만들면서 빈으로 등록된 AppConfig의 Class 정보를 로그에 찍어보니 이름이 신기하다
◦
순수 클래스 : class com.example.demo.AppConfig
◦
@Configuration이 붙은 클래스 : class com.example.demo.AppConfig$$EnhancerBySpringCGLIB$$~~~
•
이건 내가 만든 클래스가 아니라 스프링이 CGLIB이라는 바이트코드 조작 라이브러리를 사용하여
AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고 그것을 빈으로 등록한 것이다.
•
CGLIB 클래스엔 기존 클래스를 바탕으로 동적인 코드가 만들어진다.
◦
@Bean이 붙은 메서드마다 이미 빈이 존재하면 존재하는 빈을 반환
◦
빈이 없으면 기존 클래스에 메서드를 호출해 생성한 후 빈으로 등록하고 반환
•
이걸 통해 싱글톤을 보장한다!
@Configuration을 붙이지 않으면?
•
붙이지 않아도 @Bean을 사용하면 빈은 등록이 된다
•
하지만 CGLIB을 사용하지 않아 AppConfig 클래스는 기존의 순수한 클래스로 만들어진다
•
CGLIB을 사용하지 않기 때문에 싱글톤도 보장받지 못한다
컴포넌트 스캔
•
설정 정보 없이도 자동으로 모든 스프링 빈을 등록하는 기능이다.
@Configuration
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}
Java
복사
•
컴포넌트 스캔을 사용하기 위해선 Config 으로 사용할 클래스에 @ComponentScan을 붙여주어야 한다.
//MemberServiceImpl의 일부
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
Java
복사
•
@Component가 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
•
@Configuration도 자동으로 스캔돼서 등록된다. (소스코드에 @Component가 붙었기 때문)
◦
제외하려면 excludeFilters를 이용해서 스캔 대상에서 제외할 수 있다.
•
@Autowired를 생성자에 붙여서 의존관계를 자동으로 주입해준다
•
@Component를 통해 스프링 빈으로 등록한다.
◦
빈 이름 기본 전략 : MemberServiceImpl → memberServiceImpl (앞글자만 소문자로)
◦
빈 이름 직접 지정 : @Component(”이름”)
•
@Autowired를 통해 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다
◦
기본 조회 전략은 같은 타입이다. (getBean(MemberRepository.class)와 동일)
◦
주입할 대상이 없으면 오류가 발생한다 (@Autowired(required = false)로 지정하면 오류 발생 X)
탐색 위치
•
모든 자바 클래스를 다 ComponentScan하면 시간이 오래 걸리기 때문에 탐색 위치를 지정해준다.
@ComponentScan(
basePackages = {"com.example.demo", ...},
basePackageClasses = AutoAppConfig.class,
)
Java
복사
•
basePackages : 탐색할 패키지의 시작 위치를 지정한다 (이 패키지를 포함한 하위 패키지를 모두 탐색)
◦
괄호 {}로 묶고 콤마로 구분해서 여러 시작 위치를 지정할 수도 있다
•
basePackageClasses : 지정한 클래스의 패키지를 탐색 위치로 지정한다
◦
지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
•
아무것도 지정하지 않고 Config 클래스의 위치를 프로젝트 최상단에 두는 것이 가장 추천된다.
◦
스프링 부트의 @SpringBootApplication은 root에 두는 것이 관례이고 컴포넌트 스캔을 포함한다
@ComponentScan의 기본 스캔 대상
•
useDefaultFilters 옵션은 기본으로 켜져있지만 끄면 기본 스캔 대상들이 제외된다.
•
@Component : 컴포넌트 스캔에서 사용
•
@Controller : 스프링 MVC 컨트롤러에서 사용
•
@Service : 스프링 비즈니스 로직에서 사용
•
@Repository : 스프링 데이터 접근 계층에서 사용
•
@Configuration : 스프링 설정 정보에서 사용
스프링에서 어노테이션의 비밀
•
어노테이션에는 상속관계가 없다.
•
어노테이션이 특정 어노테이션을 들고 있는걸 인식하는 것은 자바가 아닌 스프링이 지원하는 기능이다.
•
어노테이션마다 스프링이 부가 기능을 지원한다
◦
@Controller : 스프링 MVC 컨트롤러로 인식
◦
@Repository : 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예외로 변환
◦
@Configuration : 스프링 설정 정보로 인식하고 스프링 빈이 싱글톤을 유지하도록 추가 처리
◦
@Service : 특별한 기능을 하진 않지만 개발자를 위해 핵심 비즈니스 로직이 있다고 표시해준다
필터
•
includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
•
excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
@ComponentScan(
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
//MyIncludeComponent
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
//MyExcludeComponent
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
//BeanA
@MyIncludeComponent
public class BeanA {
}
//BeanB
@MyExcludeComponent
public class BeanB {
}
Java
복사
•
BeanA는 스프링 빈에 등록되고 BeanB는 스프링 빈에 등록되지 않는다.
FilterType 옵션
•
ANNOTATION : 기본값으로 어노테이션을 인식해서 동작한다.
•
ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작한다
•
ASPECTJ : AspectJ 패턴을 사용
•
REGEX : 정규표현식
•
CUSTOM : TypeFilter라는 인터페이스를 구현해서 처리
중복 등록과 충돌
•
컴포넌트 스캔 중 이름이 같은 빈을 등록하면 어떻게 될까?
◦
1번 경우 : 컴포넌트 스캔을 통한 자동 빈 등록된 빈 두 개가 이름이 같은 경우
◦
2번 경우 : 컴포넌트 스캔을 통한 자동 빈 등록된 빈과 수동으로 등록된 빈 두 개가 이름이 같은 경우
//OrderServiceImpl 빈 이름을 Service로 지정
@Component("Service")
public class OrderServiceImpl implements OrderService {
//MemberServiceImpl 빈 이름을 Service로 지정
@Component("Service")
public class MemberServiceImpl implements MemberService {
Java
복사
•
1번 경우 : 자동 빈 등록 vs 자동 빈 등록
◦
ConflictingBeanDefinitionException 예외 발생
//AutoAppConfig에 memoryMemberRepository 이름으로 수동 빈 등록
@Configuration
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
//@Component에 의해 앞글자를 소문자로 바꾼 memoryMemberRepository 이름으로 빈이 등록된다
@Component
public class MemoryMemberRepository implements MemberRepository {
Java
복사
•
2번 경우 : 수동 빈 등록 vs 자동 빈 등록
◦
수동 빈이 우선권을 가져 자동 빈을 오버라이딩한다
◦
로그에 Overriding been definition for bean~~ 으로 기록을 남겨준다.
•
개발자의 실수를 방지하기 위해 스프링 부트는 2번 경우에서 오류가 발생하도록 했다.
Description:
The bean 'memoryMemberRepository', defined in class path resource [com/example/demo/AutoAppConfig.class], could not be registered. A bean with that name has already been defined in file [/Users/chan/BackEnd/demo/out/production/classes/com/example/demo/member/MemoryMemberRepository.class] and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
Java
복사
•
오버라이딩 되게끔 하려면 application.properties에 설정을 바꿔줘야 한다.
◦
spring.main.allow-bean-definition-overriding=true
다양한 의존관계 주입 방법
•
생성자 주입
◦
생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다
◦
불변, 필수 의존관계에 사용
◦
스프링 빈의 경우 생성자가 1개만 있으면 @Autowired를 생략해도 자동 주입된다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Java
복사
•
수정자 주입 (setter 주입)
◦
setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입한다.
◦
선택, 변경 가능성이 있는 의존관계에 사용
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
Java
복사
•
필드 주입
◦
필드에 바로 주입하는 방법이다.
◦
코드가 간결해지지만 외부에서 변경이 불가능해서 테스트하기가 힘들다.
◦
테스트 코드나 @Configuration 외에는 사용하지 말자!
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
Java
복사
•
일반 메서드 주입
◦
일반 메서드를 통해 주입 받을 수 있다.
◦
한 번에 여러 필드를 주입 받을 수 있다.
◦
일반적으로 잘 사용하지 않는다. (setter 주입이랑 똑같다..)
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Java
복사
옵션 처리
•
스프링 빈 없이 동작하려면 자동 주입 옵션을 처리해줘야 한다.
◦
@Autowired(required = false) : 주입할 대상이 없으면 호출 자체가 안됨
◦
@Nullable : 주입할 대상이 없으면 null이 입력됨
◦
Optional<> : 주입할 대상이 없으면 Optional.emptry가 입력됨
public class AutowiredTest {
@Test
void AutowiredOption() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean {
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
}
}
Java
복사
생성자 주입을 사용하라!
•
불변
◦
대부분의 의존관계는 주입이 한 번되면 애플리케이션 종료시점까지 변경할 일이 없다. 변해도 안 된다.
◦
수정자 주입을 사용하면 setXXX 메서드를 public으로 열어두므로 변경될 수 있다.
◦
생성자 주입은 객체를 생성할 때 딱 한 번 호출되므로 변경되는 일이 없다.
•
누락
◦
수정자 주입을 사용한 경우 의존관계가 없을 때 스프링 프레임워크에선 오류가 발생하지만
순수한 자바 코드에선 오류가 발생하지 않아 실행 단계에서 NPE로 확인하게 된다.
◦
생성자 주입을 사용한 경우 의존관계를 누락했을 때 컴파일 오류가 발생해 빠르게 파악할 수 있다.
•
final 키워드
◦
생성자 주입을 사용할 때만 final을 사용할 수 있다.
◦
final 키워드가 붙은 경우 무조건 값을 가져야하기 때문에 컴파일 오류나 IDE를 통해 파악할 수 있다.
롬복
•
컴파일 과정에서 해당 기능을 추가해서 컴파일!
•
out 폴더에 class를 디컴파일해서 확인해보면 추가된걸 확인할 수 있다.
@Getter, @Setter : 클래스 내의 변수들에 대한 게터, 세터를 자동으로 만들어준다
@ToString : 클래스 내의 변수들에 대한 toString을 자동으로 만들어준다
@NoArgsConstructor : 클래스 내의 변수들에 대한 생성자를 자동으로 만들어준다
@RequiredArgsConstructor : 클래스 내의 final이 붙은 변수들에 대한 생성자를 자동으로 만들어준다
Java
복사
자동 주입에서 스프링 빈 중복 조회 문제
•
@Autowired는 타입으로 조회한다. (ac.getBean(DiscountPolicy.class 와 비슷한 동작)
//FixDiscountPolicy를 빈으로 등록
@Component
public class FixDiscountPolicy implements DiscountPolicy {
//RateDiscountPolicy를 빈으로 등록
@Component
public class RateDiscountPolicy implements DiscountPolicy {
//
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Java
복사
•
이 경우엔 DiscountPolicy의 하위 타입 FixDiscountPolicy, RateDiscountPolicy를 다 빈으로 등록해서
NoUniqueBeanDefinitionException 이 발생한다.
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.example.demo.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
Java
복사
•
그럼 어떻게 해결할까? → @Autowired를 통해 자동 주입할 대상을 하위타입으로 지정할 수 있다.
하지만 이 방법은 DIP를 위배하고 유연성을 떨어트린다.
해결 방법
•
@Autowired 필드 명 매칭
◦
@Autowired는 타입 매칭이지만 중복 빈인 경우 필드 이름, 파라미터 이름으로 추가 매칭을 시도한다.
//기존 코드
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//필드 명 매칭 코드
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
Java
복사
•
@Qualifier
◦
직접 빈과 생성자 매개변수에 추가 구분자를 붙여주는 방법이다. (빈의 이름을 바꾸진 않음)
//RateDiscountPolicy
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
//OrderServiceImpl
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
Java
복사
◦
@Qualifier(”이름”)으로 지정한 빈을 못찾으면 “이름" 이라는 이름을 가진 빈을 추가로 찾는다.
•
@Primary
◦
우선순위를 정하는 방법으로 중복 빈이 조회되면 @Primary가 우선권을 가진다.
//RateDiscountPolicy
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
Java
복사
◦
@Qualifier보다 간편한 대신 우선권은 낮다.
여러 스프링 빈을 조회할 때
◦
같은 타입을 갖는 스프링 빈이 다 필요한 경우 Map, List를 사용해 빈들을 조회할 수 있다.
◦
이를 통해 전략 패턴을 간단하게 구현할 수 있다.
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(rateDiscountPrice).isEqualTo(2000);
}
//자동 주입을 통해 Map, List 형태로 같은 타입의 빈을 모두 조회한다.
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
//Map.get()을 사용해 전달받은 discountCode에 따라 다른 빈을 적용한다.
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
Java
복사
자동? 수동?
•
편리한 자동 기능을 기본을 사용하자
◦
빈을 등록할 때 @Component만 붙이면 될걸 @Configuration에서 @Bean을 붙이고 new로 객체 생성하고 의존관계 신경쓰고 하는건 굉장히 복잡하고 귀찮은 일이다…
◦
스프링 부트 또한 @ComponentScan을 기본적으로 사용하여 자동을 선호하는 추세이다.
◦
자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.
•
수동 빈은 언제 사용할까?
◦
업무 로직 빈 : 컨트롤러, 서비스, 리포지토리 등이 해당하며 유사한 패턴이 존재하기 때문에 자동 기능을 사용하는 것이 좋다!
◦
기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 사용되며 수가 적고 광범위하게 영향을 미치기 때문에 명확하게 드러내기 위해 수동 빈 등록을 사용하는 것이 좋다!
•
비즈니스 로직에 다형성이 활용되는 경우
@Component
@RequiredArgsConstructor
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
Java
복사
◦
위 코드를 보고선 DiscountPolicy의 구현객체가 어떤건지 한 눈에 알기 어렵다.
◦
그렇기에 구현 객체들을 한 패키지에 몰아놓거나 수동으로 빈을 등록해 명확하게 드러내는 것이 좋다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
Java
복사
객체의 초기화와 종료 작업
•
데이터베이스 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다.
//테스트 코드
public class BeanLifeCycleTest {
@Test
public void lifeCycleTest() {
ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
NetworkClient client = ac.getBean(NetworkClient.class);
ac.close();
}
@Configuration
static class LifeCycleConfig {
@Bean
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
}
//NetworkClient 코드
public class NetworkClient {
private String url;
public NetworkClient() {
System.out.println("생성자 호출, url = " + url);
connect();
call("초기화 연결 메시지");
}
public void setUrl(String url) {
this.url = url;
}
public void connect() {
System.out.println("connect : " + url);
}
public void call(String message) {
System.out.println("call : " + url + " message = " + message);
}
public void disconnect() {
System.out.println("close : " + url);
}
}
//실행 결과
생성자 호출, url = null
connect : null
call : null message = 초기화 연결 메시지
Java
복사
•
위 코드에선 객체를 생성한 후에야 setter를 통해 url이 주입되기 때문에 null을 표시한다.
•
그렇다면 생성자에서 url을 주입해주면 되지 않나?
객체의 생성과 초기화를 분리하자
•
생성자는 필수 정보(파라미터)를 받고 메모리를 할당해서 객체를 생성하는 책임을 가진다.
•
초기화는 생성된 값들을 활용해 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.
•
그렇기 때문에 객체를 생성하는 부분과 초기화하는 부분을 나누는 것이 유지보수에 좋다.
스프링 빈의 라이프사이클
•
크게 보면 객체 생성 → 의존관계 주입의 라이프사이클을 갖는다.
초기화 작업은 의존관계 주입이 완료된 후에 해야하는데 그게 언젠지 알 수 있는가?
•
스프링에선 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려준다.
또한 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다.
•
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전 콜백 → 스프링 종료
◦
초기화 콜백 : 빈이 생성되고 빈의 의존관계 주입이 완료된 후 호출
◦
소멸전 콜백 : 빈이 소멸되기 직전에 호출
빈 생명주기 콜백
•
스프링은 크게 3가지의 방법으로 빈 생명주기 콜백을 지원한다.
•
인터페이스 (InitializingBean, DisposableBean)
◦
InitializingBean의 afterPropertiesSet()
◦
DisposableBean의 destroy()
//InitializingBean, DisposableBean 인터페이스를 구현하도록 해주어야 한다.
public class NetworkClient implements InitializingBean, DisposableBean {
//해당 인터페이스의 추상 메서드를 구현해주어야 한다.
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메시지");
}
@Override
public void destroy() throws Exception {
System.out.println("NetworkClient.destroy");
disconnect();
}
Java
복사
◦
이 인터페이스는 스프링 전용 인터페이스이기 때문에 스프링에 의존적이다.
◦
초기화, 소멸 메서드의 이름을 변경할 수 없다.
◦
내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
◦
초기에 나온 방법으로 잘 사용하지 않는다.
•
설정 정보(Config)에 초기화 메서드, 종료 메서드 지정
◦
@Bean(initMethod = “init”, destroyMethod = “close”)
//NetworkClient
public class NetworkClient {
...
public void init() throws Exception {
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메시지");
}
public void close() throws Exception {
System.out.println("NetworkClient.destroy");
disconnect();
}
}
//BeanLifeCycleTest
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hello-spring.dev");
return networkClient;
}
}
Java
복사
◦
메서드 이름을 자유롭게 줄 수 있다.
◦
스프링 빈이 스프링 코드에 의존하지 않는다.
◦
설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.
destroyMethod의 비밀
◦
라이브러리는 대부분 close, shutdown 이름의 종료 메서드를 사용한다.
◦
@Bean의 destroyMethod는 기본값이 (inferred) 이다. (destroyMethod = “(inferred)”)
◦
뜻 그대로 close, shutdown 등의 이름을 자동으로 추론해서 호출해준다.
◦
따라서 종료 메서드는 생략해도 잘 작동한다.
◦
만약 사용하고 싶지 않다면? destroyMethod = “” 처럼 공백으로 지정하면 된다.
•
@PostConstruct, @PreDestroy
◦
초기화, 종료 메서드에 어노테이션만 붙여주면 된다!
//NetworkClient
@PostConstruct
public void init() throws Exception {
System.out.println("NetworkClient.afterPropertiesSet");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() throws Exception {
System.out.println("NetworkClient.destroy");
disconnect();
}
Java
복사
◦
최신 스프링에서 가장 권장하는 방법이다.
◦
javax 패키지의 기능으로 자바 표준이라 다른 컨테이너에서도 동작한다.
◦
컴포넌트 스캔과 잘 어울린다.
◦
외부 라이브러리에는 적용하지 못하기 때문에 외부 라이브러리를 사용할 땐 @Bean의 기능을 사용하자
빈 스코프
•
빈이 존재할 수 있는 범위를 말한다.
•
스코프의 종류
◦
싱글톤 : 기본 스코프로 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
◦
프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
◦
웹 관련 스코프
▪
request : 웹 요청이 들어오고 나갈 때까지 유지되는 스코프이다.
▪
session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프이다.
▪
application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.
•
빈 스코프 지정 방법
◦
컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class Bean {}
Java
복사
◦
수동 등록
@Scope("prototype")
@Bean
PrototypeBean Bean() {
return new Bean();
}
Java
복사
프로토타입 스코프
•
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
1.
싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다
2.
스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다
3.
이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환한다.
•
반면 프로토타입 스코프의 빈을 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
1.
프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
2.
스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고 필요한 의존관계를 주입한다.
3.
스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
4.
이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.
•
핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고 의존관계 주입, 초기화까지만 처리한다는 것이다.
•
관리할 책임은 프로토타입 빈을 받는 클라이언트에 있다.
•
그러므로 @PreDestroy 같은 종료 메서드가 호출되지 않는다. (필요 시 직접 호출)
프로토타입 스코프의 문제점 - 싱글톤 빈과 함께 사용 시
•
ClientBean이라는 싱글톤 빈이 DI를 통해 프로토타입 빈을 주입 받아 사용하는 예제이다.
•
clientBean은 자동 주입을 통해 주입 시점에 스프링 컨테이너에게 프로토타입 빈을 요청한다.
•
스프링 컨테이너는 프로토타입 빈을 생성해서 반환하고 이 때 count 필드 값은 0이다.
•
clientBean은 프로토타입 빈을 내부 필드에 참조값을 보관한다.
•
클라이언트 A가 싱글톤 빈인 clientBean을 스프링 컨테이너에 요청해서 받는다.
•
클라이언트 A가 prototypeBean.addCount()를 포함한 clientBean.logic()을 호출하고 count는 증가해서 1이 된다.
•
클라이언트 B도 clientBean을 스프링 컨테이너에 요청해서 받는다.
•
이 때 프로토타입 빈은 clientBean 생성 시점에 주입이 끝났기 때문에 새로운 프로토타입 빈이 생성되지 않는다.
•
클라이언트 B가 logic()을 호출해 프로토타입 빈의 count가 증가해서 2가 된다.
프로토타입 빈을 사용할 때마다 생성하고 싶은데..?
•
위처럼 clientBean이 생성될 때 프로토타입 빈을 주입 받으면 싱글톤 빈과 프로토타입 빈이 함께 유지된다..
@Scope("singleton")
static class ClientBean {
// private final PrototypeBean prototypeBean;
//
// @Autowired
// public ClientBean(PrototypeBean prototypeBean) {
// this.prototypeBean = prototypeBean;
// }
@Autowired
ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
Java
복사
•
clientBean에 프로토타입 빈을 주입 받는게 아니라 applicationContext를 주입 받은 후 프로토타입 빈을 사용할 때마다 스프링 컨테이너에게 요청해 각각 다른 인스턴스의 프로토타입 빈을 사용할 순 있다.
•
이렇게 외부에서 주입 받는게 아니라 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라고 한다.
•
하지만 applicationContext 전체를 주입 받게 되면 스프링 컨테이너에 종속적인 코드가 되고 테스트도 어려워진다.
@Scope("singleton")
static class ClientBean {
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
@Scope("singleton")
static class ClientBean2 {
private final PrototypeBean prototypeBean;
@Autowired
public ClientBean2(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
Java
복사
•
clientBean1과 clientBean2에서 각각 프로토타입 빈을 주입 받아서 새로운 인스턴스의 프로토타입 빈을 생성할 수도 있다.
•
하지만 이 방법은 사용할 때마다 생성하는 방법은 아니다..
ObjectFactory, ObjectProvider로 문제 해결
•
우리가 필요했던 딱 DL 기능만 하는 무언가를 찾았다.
◦
ObjectFactory : getObject()만 지원
◦
ObjectProvider : ObjectFactory를 상속 받아 getObject() 외에도 여러 기능을 지원
•
Provider의 getObject()를 호출하면 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.
@Scope("singleton")
static class ClientBean {
// private final PrototypeBean prototypeBean;
// @Autowired
// public ClientBean(PrototypeBean prototypeBean) {
// this.prototypeBean = prototypeBean;
// }
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeansProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeansProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
Java
복사
•
하지만 ObjectProvider는 스프링에 의존적이다.
JSR-330 Provider
•
javax.inject.Provider 패키지를 사용한 자바 표준 방법이다.
•
사용하기 위해선 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.
@Autowired
private Provider<PrototypeBean> prototypeBeansProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeansProvider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
Java
복사
•
ObjectProvider<>에서 Provider<>로 바꿔주고 getObject() 대신 get()로만 바꿔주면 된다.
•
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
웹 스코프
•
웹 스코프는 웹 환경에서만 동작한다.
•
스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.
•
웹 스코프의 종류
◦
request : HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프
HTTP 요청마다 별도의 인스턴스가 생성되고 관리된다.
◦
session : HTTP session과 동일한 생명주기를 가지는 스코프
◦
application : ServletContext와 동일한 생명주기를 가지는 스코프
◦
websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프
request 스코프
//LogDemoController
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
Java
복사
//LogDemoService
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
Java
복사
//MyLogger
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
}
@PostConstruct
public void init() {
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean create:" + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
Java
복사
•
@RequestMapping(”log-demo”)를 통해 /log-demo로 들어오는 요청을 처리한다.
•
HttpServletRequest를 통해 requestURL을 받아서 저장하고
MyLogger는 자동 주입으로 생성될 때 UUID를 받아 저장한다.
•
controller에서 MyLogger의 log()를 호출해 로그를 찍어볼 수 있고
service의 logic()을 호출해 다시 MyLogger 의 log()를 호출해 로그를 찍어볼 수 있다.
Scope ‘request’ is not active !!!
•
위의 코드로는 에러가 발생한다.
•
request 스코프는 HTTP 요청이 있어야 빈이 생성되는데
애플리케이션을 실행할 땐 요청이 없지만 컨테이너는 주입 요청을 받았기 때문에 에러가 발생하는 것이다.
•
Provider를 사용하면 해결할 수 있다.
//LogDemoController
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
MyLogger myLogger = myLoggerProvider.getObject();
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
//LogDemoService
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
Java
복사
•
Provider를 통해 사용될 때만 빈이 생성되도록 했다.
•
이제 결과를 보자 HTTP 요청이 생길 때마다 UUID를 독립적으로 만드는걸 확인할 수 있고
MyLogger 빈 또한 같은 요청 내에선 같은 인스턴스인걸 확인할 수 있다.
Provider는 너무 길다.. 프록시 방식!
•
@Scope에 proxyMode를 이용해 가짜 프록시 클래스를 만들어 미리 주입해 둘 수 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
Java
복사
•
적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS, 인터페이스면 INTERFACES
System.out.println("myLogger = " + myLogger.getClass());
//결과
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
Java
복사
•
예전에 봤던 바이트 코드를 조작하는 라이브러리인 CGLIB이 또 등장했다.
•
proxyMode가 붙으면 스프링 컨테이너에 CGLIB을 통해 가짜 프록시 객체를 만들어서 등록한다.
•
가짜 프록시 객체는 요청이 오면 그 때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
•
클라이언트가 logic()을 호출하면 가짜 프록시 객체의 메서드를 호출한 것이지만
가짜 프록시 객체는 request 스코프의 진짜 logic()을 호출한다.
•
이를 통해 진짜 객체를 필요한 시점까지 지연처리할 수 있다.
•
웹 스코프가 아니어도 프록시를 사용할 수 있다.