Spring Bean(1) - 스프링 빈(?)이란?

스프링에서 빈(Bean)은 컨테이너에 의해 직접 생성, 관리되는 핵심 객체이다. 스프링 거의 그 자체라고 봐도 될 정도로 중요도가 높으며, 많은 역할을 한다. 개발자 면접에서 단골로 등장하는 질문 중 하나인 IoC나 DI 등도 모두 빈과 관련이 있는 만큼, 스프링 프로그래밍을 위해서는 빈을 잘 아는 것이 중요하다. 필자는 빈을 이해하는 것이 스프링 프레임워크를 익히는 첫 단계라고 생각한다. 이번 포스팅에서는 빈의 핵심 개념과 사용법 등에 대해 알아보고자 한다.

빈(Bean)이란?

빈은 스프링 IoC 컨테이너에 의해 관리되는 POJO(Plain, Old Java Object) 형식의 인스턴스이다. 여기서 관리라 함은, 객체의 생성과 참조를 모두 스프링 컨테이너가 담당함을 의미한다. 다시 말해, 빈은 스프링 컨테이너가 직접 생성하고 관리하는 인스턴스 객체를 가리킨다. 군대로 치면 사단 직속의 특임대 정도일 것이고, 게임으로 치면 네임드 급의 인스턴스라고 할 수 있다.

빈은 개발자가 new 키워드를 이용하여 직접 생성할 필요가 없다. 스프링 컨테이너가 알아서 생성해 주기 때문에, @Autowired 어노테이션이나 getBean() 메소드 등을 이용하여 가져와 사용하기만 하면 될 뿐이다.

일반적으로 빈의 생성 시점은 스프링 컨테이너 구동 시점과 일치한다. 컨테이너 초기화가 끝났다면 빈으로 선언한 객체도 모두 만들어진 상태라고 생각해도 무방하다. 앞서 말한 것처럼 우리가 직접 빈을 생성할 필요가 없으므로, 아래와 같이 작성해두면 스프링이 ‘알아서’ 객체를 찾아 대입해 줄 것이다.

스프링 컨테이너에서의 빈(Bean) 사용 예시

@Autowired
SimpleBeanObject beanObject; // 이걸로 끝이다. 뭔가를 대입해 주지 않아도 된다.  

비교) 일반 인스턴스 사용 예시

NormalObject normalObject = new NormalObject(); 

위 예제에서 normalObject의 경우와 달리, 변수 beanObject에는 뭔가를 대입하거나 초기화하는 일체의 구문이 생략되어 있다. 단지 SimpleBeanObject 타입만 선언했을 뿐이다. 하지만 신기하게도, 이렇게만 해 두어도 변수에는 객체가 대입된다. 빈과 관련된 모든 것은 스프링 IoC 컨테이너가 알아서 처리해 주기 때문이다.

이처럼 개발자가 컨트롤하지 않고 컨테이너가 객체를 생성하고 변수에 주입하는 구조를 DI(Dependency Injection, 의존성 주입)라고 한다. 스프링을 가장 잘 대표하는 특성 중 하나이므로 잘 알아두기 바란다.

빈을 선언하는 방법

스프링 프로그래밍에서 빈을 선언하는 방식은 세 가지 정도로 요약할 수 있다.

  1. @Component 어노테이션을 사용하여 정의
  2. @Bean 어노테이션을 사용하여 정의
  3. XML 설정 파일을 통해 정의

3번의 XML 방식은 최근의 스프링 개발 트렌드인 스프링 부트(Spring Boot)에서는 거의 사용되지 않는다. 따라서 여기서는 1과 2 두 가지 방식에 대해서만 알아보도록 한다.

1. @Component 어노테이션

이 방식은 매우 간단하다. 임의의 클래스를 정의한 다음, @Component 어노테이션을 붙여주기만 하면 된다. 다음 예제를 보자.

public class PlayerUtils {
  
  public String buildPlayerId() {
    return UUID.randomUUID().toString()    
  }
}

PlayerUtils라는 임의의 클래스를 정의했다. 이 클래스에 포함된 buildPlayerId()는 호출될 때마다 랜덤 UUID를 만들어 반환한다. 이제 이 클래스를 빈으로 만들어 보자.

@Component
public class PlayerUtils {
... 

끝이다 :) @Component 하나만으로 PlayerUtils는 빈이 되었다. 무척 간단하다(고 생각한다). 이제 이 객체는 다음과 같은 방식으로 참조하면 된다.

@Autowired 
PlayerUtils playerUtils; 

...

public void foo() {
    String playerId = playerUtils.buildPlayerId();
    System.out.println("Player New ID: " + playerId);
}

스프링 컨테이너는 초기 구동시 모든 클래스를 순회하면서 @Component 어노테이션이 붙었는지 여부를 체크한다. 만약 해당 어노테이션이 부여된 클래스가 있다면 스프링은 이 클래스를 동적으로 로딩하여 빈으로 등록한다. 그리고 @Autowired 어노테이션이 붙은 변수를 찾아 해당 빈을 주입한다.

이런 구조 덕분에, 우리는 POJO 클래스에 @Component 어노테이션을 붙이기만 하면 빈을 만들어 낼 수 있다. 이후, 우리가 이 클래스를 직접 생성하지 않아도 PlayerUtils 인스턴스가 자동으로 만들어진다.

일반 객체가 시간이 지나면 GC로 인해 소멸되는 것과 달리, 한번 생성된 빈은 시간이 흘러도 GC로 소멸되지 않는다. 스프링 내부 컨택스트에 등록되어 지속적으로 참조되는 까닭에 가비지 수집 대상에 포함되지 않는 것이다. 또, 여러 곳에서 호출되더라도 매번 새로운 인스턴스가 생성되지 않고 기존 인스턴스가 지속적으로 재사용된다. 매 호출시 기존에 생성된 인스턴스를 재사용하는 매커니즘을 가지기 때문이다. 스프링에서 빈은 기본적으로 싱글톤(Singleton)이다.

빈은 스프링 컨테이너가 알아서 생성하기 때문에, 처음 빈을 다루다 보면 커스텀 초기화 구문을 어디에 작성해야 할지 난감할 수 있다. 마음대로 생성자를 정의할 수 없기 때문이다. 이 때에는 @PostConstructor 어노테이션이 유용하게 쓰인다.

이 어노테이션이 붙은 메소드는 빈이 생성된 직후 스프링 컨테이너에 의해 자동으로 호출된다. 따라서 초기화하고 싶은 내용이 있다면 메소드로 작성한 다음, 이 어노테이션을 붙여주기만 하면 된다. 주의해야 할 부분이 있다. 어노테이션이 붙을 메소드의 이름은 무엇이든 상관없지만, 자동으로 호출되는 함수이니만큼 파라미터가 존재해서는 안된다. 어떤 값을 넣어주어야 할지 스프링 컨테이너가 알 수 없기 때문이다. 만약 파라미터가 있는 메소드에 @PostConstructor를 붙이면 오류가 발생한다. Return 타입은 정의되어 있어도 오류가 발생하지는 않지만 실행시 무시된다.

사용 예시는 다음과 같다.

@Slf4j
@Component 
public class PlayerUtils {

  @PostConstructor
  private void init() {
    log.info(">>>>>>> PlayerUtils instance has created"); 
  }

	public String buildPlayerId() {
    return UUID.randomUUID().toString();
  }
}

스프링 컨테이너를 실행시키면 초기 구동 로그 중에서 >>>>>>> PlayerUtils instance has created를 볼 수 있다. 참고로, @Slf4j는 로깅 컴포넌트 사용을 가능하게 해주는 롬복(Lombok)의 기능 중 하나이다.

@Component 대신 사용할 수 있는 어노테이션

@Component 대신 @Configuration, @Service, @Repository 등을 사용해도 빈을 만들 수 있다. 이는 이들 어노테이션 내부에 @Component가 포함되어 있기 때문으로, 실제 어노테이션 코드를 확인하면 내부에 @Component가 포함되어 있는 것을 알 수 있다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
...

@Bean 어노테이션 방식

빈을 만드는 또다른 방법은 @Bean 어노테이션을 사용하는 것이다. 설명에 앞서 사용 방식을 알아보자.

public class MemberUtils {

	public String buildMemberId() {
    return UUID.randomUUID().toString();    
  }
}
...

POJO 타입의 일반 클래스를 정의하는 것 자체는 앞서의 방식과 그리 다르지 않다. @Component가 붙어있지 않은 것만 빼면 말이다.

@Component 어노테이션이 붙어 있지 않기 때문에, 이 클래스는 아직 빈이 아니다. 빈으로 만들기 위해서는 다음과 같은 메소드 하나가 더 필요하다.

@Bean 
public MemberUtils memberUtils() {
    return new MemberUtils();
}

@Bean이 붙은 이 메소드는 스프링에 의해 참조되어, 빈을 생성하는 데 사용된다. 예를 들어 개발자가 MemberUtils 빈을 사용할 거에요 라고 코드를 작성하면 스프링은 @Bean 어노테이션이 붙은 메소드 중에서 MemberUtils 객체를 반환하는 메소드를 찾아 실행하고, 그 결과를 빈으로 만들어 주입한다. (엄밀하게 따지면 @Bean 메소드를 찾아 빈을 등록하는 것이 먼저이고, 그 후에 유저가 이를 가져다 사용한다)

@Bean을 사용하는 전체 구문은 다음과 같다.

// MemberUtils.java 
public class MemberUtils {
  public String buildMemberId() {
    return UUID.randomUUID().toString();
  }
}

// BeanConfiguration.java 
@Configuration
public class BeanConfiguration {
  @Bean 
  public MemberUtils memberUtils() {
    return new MemberUtils();
  } 
}

얼핏 보기엔 @Component 방식보다 조금 더 번거로워 보이지만, 장점은 명확합니다. 이 방식을 사용하면 @Component가 붙지 않은 클래스를 코드 수정 없이도 빈으로 만들 수 있습니다. 원하는 클래스가 있다면 이를 반환하는 @Bean 메소드만 작성하면 되기 때문입니다. 따라서 다른 라이브러리에 정의된 클래스를 빈으로 다루어야 할 경우에 이 방식이 자주 사용됩니다.

간단한 예를 살펴봅시다. RabbitTemplate은 메시지 큐인 RabbitMQ를 다루기 위한 스프링 부트 기반 클래스입니다. 이 클래스는 spring-boot-starter-amqp 라이브러리 안에 이미 정의되어 있기 때문에, 우리가 임의로 수정을 가하여 @Component를 붙여줄 수 없습니다. 하지만 다음과 같이 @Bean 메소드를 구현하면 빈으로 만들어 사용할 수 있습니다.

@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
    RabbitTemplate rt = new RabbitTemplate();
    rt.setConnectionFactory(connectionFactory);
    rt.setMandatory(true);
    rt.setChannelTransacted(true);
    rt.setReplyTimeout(60000);
    rt.setMessageConverter(new Jackson2JsonMessageConverter());
    return rt;
}

@Bean 메소드 구현 방식은 다음과 같은 특징을 가진다.

  1. 메소드 이름은 빈 생성시 크게 영향을 미치지 않는다. 메소드 이름보다는 무엇을 반환하는가가 더 중요하다.
  2. 다만 동일한 타입을 반환하는 여러개의 @Bean 메소드가 있다면 메소드 명으로 빈을 구분할 수 있다.
  3. @Bean 메소드에 매개변수가 정의되어 있을 경우, 스프링 컨테이너는 빈으로 등록된 객체 중에서 일치하는 타입을 찾아 인자값으로 넣어준다. 위 예의 경우, ConnectionFactory 객체가 인자값으로 전달된다.

@Lazy

앞서 전술한 것처럼, 빈으로 지정된 객체는 스프링 컨테이너의 구동 시점에 자동으로 생성된다. 따라서 개발자가 생성 시점을 통제할 수 없다.

하지만 많은 빈을 다루다 보면 처음부터 모든 빈을 만들어 관리하는 것이 부담스럽게 느껴질 때도 있다. 이런 경우를 위해 @Lazy 어노테이션이 제공된다. 이 어노테이션을 @Component , @Bean과 함께 사용하면 해당 객체의 첫 호출 시점까지 빈 생성이 지연된다. 말하자면 미리 만들어져 서빙되는 것이 아니라 주문이 있을 때 만들어져 빈으로 등록되는 식이다.

사용 예는 다음과 같다.

@Lazy 
@Component 
public class PlayerUtils {
  ...
}

어떤 경우에 빈을 사용할까?

빈의 정체(?)에 대해 알았으니, 빈을 언제 사용해야 하는지 알아보자. 앞에서 말한 것처럼, 빈 자체는 그냥 평범한 클래스이다. 이를 스프링을 통해 생성하고 참조한다는 것이 다를 뿐이다. 어떤 경우에 빈을 만들어 사용하고, 어떤 경우에 일반 인스턴스를 사용해야 할까?

구분은 간단하다. 일회성 객체이거나 함수 안에서 사용할 객체일 경우 등 가벼운 용도로 만들어 사용하려면 일반 인스턴스로 사용하는 것이 낫다. SampleObject obj = new SampleObject() 처럼 new 키워드를 이용해서 생성하는 것 말이다.

반면, 한번 만들어 둔 객체를 지속적으로 유지해야 할 필요가 있다면 이는 빈으로 선언하는 편이 좋다. 애플리케이션의 실행 관련 정보를 관리하는 Context 객체 같은 것이 이에 해당한다.

일반적으로 빈의 라이프사이클은 스프링 컨테이너의 라이프사이클과 동일하다. 일반 객체는 참조하는 곳이 더이상 없으면 조만간 GC의 대상이 되어 소멸되지만, 빈은 스프링 컨테이너가 계속 관리하기 때문에 GC의 대상이 되지 않고 유지되다가 스프링 컨테이너가 종료될 때 함께 소멸한다. 따라서 만약 한번 생성해 두고 오랫동안 사용해야 하는 값이라면 빈으로 만드는 것이 유리하다.

또, 여러 곳에서 참조하는 객체 역시 빈으로 관리하는 것이 좋다. 예를 들면 데이터베이스에 엑세스하는 쿼리 관련 객체는 서비스 내 여러 곳에서 수시로 참조된다. 이런 객체를 빈으로 만들어 두면 스프링 컨테이너를 통해 손쉽게 참조할 수 있기 때문에, 굳이 참조를 파라미터 형태로 넘겨 전달할 필요가 없어 훨씬 깔끔한 코드를 유지할 수 있다.