Spring Core Technolohies 1. IoC Container (3) Bean Scope와 Component Scanning
2023-05-21 13:24
스프링 공식 Document를 기반으로 스프링 코어 기술들에 대해 정리하는 시리즈물로써, IoC, DI, AOP 세 가지 개념을 중심으로 간단한 예시와 함께 스프링 컨테이너를 이해하고 코드 작성 방법을 익혀봅니다. IoC 마지막 글에서는 스프링에서 제공하는 Bean Scope와 빈을 정의하는 방법에 대해 설명합니다.
개요
마지막 장에서는 앞에서 간단히 설명했던 빈을 본격적으로 사용하기위해 예제 위주로 익혀본다. 빈 스코프에 대한 개념과 종류, 특징에 대해 간단히 알아본 뒤 어노테이션으로 빈을 설정하고 스프링에서 제공하는 컴포넌트 및 클래스패스 스캔을 통한 빈 등록 방법에 대해서 알아본다.
핵심 개념
Scope
- 생성된 빈 인스턴스가 존재할 수 있는 컨테이너 영역
- 빈을 생성하고, 소멸시키는 방법을 결정하며 이를 통해 메모리관리와 빈의 생성 시점 등을 세밀하게 제어할 수 있다.
- 스프링 프레임워크에서 제공하는 기본 스코프에는 singleton, prototype, request, session, application, websocket이 있다.
- 이 외에도 사용자 정의 스코프를 만들 수 있다.
Lifecycle Callbacks
- 빈의 생명주기와 관련된 콜백(callback) 메소드를 정의하기 위한 인터페이스를 그룹화한 것.
- 이 인터페이스를 구현하여, 빈이 생성, 초기화, 소멸될 때 실행될 메소드를 정의할 수 있다.
ApplicationContextAware and BeanNameAware
- 빈이 애플리케이션 컨텍스트에서 생성될 때, 빈의 이름과 애플리케이션 컨텍스트에 대한 참조를 제공하기 위한 인터페이스를 그룹화한 것.
- 이 인터페이스를 구현하여, 빈이 생성될 때, 애플리케이션 컨텍스트와 빈의 이름에 대한 정보를 받아서 사용할 수 있다.
Other Aware Interfaces
- 빈이 다른 객체와 관계를 맺을 때 사용되는 인터페이스를 그룹화한 것.
- 예를 들어, 빈이 다른 빈을 참조하거나, 스프링의 리소스(resource)를 사용할 때, 해당 인터페이스를 구현하여 사용할 수 있다.
Bean Scope란?
빈 스코프는 스프링 프레임워크에서 관리되는 객체의 생명주기인 인스턴스 생성 및 유지에 관한 규칙과 범위를 정의하는 개념이다. 즉, 스프링은 개발자가 빈 객체를 생성하고 관리할 수 있도록 지원하는데, 이때 빈 객체의 스코프는 객체가 생성되고 존재하는 범위를 의미한다.
예를 들어, 싱글톤 스코프는 하나의 인스턴스만을 생성하고 해당 컨텍스트 내에서 공유한다. 반면에 프로토타입 스코프는 매번 새로운 인스턴스를 생성하여 반환한다. 이외에도 요청(Request), 세션(Session), 어플리케이션(Application) 등이 있다. 이러한 스코프 개념을 통해 스프링은 객체의 범위와 생명주기를 효과적으로 관리하여 애플리케이션의 유연성과 성능을 향상시킬 수 있다.
빈 스코프를 통해 Bean Definition에 따라 생성된 객체에 연결되는 다양한 의존성과 값들을 제어하고 생성된 객체의 범위를 설정할 수 있다. 기본적인 빈 스코프에는 여섯가지가 있다.
이름 | 생성 범위 | 설명 |
---|---|---|
singleton | Spring IoC 컨테이너 | Singleton 스코프를 사용하면 하나의 객체 인스턴스를 모든 빈이 공유한다. |
prototype | - | 매번 요청 시에 새로운 빈 인스턴스를 생성한다. |
request | 각 HTTP 요청 | 이 스코프는 Web-aware한 Spring ApplicationContext에서만 사용할 수 있다. |
session | 각 HTTP 세션 | 이 스코프는 Web-aware한 Spring ApplicationContext에서만 사용할 수 있다. |
application | 각 ServletContext | 이 스코프는 Web-aware한 Spring ApplicationContext에서만 사용할 수 있다. |
websocket | 각 WebSocket 연결 | 이 스코프는 Web-aware한 Spring ApplicationContext에서만 사용할 수 있다. |
Web-aware한 Spring ApplicationContext란 일반적인 ApplicationContext과 다르게 웹 어플리케이션에서 사용하는 ServletContext, HttpSession, 그리고 WebSocket 등과 같은 웹 관련된 객체에 대한 지원을 추가적으로 제공하는 컨텍스트를 의미한다.
application 스코프와 websocket 스코프는 각각 ServletContext와 ApplicationContext에 특화된 스코프로써 스프링 프레임워크 이외에 다른 컨테이너에서 지원되지 않을 수 있으니 주의한다.
이제 각 스코프에 대해 알아보도록 하자.
Singleton scope
싱글톤으로 정의된 빈은 스프링 컨테이너 내에 단 하나만 존재하는 공유 인스턴스로 관리된다. 따라서 빈이 처음으로 요청되면 이는 스프링 컨테이너 내부의 캐시에 저장되었다가 해당 BeanDefinition과 일치하는 id를 가진 빈에 대한 모든 요청은 동일한, 캐시된 빈을 반환한다.
주의할 것은 이 때 싱글톤의 개념은 디자인 패턴에서 이야기하는 싱글톤 패턴과는 다르다는 것이다.
디자인 패턴에서의 싱글톤 패턴은 (빈이 아닌) 어떤 클래스가 오로지 하나의 인스턴스만 생성하도록 제한하는 것이며 싱글톤 스코프는 스프링 컨테이너가 관리하는 라이프사이클 하에서 하나의 객체 인스턴스를 생성해 이를 공유하도록 하는 것이다.
그 차이가 모호해 보일 수 있는데, 디자인 패턴과 빈이라는 큰 차이점과 그로 인해서 오는 제어의 주체가 누구인지 생각하면 될 것이다.
Prototype scope
프로토타입 스코프로 정의된 빈은 요청이 있을 때마다 새로운 빈 인스턴스를 생성한다.
다른 빈에 주입되거나 해당 빈이 호출 될 때 컨테이너는 getBean()
메소드를 호출하고 이 때 캐시된 값을 가져오는 싱글톤 빈과 다르게 매번 새로운 객체 인스턴스를 생성한다. 따라서 프로토타입 빈에 종속성을 가지고 있는 여러개의 빈이 있다고 할 때, 그 빈들은 각기 다른 프로토타입 빈을 참조한다.
여러 클라이언트(스코프 빈 등) 간에 공유되지 않는 빈이기 때문에 주로 각 빈의 고유한 상태 정보를 유지해야할 때 사용한다. 예를 들어, HTTP 요청을 처리할 때 사용되는 빈은 일반적으로 Prototype 스코프를 사용하는데 이를 통해 각 요청마다 새로운 인스턴스를 생성하고 유지할 수 있기 때문이다.
특이한 점은 프로토타입 스코프로 정의된 빈은 스프링이 라이프사이클을 전적으로 관리하지 않는다는 사실이다. 호출에 의해 생성된 빈이 요청자에게 전달되면 컨테이너는 해당 빈에 대한 기록을 유지하지 않는다. 따라서 빈의 소멸 또한 스프링에서 자동적으로 관리해주지 않으며 코드 내에서 자원을 직접 정리하고 메모리를 해제해야한다.
이렇게만 보면 new
커멘드를 이용해 개발자가 직접 객체를 생성하는 것과 별 다를 것 없어보이는데 왜 프로토타입 스코프 빈을 사용하는 것일까?
아래 코드를 보며 생각해보자.
public class Order {
private Customer customer;
public Order() {
this.customer = new Customer();
}
// ...
}
Order
객체가 생성될 때마다 새로운 Customer
객체를 생성한다.
Order
마다 다른 Customer
객체를 필요로 하므로 정석적으로 작성할 수 있는 코드이다.
크게 문제 없어보이는 간단한 코드지만 유지보수 상의 몇가지 문제가 생길 수 있다. 이를테면 다음과 같은 것들이 있다.
Customer
클래스가 변경될 경우,Order
클래스도 함께 변경되어야 한다.Customer
클래스의 생성 방식이 변경될 경우,Order
클래스도 함께 변경되어야 한다.Customer
객체를 사용하는 다른 클래스가 있다면, 해당 클래스도 함께 변경되어야 한다.
즉, 개발자가 직접 Customer
를 만들어 Order
에게 할당해줘야 하므로 손볼 곳이 자꾸 많아진다. 이전 시리즈에서 설명한 강한 의존성을 가졌을 떄 생기는 문제와 동일하다.
만약 Order
를 빈으로 정의하면 어떻게 될까?
@Component
@Scope("prototype")
public class Customer {
// ...
}
@Component
@Scope("prototype")
public class Order {
private Customer customer;
@Autowired
public Order(Customer customer) {
this.customer = customer;
}
// ...
}
Order
는 프로토타입 빈이기 때문에 각기 다른 인스턴스를 여러개 생성할 수 있고, 매번 다른 Customer
를 주입받을 것이다.
하지만 스프링 컨테이너에서 빈으로 정의된 Customer
인스턴스를 직접 생성해 주입해 줄 것이기 때문에 사용자는 Customer
클래스에 변경이 있어도 크게 신경 쓸 것이 없어진다.
Request, Session, Application, and WebSocket Scopes
request, session, application, websocket 스코프는 web-aware Spring ApplicationContext
구현체(XmlWebApplicationContext
등)를 사용할 때만 쓸 수 있다.
이 네가지 스코프를 사용하기 위해선 별개의 configuration 설정이 필요하다. 단, Spring Web MVC에서 사용하는 경우에는
DispatcherServlet
이 HTTP 요청을 처리할 때 필요한 정보들(ex. HTTP request, response 등)을 expose하기 때문에 추가 설정이 필요하지 않다.DispatcherServlet
이 expose한 정보들은 웹 어플리케이션 전반에 걸쳐 접근 가능한 상태가 되기 때문에 이를 활용해 request, session, application, websocket 스코프를 사용할 수 있는 것이다.
Request Scope
@RequestScope
@Component
public class UserInfo {
// ...
}
RequestScope
로 정의된 UserInfo
은 각 HTTP 요청마다 새로운 빈 인스턴스가 생성되어 사용되고 요청이 종료될 때 생성됐던 인스턴스는 제거된다.
웹 어플리케이션에서 요청을 보낸 사용자의 정보를 가져오는 기능을 구현할 때 UserInfo
를 사용할 수 있을 것이다. 서로 다른 HTTP 요청들은 UserInfo
를 공유하지 않고 독립적으로 처리할 수 있다.
Session Scope
@SessionScope
@Component
public class UserPreferences {
// ...
}
HTTP 세션이 유지되는 동안 UserPreferences
는 동일한 인스턴스를 유지한다. 세션이 만료될 때 Session Scope로 설정된 빈들도 함께 제거될 것이다.
웹 어플리케이션 세션에 대한 데이터를 유지하고 관리할 때 유용하게 사용될 수 있다. 예를 들어, 사용자의 로그인 정보나 서비스에서 사용자가 선택한 설정 등을 HTTP 세션에 저장해두면 세션이 유지되는 동안 하나의 빈을 사용하여 여러 HTTP 요청에서 동일한 상태값들을 가져올 수 있다.
Application Scope
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
ServletContext
레벨에서 유일한 인스턴스를 생성한다. 싱글톤 빈과 유사해보이나 멀티스레드 환경에서 차이점이 발생한다.
HTTP 요청이 들어오면 웹 서버는 요청을 처리하기 위해 새로운 스레드와 서블릿을 생성한다. 따라서 각 스레드에서 Application Scope로 정의된 빈들은 공유되지 않는다. 반면에 Singleton Scope로 정의된 빈들은 모든 스레드에 걸쳐서 공유된다.
스레드풀같은 메커니즘을 사용해 서블릿 인스턴스를 재사용한다해도, 서블릿 스코프로 생성된 빈은 스레드마다 별도의 인스턴스가 생성되어 사용되기 때문에 멀티스레드 환경에서 안전하게 빈을 사용할 수 있다.
WebSocket Scope
@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyWebSocketHandler {
// ...
}
웹소켓은 HTTP와는 다른 프로토콜을 사용하며, 클라이언트와 서버 간 양방향 통신을 가능하게 합니다. 웹소켓 스코프는 이러한 웹소켓 프로토콜을 사용하는 웹 애플리케이션에서 사용된다.
웹소켓 스코프를 사용하면, 각 웹소켓 세션마다 별도의 빈 인스턴스가 생성되며 웹소켓 세션의 생명주기와 함께 관리되므로, 해당 웹소켓 세션이 종료될 때까지 유지된다.
@WebSocketScope
는 Spring의WebSocket
구현체인 Spring Websocket에서만 사용할 수 있다.
어노테이션 기반 빈 설정하기
XML-based configuration과 Annotation-based configuration 중 뭐가 더 좋을까?
각 방식은 장단점이 있기에 경우에 다르다고 할 수 있다. 명확히 A 보다 B가 낫다고 말 하기가 애매하며 상황에 맞게 적절한 방법을 선택하는 것이 개발자의 몫이다.
스프링은 혼합된 configuration도 모두 수용하기 때문에 두 방식을 섞어서 사용할 수 있다. 예를 들어서 XML 파일에서 빈을 정의하고, 어노테이션을 사용하여 의존성을 주입하는 것도 가능하다.
Spring에서 XML 방식과 Annotation 방식을 섞어서 사용하는 경우, 먼저 XML 파일에서 등록한 빈들은 그대로 사용된다. 그리고나서 Annotation 방식으로 작성된 클래스들이 컴포넌트 스캔을 통해 빈으로 등록된다.
그러나 이렇게 두 가지 방식을 함께 사용하는 경우, 설정이 분산되어 같은 빈을 두 번 등록하는 등의 오류가 발생하는 등 유지보수가 어려울 수 있다. 따라서, 가능하면 한 가지 방식을 사용하는 것이 좋다.
이번 장에서는 가장 많이 사용하는 방식인 Annotation-based configuration을 사용하는 방법에 대해 정리하도록 한다.
@Autowired
@Inject vs. @Autowired
@Inject
와@Autowired
는 둘 다 의존성 주입을 위한 어노테이션으로 비슷한 기능을 수행한다.
@Inject
는 JSR 330에서 정의한 어노테이션으로, 자바 표준 스펙이므로 스프링 뿐만 아니라 다른 DI 프레임워크에서도 사용될 수 있다. 반면에@Autowired
는 스프링에서 제공하는 어노테이션으로, 스프링 프레임워크에서만 사용할 수 있다.
@Autowired
의 경우에는required
속성을 지정하여 주입할 빈이 반드시 존재해야 하는지 여부를 결정할 수 있다. 반면,@Inject
에는required
속성이 없다. 또한,@Autowired
의 경우 생성자, 세터, 필드에 모두 적용할 수 있지만,@Inject
는 생성자, 세터에만 적용할 수 있다.결론적으로, 스프링에서는
@Autowired
를 사용하는 것이 일반적이지만, JSR 330을 준수해야 하는 경우@Inject
를 사용할 수도 있다.
@Autowired
는 생성자, 세터, 필드에 선언하여 사용한다.
// 생성자에 선언
@Service
public class MyService {
private final MyRepository myRepository;
@Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
// 세터에 선언
@Service
public class MyService {
private MyRepository myRepository;
@Autowired
public void setMyRepository(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
// 필드에 선언
@Service
public class MyService {
@Autowired
private MyRepository myRepository;
}
단, @Autowired
를 필드에 사용하는 것은 권장되지 않는데 이유는 다음과 같다.
- 테스트하기 어렵다.
- 일반적으로 테스트에서 필드를 직접 주입하기 어렵기 때문에 생성자나 세터 주입보다 테스트하기 어렵다.
- 순환 참조 가능성
- 스프링 빈이 생성될 때, 모든 필드가 먼저 초기화되므로 두 개의 빈이 서로를 참조하는 경우, 한 쪽이 아직 초기화되지 않아
NullPointerException
이 발생할 수 있다. - 생성자 주입 방식을 사용하면 해당 빈에 필요한 의존성이 모두 주입된 후에 객체가 생성된다. 즉, 빈이 생성되기 전에 의존하는 다른 빈이 모두 생성되어 있어야 하기에 순환 참조가 발생할 가능성이 매우 적다.
- 세터 주입 방식에서는 빈이 생성된 후에 해당 빈의 의존성이 주입된다. 이 때문에 의존하는 다른 빈이 생성되지 않은 상태에서 해당 빈의 세터 메서드를 호출하면, 순환 참조가 발생할 가능성이 있지만 필드 주입에 비해 적은 편이다.
- 스프링 빈이 생성될 때, 모든 필드가 먼저 초기화되므로 두 개의 빈이 서로를 참조하는 경우, 한 쪽이 아직 초기화되지 않아
빈은 이름만 다르게 설정하면 같은 클래스여도 여러개 선언이 가능한데, 한 클래스에서 파생된 여러 빈을 모두 가지고 오고 싶을 때는 배열 타입으로 작성하여 가져올 수 있다.
@Configuration
public class AppConfig {
@Bean
public MyBean myBean1() {
return new MyBean("first");
}
@Bean
public MyBean myBean2() {
return new MyBean("second");
}
@Bean
public MyBean myBean3() {
return new MyBean("third");
}
}
@Component
public class MyComponent {
private MyBean[] myBeans;
@Autowired
public void setMyBeans(MyBean[] myBeans) {
this.myBeans = myBeans;
}
}
@Primary
@Autowired
를 사용하는 경우, 어떤 빈을 가져올 것인지에 대한 제어가 필요해진다.
@Primary
사용 시 동일한 클래스에 대해 우선권을 부여할 수 있다.
@Configuration
public class MovieConfiguration {
@Bean
@Primary
public MovieCatalog firstMovieCatalog() { ... }
@Bean
public MovieCatalog secondMovieCatalog() { ... }
// ...
}
public class MovieRecommender {
@Autowired
private MovieCatalog movieCatalog;
// ...
}
위의 예제에서 movieCatalog
빈은 firstMovieCatalog
이름을 가진 빈으로 할당된다.
@Primary
가 두 개 이상에 쓰일 경우 어떤 빈을 주입받을지 명확하지 않으므로 반드시 하나의 빈에만 적용하도록 한다.
@Qualifier
@Primary
는 여러 인스턴스 중 하나의 디폴트 빈을 설정할 때 유용하게 사용되며, @Primary
를 사용하지 않고 @Autowired
와 함께 빈의 이름을 직접 지정하는 @Qualifier
를 사용할 수도 있다.
필드 주입
public class MovieRecommender {
@Autowired
@Qualifier("firstMovieCatalog")
private MovieCatalog movieCatalog;
// ...
}
생성자 주입
public class MovieRecommender {
private final MovieCatalog movieCatalog;
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(@Qualifier("firstMovieCatalog") MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
@Autowired
+ @Qualifier
를 사용하는 것과 유사하게 JSR-250의 표준 어노테이션인 @Resource
어노테이션으로도 특정 빈을 주입받을 수 있다.
단, @Autowired
+ @Qualifier
는 스프링의 DI 기능을 사용하므로 Spring의 다른 기능들과 호환성이 좋아 Spring 프레임워크 내에서는 주로 이 방식을 사용한다. 반면에 @Resource
어노테이션은 Spring 프레임워크에 의존하지 않으므로 Spring에서 제공하는 다른 기능과의 호환성이 떨어질 수 있다.
Spring에 의존하지 않는 환경에서는 @Resource
를 사용하는 것이 더 나을 수도 있다.
두 방식은 작동 방식 또한 미세하게 다르다.
@Autowired
+ @Qualifier
는 먼저 타입을 확인 한 뒤 이름으로 빈을 찾는다.
@Resource
는 타입을 보지 않고 이름만으로 빈을 가져오려고 시도한다.
@Configuration
public class MyConfiguration {
@Bean
@Qualifier("myBean")
public ATypeBean myBean() {
return new ATypeBean();
}
@Bean
@Qualifier("mySecondBean")
public ATypeBean mySecondBean() {
return new ATypeBean();
}
@Bean
@Qualifier("myBean")
public BTypeBean bTypeBean() {
return new BTypeBean();
}
// ...
}
ATypeBean
은myBean
,mySecondBean
이라는 각기 다른 이름의 두 개의 빈이 존재한다.BTypeBean
은myBean
이라는 이름의 빈이 하나 존재하나,ATypeBean
인 빈과 이름이 동일하다.
이 빈들을 가져오는 코드를 살펴보자.
public class MyComponent {
@Autowired
private ATypeBean myBean;
// ...
}
이 코드는 @Autowired
만 사용했기에 같은 타입(ATypeBean
)을 가진 빈이 두 개 이상 존재하여 NoUniqueBeanDefinitionException
예외가 발생한다.
이 때, @Qualifier("myBean")
을 추가하면 myBean
이라는 이름의 빈을 가져 올 수 있다.
public class MyComponent {
@Autowired
@Qualifier("myBean")
private ATypeBean myBean;
// ...
}
myBean
이라는 이름은 ATypeBean
, BTypeBean
두 군데에서 사용하고 있지만 @Autowired
+ @Qualifier
를 사용할 땐 1) 타입을 확인 → 2) 이름으로 구분하기 때문에 적절한 빈을 가져올 수 있는 것이다.
반면에 @Resource
를 사용하면 오류가 발생한다.
public class MyComponent {
@Resource(name = "myBean")
private ATypeBean myBean;
// ...
}
@Resource
는 이름을 보고 빈을 찾기 때문에 ATypeBean
, BTypeBean
중 어떤 것을 가져와야 할지 알 수 없다. 따라서 타입이 다르지만 이름이 같은 빈이 존재한다면 @Autowired
+ @Qualifier
를 사용해야한다.
또한 @Resource
에서 name 값을 명시하지 않는다면 필드 이름을 보고 일치하는 것을 찾아오는데,
public class MyComponent {
@Resource
private ATypeBean mySecondBean;
// ...
}
위 코드는 mySecondBean
이라는 이름의 빈을 가져온다.
따라서 name을 생략하는 경우 필드 이름을 반드시 빈 이름과 매치되게 작성해주어야한다.
public class MyComponent {
@Resource
private BTypeBean mySecondBean;
// ...
}
BTypeBean
에 mySecondBean
이라는 이름을 가진 빈이 없기 때문에 해당 코드는 오류가 발생한다.
예시에서 보았듯이 @Resource
는 계속 필드에서만 사용되고 있는데, @Autowired
+ @Qualifier
가 필드, 생성자, 다중 인수 세터 메서드에 적용되는 반면 @Resource
는 단일 인수가 있는 필드나 세터 메소드에서만 사용 가능하다.
@Qulifier 아름답게 사용하기
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {
}
@Qualifier
어노테이션을 메타 어노테이션으로 가지는 Offline
어노테이션을 선언한다.
이 때, @Offline
은 @Qualifier("offline")
어노테이션과 동일한 역할을 한다.
public class MovieRecommender {
@Autowired
@Offline
private MovieCatalog offlineCatalog;
// ...
}
offlineCatalog
에는 offline
이라는 이름을 가진 MovieCatalog
빈이 주입될 것이다.
@Qualifier
메타 어노테이션과 함께 빈을 구분할 수 있는 다양한 필드를 가진 어노테이션을 활용할 수도 있다.
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {
String genre();
Format format();
}
public enum Format {
VHS, DVD, BLURAY
}
위와 같은 MovieQualifier
어노테이션을 만들고 genre
, format
을 이용해서 빈을 선언해보자.
@Component
@MovieQualifier(format = Format.VHS, genre = "Action")
public class ActionVhsCatalog implements MovieCatalog {
// ...
}
@Component
@MovieQualifier(format = Format.VHS, genre = "Comedy")
public class ComedyBluRayCatalog implements MovieCatalog {
// ...
}
@Component
@MovieQualifier(format = Format.DVD, genre = "Action")
public class ActionDvdCatalog implements MovieCatalog {
// ...
}
각 빈들은 이름으로도 구분할 수 있지만, format
과 genre
값을 이용해서도 구분될 수 있다.
public class MovieRecommender {
@Autowired
@MovieQualifier(format=Format.VHS, genre="Action")
private MovieCatalog actionVhsCatalog;
@Autowired
@MovieQualifier(format=Format.VHS, genre="Comedy")
private MovieCatalog comedyVhsCatalog;
@Autowired
@MovieQualifier(format=Format.DVD, genre="Action")
private MovieCatalog actionDvdCatalog;
// ...
}
자동 주입을 사용할 곳에 @MovieQualifier
를 이용하여 보다 복잡한 분류로 정의된 빈들을 가져올 수 있다.
위와 같이 특정 인터페이스의 구현체가 여러개 있고 각각을 빈으로 등록하는 등 여러 개의 빈이 존재하고 특정 조건을 만족하는 빈을 주입해야할 때, 조건이 복잡할수록 커스텀마이징된 어노테이션을 사용하는 것이 가독성과 유지보수 측면에서 좋을 수 있다.
보통은 빈의 이름으로만 구분해서 사용되지만 추가적으로 다른 속성을 사용해야 할 필요가 있을 때 이를 활용해 보는 것을 추천한다.
제네릭을 이용한 @Autowired
여기 제네릭 타입 파라미터를 가지는 Store
클래스와 이 클래스를 상속받는 빈으로 선언된 두 개의 클래스가 있다.
public class Store<T> {
private T value;
public Store(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
@Component
public class StringStore extends Store<String> {
// ...
}
@Component
public class IntegerStore extends Store<Integer> {
// ...
}
위 두 빈을 주입 받기 위해서 어떻게 코드를 짤 수 있을까?
여태 살펴본 방식들을 활용할 수 있을 것이다.
@Component
public class StoreRegister {
@Autowired
@Qualifier("stringStore")
private Store stringStore;
@Resource(name = "integerStore")
private Store integerStore
//...
}
특이하게도 제네릭 필드를 가지는 경우에는 제네릭 타입을 명시하는 것만으로도 적절한 빈을 가져올 수 있다.
@Component
public class StoreRegister {
@Autowired
private Store<String> s1; //stringStore bean
@Autowired
private Store<Integer> s2; //integerStore bean
//...
}
StringStore
타입으로 자동으로 유추되어 적절한 빈을 가져온다.
Class Scanning and Managed Component
마지막으로 클래스패스를 스캔하여 빈으로 등록된 컴포넌트들을 암시적으로 검색하는 옵션에 대해 설명한다.
빈의 후보가 될 컴포넌트는 필터 조건과 일치하는 클래스이며 어노테이션(@Component
등), AspectJ 타입 표현식 또는 사용자 지정 필터 조건을 사용해 어떤 클래스가 컨테이너에 BeanDefinition
으로 등록되어야 하는지 선택할 수 있다.
스테레오 타입 어노테이션
스프링은 @Component
외에 @Repository
, @Service
, @Controller
와 같은 추가 스테레오 타입 어노테이션을 제공한다.
@Repository
, @Service
, @Controller
는 레이어 계층을 나눌 때 사용하는 곳에 따라 보다 구체적인 사용 사례를 위해 @Component
를 특수화한 버전이다. 이들을 이용해 처리와 관련된 측면과 연관시켜 표현하는 것이 더 적절하며 포인트컷의 이상적인 대상이 된다.
포인트 컷(Pointcut) AOP(Aspect-Oriented Programming)에서 횡단 관심사(cross-cutting concern)를 정의하는 데 사용되는 표현식이다. *횡단 관심사 : 여러 모듈의 객체에 걸쳐 적용되어야하는 기능 (여기저기서 쓰이는 공통 기능이라고 생각하면 쉽다) AOP는 OOP(Object-Oriented Programming)에서는 처리하기 어려운 시스템 전반에 걸쳐 나타나는 코드의 중복과 유지보수성 문제를 해결하는 프로그래밍 패러다임이다. AOP에서는 핵심 기능과 횡단 관심사를 분리해서 관리할 수 있으며, 이를 위해 포인트 컷을 사용한다. 포인트 컷은 어디에서 실행될지를 결정하는데 사용되는 규칙 집합이다. 예를 들어, 모든 메소드 호출, 특정 메소드 호출, 특정 패키지의 메소드 호출 등이 포인트 컷이 될 수 있다. AOP에서는 이러한 포인트 컷에 맞춰서 횡단 관심사를 적용한다.
Meta-annotation과 Composed Annotation
@RestController
어노테이션을 보자.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(annotation = Controller.class)
String value() default "";
}
@RestController
어노테이션은 메타 어노테이션으로 사용되고 있는 @Controller
와 @ResponseBody
를 합쳐서 Composed Annotation으로 활용되고 있다.
여러 메타 어노테이션의 조합으로 생성된 Composed Annotation은 메타 어노테이션으로 사용된 어노테이션들의 특성을 상속받는데, @AliasFor 어노테이션을 사용해 메타 어노테이션 속성을 재정의 할 수 있다.
위 코드에서 @RestController
어노테이션은 @Controller
의 속성인 value의 기본값을 빈 문자열로 설정한다.
비슷한 예로 Composed Annotation인 @SessionScope
를 한 번 살펴보자.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
위 어노테이션은 다음처럼 해석할 수 있다.
@Scope
어노테이션을 메타 어노테이션으로 가지며, 스코프를 지정하기 위해WebApplicationContext
의SCOPE_SESSION
상수를 값으로 사용한다.proxyMode
속성은@Scope
어노테이션에 이미 정의되어 있는 속성인데,@SessionScope
어노테이션에서는 이 속성을 다시 선언하고, 기본값을ScopedProxyMode.TARGET_CLASS
로 설정한다.@Scope
에서 해당 속성은ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
으로 선언되어있다.
@AliasFor
어노테이션을 사용하여 proxyMode 속성이@Scope
어노테이션의 proxyMode 속성과 동일하다는 것을 나타낸다. 쉽게 말해 오버라이딩 됐음을 명시한다. 이렇게 함으로써, @SessionScope 어노테이션 사용 시 proxyMode 속성을 지정하면 실제로는 @Scope 어노테이션의 proxyMode 속성 값이ScopedProxyMode.TARGET_CLASS
로 변경되는 효과가 생긴다.
실제로 해당 어노테이션을 사용하는 예제를 보면서 마무리한다.
@Service
@SessionScope
public class SessionScopedService {
// ...
}
위와 같이 사용할 경우 proxyMode
속성 값은 ScopedProxyMode.TARGET_CLASS
이다.
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}
@SessionScope
에서 proxyMode
속성을 필드로 가지고 있기 때문에 직접 값을 넘겨 다른 값으로 설정할 수도 있다.
클래스 감지 자동화와 BeanDefinition 등록
위에서 설명한 스테레오 타입 어노테이션(@Component
, @Repository
, @Service
, @Controller
)이 추가된 클래스 는 자동으로 스캔되어 BeanDefinition
을 생성한다.
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
// ...
}
@Configuration
어노테이션으로 정의한 설정 클래스에 @ComponentScan
을 이용해 상위 패키지를 등록할 수 있다.
해당 예제에서는 org.example
패키지 하위의 모든 스테레오 타입 어노테이션을 스캔하고 빈으로 등록한다.
어떤 경우에는 스테레오 타입 어노테이션이 추가된 클래스일지라도 빈으로 등록되지 않게 정의하고 싶을 때가 있을 것이다.
@ComponentScan
은 이런 경우를 위해 includeFilters
, excludeFilters
속성을 제공한다.
includeFilters
: 스캔할 대상을 지정한다.excludeFilters
: 스캔 대상에서 제외할 대상을 지정한다.
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
// ...
}
위 코드에서 기본적으로 org.example
패키지를 스캔한다.
이 때 includeFilters
를 사용하는데 FilterType.REGEX
타입을 사용해 명시한 정규식 패턴에 맞는 클래스에 대해서만 빈으로 등록할 것을 지시한다.
excludeFilters
에서는 Repository
클래스를 상속하는 클래스를 스캔 대상에서 제외하도록 하고 있다.
스테레오 타입 어노테이션을 사용할 때 빈 이름 설정하기
@Bean
의 name 속성이나 어노테이션에서 value속성을 명시하면 해당 값으로 빈의 이름을 설정할 수 있다.
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
어떤 방식으로든 이름을 설정하지 않으면 클래스명(첫자리 소문자로 변경)이 빈의 이름이 된다.
만약 이 기본 설정을 바꾸고 싶으면 BeanNameGenerator
인터페이스를 구현해 원하는 방식으로 빈 이름을 자동 생성하게 할 수 있다.
public class MyNameGenerator extends AnnotationBeanNameGenerator {
@Override
public String generateBeanName(BeanDefinition definition, org.springframework.beans.factory.support.BeanDefinitionRegistry registry) {
String name = super.generateBeanName(definition, registry);
return "myBean_" + name;
}
}
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
// ...
}
맺음말
이렇게 세 번의 포스팅을 통해 스프링의 핵심 개념인 IoC 컨테이너와 DI, 빈 스코프, 그리고 컴포넌트 스캐닝에 대해 살펴보았다. 스프링 프레임워크는 이러한 개념들을 기반으로 유연하고 확장 가능한 애플리케이션을 개발할 수 있게 해주는 강력한 도구이다. IoC 컨테이너를 통해 객체의 생명주기와 의존성을 관리하고, DI를 통해 객체 간의 결합도를 낮추며 재사용성을 높일 수 있다. 또한 빈 스코프를 설정하여 객체의 범위를 조절하고, 컴포넌트 스캐닝을 통해 자동으로 빈을 등록하는 편리한 기능을 활용할 수 있다.
이번 시리즈를 통해 스프링의 핵심 개념을 이해하고, 빈 설정과 활용에 대한 기본적인 지식을 습득하셨을 것으로 기대한다. 하지만 이것은 시작에 불과하다. 스프링은 더욱 다양하고 심화된 기능들을 제공하며, 실제 프로젝트에 적용하기 위해서는 추가적인 공부와 실습이 필요하다. 지금부터는 실제 프로젝트를 구성하고 개발하는 과정에서 스프링의 다른 모듈과 기능들을 탐구하고 익히길 바란다.
마지막으로, 이 시리즈를 통해 함께 학습하는 것은 필자에게도 매우 소중한 시간들이었다. 앞으로의 개발 여정에서도 꾸준한 공부를 통해 개발 역량을 향상시킬 수 있기를!
참고
- https://docs\.spring\.io/spring\-framework/reference/core/beans/factory\-scopes\.html - https://docs\.spring\.io/spring\-framework/reference/core/beans/annotation\-config\.html - https://docs\.spring\.io/spring\-framework/reference/core/beans/classpath\-scanning\.html