본문 바로가기

OCP·DIP와 생성자 주입의 경계

이미 생성자 주입된 회원가입 서비스에서 구체 구현 선택을 분리해야 하는 이유와 필수 역할의 null 방어, 중복 실패 뒤 호출 횟수를 실행 가능한 코드로 검증합니다.

앞 문서까지의 MemberRegistrationService는 이미 MemberRepository와 PasswordHasher를 생성자로 받습니다.

이 문서에서 생성자 주입을 새로 도입하지 않습니다.

대신 현재 구조가 구체 구현 교체를 어디에서 흡수하고, 생성·호출 경계에서 무엇을 검증할 수 있게 하는지 확인합니다.

앞 문서에서 확장한 PasswordHasher의 hash + matches 계약도 그대로 이어집니다.


구체 구현 선택을 서비스로 되돌린 반례

아래 파일은 의존 방향을 비교하기 위한 완전한 반례이며 운영 소스에 추가하지 않습니다.

회원가입 순서는 같지만 서비스가 저장소와 PBKDF2 설정을 직접 선택합니다.

board-core/src/example/java/board/member/CoupledMemberRegistrationService.java - 비교용 반례
package board.member;

import java.util.Objects;

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

    public Member register(SignUpCommand command) {
        Objects.requireNonNull(command, "command must not be null");
        if (members.existsByEmail(command.email())) {
            throw new DuplicateEmailException(command.email());
        }
        String passwordHash = passwordHasher.hash(
                command.rawPassword());
        return members.save(Member.signUp(
                command.email(), command.name(), passwordHash));
    }
}

같은 PasswordHasher 역할을 쓰더라도 new Pbkdf2PasswordHasher(...)가 서비스 소스에 있으면 구현 선택과 정책 설정이 고수준 사용 사례에 다시 결합됩니다.

회원가입 서비스 안에서 PBKDF2 구현과 설정을 직접 선택할 때 생기는 소스 의존과 변경 파급을, 구성 루트가 역할 구현을 선택하는 구조와 비교합니다.

SOURCE DEPENDENCY · CHANGE RIPPLE

구현 선택이 서비스에 남으면 교체가 업무 소스로 번진다

필드 타입이 역할이어도 구체 구현을 만드는 소스는 그 구현에 의존합니다. 선택 위치를 구성 영역으로 옮겨야 안정된 회원가입 순서가 구현 교체에서 분리됩니다.

구체 구현을 선택하는 위치에 따른 소스 의존 위 반례는서비스가구체해시를생성한다. 아래분리는구성이구체해시를생성하고서비스는역할에만소스의존한다. 아래두관계는같은독립책임을보여주며서로의실행순서간선이아니다. 6노드3간선이다. 구성에서서비스로실제주입하는연결은다음구성루트그림에서다룬다. 반례 · 업무 소스가 구현을 선택 분리 · 구성과 업무의 다른 의존 서비스 안의 선택 구체 클래스·설정 의존 구체 해시 생성 PBKDF2·정책 값 구성 영역의 선택 구체 구현·설정 구체 해시 생성 같은 PasswordHasher 계약 회원가입 서비스 역할만 선언 해시 역할 PasswordHasher 생성 의존 생성 소스 의존
구체 구현을 선택하는 위치에 따른 소스 의존 위 반례는서비스가구체해시를생성한다. 아래분리는구성이구체해시를생성하고서비스는역할에만소스의존한다. 아래두관계는같은독립책임을보여주며서로의실행순서간선이아니다. 6노드3간선이다. 구성에서서비스로실제주입하는연결은다음구성루트그림에서다룬다. 반례 분리 업무 소스의 선택 구체 생성·설정 PBKDF2 생성 서비스 변경 필요 구성의 선택 구체 생성·설정 PBKDF2 생성 선택을 구성에 둠 회원가입 서비스 PasswordHasher 생성 의존 생성 소스 의존

SERVICE-OWNED SELECTION

구체 선택 한 줄이 변경 파급의 출발점이 된다

  1. 선택 소스

    new Pbkdf2PasswordHasher(...)가 회원가입 서비스 안에 있습니다.

  2. 구체 의존 추가

    서비스가 Pbkdf2PasswordHasher와 PasswordHashSettings의 생성 방법까지 알아야 합니다.

  3. 교체 때 업무 소스 수정

    역할 계약은 같아도 새 어댑터의 생성자와 설정에 맞춰 서비스 파일을 바꿉니다.

  4. 검증 범위 확산

    회원가입 순서가 바뀌지 않았는데 서비스 컴파일과 회귀 검증이 교체 작업에 묶입니다.

구체 구현 선택 위치에 따른 소스 영향
선택 위치 서비스가 아는 것 같은 계약의 구현 교체
서비스 내부 역할 + 구체 클래스 + 생성 설정 MemberRegistrationService 소스 수정
구성 영역 MemberRepository와 PasswordHasher 역할 선택 소스만 수정하고 서비스 순서는 유지

구체 구현 선택 위치에 따른 소스 영향

서비스 내부

서비스가 아는 것: 역할 + 구체 클래스 + 생성 설정

같은 계약의 구현 교체: MemberRegistrationService 소스 수정

구성 영역

서비스가 아는 것: MemberRepository와 PasswordHasher 역할

같은 계약의 구현 교체: 선택 소스만 수정하고 서비스 순서는 유지

SCOPED OCP

안정된 계약 아래의 구현 교체를 분리한다

OCP 주장은 역할 계약이 유지되는 교체 범위입니다. 모든 파일을 영원히 수정하지 않는다는 뜻이 아닙니다.

SCOPED DIP

고수준 소스는 역할을, 구성 영역은 선택을 안다

생성자 문법만이 아니라 소스 의존과 구현 선택의 소유권을 함께 분리해야 합니다.

구성 루트는 다음 문서의 책임입니다. 이 문서는 구현 교체의 소스 경계만 설명하며 배포 무중단이나 트랜잭션 성질을 주장하지 않습니다.


OCP와 DIP의 주장 범위

여기서 OCP는 PasswordHasher 계약이 안정된 동안 구현만 교체할 때 회원가입 순서의 소스를 수정하지 않는다는 범위로 읽습니다.

앞 문서처럼 hash 전용 계약에 matches를 추가하면 구현 클래스가 함께 수정되는 것은 의도적인 계약 확장입니다. 모든 변경을 막는다는 주장이 아닙니다.

DIP도 생성자 문법만으로 성립하지 않습니다.

고수준 서비스의 소스가 MemberRepository와 PasswordHasher 역할만 알고, 구체 어댑터 선택을 서비스 밖의 구성 영역이 소유할 때 이 예제의 의존 방향이 분리됩니다.


이미 주입된 서비스를 완전한 파일로 확인

다음 파일은 앞 문서의 생성자 주입을 유지하면서 두 필수 역할과 요청 명령에 null이 전달되면 거부합니다.

board-core/src/main/java/board/member/MemberRegistrationService.java
package board.member;

import java.util.Objects;

public final class MemberRegistrationService {
    private final MemberRepository members;
    private final PasswordHasher passwordHasher;

    public MemberRegistrationService(
            MemberRepository members,
            PasswordHasher passwordHasher
    ) {
        this.members = Objects.requireNonNull(
                members, "members must not be null");
        this.passwordHasher = Objects.requireNonNull(
                passwordHasher, "passwordHasher must not be null");
    }

    public Member register(SignUpCommand command) {
        Objects.requireNonNull(command, "command must not be null");
        if (members.existsByEmail(command.email())) {
            throw new DuplicateEmailException(command.email());
        }
        String passwordHash = passwordHasher.hash(
                command.rawPassword());
        return members.save(Member.signUp(
                command.email(), command.name(), passwordHash));
    }
}

생성 직후부터 두 역할이 존재하므로 setter 호출 순서나 부분 초기화 상태를 따로 관리하지 않습니다.

구체 클래스가 아닌 역할 타입을 받는다는 점까지 함께 봐야 합니다.


생성 실패와 중복 빠른 실패를 호출 횟수로 검증

테스트 대역은 확장된 PasswordHasher의 두 메서드를 모두 구현합니다.

아래 테스트는 생성자 null 방어와 중복 경로의 existsCalls == 1, hashCalls == 0, saveCalls == 0을 한 파일에서 검증합니다.

board-core/src/test/java/board/member/MemberRegistrationServiceInteractionTest.java
package board.member;

import java.util.Optional;
import org.junit.jupiter.api.Test;

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

final class MemberRegistrationServiceInteractionTest {
    @Test
    void 필수_역할이_null이면_생성_즉시_거절한다() {
        var members = new RecordingMemberRepository(false);
        var hasher = new RecordingPasswordHasher();

        assertThatNullPointerException()
                .isThrownBy(() -> new MemberRegistrationService(
                        null, hasher))
                .withMessage("members must not be null");
        assertThatNullPointerException()
                .isThrownBy(() -> new MemberRegistrationService(
                        members, null))
                .withMessage("passwordHasher must not be null");
    }

    @Test
    void 중복_이메일이면_해시와_저장을_호출하지_않는다() {
        var members = new RecordingMemberRepository(true);
        var hasher = new RecordingPasswordHasher();
        var service = new MemberRegistrationService(members, hasher);

        assertThatThrownBy(() -> service.register(new SignUpCommand(
                "[email protected]", "회원", "secret-1234")))
                .isInstanceOf(DuplicateEmailException.class);

        assertThat(members.existsCalls).isEqualTo(1);
        assertThat(hasher.hashCalls).isZero();
        assertThat(members.saveCalls).isZero();
    }

    private static final class RecordingMemberRepository
            implements MemberRepository {
        private final boolean duplicate;
        private int existsCalls;
        private int saveCalls;

        private RecordingMemberRepository(boolean duplicate) {
            this.duplicate = duplicate;
        }

        @Override
        public boolean existsByEmail(String email) {
            existsCalls++;
            return duplicate;
        }

        @Override
        public Member save(Member member) {
            saveCalls++;
            return member.identifiedBy(saveCalls);
        }

        @Override
        public Optional<Member> findByEmail(String email) {
            return Optional.empty();
        }
    }

    private static final class RecordingPasswordHasher
            implements PasswordHasher {
        private int hashCalls;

        @Override
        public String hash(String rawPassword) {
            hashCalls++;
            return "{recording}";
        }

        @Override
        public boolean matches(
                String rawPassword,
                String encodedPasswordHash
        ) {
            return false;
        }
    }
}

이 증거는 중복을 발견한 단일 호출 경로에서 후속 협력자를 실행하지 않는다는 뜻입니다.

existsByEmail → save를 원자적으로 만들거나 동시 가입과 외부 시스템을 함께 롤백한다는 뜻은 아닙니다.

필수 역할과 중복 실패 순서는 생성과 호출 경계에서 고정한다

중복 판정 true에서 즉시 끝나 hash/save 0, 정상에서 해시·저장으로 가는 경계가 관찰된다.

회원가입 서비스가 생성자에서 두 필수 역할의 null을 거절하고, 중복 이메일 경로에서 해시와 저장을 한 번도 호출하지 않는 증거를 구성 루트 인계와 함께 설명합니다.

CONSTRUCTION · FAST-FAIL EVIDENCE

필수 역할과 중복 실패 순서는 생성과 호출 경계에서 잠근다

완성되지 않은 객체는 생성 시 거절하고 중복 뒤 작업은 호출 횟수로 막습니다. 생성자 주입은 구현 선택을 밖으로 넘기는 연결점이자 필수 협력자를 고정하는 경계입니다.

중복 판정에서 뒤 협력자 호출을 차단하기 이그림은생성자에서필수역할두개를이미받은뒤의가입호출범위다. 중복판정true는예외로직접끝나며hashCalls와saveCalls가0이다. 정상분기는해시와저장으로진행한다. 앞의생성자null실패와호출경로의실패는아래증거에서별도로구분한다. 6노드5간선; 알림연습의외부호출이나트랜잭션원자성을이코드에추가한그림이아니다. 가입 명령 입력 정규화·검사 중복 확인 existsByEmail 해시 생성 원문을 그대로 전달 저장 전 회원 id = 0 · 해시만 보유 저장 결과 새 값의 id > 0 중복 예외 뒤 작업 미실행 명령 중복 아님 해시 save 중복
중복 판정에서 뒤 협력자 호출을 차단하기 이그림은생성자에서필수역할두개를이미받은뒤의가입호출범위다. 중복판정true는예외로직접끝나며hashCalls와saveCalls가0이다. 정상분기는해시와저장으로진행한다. 앞의생성자null실패와호출경로의실패는아래증거에서별도로구분한다. 6노드5간선; 알림연습의외부호출이나트랜잭션원자성을이코드에추가한그림이아니다. 가입 명령 입력 정규화·검사 중복 확인 existsByEmail 해시 생성 원문을 그대로 전달 저장 전 회원 id = 0 · 해시만 보유 저장 결과 새 값의 id > 0 중복 예외 해시·저장 미호출 명령 전달 중복 아님 해시 결과 save 중복

CONSTRUCTOR BOUNDARY

두 역할은 생성 직후부터 non-null이다

  1. members 검사

    Objects.requireNonNull이 저장 역할 누락을 즉시 거절합니다.

  2. passwordHasher 검사

    확장된 PasswordHasher 역할 누락도 같은 생성 경계에서 거절합니다.

  3. 완성된 서비스 전달

    부분 초기화나 setter 호출 순서 없이 두 역할 참조가 고정됩니다.

DUPLICATE PATH

중복 판정은 뒤 협력자 호출을 차단한다

  1. 명령 null 검사

    요청 경계가 잘못된 명령을 먼저 거절합니다.

  2. existsByEmail == true

    저장소가 중복을 보고하면 서비스가 즉시 예외를 선택합니다.

  3. DuplicateEmailException

    해시와 회원 생성·저장 이전에 호출이 끝납니다.

실행 가능한 테스트가 고정하는 경계 증거
관찰 경로 기대 결과 증명하지 않는 것
members = null 서비스 생성 실패 · 메시지 고정 구성 루트의 구현 선택 정확성
hasher = null 서비스 생성 실패 · 메시지 고정 해시 구현의 보안 강도
중복 이메일 existsCalls == 1 · hashCalls == 0 · saveCalls == 0 동시 가입 원자성 · 외부 시스템 롤백

실행 가능한 테스트가 고정하는 경계 증거

members = null

기대 결과: 서비스 생성 실패 · 메시지 고정

증명하지 않는 것: 구성 루트의 구현 선택 정확성

hasher = null

기대 결과: 서비스 생성 실패 · 메시지 고정

증명하지 않는 것: 해시 구현의 보안 강도

중복 이메일

기대 결과: existsCalls == 1 · hashCalls == 0 · saveCalls == 0

증명하지 않는 것: 동시 가입 원자성 · 외부 시스템 롤백

PORT CONTINUITY

기록 대역도 hash + matches를 구현한다

회원가입은 성공 경로에서 hash만 호출하지만 테스트 대역은 앞 문서에서 확장된 포트 전체를 구현합니다.

COMPOSITION HANDOFF

구성 영역이 공유 저장소와 활성 해시 구현을 고른다

다음 문서가 구현을 선택하고 완성된 MemberRegistrationService를 호출자에게 넘깁니다.

existsCalls == 1, hashCalls == 0, saveCalls == 0은 관찰한 중복 단일 호출의 빠른 실패 증거입니다. existsByEmail → save를 트랜잭션이나 동시성 경계로 만들지는 않습니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 이 그림은 생성자에서 필수 역할 두 개를 이미 받은 뒤의 가입 호출 범위다. 중복 판정 true는 예외로 직접 끝나며 hashCalls와 saveCalls가 0이다. 정상 분기는 해시와 저장으로 진행한다. 앞의 생성자 null 실패와 호출 경로의 실패는 아래 증거에서 별도로 구분한다. 6노드와 5간선이다. 알림 연습의 외부 호출이나 트랜잭션 원자성을 이 코드에 추가한 그림이 아니다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1필수 역할과 중복 실패 순서는 생성과 호출 경계에서 고정한다
6중복 판정에서 후속 협력자 호출을 차단하기
7이 그림은 생성자에서 필수 역할 두 개를 이미 받은 뒤의 가입 호출 범위다. 중복 판정 true는 예외로 직접 끝나며 hashCalls와 saveCalls가 0이다. 정상 분기는 해시와 저장으로 진행한다. 앞의 생성자 null 실패와 호출 경로의 실패는 아래 증거에서 별도로 구분한다. 6노드와 5간선이다. 알림 연습의 외부 호출이나 트랜잭션 원자성을 이 코드에 추가한 그림이 아니다.
41중복 판정은 후속 협력자 호출을 차단한다
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
필수 역할 뒤 중복 검사

생성자로 두 역할을 받은 서비스가 가입 중복을 확인합니다.

전제: 필수 두 역할 · 중복: 직접 예외호출 중복 실패를 생성 실패와 구분합니다.
정상 해시 실행

중복이 아닌 입력만 hash를 호출합니다.

중복 시 hashCalls: 0 · 정상: 해시검사를 통과한 값만 해시 경계로 갑니다.
정상 저장

해시를 저장에 전달합니다.

중복 시 saveCalls: 0 · 정상: save실패 뒤 저장 상태를 바꾸지 않습니다.

필수 역할과 중복 실패 순서는 생성과 호출 경계에서 고정한다

중복 판정 true에서 즉시 끝나 hash/save 0, 정상에서 해시·저장으로 가는 경계가 관찰된다.

회원가입 서비스가 생성자에서 두 필수 역할의 null을 거절하고, 중복 이메일 경로에서 해시와 저장을 한 번도 호출하지 않는 증거를 구성 루트 인계와 함께 설명합니다.

CONSTRUCTION · FAST-FAIL EVIDENCE

필수 역할과 중복 실패 순서는 생성과 호출 경계에서 잠근다

완성되지 않은 객체는 생성 시 거절하고 중복 뒤 작업은 호출 횟수로 막습니다. 생성자 주입은 구현 선택을 밖으로 넘기는 연결점이자 필수 협력자를 고정하는 경계입니다.

중복 판정에서 뒤 협력자 호출을 차단하기 이그림은생성자에서필수역할두개를이미받은뒤의가입호출범위다. 중복판정true는예외로직접끝나며hashCalls와saveCalls가0이다. 정상분기는해시와저장으로진행한다. 앞의생성자null실패와호출경로의실패는아래증거에서별도로구분한다. 6노드5간선; 알림연습의외부호출이나트랜잭션원자성을이코드에추가한그림이아니다. 가입 명령 입력 정규화·검사 중복 확인 existsByEmail 해시 생성 원문을 그대로 전달 저장 전 회원 id = 0 · 해시만 보유 저장 결과 새 값의 id > 0 중복 예외 뒤 작업 미실행 명령 중복 아님 해시 save 중복
중복 판정에서 뒤 협력자 호출을 차단하기 이그림은생성자에서필수역할두개를이미받은뒤의가입호출범위다. 중복판정true는예외로직접끝나며hashCalls와saveCalls가0이다. 정상분기는해시와저장으로진행한다. 앞의생성자null실패와호출경로의실패는아래증거에서별도로구분한다. 6노드5간선; 알림연습의외부호출이나트랜잭션원자성을이코드에추가한그림이아니다. 가입 명령 입력 정규화·검사 중복 확인 existsByEmail 해시 생성 원문을 그대로 전달 저장 전 회원 id = 0 · 해시만 보유 저장 결과 새 값의 id > 0 중복 예외 해시·저장 미호출 명령 전달 중복 아님 해시 결과 save 중복

CONSTRUCTOR BOUNDARY

두 역할은 생성 직후부터 non-null이다

  1. members 검사

    Objects.requireNonNull이 저장 역할 누락을 즉시 거절합니다.

  2. passwordHasher 검사

    확장된 PasswordHasher 역할 누락도 같은 생성 경계에서 거절합니다.

  3. 완성된 서비스 전달

    부분 초기화나 setter 호출 순서 없이 두 역할 참조가 고정됩니다.

DUPLICATE PATH

중복 판정은 뒤 협력자 호출을 차단한다

  1. 명령 null 검사

    요청 경계가 잘못된 명령을 먼저 거절합니다.

  2. existsByEmail == true

    저장소가 중복을 보고하면 서비스가 즉시 예외를 선택합니다.

  3. DuplicateEmailException

    해시와 회원 생성·저장 이전에 호출이 끝납니다.

실행 가능한 테스트가 고정하는 경계 증거
관찰 경로 기대 결과 증명하지 않는 것
members = null 서비스 생성 실패 · 메시지 고정 구성 루트의 구현 선택 정확성
hasher = null 서비스 생성 실패 · 메시지 고정 해시 구현의 보안 강도
중복 이메일 existsCalls == 1 · hashCalls == 0 · saveCalls == 0 동시 가입 원자성 · 외부 시스템 롤백

실행 가능한 테스트가 고정하는 경계 증거

members = null

기대 결과: 서비스 생성 실패 · 메시지 고정

증명하지 않는 것: 구성 루트의 구현 선택 정확성

hasher = null

기대 결과: 서비스 생성 실패 · 메시지 고정

증명하지 않는 것: 해시 구현의 보안 강도

중복 이메일

기대 결과: existsCalls == 1 · hashCalls == 0 · saveCalls == 0

증명하지 않는 것: 동시 가입 원자성 · 외부 시스템 롤백

PORT CONTINUITY

기록 대역도 hash + matches를 구현한다

회원가입은 성공 경로에서 hash만 호출하지만 테스트 대역은 앞 문서에서 확장된 포트 전체를 구현합니다.

COMPOSITION HANDOFF

구성 영역이 공유 저장소와 활성 해시 구현을 고른다

다음 문서가 구현을 선택하고 완성된 MemberRegistrationService를 호출자에게 넘깁니다.

existsCalls == 1, hashCalls == 0, saveCalls == 0은 관찰한 중복 단일 호출의 빠른 실패 증거입니다. existsByEmail → save를 트랜잭션이나 동시성 경계로 만들지는 않습니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 이 그림은 생성자에서 필수 역할 두 개를 이미 받은 뒤의 가입 호출 범위다. 중복 판정 true는 예외로 직접 끝나며 hashCalls와 saveCalls가 0이다. 정상 분기는 해시와 저장으로 진행한다. 앞의 생성자 null 실패와 호출 경로의 실패는 아래 증거에서 별도로 구분한다. 6노드와 5간선이다. 알림 연습의 외부 호출이나 트랜잭션 원자성을 이 코드에 추가한 그림이 아니다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1필수 역할과 중복 실패 순서는 생성과 호출 경계에서 고정한다
6중복 판정에서 후속 협력자 호출을 차단하기
7이 그림은 생성자에서 필수 역할 두 개를 이미 받은 뒤의 가입 호출 범위다. 중복 판정 true는 예외로 직접 끝나며 hashCalls와 saveCalls가 0이다. 정상 분기는 해시와 저장으로 진행한다. 앞의 생성자 null 실패와 호출 경로의 실패는 아래 증거에서 별도로 구분한다. 6노드와 5간선이다. 알림 연습의 외부 호출이나 트랜잭션 원자성을 이 코드에 추가한 그림이 아니다.
41중복 판정은 후속 협력자 호출을 차단한다
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
필수 역할 뒤 중복 검사

생성자로 두 역할을 받은 서비스가 가입 중복을 확인합니다.

전제: 필수 두 역할 · 중복: 직접 예외호출 중복 실패를 생성 실패와 구분합니다.
정상 해시 실행

중복이 아닌 입력만 hash를 호출합니다.

중복 시 hashCalls: 0 · 정상: 해시검사를 통과한 값만 해시 경계로 갑니다.
정상 저장

해시를 저장에 전달합니다.

중복 시 saveCalls: 0 · 정상: save실패 뒤 저장 상태를 바꾸지 않습니다.

구현 선택은 구성 루트로 넘긴다

서비스에서 구체 구현 생성을 제거하면 구성 영역이 공유할 MemberRepository와 활성 PasswordHasher를 선택해 완성된 서비스를 전달해야 합니다.

이 문서에서는 구성 코드를 미리 구현하지 않습니다.

다음 문서의 순수 Java BoardConfig 객체 조립에서 현재 MemberRegistrationService와 확장된 해시 계약을 연결하고 main 소스의 PBKDF2 구현을 선택합니다.


연습 문제

회원가입 뒤 환영 알림을 보내되 저장 호출이 실패하면 알림 호출 횟수가 0인지 기록 대역으로 검증하세요.

WelcomeNotifier를 생성자로 받고 구현 선택을 구성 영역으로 넘기되, 이 호출 순서만으로 저장과 알림 사이의 트랜잭션 원자성이 생긴다고 주장하지 마세요.

기록 대역의 실패 조건을 고정하기

앞 연습에서는 중복 검사 결과를 false로, 해시 결과를 유효한 고정 문자열로 둡니다. 저장 대역은 saveCalls를 한 번 올린 뒤 실제 상태를 변경하거나 회원을 반환하기 전에 예외를 던지게 하세요. 알림 대역은 호출 횟수만 기록하며, 저장 성공 결과를 받은 뒤에만 호출하는 순서를 사용합니다.

저장 실패와 알림 실패를 구분한 풀이

이 조건의 저장 실패에서는 중복 검사 1회·해시 1회·저장 1회·알림 0회입니다. 저장이 정상 반환하지 않아 뒤 알림으로 진행하지 않습니다. 이 기록은 정한 호출 경로를 검증합니다. 모든 저장소가 예외를 던지기 전 상태를 바꾸지 않는다는 일반 보장은 아닙니다.

반대로 저장이 성공한 뒤 알림 대역이 예외를 던지는 별도 경우에는 저장 1회·알림 1회이고 이미 저장된 회원은 자동으로 롤백되지 않습니다. 두 경우를 다른 테스트로 두어, 호출 순서와 저장·외부 발송의 원자성을 구분합니다.