GraphQL API ๊ฐ๋ฐ: C#๊ณผ Hot Chocolate ๐ซ๐ฅ

์๋ , ์น๊ตฌ๋ค! ์ค๋์ ์ ๋ง ํซํ ์ฃผ์ ๋ก ์ฐพ์์์ด. ๋ฐ๋ก GraphQL API ๊ฐ๋ฐ์ ๋ํด C#๊ณผ Hot Chocolate๋ฅผ ์ฌ์ฉํด์ ์์๋ณผ ๊ฑฐ์ผ. ๐ ์ด ์ฃผ์ ๋ ํ๋ก๊ทธ๋จ ๊ฐ๋ฐ ์นดํ ๊ณ ๋ฆฌ์ C# ๋ถ์ผ์ ์ํ๋ ๋ด์ฉ์ด๋๊น, ์ฐ๋ฆฌ C# ๊ฐ๋ฐ์๋ค์๊ฒ ํนํ ์ ์ฉํ ๊ฑฐ์ผ!
๋จผ์ , GraphQL์ด ๋ญ์ง ๊ฐ๋จํ ์ค๋ช ํ๊ณ ์์ํ ๊ฒ. GraphQL์ Facebook์์ ๊ฐ๋ฐํ ์ฟผ๋ฆฌ ์ธ์ด์ผ. REST API์ ํ๊ณ๋ฅผ ๊ทน๋ณตํ๊ณ ์ ๋ง๋ค์ด์ก์ง. ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํํ๊ฒ ์์ฒญํ๊ณ ๋ฐ์ ์ ์์ด์ ํจ์จ์ ์ด๊ณ ์ ์ฐํด. ๐
๊ทธ๋ฆฌ๊ณ Hot Chocolate๋ ๋ญ๋๊ณ ? C#์ผ๋ก GraphQL ์๋ฒ๋ฅผ ๊ตฌํํ ์ ์๊ฒ ํด์ฃผ๋ ๊ฐ๋ ฅํ ํ๋ ์์ํฌ์ผ. ์ด๋ฆ๋ถํฐ ๋ง์์์? ๐ ์ฝ๋๋ ๋ง์๊ฒ ์งค ์ ์์ ๊ฑฐ์ผ!
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก GraphQL API๋ฅผ C#๊ณผ Hot Chocolate๋ก ๊ฐ๋ฐํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์. ์ค๋น๋์ด? ๊ทธ๋ผ ์ถ๋ฐ~! ๐
1. GraphQL ๊ธฐ์ด ๋ค์ง๊ธฐ ๐
GraphQL์ ๋ํด ๋ ์์ธํ ์์๋ณด์. REST API์ ๋น๊ตํ๋ฉด ์ดํด๊ฐ ์ฌ์ธ ๊ฑฐ์ผ.
REST API vs GraphQL
- REST API: ์ฌ๋ฌ ์๋ํฌ์ธํธ, ๊ณ ์ ๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ
- GraphQL: ๋จ์ผ ์๋ํฌ์ธํธ, ์ ์ฐํ ๋ฐ์ดํฐ ์์ฒญ
GraphQL์ ์ฅ์ ์ ๋ญ๊น? ๐ค
- Over-fetching ๋ฐฉ์ง: ํ์ํ ๋ฐ์ดํฐ๋ง ์์ฒญํด์ ๊ฐ์ ธ์ฌ ์ ์์ด.
- Under-fetching ํด๊ฒฐ: ์ฌ๋ฌ ๋ฆฌ์์ค๋ฅผ ํ ๋ฒ์ ์์ฒญ์ผ๋ก ๊ฐ์ ธ์ฌ ์ ์์ง.
- ๊ฐ๋ ฅํ ํ์ ์์คํ : ์คํค๋ง๋ฅผ ํตํด ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋ช ํํ ์ ์ํ ์ ์์ด.
- ์ค์๊ฐ ์ ๋ฐ์ดํธ: Subscription์ ํตํด ์ค์๊ฐ ๋ฐ์ดํฐ ๋ณ๊ฒฝ์ ๊ฐ์งํ ์ ์์ด.
์ด๋ฐ ์ฅ์ ๋ค ๋๋ฌธ์ GraphQL์ด ์ธ๊ธฐ๋ฅผ ๋๊ณ ์๋ ๊ฑฐ์ผ. ํนํ ๋ณต์กํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ฅผ ๋ค๋ฃจ๋ ํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ง๊ฐ๋ฅผ ๋ฐํํ์ง.
๊ทธ๋ฐ๋ฐ ๋ง์ด์ผ, GraphQL์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์๋ ํฐ ๋์์ด ๋ ๊ฑฐ์ผ. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ํ๋กํ, ์ ๊ณตํ๋ ์๋น์ค, ๋ฆฌ๋ทฐ ๋ฑ ๋ค์ํ ์ ๋ณด๋ฅผ ํจ์จ์ ์ผ๋ก ๊ฐ์ ธ์ฌ ์ ์๊ฑฐ๋ . ํด๋ผ์ด์ธํธ ๊ฐ๋ฐ์๋ค์ด ์ข์ํ ๊ฑฐ์ผ! ๐
์ ๊ทธ๋ฆผ์ ๋ณด๋ฉด REST API์ GraphQL์ ์ฐจ์ด๊ฐ ํ๋์ ๋ค์ด์ค์ง? REST๋ ์ฌ๋ฌ ๊ฐ์ ๊ณ ์ ๋ ์๋ํฌ์ธํธ๊ฐ ์๊ณ , GraphQL์ ํ๋์ ์ ์ฐํ ์๋ํฌ์ธํธ๋ก ๋ชจ๋ ๊ฑธ ํด๊ฒฐํด. ๐
์, ์ด์ GraphQL์ ๊ธฐ๋ณธ ๊ฐ๋ ์ ์์์ผ๋ Hot Chocolate๋ฅผ ์ฌ์ฉํด์ ์ค์ ๋ก C#์์ ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ์์๋ณด์!
2. Hot Chocolate ์์ํ๊ธฐ ๐ซ
Hot Chocolate, ์ด๋ฆ๋ถํฐ ๋ฌ์ฝคํ๊ณ ๋ง์์ง? C#์ผ๋ก GraphQL ์๋ฒ๋ฅผ ๋ง๋ค ๋ ์ฌ์ฉํ๋ ํ๋ ์์ํฌ์ผ. ์ด์ Hot Chocolate๋ฅผ ์ฌ์ฉํด์ GraphQL API๋ฅผ ๋ง๋ค์ด๋ณผ ๊ฑฐ์ผ. ์ค๋น๋์ด? ๐
2.1 ํ๋ก์ ํธ ์ค์
๋จผ์ , ์๋ก์ด ASP.NET Core ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์. ํฐ๋ฏธ๋์ ์ด๊ณ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํด:
dotnet new webapi -n HotChocolateDemo
๊ทธ๋ฆฌ๊ณ Hot Chocolate ํจํค์ง๋ฅผ ์ค์นํด์ผ ํด. NuGet ํจํค์ง ๋งค๋์ ๋ฅผ ์ฌ์ฉํด์ ๋ค์ ํจํค์ง๋ค์ ์ค์นํ์:
- HotChocolate.AspNetCore
- HotChocolate.Data
ํฐ๋ฏธ๋์์ ์ด๋ ๊ฒ ์ ๋ ฅํ๋ฉด ๋ผ:
dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data
์ด์ ๊ธฐ๋ณธ์ ์ธ ์ค์ ์ ๋๋ฌ์ด! ๋ค์ ๋จ๊ณ๋ก ๋์ด๊ฐ๋ณผ๊น?
2.2 GraphQL ์คํค๋ง ์ ์ํ๊ธฐ
GraphQL์ ํต์ฌ์ ์คํค๋ง์ผ. ์คํค๋ง๋ API์์ ์ฌ์ฉํ ์ ์๋ ๋ฐ์ดํฐ ํ์ ๊ณผ ์ฐ์ฐ์ ์ ์ํด. C#์์๋ ํด๋์ค๋ฅผ ์ฌ์ฉํด์ GraphQL ํ์ ์ ์ ์ํ ์ ์์ด.
์๋ฅผ ๋ค์ด, ์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์ ์ํ ๊ฐ๋จํ ์คํค๋ง๋ฅผ ๋ง๋ค์ด๋ณด์:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public List<Talent> Talents { get; set; }
}
public class Talent
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public User Owner { get; set; }
}
์ด๋ ๊ฒ ํ๋ฉด User์ Talent๋ผ๋ ๋ ๊ฐ์ ํ์ ์ ์ ์ํ ๊ฑฐ์ผ. User๋ ์ฌ๋ฌ ๊ฐ์ Talent๋ฅผ ๊ฐ์ง ์ ์๊ณ , Talent๋ ํ ๋ช ์ Owner(User)๋ฅผ ๊ฐ์ง๊ณ ์์ง.
์ด๋ฐ ์์ผ๋ก ์ค์ ๋๋ฉ์ธ ๋ชจ๋ธ์ GraphQL ์คํค๋ง๋ก ํํํ ์ ์์ด. coolํ์ง? ๐
2.3 Query ํ์ ๋ง๋ค๊ธฐ
์ด์ Query ํ์ ์ ๋ง๋ค์ด๋ณผ ๊ฑฐ์ผ. Query ํ์ ์ GraphQL API์์ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ๋ ์ฌ์ฉํด.
public class Query
{
public User GetUser(int id) =>
// ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฌ์ฉ์๋ฅผ ์กฐํํ๋ ๋ก์ง์ด ๋ค์ด๊ฐ ๊ฑฐ์ผ
new User { Id = id, Name = "ํ๊ธธ๋", Email = "hong@example.com" };
public IEnumerable<Talent> GetTalents() =>
// ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฌ๋ฅ ๋ชฉ๋ก์ ์กฐํํ๋ ๋ก์ง์ด ๋ค์ด๊ฐ ๊ฑฐ์ผ
new List<Talent>
{
new Talent { Id = 1, Name = "ํ๋ก๊ทธ๋๋ฐ", Description = "C# ๊ฐ๋ฐ" },
new Talent { Id = 2, Name = "๋์์ธ", Description = "UI/UX ๋์์ธ" }
};
}
์ด Query ํด๋์ค๋ GraphQL์ ๋ฃจํธ ์ฟผ๋ฆฌ ํ์ ์ด ๋ผ. GetUser์ GetTalents ๋ฉ์๋๋ ๊ฐ๊ฐ GraphQL ์ฟผ๋ฆฌ์ ํ๋๊ฐ ๋๋ ๊ฑฐ์ง.
์ด๋ ๊ฒ ํ๋ฉด ํด๋ผ์ด์ธํธ์์ ์ฌ์ฉ์ ์ ๋ณด๋ ์ฌ๋ฅ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ด๋ฐ ์ฟผ๋ฆฌ๋ค์ด ์ค์ ๋ก ๋ง์ด ์ฌ์ฉ๋ ๊ฑฐ์ผ!
2.4 Mutation ํ์ ๋ง๋ค๊ธฐ
Query๊ฐ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ค๋ฉด, Mutation์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ ๋ ์ฌ์ฉํด. ์๋ก์ด ์ฌ๋ฅ์ ๋ฑ๋กํ๋ Mutation์ ๋ง๋ค์ด๋ณด์:
public class Mutation
{
public Talent AddTalent(string name, string description)
{
// ์ค์ ๋ก๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋ก์ด ์ฌ๋ฅ์ ์ถ๊ฐํ๋ ๋ก์ง์ด ๋ค์ด๊ฐ ๊ฑฐ์ผ
var newTalent = new Talent
{
Id = 3, // ์ค์ ๋ก๋ ์๋ ์์ฑ๋๋ ID๋ฅผ ์ฌ์ฉํด์ผ ํด
Name = name,
Description = description
};
return newTalent;
}
}
์ด Mutation ํด๋์ค๋ ์๋ก์ด ์ฌ๋ฅ์ ์ถ๊ฐํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํด. ํด๋ผ์ด์ธํธ์์ ์ด Mutation์ ํธ์ถํ๋ฉด ์๋ก์ด ์ฌ๋ฅ์ด ์์คํ ์ ์ถ๊ฐ๋๋ ๊ฑฐ์ง.
2.5 Hot Chocolate ์ค์ ํ๊ธฐ
์ด์ ๋ง๋ ํ์ ๋ค์ Hot Chocolate์ ๋ฑ๋กํด์ผ ํด. Startup.cs ํ์ผ(๋๋ Program.cs, .NET ๋ฒ์ ์ ๋ฐ๋ผ ๋ค๋ฅผ ์ ์์ด)์ ์ด๊ณ ๋ค์๊ณผ ๊ฐ์ด ์์ ํด:
public void ConfigureServices(IServiceCollection services)
{
services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ๋ค๋ฅธ ๋ฏธ๋ค์จ์ด ์ค์ ๋ค...
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
}
์ด๋ ๊ฒ ํ๋ฉด Hot Chocolate๊ฐ ์ฐ๋ฆฌ๊ฐ ์ ์ํ Query์ Mutation ํ์ ์ ์ฌ์ฉํด์ GraphQL ์๋ฒ๋ฅผ ์ค์ ํด.
์ถํํด! ๐ ์ด์ ๊ธฐ๋ณธ์ ์ธ GraphQL API๊ฐ ์์ฑ๋์ด! ์๋ฒ๋ฅผ ์คํํ๋ฉด /graphql ์๋ํฌ์ธํธ๋ก GraphQL ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ผ ์ ์์ด.
์ฌ๊ธฐ๊น์ง Hot Chocolate๋ฅผ ์ฌ์ฉํด์ ๊ธฐ๋ณธ์ ์ธ GraphQL API๋ฅผ ๋ง๋ค์ด๋ดค์ด. ์ด๋, ์๊ฐ๋ณด๋ค ๊ฐ๋จํ์ง? C#์ ๊ฐ๋ ฅํ ํ์ ์์คํ ๊ณผ Hot Chocolate์ ํธ๋ฆฌํ ๊ธฐ๋ฅ๋ค์ด ๋ง๋์ ์ ๋ง ๋ฉ์ง ์กฐํฉ์ ๋ง๋ค์ด๋ด๋ ๊ฒ ๊ฐ์.
๋ค์ ์น์ ์์๋ ์ข ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ดํด๋ณผ ๊ฑฐ์ผ. ๋ฐ์ดํฐ ๋ก๋, ๋ฏธ๋ค์จ์ด, ์ธ์ฆ/์ธ๊ฐ ๋ฑ ์ค์ ํ๋ก๋์ ํ๊ฒฝ์์ ํ์ํ ๊ธฐ๋ฅ๋ค์ ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ์์๋ณด์๊ณ !
3. Hot Chocolate ๊ณ ๊ธ ๊ธฐ๋ฅ ํ์ฉํ๊ธฐ ๐
์, ์ด์ ๊ธฐ๋ณธ์ ์ธ GraphQL API๋ ๋ง๋ค์์ด. ํ์ง๋ง ์ค์ ํ๋ก๋์ ํ๊ฒฝ์์๋ ๋ ๋ง์ ๊ธฐ๋ฅ์ด ํ์ํ์ง? ์ด๋ฒ์๋ Hot Chocolate์ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ดํด๋ณด๋ฉด์ ์ฐ๋ฆฌ์ API๋ฅผ ํ ๋จ๊ณ ์ ๊ทธ๋ ์ด๋ ํด๋ณผ ๊ฑฐ์ผ! ๐
3.1 ๋ฐ์ดํฐ ๋ก๋ (DataLoader) ์ฌ์ฉํ๊ธฐ
GraphQL์ ์ฌ์ฉํ๋ค ๋ณด๋ฉด N+1 ๋ฌธ์ ๋ฅผ ์์ฃผ ๋ง๋๊ฒ ๋ผ. ์ด๊ฒ ๋ญ๋๊ณ ? ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ๋ชฉ๋ก์ ๊ฐ์ ธ์ค๋ฉด์ ๊ฐ ์ฌ์ฉ์์ ์ฌ๋ฅ ๋ชฉ๋ก๋ ํจ๊ป ๊ฐ์ ธ์ค๋ ค๊ณ ํ ๋ ๋ฐ์ํด. ์ฌ์ฉ์ ๋ชฉ๋ก์ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ 1๋ฒ, ๊ฐ ์ฌ์ฉ์์ ์ฌ๋ฅ์ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ N๋ฒ, ์ด N+1๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋๋ ๊ฑฐ์ง.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด Hot Chocolate๋ DataLoader ํจํด์ ์ง์ํด. ์ด๋ฅผ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ๊ฐ์ ์ฟผ๋ฆฌ๋ฅผ ํ๋๋ก ํฉ์ณ์ ํจ์จ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด.
์๋ฅผ ๋ค์ด, ์ฌ์ฉ์์ ์ฌ๋ฅ์ ๊ฐ์ ธ์ค๋ DataLoader๋ฅผ ๋ง๋ค์ด๋ณด์:
public class TalentsByUserDataLoader : BatchDataLoader<int, IEnumerable<Talent>>
{
private readonly ITalentRepository _talentRepository;
public TalentsByUserDataLoader(
IBatchScheduler batchScheduler,
ITalentRepository talentRepository)
: base(batchScheduler)
{
_talentRepository = talentRepository;
}
protected override async Task<IReadOnlyDictionary<int, IEnumerable<Talent>>> LoadBatchAsync(
IReadOnlyList<int> keys,
CancellationToken cancellationToken)
{
var talents = await _talentRepository.GetTalentsByUserIds(keys);
return talents.ToLookup(t => t.OwnerId);
}
}
์ด๋ ๊ฒ ๋ง๋ DataLoader๋ฅผ ์ฌ์ฉํ๋ฉด, ์ฌ๋ฌ ์ฌ์ฉ์์ ์ฌ๋ฅ์ ํ ๋ฒ์ ํจ์จ์ ์ผ๋ก ๊ฐ์ ธ์ฌ ์ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ฌ์ฉ์ ๋ชฉ๋ก๊ณผ ๊ทธ๋ค์ ์ฌ๋ฅ์ ํ์ํ ๋ ์์ฃผ ์ ์ฉํ๊ฒ ์ง? ๐
3.2 ๋ฏธ๋ค์จ์ด ์ถ๊ฐํ๊ธฐ
Hot Chocolate์์๋ ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํด์ ์ฟผ๋ฆฌ ์คํ ๊ณผ์ ์ ๊ฐ์ ํ ์ ์์ด. ์๋ฅผ ๋ค์ด, ์ฟผ๋ฆฌ ์คํ ์๊ฐ์ ์ธก์ ํ๋ ๋ฏธ๋ค์จ์ด๋ฅผ ๋ง๋ค์ด๋ณด์:
public class QueryTimingMiddleware
{
private readonly FieldDelegate _next;
public QueryTimingMiddleware(FieldDelegate next)
{
_next = next;
}
public async Task InvokeAsync(IMiddlewareContext context)
{
var stopwatch = Stopwatch.StartNew();
await _next(context);
stopwatch.Stop();
context.ContextData["ExecutionTime"] = stopwatch.ElapsedMilliseconds;
}
}
์ด ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ ํ๋์ ์คํ ์๊ฐ์ ์ธก์ ํ ์ ์์ด. ์ฑ๋ฅ ์ต์ ํ์ ์์ฃผ ์ ์ฉํ๊ฒ ์ง? ์ฌ๋ฅ๋ท์์ ์ด๋ค ์ฟผ๋ฆฌ๊ฐ ์ค๋ ๊ฑธ๋ฆฌ๋์ง ์ฝ๊ฒ ํ์ ํ ์ ์์ ๊ฑฐ์ผ.
3.3 ์ธ์ฆ๊ณผ ์ธ๊ฐ ๊ตฌํํ๊ธฐ
์ค์ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ๋ณด์์ด ์ค์ํด. Hot Chocolate์์๋ ASP.NET Core์ ์ธ์ฆ/์ธ๊ฐ ์์คํ ๊ณผ ํตํฉํด์ ์ฌ์ฉํ ์ ์์ด.
์๋ฅผ ๋ค์ด, ํน์ ์ฟผ๋ฆฌ๋ ๋ฎคํ ์ด์ ์ ์ธ์ฆ์ ์๊ตฌํ๋ ๋ฐฉ๋ฒ์ ์ดํด๋ณด์:
[Authorize]
public class SecureMutation
{
public async Task<Talent> AddTalent(string name, string description, [Service] ITalentService talentService)
{
// ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์์ ID๋ฅผ ๊ฐ์ ธ์ด
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// ์ฌ๋ฅ ์ถ๊ฐ ๋ก์ง
return await talentService.AddTalent(name, description, userId);
}
}
์ด๋ ๊ฒ ํ๋ฉด ์ธ์ฆ๋ ์ฌ์ฉ์๋ง ์๋ก์ด ์ฌ๋ฅ์ ์ถ๊ฐํ ์ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ฌ์ฉ์ ์ธ์ฆ์ ์ ๋ง ์ค์ํ์ง? ์ด๋ฐ ์์ผ๋ก ๋ณด์์ ๊ฐํํ ์ ์์ด.
3.4 ๊ตฌ๋ (Subscription) ๊ตฌํํ๊ธฐ
GraphQL์ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ ์ค ํ๋๊ฐ ๋ฐ๋ก ์ค์๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ์ํ ๊ตฌ๋ ์ด์ผ. Hot Chocolate์์๋ ์ด๋ฅผ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด.
์๋ฅผ ๋ค์ด, ์๋ก์ด ์ฌ๋ฅ์ด ์ถ๊ฐ๋ ๋๋ง๋ค ์๋ฆผ์ ๋ฐ๋ ๊ตฌ๋ ์ ๋ง๋ค์ด๋ณด์:
public class Subscription
{
[Subscribe]
public Talent OnTalentAdded([EventMessage] Talent talent) => talent;
}
public class Mutation
{
public async Task<Talent> AddTalent(string name, string description, [Service] ITalentService talentService, [Service] ITopicEventSender eventSender)
{
var newTalent = await talentService.AddTalent(name, description);
await eventSender.SendAsync(nameof(Subscription.OnTalentAdded), newTalent);
return newTalent;
}
}
์ด๋ ๊ฒ ํ๋ฉด ํด๋ผ์ด์ธํธ์์ ์๋ก์ด ์ฌ๋ฅ์ด ์ถ๊ฐ๋ ๋๋ง๋ค ์ค์๊ฐ์ผ๋ก ์๋ฆผ์ ๋ฐ์ ์ ์์ด. ์ฌ๋ฅ๋ท์์ ์๋ก์ด ์ฌ๋ฅ์ด ๋ฑ๋ก๋๋ฉด ๋ฐ๋ก ์ ์ ์์ผ๋ ์ผ๋ง๋ ํธ๋ฆฌํ ๊น? ๐
3.5 ํ์ด์ง๋ค์ด์ ๊ตฌํํ๊ธฐ
๋๋์ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃฐ ๋๋ ํ์ด์ง๋ค์ด์ ์ด ํ์์ผ. Hot Chocolate์์๋ ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์ ์ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด.
public class Query
{
[UsePaging]
public IQueryable<Talent> GetTalents([Service] ITalentRepository repository)
{
return repository.GetTalents();
}
}
์ด๋ ๊ฒ ํ๋ฉด ์๋์ผ๋ก ํ์ด์ง๋ค์ด์ ์ด ์ ์ฉ๋ผ. ์ฌ๋ฅ๋ท์์ ์ฌ๋ฅ ๋ชฉ๋ก์ ๋ณผ ๋ ํ์ด์ง ๋จ์๋ก ๋ก๋ฉํ ์ ์์ผ๋ ์ฑ๋ฅ๋ ์ข์์ง๊ณ ์ฌ์ฉ์ ๊ฒฝํ๋ ํฅ์๋๊ฒ ์ง?
3.6 ํํฐ๋ง๊ณผ ์ ๋ ฌ ๊ตฌํํ๊ธฐ
Hot Chocolate๋ ํํฐ๋ง๊ณผ ์ ๋ ฌ ๊ธฐ๋ฅ๋ ์ ๊ณตํด. ์ด๋ฅผ ์ฌ์ฉํ๋ฉด ํด๋ผ์ด์ธํธ์์ ์ํ๋ ๋๋ก ๋ฐ์ดํฐ๋ฅผ ํํฐ๋งํ๊ณ ์ ๋ ฌํ ์ ์์ด.
[UseFiltering]
[UseSorting]
public IQueryable<Talent> GetTalents([Service] ITalentRepository repository)
{
return repository.GetTalents();
}
์ด๋ ๊ฒ ํ๋ฉด ํด๋ผ์ด์ธํธ์์ ๋ค์ํ ์กฐ๊ฑด์ผ๋ก ์ฌ๋ฅ์ ๊ฒ์ํ๊ณ ์ ๋ ฌํ ์ ์์ด. ์ฌ๋ฅ๋ท ์ฌ์ฉ์๋ค์ด ์ํ๋ ์ฌ๋ฅ์ ์ฝ๊ฒ ์ฐพ์ ์ ์๊ฒ ์ง? ๐
์ฌ๊ธฐ๊น์ง Hot Chocolate์ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ดํด๋ดค์ด. ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ํ์ฉํ๋ฉด ์ ๋ง ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ GraphQL API๋ฅผ ๋ง๋ค ์ ์์ด. ์ฌ๋ฅ๋ท ๊ฐ์ ๋ณต์กํ ํ๋ซํผ์์๋ ์ถฉ๋ถํ ํ์ฉํ ์ ์์ ๊ฑฐ์ผ!
๋ค์ ์น์ ์์๋ ์ด๋ฐ ๊ธฐ๋ฅ๋ค์ ์ค์ ๋ก ์ด๋ป๊ฒ ์กฐํฉํด์ ์ฌ์ฉํ๋์ง, ๊ทธ๋ฆฌ๊ณ ์ฑ๋ฅ ์ต์ ํ๋ ์ด๋ป๊ฒ ํ๋์ง ์์๋ณผ ๊ฑฐ์ผ. ๊ณ์ ๋ฐ๋ผ์! ๐
4. ์ค์ ์์ : ์ฌ๋ฅ๋ท GraphQL API ๊ตฌํํ๊ธฐ ๐
์, ์ด์ ์ฐ๋ฆฌ๊ฐ ๋ฐฐ์ด ๋ด์ฉ์ ํ ๋๋ก ์ค์ ์ฌ๋ฅ๋ท์ GraphQL API๋ฅผ ๊ตฌํํด๋ณผ ๊ฑฐ์ผ. ์ฌ๋ฅ๋ท์ ๋ค์ํ ์ฌ๋ฅ์ ๊ฐ์ง ์ฌ๋๋ค์ด ๋ชจ์ฌ ์๋ก์ ์ฌ๋ฅ์ ๊ณต์ ํ๊ณ ๊ฑฐ๋ํ๋ ํ๋ซํผ์ด์ง? ์ด๋ฐ ๋ณต์กํ ์์คํ ์ GraphQL๋ก ๊ตฌํํ๋ฉด ์ผ๋ง๋ ๋ฉ์ง์ง ์์์ด ๊ฐ๋? ๐
4.1 ์คํค๋ง ์ ์ํ๊ธฐ
๋จผ์ ์ฌ๋ฅ๋ท์ ์ฃผ์ ์ํฐํฐ๋ค์ ์ ์ํด๋ณด์:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public List<Talent> Talents { get; set; }
public List<Review> Reviews { get; set; }
}
public class Talent
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public User Owner { get; set; }
public List<Category> Categories { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public List<Talent> Talents { get; set; }
}
public class Review
{
public int Id { get; set; }
public int Rating { get; set; }
public string Comment { get; set; }
public User Reviewer { get; set; }
public Talent Talent { get; set; }
}
public class Order
{
public int Id { get; set; }
public User Buyer { get; set; }
public Talent Talent { get; set; }
public DateTime OrderDate { get; set; }
public OrderStatus Status { get; set; }
}
public enum OrderStatus
{
Pending,
Completed,
Cancelled
}
์ด๋ ๊ฒ ์ ์ํ ์ํฐํฐ๋ค์ด ์ฌ๋ฅ๋ท์ ํต์ฌ ๋ฐ์ดํฐ ๋ชจ๋ธ์ด ๋๋ ๊ฑฐ์ผ. ์ฌ์ฉ์, ์ฌ๋ฅ, ์นดํ ๊ณ ๋ฆฌ, ๋ฆฌ๋ทฐ, ์ฃผ๋ฌธ ๋ฑ ํ์ํ ๋ชจ๋ ์ ๋ณด๋ฅผ ๋ด๊ณ ์์ง?
4.2 ์ฟผ๋ฆฌ ํ์ ๊ตฌํํ๊ธฐ
์ด์ ์ด ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ์ฟผ๋ฆฌ๋ค์ ๋ง๋ค์ด๋ณด์:
public class Query
{
[UseDbContext(typeof(AppDbContext))]
[UsePaging]
[UseFiltering]
[UseSorting]
public IQueryable<User> GetUsers([ScopedService] AppDbContext context)
{
return context.Users;
}
[UseDbContext(typeof(AppDbContext))]
[UsePaging]
[UseFiltering]
[UseSorting]
public IQueryable<Talent> GetTalents([ScopedService] AppDbContext context)
{
return context.Talents;
}
[UseDbContext(typeof(AppDbContext))]
public async Task<User> GetUserById(int id, [ScopedService] AppDbContext context)
{
return await context.Users.FindAsync(id);
}
[UseDbContext(typeof(AppDbContext))]
public async Task<Talent> GetTalentById(int id, [ScopedService] AppDbContext context)
{
return await context.Talents.FindAsync(id);
}
[UseDbContext(typeof(AppDbContext))]
[UsePaging]
[UseFiltering]
[UseSorting]
public IQueryable<Category> GetCategories([ScopedService] AppDbContext context)
{
return context.Categories;
}
}
์ด๋ ๊ฒ ํ๋ฉด ์ฌ์ฉ์, ์ฌ๋ฅ, ์นดํ ๊ณ ๋ฆฌ ๋ฑ์ ์กฐํํ ์ ์๋ ๋ค์ํ ์ฟผ๋ฆฌ๊ฐ ์๊ธฐ๋ ๊ฑฐ์ผ. ํ์ด์ง, ํํฐ๋ง, ์ ๋ ฌ ๊ธฐ๋ฅ๋ ์ถ๊ฐํ์ผ๋ ํด๋ผ์ด์ธํธ์์ ์ํ๋ ๋๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด!
4.3 ๋ฎคํ ์ด์ ํ์ ๊ตฌํํ๊ธฐ
์ด๋ฒ์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฎคํ ์ด์ ์ ๋ง๋ค์ด๋ณผ ๊ฑฐ์ผ:
public class Mutation
{
[UseDbContext(typeof(AppDbContext))]
public async Task<User> CreateUser(CreateUserInput input, [ScopedService] AppDbContext context)
{
var user = new User
{
Name = input.Name,
Email = input.Email
};
context.Users.Add(user);
await context.SaveChangesAsync();
return user;
}
[UseDbContext(typeof(AppDbContext))]
public async Task<Talent> CreateTalent(CreateTalentInput input, [ScopedService] AppDbContext context)
{
var talent = new Talent
{
Name = input.Name,
Description = input.Description,
Price = input.Price,
OwnerId = input.OwnerId
};
context.Talents.Add(talent);
await context.SaveChangesAsync();
return talent;
}
[UseDbContext(typeof(AppDbContext))]
public async Task<Review> CreateReview(CreateReviewInput input, [ScopedService] AppDbContext context)
{
var review = new Review
{
Rating = input.Rating,
Comment = input.Comment,
ReviewerId = input.ReviewerId,
TalentId = input.TalentId
};
context.Reviews.Add(review);
await context.SaveChangesAsync();
return review;
}
[UseDbContext(typeof(AppDbContext))]
public async Task<Order> CreateOrder(CreateOrderInput input, [ScopedService] AppDbContext context)
{
var order = new Order
{
BuyerId = input.BuyerId,
TalentId = input.TalentId,
OrderDate = DateTime.UtcNow,
Status = OrderStatus.Pending
};
context.Orders.Add(order);
await context.SaveChangesAsync();
return order;
}
}
์ด๋ ๊ฒ ํ๋ฉด ์๋ก์ด ์ฌ์ฉ์ ๋ฑ๋ก, ์ฌ๋ฅ ๋ฑ๋ก, ๋ฆฌ๋ทฐ ์์ฑ, ์ฃผ๋ฌธ ์์ฑ ๋ฑ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ด. ์ฌ๋ฅ๋ท์ ํต์ฌ ๊ธฐ๋ฅ๋ค์ด ๋ค ์ฌ๊ธฐ์ ์์ง?
4.4 ๊ตฌ๋ ํ์ ๊ตฌํํ๊ธฐ
์ค์๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ์ํ ๊ตฌ๋ ๊ธฐ๋ฅ๋ ์ถ๊ฐํด๋ณด์:
public class Subscription
{
[Subscribe]
[Topic]
public Talent OnTalentAdded([EventMessage] Talent talent) => talent;
[Subscribe]
[Topic]
public Review OnReviewAdded([EventMessage] Review review) => review;
[Subscribe]
[Topic]
public Order OnOrderStatusChanged([EventMessage] Order order) => order;
}
์ด๋ ๊ฒ ํ๋ฉด ์๋ก์ด ์ฌ๋ฅ์ด ๋ฑ๋ก๋๊ฑฐ๋, ์๋ก์ด ๋ฆฌ๋ทฐ๊ฐ ์์ฑ๋๊ฑฐ๋, ์ฃผ๋ฌธ ์ํ๊ฐ ๋ณ๊ฒฝ๋ ๋ ์ค์๊ฐ์ผ๋ก ์๋ฆผ์ ๋ฐ์ ์ ์์ด. ์ฌ๋ฅ๋ท ์ฌ์ฉ์๋ค์ด ์ค์๊ฐ์ผ๋ก ์ ๋ณด๋ฅผ ๋ฐ์๋ณผ ์ ์๊ฒ ์ง?
4.5 ๋ฐ์ดํฐ ๋ก๋ ๊ตฌํํ๊ธฐ
์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด ๋ฐ์ดํฐ ๋ก๋๋ ๊ตฌํํด๋ณด์:
public class TalentsByUserDataLoader : BatchDataLoader<int, IEnumerable<Talent>>
{
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
public TalentsByUserDataLoader(
IBatchScheduler batchScheduler,
IDbContextFactory<AppDbContext> dbContextFactory)
: base(batchScheduler)
{
_dbContextFactory = dbContextFactory;
}
protected override async Task<IReadOnlyDictionary<int, IEnumerable<Talent>>> LoadBatchAsync(
IReadOnlyList<int> keys,
CancellationToken cancellationToken)
{
await using AppDbContext dbContext =
await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Talents
.Where(t => keys.Contains(t.OwnerId))
.GroupBy(t => t.OwnerId)
.ToDictionaryAsync(g => g.Key, g => g.AsEnumerable(), cancellationToken);
}
}
์ด ๋ฐ์ดํฐ ๋ก๋๋ฅผ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ์ฌ์ฉ์์ ์ฌ๋ฅ์ ํ ๋ฒ์ ํจ์จ์ ์ผ๋ก ๊ฐ์ ธ์ฌ ์ ์์ด. N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์ง!
4.6 ๋ฏธ๋ค์จ์ด ์ถ๊ฐํ๊ธฐ
๋ง์ง๋ง์ผ๋ก, ์๋ฌ ์ฒ๋ฆฌ์ ๋ก๊น ์ ์ํ ๋ฏธ๋ค์จ์ด๋ฅผ ์ถ๊ฐํด๋ณด์:
public class ErrorLoggingMiddleware
{
private readonly FieldDelegate _next;
private readonly ILogger _logger;
public ErrorLoggingMiddleware(FieldDelegate next, ILogger<ErrorLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(IMiddlewareContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while executing the GraphQL request");
throw;
}
}
}
์ด ๋ฏธ๋ค์จ์ด๋ฅผ ์ฌ์ฉํ๋ฉด GraphQL ์์ฒญ ์ฒ๋ฆฌ ์ค ๋ฐ์ํ๋ ๋ชจ๋ ์๋ฌ๋ฅผ ๋ก๊น ํ ์ ์์ด. ๋ฌธ์ ๊ฐ ์๊ฒผ์ ๋ ๋น ๋ฅด๊ฒ ๋์ํ ์ ์๊ฒ ์ง?
4.7 ์ต์ข ์ค์
์ด์ ๋ชจ๋ ๊ฒ์ ์กฐํฉํด์ ์ต์ข ์ค์ ์ ํด๋ณด์:
public void ConfigureServices(IServiceCollection services)
{
services
.AddPooledDbContextFactory<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")))
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddSubscriptionType<Subscription>()
.AddFiltering()
.AddSorting()
.AddProjections()
.AddDataLoader<TalentsByUserDataLoader>()
.AddErrorFilter<ErrorLoggingMiddleware>()
.AddInMemorySubscriptions();
}
์ด๋ ๊ฒ ํ๋ฉด ์ฐ๋ฆฌ๊ฐ ๊ตฌํํ ๋ชจ๋ ๊ธฐ๋ฅ์ด ํ๋๋ก ํตํฉ๋๋ ๊ฑฐ์ผ! ์ฌ๋ฅ๋ท์ ์์ ํ GraphQL API๊ฐ ํ์ํ ๊ฑฐ์ง. ๐
์, ์ฌ๊ธฐ๊น์ง ์ฌ๋ฅ๋ท์ GraphQL API๋ฅผ ๊ตฌํํด๋ดค์ด. ์ด๋, ์๊ฐ๋ณด๋ค ๋ณต์กํ์ง ์์ง? C#๊ณผ Hot Chocolate๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ ๊ฒ ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ API๋ฅผ ๋ง๋ค ์ ์์ด. ์ค์ ํ๋ก์ ํธ์์ ์ด๋ฐ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ฉด ํด๋ผ์ด์ธํธ ๊ฐ๋ฐ์๋ค์ด ์ ๋ง ์ข์ํ ๊ฑฐ์ผ. ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํํ๊ฒ ์์ฒญํ ์ ์๊ณ , ์ค์๊ฐ ์ ๋ฐ์ดํธ๋ ๋ฐ์ ์ ์์ผ๋๊น!
GraphQL์ ์ฅ์ ์ ์ต๋ํ ์ด๋ฆฌ๋ฉด์๋, C#์ ๊ฐ๋ ฅํ ํ์ ์์คํ ๊ณผ Hot Chocolate์ ํธ๋ฆฌํ ๊ธฐ๋ฅ๋ค์ ํ์ฉํด ์์ ์ ์ด๊ณ ์ฑ๋ฅ ์ข์ API๋ฅผ ๋ง๋ค ์ ์์์ด. ์ด์ ์ด API๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ฉ์ง ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์ ๋ง๋ค ์ ์๊ฒ ์ง?
๊ณ์ํด์ GraphQL๊ณผ C#, Hot Chocolate์ ๋ํด ๊ณต๋ถํ๊ณ ์คํํด๋ด. ๋ ๋ง์ ๊ธฐ๋ฅ๊ณผ ์ต์ ํ ๋ฐฉ๋ฒ์ ๋ฐ๊ฒฌํ ์ ์์ ๊ฑฐ์ผ. ํ์ดํ ! ๐
- ์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์ฌ๋ฅ๋ท์ ์ต์ AI ๊ธฐ์ ๊ณผ ๋ฒ๋ฅ ์ ๊ธฐ๋ฐํ์ฌ ์์ฌ์ ์ง์ ์ฌ์ฐ๊ถ์ ์ ๊ทน์ ์ผ๋ก ๋ณดํธํ๋ฉฐ,
๋ฌด๋จ ์ฌ์ฉ ๋ฐ ์นจํด ํ์์ ๋ํด ๋ฒ์ ๋์์ ํ ๊ถ๋ฆฌ๋ฅผ ๋ณด์ ํฉ๋๋ค.
ยฉ 2025 ์ฌ๋ฅ๋ท | All rights reserved.
๋๊ธ 0๊ฐ