5080 단어
25 분
[ Spring ] Spring Container, Bean

들어가며#


스프링 컨테이너#

스프링 컨테이너란?#

스프링 컨테이너는 어플리케이션에서 사용되는 객체(스프링 빈)들을 관리하는 주체이다. 즉, 스프링 컨테이너는 XML, 자바 설정, 또는 어노테이션 등을 통해 정의된 빈의 정보를 읽어 들여 해당 빈들을 생성하고, 필요한 의존관계를 자동으로 주입하며, 빈의 생명주기를 관리한다.

BeanFactory, ApplicationContext#

대체 텍스트

BeanFactory는 스프링 컨테이너의 최상위 인터페이스로, 스프링 빈을 관리하고 조회하는 기본 기능(예: getBean() 메서드)을 제공한다.

반면, Application Context는 단순히 빈을 관리하고 검색하는 기능을 넘어서, 애플리케이션 개발에 필요한 다양한 부가기능을 포함한다. Application Context는 별도의 설정 정보를 참고하여 빈(오브젝트)의 생성과 관계설정 등의 제어 작업을 총괄하며, IoC 방식을 구현한 일종의 빈 팩토리이다.

이 때문에 스프링에서는 Application Context를 IoC 컨테이너 또는 간단히 스프링 컨테이너라고 부르기도 하고, 때로는 빈 팩토리라고도 지칭한다. 실제로 ApplicationContext 인터페이스는 BeanFactory 인터페이스를 상속함으로써 빈 팩토리의 확장된 버전이라고 할 수 있으며, 이를 구현한 클래스들이 스프링의 대표적인 컨테이너 오브젝트로 활용된다.

NOTE

ApplicationContext는 BeanFactory의 기능을 모두 상속받으며, 메시지 소스를 활용한 국제화, 환경변수 관리, 애플리케이션 이벤트, 그리고 편리한 리소스 조회와 같은 부가기능들을 추가로 제공한다. 실무에서는 주로 이러한 부가기능이 포함된 ApplicationContext를 사용하며, BeanFactory를 직접 사용하는 경우는 거의 없다.

스프링 컨테이너의 생성 과정 (자바 설정)#

스프링 컨테이너가 생성되는 과정을 알아보자. 스프링 컨테이너는 XML이나 어노테이션 기반의 자바 설정 클래스로 생성할 수 있다.

//스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

위 코드와 같이 어노테이션 기반의 자바 설정 클래스를 사용하여 스프링 컨테이너를 생성할 수 있다.

여기서 ApplicationContext를 보통 스프링 컨테이너라 하며, 이는 인터페이스이다. 스프링 컨테이너는 전달받은 설정 정보(예, AppConfig.class)를 사용해 빈을 등록하고, 이후 의존관계 주입(DI)을 수행한다. 실제 개발에서는 BeanFactory와 ApplicationContext를 구분하지만, 일반적으로 부가기능이 포함된 ApplicationContext를 사용한다.

XML 설정#

대체 텍스트

스프링 컨테이너는 자바 코드, XML, Groovy 등 다양한 형식의 설정 정보를 지원하도록 설계되었다. 어노테이션 기반 자바 코드 설정은 new AnnotationConfigApplicationContext(AppConfig.class)와 같이 사용하고, XML 기반 설정은 GenericXmlApplicationContext를 이용해 XML 파일로부터 설정 정보를 읽어온다.

package hello.core.xml;

import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import static org.assertj.core.api.Assertions.*;

public class XmlAppContext {
    @Test
    void xmlAppContext() {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

위 코드와 같이 XML 설정 파일인 appConfig.xmlGenericXmlApplicationContext를 사용하여 스프링 컨테이너를 생성할 수 있다. XML 기반의 스프링 설정은 자바 코드로 작성한 AppConfig.java 설정 정보와 유사한 구성을 가지며, XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점이 있다.

BeanDefinition#

스프링은 다양한 설정 형식을 지원하기 위해, XML이나 자바 코드를 읽어들여 빈 설정 메타 정보인 BeanDefinition을 생성한다. 이 추상화를 통해 스프링 컨테이너는 자바 코드나 XML 설정 정보의 차이를 신경 쓰지 않고, 오직 BeanDefinition만으로 빈을 생성할 수 있다. @Bean이나 <bean>마다 각각 하나의 BeanDefinition이 생성되며, 이에는 빈의 클래스 이름, 팩토리 빈 이름, 팩토리 메서드 이름, 스코프, 초기화/소멸 메서드, 생성자 인자, 속성 등 다양한 정보가 포함된다.

대체 텍스트

AnnotationConfigApplicationContextAnnotatedBeanDefinitionReader 를 사용해서AppConfig.class 를 읽고 BeanDefinition 을 생성한다.

GenericXmlApplicationContextXmlBeanDefinitionReader 를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition 을 생성한다.

새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition 을 생성하면 된다.

package hello.core.beandefinition;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class BeanDefinitionTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    // GenericXmlApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

    @Test
    @DisplayName("빈 설정 메타정보 확인")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                System.out.println("beanDefinitionName" + beanDefinitionName +
                                   " beanDefinition = " + beanDefinition);
            }
        }
    }
}

위 코드는 BeanDefinition 정보를 확인하는 방법을 보여준다. BeanDefinition을 직접 생성해 스프링 컨테이너에 등록할 수도 있지만, 실무에서는 빈 설정 정보를 추상화하여 관리하는 방식으로 활용하며, 너무 깊게 이해하지 않아도 전체 메커니즘을 파악하는 데 충분하다.


스프링 빈#

스프링 빈이란#

스프링 빈은 스프링 컨테이너에 의해 생성되고 관리되는 객체를 의미한다. 스프링은 이 빈들을 직접 생성, 초기화, 의존관계 주입(DI), 그리고 소멸하는 전 과정을 관리한다.

빈 등록#

빈을 등록하는 방법은 크게 2가지가 있다. 어노테이션을 활용하는 방법, XML로 등록하는 방법이다.

어노테이션을 활용하는 방식은, 빈으로 등록하려는 클래스에 @Component, @Service, @Repository, @Controller 등을 붙여 컴포넌트 스캔을 통해 자동으로 빈을 등록하는 방법과, @Configuration 클래스 내의 @Bean 메서드를 통해 직접 빈을 등록하는 방법이 있다.

보통 반복적인 작업이나 같은 방식으로 빈을 등록해야 하는 부분에는 클래스에 어노테이션을 붙여 등록하고, 그 외에 특수한 작업들은 @Configuration 클래스 내의 @Bean 메서드를 통해 등록한다.

그리고 XML 파일 내에 <bean> 태그를 사용해 빈을 정의할 수 있다.(이 방법은 최근 프로젝트들에서는 잘 하지 않고, 레거시 프로젝트에서 많이 사용한다.)

의존관계 주입(Dependency Injection)#

생성된 빈들간의 관계를 정의해주자. 의존관계를 주입하는 방법에는 크게 3가지가 있다.

NOTE

일반 메서드를 주입하는 방법도 있지만 일반적으로 사용하지 않아 넣지 않았다. 있다는 것만 알아두자.

생성자 주입#

@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;
	}
}

생성자를 통해서 의존 관계를 주입 받는 방법이다.

  • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
  • 불변, 필수 의존관계에 사용
IMPORTANT

생성자가 1개만 있으면 @Autowired 생략해도 자동 주입 된다.

@Component

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;
	}
}

setter 주입#

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.

  • 선택, 변경 가능성이 있는 의존관계에 사용
  • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
@Component

public class OrderServiceImpl implements OrderService {

	private MemberRepository memberRepository;
	private DiscountPolicy discountPolicy;

	@Autowired
	public void setMemberRepository(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}

	@Autowired
	public void setDiscountPolicy(DiscountPolicy discountPolicy) {
		this.discountPolicy = discountPolicy;
	}

}
NOTE

@Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 지정하면 된다.

필드 주입#

@Component
public class OrderServiceImpl implements OrderService {

	@Autowired
	private MemberRepository memberRepository;

	@Autowired
	private DiscountPolicy discountPolicy;
}
  • 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다.
  • DI 프레임워크가 없으면 아무것도 할 수 없다.

보편적인 상황에서는 사용하지 말자. 애플리케이션의 실제 코드와 관계 없는 테스트 코드나 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용한다.

NOTE

순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다. @SpringBootTest 처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다.

빈 조회 방법: 이름, 타입, 구체 타입#

컨테이너에 등록된 모든 빈 조회#

스프링 컨테이너에 실제로 등록된 스프링 빈들이 올바르게 등록되었는지 확인하기 위해, 모든 빈 정보를 출력할 수 있다. 아래 예제 코드는 ac.getBeanDefinitionNames()로 스프링에 등록된 모든 빈 이름을 조회하고, ac.getBean()으로 해당 빈 객체를 가져와 출력하는 방법을 보여준다. 또한, 스프링이 내부적으로 사용하는 빈과 사용자가 직접 등록한 애플리케이션 빈을 구분하여 출력할 수도 있다.

package hello.core.beanfind;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;

class ApplicationContextInfoTest {
    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("name=" + beanDefinitionName + " object=" + bean);
        }
    }
    
    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            // Role ROLE_APPLICATION: 사용자가 정의한 빈, ROLE_INFRASTRUCTURE: 스프링 내부 빈
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name=" + beanDefinitionName + " object=" + bean);
            }
        }
    }
}

스프링 빈을 조회하는 기본적인 방법#

스프링 컨테이너에서 스프링 빈을 조회하는 기본 방법은 ac.getBean(빈이름, 타입) 또는 ac.getBean(타입)을 사용하는 것이다. 조회 대상의 빈이 존재하지 않으면 NoSuchBeanDefinitionException이 발생한다.

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;

class ApplicationContextBasicFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    
    @Test
    @DisplayName("이름 없이 타입만으로 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    
    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
    
    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("xxxxx", MemberService.class));
    }
}

위 예제 코드는 빈 이름, 타입, 구체 타입으로 스프링 빈을 조회하는 방법과, 존재하지 않는 빈 조회 시 발생하는 예외를 테스트하는 방법을 보여준다.

NOTE

구체 타입으로 조회하면 변경시 유연성이 떨어진다.

동일 타입 빈 조회 및 상속관계를 활용한 조회#

동일한 타입이 둘 이상일때 조회#

타입으로 스프링 빈을 조회할 때 동일한 타입의 빈이 둘 이상 있으면, 단순 조회 시 중복 오류가 발생한다. 이때는 빈 이름을 지정하여 문제를 해결할 수 있으며, ac.getBeansOfType()을 사용해 해당 타입의 모든 빈을 조회할 수 있다.

package hello.core.beanfind;

import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

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 + " value = " + beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }
    
    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }
        
        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}

위 코드는 동일 타입의 빈이 두 개 있을 때 발생하는 오류와 이를 해결하는 방법, 그리고 모든 빈을 조회하는 방법을 보여준다.

@Qualifier, @Primary에 대한 설명을 추가할지, 추가하려면 어디에 추가해야 할지

상속관계를 활용한 조회#

부모 타입으로 빈을 조회하면 자식 타입의 빈도 함께 조회된다. 만약 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 스프링 컨테이너에 등록된 모든 빈을 조회할 수 있다.

package hello.core.beanfind;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    
    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
    }
    
    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }
    
    @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 + " value=" + 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 + " value=" + beansOfType.get(key));
        }
    }
    
    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
        
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}

위 예제는 부모 타입 조회 시 자식 빈이 둘 이상 있을 경우 중복 오류가 발생하는 상황과, 빈 이름을 지정하여 해결하는 방법, 그리고 특정 하위 타입으로 조회하는 방법을 보여준다.


빈 생명주기 관리#

- 빈의 생성, 초기화, 사용, 소멸 단계
- 초기화와 종료가 필요한 이유 (리소스 관리 등)

빈의 생성, 초기화, 사용, 소멸 단계#

초기화와 종료가 필요한 이유 (리소스 관리 등)#


빈 생명주기 콜백 구현 방법#

- **인터페이스 기반 접근:** InitializingBean, DisposableBean
- **빈 등록 시 초기화/소멸 메서드 지정:** init-method, destroy-method
- **애노테이션 기반 접근:** @PostConstruct, @PreDestroy

인터페이스 기반 접근: InitializingBean, DisposableBean#

package hello.core.lifecycle;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }

    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);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception {
        disConnect();
    }

}

InitializingBeanafterPropertiesSet() 메서드로 초기화를 지원하며,
DisposableBeandestroy() 메서드로 소멸을 지원한다.

출력 결과

생성자 호출, url = null
NetworkClient.afterPropertiesSet
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:24:49.043 [main] DEBUG 
org.springframework.context.annotation.AnnotationConfigApplicationContext - 
Closing NetworkClient.destroy
close + http://hello-spring.dev

출력 결과를 보면 초기화 메서드가 주입 완료 후에 적절하게 호출된 것을 확인할 수 있으며, 스프링 컨테이너의 종료 시점에 소멸 메서드가 호출된 것도 확인할 수 있다.

초기화, 소멸 인터페이스 단점

  • 이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
  • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
  • 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

참고: 인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이며, 지금은 더 나은 방법들이 있어서 거의 사용하지 않는다.

빈 등록 시 초기화/소멸 메서드 지정: init-method, destroy-method#

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")처럼 초기화, 소멸 메서드를 지정할 수 있다.

설정 정보를 사용하도록 변경

package hello.core.lifecycle;

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }

    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);
    }

    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }

    public void close() {
        System.out.println("NetworkClient.close");
        disConnect();
    }

}

설정 정보에 초기화 소멸 메서드 지정

@Configuration
static class LifeCycleConfig {

    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }

}

결과

생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:33:10.029 [main] DEBUG 
org.springframework.context.annotation.AnnotationConfigApplicationContext - 
Closing NetworkClient.close
close + http://hello-spring.dev

설정 정보 사용 특징

  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

종료 메서드 추론

@BeandestroyMethod 속성은 기본값이 (inferred)로 등록되어 있다.
이 추론 기능은 close, shutdown이라는 이름의 메서드를 자동으로 호출해준다.
따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.
추론 기능을 사용하기 싫으면 destroyMethod=""처럼 빈 공백을 지정하면 된다.

어노테이션 기반 접근: @PostConstruct, @PreDestroy#

우선 코드 먼저 보고 설명하겠다.

package hello.core.lifecycle;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }

    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);
    }

    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }

    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.close");
        disConnect();
    }

}
@Configuration
static class LifeCycleConfig {

    @Bean
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.dev");
        return networkClient;
    }

}

실행 결과

생성자 호출, url = null

NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
19:40:50.269 [main] DEBUG 
org.springframework.context.annotation.AnnotationConfigApplicationContext - 
Closing NetworkClient.close
close + http://hello-spring.dev

@PostConstruct, @PreDestroy 애노테이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다.

@PostConstruct, @PreDestroy 애노테이션 특징

  • 최신 스프링에서 가장 권장하는 방법이다.
  • 애노테이션 하나만 붙이면 되므로 매우 편리하다.
  • 패키지를 잘 보면 javax.annotation.PostConstruct이다. 스프링에 종속적인 기술이 아니라 JSR-250라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료해야 하면 @Bean의 기능을 사용하자.

빈의 스코프와 관리 전략#

- 싱글턴 스코프와 프로토타입 스코프의 차이
- 웹 관련 스코프 (request, session 등)

싱글턴 스코프와 프로토타입 스코프의 차이#

웹 관련 스코드(request, session 등)#


출처#

- 주요 내용 요약
- 추가 학습 자료 (토비의 스프링, 스프링의 핵심 원리 등)
[ Spring ] Spring Container, Bean
https://blog-full-of-desire-v3.vercel.app/posts/spring/spring-container-bean/
저자
SpeculatingWook
게시일
2025-03-14
라이선스
CC BY-NC-SA 4.0