본문 바로가기

filter·map 조합

단계별 즉시 컬렉션과 한 번의 융합 반복문을 비교하고, 명령형·선언형 표현에서 실행 순서·중간 값·오류 위치·가변 누산기가 어떻게 달라지는지 분석합니다.

필터 결과를 map에 넘기면 “조건을 통과한 원소를 다른 표현으로 바꾼다”는 파이프라인을 만듭니다.

두 메서드를 이어 쓰는 코드는 단계 이름이 보이고 중간 결과를 조사하기 쉽습니다.

대신 필터 목록과 map 목록을 각각 할당합니다.

한 반복문에서 조건과 변환을 연속 수행하면 할당을 줄이지만 두 단계 의미를 본문 안에 다시 섞을 수 있습니다.

명령형과 선언형은 선악 구도가 아닙니다.

명령형 반복문은 제어 흐름과 변경 위치를 직접 보여 주고, 선언형 파이프라인은 무엇을 선택하고 변환하는지 연산 이름으로 보여 줍니다.

실패 처리, 단락 평가, 성능 요구에 따라 더 설명 가능한 형태를 고릅니다.


공유 가변 결과

다음 처리기는 필드 목록에 결과를 계속 추가합니다.

같은 인스턴스로 두 요청을 처리하면 두 번째 결과에 첫 번째 값이 남습니다.

반환한 가변 목록을 호출자가 수정하면 처리기 내부 상태까지 바뀝니다.

반복문 자체보다 누산기 소유권이 문제입니다.

bad/SharedMutablePipelineResult.java
import java.util.ArrayList;
import java.util.List;

public final class SharedMutablePipelineResult {
    private final List<String> output = new ArrayList<>();

    List<String> process(List<Integer> values) {
        for (Integer value : values) {
            if (value > 0) {
                output.add("N" + (value * 2));
            }
        }
        return output;
    }

    public static void main(String[] args) {
        SharedMutablePipelineResult processor = new SharedMutablePipelineResult();
        System.out.println(processor.process(List.of(1, -1)));
        System.out.println(processor.process(List.of(2)));
    }
}

누산기는 한 호출 안에서 만들고 결과를 스냅샷으로 반환합니다.

파이프라인 객체 자체가 재사용 가능하다면 연산 메타데이터는 불변으로 보관하고 실행 상태는 최종 호출마다 새로 만듭니다.

이 구분은 여러 스레드가 같은 파이프라인 정의를 공유할 때도 중요합니다.

필터와 map 함수가 가변 외부 상태를 캡처하면 지역 결과를 사용해도 호출 간 영향이 남습니다.

순수 함수를 강제할 수는 없지만 API 문서에서 비간섭과 무상태 동작을 요구할 수 있습니다.


즉시 파이프라인

다음 예제는 제네릭 필터와 map을 연결하고 각 단계 결과를 record에 담습니다.

디버깅과 교육에서는 어느 원소가 탈락하고 어떤 값으로 변환됐는지 쉽게 봅니다.

각 단계는 호출 중 자신의 입력을 순회합니다. filter는 원본을, map은 선택된 목록을 처리하며 콜백 예외가 발생하면 해당 단계에서 중단합니다.

src/EagerFilterMapPipeline.java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

public final class EagerFilterMapPipeline {
    record PipelineResult<T, R>(List<T> filtered, List<R> mapped) {
        PipelineResult {
            filtered = List.copyOf(filtered);
            mapped = List.copyOf(mapped);
        }
    }

    static <T> List<T> filter(List<T> source, Predicate<? super T> predicate) {
        List<T> result = new ArrayList<>();
        for (T value : source) {
            if (predicate.test(value)) {
                result.add(value);
            }
        }
        return result;
    }

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

    static <T, R> PipelineResult<T, R> run(
            List<T> source,
            Predicate<? super T> predicate,
            Function<? super T, ? extends R> mapper) {
        List<T> filtered = filter(source, predicate);
        List<R> mapped = map(filtered, mapper);
        return new PipelineResult<>(filtered, mapped);
    }

    record Order(String id, int total) {
    }

    public static void main(String[] args) {
        PipelineResult<Order, String> result = run(
                List.of(new Order("A", 40), new Order("B", 150), new Order("C", 90)),
                order -> order.total() >= 80,
                order -> order.id() + "=" + order.total());
        System.out.println(result.filtered());
        System.out.println(result.mapped());
    }
}

단계 결과 record는 운영 API에 항상 필요한 것은 아닙니다.

교육이나 진단 모드에서 유용하며, 큰 데이터에서는 중간 값을 보관하는 비용이 큽니다.

추적 필요와 실행 비용을 별도 옵션으로 두는 방법도 있습니다.

필터 뒤 map 순서는 map 뒤 필터와 일반적으로 같지 않습니다.

매퍼가 타입을 바꾸면 조건식 자체가 다른 도메인을 보게 되고, 예외나 부수 효과 발생 횟수도 달라집니다.

연산 재정렬은 함수가 순수하더라도 의미 동등성을 증명한 뒤 수행합니다.


연산과 호출 추적

아래 프로그램은 같은 결과를 외부 반복문과 Stream 파이프라인으로 만듭니다.

추적을 남겨 두 형태의 호출 순서를 비교합니다.

선택과 변환을 원소별로 이어 수행하면 중간 필터 목록을 피할 수 있습니다.

src/ImperativeDeclarativeTrace.java
import java.util.ArrayList;
import java.util.List;

public final class ImperativeDeclarativeTrace {
    static List<String> imperative(List<Integer> source, List<String> trace) {
        List<String> result = new ArrayList<>();
        for (Integer value : source) {
            trace.add("loop-test:" + value);
            if (value % 2 == 0) {
                trace.add("loop-map:" + value);
                result.add("N" + (value * 10));
            }
        }
        return List.copyOf(result);
    }

    static List<String> declarative(List<Integer> source, List<String> trace) {
        return source.stream()
                .filter(value -> {
                    trace.add("stream-test:" + value);
                    return value % 2 == 0;
                })
                .map(value -> {
                    trace.add("stream-map:" + value);
                    return "N" + (value * 10);
                })
                .toList();
    }

    public static void main(String[] args) {
        List<Integer> source = List.of(1, 2, 3, 4);
        List<String> imperativeTrace = new ArrayList<>();
        List<String> streamTrace = new ArrayList<>();
        System.out.println(imperative(source, imperativeTrace));
        System.out.println(declarative(source, streamTrace));
        System.out.println(imperativeTrace);
        System.out.println(streamTrace);
    }
}

두 단계 순회와 융합 순회의 호출 순서

즉시 filter/map 두 순회와 융합 순회는 같은 [N20,N40]을 만들지만 test/map 호출이 섞이는 순서가 다릅니다. 원 코드 trace의 누적을 비교하여 평가 시점을 설명할 수 있습니다.

두 단계 순회와 융합 순회의 호출 순서

두 단계 순회와 융합 순회의 호출 순서

순회 방식과 호출 순서

두 단계 순회와 융합 순회의 호출 순서

결과는 같아도 test와 map의 순서는 다릅니다. 두 단계 순회는 모든 test가 끝난 뒤 map을 시작합니다.

같은 결과를 만드는 두 반복문의 호출 순서짝수 선택과 N+(값×10) 변환을 [1,2,3,4]에 적용하는 별도 비교조건이다. 왼쪽 즉시 두단계는 test 네 번 뒤 map 두 번이고 오른쪽 융합은 2와4가 통과한 즉시 map을 호출한다. 각 영역의 화살표는 그 반복문의 호출·중간값 순서이며 두 영역 사이 호출을 연결하지 않는다. 같은 최종값만으로 예외·부수효과 순서가 같아지지 않으며 모든 Stream실행의 로그보장으로 확대하지 않는다. 모바일도 같은 두 영역과12노드10관계를 유지한다.즉시 두 단계입력 [1,2,3,4]test:1·test:2test:3·test:4선택 목록 [2,4]map:2·map:4결과 [N20,N40]융합 단일 순회입력 [1,2,3,4]test:1→falsetest:2→true곧바로 map:2test:3→falsetest:4→true곧바로 map:4결과 [N20,N40]
같은 결과를 만드는 두 반복문의 호출 순서짝수 선택과 N+(값×10) 변환을 [1,2,3,4]에 적용하는 별도 비교조건이다. 왼쪽 즉시 두단계는 test 네 번 뒤 map 두 번이고 오른쪽 융합은 2와4가 통과한 즉시 map을 호출한다. 각 영역의 화살표는 그 반복문의 호출·중간값 순서이며 두 영역 사이 호출을 연결하지 않는다. 같은 최종값만으로 예외·부수효과 순서가 같아지지 않으며 모든 Stream실행의 로그보장으로 확대하지 않는다. 모바일도 같은 두 영역과12노드10관계를 유지한다.즉시 두 단계입력 [1,2,3,4]test:1·test:2test:3·test:4선택 목록 [2,4]map:2·map:4결과 [N20,N40]융합 단일 순회입력 [1,2,3,4]test:1→falsetest:2→true곧바로 map:2test:3→falsetest:4→true곧바로 map:4결과 [N20,N40]

필터 전체 뒤 매핑

호출 순서
test:1, test:2, test:3, test:4, map:2, map:4
결과
[N20, N40]

한 반복문에서 선택·매핑

호출 순서
test:1, test:2, map:2, test:3, test:4, map:4
결과
[N20, N40]

같은 입력 [1, 2, 3, 4]에 짝수 선택과 N+(값×10) 변환을 적용한 소스 비교입니다. JDK Stream의 부수 효과 순서를 보장하거나 실행 로그를 제시한 표가 아닙니다.

동일 입력 [1, 2, 3, 4]에서 짝수를 선택하고 10을 곱합니다. 서로 다른 콜백 순서로 같은 [N20, N40]을 만듭니다.

이 예의 조건

  • 별도 비교 조건 [1,2,3,4]이며 Stream 전체 로그 보장으로 확대하지 않습니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2짝수 선택과 N+(값×10) 변환을 [1,2,3,4]에 적용하는 별도 비교 조건이다. 왼쪽 즉시 두 단계는 test 네 번 뒤 map 두 번이고 오른쪽 융합은 2와 4가 통과한 즉시 map을 호출한다. 각 영역의 화살표는 그 반복문의 호출·중간값 순서이며 두 영역 사이 호출을 연결하지 않는다. 같은 최종값만으로 예외·부수 효과 순서가 같아지지 않으며 모든 Stream 실행의 로그 보장으로 확대하지 않는다. 모바일도 같은 두 영역과 12개 노드, 10개 관계를 유지한다.
원 도표의 설명과 주석
구분내용
설명짝수 선택과 N+(값×10) 변환을 [1,2,3,4]에 적용하는 별도 비교 조건이다. 왼쪽 즉시 두 단계는 test 네 번 뒤 map 두 번이고 오른쪽 융합은 2와 4가 통과한 즉시 map을 호출한다. 각 영역의 화살표는 그 반복문의 호출·중간값 순서이며 두 영역 사이 호출을 연결하지 않는다. 같은 최종값만으로 예외·부수 효과 순서가 같아지지 않으며 모든 Stream 실행의 로그 보장으로 확대하지 않는다. 모바일도 같은 두 영역과 12개 노드, 10개 관계를 유지한다.
전체 읽기 정보두 단계 순회와 융합 순회의 호출 순서 두 단계 순회와 융합 순회의 호출 순서 두 단계 순회와 융합 순회의 호출 순서 순회 방식과 호출 순서 두 단계 순회와 융합 순회의 호출 순서 결과는 같아도 test와 map의 순서는 다릅니다. 두 단계 순회는 모든 test가 끝난 뒤 map을 시작합니다. 필터 전체 뒤 매핑 호출 순서 test:1, test:2, test:3, test:4, map:2, map:4 결과 [N20, N40] 한 반복문에서 선택·매핑 호출 순서 test:1, test:2, map:2, test:3, test:4, map:4 결과 [N20, N40] 같은 입력 [1, 2, 3, 4]에 짝수 선택과 N+(값×10) 변환을 적용한 소스 비교입니다. JDK Stream의 부수 효과 순서를 보장하거나 실행 로그를 제시한 표가 아닙니다. 동일 입력 [1, 2, 3, 4]에서 짝수를 선택하고 10을 곱합니다. 서로 다른 콜백 순서로 같은 [N20, N40]을 만듭니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
선택 호출 시작

두 반복문은 1을 test하고 2를 test합니다.

  • 즉시 두 단계: 선택한 값 2를 누적
  • 융합: 2가 통과하면 즉시 map 2
  • 융합 순회는 전체 선택이 완료되기 전에 변환합니다.
뒤 입력 평가

3은 제외되고 4는 통과합니다.

  • 두 단계: test 1..4 뒤 [2,4]
  • 융합: test3,test4,map4
  • 같은 조건이라도 부수 효과·예외 순서가 달라집니다.
최종값 비교

두 단계는 map 2, map 4 뒤 같은 목록을 반환합니다.

  • 두 결과: [N20,N40]
  • 호출 순서: 두 단계와 융합이 다름
  • 최종 값 동일성이 평가 순서 동일성을 뜻하지 않습니다.

두 단계 순회와 융합 순회의 호출 순서

즉시 filter/map 두 순회와 융합 순회는 같은 [N20,N40]을 만들지만 test/map 호출이 섞이는 순서가 다릅니다. 원 코드 trace의 누적을 비교하여 평가 시점을 설명할 수 있습니다.

두 단계 순회와 융합 순회의 호출 순서

두 단계 순회와 융합 순회의 호출 순서

순회 방식과 호출 순서

두 단계 순회와 융합 순회의 호출 순서

결과는 같아도 test와 map의 순서는 다릅니다. 두 단계 순회는 모든 test가 끝난 뒤 map을 시작합니다.

같은 결과를 만드는 두 반복문의 호출 순서짝수 선택과 N+(값×10) 변환을 [1,2,3,4]에 적용하는 별도 비교조건이다. 왼쪽 즉시 두단계는 test 네 번 뒤 map 두 번이고 오른쪽 융합은 2와4가 통과한 즉시 map을 호출한다. 각 영역의 화살표는 그 반복문의 호출·중간값 순서이며 두 영역 사이 호출을 연결하지 않는다. 같은 최종값만으로 예외·부수효과 순서가 같아지지 않으며 모든 Stream실행의 로그보장으로 확대하지 않는다. 모바일도 같은 두 영역과12노드10관계를 유지한다.즉시 두 단계입력 [1,2,3,4]test:1·test:2test:3·test:4선택 목록 [2,4]map:2·map:4결과 [N20,N40]융합 단일 순회입력 [1,2,3,4]test:1→falsetest:2→true곧바로 map:2test:3→falsetest:4→true곧바로 map:4결과 [N20,N40]
같은 결과를 만드는 두 반복문의 호출 순서짝수 선택과 N+(값×10) 변환을 [1,2,3,4]에 적용하는 별도 비교조건이다. 왼쪽 즉시 두단계는 test 네 번 뒤 map 두 번이고 오른쪽 융합은 2와4가 통과한 즉시 map을 호출한다. 각 영역의 화살표는 그 반복문의 호출·중간값 순서이며 두 영역 사이 호출을 연결하지 않는다. 같은 최종값만으로 예외·부수효과 순서가 같아지지 않으며 모든 Stream실행의 로그보장으로 확대하지 않는다. 모바일도 같은 두 영역과12노드10관계를 유지한다.즉시 두 단계입력 [1,2,3,4]test:1·test:2test:3·test:4선택 목록 [2,4]map:2·map:4결과 [N20,N40]융합 단일 순회입력 [1,2,3,4]test:1→falsetest:2→true곧바로 map:2test:3→falsetest:4→true곧바로 map:4결과 [N20,N40]

필터 전체 뒤 매핑

호출 순서
test:1, test:2, test:3, test:4, map:2, map:4
결과
[N20, N40]

한 반복문에서 선택·매핑

호출 순서
test:1, test:2, map:2, test:3, test:4, map:4
결과
[N20, N40]

같은 입력 [1, 2, 3, 4]에 짝수 선택과 N+(값×10) 변환을 적용한 소스 비교입니다. JDK Stream의 부수 효과 순서를 보장하거나 실행 로그를 제시한 표가 아닙니다.

동일 입력 [1, 2, 3, 4]에서 짝수를 선택하고 10을 곱합니다. 서로 다른 콜백 순서로 같은 [N20, N40]을 만듭니다.

이 예의 조건

  • 별도 비교 조건 [1,2,3,4]이며 Stream 전체 로그 보장으로 확대하지 않습니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2짝수 선택과 N+(값×10) 변환을 [1,2,3,4]에 적용하는 별도 비교 조건이다. 왼쪽 즉시 두 단계는 test 네 번 뒤 map 두 번이고 오른쪽 융합은 2와 4가 통과한 즉시 map을 호출한다. 각 영역의 화살표는 그 반복문의 호출·중간값 순서이며 두 영역 사이 호출을 연결하지 않는다. 같은 최종값만으로 예외·부수 효과 순서가 같아지지 않으며 모든 Stream 실행의 로그 보장으로 확대하지 않는다. 모바일도 같은 두 영역과 12개 노드, 10개 관계를 유지한다.
원 도표의 설명과 주석
구분내용
설명짝수 선택과 N+(값×10) 변환을 [1,2,3,4]에 적용하는 별도 비교 조건이다. 왼쪽 즉시 두 단계는 test 네 번 뒤 map 두 번이고 오른쪽 융합은 2와 4가 통과한 즉시 map을 호출한다. 각 영역의 화살표는 그 반복문의 호출·중간값 순서이며 두 영역 사이 호출을 연결하지 않는다. 같은 최종값만으로 예외·부수 효과 순서가 같아지지 않으며 모든 Stream 실행의 로그 보장으로 확대하지 않는다. 모바일도 같은 두 영역과 12개 노드, 10개 관계를 유지한다.
전체 읽기 정보두 단계 순회와 융합 순회의 호출 순서 두 단계 순회와 융합 순회의 호출 순서 두 단계 순회와 융합 순회의 호출 순서 순회 방식과 호출 순서 두 단계 순회와 융합 순회의 호출 순서 결과는 같아도 test와 map의 순서는 다릅니다. 두 단계 순회는 모든 test가 끝난 뒤 map을 시작합니다. 필터 전체 뒤 매핑 호출 순서 test:1, test:2, test:3, test:4, map:2, map:4 결과 [N20, N40] 한 반복문에서 선택·매핑 호출 순서 test:1, test:2, map:2, test:3, test:4, map:4 결과 [N20, N40] 같은 입력 [1, 2, 3, 4]에 짝수 선택과 N+(값×10) 변환을 적용한 소스 비교입니다. JDK Stream의 부수 효과 순서를 보장하거나 실행 로그를 제시한 표가 아닙니다. 동일 입력 [1, 2, 3, 4]에서 짝수를 선택하고 10을 곱합니다. 서로 다른 콜백 순서로 같은 [N20, N40]을 만듭니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
선택 호출 시작

두 반복문은 1을 test하고 2를 test합니다.

  • 즉시 두 단계: 선택한 값 2를 누적
  • 융합: 2가 통과하면 즉시 map 2
  • 융합 순회는 전체 선택이 완료되기 전에 변환합니다.
뒤 입력 평가

3은 제외되고 4는 통과합니다.

  • 두 단계: test 1..4 뒤 [2,4]
  • 융합: test3,test4,map4
  • 같은 조건이라도 부수 효과·예외 순서가 달라집니다.
최종값 비교

두 단계는 map 2, map 4 뒤 같은 목록을 반환합니다.

  • 두 결과: [N20,N40]
  • 호출 순서: 두 단계와 융합이 다름
  • 최종 값 동일성이 평가 순서 동일성을 뜻하지 않습니다.

부수 효과를 업무 로직으로 삼거나 다른 최종 연산·병렬 실행에서도 같은 추적을 기대하지 않습니다.


단일 순회 최적화

필터와 map을 한 반복문에 넣으면 중간 목록 하나를 줄입니다.

매퍼는 조건식이 true인 원소에만 호출되어야 합니다. 원소 순서와 콜백 예외 시 중단 정책은 유지할 수 있지만, 필터 전체 뒤 매핑하던 순서를 융합하면 어느 콜백과 예외가 먼저 실행되는지는 달라질 수 있습니다.

결과 용량을 원본 크기로 미리 잡으면 일부만 선택될 때 여유 메모리가 생기므로 데이터 특성에 따라 결정합니다.

병렬 실행까지 염두에 두면 공유 ArrayList에 직접 add하는 구현은 안전하지 않습니다.

각 파티션에 독립 누산기를 만들고 결합하는 축약 계약이 필요합니다.

단일 스레드 최적화를 병렬 안전하다고 확대 해석하지 않습니다.

원래 입력 [1, 2, 3, 4]를 짝수로 선택해 N+(값×10)으로 바꾸면 두 방식의 결과는 [N20, N40]입니다. 하지만 두 단계 방식에서는 test:1·2·3·4가 모두 끝난 뒤 map:2·4이고, 융합 방식에서는 test:2 다음에 map:2를 바로 호출합니다.

독립 확인: test가 값 3에서 P3 예외를, map이 값 2에서 M2 예외를 던진다고 바꿉니다. 두 단계 filter 뒤 map과 원소별 융합 반복문은 각각 어느 예외에서 중단하나요? 콜백이 모두 순수하고 정상 반환할 때 같은 최종값을 만든다는 사실로 이 입력의 실패도 같다고 판단할 수 있나요?

풀이 확인

두 단계 방식은 전체 filter에서 3을 검사하다 P3로 중단해 map을 시작하지 않습니다. 융합 방식은 1을 제외하고 2를 통과시킨 직후 map:2에서 M2로 중단해 test:3을 실행하지 않습니다. 실패한 호출에서는 어느 쪽도 정상 결과 목록을 반환하지 않습니다. 같은 성공값만 비교하면 콜백 평가·실패 순서 차이를 놓칩니다. 이 조건은 명시한 반복문의 흐름이며 다른 최종 연산이나 병렬 Stream의 부수 효과를 일반적으로 보장하지 않습니다.


연습 문제

원본을 한 번만 순회해 조건식을 통과한 원소 수, 매퍼 호출 수, 결과 목록을 반환합니다.

조건식이 false인 원소에는 매퍼를 호출하지 않아야 합니다.

결과와 카운터는 불변 record로 제공합니다.

해답 보기
src/SinglePassCollectSolution.java
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

public final class SinglePassCollectSolution {
    record Collected<R>(int tested, int accepted, int mapped, List<R> values) {
        Collected {
            values = List.copyOf(values);
        }
    }

    static <T, R> Collected<R> collect(
            Iterable<? extends T> source,
            Predicate<? super T> predicate,
            Function<? super T, ? extends R> mapper) {
        List<R> values = new ArrayList<>();
        int tested = 0;
        int accepted = 0;
        int mapped = 0;
        for (T value : source) {
            tested += 1;
            if (!predicate.test(value)) {
                continue;
            }
            accepted += 1;
            values.add(mapper.apply(value));
            mapped += 1;
        }
        return new Collected<>(tested, accepted, mapped, values);
    }

    public static void main(String[] args) {
        Collected<String> result = collect(
                List.of(1, 2, 3, 4, 5),
                value -> value % 2 == 1,
                value -> "odd:" + value);
        System.out.println(result);
    }
}

해답은 Iterable을 받아 임의 접근 가정을 없애고, accepted와 mapped가 같은지 결과로 확인할 수 있게 합니다.

매퍼가 예외를 던지면 record는 반환되지 않으며 이 빠른 실패 정책도 계약에 포함합니다.

독립 확인: source=[0, 1, 2, 3], predicate는 value>=2, mapper는 "v"+value입니다. collect의 tested·accepted·mapped와 values를 구하세요. mapper가 3에서 예외를 던지면 같은 record의 부분 결과를 받을 수 있나요?

풀이 확인

정상 반환 시 tested=4, accepted=2, mapped=2, values=[v2, v3]입니다. 0·1에서는 매퍼를 호출하지 않습니다. 3의 매퍼가 예외를 던지면 생성자 호출까지 도달하지 않아 Collected 자체가 반환되지 않습니다. 호출 추적으로 앞의 v2 계산은 관찰할 수 있어도 부분 record를 정상 반환하는 API로 바뀐 것은 아닙니다.