[♻️] PUSH API 리팩토링 진행 중_3 : 대용량 발신 위한 버전 작성 중

This commit is contained in:
김선규 2025-02-27 17:09:35 +09:00
parent 59fa5bd014
commit 242f1a48df
8 changed files with 142 additions and 89 deletions

View File

@ -12,6 +12,7 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="7.1.0" /> <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="7.1.0" />

View File

@ -114,7 +114,7 @@ else
// 로컬 테스트 위한 부분 (올릴때는 꺼두기) // 로컬 테스트 위한 부분 (올릴때는 꺼두기)
// builder.WebHost.UseUrls("http://0.0.0.0:5144"); builder.WebHost.UseUrls("http://0.0.0.0:5144");
///// ===== builder 설정 부 ===== ///// ///// ===== builder 설정 부 ===== /////
@ -134,7 +134,7 @@ else
} }
// 로컬 테스트 위한 부분 (올릴떄는 켜두기) // 로컬 테스트 위한 부분 (올릴떄는 켜두기)
app.UseHttpsRedirection(); // app.UseHttpsRedirection();
app.UseRouting(); app.UseRouting();
// app.MapControllers(); // app.MapControllers();

View File

@ -4,10 +4,9 @@ using Microsoft.AspNetCore.Mvc;
using AcaMate.V1.Services; using AcaMate.V1.Services;
namespace AcaMate.V1.Controllers; namespace AcaMate.V1.Controllers;
[ApiController] [ApiController]
[Route("/api/v1/in/push")] [Route("/api/v1/in/push")]
[ApiExplorerSettings(GroupName = "공통")] [ApiExplorerSettings(GroupName = "공통")]
@ -16,10 +15,12 @@ public class PushController : ControllerBase
private readonly IWebHostEnvironment _env; private readonly IWebHostEnvironment _env;
private readonly ILogger<PushController> _logger; private readonly ILogger<PushController> _logger;
public PushController(IWebHostEnvironment env, ILogger<PushController> logger) private readonly IPushQueue _pushQueue;
public PushController(IWebHostEnvironment env, ILogger<PushController> logger, IPushQueue pushQueue)
{ {
_env = env; _env = env;
_logger = logger; _logger = logger;
_pushQueue = pushQueue;
} }
[HttpGet()] [HttpGet()]
@ -44,48 +45,58 @@ public class PushController : ControllerBase
[HttpPost("send")] [HttpPost("send")]
[CustomOperation("푸시전송", "저장된 양식으로, 사용자에게 푸시를 전송한다.(로컬 테스트 불가)", "푸시")] [CustomOperation("푸시전송", "저장된 양식으로, 사용자에게 푸시를 전송한다.(로컬 테스트 불가)", "푸시")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(APIResponseStatus<object>))]
public async Task<IActionResult> SendPush(string deviceToken, [FromBody] Payload? payload) public async Task<IActionResult> SendPush(string deviceToken, [FromBody] Payload payload)
{ {
if (string.IsNullOrWhiteSpace(deviceToken) || payload == null) if (string.IsNullOrWhiteSpace(deviceToken) || payload == null)
return BadRequest(APIResponse.InvalidInputError()); return BadRequest(APIResponse.InvalidInputError());
var isDev = _env.IsDevelopment(); var pushRequest = new PushRequest
var pushFile = new PushFile()
{ {
uri = isDev ? "https://api.sandbox.push.apple.com/" : "https://api.push.apple.com/", deviceToken = deviceToken,
p12Path = isDev ? "/src/private/AM_Push_Sandbox.p12" : "/src/private/AM_Push.p12", payload = payload
p12PWPath = "/src/private/appleKeys.json",
apnsTopic = "me.myds.ipstein.acamate.AcaMate"
}; };
_pushQueue.Enqueue(pushRequest);
return Ok("[푸시] 접수 완료");
try //
{ //
if (await new ApnsPushService().SendPushNotificationAsync(deviceToken, pushFile, payload)) // var isDev = _env.IsDevelopment();
{ //
return Ok(APIResponse.Send("000", "정상", Empty)); // var pushFileSetting = new PushFileSetting()
} // {
else // uri = isDev ? "https://api.sandbox.push.apple.com/" : "https://api.push.apple.com/",
{ // p12Path = isDev ? "/src/private/AM_Push_Sandbox.p12" : "/src/private/AM_Push.p12",
return StatusCode(500, APIResponse.UnknownError()); // p12PWPath = "/src/private/appleKeys.json",
} // apnsTopic = "me.myds.ipstein.acamate.AcaMate"
// };
} //
catch (ServiceConnectionFailedException failEx) // try
{ // {
_logger.LogError($"[푸시][에러] : {failEx}"); // if (await new ApnsPushService().SendPushNotificationAsync(deviceToken, pushFileSetting, payload))
return StatusCode(300, APIResponse.InternalSeverError()); // {
} // return Ok(APIResponse.Send("000", "정상", Empty));
catch (HttpRequestException httpEx) // }
{ // else
_logger.LogError($"[푸시][에러] : {httpEx}"); // {
return StatusCode(300, APIResponse.InternalSeverError()); // return StatusCode(500, APIResponse.UnknownError());
} // }
catch (Exception ex) //
{ // }
_logger.LogError($"[푸시][에러] : {ex}"); // catch (ServiceConnectionFailedException failEx)
return StatusCode(500, APIResponse.UnknownError()); // {
} // _logger.LogError($"[푸시][에러] : {failEx}");
// return StatusCode(300, APIResponse.InternalSeverError());
// }
// catch (HttpRequestException httpEx)
// {
// _logger.LogError($"[푸시][에러] : {httpEx}");
// return StatusCode(300, APIResponse.InternalSeverError());
// }
// catch (Exception ex)
// {
// _logger.LogError($"[푸시][에러] : {ex}");
// return StatusCode(500, APIResponse.UnknownError());
// }
} }
} }

View File

@ -43,10 +43,16 @@ public class Alert
/// <summary> /// <summary>
/// 푸시 등록하기 위한 여러 데이터 목록 /// 푸시 등록하기 위한 여러 데이터 목록
/// </summary> /// </summary>
public class PushFile public class PushFileSetting
{ {
public string uri { get; set; } public string uri { get; set; }
public string p12Path { get; set; } public string p12Path { get; set; }
public string p12PWPath { get; set; } public string p12PWPath { get; set; }
public string apnsTopic { get; set; } public string apnsTopic { get; set; }
} }
public class PushRequest
{
public string deviceToken { get; set; }
public Payload payload { get; set; }
}

View File

@ -6,47 +6,55 @@ using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Mvc; using Polly;
using AcaMate.V1.Models; using AcaMate.V1.Models;
using Microsoft.Extensions.Options;
using Version = System.Version; using Version = System.Version;
namespace AcaMate.V1.Services; namespace AcaMate.V1.Services;
public class ApnsPushService
{
public ApnsPushService()
{
public interface IApnsPushService
{
Task SendPushNotificationAsync(string deviceToken, Payload payload);
}
public class ApnsPushService: IApnsPushService
{
private readonly HttpClient _httpClient;
private readonly PushFileSetting _setting;
public ApnsPushService(HttpClient httpClient, IOptions<PushFileSetting> options)
{
_httpClient = httpClient;
_setting = options.Value;
} }
public async Task<bool> SendPushNotificationAsync(string deviceToken, PushFile pushFile, Payload payload) public async Task SendPushNotificationAsync(string deviceToken, Payload payload)
{ {
// 존재 안하면 예외 던져 버리는거 // 존재 안하면 예외 던져 버리는거
if (!File.Exists(pushFile.p12Path) || !File.Exists(pushFile.p12PWPath)) if (!File.Exists(_setting.p12Path) || !File.Exists(_setting.p12PWPath))
throw new FileNotFoundException("[푸시] : p12 관련 파일 확인 필요"); throw new FileNotFoundException("[푸시] : p12 관련 파일 확인 필요");
var jsonPayload = JsonSerializer.Serialize(payload); var jsonPayload = JsonSerializer.Serialize(payload);
var keys = // var keys =
JsonSerializer.Deserialize<Dictionary<string, string>>(await File.ReadAllTextAsync(pushFile.p12PWPath)) // JsonSerializer.Deserialize<Dictionary<string, string>>(await File.ReadAllTextAsync(_setting.p12PWPath))
?? throw new FileContentNotFoundException("[푸시] : 파일 내부의 값을 읽어오지 못함"); // ?? throw new FileContentNotFoundException("[푸시] : 파일 내부의 값을 읽어오지 못함");
//
try // try
{ // {
// var certificate = new X509Certificate2(pushFile.p12Path, keys["Password"]); // var handler = new HttpClientHandler();
// handler.ClientCertificates
var handler = new HttpClientHandler(); // .Add(new X509Certificate2(_setting.p12Path, keys["Password"]));
handler.ClientCertificates // handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
.Add(new X509Certificate2(pushFile.p12Path, keys["Password"])); //
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12; //
// using var client = new HttpClient(handler)
// {
using var client = new HttpClient(handler) // BaseAddress = new Uri(_setting.uri),
{ // Timeout = TimeSpan.FromSeconds(60)
BaseAddress = new Uri(pushFile.uri), // };
Timeout = TimeSpan.FromSeconds(60)
};
var request = new HttpRequestMessage(HttpMethod.Post, $"/3/device/{deviceToken}") var request = new HttpRequestMessage(HttpMethod.Post, $"/3/device/{deviceToken}")
{ {
@ -55,22 +63,36 @@ public class ApnsPushService
}; };
// 필수 헤더 추가 // 필수 헤더 추가
request.Headers.Add("apns-topic", pushFile.apnsTopic); request.Headers.Add("apns-topic", _setting.apnsTopic);
request.Headers.Add("apns-push-type", "alert"); request.Headers.Add("apns-push-type", "alert");
var response = await client.SendAsync(request); var policy = Policy.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
if (response.IsSuccessStatusCode) return true; await policy.ExecuteAsync(async () =>
else
{ {
var errorContent = await response.Content.ReadAsStringAsync(); var response = await _httpClient.SendAsync(request);
throw new ServiceConnectionFailedException($"[푸시] : APNS 통신 실패 - {errorContent}"); if (!response.IsSuccessStatusCode)
} {
} var errorContent = await response.Content.ReadAsStringAsync();
catch (HttpRequestException httpEx) throw new ServiceConnectionFailedException($"[푸시] : APNS 통신 실패 - {errorContent}");
{ }
Console.WriteLine($"HttpRequestException: {httpEx.Message}"); });
throw new ServiceConnectionFailedException($"[푸시] : APNS 통신 실패 - {httpEx.Message}");
} // var response = await client.SendAsync(request);
//
// if (response.IsSuccessStatusCode) return true;
// else
// {
// var errorContent = await response.Content.ReadAsStringAsync();
// throw new ServiceConnectionFailedException($"[푸시] : APNS 통신 실패 - {errorContent}");
// }
// }
// catch (HttpRequestException httpEx)
// {
// Console.WriteLine($"HttpRequestException: {httpEx.Message}");
// throw new ServiceConnectionFailedException($"[푸시] : APNS 통신 실패 - {httpEx.Message}");
// }
} }
} }

View File

@ -7,10 +7,11 @@
- JetBrains Rider - JetBrains Rider
### 추가 패키지 ### 추가 패키지
| No. | Name | Version | Description | | No. | Name | Version | Description |
|:---:|:-------------------------------------------:|:-------:|:---------------------------| |:---:|:---------------------------------------------:|:-------:|:---------------------------|
| 1 | Microsoft.AspNetCore.OpenApi | 8.0.10 | OpenAPI 를 지원하기 위해 사용 | | 1 | Microsoft.AspNetCore.OpenApi | 8.0.10 | OpenAPI 를 지원하기 위해 사용 |
| 2 | Microsoft.EntityFrameworkCore | 8.0.10 | 데이터베이스 작업을 간편하게 수행하기 위해 사용 | | 2 | Microsoft.EntityFrameworkCore | 8.0.10 | 데이터베이스 작업을 간편하게 수행하기 위해 사용 |
| 3 | Pomelo.EntityFrameworkCore.MySql | 8.0.2 | MariaDB 연결 | | 3 | Pomelo.EntityFrameworkCore.MySql | 8.0.2 | MariaDB 연결 |
| 4 |Microsoft.AspNetCore.Authentication.JwtBearer| 8.0.10 | | | 4 | Microsoft.AspNetCore.Authentication.JwtBearer | 8.0.10 | |
| 5 |Dapper|2.1.35|SQL 직접 작성| | 5 | Dapper | 2.1.35 | SQL 직접 작성 |
| 6 | Polly | 8.5.2 | 일시적 장애 및 예외상황 대처용| |

View File

@ -4,5 +4,11 @@
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
},
"PushFileSetting": {
"uri": "https://api.sandbox.push.apple.com/",
"p12Path": "/src/private/AM_Push_Sandbox.p12",
"p12PWPath": "/src/private/appleKeys.json",
"apnsTopic": "me.myds.ipstein.acamate.AcaMate"
} }
} }

View File

@ -5,5 +5,11 @@
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"AllowedHosts": "*" "PushFileSetting": {
"Uri": "https://api.push.apple.com/",
"P12Path": "/src/private/AM_Push.p12",
"P12PWPath": "/src/private/appleKeys.json",
"ApnsTopic": "me.myds.ipstein.acamate.AcaMate"
},
"AllowedHosts": "*"
} }