Ver3.0 ๐ WCF์ gRPC๋ก ๋ง๋๋ ๋ถ์ฐ ์์คํ ์์ ์ ๋ณต

๐ WCF์ gRPC๋ก ๋ง๋๋ ๋ถ์ฐ ์์คํ ์์ ์ ๋ณต
์๋น์ค ๊ฐ ํต์ ์ ๋ชจ๋ ๊ฒ, ์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ์๋ ค์ค๊ฒ!
๐ฏ ์ ๋ถ์ฐ ์์คํ ์ด ํ์ํ ๊น?
์ผ, ์์ฆ ํ๋ก๊ทธ๋จ ๊ฐ๋ฐํ๋ค ๋ณด๋ฉด ์ง์ง ๋ณต์กํ์ง ์์? ์์ ์๋ ํ๋์ ๊ฑฐ๋ํ ํ๋ก๊ทธ๋จ์ผ๋ก ๋ชจ๋ ๊ฑธ ์ฒ๋ฆฌํ๋๋ฐ, ์ด์ ๋ ๊ทธ๋ ๊ฒ ํ๋ฉด ์ ๋๋ ์๋๊ฐ ์์ด. ๐ค
์๊ฐํด๋ด. ๋ทํ๋ฆญ์ค ๊ฐ์ ์๋น์ค๋ฅผ ๋ง๋ ๋ค๊ณ ์น์. ์ฌ์ฉ์ ๋ก๊ทธ์ธ, ์์ ์คํธ๋ฆฌ๋ฐ, ์ถ์ฒ ์๊ณ ๋ฆฌ์ฆ, ๊ฒฐ์ ์์คํ
... ์ด ๋ชจ๋ ๊ฑธ ํ๋์ ํ๋ก๊ทธ๋จ์ ๋๋ ค ๋ฃ์ผ๋ฉด ์ด๋ป๊ฒ ๋ ๊น? ์ ์ง๋ณด์ ์ง์ฅ์ด ํผ์ณ์ง๊ฒ ์ง! ๐ฑ
๊ทธ๋์ ๋ฑ์ฅํ ๊ฒ ๋ฐ๋ก ๋ถ์ฐ ์์คํ
(Distributed System)์ด์ผ. ๊ฐ๊ฐ์ ๊ธฐ๋ฅ์ ๋
๋ฆฝ์ ์ธ ์๋น์ค๋ก ์ชผ๊ฐ์, ์๋ก ํต์ ํ๋ฉด์ ํ๋ ฅํ๊ฒ ๋ง๋๋ ๊ฑฐ์ง. ๋ง์น ์ค์ผ์คํธ๋ผ์ฒ๋ผ ๊ฐ์์ ์
๊ธฐ๊ฐ ์กฐํ๋กญ๊ฒ ์ฐ์ฃผํ๋ ๊ฒ์ฒ๋ผ ๋ง์ด์ผ! ๐ผ
โจ ์ ์ง๋ณด์: ํ ๋ถ๋ถ๋ง ์์ ํด๋ ์ ์ฒด์ ์ํฅ ์์ด
โจ ์ฅ์ ๊ฒฉ๋ฆฌ: ํ ์๋น์ค๊ฐ ์ฃฝ์ด๋ ๋ค๋ฅธ ์๋น์ค๋ ์ด์์์ด
โจ ๊ธฐ์ ๋ค์์ฑ: ๊ฐ ์๋น์ค๋ง๋ค ์ต์ ์ ๊ธฐ์ ์คํ ์ ํ ๊ฐ๋ฅ
๊ทผ๋ฐ ์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ์๊ฒจ. ์ด๋ ๊ฒ ์ชผ๊ฐ์ง ์๋น์ค๋ค์ด ์ด๋ป๊ฒ ์๋ก ๋ํ๋ฅผ ๋๋๊น? ๐ฌ
๋ฐ๋ก ์ฌ๊ธฐ์ WCF(Windows Communication Foundation)์ gRPC(Google Remote Procedure Call)๊ฐ ๋ฑ์ฅํ๋ ๊ฑฐ์ผ! ์ด ๋ ์น๊ตฌ๋ค์ ์๋น์ค ๊ฐ ํต์ ์ ๊ฐ๋ฅํ๊ฒ ํด์ฃผ๋ ๊ฐ๋ ฅํ ํ๋ ์์ํฌ๋ค์ด์ง.
๐๏ธ WCF: ๋ง์ดํฌ๋ก์ํํธ์ ํต์ ํ๋ ์์ํฌ
WCF๋ 2006๋
์ ๋ง์ดํฌ๋ก์ํํธ๊ฐ .NET Framework 3.0๊ณผ ํจ๊ป ์ถ์ํ ํต์ ํ๋ ์์ํฌ์ผ. ๋น์์๋ ์ ๋ง ํ์ ์ ์ด์์ด! ๐
WCF ์ด์ ์๋ ์น ์๋น์ค๋ฅผ ๋ง๋ค๋ ค๋ฉด ASMX, Remoting, MSMQ ๋ฑ ์ฌ๋ฌ ๊ธฐ์ ์ ๋ฐ๋ก๋ฐ๋ก ๋ฐฐ์์ผ ํ๊ฑฐ๋ . ๊ทผ๋ฐ WCF๋ ์ด ๋ชจ๋ ๊ฑธ ํ๋๋ก ํตํฉํ์ด. ๋ง์น ์ค์์ค ์๋ฏธ ๋์ดํ์ฒ๋ผ ํ๋๋ก ์ฌ๋ฌ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๊ฑฐ์ง! ๐ง
WCF๋ฅผ ์ดํดํ๋ ค๋ฉด ABC๋ฅผ ์์์ผ ํด. ์ด๊ฑด Address, Binding, Contract์ ์ฝ์์ผ.
๐ Address (์ฃผ์): ์๋น์ค๊ฐ ์ด๋์ ์๋์ง ์๋ ค์ฃผ๋ ์์น ์ ๋ณด์ผ. ๋ง์น ์ง ์ฃผ์์ฒ๋ผ!
์๋ฅผ ๋ค๋ฉด http://localhost:8080/MyService ์ด๋ฐ ์์ด์ง.
๐ Binding (๋ฐ์ธ๋ฉ): ์ด๋ค ๋ฐฉ์์ผ๋ก ํต์ ํ ๊ฑด์ง ์ ํ๋ ๊ฑฐ์ผ. HTTP๋ฅผ ์ธ ๊ฑด์ง, TCP๋ฅผ ์ธ ๊ฑด์ง, ๋ณด์์ ์ด๋ป๊ฒ ํ ๊ฑด์ง ๋ฑ๋ฑ.
๐ Contract (๊ณ์ฝ): ์๋น์ค๊ฐ ์ด๋ค ๊ธฐ๋ฅ์ ์ ๊ณตํ๋์ง ์ ์ํ๋ ์ธํฐํ์ด์ค์ผ. ๋ฉ๋ดํ ๊ฐ์ ๊ฑฐ๋ผ๊ณ ์๊ฐํ๋ฉด ๋ผ!
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();
}
}
WCF์ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค
WCF๋ ๊ธฐ๋ณธ์ ์ธ ํต์ ์ธ์๋ ์์ฒญ๋๊ฒ ๋ง์ ๊ธฐ๋ฅ์ ์ ๊ณตํด. ๋ช ๊ฐ์ง ์ค์ํ ๊ฒ๋ค์ ์ดํด๋ณด์! ๐
๋ณต์กํ ๊ฐ์ฒด๋ฅผ ์ฃผ๊ณ ๋ฐ์ ๋๋ ๋ฐ์ดํฐ ๊ณ์ฝ์ ์ฌ์ฉํด์ผ ํด:
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);
}
์๋น์ค์์ ๋ฐ์ํ ์ค๋ฅ๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋๋ก ์ ๋ฌํ๋ ค๋ฉด 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 = "ํ๊ธธ๋" };
}
}
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();
}
}
โก gRPC: ๊ตฌ๊ธ์ ์ฐจ์ธ๋ RPC ํ๋ ์์ํฌ
์, ์ด์ gRPC ์ด์ผ๊ธฐ๋ฅผ ํด๋ณผ๊น? gRPC๋ ๊ตฌ๊ธ์ด ๋ง๋ ํ๋์ ์ธ RPC(Remote Procedure Call) ํ๋ ์์ํฌ์ผ. ๐
2015๋
์ ์คํ์์ค๋ก ๊ณต๊ฐ๋๋๋ฐ, ์์ฆ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ
์ฒ์์ ์์ฒญ๋๊ฒ ์ธ๊ธฐ๊ฐ ๋ง์. ์ ๊ทธ๋ด๊น? ์ฑ๋ฅ์ด ๋ฏธ์น ๋ฏ์ด ๋น ๋ฅด๊ฑฐ๋ ! โก
gRPC์ ๊ฐ์ฅ ํฐ ํน์ง์ HTTP/2๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋ค๋ ๊ฑฐ์ผ. HTTP/1.1๊ณผ ๋น๊ตํ๋ฉด ์์ฒญ๋ ์ฑ๋ฅ ํฅ์์ด ์์ง. ๊ทธ๋ฆฌ๊ณ Protocol Buffers(์ค์ฌ์ Protobuf)๋ผ๋ ์ง๋ ฌํ ํฌ๋งท์ ์ฌ์ฉํด์ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์.
๐ฅ 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 ๋ฑ)๋ ํ๋ ๋ฒํธ์ผ. ์ด๊ฒ ์ค์ ๋ฐ์ด๋๋ฆฌ ์ธ์ฝ๋ฉ์์ ์ฌ์ฉ๋๋ ์๋ณ์๊ฑฐ๋ .
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์ 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;
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+ ์ฌ์ฉ
์ฑ๋ฅ ๋น๊ต: ์ค์ ๋ฒค์น๋งํฌ
์ค์ ๋ก ์ผ๋ง๋ ์ฐจ์ด๊ฐ ๋ ๊น? ๊ฐ๋จํ ๋ฒค์น๋งํฌ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์! ๐
โข ์์ฒญ ์: 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. ์ฑ๋ฅ ์ต์ ํ
์ฑ๋ฅ์ ๊ทน๋ํํ๊ธฐ ์ํ ๋ช ๊ฐ์ง ํ์ด์ผ:
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);
}
}
๐ ์ค์ ํ๋ก์ ํธ: ์ ์์๊ฑฐ๋ ๋ง์ดํฌ๋ก์๋น์ค
์ด๋ก ๋ง ๋ฐฐ์ฐ๋ฉด ์ฌ๋ฏธ์์ง? ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! ๊ฐ๋จํ ์ ์์๊ฑฐ๋ ์์คํ ์ ๋ง์ดํฌ๋ก์๋น์ค๋ก ๊ตฌํํด๋ณผ๊ฒ. ๐
์ํคํ ์ฒ ์ค๊ณ
์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์์คํ ์ ๋ค์ ์๋น์ค๋ค๋ก ๊ตฌ์ฑ๋ผ:
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;
}
}
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. ์ค๋ฌด์์๋ ์ด๋ก ๋ณด๋ค ๊ฒฝํ์ด ์ค์
์ง์ ๋ง๋ค์ด๋ณด๊ณ , ์คํจํ๊ณ , ๊ฐ์ ํ๋ฉด์ ๋ฐฐ์ฐ๋ ๊ฒ ์ต๊ณ ์ผ.
2๋จ๊ณ: ์คํธ๋ฆฌ๋ฐ ํจํด ์ค์ต
3๋จ๊ณ: ์ฌ๋ฌ ์๋น์ค ๊ฐ ํต์ ๊ตฌํ
4๋จ๊ณ: ๋ณด์, ์ธ์ฆ, ์ค๋ฅ ์ฒ๋ฆฌ ์ถ๊ฐ
5๋จ๊ณ: Docker๋ก ์ปจํ ์ด๋ํ
6๋จ๊ณ: Kubernetes๋ก ๋ฐฐํฌ
7๋จ๊ณ: ๋ชจ๋ํฐ๋ง๊ณผ ๋ก๊น ์์คํ ๊ตฌ์ถ
ํผ์ ํ๊ธฐ ์ด๋ ต๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ๋ฉํ ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ!
๋ถ์ฐ ์์คํ
๊ฐ๋ฐ์ ๋ง๋ผํค์ด์ผ. ํ ๋ฒ์ ๋ชจ๋ ๊ฑธ ์๋ฒฝํ๊ฒ ๋ง๋ค๋ ค๊ณ ํ์ง ๋ง. ์์ ๊ฒ๋ถํฐ ์์ํด์ ์ ์ง์ ์ผ๋ก ๊ฐ์ ํด ๋๊ฐ๋ฉด ๋ผ. ๐
๊ทธ๋ฆฌ๊ณ ๊ธฐ์ตํด! ๊ธฐ์ ์ ๋๊ตฌ์ผ ๋ฟ์ด์ผ. ์ง์ง ์ค์ํ ๊ฑด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฑฐ์ง. WCF๋ gRPC๋ , ๋น์ ์ ํ๋ก์ ํธ์ ๋ง๋ ์ต์ ์ ์ ํ์ ํ๋ฉด ๋ผ.
์ด ๊ธ์ด ๋น์ ์ ๋ถ์ฐ ์์คํ
์ฌ์ ์ ๋์์ด ๋์๊ธธ ๋ฐ๋ผ! ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋ ๊น์ด ๋ฐฐ์ฐ๊ณ ์ถ๋ค๋ฉด, ์ค์ ๋ก ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด๋ฉด์ ๊ฒฝํ์ ์์๋ด. ๊ทธ๊ฒ ๊ฐ์ฅ ๋น ๋ฅธ ํ์ต ๋ฐฉ๋ฒ์ด๊ฑฐ๋ ! ๐ช
ํ์ด์ ๋น์ด! ๋ฉ์ง ๋ถ์ฐ ์์คํ
์ ๋ง๋ค์ด๋ณด์! ๐
Happy Coding! ๐จโ๐ป๐ฉโ๐ป
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.