스프링 컨테이너의 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext는 스프링 컨테이너로 인터페이스이다. 해당 인터페이스의 구현체로 XML 기반, 애노테이션 기반의 자바 설정 클래스 등으로 스프링 컨테이너를 생성할 수 있다.
AnnotationConfigApplicationContext → 애노테이션 기반 자바 설정 클래스로 스프링 컨테이너 생셩
참고로 컨테이너란 객체들의 생명주기를 관리, 생성된 인스턴스들에게 추가적인 기능을 제공하는 객체를 담는 공간을 의미한다.
스프링 컨테이너의 종류에는 대표적으로 2가지가 있다
- BeanFactory : 자바 객체(bean) 인스턴스를 생성, 설정, 관리하는 실질적인 컨테이너로 최상위 인터페이스
- ApplicationContext : BeanFactory를 상속하여 구현한 것으로 국제화가 지원되는 텍스트 메시지 관리하고 파일 자원을 로드할 수 있는 포괄적인 방법을 제공하는 등 BeanFactory보다 더 다양한 기능을 제공한다.
- BeanFactory를 제외하고도 다양한 기능에 대한 인터페이스를 다중 구현하였음
- 스프링에서 자바 객체를 빈(Bean)이라고 한다.
주로 여러 기능들을 구현한 ApplicationContext를 사용함
- 생성 과정
- 위와 같이 AppConfig.class를 인자로 넘겨 자바 설정 클래스 기반의 스프링 컨테이너를 생성
- AppConfig 내부의 정보를 가지고 컨테이너 내부의 스프링 빈 저장소에 스프링 빈을 등록하고 의존 관계를 주입한다.
- 자바 코드로 스프링 빈 등록 시 생성자를 호출하며 의존 관계 주입이 한 번에 일어남.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
즉 다음과 같은 AppConfig가 있다면 memberService, memberRepository가 빈으로 등록된다.
이 때 빈 이름-빈 객체가 Key-Value 형태로 등록이 된다.
빈 이름은 주로 메서드 이름을 사용하며, 직접 이름을 지정해 줄 수도 있다. @Bean(name = ” … ”)
그리고 빈 이름은 중복되면 안된다.
따라서 memberService - MemberServiceImpl@x001, memberRepository - MemoryMemberRepository@x002 이런 식의 쌍으로 빈 저장소에 등록되며 Service의 경우 Repository를 의존하므로 이러한 의존 관계 역시 설정된다.
스프링 컨테이너 빈 조회
- 빈 조회
- ac.getBean(타입)
- 조회 대상 빈이 없으면 NoSuchBeanDefinitionException 예외 발생
- ac.getBean(빈이름, 타입)
- 동일한 타입의 빈이 둘 이상일 때 조회
- ac.getBeansOfType(타입) → return Map<String, 조회 타입>
- @Autowired로 주입받는 경우 다음과 같이 받을 수 있다.
class PaymentController {
private final Map<String, PaymentService> serviceMap;
private final List<PaymentService> serviceList;
@Autowired
public PaymentController(Map<String, PaymentService> serviceMap, List<PaymentService> serviceList) {
this.serviceMap = serviceMap;
this.serviceList = serviceList;
}
}
Map 사용 시 키(빈 이름)-Value(조회 타입) 쌍으로 조회된 모든 빈 주입
List 역시 해당 타입의 모든 빈 주입
이 때 해당 타입의 스프링 빈이 없다면 빈 컬렉션 반환
여기서 스프링 빈을 조회할 때 부모 타입을 조회하면 자식 타입 역시 조회된다.
+ BeanDefinition - 빈 설정 메타 정보로 스프링의 다양한 형태의 설정 정보(xml, 자바 기반 등)를 추상화 한 것
싱글톤 컨테이너
스프링 컨테이너의 큰 특징은 싱글톤이라는 것이다.
싱글톤(Singleton) → 클래스의 인스턴스를 딱 1개만 생성하도록 하는 디자인 패턴
public class UserService {
private UserRepository userRepository;
public UserService() {
// 싱글톤 인스턴스를 직접 생성
this.userRepository = UserRepository.getInstance();
}
public void createUser() {
userRepository.save();
}
}
// UserRepository -> 싱글톤
public class UserRepository {
// 정적 변수로 유일한 인스턴스를 저장
private static UserRepository instance;
// 생성자를 private으로 설정하여 외부에서 인스턴스를 생성할 수 없도록 함
private UserRepository() {}
// 인스턴스를 해당 메서드로 조회하도록 함
public static UserRepository getInstance() {
// 인스턴스가 아직 생성되지 않은 경우에만 생성
if (instance == null) {
instance = new UserRepository();
}
return instance;
}
// 로직
public void save() {
System.out.println("저장 완료!");
}
}
위에서 UserRepository는 싱글톤 패턴으로 구현한 예이다.
외부에서 생성하지 못하도록 하고 해당 인스턴스를 조회할 때는 getInstance() 메서드를 통해 조회 가능하며 항상 같은 인스턴스를 반환하게 된다.
하지만 싱글톤 패턴은 위에서 보다시피 로직이 아닌 싱글톤 패턴을 위한 코드가 많이 들어가며,
싱글톤 클래스를 사용하는 클라이언트(MemberService)의 경우 구체 클래스에 의존하므로 DIP를 위반한다.
또한 전역 변수를 사용하므로 단위 테스트가 어려우며 private 생성자로 인해 자식 클래스를 생성할 수 없어 유연성마저 떨어진다.
스프링 컨테이너는 위 문제를 해결하기 위해 우리가 직접 싱글톤 클래스로 작성하지 않아도 스프링 빈들은 싱글톤으로 관리된다.
→ 불필요한 코드가 제거되어 가독성이 올라감
→ 컨테이너의 의존 관계 주입으로 인해 DIP를 위반하지 않음 → 결합도가 낮아지므로 테스트가 용이
따라서 스프링 컨테이너는 클라이언트의 요청이 올 때 마다 이미 생성된 객체를 공유해서 사용한다.
- 싱글톤 주의 사항
- 위에서 여러 클라이언트들은 이미 생성된 객체를 공유해서 사용한다고 하였다.
- 즉 싱글톤 객체는 상태를 유지하면 안된다.
- 읽기 전용으로 사용
- 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 함
- 특정 클라이언트에 의존하거나 값을 변경할 수 있는 필드가 존재하면 안됨
- 1번 스레드와 2번 스레드가 동시에 하나의 필드의 값을 변경한다면?
- 정상적으로 동작할 수도 있지만 예상치 못한 문제가 발생할 수 있으므로 항상 무상태로 설계할 것
@Configuration
repository의 경우 여러 서비스에서 참조할 수 있는데, 아래와 같은 코드가 있다고 하자.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
orderRepository());
}
...
}
이 경우 memberRepository() 메서드가 세 번 호출되는 것으로 보인다.
즉 new MemoryMemberRepository()를 세 번 호출하여 인스턴스를 세 번 생성하는데 그럼 싱글톤이 깨지는 것이 아닌가?
결론부터 이야기하면 각 메서드는 한 번씩만 호출된다.
(memberSerivice 1, memberRepository 1, orderService 1)
이것이 가능한 이유는 @Configuration 애노테이션 때문이다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
이렇게 생성을 하면 AppConfig bean = ac.getBean(AppConfig.class)으로 조회 시 AppConfig 역시 스프링 빈으로 등록된다.
하지만 우리가 작성한 AppConfig 클래스가 아닌 CGLIB 라이브러리로 조작한 AppConfig 가 등록된다.
이 AppConfig$CGLIB 클래스는 우리가 작성한 AppConfig 클래스를 상속받아 만들어진 클래스이다.
→ AppConfig 부모 타입으로 조회가 가능
이 클래스의 내부는 예시로 아래와 같이 동작하도록 바이트 코드를 조작한다.
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있다면?) {
스프링 컨테이너에서 MemberRepository 찾아서 반환;
} else { //스프링 컨테이너에 없다면?
기존 로직을 호출, 생성하고 스프링 컨테이너에 등록 후 반환
}
}
따라서 new MemoryMemberRepository()는 한 번만 호출되고 싱글톤이 보장된다.
만약 @Configuration을 사용하지 않는다면 @Bean이 붙은 클래스는 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않음.
스프링 빈 등록 방법
빈 등록 방법에는 2가지 방법이 존재한다.
1. @Bean - 수동 빈 등록
- 명시적으로 빈을 정의할 때 사용하며, 메서드 내에서 객체를 생성하고 반환
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
...
}
2. 컴포넌트 스캔 - 자동 빈 등록
- @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록
- @Configuration, @Controller, @RestController, @Service, @Repository 등… 모두 내부에 해당 애노테이션이 붙어 있으므로 빈 등록을 직접 안해도 자동 등록
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // <- 해당 부분
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";
boolean proxyBeanMethods() default true;
boolean enforceUniqueMethods() default true;
}
- 중복 발생 시
- 자동 등록과 자동 등록
- ConflictingBeanDefinitionException 예외 발생
- 자동 등록과 수동 등록 시
- 더 자세한 수동 빈 등록이 우선 순위
- 스프링 부트로 실행한 경우 에러 발생
- 자동 등록과 자동 등록
스프링 빈 생명주기
스프링 컨테이너는 빈들을 관리한다.
위에서 스프링 빈은 객체 생성 → 의존 관계 주입을 한다고 했다. 따라서 객체의 초기화 작업은 의존 관계 주입이 모두 일어난 뒤에 이루어져야 하는데 개발자가 의존 관계 주입 완료 시점을 어떻게 알 수 있을까?
스프링은 의존 관계 주입 완료 시 콜백 메서드를 통해서 스프링 빈에게 초기화 시점을 알려주는 기능을 제공한다.
콜백(Callback)
콜백이란 다른 코드의 인수로 넘겨주는 실행 가능한 코드로 특정 이벤트가 발생했을 때 호출되는 함수를 의미한다.
따라서 스프링 빈의 생명주기는 다음과 같다.
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸 전 콜백 → 스프링 종료
빈 생명주기 콜백 기능
- 설정 정보에 초기화, 소멸 메서드 지정
- @Bean(initMethod = "init", destroyMethod = "close")와 같이 지정하면 초기화 시 해당 빈 내부에 init(), 종료 시 close() 메서드를 호출한다.
- 스프링 빈이 스프링 코드에 의존하지 않으며, 메서드 이름을 자유롭게 지정할 수 있으며 설정 정보에서 초기화, 소멸 메서드를 지정하므로 코드를 고칠 수 없는 외부 라이브러리에도 적용이 가능하다.
- 참고로 destroyMethod의 경우 지정하지 않으면 자동으로 close, shutdown 이름의 종료 함수를 추론하여 호출한다.
- 따라서 추론 기능을 사용하지 않을 경우 destroyMethod=“” 지정해야함.
- @PostConstruct, @PreDestroy
- 최신 스프링에서 권장하는 방법으로 초기화 메서드에 @PostConstruct를 붙이고, 종료 메서드에 @PreDestroy를 붙인다.
- 이는 자바 표준 기술이므로 스프링이 아닌 다른 컨테이너에서도 동작하며 편리하다.
- 하지만 코드 내부에 초기화, 종료 메서드를 지정해야하므로 외부 라이브러리에는 적용할 수 없다.
빈 스코프
빈 스코프란 빈이 생존할 수 있는 범위를 뜻한다.
기본적으로 빈은 싱글톤으로 생성되어 컨테이너가 시작되고 종료될 때 까지 유지된다. 하지만 빈은 싱글톤이외에 다양한 스코프가 존재한다.
@Scope("prototype") // 프로토 타입 빈 지정
@Component
public class Bean {
}
---------------------------------------------------
@Scope("singleton") // 기본 - 싱글톤 빈 지정
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
- 싱글톤
- 기본 스코프로 스프링이 시작되고 종료될 때 까지 유지되는 스코프
- 클라이언트가 빈을 스프링 컨테이너에 요청 시 컨테이너가 관리하는 똑같은 스프링 빈을 항상 반환.
- 프로토타입
- 빈의 생성과 의존관계 주입까지 스프링 컨테이너가 관여하는 스코프
- 싱글톤 스코프 빈과는 다르게 스프링 컨테이너는 클라이언트가 요청 시 항상 새로운 인스턴스를 생성하고 의존관계 주입, 초기화 작업 후 반환한다. 이후 컨테이너는 해당 빈을 관리하지 않음
- 클라이언트가 빈을 관리할 책임을 가지며 컨테이너가 @PreDestroy같은 종료 메서드를 호출하지 않으므로 클라이언트가 직접 해야함.
- 만약 싱글톤 빈 내부에서 프로토타입 빈을 주입받는다면?
public class Test {
@Test
void singletonInPrototype() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean1.class, SingletonBean2.class, PrototypeBean.class);
SingletonBean1 singletonBean1 = ac.getBean(SingletonBean1.class);
SingletonBean1 singletonBean1_1 = ac.getBean(SingletonBean1.class);
SingletonBean2 singletonBean2 = ac.getBean(SingletonBean2.class);
assertThat(singletonBean1.prototypeBean).isEqualTo(singletonBean1_1.prototypeBean);
assertThat(singletonBean1.prototypeBean).isNotEqualTo(singletonBean2.prototypeBean);
}
static class SingletonBean1 {
private final PrototypeBean prototypeBean;
@Autowired
public SingletonBean1(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
}
static class SingletonBean2 {
private final PrototypeBean prototypeBean;
@Autowired
public SingletonBean2(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init " + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
- SingletonBean1은 싱글톤이므로 컨테이너가 생성될 때 생성, 의존관계 주입이 이루어진다.
- 따라서 의존 관계 주입 때 스프링 컨테이너에 프로토타입 빈을 요청하면 컨테이너가 빈 생성 후 클라이언트(SingletonBean1)에 반환.
- SingletonBean은 내부에 반환받은 참조값을 가지고 있음
- ac.getBean(SingletonBean1.class) 호출 시 스프링 컨테이너에 있는 SingletonBean1을 반환.
- 싱글톤이므로 이후 요청에도 같은 빈을 반환받음.
- 두 객체의 참조 비교 → SingletonBean1이 가지고 있는 프로토타입 빈은 의존관계 주입 시점에 주입이 끝난 빈. 사용할 때 마다 생성되는 것이 아님.
- SingletonBean2 역시 싱글톤 빈이지만 이 때 내부의 프로토타입 빈은 의존 관계 주입 시점에 컨테이너가 새로 생성하고 반환해준 빈이므로 SingletonBean1, SingletonBean2 내부의 프로토타입 빈 참조 비교는 다름.
참고로 위 코드에서 destroy()는 호출되지 않는다. → 프로토타입은 컨테이너가 초기화 이후로 관여 X
→ 그렇다면 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때 마다 새로 생성해서 사용하려면 어떻게 해야할까?
Provider
의존 관계를 외부에서 주입받는게 아니라 직접 필요한 의존관계를 찾는 것 Dependency Lookup(DL) 의존관계 조회(탐색)라고 한다.
따라서 지정한 프로토타입 빈을 컨테이너에서 찾아주는 기능을 제공해주는 것이 Provider
ObjectFactory, ObjectProvider
- ObjectProvider<PrototypeBean> prototypeBeanProvider를 통해 Provider 인스턴스를 생성. 의존관계를 주입받음.
- prototypeBeanProvider.getObject() 호출 시 새로운 프로토타입 빈 생성(스프링 컨테이너가 내부에서 해당 빈을 찾아서 반환)
- ObjectProvider는 ObjectFactory를 상속받은 후 다양한 기능을 추가.
- 둘 다 스프링에 의존하므로 다른 컨테이너로 바꿀 가능성이 있다면 적합하지 않음
static class SingletonBean1 {
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
@Autowired
public SingletonBean1(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public PrototypeBean getPrototypeBean() {
return prototypeBeanProvider.getObject();
}
}
자바 표준 Provider
- get() 메서드 하나만 가지고 있어 기능이 단순.
- 별도의 라이브러리를 추가해야함. (jakarta.inject:jakarta.inject-api)
- 위와 동일하지만 스프링이 아닌 다른 컨테이너에서도 사용 가능
static class SingletonBean1 {
private Provider<PrototypeBean> provider;
@Autowired
public SingletonBean1(Provider<PrototypeBean> provider) {
this.provider = provider;
}
public PrototypeBean getPrototypeBean() {
return provider.get();
}
}
3. 웹 스코프
웹 환경에서만 동작하는 스코프로 프로토타입과 다르게 스프링이 해당 스코프의 종료 시점까지 관리.
→ 종료 메서드가 호출됨
- request: 웹 요청이 들어오고 나갈 때 까지 유지되는 스코프로 각 HTTP 요청마다 별도의 인스턴스가 생성되고 관리
- 아래와 같은 코드가 있다고 하자. (MyLogger는 Request 웹 스코프임)
- 이 때 LogService는 빈이므로 스프링 컨테이너가 시작될 때 생성되고 종료 시 까지 살아있다.
- 생성되고 의존관계 주입이 이루어 지는데 MyLogger의 경우 웹 스코프로 요청이 들어올 때 인스턴스가 생성된다.
- 즉 의존관계 주입 시점에 해당 빈이 컨테이너에 없으므로 해당 코드는 컴파일 에러가 난다.
@Service
@RequiredArgsConstructor
public class LogService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
- Provider
- Provider를 이용해 요청이 오는 시점까지 빈의 생성을 지연할 수 있다.
- HTTP 요청이 오면 Provider가 새로운 request 스코프의 빈 생성
- 만약 컨트롤러에서도 myLoggerProvider.getObject()를 호출한다면 반환되는 request 스코프 빈은 서비스에서 반환되는 빈과 같다.
- → 하나의 요청에서 생성된 빈이므로
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
- 프록시
- 클래스일 때 → proxyMode = ScopedProxyMode.TARGET_CLASS
- 인터페이스일 때 → proxyMode = ScopedProxyMode.INTERFACES
- 이렇게 스코프 지정 시 해당 옵션을 추가하면 해당 빈의 프록시 클래스를 만들어두고
- HTTP 요청과 상관 없이 해당 프록시 클래스를 다른 빈에 주입.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
...
}
@Service
@RequiredArgsConstructor
public class LogService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
즉 처음의 해당 코드가 성공적으로 실행됨.
이 때 주입된 MyLogger 클래스는 CGLIB라는 라이브러리로 인해 MyLogger 클래스를 상속받은 프록시 클래스임.
그리고 요청이 올 때 프록시의 myLogger.log() 메서드는 실제로 내가 작성한 MyLogger.log() 메서드를 호출한다.
즉 프록시 객체는 실제 요청이 오는 시점에 실제 빈을 요청하는 위임 역할을 하며 싱글톤임.
그리고 실제 빈을 요청하면 request 스코프를 가진 실제 객체의 빈이 생성되고 메서드 호출. 여기서 생성되는 request 스코프 빈은 싱글톤이 아니고 요청마다 생성됨.
- session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
- websocket: 웹소켓과 동일한 생명주기를 가지는 스코프
참고
스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런
김영한 | 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보
www.inflearn.com
'Spring' 카테고리의 다른 글
[Spring] 메시지, 국제화 (0) | 2024.12.27 |
---|---|
[Spring] Spring MVC (0) | 2024.12.27 |
[Spring] 서블릿과 컨트롤러 (0) | 2024.12.27 |
[Spring] @Qualifier, @Primary (0) | 2024.12.27 |
[Spring] 스프링의 이해, 스프링 부트 (0) | 2024.12.27 |