본문 바로가기

순수 Java 객체 공유와 구현 교체

저장소를 호출마다 다시 만들 때 가입과 로그인 상태가 갈라지는 실패를 재현하고, 한 BoardConfig 인스턴스의 공유 경계와 구현 교체가 실제로 증명하는 범위를 구분합니다.

앞 글의 BoardConfig는 저장소와 해시 어댑터를 필드에서 한 번 만들고 두 서비스에 넘겼습니다. 그런데 같은 구성 객체를 쓴다고 해서 메서드가 돌려주는 객체가 저절로 공유되는 것은 아닙니다. 공유되는지는 객체를 어디서 몇 번 만들고 어떤 참조를 넘겼는지로 정해집니다.


호출마다 새 저장소를 만드는 실패

다음 테스트 전용 구성은 저장소가 필요할 때마다 new MemoryMemberRepository()를 실행합니다. 그래서 회원가입 서비스는 저장소 R#1에 저장하지만, 로그인 서비스는 새로 만든 R#2에서 회원을 찾습니다.

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

import board.member.InvalidCredentialsException;
import board.member.LoginService;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

final class PerCallRepositoryConfigTest {
    @Test
    void 저장소를_호출마다_만들면_가입한_회원을_로그인에서_찾지_못한다() {
        var config = new PerCallRepositoryConfig();

        var registered = config.memberRegistrationService()
                .register(new SignUpCommand(
                        "[email protected]",
                        "회원",
                        "secret-1234"));

        assertThat(registered.id()).isPositive();
        assertThatThrownBy(() -> config.loginService().authenticate(
                "[email protected]", "secret-1234"))
                .isInstanceOf(InvalidCredentialsException.class)
                .hasMessage("invalid credentials");
    }

    private static final class PerCallRepositoryConfig {
        private final PasswordHasher passwordHasher =
                new TestPasswordHasher();

        MemberRegistrationService memberRegistrationService() {
            return new MemberRegistrationService(
                    memberRepository(), passwordHasher);
        }

        LoginService loginService() {
            return new LoginService(
                    memberRepository(), passwordHasher);
        }

        private MemberRepository memberRepository() {
            return new MemoryMemberRepository();
        }
    }
}

해시는 두 경로 모두 같은 TestPasswordHasher를 쓰도록 고정했습니다. 그러니 이 실패의 원인은 해시가 아니라, 저장 상태가 R#1과 R#2 두 개로 나뉜 데 있습니다. 순수 Java 메서드는 돌려준 객체를 기억해 두었다가 다시 주지 않습니다. 공유가 필요하다면 한 번 만든 참조를 필드에 두거나, 상위의 조립 코드에서 두 서비스 생성자에 같은 참조를 넘겨야 합니다.

저장소를 호출마다 만들면 가입과 로그인이 다른 상태를 본다

가입은 R#1에만 저장되고 새 R#2의 로그인에는 보이지 않아 조립 호출마다 저장 상태가 분리되는 변화를 설명한다.

테스트 전용 구성 객체의 두 서비스 조립 메서드가 MemoryMemberRepository를 각각 새로 만들어 회원가입은 R#1에 저장하고 로그인은 빈 R#2를 조회하므로, 같은 저장소 타입과 같은 config만으로는 상태 공유가 성립하지 않는 실패를 설명합니다.

PER-CALL CREATION · SPLIT STATE

저장소를 호출마다 만들면 가입과 로그인이 다른 상태를 본다

같은 클래스와 같은 구성 객체는 같은 인스턴스를 뜻하지 않습니다. 두 서비스 조립 호출이 각각 new MemoryMemberRepository()를 실행하면 저장과 조회가 서로 다른 Map에 도착합니다.

같은 config의 호출마다 새 저장소가 생기는 반례 두구획은같은구성객체의서로다른조립호출이다. 각각새저장소R#1/R#2를만들어해당서비스에주입한다. 가입이저장한회원은R#1에만있고R#2의로그인조회에는없다. 생성·주입6노드4간선이며구획사이상태공유간선은없다. 같은해시대역은실패원인을저장소생성횟수로고정한별도조건이다. 가입 조립 로그인 조립 구성 메서드 1 new 실행 새 저장소 R#1 별도 Map 가입 서비스 회원 저장 구성 메서드 2 new 실행 새 저장소 R#2 별도 Map 로그인 서비스 회원 없음 생성 주입 생성 주입
같은 config의 호출마다 새 저장소가 생기는 반례 두구획은같은구성객체의서로다른조립호출이다. 각각새저장소R#1/R#2를만들어해당서비스에주입한다. 가입이저장한회원은R#1에만있고R#2의로그인조회에는없다. 생성·주입6노드4간선이며구획사이상태공유간선은없다. 같은해시대역은실패원인을저장소생성횟수로고정한별도조건이다. 가입 조립 로그인 조립 구성 메서드 1 new 실행 새 저장소 R#1 별도 메모리 상태 가입 서비스 회원 저장 구성 메서드 2 new 실행 새 저장소 R#2 별도 메모리 상태 로그인 서비스 회원 없음 생성 주입 생성 주입

REGISTER ON R#1 → AUTHENTICATE ON R#2

호출 횟수가 저장소 identity를 둘로 나눈다

  1. 테스트 config G#1 생성

    해시 대역 H#1은 필드에서 한 번 만들지만 저장소는 아직 만들지 않습니다.

  2. 회원가입 서비스 조립

    memberRepository() 첫 호출이 R#1을 만들고 회원을 그 Map에 저장합니다.

  3. 로그인 서비스 조립

    memberRepository() 두 번째 호출이 비어 있는 R#2를 만듭니다.

  4. 동일한 이메일 조회 실패

    로그인은 R#2에서 회원을 찾지 못해 고정된 invalid credentials로 끝납니다.

REPOSITORY R#1

가입 경로

회원가입 서비스가 저장한 양수 ID 회원은 R#1의 Map에만 존재합니다.

REPOSITORY R#2

로그인 경로

로그인 서비스가 받은 R#2는 같은 클래스의 새 객체이며 저장 상태가 비어 있습니다.

두 서비스 조립 호출이 받은 협력자와 관찰 결과
조립 호출 저장소 참조 해시 참조 관찰
memberRegistrationService() R#1 · 새 Map H#1 · test 대역 가입 성공 · R#1에 저장
loginService() R#2 · 새 Map H#1 · 같은 제어 변수 회원 없음 · 동일 자격 증명 실패

두 서비스 조립 호출이 받은 협력자와 관찰 결과

memberRegistrationService()

저장소 참조: R#1 · 새 Map

해시 참조: H#1 · test 대역

관찰: 가입 성공 · R#1에 저장

loginService()

저장소 참조: R#2 · 새 Map

해시 참조: H#1 · 같은 제어 변수

관찰: 회원 없음 · 동일 자격 증명 실패

CAUSE, NOT COINCIDENCE

같은 해시 대역을 써도 분리된 상태는 이어지지 않는다

H#1 공유는 실패 원인을 저장소 생성 횟수로 고정하기 위한 테스트 조건입니다. 모든 PasswordHasher가 같은 객체 identity를 공유해야 한다는 계약은 아닙니다.

순수 Java 메서드는 반환 객체를 자동 캐시하지 않습니다. 공유가 필요하면 한 번 만든 참조를 필드나 명시적인 상위 조립 경계에서 두 서비스 생성자에 전달해야 합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 두 구획은 같은 구성 객체의 서로 다른 조립 호출이다. 각각 새 저장소 R#1/R#2를 만들어 해당 서비스에 주입한다. 가입이 저장한 회원은 R#1에만 있고 R#2의 로그인 조회에는 없다. 생성·주입의 6노드와 4간선이며 구획 사이에 상태 공유 간선은 없다. 같은 해시 대역은 실패 원인을 저장소 생성 횟수로 고정한 별도 조건이다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
9두 구획은 같은 구성 객체의 서로 다른 조립 호출이다. 각각 새 저장소 R#1/R#2를 만들어 해당 서비스에 주입한다. 가입이 저장한 회원은 R#1에만 있고 R#2의 로그인 조회에는 없다. 생성·주입의 6노드와 4간선이며 구획 사이에 상태 공유 간선은 없다. 같은 해시 대역은 실패 원인을 저장소 생성 횟수로 고정한 별도 조건이다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
가입용 새 저장소

첫 조립 호출이 새 R#1을 가입에 주입합니다.

가입 저장소: R#1 · 로그인 저장소: 아직 별도첫 사용 사례의 저장 범위가 생깁니다.
회원은 R#1에 저장

가입이 회원을 R#1에 넣습니다.

R#1: 회원 있음 · 공유 간선: 없음회원이 첫 저장소에만 존재합니다.
새 R#2의 로그인

다른 조립 호출이 새 R#2를 주입하여 로그인 조회는 회원을 못 봅니다.

로그인 저장소: R#2 · R#2: 회원 없음조립마다 생성하여 저장 상태가 분리됩니다.

저장소를 호출마다 만들면 가입과 로그인이 다른 상태를 본다

가입은 R#1에만 저장되고 새 R#2의 로그인에는 보이지 않아 조립 호출마다 저장 상태가 분리되는 변화를 설명한다.

테스트 전용 구성 객체의 두 서비스 조립 메서드가 MemoryMemberRepository를 각각 새로 만들어 회원가입은 R#1에 저장하고 로그인은 빈 R#2를 조회하므로, 같은 저장소 타입과 같은 config만으로는 상태 공유가 성립하지 않는 실패를 설명합니다.

PER-CALL CREATION · SPLIT STATE

저장소를 호출마다 만들면 가입과 로그인이 다른 상태를 본다

같은 클래스와 같은 구성 객체는 같은 인스턴스를 뜻하지 않습니다. 두 서비스 조립 호출이 각각 new MemoryMemberRepository()를 실행하면 저장과 조회가 서로 다른 Map에 도착합니다.

같은 config의 호출마다 새 저장소가 생기는 반례 두구획은같은구성객체의서로다른조립호출이다. 각각새저장소R#1/R#2를만들어해당서비스에주입한다. 가입이저장한회원은R#1에만있고R#2의로그인조회에는없다. 생성·주입6노드4간선이며구획사이상태공유간선은없다. 같은해시대역은실패원인을저장소생성횟수로고정한별도조건이다. 가입 조립 로그인 조립 구성 메서드 1 new 실행 새 저장소 R#1 별도 Map 가입 서비스 회원 저장 구성 메서드 2 new 실행 새 저장소 R#2 별도 Map 로그인 서비스 회원 없음 생성 주입 생성 주입
같은 config의 호출마다 새 저장소가 생기는 반례 두구획은같은구성객체의서로다른조립호출이다. 각각새저장소R#1/R#2를만들어해당서비스에주입한다. 가입이저장한회원은R#1에만있고R#2의로그인조회에는없다. 생성·주입6노드4간선이며구획사이상태공유간선은없다. 같은해시대역은실패원인을저장소생성횟수로고정한별도조건이다. 가입 조립 로그인 조립 구성 메서드 1 new 실행 새 저장소 R#1 별도 메모리 상태 가입 서비스 회원 저장 구성 메서드 2 new 실행 새 저장소 R#2 별도 메모리 상태 로그인 서비스 회원 없음 생성 주입 생성 주입

REGISTER ON R#1 → AUTHENTICATE ON R#2

호출 횟수가 저장소 identity를 둘로 나눈다

  1. 테스트 config G#1 생성

    해시 대역 H#1은 필드에서 한 번 만들지만 저장소는 아직 만들지 않습니다.

  2. 회원가입 서비스 조립

    memberRepository() 첫 호출이 R#1을 만들고 회원을 그 Map에 저장합니다.

  3. 로그인 서비스 조립

    memberRepository() 두 번째 호출이 비어 있는 R#2를 만듭니다.

  4. 동일한 이메일 조회 실패

    로그인은 R#2에서 회원을 찾지 못해 고정된 invalid credentials로 끝납니다.

REPOSITORY R#1

가입 경로

회원가입 서비스가 저장한 양수 ID 회원은 R#1의 Map에만 존재합니다.

REPOSITORY R#2

로그인 경로

로그인 서비스가 받은 R#2는 같은 클래스의 새 객체이며 저장 상태가 비어 있습니다.

두 서비스 조립 호출이 받은 협력자와 관찰 결과
조립 호출 저장소 참조 해시 참조 관찰
memberRegistrationService() R#1 · 새 Map H#1 · test 대역 가입 성공 · R#1에 저장
loginService() R#2 · 새 Map H#1 · 같은 제어 변수 회원 없음 · 동일 자격 증명 실패

두 서비스 조립 호출이 받은 협력자와 관찰 결과

memberRegistrationService()

저장소 참조: R#1 · 새 Map

해시 참조: H#1 · test 대역

관찰: 가입 성공 · R#1에 저장

loginService()

저장소 참조: R#2 · 새 Map

해시 참조: H#1 · 같은 제어 변수

관찰: 회원 없음 · 동일 자격 증명 실패

CAUSE, NOT COINCIDENCE

같은 해시 대역을 써도 분리된 상태는 이어지지 않는다

H#1 공유는 실패 원인을 저장소 생성 횟수로 고정하기 위한 테스트 조건입니다. 모든 PasswordHasher가 같은 객체 identity를 공유해야 한다는 계약은 아닙니다.

순수 Java 메서드는 반환 객체를 자동 캐시하지 않습니다. 공유가 필요하면 한 번 만든 참조를 필드나 명시적인 상위 조립 경계에서 두 서비스 생성자에 전달해야 합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 두 구획은 같은 구성 객체의 서로 다른 조립 호출이다. 각각 새 저장소 R#1/R#2를 만들어 해당 서비스에 주입한다. 가입이 저장한 회원은 R#1에만 있고 R#2의 로그인 조회에는 없다. 생성·주입의 6노드와 4간선이며 구획 사이에 상태 공유 간선은 없다. 같은 해시 대역은 실패 원인을 저장소 생성 횟수로 고정한 별도 조건이다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
9두 구획은 같은 구성 객체의 서로 다른 조립 호출이다. 각각 새 저장소 R#1/R#2를 만들어 해당 서비스에 주입한다. 가입이 저장한 회원은 R#1에만 있고 R#2의 로그인 조회에는 없다. 생성·주입의 6노드와 4간선이며 구획 사이에 상태 공유 간선은 없다. 같은 해시 대역은 실패 원인을 저장소 생성 횟수로 고정한 별도 조건이다.
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
가입용 새 저장소

첫 조립 호출이 새 R#1을 가입에 주입합니다.

가입 저장소: R#1 · 로그인 저장소: 아직 별도첫 사용 사례의 저장 범위가 생깁니다.
회원은 R#1에 저장

가입이 회원을 R#1에 넣습니다.

R#1: 회원 있음 · 공유 간선: 없음회원이 첫 저장소에만 존재합니다.
새 R#2의 로그인

다른 조립 호출이 새 R#2를 주입하여 로그인 조회는 회원을 못 봅니다.

로그인 저장소: R#2 · R#2: 회원 없음조립마다 생성하여 저장 상태가 분리됩니다.

그림의 두 구역은 같은 구성 객체에서 일어난 서로 다른 조립 호출입니다. 첫 호출은 새 저장소 R#1을 만들어 가입 서비스에 넣고, 둘째 호출은 또 다른 새 저장소 R#2를 만들어 로그인 서비스에 넣습니다. 두 구역 사이에는 상태를 잇는 선이 없으므로, 가입이 저장한 회원은 R#1에만 있고 R#2를 조회하는 로그인은 그 회원을 볼 수 없습니다.


한 구성 객체 안의 공유 범위

앞 글의 실제 BoardConfig는 members와 passwordHasher를 인스턴스 필드에서 초기화합니다. 그래서 같은 BoardConfig의 구성 메서드를 여러 번 호출하면 서비스 객체는 새로 생기지만, 각 생성자에는 같은 필드 참조가 넘어갑니다. 무엇이 언제 만들어지는지를 정리하면 다음과 같습니다.

대상만들어지는 때이 예제에서 확인하는 것
BoardConfignew BoardConfig()를 실행할 때마다 새 그래프서로 다른 config의 메모리 상태는 나뉜다
MemoryMemberRepositoryconfig 인스턴스의 필드를 초기화할 때 한 번같은 config가 만든 서비스들은 같은 상태를 본다
Pbkdf2PasswordHasherconfig 인스턴스의 필드를 초기화할 때 한 번지금의 운영 그래프가 고른 해시 구현
회원가입·로그인 서비스구성 메서드를 호출할 때마다새 서비스도 같은 협력 객체의 참조를 받는다
SignUpCommand와 메서드 입력사용 사례를 호출할 때마다호출마다 따로 생기는 값이며 공유 상태가 아니다

구성 객체를 더 이상 아무도 참조하지 않더라도, 서비스가 저장소와 해시 어댑터를 참조하고 있으면 그 객체들은 계속 살아 있습니다. 이처럼 순수 Java 구성은 객체를 몇 번 만들고 어떤 참조를 함께 쓰는지는 분명하게 보여 주지만, 가비지 컬렉션의 시점이나 자원의 정리는 관리해 주지 않습니다.


구현 교체가 증명하는 것

운영 그래프는 Pbkdf2PasswordHasher를 고르고, 단위 테스트는 필요한 서비스에 TestPasswordHasher를 직접 넘길 수 있습니다. 두 서비스가 PasswordHasher 역할에만 의존하므로, 역할이 그대로인 동안 어떤 구현을 쓸지는 구성 쪽에서만 정합니다. 각각의 주장을 무엇으로 확인했는지 정리하면 다음과 같습니다.

확인할 것확인한 방법여기서 확인하지 않는 것
같은 config의 상태 공유앞 글의 가입 뒤 로그인 성공 테스트JVM 전체에 하나뿐인 객체
호출마다 저장소를 만들면 끊김이 글의 R#1/R#2 실패 테스트공유 저장소의 스레드 안전성
해시 구현이 지킬 규칙비밀번호 해시 역할의 두 구현 공통 테스트같은 해시 객체를 함께 써야 하는지
구성의 구현 선택서비스의 소스는 역할만 참조기존 저장 해시의 자동 호환과 이전
메모리 그래프의 수명config와 서비스가 협력 객체를 참조트랜잭션, 영속화, 자원 종료

같은 PasswordHasher 객체를 넘기는 것은 지금 그래프의 선택일 뿐, 모든 구현이 따라야 하는 규칙은 아닙니다. 반대로 두 구현이 같은 역할을 구현한다고 해서 서로가 만든 저장 문자열을 해석할 수 있다는 보장도 없습니다.

한 BoardConfig 인스턴스가 R#1과 H#1을 필드에서 한 번 만들고 새 회원가입·로그인 서비스에 같은 참조를 전달하는 그래프 경계와, PasswordHasher 구현 선택을 바꿀 때 서비스 소스 변경 범위·저장 데이터 호환성·운영 수명 보장을 별도로 검증해야 함을 설명합니다.

GRAPH BOUNDARY · REPLACEMENT EVIDENCE

공유 범위와 구현 교체는 서로 다른 증거를 요구한다

한 config의 필드 공유는 상태 연결을, 역할 의존은 소스 변경 범위를 설명합니다. 둘 다 JVM 전역 수명이나 기존 저장 형식의 자동 호환을 뜻하지 않습니다.

같은 호출 포트가 저장 형식의 호환을 보장하지 않는다 테스트와PBKDF2는같은PasswordHasher를구현하지만서로다른인코딩을생성한다. {test}저장값을본문의PBKDF2matches로검증하면형식파서가false로거절한다. 4노드4관계를보존한다. 이도식은구현교체시호환성판단의반례이며자동migration이나운영hotswap절차가아니다. config별객체공유는앞그림및아래표에서별도로판단한다. 같은 역할 PasswordHasher 테스트 대역 TestPasswordHasher PBKDF2 어댑터 Pbkdf2PasswordHasher 테스트 저장 값 {test} 형식 타입 구현 타입 구현 hash matches: false
같은 호출 포트가 저장 형식의 호환을 보장하지 않는다 테스트와PBKDF2는같은PasswordHasher를구현하지만서로다른인코딩을생성한다. {test}저장값을본문의PBKDF2matches로검증하면형식파서가false로거절한다. 4노드4관계를보존한다. 이도식은구현교체시호환성판단의반례이며자동migration이나운영hotswap절차가아니다. config별객체공유는앞그림및아래표에서별도로판단한다. 같은 역할 PasswordHasher 테스트 대역 {test} 인코딩 생성 테스트 저장 값 운영 형식과 다름 PBKDF2 어댑터 다른 형식 거절 타입 구현 구현 hash false

BOARD CONFIG G#1

필드는 한 번 만들고 서비스 생성자는 같은 참조를 받는다

  1. new BoardConfig()

    config 인스턴스 G#1의 필드 초기화가 저장소 R#1과 PBKDF2 어댑터 H#1을 각각 한 번 실행합니다.

  2. 회원가입 서비스 생성

    새 서비스 S#1이 R#1과 H#1 참조를 받아 중복 확인·해시·저장을 수행합니다.

  3. 로그인 서비스 생성

    새 서비스 S#2가 같은 R#1과 H#1 참조를 받아 회원 조회와 해시 검증을 수행합니다.

  4. 추가 구성 메서드 호출

    S#3처럼 서비스는 더 생길 수 있지만 G#1의 필드 참조는 그대로 전달됩니다.

SAME CONFIG G#1

같은 상태 경계

G#1이 만든 서비스들은 R#1을 통해 가입 뒤 로그인의 같은 회원 상태를 관찰합니다.

NEW CONFIG G#2

별도 객체 그래프

new BoardConfig()를 다시 실행하면 R#2와 H#2를 가진 독립 그래프가 시작됩니다.

공유와 구현 교체를 판단하는 서로 다른 증거
질문 근거 확인되는 범위 확인되지 않는 범위
상태가 이어지는가 같은 G#1에서 가입 후 로그인 두 서비스가 같은 R#1 참조 사용 JVM 전역 singleton
재생성이 끊는가 R#1 저장 후 R#2 조회 실패 메서드별 new가 상태 분리 공유 저장소의 thread safety
구현을 바꿀 수 있는가 서비스는 PasswordHasher만 참조 안정된 계약에서 서비스 소스 유지 기존 해시 형식의 자동 호환
수명이 관리되는가 config와 서비스가 협력자 참조 보유 참조가 있는 동안 객체 도달 가능 GC 시점·transaction·resource shutdown

공유와 구현 교체를 판단하는 서로 다른 증거

상태가 이어지는가

근거: 같은 G#1에서 가입 후 로그인

확인되는 범위: 두 서비스가 같은 R#1 참조 사용

확인되지 않는 범위: JVM 전역 singleton

재생성이 끊는가

근거: R#1 저장 후 R#2 조회 실패

확인되는 범위: 메서드별 new가 상태 분리

확인되지 않는 범위: 공유 저장소의 thread safety

구현을 바꿀 수 있는가

근거: 서비스는 PasswordHasher만 참조

확인되는 범위: 안정된 계약에서 서비스 소스 유지

확인되지 않는 범위: 기존 해시 형식의 자동 호환

수명이 관리되는가

근거: config와 서비스가 협력자 참조 보유

확인되는 범위: 참조가 있는 동안 객체 도달 가능

확인되지 않는 범위: GC 시점·transaction·resource shutdown

SOURCE CHANGE ≠ STORED-DATA COMPATIBILITY

객체 identity보다 포트와 저장 형식의 호환을 검증한다

main의 PBKDF2 어댑터와 test-source 대역은 같은 호출 포트를 구현하지만 서로 다른 인코딩을 만듭니다. 구성 선택을 바꿔도 서비스 소스가 유지된다는 사실은 기존 {test} 값이 $pbkdf2-sha256$v1$ 어댑터에서 검증되거나 migration 없이 hot swap된다는 보장이 아닙니다.

순수 Java 구성의 보장과 명시적 한계
구분 이 예제의 결론 별도 경계
구성 G#1 안의 R#1·H#1 선택과 생성자 연결을 읽을 수 있음 다른 진입점이 별도 config를 만들지 검증
원칙 안정된 역할에서 구현 교체라는 특정 변경 축의 OCP·DIP 증거 생성자 주입만으로 모든 원칙을 증명하지 않음
운영 순차 메모리 예제의 상태 연속성 동시성·원자성·영속성·종료·migration

순수 Java 구성의 보장과 명시적 한계

구성

이 예제의 결론: G#1 안의 R#1·H#1 선택과 생성자 연결을 읽을 수 있음

별도 경계: 다른 진입점이 별도 config를 만들지 검증

원칙

이 예제의 결론: 안정된 역할에서 구현 교체라는 특정 변경 축의 OCP·DIP 증거

별도 경계: 생성자 주입만으로 모든 원칙을 증명하지 않음

운영

이 예제의 결론: 순차 메모리 예제의 상태 연속성

별도 경계: 동시성·원자성·영속성·종료·migration

이 장의 공유 범위는 config 인스턴스별 객체 그래프입니다. 컨테이너가 관리하는 빈 후보와 스코프는 다음 문서 이후의 별도 규칙입니다.

그림의 위쪽이 바로 그런 경우입니다. TestPasswordHasher와 Pbkdf2PasswordHasher는 같은 PasswordHasher를 구현하지만 서로 다른 형식의 문자열을 만들며, {test} 형식으로 저장된 값을 PBKDF2의 matches로 검증하면 형식 검사에서 false가 됩니다. 구성에서 구현을 바꾸어도 서비스의 소스가 그대로라는 것은, 이미 저장된 {test} 값이 $pbkdf2-sha256$v1$ 어댑터에서 검증된다는 뜻도, 데이터를 옮기지 않고 운영 중에 구현을 갈아 끼울 수 있다는 뜻도 아닙니다. 이 메모리 예제는 새 BoardConfig를 만들면 저장 상태도 새로 시작하므로 그런 이전 과정을 보여 주지 않으며, 두 형식을 함께 받아 주는 호환 처리도 넣지 않습니다. 그림의 가운데는 config G#1이 R#1과 H#1을 한 번 만들고 새 서비스 S#1, S#2, S#3에 같은 참조를 넘기며, 새 config G#2는 독립된 그래프를 시작한다는 것을 보여 줍니다.


순수 Java 구성의 한계

객체 그래프 하나를 올바르게 조립했다고 해서 동시 접근, 원자적 중복 방지, 트랜잭션, 영속화, 외부 자원의 정리가 해결되지는 않습니다. 또 다른 코드가 별도의 저장소나 구성 객체를 만들면 또 다른 그래프가 생기므로, 실제 진입점이 어떤 config를 쓰는지도 함께 확인해야 합니다. 다음 글에서는 같은 역할과 구현을 Spring 컨테이너로 옮기며, 빈 후보 선택과 스코프의 규칙은 그때 따로 살펴봅니다.


정리

  • 같은 구성 객체를 쓴다고 객체가 저절로 공유되지 않으며, 공유 여부는 어디서 몇 번 만들고 어떤 참조를 넘겼는지로 정해집니다.
  • 메서드가 호출될 때마다 저장소를 새로 만들면 가입과 로그인이 서로 다른 상태를 보게 됩니다.
  • 순수 Java 구성은 객체의 생성 횟수와 참조 공유를 보여 주지만, 수명이나 자원 정리는 관리하지 않습니다.
  • 역할에만 의존하면 구현을 바꿀 때 서비스의 소스는 그대로지만, 이미 저장된 데이터의 형식이 호환되는지는 따로 확인해야 합니다.
  • 이 장에서 공유의 범위는 config 인스턴스마다 만들어지는 객체 그래프이며, JVM 전체에 하나뿐인 객체가 아닙니다.

연습 문제

1. 두 config 사이의 상태

같은 config에서는 회원가입 뒤 로그인이 성공하고, 서로 다른 두 config 사이에서는 회원 상태가 보이지 않는다는 테스트를 작성하세요. 그 결과를 "JVM 전역 싱글톤"이 아니라 "config 인스턴스마다의 객체 그래프"라는 말로 설명합니다.

해설 보기

같은 config에서 가입한 뒤 로그인하면 같은 저장소의 회원을 찾으므로 가입 식별자와 로그인 식별자가 같습니다. 새로 만든 다른 config에는 비어 있는 별도의 저장소가 생기므로, 첫 config에서 가입한 이메일로 로그인하면 InvalidCredentialsException이 납니다. 두 config의 PBKDF2 설정이 같아도 없는 회원 상태를 만들어 주지는 않습니다.

주의할 점이 하나 있습니다. 첫 config와 둘째 config에 서로 다른 회원을 하나씩 가입시키면 양쪽 모두 식별자 1을 돌려줄 수 있습니다. 각 저장소의 번호가 0에서 시작했기 때문이며, 객체나 상태를 공유한다는 뜻이 아닙니다. 그래서 식별자만 비교하지 말고, 한 config의 이메일로 다른 config에서 실제로 로그인해 보는 행동을 함께 확인해야 합니다.

2. 변수에 새 config를 대입하면

첫 config에서 얻은 가입 서비스의 참조를 변수에 들고 있다가, config 변수에 new BoardConfig()를 다시 대입했습니다. 이미 들고 있는 가입 서비스가 쓰는 저장소도 바뀔까요?

해설 보기

바뀌지 않습니다. 기존 가입 서비스의 필드에는 처음 생성자로 받은 저장소의 참조가 그대로 남아 있고, 변수에 새 객체를 대입하는 것은 이미 만든 서비스의 필드를 다시 채우지 않습니다. 새 config에서 만든 서비스가 다른 상태를 보는 것과, 이미 만든 서비스의 연결을 바꾸는 것은 서로 다른 일입니다. 이미 만든 서비스는 그 config가 더 이상 참조되지 않아도 첫 저장소를 계속 씁니다.

다음 글 Spring 컨테이너 전환에서는 같은 역할과 구현을 Spring 컨테이너로 옮기고, 컨테이너가 빈 후보를 고르고 스코프를 관리하는 규칙을 순수 Java 구성과 비교합니다.