[주간 무심코 10月.2] 스프링 빈(Bean)에 관하여

무심코·2022년 10월 12일
1

Bean?

우리가 알던 기존의 Java Programming 에서는 Class를 생성하고 new를 입력하여 원하는 객체를 직접 생성한 후에 사용했다. 하지만 Spring에서는 직접 new 를 이용하여 생성한 객체가 아니라, Spring에 의하여 관리당하는 자바 객체를 사용한다. 이렇게 Spring에 의하여 생성되고 관리되는 자바 객체를 Bean이라고 한다. Spring 제어의 역전(IOC) 특징을 Bean을 사용하여 구현한다.

Spring Framework 에서는 Spring Bean 을 얻기 위하여 ApplicationContext.getBean() 와 같은 메소드를 사용하여 Spring 에서 직접 자바 객체를 얻어서 사용한다.

Bean 어떻게 사용해?

스프링에서 Bean을 사용하기 위해서는 어노테이션을 사용하거나 직접 등록하여 사용할 수 있다.

1. 어노테이션 사용하기

스프링에서는 @ComponentScan 어노테이션과 @Component 어노테이션을 사용해서 빈을 등록할 수 있다.

두 어노테이션이 사용되는 절차를 이해하기 위해서는 Spring IoC 컨테이너가 빈을 컨테이너 안에 등록하는 방식을 이해할 필요가 있다.

Spring IoC 컨테이너가 IoC 컨테이너를 만들고 그 안에 빈을 등록할때 사용하는 인터페이스들을 ‘라이프 사이클 콜백’이라고 부르는데 라이프 사이클 콜백 중에는 @Component 어노테이션을 찾아서 이 어노테이션이 붙어있는 모든 클래스의 인스턴스를 생성해 빈으로 등록하는 작업을 수행하는 어노테이션 프로세서가 등록되어 있다.

이때 @ComponentScan가 붙어있는 클래스가 있는 패키지를 기준으로 모든 하위 패키지의 모든 클래스를 훑어보면서 @Component 어노테이션(+ @Component를 사용하는 다른 어노테이션)을 찾는다.

즉, Spring IoC 에서 Spring에서 생성하고 관리될 객체로 만들 아이들을 찾는데 이렇게 사용될 아이들의 문지방에 @Component 어노테이션을 붙여 여기에요~ 하고 표시하는 것이다. (@Component는 클래스 또는 인터페이스 단위에 붙는다)

SpringBoot에서 사용하는 Controller , Service 어노테이션 안에도 다음과 같이 @Component 어노테이션이 들어가 있다.

In Life Cycle CallBack(by Spring IoC 컨테이너)

@ComponentScan 찾는다
→ 해당 클래스가 속해있는 패키지부터 그 하위 패키지를 전부 탐색한다
→ @Component(+ @Component를 사용하는 다른 어노테이션) 찾는다
→ 해당 클래스의 인스턴스 생성
→ Spring IoC 컨테이너안에 Bean으로 등록

2. 직접 등록하기

빈 설정파일에 직접 빈으로 등록할 수 있다. 직접 빈을 등록하는 방법은 개발자가 컨트롤이 불가능한 외부 라이브러리들을 Bean으로 등록하고 싶은 경우에 사용된다.
빈 설정파일은 XML과 자바 설정파일로 작성할 수 있는데 최근 추세는 자바 설정파일을 좀 더 많이 사용한다.

자바 설정파일은 자바 클래스를 생성해서 작성할 수 있으며 일반적으로 xxxxConfiguration와 같이 명명한다.

클래스에 @Configuration 어노테이션을 붙이고 그 안에 @Bean 어노테이션을 사용해 직접 빈을 정의한다.

// Hello.java
@Configuration // Configuration 역할을 하는 Class 지정
public class ApplicationConfiguration {
    @Bean // 해당 File 하위에 Bean으로 등록할 Class 지정
    public HelloController sampleController() {
        return new SampleController;
    }
}

@Configuration 어노테이션도 @Component를 사용하기 때문에 @ComponentScan의 스캔 대상이 되고 그에 따라 Bean 설정파일이 읽힐때 그 안에 정의한 Bean 들이 IoC 컨테이너에 등록되게 된다.

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import java.util.Arrays;
 
public class SpringStudyApplication {
 
    public static void main(String[] args) {
 
        ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfiguration.class);
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        System.out.println(Arrays.toString(beanDefinitionNames));
 
        ChatService chatService = (ChatService) context.getBean("chatService");
        System.out.println("chatService.getStatus() = " + chatService.getStatus());
    }
}

작성한 자바 설정파일을 위와 같이

new AnnotationConfigApplicationContext(ApplicationConfiguration.class) 메소드를 사용해 ApplicationContext 에 등록하면 Bean으로 등록하고 사용이 가능하다.

물론 이와 같은 방식은 Component Scan 방식보다 귀찮음을 유발한다. 그래서 자바 설정 파일 안에서도 Component scan을 설정할 수 있다.

Component scan을 사용하려면 Scan을 시작하는 패키지 위치를 당연히 지정해 줘야하는데 지정해주는 방식이 두가지가 있다. basePackages는 패키지 이름을 문자열로, basePackagesClasses는 scan을 시작할 위치의 클래스를 직접 지정한다. basePackagesClasses가 더 type safe한 방법이다.

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
 
@Configuration
//@ComponentScan(basePackages = "com.atoz_develop.springapplicationcontext")
@ComponentScan(basePackageClasses = ApplicationConfiguration.class)    // 더 type safe한 방법
public class ApplicationConfiguration {
...
}

3. 추가적인 Bean 사용 방법

직접 Bean을 등록하는 방법으로 XML에 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
			 http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="chatService"
          class="com.example.springstudy.ChatService">
				<!-- 의존성 주입 방식 -->
        <property name="chatRepository" ref="chatRepository"/>
    </bean>

    <bean id="chatRepository"
          class="com.example.springstudy.ChatRepository"/>
</beans>
@SpringBootApplication
public class SpringStudyApplication {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        System.out.println("Arrays.toString(beanDefinitionNames) = " + Arrays.toString(beanDefinitionNames));

				ChatService chatService = (ChatService) context.getBean("chatService");
        System.out.println("chatService.getStatus() = " + chatService.getStatus());
    }
}

위와 같이 XML 파일 안에 직접 Bean으로 등록하기 원하는 자바 객체를 등록해주고 이를 ClassPathXmlApplicationContext("application.xml"); 메소드로 ApplicationContext를 생성해주면 Bean으로 등록이 되고 사용이 가능하다.

Component scan in XML

짐작이 가겠지만 위와 같은 방식은 개발시 Bean으로 등록하기 원하는 모든 객체를 일일이 XML에 등록해줘야 하는 굉장한 귀찮음이 발생한다.

그래서 XML에도 Component scan을 적용할 수 있다.

<?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 ">

    <context:component-scan base-package="com.example.springstudy"/>

</beans>

위 코드에서 보듯이 <context:component-scan base-package="com.example.springstudy"/> 를 사용하여 component scan를 시작할 base-package를 지정하고 사용하기 원하는 객체들에 @Component 어노테이션을 사용해주면 Bean으로 등록하고 사용할 수 있다.

스프링에서 제공하는 Bean Scope

Bean Scope = 빈 범위?

Bean 스코프는 빈의 범위라기 보단 빈을 어떤 방식으로 생성할건지에 대한 결정이다.

Spring이 구동 될때 ApplicationContext에서 하나의 객체에서 한 개의 빈만 가지도록 한번에 생성하는 방식이 있고 DI가 발생할 때마다 다른 빈을 생성하여 매번 사용될 때마다 빈을 생성하는 방식이 있다.

전자의 경우 ‘singleton bean scope’이고 후자의 경우 ‘prototype bean scope’이다.

Singleton bean scope

앞서 이야기 했듯이 싱글톤 빈은 스프링 컨테이너에서 한 번만 생성되며, 컨테이너가 사라질 때 제거된다. 생성된 하나의 인스턴스는 ‘Spring Beans Cache’에 저장되고, 해당 빈에 대한 요청과 참조가 있으며 캐시된 객체를 반환한다. 하나만 생성되기 때문에 동일 참조를 보장한다.

기본적으로 모든 빈은 스코프가 명시적으로 지정되지 않으면 싱글톤이다. 물론 명시적으로 대상 클래스에 @Scope(”singletone”) 을 붙일 수도 있다.

싱글톤 타입으로 적합한 객체는 다음과 같다.

  • 상태가 없는 공유 객체
  • 읽기 전용으로만 상태를 가진 객체
  • 쓰기가 가능한 상태를 지니면서도 사용 빈도가 매우 높은 객체(단, 이때는 동기화 전략이 필요함)

@Configuration에서 싱글톤

@Configuration
public class ApplicationConfiguration {
    @Bean
    public HelloService helloService() {
        return new HelloServiceImpl(helloRepository());
    }
		@Bean
    public ByeService byeService() {
        return new byeServiceImpl(helloRepository());
    }
		@Bean
    public HelloRepository helloRepository() {
        return new MemoryHelloRepository;
    }
}

위와 같은 코드에서 HelloService와 ByeService를 빈으로 등록할때 각각 helloRepository()를 호출한다. 이때 각각 한개씩 총 두개의 MemoryHelloRepository가 생성되므로 싱글톤 규칙이 깨진다고 생각할 수 있지만 실제로는 깨지지 않는다.

이는 @Configuration 어노테이션에서 CGLIB를 사용하여 ApplicationConfiguration이 Bean으로 등록될 때 ApplicationConfiguration 대신 ApplicationConfiguration을 상속 받은 ApplicationConfiguration$CGLIB 형태의 프록시 객체로 스프링 컨테이너에 등록된다.

CGLIB는 아래와 같이 만약 해당 객체가 이미 스프링 컨테이너에 등록되어 있으면 등록된 객체를 컨테이너에서 찾아 사용하고 등록되어 있지 않다면 새로 등록하도록 구현되있다고 생각하면 된다. 이때 ApplicationConfiguration$CGLIBApplicationConfiguration의 자식 타입이므로 ApplicationConfiguration 타입으로 조회가 가능하다.


@Bean
public HelloRepository helloRepository() {
    if(MemoryHelloRepository가 이미 스프링 컨테이너에 등록되어있으면?) {
        return 스프링 컨테이너에서 찾아서 반환;
    } else { // 스프링 컨테이너에 없으면
        기존로직을 호출해서 MemoryHelloRepository를 생성하고 스프링 컨테이너에 등록
        return 반환;
    }
}

Singleton bean 생명주기

Singleton bean 생명주기는 다음과 같다

스프링 컨테이너 생성 → 스프링 빈 생성 → 의존 관계 주입 → 초기화 콜백(@PostConstruct) 
→ 사용 
→ 소멸 전 콜백(@PreDestroy) → 스프링 종료

Singleton bean is not Thread-Safe

싱글톤 패턴은 인스턴스가 전체 애플리케이션 중에서 단 한 번만 초기화되어 애플리케이션이 종료될 때까지 메모리에 상주한다는 특징이 있다. 만약 싱글톤이 상태를 갖게 된다면 멀티 스레드 환경에서 동기화 문제가 발생할 수 있다.

다시 스프링으로 돌아와서, 스프링 빈은 별다른 설정을 주지 않으면 싱글톤 빈이 된다. 그런데, 우리는 싱글톤 빈을 사용할 때 위와 같이 private 생성자, static 변수, static 메소드를 정의하지 않고도 싱글톤으로 잘 사용한다. 그래서 간혹 가다 개발자들이 싱글톤 빈은 상태를 가져도 Thread-Safe할 것이라는 착각을 하는 경우가 있다.

결론부터 말하자면, 스프링은 싱글톤 레지스트리를 통해 private 생성자, static 변수 등의 코드 없이 비즈니스 로직에 집중하고 테스트 코드에 용이한 싱글톤 객체를 제공해 주는 것 뿐이지, 동기화 문제는 개발자가 처리해야 한다. 만약에 싱글톤 빈이 상태를 갖게 되고, 아무런 동기화 처리를 하지 않는다면 멀티 스레드 환경에서 부작용이 발생할 수 있으니 주의해야 한다.

Prototype bean scope

프로토 타입 빈은 DI가 발생할 때마다 새로운 객체가 생성되어 주입된다. 빈 소멸에는 싱글톤과는 다르게 스프링 컨테이너가 관여하지 않고, gc에 의해 빈이 제거된다.

대상 클래스에 Scope("prototype") 을 붙이면 지정된다.

프로토 타입으로 적합한 객체로는 다음과 같다.

  • 사용할 때마다 상태가 달라져야 하는 객체
  • 쓰기가 가능한 상태가 있는 객체

Prototype bean 생명 주기

Prototype bean 생명주기는 다음과 같다

스프링 컨테이너 생성 → 스프링 빈 생성 → 의존 관계 주입 → 초기화 콜백(@PostConstruct) 
→ 사용 
→ GC에 의한 수거 → 스프링 종료

* 출처 *
[Spring] Spring Bean 총 정리
'Spring' 카테고리의 글 목록

profile
지나치지 않기 위하여

0개의 댓글