이번 포스트에서는 asp.net core에서 graphql query에 대한 가장 기초적인 부분에 대해 알아본다.
결국 graphql은 웹 환경에서 서버와 클라이언트간의 데이터 통신 스키마를 잘(?) 잘 정해 놓고 사용하자는 것.
graphql을 처음 봤을때 이건 또 뭐야 알아야 할께 또 생겼네 싶었다. 그러다 곰곰이 생각해보니 이걸 적용하면 개발자간의 다툼이
줄겠구나 하는 생각이 들었다.
예전에는 어떻게 데이터 스키마를 정했는지 내가 했던 경험을 얘기보겠다.
{
"lastName":"kim",
"checkinDate":"2020-01-08",
"checkoutDate":"2020-01-09",
"status":18,
"nation":"KR"
}
{
"results":[{....},{....},{....}]
"isError":false,
"exception":{
"name":null,
"message":null,
}
}
entity 작성
- Member.cs
dto 작성
- MemberSearchRequest.cs
- MemberVO.cs
서비스 및 리파지토리 클래스 작성
- PensionService.cs
- MemberRepository.cs
"HotChocolate.AspNetCore" Version="11.3.7"
- 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);
}
}
}
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();
});
}
실행
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"
},
........
원하는 결과가 나왔는가? 그렇치 않다. 2가지 문제점이 있는데
1. 큰 수에 대한 id값이 잘려있다.
2. 날짜만 필요한데 시간도 같이 나왔다.
id값의 타입은 long인데 javascript의 number 타입은 온전히 8byte 숫자를 담을 수 없어서 뒷자리가 000으로 대체 된다.
birthDay값은 날짜만 원하는데 시간 포맷과 +09:00의 system zone의 offset값도 같이 출력되었다.
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 타입으로 인식시켜준다.
위 방법중 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"
},
......