ABP vNext에서 동적 C# API 클라이언트를 통한 원격 서비스 호출 구현 및 내부 동작 분석

핵심 문제 인식: 왜 동적 클라이언트가 필요한가?

다수의 마이크로서비스 환경에서 서로 다른 서비스 간에 HTTP 기반 통신이 빈번하게 발생합니다. 전통적인 방식으로는 HttpClient를 직접 주입하고, 문자열 기반의 URL과 강타입이 아닌 파라미터를 다루며, 매번 요청/응답 검증 로직을 반복해야 합니다. 이는 유지보수성과 코드 품질에 악영향을 미칩니다.

특히 서비스 갯수가 증가하면, 각기 다른 엔드포인트에 대한 수동 관리가 불가피해지고, 실수 유발 가능성이 커집니다. 따라서 다음과 같은 요구사항이 자연스럽게 등장합니다:

  • 원격 메서드 호출을 마치 로컬 메서드처럼 사용할 수 있어야 한다.
  • 인터페이스 기반의 명세가 존재하여, 클라이언트와 서버 간 일관된 계약이 보장되어야 한다.
  • HTTP 세부 사항(메서드 타입, 쿼리, 바디 등)은 구현 세부사항이 되어야 하며, 개발자는 관심 없이 사용할 수 있어야 한다.

ABP vNext의 동적 클라이언트 솔루션

ABP Framework는 이러한 문제를 해결하기 위해 동적 클라이언트 프록시를 제공합니다. 이는 실제 HttpClient를 직접 다루지 않고도, 원격 서비스의 메서드를 마치 로컬 인터페이스처럼 호출할 수 있게 해줍니다.

1. 인터페이스 정의 및 서버 구현

먼저, 공유 계약을 위한 인터페이스를 정의합니다. 이 인터페이스는 IRemoteService를 상속받아야 하며, 프레임워크가 이를 식별할 수 있도록 합니다.

public interface IMoneyAppService : IRemoteService
{
    Task<List<Money>> GetMoneyAsync();
}

public class MoneyAppService : IMoneyAppService
{
    [HttpGet]
    public async Task<List<Money>> GetMoneyAsync()
    {
        return new List<Money> { new Money(5000) };
    }
}

2. 클라이언트 모듈 구성

클라이언트 측에서는 Volo.Abp.Http.Client 패키지를 통해 프록시 생성 기능을 활성화합니다. 모듈에 다음과 같이 의존성 주입을 설정합니다.

[DependsOn(typeof(AbpHttpClientModule), typeof(MoneyApplicationContractsModule))]
public class ClientModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddHttpClientProxies(
            assembly: typeof(MoneyApplicationContractsModule).Assembly,
            remoteServiceName: "MoneyService",
            isDefault: false
        );
    }
}

3. 구성 파일 설정

서비스 이름과 기본 주소를 연결하는 설정을 추가합니다.

{
  "RemoteServices": {
    "MoneyService": {
      "BaseUrl": "http://localhost:8080/"
    }
  }
}

이 설정은 프레임워크가 MoneyService라는 이름으로 특정 엔드포인트에 접근할 수 있도록 도와줍니다. 내부적으로는 이름-주소 매핑을 관리하는 딕셔너리 구조를 활용합니다.

4. 실제 사용 예시

사용자는 단순히 인터페이스를 주입하고, 마치 로컬 메서드처럼 호출하면 됩니다.

public class MyService : ITransientDependency
{
    private readonly IMoneyAppService _moneyService;

    public MyService(IHttpClientProxy<IMoneyAppService> proxy)
    {
        _moneyService = proxy.Service;
    }

    public async Task DoWork()
    {
        var result = await _moneyService.GetMoneyAsync(); // 원격 호출!
    }
}

내부 동작 원리 분석

이제 핵심인 동적 프록시의 작동 방식을 살펴봅니다. 이 기능은 Castle.Core동적 프록시 기술에 기반합니다.

1. 프록시 생성 과정

프레임워크는 지정된 어셈블리에서 IRemoteService를 상속하는 인터페이스를 찾습니다. 이 과정에서 다음과 같은 필터링이 이루어집니다:

  • 공개된 클래스만 대상
  • 제네릭 타입 제외
  • 실제 구현체 없이 인터페이스만 존재

이러한 인터페이스들은 CreateInterfaceProxyWithoutTarget로 동적으로 프록시 객체로 변환됩니다. 이 프록시는 실제 메서드 구현 없이, 인터셉터(Interceptor)를 통해 요청을 가로챕니다.

2. 인터셉터 체인 처리

프록시 호출 시 두 가지 인터셉터가 순차적으로 실행됩니다:

  1. 검증 인터셉터: 입력 파라미터 유효성 검사 (예: Required 어노테이션 등).
  2. HTTP 호출 인터셉터: 실제 원격 서비스에 대한 요청을 생성하고 전송합니다.

이 두 인터셉터는 DynamicHttpProxyInterceptor 내부에서 처리되며, 요청 정보를 ClientProxyRequestContext로 추상화합니다.

3. 원격 메서드 매핑

프록시는 요청을 보내기 전, API 정의 메타데이터를 조회합니다. 이는 다음 경로에서 얻을 수 있습니다:

GET /abp/api-definition

응답은 JSON 형식의 메타데이터이며, 각 메서드의 HTTP Method, URL 패턴, 파라미터 형식 등을 포함합니다. 이를 바탕으로 현재 호출되는 메서드에 해당하는 원격 엔드포인트를 정확히 결정합니다.

4. 요청/응답 처리

메서드 반환값이 Task<T>라면, 응답 본문을 T로 역직렬화합니다. 만약 반환 타입이 void이거나 Task라면, 단순히 성공 여부만 확인합니다.

핵심 기술 요소 요약

  • 반사(Reflection): 인터페이스 및 메서드 메타데이터 수집
  • 제네릭과 위임: 프록시의 유연한 타입 처리
  • 의존성 주입: 클라이언트 프록시의 생명주기 관리
  • 프록시 패턴 + AOP: 요청 가로채기 및 비즈니스 로직 삽입
  • Castle.Core: 실제 동적 프록시 생성 기반

고려 사항 및 한계

  • 메타데이터 캐싱: 서버 변경 후 클라이언트가 즉시 반영되지 않음. 재시작 또는 외부 캐시(예: Redis) 활용 필요.
  • 타임아웃 설정: 최근 버전에서는 개별 요청에 타임아웃 설정 가능.
  • 호환성: 일부 특수 타입(예: IFormFile)은 추가 처리 필요.

결론적으로, ABP의 동적 클라이언트는 의사코드(코드 생성 없이)로 원격 서비스 호출을 가능하게 하는 매우 우아한 설계입니다. 이 구조를 이해하면, 자체적으로 유사한 시스템을 구현할 수도 있으며, RefitgRPC 등의 다른 도구들과 비교하며 장단점을 평가할 수 있습니다.

태그: ABP vNext 동적 프록시 HttpClient Castle.Core AOP

7월 13일 04:14에 게시됨