인터페이스 계약 위반

인터페이스 계약 위반

개요

정의된 인터페이스가 존재함에도 불구하고 구현체나 비공식 경로를 사용하는 문제는 구조를 무력화시키며, 이는 하네스를 통해 강제적으로 차단되어야 한다.


이 문서의 질문

이 문서는 하네스 개요에서 정리한 다섯 공통 패턴 중 contract flattening에 가장 가깝다. 핵심 질문은 다음과 같다.

인터페이스 계약이 남아 있어도 구현체 직접 진입점이 사실상 열려 있지 않은가

문제

이 케이스는 이전보다 더 미묘하다.

  • 레이어는 지켜졌다
  • 호출 경로도 맞다

하지만 다음과 같은 코드가 등장한다.

var repo = (FileDocumentRepository)_repository;
repo.SpecialLoad(path);

또는

if (_repository is FileDocumentRepository impl)
{
    impl.InternalReload();
}

또는

var impl = serviceProvider.GetService<FileDocumentRepository>();

이 코드는 다음을 의미한다.

인터페이스는 있지만, 실제로는 무시되고 있다


문제의 본질

이 문제는 “경로 위반”이 아니라
계약 무력화다.


인터페이스의 의미 붕괴

인터페이스는 다음을 보장해야 한다.

  • 호출 가능한 기능의 범위

  • 구현체와의 분리

  • 교체 가능성

하지만 캐스팅이 허용되는 순간:

  • 인터페이스는 단순한 껍데기가 된다

  • 실제 의존은 구현체로 이동한다


숨겨진 의존성 생성

이 구조에서는 다음이 발생한다.

  • 코드가 특정 구현체에 암묵적으로 의존

  • 테스트에서 Mock 교체 불가능

  • 구현 변경 시 런타임 오류 발생


AI의 선택 패턴

사람과 AI는 다음 상황에서 이 패턴을 선택한다.

  • 인터페이스에 없는 기능이 필요할 때

  • 빠르게 해결해야 할 때

  • 기존 구조를 확장하기 어려울 때

즉:

계약을 수정하지 않고 우회한다


목표 구조

핵심은 다음이다.

인터페이스 밖으로 나가는 모든 시도를 차단한다


사례 규칙 요약

  • 인터페이스 → 구현체 캐스팅 금지

  • 특정 구현체 타입 참조 금지

  • DI에서 구현체 직접 조회 금지

자세한 rule/validator 설계 원칙은 규칙 설계검증기 설계를 따른다. 이 케이스에서는 위 금지 표면과 합법 경로가 바로 검사 대상이다.

하네스 적용 위치

이 케이스의 하네스는 contract surface, implementation visibility, container access에 걸린다.

계약 레벨

호출자는 interface나 trait만 보고 concrete type 이름조차 모르게 해야 한다.

노출 레벨

구현체는 internal, private, pub(crate) 같은 가시성으로 숨겨서 캐스팅 표면을 줄인다.

검증 레벨

다운캐스트, is 검사, GetService<Concrete>() 같은 계약 우회 패턴을 정적 분석으로 막는다.

실제 구현

인터페이스 설계 강화

먼저 계약을 명확하게 만든다.

public interface IDocumentRepository
{
    Task<string> LoadAsync(string id);
    Task ReloadAsync(string id);
}

👉 핵심:

  • 필요한 기능은 인터페이스에 추가

  • 구현체에 숨기지 않는다


구현체 은닉

internal sealed class FileDocumentRepository : IDocumentRepository
{
}
  • 외부에서 접근 불가

  • 캐스팅 자체가 불가능해짐


DI 등록 제한

services.AddScoped<IDocumentRepository, FileDocumentRepository>();

👉 중요한 점:

  • FileDocumentRepository는 등록하지 않음

  • 인터페이스로만 접근 가능


위반 예시와 실패 결과

다음 코드는 반드시 실패해야 한다.

var concrete = (FileDocumentRepository)_repository;
await concrete.RebuildIndexAsync();

또는

var concrete = provider.GetService<FileDocumentRepository>();

실패 결과:

error: Concrete repository access bypasses the declared contract boundary.

계약이 남아 있어도 concrete 진입점이 열려 있으면 경계는 이미 무너진 것이다.

보조 강제 수단

이 케이스의 보조 강제 수단은 가시성 제한과 container usage rule을 함께 두는 데 있다. 인터페이스만 정의하고 구현체를 public으로 남겨 두면 캐스팅과 service locator가 바로 지름길이 된다.

Roslyn Analyzer

캐스팅 금지

private static void AnalyzeCast(OperationAnalysisContext context)
{
    var operation = (IConversionOperation)context.Operation;
    var targetType = operation.Type;

    if (targetType == null)
        return;

    if (targetType.Name.EndsWith("Repository"))
    {
        context.ReportDiagnostic(...);
    }
}

is 패턴 금지

if (operation is IIsTypeOperation isOp)
{
    var type = isOp.TypeOperand;

    if (type.Name.EndsWith("Repository"))
    {
        context.ReportDiagnostic(...);
    }
}

DI 직접 조회 금지

if (invocation.TargetMethod.Name == "GetService")
{
    var typeArg = invocation.TargetMethod.TypeArguments.FirstOrDefault();

    if (typeArg?.Name.EndsWith("Repository") == true)
    {
        context.ReportDiagnostic(...);
    }
}

Rust 측 대응

Rust에서는 trait 기반으로 더 강하게 막을 수 있다.

pub trait DocumentRepository {
    fn load(&self, id: &str) -> anyhow::Result<String>;
}

구현체는 외부로 노출하지 않는다.

pub(crate) struct FileDocumentRepository;

👉 외부에서:

  • downcast 불가

  • concrete 접근 불가


결과

계약 유지

  • 인터페이스가 실제 경계로 작동

  • 구현체 의존 제거


구조 안정성 확보

  • 교체 가능성 유지

  • 테스트 가능성 유지


행위자 행동 변화

이제 사람과 AI는 더 이상:

  • 캐스팅

  • 구현체 접근

  • shortcut

을 사용할 수 없다


실무 적용 팁

  • interface를 만들었다면 concrete type은 가능한 한 같은 파일이나 내부 모듈 뒤로 숨기는 편이 좋다.
  • container에서 concrete 조회를 허용하지 말고 registration도 interface 중심으로 유지해야 한다.
  • 테스트도 concrete downcast 대신 contract-compatible fake로 맞추는 편이 구조를 덜 흐린다.

요약

인터페이스 위반 문제는 하네스를 통해서만 안정적으로 제어된다.

  • 구현체 은닉

  • DI 제한

  • Analyzer로 캐스팅 차단

  • 빌드 에러로 강제

이 구조를 통해
계약은 더 이상 우회할 수 없는 경계가 된다.