ADR-0012: Application - FinResponse 파이프라인 타입 제약 계층
맥락과 문제
섹션 제목: “맥락과 문제”유효성 검증 파이프라인에서 검증 실패 시 FinResponse<T>.Fail(error)을 생성해야 하는데, 파이프라인의 TResponse가 object로만 제약되어 있어 (TResponse)(object)FinResponse<T>.Fail(error) 같은 이중 캐스팅이 필요했다. 이 캐스팅은 컴파일러가 검증하지 않으므로, 응답 타입이 FinResponse<T>가 아닌 파이프라인에 잘못 등록되면 런타임에 InvalidCastException이 터졌다. 로깅 파이프라인은 IsSucc/IsFail만 읽으면 되고, 유효성 검증 파이프라인은 실패 응답을 생성해야 하며, 관측성 파이프라인은 에러 상세 정보에 접근해야 하는데, 이 서로 다른 수준의 접근 요구가 하나의 object 캐스팅 안에 뒤섞여 있었다.
각 파이프라인이 필요한 최소 계약만 타입 시그니처로 선언하여, 컴파일 타임에 잘못된 조합을 차단하고 파이프라인의 의도를 코드에서 바로 읽을 수 있는 제약 체계가 필요했다.
검토한 옵션
섹션 제목: “검토한 옵션”- 옵션 1:
object캐스팅 - 옵션 2: 단일 인터페이스로 모든 기능 통합
- 옵션 3: IFinResponse 4단계 인터페이스 계층 +
where제약 - 옵션 4: 리플렉션 기반 동적 디스패치
옵션 3: IFinResponse 4단계 인터페이스 계층 + where 제약을 채택한다.
파이프라인마다 필요한 접근 수준이 다르다. 로깅은 성공/실패 여부만 읽으면 되고, 유효성 검증은 실패 응답을 생성해야 하며, 관측성은 에러 상세 정보에 접근해야 한다. 이 차이를 object 캐스팅으로 뭉뚱그리는 대신, 4단계 인터페이스로 분리하여 각 파이프라인이 where TResponse : 제약으로 최소 계약만 선언하게 한다.
- IFinResponse (마커): 응답이 FinResponse 계열임을 표시. 가장 느슨한 제약으로, 모든 FinResponse 파이프라인의 기본 진입점.
- IFinResponseCovariant (공변):
IsSucc/IsFail성공 여부 판별. 로깅 파이프라인처럼 읽기 전용 접근만 필요한 곳에 사용. - IFinResponseFactory (CRTP 팩토리):
Fail<T>(error)등 새로운 응답 인스턴스 생성. 유효성 검증 파이프라인에서 실패 응답을 직접 만들 때 사용. - IFinResponseErrorAccessor (에러 접근): 실패 시 에러 상세 정보 접근. 에러 분류, 관측성 파이프라인에서 사용.
파이프라인의 where TResponse : 시그니처만 보면 그 파이프라인이 응답의 어떤 측면에 접근하는지 즉시 파악할 수 있다.
- 긍정적:
object캐스팅이 전면 제거되어 런타임InvalidCastException이 원천 차단된다. 로깅 파이프라인에IFinResponseCovariant만 제약한 덕분에, 팩토리나 에러 접근 메서드가 노출되지 않아 의도치 않은 사용이 불가능하다. 새 파이프라인을 추가할 때 4단계 중 어떤 인터페이스를 선택해야 하는지가 명확하여 설계 의사결정이 빨라진다. 85개 테스트가 모든 파이프라인-인터페이스 조합의 정상 동작과 컴파일 제약을 검증한다. - 부정적: 4단계 인터페이스 계층과 CRTP 패턴이 초기 학습 곡선을 높인다.
IFinResponse로 시작하는 인터페이스가 4개이므로 IDE 자동완성 목록이 길어질 수 있다.
- 85개 단위 테스트가 모든 파이프라인과 인터페이스 조합을 검증한다.
- 잘못된
where제약을 사용할 경우 컴파일 에러가 발생하는지 확인한다. - 새로운 파이프라인을 추가할 때 적절한 인터페이스 단계를 선택하는 가이드를 제공한다.
옵션별 장단점
섹션 제목: “옵션별 장단점”옵션 1: object 캐스팅
섹션 제목: “옵션 1: object 캐스팅”- 장점: 인터페이스 설계가 불필요하다.
(FinResponse<T>)(object)response한 줄로 동작한다. - 단점: 응답 타입이
FinResponse<T>가 아닌 파이프라인에 잘못 등록되면 런타임에InvalidCastException이 터진다. 파이프라인 시그니처만 보고는 어떤 응답 타입을 기대하는지 알 수 없어, 새 파이프라인 작성 시 기존 코드를 뒤져봐야 한다. 리팩토링 시 컴파일러가 잘못된 캐스팅을 감지하지 못한다.
옵션 2: 단일 인터페이스로 모든 기능 통합
섹션 제목: “옵션 2: 단일 인터페이스로 모든 기능 통합”- 장점: 인터페이스가 하나이므로 학습 곡선이 낮다. 모든 파이프라인이 동일한 제약을 사용하여 일관적이다.
- 단점: 로깅 파이프라인은
IsSucc만 읽으면 되는데Fail()팩토리와 에러 접근 메서드까지 노출된다. 인터페이스 분리 원칙(ISP) 위반이다. 인터페이스에 새 메서드를 추가하면 모든 구현체를 수정해야 하므로 확장 비용이 높다.
옵션 3: IFinResponse 4단계 인터페이스 계층
섹션 제목: “옵션 3: IFinResponse 4단계 인터페이스 계층”- 장점: 로깅 파이프라인은
IFinResponseCovariant만, 유효성 검증은IFinResponseFactory를 제약하여 각자 필요한 최소 계약만 선언한다. 공변성 인터페이스 덕분에 읽기 전용 파이프라인에서 제네릭 타입 파라미터를 유연하게 다룰 수 있다. CRTP 팩토리로Fail<T>(error)호출이 타입 안전하다. 잘못된where제약은 컴파일 에러로 즉시 잡힌다. - 단점: 4단계 인터페이스 계층과 CRTP 패턴의 설계 의도를 새 팀원에게 설명해야 하는 온보딩 비용이 있다.
IFinResponse로 시작하는 인터페이스가 4개이므로 IDE 자동완성과 문서화 부담이 늘어난다.
옵션 4: 리플렉션 기반 동적 디스패치
섹션 제목: “옵션 4: 리플렉션 기반 동적 디스패치”- 장점: 인터페이스 설계 없이 런타임에 응답 타입을 동적으로 검사하여 처리할 수 있다. 기존 타입을 수정할 필요가 없다.
- 단점: 매 요청마다 리플렉션 호출이 발생하여 Hot path 성능이 저하된다. 컴파일 타임 검증이 전혀 없어 타입 불일치가 프로덕션에서 발견된다. 리플렉션 내부를 거치는 스택 트레이스가 디버깅을 어렵게 만든다.
관련 정보
섹션 제목: “관련 정보”- 커밋: ace89d39 (85개 테스트), 91b57254, 33821633