Spring이 객체를 생성하고 보관하며, 객체 사이의 의존관계를 연결해 주는 공간이다.
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext가 Spring 컨테이너의 대표적인 인터페이스다.
Spring 컨테이너가 생성하고 관리하는 객체를 Spring Bean이라고 한다.
다음 두 방법 모두 결과적으로 Spring Bean을 등록한다.
@Service, @Repository, @Component 등을 이용한 컴포넌트 스캔@Configuration과 @Bean을 이용한 명시적 등록어떤 구현 객체를 생성하고, 어느 객체에 연결할지 정의하는 설정 클래스다.
AppConfig와 Spring Bean은 서로 반대되는 개념이 아니다. @Configuration과 @Bean이 붙은 AppConfig는 Spring Bean을 등록하는 방법 중 하나다.
현재 MemberServiceImpl은 MemberRepository 인터페이스에 의존한다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
이 생성자는 "MemberServiceImpl을 만들려면 MemberRepository가 필요하다"는 사실을 표현한다.
생성자 주입 자체는 Spring 전용 기능이 아니다. 누가 생성자를 호출하느냐에 따라 수동 DI와 Spring DI로 나뉜다.
MemberRepository repository = new MemoryMemberRepository();
MemberService memberService = new MemberServiceImpl(repository);
개발자가 직접 객체를 생성하고 생성자에 전달한다.
@Service
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
@Repository
public class MemoryMemberRepository implements MemberRepository {
}
Spring 컨테이너가 실행되면 다음 순서로 처리한다.
MemoryMemberRepository를 Bean으로 생성한다.MemberServiceImpl을 Bean으로 생성하려고 생성자를 확인한다.MemberRepository 타입의 Bean을 찾는다.생성자가 하나라면 @Autowired는 생략할 수 있다.
중요한 점은 직접 new로 생성한 객체에는 Spring이 자동 주입하지 않는다는 것이다.
new MemberServiceImpl(); // 컴파일 오류: MemberRepository 인자가 필요함
new MemberServiceImpl(new MemoryMemberRepository()); // 수동 DI
SpringApplication.run(CoreApplication.class, args);
이 코드를 실행하면 Spring 컨테이너가 시작되고 컴포넌트 탐색, Bean 생성, 의존성 주입이 이루어진다.
@SpringBootTest
class CoreApplicationTests {
}
@SpringBootTest도 Spring 컨테이너를 시작한다.
main 또는 일반 단위 테스트Spring 컨테이너를 시작하지 않으면 @Service, @Repository가 붙어 있어도 자동 주입은 일어나지 않는다. 이 경우 객체를 직접 생성해야 한다.
판단 기준은 "현재 코드가 Spring 컨테이너 안에서 실행되는가?"이다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
}
@Configuration과 @Bean이 없으면 Spring 설정이 아니라 일반 Java 클래스다. 여기서 반환한 객체도 Spring Bean이 아니다.
AppConfig config = new AppConfig();
MemberService memberService = config.memberService();
개발자가 AppConfig를 직접 생성하고 객체 조립을 요청하는 수동 DI 방식이다.
현재 프로젝트의 AppConfig는 다음 구조다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy()
);
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
@Bean 메서드가 반환한 객체들은 Spring Bean으로 등록된다.
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService =
context.getBean("memberService", MemberService.class);
일반 AppConfig에서는 메서드를 호출할 때마다 새로운 객체가 만들어질 수 있다.
AppConfig config = new AppConfig();
MemberService first = config.memberService();
MemberService second = config.memberService();
// 일반 Java AppConfig라면 서로 다른 객체가 될 수 있음
System.out.println(first == second);
Spring Bean의 기본 스코프는 싱글톤이다. 같은 컨테이너에서 같은 Bean을 여러 번 조회하면 같은 객체를 반환한다.
MemberService first = context.getBean(MemberService.class);
MemberService second = context.getBean(MemberService.class);
System.out.println(first == second); // true
또한 memberService()와 orderService()에서 호출하는 memberRepository()도 Spring이 관리하는 동일한 Bean을 사용한다.
MemberServiceImpl ─┐
├─ 같은 MemoryMemberRepository Bean
OrderServiceImpl ──┘
@Service
public class MemberServiceImpl implements MemberService {
}
@Repository
public class MemoryMemberRepository implements MemberRepository {
}
Spring이 클래스를 탐색해 자동으로 Bean을 등록한다.
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
개발자가 어떤 구현체를 어떻게 생성할지 명확하게 작성한다.
두 방식 모두 등록이 끝난 뒤에는 동일한 Spring Bean이다. 차이는 Bean 등록 정보를 어디에 작성하느냐이다.
| 구분 | 컴포넌트 스캔 | AppConfig |
|---|---|---|
| 등록 방법 | @Service, @Repository, @Component | @Configuration, @Bean |
| 장점 | 작성이 간단함 | 객체 생성과 구현체 선택이 명확함 |
| 주 사용처 | 애플리케이션 서비스, 저장소, 컨트롤러 | 외부 라이브러리, 복잡한 생성 설정, 정책 선택 |
public interface DiscountPolicy {
int discount(Member member, int price);
}
구현체가 두 개 모두 Bean으로 등록되어 있다고 가정한다.
@Component
public class FixDiscountPolicy implements DiscountPolicy {
}
@Component
public class RateDiscountPolicy implements DiscountPolicy {
}
다음 생성자에 주입할 때 Spring은 둘 중 어느 구현체를 선택해야 할지 알 수 없다.
public OrderServiceImpl(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
이 문제는 컴파일 오류가 아니라 Spring 컨테이너 실행 시 발생하는 런타임 오류다.
NoUniqueBeanDefinitionException
해결 방법은 다음과 같다.
@Primary로 기본 구현체 지정@Qualifier로 특정 구현체 지정@Profile이나 조건 설정으로 환경에 맞는 구현체 등록@Bean으로 등록현재 AppConfig에서는 다음 한 줄로 할인 정책을 선택한다.
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
고정 금액 할인으로 변경할 때는 OrderServiceImpl을 수정하지 않고 설정만 바꿀 수 있다.
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
현재 프로젝트는 다음 두 등록 방식을 함께 사용하고 있다.
MemberService, MemberRepository를 @Bean으로 등록MemberServiceImpl에 @Service 사용MemoryMemberRepository에 @Repository 사용new AnnotationConfigApplicationContext(AppConfig.class)처럼 AppConfig만 넘기면 별도의 컴포넌트 스캔 설정이 없으므로 AppConfig의 @Bean들이 중심이 된다.
하지만 CoreApplication의 @SpringBootApplication으로 실행하면 hello.core 아래를 컴포넌트 스캔한다. 이때 AppConfig의 Bean과 @Service, @Repository Bean이 함께 등록되어 같은 역할의 객체가 여러 개 생길 수 있다.
학습 단계에서는 다음 중 한 가지 방식으로 통일하는 편이 명확하다.
MemberServiceImpl의 @Service 제거MemoryMemberRepository의 @Repository 제거@Bean으로만 등록@Service, @Repository, @Component 사용@Bean 제거실무에서는 두 방식을 함께 사용하는 경우가 많다.
직접 작성한 일반 애플리케이션 코드는 컴포넌트 스캔으로 등록하는 경우가 많다.
@RestController
class OrderController {
}
@Service
class OrderServiceImpl {
}
@Repository
class JpaOrderRepository {
}
다음과 같은 객체는 설정 클래스로 등록하는 경우가 많다.
실무에서는 하나의 거대한 AppConfig보다 목적별 설정 클래스를 많이 사용한다.
SecurityConfig
DatabaseConfig
RedisConfig
JacksonConfig
PaymentConfig
DiscountConfig
new로 연결하면 수동 DI다.@Service와 @Repository는 컴포넌트 스캔을 통한 Bean 등록 방법이다.@Configuration과 @Bean은 설정 클래스를 통한 명시적 Bean 등록 방법이다.가장 중요한 질문은 다음 두 가지다.
이 객체는 누가 생성하는가?
이 객체의 의존관계는 누가 연결하는가?
둘 다 Spring 컨테이너가 담당한다면 Spring Bean과 Spring DI를 사용하는 것이다.