Spring에서 Bean은 Spring 컨테이너가 직접 생성하고 관리하는 Java 객체를 의미합니다.
일반적인 Java 객체는 개발자가 new 연산자를 통해 생성하고 관리하지만, Spring Bean은 스프링 컨테이너가 생성, 의존성 주입, 초기화, 소멸까지의 생명주기(Lifecycle)을 전적으로 관리합니다.
제어의 역전(IoC)과 의존성 주입(DI)
스프링이 Bean을 관리하는 원리를 이해하기 위해서는 IoC와 DI라는 개념을 알아야 합니다.
제어의 역전(IoC : Inversion of Control)
객체의 제어권이 개발자가 아닌 **프레임워크(Spring)**에 있는 것을 말합니다.
- 전통적인 프로그래밍에서는 개발자가 직접 객체를 생성하고 흐름을 제어했지만, 스프링에서는 프레임워크가 객체의 생명주기(생성, 설정, 초기화, 메서드 호출, 소멸)를 대신 관리합니다.
의존성 주입(DI : Dependency Injection)
IoC를 실제로 구현하는 방법 중 하나로, 객체를 직접 생성하지 않고 외부에서 생성하여 주입받는 방식입니다.
- 스프링 컨테이너가 각 객체 간의 의존 관계를 파악하여 자동으로 연결해 줍니다.
- 개발자는 설정 파일(XML)이나 어노테이션(@Component, @Autowired 등)을 통해 의존 관계가 필요하다는 정보만 제공하면 됩니다.
왜 Spring에게 제어권을 넘길까?
- 유연한 결합 (Loose Coupling): 인터페이스를 통해 Bean을 주입받으면, 구현체가 바뀌어도 사용하는 코드를 수정할 필요가 없습니다.
- 테스트 용이성: Mock 객체를 주입하기 쉬워져 단위 테스트 작성이 수월해집니다.
- 중복 생성 방지: Singleton 방식을 통해 메모리 자원을 효율적으로 사용합니다.
- 생명주기 콜백: @PostConstruct, @PreDestroy 등을 통해 객체가 생성되거나 소멸될 때 특정 로직(연결 종료 등)을 실행할 수 있습니다.
Spring Bean 등록하기
Bean을 등록하는 방법은 크게 두 가지가 있으며, 주로 어노테이션 방식을 사용합니다.
1) 어노테이션 기반 등록 (@Component)
클리스 위에 어노테이션을 붙이면, Spring이 컴포넌트 스캔(Component Scan)을 통해 자동으로 Bean으로 등록합니다.
- @Component: 기본적인 Bean 등록 어노테이션
- @Service: 비즈니스 로직을 처리하는 계층
- @Repository: 데이터베이스 접근 계층 (DAO)
- @Controller / @RestController: 웹 요청을 처리하는 계층
→ 각 어노테이션은 @Component 와 같은 역할을 하지만, 계층 구조와 역할을 명확하게 구분하기 위해 의미에 따라 사용합니다.
→ @Repository 의 경우 DB 관련 예외를 Spring 예외인 DataAccessException으로 변환해 줍니다.
2) 자바 설정 클래스 기반 등록 (@Configuration + @Bean)
메서드 레벨에서 직접 객체를 반환하도록 설정할 수 있습니다.
객체 생성 로직을 한눈에 파악 가능하여, 클래스 소스 코드를 수정할 수 없는 외부 라이브러리를 등록할 때 사용할 수 있습니다.
RestTemplate, ObjectMapper, SecurityFilterChain 등 외부 라이브러리 설정에 사용할 수 있습니다.
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
@Configuration
public class NetworkConfig {
@Bean
public RestTemplate restTemplate() {
// 타임아웃 등 세부 설정을 커스텀하여 빈으로 등록
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(3000);
return new RestTemplate(factory);
}
}
Bean Scope
Bean은 기본적으로 싱글톤 방식으로 적용되지만, 필요한 경우 Scope 변경이 가능합니다.
@Service
@Scope("prototype") // 요청할 때마다 새 객체 생성
public class PrototypeService { }
Scope 설명
| Singleton | (기본값) 스프링 컨테이너에 단 하나의 객체만 생성하여 공유합니다. |
| prototype | 빈을 조회할 때마다 새로운 객체를 생성하여 반환합니다. |
| request | 각각의 HTTP 요청마다 별도의 객체가 생성되고 요청이 끝나면 소멸합니다. |
| session | HTTP 세션과 동일한 생명주기를 갖습니다. |
의존성 주입(DI)의 3가지 방법
Spring에서 Bean을 주입받는 방법은 크게 세 가지가 있습니다.
1) 필드 주입(Field Injection)
변수 위에 바로 @Autowired 어노테이션을 붙이는 방식입니다.
@Service
public class OrderService {
@Autowired
private MemberRepository memberRepository;
}
간결하게 사용할 수 있지만, 외부에서 변경 불가능하다는 문제가 있습니다.
예를 들어, Spring 컨테이너 없이 순수 자바 코드로 단위 테스트를 짤 때, memberRepository에 가짜 객체(Mock)을 넣을 방법이 없습니다(Reflection을 써야 함).
또한, 의존 관계가 너무 숨겨져 있어 설계가 복잡해지기 쉽습니다.
2) Setter 주입(Setter Injection)
Setter 메서드에 @Autowired를 붙이는 방식입니다.
@Service
public class OrderService {
private MemberRepository memberRepository;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
Setter 형식으로, 주입받는 객체가 변경될 가능성이 있는 경우에 사용합니다.
주입이 완료되지 않은 상태에서도 객체가 생성될 수 있어, 메서드 호출 시 NullPointerException이 발생할 위험이 있습니다.
3) 생성자 주입(Constructor Injection)
생성자를 통해 의존성을 주입받는 방식으로 권장되는 방식입니다.
final 키워드를 통해 객체의 **불변성(Immutable)**을 보장합니다.
@Service
public class UserService {
private final UserRepository userRepository;
// 생성자 주입: Spring이 자동으로 UserRepository Bean을 주입해줌
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
왜 생성자 주입을 사용하라고 할까?
스프링 강의에서는 생성자 주입을 권장하는데, 아래의 세가지 장점이 있습니다.
1) 순환 참조(Circular Dependency)를 미리 방지
A가 B를 참조하고, B가 A를 참조하는 상황을 가정해 봅시다.
- 필드/수정자 주입:
일단 객체를 생성한 뒤 주입하기 때문에, 애플리케이션이 구동되다가 해당 빈을 호출하는 시점에 에러가 나거나 무한 루프에 빠집니다. - 생성자 주입:
객체를 생성하는 시점에 의존 관계를 모두 해결해야 하므로,
애플리케이션 실행 단계에서 BeanCurrentlyInCreationException이 발생하여 배포 전에 문제를 발견할 수 있습니다.
2) 테스트 코드 작성이 용이함
Spring 컨테이너를 띄우지 않고 자바 코드로만 테스트(Unit Test)를 짤 때,
생성자 주입은 new OrderService(new MockMemberRepository())처럼 직접 의존성을 넣어줄 수 있습니다.
@Autowired 필드 주입은 컴파일 시점에 의존성을 강제할 방법이 없습니다.
3) final 키워드를 통한 안정성
생성자 주입을 쓰면 필드에 final을 붙일 수 있습니다.
이는 "한 번 주입된 Bean은 애플리케이션 종료 시까지 절대 바뀌지 않는다"는 것을 보장하며,
혹시라도 생성자에서 할당을 누락하면 컴파일 에러로 바로 알려줍니다.
Lombok과의 조합 (@RequiredArgsConstructor)
생성자 주입이 좋기는 하지만, 필드가 많아질 수록 코드가 길어지는 단점이 잇습니다.
Lombok의 @RequiredArgsConstructor 어노테이션을 사용하면, 코드의 간결함과 생성자 주입의 장점을 살릴 수 있습니다.
@Service
@RequiredArgsConstructor // final이 붙은 필드만 모아서 생성자를 자동으로 만들어줌
public class OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자 코드를 따로 작성할 필요 없음!
}
Bean의 생명주기란? 어떻게 동작하는 걸까?
Spring Bean의 생명주기는 크게 5단계로 나뉩니다.
- 스프링 컨테이너 생성 : 설정 정보(AppConfig 등)를 읽어 빈 정의(Bean Definition)을 생성합니다.
- 빈 인스턴스화 : 자바 클래스를 기반으로 객체를 생성합니다. (new 연산자와 유사)
- 의존관계 주입(DI) : @Autowired 등으로 연결된 빈들을 주입합니다.
- 초기화 콜백 : 객체가 사용되기 전, 필요한 설정이나 연결 작업을 수행합니다. (@PostDestroy, InitializingBean 실행)
- 사용 : 애플리케이션에서 Bean 사용
- 소멸 콜백 : 애플리케이션 종료 전, 연결을 끊거나 리소스를 반납합니다. (@PreDestroy, DesposableBean 실행)
Bean의 초기화와 소멸 콜백 제어방법
Bean의 초기화와 소멸 콜백 제어를 위해 PostConstruct, @PreDestroy 어노테이션을 사용하는 방식이 권장되고 있습니다.
@Component
public class NetworkClient {
private String url;
public void setUrl(String url) { this.url = url; }
@PostConstruct
public void init() {
System.out.println("연결 시작: " + url);
// 실제 접속 로직
}
@PreDestroy
public void close() {
System.out.println("연결 종료");
// 리소스 해제 로직
}
}
초기화/소멸 메서드 위에 어노테이션을 붙이는 방식이기 때문에, 간결하고 가독성이 좋습니다.
Bean의 생명주기를 안다면, 아래와 같은 상황에서 유용하게 사용할 수 있습니다.
- 데이터베이스 커넥션 풀 관리 : 앱이 실행할 때 DB와 연결을 초기화하고(init), 앱을 종료할 때 안전하게 연결을 끊어야(destroy) DB의 과부하를 막을 수 있습니다.
- 캐시 로딩 : 앱 시작 시점에 자주 쓰이는 데이터를 메모리에 미리 올려두어 초기 성능을 높일 수 있습니다.
- 외부 API 서버 연동 : 소켓 연동 등을 유지해야하는 서비스에서 유용하게 사용할 수 있습니다.
참고 문서
'Backend' 카테고리의 다른 글
| Maven vs Gradle vs Ant : 빌드 방식의 변화 (0) | 2025.12.27 |
|---|---|
| Springboot 공통 예외처리를 위한 로직 (0) | 2025.12.13 |
| Spring Boot 테스트 코드를 통한 안정적인 애플리케이션 관리 (0) | 2025.12.10 |
| Spring에서 기상청 API 사용하기 (0) | 2023.09.08 |
| Jackson 라이브러리란? (0) | 2023.09.07 |