본문으로 건너뛰기

안동민 개발노트

본문 시작

순수 Java BoardConfig 객체 조립

main 소스의 구성 루트가 저장소와 PBKDF2 해시 구현을 선택해 회원가입·로그인 서비스를 조립하고, 같은 저장 상태를 보는지 실제 기본 구성으로 검증합니다.

앞 문서의 MemberRegistrationServiceLoginService는 이미 MemberRepositoryPasswordHasher 역할을 생성자로 받습니다.

이제 구체 구현 선택과 객체 연결을 순수 Java 구성 루트인 BoardConfig 한곳에 모읍니다.

이 클래스는 Spring @Configuration이 아니며 빈 후보를 검색하거나 스코프를 관리하지 않습니다.


main 소스에서 완성하는 구성 루트

BoardConfig의 main 소스는 test 소스에 있는 TestPasswordHasher를 참조하지 않습니다.

현재 실행 예제는 메모리 저장소 한 인스턴스와 PBKDF2 해시 어댑터 한 인스턴스를 필드에 보관하고 두 서비스 생성자에 전달합니다.

board-core/src/main/java/board/BoardConfig.java
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;

public final class BoardConfig {
    private final MemberRepository members =
            new MemoryMemberRepository();
    private final PasswordHasher passwordHasher =
            new Pbkdf2PasswordHasher(
                    PasswordHashSettings.productionDefaults());

    public MemberRegistrationService memberRegistrationService() {
        return new MemberRegistrationService(
                members, passwordHasher);
    }

    public LoginService loginService() {
        return new LoginService(members, passwordHasher);
    }
}

members 필드가 가리키는 하나의 메모리 저장소를 두 서비스에 전달하므로 한쪽에서 저장한 상태를 다른 쪽이 조회할 수 있습니다.

같은 passwordHasher 필드도 두 생성자에 전달하지만, 모든 PasswordHasher 구현이 반드시 객체 동일성을 공유해야 한다는 일반 계약은 아닙니다.

두 구성 메서드는 호출할 때마다 새 서비스 객체를 만들 수 있습니다.

이 예제에서 공유하는 핵심 상태는 저장소이며 서비스 자체가 싱글톤이라는 뜻은 아닙니다.

순수 Java BoardConfig가 main 소스의 MemoryMemberRepository와 Pbkdf2PasswordHasher를 한 번 선택하고 같은 참조를 회원가입과 로그인 서비스 생성자에 전달하는 객체 그래프를 설명합니다.

PURE JAVA · COMPOSITION ROOT

BoardConfig는 main-source 구현을 선택해 두 서비스를 조립한다

구성 객체 하나가 구체 구현을 선택하고 두 사용 사례에 역할 참조를 전달합니다. 서비스는 업무 순서를 알고, 구성 루트는 생성과 연결을 압축해 보여 줍니다.

ONE CONFIG INSTANCE

main 소스의 두 필드를 먼저 만든다

  1. members = R#1

    MemoryMemberRepository 한 인스턴스가 순차 학습 실행의 회원 상태를 보관합니다.

  2. passwordHasher = H#1

    Pbkdf2PasswordHasher와 운영 기본 설정을 main 소스에서 선택합니다.

  3. 두 생성자에 역할로 전달

    구체 구현 생성은 서비스 안으로 돌아가지 않고 BoardConfig에 남습니다.

한 BoardConfig 인스턴스가 완성하는 객체 그래프
구성 필드 활성 main 구현 회원가입 서비스 로그인 서비스
members · R#1 MemoryMemberRepository 같은 R#1 전달 같은 R#1 전달
passwordHasher · H#1 Pbkdf2PasswordHasher 같은 활성 참조 전달 같은 활성 참조 전달

SOURCE-SET BOUNDARY

test 전용 해시는 main 구성에 들어오지 않는다

TestPasswordHashersrc/test/java에만 남고, 기본 실행 그래프는 main 소스의 PBKDF2 어댑터를 사용합니다.

SERVICE IDENTITY

공유 필드와 서비스 싱글톤은 다른 주장이다

두 팩터리 메서드는 호출마다 새 서비스 객체를 반환할 수 있습니다. 이 도식의 상태 공유 근거는 두 생성자에 전달되는 R#1입니다.

이 순수 Java 구성은 명시적으로 구현을 선택합니다. Spring 빈 검색·후보 선택·빈 스코프와, 컨테이너가 수명 주기를 관리하는 빈의 종료 콜백은 아직 등장하지 않습니다.


실행 진입점

실행 코드는 같은 BoardConfig에서 두 서비스를 한 번씩 얻고 회원가입 뒤 로그인 결과를 비교합니다.

board-core/src/main/java/board/BoardApplication.java
package board;

import board.member.Member;
import board.member.SignUpCommand;

public final class BoardApplication {
    private BoardApplication() {}

    public static void main(String[] args) {
        var config = new BoardConfig();
        var registration = config.memberRegistrationService();
        var login = config.loginService();

        Member registered = registration.register(new SignUpCommand(
                "member@example.com", "안동민", "secret-1234"));
        long loginId = login.authenticate(
                "member@example.com", "secret-1234");

        if (registered.id() != loginId) {
            throw new IllegalStateException(
                    "composition returned different member ids");
        }
        System.out.println("memberId=" + loginId);
    }
}

BoardApplication은 Map 저장 방식, PBKDF2 저장 문자열 형식, salt 생성 방법을 모릅니다.

구성 루트를 만들고 완성된 사용 사례를 호출할 뿐입니다.


생성 책임과 실행 책임

객체이 문서에서 맡는 책임바뀌는 이유
MemberRegistrationService중복 확인 → 해시 → 저장 순서회원가입 규칙 변경
MemoryMemberRepository순차 학습 실행의 메모리 상태 저장저장 기술 변경
Pbkdf2PasswordHashermain 실행의 해시·비교 계약 구현해시 정책 변경
BoardConfig구체 구현 선택과 생성자 연결실행 구성 변경

이 분리는 객체 생성과 업무 실행을 구분합니다.

구성 루트가 스레드 안전성, 트랜잭션, 자원 종료를 자동으로 제공한다는 뜻은 아닙니다.


실제 기본 구성의 행동 검증

구성 테스트는 test 전용 대역으로 그래프를 바꾸지 않고 BoardConfig의 실제 기본 구성을 한 번 통과합니다.

가입 결과 ID와 로그인 결과 ID가 정확히 같아야 합니다.

board-core/src/test/java/board/BoardConfigTest.java
package board;

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

import board.member.Member;
import board.member.SignUpCommand;
import org.junit.jupiter.api.Test;

final class BoardConfigTest {
    @Test
    void 같은_구성에서_가입한_회원으로_로그인한다() {
        var config = new BoardConfig();

        Member registered = config.memberRegistrationService()
                .register(new SignUpCommand(
                        "member@example.com",
                        "회원",
                        "secret-1234"));
        long loginId = config.loginService().authenticate(
                "member@example.com", "secret-1234");

        assertThat(loginId).isEqualTo(registered.id());
    }
}

이 테스트와 필드 연결을 함께 보면 같은 BoardConfig 안에서 회원가입과 로그인이 동일한 메모리 상태를 사용한다는 사실을 확인할 수 있습니다.

반면 참조 동일성을 공개 API로 노출하거나 운영 동시성·트랜잭션·자원 수명까지 증명하지는 않습니다.

같은 BoardConfig에서 얻은 회원가입 서비스가 메모리 저장소에 회원을 저장하고 로그인 서비스가 그 상태를 조회해 같은 회원 ID를 반환하는 구성 테스트의 행동 증거를 설명합니다.

COMPOSITION SMOKE · BEHAVIOR EVIDENCE

구성 테스트는 두 사용 사례가 같은 저장 상태를 보는지 검증한다

참조를 노출하지 않고 가입 결과와 로그인 결과의 동일한 ID를 관찰합니다. 한 구성 객체에서 시작한 두 경로가 현재 메모리 상태를 이어 보는지 행동으로 확인합니다.

REGISTER → AUTHENTICATE → ASSERT

한 구성 객체를 통과한 성공 경로

  1. 두 사용 사례 조립

    같은 BoardConfig에서 회원가입 서비스와 로그인 서비스를 얻습니다.

  2. 회원가입이 R#1에 저장

    H#1.hash로 만든 해시와 양수 회원 ID가 메모리 저장 상태에 남습니다.

  3. 로그인이 같은 상태를 조회

    R#1.findByEmail과 활성 해시의 matches가 저장된 회원을 검증합니다.

  4. loginId == registered.id()

    로그인 결과가 방금 가입한 바로 그 회원 ID와 정확히 같아야 테스트가 통과합니다.

구성 테스트가 고정하는 증거와 남겨 두는 경계
관찰 말할 수 있는 것 말할 수 없는 것
ID 정확히 일치 현재 구성의 두 사용 사례가 같은 메모리 회원 상태를 봅니다. 운영 영속화·분산 상태 공유
필드 연결 코드상 두 생성자에 같은 members 참조를 전달합니다. 모든 저장소 구현의 수명 정책
순차 smoke test 기본 PBKDF2 구성의 가입·로그인 협업이 한 번 완주합니다. 스레드 안전성·트랜잭션·자원 종료

IDENTITY SCOPE

저장소 동일성은 현재 메모리 상태에 필요하다

해시 역할은 호환되는 활성 구성이 핵심이며 모든 구현에서 객체 동일성까지 요구하는 포트 계약은 아닙니다.

이 증거는 같은 구성 객체의 순차 학습 실행에 한정됩니다. Spring 빈 후보 선택과 스코프는 이후 컨테이너 전환 문서의 책임입니다.


다음 문서에서는 저장소를 서비스마다 다시 만들 때 상태가 끊기는 실패를 통해 공유 범위와 구현 교체를 다룹니다.

Spring 빈 후보 선택과 스코프는 순수 Java 구성과 섞지 않고 이후 컨테이너 전환 문서에서 다룹니다.