본문 바로가기

빈 정의와 조회

빈 정의·이름·타입·인스턴스를 분리하고 이름 조회, 타입 복수 조회, 단일 타입 조회의 결과와 사용 경계를 완전한 Spring 테스트로 검증합니다.

앞 글에서는 애플리케이션 컨텍스트가 빈 조회 기능을 제공하더라도 업무 객체에는 필요한 역할만 생성자로 주입해야 한다고 정리했습니다. 이번 글에서는 컨테이너를 구성하거나 테스트할 때 만나는 빈 정의, 빈 이름, 빈 타입, 빈 인스턴스를 서로 다른 개념으로 구분합니다. 모든 빈이 "빈 정의에서 인스턴스로"라는 한 줄의 단계를 거친다고 일반화하지도 않습니다.

앞 글과 같은 학습 프로젝트의 src/test/java/board/container에 테스트를 추가합니다. 비밀번호 해시 역할에서 만든 PasswordHasher, PBKDF2 구현과 설정, 테스트 소스의 TestPasswordHasher가 먼저 있어야 합니다. 이 글의 테스트는 안쪽에 정의한 HasherConfig만 등록하므로 전체 BoardConfig나 Boot 시작 클래스는 함께 띄우지 않습니다.


정의·이름·타입·인스턴스를 분리해서 읽기

BeanDefinition은 컨테이너가 객체를 만들고 구성할 때 쓰는 메타데이터이며, 실제로 메서드를 호출하는 대상 객체가 아닙니다. 이 글의 테스트 구성처럼 @Bean 메서드를 등록하면 컨테이너는 그 생성 규칙을 빈 정의로 처리합니다. 반면 registerSingleton()은 이미 만들어진 객체를 이름과 함께 싱글톤 저장소에 바로 등록할 수 있어서, 그 빈에는 대응하는 빈 정의가 없을 수 있습니다. 그러니 모든 빈을 정의에서 시작해 초기화된 싱글톤으로 끝나는 한 줄의 생명 주기로 그리면 안 됩니다.

빈 이름은 컨테이너 안의 식별자이고, 빈 타입은 조회할 때 요구하는 형식이며, 빈 인스턴스는 실제로 메서드가 호출되는 객체입니다. 클래스의 수, 빈 정의의 수, 빈 이름의 수, 인스턴스의 수가 서로 같다고 가정하지 않습니다.

빈 정의, 빈 이름, 빈 타입, 빈 인스턴스의 역할을 분리하고, 기존 객체를 registerSingleton으로 등록하면 빈 정의가 없을 수 있으므로 모든 빈을 하나의 생성 선형 단계로 일반화할 수 없음을 설명합니다.

CONTAINER VOCABULARY · NON-LINEAR REGISTRATION

빈 정의·이름·타입·인스턴스는 서로 다른 컨테이너 계약이다

메타데이터, 식별자, 조회 계약, 호출 대상을 한 단어로 뭉치지 않습니다. 한 클래스·한 정의·한 이름·한 인스턴스라고 가정하면 등록 방식과 조회 결과를 잘못 설명하게 됩니다.

생성 규칙 등록과 기존 객체 등록은 서로 다른 경로@Bean의 생성 규칙은 빈 정의로 처리되고 컨테이너의 생성·구성을 통해 호출 대상 빈을 얻는다. registerSingleton은 이미 만들어진 객체를 이름과 함께 등록하므로 대응하는 빈 정의 없이 그 객체에 접근할 수 있다. 두 경로를 독립적으로 비교하며 모든 빈에 하나의 lifecycle을 강제하지 않는다.정의 기반 경로이미 만든 객체의 등록 경로@Bean 생성 규칙빈 정의로 처리컨테이너생성·구성정의가 규칙을 표현호출 대상 빈실제 객체 참조이미 만든 객체호출자가 생성registerSingleton이름과 객체 등록registry에 연결등록된 객체빈 정의가 없을 수 있음
생성 규칙 등록과 기존 객체 등록은 서로 다른 경로@Bean의 생성 규칙은 빈 정의로 처리되고 컨테이너의 생성·구성을 통해 호출 대상 빈을 얻는다. registerSingleton은 이미 만들어진 객체를 이름과 함께 등록하므로 대응하는 빈 정의 없이 그 객체에 접근할 수 있다. 두 경로를 독립적으로 비교하며 모든 빈에 하나의 lifecycle을 강제하지 않는다.정의 기반 경로기존 객체 등록 경로@Bean 생성 규칙빈 정의로 처리컨테이너생성·구성정의가 규칙을 표현호출 대상 빈실제 객체 참조이미 만든 객체호출자가 생성registerSingleton이름과 객체 등록registry에 연결등록된 객체빈 정의가없을 수 있음

위·아래 경로는 독립 비교입니다. 정의는 생성 규칙이고, 빈 인스턴스는 실제 호출 대상 객체입니다.

IMPORTANT EXCEPTION

모든 bean이 BeanDefinition에서 시작하지는 않는다

@Bean 경로에는 생성 규칙을 표현하는 정의가 있지만, registerSingleton()은 이미 만들어진 객체를 registry에 직접 넣을 수 있어 대응하는 빈 정의가 없을 수 있습니다.

따라서 정의 → 생성 → 초기화된 singleton이라는 한 줄을 모든 등록 방식의 공통 lifecycle로 사용하지 않습니다.

컨테이너에서 구분해야 하는 네 계약과 현재 예제의 증거
구분 현재 예제 의미 동일시하지 않는 것
BeanDefinition @Bean의 생성·구성 메타데이터 컨테이너가 객체를 만들고 구성하는 규칙 registerSingleton() 객체와 실제 인스턴스 자체
빈 이름 testPasswordHasher 컨테이너 내부 식별자 HTTP 파라미터·업무 선택 키
빈 타입 PasswordHasher 조회에 필요한 할당 가능 계약 후보가 반드시 하나라는 보장
빈 인스턴스 TestPasswordHasher 또는 Pbkdf2PasswordHasher 객체 실제 메서드 호출 대상 클래스·정의·이름의 개수

컨테이너에서 구분해야 하는 네 계약과 현재 예제의 증거

BeanDefinition

현재 예제

@Bean의 생성·구성 메타데이터

의미

컨테이너가 객체를 만들고 구성하는 규칙

동일시하지 않는 것

registerSingleton() 객체와 실제 인스턴스 자체

빈 이름

현재 예제

testPasswordHasher

의미

컨테이너 내부 식별자

동일시하지 않는 것

HTTP 파라미터·업무 선택 키

빈 타입

현재 예제

PasswordHasher

의미

조회에 필요한 할당 가능 계약

동일시하지 않는 것

후보가 반드시 하나라는 보장

빈 인스턴스

현재 예제

TestPasswordHasher 또는 Pbkdf2PasswordHasher 객체

의미

실제 메서드 호출 대상

동일시하지 않는 것

클래스·정의·이름의 개수

클래스 수, 정의 수, 이름 수, 인스턴스 수는 같은 값이 아닙니다. singleton identity, lifecycle callback, 다른 scope의 상세는 각각 뒤 문서의 별도 증거로 남깁니다.

그림의 위쪽은 두 등록 경로를 나란히 비교합니다. @Bean의 생성 규칙은 빈 정의로 처리되어 컨테이너가 객체를 만들고 구성하지만, registerSingleton()은 호출하는 쪽이 이미 만든 객체를 이름과 함께 등록하므로 빈 정의 없이도 그 객체에 접근할 수 있습니다. 아래쪽 표는 네 개념이 지금 예제에서 각각 무엇에 해당하는지와, 서로 같은 것으로 보면 안 되는 것을 정리합니다. 예를 들어 빈 이름 testPasswordHasher는 컨테이너 안의 식별자일 뿐, HTTP 파라미터나 업무에서 쓰는 선택 키가 아닙니다.


두 구현을 한 테스트에서 조회

다음 파일은 테스트 소스 안에서만 두 PasswordHasher 구현을 같은 컨텍스트에 등록합니다. TestPasswordHasher는 결과가 항상 같은 테스트 대역이며 실제 실행 구성의 기본 구현으로 쓰지 않습니다. @Bean에 이름을 따로 주지 않았고 별도의 이름 생성기도 설정하지 않았으므로, 이 구성에서는 메서드 이름이 기본 빈 이름이 됩니다.

board-core/src/test/java/board/container/PasswordHasherLookupTest.java
package board.container;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import board.member.PasswordHasher;
import board.member.PasswordHashSettings;
import board.member.Pbkdf2PasswordHasher;
import board.member.TestPasswordHasher;
import java.util.Map;
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 PasswordHasherLookupTest {
    @Test
    void 이름_복수_타입_단일_타입_조회의_결과를_구분한다() {
        try (var context =
                     new AnnotationConfigApplicationContext(
                             HasherConfig.class)) {
            PasswordHasher byName = context.getBean(
                    "testPasswordHasher",
                    PasswordHasher.class);
            Map<String, PasswordHasher> all =
                    context.getBeansOfType(PasswordHasher.class);

            assertThat(byName)
                    .isInstanceOf(TestPasswordHasher.class);
            assertThat(all).hasSize(2);
            assertThat(all.keySet())
                    .containsExactlyInAnyOrder(
                            "testPasswordHasher",
                            "pbkdf2PasswordHasher");
            assertThat(all.get("testPasswordHasher"))
                    .isSameAs(byName);
            assertThat(all.get("pbkdf2PasswordHasher"))
                    .isInstanceOf(Pbkdf2PasswordHasher.class);

            assertThatThrownBy(() ->
                    context.getBean(PasswordHasher.class))
                    .isExactlyInstanceOf(
                            NoUniqueBeanDefinitionException.class);
            assertThatThrownBy(() ->
                    context.getBean(
                            "missingPasswordHasher",
                            PasswordHasher.class))
                    .isExactlyInstanceOf(
                            NoSuchBeanDefinitionException.class);
        }
    }

    @Configuration(proxyBeanMethods = false)
    static class HasherConfig {
        @Bean
        PasswordHasher testPasswordHasher() {
            return new TestPasswordHasher();
        }

        @Bean
        PasswordHasher pbkdf2PasswordHasher() {
            return new Pbkdf2PasswordHasher(
                    PasswordHashSettings.productionDefaults());
        }
    }
}
조회 테스트 실행
./gradlew test --tests board.container.PasswordHasherLookupTest

모든 조회와 예외 검사는 하나의 try-with-resources 안에서 일어납니다. 맵은 순서를 고정하지 않고 두 이름이 있는지와 전체 크기만 확인합니다. 앞의 이름 조회가 성공했다고 해서 두 후보 중 하나가 컨텍스트에서 빠지는 것은 아니므로, 뒤의 단일 타입 조회는 여전히 두 후보 사이에서 실패합니다. 예외 검사가 예외를 받아 확인하므로, 이 두 실패가 예상대로 일어나면 테스트 전체는 통과합니다.


이름·복수 타입·단일 타입 조회의 결과

getBean("testPasswordHasher", PasswordHasher.class)는 지정한 이름의 빈을 찾고, 그 빈이 요청한 타입에 대입될 수 있는지도 함께 확인합니다. 없는 이름인 missingPasswordHasher를 조회하면 NoSuchBeanDefinitionException으로 끝납니다.

getBeansOfType(PasswordHasher.class)는 현재 팩토리에서 타입이 맞는 빈 인스턴스들을 "이름에서 인스턴스로"의 맵으로 돌려줍니다. 이 조회는 빈 정의 없이 수동으로 등록한 싱글톤도 포함하지만, 부모 팩토리의 빈은 포함하지 않습니다. 조상 팩토리까지 합쳐서 찾아야 한다면 BeanFactoryUtils의 별도 메서드를 써야 합니다.

getBean(PasswordHasher.class)는 객체 하나를 요구합니다. 위 구성에는 고를 기준이 없는 후보가 둘 있으므로 NoUniqueBeanDefinitionException으로 실패하며, 컨테이너가 아무것이나 하나를 고르지 않습니다. @Qualifier는 주입 지점의 후보를 좁히는 메타데이터이지, 이런 getBean(Class) 호출에 문자열 선택자를 넘기는 API가 아닙니다. 선택 규칙은 의존성 후보 선택에서 자세히 다룹니다.

이름과 타입을 함께 지정하는 조회, 현재 factory의 복수 타입 조회, 선택 메타데이터 없는 두 후보의 단일 타입 조회, 누락된 이름 조회의 결과를 비교하고 컨테이너 경계 조회와 업무 생성자 주입을 구분합니다.

LOOKUP MATRIX · INJECTION BOUNDARY

이름·복수 타입·단일 타입 조회는 서로 다른 결과와 경계를 가진다

같은 두 bean을 보더라도 호출 형태가 요구하는 결과는 다릅니다. 목록 조회는 이름→인스턴스 집합을 반환하고, 단일 조회는 현재 선택 규칙으로 한 후보가 결정되어야 합니다.

같은 두 후보에 대한 네 조회와 결과testPasswordHasher와 pbkdf2PasswordHasher 두 후보가 있고 선택 메타데이터는 없는 같은 구성을 비교한다. 이름과 타입 조회는 TestPasswordHasher, 복수 타입 조회는 두 이름과 인스턴스의 맵, 단일 타입 조회는 NoUniqueBeanDefinitionException, 누락된 이름 조회는 NoSuchBeanDefinitionException이다. 각 선은 독립 호출과 그 결과를 연결하며 조회가 다음 조회를 실행하거나 후보를 제거하지 않는다.
이름 + 타입testPasswordHasher 지정
연결 관계이름 + 타입 · testPasswordHasher 지정 → TestPasswordHasher · 지정한 이름의 객체
TestPasswordHasher지정한 이름의 객체
복수 타입PasswordHasher 목록
연결 관계복수 타입 · PasswordHasher 목록 → 두 이름 → 인스턴스 · 맵 크기 2 · 순서 미고정
두 이름 → 인스턴스맵 크기 2 · 순서 미고정
단일 타입PasswordHasher 하나 요구
연결 관계단일 타입 · PasswordHasher 하나 요구 → NoUniqueBean · DefinitionException · 선택되지 않은 두 후보
NoUniqueBeanDefinitionException선택되지 않은 두 후보
누락 이름missingPasswordHasher
연결 관계누락 이름 · missingPasswordHasher → NoSuchBean · DefinitionException · 그 이름의 빈이 없음
NoSuchBeanDefinitionException그 이름의 빈이 없음

모든 선은 같은 구성에서 호출 → 결과의 독립 대응입니다.

같은 두 후보에 대한 네 조회와 결과testPasswordHasher와 pbkdf2PasswordHasher 두 후보가 있고 선택 메타데이터는 없는 같은 구성을 비교한다. 이름과 타입 조회는 TestPasswordHasher, 복수 타입 조회는 두 이름과 인스턴스의 맵, 단일 타입 조회는 NoUniqueBeanDefinitionException, 누락된 이름 조회는 NoSuchBeanDefinitionException이다. 각 선은 독립 호출과 그 결과를 연결하며 조회가 다음 조회를 실행하거나 후보를 제거하지 않는다.이름 + 타입현재 조회testPasswordHasher 지정TestPasswordHasher지정한 이름의 객체복수 타입현재 조회PasswordHasher 목록두 이름 → 인스턴스맵 크기 2 · 순서 미고정단일 타입현재 조회PasswordHasher하나 요구NoUniqueBeanDefinitionException선택되지 않은 두 후보누락 이름현재 조회missingPasswordHasher 이름NoSuchBeanDefinitionException그 이름의 빈이 없음

같은 두 후보의 구성에서 네 조회를 독립적으로 비교합니다. 이름 조회에 성공해도 나머지 후보가 제거되는 것은 아닙니다.

EXACT TEST CONFIGURATION

두 이름·두 구현·선택 메타데이터 없음

testPasswordHasher와 pbkdf2PasswordHasher가 등록된 현재 factory의 조회 결과
조회 결과·범위·경계
이름 + 타입

현재 결과

getBean("testPasswordHasher", PasswordHasher.class) → TestPasswordHasher

조회 범위

지정 이름을 찾고 요청 타입을 확인

경계

bean name은 컨테이너 식별자

복수 타입

현재 결과

getBeansOfType(PasswordHasher.class) → Map<String, PasswordHasher>

조회 범위

현재 factory의 두 이름→인스턴스, 수동 singleton도 확인

경계

ancestor factory는 포함하지 않음

단일 타입

현재 결과

getBean(PasswordHasher.class) → 실패

조회 범위

현재 구성의 선택되지 않은 두 후보

경계

NoUniqueBeanDefinitionException

누락 이름

현재 결과

getBean("missingPasswordHasher", PasswordHasher.class) → 실패

조회 범위

해당 이름 없음

경계

NoSuchBeanDefinitionException

testPasswordHasher와 pbkdf2PasswordHasher가 등록된 현재 factory의 조회 결과

이름 + 타입

결과·범위·경계

현재 결과

getBean("testPasswordHasher", PasswordHasher.class) → TestPasswordHasher

조회 범위

지정 이름을 찾고 요청 타입을 확인

경계

bean name은 컨테이너 식별자

복수 타입

결과·범위·경계

현재 결과

getBeansOfType(PasswordHasher.class) → Map<String, PasswordHasher>

조회 범위

현재 factory의 두 이름→인스턴스, 수동 singleton도 확인

경계

ancestor factory는 포함하지 않음

단일 타입

결과·범위·경계

현재 결과

getBean(PasswordHasher.class) → 실패

조회 범위

현재 구성의 선택되지 않은 두 후보

경계

NoUniqueBeanDefinitionException

누락 이름

결과·범위·경계

현재 결과

getBean("missingPasswordHasher", PasswordHasher.class) → 실패

조회 범위

해당 이름 없음

경계

NoSuchBeanDefinitionException

테스트는 Map 순서나 변할 수 있는 예외 message가 아니라 key 집합·크기와 정확한 예외 타입을 확인합니다.

STARTUP · TEST · DIAGNOSTICS

컨테이너 경계에서 구성을 확인한다

시작 코드와 통합 테스트, 제한된 진단 코드는 이름·타입·후보 집합을 조회해 등록 결과를 검증할 수 있습니다.

Map<String, PasswordHasher>의 key는 bean name이며 HTTP나 업무 키가 아닙니다.

BUSINESS OBJECT · CONSTRUCTOR

업무 객체에는 역할을 주입한다

서비스는 ApplicationContext를 조회하는 service locator가 아니라 필요한 PasswordHasher를 생성자로 받습니다.

외부 선택은 PasswordAlgorithm.PBKDF2 같은 enum과 명시적 allowlist mapping으로 bean name과 분리합니다.

이 표는 현재 factory와 선택 메타데이터 없는 정확한 테스트 구성만 설명합니다. 후보 선택 annotation의 상세는 ch3-7, lifecycle과 scope의 상세는 뒤 문서에서 다룹니다.

그림의 위쪽은 같은 두 후보에 대한 네 가지 조회와 그 결과를 하나씩 이어 보여 줍니다. 각 선은 서로 독립된 호출이며, 한 조회가 다음 조회를 실행하거나 후보를 없애지 않습니다. 가운데 표는 조회마다 결과와 범위를 정리하고, 테스트가 맵의 순서나 바뀔 수 있는 예외 메시지 대신 키 집합과 크기, 정확한 예외 타입을 확인한다는 점을 짚습니다. 아래쪽 두 상자는 다음 절의 내용입니다.


빈 이름과 업무 선택 키를 분리

getBeansOfType이 돌려준 Map<String, PasswordHasher>의 키는 빈 이름입니다. 이 값을 HTTP의 algorithm 파라미터나 저장된 업무 데이터에 그대로 쓰면, 구성에서 빈 이름을 바꾸는 것만으로 바깥에 약속한 값이 바뀌어 버립니다. 바깥의 입력이 구현 선택에 영향을 준다면 PasswordAlgorithm.PBKDF2 같은 별도의 열거형과 명시적인 허용 목록(allowlist) 매핑을 두고, 알 수 없는 값은 컨테이너를 조회하기 전에 거부합니다.

시작 코드, 구성 검증, 통합 테스트, 범위를 제한한 진단 코드는 컨테이너 쪽에서 조회 API를 써도 됩니다. 하지만 업무 서비스는 ApplicationContext를 받아 실행 중에 getBean을 되풀이하는 서비스 로케이터가 아니라, 필요한 PasswordHasher 역할을 생성자로 받아야 합니다.


이 글이 확인하지 않는 것

이 테스트는 두 이름과 두 타입 후보로 이루어진 지금의 구성만 확인합니다. 싱글톤이 같은 객체인지는 싱글톤과 공유 상태에서, 후보 선택 메타데이터는 의존성 후보 선택에서, 생명 주기 콜백은 빈 생명 주기 콜백에서, 다른 스코프는 프로토타입과 요청 스코프에서 따로 다룹니다. 이 글만으로는 객체의 수명이나 종료 콜백, 스레드 안전성, 트랜잭션, 영속화, 해시 저장 형식의 호환을 보장하지 않습니다.


정리

  • 빈 정의는 생성 규칙을 담은 메타데이터이고, 빈 이름은 식별자, 빈 타입은 조회에 쓰는 형식, 빈 인스턴스는 실제 호출 대상입니다.
  • registerSingleton()으로 등록한 빈처럼 빈 정의가 없는 빈도 있으므로, 모든 빈이 같은 생성 단계를 거친다고 보지 않습니다.
  • 이름과 타입으로 조회하면 그 이름의 빈을, getBeansOfType은 현재 팩토리에서 타입이 맞는 모든 빈을 이름과 함께 돌려줍니다.
  • 고를 기준이 없는 후보가 여럿이면 getBean(Class)는 NoUniqueBeanDefinitionException으로 실패하며, 컨테이너가 임의로 고르지 않습니다.
  • 빈 이름은 컨테이너의 식별자이므로, 바깥에서 들어오는 선택 값은 별도의 열거형과 허용 목록으로 분리합니다.

연습 문제

1. 후보를 하나로 줄이면

테스트 안의 구성에서 두 @Bean 중 하나를 지웠을 때, 이름 조회와 복수 타입 조회, 단일 타입 조회의 결과가 각각 어떻게 달라지는지 먼저 예측한 뒤 확인하세요. 원래의 HasherConfig와 테스트 파일은 그대로 두고, 후보가 하나인 새 구성을 별도의 테스트에 만듭니다. 두 후보를 기대하는 원래 테스트의 hasSize(2)는 후보를 지우면 실패하는 것이 당연하므로, 바뀐 구성에 맞는 기대값을 새로 세웁니다. 빈 이름을 바깥의 algorithm 값으로 쓰지 않고 열거형과 허용 목록 매핑을 두는 이유도 함께 설명하세요.

해설 보기
지운 빈 메서드testPasswordHasher 이름 조회복수 타입 조회의 키단일 타입 조회
pbkdf2PasswordHasherTestPasswordHasher 반환testPasswordHasher 하나TestPasswordHasher 반환
testPasswordHasherNoSuchBeanDefinitionExceptionpbkdf2PasswordHasher 하나Pbkdf2PasswordHasher 반환

어느 경우에도 missingPasswordHasher라는 이름은 생기지 않으므로 그 이름의 조회는 계속 실패합니다. 다음 테스트는 표의 첫 번째 행을 확인합니다.

board-core/src/test/java/board/container/SingleHasherLookupExerciseTest.java
package board.container;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import board.member.PasswordHasher;
import board.member.TestPasswordHasher;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

final class SingleHasherLookupExerciseTest {
    @Configuration(proxyBeanMethods = false)
    static class SingleHasherConfig {
        @Bean
        PasswordHasher testPasswordHasher() {
            return new TestPasswordHasher();
        }
    }

    @Test
    void 하나인_후보의_이름_목록_단일_조회가_같은_객체를_가리킨다() {
        try (var context = new AnnotationConfigApplicationContext(
                SingleHasherConfig.class)) {
            var byName = context.getBean(
                    "testPasswordHasher", PasswordHasher.class);
            var all = context.getBeansOfType(PasswordHasher.class);
            var single = context.getBean(PasswordHasher.class);

            assertThat(all.keySet()).containsExactly("testPasswordHasher");
            assertThat(all.get("testPasswordHasher")).isSameAs(byName);
            assertThat(single).isSameAs(byName);
            assertThatThrownBy(() -> context.getBean(
                    "missingPasswordHasher", PasswordHasher.class))
                    .isExactlyInstanceOf(NoSuchBeanDefinitionException.class);
        }
    }
}

./gradlew test --tests board.container.SingleHasherLookupExerciseTest로 실행합니다. 두 번째 행도 확인하려면 원래 구성의 PBKDF2 메서드만 새 구성에 남기고, 표에 맞추어 이름 조회의 예외와 맵의 키, 돌려받는 구현을 바꿉니다.

바깥의 algorithm 값은 사용자와 약속한 업무 값이고, 빈 이름은 구성의 식별자입니다. 둘을 나누어 두면 빈 메서드의 이름을 바꾸어도 허용한 알고리즘 값은 그대로 유지할 수 있습니다. 다만 열거형에 값을 추가한다고 그 구현이 저절로 등록되지는 않으므로, 허용 목록 매핑과 구성 검증도 함께 있어야 합니다.

다음 글 싱글톤과 공유 상태에서는 컨테이너가 싱글톤 빈 하나를 여러 요청에 함께 쓰게 할 때, 그 빈에 상태를 두면 어떤 문제가 생기는지 살펴봅니다.