
이번 챕터에서는 여러가지 스프링 annotation 및 활용법을 확인하게 된다.
기본적으로 Spring에서 Bean은 Eager하게 초기화 된다. 그런데 혹시나 만약 요구사항에 따라서 필요할때만 사용해야하는 빈이면?
어떻게 해야할까?
@Lazy를 사용하면 된다.Lazy를 사용하기 이전과 이후의 코드를 한 번 살펴보자.
@Component
class ClassB{
private ClassA classA;
public ClassB(ClassA classA) {
System.out.println("Some Initialization");
this.classA = classA;
}
@Override
public String toString() {
return "ClassB{" +
"classA=" + classA +
'}';
}
}
@ComponentScan
public class LazyInitializationLauncherApplication {
public static void main(String[] args) {
try(var context = new AnnotationConfigApplicationContext(LazyInitializationLauncherApplication.class)){
System.out.println("Initialization of context is completed");
System.out.println(context.getBean(ClassB.class)); // lazy 쓰면 사용하는 시점에서 초기화 진행! 이렇게 하면 스프링 초기 구성시 오류 발견 못함! 조심해서 사용! 거의 사용도 안함!
}
}
}
- Some Initialization
- Initialization of context is completed
- ClassB{classA=com.example.learnspringframework.examples.d1.ClassA@69e1dd28}
import org.springframework.context.annotation.Lazy;
@Component
@Lazy
class ClassB {
private ClassA classA;
public ClassB(ClassA classA) {
System.out.println("Some Initialization");
this.classA = classA;
}
@Override
public String toString() {
return "ClassB{" +
"classA=" + classA +
'}';
}
}
- Initialization of context is completed
- Some Initialization
- ClassB{classA=com.example.learnspringframework.examples.d1.ClassA@696da30b}
다음과 같이 @Lazy를 추가한 것으로 ClassB를 사용할 때 초기화가 진행되어진다! lazy하게 진행이 되어졌다.
@Lazy를 자주 사용하지 않는데는 이유가 또 있다! 초기에 오류를 잡을 수가 없기 때문이다.
스프링을 구성하면서 오류를 잡을 수 있지만 @Lazy가 되어져버리면 이후에 잡아버리게 되니 런타임중에 Error가 터질 수 있다!
그래서 진짜 필요한 상황이 아니라면 잘 사용하지 않는다고 한다!
흔히들 Spring Bean의 Scope라고 하면 대부분 Singleton이라고 얘기를 많이 하는 편이다.
그렇지만 Singleton도 종류가 있다.
이 점을 유의해야한다.
그래서 어찌 되었든 우리는 Spring의 Singleton이다. 그 점을 알고 가보자.
흔히 Spring Bean Scope라고 할 때 Singleton 도 있지만 사실 Prototype 이 있다.
예제를 통해서 어떤 차이가 있나 알아보자.
@Component // 기본 싱글톤
class NormalClass{
}
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 빈 요청시 새로운 인스턴스
@Component
class PrototypeClass{
}
@Configuration
@ComponentScan // 자동으로 현재 패키지 기준 아래쪽으로 스캔
public class BeanScopeLauncherApplication {
public static void main(String[] args) {
try(var context = new AnnotationConfigApplicationContext(
BeanScopeLauncherApplication.class)
){
System.out.println(context.getBean(NormalClass.class));
System.out.println(context.getBean(NormalClass.class));
System.out.println(context.getBean(PrototypeClass.class));
System.out.println(context.getBean(PrototypeClass.class));
}
}
}
해당하는 방식으로 진행을 하게 되었을 때 결과가 다음과 같이 나오게 된다!
com.example.learnspringframework.examples.e1.NormalClass@503d687a
com.example.learnspringframework.examples.e1.NormalClass@503d687a
com.example.learnspringframework.examples.e1.PrototypeClass@6a370f4
com.example.learnspringframework.examples.e1.PrototypeClass@2abf4075
보면 기존의 싱글톤은 같은 객체를 던지고 나머지 Prototype을 선언하니까 부를때마다 다른 객체를 던진다.
사실 이 Prototype 말고도 다른 타입들이 존재한다. 참고하고 이후에 사용하자!
그런 순간이 있다. bean의 생성되고 의존성이 주입된 이후 바로 실행하고 싶은 로직이 있을 수 있다.
그럴 때 @PostContruct를 사용한다.
순서는 다음과 같다.
생성자 호출 -> 의존성 주입 끝 -> @PostConstruct
그리고 bean lifeCycle이 끝나고 객체가 없어지기 직전에 자원을 정리하는등의 로직을 사용하고 싶을 수 있다.
close() 하기 직전에 실행 -> ((AbstractApplicationContext) context).close()
그럴 때 @PreDestroy를 사용한다.
예제를 통해 알아보자.
@Component
class SomeClass{
private SomeDependency someDependency;
public SomeClass(SomeDependency someDependency) {
this.someDependency = someDependency;
System.out.println("All dependencies are ready");
}
@PostConstruct // 의존성 연결하고 연결한 순간 바로 주입 완료된 후 메서드 바로 spring 이 실행함!
public void initialize(){
someDependency.getReady();
}
@PreDestroy // 빈이 삭제되기 직전 수행할 로직! db 연결 끊을 때 관련 혹은 데이터 초기ㅗ하 시킬 때 관련 applicationContext에서 삭제되기 직전 사용함!
public void cleanup(){
someDependency.clean();
}
}
@Component
class SomeDependency{
public void getReady() {
System.out.println("Some logic using SomeDependency");
}
public void clean() {
System.out.println("Clean this Database");
}
}
@ComponentScan // 자동으로 현재 패키지 기준 아래쪽으로 스캔
public class PrePostAnnotationsContextLauncherApplication {
public static void main(String[] args) {
try(var context = new AnnotationConfigApplicationContext(
PrePostAnnotationsContextLauncherApplication.class)
){
//Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
}
}
}
이렇게 실행을 하게 되면
All dependencies are ready
Some logic using SomeDependency
Clean this Database
다음과 같이 진행됨을 통해서 알 수 있다.
CDI라고 하는 친구가 있다. Contexts and Dependency Injection이라고 부르는 친구인데 의존성 주입의 규약과 같은 친구이다.
강의에서 말하는 바는 다음과 같다.
그래서 신기한 것이 이 친구들을 import 하면
import jakarta.inject.Inject;
import jakarta.inject.Named;
이렇게 import가 되어지는데 이를 통해 spring 말고도 jakarta 자바진영에서도 의존성 주입에 대한 어노테이션을 제공한다는 것을 알 수 있다.
이를 통해 외부에 의존하지 않는 때 묻지 않는 객체를 만들 수 있을 거 같다는 생각이 든다.
사실 고대의 유산이다. 왜냐하면 이전 글에서도 봤듯이 bean을 등록하는 쉬운 방법들이 많이 존재한다. 그렇지만 이 항목을 알아야 하는 이유가 있긴 있다.
사실 우리가 건들게 될 코드가 과거의 코드를 개선하는 작업을 할 수도 있기 때문이다. 그렇기 때문에 알아야 한다!
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- bean definitions here -->
<bean id="name" class="java.lang.String">
<constructor-arg value="khyojun"/>
</bean>
<context:component-scan base-package="com.example.learnspringframework.game"></context:component-scan>
<bean id="game" class="com.example.learnspringframework.game.PacmanGame">
</bean>
<bean id="gameRunner" class="com.example.learnspringframework.game.GameRunner">
<constructor-arg ref="game"></constructor-arg>
</bean>
</beans>
빈을 등록하는 친구인데 요약하면 다음과 같다.
- name이라는 빈이고 String의 인스턴스이다. 생성자 인자로 khyojun라는 값을 제공한다.
- component-scan을 작성한 패키지부터 진행한다.
- game이라는 이름의 빈이고 PacmanGame의 인스턴스이다.
- gameRunner라는 이름의 빈이고 GameRunner의 인스턴스이다. 그리고 다른 빈 ID가 game이라는 것을 참조해 현재 GameRunner 인스턴스의 생성자에 주입을 한다.
이와 같이 어렵지만 그래도 어떻게 보는지 알아야 한다는 것을 기억해야 한다!