스프링 빈과 컨테이너 살펴보기
스프링 빈에 대해 발표를 준비하며 학습한 스프링 빈과 컨테이너를 정리해보겠습니다.
스프링 컨테이너
스프링 컨테이너는 애플리케이션 컴포넌트의 중앙 저장소로 스프링 빈을 관리하는 공간입니다.
스프링의 특징인 IOC/DI는 스프링 컨테이너가 제공하는 기능으로 객체의 생성과 주입을 컨테이너가 도맡아 하여개발자가 직접 객체를 생성하고 라이프사이클을 관리하지 않아도 된다는 장점이 있습니다.
BeanFactory
인터페이스와 이를 상속한 ApplicationContext
를 스프링 컨테이너라고 합니다.
Bean Factory
스프링 컨테이너의 최상위 인터페이스로 스프링 빈을 관리하고 조회하는 역할을 담당합니다.
ApplicationContext
와 차이점은 스프링 빈을 생성하는 것이 아닌 지연로딩 방식으로 작동한다는 것입니다.
public class Pojo {
public static boolean isBeanInstantiated = false;
public void postConstruct() {
setIsBeanInstantiated(true);
}
public static boolean isIsBeanInstantiated() {
return isBeanInstantiated;
}
public static void setIsBeanInstantiated(boolean isBeanInstantiated) {
Pojo.isBeanInstantiated = isBeanInstantiated;
}
}
Pojo 객체는 beanInstantiated
라는 boolean 값을 false로 가지고 있습니다.
postConstruct
메서드는 객체가 생성되기 전에 호출될 메서드입니다.
<bean id="pojo"
class="com.practice.spring_ioc.Pojo"
init-method="postConstruct"
/>
Pojo 객체를 빈으로 관리하기 위해 XML로 설정했으며 postConstruct
를 객체 생성하기 전에 호출하도록 했습니다.
XmlBeanFactory
로 Pojo의 정보는 읽어왔지만 빈이 초기화되지 않았고, getBean
메서드가 호출된 시점에 스프링 빈을 생성한 것을 확인할 수 있습니다.
ApplicationContext
스프링 문서에서는 컨테이너를 다룰 때는 ApplicationContext
를 사용할 것을 권장하고 있습니다.
ApplicationContext
는 위에 상술한 BeanFactory
이외에도 국제화, 이벤트퍼블리싱, 리소스 로딩 등을 포함한 다양한 기능을 제공합니다.
또한 스프링 빈들을 모두 사전로딩하기 때문에 무거운 컨테이너로 간주됩니다.
싱글톤 컨테이너
스프링 컨테이너는 스프링 빈들을 싱글톤으로 관리합니다. 매 요청 시마다 스프링 빈이 생성되고 GC에 의해 제거되는 과정이 매우 비효율적이기 때문입니다.
스프링 컨테이너는 싱글톤 컨테이너로 싱글톤 객체를 생성하고 관리하는 기능을 하여 싱글톤 패턴의 단점을 억제하고 객체의 단일성을 유지할 수 있게 도와줍니다.
두 객체가 동일한 싱글톤 객체임을 알 수 있습니다.
만약 new
연산자로 스프링 빈을 여러개 만든다면 어떨까요?
@Configuration
public class AppConfig {
@Bean
public OrderService orderService() {
return new OrderService(bookRepository());
}
@Bean
public OrderServiceV2 orderServiceV2() {
return new OrderServiceV2(bookRepository());
}
@Bean
public BookRepository bookRepository() {
return new BookRepository();
}
}
OrderService
들은 BookRepository
를 각각 주입받습니다.
new
연산자를 통해 인스턴스를 각각 만드니 싱글톤이 아니지 않을까요?
AppConfig
의 클래스 정보를 보면 CGLIB
가 붙어있는데 이는 바이트코드 조작 라이브러리입니다.
이 라이브러리를 사용해 AppConfig
를 상속한 임의의 프록시 클래스를 생성하고 싱글톤이 보장되도록 해줍니다.
때문에 OrderService
클래스들은 똑같은 BookRepository
를 참조하고 있음을 확인할 수 있습니다.
스프링 빈
스프링 빈은 위에서 언급한 스프링 컨테이너가 관리하는 POJO를 의미합니다.
POJO는 Plain Old Java Object로 순수한 자바 객체를 말하는데 스프링 컨테이너는 이러한 POJO를 빈으로 등록하고 관리합니다.
스프링 컨테이너가 스프링 빈을 등록하는 방법은 크게 XML 설정과 자바 설정 두 방법이 있습니다.
XML 설정
public class MemberService {
public MemberRepository memberRepository;
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
public class MemberRepository {}
<bean id="memberService"
class="com.practice.spring_ioc.bean.MemberService">
<property name="memberRepository" ref="memberRepository"/>
</bean>
<bean id="memberRepository"
class="com.practice.spring_ioc.bean.MemberRepository"/>
XML로 설정할 땐 <bean>
태그를 사용합니다. MemberRepository
를 주입하기 위해 MemberService
빈 내부에 <property>
태그를 작성한 것을 확인할 수 있습니다.
미리 작성한 두 객체가 모두 빈으로 등록되어 테스트를 통과했습니다.
하지만 앞선 방식처럼 작성하는 것은 굉장히 번거롭고 개발자의 실수가 발생할 확률이 높습니다.
@Component
public class CoinService {
@Autowired
public CoinRepository coinRepository;
public void setCoinRepository(CoinRepository coinRepository) {
this.coinRepository = coinRepository;
}
}
@Component
public class CoinRepository {
}
<context:component-scan base-package="com.practice.spring_ioc"/>
component-scan
을 사용하면 지정한 패키지 내부의 컴포넌트들을 모두 스프링 빈으로 등록해줍니다.
자바 설정
@Service
public class BookService {
public BookRepository bookRepository;
public void setBookRepository(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
@Repository
public class BookRepository {
}
여기서 @Service
와 @Repository
는 @Component
을 상속한 어노테이션입니다.
@Configuration
public class AppConfig {
@Bean
public BookService bookService() {
BookService bookService = new BookService();
bookService.setBookRepository(bookRepository());
return bookService;
}
@Bean
public BookRepository bookRepository() {
return new BookRepository();
}
}
@Configuration
어노테이션은 스프링 빈을 설정하는 클래스임을 나타내는 어노테이션입니다.
AppConfig
내부의 @Bean
으로 설정한 클래스들은 모두 @Configuration
에 의해 스프링 빈으로 등록됩니다.
@Configuration
@ComponentScan(basePackageClasses = SpringIocApplication.class)
public class AppConfig {}
물론 자바 설정에서도 @ComponentScan
어노테이션을 제공하기 때문에 이 방식을 사용할 수도 있습니다.
컴포넌트 스캔과 @Component
스프링부트를 사용하면 직접 ApplicationContext
를 사용할 필요 없이 컴포넌트 스캔이 이루어집니다.
@SpringBootApplication
public class SpringIocApplication {
public static void main(String[] args) {
SpringApplication.run(SpringIocApplication.class, args);
}
}
스프링부트로 프로젝트를 생성하면 위와 같이 @SpringBootApplication
어노테이션이 설정되어 있습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}
익숙한 어노테이션을 확인할 수 있는데 @SpringBootApplication
자체가 거대한 설정 클래스를 나타내며 이 덕분에 저희는 직접 스프링 빈을 등록하고 설정하지 않았더라도 쉽게 사용할 수 있게 됩니다.
@Component
어노테이션은 스프링 스테레오타입 중 하나로 위에서 살펴봤던 컴포넌트 스캔의 대상이 되며 해당 어노테이션이 있는 클래스들은 자동으로 스프링 빈으로 등록이 됩니다.
@Controller
: 프레젠테이션 계층에서 사용되며 웹 컨트롤러임을 명시@Service
: 서비스 계층에서 사용되며 도메인 기반으로 정의된 서비스를 명시@Repository
: 퍼시스턴스 계층에서 사용되며 DAO의 구현체로 DB에 접근하는 것을 명시 → 해당 객체에서 발생하는 예외는 모두DataAccessException
으로 변환@Configuration
: 한 개 이상의@Bean
어노테이션으로 정의한 스프링 빈들을 등록하기 위해 사용 → 등록된 스프링 빈들은CGLIB
라이브러리를 통해 프록시 객체로 생성되고 싱글톤 패턴이 적용됨@Bean
: 메서드 또는 어노테이션 레벨에 붙일 수 있으며 써드파티 라이브러리 등을 등록하기 위해 사용 →@Configuration
을 사용하지 않아도 스프링 빈으로 등록되지만 싱글톤 패턴이 적용되지 않음
이 외에도 @ControllerAdivce
, REST로 응답하기 위한 @RestController
등이 있습니다.
@Autowired
어노테이션을 생성자 혹은 필드에 지정하게 되면 스프링 컨테이너는 자동으로 해당 스프링 빈을 찾아 등록해줍니다. 이 덕분에 개발자는 쉽게 의존관계 주입을 자동으로 할 수 있게 됩니다.
의존관계 주입
생성자 주입
@Service
public class GameService {
private final GameRepository gameRepository;
public GameService(GameRepository gameRepository) {
this.gameRepository = gameRepository;
}
}
생성자를 통해 의존관계를 주입받는 것으로 생성자 호출 시점에 단 한 번만 호출되는 것이 보장됩니다.
final
키워드를 사용할 수 있기 때문에 불변, 필수를 보장할 수 있으며 가장 권장되는 방식입니다.
만약 생성자가 여러개라면 @Autowired
어노테이션을 생성자에 명시해야 합니다.
수정자 주입
@Service
public class DeliveryService {
private BookRepository bookRepository;
@Autowired
public void setBookRepository(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
Setter
를 통해 의존관계를 주입하는 방법으로 선택, 변경 가능성이 있는 의존관계에 사용됩니다.
@Autowired
어노테이션을 부여하지 않으면 Null
상태가 됩니다.
수정자 주입은 스프링 빈이 등록되지 않아 Null
상태일 수 있기 때문에 @Autowired
어노테이션에서 주입할 대상이 없어 예외가 발생할 수 있습니다.
이는 required = false
속성을 지정해서 방지할 수 있습니다.
필드 주입
@Component
public class OrderServiceImpl implements OrderService{
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
}
필드에 바로 주입하는 방식으로 권장되지 않습니다.
접근제어자가 private
이어도 주입은 되지만 테스트가 어렵습니다.
테스트를 할 땐 보통 의존할 클래스를 Mocking 하게 되는데 필드 주입 방식은 수행할 수 없거나 매우 번거로운데다가 주입을 위해 Setter
를 사용해 주입해줘야 하는데 그 경우엔 수정자 주입을 하면 되기 때문입니다.
일반 메서드 주입
일반 메서드를 통해 의존관계를 주입할 수 있지만 생성자 주입, 수정자 주입 방식이 있기 때문에 권장되지 않으며 잘 사용하지도 않습니다.
스프링 빈의 스코프
스프링 빈의 스코프는 싱글톤외에도 프로토타입, 요청, 세션, 글로벌세션, 애플리케이션, 웹 소켓 스코프를 제공합니다.
이 중 애플리케이션을 제외한 스코프들은 싱글톤과 다르게 독립적인 상태를 저장하고 사용하기 위해 사용합니다.
우리가 일반적으로 정의해서 사용하는 스프링 빈은 대부분 싱글톤 스코프로 사용하게 됩니다.
프로토타입 스코프
프로토타입 스코프는 모든 요청에 대해 매번 새로운 인스턴스를 만들어서 사용하는 스코프입니다.
@Component
@Scope(value = "prototype")
public class PrototypeBean {}
@Component
public class SingleBean {}
@Scope
어노테이션을 사용하여 직접 스코프를 지정할 수 있습니다.
테스트 결과를 보면 SingleBean
은 싱글톤이기 때문에 getBean
을 여러번 호출해도 동일한 인스턴스지만PrototypeBean
은 생성될 때마다 새로운 인스턴스를 반환하므로 다른 인스턴스 주소를 참조하고 있습니다.
때문에 프로토타입 빈이 싱글톤 빈을 참조할 때는 이미 생성된 상태이기 때문에 아무 문제가 없지만 싱글톤 빈이 프로토타입 빈을 참조할 때는 문제가 발생합니다.
싱글톤 빈이 참조하고 있는 프로토타입 빈의 인스턴스가 업데이트되지 않습니다.
이 문제는 @Scope
에 proxyMode = ScopedProxyMode.TARGET_CLASS
속성을 지정하거나 해당 프로토타입 빈을 ObjectProvider
로 지정하여 해결할 수 있습니다.
웹 스코프
- 요청 스코프
- 하나의 웹 요청 안에서 만들어지고 해당 요청이 끝날 때 제거됩니다.
- 각 요청 별로 독립적인 스프링 빈이 생성되기 때문에 상태를 저장해서 사용해도 되며 상태를 프레임워크 레벨의 서비스 또는 인터셉터에 전달하기 위해 사용합니다.
- 세션 스코프
- HTTP 세션과 같은 존재 범위를 갖는 빈으로 생성하는 스코프입니다.
- 로그인 정보나 사용자별 옵션을 저장하기에 유용하지만 매우 번거롭습니다.
- 애플리케이션 스코프
- 싱글톤 스코프와 비슷한 존재 범위를 가집니다.
- 웹 애플리케이션과 애플리케이션 컨텍스트의 존재 범위가 다른 경우가 있을 때 사용합니다.
- 웹 소켓 스코프
- 웹 소켓과 동일한 생명주기를 갖는 스코프입니다.
웹 스코프 또한 프로토타입 스코프처럼 프록시 방식 또는 ObjectProvider
를 통해 의존관계를 주입받아 사용할 수 있으며 만약 사용하는 경우가 있다면 CGLIB
의 도움을 받는 프록시 방식을 주로 사용합니다.
Provider
를 사용하던 프록시를 사용하던 객체 조회를 지연처리한다는 것이 핵심이며 꼭 필요한 곳에서만 최소화해서 사용해야합니다.
스프링 빈의 생명주기
스프링 빈의 생명주기는 스프링 컨테이너에 의해 관리되며, 생성이나 소멸 시 호출될 수 있는 콜백 메서드를 제공합니다.
스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원합니다.
InitializingBean, DisposableBean
@Slf4j
public class LifeCycleBean implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
log.info("빈 생성 콜백");
}
@Override
public void destroy() throws Exception {
log.info("빈 소멸 콜백");
}
}
InitializingBean
과 DisposableBean
은 각각 빈의 생성과 소멸주기 콜백 메서드를 제공하는 인터페이스입니다.
빈이 생성되고 소멸될 때 로그를 찍게하여 어떤 결과가 나오는 지 테스트해보겠습니다.
스프링 컨테이너가 생성되고 해당 스프링 빈을 생성하여 등록할 때 빈 생성 콜백이 호출되고, 컨테이너를 종료하면 모든 빈이 제거되는 데 이 때 빈 소멸 콜백이 호출된 것을 확인할 수 있습니다.
다만 이 방식은 스프링 전용 인터페이스로 초기화, 소멸 메서드의 이름을 변경할 수 없고 외부 라이브러리에 적용할 수 없어서 잘 사용되지 않습니다.
빈 등록 초기화, 소멸 메서드 지정
@Slf4j
public class LifeCycleMethods {
public void init() {
log.info("빈 생성 콜백 메서드 호출");
}
public void close() {
log.info("빈 소멸 콜백 메서드 호출");
}
}
@Bean(initMethod = "init", destroyMethod = "close")
public LifeCycleMethods lifeCycleMethods() {
return new LifeCycleMethods();
}
스프링 빈을 등록할 때 소멸과 생성 시점에 호출할 메서드를 작성하고 initMethod
, destroyMethod
를 지정하면 직접 명시한 메서드를 콜백 메서드로 활용할 수 있습니다.
이 방법은 메서드를 자유롭게 지정할 수 있고, 코드를 고칠 수 없는 외부 라이브러리에도 적용가능한 장점이 있습니다. 또 destroyMethod
는 기본값이 infferred
로 되어 있어 종료 메서드를 알아서 추론하여 호출해줍니다.
@PostConstruct, @PostDestory
@Slf4j
public class LifeCycleAnnotations {
@PostConstruct
public void init() {
log.info("빈 생성 콜백 어노테이션");
}
@PreDestroy
public void close() {
log.info("빈 소멸 콜백 어노테이션");
}
}
해당 어노테이션들을 지정하여 사용하는 방법으로 가장 간편하게 사용할 수 있습니다.
자바 9버전 이후로 해당 어노테이션을 포함한 Java EE가 deprecated 되었고 자바 11버전 부터는 삭제되었다고 하지만 현재 자바 11버전의 javax.annotation
이 제공하고 있기 때문에 문제없이 사용할 수 있습니다.
참고자료
https://haruhiism.tistory.com/186
https://beststar-1.tistory.com/39
스프링 핵심 원리 - 기본편
댓글남기기