본문 바로가기

제네릭 함수와 변성

제네릭 함수의 입력·출력 타입 조합에서 무공변성이 만드는 제약을 확인하고 하한 입력·상한 출력, 타입 추론, 와일드카드 캡처로 안전한 재사용 범위를 넓힙니다.

Function<Dog, Animal>과 Function<Animal, Dog>은 타입 인수 관계만 보고 서로 대입할 수 없습니다.

Java 제네릭은 기본적으로 무공변입니다.

하지만 알고리즘이 함수에 Dog를 넣기만 한다면 Animal을 받을 수 있는 함수도 안전합니다.

알고리즘이 함수의 결과를 Animal로 읽기만 한다면 Dog를 반환하는 함수도 안전합니다.

이 방향을 와일드카드로 표현합니다.

함수형 매개변수에서 입력은 소비되고 출력은 생산됩니다.

Function<? super T, ? extends R>은 T 또는 그 부모를 받을 수 있고 R 또는 그 자식을 반환할 수 있다는 뜻입니다.

흔히 PECS라 부르는 생산자 extends, 소비자 super가 한 함수 시그니처 안에 동시에 나타납니다.


비매개변수화 Function과 형변환

다음 파이프라인은 원시 타입을 사용해 컴파일러 검사를 포기합니다.

String 길이 함수 뒤에 Integer 제곱 함수는 우연히 맞지만, 순서를 바꾸거나 다른 함수를 넣어도 컴파일될 수 있고 ClassCastException이 실행 중 발생합니다.

@SuppressWarnings는 안전성을 만드는 것이 아니라 경고를 숨길 뿐입니다.

bad/RawFunctionChain.java
import java.util.List;
import java.util.function.Function;

public final class RawFunctionChain {
    @SuppressWarnings({ "rawtypes", "unchecked" })
    static Object applyAll(Object input, List<Function> functions) {
        Object current = input;
        for (Function function : functions) {
            current = function.apply(current);
        }
        return current;
    }

    public static void main(String[] args) {
        Function length = (Function<String, Integer>) String::length;
        Function square = (Function<Integer, Integer>) value -> value * value;
        System.out.println(applyAll("java", List.of(length, square)));
    }
}

타입 지정 파이프라인은 각 연결점의 중간 타입을 컴파일러가 알아야 합니다.

길이가 String에서 Integer로, 제곱이 Integer에서 Integer로 이어진다는 정보가 있으면 잘못된 순서를 컴파일 단계에서 막습니다.

이종 함수 목록은 원소마다 타입이 다르므로 단순 List<Function<?, ?>>로 저장한 뒤 안전하게 연속 호출할 수 없습니다.

조합할 때 타입 정보를 보존한 하나의 Function으로 접는 편이 낫습니다.

와일드카드는 값을 넣을 수 있는 범위를 제한합니다.

List<? extends Animal>에서는 Animal을 읽을 수 있지만 어떤 구체 하위 타입 목록인지 모르므로 null 외에 새 Animal을 넣을 수 없습니다.

List<? super Dog>에는 Dog를 넣을 수 있지만 읽을 때는 Object로만 보장됩니다.

함수 매개변수에서도 같은 원리가 적용됩니다.


소비자 super·생산자 extends

아래 map()은 Dog 목록에 Animal을 받는 설명 함수를 재사용합니다. 출력의 ? extends R은 결과 타입 R의 하위 타입을 반환하는 함수를 허용합니다. String 자체는 final 클래스이므로 그 하위 클래스가 있는 것은 아닙니다.

compose()는 첫 함수의 결과가 다음 함수의 입력으로 안전하게 전달되도록 와일드카드 방향을 정합니다.

src/VarianceFunctionComposition.java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public final class VarianceFunctionComposition {
    sealed interface Animal permits Dog, Cat {
        String name();
    }

    record Dog(String name, int tricks) implements Animal {
    }

    record Cat(String name) implements Animal {
    }

    static <T, R> List<R> map(
            List<? extends T> source,
            Function<? super T, ? extends R> mapper) {
        List<R> result = new ArrayList<>(source.size());
        for (T value : source) {
            result.add(mapper.apply(value));
        }
        return List.copyOf(result);
    }

    static <A, B, C> Function<A, C> compose(
            Function<? super A, ? extends B> first,
            Function<? super B, ? extends C> second) {
        return value -> second.apply(first.apply(value));
    }

    public static void main(String[] args) {
        List<Dog> dogs = List.of(new Dog("Bori", 4), new Dog("Maro", 2));
        Function<Animal, String> describe = animal -> animal.getClass().getSimpleName()
                + ":" + animal.name();
        Function<CharSequence, Integer> length = CharSequence::length;
        Function<Dog, Integer> descriptionLength = compose(describe, length);

        System.out.println(map(dogs, describe));
        System.out.println(map(dogs, descriptionLength));
        System.out.println(map(List.<Animal>of(new Cat("Nabi")), describe));
    }
}

넓은 입력 함수와 구체적인 결과 타입을 반환하는 함수를 값의 이동 방향으로 읽는다

Dog Bori가 Animal 입력 함수로 Dog:Bori가 된 뒤 CharSequence 길이 함수에서 Integer 8이 됩니다. 고정 입력·현재 함수·출력의 값과 타입을 함께 확인하는 합성 예제입니다.

넓은 입력 함수와 구체 결과 함수를 값의 이동 방향으로 읽는다

VarianceFunctionComposition의 descriptionLength 합성을 해석합니다.

값과 타입의 이동

넓은 입력 함수와 구체 결과 함수를 값의 이동 방향으로 읽는다

Dog → String → Integer로 값이 이동합니다. 각 함수는 전달되는 값보다 넓은 입력 타입을 받을 수 있습니다.

실제 값과 다음 함수가 받을 수 있는 타입원 descriptionLength의 Bori 한 값이 Dog 입력에서 describe, String 결과, length, Integer 결과로 이동한다. Animal을 받는 describe는 Dog를 받고 CharSequence를 받는 length는 String을 받는다. 화살표는 값의 전달 순서이다. 모바일도 같은 5노드4관계이며 원 상세 타입 목록은 보조로 보존한다.
Dog 입력Bori
연결 관계Dog 입력 · Bori → describe · Animal을 받음
describeAnimal을 받음
연결 관계describe · Animal을 받음 → String 결과 · Dog:Bori
String 결과Dog:Bori
연결 관계String 결과 · Dog:Bori → length · CharSequence 입력
lengthCharSequence 입력
연결 관계length · CharSequence 입력 → Integer 결과 · 8
Integer 결과8
  1. 호출자 → describe

    전달되는 타입
    Dog
    원문에서 허용하는 연결
    Animal 입력으로 수용
  2. describe → length

    전달되는 타입
    String
    원문에서 허용하는 연결
    CharSequence 입력으로 수용
  3. length → 호출자

    전달되는 타입
    Integer
    원문에서 허용하는 연결
    Function<Dog, Integer>의 결과

이 클래스의 compose(first, second)는 first 뒤 second를 호출합니다. 인자로 받은 함수를 먼저 실행하는 JDK Function.compose(before)와 이름만으로 순서를 혼동하지 않습니다.

describe는 Animal을 받고 String을 돌려줍니다. length는 CharSequence를 받고 Integer를 돌려줍니다.

이 예의 조건

  • 함수의 값 변환과 입력/출력 타입을 함께 유지한다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원 descriptionLength의 Bori 한 값이 Dog 입력에서 describe, String 결과, length, Integer 결과로 이동한다. Animal을 받는 describe는 Dog를 받고 CharSequence를 받는 length는 String을 받는다. 화살표는 값의 전달 순서이다. 모바일도 같은 5개 노드와 4개 관계이며 원 상세 타입 목록은 보조로 보존한다.
원 도표의 설명과 주석
구분내용
설명원 descriptionLength의 Bori 한 값이 Dog 입력에서 describe, String 결과, length, Integer 결과로 이동한다. Animal을 받는 describe는 Dog를 받고 CharSequence를 받는 length는 String을 받는다. 화살표는 값의 전달 순서이다. 모바일도 같은 5개 노드와 4개 관계이며 원 상세 타입 목록은 보조로 보존한다.
전체 읽기 정보넓은 입력 함수와 구체적인 결과 타입을 반환하는 함수를 값의 이동 방향으로 읽는다 넓은 입력 함수와 구체적인 결과 타입을 반환하는 함수를 값의 이동 방향으로 읽는다 VarianceFunctionComposition의 descriptionLength 합성을 해석합니다. 값과 타입의 이동 넓은 입력 함수와 구체적인 결과 타입을 반환하는 함수를 값의 이동 방향으로 읽는다 Dog → String → Integer로 값이 이동합니다. 각 함수는 전달되는 값보다 넓은 입력 타입을 받을 수 있습니다. 호출자 → describe 전달되는 타입 Dog 원문에서 허용하는 연결 Animal 입력으로 수용 describe → length 전달되는 타입 String 원문에서 허용하는 연결 CharSequence 입력으로 수용 length → 호출자 전달되는 타입 Integer 원문에서 허용하는 연결 Function<Dog, Integer>의 결과 이 클래스의 compose(first, second)는 first 뒤 second를 호출합니다. 인자로 받은 함수를 먼저 실행하는 JDK Function.compose(before)와 이름만으로 순서를 혼동하지 않습니다. describe는 Animal을 받고 String을 돌려줍니다. length는 CharSequence를 받고 Integer를 돌려줍니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력

Dog Bori를 합성 함수에 전달합니다.

  • 값: Dog Bori
  • 정적 호출 입력: Dog
  • Animal을 받는 describe가 하위 Dog 입력을 수용한다.
첫 변환

describe가 문자열을 반환합니다.

  • 현재 함수: Animal → String
  • 값: Dog:Bori
  • 첫 출력 String이 다음 CharSequence 입력과 호환된다.
둘째 변환

length가 문자열 길이를 반환합니다.

  • 현재 함수: CharSequence → Integer
  • 값: 8
  • 첫 함수의 결과를 다음 함수에 전달해 합성 출력 Integer를 만든다.

넓은 입력 함수와 구체적인 결과 타입을 반환하는 함수를 값의 이동 방향으로 읽는다

Dog Bori가 Animal 입력 함수로 Dog:Bori가 된 뒤 CharSequence 길이 함수에서 Integer 8이 됩니다. 고정 입력·현재 함수·출력의 값과 타입을 함께 확인하는 합성 예제입니다.

넓은 입력 함수와 구체 결과 함수를 값의 이동 방향으로 읽는다

VarianceFunctionComposition의 descriptionLength 합성을 해석합니다.

값과 타입의 이동

넓은 입력 함수와 구체 결과 함수를 값의 이동 방향으로 읽는다

Dog → String → Integer로 값이 이동합니다. 각 함수는 전달되는 값보다 넓은 입력 타입을 받을 수 있습니다.

실제 값과 다음 함수가 받을 수 있는 타입원 descriptionLength의 Bori 한 값이 Dog 입력에서 describe, String 결과, length, Integer 결과로 이동한다. Animal을 받는 describe는 Dog를 받고 CharSequence를 받는 length는 String을 받는다. 화살표는 값의 전달 순서이다. 모바일도 같은 5노드4관계이며 원 상세 타입 목록은 보조로 보존한다.
Dog 입력Bori
연결 관계Dog 입력 · Bori → describe · Animal을 받음
describeAnimal을 받음
연결 관계describe · Animal을 받음 → String 결과 · Dog:Bori
String 결과Dog:Bori
연결 관계String 결과 · Dog:Bori → length · CharSequence 입력
lengthCharSequence 입력
연결 관계length · CharSequence 입력 → Integer 결과 · 8
Integer 결과8
  1. 호출자 → describe

    전달되는 타입
    Dog
    원문에서 허용하는 연결
    Animal 입력으로 수용
  2. describe → length

    전달되는 타입
    String
    원문에서 허용하는 연결
    CharSequence 입력으로 수용
  3. length → 호출자

    전달되는 타입
    Integer
    원문에서 허용하는 연결
    Function<Dog, Integer>의 결과

이 클래스의 compose(first, second)는 first 뒤 second를 호출합니다. 인자로 받은 함수를 먼저 실행하는 JDK Function.compose(before)와 이름만으로 순서를 혼동하지 않습니다.

describe는 Animal을 받고 String을 돌려줍니다. length는 CharSequence를 받고 Integer를 돌려줍니다.

이 예의 조건

  • 함수의 값 변환과 입력/출력 타입을 함께 유지한다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원 descriptionLength의 Bori 한 값이 Dog 입력에서 describe, String 결과, length, Integer 결과로 이동한다. Animal을 받는 describe는 Dog를 받고 CharSequence를 받는 length는 String을 받는다. 화살표는 값의 전달 순서이다. 모바일도 같은 5개 노드와 4개 관계이며 원 상세 타입 목록은 보조로 보존한다.
원 도표의 설명과 주석
구분내용
설명원 descriptionLength의 Bori 한 값이 Dog 입력에서 describe, String 결과, length, Integer 결과로 이동한다. Animal을 받는 describe는 Dog를 받고 CharSequence를 받는 length는 String을 받는다. 화살표는 값의 전달 순서이다. 모바일도 같은 5개 노드와 4개 관계이며 원 상세 타입 목록은 보조로 보존한다.
전체 읽기 정보넓은 입력 함수와 구체적인 결과 타입을 반환하는 함수를 값의 이동 방향으로 읽는다 넓은 입력 함수와 구체적인 결과 타입을 반환하는 함수를 값의 이동 방향으로 읽는다 VarianceFunctionComposition의 descriptionLength 합성을 해석합니다. 값과 타입의 이동 넓은 입력 함수와 구체적인 결과 타입을 반환하는 함수를 값의 이동 방향으로 읽는다 Dog → String → Integer로 값이 이동합니다. 각 함수는 전달되는 값보다 넓은 입력 타입을 받을 수 있습니다. 호출자 → describe 전달되는 타입 Dog 원문에서 허용하는 연결 Animal 입력으로 수용 describe → length 전달되는 타입 String 원문에서 허용하는 연결 CharSequence 입력으로 수용 length → 호출자 전달되는 타입 Integer 원문에서 허용하는 연결 Function<Dog, Integer>의 결과 이 클래스의 compose(first, second)는 first 뒤 second를 호출합니다. 인자로 받은 함수를 먼저 실행하는 JDK Function.compose(before)와 이름만으로 순서를 혼동하지 않습니다. describe는 Animal을 받고 String을 돌려줍니다. length는 CharSequence를 받고 Integer를 돌려줍니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력

Dog Bori를 합성 함수에 전달합니다.

  • 값: Dog Bori
  • 정적 호출 입력: Dog
  • Animal을 받는 describe가 하위 Dog 입력을 수용한다.
첫 변환

describe가 문자열을 반환합니다.

  • 현재 함수: Animal → String
  • 값: Dog:Bori
  • 첫 출력 String이 다음 CharSequence 입력과 호환된다.
둘째 변환

length가 문자열 길이를 반환합니다.

  • 현재 함수: CharSequence → Integer
  • 값: 8
  • 첫 함수의 결과를 다음 함수에 전달해 합성 출력 Integer를 만든다.

map(List<? extends T>, ...)의 원본 와일드카드는 호출자가 Dog 목록을 Animal 관점으로 전달할 수 있게 합니다.

메서드 본문은 원소를 T로 읽을 뿐 원본에 새 값을 넣지 않습니다.

지나치게 유연한 시그니처가 항상 필요한 것은 아닙니다.

비공개 도우미는 단순 List<T>로 충분할 수 있고, 재사용 경계에서 실제 호출자 요구가 나타날 때 변성을 확장합니다.

Function.andThen()과 compose()의 JDK 시그니처도 같은 방향을 따릅니다.

타입 오류가 길게 보일 때는 중간 함수를 타입 지정 지역 변수로 나눠 컴파일러가 어느 연결에서 실패했는지 확인합니다.

예시 예제의 Bori는 Dog이므로 Animal을 받는 describe에 넣을 수 있습니다. 반환 문자열은 Dog:Bori이고, String이 CharSequence를 구현하므로 length에 전달할 수 있습니다. 길이는 Dog:의 4글자와 Bori의 4글자를 더한 8입니다. Maro도 8이며, 실제 결과 목록은 [8, 8]입니다. 중간 값의 타입과 다음 함수가 받을 수 있는 타입을 맞추는 것이 합성의 조건입니다.

독립 확인: 같은 Dog 레코드의 이름만 Bo로 바꿉니다. describe와 length의 입력·출력 타입, 최종 값을 각각 쓰세요. 예제 클래스의 compose(describe, length)를 length.compose(describe)와 비교할 때 호출 순서는 어떻게 되나요?

풀이 확인

Dog를 describe가 Animal 입력으로 받고 String인 Dog:Bo를 반환합니다. length는 그 값을 CharSequence 입력으로 받아 Integer인 6을 반환합니다. 둘 다 describe 다음 length 순서입니다. 예제 클래스의 compose(first, second)는 first 다음 second이며, JDK length.compose(before)는 before 다음 length이기 때문입니다. 이름만 보고 서로 반대 순서를 만들지 않습니다.

JDK 21 Function 계약은 compose와 andThen의 평가 순서, 입력의 super·결과의 extends 방향을 확인할 때 참고할 수 있습니다.


타입 추론

제네릭 메서드를 호출하면 컴파일러는 실제 인수, 람다 본문, 대입 대상에서 타입 제약을 모아 T와 R을 추론합니다.

문맥이 충분하지 않거나 서로 모순되면 명시적 타입 인수나 타입 지정 변수가 필요합니다.

형변환으로 강제하기 전에 API의 매개변수 관계가 올바른지 봅니다.

다음 예제에서 transform()의 I는 원본 원소로, O는 람다 결과와 대입 대상으로 정해집니다.

choose()는 두 후보의 공통 상위 타입을 찾습니다.

구체 타입이 필요한 경우 GenericMethodInference.<Number>choose()처럼 의도를 밝힐 수 있습니다.

src/GenericMethodInference.java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public final class GenericMethodInference {
    static <I, O> List<O> transform(List<I> source, Function<? super I, ? extends O> mapper) {
        List<O> result = new ArrayList<>();
        for (I input : source) {
            result.add(mapper.apply(input));
        }
        return List.copyOf(result);
    }

    static <T> T choose(boolean first, T left, T right) {
        return first ? left : right;
    }

    record Code(String value) {
    }

    public static void main(String[] args) {
        List<Code> codes = transform(List.of("A", "B"), Code::new);
        List<CharSequence> labels = transform(codes, code -> "code=" + code.value());
        Number inferred = choose(true, 10, 2.5);
        Number explicit = GenericMethodInference.<Number>choose(false, 10, 2.5);
        System.out.println(codes);
        System.out.println(labels);
        System.out.println(inferred.getClass().getSimpleName());
        System.out.println(explicit);
    }
}

람다 매개변수에 명시적 타입을 쓰면 추론에 제약을 추가합니다.

이는 오류를 설명하거나 오버로드된 메서드를 선택할 때 유용하지만, 구현 세부 타입을 너무 구체적으로 적으면 재사용 범위를 좁힐 수 있습니다.

목표 타입을 먼저 변수로 선언하는 방식이 복잡한 호출식 안의 형변환보다 읽기 좋습니다.


와일드카드 캡처

List<?>의 원소 타입은 “아무 타입”이 아니라 “하나이지만 현재 이름을 모르는 타입”입니다.

도우미 메서드에 넘기면 컴파일러가 그 타입을 캡처해 같은 원소를 읽고 다시 넣는 연산을 안전하게 허용할 수 있습니다.

서로 다른 List<?> 두 개는 캡처가 다르므로 임의로 값을 이동할 수 없습니다.

함수 레지스트리처럼 이종 타입을 한 컬렉션에 담으면 어느 지점에서는 실행 시점 타입 토큰과 검증된 형변환이 필요할 수 있습니다.

형변환을 호출자 전체에 흩뜨리지 말고 레지스트리 내부 한 곳에서 Class<T> 검사를 거쳐 격리합니다.

타입 소거 때문에 Class<List<String>>를 직접 얻을 수 없다는 한계도 기억합니다.


연습 문제

서로 다른 이벤트 타입을 하나의 라우터에 등록합니다.

subscribe(Class<E>, Consumer<? super E>)와 publish(Object)를 제공하고, unchecked 형변환은 비공개 메서드 한 곳에만 둡니다.

등록할 때 이벤트 타입과 처리기를 묶고 게시할 때 같은 실행 클래스의 목록을 선택합니다. 이벤트에 대한 클래스 검사와 처리기 타입 관계를 유지하는 책임을 구분합니다.

해답 보기
src/TypedEventRouterSolution.java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

public final class TypedEventRouterSolution {
    static final class EventRouter {
        private final Map<Class<?>, List<Consumer<?>>> handlers = new LinkedHashMap<>();

        <E> void subscribe(Class<E> eventType, Consumer<? super E> handler) {
            handlers.computeIfAbsent(eventType, ignored -> new ArrayList<>()).add(handler);
        }

        void publish(Object event) {
            List<Consumer<?>> selected = handlers.getOrDefault(event.getClass(), List.of());
            for (Consumer<?> handler : List.copyOf(selected)) {
                invokeChecked(event.getClass(), handler, event);
            }
        }

        private static <E> void invokeChecked(
                Class<E> eventType,
                Consumer<?> rawHandler,
                Object event) {
            E checkedEvent = eventType.cast(event);
            @SuppressWarnings("unchecked")
            Consumer<? super E> handler = (Consumer<? super E>) rawHandler;
            handler.accept(checkedEvent);
        }
    }

    record UserCreated(String id) {
    }

    record OrderPaid(long orderId) {
    }

    public static void main(String[] args) {
        EventRouter router = new EventRouter();
        List<String> audit = new ArrayList<>();
        router.subscribe(UserCreated.class, event -> audit.add("user:" + event.id()));
        router.subscribe(OrderPaid.class, event -> audit.add("order:" + event.orderId()));
        router.publish(new UserCreated("U-1"));
        router.publish(new OrderPaid(42));
        router.publish("ignored");
        System.out.println(audit);
    }
}

처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다

게시 객체의 정확한 실행 클래스와 일치하는 등록된 Class<E> 키를 찾고 목록 복사·Class.cast 뒤 handler를 호출합니다. 키 선택과 호출 직전 타입 검사의 실제 순서가 독자에게 도움이 됩니다.

처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다

TypedEventRouterSolution의 등록·선택·호출 경계를 구분합니다.

등록에서 호출까지

처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다

등록 계약이 이벤트 클래스와 처리기의 타입 관계를 보존합니다. 게시할 때는 정확히 같은 실행 클래스의 목록을 선택합니다.

등록해 보관한 타입 관계와 게시할 때의 선택등록은 클래스 토큰과 handler 타입 관계를 같은 API에서 받아 해당 Map키의 목록에 보관한다. 게시한 event의 정확한 실행 클래스로 목록을 골라 복사하고 각 처리기에 Class.cast한 event를 전달한다. 화살표는 보관·입력·호출 관계이며 두 actor를 무조건 시간순 사슬로 합치지 않는다. 등록되지 않은 정확한 키는 빈 목록이고 Class.cast가 처리기의 제네릭 타입까지 검사하지는 않는다. 모바일도 같은 6노드5관계이다.등록 계약Class<E>·handlerMap의 해당 키처리기 목록게시 event실행 클래스정확한 키 선택목록을 복사Class.castevent 클래스 검사handler.accept검사한 event
등록해 보관한 타입 관계와 게시할 때의 선택등록은 클래스 토큰과 handler 타입 관계를 같은 API에서 받아 해당 Map키의 목록에 보관한다. 게시한 event의 정확한 실행 클래스로 목록을 골라 복사하고 각 처리기에 Class.cast한 event를 전달한다. 화살표는 보관·입력·호출 관계이며 두 actor를 무조건 시간순 사슬로 합치지 않는다. 등록되지 않은 정확한 키는 빈 목록이고 Class.cast가 처리기의 제네릭 타입까지 검사하지는 않는다. 모바일도 같은 6노드5관계이다.등록 계약Class<E>·handlerMap의 해당 키처리기 목록게시 event실행 클래스정확한 키 선택목록을 복사Class.castevent 클래스 검사handler.accept검사한 event
  1. subscribe

    원문의 검사 또는 선택
    Class<E>와 Consumer<? super E>를 함께 받음
    안전성이 의존하는 조건
    제네릭 등록 API의 타입 관계 유지
  2. publish

    원문의 검사 또는 선택
    event.getClass()와 정확히 같은 키만 선택
    안전성이 의존하는 조건
    부모 타입 처리기를 자동으로 찾지 않음
  3. invokeChecked

    원문의 검사 또는 선택
    클래스 토큰으로 event를 cast한 뒤 처리기 호출
    안전성이 의존하는 조건
    cast는 event를 검사하며 처리기 제네릭 타입을 검사하지 않음

Class.cast는 이벤트 객체를 검사합니다. 지워진 처리기의 제네릭 타입까지 실행 시 검사하는 장치는 아닙니다.

이 예의 조건

  • 등록·게시 관계이며 두 actor를 무조건 하나의 실행 순서로 합치지 않습니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2등록은 클래스 토큰과 handler 타입 관계를 같은 API에서 받아 해당 Map 키의 목록에 보관한다. 게시한 event의 정확한 실행 클래스로 목록을 골라 복사하고 각 처리기에 Class.cast한 event를 전달한다. 화살표는 보관·입력·호출 관계이며 두 actor를 무조건 시간순으로 이어 붙이지 않는다. 등록되지 않은 정확한 키는 빈 목록이고 Class.cast가 처리기의 제네릭 타입까지 검사하지는 않는다. 모바일도 같은 6개 노드와 5개 관계이다.
원 도표의 설명과 주석
구분내용
설명등록은 클래스 토큰과 handler 타입 관계를 같은 API에서 받아 해당 Map 키의 목록에 보관한다. 게시한 event의 정확한 실행 클래스로 목록을 골라 복사하고 각 처리기에 Class.cast한 event를 전달한다. 화살표는 보관·입력·호출 관계이며 두 actor를 무조건 시간순으로 이어 붙이지 않는다. 등록되지 않은 정확한 키는 빈 목록이고 Class.cast가 처리기의 제네릭 타입까지 검사하지는 않는다. 모바일도 같은 6개 노드와 5개 관계이다.
전체 읽기 정보처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다 처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다 TypedEventRouterSolution의 등록·선택·호출 경계를 구분합니다. 등록에서 호출까지 처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다 등록 계약이 이벤트 클래스와 처리기의 타입 관계를 보존합니다. 게시할 때는 정확히 같은 실행 클래스의 목록을 선택합니다. subscribe 원문의 검사 또는 선택 Class<E>와 Consumer<? super E>를 함께 받음 안전성이 의존하는 조건 제네릭 등록 API의 타입 관계 유지 publish 원문의 검사 또는 선택 event.getClass()와 정확히 같은 키만 선택 안전성이 의존하는 조건 부모 타입 처리기를 자동으로 찾지 않음 invokeChecked 원문의 검사 또는 선택 클래스 토큰으로 event를 cast한 뒤 처리기 호출 안전성이 의존하는 조건 cast는 event를 검사하며 처리기 제네릭 타입을 검사하지 않음 Class.cast는 이벤트 객체를 검사합니다. 지워진 처리기의 제네릭 타입까지 실행 시 검사하는 장치는 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
등록 계약

Class<E>와 handler의 관계를 받아 해당 Map 키 목록에 보관합니다.

  • 키: 클래스 토큰
  • 목록: 타입에 맞는 handler
  • 등록 API가 보관 타입 관계의 출발점입니다.
게시 선택

event의 정확한 실행 클래스로 목록을 찾아 복사합니다.

  • 등록 없음: 빈 목록
  • 선택: 정확한 키
  • 상위 타입 키를 자동 선택하는 계약이 아닙니다.
검사 후 전달

Class.cast로 검사한 event를 handler.accept에 넘깁니다.

  • 검사: event 클래스
  • 검사 범위: handler 제네릭 전체 아님
  • 클래스 검사와 등록 시 타입 관계를 함께 사용합니다.

처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다

게시 객체의 정확한 실행 클래스와 일치하는 등록된 Class<E> 키를 찾고 목록 복사·Class.cast 뒤 handler를 호출합니다. 키 선택과 호출 직전 타입 검사의 실제 순서가 독자에게 도움이 됩니다.

처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다

TypedEventRouterSolution의 등록·선택·호출 경계를 구분합니다.

등록에서 호출까지

처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다

등록 계약이 이벤트 클래스와 처리기의 타입 관계를 보존합니다. 게시할 때는 정확히 같은 실행 클래스의 목록을 선택합니다.

등록해 보관한 타입 관계와 게시할 때의 선택등록은 클래스 토큰과 handler 타입 관계를 같은 API에서 받아 해당 Map키의 목록에 보관한다. 게시한 event의 정확한 실행 클래스로 목록을 골라 복사하고 각 처리기에 Class.cast한 event를 전달한다. 화살표는 보관·입력·호출 관계이며 두 actor를 무조건 시간순 사슬로 합치지 않는다. 등록되지 않은 정확한 키는 빈 목록이고 Class.cast가 처리기의 제네릭 타입까지 검사하지는 않는다. 모바일도 같은 6노드5관계이다.등록 계약Class<E>·handlerMap의 해당 키처리기 목록게시 event실행 클래스정확한 키 선택목록을 복사Class.castevent 클래스 검사handler.accept검사한 event
등록해 보관한 타입 관계와 게시할 때의 선택등록은 클래스 토큰과 handler 타입 관계를 같은 API에서 받아 해당 Map키의 목록에 보관한다. 게시한 event의 정확한 실행 클래스로 목록을 골라 복사하고 각 처리기에 Class.cast한 event를 전달한다. 화살표는 보관·입력·호출 관계이며 두 actor를 무조건 시간순 사슬로 합치지 않는다. 등록되지 않은 정확한 키는 빈 목록이고 Class.cast가 처리기의 제네릭 타입까지 검사하지는 않는다. 모바일도 같은 6노드5관계이다.등록 계약Class<E>·handlerMap의 해당 키처리기 목록게시 event실행 클래스정확한 키 선택목록을 복사Class.castevent 클래스 검사handler.accept검사한 event
  1. subscribe

    원문의 검사 또는 선택
    Class<E>와 Consumer<? super E>를 함께 받음
    안전성이 의존하는 조건
    제네릭 등록 API의 타입 관계 유지
  2. publish

    원문의 검사 또는 선택
    event.getClass()와 정확히 같은 키만 선택
    안전성이 의존하는 조건
    부모 타입 처리기를 자동으로 찾지 않음
  3. invokeChecked

    원문의 검사 또는 선택
    클래스 토큰으로 event를 cast한 뒤 처리기 호출
    안전성이 의존하는 조건
    cast는 event를 검사하며 처리기 제네릭 타입을 검사하지 않음

Class.cast는 이벤트 객체를 검사합니다. 지워진 처리기의 제네릭 타입까지 실행 시 검사하는 장치는 아닙니다.

이 예의 조건

  • 등록·게시 관계이며 두 actor를 무조건 하나의 실행 순서로 합치지 않습니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2등록은 클래스 토큰과 handler 타입 관계를 같은 API에서 받아 해당 Map 키의 목록에 보관한다. 게시한 event의 정확한 실행 클래스로 목록을 골라 복사하고 각 처리기에 Class.cast한 event를 전달한다. 화살표는 보관·입력·호출 관계이며 두 actor를 무조건 시간순으로 이어 붙이지 않는다. 등록되지 않은 정확한 키는 빈 목록이고 Class.cast가 처리기의 제네릭 타입까지 검사하지는 않는다. 모바일도 같은 6개 노드와 5개 관계이다.
원 도표의 설명과 주석
구분내용
설명등록은 클래스 토큰과 handler 타입 관계를 같은 API에서 받아 해당 Map 키의 목록에 보관한다. 게시한 event의 정확한 실행 클래스로 목록을 골라 복사하고 각 처리기에 Class.cast한 event를 전달한다. 화살표는 보관·입력·호출 관계이며 두 actor를 무조건 시간순으로 이어 붙이지 않는다. 등록되지 않은 정확한 키는 빈 목록이고 Class.cast가 처리기의 제네릭 타입까지 검사하지는 않는다. 모바일도 같은 6개 노드와 5개 관계이다.
전체 읽기 정보처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다 처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다 TypedEventRouterSolution의 등록·선택·호출 경계를 구분합니다. 등록에서 호출까지 처리기 타입의 근거는 등록 계약과 클래스별 보관 규칙이다 등록 계약이 이벤트 클래스와 처리기의 타입 관계를 보존합니다. 게시할 때는 정확히 같은 실행 클래스의 목록을 선택합니다. subscribe 원문의 검사 또는 선택 Class<E>와 Consumer<? super E>를 함께 받음 안전성이 의존하는 조건 제네릭 등록 API의 타입 관계 유지 publish 원문의 검사 또는 선택 event.getClass()와 정확히 같은 키만 선택 안전성이 의존하는 조건 부모 타입 처리기를 자동으로 찾지 않음 invokeChecked 원문의 검사 또는 선택 클래스 토큰으로 event를 cast한 뒤 처리기 호출 안전성이 의존하는 조건 cast는 event를 검사하며 처리기 제네릭 타입을 검사하지 않음 Class.cast는 이벤트 객체를 검사합니다. 지워진 처리기의 제네릭 타입까지 실행 시 검사하는 장치는 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
등록 계약

Class<E>와 handler의 관계를 받아 해당 Map 키 목록에 보관합니다.

  • 키: 클래스 토큰
  • 목록: 타입에 맞는 handler
  • 등록 API가 보관 타입 관계의 출발점입니다.
게시 선택

event의 정확한 실행 클래스로 목록을 찾아 복사합니다.

  • 등록 없음: 빈 목록
  • 선택: 정확한 키
  • 상위 타입 키를 자동 선택하는 계약이 아닙니다.
검사 후 전달

Class.cast로 검사한 event를 handler.accept에 넘깁니다.

  • 검사: event 클래스
  • 검사 범위: handler 제네릭 전체 아님
  • 클래스 검사와 등록 시 타입 관계를 함께 사용합니다.

리스너 목록은 복사해서 순회하지만 Map과 등록 동작에는 동기화가 없습니다. 이 복사만으로 동시 등록·게시가 안전해지는 것은 아니며, 처리기가 던진 예외도 따로 격리하지 않습니다.

하위 클래스 이벤트를 부모 처리기에도 전달하려면 대입 가능성 순회와 중복 순서 정책을 별도로 설계해야 합니다.

독립 확인: 기존 두 등록에 Object.class 처리기를 추가하고 UserCreated("U-2"), "ignored", null을 각각 별도로 게시합니다. UserCreated와 문자열을 Object 처리기에 전달하나요? null은 빈 처리기 목록으로 끝나나요?

풀이 확인

UserCreated는 정확한 UserCreated.class 목록만 골라 기존 사용자 처리기를 호출하고 Object 처리기는 호출하지 않습니다. String.class 등록이 없으므로 문자열도 Object 처리기에 전달되지 않습니다. null은 getClass()를 호출하는 곳에서 NullPointerException을 던져 목록 선택에 도달하지 않습니다. Class.cast는 선택한 이벤트의 클래스 검사이며 등록 때 유지한 handler 타입 관계를 대신 검사하지 않습니다. 이 조건은 순차 호출이고 동시 게시 안전성을 증명한 실험은 아닙니다.