[Spring] @Bean을 사용한 스프링 빈 등록
이번 포스트에는 스프링에서 가장 중요한 빈 등록 방식 @Bean에 대해 소개한다.
📌 서론
Spring Framework에서 @Bean은 매우 중요한 개념이다. 이는 개발자가 직접 제어할 수 없는 외부 라이브러리나 복잡한 구성이 필요한 객체를 스프링의 관리하에 두기 위해 사용되는 어노테이션이다. @Bean을 이해하고 올바르게 사용하는 것은 Spring 기반 애플리케이션의 효율적인 관리와 유연한 구성을 위해 필수적이다. 이를 통해 개발자는 스프링 컨테이너가 관리하는 빈(Bean) 객체를 생성하고, 이를 애플리케이션 전반에서 재사용할 수 있다.
@Bean의 이해는 Spring의 의존성 주입(Dependency Injection) 기능을 깊이 이해하는 데에도 중요하며, 이는 애플리케이션의 결합도를 낮추고 유지보수를 용이하게 한다.
1. @Bean 사용법 및 특징
기본 사용법
- @Bean 어노테이션은 스프링 빈을 정의하는 메서드에 적용되며, 해당 메서드가 반환하는 객체를 스프링 컨테이너의 빈으로 등록한다. 이 어노테이션은 주로 @Configuration 어노테이션이 붙은 클래스 내에서 사용된다.
- 아래의 코드를 보면 myBean() 메서드가 @Bean 어노테이션으로 정의되어 있다. meBean() 메서드가 생성한 MyBean 객체가 스프링 빈으로 등록된다.
@Configuration
public class MyConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
public class MyBean {
// 필드, 메서드, 생성자 등
}
@Bean의 주요 특징들
1. 빈의 이름
- 기본적으로 @Bean 어노테이션이 적용된 메서드의 이름이 빈의 이름으로 사용된다.
- 예를 들어, 아래의 코드에서 빈의 이름은 myBean이 된다.
@Configuration
public class MyConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
- 만약 빈의 이름을 내가 원하는 것으로 지정하려면 아래처럼 @Bean에 name 속성을 사용해서 작성하면 된다.
@Configuration
public class MyConfig {
@Bean(name = "customBeanName")
public MyBean anotherBean() {
return new MyBean();
}
}
2. 빈의 스코프
- 기본적으로 스프링 빈은 싱글톤(Singleton) 스코프를 가진다. 이는 스프링 컨테이너에서 하나의 인스턴스만 생성되고 공유됨을 의미한다. 그러나 @Scope 어노테이션을 사용하여 빈의 스코프를 변경할 수 있다.
- 예를 들어, 프로토타입 스코프로 설정하려면 다음과 같이 할 수 있다.
@Bean
@Scope("prototype")
public MyBean prototypeBean() {
return new MyBean();
}
3. 종속성 주입
- @Bean 메서드는 다른 빈을 주입받을 수 있으며, 이를 통해 빈 간의 관계를 설정할 수 있다.
- 예를 들어, 다음과 같이 다른 빈을 주입하여 관계를 형성할 수 있다.
@Bean
public MyBean myBean(AnotherBean anotherBean) {
MyBean myBean = new MyBean();
myBean.setAnotherBean(anotherBean);
return myBean;
}
4. 빈의 생명주기 콜백
- @Bean으로 생성된 빈은 초기화와 소멸 과정에서 콜백 메서드를 정의할 수 있다.
- 예를 들어, @PostConstruct와 @PreDestroy 어노테이션을 사용하여 초기화와 소멸 시의 동작을 정의할 수 있다. 이러한 콜백 메서드는 빈의 생명주기 중 각각 한 번 호출된다.
public class MyBean {
@PostConstruct
public void init() {
// 초기화 로직
}
@PreDestroy
public void destroy() {
// 소멸 로직
}
}
@PostConstruct
- 이 어노테이션은 빈이 생성된 후에 호출되는 메서드를 정의할 때 사용한다. 즉, Bean 객체가 생성되고 모든 의존성 주입이 완료된 후에 해당 메서드가 실행된다는 의미이다. 이를 통해 Bean의 초기화 로직을 정의할 수 있다. 예를 들어, 데이터베이스 연결 풀을 설정하거나 초기 데이터를 불러오는 작업을 수행할 때 유용하다.
@PreDestroy
- 이 어노테이션은 Bean이 소멸되기 직전에 호출되는 메서드를 정의할 때 사용한다. 빈이 소멸되기 전에 정리 작업을 수행하기 위해 이 어노테이션을 활용할 수 있다. 예를 들어, 데이터베이스 연결을 닫거나 리소스를 정리하는 작업을 수행할 때 사용된다. 즉, 이 메서드는 스프링 부트 애플리케이션을 종료하는 경우, 빈들이 소멸되고 그 과정에서 실행된다.
2. @Bean과 다양한 스코프 활용
활용 예시
- @Bean 어노테이션은 주로 외부 라이브러리와의 통합이나 복잡한 객체 생성 로직이 필요한 경우에 활용된다. 예를 들어, 데이터베이스 연결 풀을 설정하거나, 외부 서비스와의 통합을 위한 클라이언트 객체를 생성하는 데 사용할 수 있다.
- 이 예제에서 externalServiceClient() 메서드는 외부 서비스와의 통신을 위한 클라이언트 객체를 생성하여 반환한다. 이 객체는 스프링 빈으로 등록되어 애플리케이션 전반에서 사용될 수 있다. @Bean을 사용하는 것은 복잡한 객체 생성 로직을 갖는 경우, 또는 스프링이 자동으로 관리하지 못하는 외부 라이브러리 클래스를 스프링 빈으로 등록하고자 할 때 특히 유용하다.
@Configuration
public class ExternalServiceConfig {
@Bean
public ExternalServiceClient externalServiceClient() {
// 외부 서비스에 대한 설정을 포함한 클라이언트 생성
return new ExternalServiceClient("apiKey", "apiSecret");
}
}
싱글톤 스코프
싱글톤(Singleton) 스코프
- 이 스코프는 스프링의 기본 스코프로, 각 빈에 대해 스프링 컨테이너 내에 단 하나의 인스턴스만을 생성한다.
- 모든 요청에서 같은 객체 인스턴스가 사용되며, 애플리케이션의 시작과 종료 시점에 각각 생성되고 소멸된다.
싱글톤 스코프 사용 사례
- 상황: 애플리케이션 전반에 걸쳐 공유되는 리소스나 서비스가 필요한 경우.
- 해결: @Bean 어노테이션을 사용하여 싱글톤 스코프의 빈을 정의. 이는 기본 설정이므로 @Scope 지정이 필요 없음.
@Configuration
public class AppConfig {
@Bean
public ApplicationService applicationService() {
return new ApplicationService();
}
}
프로토타입 스코프
프로토타입(Prototype) 스코프
- 프로토타입 스코프를 갖는 빈은 스프링 컨테이너에서 getBean()을 호출할 때마다 새로운 인스턴스가 생성된다. 이 스코프는 각 요청이 독립적인 객체 인스턴스를 필요로 할 때 유용하다.
프로토타입 스코프 사용 사례
- 애플리케이션에서 각각의 HTTP 요청에 대해 개별적인 상태를 유지해야 하는 객체가 필요한 경우에는 @Bean 메서드에 @Scope("prototype")을 지정하여 각 요청마다 새로운 객체 인스턴스가 생성되도록 설정한다.
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public MyPrototypeBean myPrototypeBean() {
return new MyPrototypeBean();
}
}
스코프의 선택은 빈의 생명주기와 관련이 있다. 예를 들어, 상태를 가지고 있지 않는 서비스나 싱글톤으로 사용되는 인프라스트럭처 빈들은 싱글톤 스코프가 적합하고, 사용자의 특정 요청에 따라 상태를 유지해야 하는 빈들은 요청이나 세션 스코프가 적합하다.
빈 사용 시 주의사항
프로토타입 스코프 빈의 사용 주의사항
- 프로토타입 스코프 빈은 스프링 컨테이너에 의해 전체 생명주기(생성, 사용, 파기)가 관리되지 않는다. 따라서 빈의 생성과 파기에 대한 책임이 개발자에게 있으며, 이를 적절히 관리해야 한다.
싱글톤 스코프 빈과의 상호작용에서 발생하는 문제점
- 스프링에서 싱글톤 스코프는 애플리케이션의 생명주기 동안 단 하나의 인스턴스만을 생성한다. 반면, 프로토타입 스코프는 빈을 요청할 때마다 새로운 인스턴스를 생성한다.
- 문제는 싱글톤 빈이 프로토타입 빈을 주입받을 때 발생하게 된다. 싱글톤 빈이 생성될 때 한 번만 프로토타입 빈이 주입되고, 이후로는 계속 같은 프로토타입 빈 인스턴스를 사용하게 된다. 즉, 프로토타입 빈이 제공하는 "요청마다 새로운 인스턴스"라는 특성이 무시되는 셈이다.
이 문제의 해결방법은 ObjectProvider, ApplicationContext를 사용하는 것이다.
해결방법1: ObjectProvider 사용 예시
- NotificationService라는 싱글톤 스코프의 서비스가 있고 이 서비스는 사용자별 알림을 처리하는데, 각 알림에 대해서 고유한 상태 정보를 필요로 한다. 이때 UserNotifier라는 프로토타입 스코프의 클래스로 이 상태 정보를 관리하게 된다. 이 상황에는 각 사용자 알림을 처리해야 할 때마다 UserNotifier의 새 인스턴스가 필요하다.
문제 상황
- 이 상황은 NotificationService는 애플리케이션 생명주기 동안 단 하나의 인스턴스만을 가지지만, 각 알림 처리에 따라 독립적인 UserNotifier 인스턴스를 사용해야 하는 상황이다.
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public UserNotifier userNotifier() {
return new UserNotifier();
}
@Bean
public NotificationService notificationService(ObjectProvider<UserNotifier> notifierProvider) {
return new NotificationService(notifierProvider);
}
}
- NotificationService의 sendNotification() 메서드에서 notifierProvider.getObject()를 호출하여 UserNotifier의 새 인스턴스를 받는다. 이 방식은 UserNotifier 빈의 프로토타입 스코프 특성을 올바르게 활용하여 각 알림 처리에 새로운 인스턴스를 사용한다.
// 알림 서비스 클래스
public class NotificationService {
// ObjectProvider를 의존한다.(주입받아야 한다.)
private final ObjectProvider<UserNotifier> notifierProvider;
// 생성자
public NotificationService(ObjectProvider<UserNotifier> notifierProvider) {
this.notifierProvider = notifierProvider;
}
// 알림보내는 메서드
public void sendNotification(String userId, String message) {
UserNotifier notifier = notifierProvider.getObject();
notifier.notify(userId, message);
// 여기서 UserNotifier는 매번 새로운 인스턴스
}
}
// 유저 알림 클래스
public class UserNotifier {
public void notify(String userId, String message) {
// 사용자에게 알림을 보내는 로직, 각 인스턴스는 다른 userId와 message를 기반으로 작동
}
}
해결방법2: ApplicationContext 사용 예시
- UserService라는 싱글톤 스코프의 서비스가 있고, 이 서비스는 사용자 요청마다 고유한 로깅을 처리해야 한다고 가정한다. 로그를 처리하는 UserActionLogger 클래스는 상태 정보(예: 사용자 ID, 요청 시간 등)를 가지고 있어야 하므로, 매 요청마다 새로운 인스턴스가 필요하다.
문제 상황
- UserService는 애플리케이션 생명주기 동안 단 하나의 인스턴스만을 가지지만, 각 사용자의 요청에 따라 독립적인 UserActionLogger 인스턴스를 사용해야 한다.
@Bean을 사용한 해결 방법
@Configuration
public class AppConfig {
@Bean
@Scope("prototype")
public UserActionLogger userActionLogger() {
return new UserActionLogger();
}
@Bean
public UserService userService(ApplicationContext context) {
return new UserService(context);
}
}
- 이 예시에서 UserService는 싱글톤 스코프를 갖지만, ApplicationContext를 사용하여 processUserRequest 메서드가 호출될 때마다 새로운 UserActionLogger 인스턴스를 요청한다. 이렇게 하면 UserActionLogger는 프로토타입 스코프를 가지게 되어, 각 요청에 따라 독립적인 상태 정보를 가질 수 있다.
public class UserService {
private final ApplicationContext context;
public UserService(ApplicationContext context) {
this.context = context;
}
public void processUserRequest(String userId) {
UserActionLogger logger = context.getBean(UserActionLogger.class);
logger.logAction(userId, "User request processed");
// 사용자 요청 처리 로직
}
}
public class UserActionLogger {
public void logAction(String userId, String action) {
// 로그 처리 로직, 각 인스턴스는 다른 userId를 기반으로 작동
}
}
3. @Bean의 다양한 사용
조건부 빈 등록
- @Conditional 어노테이션은 스프링에서 조건부 빈 등록을 가능하게 하는 어노테이션이다. 이 어노테이션을 사용하면 빈을 등록하는 조건을 프로그래밍적으로 설정할 수 있다.
예를 들어, 특정 조건을 만족하는 경우에만 빈을 등록하거나 등록하지 않을 수 있다.
- OnDevelopmentCondition 클래스는 특정 조건을 확인하고 그 결과에 따라 matches() 메서드를 반환한다. 이 메서드가 true를 반환하면 위의 myBean() 메서드가 실행되어 빈이 등록된다.
@Configuration
public class AppConfig {
@Bean
@Conditional(OnDevelopmentCondition.class)
public MyBean myBean() {
return new MyBean();
}
}
- 이렇게 설정하면 "development" 프로파일이 활성화되었을 때만 myBean() 메서드가 실행되어 빈이 등록된다. 다른 프로파일이나 환경에서는 빈이 등록되지 않는다. 이러한 조건 클래스를 사용하여 프로파일이나 환경 변수 등 다양한 조건을 검사할 수 있으며, 조건이 충족되면 해당 빈을 등록할 수 있다.
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class OnDevelopmentCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 여기에 조건을 정의하면 된다.
// 예를 들어, "development" 프로파일이 활성화되었을 때만 true를 반환하도록 설정할 수 있다.
String[] activeProfiles = context.getEnvironment().getActiveProfiles();
for (String profile : activeProfiles) {
// "development" 프로파일이 활성화되면 true 반환
if ("development".equals(profile)) {
return true;
}
}
return false; // 다른 경우에는 false 반환
}
}
외부 소스로부터 빈 구성
- 먼저 application.yml 파일에 빈에 주입할 API 키를 작성한다.
myapp:
apiKey: your-api-key-value
- 다음으로 위에서 작성한 프로퍼티 파일을 사용하여 빈을 구성하는 코드를 작성한다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:application.yml")
public class AppConfig {
@Value("${myapp.apiKey}")
private String apiKey;
@Bean
public MyApiClient myApiClient() {
// 외부 프로퍼티 파일에서 읽어온 API 키를 사용하여 MyApiClient 빈을 생성
return new MyApiClient(apiKey);
}
}
- 위의 코드에서는 @PropertySource 어노테이션을 사용하여 application.yml 파일을 클래스패스에서 읽어온다. 그리고 @Value 어노테이션을 사용하여 apiKey 프로퍼티 값을 가져와서 MyApiClient 빈을 생성할 때 사용한다.이렇게 하면 외부 소스로부터 빈의 구성 정보를 가져와서 빈을 생성할 수 있다.
📌 결론
이번에는 @Bean에 대한 시리즈를 만들면서 공부를 했다. @Bean은 SpringBoot를 사용하면서 많이 사용했지만 이 어노테이션이 스코프와 함께 동작하는지도 몰랐으며 요즘에 외부 라이브러리를 자주 사용하면서 많이 사용 중인데 대체 왜 @Bean을 사용해서 주입할까? @Component도 있는데 라는 고민을 가졌던 적이 있었다. 이번에 정리를 하며 이것들에 대한 이유를 알게 되었고 많은 공부가 되었다.