1. ASP.NET 웹 페이지 간 데이터 전달 방법
ASP.NET 환경에서 웹 페이지 간 데이터를 교환하는 여러 가지 방식이 있습니다. 각 방법은 데이터의 크기, 보안 요구 사항, 지속성 등에 따라 적합한 상황이 다릅니다.
- QueryString (쿼리 스트링): URL에 데이터를 추가하여 전달합니다. 간단한 데이터 전달에 용이하지만, 노출되기 쉬우므로 민감한 정보에는 적합하지 않습니다.
- Cookie (쿠키): 클라이언트 측 브라우저에 데이터를 저장합니다. 사용자 설정이나 로그인 상태 유지에 사용되며, 크기 제한이 있고 보안에 주의해야 합니다.
- Session (세션): 서버 메모리에 사용자별 데이터를 저장합니다. 로그인 정보나 장바구니 등 사용자 고유의 상태를 유지하는 데 유용하지만, 서버 자원을 소모합니다.
- Application (애플리케이션): 모든 사용자에게 공유되는 데이터를 서버 메모리에 저장합니다. 웹 애플리케이션 전반에 걸쳐 공통으로 사용되는 설정 값 등에 적합합니다.
- Server.Transfer: 서버 내부에서 요청을 다른 페이지로 전달합니다. 클라이언트의 URL은 변경되지 않으며, 서버 간 직접적인 전환으로 효율적입니다.
2. .NET Core 미들웨어 이해
.NET Core 미들웨어는 HTTP 요청 처리 파이프라인을 구성하는 핵심 요소입니다. 각 미들웨어 컴포넌트는 들어오는 HTTP 요청을 처리하고, 필요에 따라 다음 미들웨어로 요청을 전달하거나 직접 응답을 생성하여 파이프라인의 흐름을 제어합니다.
이러한 미들웨어 체인은 요청이 애플리케이션에 도달하는 시점부터 응답이 클라이언트에 전송되기까지 다양한 작업을 수행할 수 있도록 합니다. 일반적인 미들웨어의 역할은 다음과 같습니다:
- 사용자 인증 및 권한 부여
- 요청/응답 로깅
- 예외 처리
- 정적 파일 서비스
- HTTP 헤더 조작
미들웨어는 IApplicationBuilder 인터페이스의 확장 메서드(예: UseAuthentication(), UseStaticFiles())를 통해 Program.cs 또는 Startup.cs 파일의 Configure 메서드에서 구성됩니다.
3. ASP.NET Core에서 의존성 주입(Dependency Injection) 설정
ASP.NET Core는 의존성 주입(DI)을 핵심 기능으로 제공하여, 컴포넌트 간 결합도를 낮추고 테스트 용이성을 높입니다. 서비스는 일반적으로 Program.cs (또는 이전 버전의 Startup.cs) 파일의 ConfigureServices 메서드에서 등록됩니다.
서비스는 세 가지 생명주기(Transient, Scoped, Singleton) 중 하나로 등록할 수 있으며, 컨트롤러나 다른 클래스에서 생성자 주입을 통해 사용됩니다.
- Transient (일시적): 요청될 때마다 새로운 인스턴스가 생성됩니다. 가볍고 상태가 없는 서비스에 적합합니다.
- Scoped (스코프): 단일 HTTP 요청 내에서 한 번만 인스턴스화되고, 해당 요청이 완료되면 폐기됩니다. 요청 관련 상태를 공유하는 서비스에 적합합니다.
- Singleton (싱글턴): 애플리케이션의 전체 생명주기 동안 단 하나의 인스턴스만 생성됩니다. 전역 상태를 유지하거나 비용이 많이 드는 객체에 사용됩니다.
서비스 등록 예시 (Program.cs 또는 Startup.cs)
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// 서비스 컨테이너 구성
builder.Services.AddTransient<IApplicationService, ConcreteApplicationService>(); // 일시적 서비스
builder.Services.AddScoped<IUserSessionTracker, UserSessionTracker>(); // 스코프 서비스
builder.Services.AddSingleton<ISystemConfiguration, SystemConfiguration>(); // 싱글턴 서비스
builder.Services.AddControllersWithViews();
var app = builder.Build();
// ... 미들웨어 구성
app.Run();
}
}
컨트롤러에서의 서비스 사용 예시
public class ReportController : ControllerBase
{
private readonly IApplicationService _appService;
// 생성자 주입을 통해 서비스 사용
public ReportController(IApplicationService appService)
{
_appService = appService;
}
[HttpGet("generate")]
public IActionResult GenerateReport()
{
var reportData = _appService.ProcessDataForReport();
return Ok(reportData);
}
}
4. ASP.NET Core의 구성(Configuration) 관리
ASP.NET Core의 구성 시스템은 유연하며, 다양한 소스(예: appsettings.json 파일, 환경 변수, 명령줄 인수 등)에서 설정을 로드하고 계층적으로 병합합니다. 애플리케이션 코드에서는 IConfiguration 인터페이스를 통해 이러한 구성 값에 접근합니다.
구성 공급자(Configuration Providers)는 appsettings.json과 같은 각 소스로부터 데이터를 읽어와 애플리케이션이 사용할 수 있는 형태로 제공합니다.
구성 사용 예시 (Program.cs 또는 Startup.cs)
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// IConfiguration 인스턴스는 자동으로 주입되거나 builder.Configuration으로 접근 가능
IConfiguration appConfig = builder.Configuration;
// 구성에서 값 읽기
var appName = appConfig["Application:Name"];
var dbConnection = appConfig.GetConnectionString("DefaultDatabase");
Console.WriteLine($"Application Name: {appName}");
Console.WriteLine($"Default Database Connection String: {dbConnection}");
builder.Services.AddControllers();
// ... 기타 서비스 등록
var app = builder.Build();
// ... 미들웨어 구성
app.Run();
}
}
appsettings.json 파일 예시
{
"Application": {
"Name": "My Awesome App"
},
"ConnectionStrings": {
"DefaultDatabase": "Server=localhost;Database=MyAppDb;User Id=user;Password=password;"
}
}
5. ASP.NET Core 라우팅(Routing) 메커니즘
ASP.NET Core의 라우팅 시스템은 들어오는 HTTP 요청의 URL을 애플리케이션 내의 적절한 컨트롤러 액션 또는 Razor Pages 핸들러에 매핑하는 역할을 합니다. 이는 요청이 어디로 전달되어 처리될지 결정하는 핵심 메커니즘입니다.
라우팅은 크게 두 가지 방식으로 구성할 수 있습니다.
5.1. 컨벤션 기반 라우팅 (Conventional Routing)
Program.cs (또는 Startup.cs) 파일의 Configure 메서드에서 미리 정의된 URL 패턴 규칙에 따라 라우팅을 설정합니다. 이는 대부분의 MVC 애플리케이션에서 표준적으로 사용됩니다.
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"); // 일반적인 기본 라우트
});
app.Run();
}
}
5.2. 속성 기반 라우팅 (Attribute Routing)
컨트롤러 클래스나 개별 액션 메서드에 직접 [Route] 속성을 사용하여 URL 패턴을 정의합니다. 이는 RESTful API를 구현하거나 특정 URL에 대한 더 세밀한 제어가 필요할 때 유용합니다.
[Route("api/catalog")] // 컨트롤러 수준의 라우트 접두사
public class CatalogController : ControllerBase
{
[HttpGet("{productId:int}")] // GET 요청 및 productId 매개변수
public IActionResult GetProductById(int productId)
{
// 특정 제품 데이터를 조회하고 반환
return Ok(new { ProductId = productId, Name = $"Product {productId}" });
}
[HttpGet("categories/{categoryName}")] // 특정 카테고리의 제품을 조회
public IActionResult GetProductsByCategory(string categoryName)
{
// 특정 카테고리에 속하는 제품 목록 반환
return Ok(new { Category = categoryName, Items = new[] { "Item1", "Item2" } });
}
}
위 예시에서 /api/catalog/123 또는 /api/catalog/categories/Electronics와 같은 URL이 특정 액션 메서드로 매핑됩니다.
6. ASP.NET Core에서 인증(Authentication) 구현
ASP.NET Core에서 인증은 사용자의 신원을 확인하는 과정입니다. ASP.NET Core Identity, 쿠키, JWT(JSON Web Tokens), OAuth 등 다양한 메커니즘을 통해 구현할 수 있습니다.
6.1. 인증 서비스 구성
Program.cs (또는 Startup.cs)의 ConfigureServices 메서드에서 인증 메커니즘을 설정합니다.
ASP.NET Core Identity
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AuthApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));
services.AddIdentity<AppUser, IdentityRole>() // AppUser는 IdentityUser를 상속받는 사용자 클래스
.AddEntityFrameworkStores<AuthApplicationDbContext>()
.AddDefaultTokenProviders(); // 이메일 확인, 비밀번호 재설정 토큰 등
services.AddControllersWithViews();
services.AddRazorPages();
}
쿠키(Cookie) 기반 인증
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("AppCookieScheme") // 인증 스킴 이름 정의
.AddCookie("AppCookieScheme", options =>
{
options.Cookie.Name = "MyAppSessionCookie";
options.LoginPath = "/Auth/Login"; // 로그인 페이지 경로
options.AccessDeniedPath = "/Auth/Forbidden"; // 접근 거부 페이지 경로
});
services.AddControllersWithViews();
}
JWT (JSON Web Token) 기반 인증
public void ConfigureServices(IServiceCollection services)
{
var jwtKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("SuperSecretAuthKey_DoNotShare")); // 보안 키
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "MyWebAppIssuer", // 토큰 발행자
ValidAudience = "MyWebAppAudience", // 토큰 소비자
IssuerSigningKey = jwtKey
};
});
services.AddControllersWithViews();
}
6.2. 인증 미들웨어 활성화
Program.cs (또는 Startup.cs)의 Configure 메서드에서 인증 및 권한 부여 미들웨어를 요청 파이프라인에 추가합니다.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... 기타 미들웨어
app.UseRouting();
app.UseAuthentication(); // 인증 미들웨어 활성화 (필수)
app.UseAuthorization(); // 권한 부여 미들웨어 활성화 (인증 후에 위치해야 함)
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
}
6.3. 등록 및 로그인 예시
Identity를 사용한 사용자 등록
public class AuthenticationController : Controller
{
private readonly UserManager<AppUser> _userManager;
private readonly SignInManager<AppUser> _signInManager;
public AuthenticationController(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpGet]
public IActionResult Register() => View();
[HttpPost]
public async Task<IActionResult> Register(UserRegistrationModel model)
{
if (ModelState.IsValid)
{
var newUser = new AppUser { UserName = model.Email, Email = model.Email, FullName = model.FullName };
var result = await _userManager.CreateAsync(newUser, model.Password);
if (result.Succeeded)
{
await _signInManager.SignInAsync(newUser, isPersistent: false);
return RedirectToAction("Index", "Home");
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
return View(model);
}
}
쿠키를 사용한 사용자 로그인
public class AuthenticationController : Controller
{
[HttpGet]
public IActionResult Login() => View();
[HttpPost]
public async Task<IActionResult> Login(UserLoginModel model)
{
if (ModelState.IsValid)
{
// 실제 사용자 자격 증명 확인 로직 (예: 데이터베이스 조회)
var authenticatedUser = ValidateCredentials(model.Username, model.Password);
if (authenticatedUser != null)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, authenticatedUser.Username),
new Claim(ClaimTypes.Role, authenticatedUser.Role)
};
var claimsIdentity = new ClaimsIdentity(claims, "AppCookieScheme");
var authProperties = new AuthenticationProperties
{
IsPersistent = model.RememberMe // "로그인 유지" 기능
};
await HttpContext.SignInAsync("AppCookieScheme", new ClaimsPrincipal(claimsIdentity), authProperties);
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError(string.Empty, "잘못된 로그인 시도입니다.");
}
return View(model);
}
private UserAccount ValidateCredentials(string username, string password)
{
// 사용자 이름과 비밀번호를 검증하고, 유효하면 UserAccount 객체를 반환합니다.
// 실제 구현에서는 데이터베이스에서 사용자 정보를 조회하고 비밀번호를 해싱하여 비교합니다.
if (username == "testuser" && password == "password")
{
return new UserAccount { Username = "testuser", Role = "User" };
}
return null;
}
public class UserAccount // 예시 사용자 클래스
{
public string Username { get; set; }
public string Role { get; set; }
}
}
JWT 서비스 구현 예시
public class JwtTokenService
{
private readonly IConfiguration _config;
public JwtTokenService(IConfiguration configuration)
{
_config = configuration;
}
public string GenerateAuthToken(AuthenticatedUser user)
{
var securityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(
securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserId.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // JWT ID
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, user.Role)
};
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddHours(2), // 2시간 유효
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public class AuthenticatedUser // 예시 사용자 클래스
{
public int UserId { get; set; }
public string Username { get; set; }
public string Role { get; set; }
}
}
6.4. 권한 부여(Authorization) 특성 사용
[Authorize] 특성을 사용하여 컨트롤러 또는 액션 메서드에 대한 접근을 제한합니다.
[Authorize] // 인증된 사용자만 접근 가능
public class SecureDataController : ControllerBase
{
[AllowAnonymous] // 익명 사용자도 접근 허용
[HttpGet("public-info")]
public IActionResult GetPublicInformation()
{
return Ok("이것은 모든 사람이 접근할 수 있는 정보입니다.");
}
[Authorize(Roles = "Administrator")] // 'Administrator' 역할의 사용자만 접근 가능
[HttpGet("admin-dashboard")]
public IActionResult ViewAdminDashboard()
{
return Ok("관리자 대시보드 데이터.");
}
[Authorize(Policy = "ManagerAccessPolicy")] // 'ManagerAccessPolicy'라는 사용자 정의 정책 적용
[HttpGet("manager-report")]
public IActionResult GetManagerReport()
{
return Ok("매니저 전용 보고서.");
}
}
6.5. 사용자 정의 권한 부여 정책 구성 (선택 사항)
ConfigureServices 메서드에서 AddAuthorization을 통해 더 유연한 권한 부여 정책을 정의할 수 있습니다.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy("AdminAccessPolicy", policy => policy.RequireRole("Administrator", "SuperUser"));
options.AddPolicy("StandardUserPolicy", policy => policy.RequireAuthenticatedUser()); // 인증된 모든 사용자
});
services.AddControllersWithViews();
}
7. ASP.NET Core의 필터(Filters)
필터는 ASP.NET Core MVC 또는 Razor Pages의 특정 실행 단계에서 사용자 정의 코드를 주입할 수 있는 메커니즘입니다. 컨트롤러, 액션 메서드, 또는 전역적으로 적용될 수 있으며, 교차 관심사(cross-cutting concerns)를 처리하는 데 유용합니다.
ASP.NET Core는 다음과 같은 다양한 유형의 내장 필터를 제공합니다:
- 인증 필터 (Authentication Filters): 사용자의 신원을 확인하기 전에 실행됩니다. (주로 미들웨어에서 처리)
- 권한 부여 필터 (Authorization Filters): 요청이 특정 리소스에 접근할 권한이 있는지 확인합니다.
- 리소스 필터 (Resource Filters): 모델 바인딩 전후에 실행되며, 단축 회로(short-circuit) 로직이나 캐싱을 구현하는 데 사용될 수 있습니다.
- 액션 필터 (Action Filters): 액션 메서드 실행 전후에 실행됩니다. 로깅, 유효성 검사, 추가적인 데이터 조작 등에 유용합니다.
- 예외 필터 (Exception Filters): 액션이나 결과 처리 중에 발생하는 예외를 처리합니다.
- 결과 필터 (Result Filters): 액션 결과(예: ViewResult, JsonResult)가 실행되기 전후에 실행됩니다. 응답 헤더 추가, 응답 내용 수정, 캐싱 등에 사용됩니다.
사용자 정의 액션 필터 예시
public class RequestLoggingFilter : IActionFilter
{
private readonly ILogger<RequestLoggingFilter> _logger;
public RequestLoggingFilter(ILogger<RequestLoggingFilter> logger)
{
_logger = logger;
}
public void OnActionExecuting(ActionExecutingContext context)
{
// 액션 메서드 실행 전
_logger.LogInformation($"Action '{context.ActionDescriptor.DisplayName}' 실행 시작. 요청: {context.HttpContext.Request.Path}");
}
public void OnActionExecuted(ActionExecutedContext context)
{
// 액션 메서드 실행 후
if (context.Exception != null)
{
_logger.LogError(context.Exception, $"Action '{context.ActionDescriptor.DisplayName}' 실행 중 오류 발생.");
}
else
{
_logger.LogInformation($"Action '{context.ActionDescriptor.DisplayName}' 실행 완료. 상태 코드: {context.HttpContext.Response.StatusCode}");
}
}
}
컨트롤러 또는 액션에 필터 적용
[ServiceFilter(typeof(RequestLoggingFilter))] // 서비스 컨테이너에서 필터 인스턴스를 주입받음
public class DashboardController : ControllerBase
{
[HttpGet("overview")]
public IActionResult GetDashboardOverview()
{
// 대시보드 개요 데이터 반환
return Ok("대시보드 개요 정보입니다.");
}
}
전역 필터로 등록
Program.cs (또는 Startup.cs)에서 모든 컨트롤러 액션에 적용되는 전역 필터를 등록할 수 있습니다.
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews(options =>
{
options.Filters.Add(typeof(RequestLoggingFilter)); // 전역적으로 필터 적용
});
// ... 기타 서비스 등록
var app = builder.Build();
// ... 미들웨어 구성
app.Run();
}
}
필터와 미들웨어 비교
| 특성 | 미들웨어 (Middleware) | 필터 (Filters) |
|---|---|---|
| 적용 범위 | HTTP 요청 파이프라인 전체, 모든 요청에 적용 | MVC/Razor Pages 파이프라인 내, 특정 컨트롤러, 액션 또는 전역적으로 적용 |
| 실행 시점 | 요청 파이프라인의 초기 단계에서 실행, 저수준 HTTP 처리 | MVC/Razor Pages의 특정 라이프사이클(인증, 액션, 결과 등)에 맞춰 실행 |
| 사용 사례 | 정적 파일 서빙, 인증/권한 부여, 로깅, 예외 처리 등 전역적인 HTTP 관심사 | 모델 유효성 검사, 액션 전후 로직, 응답 캐싱, 예외 처리 등 MVC 관련 관심사 |
| 유연성 | 더 낮은 수준에서 HTTP 컨텍스트 직접 조작 | MVC 컨텍스트 및 모델 데이터에 접근하여 높은 수준의 처리 가능 |
일반적으로 전역적인 HTTP 요청/응답 처리는 미들웨어로, MVC 모델/컨트롤러 액션과 관련된 처리는 필터로 구현하는 것이 좋습니다.
8. ASP.NET Core 캐싱(Caching) 구현
ASP.NET Core는 애플리케이션 성능을 향상시키기 위해 다양한 캐싱 메커니즘을 제공합니다. 주요 캐싱 방법으로는 인메모리 캐시, 분산 캐시, 응답 캐시가 있습니다.
8.1. 인메모리 캐시 (In-Memory Cache)
애플리케이션이 실행되는 서버의 메모리에 데이터를 저장합니다. 단일 서버 환경에 적합합니다.
(1) 서비스 구성
ConfigureServices 메서드에 AddMemoryCache()를 추가합니다.
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache(); // 인메모리 캐시 서비스 등록
services.AddControllersWithViews();
}
(2) 캐시 사용
IMemoryCache를 주입받아 데이터를 캐싱합니다.
using Microsoft.Extensions.Caching.Memory;
public class ProductCatalogController : ControllerBase
{
private readonly IMemoryCache _memoryCache;
private readonly ILogger<ProductCatalogController> _logger;
public ProductCatalogController(IMemoryCache memoryCache, ILogger<ProductCatalogController> logger)
{
_memoryCache = memoryCache;
_logger = logger;
}
[HttpGet("products")]
public IActionResult GetAllProducts()
{
const string cacheKey = "AllProductsCache";
if (!_memoryCache.TryGetValue(cacheKey, out List<string> products))
{
// 캐시에 데이터가 없으면 새로 생성하여 캐시에 저장
products = new List<string> { "Laptop", "Mouse", "Keyboard" };
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromMinutes(2)) // 2분 동안 미사용 시 만료
.SetAbsoluteExpiration(TimeSpan.FromMinutes(10)); // 10분 후 절대 만료
_memoryCache.Set(cacheKey, products, cacheEntryOptions);
_logger.LogInformation("제품 목록을 캐시에 저장했습니다.");
}
else
{
_logger.LogInformation("제품 목록을 캐시에서 로드했습니다.");
}
return Ok(products);
}
}
8.2. 분산 캐시 (Distributed Cache)
여러 서버 간에 공유되는 외부 저장소(예: Redis, SQL Server)에 데이터를 저장합니다. 다중 서버 환경이나 클러스터 환경에 적합합니다.
(1) 서비스 구성 (Redis 예시)
Microsoft.Extensions.Caching.StackExchangeRedis NuGet 패키지를 설치한 후, ConfigureServices 메서드에 AddStackExchangeRedisCache()를 추가합니다.
public void ConfigureServices(IServiceCollection services)
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379"; // Redis 서버 연결 문자열
options.InstanceName = "MyApp_"; // 캐시 키에 사용될 인스턴스 접두사
});
services.AddControllersWithViews();
}
(2) 캐시 사용
IDistributedCache를 주입받아 데이터를 캐싱합니다.
using Microsoft.Extensions.Caching.Distributed;
using System.Text;
public class ReportingController : ControllerBase
{
private readonly IDistributedCache _distributedCache;
public ReportingController(IDistributedCache distributedCache)
{
_distributedCache = distributedCache;
}
[HttpGet("daily-summary")]
public async Task<IActionResult> GetDailySummary()
{
const string summaryCacheKey = "DailySalesSummary";
string cachedSummary = await _distributedCache.GetStringAsync(summaryCacheKey);
if (cachedSummary == null)
{
// 캐시에 데이터가 없으면 DB에서 조회 또는 계산 후 캐시에 저장
var currentSummary = $"일일 판매 요약: {DateTime.Now.ToShortDateString()} 데이터";
var options = new DistributedCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromHours(1)) // 1시간 동안 미사용 시 만료
.SetAbsoluteExpiration(DateTimeOffset.Now.AddDays(1)); // 다음 날까지 절대 만료
await _distributedCache.SetStringAsync(summaryCacheKey, currentSummary, options);
cachedSummary = currentSummary;
}
return Ok(cachedSummary);
}
}
8.3. 응답 캐시 (Response Cache)
HTTP 응답 전체를 캐싱하여, 동일한 요청에 대해 서버가 다시 처리하지 않고 캐시된 응답을 즉시 반환하게 합니다. 주로 정적인 페이지나 API 응답에 유용합니다.
(1) 서비스 구성
ConfigureServices 메서드에 AddResponseCaching()을 추가하고, Configure 메서드에 UseResponseCaching()을 추가합니다.
public void ConfigureServices(IServiceCollection services)
{
services.AddResponseCaching(); // 응답 캐시 서비스 등록
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ... 다른 미들웨어
app.UseResponseCaching(); // 응답 캐시 미들웨어 활성화 (정적 파일 미들웨어 전에 위치하는 것이 좋음)
// ...
app.UseRouting();
app.UseEndpoints(endpoints => { /* ... */ });
}
(2) 캐시 사용
컨트롤러나 액션 메서드에 [ResponseCache] 특성을 적용합니다.
public class PublicDataController : ControllerBase
{
[ResponseCache(Duration = 60, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "param" })]
[HttpGet("cached-data")]
public IActionResult GetCachedData(string param)
{
// 이 액션은 60초 동안 캐시되며, 'param' 쿼리 키 값에 따라 다른 캐시 항목을 생성할 수 있습니다.
return Ok($"캐시된 데이터입니다. 현재 시각: {DateTime.Now}, 매개변수: {param}");
}
}
9. ASP.NET Core 성능 최적화 전략
ASP.NET Core 애플리케이션의 성능을 개선하기 위한 다양한 접근 방식이 있습니다. 주요 최적화 기법들은 다음과 같습니다.
9.1. 비동기 프로그래밍 활용
async 및 await 키워드를 사용하여 데이터베이스 질의, 파일 I/O, 네트워크 요청과 같은 I/O 바운드 작업을 처리합니다. 이는 스레드를 블로킹하지 않아 애플리케이션의 동시 처리량을 크게 향상시킵니다.
public class DataProcessingController : ControllerBase
{
private readonly IDataRetrievalService _dataRetrievalService;
private readonly IReportingService _reportingService;
public DataProcessingController(IDataRetrievalService dataRetrievalService, IReportingService reportingService)
{
_dataRetrievalService = dataRetrievalService;
_reportingService = reportingService;
}
[HttpGet("aggregated-report")]
public async Task<IActionResult> GetAggregatedReport()
{
// 여러 비동기 작업을 병렬로 실행
var productTask = _dataRetrievalService.GetProductListAsync();
var salesTask = _reportingService.GenerateSalesSummaryAsync();
await Task.WhenAll(productTask, salesTask); // 모든 작업이 완료될 때까지 기다림
return Ok(new
{
Products = productTask.Result,
SalesSummary = salesTask.Result
});
}
[HttpPost("submit-feedback")]
public async Task<IActionResult> SubmitFeedback([FromBody] FeedbackModel model)
{
// async void는 피하고, Task를 반환하여 호출자가 비동기 작업의 완료를 기다릴 수 있도록 함
await _dataRetrievalService.SaveFeedbackAsync(model);
return Ok("피드백이 성공적으로 제출되었습니다.");
}
}
9.2. 데이터베이스 최적화
- 인덱스 사용: 자주 검색되거나 조인되는 컬럼에 인덱스를 생성하여 쿼리 성능을 향상시킵니다.
CREATE INDEX IX_Customers_Email ON Customers (Email); - 벌크(Batch) 작업: 여러 개의 데이터를 한 번의 요청으로 삽입, 업데이트, 삭제하여 데이터베이스 왕복 횟수를 줄입니다.
_dbContext.Orders.AddRange(newOrderList); // 여러 엔티티를 한 번에 추가 await _dbContext.SaveChangesAsync(); - 페이징 처리: 대량의 데이터를 한 번에 가져오는 대신, 필요한 만큼만 분할하여 조회함으로써 메모리 사용량과 응답 시간을 최적화합니다.
public async Task<(List<Product> Items, int TotalCount)> GetPagedProductsAsync(int pageNumber, int pageSize) { var query = _dbContext.Products.AsNoTracking(); // 읽기 전용 쿼리에 AsNoTracking 사용 var total = await query.CountAsync(); var items = await query .OrderBy(p => p.Name) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (items, total); } - LINQ 쿼리 최적화: 필요한 필드만 선택(Projection)하거나, N+1 쿼리 문제를 방지하기 위해 Eager Loading (
Include)을 적절히 사용합니다.public async Task<List<ProductSummary>> GetProductSummariesAsync() { return await _dbContext.Products .AsNoTracking() .Select(p => new ProductSummary // 필요한 필드만 선택하여 전송 부하 감소 { ProductId = p.Id, ProductName = p.Name, Price = p.Price }) .ToListAsync(); } public class ProductSummary // Projection을 위한 DTO { public int ProductId { get; set; } public string ProductName { get; set; } public decimal Price { get; set; } }
9.3. 메모리 최적화
가비지 컬렉션(GC) 부담을 줄이고 메모리 사용 효율을 높입니다.
- 값 형식(Value Type) 활용: 작은 데이터 구조에
struct를 사용하여 힙 메모리 할당을 줄입니다.public struct Coordinate { public int X { get; set; } public int Y { get; set; } } - 박싱(Boxing) 및 언박싱(Unboxing) 회피: 일반 컬렉션(예:
ArrayList) 대신 제네릭 컬렉션(예:List<T>)을 사용하여 불필요한 형식 변환을 방지합니다.var numbers = new List<int>(); // 박싱 회피 numbers.Add(100); - StringBuilder 사용: 반복적인 문자열 연결 시
StringBuilder를 사용하여 많은 임시 문자열 객체 생성을 방지합니다.var builder = new StringBuilder(); for (int i = 0; i < 1000; i++) { builder.Append("item " + i + ";"); } var result = builder.ToString(); - 대규모 객체 재사용:
ArrayPool<T>또는 커스텀 객체 풀을 사용하여 큰 배열이나 객체의 재사용을 통해 잦은 할당 및 해제를 줄입니다.var sharedPool = ArrayPool<byte>.Shared; byte[] buffer = sharedPool.Rent(1024); // 풀에서 배열 빌려오기 try { // buffer 사용 } finally { sharedPool.Return(buffer); // 풀로 배열 반환 } - 데이터 구조 최적화:
Span<T>및Memory<T>를 사용하여 연속된 메모리 블록을 효율적으로 조작하고 메모리 복사를 줄입니다.byte[] rawData = new byte[256]; Span<byte> dataSpan = rawData; // Span은 값 타입으로, 힙 할당 없음 dataSpan.Fill(0xFF); // 메모리 블록 직접 조작 - 고성능 컬렉션: 동시성 환경에서
ConcurrentQueue<T>,ConcurrentDictionary<TKey, TValue>와 같은System.Collections.Concurrent네임스페이스의 컬렉션을 사용하여 잠금 오버헤드를 줄입니다.var concurrentQueue = new ConcurrentQueue<int>(); concurrentQueue.Enqueue(5); if (concurrentQueue.TryDequeue(out var item)) { // item 처리 } - 메모리 누수 방지:
IDisposable패턴을 올바르게 구현하고, 이벤트 구독 해제를 통해 불필요한 객체 참조 유지를 방지합니다.using (var connection = new SqlConnection("...")) { // connection 사용 } // connection 객체가 자동으로 Dispose 됨 - ValueTask 사용: 비동기 메서드의 결과가 즉시 사용 가능할 때
ValueTask를 사용하여Task객체 할당을 피하고 성능을 향상시킵니다.public async ValueTask<int> GetCachedValueAsync() { if (_cacheAvailable) { return new ValueTask<int>(_cachedResult); // 동기적으로 결과 반환 } return new ValueTask<int>(await FetchValueFromDatabaseAsync()); // 비동기적으로 결과 반환 } - GC 설정 조정: `.csproj` 파일에서
ServerGarbageCollection및ConcurrentGarbageCollection설정을 조정하여 서버 환경에 맞는 GC 동작을 구성합니다.<PropertyGroup> <ServerGarbageCollection>true</ServerGarbageCollection> <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection> </PropertyGroup>
9.4. 캐싱 최적화
이전 섹션에서 설명한 인메모리 캐시 (IMemoryCache), 분산 캐시 (IDistributedCache), 응답 캐시 ([ResponseCache])를 적절히 활용하여 자주 접근하는 데이터나 응답을 저장하고 재사용합니다.
9.5. 환경 및 서버 구성 최적화
- 응답 압축 활성화: Gzip 또는 Brotli 압축을 사용하여 HTTP 응답 크기를 줄이고 전송 속도를 높입니다.
public void ConfigureServices(IServiceCollection services) { services.AddResponseCompression(options => { options.EnableForHttps = true; options.Providers.Add<GzipCompressionProvider>(); options.Providers.Add<BrotliCompressionProvider>(); }); // ... } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseResponseCompression(); // 미들웨어 추가 // ... } - Kestrel 서버 설정: Kestrel 웹 서버의 동시 연결 제한, 버퍼 크기 등을 조정하여 성능을 최적화합니다.
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseKestrel(serverOptions => { serverOptions.Limits.MaxConcurrentConnections = 5000; // 최대 동시 연결 수 serverOptions.Limits.MaxConcurrentUpgradedConnections = 1000; // WebSocket 연결 수 serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2); // Keep-alive 시간 }); webBuilder.UseStartup<Startup>(); }); - 생산 환경 설정:
appsettings.Production.json과 같은 환경별 설정 파일을 사용하여 생산 환경에 맞는 데이터베이스 연결 문자열, 로깅 레벨 등을 설정합니다. 개발 환경에서 불필요한 로깅이나 디버그 기능은 생산 환경에서 비활성화하여 오버헤드를 줄입니다.{ "ConnectionStrings": { "DefaultConnection": "Server=prod_db_server;Database=MyProdDb;User Id=prodUser;Password=prodPassword;" }, "Logging": { "LogLevel": { "Default": "Warning", // 생산 환경에서는 기본 로깅 레벨을 높여 불필요한 로그를 줄임 "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } } }