asp.net core graphql with hotchocolate #1

멀리보다·2021년 9월 9일
0

asp.net core graphql

목록 보기
1/2

이번 포스트에서는 asp.net core에서 graphql query에 대한 가장 기초적인 부분에 대해 알아본다.

목차

graphql이란 무엇인가?

얄코
GraphQL 개념잡기
공식사이트
핫초콜릿

결국 graphql은 웹 환경에서 서버와 클라이언트간의 데이터 통신 스키마를 잘(?) 잘 정해 놓고 사용하자는 것.

graphql을 처음 봤을때 이건 또 뭐야 알아야 할께 또 생겼네 싶었다. 그러다 곰곰이 생각해보니 이걸 적용하면 개발자간의 다툼이
줄겠구나 하는 생각이 들었다.

예전에는 어떻게 데이터 스키마를 정했는지 내가 했던 경험을 얘기보겠다.

  1. client가 db에 바로 쿼리를 날리던 시절에는 포맷에 대한 이슈 없음
  2. TCP-IP 통신 작성할 때 패킷 사이즈는 어떻게 할것 인가 빅엔디언, 리틀엔디언 뭘로 할건가에 대한 이슈
  3. java corba 등장 이게 다 앂어 먹을것 같았는데 별거 없이 끝남.
  4. java rmi
  5. .net remoting service 꽤 잘 사용했지만 xml로 직렬화 할지 binary로 직렬화 할지에 대한 작은 이슈
  6. wcf 등장 이걸로 프로젝트 한 경험 없음 이때 이미 json으로 대동 단결되었던것 같음
  7. 데이터 포맷은 json 대동단결 back-end, front-end 구분이 모호하던 시절 개발자 마음대로 포맷 정함
  8. rest등장 내가 생각하는 가장 쓰레기 같은 방식, 외부 연동 api를 해봐도 정말 규칙되로 된곳은 없음
    나도 이규칙대로는 도저히 못하겠음. criteria가 복잡한 경우 미쳐버림. 결국 post로 다 요청함
  9. 프로젝트마다 나름대로 포맷 규칙 정해서 사용
예) 요청
{
	"lastName":"kim",
    "checkinDate":"2020-01-08",
    "checkoutDate":"2020-01-09",
    "status":18,
    "nation":"KR"
}
응답
{
	"results":[{....},{....},{....}]
    "isError":false,
    "exception":{
    	"name":null,
        "message":null,
    }
}
  1. graphql등장 9에 했던 스키마를 프로젝트마다가 아니라 graphql의 도움을 받아서 일원화 할 수 있음.
    graphql도 만능은 아님, 특정언어에 국한된게 아니기 때문에 제약 사항이 꽤 있고, 간단히 처리 할 수 있는 부분을
    복잡해질 수도 있지만, 서버와 클라이언트는 이미 스키마가 정해졌기 때문에 규칙대로만 개발하면됨.
    그리고 ORM을 사용시 entity 객체를 client까지 사용할지 항상 고민되는데 고민없이 무조건 VO, DTO를 찍으면됨

1. nuget downlaod

2. 기본적인 코드 작성

  • entity 작성
    - Member.cs

  • dto 작성
    - MemberSearchRequest.cs
    - MemberVO.cs

  • 서비스 및 리파지토리 클래스 작성
    - PensionService.cs
    - MemberRepository.cs

3 Startup.cs 수정

4 graphql test

5 문제점

6 해결방법


본문

1. nuget downlaod

"HotChocolate.AspNetCore" Version="11.3.7"

2. 기본적인 코드 작성

- entity 작성
Member.cs

using System;

namespace MyPension.Entities
{
    public class Member
    {
        public string Name { get; set; }
        public long Id { get; set; }
        public DateTime BirthDay { get; set; }
        public string Email { get; set; }
    }
}

- vo (MemberVO.cs)

using MyPension.Entities;
using System;

namespace MyPension.Dtos.VO
{
    public class MemberVO
    {
        public MemberVO(Member entity)
        {
            Name = entity.Name;
            Id = entity.Id;
            BirthDay = entity.BirthDay.Date;
            Email = entity.Email;
        }

        public string Name { get; set; }
        public long Id { get; set; }
        public DateTime BirthDay { get; set; }
        public string Email { get; set; }
    }
}

- 검색 criteria request (MemberSearchRequest.cs)

namespace MyPension.Dtos.Requests
{
    public class MemberSearchRequest
    {
        public long? Id { get; set; }
        public string Email { get; set; }
        public string Name { get; set; }
    }
}

- Repository (MemberRepository.cs)

여기서는 실제 DB에 접근하는게 아닌 mock 데이터로 테스트

using MyPension.Dtos.Requests;
using MyPension.Dtos.VO;
using MyPension.Entities;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MyPension.Repositories
{
    public class MemberRepository
    {
        public IEnumerable<MemberVO> Search(MemberSearchRequest request)
        {
            var byId = request.Id;
            var byName = request.Name;
            var byEmail = request.Email;

            var query = GetMocks().AsQueryable();

            if (byId.HasValue)
            {
                query = query.Where(x => x.Id == byId.Value);
            }
            else
            {
                if (!String.IsNullOrEmpty(byName))
                {
                    query = query.Where(x => x.Name.Contains(byName));
                }

                if (!String.IsNullOrEmpty(byEmail))
                {
                    query = query.Where(x => x.Email.Contains(byEmail));
                }
            }

            //vo의 생성자로 entity를 던진다.
            //vo를 만드는 여러가지 방법이 있지만 정답은 없다.
            var results = query.Select(x => new MemberVO(x));

            return results;
        }

        /// <summary>
        /// 테스트 데이터를 만들자
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Member> GetMocks()
        {
            List<Member> mock = new List<Member>();
            for (int i = 0; i < 1000; i++)
            {
                mock.Add(new Member
                {
                    BirthDay = DateTime.Now.AddYears(-50).AddMonths(i),
                    Email = $"jindalre{i}@gmail.com",
                    //테스트를 위해 id값을 큰값으로도 만듦
                    Id = i % 2 == 0 ? long.MaxValue - i : i,
                    Name = $"kim abc {i}"
                });
            }
            return mock;
        }
    }
}

- Service (PensionService.cs)

using MyPension.Dtos.Requests;
using MyPension.Dtos.VO;
using MyPension.Repositories;
using System.Collections.Generic;

namespace MyPension.Services
{
    public class PensionService
    {
        private readonly MemberRepository _memberRepository;

        public PensionService(MemberRepository memberRepository)
        {
            _memberRepository = memberRepository;
        }

        public IEnumerable<MemberVO> SearchMember(MemberSearchRequest request)
        {
            return _memberRepository.Search(request);
        }
    }
}

3 startup.cs 수정

public void ConfigureServices(IServiceCollection services)
{
	............
	// Graphql server등록
	services.AddGraphQLServer().AddQueryType<MyPension.Services.PensionService>();
	// 서비스 등록
	services.AddScoped<MyPension.Services.PensionService>();
	// 리파지토리 등록
	services.AddScoped<MyPension.Repositories.MemberRepository>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
	............
     app.UseRouting().UseEndpoints(endpoints =>
	{
		endpoints.MapGraphQL();
	});
}

4 graphql test

실행

http://localhost:5000/graphql/ 에서 아래 graphql query 작성 후 실행

query{
  results:searchMember(request:{name:"kim"}){
    id,
    name,
    birthDay
  }
}

결과

{
  "data": {
    "results": [
      {
        "id": 9223372036854776000,
        "name": "kim abc 0",
        "birthDay": "1971-09-09T00:00:00.000+09:00"
      },
      {
        "id": 1,
        "name": "kim abc 1",
        "birthDay": "1971-10-09T00:00:00.000+09:00"
      },
      {
        "id": 9223372036854776000,
        "name": "kim abc 2",
        "birthDay": "1971-11-09T00:00:00.000+09:00"
      },
      {
        "id": 3,
        "name": "kim abc 3",
        "birthDay": "1971-12-09T00:00:00.000+09:00"
      },
........      

5. 문제점

원하는 결과가 나왔는가? 그렇치 않다. 2가지 문제점이 있는데
1. 큰 수에 대한 id값이 잘려있다.
2. 날짜만 필요한데 시간도 같이 나왔다.

id값의 타입은 long인데 javascript의 number 타입은 온전히 8byte 숫자를 담을 수 없어서 뒷자리가 000으로 대체 된다.
birthDay값은 날짜만 원하는데 시간 포맷과 +09:00의 system zone의 offset값도 같이 출력되었다.

c#은 날짜타입이 DateTime만 있고 Date형이 따로 존재하지는 않는다.
참고로 graphql에서의 날짜는 ISO 8061 규칙을 따른다. ISO 8061

6. 해결방법

graphql에서 지원하는 Scralars 타입은 아래 5개가 전부이며 그나마 ID는 String에 대한 alias에 불가하다.
String, Boolean, Int, Float, ID
즉 Boolean, Int, Float를 제외한 모든 타입은 모두 문자이다.
그러면 이제 해결 방법중 가장 간단한 방법을 알아보자
1. id값 long type문제
Startup.cs의 설정을 다음과 같이 수정한다.

 services.AddGraphQLServer()
         .AddQueryType<MyPension.Services.PensionService>()
         .BindRuntimeType<long, HotChocolate.Types.StringType>();
BindRuntimeType<long, StringType> long 타입을 String 타입으로 인식시켜준다.
  1. 날짜 형식 문제
    세 가지 정도 해결방법이 있다.
    1. attribute를 사용해서 DateTime type을 HotCholate.Types.DateType으로 변환
    2. ObjectType을 상속받아서 Graphql용 type을 재정의
    3. Query Resolver에서 DateTime 형을 String으로 변환

위 방법중 2, 3번은 기존 소스코드를 변경해야 하므로 다음에 알아보고 1번 방법을 설명하겠다.

MemberVO.cs

[GraphQLType(typeof(HotChocolate.Types.DateType))]
public DateTime BirthDay { get; set; }

수정 후 결과

{
  "data": {
    "results": [
      {
        "id": "9223372036854775807",
        "name": "kim abc 0",
        "birthDay": "1971-09-09"
      },
      {
        "id": "1",
        "name": "kim abc 1",
        "birthDay": "1971-10-09"
      },
      {
        "id": "9223372036854775805",
        "name": "kim abc 2",
        "birthDay": "1971-11-09"
      },
      {
        "id": "3",
        "name": "kim abc 3",
        "birthDay": "1971-12-09"
      },
......      

0개의 댓글