Spring 컨테이너 전환
같은 회원가입 객체 그래프의 생성과 연결을 Spring 컨테이너로 옮기고, 단일 생성자 주입·컨텍스트별 기본 singleton·후보 결정 실패를 Spring 7 API로 검증합니다.
앞 문서의 순수 Java BoardConfig는 저장소와 해시 구현을 직접 만들고 같은 참조를 두 서비스에 전달했습니다.
Spring으로 옮겨도 MemberRepository와 PasswordHasher 역할, 서비스 생성자, 회원가입·로그인 규칙은 바뀌지 않습니다.
달라지는 지점은 객체를 만들고 연결하는 구성 경계입니다.
앞 절의 같은 프로젝트에서 BoardConfig·BoardApplication·BoardConfigTest를 아래 Spring 버전으로 교체합니다. 회원·저장소·해시·두 서비스 파일은 유지합니다. 같은 이름의 순수 Java 구성과 Spring 구성을 동시에 컴파일하지 않습니다. 첫 장의 Boot 4.1.1 의존성 기준에는 여기서 쓰는 Spring Framework 7의 context API와 JUnit·AssertJ가 포함되므로 새 버전의 context 라이브러리를 임의로 섞지 않습니다.
이 절도 board.BoardApplication.main과 ./gradlew test --tests board.BoardConfigTest로 시작하되, main의 컨텍스트 생성과 refresh()가 성공한 다음 memberId=1이 출력되는 순서를 관찰합니다. Spring Boot 웹 서버의 자동 설정을 실행하는 경로와 구분하세요.
같은 역할 그래프를 빈으로 등록
@Configuration은 @Bean 메서드를 처리할 구성 클래스임을 표시하고, 각 @Bean 메서드는 컨테이너가 관리할 객체의 생성 규칙을 제공합니다.
다음 구성은 메서드끼리 직접 호출하지 않고 필요한 빈을 매개변수로 받으므로 proxyBeanMethods = false를 사용합니다.
TestPasswordHasher는 test source에만 남기고, main 그래프는 앞 문서와 같은 PBKDF2 어댑터를 선택합니다.
package board;
import board.member.LoginService;
import board.member.MemberRegistrationService;
import board.member.MemberRepository;
import board.member.MemoryMemberRepository;
import board.member.PasswordHasher;
import board.member.PasswordHashSettings;
import board.member.Pbkdf2PasswordHasher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public final class BoardConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public PasswordHasher passwordHasher() {
return new Pbkdf2PasswordHasher(
PasswordHashSettings.productionDefaults());
}
@Bean
public MemberRegistrationService memberRegistrationService(
MemberRepository members,
PasswordHasher passwordHasher
) {
return new MemberRegistrationService(
members, passwordHasher);
}
@Bean
public LoginService loginService(
MemberRepository members,
PasswordHasher passwordHasher
) {
return new LoginService(members, passwordHasher);
}
}refresh()가 실행되면 컨테이너는 기본적으로 지연되지 않은 singleton 빈을 만들면서 @Bean 매개변수의 의존성을 해결합니다.
이 구성에서는 하나의 MemberRepository 빈과 하나의 PasswordHasher 빈이 각 서비스 생성자에 전달됩니다.
그 참조 연결은 한 ApplicationContext 안의 빈 정의를 기준으로 하며 JVM 전역 객체라는 뜻이 아닙니다. Spring의 기본 singleton 범위는 컨테이너와 빈 정의를 함께 기준으로 삼습니다.
Spring은 같은 역할 그래프의 생성과 연결을 컨테이너로 옮긴다
등록·refresh의 정의 해석/생성/주입·App 조회·일반 Java 업무 실행이라는 다섯 시작 단계이다.
SPRING CONTAINER · SAME ROLE GRAPH
Spring은 같은 역할 그래프의 생성과 연결을 컨테이너로 옮긴다
역할과 업무 코드는 그대로 두고 구성 경계만 옮깁니다. 등록된 생성 규칙은 refresh()에서 해석되고, 완성된 애플리케이션은 이후 일반 Java 호출로 실행됩니다.
REGISTER → REFRESH → WIRED GRAPH
컨테이너 시작은 생성 규칙 등록과 의존성 해결을 분리한다
-
구성 클래스 등록
context.register(...)가BoardConfig.class와BoardApplication.class를 컨텍스트에 등록합니다. -
빈 정의 해석
@Configuration과 네@Bean메서드가 저장소·해시 어댑터·두 서비스의 생성 규칙이 됩니다. -
refresh()에서 생성·연결컨테이너가 매개변수 타입으로 후보를 결정해 R#1과 H#1을 두 서비스 생성자에 전달합니다.
-
단일 생성자에 서비스 주입
BoardApplication의 유일한 생성자가 회원가입·로그인 서비스를 받고 완성된 실행 객체가 됩니다. -
진입점 뒤에는 일반 호출
시작 경계에서 애플리케이션 빈을 얻은 뒤 서비스는 컨텍스트를 조회하지 않고 직접 협력합니다.
| 빈 생성 규칙 | 현재 구현 | 주입 지점 | 유지되는 경계 |
|---|---|---|---|
memberRepository() |
MemoryMemberRepository R#1 |
두 서비스의 MemberRepository |
저장 기술은 구성에 남음 |
passwordHasher() |
Pbkdf2PasswordHasher H#1 |
두 서비스의 PasswordHasher |
해시 정책 선택은 구성에 남음 |
| 두 서비스 메서드 | 현재 서비스 클래스 | BoardApplication의 단일 생성자 |
업무 코드는 Spring API를 모름 |
| 명시적 컴포넌트 등록 | BoardApplication |
실행 진입점의 한 번의 조회 | 컨테이너 조회는 시작 경계에 한정 |
같은 역할 그래프에서 컨테이너가 선택하고 연결하는 대상
memberRepository()
현재 구현: MemoryMemberRepository R#1
주입 지점: 두 서비스의 MemberRepository
유지되는 경계: 저장 기술은 구성에 남음
passwordHasher()
현재 구현: Pbkdf2PasswordHasher H#1
주입 지점: 두 서비스의 PasswordHasher
유지되는 경계: 해시 정책 선택은 구성에 남음
두 서비스 메서드
현재 구현: 현재 서비스 클래스
주입 지점: BoardApplication의 단일 생성자
유지되는 경계: 업무 코드는 Spring API를 모름
명시적 컴포넌트 등록
현재 구현: BoardApplication
주입 지점: 실행 진입점의 한 번의 조회
유지되는 경계: 컨테이너 조회는 시작 경계에 한정
BEFORE · PURE JAVA CONFIG
구성 객체가 필드 참조를 직접 전달
앞 문서에서는 한 BoardConfig 인스턴스가 R#1·H#1을 만들고 서비스 생성자를 직접 호출했습니다.
NOW · EXPLICIT SPRING CONFIG
컨테이너가 빈 정의와 주입 지점을 연결
@Bean 메서드는 서로 직접 호출하지 않습니다. 매개변수 주입이 컨텍스트 안의 같은 역할 그래프를 연결합니다.
이 전환은 Spring Framework의 명시적 Java 구성입니다. Spring Boot 자동 설정이나 thread safety·transaction·persistence 보장은 이 그래프에 포함되지 않습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 등록→refresh의 정의 해석/빈 생성/매개변수 의존성 해결→완성된 App 조회→일반 Java 업무 실행이다. 5단계와 4관계의 시작 흐름이며 모든 업무 호출이 컨테이너를 다시 조회한다는 그림이 아니다. 현재 명시적 등록/@Bean/기본 singleton 조건을 가정하며 후보 선택 실패는 다음 그림에서 분기한다. Boot 웹 자동 설정이나 동시성 보장을 포함하지 않는다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 9 | 등록→refresh의 정의 해석/빈 생성/매개변수 의존성 해결→완성된 App 조회→일반 Java 업무 실행이다. 5단계와 4관계의 시작 흐름이며 모든 업무 호출이 컨테이너를 다시 조회한다는 그림이 아니다. 현재 명시적 등록/@Bean/기본 singleton 조건을 가정하며 후보 선택 실패는 다음 그림에서 분기한다. Boot 웹 자동 설정이나 동시성 보장을 포함하지 않는다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 정의 등록 명시 등록·Bean 정의를 컨테이너에 전달합니다. | 시점: refresh 전 · 대상: 빈 정의 | 생성할 그래프 입력이 준비됩니다. |
| refresh 조립 정의를 해석하고 빈 생성·매개변수 의존성을 해결합니다. | 범위: 기본 singleton · 산출물: 완성 그래프 | 컨테이너가 객체와 참조를 연결합니다. |
| App 조회·업무 완성 App을 조회한 뒤 일반 Java 업무를 실행합니다. | 조회: 완성 App · 업무: 일반 Java 호출 | 매 업무마다 컨테이너를 다시 조회하지 않습니다. |
Spring은 같은 역할 그래프의 생성과 연결을 컨테이너로 옮긴다
등록·refresh의 정의 해석/생성/주입·App 조회·일반 Java 업무 실행이라는 다섯 시작 단계이다.
SPRING CONTAINER · SAME ROLE GRAPH
Spring은 같은 역할 그래프의 생성과 연결을 컨테이너로 옮긴다
역할과 업무 코드는 그대로 두고 구성 경계만 옮깁니다. 등록된 생성 규칙은 refresh()에서 해석되고, 완성된 애플리케이션은 이후 일반 Java 호출로 실행됩니다.
REGISTER → REFRESH → WIRED GRAPH
컨테이너 시작은 생성 규칙 등록과 의존성 해결을 분리한다
-
구성 클래스 등록
context.register(...)가BoardConfig.class와BoardApplication.class를 컨텍스트에 등록합니다. -
빈 정의 해석
@Configuration과 네@Bean메서드가 저장소·해시 어댑터·두 서비스의 생성 규칙이 됩니다. -
refresh()에서 생성·연결컨테이너가 매개변수 타입으로 후보를 결정해 R#1과 H#1을 두 서비스 생성자에 전달합니다.
-
단일 생성자에 서비스 주입
BoardApplication의 유일한 생성자가 회원가입·로그인 서비스를 받고 완성된 실행 객체가 됩니다. -
진입점 뒤에는 일반 호출
시작 경계에서 애플리케이션 빈을 얻은 뒤 서비스는 컨텍스트를 조회하지 않고 직접 협력합니다.
| 빈 생성 규칙 | 현재 구현 | 주입 지점 | 유지되는 경계 |
|---|---|---|---|
memberRepository() |
MemoryMemberRepository R#1 |
두 서비스의 MemberRepository |
저장 기술은 구성에 남음 |
passwordHasher() |
Pbkdf2PasswordHasher H#1 |
두 서비스의 PasswordHasher |
해시 정책 선택은 구성에 남음 |
| 두 서비스 메서드 | 현재 서비스 클래스 | BoardApplication의 단일 생성자 |
업무 코드는 Spring API를 모름 |
| 명시적 컴포넌트 등록 | BoardApplication |
실행 진입점의 한 번의 조회 | 컨테이너 조회는 시작 경계에 한정 |
같은 역할 그래프에서 컨테이너가 선택하고 연결하는 대상
memberRepository()
현재 구현: MemoryMemberRepository R#1
주입 지점: 두 서비스의 MemberRepository
유지되는 경계: 저장 기술은 구성에 남음
passwordHasher()
현재 구현: Pbkdf2PasswordHasher H#1
주입 지점: 두 서비스의 PasswordHasher
유지되는 경계: 해시 정책 선택은 구성에 남음
두 서비스 메서드
현재 구현: 현재 서비스 클래스
주입 지점: BoardApplication의 단일 생성자
유지되는 경계: 업무 코드는 Spring API를 모름
명시적 컴포넌트 등록
현재 구현: BoardApplication
주입 지점: 실행 진입점의 한 번의 조회
유지되는 경계: 컨테이너 조회는 시작 경계에 한정
BEFORE · PURE JAVA CONFIG
구성 객체가 필드 참조를 직접 전달
앞 문서에서는 한 BoardConfig 인스턴스가 R#1·H#1을 만들고 서비스 생성자를 직접 호출했습니다.
NOW · EXPLICIT SPRING CONFIG
컨테이너가 빈 정의와 주입 지점을 연결
@Bean 메서드는 서로 직접 호출하지 않습니다. 매개변수 주입이 컨텍스트 안의 같은 역할 그래프를 연결합니다.
이 전환은 Spring Framework의 명시적 Java 구성입니다. Spring Boot 자동 설정이나 thread safety·transaction·persistence 보장은 이 그래프에 포함되지 않습니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 등록→refresh의 정의 해석/빈 생성/매개변수 의존성 해결→완성된 App 조회→일반 Java 업무 실행이다. 5단계와 4관계의 시작 흐름이며 모든 업무 호출이 컨테이너를 다시 조회한다는 그림이 아니다. 현재 명시적 등록/@Bean/기본 singleton 조건을 가정하며 후보 선택 실패는 다음 그림에서 분기한다. Boot 웹 자동 설정이나 동시성 보장을 포함하지 않는다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 9 | 등록→refresh의 정의 해석/빈 생성/매개변수 의존성 해결→완성된 App 조회→일반 Java 업무 실행이다. 5단계와 4관계의 시작 흐름이며 모든 업무 호출이 컨테이너를 다시 조회한다는 그림이 아니다. 현재 명시적 등록/@Bean/기본 singleton 조건을 가정하며 후보 선택 실패는 다음 그림에서 분기한다. Boot 웹 자동 설정이나 동시성 보장을 포함하지 않는다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 정의 등록 명시 등록·Bean 정의를 컨테이너에 전달합니다. | 시점: refresh 전 · 대상: 빈 정의 | 생성할 그래프 입력이 준비됩니다. |
| refresh 조립 정의를 해석하고 빈 생성·매개변수 의존성을 해결합니다. | 범위: 기본 singleton · 산출물: 완성 그래프 | 컨테이너가 객체와 참조를 연결합니다. |
| App 조회·업무 완성 App을 조회한 뒤 일반 Java 업무를 실행합니다. | 조회: 완성 App · 업무: 일반 Java 호출 | 매 업무마다 컨테이너를 다시 조회하지 않습니다. |
단일 생성자 주입과 실행 경계
BoardApplication은 Spring의 ApplicationContext를 필드로 받지 않습니다.
명시적으로 등록된 @Component에 생성자가 하나뿐이면 @Autowired를 붙이지 않아도 그 생성자가 주입 지점이 됩니다.
진입점만 컨텍스트를 만들고 register → refresh → getBean 순서로 시작합니다.
업무 실행은 완성된 BoardApplication에서 일반 Java 메서드 호출로 이어집니다.
package board;
import board.member.LoginService;
import board.member.Member;
import board.member.MemberRegistrationService;
import board.member.SignUpCommand;
import java.util.Objects;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;
@Component
public final class BoardApplication {
private final MemberRegistrationService registration;
private final LoginService login;
public BoardApplication(
MemberRegistrationService registration,
LoginService login
) {
this.registration = Objects.requireNonNull(
registration, "registration must not be null");
this.login = Objects.requireNonNull(
login, "login must not be null");
}
public long registerAndAuthenticate(SignUpCommand command) {
Objects.requireNonNull(command, "command must not be null");
Member registered = registration.register(command);
long loginId = login.authenticate(
command.email(), command.rawPassword());
if (registered.id() != loginId) {
throw new IllegalStateException(
"composition returned different member ids");
}
return loginId;
}
public static void main(String[] args) {
try (var context =
new AnnotationConfigApplicationContext()) {
context.register(
BoardConfig.class, BoardApplication.class);
context.refresh();
long memberId = context.getBean(BoardApplication.class)
.registerAndAuthenticate(new SignUpCommand(
"[email protected]",
"안동민",
"secret-1234"));
System.out.println("memberId=" + memberId);
}
}
}getBean은 이 예제의 시작 경계에서 완성된 애플리케이션 객체를 한 번 얻는 데만 사용합니다.
서비스가 실행 중 컨테이너를 다시 조회하지 않으므로 업무 코드의 의존성은 생성자에 그대로 드러납니다.
하나로 결정되지 않으면 refresh에서 실패
단일 값 주입 지점은 타입과 선택 규칙을 적용한 결과 후보 하나가 결정될 때만 완성됩니다.
이 예제처럼 MemberRepository 후보가 하나면 주입에 성공합니다.
후보가 0개이면 가장 깊은 원인은 NoSuchBeanDefinitionException이고, 선택 기준 없이 후보가 복수이면 NoUniqueBeanDefinitionException입니다.
상위 예외나 상세 메시지 문자열은 구현 문맥에 따라 달라질 수 있으므로 테스트는 refresh() 실패의 root exception 타입만 검사합니다.
package board;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import board.member.MemberRegistrationService;
import board.member.MemberRepository;
import board.member.MemoryMemberRepository;
import board.member.PasswordHasher;
import board.member.SignUpCommand;
import board.member.TestPasswordHasher;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
final class BoardConfigTest {
@Test
void 단일_생성자에_서비스를_주입해_같은_저장_상태를_사용한다() {
try (var context = contextWith(
BoardConfig.class, BoardApplication.class)) {
long memberId = context.getBean(BoardApplication.class)
.registerAndAuthenticate(new SignUpCommand(
"[email protected]",
"회원",
"secret-1234"));
assertThat(memberId).isPositive();
assertThat(context.getBean(MemberRepository.class))
.isSameAs(context.getBean(MemberRepository.class));
}
}
@Test
void 기본_singleton은_컨텍스트별_빈_정의에_한정된다() {
try (var first = contextWith(BoardConfig.class);
var second = contextWith(BoardConfig.class)) {
assertThat(first.getBean(MemberRepository.class))
.isNotSameAs(second.getBean(MemberRepository.class));
}
}
@Test
void 저장소_후보가_없으면_refresh가_실패한다() {
try (var context =
new AnnotationConfigApplicationContext()) {
context.register(MissingRepositoryConfig.class);
assertThatThrownBy(context::refresh)
.hasRootCauseExactlyInstanceOf(
NoSuchBeanDefinitionException.class);
}
}
@Test
void 저장소_후보가_복수이고_선택_기준이_없으면_refresh가_실패한다() {
try (var context =
new AnnotationConfigApplicationContext()) {
context.register(AmbiguousRepositoryConfig.class);
assertThatThrownBy(context::refresh)
.hasRootCauseExactlyInstanceOf(
NoUniqueBeanDefinitionException.class);
}
}
private static AnnotationConfigApplicationContext contextWith(
Class<?>... componentClasses
) {
var context = new AnnotationConfigApplicationContext();
context.register(componentClasses);
context.refresh();
return context;
}
@Configuration(proxyBeanMethods = false)
static class MissingRepositoryConfig {
@Bean
PasswordHasher passwordHasher() {
return new TestPasswordHasher();
}
@Bean
MemberRegistrationService memberRegistrationService(
MemberRepository missingRepository,
PasswordHasher passwordHasher
) {
return new MemberRegistrationService(
missingRepository, passwordHasher);
}
}
@Configuration(proxyBeanMethods = false)
static class AmbiguousRepositoryConfig {
@Bean
MemberRepository firstRepository() {
return new MemoryMemberRepository();
}
@Bean
MemberRepository secondRepository() {
return new MemoryMemberRepository();
}
@Bean
PasswordHasher passwordHasher() {
return new TestPasswordHasher();
}
@Bean
MemberRegistrationService memberRegistrationService(
MemberRepository ambiguousRepository,
PasswordHasher passwordHasher
) {
return new MemberRegistrationService(
ambiguousRepository, passwordHasher);
}
}
}테스트 대역은 실패 원인을 저장소 후보 수로 통제하기 위해 test-source 구성 안에서만 사용합니다.
0개 구성은 저장소만 빠뜨리고, 복수 구성은 이름이 다른 저장소 두 개를 등록하되 매개변수 이름과 일치하는 후보를 만들지 않습니다.
CANDIDATE RESOLUTION · CONTEXT BOUNDARY
단일 주입점은 후보 하나가 결정될 때만 완성된다
후보 결정과 객체 공유는 서로 다른 질문입니다. 먼저 단일 값 주입점에 넣을 후보를 하나로 결정하고, 그다음 기본 singleton 빈은 해당 컨텍스트와 빈 정의 안에서 공유됩니다.
SINGLE-VALUED INJECTION POINT
MemberRepository 하나를 요구하는 매개변수의 세 결과
| 결정 가능한 후보 | 예제 구성 | 결과 | root exception |
|---|---|---|---|
| 0개 | 저장소 빈 누락 | 의존성을 채울 수 없어 refresh 실패 | NoSuchBeanDefinitionException |
| 1개 | memberRepository() |
R#1을 서비스 생성자에 주입 | 없음 |
| 복수 | 선택 기준 없는 R#1·R#2 | 하나를 고를 수 없어 refresh 실패 | NoUniqueBeanDefinitionException |
refresh()에서 확인하는 후보 결정 결과
0개
예제 구성: 저장소 빈 누락
결과: 의존성을 채울 수 없어 refresh 실패
root exception: NoSuchBeanDefinitionException
1개
예제 구성: memberRepository()
결과: R#1을 서비스 생성자에 주입
root exception: 없음
복수
예제 구성: 선택 기준 없는 R#1·R#2
결과: 하나를 고를 수 없어 refresh 실패
root exception: NoUniqueBeanDefinitionException
테스트는 변할 수 있는 상세 메시지가 아니라 가장 깊은 예외의 정확한 타입을 검사합니다.
CONTEXT A · BEAN DEFINITION R
같은 컨텍스트
기본 singleton인 저장소 빈 R을 두 번 요청하면 Context A 안에서는 같은 R#A 참조를 얻습니다.
CONTEXT B · SAME CONFIG CLASS
다른 컨텍스트
같은 BoardConfig 클래스를 다시 등록해도 Context B는 별도의 R#B를 만듭니다. R#A와 R#B는 전역 객체가 아닙니다.
| 질문 | 증거 | 확인되는 범위 | 확인되지 않는 범위 |
|---|---|---|---|
| 후보가 결정되는가 | 0·1·복수 구성의 refresh() |
단일 값 주입점의 성공과 root exception | 예외 메시지 문자열의 안정성 |
| 저장소가 공유되는가 | 같은 컨텍스트에서 R을 두 번 조회 | 컨테이너별·빈 정의별 기본 singleton | JVM·프로세스 전역 singleton |
| 해시 빈이 같은가 | 현재 그래프의 한 빈 정의 | 이 컨텍스트가 선택한 주입 참조 | 포트의 identity 요구·저장 형식 호환 |
| 운영 성질이 생기는가 | 이 장에는 별도 증거 없음 | 명시적 생성과 연결만 확인 | thread safety·transaction·persistence |
각 결론에 필요한 증거와 이 장의 한계
후보가 결정되는가
증거: 0·1·복수 구성의 refresh()
확인되는 범위: 단일 값 주입점의 성공과 root exception
확인되지 않는 범위: 예외 메시지 문자열의 안정성
저장소가 공유되는가
증거: 같은 컨텍스트에서 R을 두 번 조회
확인되는 범위: 컨테이너별·빈 정의별 기본 singleton
확인되지 않는 범위: JVM·프로세스 전역 singleton
해시 빈이 같은가
증거: 현재 그래프의 한 빈 정의
확인되는 범위: 이 컨텍스트가 선택한 주입 참조
확인되지 않는 범위: 포트의 identity 요구·저장 형식 호환
운영 성질이 생기는가
증거: 이 장에는 별도 증거 없음
확인되는 범위: 명시적 생성과 연결만 확인
확인되지 않는 범위: thread safety·transaction·persistence
prototype·lifecycle callback·반복 lookup과 다른 선택 규칙의 상세는 후속 문서의 별도 주제입니다.
기본 singleton이 보장하는 정확한 범위
| 관찰 | 이 문서에서 말할 수 있는 것 | 여기서 말하지 않는 것 |
|---|---|---|
| 같은 컨텍스트에서 같은 빈 정의를 두 번 조회 | 같은 MemberRepository 인스턴스 반환 | JVM·프로세스 전체의 singleton |
| 서로 다른 두 컨텍스트에서 같은 구성 등록 | 서로 다른 저장소 인스턴스 | 컨텍스트 사이의 회원 상태 공유 |
| 두 서비스에 같은 저장소 빈 주입 | 순차 메모리 예제에서 가입 상태를 로그인에서 조회 | 저장소의 thread safety·transaction·persistence |
| 같은 해시 빈 주입 | 현재 컨텍스트 그래프의 구현 선택 | 모든 해시 구현의 객체 identity 요구·저장 형식 호환 |
| 단일 생성자 주입 | 생성 시 필요한 서비스 참조가 완성 | 서비스 자체의 동시 실행 안전성 |
Spring의 기본 singleton은 컨테이너별·빈 정의별 공유입니다.
@Bean을 붙였다는 사실만으로 스레드 안전성, 트랜잭션, 영속성, 기존 해시 migration이 생기지 않습니다.
이 문서는 Spring Framework의 명시적 Java 구성만 다루며 Spring Boot 자동 설정을 사용하거나 설명하지 않습니다.
prototype과 다른 스코프, lifecycle callback, 반복 lookup의 의미는 다음 문서 이후에 별도로 다룹니다.
예시
proxyBeanMethods = false인 현재 구성에서 컨텍스트가 관리하는 저장소 빈을 두 번 getBean하면 같은 참조입니다. 반면 new BoardConfig()로 별도 일반 객체를 만든 뒤 그 객체의 memberRepository()를 직접 두 번 호출하면, 메서드 안의 new MemoryMemberRepository()가 매번 실행되어 두 저장소가 생깁니다. 애노테이션이 평범한 Java 메서드 호출까지 전역적으로 바꾸는 것은 아닙니다. 공식 Java 구성 설명도 매개변수 주입과 이런 직접 팩터리 호출의 차이를 구분합니다.
시작 실패와 상태 분리를 따로 판단하기
① MissingRepositoryConfig만 등록하고 refresh, ② AmbiguousRepositoryConfig만 등록하고 refresh, ③ 정상 BoardConfig로 새 컨텍스트 A·B를 각각 시작해 A에서만 가입한 이메일로 B에서 로그인하기를 독립적으로 실행한다고 가정합니다. 예제의 단일 값 매개변수 이름·타입과 선택 기준은 그대로 두며, A·B 호출은 순차입니다. 각각 컨텍스트 시작 단계와 업무 실행 단계 중 어디에서 끝나는지 먼저 구분하세요.
후보 결정과 컨텍스트별 상태 풀이
①은 필요한 저장소 후보가 없으므로 refresh에서 실패하고 root 원인은 NoSuchBeanDefinitionException입니다. ②는 선택 기준 없는 두 후보 중 하나를 결정하지 못해 refresh에서 실패하고 root 원인은 NoUniqueBeanDefinitionException입니다. 두 경우 모두 가입 업무까지 실행되지 않습니다.
③은 각각 후보가 하나인 정상 컨텍스트이므로 시작에 성공합니다. 그러나 B의 별도 저장소에는 A의 회원이 없어 업무 실행 중 InvalidCredentialsException입니다. 후보 선택 성공은 컨텍스트 사이의 회원 상태 공유와 다른 조건입니다. try-with-resources 밖에서는 닫힌 컨텍스트를 새 조회에 사용하지 않도록 실습 범위를 지킵니다.