강의/Java Spring Boot

Spring Framework 고급 기능 살펴보기 - Lazy initialization, Prototype, PostConstruct, PreDestroy

studylida 2024. 7. 26. 21:00

이번 게시글에서는 Spring Framework에서 사용할 수 있는 여러 기능에 대해서 알아보자.

- Lazy Intialzation
- Prototype
- PostConstruct
- PreDestroy

가장 먼저 Lazy initialization에 대해서 알아볼 것이다.

스프링의 초기화 방식에는 즉시 초기화와 지연 초기화 두 가지가 있다. default 설정은 즉시 초기화이다.

오늘도 코드로 예시를 들기 위해 기반 클래스를 하나 만들어보자. 기반 클래스의 이름은 LazyInitializationLauncherApplication이라고 가정하자.

그리고 이 클래스 안에 Bean 몇가지를 만들어보자.

@Component
class ClassA {

}

@Component
class ClassB {

}

여기서 ClassB는 멤버변수로 ClassA classA를 가지고, 초기화 로직을 가진다고 가정하자.

@Component
class ClassB {

    private ClassA classA;

    public ClassB(ClassA classA) {
        //Logic
        System.out.println("Some Initialization Logic");
        this.classA = classA;
    }
}

이 코드를 실행하면 Some Initialzation Logic라고 출력되는 걸 확인할 수 있다.

중요한 것 이것이다. 우리는 기반 클래스를 작성하고 아무 코드도 작성하지 않았다. 즉, 우리는 ClassB 객체를 생성하거나 사용하지 않았다. 그럼에도 초기화가 자동으로 이루어졌다.

만약 이러한 상황을 원하지 않는다면 어떻게 해야 할까?

우리는 이러한 상황에서 @Lazy 어노테이션을 이용할 수 있다.

@Component
@Lazy
class ClassB {

    private ClassA classA;

    public ClassB(ClassA classA) {
        //Logic
        System.out.println("Some Initialization Logic");
        this.classA = classA;
    }
}

이제는 Some Initialization Logic이라는 출력을 볼 수 없다. 왜냐하면 @Lazy 어노테이션을 사용하는 경우, 해당 클래스의 객체를 사용해야 초기화가 이루어지기 때문이다.

아래와 같이 ClassB 객체를 사용하면 초기화가 이루어지는 것을 확인할 수 있다. 적는 김에 주석에 이것저것 적어놓았으니 확인하라.

package com.in28minutes.learn_spring_framework.example.d1;

import ....

@Component
class ClassA {

}

@Component
@Lazy // The bean is only loaded when you try to use that class.
/*
Exploring Lazy Initialization of Spring Beans

Default initialization for Spring Beans: Eager
Eager initialization is recommended:
- Errors in the configuration are discovered immediately at application startup

However, you can configure beans to be lazily initialized using Lazy annotation:
- Not recommended, Not frequently used

Lazy annotation:
- Can be used almost everywhere @Component and @Bean are used
- Lazy-resolution proxy will be injected instead of actual dependency
- Can be used on Configuration(@Configuration) class:
    - All @Bean methods within the @Configuration will be lazily initialized

If you use lazy initialization,
Spring configuration errors won't be detected when the application starts;
you'll get runtime errors
*/
class ClassB {
    ClassA classA;

    public ClassB(ClassA classA) {
        //Logic
        System.out.println("Some Initialization Logic");
        this.classA = classA;
    }

    public void doSomething() {
        System.out.println("Do Something");
    }
}

@Configuration
@ComponentScan//("com.in28minutes.learn_spring_framework.example.a1") // Spring needs to know where to find the Component.
public class LazyInitializationLauncherApplication {

    public static void main(String[] args) {

        try(var context =
                    new AnnotationConfigApplicationContext
                            (LazyInitializationLauncherApplication.class)) {

            System.out.println("Initialization of context is completed");

            context.getBean(ClassB.class).doSomething();
        }
    }
}

물론 이렇게 지연 초기화를 사용할 일은 자주 없다. 왜냐하면 즉시 초기화를 사용하는 경우에는 Spring 구성에 오류가 있을 경우 애플리케이션이 시작할 때 오류를 바로 확인할 수 있지만 지연 초기화를 사용하면 그렇지 않아 런타임 오류가 발생한다.

 

Heading Lazy Initialization Eager Initialization
Initialization time Bean initialized when it is first made use of in the application Bean initialized at startup of the application
Default Not Default Default
Code Snippet @Lazy OR @Lazy(vaule=true) @Lazy(value=false) OR Absence of @Lazy
What happens if there are errors in initializing? Errors will result in runtime exceptions Errors will prevent application from starting up
Usage Rarely used Very frequently used
Memory Consumption Less(until bean is intialized) All beans are initialized at startup
Recommended Scenario Beans very rarely used in your app Most of your beans

다음은 Prototype에 대해 알아 보자.

이번에 만들 기반 클래스의 이름은 BeanScopesLauncherApplication이라고 지어보자.

그리고 NormalClass과 PrototypeClass라는 이름의 빈 두 가지를 만들어보자.

@Component
class NormalClass {

}

@Component
class PrototypeClass {

}

그리고 PrototypeClass에 @Scope 어노테이션을 추가해보자. Scope 옆의 괄호에 `value=ConfigurableBeanFactory를 입력하고 점을 찍으면 두 가지 스코프가 표시된다. 싱글톤과 프로토타입을 확인할 수 있는데, 싱글톤이 기본으로 설정되어 있는 스코프이다. 여기에는 SCOPE_PROTOTYPE을 추가하자.

@Component
class NormalClass {

}

@Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Component
class PrototypeClass {

}

이렇게 하면 프로토타입 클래스가 된다. 그런데 일반 클래스와 프로토타입 클래스는 무엇이 다른 걸까?

코드를 통해 알아보자.

@Configuration
@ComponentScan//("com.in28minutes.learn_spring_framework.example.a1") // Spring needs to know where to find the Component.
public class BeanScopesLauncherApplication {

    public static void main(String[] args) {

        try(var context =
                    new AnnotationConfigApplicationContext
                            (BeanScopesLauncherApplication.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));
            System.out.println(context.getBean(PrototypeClass.class));

        }
    }
}

위의 코드를 작성한 뒤 실행해보자. NormalClass 객체를 호출하는 경우에는 특정 객체, 그러니까 동일한 Bean을 계속해서 호출하고 있지만, PrototypeClass 객체를 호출하는 경우에는 호출할 때마다 새로운 Bean이 호출된다.

이는 싱글톤은 Spring IoC 컨테이너 당 객체 인스턴스가 하나이지만, 프로토타입은 Spring IoC 컨테이너 당 객체 인스턴스가 여러 개일 수 있다는 차이점에서 비롯된다.

싱글톤, 프로토타입 외에도 Request, Session, Application, Websocket과 같은 여러 스코프가 있지만 이 강의에서는 알 필요 없다.

다만 싱글톤을 앎에 있어 주의해야 할 것은 Spring 싱글톤과 GOF에서 설명하는 Java 싱글톤이 다르다는 것이다.

    자바 싱글톤(GOF) 대 스프링 싱글톤
    - 스프링 싱글톤: Spring IoC 컨테이너당 하나의 객체 인스턴스
    - 자바 싱글톤(GOF): JVM당 하나의 객체 인스턴스

JVM에서 하나의 Spring IoC 컨테이너만 실행하는 경우, Spring 싱글톤과 Java 싱글톤을 서로 바꿔 사용할 수 있다.
그러나 JVM에서 여러 개의 Spring IoC 컨테이너를 실행하는 경우 Spring 싱글톤은 Java 싱글톤과 다르다.

일반적으로 JVM에서 여러 개의 Spring IoC 컨테이너를 실행하지는 않는다. 따라서 99.99%의 경우 Spring 싱글톤과 Java 싱글톤은 정확히 동일하다.

 

 

Heading Prototype Singleton
Instances Possibly Many per Spring IOC Container One per Spring IOC Container
Beans New bean instance created every time the bean is referred to Same Bean instance reused
Default NOT Default Default
Code Snippet @Scope(value=ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Scope(value=ConfigurableBeanFactory.SCOPE_SINGLETON) OR Defalut
Usage Rarely used Very frequently used
Recommended Scenario Stateful beans(e.g., If you need to create a bean that holds user information, you must create a separate bean for each user.) Stateless beans

이제 마지막으로 PostConstruct와 PreDestroy에 대해 알아보자.

이번 기반 클래스의 이름은 PrePostAnnotationsContextLauncherApplication이라고 하자.

그리고 SomeClass, SomeDependency라는 이름의 클래스를 만들어보자. SomeClass는 SomeDependency 인스턴스를 멤버변수로 가진다.

@Component
class SomeClass {
    private SomeDependency someDependency;

    public SomeClass(SomeDependency someDependency) {
        super();
        this.someDependency = someDependency;
        System.out.println("All dependencies are ready~!");
    }
}

@Component
class SomeDependency {

}

우리가 지금까지 많이 봐왔던 코드라고 할 수 있다.

그런데 여기서 우리가 초기화 과정의 일부로 의존성에서(멤버변수에서)특정 메소드를 호출하고자 한다면 어떻게 해야 할까? 이때 우리는 @PostConstruct를 사용한다. 사용 방법은 다음과 같다.

@Component
class SomeClass {
    private SomeDependency someDependency;

    public SomeClass(SomeDependency someDependency) {
        super();
        this.someDependency = someDependency;
        System.out.println("All dependencies are ready~!");
    }

    @PostConstruct
    public void initialize() {
        someDependency.getReady();
    }
}

@Component
class SomeDependency {

    public void getReady() {
        System.out.println("Some Logic using SomeDependency");
    }
}

위와 같이 어떤 메소드에 @PostConstruct 어노테이션을 붙이면, Spring은 의존성을 연결한 다음 @PostConstruct 어노테이션을 찾아 해당 어노테이션이 붙어있는 메소드를 호출한다.

@PostConsturct 어노테이션은 초기화를 수행하기 위해 의존성 주입이 완료된 후 실행해야 하는 메소드에서 사용하며, 이 메소드는 클래스를 사용하기 전에 호출되어야 한다. 즉, 다른 Bean이 이 Bean을 사용할 수 있게 되기 전에 이 메소드가 호출된다.

만약 우리가 어플리케이션을 종료하기 전에 어떤 작업을 수행하고 싶다면 이때는 @PreDestroy를 사용할 수 있다.

    @PreDestroy
    public void cleanup() {
        System.out.println("Clean Up!");
        /*
        Before the bean is deleted from the container, before it is deleted from the application context
        If you want to perform some behavior, you can use the PreDestroy annotation
        */
    }

@PreDestroy어노테이션은 인스턴스를 삭제하는 과정 중에 있음을 알려주는 콜백 알림으로 트리거로 하여 메소드를 사용한다. 이를 통해 일반적으로 리소스를 해제하거나, 활성화된 연결을 닫는데 사용한다.