본문으로 건너뛰기

안동민 개발노트

본문 시작

Spring 컨테이너 전환

순수 Java BoardConfig와 같은 회원가입 객체 그래프를 Spring 빈으로 등록하고 타입 조회·생성자 주입·모호성 실패를 확인합니다.

Spring 컨테이너는 우리가 직접 작성한 BoardConfig와 같은 객체 조립을 일관되게 수행합니다.

은 컨테이너가 만들고 관리하는 객체이고, @Bean은 메서드 반환 객체를 빈으로 등록하라는 표시입니다.

Spring 컨테이너가 구성 클래스를 빈 정의로 바꾼다

AnnotationConfigApplicationContext는 설정을 읽어 빈을 생성하고 연결한 뒤 이름과 타입으로 보관한다.

  1. @Configuration

    구성 클래스 등록

  2. @Bean 메서드

    빈 정의와 의존관계 해석

  3. ApplicationContext

    인스턴스 생성·연결·보관

  4. getBean

    름 또는 타입으로 조회


빈 정의

BoardConfig.java
@Configuration(proxyBeanMethods = false)
public class BoardConfig {
    @Bean
    MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    PasswordHasher passwordHasher() {
        return new TestPasswordHasher();
    }

    @Bean
    MemberRegistrationService memberRegistrationService(
            MemberRepository members,
            PasswordHasher passwordHasher
    ) {
        return new DefaultMemberRegistrationService(
                members, passwordHasher);
    }

    @Bean
    LoginService loginService(
            MemberRepository members,
            PasswordHasher passwordHasher
    ) {
        return new LoginService(members, passwordHasher);
    }
}

@Bean 메서드의 매개변수는 컨테이너가 타입으로 찾습니다.

두 서비스의 MemberRepository 매개변수에는 같은 싱글톤 빈이 전달됩니다.

빈 이름과 타입은 서로 다른 조회 계약이다

같은 타입의 빈이 둘이면 타입 단독 조회가 모호해지므로 선택 기준이 필요하다.

조회성공 조건실패 예
이름정확한 빈 이름 존재NoSuchBeanDefinition
타입후보가 정확히 하나NoUniqueBeanDefinition
이름+타입둘 다 일치타입 불일치

컨테이너 실행

try (var context = new AnnotationConfigApplicationContext(
        BoardConfig.class)) {
    var registration = context.getBean(
            MemberRegistrationService.class);
    var login = context.getBean(LoginService.class);

    Member member = registration.register(new SignUpCommand(
            "member@example.com", "회원", "secret-1234"));
    long loginId = login.authenticate(
            "member@example.com", "secret-1234");

    System.out.println(member.id() == loginId);
}

getBean은 학습과 기반 코드에서 컨테이너를 관찰하기 위한 API입니다.

일반 컨트롤러는 컨테이너를 직접 조회하지 않고 생성자로 필요한 서비스를 받습니다.

수동 조립과 컨테이너 조립의 설계 목표는 같다

Spring을 도입해도 서비스가 인터페이스에만 의존한다는 객체지향 경계는 변하지 않는다.

  1. 수동 생성

    new를 구성 루트에 모음

  2. 수동 연결

    생성자로 역할 전달

  3. Container 정의

    bean definition에 생성 규칙

  4. Container 연결

    dependency injection으로 전달


빠르게 실패하는 구성

MemberRepository 빈이 없으면 컨테이너는 MemberRegistrationService를 만들 수 없으므로 시작 중 실패합니다.

후보가 두 개인데 선택 기준이 없어도 실패합니다.

요청을 받은 뒤 null 오류가 나는 것보다 설정 단계에서 원인을 드러내는 편이 안전합니다.

No qualifying bean of type 'MemberRepository' available
컨텍스트 생명주기는 등록에서 종료까지 이어진다

빈 정의, 인스턴스 생성, 의존관계 연결, 사용, 종료가 하나의 관리 경계를 이룬다.

  1. 구성 읽기

    @Configuration과 @Bean 발견

  2. 빈 생성

    후보를 해결해 생성자와 메서드에 전달

  3. 애플리케이션 사용

    singleton 빈 공유

  4. context.close

    관리 빈의 종료 콜백 실행


수동 구성과 Spring 구성 비교

항목순수 Java 구성Spring 구성
역할·구현 설계개발자가 작성동일
객체 생성new와 필드@Bean 메서드
의존성 연결생성자 직접 호출타입 기반 매개변수 주입
공유 수명구성 객체가 관리컨테이너가 관리

Spring이 나쁜 객체 설계를 자동으로 고쳐 주지는 않습니다.

먼저 역할을 나눈 뒤 조립을 맡길 때 의미가 있습니다.