GraphQL API ๊ฐœ๋ฐœ: C#๊ณผ Hot Chocolate ๐Ÿซ๐Ÿ”ฅ

์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - 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์„ ์‚ฌ์šฉํ•˜๋ฉด ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ์—์„œ๋„ ํฐ ๋„์›€์ด ๋  ๊ฑฐ์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž ํ”„๋กœํ•„, ์ œ๊ณตํ•˜๋Š” ์„œ๋น„์Šค, ๋ฆฌ๋ทฐ ๋“ฑ ๋‹ค์–‘ํ•œ ์ •๋ณด๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๊ฑฐ๋“ . ํด๋ผ์ด์–ธํŠธ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ข‹์•„ํ•  ๊ฑฐ์•ผ! ๐Ÿ˜‰

GraphQL vs REST API ๋น„๊ต REST API 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์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹คํ—˜ํ•ด๋ด. ๋” ๋งŽ์€ ๊ธฐ๋Šฅ๊ณผ ์ตœ์ ํ™” ๋ฐฉ๋ฒ•์„ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ. ํ™”์ดํŒ…! ๐Ÿš€