백엔드 레이어 우회
개요
Backend 내부에서 정의된 레이어를 우회하고 직접 구현체나 하위 계층을 호출하는 문제는 구조를 내부에서 붕괴시키며, 이는 하네스를 통해 차단되어야 한다.
이 문서의 질문
이 문서는 하네스 개요에서 정리한 다섯 공통 패턴 중 direct path bypass에 가장 가깝다.
핵심 질문은 다음과 같다.
내부 레이어가 정해진 계약을 건너뛰고 직접 호출될 수 있는가
문제
UI → Backend 경계는 지켜졌지만,
Backend 내부에서 다음과 같은 코드가 발생한다.
- Service가 Repository를 직접 호출
- Service가 Engine 내부 모듈을 직접 접근
- UseCase를 건너뛰고 하위 로직을 직접 실행
- DI를 무시하고 구현체를 직접 생성
예시:
var repo = new FileRepository();
var data = repo.Load(id);또는
var result = engine.InternalProcessor.Process(raw);이 코드는 동작한다.
그리고 대부분 빠르게 문제를 해결한다.
하지만 구조 관점에서는 심각한 문제다.
문제의 본질
이 우회는 외부 경계를 뚫는 문제가 아니라,
application/service 레이어가 concrete 선택권을 다시 가져가는 문제다.
- Service가 데이터 접근과 내부 처리 선택까지 떠안아 책임 분리가 평탄화된다.
- 테스트와 변경 영향 범위가 contract가 아니라 concrete 구현에 묶인다.
- 사람과 AI가 가장 짧은 직접 호출을 다시 정본처럼 복제한다.
목표 구조
이 문제의 핵심은 다음이다.
레이어를 “지키도록 유도"하는 것이 아니라
다른 레이어에 접근 자체를 불가능하게 만든다
상위 레이어는 지정된 인터페이스만 호출 가능
하위 레이어 구현체 직접 참조 금지
new를 통한 직접 생성 금지 (DI 강제)
자세한 rule/validator 설계 원칙은 규칙 설계와 검증기 설계를 따른다. 이 케이스에서는 위 금지 표면과 합법 경로가 바로 검사 대상이다.
하네스 적용 위치
이 케이스의 하네스는 세 지점에 놓인다.
구조 레벨
Application과 Service는 계약 인터페이스만 보고 Infrastructure 구현체는 별도 레이어 뒤로 숨긴다.
생성 레벨
구현체 생성은 composition root와 DI 등록으로만 허용한다. 서비스 코드 안의 new FileDocumentRepository(...) 같은 생성이 바로 우회 표면이다.
검증 레벨
금지 namespace 참조, concrete 타입 사용, 직접 생성 패턴을 정적 분석과 프로젝트 참조 규칙으로 함께 막는다.
실제 구현
핵심 구현은 두 축이다.
- 상위 계층은 contract만 안다.
- concrete 생성은 composition root에서만 일어난다.
구현 예시 펼치기
상위 계층은 계약만 알고 구현은 조립 계층에 숨긴다
이 패턴의 핵심은 application/service 레이어가 concrete infrastructure를 직접 만들거나 참조하지 못하게 만드는 것이다.
public interface IDocumentRepository
{
Task<string> LoadAsync(string id);
}
internal sealed class FileDocumentRepository : IDocumentRepository
{
public Task<string> LoadAsync(string id)
{
// 실제 파일 접근
}
}상위 계층은 IDocumentRepository만 알고, FileDocumentRepository는 조립 계층이나 infrastructure 안에 숨는다.
생성 경로도 DI나 조립 계층으로 고정한다
services.AddScoped<IDocumentRepository, FileDocumentRepository>();
services.AddScoped<IDocumentService, DocumentService>();public sealed class DocumentService : IDocumentService
{
private readonly IDocumentRepository _repository;
public DocumentService(IDocumentRepository repository)
{
_repository = repository;
}
}이 구조가 있어야 service가 concrete repository를 직접 new 하거나, infrastructure namespace를 직접 물고 들어가는 경로가 줄어든다.
다른 언어에서도 기준은 같다
Rust에서도 service는 trait만 의존하고, 파일 접근 구현은 trait 구현체 뒤에 숨기는 편이 맞다. 핵심은 언어가 아니라 “상위 계층이 concrete를 직접 선택하지 못하게 만드는 것”이다.
테스트도 같은 계약 경로를 따라야 한다
fake 구현이 필요하더라도 generic parameter, trait, DI를 통해 주입해야지, service 내부에서 concrete fake를 직접 고르는 방향으로 가면 같은 우회가 반복된다.
위반 예시와 실패 결과
다음 코드는 반드시 실패해야 한다.
public sealed class DocumentService
{
public async Task<string> GetDocumentAsync(string id)
{
var repo = new FileDocumentRepository();
return await repo.LoadAsync(id);
}
}실패 결과:
error: Application layer must not construct infrastructure repository directly. Use the declared repository contract instead.이 순간부터 레이어는 설명이 아니라 컴파일 가능한 경계가 된다.
보조 강제 수단
이 케이스는 namespace/assembly 경계와 new concrete 탐지를 함께 묶어야 한다. 한쪽만 두면 직접 생성, 캐스팅, 직접 참조 중 하나가 다시 지름길이 된다.
상위 계층의 infrastructure direct reference를 막는다
application 레이어에서 Infrastructure.* namespace를 직접 참조하거나 concrete repository를 직접 생성하는 패턴을 잡는 규칙이 먼저 필요하다. 이 규칙의 목적은 호출 자체보다 경로 선택을 고정하는 데 있다.
프로젝트 참조에서도 경계를 잠근다
가능하면 application 프로젝트에서 infrastructure assembly를 직접 참조하지 않거나, 최소한 concrete 타입이 상위 계층으로 새지 않도록 조립 방향을 명확히 둔다. 컴파일 레벨 경계가 있어야 문서 설명이 실제 구조로 남는다.
빌드 실패로 끝까지 밀어붙인다
분석기가 잡는 레이어 우회는 경고가 아니라 빌드 실패로 취급하는 편이 맞다. 레이어 문제는 품질 이슈가 아니라 시스템 선택지를 여는 문제이기 때문이다.
결과
하네스 적용 이후 변화는 간단하다.
- 레이어 경계와 의존 방향이 contract 기준으로 다시 고정된다.
new concrete, 금지 namespace, internal engine direct call이 모두 빌드 실패가 된다.- 사람과 AI 모두 구현체 대신 정의된 인터페이스 경로와 DI 조립 경로를 먼저 찾게 된다.
실무 적용 팁
- Infrastructure 구현체를 public으로 노출하지 말고 조립 계층에서만 알게 하는 편이 좋다.
Repository,Engine,Adapter같은 concrete suffix를 analyzer 규칙과 맞춰 두면 우회 탐지가 쉬워진다.- 테스트도 service가 concrete를 직접 만들지 않게 하고, fake는 DI 또는 generic parameter로만 주입하는 편이 안전하다.
요약
Backend 내부 레이어 우회 문제는 하네스를 통해서만 안정적으로 제어할 수 있다.
인터페이스로 경계 정의
DI로 생성 경로 고정
Analyzer로 우회 차단
빌드 에러로 강제
이 과정을 통해
구조는 선택이 아니라 필수 조건이 된다.