빈 등록과 생성자 주입
BoardConfig가 저장소·시계·서비스를 빈으로 만들게 하고 Spring 컨테이너의 생성자 주입과 시작 시 실패를 실제 컨텍스트 테스트로 확인합니다.
Spring에서 빈(bean)은 Spring이 만들고 보관하는 객체이고, 컨테이너(container)는 빈을 만들고 필요한 빈끼리 연결하며 끝날 때까지 관리하는 곳입니다. 한 객체가 필요로 하는 다른 객체를 생성자 등으로 바깥에서 넘겨주는 방식을 의존성 주입(dependency injection)이라고 하며, 의존관계 주입이라고도 부릅니다.
앞 글의 서비스 테스트는 필요한 객체를 직접 만들어 연결했습니다.
var service = new PostService(
new MemoryPostRepository(),
fixedClock);이 방식은 틀리지 않았고, 오히려 객체가 무엇을 필요로 하는지를 가장 분명하게 보여 줍니다. 하지만 실제 애플리케이션에는 컨트롤러, 서비스, 저장소, 시계처럼 객체가 많고, 여러 객체가 같은 저장소를 함께 써야 합니다. 객체를 만드는 코드가 여러 main 메서드와 컨트롤러에 흩어져 있으면, 구현 하나를 바꿀 때마다 그 객체를 쓰는 코드를 모두 찾아다녀야 합니다. 그래서 객체를 만들고 연결하는 일을 Spring 컨테이너에 맡깁니다. 다만 컨테이너에 새 업무 규칙을 맡기지는 않습니다. 컨테이너는 이미 만들어 둔 클래스를 어떤 구현으로 만들고 어떻게 연결할지만 맡습니다.
구성 클래스의 결정
BoardConfig는 세 가지를 정합니다. PostRepository 역할에 어떤 구현을 쓸지, 애플리케이션의 날짜를 어느 시간대로 계산할지, PostService 생성자에 어떤 객체를 넘길지입니다.
package board.config;
import board.application.PostService;
import board.domain.PostRepository;
import board.infrastructure.MemoryPostRepository;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class BoardConfig {
@Bean
PostRepository postRepository() {
return new MemoryPostRepository();
}
@Bean
Clock postClock() {
return Clock.systemUTC();
}
@Bean
PostService postService(
PostRepository repository,
Clock postClock
) {
return new PostService(repository, postClock);
}
}@Bean이 붙은 메서드 하나가 빈 정의 하나가 되며, 메서드 이름이 기본 빈 이름이 됩니다. @Bean 메서드의 파라미터는 컨테이너가 찾아서 넣어 줄 의존성입니다. postService는 postRepository()나 postClock()을 직접 호출하지 않고 파라미터로 받기 때문에, @Bean 메서드 호출을 가로채는 프록시가 필요 없습니다. 그래서 proxyBeanMethods = false로 둡니다. 이 설정의 뜻은 3장에서 자세히 다룹니다.
Clock.systemUTC()는 PostService.register가 호출될 때마다 오늘 날짜를 UTC 기준으로 계산하게 합니다. 앞 글에서 정한 대로 서비스는 그 날짜를 저장소를 호출하기 전에 validateDate(today)에 넘깁니다. 시작 클래스 BoardApplication이 board 패키지에 있고 BoardConfig가 그 아래의 board.config에 있으므로, 컴포넌트 스캔이 @Configuration을 찾아 등록합니다. 따로 @Import를 적을 필요는 없습니다.
객체 그래프 조회
Boot 애플리케이션 전체를 띄우지 않고, AnnotationConfigApplicationContext로 구성 클래스 하나만 올려 확인합니다. 생성자에 BoardConfig.class를 넘기면 그 클래스의 @Bean 메서드만으로 컨테이너를 만들고 바로 갱신하므로, 컴포넌트 스캔이나 자동 설정, 내장 서버는 끼어들지 않습니다. 컨텍스트는 try-with-resources로 감싸 테스트가 끝날 때 닫고, 마지막 테스트는 Clock 빈을 뺀 구성으로 컨테이너를 만들어 의존성을 찾지 못할 때의 예외를 확인합니다.
package board.config;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import board.application.PostService;
import board.domain.PostRepository;
import board.infrastructure.MemoryPostRepository;
import java.time.Clock;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
class BoardConfigTest {
@Test
void 서비스와_이름이_지정된_의존성_빈을_조회한다() {
try (var context = new AnnotationConfigApplicationContext(
BoardConfig.class)) {
var service = context.getBean(PostService.class);
var repository = context.getBean(PostRepository.class);
var clock = context.getBean(Clock.class);
assertThat(service).isNotNull();
assertThat(repository).isSameAs(
context.getBean("postRepository"));
assertThat(clock).isSameAs(
context.getBean("postClock"));
}
}
@Test
void 기본_스코프는_같은_인스턴스를_반환한다() {
try (var context = new AnnotationConfigApplicationContext(
BoardConfig.class)) {
assertThat(context.getBean(PostService.class))
.isSameAs(context.getBean(PostService.class));
assertThat(context.getBean(PostRepository.class))
.isSameAs(context.getBean(PostRepository.class));
}
}
@Test
void 시계가_없으면_컨텍스트_갱신이_실패한다() {
assertThatThrownBy(() ->
new AnnotationConfigApplicationContext(
MissingClockConfig.class))
.isInstanceOf(UnsatisfiedDependencyException.class)
.hasMessageContaining("postService")
.hasRootCauseExactlyInstanceOf(
NoSuchBeanDefinitionException.class);
}
@Configuration(proxyBeanMethods = false)
static class MissingClockConfig {
@Bean
PostRepository postRepository() {
return new MemoryPostRepository();
}
@Bean
PostService postService(
PostRepository repository,
Clock clock
) {
return new PostService(repository, clock);
}
}
}try-with-resources로 컨텍스트를 닫으면 빈에 등록된 종료 콜백도 실행됩니다. 지금의 빈들은 닫을 외부 자원이 없지만, 뒤에서 DataSource처럼 수명을 관리해야 하는 객체를 다룰 때 이 습관이 중요해집니다.
./gradlew test --tests board.config.BoardConfigTest세 테스트가 모두 통과하면 BUILD SUCCESSFUL로 끝납니다.
DEPENDENCY DAG · PER CONTAINER
컨테이너는 세 빈 정의를 해석해 하나의 객체 그래프를 완성한다
흐름은 소스 줄 순서가 아니라 의존성 관계를 나타냅니다. 이 구성의 기본 non-lazy singleton들은 컨텍스트 갱신 중 미리 만들어지며, 서로 독립인 저장소와 시계의 전체 생성 순서는 계약하지 않습니다.
ASSEMBLY FLOW · FACTORY METHOD ARGUMENTS
세 application bean이 의존성 그래프로 연결된다
-
BoardConfig빈 정의 등록postRepository,postClock,postService세 팩토리 메서드가 각각 빈 정의가 됩니다. -
ApplicationContext.refresh()컨테이너가 기본 non-lazy singleton을 미리 만들며 팩토리 메서드 인자를 후보 해석 규칙으로 해결합니다.
-
저장소와 시계 후보 해결
PostRepository와Clock빈은 각 빈 정의마다, 이 컨테이너 안에서 한 기본 singleton 인스턴스로 관리됩니다. -
PostService생성자 호출선택된 저장소와 시계 참조가 팩토리 메서드 인자로 전달되고, 완성된 서비스 빈이 컨테이너에 보관됩니다.
proxyBeanMethods=false · 직접 @Bean 메서드를 부르지 않고 인자 주입으로 연결
EXECUTED ASSERTIONS · SUCCESS
관찰한 성공 경계
- 서비스 빈이 존재합니다.
- 저장소·시계의 타입 조회와 이름 조회가 각각 같은 인스턴스입니다.
- 같은 컨텍스트에서 서비스·저장소의 반복 조회가 같은 인스턴스입니다.
서비스 내부 필드의 참조 동일성은 직접 assertion하지 않았고, singleton도 JVM 전역이 아니라 빈 정의당·컨테이너당 범위입니다.
REFRESH FAILURE · CLOCK CANDIDATES 0
시계 누락
Clock 후보가 없으면 postService 팩토리 인자를 해결하지 못해 컨텍스트 갱신이 실패합니다.
테스트는 의존성 해결 예외와 후보 없음 근본 원인을 확인합니다. HTTP 요청이 시작될 때까지 실패를 미루지 않습니다.
postClock은 PostService.register 호출마다 날짜를 한 번 계산할 기준을 제공합니다. 저장소 호출 전 시간 검증은 컨테이너가 아니라 PostService의 application 경계가 맡습니다.
그림은 컨테이너가 세 빈 정의를 하나의 객체 그래프로 연결하는 모습을 보여 줍니다. 화살표는 소스 코드의 줄 순서가 아니라 의존 관계입니다. 컨텍스트를 갱신(refresh)하는 동안 컨테이너는 기본 싱글톤 빈들을 미리 만드는데, 서로 의존하지 않는 저장소와 시계 가운데 무엇을 먼저 만들지는 정해져 있지 않습니다. 두 빈이 모두 준비되어야 postService 팩토리 메서드가 호출되어 PostService가 완성되고, Clock 후보가 하나도 없으면 서비스를 만들기 전에 갱신이 실패합니다.
정상 구성 테스트가 직접 확인한 것은 세 가지입니다. 서비스 빈이 있다는 것, 저장소와 시계를 타입으로 찾든 이름으로 찾든 같은 인스턴스가 나온다는 것, 같은 컨텍스트에서 서비스와 저장소를 여러 번 꺼내도 같은 인스턴스가 나온다는 것입니다. 이 테스트는 PostService 안의 필드를 꺼내 조회한 저장소나 시계와 같은 객체인지까지 비교하지는 않습니다. 그 연결은 BoardConfig.postService가 파라미터로 받은 객체를 그대로 생성자에 넘긴다는 코드와, 같은 컨텍스트 안에서 싱글톤 빈은 하나뿐이라는 Spring의 규칙으로 알 수 있습니다. 여기서 싱글톤은 JVM 전체에 하나뿐이라는 뜻이 아니라, 한 ApplicationContext 안에서 빈 정의마다 인스턴스를 하나씩 다시 쓴다는 뜻입니다. 시계 누락 테스트는 postService를 만드는 중에 UnsatisfiedDependencyException이 나고, 그 근본 원인이 후보 빈이 없다는 NoSuchBeanDefinitionException인지를 확인합니다.
생성자 주입과 빠른 실패
PostService의 두 필드는 final이며 생성자 파라미터로 받은 값으로 채워집니다. 생성자 주입은 이렇게 협력 객체를 객체가 만들어지는 순간에 넘기고 final 필드로 유지하게 해 줍니다. 다만 생성자나 final이 null까지 막아 주지는 않습니다. PostService가 생성자에서 Objects.requireNonNull을 호출하기 때문에, 컨테이너 없이 직접 만들 때도 협력 객체가 빠지면 바로 거부됩니다.
컨테이너를 쓸 때는 실패가 더 이릅니다. 앞의 MissingClockConfig처럼 넣어 줄 Clock 빈이 없으면, 첫 HTTP 요청이 올 때까지 기다리지 않고 컨텍스트를 갱신하는 단계에서 실패합니다. 실제 예외 메시지는 길지만 핵심은 다음 부분입니다.
Error creating bean with name 'postService' ...
Unsatisfied dependency expressed through method 'postService' parameter 1:
No qualifying bean of type 'java.time.Clock' available이 메시지에서 만들지 못한 빈이 postService라는 것, 두 번째 파라미터(번호는 0부터 셉니다)인 Clock을 채우지 못했다는 것, 그 타입의 후보가 하나도 없었다는 것을 읽을 수 있습니다.
필드 주입은 객체를 먼저 만든 다음 리플렉션으로 필드에 값을 채웁니다. 그래서 컨테이너 없이 new로 만들면 필수 협력 객체가 빈 상태가 되고, 그 필드를 final로 둘 수도 없습니다. 세터 주입은 만든 뒤에 값을 바꿀 수 있어야 하는 설계에서 고를 수 있지만, 그렇다고 Spring이 실행 중에 의존성을 자동으로 바꿔 주는 것은 아닙니다. 핵심 서비스의 필수 협력 객체에는 생성자 주입이 가장 확실합니다.
복수 빈의 모호성
이번에는 일부러 실패를 만들어 봅니다. 기존 postClock 빈 대신 다음 두 빈이 있고, 서비스 팩토리 메서드의 파라미터 이름이 어느 빈 이름과도 같지 않은 clock인 구성을 생각해 봅니다. BoardConfig는 그대로 둡니다.
@Bean
Clock utcClock() {
return Clock.systemUTC();
}
@Bean
Clock seoulClock() {
return Clock.system(ZoneId.of("Asia/Seoul"));
}
@Bean
PostService postService(
PostRepository repository,
Clock clock
) {
return new PostService(repository, clock);
}PostService에 필요한 Clock은 하나인데 후보는 둘입니다. 한정자나 기본 후보를 표시하지 않았을 때, Spring은 마지막 수단으로 주입 지점의 파라미터 이름과 같은 이름의 빈을 고르기도 합니다. Spring Boot의 Gradle 플러그인이 컴파일러에 -parameters 옵션을 켜 두어 파라미터 이름이 클래스 파일에 남기 때문에 가능한 일입니다. 하지만 여기서는 clock이 utcClock과도 seoulClock과도 다르므로, 근본 원인이 NoUniqueBeanDefinitionException인 예외와 함께 컨텍스트 갱신이 실패합니다. 다음 테스트는 이 실패를 별도의 구성으로 확인합니다.
package board.config;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import board.application.PostService;
import board.domain.PostRepository;
import board.infrastructure.MemoryPostRepository;
import java.time.Clock;
import java.time.ZoneId;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
class BoardConfigAmbiguityTest {
@Test
void 일치하는_이름도_선택_단서도_없으면_시작이_실패한다() {
assertThatThrownBy(() ->
new AnnotationConfigApplicationContext(
AmbiguousClockConfig.class))
.isInstanceOf(UnsatisfiedDependencyException.class)
.hasMessageContaining("postService")
.hasRootCauseExactlyInstanceOf(
NoUniqueBeanDefinitionException.class);
}
@Configuration(proxyBeanMethods = false)
static class AmbiguousClockConfig {
@Bean
PostRepository postRepository() {
return new MemoryPostRepository();
}
@Bean
Clock utcClock() {
return Clock.systemUTC();
}
@Bean
Clock seoulClock() {
return Clock.system(ZoneId.of("Asia/Seoul"));
}
@Bean
PostService postService(
PostRepository repository,
Clock clock
) {
return new PostService(repository, clock);
}
}
}해결 방법은 무엇을 고르고 싶은지에 따라 다릅니다.
| 상황 | 방법 |
|---|---|
| 애플리케이션 전체가 쓰는 시계가 하나뿐이다 | 빈을 하나만 남긴다 |
| 대부분이 쓰는 기본값이 있고 예외가 조금 있다 | 기본값에 @Primary를 붙인다 |
| "작성일 계산용"처럼 쓰임새가 다르다 | @Qualifier("publishedOn") 같은 한정자를 만드는 쪽과 주입받는 쪽에 함께 붙인다 |
| 여러 시계를 함께 쓴다 | Map<String, Clock>으로 모든 후보를 빈 이름과 함께 받고 애플리케이션 코드가 고른다 |
지금은 작성일 정책이 UTC 하나뿐이므로 빈을 하나만 두는 것이 가장 단순합니다. 나중에 회원마다 시간대를 지원하게 되면 Clock을 여러 개 두기보다 회원의 ZoneId를 입력으로 받는 정책이 더 자연스러울 수 있습니다.
INJECTION CONTRACT · CANDIDATE SELECTION
객체 생성 계약과 빈 선택 의미를 분리해 결정한다
생성자 주입은 협력자를 언제 전달할지 정하고, 후보 선택 규칙은 어떤 빈을 전달할지 정합니다. 두 결정을 하나의 애노테이션 효과처럼 섞지 않습니다.
| 방식 | 생성·사용 시 계약 | 이 문서의 판단 |
|---|---|---|
| 생성자 | 협력자를 생성 시 전달하고 필드를 final로 둘 수 있습니다. 값과 null 검증은 생성자 코드의 책임입니다. |
PostService는 Objects.requireNonNull과 함께 필수 저장소·시계 계약을 고정합니다. |
| setter | 공개 API가 생성 후 재설정을 허용합니다. Spring이 실행 중 협력자를 자동으로 교체한다는 뜻은 아닙니다. | 의도적으로 재설정 가능한 설계에만 사용하고 현재 핵심 서비스에는 쓰지 않습니다. |
| 필드 | 평범한 생성 직후에는 컨테이너 주입 전 상태이며, 주입 필드를 final 계약으로 두기 어렵습니다. |
생성자만으로 완전한 객체를 만들기 어렵고 단위 테스트와 필수 협력자 가시성이 약해집니다. |
이 문서의 핵심 서비스에 적용한 주입 방식 비교
생성자
생성·사용 시 계약: 협력자를 생성 시 전달하고 필드를 final로 둘 수 있습니다. 값과 null 검증은 생성자 코드의 책임입니다.
이 문서의 판단: PostService는 Objects.requireNonNull과 함께 필수 저장소·시계 계약을 고정합니다.
setter
생성·사용 시 계약: 공개 API가 생성 후 재설정을 허용합니다. Spring이 실행 중 협력자를 자동으로 교체한다는 뜻은 아닙니다.
이 문서의 판단: 의도적으로 재설정 가능한 설계에만 사용하고 현재 핵심 서비스에는 쓰지 않습니다.
필드
생성·사용 시 계약: 평범한 생성 직후에는 컨테이너 주입 전 상태이며, 주입 필드를 final 계약으로 두기 어렵습니다.
이 문서의 판단: 생성자만으로 완전한 객체를 만들기 어렵고 단위 테스트와 필수 협력자 가시성이 약해집니다.
ONE TYPE CANDIDATE
타입 후보가 하나면 그대로 주입
현재 BoardConfig의 Clock 하나처럼 다른 선택 단서가 필요 없는 가장 단순한 상태입니다.
DEFAULT AMONG MANY
@Primary로 넓은 기본값 표시
같은 타입 후보가 여럿이고 대부분의 주입 지점이 사용할 기본이 하나일 때 선택합니다.
SEMANTIC PURPOSE
@Qualifier로 사용 의미 표시
@Qualifier("publishedOn")처럼 생산자와 주입 지점이 의미를 공유합니다. 한정자 값은 빈 이름과 독립적일 수 있습니다.
ALL TYPE CANDIDATES
Map<String, Clock>으로 모두 받기
타입이 맞는 빈을 빈 이름→인스턴스로 주입한 뒤 애플리케이션 코드가 키를 선택합니다. 컨테이너의 자동 런타임 선택이 아닙니다.
명시적 한정자나 기본 후보가 없고 Java의 -parameters처럼 메타데이터가 보존되면 주입 지점 이름도 후보 좁히기에 쓰일 수 있습니다. 현재 실패 테스트의 clock은 utcClock·seoulClock과 모두 달라 복수 후보 오류가 납니다.
그림은 두 가지 결정을 나누어 보여 줍니다. 위쪽 표는 협력 객체를 언제 넘길지에 따라 생성자, 세터, 필드 주입을 비교하고, 아래쪽 상자들은 같은 타입의 후보가 여럿일 때 어떤 빈을 넘길지를 정하는 네 가지 방법을 보여 줍니다. 주입 방식과 후보 선택 규칙은 서로 다른 결정이므로, 애노테이션 하나로 둘이 함께 정해진다고 생각하지 않습니다.
더 깊이: 파라미터 이름은 어디에서 오는가
Java 컴파일러는 기본적으로 메서드 파라미터의 이름을 클래스 파일에 남기지 않습니다. 리플렉션의 Parameter.getName()은 클래스 파일에 이름이 기록되어 있을 때만 소스에 적은 clock 같은 이름을 돌려주고, 기록이 없으면 arg0, arg1처럼 위치로 만든 이름을 돌려줍니다. 이 이름을 클래스 파일의 MethodParameters 속성에 기록하게 하는 것이 javac의 -parameters 옵션입니다.
예전 Spring은 이 옵션이 없어도 디버그 정보의 지역 변수 표를 읽는 LocalVariableTableParameterNameDiscoverer로 이름을 찾아낼 수 있었습니다. 그러나 Spring Framework 6.1 릴리스 노트에 따르면 이 클래스는 6.1에서 제거되었고, 지금은 -parameters로 컴파일해야 이름이 남습니다. 레퍼런스 문서도 파라미터 이름을 빈 이름과 비교하는 마지막 수단이 6.1부터 이 옵션을 요구한다고 밝힙니다. @PathVariable처럼 애노테이션에서 이름을 생략한 웹 인자도 같은 정보에 기댑니다.
Spring Boot의 Gradle 플러그인은 프로젝트의 모든 JavaCompile 작업에 -parameters를 붙이고, 인코딩을 따로 정하지 않은 작업에는 UTF-8을 지정합니다. 그래서 1장의 프로젝트에서는 아무 설정 없이도 이름 비교가 동작합니다. 반대로 Boot 플러그인 없이 BOM만 가져오는 프로젝트라면 이 옵션을 직접 켜야 하며, 6장의 예제 빌드가 options.compilerArgs에 -parameters를 따로 적는 것도 이 때문입니다.
수동 구성과 컴포넌트 스캔
MemoryPostRepository에 @Repository를, PostService에 @Service를 붙이고 컴포넌트 스캔에 맡겨도 같은 객체 그래프를 만들 수 있습니다. 이 장에서는 어떤 구현을 골랐는지 한 파일에서 읽을 수 있도록 @Bean을 직접 씁니다. 우리가 작성한 클래스이고 구현이 하나뿐이며 자동으로 찾아도 자연스러운 컨트롤러 같은 객체에는 스테레오타입 애노테이션이 편합니다. 반면 외부 라이브러리의 타입, 환경에 따라 구현을 고르는 객체, 생성 인자가 복잡한 객체는 @Bean이 의도를 더 잘 드러냅니다. 같은 역할의 구현이 여럿이라면 스캔으로 모두 등록해 두고 우연에 맡기기보다, 구성 클래스에서 어떤 조합을 쓸지 밝히는 편이 낫습니다.
스테레오타입 애노테이션으로 바꿀 때는 대응하는 @Bean 정의를 지웁니다. 둘을 함께 두면 같은 인스턴스가 두 번 보관되는 것이 아니라, 빈 이름이 다르면 별도의 빈 정의와 인스턴스가 두 개 생겨 타입 후보가 모호해지고, 이름이 같으면 빈 이름 충돌이나 덮어쓰기 정책의 영향을 받습니다.
테스트 전용 구성
서비스 규칙을 확인하는 단위 테스트는 지금처럼 Spring 없이 고정 Clock을 직접 넘기는 것이 가장 빠릅니다. 컨테이너가 연결하는 것까지 확인해야 할 때만 테스트용 구성을 따로 만듭니다.
@Configuration(proxyBeanMethods = false)
@Import(BoardConfig.class)
static class FixedClockConfig {
@Bean
@Primary
Clock fixedClock() {
return Clock.fixed(
Instant.parse("2026-07-13T09:00:00Z"),
ZoneOffset.UTC);
}
}이 구성으로 컨텍스트를 만들면 기존 postClock과 fixedClock 두 후보가 생기고, @Primary가 붙은 fixedClock이 선택됩니다. 다만 이 조각에는 컨텍스트를 열어 실제로 어떤 시계가 주입되었는지 검사하는 테스트가 없으므로, 아직 확인한 동작이라고 말할 수는 없습니다. 테스트마다 필요한 최소한의 범위만 띄우는 습관이 전체 테스트의 속도와, 실패했을 때 원인을 찾는 쉬움을 좌우합니다.
정리
- 빈은 Spring 컨테이너가 만들고 보관하는 객체이며,
@Bean메서드 하나가 빈 정의 하나가 되고 메서드 이름이 기본 빈 이름이 됩니다. @Bean메서드의 파라미터는 컨테이너가 찾아 넣어 주는 의존성이며, 서로 직접 호출하지 않으므로proxyBeanMethods = false로 둡니다.- 생성자 주입은 필수 협력 객체를 생성 시점에 받아
final필드로 지키게 해 주고, 빠진 의존성은 컨텍스트 갱신 단계에서 바로 실패로 드러납니다. - 기본 싱글톤은 JVM 전체가 아니라 한 컨텍스트 안에서 빈 정의마다 인스턴스가 하나라는 뜻입니다.
- 같은 타입의 후보가 여럿이면 빈을 하나로 줄이거나
@Primary,@Qualifier,Map주입 가운데 쓰임새에 맞는 방법으로 고릅니다.
연습 문제
1. 한정자로 시계 고르기
postClock을 utcClock과 seoulClock 두 빈으로 바꾸고 주입 파라미터 이름을 clock으로 두어 후보가 여럿인 실패를 직접 재현한 뒤, @Qualifier("publishedOn")으로 해결하세요. @Primary 대신 한정자를 고른 이유도 한 문장으로 적으세요.
해설 보기
작성일 계산에 쓸 시계를 만드는 쪽과 주입받는 쪽에 같은 한정자를 붙입니다.
@Bean
@Qualifier("publishedOn")
Clock utcClock() {
return Clock.systemUTC();
}
@Bean
Clock seoulClock() {
return Clock.system(ZoneId.of("Asia/Seoul"));
}
@Bean
PostService postService(
PostRepository repository,
@Qualifier("publishedOn") Clock clock
) {
return new PostService(repository, clock);
}@Primary는 "대부분의 주입 지점이 쓸 기본값"이라는 뜻입니다. 여기서는 이 시계가 작성일 계산용이라는 쓰임새를 주입 지점에 남기고 싶으므로 한정자가 더 정확합니다. publishedOn은 utcClock이라는 빈 이름과 상관없는 표시이므로, 나중에 빈 이름을 바꾸어도 주입 지점은 그대로 둘 수 있습니다. 시계가 하나뿐이라면 한정자 자체가 필요 없으므로, 실제로 요구가 생기기 전까지는 빈 하나만 둡니다. 이 연습처럼 BoardConfig에 시계 빈을 두 개 두면 BoardConfigTest의 첫 테스트가 context.getBean(Clock.class)에서 후보가 둘이라 NoUniqueBeanDefinitionException으로 실패하므로, 확인을 마친 뒤에는 BoardConfig를 본문의 postClock 하나로 되돌립니다.
이제 컨테이너가 서비스와 저장소, 시계를 연결합니다. 다음 글 게시판 웹 등록·조회에서는 이 PostService를 REST 컨트롤러에 주입해, 게시글 작성과 조회를 HTTP 상태 코드와 Location 헤더, JSON 본문으로 완성합니다.