자바 디자인 패턴 용어 사전
실무 적용인터셉터 · 필터 체인 · VirtualFilterChain · proceed()

필터·인터셉터 체인

리스트 + 인덱스 + 다음 호출. 서블릿·스프링 시큐리티·스프링 AOP·OkHttp·Netty가 같은 코드를 쓴다.

Chain of Responsibility가 실무에서 나타나는 가장 흔한 형태. 겉보기에 전혀 다른 프레임워크들이 사실상 같은 코드를 쓴다.

공통 뼈대

리스트 + 현재 위치 인덱스 + '다음을 부르는' 메서드

  • proceed() {
    • if (인덱스가 끝) → 진짜 대상을 호출
    • 다음 요소를 꺼내고 인덱스++
    • 그 요소에게 나(this)를 넘기며 실행
  • }

실제 소스 — 스프링 시큐리티

private static final class VirtualFilterChain implements FilterChain {
    private int currentPosition = 0;

    public void doFilter(ServletRequest request, ServletResponse response) {
        if (this.currentPosition == this.size) {
            this.originalChain.doFilter(request, response);   // 사슬 끝
            return;
        }
        this.currentPosition++;
        Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
        nextFilter.doFilter(request, response, this);         // 자기 자신을 넘긴다
    }
}

실제 소스 — 스프링 AOP

public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex ==
            this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint();          // 어드바이스가 끝났다 → 실제 메서드
    }
    Object advice = this.interceptorsAndDynamicMethodMatchers
            .get(++this.currentInterceptorIndex);
    return ((MethodInterceptor) advice).invoke(this);
}

구조가 동일하다. @Transactional·@Cacheable이 전부 이 위에서 돈다.

this를 넘기는가

각 요소는 "다음이 누구인지" 몰라도 된다. 체인 객체만 들고 있으면 된다 — 이 한 줄이 요소끼리의 결합을 0으로 만든다. 순서를 바꿔도, 중간에 하나를 빼도 나머지 코드는 변하지 않는다.

순서가 곧 의미다

스프링 시큐리티에서 ExceptionTranslationFilter 는 AuthorizationFilter 보다 '앞' 에 있다

  • 뒤에서 던져진 AccessDeniedException 을 잡아
    • 401/403 응답이나 로그인 리다이렉트로 바꿔야 하기 때문이다
  • 뒤에 있었다면 예외가 톰캣까지 올라가 500 이 된다

실무 함정

  • ① 다음을 안 불러 요청이 증발한다

    • 의도한 차단이면 정상이지만, 예외 경로에서 실수로 빠지면
    • 응답이 비어 있고 로그도 남지 않는다
  • ② 응답이 이미 커밋된 뒤 헤더를 건드린다

    • 앞 요소가 flush 했으면 뒤에서 setHeader 가 무시되거나 예외가 난다
  • ③ 예외 처리 위치

    • 사슬은 호출 스택이므로 예외는 바깥으로 올라간다
    • 공통 예외 처리는 가장 바깥 요소에 둔다

OkHttp — 위치가 동작을 정한다

interceptors += client.interceptors            // ① 애플리케이션 인터셉터
interceptors += RetryAndFollowUpInterceptor()  // ② 재시도·리다이렉트
interceptors += BridgeInterceptor()            // ③ 헤더 보정·gzip 해제
interceptors += CacheInterceptor()             // ④ 캐시 판정
interceptors += ConnectInterceptor             // ⑤ 연결 확보
interceptors += client.networkInterceptors     // ⑥ 네트워크 인터셉터
interceptors += CallServerInterceptor          // ⑦ 실제 송수신
  • 애플리케이션 인터셉터(①) — 재시도·리다이렉트와 무관하게 1번 호출

    • 캐시 히트여도 호출된다
  • 네트워크 인터셉터(⑥) — 실제 요청마다 호출 (리다이렉트 3번이면 3번)

    • 캐시 히트면 ④에서 끊겨 아예 실행되지 않는다
  • "로그가 안 찍힌다" 의 원인이 대개 이 위치 선택이다

방향이 둘인 사슬 — Netty

p.addLast("1", InboundHandlerA); p.addLast("2", InboundHandlerB); p.addLast("3", OutboundHandlerA); p.addLast("4", OutboundHandlerB); p.addLast("5", InboundOutboundHandlerX);

  • 인바운드(수신) — 1 → 2 → 5 (3·4는 건너뛴다)
  • 아웃바운드(송신) 5 → 4 → 3 — (1·2를 건너뛰고 역방향)

하나의 리스트인데 방향에 따라 다른 부분집합이 실행된다. CoR 변형 중 가장 헷갈리는 형태다.

면접 함정

  • "필터와 인터셉터는 같다" → 서블릿 필터는 컨테이너가, 스프링 인터셉터는 디스패처가 호출한다. 예외 처리 범위가 다르다.
  • "순서는 상관없다" → 순서가 곧 동작 정의다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 실무 적용 심화 — 프레임워크 소스로 읽는 패턴