728x90
반응형
IoC, DI, 그리고 컨테이너
IoC(제어의 역전)
- 기존에는 클라이언트 코드가 직접 구현체를 생성하고 실행 흐름을 제어했지만, IoC는 이 흐름 제어를 외부(AppConfig 등)가 담당하게 한다.
- 클라이언트는 인터페이스에만 의존하여, 어떤 구현체가 주입될지 모르게 됨.
- Framework vs. Library:
- 프레임워크는 애플리케이션의 흐름을 제어하고 실행함.
- 라이브러리는 개발자가 직접 흐름을 제어함.DI(의존관계 주입)
- 정적인 클래스 의존관계: 코드 상에서 클래스 간의 관계를 확인할 수 있음(예: 클래스 다이어그램)
- 동적인 객체 인스턴스 의존관계: 런타임 시 실제 객체 인스턴스가 외부에서 생성되어 클라이언트에 주입됨
- DI를 통해 클라이언트 코드를 수정하지 않고도 주입될 객체를 변경할 수 있음
IoC/DI 컨테이너
AppConfig
와 같이 객체를 생성, 관리하며 의존관계를 연결해주는 역할을 한다.- 애플리케이션의 흐름과 객체 간의 실제 의존관계를 결정하여, 코드의 유연성과 확장성을 높인다.
Spring Container / Spring Bean
스프링 컨테이너(ApplicationContext)
package com.study.springcore;
import com.study.springcore.member.Grade;
import com.study.springcore.member.Member;
import com.study.springcore.member.MemberService;
import com.study.springcore.member.MemberServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new Member = " + member);
System.out.println("findMember = " + findMember);
}
}
// 기존과 동일한 결과 출력
- 스프링 컨테이너는 애플리케이션의 객체(스프링 빈)를 생성, 관리, 그리고 의존관계를 주입하는 역할을 한다.
AnnotationConfigApplicationContext
와 같이 자바 기반 설정(AppConfig 클래스)을 사용하여 컨테이너를 생성할 수 있다.
스프링 빈 등록과 조회
AppConfig
에@Configuration
과@Bean
어노테이션을 사용하면, 해당 메소드들이 반환하는 객체들이 스프링 빈으로 등록된다.- 등록된 빈은 기본적으로 메소드 이름(예: "memberService")을 이름으로 사용하며,
applicationContext.getBean()
을 통해 조회할 수 있다.
의존관계 주입(DI)와 컨테이너 역할
- 스프링 컨테이너는 설정 정보를 기반으로 각 빈 간의 의존관계를 동적으로 연결(주입)한다.
- 이는 기존에 직접
AppConfig
를 통해 객체를 생성하고 주입하던 방식과 동일한 결과를 내지만, 관리와 확장이 더욱 용이해진다.
스프링 컨테이너 생성 과정
- 스프링 컨테이너 생성
AppConfig.class
등을 통해 구성 정보를 지정해주어야 함
- 스프링 빈 등록
- 스프링 컨테이너는 설정 클래스 정보(구성 정보)를 사용해서 스프링 빈을 등록
- 스프링 빈 의존관계 설정 - 준비
- 스프링 빈 의존관계 설정 - 완료
- 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI) = 동적인 의존관계 연결
- 단순히 자바 코드를 호출하는 것 같지만, 차이가 있다. (뒤 싱글톤 컨테이너 부분 참고)
- 스프링 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있지만, 사실 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다. (의존관계 자동 주입 부분 참고)
장점
- 동적 의존관계 연결: 런타임에 실제 객체 인스턴스가 주입되므로, 클라이언트 코드를 수정하지 않고도 구현체를 쉽게 변경할 수 있다.
- 관심사의 분리: 객체 생성과 비즈니스 로직이 분리되어 코드의 유지보수성과 확장성이 향상된다.
스프링 컨테이너와 빈 조회
- 등록된 빈 조회
ac.getBeanDefinitionNames()
로 컨테이너에 등록된 모든 빈 이름을 확인하고, 각 빈의 역할(일반 사용자 정의 vs. 스프링 내부용)을BeanDefinition.getRole()
로 알 수 있음.
- 빈 조회 방법
- '이름+타입' 또는 '타입'만으로 빈을 조회할 수 있으며, 대상 빈이 없으면 예외 발생
- 동일 타입의 빈이 여러 개 있을 경우, 빈 이름이나
getBeansOfType()
을 사용해 모두 조회 가능 - 상속 관계에서도 부모 타입으로 조회하면 자식 빈도 함께 검색됨
동일한 타입이 둘 이상 있는 스프링 빈 조회 예시
@Test
@DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다.")
void findBeanByTypeDuplicate() {
AnnotationConfigApplicationContext ac2 = new AnnotationConfigApplicationContext(SameBeanConfig.class);
assertThrows(NoUniqueBeanDefinitionException.class, () -> ac2.getBean(MemberRepository.class));
}
@Test
@DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
void findBeanByNameWhenTypeDuplication() {
AnnotationConfigApplicationContext ac2 = new AnnotationConfigApplicationContext(SameBeanConfig.class);
MemberRepository memberRepository = ac2.getBean("memberRepository1", MemberRepository.class);
assertThat(memberRepository).isInstanceOf(MemberRepository.class);
}
@Test
@DisplayName("특정 타입을 모두 조회하기.")
void findAllBeanByType() {
AnnotationConfigApplicationContext ac2 = new AnnotationConfigApplicationContext(SameBeanConfig.class);
Map<String, MemberRepository> beansOfType = ac2.getBeansOfType(MemberRepository.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value = " + beansOfType.get(key));
}
System.out.println("beansOfType = " + beansOfType);
assertThat(beansOfType.size()).isEqualTo(2);
}
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemoryMemberRepository();
}
}
BeanFactory vs. ApplicationContext
BeanFactory
- 스프링 빈을 관리·검색하는 최상위 인터페이스
- 스프링 빈을 관리하고 조회하는 역할 담당
getBean()
제공
ApplicationContext
BeanFactory
의 기능을 모두 상속 받고 이외에도 국제화, 환경변수, 이벤트 발행, 리소스 조회 등 부가 기능을 제공하므로 실제로 많이 사용됨- 부가 기능
MessageSource
-> 메시지 소스를 활용한 국제화 기능EnvironmentCapable
-> 환경변수 기능ApplicationEventPublisher
-> 애플리케이션 이벤트를 발행하고 구독하는 모델 지원ResourceLoader
-> 편리한 리소스 조회 기능
차피 ApplicationContext가 모든 기능을 갖고 있으므로, BeanFactory를 직접 사용할 일은 거의 없음. BeanFactory나 ApplicationContext를 스프링 컨테이너라고 함.
빈 설정 메타 정보와 다양한 설정 방식
- 모든 빈은 결국
BeanDefinition
이라는 빈 설정 메타 정보로 등록되며, 이는 자바 설정(@Configuration
,@Bean
)이나 XML 설정을 통해 생성됨- 즉, 역할과 구현을 개념적으로 나눈 것으로 어떤 설정 형식이든 결국
BeanDefinition
을 만들도록 하고,ApplicationContext
는 이BeanDefinition
만 알면 됨
- 즉, 역할과 구현을 개념적으로 나눈 것으로 어떤 설정 형식이든 결국
- 최근에는 애노테이션 기반 자바 설정이 주로 사용되고, XML은 레거시 환경에서 활용됨.
- BeanDefinition 살펴보기
BeanClassName
: 생성할 빈의 클래스 명(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음.)factoryBeanName
: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfigfactoryMethodName
: 빈을 생성할 팩토리 메소드 지정, 예) memberServiceScope
: 싱글톤(기본값)LazyInit
: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때까지 최대한 생성을 지연처리 하는지 여부InitMethodName
: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메소드 명DestroyMethodname
: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메소드 명Constructor arguments, Properties
: 의존관계 주입에서 사용한다. (자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)
실무에서 BeanDefinition을 직접 정의하거나 사용할 일은 거의 없으니 굳이 외우진말자
싱글톤 컨테이너와 싱글톤 패턴
- 싱글톤 패턴: 객체를 단 한 번만 생성해 공유하는 디자인 패턴으로 메모리를 절약할 수는 있지만, 다음과 같은 단점이 있음..
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계 상 클라이언트가 구체 클래스에 의존한다. → DIP를 위반한다.
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화하기 어렵다.
- private 생성자로 자식 클래스를 만들기 어렵다.
- 결론적으로 유연성이 떨어진다. → DI 적용하기 어려워짐..
- 안티패턴으로 불리기도 한다.
- 스프링 컨테이너는 기본적으로 싱글톤 방식으로 빈을 관리하여, 동일한 빈 인스턴스를 재사용한다.
- 따로 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리해주며, 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지한다
@Configuration
이 붙은 클래스는CGLIB
를 통해 바이트코드 조작으로 싱글톤 보장이 이루어짐.- 만약
@Configuration
없이@Bean
만 사용하면 싱글톤 보장이 깨질 수 있음.
싱글톤 방식은 여러 클라이언트가 같은 객체 인스턴스를 공유하기 때문에, 싱글톤 객체는 반드시 stateless로 설계해야 한다. (동시성 문제 예방)
컴포넌트 스캔과 의존관계 자동 주입
컴포넌트 스캔
- 이전까지
AppConfig
에서@Bean
을 통해 직접 빈을 등록하여 스프링 빈을 나열했지만, 이러한 설정 정보(AppConfig.class
) 없이도 자동으로 스프링 빈을 등록하는 '컴포넌트 스캔'이라는 기능 제공 - 컴포넌트 스캔:
@Component
(및@Controller
,@Service
,@Repository
,@Configuration
)가 붙은 클래스를 자동으로 스캔하여 빈으로 등록- 이때 스프링 빈의 기본 이름은 '클래스명'을 사용하되, 맨 앞글자만 소문자를 사용 (물론 직접 지정 가능)
- 이렇게 애노테이션 방식으로 스프링 빈을 등록하면, 의존관계를 명시해줄 수가 없게 되는데, 이러한 의존관계 주입 역시 클래스 안에서
@Autowired
애노테이션으로 해결 -> 의존관계 자동 주입- 생성자에
@Autowired
를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입 - 이때 기본 조회 전략은 '타입'이 같은 빈을 찾아서 수행(=
getBean(타입클래스)
)
- 생성자에
- 컴포넌트 스캔을 사용하려면
@ComponentScan
을 붙여주어야 하지만,@SpringBootApplication
애노테이션 안에 이미 포함되어 있어서 따로 명시해주지 않아도 사용 가능하다.- 물론, 컴포넌트 스캔 위치를 커스텀하고 싶다면 변경이 필요하다.
컴포넌트 스캔 탐색 위치
모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.
@ComponentScan(
basePackages = "hello.core",
)
basePackages
: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.- basePackages = { “hello.core”, “hello.service” } 이렇게 여러 시작위치를 지정할 수도 있다.
basePackageClass
: 지정한 클래스의 패키지를 탐색 시작 위로 지정한다.- 만약 지정하지 않으면
@ComponentScan
이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.- => 별도의 설정을 하지 않을 경우,
@SpringBootApplication
가 있는 패키지가 기본 스캔 시작 위치
- => 별도의 설정을 하지 않을 경우,
컴포넌트 스캔 필터
includeFilteres
: 컴포넌트 스캔 대상을 추가로 지정한다.excludeFilters
: 컴포넌트 스캔에서 제외할 대상을 지정한다.- FilterType 옵션
ANNOTATION
: 기본값, 애노테이션을 인식해서 동작한다.- ex)
org.example.SomeAnnotation
- ex)
ASSIGNABLE_TYPE
: 지정한 타입과 자식 타입을 인식해서 동작한다.- ex)
org.example.SomeClass
- ex)
ASPECTJ
: AspectJ 패턴 사용- ex)
org.example.*Service*
- ex)
REGEX
: 정규 표현식- ex)
org\.example\.Default.*
- ex)
CUSTOM
:TypeFilter
이라는 인터페이스를 구현해서 처리- ex)
org.example.MyTypeFilter
- ex)
@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = {
@Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
@Filter(type = FilterType.ASSEIGNABLE_TYPE, classes = BeanA.class)
}
)
static class ComponentFilterAppConfig {
}
의존관계 자동 주입(@Autowired
)
- 생성자, setter, 필드, 일반 메소드 주입 방식이 있으며, 생성자 주입이 불변성 보장과 누락 방지 때문에 권장됨.
- 타입이 여러 개일 경우, 필드명 매칭,
@Qualifier
또는@Primary
를 통해 해결할 수 있음.- 동일한 타입의 여러 빈을 사용해야 하는 경우, @Primary를 메인 비즈니스 로직을 담는 빈에 사용하고, @Qualifier를 서브 비즈니스 로직을 담는 빈에 사용하는 형태가 일반적이다.
- @Primary보다 @Qualifier가 더 우선순위가 높다.
- 필요 시 커스텀 애노테이션을 만들어 보다 명시적으로 구분할 수 있음.
빈 컬렉션 주입과 실무 운영 기준
- 모든 빈 주입: 특정 타입의 빈을 모두 List나 Map으로 주입받아, 전략 패턴 등 동적 빈 선택에 활용 가능.
- 자동(
@ComponentScan
+@Autowired
) vs. 수동(@Configuration
+@Bean
) 빈 등록- 업무 로직(많은 빈)은 컴포넌트 스캔과 자동 주입을 사용하여 관리 편의성을 높이고,
- 기술 지원 로직(중요, 변경이 적은 빈)은 수동 등록(AppConfig 등)으로 명확하게 관리하는 것이 좋음.
빈 생명주기 콜백(Bean Lifecycle Callback)
- 목적: 외부 리소스(예: 데이터베이스 커넥션, 네트워크 소켓)와 같이 애플리케이션 시작 시 연결하고 종료 시 정리해야 하는 작업을 위해, 빈의 초기화와 종료 시점을 관리
라이프사이클 단계
- 컨테이너 생성
- 빈 생성: 생성자 주입을 통해 인스턴스가 만들어짐.
- 의존관계 주입: setter나 필드 주입을 통해 의존성이 할당됨.
- 초기화 콜백: 빈이 완전히 준비된 후 호출되어 외부 연결이나 초기 작업을 수행함.
- 사용: 빈이 실제 비즈니스 로직에서 활용됨.
- 소멸 전 콜백: 컨테이너 종료 전 호출되어 리소스를 정리함.
- 컨테이너 종료
콜백을 구현하는 3가지 방법
- 인터페이스 구현 (InitializingBean, DisposableBean)
- 빈 클래스가 해당 스프링 전용 인터페이스(
InitializingBean
,DisposableBean
)를 구현하여afterPropertiesSet()
와destroy()
메서드를 오버라이드해 초기화와 소멸 콜백을 받음 - 단점: 스프링에 종속되며, 메서드 이름 변경이 불가능하고 외부 라이브러리에는 적용하기 어려움
- 빈 클래스가 해당 스프링 전용 인터페이스(
- 빈 등록 시
initMethod
,destroyMethod
지정@Bean
애노테이션에서initMethod
와destroyMethod
속성을 사용하여 초기화 및 종료 메서드를 지정할 수 있음- 장점: 스프링 종속성을 제거할 수 있으며, 외부 라이브러리에도 적용할 수 있고, 메서드 이름을 자유롭게 설정 가능
- 또한, 종료 메서드에 대해서는 close나 shutdown 등의 메서드 이름을 자동으로 추론하는 기능도 제공
- JSR-250 애노테이션 (
@PostConstruct
,@PreDestroy
)- 가장 최신이자 권장되는 방법으로, 해당 애노테이션을 메서드에 붙이면 의존관계 주입 후 초기화(
@PostConstruct
)와 소멸 전(@PreDestroy
) 시 자동으로 호출 - 장점: 코드가 간결하고 자바 표준(JSR-250)을 따르므로 스프링 외의 컨테이너에서도 사용 가능
- 단점: 외부 라이브러리에는 적용할 수 없으므로, 그런 경우에는 initMethod/destroyMethod를 사용해야 함
- 가장 최신이자 권장되는 방법으로, 해당 애노테이션을 메서드에 붙이면 의존관계 주입 후 초기화(
빈 스코프 (Bean Scope)
기본 개념
- 빈 스코프는 빈이 컨테이너 내에서 얼마나 오래, 어디까지 관리되는지를 결정함
- 기본 스코프는 싱글톤이며, 컨테이너 생성 시 초기화되고 종료 시까지 동일한 인스턴스를 사용
주요 스코프 종류
- 싱글톤(Singleton): 컨테이너 시작과 종료까지 하나의 인스턴스만 유지
- 프로토타입(Prototype): 스프링 컨테이너가 프로토타입 빈을 생성하고 의존관계 주입, 초기화까지만 관여하며, '요청할 때마다' 새로운 인스턴스를 반환
- 생성 시마다 새로운 인스턴스가 만들어지며, 컨테이너는 소멸 콜백(종료 메서드)을 호출하지 않음
- 프로토타입 빈은 클라이언트가 직접 관리해야 함
- 웹 스코프
- request: 각 HTTP 요청마다 새 빈 인스턴스가 생성되고, 요청 종료 시 소멸
- session: HTTP 세션 생명주기와 동일하게 관리됨
- application: 서블릿 컨텍스트의 생명주기와 동일
- websocket: 웹소켓 연결 생명주기에 맞춰 관리됨
프로토타입 빈과 싱글톤 빈의 결합 문제
- 만약 싱글톤 빈에 프로토타입 빈을 의존관계 주입하면, 싱글톤 빈이 생성될 때 한 번만 프로토타입 빈이 생성되어 주입됨
- 따라서 여러 번 호출해도 항상 같은 프로토타입 인스턴스가 사용되어 의도한 "매번 새로 생성"하는 효과를 얻지 못함
- 해결 방법 (의존관계 조회, DL):
- ApplicationContext 직접 조회
- 싱글톤 빈 내부에서 필요할 때마다
ac.getBean(PrototypeBean.class)
를 호출하여 새 인스턴스를 얻는 방법
- 싱글톤 빈 내부에서 필요할 때마다
- ObjectFactory / ObjectProvider 사용
- 스프링이 제공하는 DL(Dependency Lookup) 기능으로,
ObjectProvider
의getObject()
를 호출하면 매번 새로운 프로토타입 빈을 생성하여 반환
- 스프링이 제공하는 DL(Dependency Lookup) 기능으로,
- JSR-330 Provider 사용
jakarta.inject.Provider
를 이용하면,provider.get()
호출 시마다 새로운 프로토타입 빈을 생성 가능- 별도의 라이브러리가 필요
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 가능
- ApplicationContext 직접 조회
- 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하자
- => 여기서도 굳이 DL을 위한 편의 기능을 많이 제공해주는
ObjectProvider
가 아니라 별도의 라이브러리를 필요로 하는 JSR-330 Provider를 사용할 필요는 없다.
- => 여기서도 굳이 DL을 위한 편의 기능을 많이 제공해주는
728x90
반응형
'Spring' 카테고리의 다른 글
[Spring] Spring Transaction 핵심 요약 (0) | 2025.04.10 |
---|---|
[Spring] 스프링의 데이터 접근 예외 추상화와 JdbcTemplate (0) | 2025.04.07 |
[Spring Core] 스프링을 사용하는 이유? (0) | 2025.02.15 |