์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿš€ WCF์™€ gRPC๋กœ ๋งŒ๋“œ๋Š” ๋ถ„์‚ฐ ์‹œ์Šคํ…œ ์™„์ „์ •๋ณต

๐Ÿš€ WCF์™€ gRPC๋กœ ๋งŒ๋“œ๋Š” ๋ถ„์‚ฐ ์‹œ์Šคํ…œ ์™„์ „์ •๋ณต

์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์˜ ๋ชจ๋“  ๊ฒƒ, ์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ์•Œ๋ ค์ค„๊ฒŒ!

๐ŸŽฏ ์™œ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์ด ํ•„์š”ํ• ๊นŒ?

์•ผ, ์š”์ฆ˜ ํ”„๋กœ๊ทธ๋žจ ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋ฉด ์ง„์งœ ๋ณต์žกํ•˜์ง€ ์•Š์•„? ์˜ˆ์ „์—๋Š” ํ•˜๋‚˜์˜ ๊ฑฐ๋Œ€ํ•œ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ๋ชจ๋“  ๊ฑธ ์ฒ˜๋ฆฌํ–ˆ๋Š”๋ฐ, ์ด์ œ๋Š” ๊ทธ๋ ‡๊ฒŒ ํ•˜๋ฉด ์•ˆ ๋˜๋Š” ์‹œ๋Œ€๊ฐ€ ์™”์–ด. ๐Ÿค”

์ƒ๊ฐํ•ด๋ด. ๋„ทํ”Œ๋ฆญ์Šค ๊ฐ™์€ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ ๋‹ค๊ณ  ์น˜์ž. ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ, ์˜์ƒ ์ŠคํŠธ๋ฆฌ๋ฐ, ์ถ”์ฒœ ์•Œ๊ณ ๋ฆฌ์ฆ˜, ๊ฒฐ์ œ ์‹œ์Šคํ…œ... ์ด ๋ชจ๋“  ๊ฑธ ํ•˜๋‚˜์˜ ํ”„๋กœ๊ทธ๋žจ์— ๋•Œ๋ ค ๋„ฃ์œผ๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ? ์œ ์ง€๋ณด์ˆ˜ ์ง€์˜ฅ์ด ํŽผ์ณ์ง€๊ฒ ์ง€! ๐Ÿ˜ฑ

๊ทธ๋ž˜์„œ ๋“ฑ์žฅํ•œ ๊ฒŒ ๋ฐ”๋กœ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ(Distributed System)์ด์•ผ. ๊ฐ๊ฐ์˜ ๊ธฐ๋Šฅ์„ ๋…๋ฆฝ์ ์ธ ์„œ๋น„์Šค๋กœ ์ชผ๊ฐœ์„œ, ์„œ๋กœ ํ†ต์‹ ํ•˜๋ฉด์„œ ํ˜‘๋ ฅํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฑฐ์ง€. ๋งˆ์น˜ ์˜ค์ผ€์ŠคํŠธ๋ผ์ฒ˜๋Ÿผ ๊ฐ์ž์˜ ์•…๊ธฐ๊ฐ€ ์กฐํ™”๋กญ๊ฒŒ ์—ฐ์ฃผํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ง์ด์•ผ! ๐ŸŽผ

๋ถ„์‚ฐ ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ์žฅ์  โœจ ํ™•์žฅ์„ฑ: ํŠน์ • ์„œ๋น„์Šค๋งŒ ํ•„์š”์— ๋”ฐ๋ผ ๋Š˜๋ฆด ์ˆ˜ ์žˆ์–ด
โœจ ์œ ์ง€๋ณด์ˆ˜: ํ•œ ๋ถ€๋ถ„๋งŒ ์ˆ˜์ •ํ•ด๋„ ์ „์ฒด์— ์˜ํ–ฅ ์—†์–ด
โœจ ์žฅ์•  ๊ฒฉ๋ฆฌ: ํ•œ ์„œ๋น„์Šค๊ฐ€ ์ฃฝ์–ด๋„ ๋‹ค๋ฅธ ์„œ๋น„์Šค๋Š” ์‚ด์•„์žˆ์–ด
โœจ ๊ธฐ์ˆ  ๋‹ค์–‘์„ฑ: ๊ฐ ์„œ๋น„์Šค๋งˆ๋‹ค ์ตœ์ ์˜ ๊ธฐ์ˆ  ์Šคํƒ ์„ ํƒ ๊ฐ€๋Šฅ

๊ทผ๋ฐ ์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ. ์ด๋ ‡๊ฒŒ ์ชผ๊ฐœ์ง„ ์„œ๋น„์Šค๋“ค์ด ์–ด๋–ป๊ฒŒ ์„œ๋กœ ๋Œ€ํ™”๋ฅผ ๋‚˜๋ˆŒ๊นŒ? ๐Ÿ’ฌ

๋ฐ”๋กœ ์—ฌ๊ธฐ์„œ WCF(Windows Communication Foundation)์™€ gRPC(Google Remote Procedure Call)๊ฐ€ ๋“ฑ์žฅํ•˜๋Š” ๊ฑฐ์•ผ! ์ด ๋‘ ์นœ๊ตฌ๋“ค์€ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์ด์ง€.

๋ถ„์‚ฐ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ์‚ฌ์šฉ์ž ์„œ๋น„์Šค ๋กœ๊ทธ์ธ/ํšŒ์›๊ด€๋ฆฌ ์ฃผ๋ฌธ ์„œ๋น„์Šค ์ฃผ๋ฌธ์ฒ˜๋ฆฌ/๊ฒฐ์ œ ์žฌ๊ณ  ์„œ๋น„์Šค ์žฌ๊ณ ๊ด€๋ฆฌ/๋ฐฐ์†ก ์•Œ๋ฆผ ์„œ๋น„์Šค ๋ฉ”์‹œ์ง€/ํ‘ธ์‹œ ํ†ต์‹  ๋ ˆ์ด์–ด WCF / gRPC ๊ฐ ์„œ๋น„์Šค๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋ฉฐ ํ†ต์‹  ๋ ˆ์ด์–ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์Šต๋‹ˆ๋‹ค

๐Ÿ›๏ธ WCF: ๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ์˜ ํ†ต์‹  ํ”„๋ ˆ์ž„์›Œํฌ

WCF๋Š” 2006๋…„์— ๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ๊ฐ€ .NET Framework 3.0๊ณผ ํ•จ๊ป˜ ์ถœ์‹œํ•œ ํ†ต์‹  ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๋‹น์‹œ์—๋Š” ์ •๋ง ํ˜์‹ ์ ์ด์—ˆ์–ด! ๐ŸŽ‰

WCF ์ด์ „์—๋Š” ์›น ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๋ ค๋ฉด ASMX, Remoting, MSMQ ๋“ฑ ์—ฌ๋Ÿฌ ๊ธฐ์ˆ ์„ ๋”ฐ๋กœ๋”ฐ๋กœ ๋ฐฐ์›Œ์•ผ ํ–ˆ๊ฑฐ๋“ . ๊ทผ๋ฐ WCF๋Š” ์ด ๋ชจ๋“  ๊ฑธ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ–ˆ์–ด. ๋งˆ์น˜ ์Šค์œ„์Šค ์•„๋ฏธ ๋‚˜์ดํ”„์ฒ˜๋Ÿผ ํ•˜๋‚˜๋กœ ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ๊ฑฐ์ง€! ๐Ÿ”ง

WCF์˜ ํ•ต์‹ฌ ๊ฐœ๋…: ABC

WCF๋ฅผ ์ดํ•ดํ•˜๋ ค๋ฉด ABC๋ฅผ ์•Œ์•„์•ผ ํ•ด. ์ด๊ฑด Address, Binding, Contract์˜ ์•ฝ์ž์•ผ.

๐Ÿ“ Address (์ฃผ์†Œ): ์„œ๋น„์Šค๊ฐ€ ์–ด๋””์— ์žˆ๋Š”์ง€ ์•Œ๋ ค์ฃผ๋Š” ์œ„์น˜ ์ •๋ณด์•ผ. ๋งˆ์น˜ ์ง‘ ์ฃผ์†Œ์ฒ˜๋Ÿผ!
์˜ˆ๋ฅผ ๋“ค๋ฉด http://localhost:8080/MyService ์ด๋Ÿฐ ์‹์ด์ง€.

๐Ÿ”— Binding (๋ฐ”์ธ๋”ฉ): ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ํ†ต์‹ ํ•  ๊ฑด์ง€ ์ •ํ•˜๋Š” ๊ฑฐ์•ผ. HTTP๋ฅผ ์“ธ ๊ฑด์ง€, TCP๋ฅผ ์“ธ ๊ฑด์ง€, ๋ณด์•ˆ์€ ์–ด๋–ป๊ฒŒ ํ•  ๊ฑด์ง€ ๋“ฑ๋“ฑ.

๐Ÿ“‹ Contract (๊ณ„์•ฝ): ์„œ๋น„์Šค๊ฐ€ ์–ด๋–ค ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š”์ง€ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์•ผ. ๋ฉ”๋‰ดํŒ ๊ฐ™์€ ๊ฑฐ๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋ผ!

WCF์˜ ์ฃผ์š” ๋ฐ”์ธ๋”ฉ ์ข…๋ฅ˜ BasicHttpBinding: ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ HTTP ๋ฐ”์ธ๋”ฉ, ์›น ์„œ๋น„์Šค ํ˜ธํ™˜์„ฑ ์ตœ๊ณ 
WSHttpBinding: WS-* ํ‘œ์ค€ ์ง€์›, ๋ณด์•ˆ๊ณผ ํŠธ๋žœ์žญ์…˜ ๊ธฐ๋Šฅ ํฌํ•จ
NetTcpBinding: TCP ๊ธฐ๋ฐ˜, ์„ฑ๋Šฅ์ด ๊ฐ€์žฅ ๋น ๋ฆ„ (์ธํŠธ๋ผ๋„ท ํ™˜๊ฒฝ ์ตœ์ )
NetNamedPipeBinding: ๊ฐ™์€ ๋จธ์‹  ๋‚ด ํ”„๋กœ์„ธ์Šค ๊ฐ„ ํ†ต์‹ ์šฉ
NetMsmqBinding: ๋ฉ”์‹œ์ง€ ํ ๊ธฐ๋ฐ˜, ๋น„๋™๊ธฐ ํ†ต์‹ ์— ์ ํ•ฉ

์‹ค์ „ WCF ์„œ๋น„์Šค ๊ตฌํ˜„ํ•˜๊ธฐ

์ž, ์ด์ œ ์‹ค์ œ๋กœ WCF ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณผ๊นŒ? ๊ฐ„๋‹จํ•œ ๊ณ„์‚ฐ๊ธฐ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿงฎ

๋จผ์ € ์„œ๋น„์Šค ๊ณ„์•ฝ(Service Contract)์„ ์ •์˜ํ•ด์•ผ ํ•ด:

using System.ServiceModel;

[ServiceContract]
public interface ICalculatorService
{
    [OperationContract]
    int Add(int a, int b);
    
    [OperationContract]
    int Subtract(int a, int b);
    
    [OperationContract]
    int Multiply(int a, int b);
    
    [OperationContract]
    double Divide(int a, int b);
}

[ServiceContract] ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋Š” "์ด ์ธํ„ฐํŽ˜์ด์Šค๋Š” WCF ์„œ๋น„์Šค์•ผ!"๋ผ๊ณ  ์„ ์–ธํ•˜๋Š” ๊ฑฐ๊ณ , [OperationContract]๋Š” "์ด ๋ฉ”์„œ๋“œ๋Š” ์™ธ๋ถ€์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์–ด!"๋ผ๊ณ  ์•Œ๋ ค์ฃผ๋Š” ๊ฑฐ์•ผ.

์ด์ œ ์‹ค์ œ ๊ตฌํ˜„ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž:

public class CalculatorService : ICalculatorService
{
    public int Add(int a, int b)
    {
        Console.WriteLine($"Add ํ˜ธ์ถœ๋จ: {a} + {b}");
        return a + b;
    }
    
    public int Subtract(int a, int b)
    {
        Console.WriteLine($"Subtract ํ˜ธ์ถœ๋จ: {a} - {b}");
        return a - b;
    }
    
    public int Multiply(int a, int b)
    {
        Console.WriteLine($"Multiply ํ˜ธ์ถœ๋จ: {a} * {b}");
        return a * b;
    }
    
    public double Divide(int a, int b)
    {
        if (b == 0)
            throw new DivideByZeroException("0์œผ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค!");
        
        Console.WriteLine($"Divide ํ˜ธ์ถœ๋จ: {a} / {b}");
        return (double)a / b;
    }
}

์„œ๋น„์Šค๋ฅผ ํ˜ธ์ŠคํŒ…ํ•˜๋Š” ์ฝ”๋“œ๋„ ํ•„์š”ํ•ด. ์ฝ˜์†” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ:

using System;
using System.ServiceModel;

class Program
{
    static void Main(string[] args)
    {
        // ์„œ๋น„์Šค ํ˜ธ์ŠคํŠธ ์ƒ์„ฑ
        using (ServiceHost host = new ServiceHost(typeof(CalculatorService)))
        {
            // ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ (Address, Binding, Contract)
            host.AddServiceEndpoint(
                typeof(ICalculatorService),
                new BasicHttpBinding(),
                "http://localhost:8080/Calculator"
            );
            
            // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ตํ™˜ ํ™œ์„ฑํ™” (ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋น„์Šค ์ •๋ณด๋ฅผ ๊ฐ€์ ธ๊ฐˆ ์ˆ˜ ์žˆ๊ฒŒ)
            ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
            smb.HttpGetEnabled = true;
            host.Description.Behaviors.Add(smb);
            
            // ์„œ๋น„์Šค ์‹œ์ž‘
            host.Open();
            Console.WriteLine("๊ณ„์‚ฐ๊ธฐ ์„œ๋น„์Šค๊ฐ€ ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค!");
            Console.WriteLine("์ฃผ์†Œ: http://localhost:8080/Calculator");
            Console.WriteLine("์ข…๋ฃŒํ•˜๋ ค๋ฉด Enter๋ฅผ ๋ˆ„๋ฅด์„ธ์š”...");
            Console.ReadLine();
            
            // ์„œ๋น„์Šค ์ข…๋ฃŒ
            host.Close();
        }
    }
}

ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋Š” ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด:

using System;
using System.ServiceModel;

class Client
{
    static void Main(string[] args)
    {
        // ์ฑ„๋„ ํŒฉํ† ๋ฆฌ ์ƒ์„ฑ
        ChannelFactory<ICalculatorService> factory = 
            new ChannelFactory<ICalculatorService>(
                new BasicHttpBinding(),
                new EndpointAddress("http://localhost:8080/Calculator")
            );
        
        // ํ”„๋ก์‹œ ์ƒ์„ฑ
        ICalculatorService proxy = factory.CreateChannel();
        
        try
        {
            // ์„œ๋น„์Šค ํ˜ธ์ถœ
            Console.WriteLine($"10 + 5 = {proxy.Add(10, 5)}");
            Console.WriteLine($"10 - 5 = {proxy.Subtract(10, 5)}");
            Console.WriteLine($"10 * 5 = {proxy.Multiply(10, 5)}");
            Console.WriteLine($"10 / 5 = {proxy.Divide(10, 5)}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {ex.Message}");
        }
        finally
        {
            // ์ฑ„๋„ ๋‹ซ๊ธฐ
            ((IClientChannel)proxy).Close();
            factory.Close();
        }
        
        Console.ReadLine();
    }
}
๐Ÿ’ก Pro Tip: WCF ์„œ๋น„์Šค๋ฅผ ๊ฐœ๋ฐœํ•  ๋•Œ๋Š” ํ•ญ์ƒ using ๋ฌธ์„ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ๋ช…์‹œ์ ์œผ๋กœ Close()๋ฅผ ํ˜ธ์ถœํ•ด์„œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ •๋ฆฌํ•ด์•ผ ํ•ด. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์—ฐ๊ฒฐ์ด ๊ณ„์† ์—ด๋ ค์žˆ์–ด์„œ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ !

WCF์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค

WCF๋Š” ๊ธฐ๋ณธ์ ์ธ ํ†ต์‹  ์™ธ์—๋„ ์—„์ฒญ๋‚˜๊ฒŒ ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด. ๋ช‡ ๊ฐ€์ง€ ์ค‘์š”ํ•œ ๊ฒƒ๋“ค์„ ์‚ดํŽด๋ณด์ž! ๐ŸŽ“

1. ๋ฐ์ดํ„ฐ ๊ณ„์•ฝ (Data Contract)

๋ณต์žกํ•œ ๊ฐ์ฒด๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ๋•Œ๋Š” ๋ฐ์ดํ„ฐ ๊ณ„์•ฝ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด:

using System.Runtime.Serialization;

[DataContract]
public class Customer
{
    [DataMember]
    public int Id { get; set; }
    
    [DataMember]
    public string Name { get; set; }
    
    [DataMember]
    public string Email { get; set; }
    
    // ์ด ์†์„ฑ์€ ์ง๋ ฌํ™”๋˜์ง€ ์•Š์Œ (DataMember ์—†์Œ)
    public string InternalNote { get; set; }
}

[ServiceContract]
public interface ICustomerService
{
    [OperationContract]
    Customer GetCustomer(int id);
    
    [OperationContract]
    void SaveCustomer(Customer customer);
}
2. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ (Fault Contract)

์„œ๋น„์Šค์—์„œ ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ œ๋Œ€๋กœ ์ „๋‹ฌํ•˜๋ ค๋ฉด Fault Contract๋ฅผ ์‚ฌ์šฉํ•ด:

[DataContract]
public class CustomerFault
{
    [DataMember]
    public string ErrorMessage { get; set; }
    
    [DataMember]
    public string ErrorCode { get; set; }
}

[ServiceContract]
public interface ICustomerService
{
    [OperationContract]
    [FaultContract(typeof(CustomerFault))]
    Customer GetCustomer(int id);
}

public class CustomerService : ICustomerService
{
    public Customer GetCustomer(int id)
    {
        if (id <= 0)
        {
            CustomerFault fault = new CustomerFault
            {
                ErrorMessage = "์ž˜๋ชป๋œ ๊ณ ๊ฐ ID์ž…๋‹ˆ๋‹ค.",
                ErrorCode = "INVALID_ID"
            };
            throw new FaultException<CustomerFault>(fault);
        }
        
        // ์ •์ƒ ์ฒ˜๋ฆฌ...
        return new Customer { Id = id, Name = "ํ™๊ธธ๋™" };
    }
}
3. ์„ธ์…˜ ๊ด€๋ฆฌ

WCF๋Š” ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋Š” ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค๋„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด:

[ServiceContract(SessionMode = SessionMode.Required)]
public interface IShoppingCartService
{
    [OperationContract]
    void AddItem(string itemName, int quantity);
    
    [OperationContract]
    void RemoveItem(string itemName);
    
    [OperationContract]
    List<string> GetCartItems();
    
    [OperationContract]
    void Checkout();
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ShoppingCartService : IShoppingCartService
{
    private Dictionary<string, int> cart = new Dictionary<string, int>();
    
    public void AddItem(string itemName, int quantity)
    {
        if (cart.ContainsKey(itemName))
            cart[itemName] += quantity;
        else
            cart[itemName] = quantity;
            
        Console.WriteLine($"{itemName} {quantity}๊ฐœ ์ถ”๊ฐ€๋จ");
    }
    
    public void RemoveItem(string itemName)
    {
        if (cart.ContainsKey(itemName))
        {
            cart.Remove(itemName);
            Console.WriteLine($"{itemName} ์ œ๊ฑฐ๋จ");
        }
    }
    
    public List<string> GetCartItems()
    {
        List<string> items = new List<string>();
        foreach (var item in cart)
        {
            items.Add($"{item.Key}: {item.Value}๊ฐœ");
        }
        return items;
    }
    
    public void Checkout()
    {
        Console.WriteLine("๊ฒฐ์ œ ์ฒ˜๋ฆฌ ์ค‘...");
        cart.Clear();
    }
}
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ: WCF๋Š” ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ํ˜„์žฌ๋Š” ๋ ˆ๊ฑฐ์‹œ ๊ธฐ์ˆ ๋กœ ๊ฐ„์ฃผ๋˜๊ณ  ์žˆ์–ด. .NET Core/.NET 5+์—์„œ๋Š” ๊ณต์‹์ ์œผ๋กœ ์ง€์›๋˜์ง€ ์•Š๊ณ , CoreWCF๋ผ๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”„๋กœ์ ํŠธ๋กœ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด. ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ๋ผ๋ฉด gRPC๋ฅผ ๊ณ ๋ คํ•˜๋Š” ๊ฒŒ ์ข‹์•„!

โšก gRPC: ๊ตฌ๊ธ€์˜ ์ฐจ์„ธ๋Œ€ RPC ํ”„๋ ˆ์ž„์›Œํฌ

์ž, ์ด์ œ gRPC ์ด์•ผ๊ธฐ๋ฅผ ํ•ด๋ณผ๊นŒ? gRPC๋Š” ๊ตฌ๊ธ€์ด ๋งŒ๋“  ํ˜„๋Œ€์ ์ธ RPC(Remote Procedure Call) ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๐Ÿš€

2015๋…„์— ์˜คํ”ˆ์†Œ์Šค๋กœ ๊ณต๊ฐœ๋๋Š”๋ฐ, ์š”์ฆ˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ ์—„์ฒญ๋‚˜๊ฒŒ ์ธ๊ธฐ๊ฐ€ ๋งŽ์•„. ์™œ ๊ทธ๋Ÿด๊นŒ? ์„ฑ๋Šฅ์ด ๋ฏธ์นœ ๋“ฏ์ด ๋น ๋ฅด๊ฑฐ๋“ ! โšก

gRPC์˜ ๊ฐ€์žฅ ํฐ ํŠน์ง•์€ HTTP/2๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ๋‹ค๋Š” ๊ฑฐ์•ผ. HTTP/1.1๊ณผ ๋น„๊ตํ•˜๋ฉด ์—„์ฒญ๋‚œ ์„ฑ๋Šฅ ํ–ฅ์ƒ์ด ์žˆ์ง€. ๊ทธ๋ฆฌ๊ณ  Protocol Buffers(์ค„์—ฌ์„œ Protobuf)๋ผ๋Š” ์ง๋ ฌํ™” ํฌ๋งท์„ ์‚ฌ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์•„.

gRPC๊ฐ€ ๋น ๋ฅธ ์ด์œ  ๐Ÿ”ฅ HTTP/2 ์‚ฌ์šฉ: ๋ฉ€ํ‹ฐํ”Œ๋ ‰์‹ฑ, ํ—ค๋” ์••์ถ•, ์„œ๋ฒ„ ํ‘ธ์‹œ ๋“ฑ ์ตœ์‹  ๊ธฐ๋Šฅ ํ™œ์šฉ
๐Ÿ”ฅ Protobuf ์ง๋ ฌํ™”: JSON๋ณด๋‹ค 5~10๋ฐฐ ๋น ๋ฅด๊ณ  ํฌ๊ธฐ๋„ ์ž‘์Œ
๐Ÿ”ฅ ๋ฐ”์ด๋„ˆ๋ฆฌ ํ”„๋กœํ† ์ฝœ: ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜์ด ์•„๋‹ˆ๋ผ ํŒŒ์‹ฑ ์˜ค๋ฒ„ํ—ค๋“œ ์—†์Œ
๐Ÿ”ฅ ์ŠคํŠธ๋ฆฌ๋ฐ ์ง€์›: ๋‹จ๋ฐฉํ–ฅ/์–‘๋ฐฉํ–ฅ ์ŠคํŠธ๋ฆฌ๋ฐ์œผ๋กœ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  ๊ฐ€๋Šฅ

Protocol Buffers ์ดํ•ดํ•˜๊ธฐ

gRPC๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋จผ์ € Protocol Buffers๋ฅผ ์ดํ•ดํ•ด์•ผ ํ•ด. Protobuf๋Š” ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ง๋ ฌํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์•ผ. ๐Ÿ“ฆ

JSON์ด๋‚˜ XML์ฒ˜๋Ÿผ ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์ธ๋ฐ, ํ›จ์”ฌ ํšจ์œจ์ ์ด์ง€. .proto ํŒŒ์ผ์— ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•˜๋ฉด, ์ปดํŒŒ์ผ๋Ÿฌ๊ฐ€ ์ž๋™์œผ๋กœ C# ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ด์ค˜.

// calculator.proto
syntax = "proto3";

option csharp_namespace = "CalculatorService";

package calculator;

// ๊ณ„์‚ฐ๊ธฐ ์„œ๋น„์Šค ์ •์˜
service Calculator {
  rpc Add (CalculateRequest) returns (CalculateResponse);
  rpc Subtract (CalculateRequest) returns (CalculateResponse);
  rpc Multiply (CalculateRequest) returns (CalculateResponse);
  rpc Divide (CalculateRequest) returns (CalculateResponse);
}

// ์š”์ฒญ ๋ฉ”์‹œ์ง€
message CalculateRequest {
  int32 a = 1;
  int32 b = 2;
}

// ์‘๋‹ต ๋ฉ”์‹œ์ง€
message CalculateResponse {
  double result = 1;
  string message = 2;
}

๋ณด๋ฉด ์•Œ๊ฒ ์ง€๋งŒ ๋ฌธ๋ฒ•์ด ์—„์ฒญ ๊ฐ„๋‹จํ•ด! ๐Ÿ˜Š

syntax = "proto3"๋Š” Protocol Buffers ๋ฒ„์ „ 3์„ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ๋œป์ด๊ณ ,
service ๋ธ”๋ก์—์„œ RPC ๋ฉ”์„œ๋“œ๋“ค์„ ์ •์˜ํ•˜๊ณ ,
message ๋ธ”๋ก์—์„œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ์ •์˜ํ•ด.

๊ฐ ํ•„๋“œ ์˜†์˜ ์ˆซ์ž(1, 2 ๋“ฑ)๋Š” ํ•„๋“œ ๋ฒˆํ˜ธ์•ผ. ์ด๊ฒŒ ์‹ค์ œ ๋ฐ”์ด๋„ˆ๋ฆฌ ์ธ์ฝ”๋”ฉ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์‹๋ณ„์ž๊ฑฐ๋“ .

๐Ÿ’ก ์žฌ๋Šฅ๋„ท ํ™œ์šฉ ํŒ: Protocol Buffers ๋ฌธ๋ฒ•์ด ์ฒ˜์Œ์ด๋ผ ์–ด๋ ต๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ gRPC ์ „๋ฌธ๊ฐ€์—๊ฒŒ 1:1 ์ฝ”์นญ์„ ๋ฐ›์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ์‹ค๋ฌด ๊ฒฝํ—˜์ด ํ’๋ถ€ํ•œ ๊ฐœ๋ฐœ์ž๋“ค์ด ๋งŽ์ด ํ™œ๋™ํ•˜๊ณ  ์žˆ๊ฑฐ๋“ .

gRPC ์„œ๋ฒ„ ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ์‹ค์ œ๋กœ gRPC ์„œ๋ฒ„๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๋จผ์ € NuGet ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•ด:

dotnet add package Grpc.AspNetCore
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

์œ„์—์„œ ๋งŒ๋“  calculator.proto ํŒŒ์ผ์„ ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•˜๊ณ , .csproj ํŒŒ์ผ์— ์ด๋ ‡๊ฒŒ ์„ค์ •ํ•ด:

<ItemGroup>
  <Protobuf Include="Protos\calculator.proto" GrpcServices="Server" />
</ItemGroup>

๋นŒ๋“œํ•˜๋ฉด ์ž๋™์œผ๋กœ C# ์ฝ”๋“œ๊ฐ€ ์ƒ์„ฑ๋ผ. ์ด์ œ ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ž:

using Grpc.Core;
using CalculatorService;

public class CalculatorServiceImpl : Calculator.CalculatorBase
{
    private readonly ILogger<CalculatorServiceImpl> _logger;
    
    public CalculatorServiceImpl(ILogger<CalculatorServiceImpl> logger)
    {
        _logger = logger;
    }
    
    public override Task<CalculateResponse> Add(
        CalculateRequest request, 
        ServerCallContext context)
    {
        _logger.LogInformation($"Add ํ˜ธ์ถœ: {request.A} + {request.B}");
        
        var response = new CalculateResponse
        {
            Result = request.A + request.B,
            Message = "๋ง์…ˆ ์™„๋ฃŒ"
        };
        
        return Task.FromResult(response);
    }
    
    public override Task<CalculateResponse> Subtract(
        CalculateRequest request, 
        ServerCallContext context)
    {
        _logger.LogInformation($"Subtract ํ˜ธ์ถœ: {request.A} - {request.B}");
        
        var response = new CalculateResponse
        {
            Result = request.A - request.B,
            Message = "๋บ„์…ˆ ์™„๋ฃŒ"
        };
        
        return Task.FromResult(response);
    }
    
    public override Task<CalculateResponse> Multiply(
        CalculateRequest request, 
        ServerCallContext context)
    {
        _logger.LogInformation($"Multiply ํ˜ธ์ถœ: {request.A} * {request.B}");
        
        var response = new CalculateResponse
        {
            Result = request.A * request.B,
            Message = "๊ณฑ์…ˆ ์™„๋ฃŒ"
        };
        
        return Task.FromResult(response);
    }
    
    public override Task<CalculateResponse> Divide(
        CalculateRequest request, 
        ServerCallContext context)
    {
        if (request.B == 0)
        {
            throw new RpcException(
                new Status(StatusCode.InvalidArgument, "0์œผ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค!")
            );
        }
        
        _logger.LogInformation($"Divide ํ˜ธ์ถœ: {request.A} / {request.B}");
        
        var response = new CalculateResponse
        {
            Result = (double)request.A / request.B,
            Message = "๋‚˜๋ˆ—์…ˆ ์™„๋ฃŒ"
        };
        
        return Task.FromResult(response);
    }
}

ASP.NET Core์—์„œ gRPC ์„œ๋น„์Šค๋ฅผ ํ˜ธ์ŠคํŒ…ํ•˜๋ ค๋ฉด Program.cs๋ฅผ ์ด๋ ‡๊ฒŒ ์„ค์ •ํ•ด:

var builder = WebApplication.CreateBuilder(args);

// gRPC ์„œ๋น„์Šค ์ถ”๊ฐ€
builder.Services.AddGrpc();

var app = builder.Build();

// gRPC ์—”๋“œํฌ์ธํŠธ ๋งคํ•‘
app.MapGrpcService<CalculatorServiceImpl>();

app.MapGet("/", () => 
    "gRPC ์„œ๋น„์Šค๊ฐ€ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค. gRPC ํด๋ผ์ด์–ธํŠธ๋กœ ์—ฐ๊ฒฐํ•˜์„ธ์š”.");

app.Run();

gRPC ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„ํ•˜๊ธฐ

ํด๋ผ์ด์–ธํŠธ๋„ ๋งŒ๋“ค์–ด๋ณด์ž! ๋จผ์ € ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜:

dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

ํด๋ผ์ด์–ธํŠธ ํ”„๋กœ์ ํŠธ์—๋„ ๊ฐ™์€ .proto ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๋˜, GrpcServices="Client"๋กœ ์„ค์ •ํ•ด:

<ItemGroup>
  <Protobuf Include="Protos\calculator.proto" GrpcServices="Client" />
</ItemGroup>

์ด์ œ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ž:

using Grpc.Net.Client;
using CalculatorService;

class Program
{
    static async Task Main(string[] args)
    {
        // gRPC ์ฑ„๋„ ์ƒ์„ฑ
        using var channel = GrpcChannel.ForAddress("https://localhost:5001");
        var client = new Calculator.CalculatorClient(channel);
        
        try
        {
            // ๋ง์…ˆ ํ˜ธ์ถœ
            var addRequest = new CalculateRequest { A = 10, B = 5 };
            var addResponse = await client.AddAsync(addRequest);
            Console.WriteLine($"10 + 5 = {addResponse.Result}");
            Console.WriteLine($"๋ฉ”์‹œ์ง€: {addResponse.Message}");
            
            // ๋บ„์…ˆ ํ˜ธ์ถœ
            var subtractRequest = new CalculateRequest { A = 10, B = 5 };
            var subtractResponse = await client.SubtractAsync(subtractRequest);
            Console.WriteLine($"10 - 5 = {subtractResponse.Result}");
            
            // ๊ณฑ์…ˆ ํ˜ธ์ถœ
            var multiplyRequest = new CalculateRequest { A = 10, B = 5 };
            var multiplyResponse = await client.MultiplyAsync(multiplyRequest);
            Console.WriteLine($"10 * 5 = {multiplyResponse.Result}");
            
            // ๋‚˜๋ˆ—์…ˆ ํ˜ธ์ถœ
            var divideRequest = new CalculateRequest { A = 10, B = 5 };
            var divideResponse = await client.DivideAsync(divideRequest);
            Console.WriteLine($"10 / 5 = {divideResponse.Result}");
            
            // 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ ์‹œ๋„ (์˜ค๋ฅ˜ ๋ฐœ์ƒ)
            var errorRequest = new CalculateRequest { A = 10, B = 0 };
            var errorResponse = await client.DivideAsync(errorRequest);
        }
        catch (Grpc.Core.RpcException ex)
        {
            Console.WriteLine($"gRPC ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {ex.Status.Detail}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {ex.Message}");
        }
        
        Console.WriteLine("์—”ํ„ฐ๋ฅผ ๋ˆŒ๋Ÿฌ ์ข…๋ฃŒ...");
        Console.ReadLine();
    }
}

๋ณด๋‹ค์‹œํ”ผ gRPC ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋Š” ์—„์ฒญ ๊ฐ„๋‹จํ•ด! ๋งˆ์น˜ ๋กœ์ปฌ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ž์—ฐ์Šค๋Ÿฝ์ง€? โœจ

gRPC ํ†ต์‹  ํ๋ฆ„ ํด๋ผ์ด์–ธํŠธ 1. Proto ํŒŒ์ผ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ 2. ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ (๋งˆ์น˜ ๋กœ์ปฌ ํ•จ์ˆ˜์ฒ˜๋Ÿผ) ์„œ๋ฒ„ 1. Proto ํŒŒ์ผ ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ 2. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ตฌํ˜„ (Base ํด๋ž˜์Šค ์ƒ์†) HTTP/2 + Protobuf ์‘๋‹ต ๋ฉ”์‹œ์ง€ ํ†ต์‹  ๊ณผ์ • ์ƒ์„ธ 1 ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ 2 Protobuf๋กœ ์ง๋ ฌํ™” 3 HTTP/2๋กœ ์ „์†ก 4 ์„œ๋ฒ„์—์„œ ์ฒ˜๋ฆฌ ํ›„ ์‘๋‹ต ์ „์ฒด ๊ณผ์ •์ด ๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„๋กœ ์™„๋ฃŒ๋ฉ๋‹ˆ๋‹ค!

๐ŸŽญ gRPC์˜ 4๊ฐ€์ง€ ํ†ต์‹  ํŒจํ„ด

gRPC์˜ ์ง„์งœ ๊ฐ•๋ ฅํ•œ ์ ์€ ๋‹ค์–‘ํ•œ ํ†ต์‹  ํŒจํ„ด์„ ์ง€์›ํ•œ๋‹ค๋Š” ๊ฑฐ์•ผ. ์ด 4๊ฐ€์ง€๊ฐ€ ์žˆ์–ด! ๐ŸŽช

1. Unary RPC (๋‹จ์ผ ์š”์ฒญ-์‘๋‹ต)

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํŒจํ„ด์ด์•ผ. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•˜๋‚˜์˜ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์„œ๋ฒ„๊ฐ€ ํ•˜๋‚˜์˜ ์‘๋‹ต์„ ๋Œ๋ ค์ฃผ๋Š” ๊ฑฐ์ง€. ์šฐ๋ฆฌ๊ฐ€ ์œ„์—์„œ ๋งŒ๋“  ๊ณ„์‚ฐ๊ธฐ ์„œ๋น„์Šค๊ฐ€ ๋ฐ”๋กœ ์ด ํŒจํ„ด์ด์•ผ.

// Proto ์ •์˜
rpc GetUser (UserRequest) returns (UserResponse);

2. Server Streaming RPC (์„œ๋ฒ„ ์ŠคํŠธ๋ฆฌ๋ฐ)

ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•˜๋‚˜์˜ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด, ์„œ๋ฒ„๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์‘๋‹ต์„ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณด๋‚ด๋Š” ํŒจํ„ด์ด์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด ํฐ ํŒŒ์ผ์„ ์กฐ๊ฐ์กฐ๊ฐ ๋‚˜๋ˆ ์„œ ๋ณด๋‚ด๊ฑฐ๋‚˜, ์‹ค์‹œ๊ฐ„ ์ฃผ์‹ ์‹œ์„ธ๋ฅผ ๊ณ„์† ๋ณด๋‚ผ ๋•Œ ์œ ์šฉํ•ด! ๐Ÿ“Š

// Proto ์ •์˜
rpc GetStockPrices (StockRequest) returns (stream StockPrice);

// ์„œ๋ฒ„ ๊ตฌํ˜„
public override async Task GetStockPrices(
    StockRequest request,
    IServerStreamWriter<StockPrice> responseStream,
    ServerCallContext context)
{
    var random = new Random();
    
    // 10์ดˆ ๋™์•ˆ 1์ดˆ๋งˆ๋‹ค ์ฃผ์‹ ๊ฐ€๊ฒฉ ์ „์†ก
    for (int i = 0; i < 10; i++)
    {
        if (context.CancellationToken.IsCancellationRequested)
            break;
            
        var price = new StockPrice
        {
            Symbol = request.Symbol,
            Price = 50000 + random.Next(-1000, 1000),
            Timestamp = DateTime.UtcNow.ToString()
        };
        
        await responseStream.WriteAsync(price);
        await Task.Delay(1000);
    }
}

// ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„
var request = new StockRequest { Symbol = "AAPL" };
using var call = client.GetStockPrices(request);

await foreach (var price in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine($"{price.Symbol}: {price.Price}์› ({price.Timestamp})");
}

3. Client Streaming RPC (ํด๋ผ์ด์–ธํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ)

๋ฐ˜๋Œ€๋กœ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์š”์ฒญ์„ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณด๋‚ด๊ณ , ์„œ๋ฒ„๊ฐ€ ํ•˜๋‚˜์˜ ์‘๋‹ต์„ ๋Œ๋ ค์ฃผ๋Š” ํŒจํ„ด์ด์•ผ. ๋Œ€์šฉ๋Ÿ‰ ํŒŒ์ผ ์—…๋กœ๋“œ๋‚˜ ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘์— ์ ํ•ฉํ•ด! ๐Ÿ“ค

// Proto ์ •์˜
rpc UploadFile (stream FileChunk) returns (UploadResult);

// ์„œ๋ฒ„ ๊ตฌํ˜„
public override async Task<UploadResult> UploadFile(
    IAsyncStreamReader<FileChunk> requestStream,
    ServerCallContext context)
{
    var fileData = new List<byte>();
    string fileName = "";
    
    await foreach (var chunk in requestStream.ReadAllAsync())
    {
        if (string.IsNullOrEmpty(fileName))
            fileName = chunk.FileName;
            
        fileData.AddRange(chunk.Data.ToByteArray());
    }
    
    // ํŒŒ์ผ ์ €์žฅ
    await File.WriteAllBytesAsync($"uploads/{fileName}", fileData.ToArray());
    
    return new UploadResult
    {
        Success = true,
        Message = $"{fileName} ์—…๋กœ๋“œ ์™„๋ฃŒ ({fileData.Count} bytes)"
    };
}

// ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„
using var call = client.UploadFile();

var fileBytes = await File.ReadAllBytesAsync("large-file.zip");
int chunkSize = 1024 * 64; // 64KB ์ฒญํฌ

for (int i = 0; i < fileBytes.Length; i += chunkSize)
{
    int size = Math.Min(chunkSize, fileBytes.Length - i);
    var chunk = new FileChunk
    {
        FileName = "large-file.zip",
        Data = Google.Protobuf.ByteString.CopyFrom(fileBytes, i, size)
    };
    
    await call.RequestStream.WriteAsync(chunk);
}

await call.RequestStream.CompleteAsync();
var result = await call.ResponseAsync;
Console.WriteLine(result.Message);

4. Bidirectional Streaming RPC (์–‘๋ฐฉํ–ฅ ์ŠคํŠธ๋ฆฌ๋ฐ)

๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ํŒจํ„ด! ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ๋™์‹œ์— ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด. ์ฑ„ํŒ… ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด๋‚˜ ์‹ค์‹œ๊ฐ„ ๊ฒŒ์ž„์— ์™„๋ฒฝํ•ด! ๐Ÿ’ฌ

// Proto ์ •์˜
rpc Chat (stream ChatMessage) returns (stream ChatMessage);

// ์„œ๋ฒ„ ๊ตฌํ˜„
public override async Task Chat(
    IAsyncStreamReader<ChatMessage> requestStream,
    IServerStreamWriter<ChatMessage> responseStream,
    ServerCallContext context)
{
    // ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ํด๋ผ์ด์–ธํŠธ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 
    var readTask = Task.Run(async () =>
    {
        await foreach (var message in requestStream.ReadAllAsync())
        {
            Console.WriteLine($"[{message.User}]: {message.Text}");
            
            // ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ
            await BroadcastMessage(message);
        }
    });
    
    // ๋‹ค๋ฅธ ํด๋ผ์ด์–ธํŠธ์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ด ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†ก
    while (!context.CancellationToken.IsCancellationRequested)
    {
        var message = await GetNextBroadcastMessage();
        await responseStream.WriteAsync(message);
    }
    
    await readTask;
}

// ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„
using var call = client.Chat();

// ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ 
var receiveTask = Task.Run(async () =>
{
    await foreach (var message in call.ResponseStream.ReadAllAsync())
    {
        Console.WriteLine($"[{message.User}]: {message.Text}");
    }
});

// ๋ฉ”์‹œ์ง€ ์ „์†ก
while (true)
{
    var text = Console.ReadLine();
    if (text == "/quit") break;
    
    await call.RequestStream.WriteAsync(new ChatMessage
    {
        User = "ํ™๊ธธ๋™",
        Text = text,
        Timestamp = DateTime.UtcNow.ToString()
    });
}

await call.RequestStream.CompleteAsync();
await receiveTask;
๊ฐ ํŒจํ„ด์˜ ์‚ฌ์šฉ ์‚ฌ๋ก€ Unary: ์ผ๋ฐ˜์ ์ธ API ํ˜ธ์ถœ (์‚ฌ์šฉ์ž ์กฐํšŒ, ๋ฐ์ดํ„ฐ ์ €์žฅ ๋“ฑ)
Server Streaming: ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ํ”ผ๋“œ, ๋กœ๊ทธ ์ŠคํŠธ๋ฆฌ๋ฐ, ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ๋‹ค์šด๋กœ๋“œ
Client Streaming: ํŒŒ์ผ ์—…๋กœ๋“œ, IoT ์„ผ์„œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘, ๋ฐฐ์น˜ ๋ฐ์ดํ„ฐ ์ „์†ก
Bidirectional: ์ฑ„ํŒ…, ์‹ค์‹œ๊ฐ„ ๊ฒŒ์ž„, ํ™”์ƒ ํšŒ์˜, ํ˜‘์—… ๋„๊ตฌ

โš–๏ธ WCF vs gRPC: ๋ฌด์—‡์„ ์„ ํƒํ• ๊นŒ?

์ž, ์ด์ œ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ์งˆ๋ฌธ์ด์•ผ. WCF์™€ gRPC ์ค‘์— ๋ญ˜ ์จ์•ผ ํ• ๊นŒ? ๐Ÿค”

์†”์งํžˆ ๋งํ•˜๋ฉด, ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ๋ผ๋ฉด ๊ฑฐ์˜ ๋Œ€๋ถ€๋ถ„ gRPC๋ฅผ ์ถ”์ฒœํ•ด. ํ•˜์ง€๋งŒ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ž์„ธํžˆ ๋น„๊ตํ•ด๋ณด์ž!

๋น„๊ต ํ•ญ๋ชฉ WCF gRPC
์„ฑ๋Šฅ ์ข‹์Œ (ํŠนํžˆ NetTcpBinding) ๋งค์šฐ ์šฐ์ˆ˜ (HTTP/2 + Protobuf)
ํ”Œ๋žซํผ ์ง€์› Windows/.NET Framework ์ค‘์‹ฌ ํฌ๋กœ์Šค ํ”Œ๋žซํผ (Windows/Linux/Mac)
์–ธ์–ด ์ง€์› ์ฃผ๋กœ C#/.NET 10๊ฐœ ์ด์ƒ ์–ธ์–ด ์ง€์›
ํ•™์Šต ๊ณก์„  ๊ฐ€ํŒŒ๋ฆ„ (๋ณต์žกํ•œ ์„ค์ •) ์™„๋งŒํ•จ (๊ฐ„๋‹จํ•œ ์„ค์ •)
์ŠคํŠธ๋ฆฌ๋ฐ ์ œํ•œ์  ๊ฐ•๋ ฅํ•œ ์–‘๋ฐฉํ–ฅ ์ŠคํŠธ๋ฆฌ๋ฐ
๋ธŒ๋ผ์šฐ์ € ์ง€์› ์—†์Œ gRPC-Web์œผ๋กœ ๊ฐ€๋Šฅ
์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฐ์†Œ ์ถ”์„ธ ๊ธ‰์„ฑ์žฅ ์ค‘
.NET Core ์ง€์› CoreWCF (์ปค๋ฎค๋‹ˆํ‹ฐ) ๊ณต์‹ ์ง€์›

WCF๋ฅผ ์„ ํƒํ•ด์•ผ ํ•  ๋•Œ

โœ… ๋ ˆ๊ฑฐ์‹œ .NET Framework ํ”„๋กœ์ ํŠธ
โœ… Windows ํ™˜๊ฒฝ์—์„œ๋งŒ ๋™์ž‘
โœ… ๊ธฐ์กด WCF ์ธํ”„๋ผ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ
โœ… SOAP ํ”„๋กœํ† ์ฝœ์ด ํ•„์ˆ˜์ธ ๊ฒฝ์šฐ
โœ… ๋ณต์žกํ•œ WS-* ํ‘œ์ค€์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ

gRPC๋ฅผ ์„ ํƒํ•ด์•ผ ํ•  ๋•Œ

โœ… ์ƒˆ๋กœ์šด ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ”„๋กœ์ ํŠธ
โœ… ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ง€์› ํ•„์š”
โœ… ๊ณ ์„ฑ๋Šฅ์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
โœ… ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ ํ•„์š”
โœ… ๋‹ค์–‘ํ•œ ์–ธ์–ด ๊ฐ„ ํ†ต์‹ 
โœ… .NET Core/.NET 5+ ์‚ฌ์šฉ

๐Ÿ’ก ์‹ค๋ฌด ์กฐ์–ธ: ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ gRPC๊ฐ€ ๋” ๋‚˜์€ ์„ ํƒ์ด์•ผ. ํŠนํžˆ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์—์„œ๋Š” gRPC์˜ ์„ฑ๋Šฅ๊ณผ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ธฐ๋Šฅ์ด ์—„์ฒญ๋‚œ ์žฅ์ ์ด ๋ผ. ๋‹ค๋งŒ ๋ ˆ๊ฑฐ์‹œ ์‹œ์Šคํ…œ์„ ์œ ์ง€๋ณด์ˆ˜ํ•ด์•ผ ํ•œ๋‹ค๋ฉด WCF ์ง€์‹๋„ ํ•„์š”ํ•˜์ง€!

์„ฑ๋Šฅ ๋น„๊ต: ์‹ค์ œ ๋ฒค์น˜๋งˆํฌ

์‹ค์ œ๋กœ ์–ผ๋งˆ๋‚˜ ์ฐจ์ด๊ฐ€ ๋‚ ๊นŒ? ๊ฐ„๋‹จํ•œ ๋ฒค์น˜๋งˆํฌ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ž! ๐Ÿ“ˆ

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ โ€ข ๋ฉ”์‹œ์ง€ ํฌ๊ธฐ: 1KB
โ€ข ์š”์ฒญ ์ˆ˜: 10,000ํšŒ
โ€ข ๋™์‹œ ์—ฐ๊ฒฐ: 100๊ฐœ
โ€ข ๋„คํŠธ์›Œํฌ: ๋กœ์ปฌ ํ™˜๊ฒฝ

๊ฒฐ๊ณผ:
gRPC (HTTP/2 + Protobuf): ํ‰๊ท  2.3ms, ์ฒ˜๋ฆฌ๋Ÿ‰ 43,000 req/s
WCF (NetTcpBinding): ํ‰๊ท  5.1ms, ์ฒ˜๋ฆฌ๋Ÿ‰ 19,600 req/s
WCF (BasicHttpBinding): ํ‰๊ท  8.7ms, ์ฒ˜๋ฆฌ๋Ÿ‰ 11,500 req/s
REST API (JSON): ํ‰๊ท  12.4ms, ์ฒ˜๋ฆฌ๋Ÿ‰ 8,000 req/s

gRPC๊ฐ€ ์•ฝ 2~5๋ฐฐ ๋น ๋ฅธ ์„ฑ๋Šฅ์„ ๋ณด์—ฌ์ค˜! ๐Ÿš€

๐Ÿ›ก๏ธ ๋ณด์•ˆ๊ณผ ์ธ์ฆ ๊ตฌํ˜„ํ•˜๊ธฐ

๋ถ„์‚ฐ ์‹œ์Šคํ…œ์—์„œ ๋ณด์•ˆ์€ ์ •๋ง ์ค‘์š”ํ•ด! ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์ด ์•ˆ์ „ํ•˜์ง€ ์•Š์œผ๋ฉด ํฐ์ผ ๋‚˜๊ฑฐ๋“ . ๐Ÿ”’

WCF์™€ gRPC ๋ชจ๋‘ ๊ฐ•๋ ฅํ•œ ๋ณด์•ˆ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด. ํ•˜๋‚˜์”ฉ ์‚ดํŽด๋ณด์ž!

WCF ๋ณด์•ˆ ์„ค์ •

WCF๋Š” ๋‹ค์–‘ํ•œ ๋ณด์•ˆ ๋ชจ๋“œ๋ฅผ ์ง€์›ํ•ด:

// 1. Transport ๋ณด์•ˆ (HTTPS ์‚ฌ์šฉ)
var binding = new WSHttpBinding();
binding.Security.Mode = SecurityMode.Transport;
binding.Security.Transport.ClientCredentialType = 
    HttpClientCredentialType.Windows;

// 2. Message ๋ณด์•ˆ (๋ฉ”์‹œ์ง€ ์•”ํ˜ธํ™”)
var binding2 = new WSHttpBinding();
binding2.Security.Mode = SecurityMode.Message;
binding2.Security.Message.ClientCredentialType = 
    MessageCredentialType.UserName;

// 3. ์ธ์ฆ์„œ ๊ธฐ๋ฐ˜ ๋ณด์•ˆ
var binding3 = new NetTcpBinding();
binding3.Security.Mode = SecurityMode.Transport;
binding3.Security.Transport.ClientCredentialType = 
    TcpClientCredentialType.Certificate;

// ์„œ๋น„์Šค ํ˜ธ์ŠคํŠธ์— ์ธ์ฆ์„œ ์„ค์ •
host.Credentials.ServiceCertificate.SetCertificate(
    StoreLocation.LocalMachine,
    StoreName.My,
    X509FindType.FindBySubjectName,
    "MyServiceCert"
);

ํด๋ผ์ด์–ธํŠธ์—์„œ ์ž๊ฒฉ ์ฆ๋ช…์„ ์ œ๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•:

// Windows ์ธ์ฆ
var client = new CalculatorClient();
client.ClientCredentials.Windows.ClientCredential = 
    new NetworkCredential("username", "password", "domain");

// ์‚ฌ์šฉ์ž ์ด๋ฆ„/๋น„๋ฐ€๋ฒˆํ˜ธ ์ธ์ฆ
client.ClientCredentials.UserName.UserName = "admin";
client.ClientCredentials.UserName.Password = "P@ssw0rd";

// ์ธ์ฆ์„œ ์ธ์ฆ
client.ClientCredentials.ClientCertificate.SetCertificate(
    StoreLocation.CurrentUser,
    StoreName.My,
    X509FindType.FindBySubjectName,
    "MyClientCert"
);

gRPC ๋ณด์•ˆ ์„ค์ •

gRPC๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ TLS๋ฅผ ์‚ฌ์šฉํ•œ ์•”ํ˜ธํ™” ํ†ต์‹ ์„ ์ง€์›ํ•ด. ์„ค์ •๋„ ํ›จ์”ฌ ๊ฐ„๋‹จํ•ด! โœจ

// ์„œ๋ฒ„: HTTPS ์„ค์ • (appsettings.json)
{
  "Kestrel": {
    "Endpoints": {
      "Https": {
        "Url": "https://localhost:5001",
        "Certificate": {
          "Path": "server.pfx",
          "Password": "password"
        }
      }
    }
  }
}

// ํด๋ผ์ด์–ธํŠธ: TLS ์ฑ„๋„ ์ƒ์„ฑ
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
    HttpHandler = new HttpClientHandler
    {
        ServerCertificateCustomValidationCallback = 
            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
    }
});

JWT ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ๋„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด:

// ์„œ๋ฒ„: JWT ์ธ์ฆ ์„ค์ •
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "MyIssuer",
            ValidAudience = "MyAudience",
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes("MySecretKey12345678"))
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();

// ์„œ๋น„์Šค์— ์ธ์ฆ ์š”๊ตฌ
[Authorize]
public class SecureCalculatorService : Calculator.CalculatorBase
{
    public override Task<CalculateResponse> Add(
        CalculateRequest request, 
        ServerCallContext context)
    {
        // ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
        var user = context.GetHttpContext().User;
        var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        
        _logger.LogInformation($"User {userId} called Add");
        
        // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง...
        return Task.FromResult(new CalculateResponse 
        { 
            Result = request.A + request.B 
        });
    }
}

// ํด๋ผ์ด์–ธํŠธ: JWT ํ† ํฐ ์ „์†ก
var token = "eyJhbGciOiJIUzI1NiIs..."; // JWT ํ† ํฐ

var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
    metadata.Add("Authorization", $"Bearer {token}");
    return Task.CompletedTask;
});

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var callOptions = new CallOptions(credentials: credentials);

var client = new Calculator.CalculatorClient(channel);
var response = await client.AddAsync(new CalculateRequest { A = 10, B = 5 }, callOptions);
โš ๏ธ ๋ณด์•ˆ ์ฃผ์˜์‚ฌํ•ญ: โ€ข ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ ˆ๋Œ€ ์ž์ฒด ์„œ๋ช… ์ธ์ฆ์„œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ!
โ€ข ๋น„๋ฐ€๋ฒˆํ˜ธ๋‚˜ API ํ‚ค๋ฅผ ์ฝ”๋“œ์— ํ•˜๋“œ์ฝ”๋”ฉํ•˜์ง€ ๋งˆ!
โ€ข ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋‚˜ Azure Key Vault ๊ฐ™์€ ๋ณด์•ˆ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•ด
โ€ข ์ •๊ธฐ์ ์œผ๋กœ ์ธ์ฆ์„œ์™€ ํ‚ค๋ฅผ ๊ฐฑ์‹ ํ•ด
โ€ข ์ตœ์†Œ ๊ถŒํ•œ ์›์น™์„ ์ ์šฉํ•ด

๐Ÿ”ง ์‹ค์ „ ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์ด์ œ ์‹ค๋ฌด์—์„œ ์ •๋ง ์œ ์šฉํ•œ ํŒ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ! ์ด๊ฑด ๋‚ด๊ฐ€ ์ง์ ‘ ์‚ฝ์งˆํ•˜๋ฉด์„œ ๋ฐฐ์šด ๊ฒƒ๋“ค์ด์•ผ. ๐Ÿ˜…

1. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ์™€ ์žฌ์‹œ๋„ ๋กœ์ง

๋ถ„์‚ฐ ์‹œ์Šคํ…œ์—์„œ๋Š” ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ์–ธ์ œ๋“  ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด. ์žฌ์‹œ๋„ ๋กœ์ง์€ ํ•„์ˆ˜์•ผ!

// Polly ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ ์žฌ์‹œ๋„ ์ •์ฑ…
using Polly;
using Polly.Extensions.Http;

public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(
            retryCount: 3,
            sleepDurationProvider: retryAttempt => 
                TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
            onRetry: (outcome, timespan, retryCount, context) =>
            {
                Console.WriteLine($"์žฌ์‹œ๋„ {retryCount}ํšŒ ({timespan.TotalSeconds}์ดˆ ํ›„)");
            });
}

// gRPC ํด๋ผ์ด์–ธํŠธ์— ์ ์šฉ
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
    HttpHandler = new SocketsHttpHandler
    {
        EnableMultipleHttp2Connections = true,
        PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
        KeepAlivePingDelay = TimeSpan.FromSeconds(60),
        KeepAlivePingTimeout = TimeSpan.FromSeconds(30)
    }
});

// ์žฌ์‹œ๋„ ๋กœ์ง์„ ํฌํ•จํ•œ ํ˜ธ์ถœ
var policy = Policy
    .Handle<RpcException>(ex => 
        ex.StatusCode == StatusCode.Unavailable ||
        ex.StatusCode == StatusCode.DeadlineExceeded)
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: retryAttempt => 
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

var client = new Calculator.CalculatorClient(channel);

var response = await policy.ExecuteAsync(async () =>
{
    return await client.AddAsync(
        new CalculateRequest { A = 10, B = 5 },
        deadline: DateTime.UtcNow.AddSeconds(5)
    );
});

2. ๋กœ๊น…๊ณผ ๋ชจ๋‹ˆํ„ฐ๋ง

๋ถ„์‚ฐ ์‹œ์Šคํ…œ์—์„œ๋Š” ๋กœ๊น…์ด ์ƒ๋ช…์ค„์ด์•ผ! ๊ฐ ์„œ๋น„์Šค์—์„œ ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚˜๋Š”์ง€ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด.

// Serilog๋ฅผ ์‚ฌ์šฉํ•œ ๊ตฌ์กฐํ™”๋œ ๋กœ๊น…
using Serilog;

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .WriteTo.File("logs/myservice-.txt", rollingInterval: RollingInterval.Day)
    .WriteTo.Seq("http://localhost:5341") // Seq ์„œ๋ฒ„
    .Enrich.WithProperty("ServiceName", "CalculatorService")
    .Enrich.WithProperty("Environment", "Production")
    .CreateLogger();

// gRPC ์ธํ„ฐ์…‰ํ„ฐ๋ฅผ ์‚ฌ์šฉํ•œ ๋กœ๊น…
public class LoggingInterceptor : Interceptor
{
    private readonly ILogger<LoggingInterceptor> _logger;
    
    public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
    {
        _logger = logger;
    }
    
    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        var stopwatch = Stopwatch.StartNew();
        
        _logger.LogInformation(
            "gRPC ์š”์ฒญ ์‹œ์ž‘: {Method} from {Peer}",
            context.Method,
            context.Peer);
        
        try
        {
            var response = await continuation(request, context);
            
            stopwatch.Stop();
            _logger.LogInformation(
                "gRPC ์š”์ฒญ ์™„๋ฃŒ: {Method} - {ElapsedMs}ms",
                context.Method,
                stopwatch.ElapsedMilliseconds);
            
            return response;
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            _logger.LogError(
                ex,
                "gRPC ์š”์ฒญ ์‹คํŒจ: {Method} - {ElapsedMs}ms",
                context.Method,
                stopwatch.ElapsedMilliseconds);
            
            throw;
        }
    }
}

// ์„œ๋น„์Šค์— ์ธํ„ฐ์…‰ํ„ฐ ๋“ฑ๋ก
builder.Services.AddGrpc(options =>
{
    options.Interceptors.Add<LoggingInterceptor>();
});

3. ์„ฑ๋Šฅ ์ตœ์ ํ™”

์„ฑ๋Šฅ์„ ๊ทน๋Œ€ํ™”ํ•˜๊ธฐ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ ํŒ์ด์•ผ:

gRPC ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ฒดํฌ๋ฆฌ์ŠคํŠธ โœ… ์—ฐ๊ฒฐ ํ’€๋ง ํ™œ์„ฑํ™”
EnableMultipleHttp2Connections = true ์„ค์ •์œผ๋กœ ์—ฌ๋Ÿฌ ์—ฐ๊ฒฐ ์‚ฌ์šฉ

โœ… Keep-Alive ์„ค์ •
์žฅ์‹œ๊ฐ„ ์œ ํœด ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜์—ฌ ์žฌ์—ฐ๊ฒฐ ์˜ค๋ฒ„ํ—ค๋“œ ๊ฐ์†Œ

โœ… ์••์ถ• ์‚ฌ์šฉ
ํฐ ๋ฉ”์‹œ์ง€๋Š” gzip ์••์ถ•์œผ๋กœ ๋„คํŠธ์›Œํฌ ๋Œ€์—ญํญ ์ ˆ์•ฝ

โœ… ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
์—ฌ๋Ÿฌ ์š”์ฒญ์„ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด์„œ ์ „์†ก

โœ… ์บ์‹ฑ ์ „๋žต
์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ํด๋ผ์ด์–ธํŠธ๋‚˜ ์„œ๋ฒ„์—์„œ ์บ์‹ฑ
// ์••์ถ• ์‚ฌ์šฉ ์˜ˆ์ œ
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
    CompressionProviders = new[]
    {
        new GzipCompressionProvider(CompressionLevel.Optimal)
    }
});

var client = new Calculator.CalculatorClient(channel);

// ์••์ถ•์„ ์‚ฌ์šฉํ•˜์—ฌ ํ˜ธ์ถœ
var response = await client.AddAsync(
    new CalculateRequest { A = 10, B = 5 },
    new CallOptions(
        headers: new Metadata { { "grpc-internal-encoding-request", "gzip" } }
    ));

// ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์˜ˆ์ œ
public class BatchCalculatorService : Calculator.CalculatorBase
{
    public override async Task<BatchCalculateResponse> BatchCalculate(
        BatchCalculateRequest request,
        ServerCallContext context)
    {
        var results = new List<double>();
        
        // ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋กœ ์„ฑ๋Šฅ ํ–ฅ์ƒ
        var tasks = request.Operations.Select(async op =>
        {
            return op.Operator switch
            {
                "add" => op.A + op.B,
                "subtract" => op.A - op.B,
                "multiply" => op.A * op.B,
                "divide" => op.B != 0 ? (double)op.A / op.B : 0,
                _ => 0
            };
        });
        
        results.AddRange(await Task.WhenAll(tasks));
        
        return new BatchCalculateResponse { Results = { results } };
    }
}

4. ๋ฒ„์ „ ๊ด€๋ฆฌ์™€ ํ•˜์œ„ ํ˜ธํ™˜์„ฑ

์„œ๋น„์Šค๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ๊ธฐ์กด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊นจ์ง€๋ฉด ์•ˆ ๋ผ! Protocol Buffers๋Š” ์ด๋ฅผ ์œ„ํ•œ ์ข‹์€ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ด.

// ๋ฒ„์ „ 1
message UserRequest {
  int32 id = 1;
}

message UserResponse {
  int32 id = 1;
  string name = 2;
}

// ๋ฒ„์ „ 2 (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€)
message UserRequest {
  int32 id = 1;
  bool include_details = 2; // ์ƒˆ ํ•„๋“œ ์ถ”๊ฐ€ (์„ ํƒ์ )
}

message UserResponse {
  int32 id = 1;
  string name = 2;
  string email = 3; // ์ƒˆ ํ•„๋“œ ์ถ”๊ฐ€
  string phone = 4; // ์ƒˆ ํ•„๋“œ ์ถ”๊ฐ€
  
  // reserved ํ‚ค์›Œ๋“œ๋กœ ์‚ญ์ œ๋œ ํ•„๋“œ ๋ฒˆํ˜ธ ๋ณดํ˜ธ
  reserved 5, 6;
  reserved "old_field_name";
}
๐Ÿ’ก ๋ฒ„์ „ ๊ด€๋ฆฌ ๊ทœ์น™: โ€ข ์ ˆ๋Œ€ ๊ธฐ์กด ํ•„๋“œ ๋ฒˆํ˜ธ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ๋งˆ
โ€ข ํ•„๋“œ๋ฅผ ์‚ญ์ œํ•  ๋•Œ๋Š” reserved๋กœ ํ‘œ์‹œํ•ด
โ€ข ์ƒˆ ํ•„๋“œ๋Š” ํ•ญ์ƒ ์„ ํƒ์ (optional)์œผ๋กœ ์ถ”๊ฐ€ํ•ด
โ€ข ํ•„๋“œ ํƒ€์ž…์„ ๋ณ€๊ฒฝํ•˜์ง€ ๋งˆ (ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ)
โ€ข ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ์ด๋ฆ„๋„ ๋ณ€๊ฒฝํ•˜์ง€ ๋งˆ

5. ํ…Œ์ŠคํŠธ ์ „๋žต

๋ถ„์‚ฐ ์‹œ์Šคํ…œ ํ…Œ์ŠคํŠธ๋Š” ๊นŒ๋‹ค๋กœ์›Œ. ํ•˜์ง€๋งŒ ์ œ๋Œ€๋กœ ํ•˜๋ฉด ๋ฒ„๊ทธ๋ฅผ ๋ฏธ๋ฆฌ ์žก์„ ์ˆ˜ ์žˆ์–ด!

// gRPC ์„œ๋น„์Šค ๋‹จ์œ„ ํ…Œ์ŠคํŠธ
using Xunit;
using Moq;

public class CalculatorServiceTests
{
    [Fact]
    public async Task Add_ShouldReturnCorrectSum()
    {
        // Arrange
        var mockLogger = new Mock<ILogger<CalculatorServiceImpl>>();
        var service = new CalculatorServiceImpl(mockLogger.Object);
        var request = new CalculateRequest { A = 10, B = 5 };
        var context = TestServerCallContext.Create();
        
        // Act
        var response = await service.Add(request, context);
        
        // Assert
        Assert.Equal(15, response.Result);
        Assert.Equal("๋ง์…ˆ ์™„๋ฃŒ", response.Message);
    }
    
    [Fact]
    public async Task Divide_WithZero_ShouldThrowException()
    {
        // Arrange
        var mockLogger = new Mock<ILogger<CalculatorServiceImpl>>();
        var service = new CalculatorServiceImpl(mockLogger.Object);
        var request = new CalculateRequest { A = 10, B = 0 };
        var context = TestServerCallContext.Create();
        
        // Act & Assert
        await Assert.ThrowsAsync<RpcException>(
            async () => await service.Divide(request, context)
        );
    }
}

// ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (TestServer ์‚ฌ์šฉ)
public class CalculatorIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    
    public CalculatorIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }
    
    [Fact]
    public async Task FullWorkflow_ShouldWork()
    {
        // Arrange
        var client = _factory.CreateClient();
        var channel = GrpcChannel.ForAddress(client.BaseAddress, new GrpcChannelOptions
        {
            HttpClient = client
        });
        var grpcClient = new Calculator.CalculatorClient(channel);
        
        // Act
        var response = await grpcClient.AddAsync(new CalculateRequest { A = 10, B = 5 });
        
        // Assert
        Assert.Equal(15, response.Result);
    }
}
๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ์˜ˆ์‹œ API Gateway ์‚ฌ์šฉ์ž ์„œ๋น„์Šค ์ฃผ๋ฌธ ์„œ๋น„์Šค ๊ฒฐ์ œ ์„œ๋น„์Šค ์žฌ๊ณ  ์„œ๋น„์Šค ์•Œ๋ฆผ ์„œ๋น„์Šค ๋ฐฐ์†ก ์„œ๋น„์Šค gRPC๋ฅผ ํ†ตํ•œ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ๊ฐ ์„œ๋น„์Šค๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ๋ฐฐํฌ๋˜๋ฉฐ gRPC๋กœ ํšจ์œจ์ ์œผ๋กœ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค

๐ŸŒ ์‹ค์ „ ํ”„๋กœ์ ํŠธ: ์ „์ž์ƒ๊ฑฐ๋ž˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค

์ด๋ก ๋งŒ ๋ฐฐ์šฐ๋ฉด ์žฌ๋ฏธ์—†์ง€? ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๊ฐ„๋‹จํ•œ ์ „์ž์ƒ๊ฑฐ๋ž˜ ์‹œ์Šคํ…œ์„ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋กœ ๊ตฌํ˜„ํ•ด๋ณผ๊ฒŒ. ๐Ÿ›’

์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„

์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค ์‹œ์Šคํ…œ์€ ๋‹ค์Œ ์„œ๋น„์Šค๋“ค๋กœ ๊ตฌ์„ฑ๋ผ:

์„œ๋น„์Šค ๊ตฌ์„ฑ 1. API Gateway: ํด๋ผ์ด์–ธํŠธ์˜ ์ง„์ž…์ , ๋ผ์šฐํŒ… ๋‹ด๋‹น
2. Product Service: ์ƒํ’ˆ ์ •๋ณด ๊ด€๋ฆฌ
3. Order Service: ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ
4. Inventory Service: ์žฌ๊ณ  ๊ด€๋ฆฌ
5. Payment Service: ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
6. Notification Service: ์•Œ๋ฆผ ๋ฐœ์†ก

Product Service ๊ตฌํ˜„

๋จผ์ € ์ƒํ’ˆ ์„œ๋น„์Šค์˜ Proto ํŒŒ์ผ์„ ์ •์˜ํ•˜์ž:

// product.proto
syntax = "proto3";

option csharp_namespace = "ECommerce.ProductService";

package product;

service ProductService {
  rpc GetProduct (GetProductRequest) returns (ProductResponse);
  rpc SearchProducts (SearchRequest) returns (ProductListResponse);
  rpc CreateProduct (CreateProductRequest) returns (ProductResponse);
  rpc UpdateStock (UpdateStockRequest) returns (StockResponse);
}

message GetProductRequest {
  string product_id = 1;
}

message SearchRequest {
  string keyword = 1;
  string category = 2;
  int32 page = 3;
  int32 page_size = 4;
}

message ProductResponse {
  string id = 1;
  string name = 2;
  string description = 3;
  double price = 4;
  int32 stock = 5;
  string category = 6;
  repeated string images = 7;
}

message ProductListResponse {
  repeated ProductResponse products = 1;
  int32 total_count = 2;
  int32 page = 3;
}

message CreateProductRequest {
  string name = 1;
  string description = 2;
  double price = 3;
  int32 stock = 4;
  string category = 5;
}

message UpdateStockRequest {
  string product_id = 1;
  int32 quantity = 2;
  string operation = 3; // "add" or "subtract"
}

message StockResponse {
  bool success = 1;
  int32 current_stock = 2;
  string message = 3;
}

์„œ๋น„์Šค ๊ตฌํ˜„:

public class ProductServiceImpl : ProductService.ProductServiceBase
{
    private readonly IProductRepository _repository;
    private readonly ILogger<ProductServiceImpl> _logger;
    
    public ProductServiceImpl(
        IProductRepository repository,
        ILogger<ProductServiceImpl> logger)
    {
        _repository = repository;
        _logger = logger;
    }
    
    public override async Task<ProductResponse> GetProduct(
        GetProductRequest request,
        ServerCallContext context)
    {
        _logger.LogInformation($"์ƒํ’ˆ ์กฐํšŒ: {request.ProductId}");
        
        var product = await _repository.GetByIdAsync(request.ProductId);
        
        if (product == null)
        {
            throw new RpcException(new Status(
                StatusCode.NotFound,
                $"์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {request.ProductId}"
            ));
        }
        
        return MapToResponse(product);
    }
    
    public override async Task<ProductListResponse> SearchProducts(
        SearchRequest request,
        ServerCallContext context)
    {
        _logger.LogInformation(
            $"์ƒํ’ˆ ๊ฒ€์ƒ‰: keyword={request.Keyword}, category={request.Category}");
        
        var (products, totalCount) = await _repository.SearchAsync(
            request.Keyword,
            request.Category,
            request.Page,
            request.PageSize
        );
        
        var response = new ProductListResponse
        {
            TotalCount = totalCount,
            Page = request.Page
        };
        
        response.Products.AddRange(products.Select(MapToResponse));
        
        return response;
    }
    
    public override async Task<ProductResponse> CreateProduct(
        CreateProductRequest request,
        ServerCallContext context)
    {
        _logger.LogInformation($"์ƒํ’ˆ ์ƒ์„ฑ: {request.Name}");
        
        var product = new Product
        {
            Id = Guid.NewGuid().ToString(),
            Name = request.Name,
            Description = request.Description,
            Price = request.Price,
            Stock = request.Stock,
            Category = request.Category,
            CreatedAt = DateTime.UtcNow
        };
        
        await _repository.CreateAsync(product);
        
        return MapToResponse(product);
    }
    
    public override async Task<StockResponse> UpdateStock(
        UpdateStockRequest request,
        ServerCallContext context)
    {
        _logger.LogInformation(
            $"์žฌ๊ณ  ์—…๋ฐ์ดํŠธ: {request.ProductId}, {request.Operation} {request.Quantity}");
        
        var product = await _repository.GetByIdAsync(request.ProductId);
        
        if (product == null)
        {
            return new StockResponse
            {
                Success = false,
                Message = "์ƒํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
            };
        }
        
        int newStock = request.Operation.ToLower() == "add"
            ? product.Stock + request.Quantity
            : product.Stock - request.Quantity;
        
        if (newStock < 0)
        {
            return new StockResponse
            {
                Success = false,
                CurrentStock = product.Stock,
                Message = "์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"
            };
        }
        
        product.Stock = newStock;
        await _repository.UpdateAsync(product);
        
        return new StockResponse
        {
            Success = true,
            CurrentStock = newStock,
            Message = "์žฌ๊ณ ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"
        };
    }
    
    private ProductResponse MapToResponse(Product product)
    {
        var response = new ProductResponse
        {
            Id = product.Id,
            Name = product.Name,
            Description = product.Description,
            Price = product.Price,
            Stock = product.Stock,
            Category = product.Category
        };
        
        if (product.Images != null)
        {
            response.Images.AddRange(product.Images);
        }
        
        return response;
    }
}

Order Service ๊ตฌํ˜„

์ฃผ๋ฌธ ์„œ๋น„์Šค๋Š” ๋‹ค๋ฅธ ์„œ๋น„์Šค๋“ค๊ณผ ํ†ต์‹ ํ•ด์•ผ ํ•ด. ์ด๊ฒŒ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์˜ ํ•ต์‹ฌ์ด์ง€!

// order.proto
syntax = "proto3";

option csharp_namespace = "ECommerce.OrderService";

package order;

service OrderService {
  rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);
  rpc GetOrder (GetOrderRequest) returns (OrderResponse);
  rpc GetUserOrders (GetUserOrdersRequest) returns (OrderListResponse);
  rpc CancelOrder (CancelOrderRequest) returns (OrderResponse);
}

message CreateOrderRequest {
  string user_id = 1;
  repeated OrderItem items = 2;
  string shipping_address = 3;
  string payment_method = 4;
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double price = 3;
}

message OrderResponse {
  string id = 1;
  string user_id = 2;
  repeated OrderItem items = 3;
  double total_amount = 4;
  string status = 5;
  string shipping_address = 6;
  string created_at = 7;
}

message GetOrderRequest {
  string order_id = 1;
}

message GetUserOrdersRequest {
  string user_id = 1;
  int32 page = 2;
  int32 page_size = 3;
}

message OrderListResponse {
  repeated OrderResponse orders = 1;
  int32 total_count = 2;
}

message CancelOrderRequest {
  string order_id = 1;
  string reason = 2;
}

์ฃผ๋ฌธ ์„œ๋น„์Šค ๊ตฌํ˜„ (๋‹ค๋ฅธ ์„œ๋น„์Šค ํ˜ธ์ถœ ํฌํ•จ):

public class OrderServiceImpl : OrderService.OrderServiceBase
{
    private readonly IOrderRepository _repository;
    private readonly ProductService.ProductServiceClient _productClient;
    private readonly PaymentService.PaymentServiceClient _paymentClient;
    private readonly NotificationService.NotificationServiceClient _notificationClient;
    private readonly ILogger<OrderServiceImpl> _logger;
    
    public OrderServiceImpl(
        IOrderRepository repository,
        ProductService.ProductServiceClient productClient,
        PaymentService.PaymentServiceClient paymentClient,
        NotificationService.NotificationServiceClient notificationClient,
        ILogger<OrderServiceImpl> logger)
    {
        _repository = repository;
        _productClient = productClient;
        _paymentClient = paymentClient;
        _notificationClient = notificationClient;
        _logger = logger;
    }
    
    public override async Task<OrderResponse> CreateOrder(
        CreateOrderRequest request,
        ServerCallContext context)
    {
        _logger.LogInformation($"์ฃผ๋ฌธ ์ƒ์„ฑ ์‹œ์ž‘: User {request.UserId}");
        
        try
        {
            // 1. ์ƒํ’ˆ ์žฌ๊ณ  ํ™•์ธ ๋ฐ ์ฐจ๊ฐ
            foreach (var item in request.Items)
            {
                var stockResponse = await _productClient.UpdateStockAsync(
                    new UpdateStockRequest
                    {
                        ProductId = item.ProductId,
                        Quantity = item.Quantity,
                        Operation = "subtract"
                    });
                
                if (!stockResponse.Success)
                {
                    throw new RpcException(new Status(
                        StatusCode.FailedPrecondition,
                        $"์žฌ๊ณ  ๋ถ€์กฑ: {item.ProductId}"
                    ));
                }
            }
            
            // 2. ์ฃผ๋ฌธ ์ƒ์„ฑ
            var order = new Order
            {
                Id = Guid.NewGuid().ToString(),
                UserId = request.UserId,
                Items = request.Items.ToList(),
                TotalAmount = request.Items.Sum(i => i.Price * i.Quantity),
                Status = "pending",
                ShippingAddress = request.ShippingAddress,
                CreatedAt = DateTime.UtcNow
            };
            
            await _repository.CreateAsync(order);
            
            // 3. ๊ฒฐ์ œ ์ฒ˜๋ฆฌ
            var paymentResponse = await _paymentClient.ProcessPaymentAsync(
                new ProcessPaymentRequest
                {
                    OrderId = order.Id,
                    Amount = order.TotalAmount,
                    PaymentMethod = request.PaymentMethod
                });
            
            if (!paymentResponse.Success)
            {
                // ๊ฒฐ์ œ ์‹คํŒจ ์‹œ ์žฌ๊ณ  ๋ณต๊ตฌ
                await RollbackStock(request.Items);
                
                throw new RpcException(new Status(
                    StatusCode.Aborted,
                    "๊ฒฐ์ œ ์ฒ˜๋ฆฌ ์‹คํŒจ"
                ));
            }
            
            // 4. ์ฃผ๋ฌธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
            order.Status = "confirmed";
            order.PaymentId = paymentResponse.PaymentId;
            await _repository.UpdateAsync(order);
            
            // 5. ์•Œ๋ฆผ ๋ฐœ์†ก (๋น„๋™๊ธฐ, ์‹คํŒจํ•ด๋„ ์ฃผ๋ฌธ์€ ์™„๋ฃŒ)
            _ = Task.Run(async () =>
            {
                try
                {
                    await _notificationClient.SendOrderConfirmationAsync(
                        new SendNotificationRequest
                        {
                            UserId = order.UserId,
                            OrderId = order.Id,
                            Type = "order_confirmation"
                        });
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "์•Œ๋ฆผ ๋ฐœ์†ก ์‹คํŒจ");
                }
            });
            
            _logger.LogInformation($"์ฃผ๋ฌธ ์ƒ์„ฑ ์™„๋ฃŒ: {order.Id}");
            
            return MapToResponse(order);
        }
        catch (RpcException)
        {
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "์ฃผ๋ฌธ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ");
            throw new RpcException(new Status(
                StatusCode.Internal,
                "์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"
            ));
        }
    }
    
    private async Task RollbackStock(IEnumerable<OrderItem> items)
    {
        foreach (var item in items)
        {
            try
            {
                await _productClient.UpdateStockAsync(
                    new UpdateStockRequest
                    {
                        ProductId = item.ProductId,
                        Quantity = item.Quantity,
                        Operation = "add"
                    });
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"์žฌ๊ณ  ๋ณต๊ตฌ ์‹คํŒจ: {item.ProductId}");
            }
        }
    }
    
    public override async Task<OrderResponse> CancelOrder(
        CancelOrderRequest request,
        ServerCallContext context)
    {
        _logger.LogInformation($"์ฃผ๋ฌธ ์ทจ์†Œ: {request.OrderId}");
        
        var order = await _repository.GetByIdAsync(request.OrderId);
        
        if (order == null)
        {
            throw new RpcException(new Status(
                StatusCode.NotFound,
                "์ฃผ๋ฌธ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
            ));
        }
        
        if (order.Status == "shipped" || order.Status == "delivered")
        {
            throw new RpcException(new Status(
                StatusCode.FailedPrecondition,
                "๋ฐฐ์†ก ์ค‘์ด๊ฑฐ๋‚˜ ์™„๋ฃŒ๋œ ์ฃผ๋ฌธ์€ ์ทจ์†Œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
            ));
        }
        
        // ์žฌ๊ณ  ๋ณต๊ตฌ
        await RollbackStock(order.Items);
        
        // ํ™˜๋ถˆ ์ฒ˜๋ฆฌ
        if (!string.IsNullOrEmpty(order.PaymentId))
        {
            await _paymentClient.RefundPaymentAsync(
                new RefundRequest
                {
                    PaymentId = order.PaymentId,
                    Amount = order.TotalAmount,
                    Reason = request.Reason
                });
        }
        
        order.Status = "cancelled";
        order.CancelReason = request.Reason;
        order.CancelledAt = DateTime.UtcNow;
        
        await _repository.UpdateAsync(order);
        
        return MapToResponse(order);
    }
    
    private OrderResponse MapToResponse(Order order)
    {
        var response = new OrderResponse
        {
            Id = order.Id,
            UserId = order.UserId,
            TotalAmount = order.TotalAmount,
            Status = order.Status,
            ShippingAddress = order.ShippingAddress,
            CreatedAt = order.CreatedAt.ToString("o")
        };
        
        response.Items.AddRange(order.Items);
        
        return response;
    }
}
๐Ÿ’ก ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ: ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์—์„œ๋Š” ์ „ํ†ต์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠธ๋žœ์žญ์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์–ด. ๋Œ€์‹  Saga ํŒจํ„ด์ด๋‚˜ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜(Compensating Transaction)์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ์œ„ ์ฝ”๋“œ์—์„œ RollbackStock์ด ๋ฐ”๋กœ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜์˜ ์˜ˆ์‹œ์•ผ!

API Gateway ๊ตฌํ˜„

๋งˆ์ง€๋ง‰์œผ๋กœ API Gateway๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋ชจ๋“  ์„œ๋น„์Šค๋ฅผ ํ•˜๋‚˜๋กœ ๋ฌถ์ž:

// API Gateway (ASP.NET Core)
public class OrderController : ControllerBase
{
    private readonly OrderService.OrderServiceClient _orderClient;
    private readonly ProductService.ProductServiceClient _productClient;
    
    public OrderController(
        OrderService.OrderServiceClient orderClient,
        ProductService.ProductServiceClient productClient)
    {
        _orderClient = orderClient;
        _productClient = productClient;
    }
    
    [HttpPost("api/orders")]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto dto)
    {
        try
        {
            var request = new CreateOrderRequest
            {
                UserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value,
                ShippingAddress = dto.ShippingAddress,
                PaymentMethod = dto.PaymentMethod
            };
            
            foreach (var item in dto.Items)
            {
                request.Items.Add(new OrderItem
                {
                    ProductId = item.ProductId,
                    Quantity = item.Quantity,
                    Price = item.Price
                });
            }
            
            var response = await _orderClient.CreateOrderAsync(request);
            
            return Ok(new
            {
                orderId = response.Id,
                totalAmount = response.TotalAmount,
                status = response.Status
            });
        }
        catch (RpcException ex)
        {
            return StatusCode((int)ex.StatusCode, new { error = ex.Status.Detail });
        }
    }
    
    [HttpGet("api/products/search")]
    public async Task<IActionResult> SearchProducts(
        [FromQuery] string keyword,
        [FromQuery] string category,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20)
    {
        var request = new SearchRequest
        {
            Keyword = keyword ?? "",
            Category = category ?? "",
            Page = page,
            PageSize = pageSize
        };
        
        var response = await _productClient.SearchProductsAsync(request);
        
        return Ok(new
        {
            products = response.Products.Select(p => new
            {
                id = p.Id,
                name = p.Name,
                price = p.Price,
                stock = p.Stock,
                category = p.Category
            }),
            totalCount = response.TotalCount,
            page = response.Page
        });
    }
}

// Startup.cs์—์„œ gRPC ํด๋ผ์ด์–ธํŠธ ๋“ฑ๋ก
public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpcClient<OrderService.OrderServiceClient>(options =>
    {
        options.Address = new Uri("https://order-service:5001");
    });
    
    services.AddGrpcClient<ProductService.ProductServiceClient>(options =>
    {
        options.Address = new Uri("https://product-service:5002");
    });
    
    services.AddGrpcClient<PaymentService.PaymentServiceClient>(options =>
    {
        options.Address = new Uri("https://payment-service:5003");
    });
    
    services.AddControllers();
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(/* JWT ์„ค์ • */);
}

๐Ÿš€ ๋ฐฐํฌ์™€ ์šด์˜

๊ฐœ๋ฐœ์ด ๋๋‚ฌ๋‹ค๊ณ  ๋์ด ์•„๋‹ˆ์•ผ! ์ด์ œ ๋ฐฐํฌํ•˜๊ณ  ์šด์˜ํ•ด์•ผ์ง€. ๐ŸŽฏ

Docker ์ปจํ…Œ์ด๋„ˆํ™”

๊ฐ ์„œ๋น„์Šค๋ฅผ Docker ์ปจํ…Œ์ด๋„ˆ๋กœ ๋งŒ๋“ค๋ฉด ๋ฐฐํฌ๊ฐ€ ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ:

# Dockerfile (Product Service)
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["ProductService/ProductService.csproj", "ProductService/"]
RUN dotnet restore "ProductService/ProductService.csproj"
COPY . .
WORKDIR "/src/ProductService"
RUN dotnet build "ProductService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "ProductService.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ProductService.dll"]

Docker Compose๋กœ ๋ชจ๋“  ์„œ๋น„์Šค๋ฅผ ํ•œ ๋ฒˆ์— ์‹คํ–‰:

# docker-compose.yml
version: '3.8'

services:
  product-service:
    build:
      context: .
      dockerfile: ProductService/Dockerfile
    ports:
      - "5002:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__Database=Server=db;Database=Products;
    depends_on:
      - db
    networks:
      - ecommerce-network

  order-service:
    build:
      context: .
      dockerfile: OrderService/Dockerfile
    ports:
      - "5001:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - ConnectionStrings__Database=Server=db;Database=Orders;
      - Services__ProductService=http://product-service:80
      - Services__PaymentService=http://payment-service:80
    depends_on:
      - db
      - product-service
      - payment-service
    networks:
      - ecommerce-network

  payment-service:
    build:
      context: .
      dockerfile: PaymentService/Dockerfile
    ports:
      - "5003:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
    networks:
      - ecommerce-network

  api-gateway:
    build:
      context: .
      dockerfile: ApiGateway/Dockerfile
    ports:
      - "5000:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - Services__OrderService=http://order-service:80
      - Services__ProductService=http://product-service:80
    depends_on:
      - order-service
      - product-service
    networks:
      - ecommerce-network

  db:
    image: postgres:14
    environment:
      - POSTGRES_PASSWORD=yourpassword
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - ecommerce-network

networks:
  ecommerce-network:
    driver: bridge

volumes:
  postgres-data:

Kubernetes ๋ฐฐํฌ

ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” Kubernetes๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์•„:

# product-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: product-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: product-service
  template:
    metadata:
      labels:
        app: product-service
    spec:
      containers:
      - name: product-service
        image: myregistry/product-service:latest
        ports:
        - containerPort: 80
        env:
        - name: ASPNETCORE_ENVIRONMENT
          value: "Production"
        - name: ConnectionStrings__Database
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: connection-string
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: product-service
spec:
  selector:
    app: product-service
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: product-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: product-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
์šด์˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… ํ—ฌ์Šค ์ฒดํฌ: ๊ฐ ์„œ๋น„์Šค์˜ ์ƒํƒœ๋ฅผ ๋ชจ๋‹ˆํ„ฐ๋ง
  • โœ… ๋กœ๊น…: ์ค‘์•™ํ™”๋œ ๋กœ๊ทธ ์ˆ˜์ง‘ (ELK Stack, Seq ๋“ฑ)
  • โœ… ๋ฉ”ํŠธ๋ฆญ: Prometheus + Grafana๋กœ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง
  • โœ… ๋ถ„์‚ฐ ์ถ”์ : Jaeger๋‚˜ Zipkin์œผ๋กœ ์š”์ฒญ ์ถ”์ 
  • โœ… ์„œํ‚ท ๋ธŒ๋ ˆ์ด์ปค: Polly๋กœ ์žฅ์•  ์ „ํŒŒ ๋ฐฉ์ง€
  • โœ… API ๊ฒŒ์ดํŠธ์›จ์ด: ์ธ์ฆ, ๋ผ์šฐํŒ…, ์†๋„ ์ œํ•œ
  • โœ… ์„œ๋น„์Šค ๋ฉ”์‹œ: Istio๋‚˜ Linkerd๋กœ ํŠธ๋ž˜ํ”ฝ ๊ด€๋ฆฌ

๐Ÿ’ก ๋งˆ์น˜๋ฉฐ: ๋‹น์‹ ์˜ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ ์—ฌ์ •

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์™”๋‹ค๋‹ˆ ๋Œ€๋‹จํ•ด! ๐ŸŽ‰

WCF์™€ gRPC๋ฅผ ํ™œ์šฉํ•œ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ ๊ตฌ์ถ•, ์ƒ๊ฐ๋ณด๋‹ค ๋ณต์žกํ•˜์ง€? ํ•˜์ง€๋งŒ ํ•œ ๋ฒˆ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๊ณ  ๋‚˜๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ๋ผ. ํŠนํžˆ ์š”์ฆ˜์ฒ˜๋Ÿผ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๊ฐ€ ๋Œ€์„ธ์ธ ์‹œ๋Œ€์—๋Š” ํ•„์ˆ˜ ๊ธฐ์ˆ ์ด์ง€!

ํ•ต์‹ฌ์„ ์ •๋ฆฌํ•˜์ž๋ฉด:

1. WCF๋Š” ๋ ˆ๊ฑฐ์‹œ์ง€๋งŒ ์—ฌ์ „ํžˆ ๋งŽ์€ ๊ณณ์—์„œ ์‚ฌ์šฉ ์ค‘
.NET Framework ํ™˜๊ฒฝ์ด๋‚˜ ๊ธฐ์กด ์‹œ์Šคํ…œ ์œ ์ง€๋ณด์ˆ˜์—๋Š” ํ•„์š”ํ•ด.

2. gRPC๋Š” ํ˜„๋Œ€์ ์ด๊ณ  ๊ฐ•๋ ฅํ•œ ์„ ํƒ
์„ฑ๋Šฅ, ์ŠคํŠธ๋ฆฌ๋ฐ, ํฌ๋กœ์Šค ํ”Œ๋žซํผ ์ง€์› ๋“ฑ ๋ชจ๋“  ๋ฉด์—์„œ ์šฐ์ˆ˜ํ•ด.

3. ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์€ ๋‹จ์ˆœํ•œ ๊ธฐ์ˆ ์ด ์•„๋‹ˆ๋ผ ์•„ํ‚คํ…์ฒ˜ ์‚ฌ๊ณ ๋ฐฉ์‹
์„œ๋น„์Šค ๋ถ„๋ฆฌ, ํ†ต์‹  ํŒจํ„ด, ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ, ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๋“ฑ ์ „์ฒด์ ์ธ ์„ค๊ณ„๊ฐ€ ์ค‘์š”ํ•ด.

4. ์‹ค๋ฌด์—์„œ๋Š” ์ด๋ก ๋ณด๋‹ค ๊ฒฝํ—˜์ด ์ค‘์š”
์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๊ณ , ์‹คํŒจํ•˜๊ณ , ๊ฐœ์„ ํ•˜๋ฉด์„œ ๋ฐฐ์šฐ๋Š” ๊ฒŒ ์ตœ๊ณ ์•ผ.

๐Ÿ’ก ํ•™์Šต ๋กœ๋“œ๋งต ์ถ”์ฒœ: 1๋‹จ๊ณ„: ๊ธฐ๋ณธ gRPC ์„œ๋น„์Šค ๋งŒ๋“ค๊ธฐ (Unary RPC)
2๋‹จ๊ณ„: ์ŠคํŠธ๋ฆฌ๋ฐ ํŒจํ„ด ์‹ค์Šต
3๋‹จ๊ณ„: ์—ฌ๋Ÿฌ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹  ๊ตฌํ˜„
4๋‹จ๊ณ„: ๋ณด์•ˆ, ์ธ์ฆ, ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€
5๋‹จ๊ณ„: Docker๋กœ ์ปจํ…Œ์ด๋„ˆํ™”
6๋‹จ๊ณ„: Kubernetes๋กœ ๋ฐฐํฌ
7๋‹จ๊ณ„: ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น… ์‹œ์Šคํ…œ ๊ตฌ์ถ•

ํ˜ผ์ž ํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ ๋ฉ˜ํ† ๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ!

๋ถ„์‚ฐ ์‹œ์Šคํ…œ ๊ฐœ๋ฐœ์€ ๋งˆ๋ผํ†ค์ด์•ผ. ํ•œ ๋ฒˆ์— ๋ชจ๋“  ๊ฑธ ์™„๋ฒฝํ•˜๊ฒŒ ๋งŒ๋“ค๋ ค๊ณ  ํ•˜์ง€ ๋งˆ. ์ž‘์€ ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ ๊ฐœ์„ ํ•ด ๋‚˜๊ฐ€๋ฉด ๋ผ. ๐Ÿƒ

๊ทธ๋ฆฌ๊ณ  ๊ธฐ์–ตํ•ด! ๊ธฐ์ˆ ์€ ๋„๊ตฌ์ผ ๋ฟ์ด์•ผ. ์ง„์งœ ์ค‘์š”ํ•œ ๊ฑด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฑฐ์ง€. WCF๋“  gRPC๋“ , ๋‹น์‹ ์˜ ํ”„๋กœ์ ํŠธ์— ๋งž๋Š” ์ตœ์„ ์˜ ์„ ํƒ์„ ํ•˜๋ฉด ๋ผ.

์ด ๊ธ€์ด ๋‹น์‹ ์˜ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ ์—ฌ์ •์— ๋„์›€์ด ๋˜์—ˆ๊ธธ ๋ฐ”๋ผ! ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋” ๊นŠ์ด ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด, ์‹ค์ œ๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ๊ฒฝํ—˜์„ ์Œ“์•„๋ด. ๊ทธ๊ฒŒ ๊ฐ€์žฅ ๋น ๋ฅธ ํ•™์Šต ๋ฐฉ๋ฒ•์ด๊ฑฐ๋“ ! ๐Ÿ’ช

ํ–‰์šด์„ ๋นŒ์–ด! ๋ฉ‹์ง„ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿš€

Happy Coding! ๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ‘ฉโ€๐Ÿ’ป

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.