Skip to content

Commit e66cfe2

Browse files
committed
feat: 예외 처리 강화
1 parent 726dd1b commit e66cfe2

File tree

4 files changed

+229
-13
lines changed

4 files changed

+229
-13
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using ProjectVG.Common.Exceptions;
3+
using ProjectVG.Common.Constants;
4+
5+
namespace ProjectVG.Api.Controllers
6+
{
7+
[ApiController]
8+
[Route("api/v1/test")]
9+
public class TestController : ControllerBase
10+
{
11+
private readonly ILogger<TestController> _logger;
12+
13+
public TestController(ILogger<TestController> logger)
14+
{
15+
_logger = logger;
16+
}
17+
18+
[HttpGet("exceptions/{type}")]
19+
public IActionResult TestException(string type)
20+
{
21+
return type.ToLowerInvariant() switch
22+
{
23+
"validation" => throw new ValidationException(ErrorCode.VALIDATION_FAILED, "유효성 검사 실패"),
24+
"notfound" => throw new NotFoundException(ErrorCode.NOT_FOUND, "리소스를 찾을 수 없습니다"),
25+
"projectvg" => throw new ProjectVGException(ErrorCode.BAD_REQUEST, "ProjectVG 예외 테스트"),
26+
"external" => throw new ExternalServiceException("테스트서비스", "http://test.com/api", "외부 서비스 오류", ErrorCode.EXTERNAL_SERVICE_ERROR),
27+
"argument" => throw new ArgumentException("잘못된 인수입니다"),
28+
"invalidoperation" => throw new InvalidOperationException("잘못된 작업입니다"),
29+
"unauthorized" => throw new UnauthorizedAccessException("권한이 없습니다"),
30+
"timeout" => throw new TimeoutException("타임아웃이 발생했습니다"),
31+
"httprequest" => throw new HttpRequestException("HTTP 요청 오류"),
32+
"keynotfound" => throw new KeyNotFoundException("키를 찾을 수 없습니다"),
33+
"nullref" => throw new NullReferenceException("널 참조 오류"),
34+
"divide" => throw new DivideByZeroException("0으로 나누기 오류"),
35+
"index" => throw new IndexOutOfRangeException("인덱스 범위 오류"),
36+
"format" => throw new FormatException("형식 오류"),
37+
"overflow" => throw new OverflowException("오버플로우 오류"),
38+
"notimplemented" => throw new NotImplementedException("구현되지 않은 기능"),
39+
"notsupported" => throw new NotSupportedException("지원되지 않는 기능"),
40+
"objectdisposed" => throw new ObjectDisposedException("TestObject", "이미 해제된 객체입니다"),
41+
"aggregate" => throw new AggregateException(new Exception[] { new ArgumentException("인수 오류"), new InvalidOperationException("작업 오류") }),
42+
_ => throw new ProjectVGException(ErrorCode.BAD_REQUEST, $"알 수 없는 예외 타입: {type}")
43+
};
44+
}
45+
46+
[HttpGet("db-exceptions/{type}")]
47+
public IActionResult TestDbException(string type)
48+
{
49+
// 실제 DB 예외는 테스트하기 어려우므로 시뮬레이션
50+
return type.ToLowerInvariant() switch
51+
{
52+
"duplicate" => throw new InvalidOperationException("Cannot insert duplicate key row in object 'dbo.Users' with unique index 'IX_Users_Username'"),
53+
"foreignkey" => throw new InvalidOperationException("The DELETE statement conflicted with the REFERENCE constraint \"FK_Users_Characters\"."),
54+
"constraint" => throw new InvalidOperationException("The INSERT statement conflicted with the CHECK constraint \"CK_Users_Email\"."),
55+
_ => throw new InvalidOperationException("데이터베이스 오류 시뮬레이션")
56+
};
57+
}
58+
59+
[HttpGet("performance/{delay}")]
60+
public async Task<IActionResult> TestPerformance(int delay = 1000)
61+
{
62+
await Task.Delay(delay);
63+
return Ok(new { message = $"지연 시간 {delay}ms 완료", timestamp = DateTime.UtcNow });
64+
}
65+
66+
[HttpGet("memory/{size}")]
67+
public IActionResult TestMemory(int size = 1024)
68+
{
69+
var data = new byte[size * 1024]; // KB 단위
70+
return Ok(new { message = $"{size}KB 메모리 할당 완료", actualSize = data.Length });
71+
}
72+
}
73+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.AspNetCore.Mvc.Filters;
3+
4+
namespace ProjectVG.Api.Filters
5+
{
6+
public class ModelStateValidationFilter : ActionFilterAttribute
7+
{
8+
public override void OnActionExecuting(ActionExecutingContext context)
9+
{
10+
if (!context.ModelState.IsValid)
11+
{
12+
context.Result = new BadRequestObjectResult(context.ModelState);
13+
}
14+
}
15+
}
16+
}

ProjectVG.Api/Middleware/GlobalExceptionHandler.cs

Lines changed: 135 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@
22
using System.Text.Json;
33
using Microsoft.EntityFrameworkCore;
44
using Microsoft.AspNetCore.Mvc;
5+
using ProjectVG.Common.Exceptions;
56

67
namespace ProjectVG.Api.Middleware
78
{
89
public class GlobalExceptionHandler
910
{
1011
private readonly RequestDelegate _next;
1112
private readonly ILogger<GlobalExceptionHandler> _logger;
13+
private readonly IWebHostEnvironment _environment;
1214

13-
public GlobalExceptionHandler(RequestDelegate next, ILogger<GlobalExceptionHandler> logger)
15+
public GlobalExceptionHandler(RequestDelegate next, ILogger<GlobalExceptionHandler> logger, IWebHostEnvironment environment)
1416
{
1517
_next = next;
1618
_logger = logger;
19+
_environment = environment;
1720
}
1821

1922
public async Task InvokeAsync(HttpContext context)
@@ -37,15 +40,16 @@ private async Task HandleExceptionAsync(HttpContext context, Exception exception
3740

3841
var jsonResponse = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
3942
{
40-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
43+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
44+
WriteIndented = _environment.IsDevelopment()
4145
});
4246

4347
await context.Response.WriteAsync(jsonResponse);
4448
}
4549

4650
private ErrorResponse CreateErrorResponse(Exception exception, HttpContext context)
4751
{
48-
if (exception is ValidationException validationEx)
52+
if (exception is ProjectVG.Common.Exceptions.ValidationException validationEx)
4953
{
5054
return HandleValidationException(validationEx, context);
5155
}
@@ -75,6 +79,31 @@ private ErrorResponse CreateErrorResponse(Exception exception, HttpContext conte
7579
return HandleKeyNotFoundException(keyNotFoundEx, context);
7680
}
7781

82+
if (exception is ArgumentException argumentEx)
83+
{
84+
return HandleArgumentException(argumentEx, context);
85+
}
86+
87+
if (exception is InvalidOperationException invalidOpEx)
88+
{
89+
return HandleInvalidOperationException(invalidOpEx, context);
90+
}
91+
92+
if (exception is UnauthorizedAccessException unauthorizedEx)
93+
{
94+
return HandleUnauthorizedAccessException(unauthorizedEx, context);
95+
}
96+
97+
if (exception is TimeoutException timeoutEx)
98+
{
99+
return HandleTimeoutException(timeoutEx, context);
100+
}
101+
102+
if (exception is HttpRequestException httpEx)
103+
{
104+
return HandleHttpRequestException(httpEx, context);
105+
}
106+
78107
return HandleGenericException(exception, context);
79108
}
80109

@@ -140,23 +169,38 @@ private ErrorResponse HandleExternalServiceException(ExternalServiceException ex
140169

141170
private ErrorResponse HandleDbUpdateException(DbUpdateException exception, HttpContext context)
142171
{
143-
if (exception.InnerException?.Message.Contains("duplicate") == true)
172+
var innerMessage = exception.InnerException?.Message?.ToLowerInvariant() ?? string.Empty;
173+
174+
if (innerMessage.Contains("duplicate") || innerMessage.Contains("unique"))
144175
{
145176
_logger.LogWarning(exception, "데이터베이스 중복 키 오류 발생");
146177
return new ErrorResponse
147178
{
148-
ErrorCode = "리소스_중복",
179+
ErrorCode = "RESOURCE_CONFLICT",
149180
Message = "이미 존재하는 데이터입니다",
150181
StatusCode = 409,
151182
Timestamp = DateTime.UtcNow,
152183
TraceId = context.TraceIdentifier
153184
};
154185
}
155186

187+
if (innerMessage.Contains("foreign key") || innerMessage.Contains("constraint"))
188+
{
189+
_logger.LogWarning(exception, "데이터베이스 제약 조건 위반");
190+
return new ErrorResponse
191+
{
192+
ErrorCode = "CONSTRAINT_VIOLATION",
193+
Message = "관련 데이터가 존재하여 삭제할 수 없습니다",
194+
StatusCode = 400,
195+
Timestamp = DateTime.UtcNow,
196+
TraceId = context.TraceIdentifier
197+
};
198+
}
199+
156200
_logger.LogError(exception, "데이터베이스 업데이트 오류 발생");
157201
return new ErrorResponse
158202
{
159-
ErrorCode = "데이터베이스_오류",
203+
ErrorCode = "DATABASE_ERROR",
160204
Message = "데이터베이스 처리 중 오류가 발생했습니다",
161205
StatusCode = (int)HttpStatusCode.InternalServerError,
162206
Timestamp = DateTime.UtcNow,
@@ -170,26 +214,105 @@ private ErrorResponse HandleKeyNotFoundException(KeyNotFoundException exception,
170214

171215
return new ErrorResponse
172216
{
173-
ErrorCode = "리소스_찾을_수_없음",
217+
ErrorCode = "RESOURCE_NOT_FOUND",
174218
Message = exception.Message,
175219
StatusCode = (int)HttpStatusCode.NotFound,
176220
Timestamp = DateTime.UtcNow,
177221
TraceId = context.TraceIdentifier
178222
};
179223
}
180224

225+
private ErrorResponse HandleArgumentException(ArgumentException exception, HttpContext context)
226+
{
227+
_logger.LogWarning(exception, "잘못된 인수: {Message}", exception.Message);
228+
229+
return new ErrorResponse
230+
{
231+
ErrorCode = "INVALID_ARGUMENT",
232+
Message = "잘못된 요청 파라미터입니다",
233+
StatusCode = (int)HttpStatusCode.BadRequest,
234+
Timestamp = DateTime.UtcNow,
235+
TraceId = context.TraceIdentifier,
236+
Details = _environment.IsDevelopment() ? new List<string> { exception.Message } : null
237+
};
238+
}
239+
240+
private ErrorResponse HandleInvalidOperationException(InvalidOperationException exception, HttpContext context)
241+
{
242+
_logger.LogWarning(exception, "잘못된 작업: {Message}", exception.Message);
243+
244+
return new ErrorResponse
245+
{
246+
ErrorCode = "INVALID_OPERATION",
247+
Message = "요청한 작업을 수행할 수 없습니다",
248+
StatusCode = (int)HttpStatusCode.BadRequest,
249+
Timestamp = DateTime.UtcNow,
250+
TraceId = context.TraceIdentifier,
251+
Details = _environment.IsDevelopment() ? new List<string> { exception.Message } : null
252+
};
253+
}
254+
255+
private ErrorResponse HandleUnauthorizedAccessException(UnauthorizedAccessException exception, HttpContext context)
256+
{
257+
_logger.LogWarning(exception, "권한 없음: {Message}", exception.Message);
258+
259+
return new ErrorResponse
260+
{
261+
ErrorCode = "UNAUTHORIZED",
262+
Message = "접근 권한이 없습니다",
263+
StatusCode = (int)HttpStatusCode.Unauthorized,
264+
Timestamp = DateTime.UtcNow,
265+
TraceId = context.TraceIdentifier
266+
};
267+
}
268+
269+
private ErrorResponse HandleTimeoutException(TimeoutException exception, HttpContext context)
270+
{
271+
_logger.LogWarning(exception, "타임아웃 발생: {Message}", exception.Message);
272+
273+
return new ErrorResponse
274+
{
275+
ErrorCode = "TIMEOUT",
276+
Message = "요청 처리 시간이 초과되었습니다",
277+
StatusCode = (int)HttpStatusCode.RequestTimeout,
278+
Timestamp = DateTime.UtcNow,
279+
TraceId = context.TraceIdentifier
280+
};
281+
}
282+
283+
private ErrorResponse HandleHttpRequestException(HttpRequestException exception, HttpContext context)
284+
{
285+
_logger.LogError(exception, "HTTP 요청 오류: {Message}", exception.Message);
286+
287+
return new ErrorResponse
288+
{
289+
ErrorCode = "HTTP_REQUEST_ERROR",
290+
Message = "외부 서비스와의 통신 중 오류가 발생했습니다",
291+
StatusCode = (int)HttpStatusCode.BadGateway,
292+
Timestamp = DateTime.UtcNow,
293+
TraceId = context.TraceIdentifier
294+
};
295+
}
296+
181297
private ErrorResponse HandleGenericException(Exception exception, HttpContext context)
182298
{
183-
_logger.LogError(exception, "예상치 못한 예외 발생: {ExceptionType} - {Message}",
184-
exception.GetType().Name, exception.Message);
299+
var exceptionType = exception.GetType().Name;
300+
var isDevelopment = _environment.IsDevelopment();
301+
302+
_logger.LogError(exception, "예상치 못한 예외 발생: {ExceptionType} - {Message}", exceptionType, exception.Message);
185303

186304
return new ErrorResponse
187305
{
188-
ErrorCode = "내부_서버_오류",
189-
Message = "서버에서 예상치 못한 오류가 발생했습니다",
306+
ErrorCode = "INTERNAL_SERVER_ERROR",
307+
Message = isDevelopment ? exception.Message : "서버에서 예상치 못한 오류가 발생했습니다",
190308
StatusCode = (int)HttpStatusCode.InternalServerError,
191309
Timestamp = DateTime.UtcNow,
192-
TraceId = context.TraceIdentifier
310+
TraceId = context.TraceIdentifier,
311+
Details = isDevelopment ? new List<string>
312+
{
313+
$"Exception Type: {exceptionType}",
314+
$"Stack Trace: {exception.StackTrace}"
315+
} : null
193316
};
194317
}
195318
}

ProjectVG.Api/Program.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using ProjectVG.Api.Configuration;
1515
using ProjectVG.Api.Services;
1616
using ProjectVG.Api.Middleware;
17+
using ProjectVG.Api.Filters;
1718
using ProjectVG.Infrastructure.Realtime.WebSocketConnection;
1819
using ProjectVG.Application.Services.Messaging;
1920
using ProjectVG.Common.Models.Session;
@@ -30,7 +31,10 @@
3031
builder.Configuration.AddEnvironmentVariableSubstitution(builder.Configuration);
3132

3233
// Add services to the container.
33-
builder.Services.AddControllers();
34+
builder.Services.AddControllers(options =>
35+
{
36+
options.Filters.Add<ModelStateValidationFilter>();
37+
});
3438
builder.Services.AddEndpointsApiExplorer();
3539
builder.Services.AddSwaggerGen(c => {
3640
c.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo {

0 commit comments

Comments
 (0)