표준 함수형 인터페이스
JDK 표준 함수형 인터페이스를 입력과 출력, 부수 효과, 지연 생성, 동일 타입 연산으로 분류하고 함수 조합·단락 평가·메모이제이션의 실제 의미를 검증합니다.
표준 함수형 인터페이스는 이름을 외우기보다 데이터 흐름으로 읽습니다.
Function은 입력을 다른 값으로 바꿉니다.
Consumer는 값을 받아 부수 효과를 수행하고 반환하지 않습니다.
Supplier는 입력 없이 값을 제공합니다.
Predicate는 boolean 결과를 만듭니다.
UnaryOperator와 BinaryOperator는 입력과 결과 타입이 같은 Function, BiFunction의 특수 형태입니다.
표준 타입을 선택하면 Stream, Optional, 컬렉션의 기본 메서드와 자연스럽게 연결되고 팀원이 함수 시그니처를 바로 예상할 수 있습니다.
하지만 검사 예외, 규칙 ID, 설명, 실행 문맥처럼 추가 계약이 필요하면 사용자 정의 인터페이스나 래퍼 레코드를 사용합니다.
표준화는 정보 손실을 감수하는 축약이 되어서는 안 됩니다.
중복 사용자 인터페이스
다음 예제는 입력 하나와 결과 하나라는 같은 구조를 Parser, Formatter, Mapper로 반복합니다.
도메인 의미가 정말 다르면 이름이 도움이 되지만 조합 API를 각각 다시 작성해야 합니다.
단순 변환만 필요하다면 Function<T, R> 하나로 충분합니다.
public final class DuplicateFunctionalTypes {
@FunctionalInterface
interface Parser<T, R> {
R parse(T value);
}
@FunctionalInterface
interface Formatter<T, R> {
R format(T value);
}
@FunctionalInterface
interface Mapper<T, R> {
R map(T value);
}
public static void main(String[] args) {
Parser<String, Integer> parser = Integer::parseInt;
Formatter<Integer, String> formatter = value -> "#" + value;
Mapper<String, String> mapper = String::trim;
System.out.println(formatter.format(parser.parse(mapper.map(" 7 "))));
}
}반대로 Consumer<Order>라는 타입만 보고 감사인지 저장인지 메시지 발행인지 알 수 없는 경우가 있습니다.
매개변수 이름과 메서드 이름으로 충분히 설명되면 표준 타입을 유지할 수 있습니다.
실행 보장과 재시도 정책까지 달라지면 명령 추상화를 별도로 만듭니다.
표준 인터페이스의 기본 메서드도 의미가 있습니다.
Function의 compose는 인수 함수를 먼저, andThen은 현재 함수를 먼저 실행합니다.
Predicate의 and와 or는 단락 평가합니다.
Consumer의 andThen은 첫 소비자가 예외를 던지면 뒤 소비자를 실행하지 않습니다.
표준 인터페이스 조합
아래 프로그램은 원시 입력을 정규화하고 파싱한 뒤 연산자로 조정합니다.
Predicate가 통과한 값만 Consumer에 전달하며 Supplier가 추적 ID를 늦게 만듭니다.
각 함수 값은 한 책임만 가져 실행 순서를 main에서 읽을 수 있습니다.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
public final class StandardFunctionToolkit {
record Reading(String traceId, int value) {
}
public static void main(String[] args) {
Function<String, String> normalize = String::trim;
Function<String, Integer> parse = Integer::parseInt;
Function<String, Integer> normalizedNumber = normalize.andThen(parse);
UnaryOperator<Integer> calibrate = value -> value + 2;
Predicate<Integer> accepted = value -> value >= 0 && value <= 100;
AtomicInteger sequence = new AtomicInteger();
Supplier<String> traceId = () -> "trace-" + sequence.incrementAndGet();
List<Reading> sink = new ArrayList<>();
Consumer<Integer> store = value -> sink.add(new Reading(traceId.get(), value));
for (String raw : List.of(" 10 ", " 120 ", " -3 ", " 40 ")) {
int value = calibrate.apply(normalizedNumber.apply(raw));
if (accepted.test(value)) {
store.accept(value);
}
}
System.out.println(sink);
System.out.println("generatedIds=" + sequence.get());
}
}Supplier를 반복문 밖에서 String id = traceId.get()으로 평가했다면 모든 판독값이 같은 ID를 공유합니다.
이 예제는 승인된 값마다 store 안에서 traceId.get()을 호출하므로 ID가 하나씩 증가합니다. 이 동작은 해당 람다의 구현이며 모든 Supplier의 공통 보장은 아닙니다.
지연은 비용을 피하는 도구이면서 관찰 시점을 정하는 계약입니다.
예시 예제의 입력 10·120·-3·40은 trim·parse 뒤 보정으로 12·122·-1·42가 됩니다. 범위 0..100을 통과하는 12와 42에서만 store를 호출하므로 trace-1·trace-2 두 ID를 생성합니다. 거절한 122와 -1에서는 traceId.get()을 호출하지 않습니다. Supplier를 바깥에서 한 번 평가하는 경우와 달라지는 지점은 호출 위치입니다.
Consumer는 완료를 알리는 값이 없습니다.
호출자는 Consumer의 정상 반환과 예외를 구별할 수 있지만, 저장 결과를 값이나 상태 코드로 전달해야 한다면 Function이나 별도 결과형을 가진 명령이 적합합니다.
반환을 버리기 위해 Consumer를 선택하지 않습니다.
지연 실행과 메모이제이션
Supplier는 get()으로 결과를 제공한다는 계약이며 매번 재계산하거나 새 객체를 반환한다고 보장하지 않습니다. 아래 source는 호출될 때마다 본문을 실행합니다.
메모이제이션 래퍼는 첫 성공을 캐시해 이후 호출이 같은 값을 보게 합니다.
여러 스레드가 같은 캐시를 사용하려면 초기화와 값 게시를 동기화해야 합니다.
다음 구현은 synchronized get()으로 호출을 직렬화하고 첫 non-null 성공 값을 저장합니다. 예외와 null 실패는 캐시하지 않아 다음 호출이 다시 시도합니다. source가 같은 캐시를 재귀적으로 호출하는 재진입까지 막는 상태는 두지 않았습니다.
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public final class LazySupplierMemoizer {
static <T> Supplier<T> memoizeSuccessful(Supplier<? extends T> source) {
Objects.requireNonNull(source);
return new Supplier<>() {
private boolean initialized;
private T value;
@Override
public synchronized T get() {
if (!initialized) {
T created = Objects.requireNonNull(source.get(), "supplier returned null");
value = created;
initialized = true;
}
return value;
}
};
}
public static void main(String[] args) {
AtomicInteger calls = new AtomicInteger();
Supplier<String> source = () -> {
int call = calls.incrementAndGet();
if (call == 1) {
throw new IllegalStateException("temporary failure");
}
return "value-" + call;
};
Supplier<String> cached = memoizeSuccessful(source);
try {
cached.get();
} catch (IllegalStateException exception) {
System.out.println(exception.getMessage());
}
System.out.println(cached.get());
System.out.println(cached.get());
System.out.println("sourceCalls=" + calls.get());
}
}실패를 캐시할지 재시도할지는 유스케이스 정책입니다.
구성 파싱처럼 같은 입력으로 영원히 실패한다면 실패 캐시가 낫습니다.
외부 서비스 일시 장애라면 재시도가 필요할 수 있습니다.
독립 확인: source의 첫 호출이 예외 대신 null을 반환하고 두 번째부터 value-2를 반환한다고 바꿉니다. cached.get()을 세 번 호출하되 첫 예외는 잡습니다. source 호출 수와 두 번째·세 번째 반환값, initialized의 변화를 쓰세요.
풀이 확인
첫 null은 Objects.requireNonNull에서 NullPointerException이 되고 initialized는 false입니다. 다음 get은 source를 다시 불러 value-2를 저장하고 true로 바꿉니다. 세 번째는 저장한 value-2를 반환해 source는 총 2회입니다. 이 래퍼는 non-null 성공만 캐시하는 정책이며 null을 정상 결과로 캐시하는 모든 Supplier의 공통 규칙이 아닙니다.
성공한 값만 저장하는 세 번의 호출
미초기화 get에서 source 예외/null은 캐시를 채우지 않고 성공한 값만 저장하여 이후 get은 source 없이 반환합니다. 호출 수와 initialized의 실제 전후가 핵심입니다.
성공한 값만 저장하는 세 번의 호출
성공 값의 캐시
성공한 값만 저장하는 세 번의 호출
첫 실패는 초기화하지 않습니다. 두 번째 호출의 value-2를 저장한 뒤 세 번째 호출은 원본 source를 다시 부르지 않습니다.
첫 번째 호출
- 원본 source 호출
- 1회째: 예외 발생
- 초기화 상태와 결과
- initialized=false 유지; temporary failure
두 번째 호출
- 원본 source 호출
- 2회째: value-2 생성
- 초기화 상태와 결과
- initialized=true; value-2 반환
세 번째 호출
- 원본 source 호출
- 호출하지 않음
- 초기화 상태와 결과
- 저장된 value-2 반환
원문 main의 연속 호출을 따라간 표입니다. null 거절은 이 래퍼의 선택이며, 별도 initialized 플래그는 캐시된 null과 미계산 상태도 구분할 수 있습니다.
source는 총 두 번 호출됩니다. synchronized get과 null 거절은 이 래퍼의 정책입니다.
이 예의 조건
- 성공한 non-null 값만 캐시에 저장한다.
- source 예외와 null 실패는 독립적인 미초기화 경로이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | initialized가 true면 저장한 값을 바로 반환하고 false면 source를 호출한다. source 예외와 null 거절은 초기화를 완료하지 않고 실패한다. non-null 성공이면 value를 저장하고 initialized=true로 바꾼다. 화살표는 원 synchronized get 내부의 제어 흐름이며 실제 다중 스레드 경쟁 시험을 나타내지 않는다. 모바일도 같은 7개 노드와 8개 관계, 성공/두 실패 분기를 유지한다. |
| 6 | non-null 성공한 값? |
| 구분 | 내용 |
|---|---|
| 설명 | initialized가 true면 저장한 값을 바로 반환하고 false면 source를 호출한다. source 예외와 null 거절은 초기화를 완료하지 않고 실패한다. non-null 성공이면 value를 저장하고 initialized=true로 바꾼다. 화살표는 원 synchronized get 내부의 제어 흐름이며 실제 다중 스레드 경쟁 시험을 나타내지 않는다. 모바일도 같은 7개 노드와 8개 관계, 성공/두 실패 분기를 유지한다. |
| 전체 읽기 정보 | 성공한 값만 저장하는 세 번의 호출 성공한 값만 저장하는 세 번의 호출 성공한 값만 저장하는 세 번의 호출 성공 값의 캐시 성공한 값만 저장하는 세 번의 호출 첫 실패는 초기화하지 않습니다. 두 번째 호출의 value-2를 저장한 뒤 세 번째 호출은 원본 source를 다시 부르지 않습니다. 첫 번째 호출 원본 source 호출 1회째: 예외 발생 초기화 상태와 결과 initialized=false 유지; temporary failure 두 번째 호출 원본 source 호출 2회째: value-2 생성 초기화 상태와 결과 initialized=true; value-2 반환 세 번째 호출 원본 source 호출 호출하지 않음 초기화 상태와 결과 저장된 value-2 반환 원문 main의 연속 호출을 따라간 표입니다. null 거절은 이 래퍼의 선택이며, 별도 initialized 플래그는 캐시된 null과 미계산 상태도 구분할 수 있습니다. source는 총 두 번 호출됩니다. synchronized get과 null 거절은 이 래퍼의 정책입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 호출 준비 initialized=false라 source를 호출합니다. |
|
|
| 실패 유지 source 예외 또는 null 거절은 초기화를 끝내지 못합니다. |
|
|
| 성공 저장 다음 non-null 결과를 저장합니다. |
|
|
| 재사용 이후 get은 source를 생략합니다. |
|
|
성공한 값만 저장하는 세 번의 호출
미초기화 get에서 source 예외/null은 캐시를 채우지 않고 성공한 값만 저장하여 이후 get은 source 없이 반환합니다. 호출 수와 initialized의 실제 전후가 핵심입니다.
성공한 값만 저장하는 세 번의 호출
성공 값의 캐시
성공한 값만 저장하는 세 번의 호출
첫 실패는 초기화하지 않습니다. 두 번째 호출의 value-2를 저장한 뒤 세 번째 호출은 원본 source를 다시 부르지 않습니다.
첫 번째 호출
- 원본 source 호출
- 1회째: 예외 발생
- 초기화 상태와 결과
- initialized=false 유지; temporary failure
두 번째 호출
- 원본 source 호출
- 2회째: value-2 생성
- 초기화 상태와 결과
- initialized=true; value-2 반환
세 번째 호출
- 원본 source 호출
- 호출하지 않음
- 초기화 상태와 결과
- 저장된 value-2 반환
원문 main의 연속 호출을 따라간 표입니다. null 거절은 이 래퍼의 선택이며, 별도 initialized 플래그는 캐시된 null과 미계산 상태도 구분할 수 있습니다.
source는 총 두 번 호출됩니다. synchronized get과 null 거절은 이 래퍼의 정책입니다.
이 예의 조건
- 성공한 non-null 값만 캐시에 저장한다.
- source 예외와 null 실패는 독립적인 미초기화 경로이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | initialized가 true면 저장한 값을 바로 반환하고 false면 source를 호출한다. source 예외와 null 거절은 초기화를 완료하지 않고 실패한다. non-null 성공이면 value를 저장하고 initialized=true로 바꾼다. 화살표는 원 synchronized get 내부의 제어 흐름이며 실제 다중 스레드 경쟁 시험을 나타내지 않는다. 모바일도 같은 7개 노드와 8개 관계, 성공/두 실패 분기를 유지한다. |
| 6 | non-null 성공한 값? |
| 구분 | 내용 |
|---|---|
| 설명 | initialized가 true면 저장한 값을 바로 반환하고 false면 source를 호출한다. source 예외와 null 거절은 초기화를 완료하지 않고 실패한다. non-null 성공이면 value를 저장하고 initialized=true로 바꾼다. 화살표는 원 synchronized get 내부의 제어 흐름이며 실제 다중 스레드 경쟁 시험을 나타내지 않는다. 모바일도 같은 7개 노드와 8개 관계, 성공/두 실패 분기를 유지한다. |
| 전체 읽기 정보 | 성공한 값만 저장하는 세 번의 호출 성공한 값만 저장하는 세 번의 호출 성공한 값만 저장하는 세 번의 호출 성공 값의 캐시 성공한 값만 저장하는 세 번의 호출 첫 실패는 초기화하지 않습니다. 두 번째 호출의 value-2를 저장한 뒤 세 번째 호출은 원본 source를 다시 부르지 않습니다. 첫 번째 호출 원본 source 호출 1회째: 예외 발생 초기화 상태와 결과 initialized=false 유지; temporary failure 두 번째 호출 원본 source 호출 2회째: value-2 생성 초기화 상태와 결과 initialized=true; value-2 반환 세 번째 호출 원본 source 호출 호출하지 않음 초기화 상태와 결과 저장된 value-2 반환 원문 main의 연속 호출을 따라간 표입니다. null 거절은 이 래퍼의 선택이며, 별도 initialized 플래그는 캐시된 null과 미계산 상태도 구분할 수 있습니다. source는 총 두 번 호출됩니다. synchronized get과 null 거절은 이 래퍼의 정책입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 호출 준비 initialized=false라 source를 호출합니다. |
|
|
| 실패 유지 source 예외 또는 null 거절은 초기화를 끝내지 못합니다. |
|
|
| 성공 저장 다음 non-null 결과를 저장합니다. |
|
|
| 재사용 이후 get은 source를 생략합니다. |
|
|
Predicate 평가와 진단
predicateA.and(predicateB)는 boolean 결과만 필요할 때 간결합니다.
어떤 규칙이 실패했는지 사용자에게 보여 줘야 한다면 Predicate만으로 정보가 부족합니다.
Rule(name, predicate) 목록을 순회해 실패 이름을 모을 수 있습니다.
모든 규칙 평가가 안전하고 부수 효과가 없어야 순서 변경과 전체 수집이 예측 가능합니다.
Operator는 동일 타입을 유지합니다.
문자열 normalize 체인이나 불변 값 갱신에 UnaryOperator가 맞고, 같은 타입 둘을 합치는 축약에는 BinaryOperator가 맞습니다.
결합법칙이 필요한 병렬 reduce에서 비결합 연산자를 사용하면 순서에 따라 결과가 달라질 수 있습니다.
연습 문제
회원 가입 입력에 ID가 비어 있는지, 비밀번호가 짧은지, 이메일 도메인이 허용되는지 검사하는 규칙을 적용합니다.
Rule<T>은 이름과 Predicate를 가집니다.
빠른 실패와 전체 수집 두 실행 모드를 모두 제공하고 실제로 평가한 규칙 이름을 함께 반환합니다.
해답 보기
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
public final class PredicateRuleSetSolution {
record Signup(String id, String password, String email) {
}
record Rule<T>(String name, Predicate<T> predicate) {
}
record Validation(List<String> evaluated, List<String> failed) {
Validation {
evaluated = List.copyOf(evaluated);
failed = List.copyOf(failed);
}
}
static <T> Validation validate(List<Rule<T>> rules, T value, boolean failFast) {
List<String> evaluated = new ArrayList<>();
List<String> failed = new ArrayList<>();
for (Rule<T> rule : rules) {
evaluated.add(rule.name());
if (!rule.predicate().test(value)) {
failed.add(rule.name());
if (failFast) {
break;
}
}
}
return new Validation(evaluated, failed);
}
public static void main(String[] args) {
List<Rule<Signup>> rules = List.of(
new Rule<>("id-required", signup -> signup.id() != null && !signup.id().isBlank()),
new Rule<>("password-min-8", signup -> signup.password() != null
&& signup.password().length() >= 8),
new Rule<>("company-email", signup -> signup.email() != null
&& signup.email().endsWith("@example.com")));
Signup invalid = new Signup("", "short", "[email protected]");
System.out.println(validate(rules, invalid, true));
System.out.println(validate(rules, invalid, false));
}
}같은 가입 입력에서 달라지는 규칙 평가 범위
동일한 invalid 입력에서 failFast는 첫 id 실패 후 멈추고 전체 평가 모드는 세 실패를 모읍니다. 평가한 규칙과 누적 실패 목록을 나란히 비교합니다.
같은 가입 입력에서 달라지는 규칙 평가 범위
규칙 평가의 범위
같은 가입 입력에서 달라지는 규칙 평가 범위
같은 invalid 입력이라도 빠른 실패는 첫 규칙에서 멈추고, 전체 수집은 세 규칙을 모두 평가합니다.
빠른 실패
- 평가한 규칙
- id-required
- 실패로 수집한 규칙 수
- 1개
전체 수집
- 평가한 규칙
- id-required, password-min-8, company-email
- 실패로 수집한 규칙 수
- 3개
풀이의 invalid 입력은 세 규칙을 모두 어깁니다. 이 결과는 해당 목록과 조건식에서 도출한 것이며, 조건식이 예외를 던지는 경우까지 위반 목록으로 바꾸지는 않습니다.
이 입력은 세 규칙 모두 실패합니다. 평가한 규칙과 실패한 규칙을 따로 보관하는 계약을 유지합니다.
이 예의 조건
- 원 invalid의 id="", password="short", email="[email protected]"은 세 규칙 모두 false입니다. 두 validate 호출의 모드를 같은 평가 단계마다 비교합니다.
- id가 정상인 독립 문제의 evaluated 2대3과 구분합니다. 던진 예외를 failed로 변환하는 catch가 없습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원 invalid는 id·password·email 세 조건 모두 false다. failFast=true는 첫 id 실패 뒤 중단하며 false는 세 조건을 모두 평가한다. evaluated는 검사한 규칙이고 failed는 false였던 규칙이다. 이 그림의 1대3은 원 invalid 조건에 한정하고 별도 id가 정상인 독립 문제의 2대3과 구분한다. 던진 예외를 위반으로 변환하는 catch는 없다. 모바일도 같은 6개 노드와 5개 관계이다. |
| 구분 | 내용 |
|---|---|
| 설명 | 원 invalid는 id·password·email 세 조건 모두 false다. failFast=true는 첫 id 실패 뒤 중단하며 false는 세 조건을 모두 평가한다. evaluated는 검사한 규칙이고 failed는 false였던 규칙이다. 이 그림의 1대3은 원 invalid 조건에 한정하고 별도 id가 정상인 독립 문제의 2대3과 구분한다. 던진 예외를 위반으로 변환하는 catch는 없다. 모바일도 같은 6개 노드와 5개 관계이다. |
| 전체 읽기 정보 | 같은 가입 입력에서 달라지는 규칙 평가 범위 같은 가입 입력에서 달라지는 규칙 평가 범위 같은 가입 입력에서 달라지는 규칙 평가 범위 규칙 평가의 범위 같은 가입 입력에서 달라지는 규칙 평가 범위 같은 invalid 입력이라도 빠른 실패는 첫 규칙에서 멈추고, 전체 수집은 세 규칙을 모두 평가합니다. 빠른 실패 평가한 규칙 id-required 실패로 수집한 규칙 수 1개 전체 수집 평가한 규칙 id-required, password-min-8, company-email 실패로 수집한 규칙 수 3개 풀이의 invalid 입력은 세 규칙을 모두 어깁니다. 이 결과는 해당 목록과 조건식에서 도출한 것이며, 조건식이 예외를 던지는 경우까지 위반 목록으로 바꾸지는 않습니다. 이 입력은 세 규칙 모두 실패합니다. 평가한 규칙과 실패한 규칙을 따로 보관하는 계약을 유지합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 모드의 첫 규칙 평가 failFast true와 false 모두 id-required를 먼저 평가해 실패를 기록합니다. |
|
|
| 남은 규칙 실행 또는 생략 true는 첫 실패에서 break하고 false는 password와 email 규칙도 평가합니다. |
|
|
| 각 Validation 반환 두 모드가 실제 평가 목록과 false 목록을 수정할 수 없도록 복사하여 반환합니다. |
|
|
같은 가입 입력에서 달라지는 규칙 평가 범위
동일한 invalid 입력에서 failFast는 첫 id 실패 후 멈추고 전체 평가 모드는 세 실패를 모읍니다. 평가한 규칙과 누적 실패 목록을 나란히 비교합니다.
같은 가입 입력에서 달라지는 규칙 평가 범위
규칙 평가의 범위
같은 가입 입력에서 달라지는 규칙 평가 범위
같은 invalid 입력이라도 빠른 실패는 첫 규칙에서 멈추고, 전체 수집은 세 규칙을 모두 평가합니다.
빠른 실패
- 평가한 규칙
- id-required
- 실패로 수집한 규칙 수
- 1개
전체 수집
- 평가한 규칙
- id-required, password-min-8, company-email
- 실패로 수집한 규칙 수
- 3개
풀이의 invalid 입력은 세 규칙을 모두 어깁니다. 이 결과는 해당 목록과 조건식에서 도출한 것이며, 조건식이 예외를 던지는 경우까지 위반 목록으로 바꾸지는 않습니다.
이 입력은 세 규칙 모두 실패합니다. 평가한 규칙과 실패한 규칙을 따로 보관하는 계약을 유지합니다.
이 예의 조건
- 원 invalid의 id="", password="short", email="[email protected]"은 세 규칙 모두 false입니다. 두 validate 호출의 모드를 같은 평가 단계마다 비교합니다.
- id가 정상인 독립 문제의 evaluated 2대3과 구분합니다. 던진 예외를 failed로 변환하는 catch가 없습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원 invalid는 id·password·email 세 조건 모두 false다. failFast=true는 첫 id 실패 뒤 중단하며 false는 세 조건을 모두 평가한다. evaluated는 검사한 규칙이고 failed는 false였던 규칙이다. 이 그림의 1대3은 원 invalid 조건에 한정하고 별도 id가 정상인 독립 문제의 2대3과 구분한다. 던진 예외를 위반으로 변환하는 catch는 없다. 모바일도 같은 6개 노드와 5개 관계이다. |
| 구분 | 내용 |
|---|---|
| 설명 | 원 invalid는 id·password·email 세 조건 모두 false다. failFast=true는 첫 id 실패 뒤 중단하며 false는 세 조건을 모두 평가한다. evaluated는 검사한 규칙이고 failed는 false였던 규칙이다. 이 그림의 1대3은 원 invalid 조건에 한정하고 별도 id가 정상인 독립 문제의 2대3과 구분한다. 던진 예외를 위반으로 변환하는 catch는 없다. 모바일도 같은 6개 노드와 5개 관계이다. |
| 전체 읽기 정보 | 같은 가입 입력에서 달라지는 규칙 평가 범위 같은 가입 입력에서 달라지는 규칙 평가 범위 같은 가입 입력에서 달라지는 규칙 평가 범위 규칙 평가의 범위 같은 가입 입력에서 달라지는 규칙 평가 범위 같은 invalid 입력이라도 빠른 실패는 첫 규칙에서 멈추고, 전체 수집은 세 규칙을 모두 평가합니다. 빠른 실패 평가한 규칙 id-required 실패로 수집한 규칙 수 1개 전체 수집 평가한 규칙 id-required, password-min-8, company-email 실패로 수집한 규칙 수 3개 풀이의 invalid 입력은 세 규칙을 모두 어깁니다. 이 결과는 해당 목록과 조건식에서 도출한 것이며, 조건식이 예외를 던지는 경우까지 위반 목록으로 바꾸지는 않습니다. 이 입력은 세 규칙 모두 실패합니다. 평가한 규칙과 실패한 규칙을 따로 보관하는 계약을 유지합니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 모드의 첫 규칙 평가 failFast true와 false 모두 id-required를 먼저 평가해 실패를 기록합니다. |
|
|
| 남은 규칙 실행 또는 생략 true는 첫 실패에서 break하고 false는 password와 email 규칙도 평가합니다. |
|
|
| 각 Validation 반환 두 모드가 실제 평가 목록과 false 목록을 수정할 수 없도록 복사하여 반환합니다. |
|
|
해답은 Predicate를 그대로 재사용하면서 규칙 이름을 별도 메타데이터로 보존합니다.
메시지 다국어화나 필드 위치가 필요하면 이름 대신 오류 코드와 메시지 인수를 반환하는 규칙 계약으로 확장합니다.
독립 확인: id만 정상인 Signup("U-1", "short", "[email protected]")을 기존 규칙 목록 순서로 검사합니다. 빠른 실패와 전체 수집의 evaluated와 failed를 따로 구하세요.
풀이 확인
빠른 실패는 evaluated=[id-required, password-min-8], failed=[password-min-8]입니다. 첫 규칙은 통과하고 둘째가 실패해 끝나므로 이메일을 검사하지 않습니다. 전체 수집은 evaluated에 세 이름이 모두 있고 failed=[password-min-8, company-email]입니다. 이 계산은 조건식이 정상 boolean을 반환하는 전제이며 던진 예외를 failed 목록으로 자동 변환하지 않습니다.