스프링 프레임워크(Spring Framework)는 오늘날 자바 기반의 엔터프라이즈 애플리케이션 개발에서 사실상의 표준으로 자리 잡았습니다. 그 성공의 중심에는 '스프링 빈(Spring Bean)'이라는 핵심 개념이 있습니다. 빈은 단순히 객체를 지칭하는 용어를 넘어, 스프링이 제공하는 강력한 기능들의 기반이 되는 설계 사상이자 구체적인 기술입니다. 스프링을 사용한다는 것은 곧 스프링 컨테이너가 관리하는 빈을 활용하여 애플리케이션을 구축한다는 의미와 같습니다. 따라서 스프링 빈의 본질, 생명주기, 스코프, 그리고 관계 설정 방식을 깊이 있게 이해하는 것은 스프링을 제대로 활용하기 위한 필수적인 과정입니다.
이 글에서는 스프링 빈의 가장 기본적인 개념부터 시작하여, 다양한 설정 방식, 복잡한 생명주기의 각 단계, 상황에 맞는 스코프 활용법, 그리고 의존성 주입 패턴의 모범 사례에 이르기까지 심도 있게 탐구합니다. 단순한 문법 나열을 넘어 각 기능이 왜 필요하며, 어떤 상황에서 어떻게 사용하는 것이 효과적인지에 대한 통찰을 제공하고자 합니다. 이를 통해 독자 여러분은 더욱 견고하고 유연하며 유지보수가 용이한 스프링 애플리케이션을 설계하고 개발할 수 있는 역량을 갖추게 될 것입니다.
1. 스프링 빈(Spring Bean)의 본질
스프링의 세계로 첫발을 내디딜 때 가장 먼저 마주하는 개념은 바로 '빈(Bean)'입니다. 빈의 개념을 정확히 이해하는 것이 스프링의 핵심 철학을 파악하는 첫걸음입니다.
1.1. 빈이란 무엇인가?
가장 단순하게 정의하면, 스프링 빈은 스프링 IoC(Inversion of Control) 컨테이너에 의해 생성, 관리, 조립되는 객체입니다. 여기서 중요한 점은 '컨테이너에 의해 관리된다'는 것입니다. 개발자가 new
키워드를 사용하여 직접 객체를 생성하고 그 관계를 설정하는 전통적인 방식과는 달리, 스프링에서는 객체의 생성과 생명주기, 다른 객체와의 관계 설정 등 모든 제어권이 스프링 컨테이너에게 위임됩니다.
스프링 빈은 특별한 클래스가 아닙니다. 우리가 흔히 작성하는 평범한 자바 객체, 즉 POJO(Plain Old Java Object)가 스프링 컨테이너에 등록되어 관리되면 그것이 바로 스프링 빈이 됩니다. 특정 프레임워크의 API를 상속받거나 구현할 필요가 없기 때문에, 코드는 특정 기술에 대한 종속성이 낮아지고 테스트하기 쉬운 구조를 갖게 됩니다.
1.2. 제어의 역전(IoC)과 스프링 컨테이너
제어의 역전(IoC)은 스프링의 가장 근본적인 철학입니다. 전통적인 프로그래밍에서 객체의 생성, 구성, 사용의 흐름을 개발자가 직접 제어했다면, IoC에서는 이 제어권이 외부(프레임워크, 즉 스프링 컨테이너)로 넘어가게 됩니다. 개발자는 단지 어떤 객체가 필요하고 어떻게 구성되어야 하는지에 대한 '설정 정보'만 제공하면 됩니다.
이러한 IoC를 구현하는 주체를 'IoC 컨테이너' 또는 간단히 '스프링 컨테이너'라고 부릅니다. 스프링 컨테이너는 개발자가 제공한 설정 정보(XML, Java Annotation, Java Code 등)를 바탕으로 객체(빈)를 생성하고, 이들 간의 의존 관계를 설정하며, 생명주기를 관리하는 역할을 수행합니다. 스프링 컨테이너의 핵심 인터페이스는 다음과 같습니다.
- BeanFactory: IoC 컨테이너의 가장 기본적인 형태입니다. 빈을 생성하고 관리하는 기본적인 기능을 제공하며, 빈이 요청될 때 생성하는 지연 로딩(Lazy Loading) 방식을 주로 사용합니다.
- ApplicationContext:
BeanFactory
를 상속받은 인터페이스로,BeanFactory
의 모든 기능을 포함하면서 트랜잭션 관리, 메시지 소스 처리(다국어 지원), 이벤트 발행/구독 등 엔터프라이즈 애플리케이션 개발에 필요한 다양한 부가 기능을 제공합니다. 특별한 이유가 없는 한, 대부분의 스프링 애플리케이션에서는ApplicationContext
를 사용합니다.
1.3. 의존성 주입(DI): 느슨한 결합의 시작
제어의 역전(IoC)을 구현하는 구체적인 방법론이 바로 의존성 주입(Dependency Injection, DI)입니다. 어떤 객체가 필요로 하는 다른 객체(의존성)를 외부(컨테이너)에서 주입해주는 것을 의미합니다. 예를 들어, OrderService
가 주문 데이터를 처리하기 위해 OrderRepository
를 필요로 한다고 가정해 보겠습니다.
DI가 없는 경우:
public class OrderService {
private OrderRepository orderRepository = new MySqlOrderRepository(); // 직접 생성 (강한 결합)
public void placeOrder() {
// ...
orderRepository.save(order);
}
}
위 코드에서 OrderService
는 MySqlOrderRepository
라는 구체적인 클래스에 직접 의존합니다. 만약 데이터베이스를 Oracle로 변경해야 한다면 OrderService
코드 자체를 수정해야 합니다. 이는 OCP(개방-폐쇄 원칙)를 위반하며 유연성이 떨어지는 설계입니다.
DI를 적용한 경우:
public class OrderService {
private final OrderRepository orderRepository;
// 생성자를 통해 외부에서 의존성을 주입받음
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void placeOrder() {
// ...
orderRepository.save(order);
}
}
DI를 적용하면 OrderService
는 OrderRepository
라는 인터페이스에만 의존하게 됩니다. 실제 어떤 구현체(MySqlOrderRepository
, OracleOrderRepository
등)가 주입될지는 외부의 설정에 따라 결정됩니다. 이처럼 DI를 통해 객체 간의 결합도를 낮추고(느슨한 결합), 코드의 유연성과 확장성을 크게 향상시킬 수 있습니다.
1.4. 왜 빈을 사용하는가?
스프링 빈과 IoC/DI를 사용하는 이유는 명확합니다.
- 모듈성 및 재사용성 향상: 각 컴포넌트(빈)는 독립적으로 개발되고 테스트될 수 있으며, 필요한 곳에 쉽게 주입하여 재사용할 수 있습니다.
- 낮은 결합도: DI를 통해 컴포넌트 간의 의존성이 인터페이스를 중심으로 설정되므로, 특정 구현이 변경되어도 다른 코드에 미치는 영향을 최소화할 수 있습니다.
- 테스트 용이성: 의존성을 외부에서 주입할 수 있기 때문에, 단위 테스트 시 실제 객체 대신 테스트용 Mock 객체를 쉽게 주입하여 독립적인 테스트가 가능해집니다.
- AOP(관점 지향 프로그래밍) 연동: 스프링 AOP는 프록시 기반으로 동작하는데, 이 프록시 객체를 빈으로 등록하고 관리함으로써 로깅, 트랜잭션, 보안과 같은 횡단 관심사를 비즈니스 로직과 분리하여 깔끔하게 적용할 수 있습니다.
2. 스프링 빈을 정의하는 세 가지 여정
스프링 컨테이너에게 어떤 객체를 빈으로 관리할지 알려주는 방법은 크게 세 가지로 발전해왔습니다. 각각의 방식은 장단점이 있으며, 프로젝트의 특성이나 개발팀의 선호도에 따라 선택적으로 사용됩니다.
2.1. 클래식한 접근: XML 기반 설정
스프링 프레임워크 초창기부터 사용된 가장 전통적인 방식입니다. 별도의 XML 파일에 <bean>
태그를 사용하여 빈의 정보(클래스, ID, 의존성 등)를 명시적으로 선언합니다.
<?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">
<!-- OrderRepository 빈 정의 -->
<bean id="orderRepository" class="com.example.repository.MySqlOrderRepository" />
<!-- OrderService 빈 정의 및 의존성 주입 -->
<bean id="orderService" class="com.example.service.OrderService">
<!-- 생성자 주입 -->
<constructor-arg ref="orderRepository" />
</bean>
</beans>
- 장점: 소스 코드의 수정 없이 설정 파일만 변경하여 애플리케이션의 동작을 바꿀 수 있습니다. 전체 시스템의 구성 및 의존 관계를 한눈에 파악하기 용이합니다.
- 단점: 프로젝트 규모가 커지면 XML 파일의 양이 방대해지고 관리가 복잡해집니다. 문자열 기반 설정으로 인해 컴파일 시점 에러 체크가 어렵고, 리팩토링 시 추적이 어렵다는 단점이 있습니다.
최근에는 XML 기반 설정의 사용 빈도가 줄어들었지만, 레거시 시스템을 유지보수하거나 특정 복잡한 설정을 다룰 때 여전히 사용될 수 있습니다.
2.2. 현대적 프로그래밍: Java 기반 설정
XML의 단점을 극복하기 위해 등장한 방식으로, 자바 클래스와 어노테이션을 사용하여 빈을 설정합니다. 타입-세이프(Type-safe)하며, IDE의 지원을 받아 리팩토링과 코드 추적이 용이합니다.
@Configuration
어노테이션을 클래스에 붙여 해당 클래스가 설정 정보를 담고 있음을 알리고, @Bean
어노테이션을 메서드에 붙여 해당 메서드가 반환하는 객체를 빈으로 등록합니다.
@Configuration
public class AppConfig {
@Bean
public OrderRepository orderRepository() {
return new MySqlOrderRepository();
}
@Bean
public OrderService orderService() {
// @Bean 메서드 호출을 통한 의존성 주입
return new OrderService(orderRepository());
}
}
여기서 주목할 점은 orderService()
메서드 내에서 orderRepository()
를 직접 호출하는 부분입니다. 만약 AppConfig
가 일반 클래스라면 orderRepository()
가 호출될 때마다 새로운 MySqlOrderRepository
인스턴스가 생성될 것입니다. 하지만 @Configuration
이 붙은 클래스는 스프링에 의해 CGLIB 프록시 객체로 감싸집니다. 이 프록시는 @Bean
메서드가 호출될 때 항상 컨테이너에 등록된 동일한 싱글톤 빈 인스턴스를 반환하도록 보장하여, 의도치 않은 객체 중복 생성을 방지합니다.
- 장점: 컴파일 시점에 타입 체크가 가능하여 안전합니다. XML보다 훨씬 간결하며, 복잡한 빈 생성 로직을 자바 코드로 자유롭게 구현할 수 있습니다.
- 단점: 애플리케이션의 구성 정보가 소스 코드에 섞여 들어간다는 점을 단점으로 보기도 합니다.
2.3. 가장 편리한 방법: 컴포넌트 스캔
컴포넌트 스캔은 설정 작업을 최소화하는 가장 자동화된 방식입니다. 개발자가 빈으로 등록하고자 하는 클래스에 특정 어노테이션(스테레오타입 어노테이션)을 붙여두면, 스프링이 지정된 패키지 경로를 스캔하여 해당 어노테이션이 붙은 클래스들을 찾아 자동으로 빈으로 등록합니다.
주요 스테레오타입 어노테이션은 다음과 같습니다.
@Component
: 가장 일반적인 목적의 어노테이션으로, 빈으로 등록할 클래스에 사용됩니다.@Service
: 비즈니스 로직을 담당하는 서비스 계층의 클래스에 사용됩니다.@Component
를 포함합니다.@Repository
: 데이터 접근 계층(DAO)의 클래스에 사용됩니다. 데이터 관련 예외를 스프링의DataAccessException
으로 변환해주는 기능이 포함되어 있습니다.@Controller
(Spring MVC) /@RestController
(Spring Boot): 프레젠테이션 계층(웹 컨트롤러)의 클래스에 사용됩니다.
사용 예시:
// 데이터 접근 계층
@Repository
public class MySqlOrderRepository implements OrderRepository {
// ...
}
// 서비스 계층
@Service
public class OrderService {
private final OrderRepository orderRepository;
// @Autowired를 통한 자동 의존성 주입
@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
// ...
}
// 설정 클래스
@Configuration
@ComponentScan(basePackages = "com.example") // 이 패키지 하위를 스캔
public class AppConfig {
// 컴포넌트 스캔을 사용하므로 @Bean 정의가 필요 없어짐
}
- 장점: 설정 정보가 극도로 간소화됩니다. 개발자는 비즈니스 로직에만 집중하고, 클래스에 어노테이션 하나만 추가하면 되므로 생산성이 매우 높습니다.
- 단점: 어떤 클래스들이 빈으로 등록되는지 명시적으로 드러나지 않아, 프로젝트 구조에 대한 이해가 부족할 경우 의도치 않은 빈이 등록되거나 누락될 수 있습니다.
현대의 스프링(특히 스프링 부트) 기반 애플리케이션에서는 컴포넌트 스캔을 기본으로 사용하고, 라이브러리 객체나 조건부 설정 등 복잡한 로직이 필요한 경우에만 Java 기반 설정을 보조적으로 사용하는 방식이 일반적입니다.
3. 탄생부터 소멸까지: 스프링 빈의 생명주기
스프링 컨테이너가 빈을 생성하고 소멸시키기까지의 과정은 정교하게 정의된 생명주기(Lifecycle)를 따릅니다. 이 생명주기를 이해하면 특정 시점에 원하는 로직을 실행시키거나 리소스를 관리하는 등 고급 기능을 효과적으로 활용할 수 있습니다.
3.1. 생명주기 전체 흐름
싱글톤 스코프 빈의 생명주기는 대략 다음과 같은 단계를 거칩니다.
- 스프링 컨테이너 생성:
ApplicationContext
인스턴스가 생성됩니다. - 빈 정의(Bean Definition) 로딩: XML, Java 설정 클래스 등을 읽어들여 각 빈에 대한 메타데이터(
BeanDefinition
)를 생성하고 저장합니다. - (필요시) BeanFactoryPostProcessor 실행: 모든 빈 정의가 로드된 후, 빈이 인스턴스화되기 전에
BeanFactoryPostProcessor
가 실행되어 빈 정의를 수정할 기회를 가집니다. - 빈 인스턴스화: 설정 정보를 바탕으로 빈의 객체 인스턴스를 생성합니다 (생성자 호출).
- 빈 속성 값 설정(의존성 주입):
@Autowired
,@Resource
등을 통해 빈이 의존하는 다른 빈들을 주입합니다. - Aware 인터페이스 콜백: 빈이
BeanNameAware
,BeanFactoryAware
,ApplicationContextAware
등의 인터페이스를 구현한 경우, 해당 콜백 메서드가 호출되어 컨테이너의 다양한 자원에 접근할 수 있게 됩니다. - (핵심) BeanPostProcessor의 `postProcessBeforeInitialization` 실행: 초기화 콜백 메서드가 실행되기 전에
BeanPostProcessor
의 `postProcessBeforeInitialization` 메서드가 호출됩니다. - 초기화 콜백 메서드 실행:
@PostConstruct
어노테이션이 붙은 메서드,InitializingBean
인터페이스의 `afterPropertiesSet()` 메서드, 설정 파일의 `init-method` 속성에 지정된 메서드가 순서대로 실행됩니다. - (핵심) BeanPostProcessor의 `postProcessAfterInitialization` 실행: 초기화 콜백 메서드가 실행된 후에
BeanPostProcessor
의 `postProcessAfterInitialization` 메서드가 호출됩니다. 이 단계에서 AOP를 위한 프록시 객체가 생성되는 경우가 많습니다. - 빈 사용 가능 상태: 모든 초기화 과정이 완료되고, 애플리케이션에서 해당 빈을 사용할 수 있는 상태가 됩니다.
- 컨테이너 종료 및 빈 소멸:
ApplicationContext
가 종료될 때,@PreDestroy
,DisposableBean
의 `destroy()`, `destroy-method` 속성 등에 지정된 소멸 콜백 메서드가 호출되어 리소스를 정리합니다.
3.2. 인스턴스화 이후: Aware 인터페이스
Aware 인터페이스는 빈이 자신의 이름, 자신이 속한 컨테이너 등 스프링 컨테이너의 내부 자원에 접근해야 할 때 사용합니다. 자주 사용되는 Aware 인터페이스는 다음과 같습니다.
BeanNameAware
: 자신의 빈 이름을 알아야 할 때 구현합니다.setBeanName(String name)
메서드가 호출됩니다.BeanFactoryAware
: 자신이 속한BeanFactory
에 접근해야 할 때 구현합니다.setBeanFactory(BeanFactory beanFactory)
메서드가 호출됩니다.ApplicationContextAware
: 자신이 속한ApplicationContext
에 접근해야 할 때 구현합니다.setApplicationContext(ApplicationContext applicationContext)
메서드가 호출됩니다.
3.3. 강력한 확장 포인트: BeanPostProcessor
BeanPostProcessor
는 스프링 컨테이너의 기능을 확장하고 싶을 때 사용하는 가장 강력한 도구 중 하나입니다. 이 인터페이스를 구현한 빈은 컨테이너에 있는 다른 모든 빈들의 초기화 전후에 개입하여 추가적인 처리를 수행할 수 있습니다. @Autowired
나 @PostConstruct
와 같은 어노테이션 기반 기능들이 내부적으로 BeanPostProcessor
를 통해 처리됩니다.
postProcessBeforeInitialization(Object bean, String beanName)
: 초기화 콜백(@PostConstruct
등)이 호출되기 전에 실행됩니다.postProcessAfterInitialization(Object bean, String beanName)
: 초기화 콜백이 호출된 후에 실행됩니다. 이 메서드는 원본 빈 객체를 반환할 수도 있고, 프록시 객체 등으로 감싸서 반환할 수도 있습니다. 스프링 AOP가 이 시점을 활용하여 타겟 객체를 프록시로 교체합니다.
3.4. 초기화 콜백 메소드
의존성 주입이 완료된 후, 빈이 실질적으로 사용되기 전에 특정 초기화 작업을 수행해야 할 때 사용합니다. 예를 들어, 데이터베이스 커넥션 풀을 초기화하거나, 캐시를 미리 로딩하는 등의 작업이 이에 해당합니다. 초기화 콜백을 지정하는 방법은 세 가지가 있으며, 우선순위가 존재합니다.
- (권장)
@PostConstruct
어노테이션: JSR-250 표준 어노테이션으로, 특정 프레임워크에 종속되지 않아 가장 선호되는 방식입니다. 사용이 간편하고 직관적입니다. InitializingBean
인터페이스:afterPropertiesSet()
메서드를 구현해야 합니다. 스프링 프레임워크에 종속적인 단점이 있습니다.@Bean(initMethod = "...")
속성: 외부 라이브러리의 클래스와 같이 소스 코드를 수정할 수 없는 경우에 유용합니다.
3.5. 소멸 콜백 메소드
컨테이너가 종료될 때, 빈이 사용하던 리소스를 해제하기 위해 사용됩니다. 예를 들어, 열려 있던 파일이나 네트워크 연결을 닫는 작업이 여기에 해당합니다. 소멸 콜백 역시 세 가지 방법이 있습니다.
- (권장)
@PreDestroy
어노테이션: JSR-250 표준 어노테이션으로,@PostConstruct
와 마찬가지로 가장 권장되는 방식입니다. DisposableBean
인터페이스:destroy()
메서드를 구현해야 합니다. 역시 스프링에 종속적입니다.@Bean(destroyMethod = "...")
속성: 외부 라이브러리 등에서 리소스 해제 메서드(예:close()
,shutdown()
)를 호출해야 할 때 유용합니다.
주의: 프로토타입(Prototype) 스코프의 빈은 컨테이너가 생성과 초기화까지만 관여하고, 그 이후의 생명주기는 클라이언트에게 위임합니다. 따라서 프로토타입 빈의 소멸 콜백 메서드는 호출되지 않습니다. 리소스 해제가 필요한 프로토타입 빈을 사용한다면 클라이언트 코드에서 직접 해제 로직을 호출해야 합니다.
4. 빈의 존재 범위: 스코프(Scope)의 이해
스프링 빈 스코프는 빈 인스턴스의 생존 범위와 공유 수준을 결정하는 중요한 개념입니다. 즉, 특정 빈에 대해 컨테이너가 하나의 인스턴스만 유지할 것인지, 아니면 요청이 있을 때마다 새로운 인스턴스를 생성할 것인지를 정의합니다. 적절한 스코프를 선택하는 것은 메모리 사용량, 성능, 그리고 애플리케이션의 동작 방식에 직접적인 영향을 미칩니다.
4.1. 싱글톤(Singleton): 단 하나의 공유 인스턴스
싱글톤은 스프링 빈의 기본 스코프입니다. 컨테이너 내에서 특정 빈 정의에 대해 단 하나의 인스턴스만 생성되며, 애플리케이션 전역에서 이 단일 인스턴스가 공유됩니다. getBean()
을 몇 번 호출하든 항상 동일한 객체를 반환합니다.
- 장점: 객체 생성 비용이 한 번만 발생하고, 메모리에 하나의 인스턴스만 존재하므로 효율적입니다. 상태가 없는(stateless) 서비스나 리포지토리 객체에 매우 적합합니다.
- 주의사항 (Thread-safety): 싱글톤 빈은 여러 스레드에서 동시에 접근하고 공유되므로, 인스턴스 변수(필드)를 통해 상태를 관리하면 동시성 문제(concurrency issue)가 발생할 수 있습니다. 싱글톤 빈은 가급적 상태를 가지지 않도록(stateless) 설계해야 합니다. 상태가 꼭 필요하다면, 메서드의 파라미터로 전달하거나,
ThreadLocal
을 사용하거나, 동기화(synchronization) 처리를 통해 스레드 안전성을 확보해야 합니다.
@Service
@Scope("singleton") // 기본값이므로 생략 가능
public class MySingletonService {
// ...
}
4.2. 프로토타입(Prototype): 매번 새로운 인스턴스
프로토타입 스코프의 빈은 컨테이너에 요청(getBean()
호출 또는 의존성 주입)이 있을 때마다 항상 새로운 인스턴스가 생성되어 반환됩니다. 컨테이너는 이 빈의 생성, 의존성 주입, 초기화까지만 관여하며, 그 이후의 관리 책임은 빈을 요청한 클라이언트에게 넘어갑니다. 따라서 프로토타입 빈의 소멸 콜백은 호출되지 않습니다.
- 사용 사례: 사용자마다 고유한 상태를 가져야 하는 객체나, 짧은 시간 동안만 사용되고 버려지는 객체에 적합합니다.
- 싱글톤 빈에서 프로토타입 빈 사용 시 문제점: 싱글톤 빈이 생성될 때 프로토타입 빈을 주입받는다면, 싱글톤 빈이 살아있는 동안에는 항상 동일한 프로토타입 빈 인스턴스만 참조하게 됩니다. 이는 프로토타입 스코프의 의도와 다릅니다. 이 문제를 해결하기 위한 방법은 다음과 같습니다.
- `ObjectProvider` 또는 `Provider` (JSR-330) 사용: 프로토타입 빈 자체를 주입받는 대신, 해당 빈을 생성해주는 팩토리(Provider)를 주입받습니다. 그리고 프로토타입 빈이 필요할 때마다
provider.getObject()
또는provider.get()
를 호출하여 새로운 인스턴스를 얻습니다. - `@Lookup` 어노테이션 (Lookup Method Injection): 추상 메서드에
@Lookup
어노테이션을 붙여두면, 스프링이 런타임에 해당 메서드를 오버라이드하여 컨테이너로부터 새로운 프로토타입 빈을 찾아 반환하는 코드를 생성해줍니다.
- `ObjectProvider` 또는 `Provider` (JSR-330) 사용: 프로토타입 빈 자체를 주입받는 대신, 해당 빈을 생성해주는 팩토리(Provider)를 주입받습니다. 그리고 프로토타입 빈이 필요할 때마다
@Component
@Scope("prototype")
public class MyPrototypeBean {
// ...
}
@Component
public class SingletonBean {
@Autowired
private Provider<MyPrototypeBean> prototypeBeanProvider;
public void usePrototype() {
MyPrototypeBean prototypeBean = prototypeBeanProvider.get(); // 매번 새로운 인스턴스
// ...
}
}
4.3. 웹 환경 스코프
웹 애플리케이션 환경에서만 사용할 수 있는 스코프들입니다. DispatcherServlet
과 같은 스프링 웹 관련 컴포넌트가 활성화되어 있어야 동작합니다.
- `request` 스코프: 각각의 HTTP 요청마다 새로운 빈 인스턴스가 생성됩니다. 요청이 시작될 때 생성되고, 요청이 끝날 때 소멸됩니다. 한 번의 요청 처리 과정 내에서는 동일한 인스턴스가 사용됩니다. 사용자 요청과 관련된 정보를 저장하는 데 유용합니다.
- `session` 스코프: 각각의 HTTP 세션마다 새로운 빈 인스턴스가 생성됩니다. 세션이 시작될 때 생성되고, 세션이 만료될 때 소멸됩니다. 사용자의 로그인 정보나 장바구니 정보 등 세션 기간 동안 유지되어야 하는 데이터를 관리하는 데 적합합니다.
- `application` 스코프: 웹 애플리케이션의 전체 생명주기와 동일한 범위를 가집니다.
ServletContext
당 하나의 인스턴스만 생성되며, 모든 세션과 요청에서 공유됩니다. 애플리케이션 전역에서 사용되는 설정 정보 등을 저장하는 데 사용될 수 있습니다. - `websocket` 스코프: 웹소켓 연결이 수립될 때 생성되고, 연결이 종료될 때 소멸됩니다. 웹소켓 세션 동안 상태를 유지하는 데 사용됩니다.
5. 의존성 주입(DI) 패턴과 모범 사례
의존성 주입은 스프링의 핵심 기능으로, 객체 간의 관계를 설정하는 방법을 정의합니다. 스프링은 생성자 주입, 수정자(Setter) 주입, 필드 주입의 세 가지 주요 방식을 제공하며, 각각의 특성과 권장 사용 사례가 다릅니다.
5.1. 생성자 주입 (Constructor Injection): 권장되는 방식
객체를 생성하는 시점에 생성자의 파라미터를 통해 의존성을 주입하는 방식입니다. 스프링 공식 문서에서도 권장하는 가장 이상적인 방법입니다.
@Service
public class OrderService {
private final OrderRepository orderRepository; // final 키워드 사용 가능
private final PaymentService paymentService;
// 스프링 4.3 이후부터는 생성자가 하나뿐이면 @Autowired 생략 가능
public OrderService(OrderRepository orderRepository, PaymentService paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
}
- 장점:
- 불변성(Immutability) 확보: 의존성을
final
로 선언할 수 있어, 객체가 생성된 이후에 의존성이 변경되는 것을 막을 수 있습니다. 이는 안정적인 코드 작성에 도움이 됩니다. - 의존성 누락 방지: 객체 생성 시점에 모든 필수 의존성이 주입되어야 하므로,
NullPointerException
(NPE)을 원천적으로 방지할 수 있습니다. - 순환 참조 감지: A -> B, B -> A와 같이 빈들이 서로를 순환적으로 참조하는 경우, 애플리케이션 실행 시점에
BeanCurrentlyInCreationException
이 발생하여 잘못된 설계를 조기에 발견할 수 있습니다. - 테스트 용이성: DI 컨테이너 없이도 일반 자바 코드에서 쉽게 테스트 객체를 생성하고 Mock 의존성을 주입할 수 있습니다.
- 불변성(Immutability) 확보: 의존성을
5.2. 수정자 주입 (Setter Injection): 선택적 의존성을 위한 선택
객체가 생성된 이후에 setter 메서드를 통해 의존성을 주입하는 방식입니다.
@Service
public class OrderService {
private OrderRepository orderRepository;
private PromotionService promotionService; // 선택적 의존성
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Autowired(required = false) // 주입할 빈이 없어도 예외가 발생하지 않음
public void setPromotionService(PromotionService promotionService) {
this.promotionService = promotionService;
}
}
- 장점: 주입할 빈이 없어도 되는 선택적인(Optional) 의존성을 표현하는 데 적합합니다.
@Autowired(required = false)
옵션과 함께 사용됩니다. - 단점: 객체가 생성된 후에도 의존성이 변경될 수 있어 불변성을 보장할 수 없습니다. 필수 의존성이 주입되었는지 컴파일 시점에 확인할 방법이 없어, 런타임에 NPE가 발생할 위험이 있습니다.
5.3. 필드 주입 (Field Injection): 지양해야 하는 이유
필드에 @Autowired
어노테이션을 직접 붙여 의존성을 주입하는 방식입니다. 코드가 간결해 보이지만 여러 심각한 단점을 가지고 있어 사용을 지양해야 합니다.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository; // 필드 주입
@Autowired
private PaymentService paymentService;
}
- 단점:
- 테스트의 어려움: DI 컨테이너의 도움 없이는 의존성을 주입할 방법이 없습니다. 순수 단위 테스트를 작성하려면 리플렉션(Reflection)과 같은 복잡한 기술을 사용해야 합니다.
- 숨겨진 의존성: 필드 주입은 외부에서 해당 객체가 어떤 의존성을 필요로 하는지 명확하게 드러내지 않습니다. 이는 코드의 가독성과 유지보수성을 떨어뜨립니다.
- 단일 책임 원칙(SRP) 위반 가능성: 필드 주입은 의존성을 추가하기가 너무 쉬워서, 하나의 클래스가 너무 많은 책임을 갖게 될 가능성이 커집니다. 생성자 주입을 사용하면 파라미터가 많아지는 것을 보고 리팩토링의 필요성을 인지하기 쉽습니다.
- 허용되는 경우: 일반적으로 애플리케이션 코드에서는 사용하지 않는 것이 좋지만, 테스트 코드 작성 시에는 편의를 위해 제한적으로 사용될 수 있습니다.
5.4. 의존성 탐색과 주입을 위한 어노테이션
@Autowired
: 스프링에서 제공하는 어노테이션으로, 주로 타입을 기준으로 의존성을 주입합니다. 만약 동일한 타입의 빈이 여러 개 있다면, 필드명이나 파라미터명을 빈 이름과 비교하여 주입을 시도합니다.@Qualifier("beanName")
:@Autowired
와 함께 사용하여, 동일한 타입의 빈이 여러 개 있을 때 특정 이름의 빈을 지정하여 주입받을 수 있도록 합니다. 모호성을 해결하는 데 사용됩니다.@Primary
: 동일한 타입의 빈이 여러 개 있을 때,@Primary
가 붙은 빈이 우선적으로 주입되도록 지정합니다.@Resource(name = "beanName")
: JSR-250 표준 어노테이션으로, 기본적으로 이름을 기준으로 의존성을 주입합니다. 지정된 이름의 빈이 없으면 타입을 기준으로 다시 탐색합니다.
6. 심화 주제: 스프링 빈 전문가 되기
기본적인 내용을 넘어, 스프링 빈을 더 깊이 있게 활용하기 위한 몇 가지 고급 주제를 살펴봅니다.
6.1. 지연 초기화(Lazy Initialization)와 @Lazy
기본적으로 스프링 컨테이너는 시작 시점에 모든 싱글톤 빈을 즉시 초기화합니다. 이는 애플리케이션 시작 시점에 설정 오류를 미리 발견할 수 있다는 장점이 있지만, 초기화에 오랜 시간이 걸리는 빈이 많으면 시작 속도가 느려질 수 있습니다.
@Lazy
어노테이션을 사용하면 해당 빈의 초기화를 실제 사용되는 시점(최초로 getBean()
이 호출되거나 다른 빈에 주입될 때)까지 지연시킬 수 있습니다. 이는 애플리케이션의 시작 시간을 단축하는 데 도움이 될 수 있지만, 첫 요청 시 지연이 발생할 수 있고 오류 발견 시점이 늦춰진다는 단점이 있습니다.
6.2. 순환 참조(Circular Dependencies)의 함정
순환 참조는 두 개 이상의 빈이 서로를 물고 물리는 의존 관계를 형성하는 것을 말합니다(예: A가 B를 필요로 하고, B가 A를 필요로 함). 이는 대부분의 경우 잘못된 설계의 신호입니다.
- 수정자/필드 주입의 경우: 스프링은 3단계 캐시(singletonFactories, earlySingletonObjects, singletonObjects)를 사용하여 제한적으로 순환 참조를 해결합니다. 먼저 빈 A의 인스턴스를 생성하고 캐시에 넣은 뒤, A에 B를 주입하려고 합니다. B를 생성하는 과정에서 A가 필요하면 캐시에서 아직 불완전한 A를 가져와 주입하고 B의 생성을 마칩니다. 그 후 완전해진 B를 A에 주입하여 순환 참조를 해결합니다.
- 생성자 주입의 경우: 생성자 주입에서는 순환 참조를 해결할 수 없습니다. 빈 A를 생성하려면 B가 필요한데, B를 생성하려면 A가 필요하므로 닭과 달걀 문제가 발생합니다. 이 경우 애플리케이션 시작 시
BeanCurrentlyInCreationException
이 발생하며, 이는 개발자에게 설계를 재검토하라는 명확한 신호를 줍니다.
6.3. `FactoryBean`: 빈 생성 로직의 캡슐화
FactoryBean
인터페이스는 빈의 생성 로직이 복잡하거나, 스프링이 직접 클래스를 인스턴스화할 수 없는 경우(예: 외부 라이브러리의 객체를 특정 설정을 거쳐 생성해야 할 때) 사용하는 기술입니다. FactoryBean
자체도 하나의 빈이지만, 컨테이너가 이 빈을 요청할 때는 FactoryBean
인스턴스 자체가 아니라 그것의 getObject()
메서드가 반환하는 객체를 최종 빈으로 사용합니다. 이는 팩토리 패턴을 스프링 빈 설정에 통합하는 방법입니다.
결론
스프링 빈은 스프링 프레임워크의 심장과도 같습니다. 단순한 객체 관리 단위를 넘어, 제어의 역전(IoC)과 의존성 주입(DI)이라는 핵심 원칙을 구현하는 매개체입니다. 빈의 정의 방식, 정교하게 설계된 생명주기, 다양한 스코프의 활용, 그리고 올바른 의존성 주입 패턴의 선택은 애플리케이션의 유연성, 확장성, 테스트 용이성을 결정짓는 중요한 요소입니다.
이 글에서 다룬 내용들을 바탕으로 스프링 빈의 동작 원리를 깊이 있게 이해한다면, 여러분은 단순히 프레임워크의 기능을 사용하는 것을 넘어, 스프링의 철학에 부합하는 견고하고 효율적인 애플리케이션을 설계하고 구축하는 진정한 '스프링 개발자'로 거듭날 수 있을 것입니다. 스프링 빈을 자유자재로 다루는 능력은 결국 더 나은 소프트웨어 아키텍처를 향한 첫걸음이 될 것입니다.