API 라이브러리의 구조를 어떻게 짜야될지 궁금합니다.

A라는 클래스를 만들고 이 클래스에서 GraphQl과 REST 방식의 API를 선택해서 사용할 수 있게하려고합니다.
GraphQL과 REST 의 요청을 처리하기위해 HttpClient를 사용하고있습니다.

A클래스의 객체를 만들 때 api에 사용되는 토큰값과 HttpClient의 인스턴스를 초기화하고, GraphQl과 REST 에서는 A클래스를 초기화해서 만들어진 토큰값과 HttpClient 인스턴스를 사용할 수 있게 하려고하는데 아래 코드와 같이 코드를 작성하는게 옳은 방식인지 궁금합니다.

public class A {
    public HttpClient Client {get; private set;}
    public string Access_Token {get; private set;}

    public A() {
        Client = new HttpClient();
        Client.BaseAddress = new Uri("https://api.com/");

        Access_Token = "";
    }

    public class GraphQL {
        public readonly A API;
        public GraphQL(A api) {
            API = api;
        }

        public async Task test() {
            await API.Client.GetAsync("");
        }
    }
}

///////////////////////////////////////////////////////////////////////////////
  void main()
  {
      A api = new A();
      A.GraphQL graphql = new A.GraphQL(api);
  }
좋아요 1

graphql을 사용하시는데 graphql client로 질의하지 않으시고, HttpClient를 사용하시는 이유가 궁금하네요.

그리고 Access_Token을 어떤식으로 전달하시는지 API 명세를 확인해 보시는게 좋겠습니다.
Authorization 헤더에 토큰을 설정하는 일반적인 방식은 아래와 같이 처리할 수 있을 것 같습니다.

var graphClient = new GraphQLClient("https://api.github.com/graphql");
graphClient.DefaultRequestHeaders.Add("Authorization", $"bearer {ApiKey}");

var request = new GraphQLRequest
{
    Query = @"query { viewer { login } }"
};

var test = await graphClient.PostAsync(request);
좋아요 1

GraphQL Client 는 생각해보지못했습니다. 이 라이브러리를 사용하는 방향으로 한번 해보겠습니다.
Authorization 헤더에 토큰을 넣는 것은 위의 소스코드에는 없지만 HttpClient에서 처리하고 있습니다.

한 라이브러리에서 두 가지 API 방식을 지원할 때 어떤 형태로 구조를 짜는지 궁금합니다.
그리고 쿼리를 로직에 하드코딩하게되면 나중에 수정할 때 문제가 될것같아 쿼리와 로직을 분리하려고하는데 json, xml처럼 파일의 모든 내용을 불러와 쿼리하는 방법밖에 없을까요? key를 지정해두고 원하는 쿼리만 하나씩 뽑아와 사용할 수 있는 방법이 있는지 궁금합니다.

좋아요 1

기본적으로 REST를 쓰실땐 HttpClient, GraphQL을 쓰실땐 GraphQLClient를 사용하시면 될 것 같아요.
2개를 한꺼번에 사용하는 구조는 서비스 흐름에 따라 많이 달라질 것 같은데요.
인증만 REST를 사용한다면 아래와 같은 예시로 설계할수도 있겠네요.

public class ServiceClient
{
    private GraphQLClient _graphClient;
    private IAuthorizationService _authorizationService;

    public ServiceClient(IAuthorizationService authorizationService)
    {
        _authorizationService = authorizationService;
    }

    public async ValueTask<IEnumerable<Customer>> GetCustomers()
    {
        var client = await GetGraphClient();
        var request = new GraphQLRequest
        {
            Query = @"query { viewer { customers } }"
        };

        return await client.GetAsync(request);
    }

    private async ValueTask<GraphQLClient> GetGraphClient()
    {
        if(_graphClient == null)
        {
            var token = await _authorizationService.GetTokenAsync();
            _graphClient = new GraphQLClient("https://api.github.com/graphql");
            _graphClient.DefaultRequestHeaders.Add("Authorization", $"bearer {token}");
        }
        
        return _graphClient;
    }
}

public interface IAuthorizationService
{
    ValueTask<string> GetTokenAsync();
}

public class AuthorizationService : IAuthorizationService
{
    private HttpClient _client;

    public ValueTask<string> GetTokenAsync()
    {

        //...
    }
}

전체 쿼리를 읽어 오는 방식이 아닌 클라이언트 요구에 따라 쿼리를 유연하게 구성하길 희망하신다면 빌더 패턴을 활용해 볼 수 있겠네요.
찾아보니 연관된 패키지가 있는 것 같은데 참조해 보시면 좋겠습니다.

좋아요 3

API 디자인 방향

API를 만드는 관점에서는 REST 방식을 사용하느냐, GraphQL을 사용하느냐, 둘 다 사용하느냐는 것은 목적에 따라 결정하면 됩니다.

하지만 API를 사용하는 입장에 REST를 사용하는지, GraphQL을 사용하는지를 API로 노출할 필요는 제 생각에는 없을 것 같아요. (API를 사용하는 입장에서는 API 기능 자체에만 집중하게 하는 것이 좋습니다.)

다만 REST 및 GraphQL의 인증 관련된 내용은 API를 사용하는 입장에서 어쩔 수 없이 노출할 수 있는 부분이겠지요. 그 부분만 클래스로 추상화 해서 (추상클래스(인증정보) – REST인증정보, GraphQL 인증정보)으로 구분해두고 나머지는 REST던 GraphQL이던 API입장에서는 감추는 것이 좋습니다.

이렇게 추상화 클래스를 사용하는 것도 API 사용자가 필요할 때만 구성하고 이것조차 필요하지 않으면 인증 관련 옵션 정도로 구분하고 나머지는 노출하지 않는 것이 좋습니다.

결론적으로 API 클래스의 기능은 REST나 GraphQL의 요소 없이 API 기능 목록으로만 노출하는 것이 최대한 좋습니다.

→ 두가지 API 방식을 숨기고 제공하고자 하는 API 기능 목록만 노출한다.

하드코딩 관련

SQL 쿼리를 XML 등으로 분리하고 그것을 불러와 적용하는 것 같이 GraphQL 질의를 별도의 파일로 구성 하시겠다는 것인가요? 저는 굳이 그럴 필요가 없다고 생각합니다.

보통 이렇게 구분해야 하는 이유는 관리의 측면도 있지만 이미 컴파일된 결과를 수정하지 않고 쿼리 정보만 수정하는 것으로 결과를 수정할 수 있는 이점이 있기 때문인데요, API의 취지하고는 맞지 않아요. (API 사용자가 쿼리를 수정하는것은 아닐 테니까요)

→ 쿼리를 그냥 API 함수 안에 두어도 상관이 없습니다. 관리 측면에서 전혀 문제가 되지 않고 이것을 하드코딩이라고 생각하지 않습니다. 환경설정 처럼 변경해야 할 값이 아니니까요.

@nyjin 님이 공유주신 코드를 예로 들어서,

        var request = new GraphQLRequest
        {
            Query = @"query { viewer { customers } }"
        };

에서 `"query { viewer { customers } }"이 부분이 하드코딩이라고 생각하시는 것 같은데, 하드코딩이 아니라 마치 REST API의 매개변수 처럼 반드시 명시해야 할 인자일 뿐입니다.

이렇게 전개하시려고 하는게 되려 API 구현 코드의 가독성을 나쁘게 할 수 있습니다.

정리

  • API 구현은 API를 사용하는 입장에서 구현
    • API를 사용하는 입장에서 굳이 REST인지 GraphQL인지 알필요가 없다면 그러한 내용을 노출하지 않는 것이 가장 좋음
  • 쿼리와 로직을 굳이 분리하지 않는다.
    • 설령 여러 구현 API 함수에서 필요로 하는 쿼리 구조가 중복되어 보인다 하더라도 그 쿼리는 그 API에서 필요로 하는 정확한 쿼리임. 되려 명확한 표현이 코드 가독성을 높이고 수정할 때 번거롭지 않음
좋아요 3