<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>ASP.NET Core Archives - 어제와 내일의 나 그 사이의 이야기</title>
	<atom:link href="https://lycos7560.com/tag/asp-net-core/feed/" rel="self" type="application/rss+xml" />
	<link></link>
	<description>생각의 흐름을 타고 다니며 만드는 블로그</description>
	<lastBuildDate>Fri, 08 Aug 2025 07:45:28 +0000</lastBuildDate>
	<language>ko-KR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://lycos7560.com/wp-content/uploads/2022/11/cropped-cropped-cropped-log-1-150x150-1-80x80.png</url>
	<title>ASP.NET Core Archives - 어제와 내일의 나 그 사이의 이야기</title>
	<link></link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>ASP.NET Core Identity를 활용한 구글 로그인(OAuth) 전체 흐름 분석</title>
		<link>https://lycos7560.com/c/asp-net/asp-net-core-identity%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%9c-%ea%b5%ac%ea%b8%80-%eb%a1%9c%ea%b7%b8%ec%9d%b8oauth-%ec%a0%84%ec%b2%b4-%ed%9d%90%eb%a6%84-%eb%b6%84%ec%84%9d/40245/</link>
					<comments>https://lycos7560.com/c/asp-net/asp-net-core-identity%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%9c-%ea%b5%ac%ea%b8%80-%eb%a1%9c%ea%b7%b8%ec%9d%b8oauth-%ec%a0%84%ec%b2%b4-%ed%9d%90%eb%a6%84-%eb%b6%84%ec%84%9d/40245/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Fri, 08 Aug 2025 07:45:23 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[2FA]]></category>
		<category><![CDATA[Account]]></category>
		<category><![CDATA[AccountManagement]]></category>
		<category><![CDATA[action]]></category>
		<category><![CDATA[API]]></category>
		<category><![CDATA[appsettings]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[Authentication]]></category>
		<category><![CDATA[Authorization]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Backend]]></category>
		<category><![CDATA[Blazor]]></category>
		<category><![CDATA[Callback]]></category>
		<category><![CDATA[CD]]></category>
		<category><![CDATA[Challenge]]></category>
		<category><![CDATA[CI]]></category>
		<category><![CDATA[CICD]]></category>
		<category><![CDATA[Claim]]></category>
		<category><![CDATA[ClaimsPrincipal]]></category>
		<category><![CDATA[ClientSide]]></category>
		<category><![CDATA[CloudComputing]]></category>
		<category><![CDATA[CloudNative]]></category>
		<category><![CDATA[Configuration]]></category>
		<category><![CDATA[Controller]]></category>
		<category><![CDATA[Cookie]]></category>
		<category><![CDATA[Core]]></category>
		<category><![CDATA[Cryptography]]></category>
		<category><![CDATA[CSharp]]></category>
		<category><![CDATA[DataBase]]></category>
		<category><![CDATA[DataTransferObject]]></category>
		<category><![CDATA[DbContext]]></category>
		<category><![CDATA[Debugging]]></category>
		<category><![CDATA[Deployment]]></category>
		<category><![CDATA[Development]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[DTO]]></category>
		<category><![CDATA[EmailConfirmation]]></category>
		<category><![CDATA[EntityFramework]]></category>
		<category><![CDATA[ErrorHandling]]></category>
		<category><![CDATA[Exception]]></category>
		<category><![CDATA[ExternalLogin]]></category>
		<category><![CDATA[ExternalProvider]]></category>
		<category><![CDATA[Frontend]]></category>
		<category><![CDATA[FullStack]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[GitHub]]></category>
		<category><![CDATA[GitLab]]></category>
		<category><![CDATA[GoogleAuth]]></category>
		<category><![CDATA[GoogleCloud]]></category>
		<category><![CDATA[GoogleLogin]]></category>
		<category><![CDATA[HTTP302]]></category>
		<category><![CDATA[HttpGet]]></category>
		<category><![CDATA[HttpPost]]></category>
		<category><![CDATA[HttpRequest]]></category>
		<category><![CDATA[HttpResponse]]></category>
		<category><![CDATA[IDE]]></category>
		<category><![CDATA[Identity]]></category>
		<category><![CDATA[IIS]]></category>
		<category><![CDATA[JSONWebToken]]></category>
		<category><![CDATA[JWT]]></category>
		<category><![CDATA[Kestrel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Log]]></category>
		<category><![CDATA[Logging]]></category>
		<category><![CDATA[Login]]></category>
		<category><![CDATA[Logout]]></category>
		<category><![CDATA[Microservices]]></category>
		<category><![CDATA[MicrosoftIdentity]]></category>
		<category><![CDATA[Middleware]]></category>
		<category><![CDATA[NoSQL]]></category>
		<category><![CDATA[OAuth]]></category>
		<category><![CDATA[OAuth2.0]]></category>
		<category><![CDATA[OpenID]]></category>
		<category><![CDATA[OpenIDConnect]]></category>
		<category><![CDATA[ORM]]></category>
		<category><![CDATA[password]]></category>
		<category><![CDATA[Passwordless]]></category>
		<category><![CDATA[path]]></category>
		<category><![CDATA[pipeline]]></category>
		<category><![CDATA[Production]]></category>
		<category><![CDATA[Profile]]></category>
		<category><![CDATA[QueryString]]></category>
		<category><![CDATA[Redirect]]></category>
		<category><![CDATA[RedirectResponse]]></category>
		<category><![CDATA[Register]]></category>
		<category><![CDATA[ResetPassword]]></category>
		<category><![CDATA[RESTfulAPI]]></category>
		<category><![CDATA[Route]]></category>
		<category><![CDATA[Routing]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[SecurityToken]]></category>
		<category><![CDATA[ServerSide]]></category>
		<category><![CDATA[session]]></category>
		<category><![CDATA[SignInManager]]></category>
		<category><![CDATA[SignUp]]></category>
		<category><![CDATA[SinglePageApplication]]></category>
		<category><![CDATA[SocialLogin]]></category>
		<category><![CDATA[SPA]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[stage]]></category>
		<category><![CDATA[Startup]]></category>
		<category><![CDATA[StateManagement]]></category>
		<category><![CDATA[study]]></category>
		<category><![CDATA[Token]]></category>
		<category><![CDATA[TryCatch]]></category>
		<category><![CDATA[TwoFactor]]></category>
		<category><![CDATA[URI]]></category>
		<category><![CDATA[url]]></category>
		<category><![CDATA[UserDB]]></category>
		<category><![CDATA[UserManagement]]></category>
		<category><![CDATA[UserManager]]></category>
		<category><![CDATA[UserProfile]]></category>
		<category><![CDATA[UserStore]]></category>
		<category><![CDATA[VisualStudio]]></category>
		<category><![CDATA[VSCode]]></category>
		<category><![CDATA[WebAPI]]></category>
		<category><![CDATA[WebApp]]></category>
		<category><![CDATA[WebApplication]]></category>
		<category><![CDATA[WebDevelopment]]></category>
		<category><![CDATA[WebSecurity]]></category>
		<category><![CDATA[WebServer]]></category>
		<category><![CDATA[공부]]></category>
		<category><![CDATA[기초]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40245</guid>

					<description><![CDATA[<p>🔥 ASP.NET Core Identity를 활용한 구글 로그인(OAuth) 전체 흐름 분석 1️⃣ 사용자가 &#8220;Google로 로그인&#8221; 버튼을 클릭 Navigation.NavigateTo(..., forceLoad: true) Blazor의 내부 라우팅이 아닌, 브라우저가 직접 해당 URL(api/auth/Challenge/Google...)로 GET 요청보낸다. 2️⃣ 백엔드의 인증 시작 (AuthController &#8211; Challenge) 3️⃣ 외부 공급자 인증 (Google) 사용자는 Google 로그인 페이지로 이동 4️⃣ 백엔드의 콜백 처리 및 로그인/회원가입 (AuthController &#8211; Callback) [&#8230;]</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-identity%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%9c-%ea%b5%ac%ea%b8%80-%eb%a1%9c%ea%b7%b8%ec%9d%b8oauth-%ec%a0%84%ec%b2%b4-%ed%9d%90%eb%a6%84-%eb%b6%84%ec%84%9d/40245/">ASP.NET Core Identity를 활용한 구글 로그인(OAuth) 전체 흐름 분석</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core Identity를 활용한 구글 로그인(OAuth) 전체 흐름 분석</h2>


				<div class="wp-block-uagb-table-of-contents uagb-toc__align-left uagb-toc__columns-1  uagb-block-cbc30617      "
					data-scroll= "1"
					data-offset= "30"
					style=""
				>
				<div class="uagb-toc__wrap">
						<div class="uagb-toc__title">
							목차						</div>
																						<div class="uagb-toc__list-wrap ">
						<ol class="uagb-toc__list"><li class="uagb-toc__list"><a href="#aspnet-core-identity를-활용한-구글-로그인oauth-전체-흐름-분석" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core Identity를 활용한 구글 로그인(OAuth) 전체 흐름 분석</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-사용자가-google로-로그인-버튼을-클릭" class="uagb-toc-link__trigger">1&#x20e3; 사용자가 &quot;Google로 로그인&quot; 버튼을 클릭</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-백엔드의-인증-시작-authcontroller-challenge" class="uagb-toc-link__trigger">2&#x20e3; 백엔드의 인증 시작 (AuthController &#8211; Challenge)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-외부-공급자-인증-google" class="uagb-toc-link__trigger">3&#x20e3; 외부 공급자 인증 (Google)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#4-백엔드의-콜백-처리-및-로그인회원가입-authcontroller-callback" class="uagb-toc-link__trigger">4&#x20e3; 백엔드의 콜백 처리 및 로그인/회원가입 (AuthController &#8211; Callback)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#5-완료" class="uagb-toc-link__trigger">5&#x20e3; 완료</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#6-authcontrollercs" class="uagb-toc-link__trigger">6&#x20e3; AuthController.cs</a></ul></ol>					</div>
									</div>
				</div>
			


<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" width="1159" height="1920" src="https://lycos7560.com/wp-content/uploads/2025/08/Google-Auth-Mermaid.jpg" alt="" class="wp-image-40248" srcset="https://lycos7560.com/wp-content/uploads/2025/08/Google-Auth-Mermaid.jpg 1159w, https://lycos7560.com/wp-content/uploads/2025/08/Google-Auth-Mermaid-181x300.jpg 181w, https://lycos7560.com/wp-content/uploads/2025/08/Google-Auth-Mermaid-768x1272.jpg 768w, https://lycos7560.com/wp-content/uploads/2025/08/Google-Auth-Mermaid-927x1536.jpg 927w" sizes="(max-width: 1159px) 100vw, 1159px" /></figure>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">1&#x20e3; 사용자가 &#8220;Google로 로그인&#8221; 버튼을 클릭</h3>



<p><code>Navigation.NavigateTo(..., forceLoad: true)</code></p>



<p>Blazor의 내부 라우팅이 아닌, 브라우저가 직접 해당 URL(<code>api/auth/Challenge/Google...</code>)로 <strong>GET 요청</strong>보낸다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">
private void LoginWithGoogle()
{
    var returnUrl = "/"; // 로그인 성공 후 돌아올 주소
    var googleLoginUrl = $"/api/auth/Challenge/Google?returnUrl={Uri.EscapeDataString(returnUrl)}";

    // 이 주소로 브라우저가 페이지를 새로고침하며 이동
    Navigation.NavigateTo(googleLoginUrl, forceLoad: true);
}</pre>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2&#x20e3; 백엔드의 인증 시작 (AuthController &#8211; Challenge)</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// AuthController.cs
[HttpGet("Challenge/{provider}")]
public IActionResult Challenge(string provider, string? returnUrl = null)
{
    // ...
    var redirectUrl = Url.Action("Callback", "Auth", ...); // 1. Google이 인증 후 돌아올 우리 API 주소(Callback)를 지정
    var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); // 2. 인증 요청에 필요한 속성 구성

    return Challenge(properties, provider); // 3. Google 로그인 페이지로 리디렉션하는 응답 생성
}</pre>



<ul class="wp-block-list">
<li><code>AuthController.cs</code>의 <code>Challenge</code> 메서드가 실행됩니다. <br><code>provider</code> 매개변수에는 &#8220;Google&#8221;이 전달</li>



<li><code>_signInManager</code>는 ASP.NET Core Identity의 핵심 기능으로, 외부 로그인 과정을 도와줌</li>



<li><code>return Challenge(properties, provider);</code>는 브라우저에게 <strong>HTTP 302 Redirect</strong> 응답을 보냄<br>이 응답 헤더에는 사용자가 이동해야 할 <strong>Google의 로그인 페이지 주소</strong>가 포함</li>
</ul>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">3&#x20e3; 외부 공급자 인증 (Google)</h3>



<p><strong>사용자는 Google 로그인 페이지로 이동</strong></p>



<ul class="wp-block-list">
<li>사용자는 자신의 Google 계정으로 로그인하고, 우리 애플리케이션이 요청하는 정보(이메일, 프로필 등)에 대한 접근 권한을 허용</li>



<li>인증이 성공적으로 완료되면, Google은 2단계에서 백엔드가 지정했던 <strong>콜백 URL</strong>(<code>api/Auth/Callback</code>)로 사용자를 다시 리디렉션<br>이때 인증 코드 또는 토큰과 함께 사용자를 보냄</li>
</ul>



<ol start="1" class="wp-block-list"></ol>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">4&#x20e3; 백엔드의 콜백 처리 및 로그인/회원가입 (AuthController &#8211; Callback)</h3>



<p><strong>Google이 사용자를 우리 백엔드의 <code>Callback</code> 엔드포인트로 돌려보냄</strong></p>



<p><code>AuthController.cs</code>의 <code>Callback</code> (라우팅 경로: <code>externalLogin</code>) 메서드가 실행</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// AuthController.cs
[HttpGet("externalLogin")]
public async Task&lt;IActionResult> Callback(...)
{
    // 1. Google이 보내준 정보로 외부 로그인 정보를 가져옴
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null) { /* 에러 처리 */ }

    // 2. 이 외부 정보(공급자, 키)로 우리 DB에 연결된 사용자가 있는지 확인하고 로그인 시도
    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, ...);

    if (result.Succeeded)
    {
        // 3a. 성공! 이미 가입하고 연동된 사용자. 로그인 처리 후 returnUrl로 리디렉션
        return LocalRedirect(returnUrl);
    }
    else
    {
        // 3b. 실패. 신규 사용자이거나, 이메일은 같지만 연동되지 않은 사용자.
        return await HandleUserCreationOrLinking(info, returnUrl);
    }
}</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p><code>HandleUserCreationOrLinking</code> 메서드는 다음 두 가지 시나리오를 처리</p>



<ul class="wp-block-list">
<li><strong>시나리오 A: 이메일은 존재하지만 소셜 연동이 안 된 경우 (<code>LinkExistingUser</code>)</strong>
<ul class="wp-block-list">
<li>Google에서 받은 이메일로 우리 DB에서 사용자를 찾습니다. (<code>_userManager.FindByEmailAsync</code>)</li>



<li>기존 사용자가 있다면, 해당 계정에 이 Google 로그인 정보를 추가로 연결합니다. (<code>_userManager.AddLoginAsync</code>)</li>



<li>연결 후, 사용자를 로그인시키고 <code>returnUrl</code>로 리디렉션합니다.</li>
</ul>
</li>



<li><strong>시나리오 B: 완전히 새로운 사용자인 경우 (<code>CreateNewUserAsync</code>)</strong>
<ul class="wp-block-list">
<li>Google에서 받은 이메일과 이름으로 새로운 <code>ApplicationUser</code> 객체를 만듭니다. 이때 <code>EmailConfirmed</code>는 <code>true</code>로 설정합니다. (소셜 로그인은 이미 이메일이 인증된 것으로 간주)</li>



<li><code>_userManager.CreateAsync(user)</code>로 DB에 새 사용자를 저장합니다.</li>



<li>새로 생성된 사용자 계정에 Google 로그인 정보를 연결합니다. (<code>_userManager.AddLoginAsync</code>)</li>



<li>새 사용자를 로그인시키고 <code>returnUrl</code>로 리디렉션합니다.</li>
</ul>
</li>
</ul>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">5&#x20e3; 완료</h3>



<p>로그인 또는 회원가입 및 연동이 모두 성공적으로 끝나면, 백엔드는 최종적으로 <code>LocalRedirect(returnUrl)</code>을 통해 사용자를 맨 처음 <code>Login.razor</code>에서 지정했던 주소(<code>/</code>)로 리디렉션합니다. </p>



<p>이제 사용자는 로그인된 상태로 메인 페이지를 보게 됩니다.</p>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">6&#x20e3; AuthController.cs</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.IdentityModel.Tokens;
using Neco.BaseTemplate.Shared.Data;
using Neco.BaseTemplete.Data;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;

namespace Neco.BaseTemplete.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class AuthController : ControllerBase
    {
        private readonly UserManager&lt;ApplicationUser> _userManager;
        private readonly SignInManager&lt;ApplicationUser> _signInManager;
        private readonly IUserStore&lt;ApplicationUser> _userStore;
        private readonly IEmailSender&lt;ApplicationUser> _emailSender;
        private readonly ILogger&lt;AuthController> _logger;

        public AuthController(
            UserManager&lt;ApplicationUser> userManager,
            SignInManager&lt;ApplicationUser> signInManager,
            IUserStore&lt;ApplicationUser> userStore,
            IEmailSender&lt;ApplicationUser> emailSender,
            ILogger&lt;AuthController> logger)
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _userStore = userStore;
            _emailSender = emailSender;
            _logger = logger;
        }

        #region Basic Authentication

        /// &lt;summary>
        /// Handles user login.
        /// &lt;/summary>
        /// &lt;param name="model">Login request DTO (email, password, RememberMe option)&lt;/param>
        /// &lt;returns>The login result&lt;/returns>
        [HttpPost("login")]
        public async Task&lt;IActionResult> Login([FromBody] LoginUserRequestDTO model)
        {
            try
            {
                var result = await _signInManager.PasswordSignInAsync(
                    model.Email,
                    model.Password,
                    model.RememberMe,
                    lockoutOnFailure: true);

                if (result.Succeeded)
                {
                    _logger.LogInformation("User logged in: {Email}", model.Email);
                    return Ok(new { Success = true });
                }

                if (result.RequiresTwoFactor)
                {
                    return Ok(new { RequiresTwoFactor = true });
                }

                if (result.IsLockedOut)
                {
                    _logger.LogWarning("User account locked out: {Email}", model.Email);
                    return BadRequest("Account locked out");
                }

                return BadRequest("Invalid login attempt");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Login error for {Email}", model.Email);
                return StatusCode(500, "An error occurred");
            }
        }

        /// &lt;summary>
        /// Handles new user registration.
        /// &lt;/summary>
        /// &lt;param name="model">Registration request DTO&lt;/param>
        /// &lt;returns>The registration result&lt;/returns>
        [HttpPost("register")]
        public async Task&lt;IActionResult> Register([FromBody] CreateUserRequestDTO model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(new RegisterUserResponseDTO
                {
                    Status = RegisterUserResponseDTO.ResponseStatus.Fail_RequestInvalid,
                    Errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)
                });
            }

            var user = CreateUser();
            await _userStore.SetUserNameAsync(user, model.Email, CancellationToken.None);
            var emailStore = GetEmailStore();
            await emailStore.SetEmailAsync(user, model.Email, CancellationToken.None);

            var result = await _userManager.CreateAsync(user, model.Password);

            if (!result.Succeeded)
            {
                _logger.LogInformation("User registration failed: {Email}", model.Email);

                // Handle duplicate email error
                if (result.Errors.Any(e => e.Code == "DuplicateUserName"))
                {
                    return BadRequest(new RegisterUserResponseDTO
                    {
                        Status = RegisterUserResponseDTO.ResponseStatus.Fail_DuplicateEmail,
                    });
                }

                // Handle general Identity errors
                return BadRequest(new RegisterUserResponseDTO
                {
                    Status = RegisterUserResponseDTO.ResponseStatus.Fail_General,
                    Errors = result.Errors.Select(e => e.Description)
                });
            }

            _logger.LogInformation("User created a new account with a password.");

            // Send email confirmation link
            try
            {
                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

                // NOTE: For security, it's recommended to configure a fixed base URL
                // from your application's settings rather than relying on the request host.
                var baseUrl = model.BaseUrl ?? $"{Request.Scheme}://{Request.Host.Value}";
                var callbackUrl = $"{baseUrl}/Account/ConfirmEmail?userId={userId}&amp;code={code}";

                await _emailSender.SendConfirmationLinkAsync(user, model.Email, HtmlEncoder.Default.Encode(callbackUrl));
            }
            catch (Exception)
            {
                // Account created successfully but email sending failed.
                return Ok(new RegisterUserResponseDTO
                {
                    Status = RegisterUserResponseDTO.ResponseStatus.Success_EmailWarning,
                    Message = "The account was created, but the verification email sending failed."
                });
            }

            return Ok(new RegisterUserResponseDTO
            {
                Status = RegisterUserResponseDTO.ResponseStatus.Success_NeedConfirmation
            });
        }

        /// &lt;summary>
        /// Resends the email confirmation link.
        /// &lt;/summary>
        /// &lt;param name="model">Email confirmation request DTO&lt;/param>
        /// &lt;returns>The result of the process&lt;/returns>
        [HttpPost("emailConfirm")]
        public async Task&lt;IActionResult> ResendEmailConfirm([FromBody] RequestEmailConfirmDTO model)
        {
            if (!ModelState.IsValid)
            {
                // Always return a success response for security (to avoid user enumeration).
                return Ok(new ResponseEmailConfirmDTO
                {
                    Status = ResponseEmailConfirmDTO.ResponseStatus.Success,
                    Message = "The confirmation email has been sent successfully."
                });
            }

            try
            {
                var user = await _userManager.FindByEmailAsync(model.Email);

                // Even if the user doesn't exist or is already confirmed, return a success response for security.
                if (user == null || user.EmailConfirmed)
                {
                    return Ok(new ResponseEmailConfirmDTO
                    {
                        Status = ResponseEmailConfirmDTO.ResponseStatus.Success,
                        Message = "The confirmation email has been sent successfully."
                    });
                }

                // Generate and send the email confirmation link
                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

                // NOTE: It is recommended to use a pre-configured base URL.
                var baseUrl = model.BaseUrl ?? $"{Request.Scheme}://{Request.Host.Value}";
                var callbackUrl = $"{baseUrl}/Account/ConfirmEmail?userId={userId}&amp;code={code}";

                await _emailSender.SendConfirmationLinkAsync(user, model.Email, HtmlEncoder.Default.Encode(callbackUrl));

                return Ok(new ResponseEmailConfirmDTO
                {
                    Status = ResponseEmailConfirmDTO.ResponseStatus.Success,
                    Message = "The confirmation email has been sent successfully."
                });
            }
            catch (Exception)
            {
                // Mail server or other exception occurred.
                return BadRequest(new ResponseEmailConfirmDTO
                {
                    Status = ResponseEmailConfirmDTO.ResponseStatus.Fail,
                    Message = $"An error occurred while processing your request. (Server Error)"
                });
            }
        }

        /// &lt;summary>
        /// Handles password reset requests.
        /// &lt;/summary>
        /// &lt;param name="model">Password reset request DTO&lt;/param>
        /// &lt;returns>The result of the process&lt;/returns>
        [HttpPost("resetPassword")]
        public async Task&lt;IActionResult> ResetPassword([FromBody] ResetPasswordRequestDTO model)
        {
            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null)
            {
                return BadRequest("User not found");
            }

            var resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
            var callbackUrl = $"{model.BaseUrl}/Account/ResetPassword?email={model.Email}&amp;token={resetToken}";

            await _emailSender.SendPasswordResetLinkAsync(user, model.Email, callbackUrl);

            return Ok("Password reset link sent");
        }

        /// &lt;summary>
        /// Handles user logout.
        /// &lt;/summary>
        /// &lt;returns>The logout result&lt;/returns>
        [HttpPost("logout")]
        public async Task&lt;IActionResult> Logout()
        {
            await _signInManager.SignOutAsync();
            return Ok("Logged out successfully");
        }

        /// &lt;summary>
        /// Handles the generation of a JWT token.
        /// &lt;/summary>
        /// &lt;param name="model">Login request DTO (email, password)&lt;/param>
        /// &lt;returns>Token information&lt;/returns>
        [HttpPost("token")]
        public async Task&lt;IActionResult> GenerateToken([FromBody] LoginUserRequestDTO model)
        {
            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null || !await _userManager.CheckPasswordAsync(user, model.Password))
            {
                return Unauthorized("Invalid credentials");
            }

            var claims = new[]
            {
                new Claim(ClaimTypes.Name, user.UserName),
                new Claim(ClaimTypes.Email, user.Email)
            };

            // WARNING: Do not hard-code the key in production.
            // Move this to a secure configuration file (e.g., appsettings.json)
            // and use a strong, randomly generated key.
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("A_Very_Strong_And_Secret_Key_For_JWT_Auth"));
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            var token = new JwtSecurityToken(
                issuer: "yourdomain.com",
                audience: "yourdomain.com",
                claims: claims,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: creds);

            return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
        }

        #endregion

        #region External Authentication (OAuth)

        /// &lt;summary>
        /// Initiates a challenge request to an external authentication provider (e.g., Google).
        /// &lt;/summary>
        /// &lt;param name="provider">The name of the authentication provider (e.g., "Google")&lt;/param>
        /// &lt;param name="returnUrl">The URL to redirect to after successful authentication&lt;/param>
        /// &lt;returns>A challenge result to the external provider&lt;/returns>
        [HttpGet("Challenge/{provider}")]
        public IActionResult Challenge(string provider, string? returnUrl = null)
        {
            _logger.LogInformation("Starting {Provider} login", provider);

            if (string.IsNullOrWhiteSpace(provider))
            {
                return BadRequest(new { error = "Provider not specified." });
            }

            // Normalize and validate returnUrl
            returnUrl = NormalizeReturnUrl(returnUrl);

            // Set the callback URL
            var redirectUrl = Url.Action("Callback", "Auth", new { returnUrl }, Request.Scheme);

            // Configure authentication properties
            var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);

            return Challenge(properties, provider);
        }

        /// &lt;summary>
        /// Handles the callback from an external authentication provider.
        /// &lt;/summary>
        /// &lt;param name="returnUrl">The URL to redirect to after successful authentication&lt;/param>
        /// &lt;param name="remoteError">Error message from the external provider&lt;/param>
        /// &lt;returns>An appropriate response based on the authentication result&lt;/returns>
        [HttpGet("externalLogin")]
        public async Task&lt;IActionResult> Callback(string? returnUrl = null, string? remoteError = null)
        {
            try
            {
                returnUrl = NormalizeReturnUrl(returnUrl);

                if (!string.IsNullOrEmpty(remoteError))
                {
                    _logger.LogError("External provider error: {RemoteError}", remoteError);
                    return Redirect($"/login?error={Uri.EscapeDataString(remoteError)}");
                }

                var info = await _signInManager.GetExternalLoginInfoAsync();
                if (info == null)
                {
                    _logger.LogError("Could not retrieve external login info");
                    return Redirect("/login?error=external_login_failed");
                }

                // Attempt to sign in an existing user
                var result = await _signInManager.ExternalLoginSignInAsync(
                    info.LoginProvider,
                    info.ProviderKey,
                    isPersistent: false,
                    bypassTwoFactor: true);

                if (result.Succeeded)
                {
                    var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
                    _logger.LogInformation("User {Email} successfully logged in", user?.Email);
                    return LocalRedirect(returnUrl);
                }

                if (result.IsLockedOut)
                {
                    _logger.LogWarning("Attempt to log in with a locked-out account");
                    return Redirect("/login?error=locked_out");
                }

                // New user or linking an existing user
                return await HandleUserCreationOrLinking(info, returnUrl);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error occurred while processing external login");
                return Redirect("/login?error=server_error");
            }
        }

        #endregion

        #region Helper Methods

        /// &lt;summary>
        /// Normalizes and validates the returnUrl.
        /// &lt;/summary>
        private string NormalizeReturnUrl(string? returnUrl)
        {
            if (string.IsNullOrWhiteSpace(returnUrl))
                return "/";

            if (Uri.TryCreate(returnUrl, UriKind.Absolute, out var uri))
                returnUrl = uri.PathAndQuery;

            return Url.IsLocalUrl(returnUrl) ? returnUrl : "/";
        }

        /// &lt;summary>
        /// Handles new user creation or linking an existing user to an external login.
        /// &lt;/summary>
        private async Task&lt;IActionResult> HandleUserCreationOrLinking(ExternalLoginInfo info, string returnUrl)
        {
            var email = info.Principal.FindFirstValue(ClaimTypes.Email);
            if (string.IsNullOrEmpty(email))
            {
                _logger.LogError("Missing email information from external provider {Provider}", info.LoginProvider);
                return Redirect("/login?error=email_required");
            }

            var existingUser = await _userManager.FindByEmailAsync(email);
            return existingUser != null
                ? await LinkExistingUser(existingUser, info, returnUrl)
                : await CreateNewUserAsync(info, returnUrl);
        }

        /// &lt;summary>
        /// Links external login information to an existing user.
        /// &lt;/summary>
        private async Task&lt;IActionResult> LinkExistingUser(ApplicationUser user, ExternalLoginInfo info, string returnUrl)
        {
            var addLoginResult = await _userManager.AddLoginAsync(user, info);
            if (addLoginResult.Succeeded)
            {
                _logger.LogInformation("Successfully linked {Provider} login to existing user {Email}", info.LoginProvider, user.Email);
                await _signInManager.SignInAsync(user, isPersistent: false, info.LoginProvider);
                return LocalRedirect(returnUrl);
            }

            _logger.LogError("Failed to link external login to existing user: {Errors}",
                string.Join(", ", addLoginResult.Errors.Select(e => e.Description)));
            return Redirect("/login?error=link_failed");
        }

        /// &lt;summary>
        /// Creates a new user based on external login information.
        /// &lt;/summary>
        private async Task&lt;IActionResult> CreateNewUserAsync(ExternalLoginInfo info, string returnUrl)
        {
            var email = info.Principal.FindFirstValue(ClaimTypes.Email);
            if (string.IsNullOrEmpty(email))
            {
                _logger.LogError("Missing email information from external provider");
                return BadRequest(new { error = "Email information was not provided." });
            }

            var userName = email.Split('@')[0];
            var user = new ApplicationUser
            {
                UserName = email,
                Email = email,
                NickName = userName,
                EmailConfirmed = true // Email is already confirmed via external provider
            };

            var createResult = await _userManager.CreateAsync(user);
            if (!createResult.Succeeded)
            {
                _logger.LogError("User creation failed: {Errors}",
                    string.Join(", ", createResult.Errors.Select(e => e.Description)));

                // Attempt to create an alternative user name on a naming conflict
                if (createResult.Errors.Any(e => e.Code == "DuplicateUserName"))
                {
                    user.UserName = $"{userName}_{Guid.NewGuid().ToString("N").Substring(0, 4)}";
                    createResult = await _userManager.CreateAsync(user);

                    if (!createResult.Succeeded)
                    {
                        return BadRequest(new
                        {
                            error = "Account creation failed",
                            details = createResult.Errors.Select(e => e.Description)
                        });
                    }
                }
                else
                {
                    return BadRequest(new
                    {
                        error = "Account creation failed",
                        details = createResult.Errors.Select(e => e.Description)
                    });
                }
            }

            // Link external login information
            var addLoginResult = await _userManager.AddLoginAsync(user, info);
            if (!addLoginResult.Succeeded)
            {
                _logger.LogError("Failed to add login information: {Errors}",
                    string.Join(", ", addLoginResult.Errors.Select(e => e.Description)));

                // Rollback: Delete the created user
                await _userManager.DeleteAsync(user);

                return BadRequest(new
                {
                    error = "Failed to link external login information",
                    details = addLoginResult.Errors.Select(e => e.Description)
                });
            }

            _logger.LogInformation("New user created and logged in successfully: {Email}", email);
            await _signInManager.SignInAsync(user, isPersistent: false, info.LoginProvider);

            return LocalRedirect(returnUrl);
        }

        private ApplicationUser CreateUser()
        {
            try
            {
                return Activator.CreateInstance&lt;ApplicationUser>();
            }
            catch
            {
                throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
                    $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor.");
            }
        }

        private IUserEmailStore&lt;ApplicationUser> GetEmailStore()
        {
            if (!_userManager.SupportsUserEmail)
            {
                throw new NotSupportedException("The default UI requires a user store with email support.");
            }
            return (IUserEmailStore&lt;ApplicationUser>)_userStore;
        }

        #endregion

        #region Profile Management

        /// &lt;summary>
        /// Handles user profile updates.
        /// &lt;/summary>
        /// &lt;param name="model">Update request DTO&lt;/param>
        /// &lt;returns>The result of the process&lt;/returns>
        [HttpPut("updateProfile")]
        public async Task&lt;IActionResult> UpdateProfile([FromBody] UpdateUserProfileDTO model)
        {
            var user = await _userManager.FindByIdAsync(model.Email);
            if (user == null)
            {
                return NotFound("User not found");
            }

            user.NickName = model.Nickname;
            user.PhoneNumber = model.PhoneNumber;

            var result = await _userManager.UpdateAsync(user);
            if (!result.Succeeded)
            {
                return BadRequest(result.Errors.Select(e => e.Description));
            }

            return Ok("Profile updated successfully");
        }

        /// &lt;summary>
        /// Handles user deletion.
        /// &lt;/summary>
        /// &lt;param name="userId">The ID of the user to delete&lt;/param>
        /// &lt;returns>The result of the process&lt;/returns>
        [HttpDelete("deleteUser/{userId}")]
        public async Task&lt;IActionResult> DeleteUser(string userId)
        {
            var user = await _userManager.FindByIdAsync(userId);
            if (user == null)
            {
                return NotFound("User not found");
            }

            var result = await _userManager.DeleteAsync(user);
            if (!result.Succeeded)
            {
                return BadRequest(result.Errors.Select(e => e.Description));
            }

            return Ok("User deleted successfully");
        }

        #endregion
    }
}
</pre>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">private void LoginWithGoogle()
{
    try
    {
        var returnUrl = "/";
        var googleLoginUrl = $"/api/auth/Challenge/Google?returnUrl={Uri.EscapeDataString(returnUrl)}";

        // Redirect in the current window instead of a popup
        // This is a placeholder for a client-side navigation method, e.g., in Blazor
        // Navigation.NavigateTo(googleLoginUrl, forceLoad: true);
    }
    catch (Exception ex)
    {
        // Error handling
        Console.WriteLine($"Google login error: {ex.Message}");
    }
}
</pre>



<p></p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-identity%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%9c-%ea%b5%ac%ea%b8%80-%eb%a1%9c%ea%b7%b8%ec%9d%b8oauth-%ec%a0%84%ec%b2%b4-%ed%9d%90%eb%a6%84-%eb%b6%84%ec%84%9d/40245/">ASP.NET Core Identity를 활용한 구글 로그인(OAuth) 전체 흐름 분석</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/asp-net/asp-net-core-identity%eb%a5%bc-%ed%99%9c%ec%9a%a9%ed%95%9c-%ea%b5%ac%ea%b8%80-%eb%a1%9c%ea%b7%b8%ec%9d%b8oauth-%ec%a0%84%ec%b2%b4-%ed%9d%90%eb%a6%84-%eb%b6%84%ec%84%9d/40245/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ASP.NET Core IdentityOptions 기본 예제</title>
		<link>https://lycos7560.com/c/asp-net/asp-net-core-identityoptions-%ec%a0%95%eb%a6%ac/40232/</link>
					<comments>https://lycos7560.com/c/asp-net/asp-net-core-identityoptions-%ec%a0%95%eb%a6%ac/40232/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Wed, 06 Aug 2025 09:44:34 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[2FA]]></category>
		<category><![CDATA[2단계 인증]]></category>
		<category><![CDATA[Admin]]></category>
		<category><![CDATA[appsettings.json]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[Audit]]></category>
		<category><![CDATA[Authorization]]></category>
		<category><![CDATA[BCrypt]]></category>
		<category><![CDATA[Bearer Token]]></category>
		<category><![CDATA[Claim]]></category>
		<category><![CDATA[Configuration]]></category>
		<category><![CDATA[DI]]></category>
		<category><![CDATA[EF Core]]></category>
		<category><![CDATA[Entity Framework]]></category>
		<category><![CDATA[GDPR]]></category>
		<category><![CDATA[Guest]]></category>
		<category><![CDATA[Identity]]></category>
		<category><![CDATA[IdentityOptions]]></category>
		<category><![CDATA[ILogger]]></category>
		<category><![CDATA[IP 차단]]></category>
		<category><![CDATA[IPasswordValidator]]></category>
		<category><![CDATA[IUserValidator]]></category>
		<category><![CDATA[JWT]]></category>
		<category><![CDATA[Lockout]]></category>
		<category><![CDATA[MFA]]></category>
		<category><![CDATA[Middleware]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[OAuth]]></category>
		<category><![CDATA[password]]></category>
		<category><![CDATA[PasswordOptions]]></category>
		<category><![CDATA[Policy]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[Recovery Code]]></category>
		<category><![CDATA[Role]]></category>
		<category><![CDATA[Salt]]></category>
		<category><![CDATA[Seed Data]]></category>
		<category><![CDATA[SignIn]]></category>
		<category><![CDATA[SQL Server]]></category>
		<category><![CDATA[SQLite]]></category>
		<category><![CDATA[Stores]]></category>
		<category><![CDATA[study]]></category>
		<category><![CDATA[Token]]></category>
		<category><![CDATA[User]]></category>
		<category><![CDATA[감사]]></category>
		<category><![CDATA[개인정보 보호]]></category>
		<category><![CDATA[개인정보처리방침]]></category>
		<category><![CDATA[계정 보안]]></category>
		<category><![CDATA[계정 잠금]]></category>
		<category><![CDATA[계정 활성화]]></category>
		<category><![CDATA[공부]]></category>
		<category><![CDATA[관리자]]></category>
		<category><![CDATA[권한 부여]]></category>
		<category><![CDATA[기초]]></category>
		<category><![CDATA[다단계 인증]]></category>
		<category><![CDATA[데이터 보호]]></category>
		<category><![CDATA[데이터베이스]]></category>
		<category><![CDATA[레이트 리미팅]]></category>
		<category><![CDATA[로그인]]></category>
		<category><![CDATA[로깅]]></category>
		<category><![CDATA[마이그레이션]]></category>
		<category><![CDATA[모니터링]]></category>
		<category><![CDATA[미들웨어]]></category>
		<category><![CDATA[보안]]></category>
		<category><![CDATA[보안 이벤트]]></category>
		<category><![CDATA[보안 질문]]></category>
		<category><![CDATA[보안 토큰]]></category>
		<category><![CDATA[복구 코드]]></category>
		<category><![CDATA[브루트 포스]]></category>
		<category><![CDATA[비밀번호 재설정]]></category>
		<category><![CDATA[사용자 검증]]></category>
		<category><![CDATA[사용자 관리]]></category>
		<category><![CDATA[사용자 역할]]></category>
		<category><![CDATA[사용자 정보]]></category>
		<category><![CDATA[세션 관리]]></category>
		<category><![CDATA[소셜 로그인]]></category>
		<category><![CDATA[실패 시도]]></category>
		<category><![CDATA[암호화]]></category>
		<category><![CDATA[역할 기반 접근]]></category>
		<category><![CDATA[외부 로그인]]></category>
		<category><![CDATA[의존성 주입]]></category>
		<category><![CDATA[이메일 인증]]></category>
		<category><![CDATA[이메일 확인]]></category>
		<category><![CDATA[익명 사용자]]></category>
		<category><![CDATA[인증]]></category>
		<category><![CDATA[인증 토큰]]></category>
		<category><![CDATA[일반 사용자]]></category>
		<category><![CDATA[전화번호 인증]]></category>
		<category><![CDATA[정책 기반 인증]]></category>
		<category><![CDATA[초기 데이터]]></category>
		<category><![CDATA[커스텀 검증]]></category>
		<category><![CDATA[쿠키 인증]]></category>
		<category><![CDATA[토큰]]></category>
		<category><![CDATA[패스워드 복잡성]]></category>
		<category><![CDATA[패스워드 정책]]></category>
		<category><![CDATA[패스워드 찾기]]></category>
		<category><![CDATA[프로필 관리]]></category>
		<category><![CDATA[해싱]]></category>
		<category><![CDATA[환경 설정]]></category>
		<category><![CDATA[회원가입]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40232</guid>

					<description><![CDATA[<p>✨ ASP.NET Core IdentityOptions 개요 https://github.com/dotnet/aspnetcore/blob/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Identity/Extensions.Core/src/IdentityOptions.cs#L17C65-L65C66 ASP.NET Core의 IdentityOptions는 시스템의 설정을 한 곳에 몰아서 관리하는 클래스입니다. (중앙 집중식 구성) 이를 통해 패스워드 정책, 사용자 설정, 로그인 정책, 잠금 정책 등을 세밀하게 제어할 수 있습니다. 🔥 주요 구성 요소 1️⃣ Password 옵션 (PasswordOptions) 패스워드 복잡성 요구사항을 설정합니다. 2️⃣ Lockout 옵션 (LockoutOptions) 계정 잠금 정책을 설정합니다. 3️⃣ [&#8230;]</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-identityoptions-%ec%a0%95%eb%a6%ac/40232/">ASP.NET Core IdentityOptions 기본 예제</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[				<div class="wp-block-uagb-table-of-contents uagb-toc__align-left uagb-toc__columns-1  uagb-block-346b5c77      "
					data-scroll= "1"
					data-offset= "30"
					style=""
				>
				<div class="uagb-toc__wrap">
						<div class="uagb-toc__title">
							목차						</div>
																						<div class="uagb-toc__list-wrap ">
						<ol class="uagb-toc__list"><li class="uagb-toc__list"><a href="#aspnet-core-identityoptions-개요" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core IdentityOptions 개요</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#주요-구성-요소" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 주요 구성 요소</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-password-옵션-passwordoptions" class="uagb-toc-link__trigger">1&#x20e3; Password 옵션 (PasswordOptions)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-lockout-옵션-lockoutoptions" class="uagb-toc-link__trigger">2&#x20e3; Lockout 옵션 (LockoutOptions)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-signin-옵션-signinoptions" class="uagb-toc-link__trigger">3&#x20e3; SignIn 옵션 (SignInOptions)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#4-user-옵션-useroptions" class="uagb-toc-link__trigger">4&#x20e3; User 옵션 (UserOptions)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#5-stores-옵션-storeoptions" class="uagb-toc-link__trigger">5&#x20e3; Stores 옵션 (StoreOptions)</a></li></ul><li class="uagb-toc__list"><a href="#실제-활용-시나리오" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f431.png" alt="🐱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실제 활용 시나리오</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-다단계-인증-설정" class="uagb-toc-link__trigger">1&#x20e3; 다단계 인증 설정</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-소셜-로그인과-연계" class="uagb-toc-link__trigger">2&#x20e3; 소셜 로그인과 연계</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-커스텀-검증-규칙-적용" class="uagb-toc-link__trigger">3&#x20e3; 커스텀 검증 규칙 적용</a></li></ul><li class="uagb-toc__list"><a href="#참고-사항" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2747.png" alt="❇" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 참고 사항</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-모니터링-및-로깅" class="uagb-toc-link__trigger">1&#x20e3; 모니터링 및 로깅</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-환경별-설정-관리" class="uagb-toc-link__trigger">2&#x20e3; 환경별 설정 관리</a></ul></ul></ol>					</div>
									</div>
				</div>
			


<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h1 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core IdentityOptions 개요</h1>



<p><a href="https://github.com/dotnet/aspnetcore/blob/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Identity/Extensions.Core/src/IdentityOptions.cs#L17C65-L65C66" target="_blank" rel="noreferrer noopener">https://github.com/dotnet/aspnetcore/blob/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Identity/Extensions.Core/src/IdentityOptions.cs#L17C65-L65C66</a></p>



<p>ASP.NET Core의 IdentityOptions는 시스템의 설정을 한 곳에 몰아서 관리하는 클래스입니다. (중앙 집중식 구성)</p>



<p>이를 통해 <strong>패스워드 정책, 사용자 설정, 로그인 정책, 잠금 정책 등</strong>을 세밀하게 제어할 수 있습니다.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 주요 구성 요소</h2>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="wp-block-image size-full"><img decoding="async" width="872" height="287" src="https://lycos7560.com/wp-content/uploads/2025/08/image-12.png" alt="" class="wp-image-40235" srcset="https://lycos7560.com/wp-content/uploads/2025/08/image-12.png 872w, https://lycos7560.com/wp-content/uploads/2025/08/image-12-300x99.png 300w, https://lycos7560.com/wp-content/uploads/2025/08/image-12-768x253.png 768w" sizes="(max-width: 872px) 100vw, 872px" /><figcaption class="wp-element-caption"><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.identity.identityoptions?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.identity.identityoptions?view=aspnetcore-8.0</a></figcaption></figure>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">1&#x20e3; Password 옵션 (PasswordOptions)</h3>



<p>패스워드 복잡성 요구사항을 설정합니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// Program.cs 또는 Startup.cs

// 개발 환경용 - 간단한 패스워드
public void ConfigureDevelopmentPassword(IdentityOptions options)
{
    options.Password.RequireDigit = false;
    options.Password.RequireLowercase = false;
    options.Password.RequireUppercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Password.RequiredLength = 4;
}

// 프로덕션 환경용 - 강력한 패스워드
public void ConfigureProductionPassword(IdentityOptions options)
{
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 12;
    options.Password.RequiredUniqueChars = 4;
}</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2&#x20e3; Lockout 옵션 (LockoutOptions)</h3>



<p>계정 잠금 정책을 설정합니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public void ConfigureProgressiveLockout(IdentityOptions options)
{
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(1);   // 1분부터 시작
    options.Lockout.MaxFailedAccessAttempts = 3;                        // 3회 실패시 잠금
    options.Lockout.AllowedForNewUsers = true;
}

// 커스텀 잠금 시간 증가 로직 (서비스에서 구현)
public class CustomLockoutService
{
    public TimeSpan CalculateLockoutDuration(int failedAttempts)
    {
        return failedAttempts switch
        {
            &lt;= 3 => TimeSpan.FromMinutes(5),
            &lt;= 6 => TimeSpan.FromMinutes(15),
            &lt;= 10 => TimeSpan.FromHours(1),
            _ => TimeSpan.FromHours(24)
        };
    }
}

public void ConfigureProgressiveLockout(IdentityOptions options)
{
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(1);   // 1분부터 시작
    options.Lockout.MaxFailedAccessAttempts = 3;                        // 3회 실패시 잠금
    options.Lockout.AllowedForNewUsers = true;
}

// 커스텀 잠금 시간 증가 로직 (서비스에서 구현)
public class CustomLockoutService
{
    public TimeSpan CalculateLockoutDuration(int failedAttempts)
    {
        return failedAttempts switch
        {
            &lt;= 3 => TimeSpan.FromMinutes(5),
            &lt;= 6 => TimeSpan.FromMinutes(15),
            &lt;= 10 => TimeSpan.FromHours(1),
            _ => TimeSpan.FromHours(24)
        };
    }
}</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">3&#x20e3; SignIn 옵션 (SignInOptions)</h3>



<p>로그인 관련 설정을 구성합니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 이메일 인증 필수 설정
public void ConfigureEmailConfirmation(IdentityOptions options)
{
    options.SignIn.RequireConfirmedEmail = true;
}

// 회원가입 컨트롤러
[HttpPost]
public async Task&lt;IActionResult> Register(RegisterViewModel model)
{
    var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
    var result = await _userManager.CreateAsync(user, model.Password);
    
    if (result.Succeeded)
    {
        // 이메일 인증 토큰 생성
        var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
        var confirmationLink = Url.Action("ConfirmEmail", "Account", 
            new { userId = user.Id, token = token }, Request.Scheme);
        
        // 이메일 발송
        await _emailSender.SendEmailAsync(model.Email, "이메일 인증", 
            $"다음 링크를 클릭하여 계정을 활성화하세요: {confirmationLink}");
        
        return View("EmailConfirmationSent");
    }
    
    return View(model);
}

// 이메일 인증 필수 설정
public void ConfigureEmailConfirmation(IdentityOptions options)
{
    options.SignIn.RequireConfirmedEmail = true;
}

// 회원가입 컨트롤러
[HttpPost]
public async Task&lt;IActionResult> Register(RegisterViewModel model)
{
    var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
    var result = await _userManager.CreateAsync(user, model.Password);
    
    if (result.Succeeded)
    {
        // 이메일 인증 토큰 생성
        var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
        var confirmationLink = Url.Action("ConfirmEmail", "Account", 
            new { userId = user.Id, token = token }, Request.Scheme);
        
        // 이메일 발송
        await _emailSender.SendEmailAsync(model.Email, "이메일 인증", 
            $"다음 링크를 클릭하여 계정을 활성화하세요: {confirmationLink}");
        
        return View("EmailConfirmationSent");
    }
    
    return View(model);
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">4&#x20e3; User 옵션 (UserOptions)</h3>



<p>사용자 계정 관련 설정을 구성합니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">builder.Services.Configure&lt;IdentityOptions>(options =>
{
    // 사용자 설정
    options.User.AllowedUserNameCharacters = 
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
    options.User.RequireUniqueEmail = true;  // 중복 이메일 방지
});


사용자명 정책 예제:
public void ConfigureUserPolicies(IdentityOptions options)
{
    // 한국어 사용자명 허용
    options.User.AllowedUserNameCharacters = 
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+가-힣";
    
    // 이메일을 고유하게 설정
    options.User.RequireUniqueEmail = true;
}

// 커스텀 사용자명 검증
public class CustomUserValidator&lt;TUser> : IUserValidator&lt;TUser> 
    where TUser : class
{
    public Task&lt;IdentityResult> ValidateAsync(UserManager&lt;TUser> manager, TUser user)
    {
        var userName = manager.GetUserNameAsync(user).Result;
        
        // 사용자명이 숫자로만 구성되면 안됨
        if (userName.All(char.IsDigit))
        {
            return Task.FromResult(
                IdentityResult.Failed(new IdentityError
                {
                    Code = "NumericUserName",
                    Description = "사용자명은 숫자로만 구성될 수 없습니다."
                }));
        }
        
        return Task.FromResult(IdentityResult.Success);
    }
}</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">5&#x20e3; Stores 옵션 (StoreOptions)</h3>



<p>사용자 계정 관련 설정을 구성합니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">builder.Services.Configure&lt;IdentityOptions>(options =>
{
    // 저장소 설정
    options.Stores.MaxLengthForKeys = 128;              // 키 최대 길이
    options.Stores.ProtectPersonalData = false;         // 개인정보 암호화 여부
});</pre>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f431.png" alt="🐱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실제 활용 시나리오</h2>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">1&#x20e3; 다단계 인증 설정</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public void ConfigureTwoFactorAuthentication(IdentityOptions options)
{
    options.SignIn.RequireConfirmedPhoneNumber = true;  // 전화번호 인증 필수
    options.Tokens.AuthenticatorTokenProvider = TokenOptions.DefaultAuthenticatorProvider;
}

// 2FA 활성화 코드
[HttpPost]
public async Task&lt;IActionResult> EnableTwoFactorAuthentication()
{
    var user = await _userManager.GetUserAsync(User);
    await _userManager.SetTwoFactorEnabledAsync(user, true);
    
    return RedirectToAction("ShowRecoveryCodes");
}</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2&#x20e3; 소셜 로그인과 연계</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public void ConfigureExternalLogins(IdentityOptions options)
{
    // 외부 로그인 시에는 이메일 인증 건너뛰기
    options.SignIn.RequireConfirmedEmail = false;
    
    // 하지만 고유 이메일은 필수
    options.User.RequireUniqueEmail = true;
}

// 외부 로그인 처리
[HttpPost]
public IActionResult ExternalLogin(string provider)
{
    var redirectUrl = Url.Action("ExternalLoginCallback");
    var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
    return Challenge(properties, provider);
}</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">3&#x20e3; 커스텀 검증 규칙 적용</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 커스텀 패스워드 검증기
public class CustomPasswordValidator : IPasswordValidator&lt;ApplicationUser>
{
    public Task&lt;IdentityResult> ValidateAsync(UserManager&lt;ApplicationUser> manager, 
        ApplicationUser user, string password)
    {
        // 사용자명과 패스워드가 너무 유사하면 안됨
        if (password.Contains(user.UserName, StringComparison.OrdinalIgnoreCase))
        {
            return Task.FromResult(IdentityResult.Failed(new IdentityError
            {
                Code = "PasswordContainsUserName",
                Description = "패스워드에 사용자명이 포함될 수 없습니다."
            }));
        }
        
        return Task.FromResult(IdentityResult.Success);
    }
}

// 서비스 등록
builder.Services.AddTransient&lt;IPasswordValidator&lt;ApplicationUser>, CustomPasswordValidator>();</pre>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2747.png" alt="❇" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 참고 사항</h2>



<div style="height:15px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">1&#x20e3; 모니터링 및 로깅</h3>



<p>보안 이벤트 로깅</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public class SecurityEventLogger
{
    private readonly ILogger&lt;SecurityEventLogger> _logger;
    
    public SecurityEventLogger(ILogger&lt;SecurityEventLogger> logger)
    {
        _logger = logger;
    }
    
    public void LogFailedLogin(string userName, string ipAddress)
    {
        _logger.LogWarning("Failed login attempt for user {UserName} from IP {IpAddress}", 
            userName, ipAddress);
    }
    
    public void LogAccountLocked(string userName)
    {
        _logger.LogWarning("Account locked for user {UserName}", userName);
    }
}</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2&#x20e3; 환경별 설정 관리</h3>



<p>appsettings.json을 활용한 설정</p>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "Identity": {
    "Password": {
      "RequiredLength": 8,
      "RequireDigit": true,
      "RequireUppercase": true,
      "RequireNonAlphanumeric": false
    },
    "Lockout": {
      "MaxFailedAccessAttempts": 5,
      "DefaultLockoutTimeSpanMinutes": 15
    },
    "SignIn": {
      "RequireConfirmedEmail": true
    }
  }
}</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 설정 바인딩
public class IdentityConfig
{
    public PasswordConfig Password { get; set; }
    public LockoutConfig Lockout { get; set; }
    public SignInConfig SignIn { get; set; }
}

// Program.cs에서 적용
var identityConfig = builder.Configuration.GetSection("Identity").Get&lt;IdentityConfig>();

builder.Services.Configure&lt;IdentityOptions>(options =>
{
    options.Password.RequiredLength = identityConfig.Password.RequiredLength;
    options.Password.RequireDigit = identityConfig.Password.RequireDigit;
    // ... 기타 설정
});</pre>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-identityoptions-%ec%a0%95%eb%a6%ac/40232/">ASP.NET Core IdentityOptions 기본 예제</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/asp-net/asp-net-core-identityoptions-%ec%a0%95%eb%a6%ac/40232/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ASP.NET Core Route Constraints</title>
		<link>https://lycos7560.com/c/asp-net/asp-net-core-route-constraints/40140/</link>
					<comments>https://lycos7560.com/c/asp-net/asp-net-core-route-constraints/40140/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Mon, 28 Jul 2025 04:02:09 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[.NET Development]]></category>
		<category><![CDATA[Action Method]]></category>
		<category><![CDATA[alpha]]></category>
		<category><![CDATA[Alphabetic]]></category>
		<category><![CDATA[alphanum]]></category>
		<category><![CDATA[Alphanumeric]]></category>
		<category><![CDATA[API Development]]></category>
		<category><![CDATA[API 개발]]></category>
		<category><![CDATA[app.UseEndpoints()]]></category>
		<category><![CDATA[app.UseRouting()]]></category>
		<category><![CDATA[Application Routing]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[Attribute Routing]]></category>
		<category><![CDATA[Backend Development]]></category>
		<category><![CDATA[bool]]></category>
		<category><![CDATA[Boolean]]></category>
		<category><![CDATA[Catch-all parameter]]></category>
		<category><![CDATA[Conditional Routing]]></category>
		<category><![CDATA[Constraint]]></category>
		<category><![CDATA[Constraint Mapping]]></category>
		<category><![CDATA[Constraint Pattern]]></category>
		<category><![CDATA[ConstraintMap]]></category>
		<category><![CDATA[Controller]]></category>
		<category><![CDATA[Convention-based Routing]]></category>
		<category><![CDATA[custom]]></category>
		<category><![CDATA[datetime]]></category>
		<category><![CDATA[decimal]]></category>
		<category><![CDATA[Default Value]]></category>
		<category><![CDATA[double]]></category>
		<category><![CDATA[Dynamic Path]]></category>
		<category><![CDATA[Endpoint]]></category>
		<category><![CDATA[Endpoint Routing]]></category>
		<category><![CDATA[Explicit Routing]]></category>
		<category><![CDATA[Extensibility]]></category>
		<category><![CDATA[float]]></category>
		<category><![CDATA[guid]]></category>
		<category><![CDATA[HTTP Methods]]></category>
		<category><![CDATA[HTTP Request]]></category>
		<category><![CDATA[HTTP 메서드]]></category>
		<category><![CDATA[HTTP 요청]]></category>
		<category><![CDATA[HttpContext]]></category>
		<category><![CDATA[IEndpointRouteBuilder]]></category>
		<category><![CDATA[Implicit Routing]]></category>
		<category><![CDATA[int]]></category>
		<category><![CDATA[Integer]]></category>
		<category><![CDATA[IRouteConstraint]]></category>
		<category><![CDATA[Lambda Handler]]></category>
		<category><![CDATA[Length]]></category>
		<category><![CDATA[length()]]></category>
		<category><![CDATA[long]]></category>
		<category><![CDATA[MapGet()]]></category>
		<category><![CDATA[MapPost()]]></category>
		<category><![CDATA[max()]]></category>
		<category><![CDATA[Maximum]]></category>
		<category><![CDATA[Maximum Length]]></category>
		<category><![CDATA[maxlength()]]></category>
		<category><![CDATA[Middleware]]></category>
		<category><![CDATA[min()]]></category>
		<category><![CDATA[Minimal API]]></category>
		<category><![CDATA[Minimum]]></category>
		<category><![CDATA[Minimum Length]]></category>
		<category><![CDATA[minlength()]]></category>
		<category><![CDATA[MVC Pattern]]></category>
		<category><![CDATA[MVC 패턴]]></category>
		<category><![CDATA[Optional Parameter]]></category>
		<category><![CDATA[Parameter Binding]]></category>
		<category><![CDATA[Path Constraint]]></category>
		<category><![CDATA[Path Matching]]></category>
		<category><![CDATA[Placeholder]]></category>
		<category><![CDATA[range()]]></category>
		<category><![CDATA[Razor Pages]]></category>
		<category><![CDATA[Regex]]></category>
		<category><![CDATA[Request Processing]]></category>
		<category><![CDATA[Request Routing]]></category>
		<category><![CDATA[required]]></category>
		<category><![CDATA[Required Parameter]]></category>
		<category><![CDATA[RESTful API]]></category>
		<category><![CDATA[Route Builder]]></category>
		<category><![CDATA[Route Constraint]]></category>
		<category><![CDATA[Route Control]]></category>
		<category><![CDATA[Route Data]]></category>
		<category><![CDATA[Route Definition]]></category>
		<category><![CDATA[Route Handler]]></category>
		<category><![CDATA[Route Options]]></category>
		<category><![CDATA[Route Parameter]]></category>
		<category><![CDATA[Route Parameter Validation]]></category>
		<category><![CDATA[Route Priority]]></category>
		<category><![CDATA[Route Restriction]]></category>
		<category><![CDATA[Route Template]]></category>
		<category><![CDATA[RouteValueDictionary]]></category>
		<category><![CDATA[Routing Configuration]]></category>
		<category><![CDATA[Routing Engine]]></category>
		<category><![CDATA[Routing Middleware]]></category>
		<category><![CDATA[Routing Rules]]></category>
		<category><![CDATA[Routing System]]></category>
		<category><![CDATA[routing table]]></category>
		<category><![CDATA[SEO Friendly]]></category>
		<category><![CDATA[SEO 친화적]]></category>
		<category><![CDATA[study]]></category>
		<category><![CDATA[url]]></category>
		<category><![CDATA[URL Constraint]]></category>
		<category><![CDATA[URL Design]]></category>
		<category><![CDATA[URL Matching]]></category>
		<category><![CDATA[URL Parsing]]></category>
		<category><![CDATA[URL Path Variable]]></category>
		<category><![CDATA[URL Segment]]></category>
		<category><![CDATA[URL Structure]]></category>
		<category><![CDATA[URL Validity]]></category>
		<category><![CDATA[URL 경로 변수]]></category>
		<category><![CDATA[URL 구조]]></category>
		<category><![CDATA[URL 매칭]]></category>
		<category><![CDATA[URL 설계]]></category>
		<category><![CDATA[URL 세그먼트]]></category>
		<category><![CDATA[URL 유효성]]></category>
		<category><![CDATA[URL 파싱]]></category>
		<category><![CDATA[Validation]]></category>
		<category><![CDATA[Web Application]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[Web Framework]]></category>
		<category><![CDATA[경로 매칭]]></category>
		<category><![CDATA[경로 일치]]></category>
		<category><![CDATA[경로 제약]]></category>
		<category><![CDATA[공부]]></category>
		<category><![CDATA[규칙 기반 라우팅]]></category>
		<category><![CDATA[기본값]]></category>
		<category><![CDATA[기초]]></category>
		<category><![CDATA[동적 경로]]></category>
		<category><![CDATA[라우트]]></category>
		<category><![CDATA[라우트 데이터]]></category>
		<category><![CDATA[라우트 빌더]]></category>
		<category><![CDATA[라우트 옵션]]></category>
		<category><![CDATA[라우트 우선순위]]></category>
		<category><![CDATA[라우트 정의]]></category>
		<category><![CDATA[라우트 제약]]></category>
		<category><![CDATA[라우트 제어]]></category>
		<category><![CDATA[라우트 제한]]></category>
		<category><![CDATA[라우트 템플릿]]></category>
		<category><![CDATA[라우트 파라미터 유효성]]></category>
		<category><![CDATA[라우트 핸들러]]></category>
		<category><![CDATA[라우팅]]></category>
		<category><![CDATA[라우팅 규칙]]></category>
		<category><![CDATA[라우팅 매개변수]]></category>
		<category><![CDATA[라우팅 미들웨어]]></category>
		<category><![CDATA[라우팅 설정]]></category>
		<category><![CDATA[라우팅 시스템]]></category>
		<category><![CDATA[라우팅 엔진]]></category>
		<category><![CDATA[라우팅 테이블]]></category>
		<category><![CDATA[람다 핸들러]]></category>
		<category><![CDATA[매개변수 바인딩]]></category>
		<category><![CDATA[명시적 라우팅]]></category>
		<category><![CDATA[미들웨어]]></category>
		<category><![CDATA[백엔드 개발]]></category>
		<category><![CDATA[사용자 지정]]></category>
		<category><![CDATA[선택적 매개변수]]></category>
		<category><![CDATA[속성 라우팅]]></category>
		<category><![CDATA[암시적 라우팅]]></category>
		<category><![CDATA[애플리케이션 라우팅]]></category>
		<category><![CDATA[액션 메서드]]></category>
		<category><![CDATA[엔드포인트]]></category>
		<category><![CDATA[엔드포인트 라우팅]]></category>
		<category><![CDATA[요청 라우팅]]></category>
		<category><![CDATA[요청 처리]]></category>
		<category><![CDATA[웹 애플리케이션]]></category>
		<category><![CDATA[웹 프레임워크]]></category>
		<category><![CDATA[유효성 검사]]></category>
		<category><![CDATA[정규식]]></category>
		<category><![CDATA[제약 조건 매핑]]></category>
		<category><![CDATA[제약 조건 패턴]]></category>
		<category><![CDATA[제한 조건]]></category>
		<category><![CDATA[조건부 라우팅]]></category>
		<category><![CDATA[캐치올 매개변수]]></category>
		<category><![CDATA[컨트롤러]]></category>
		<category><![CDATA[플레이스홀더]]></category>
		<category><![CDATA[확장성]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40140</guid>

					<description><![CDATA[<p>🔥 ASP.NET Core Route Constraints (라우트 제한 조건) https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-8.0 ASP.NET Core의 라우팅 시스템에서 라우트 제한 조건(Route Constraints)은 URL 경로의 특정 세그먼트(라우팅 매개변수)에 대한 유효성 검사 규칙을 정의하는 강력한 기능입니다. 이를 통해 특정 URL 패턴이 매칭되기 위한 조건을 명시하고, 잘못된 형식의 URL 요청이 특정 라우트에 매칭되는 것을 방지할 수 있습니다. 1️⃣ 라우트 제한 조건의 필요성 1. [&#8230;]</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-route-constraints/40140/">ASP.NET Core Route Constraints</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[				<div class="wp-block-uagb-table-of-contents uagb-toc__align-left uagb-toc__columns-1  uagb-block-ce724247      "
					data-scroll= "1"
					data-offset= "30"
					style=""
				>
				<div class="uagb-toc__wrap">
						<div class="uagb-toc__title">
							목차						</div>
																						<div class="uagb-toc__list-wrap ">
						<ol class="uagb-toc__list"><li class="uagb-toc__list"><a href="#aspnet-core-route-constraints-라우트-제한-조건" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core Route Constraints (라우트 제한 조건)</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-라우트-제한-조건의-필요성" class="uagb-toc-link__trigger">1&#x20e3; 라우트 제한 조건의 필요성</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-라우트-제한-조건-정의-방법" class="uagb-toc-link__trigger">2&#x20e3; 라우트 제한 조건 정의 방법</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-주요-라우트-제한-조건-종류-및-예시" class="uagb-toc-link__trigger">3&#x20e3; 주요 라우트 제한 조건 종류 및 예시</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#타입-기반-제한-조건" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 타입 기반 제한 조건</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#길이-및-범위-기반-제한-조건" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 길이 및 범위 기반 제한 조건</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#정규식-기반-제한-조건" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 정규식 기반 제한 조건</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#다른-일반적인-제한-조건" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 다른 일반적인 제한 조건</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#사용자-지정-라우트-제한-조건-custom-route-constraints" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 사용자 지정 라우트 제한 조건 (Custom Route Constraints)</a></li></ul><li class="uagb-toc__list"><a href="#4-endpoint-selection-order-엔드포인트-선택-순서" class="uagb-toc-link__trigger">4&#x20e3; Endpoint Selection Order (엔드포인트 선택 순서)</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#명시적-순서-order-property" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 명시적 순서 (Order Property)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#라우트-템플릿의-구체성-specificity" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 라우트 템플릿의 구체성 (Specificity):</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#정의-순서-declaration-order" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 정의 순서 (Declaration Order):</a></ul></ul></ol>					</div>
									</div>
				</div>
			


<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core Route Constraints (라우트 제한 조건)</h2>



<p><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-8.0</a></p>



<p>ASP.NET Core의 라우팅 시스템에서 <strong>라우트 제한 조건(Route Constraints)</strong>은 URL 경로의 특정 세그먼트(라우팅 매개변수)에 대한 유효성 검사 규칙을 정의하는 강력한 기능입니다. </p>



<p>이를 통해 특정 URL 패턴이 매칭되기 위한 조건을 명시하고, 잘못된 형식의 URL 요청이 특정 라우트에 매칭되는 것을 방지할 수 있습니다.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">1&#x20e3; 라우트 제한 조건의 필요성</h3>



<p>1. <strong>정확한 라우트 매칭</strong>: <br>동일한 경로 프리픽스를 가지는 여러 라우트 중에서 가장 적절한 라우트를 선택할 수 있도록 돕습니다.</p>



<ul class="wp-block-list">
<li>예: <code>/products/123</code> (ID로 조회)와 <code>/products/shoes</code> (이름으로 조회)를 구분</li>
</ul>



<p>2. <strong>유효성 검사</strong>: <br>특정 매개변수가 예상하는 형식(예: 숫자, GUID, 날짜)인지 강제하여, 컨트롤러 액션 또는 핸들러에서 타입 변환 오류를 줄입니다.</p>



<p>3. <strong>보안 및 견고성</strong>: <br>잘못된 형식의 입력으로 인한 잠재적인 오류나 공격 시도를 줄이는 데 기여합니다.</p>



<p>4. <strong>URL 디자인 강화</strong>: <br>더욱 명확하고 의미론적인 URL 구조를 유지할 수 있도록 돕습니다.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2&#x20e3; 라우트 제한 조건 정의 방법</h3>



<p>라우트 제한 조건은 라우트 템플릿 내에서 라우팅 매개변수 이름 뒤에 <strong>콜론(<code>:</code>)을 붙이고 제한 조건 이름을 지정하여 정의</strong>합니다.</p>



<p>{매개변수명:제한조건이름}</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// Program.cs 또는 Startup.cs의 UseEndpoints 블록
endpoints.MapGet("/products/{id:int}", (int id) => Results.Ok($"Product ID: {id}"));</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">3&#x20e3; 주요 라우트 제한 조건 종류 및 예시</h3>



<p>ASP.NET Core는 다양한 내장 라우트 제한 조건을 제공합니다.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 타입 기반 제한 조건</h4>



<p>ASP.NET Core는 다양한 내장 라우트 제한 조건을 제공합니다.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>제한 조건</td><td>설명</td><td>예시</td><td>매칭되는 URL</td><td>매칭되지 않는 URL</td></tr><tr><td>int</td><td>32비트 정수로 매칭됩니다.</td><td>{id:int}</td><td>/items/123</td><td>/items/abc, /items/1.5</td></tr><tr><td>bool</td><td>불리언 값(true/false)으로 매칭됩니다.</td><td>{isActive:bool}</td><td>/status/true</td><td>/status/other</td></tr><tr><td>datetime</td><td>DateTime 형식으로 매칭됩니다.</td><td>{date:datetime}</td><td>/events/2023-10-26</td><td>/events/today</td></tr><tr><td>decimal</td><td>decimal 형식으로 매칭됩니다.</td><td>{price:decimal}</td><td>/products/99.99</td><td>/products/abc</td></tr><tr><td>double</td><td>double 형식으로 매칭됩니다.</td><td>{value:double}</td><td>/data/3.14159</td><td>/data/xyz</td></tr><tr><td>float</td><td>float 형식으로 매칭됩니다.</td><td>{measurement:float}</td><td>/sensor/0.5f</td><td>/sensor/abc</td></tr><tr><td>guid</td><td>GUID(Globally Unique Identifier)로 매칭됩니다.</td><td>{itemId:guid}</td><td>/item/a1b2c3d4-e5f6-7890-1234-567890abcdef</td><td>/item/invalid-guid</td></tr><tr><td>long</td><td>64비트 정수로 매칭됩니다.</td><td>{userId:long}</td><td>/users/9876543210</td><td>/users/short</td></tr></tbody></table></figure>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// Program.cs 또는 Startup.cs
app.UseEndpoints(endpoints =>
{
    // 정수형 ID만 허용
    endpoints.MapGet("/products/{id:int}", (int id) => Results.Ok($"Getting product by ID: {id}"));

    // GUID 형식의 아이템 ID만 허용
    endpoints.MapGet("/items/{itemId:guid}", (Guid itemId) => Results.Ok($"Getting item by GUID: {itemId}"));

    // 특정 날짜 형식의 이벤트만 허용
    endpoints.MapGet("/events/on/{date:datetime}", (DateTime date) => Results.Ok($"Events on: {date.ToShortDateString()}"));
});</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 길이 및 범위 기반 제한 조건</h4>



<p>매개변수 값의 길이 또는 숫자 범위를 제한합니다.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>제한 조건</td><td>설명</td><td>예시</td><td>매칭되는 URL</td><td>매칭되지 않는 URL</td></tr><tr><td>min(value)</td><td>최소값 value 이상이어야 합니다.</td><td>{age:min(18)}</td><td>/users/age/18, /users/age/25</td><td>/users/age/17, /users/age/abc</td></tr><tr><td>max(value)</td><td>최대값 value 이하여야 합니다.</td><td>{quantity:max(100)}</td><td>/order/items/50</td><td>/order/items/101</td></tr><tr><td>range(min,max)</td><td>min과 max 사이의 값이어야 합니다.</td><td>{year:range(2000,2024)}</td><td>/posts/2023</td><td>/posts/1999, /posts/2025</td></tr><tr><td>minlength(length)</td><td>최소 length 길이 이상이어야 합니다.</td><td>{code:minlength(3)}</td><td>/code/abc, /code/abcd</td><td>/code/ab</td></tr><tr><td>maxlength(length)</td><td>최대 length 길이 이하여야 합니다.</td><td>{name:maxlength(10)}</td><td>/user/john</td><td>/user/someverylongname</td></tr><tr><td>length(min,max)</td><td>min과 max 사이의 길이어야 합니다.</td><td>{zip:length(5,9)}</td><td>/zip/12345, /zip/12345-6789</td><td>/zip/123, /zip/1234567890</td></tr></tbody></table></figure>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">app.UseEndpoints(endpoints =>
{
    // 연령이 18세 이상인 사용자만 허용
    endpoints.MapGet("/users/age/{age:min(18)}", (int age) => Results.Ok($"Adult user, age: {age}"));

    // 재고 수량이 10개에서 100개 사이인 경우에만 유효
    endpoints.MapGet("/inventory/{count:range(10,100)}", (int count) => Results.Ok($"Checking inventory count: {count}"));

    // 최소 5자, 최대 10자의 사용자 이름만 허용
    endpoints.MapGet("/profile/{username:length(5,10)}", (string username) => Results.Ok($"Profile for user: {username}"));
});</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 정규식 기반 제한 조건</h4>



<p>정규 표현식(Regular Expression)을 사용하여 매개변수 값의 패턴을 지정합니다. </p>



<p>이는 가장 유연한 제한 조건입니다.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>제한 조건</td><td>설명</td><td>예시</td><td>매칭되는 URL</td><td>매칭되지 않는 URL</td></tr><tr><td>regex(pattern)</td><td>pattern 정규식과 일치해야 합니다.</td><td>{code:regex(^[A-Z]{3}\\d{4}$)}</td><td>/item/ABC1234</td><td>/item/abc1234, /item/ABC123</td></tr></tbody></table></figure>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">app.UseEndpoints(endpoints =>
{
    // "abc"로 시작하고 숫자로 끝나는 코드만 허용
    endpoints.MapGet("/validate/{code:regex(abc\\d+)}", (string code) => Results.Ok($"Validated code: {code}"));

    // 상품 코드가 "PROD-"로 시작하고 4자리 숫자로 끝나는 경우만 허용
    endpoints.MapGet("/productcodes/{productCode:regex(^PROD-\\d{{4}}$)}", (string productCode) => Results.Ok($"Valid product code: {productCode}"));
    // 참고: 정규식 내의 중괄호는 이스케이프 필요 ({{, }})
});</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 다른 일반적인 제한 조건</h4>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>제한 조건</td><td>설명</td><td>예시</td><td>매칭되는 URL</td><td>매칭되지 않는 URL</td></tr><tr><td>alpha</td><td>알파벳 문자(a-z, A-Z)만 허용합니다.</td><td>{name:alpha}</td><td>/users/john</td><td>/users/john123</td></tr><tr><td>alphanum</td><td>알파벳 문자 또는 숫자만 허용합니다.</td><td>{token:alphanum}</td><td>/api/token/abc123</td><td>/api/token/abc-123</td></tr><tr><td>required</td><td>매개변수가 반드시 존재해야 합니다. (?와 함께 사용 불가)</td><td>{country:required}</td><td>/location/korea</td><td>/location/</td></tr><tr><td>url</td><td>유효한 URL 형식이어야 합니다.</td><td>{redirectUrl:url}</td><td>/redirect/https://google.com</td><td>/redirect/not-a-url</td></tr><tr><td>minlength, maxlength, length</td><td>string 형식에 대한 길이 제한</td><td>위 &#8220;길이 및 범위 기반&#8221; 섹션 참고</td><td></td><td></td></tr></tbody></table></figure>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">app.UseEndpoints(endpoints =>
{
    // 이름은 알파벳만 허용
    endpoints.MapGet("/greet/{name:alpha}", (string name) => Results.Ok($"Hello, {name}!"));

    // 토큰은 알파벳 또는 숫자만 허용
    endpoints.MapGet("/verify/{token:alphanum}", (string token) => Results.Ok($"Verifying token: {token}"));

    // 국가 매개변수는 반드시 제공되어야 함
    endpoints.MapGet("/country/{country:required}", (string country) => Results.Ok($"Selected country: {country}"));
});</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 사용자 지정 라우트 제한 조건 (Custom Route Constraints)</h4>



<p>내장된 제한 조건만으로는 부족할 때, <code>IRouteConstraint</code> 인터페이스를 구현하여 자신만의 커스텀 라우트 제한 조건을 만들 수 있습니다.</p>



<div style="height:10px" aria-hidden="true" class="wp-block-spacer"></div>



<p><code>IRouteConstraint</code> 인터페이스 구현</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using System.Globalization;

public class CustomYearConstraint : IRouteConstraint
{
    public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
    {
        // routeKey는 매개변수 이름 (예: "year")
        // values는 모든 라우팅 매개변수와 그 값들을 담고 있는 딕셔너리
        if (values.TryGetValue(routeKey, out object? value))
        {
            if (value is string yearString &amp;&amp; int.TryParse(yearString, out int year))
            {
                // 2000년 이후의 연도만 허용하는 예시
                return year >= 2000 &amp;&amp; year &lt;= DateTime.Now.Year + 1;
            }
        }
        return false;
    }
}</pre>



<div style="height:10px" aria-hidden="true" class="wp-block-spacer"></div>



<p>제한 조건 등록</p>



<p><code>Startup.cs</code>의 <code>ConfigureServices</code> 또는 <code>Program.cs</code>에서 <code>AddRouting</code> 메서드를 사용하여 커스텀 제한 조건을 등록합니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRouting(options =>
{
    options.ConstraintMap.Add("customYear", typeof(CustomYearConstraint));
});

var app = builder.Build();

app.UseRouting();
app.UseEndpoints(endpoints =>
{
    // /articles/2023 와 같이 customYear 제한 조건을 사용
    endpoints.MapGet("/articles/{year:customYear}", (int year) => Results.Ok($"Articles from year: {year}"));
});

app.Run();

//  사용 예시:
//  /articles/2023 -> 매칭됨
//  /articles/1999 -> 매칭되지 않음
//  /articles/2026 (현재 연도가 2025년이라면) -> 매칭되지 않음 (DateTime.Now.Year + 1 조건에 따라)</pre>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">4&#x20e3; Endpoint Selection Order (엔드포인트 선택 순서)</h3>



<p>라우팅 시스템이 여러 엔드포인트 중 어떤 엔드포인트를 현재 요청에 매칭시킬지 결정하는 방식에 대한 내용입니다. </p>



<p>이는 라우트 제한 조건(Route Constraints)의 <strong>라우트 순서</strong>와 밀접하게 관련되어 있습니다.</p>



<p>ASP.NET Core의 Endpoint Selection Order는 주로 다음 원칙에 따라 이루어집니다.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>명시적 순서 (Order Property)</strong></h4>



<p>가장 직접적인 제어 방법은 엔드포인트에 <code>Order</code> 속성을 부여하는 것입니다. 숫자가 낮을수록 우선순위가 높습니다. </p>



<p>이는 <code>MapControllerRoute</code>나 <code>MapRazorPages</code>와 같은 메서드에는 직접 적용하기 어렵고, 주로 <code>MapGet</code> 등의 Minimal API 엔드포인트나 사용자 지정 <code>RouteEndpoint</code>를 만들 때 사용됩니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">app.UseEndpoints(endpoints =>
{
    // Order가 -10인 엔드포인트 (가장 높은 우선순위)
    app.MapGet("/priority-test", () => "High priority").WithOrder(-10);

    // Order가 0인 기본 엔드포인트 (기본값)
    endpoints.MapGet("/test", () => "Normal priority");
});</pre>



<p>Note: <br><code>WithMetadata</code>를 통해 <code>RouteEndpointBuilder</code>에 직접 <code>Order</code>를 지정하는 것은 일반적인 사용법은 아니며, 실제로는 내부적으로 복잡한 로직을 통해 우선순위가 결정되거나 <code>RouteEndpoint.Order</code> 속성이 설정됩니다. <br>예를 들어, <code>MapRazorPages</code>나 <code>MapControllers</code>에서 생성되는 엔드포인트는 내부적으로 우선순위를 가집니다.)</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong><strong>라우트 템플릿의 구체성 (Specificity)</strong>:</strong></h4>



<p>이것이 가장 일반적이고 중요한 순서 결정 기준입니다. </p>



<p>라우팅 시스템은 요청 URL과 가장 &#8220;구체적으로&#8221; 일치하는 라우트 템플릿을 선호합니다. </p>



<p>구체성은 다음과 같은 요소에 의해 결정됩니다:</p>



<p><strong>리터럴 세그먼트의 수</strong>: 더 많은 리터럴(고정된 문자열) 세그먼트를 포함하는 라우트가 더 높은 우선순위를 가집니다.</p>



<ul class="wp-block-list">
<li><code>/products/all</code> (2개의 리터럴)이 <code>/products/{id}</code> (1개의 리터럴, 1개의 매개변수)보다 우선순위가 높습니다.</li>
</ul>



<p><strong>라우트 제한 조건의 사용</strong>: 제한 조건이 있는 매개변수는 제한 조건이 없는 매개변수보다 더 구체적인 것으로 간주됩니다.</p>



<ul class="wp-block-list">
<li><code>/products/{id:int}</code>는 <code>/products/{id}</code>보다 우선순위가 높습니다. (<code>/products/123</code>이 주어지면, <code>id:int</code>가 먼저 매칭됨)</li>
</ul>



<p><strong>선택적 매개변수</strong>: 선택적 매개변수가 없는 라우트가 더 구체적인 것으로 간주됩니다.</p>



<ul class="wp-block-list">
<li><code>/users/{id}</code>가 <code>/users/{id?}</code>보다 우선순위가 높습니다.</li>
</ul>



<p><strong>Catch-all 매개변수 (<code>*</code> 또는 <code>**</code>)</strong>: 가장 덜 구체적인 것으로 간주되어 거의 항상 마지막에 매칭됩니다.</p>



<ul class="wp-block-list">
<li><code>/files/{*path}</code>는 일반적으로 다른 모든 구체적인 라우트 뒤에 위치해야 합니다.</li>
</ul>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong><strong><strong>정의 순서 (Declaration Order)</strong>:</strong></strong></h4>



<p>동일한 구체성을 가진 두 개 이상의 라우트가 있을 경우, <code>UseEndpoints</code> 또는 <code>Map()</code> 계열 메서드에서 <strong>먼저 정의된 라우트가 우선순위를 가집니다.</strong> </p>



<p>이 때문에 &#8220;라우트 순서&#8221; 섹션에서 언급했듯이, 더 구체적인 라우트를 항상 먼저 정의하는 것이 중요합니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">app.UseEndpoints(endpoints =>
{
    // 이 라우트가 먼저 정의됨
    endpoints.MapGet("/products/special", () => "Special Product Page");

    // 이 라우트는 구체성이 동일하지만 나중에 정의됨
    endpoints.MapGet("/products/{name}", (string name) => $"Product by Name: {name}");

    // /products/special 요청 시, "Special Product Page"가 반환됨
    // 만약 순서가 바뀌면 "/products/{name}"에 매칭되어 "Product by Name: special"이 반환될 수 있음
});</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>Endpoint Selection Order는 복합적인 요소들의 상호작용으로 결정됩니다. </strong></p>



<p><strong>라우트 제한 조건은 이 선택 과정에서 라우트의 &#8220;구체성&#8221;을 높여 특정 URL 패턴에 대한 우선순위를 부여하는 핵심적인 방법입니다.</strong> </p>



<p>개발자는 이러한 원칙을 이해하고 라우트를 신중하게 정의하여 예상치 못한 라우팅 문제를 방지해야 합니다.</p>



<p></p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-route-constraints/40140/">ASP.NET Core Route Constraints</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/asp-net/asp-net-core-route-constraints/40140/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ASP.NET Core의 미들웨어(Middleware)</title>
		<link>https://lycos7560.com/c/asp-net-core%ec%9d%98-%eb%af%b8%eb%93%a4%ec%9b%a8%ec%96%b4middleware/40130/</link>
					<comments>https://lycos7560.com/c/asp-net-core%ec%9d%98-%eb%af%b8%eb%93%a4%ec%9b%a8%ec%96%b4middleware/40130/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Thu, 24 Jul 2025 23:32:09 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[action]]></category>
		<category><![CDATA[API Gateway]]></category>
		<category><![CDATA[API 게이트웨이]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[ASP.NET Core MVC]]></category>
		<category><![CDATA[async]]></category>
		<category><![CDATA[Asynchronous]]></category>
		<category><![CDATA[Authentication]]></category>
		<category><![CDATA[Authorization]]></category>
		<category><![CDATA[Await]]></category>
		<category><![CDATA[Caching]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Compression]]></category>
		<category><![CDATA[Conditional Middleware]]></category>
		<category><![CDATA[Configuration]]></category>
		<category><![CDATA[Configure]]></category>
		<category><![CDATA[Container]]></category>
		<category><![CDATA[Controller]]></category>
		<category><![CDATA[Conventional Middleware]]></category>
		<category><![CDATA[CORS]]></category>
		<category><![CDATA[Cross-Origin Resource Sharing]]></category>
		<category><![CDATA[Custom Middleware]]></category>
		<category><![CDATA[Debugging]]></category>
		<category><![CDATA[DELETE]]></category>
		<category><![CDATA[Dependency Injection]]></category>
		<category><![CDATA[Deployment]]></category>
		<category><![CDATA[Developer]]></category>
		<category><![CDATA[Development Environment]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[DI]]></category>
		<category><![CDATA[Error Handling]]></category>
		<category><![CDATA[event]]></category>
		<category><![CDATA[Event-driven]]></category>
		<category><![CDATA[Exception Handling]]></category>
		<category><![CDATA[Extension Method]]></category>
		<category><![CDATA[Factory-based Middleware]]></category>
		<category><![CDATA[filter]]></category>
		<category><![CDATA[GET]]></category>
		<category><![CDATA[HEAD]]></category>
		<category><![CDATA[Headers]]></category>
		<category><![CDATA[HTTP Context]]></category>
		<category><![CDATA[HTTP Method]]></category>
		<category><![CDATA[HTTP Request]]></category>
		<category><![CDATA[HTTP Response]]></category>
		<category><![CDATA[HTTP 메서드]]></category>
		<category><![CDATA[HTTP 요청]]></category>
		<category><![CDATA[HTTP 응답]]></category>
		<category><![CDATA[HTTP 컨텍스트]]></category>
		<category><![CDATA[HTTP/1.1]]></category>
		<category><![CDATA[HTTP/2]]></category>
		<category><![CDATA[HttpContext]]></category>
		<category><![CDATA[HTTPS Redirection]]></category>
		<category><![CDATA[HTTPS 리디렉션]]></category>
		<category><![CDATA[IApplicationBuilder]]></category>
		<category><![CDATA[IMiddleware]]></category>
		<category><![CDATA[Integration Testing]]></category>
		<category><![CDATA[Invoke]]></category>
		<category><![CDATA[Kestrel]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Logging]]></category>
		<category><![CDATA[Logging Provider]]></category>
		<category><![CDATA[Maintainability]]></category>
		<category><![CDATA[Microservices]]></category>
		<category><![CDATA[Middleware]]></category>
		<category><![CDATA[Middleware Chain]]></category>
		<category><![CDATA[Minimal APIs]]></category>
		<category><![CDATA[Modularization]]></category>
		<category><![CDATA[next]]></category>
		<category><![CDATA[Non-Terminal Middleware]]></category>
		<category><![CDATA[Ops]]></category>
		<category><![CDATA[options]]></category>
		<category><![CDATA[Order Critical]]></category>
		<category><![CDATA[PATCH]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[pipeline]]></category>
		<category><![CDATA[POST]]></category>
		<category><![CDATA[Production Environment]]></category>
		<category><![CDATA[Program.cs]]></category>
		<category><![CDATA[Protocol]]></category>
		<category><![CDATA[PUT]]></category>
		<category><![CDATA[Request Body]]></category>
		<category><![CDATA[Request Flow]]></category>
		<category><![CDATA[Request Pipeline]]></category>
		<category><![CDATA[RequestDelegate]]></category>
		<category><![CDATA[Response Body]]></category>
		<category><![CDATA[Response Flow]]></category>
		<category><![CDATA[Reusability]]></category>
		<category><![CDATA[Reverse Proxy]]></category>
		<category><![CDATA[Routing]]></category>
		<category><![CDATA[Run]]></category>
		<category><![CDATA[Scalability]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Separation of Concerns]]></category>
		<category><![CDATA[Service Container]]></category>
		<category><![CDATA[Short-circuit]]></category>
		<category><![CDATA[Single Responsibility Principle]]></category>
		<category><![CDATA[SoC]]></category>
		<category><![CDATA[SSL]]></category>
		<category><![CDATA[Startup]]></category>
		<category><![CDATA[Static Files]]></category>
		<category><![CDATA[Status Code]]></category>
		<category><![CDATA[study]]></category>
		<category><![CDATA[Terminal Middleware]]></category>
		<category><![CDATA[Testing]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[Unit Testing]]></category>
		<category><![CDATA[Use]]></category>
		<category><![CDATA[UseWhen]]></category>
		<category><![CDATA[Web Application]]></category>
		<category><![CDATA[Web Application Framework]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[Web Server]]></category>
		<category><![CDATA[Web Service]]></category>
		<category><![CDATA[WebApplication]]></category>
		<category><![CDATA[WebApplicationBuilder]]></category>
		<category><![CDATA[개발 환경]]></category>
		<category><![CDATA[개발자]]></category>
		<category><![CDATA[공부]]></category>
		<category><![CDATA[관심사의 분리]]></category>
		<category><![CDATA[기초]]></category>
		<category><![CDATA[단락]]></category>
		<category><![CDATA[단위 테스트]]></category>
		<category><![CDATA[단일 책임 원칙]]></category>
		<category><![CDATA[디버깅]]></category>
		<category><![CDATA[라우팅]]></category>
		<category><![CDATA[로깅]]></category>
		<category><![CDATA[로깅 프로바이더]]></category>
		<category><![CDATA[리버스 프록시]]></category>
		<category><![CDATA[마이크로서비스]]></category>
		<category><![CDATA[모듈화]]></category>
		<category><![CDATA[미들웨어]]></category>
		<category><![CDATA[미들웨어 체인]]></category>
		<category><![CDATA[배포]]></category>
		<category><![CDATA[보안]]></category>
		<category><![CDATA[비동기]]></category>
		<category><![CDATA[비종료 미들웨어]]></category>
		<category><![CDATA[상태 코드]]></category>
		<category><![CDATA[서비스 메쉬]]></category>
		<category><![CDATA[서비스 컨테이너]]></category>
		<category><![CDATA[설정]]></category>
		<category><![CDATA[성능]]></category>
		<category><![CDATA[순서 중요]]></category>
		<category><![CDATA[압축]]></category>
		<category><![CDATA[액션]]></category>
		<category><![CDATA[예외 처리]]></category>
		<category><![CDATA[오류 처리]]></category>
		<category><![CDATA[요청 본문]]></category>
		<category><![CDATA[요청 파이프라인]]></category>
		<category><![CDATA[요청 흐름]]></category>
		<category><![CDATA[웹 개발]]></category>
		<category><![CDATA[웹 서버]]></category>
		<category><![CDATA[웹 서비스]]></category>
		<category><![CDATA[웹 애플리케이션]]></category>
		<category><![CDATA[웹 애플리케이션 프레임워크]]></category>
		<category><![CDATA[유지보수성]]></category>
		<category><![CDATA[응답 본문]]></category>
		<category><![CDATA[응답 흐름]]></category>
		<category><![CDATA[의존성 주입]]></category>
		<category><![CDATA[이벤트]]></category>
		<category><![CDATA[이벤트 드리븐]]></category>
		<category><![CDATA[인가]]></category>
		<category><![CDATA[인증]]></category>
		<category><![CDATA[재사용성]]></category>
		<category><![CDATA[정적 파일]]></category>
		<category><![CDATA[조건부 미들웨어]]></category>
		<category><![CDATA[종료 미들웨어]]></category>
		<category><![CDATA[최소 API]]></category>
		<category><![CDATA[캐싱]]></category>
		<category><![CDATA[커스텀 미들웨어]]></category>
		<category><![CDATA[컨벤셔널 미들웨어]]></category>
		<category><![CDATA[컨테이너]]></category>
		<category><![CDATA[컨트롤러]]></category>
		<category><![CDATA[쿠버네티스]]></category>
		<category><![CDATA[클라우드]]></category>
		<category><![CDATA[테스트]]></category>
		<category><![CDATA[통합 테스트]]></category>
		<category><![CDATA[파이프라인]]></category>
		<category><![CDATA[팩토리 기반 미들웨어]]></category>
		<category><![CDATA[프로덕션 환경]]></category>
		<category><![CDATA[프로토콜]]></category>
		<category><![CDATA[필터]]></category>
		<category><![CDATA[헤더]]></category>
		<category><![CDATA[확장 메서드]]></category>
		<category><![CDATA[확장성]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40130</guid>

					<description><![CDATA[<p>❓ 미들웨어(Middleware) https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0 ASP.NET Core의 미들웨어는 모든 HTTP 요청과 응답 파이프라인을 형성하는 일련의 구성 요소입니다. 각 미들웨어 구성 요소는 다음을 수행할 수 있습니다: 이러한 파이프라인을 통해 애플리케이션의 로직을 모듈화하고, 인증, 로깅, 오류 처리, 라우팅 등과 같은 기능을 깔끔하고 유지 관리하기 쉬운 방식으로 추가할 수 있습니다. ⛓️ 미들웨어 체인 (요청 파이프라인) ASP.NET Core 요청 파이프라인은 차례로 [&#8230;]</p>
<p>The post <a href="https://lycos7560.com/c/asp-net-core%ec%9d%98-%eb%af%b8%eb%93%a4%ec%9b%a8%ec%96%b4middleware/40130/">ASP.NET Core의 미들웨어(Middleware)</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[				<div class="wp-block-uagb-table-of-contents uagb-toc__align-left uagb-toc__columns-1  uagb-block-db9d164c      "
					data-scroll= "1"
					data-offset= "30"
					style=""
				>
				<div class="uagb-toc__wrap">
						<div class="uagb-toc__title">
							목차						</div>
																						<div class="uagb-toc__list-wrap ">
						<ol class="uagb-toc__list"><li class="uagb-toc__list"><a href="#미들웨어middleware" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2753.png" alt="❓" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 미들웨어(Middleware)</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#미들웨어-체인-요청-파이프라인" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26d3.png" alt="⛓" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 미들웨어 체인 (요청 파이프라인)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#요청-파이프라인-단락short-circuiting" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2702.png" alt="✂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 요청 파이프라인 단락(Short-circuiting)</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#단락과-nextinvoke-이후-코드-실행" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 단락과 next.Invoke() 이후 코드 실행</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#경고-응답-전송-후-작업-주의" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 경고: 응답 전송 후 작업 주의</a></li></ul><li class="uagb-toc__list"><a href="#appuse-vs-apprun" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1fae0.png" alt="🫠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> app.Use vs. app.Run</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#aspnet-core의-커스텀-미들웨어" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2753.png" alt="❓" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core의 커스텀 미들웨어</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#커스텀-미들웨어-클래스의-구조" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f527.png" alt="🔧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 커스텀 미들웨어 클래스의 구조</a></li></ul><li class="uagb-toc__list"><a href="#커스텀-컨벤셔널-미들웨어-custom-conventional-middleware" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 커스텀 컨벤셔널 미들웨어 (Custom Conventional Middleware)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#미들웨어-파이프라인의-이상적인-순서" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f500.png" alt="🔀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 미들웨어 파이프라인의 이상적인 순서</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#usewhen" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> UseWhen()</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#usewhen-작동-방식" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> UseWhen() 작동 방식</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#usewhen-사용-시점" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f98b.png" alt="🦋" class="wp-smiley" style="height: 1em; max-height: 1em;" /> UseWhen() 사용 시점</a></ul></ul></ol>					</div>
									</div>
				</div>
			


<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2753.png" alt="❓" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 미들웨어(Middleware)</h2>



<p><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0</a></p>



<p>ASP.NET Core의 <strong>미들웨어</strong>는 모든 HTTP 요청과 응답 <strong>파이프라인을 형성하는 일련의 구성 요소</strong>입니다.</p>



<p>각 미들웨어 구성 요소는 다음을 수행할 수 있습니다:</p>



<ul class="wp-block-list">
<li>들어오는 <strong>요청을 검사</strong>합니다.</li>



<li>요청 또는 응답을 <strong>수정</strong>합니다 (필요한 경우).</li>



<li>파이프라인의 다음 미들웨어를 <strong>호출</strong>하거나, 프로세스를 <strong>단락(short-circuit)</strong>시키고 자체적으로 응답을 생성합니다.</li>
</ul>



<p>이러한 파이프라인을 통해 <strong>애플리케이션의 로직을 모듈화하고, 인증, 로깅, 오류 처리, 라우팅 등과 </strong>같은 기능을 깔끔하고<strong> 유지 관리하기 쉬운 방식으로 추가</strong>할 수 있습니다.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26d3.png" alt="⛓" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 미들웨어 체인 (요청 파이프라인)</h3>



<p>ASP.NET Core 요청 파이프라인은 차례로 호출되는 일련의 요청 대리자로 구성됩니다.</p>



<p>요청 파이프라인을 일련의 연결된 파이프라고 상상해 보세요. </p>



<p>각 미들웨어 조각은 이 파이프라인의 밸브와 같아서, <strong>정보의 흐름을 제어하고 다양한 단계에서 특정 작업을 적용</strong>할 수 있습니다. </p>



<p>미들웨어를 등록하는 <strong>순서는 매우 중요</strong>하며, 등록된 순서대로 실행됩니다.</p>



<figure class="wp-block-image size-full"><img decoding="async" width="607" height="389" src="https://lycos7560.com/wp-content/uploads/2025/07/image-12.png" alt="" class="wp-image-40131" srcset="https://lycos7560.com/wp-content/uploads/2025/07/image-12.png 607w, https://lycos7560.com/wp-content/uploads/2025/07/image-12-300x192.png 300w" sizes="(max-width: 607px) 100vw, 607px" /><figcaption class="wp-element-caption"><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0</a></figcaption></figure>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<p>각 델리게이트는 <strong>다음 델리게이트를 호출하기 전과 후에 작업</strong>을 수행할 수 있습니다. </p>



<p>예외 처리 델리게이트는 파이프라인의 후반 단계에서 발생하는 예외를 Catch할 수 있도록 파이프라인의 <strong>초기에 호출</strong>되어야 합니다.</p>



<p>가장 간단한 ASP.NET Core 앱은 모든 요청을 처리하는 <strong>단일 요청 델리게이트</strong>를 설정합니다. </p>



<p>이 경우에는 실제 요청 파이프라인이 포함되지 않습니다. </p>



<p>대신, 모든 HTTP 요청에 대한 응답으로 단일 익명 함수가 호출됩니다.</p>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello world!");
});

app.Run(); // 이 app.Run은 위의 app.Run이 파이프라인을 종료하므로 실행되지 않습니다.</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong><code>Use</code> 메서드를 사용하면 여러 요청 델리게이트를 체인으로 연결</strong>할 수 있습니다. </p>



<p>이때 <code>next</code> 매개변수는 파이프라인의 다음 델리게이트를 나타냅니다.</p>



<p><code>next</code> 매개변수를 호출하지 않음으로써 <strong>파이프라인을 단락(short-circuit)</strong>시킬 수 있습니다. </p>



<p>다음 예시에서 보여주듯이, 일반적으로 <code>next</code> 델리게이트를 호출하기 전과 후에 모두 작업을 수행할 수 있습니다.</p>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">var builder = WebApplication.CreateBuilder(args); // 웹 애플리케이션 빌더를 생성합니다.
var app = builder.Build(); // 빌더를 사용하여 웹 애플리케이션 인스턴스를 생성합니다.

// 첫 번째 미들웨어: app.Use()를 사용하여 파이프라인에 추가합니다.
app.Use(async (context, next) =>
{
    // 응답에 쓸 수 있는 작업을 수행합니다. (예: 헤더 추가, 응답의 시작 부분 작성)
    // 이 부분의 코드는 다음 미들웨어가 실행되기 전에 실행됩니다.

    await next.Invoke(); // 다음 미들웨어(이 경우 app.Run)를 호출하여 제어를 넘깁니다.

    // 응답에 쓰지 않는 로깅 또는 다른 작업을 수행합니다.
    // 이 부분의 코드는 다음 미들웨어가 실행된 후(응답이 생성된 후) 실행됩니다.
});

// 두 번째 미들웨어: app.Run()을 사용하여 파이프라인에 추가합니다.
// app.Run()은 파이프라인을 종료하는 미들웨어입니다.
app.Run(async context =>
{
    // 이 미들웨어는 "Hello from 2nd delegate."를 응답에 작성하고 파이프라인을 종료합니다.
    await context.Response.WriteAsync("Hello from 2nd delegate.");
});

// 이 app.Run()은 위의 app.Run()이 이미 파이프라인을 종료했기 때문에 실행되지 않습니다.
app.Run();</pre>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2702.png" alt="✂" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 요청 파이프라인 단락(Short-circuiting)</h3>



<p>ASP.NET Core의 미들웨어 파이프라인에서, 특정 델리게이트(미들웨어)가 요청을 <strong>다음 델리게이트로 전달하지 않을 때</strong> 이를 <strong>요청 파이프라인을 단락(short-circuiting)시킨다</strong>고 합니다. </p>



<p>파이프라인 단락은 불필요한 작업을 피할 수 있기 때문에 종종 유용합니다.</p>



<p>예를 들어, <strong>정적 파일 미들웨어(Static File Middleware)</strong>는 요청된 파일이 정적 파일(이미지, CSS, JavaScript 등)일 경우 해당 요청을 처리하고</p>



<p> 파이프라인의 나머지 부분을 단락시킴으로써 <strong>종료 미들웨어(terminal middleware)</strong> 역할을 할 수 있습니다. </p>



<p>이렇게 되면 정적 파일을 제공한 후에는 <strong>인증이나 라우팅과 같은 다른 미들웨어들이 실행될 필요가 없으므로 효율성이 높아집니다.</strong></p>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 단락과 <code>next.Invoke()</code> 이후 코드 실행</h4>



<p>파이프라인의 추가적인 처리를 종료시키는 미들웨어보다 <strong>앞서 파이프라인에 추가된 미들웨어</strong>는 여전히 <code>next.Invoke()</code> 구문 이후의 코드를 처리합니다. </p>



<p>즉, 비록 파이프라인이 단락되어 최종 응답이 더 이상 아래로 전달되지 않더라도, <code>next.Invoke()</code>를 호출했던 이전 미들웨어들은 응답이 완료된 후 자신의 후처리 로직을 실행할 수 있습니다.</p>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 경고: 응답 전송 후 작업 주의</h4>



<p><strong>응답이 클라이언트에게 이미 전송되었거나 전송되기 시작한 후에는 <code>next.Invoke()</code>를 호출하거나 응답에 쓰려고 시도하지 마세요.</strong> </p>



<p><code>HttpResponse</code>가 시작된 후에는 응답에 변경을 가하려고 하면 <strong>예외가 발생</strong>합니다. </p>



<p>예를 들어, 응답이 시작된 후 헤더나 상태 코드를 설정하려고 하면 예외가 발생합니다.</p>



<p><code>next.Invoke()</code> 호출 후 응답 본문에 쓰는 것은 다음과 같은 문제를 야기할 수 있습니다:</p>



<ul class="wp-block-list">
<li><strong>프로토콜 위반:</strong> 명시된 <code>Content-Length</code>보다 더 많은 내용을 작성하는 것과 같은 프로토콜 위반을 초래할 수 있습니다.</li>



<li><strong>본문 형식 손상:</strong> CSS 파일에 HTML 푸터(footer)를 작성하는 것처럼 본문 형식을 손상시킬 수 있습니다.</li>
</ul>



<p><code>HasStarted</code> 속성은 헤더가 전송되었는지 또는 본문이 작성되었는지 여부를 나타내는 유용한 힌트가 될 수 있습니다.</p>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1fae0.png" alt="🫠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <code>app.Use</code> vs. <code>app.Run</code></h3>



<p>이 두 메서드는 파이프라인에 미들웨어를 추가하는 데 기본적이지만, 핵심적인 차이점이 있습니다:</p>



<p><code><strong>app.Use(async (context, next) => { ... })</strong></code></p>



<ul class="wp-block-list">
<li><strong>요청 수정 불가:</strong> <br>마지막 단계이므로 요청을 다음으로 전달하기 전에 수정할 수 없습니다.</li>



<li><strong>비종료(Non-Terminal) 미들웨어:</strong> <br>이 유형의 미들웨어는 일반적으로 어떤 작업을 수행한 다음, <code>next</code> 델리게이트를 호출하여 파이프라인의 다음 미들웨어로 제어를 전달합니다.</li>



<li><strong>요청/응답 수정 가능:</strong> <br>요청을 다음으로 전달하기 전에 요청이나 응답을 변경할 수 있습니다.</li>



<li><strong>예시:</strong> <br>인증, 로깅, 커스텀 헤더 추가 등.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p><code>a<strong>pp.Run(async (context) => { ... })</strong></code></p>



<ul class="wp-block-list">
<li><strong>종료(Terminal) 미들웨어:</strong> <br>이 미들웨어는 <code>next</code>를 호출하지 않습니다. <br>파이프라인을 종료하고 <strong>자체적으로 응답을 생성</strong>합니다.</li>



<li><strong>최종 응답에 주로 사용:</strong> <br>더 이상 처리가 필요 없는 요청(예: 간단한 메시지 반환)을 처리하는 데 일반적으로 사용됩니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 여러 app.Run 호출의 결과

app.Run(async (HttpContext context) => {
    await context.Response.WriteAsync("Hello");
});

app.Run(async (HttpContext context) => {
    await context.Response.WriteAsync("Hello again");
});

app.Run(); // 이 app.Run은 위의 app.Run들이 이미 파이프라인을 종료했기 때문에 절대 실행되지 않습니다.</pre>



<p>이 코드에서는 <strong>오직 첫 번째 <code>app.Run</code> 미들웨어만 실행됩니다.</strong> </p>



<p>&#8220;Hello&#8221;를 응답에 작성하여 파이프라인을 종료하고, 그 뒤의 <code>app.Run</code> (이것은 &#8220;Hello again&#8221;을 작성할 것임)은 실행될 기회를 얻지 못합니다.</p>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// app.Use와 app.Run으로 미들웨어 체인 연결하기

// 미들웨어 1
app.Use(async (context, next) => {
    await context.Response.WriteAsync("Hello "); // 1. "Hello " 작성
    await next(context); // 2. 다음 미들웨어 호출
});

// 미들웨어 2
app.Use(async (context, next) => {
    await context.Response.WriteAsync("Hello again "); // 3. "Hello again " 작성
    await next(context); // 4. 다음 미들웨어 호출 (이 경우 app.Run)
});

// 미들웨어 3 (종료 미들웨어)
app.Run(async (HttpContext context) => {
    await context.Response.WriteAsync("Hello again"); // 5. "Hello again" 작성 후 파이프라인 종료
});</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>이 코드는 미들웨어를 올바르게 연결하는 방법을 보여줍니다.</p>



<ol start="1" class="wp-block-list">
<li>첫 번째 <code>app.Use</code>는 응답에 &#8220;Hello &#8220;를 작성하고 <code>next</code>를 호출하여 다음 미들웨어로 제어를 전달합니다.</li>



<li>두 번째 <code>app.Use</code>는 &#8220;Hello again &#8220;을 작성하고 역시 <code>next</code>를 호출합니다.</li>



<li>마지막 <code>app.Run</code> (종료 미들웨어)는 &#8220;Hello again&#8221;을 작성하고 파이프라인을 종료합니다.</li>
</ol>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2753.png" alt="❓" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core의 커스텀 미들웨어</h3>



<p>ASP.NET Core는 다양한 내장 미들웨어 구성 요소를 제공하지만, 때로는 애플리케이션 고유의 특정 요구 사항을 해결하기 위해 자신만의 <strong>커스텀 미들웨어</strong>를 만들어야 할 수도 있습니다. </p>



<p>커스텀 미들웨어를 통해 다음을 수행할 수 있습니다:</p>



<ul class="wp-block-list">
<li><strong>로직 캡슐화:</strong> <br>관련 작업(예: 로깅, 보안 검사, 사용자 정의 헤더)을 재사용 가능한 구성 요소로 묶습니다.</li>



<li><strong>동작 사용자 정의:</strong> <br>애플리케이션의 요구 사항에 정확히 맞게 요청/응답 파이프라인을 조정합니다.</li>



<li><strong>코드 구성 개선:</strong> <br>미들웨어 코드를 깔끔하고 유지 관리하기 쉽게 만듭니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f527.png" alt="🔧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 커스텀 미들웨어 클래스의 구조</h4>



<p><strong><code>IMiddleware</code> 구현:</strong> 이 인터페이스는 단 하나의 메서드 <code>InvokeAsync(HttpContext context, RequestDelegate next)</code>를 요구합니다. </p>



<p>이 메서드는 미들웨어 로직의 핵심입니다</p>



<p><code>InvokeAsync</code> 또는 <code>Invoke</code> 메서드</p>



<ul class="wp-block-list">
<li><strong><code>context</code>:</strong> <br><code>HttpContext</code>는 요청 및 응답 객체에 대한 접근을 제공합니다.</li>



<li><strong><code>next</code>:</strong> <br><code>RequestDelegate</code>는 파이프라인의 다음 미들웨어를 호출할 수 있도록 합니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// MyCustomMiddleware.cs
namespace MiddlewareExample.CustomMiddleware
{
    public class MyCustomMiddleware : IMiddleware // IMiddleware 인터페이스 구현
    {
        public async Task InvokeAsync(HttpContext context, RequestDelegate next)
        {
            await context.Response.WriteAsync("My Custom Middleware - Starts\n"); // 1. 응답 시작 부분에 출력
            await next(context); // 2. 다음 미들웨어 호출
            await context.Response.WriteAsync("My Custom Middleware - Ends\n"); // 3. 다음 미들웨어 완료 후 응답 끝 부분에 출력
        }
    }

    // 쉽게 등록하기 위한 확장 메서드
    public static class CustomMiddlewareExtension
    {
        public static IApplicationBuilder UseMyCustomMiddleware(this IApplicationBuilder app)
        {
            return app.UseMiddleware&lt;MyCustomMiddleware>();
        }
    }
}</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// Program.cs (또는 Startup.cs)
using MiddlewareExample.CustomMiddleware;

// ... (다른 설정 코드) ...

builder.Services.AddTransient&lt;MyCustomMiddleware>(); // 트랜지언트 서비스로 등록

app.Use(async (HttpContext context, RequestDelegate next) => {
    await context.Response.WriteAsync("From Middleware 1\n"); // 첫 번째 미들웨어: 시작 부분
    await next(context);
});

app.UseMyCustomMiddleware(); // 확장 메서드를 사용하여 커스텀 미들웨어 추가

app.Run(async (HttpContext context) => {
    await context.Response.WriteAsync("From Middleware 3\n"); // 세 번째 미들웨어: 파이프라인 종료
});</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<ul class="wp-block-list">
<li><strong>등록:</strong> <br>ASP.NET Core가 필요할 때 <code>MyCustomMiddleware</code> 인스턴스를 생성할 수 있도록 이를 트랜지언트 서비스로 등록합니다.</li>



<li><strong>파이프라인 통합:</strong> <br><code>app.UseMyCustomMiddleware()</code> 확장 메서드는 커스텀 미들웨어를 파이프라인에 추가합니다.</li>



<li><strong>실행 순서:</strong> <br>미들웨어 구성 요소는 파이프라인에 추가된 순서대로 실행됩니다. 이 경우 순서는 미들웨어 1, <code>MyCustomMiddleware</code>, 그리고 미들웨어 3이 됩니다.</li>
</ul>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 커스텀 컨벤셔널 미들웨어 (Custom Conventional Middleware)</h3>



<p>ASP.NET Core 미들웨어에는 두 가지 유형이 있습니다: <strong>컨벤셔널(Conventional)</strong>과 <strong>팩토리 기반(Factory-based)</strong> </p>



<p>예시에서 보여진 컨벤셔널 미들웨어는 HTTP 요청 및 응답 처리를 위한 커스텀 로직을 캡슐화하는 간단하면서도 강력한 방법입니다.</p>



<p>주요 특징</p>



<ul class="wp-block-list">
<li><strong>클래스 기반:</strong> <br>컨벤셔널 미들웨어는 클래스로 구현됩니다.</li>



<li><strong>생성자 주입:</strong> <br>의존성(있는 경우)을 생성자를 통해 받습니다.</li>



<li><strong><code>Invoke</code> 메서드:</strong> <br>이 메서드는 각 요청을 처리하는 로직을 포함하는 미들웨어의 핵심입니다.</li>



<li><strong><code>RequestDelegate</code>:</strong> <br><code>Invoke</code> 메서드는 <code>RequestDelegate</code> 매개변수(<code>_next</code>로 명명)를 받습니다. 이 델리게이트는 파이프라인의 다음 미들웨어를 나타냅니다.</li>



<li><strong>유연성:</strong> <br><code>Invoke</code> 메서드 내에서 요청 및 응답 객체를 완벽하게 제어할 수 있습니다.</li>
</ul>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="wp-block-image size-full"><img decoding="async" width="689" height="343" src="https://lycos7560.com/wp-content/uploads/2025/07/image-13.png" alt="" class="wp-image-40132" srcset="https://lycos7560.com/wp-content/uploads/2025/07/image-13.png 689w, https://lycos7560.com/wp-content/uploads/2025/07/image-13-300x149.png 300w" sizes="(max-width: 689px) 100vw, 689px" /></figure>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// NameConcatenationMiddleware.cs
public class NameConcatenationMiddleware
{
    private readonly RequestDelegate _next;

    public NameConcatenationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Query.ContainsKey("firstname") &amp;&amp;
            context.Request.Query.ContainsKey("lastname"))
        {
            string fullName = $"{context.Request.Query["firstname"]} {context.Request.Query["lastname"]}";
            await context.Response.WriteAsync(fullName);
            return; // 명시적으로 반환하여 다음 미들웨어 호출 방지
        }
        
        await _next(context);
    }
}

// MiddlewareExtensions.cs
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseNameConcatenation(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware&lt;NameConcatenationMiddleware>();
    }
}</pre>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f500.png" alt="🔀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 미들웨어 파이프라인의 이상적인 순서</h3>



<p>미들웨어의 순서는 애플리케이션의 동작과 효율성, 보안에 큰 영향을 미칩니다. </p>



<p>다음은 일반적으로 권장되는 이상적인 순서입니다</p>



<figure class="wp-block-image size-full"><img decoding="async" width="858" height="485" src="https://lycos7560.com/wp-content/uploads/2025/07/image-14.png" alt="" class="wp-image-40133" srcset="https://lycos7560.com/wp-content/uploads/2025/07/image-14.png 858w, https://lycos7560.com/wp-content/uploads/2025/07/image-14-300x170.png 300w, https://lycos7560.com/wp-content/uploads/2025/07/image-14-768x434.png 768w" sizes="(max-width: 858px) 100vw, 858px" /><figcaption class="wp-element-caption"><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0</a></figcaption></figure>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>예외/오류 처리(Exception/Error Handling):</strong></p>



<ul class="wp-block-list">
<li><strong>목적:</strong> 파이프라인의 어느 곳에서든 발생하는 예외를 Catch하고 처리합니다.</li>



<li><strong>예시:</strong> <code>UseExceptionHandler</code> (프로덕션용), <code>UseDeveloperExceptionPage</code> (개발 환경용).</li>



<li><strong>이유:</strong> 예외를 초기에 Catch하여 파이프라인 아래로 전파되어 더 큰 문제를 일으키는 것을 방지합니다.</li>
</ul>



<p><strong>HTTPS 리디렉션(HTTPS Redirection):</strong></p>



<ul class="wp-block-list">
<li><strong>목적:</strong> 보안을 위해 HTTP 요청을 HTTPS로 리디렉션합니다.</li>



<li><strong>예시:</strong> <code>UseHttpsRedirection</code>.</li>



<li><strong>이유:</strong> 보안을 최우선으로 하여 모든 통신이 암호화되도록 합니다.</li>
</ul>



<p><strong>정적 파일(Static Files):</strong></p>



<ul class="wp-block-list">
<li><strong>목적:</strong> 이미지, CSS, JavaScript 파일과 같은 정적 파일을 클라이언트에게 직접 제공합니다.</li>



<li><strong>예시:</strong> <code>UseStaticFiles</code>.</li>



<li><strong>이유:</strong> 정적 파일 요청은 빠르게 처리되어야 하며, 불필요하게 파이프라인의 다른 무거운 구성 요소를 거치지 않도록 일찍 처리합니다.</li>
</ul>



<p><strong>라우팅(Routing):</strong></p>



<ul class="wp-block-list">
<li><strong>목적:</strong> URL을 기반으로 들어오는 요청을 특정 엔드포인트에 매칭합니다.</li>



<li><strong>예시:</strong> <code>UseRouting</code>, <code>UseEndpoints</code>.</li>



<li><strong>이유:</strong> 라우팅은 애플리케이션의 핵심 로직이 요청을 어떻게 처리할지 결정하는 기반이 됩니다.</li>
</ul>



<p><strong>CORS (Cross-Origin Resource Sharing):</strong></p>



<ul class="wp-block-list">
<li><strong>목적:</strong> 다른 도메인으로부터의 안전한 교차 출처(cross-origin) 요청을 가능하게 합니다.</li>



<li><strong>예시:</strong> <code>UseCors</code>.</li>



<li><strong>이유:</strong> 인증/인가 전에 위치하여, 사전 요청(preflight request)이 불필요하게 인증/인가 미들웨어를 거치지 않도록 합니다.</li>
</ul>



<p><strong>인증(Authentication):</strong></p>



<ul class="wp-block-list">
<li><strong>목적:</strong> 사용자 신원을 확인하고 사용자 주체(principal)를 설정합니다.</li>



<li><strong>예시:</strong> <code>UseAuthentication</code>.</li>



<li><strong>이유:</strong> 사용자가 누구인지 확인한 후에 리소스에 대한 접근 권한을 부여할 수 있습니다.</li>
</ul>



<p><strong>인가(Authorization):</strong></p>



<ul class="wp-block-list">
<li><strong>목적:</strong> 사용자가 특정 리소스에 접근하거나 특정 작업을 수행할 수 있는지 여부를 결정합니다.</li>



<li><strong>예시:</strong> <code>UseAuthorization</code>.</li>



<li><strong>이유:</strong> 인증된 사용자에게만 권한 부여 여부를 검사합니다.</li>
</ul>



<p><strong>커스텀 미들웨어(Custom Middleware):</strong></p>



<ul class="wp-block-list">
<li><strong>목적:</strong> 로깅, 기능 플래그 등 애플리케이션별 미들웨어 구성 요소를 처리합니다.</li>



<li><strong>이유:</strong> 애플리케이션별 로직을 적절한 단계에서 파이프라인 내에 배치합니다.</li>
</ul>



<p><strong>MVC/Razor Pages/Minimal APIs:</strong></p>



<ul class="wp-block-list">
<li><strong>목적:</strong> 실제 애플리케이션의 최종 엔드포인트 처리 로직을 실행합니다.</li>



<li><strong>예시:</strong> <code>MapControllers()</code>, <code>MapRazorPages()</code>, <code>MapGet()</code> 등.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="wp-block-image size-full"><img decoding="async" width="848" height="589" src="https://lycos7560.com/wp-content/uploads/2025/07/image-15.png" alt="" class="wp-image-40134" srcset="https://lycos7560.com/wp-content/uploads/2025/07/image-15.png 848w, https://lycos7560.com/wp-content/uploads/2025/07/image-15-300x208.png 300w, https://lycos7560.com/wp-content/uploads/2025/07/image-15-768x533.png 768w" sizes="(max-width: 848px) 100vw, 848px" /><figcaption class="wp-element-caption"><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0</a></figcaption></figure>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p><code>Program.cs</code> 파일에 미들웨어 구성 요소가 추가되는 순서는 요청 시 미들웨어 구성 요소가 호출되는 순서를 정의하며, 응답 시에는 역순으로 호출됩니다. </p>



<p>이러한 <strong>순서는 보안, 성능 및 기능에 매우 중요합니다.</strong></p>



<p><code>Program.cs</code>의 다음 강조 표시된 코드는 보안 관련 미들웨어 구성 요소를 일반적으로 권장되는 순서로 추가하는 예시입니다:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="19-43" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebMiddleware.Data;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
    ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext&lt;ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity&lt;IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores&lt;ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddControllersWithViews();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
// app.UseCookiePolicy();

app.UseRouting();
// app.UseRateLimiter();
// app.UseRequestLocalization();
// app.UseCors();

app.UseAuthentication();
app.UseAuthorization();
// app.UseSession();
// app.UseResponseCompression();
// app.UseResponseCaching();

app.MapRazorPages();
app.MapDefaultControllerRoute();

app.Run();</pre>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> UseWhen()</h3>



<p><code>UseWhen()</code>은 ASP.NET Core의 <code>IApplicationBuilder</code> 인터페이스에 있는 강력한 확장 메서드입니다. </p>



<p>이는 <strong>조건(predicate)에 따라 미들웨어를 요청 파이프라인에 조건부로 추가</strong>할 수 있도록 합니다. </p>



<p>즉, 특정 조건이 충족될 때만 특정 미들웨어 구성 요소가 실행되는 동적인 파이프라인을 만들 수 있습니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">app.UseWhen(
    context => /* 여기에 조건 */, // HttpContext를 받아 true/false 반환
    app => /* 이 분기에서 실행될 미들웨어 구성 */ // 조건이 true일 때 실행될 미들웨어 파이프라인
);</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<ul class="wp-block-list">
<li><strong><code>context</code>:</strong> <br>현재 요청을 나타내는 <code>HttpContext</code> 객체입니다.</li>



<li><strong>Predicate (조건):</strong> <br><code>HttpContext</code>를 받아들이고 미들웨어 분기가 실행되어야 할 경우 <code>true</code>를, 그렇지 않을 경우 <code>false</code>를 반환하는 함수입니다.</li>



<li><strong>Middleware Configuration (미들웨어 구성):</strong> <br>조건이 <code>true</code>일 때 실행되어야 할 미들웨어 구성 요소를 구성하는 액션입니다. <br>여기서 <code>app.Use()</code>, <code>app.Run()</code>, 또는 다른 미들웨어 등록 메서드를 사용합니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <code>UseWhen()</code> 작동 방식</h4>



<ul class="wp-block-list">
<li><strong>조건 평가:</strong> <br>요청이 들어오면 <code>UseWhen()</code> 메서드는 먼저 <code>HttpContext</code>에 대해 조건 함수를 평가합니다.</li>



<li><strong>분기(조건이 true일 경우):</strong> <br>조건이 <code>true</code>를 반환하면, 구성 액션에 지정된 미들웨어 분기가 실행됩니다. <br>요청은 이 분기를 통해 흐르며, 수정되거나 응답을 생성할 수 있습니다.</li>



<li><strong>메인 파이프라인 재진입:</strong> <br>분기가 실행된 후(또는 조건이 <code>false</code>여서 건너뛰어진 경우), 요청 흐름은 메인 파이프라인으로 다시 진입하여 <code>UseWhen()</code> 호출 뒤에 등록된 다음 미들웨어 구성 요소로 계속 진행됩니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">app.UseWhen(
    context => context.Request.Query.ContainsKey("username"), // 조건: 쿼리 문자열에 "username"이 있는지 확인
    app => {
        app.Use(async (context, next) =>
        {
            await context.Response.WriteAsync("Hello from Middleware branch\n"); // 분기 미들웨어: "Hello from Middleware branch" 작성
            await next(); // 다음 미들웨어 호출
        });
    });

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from middleware at main chain"); // 메인 파이프라인 미들웨어
});</pre>



<ul class="wp-block-list">
<li><strong>조건:</strong> <br><code>context.Request.Query.ContainsKey("username")</code> 조건은 쿼리 문자열에 &#8220;username&#8221;이라는 매개변수가 포함되어 있는지 확인합니다.</li>



<li><strong>분기 미들웨어:</strong> <br>&#8220;username&#8221; 매개변수가 존재하면 분기 미들웨어가 실행됩니다. <br>이 미들웨어는 응답에 &#8220;Hello from Middleware branch&#8221;를 작성하고 <code>next</code>를 호출하여 나머지 파이프라인이 계속되도록 합니다.</li>



<li><strong>메인 파이프라인:</strong> <br>마지막 <code>app.Run</code> 미들웨어는 메인 파이프라인의 일부입니다. <br>이는 응답에 &#8220;Hello from middleware at main chain&#8221;을 작성합니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f98b.png" alt="🦋" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <code>UseWhen()</code> 사용 시점</h4>



<ul class="wp-block-list">
<li><strong>조건부 기능:</strong> <br>요청에 따라 특정 기능을 활성화하거나 비활성화합니다 (예: 특정 사용자에게만 로깅, 쿼리 매개변수에 따른 캐싱 규칙 적용).</li>



<li><strong>동적 파이프라인:</strong> <br>다양한 요청에 맞춰 조정되는 파이프라인을 만듭니다 (예: 특정 경로에 대해 다른 인증 미들웨어).</li>



<li><strong>A/B 테스트:</strong> <br>실험을 위해 사용자 하위 집합을 대체 미들웨어 분기를 통해 라우팅합니다.</li>



<li><strong>디버깅 및 진단:</strong> <br>개발 환경에서만 진단 미들웨어를 적용합니다.</li>
</ul>



<p></p>
<p>The post <a href="https://lycos7560.com/c/asp-net-core%ec%9d%98-%eb%af%b8%eb%93%a4%ec%9b%a8%ec%96%b4middleware/40130/">ASP.NET Core의 미들웨어(Middleware)</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/asp-net-core%ec%9d%98-%eb%af%b8%eb%93%a4%ec%9b%a8%ec%96%b4middleware/40130/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MimeKit과 MailKit 패키지 빌드 오류</title>
		<link>https://lycos7560.com/c/asp-net/mimekit%ea%b3%bc-mailkit-%ed%8c%a8%ed%82%a4%ec%a7%80-%eb%b9%8c%eb%93%9c-%ec%98%a4%eb%a5%98/40164/</link>
					<comments>https://lycos7560.com/c/asp-net/mimekit%ea%b3%bc-mailkit-%ed%8c%a8%ed%82%a4%ec%a7%80-%eb%b9%8c%eb%93%9c-%ec%98%a4%eb%a5%98/40164/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Mon, 21 Jul 2025 02:31:45 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[ASP.NET Core MVC]]></category>
		<category><![CDATA[DI]]></category>
		<category><![CDATA[MailKit]]></category>
		<category><![CDATA[MimeKit]]></category>
		<category><![CDATA[study]]></category>
		<category><![CDATA[공부]]></category>
		<category><![CDATA[기초]]></category>
		<category><![CDATA[패키지]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40164</guid>

					<description><![CDATA[<p>MimeKit과 MailKit 패키지가 프로젝트에 정상적으로 설치되지 않았거나, NuGet 패키지를 빌드할 수 없는 상황이 발생 ✅ 1. 패키지 설치 확인 프로젝트 파일 (.csproj)에 다음이 반드시 있어야 합니다: ✅ 2. NuGet 패키지 수동 설치 (Visual Studio 없이) ✅ 3. 패키지 복원 아래 명령어로 패키지를 복원합니다: ✅ 4. MailKit 간단한 사용 예시</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/mimekit%ea%b3%bc-mailkit-%ed%8c%a8%ed%82%a4%ec%a7%80-%eb%b9%8c%eb%93%9c-%ec%98%a4%eb%a5%98/40164/">MimeKit과 MailKit 패키지 빌드 오류</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p><code>MimeKit</code>과 <code>MailKit</code> 패키지가 프로젝트에 <strong>정상적으로 설치되지 않았거나</strong>, NuGet 패키지를 빌드할 수 없는 상황이 발생</p>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 1. 패키지 설치 확인</h2>



<p><strong>프로젝트 파일 (<code>.csproj</code>)에 다음이 반드시 있어야 합니다:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">&lt;ItemGroup>
  &lt;PackageReference Include="MimeKit" Version="[버전]" />
  &lt;PackageReference Include="MailKit" Version="[버전]" />
&lt;/ItemGroup>

예시
&lt;ItemGroup>
  &lt;PackageReference Include="MimeKit" Version="4.13.0" />
  &lt;PackageReference Include="MailKit" Version="4.13.0" />
&lt;/ItemGroup></pre>



<ul class="wp-block-list">
<li>위 항목이 없다면 <code>.csproj</code> 파일에 직접 추가하거나 아래 명령어로 설치</li>
</ul>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 2. NuGet 패키지 수동 설치 (Visual Studio 없이)</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">dotnet add package MimeKit --version [버전]
dotnet add package MailKit --version [버전]


예시 
dotnet add package MimeKit --version 4.13.0
dotnet add package MailKit --version 4.13.0</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 3. 패키지 복원</h2>



<p>아래 명령어로 패키지를 복원합니다:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">dotnet restore</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 4. MailKit 간단한 사용 예시</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using MimeKit;
using MailKit.Net.Smtp;
using MailKit.Security;

namespace "namespace"
{
    public class EmailService : IEmailService
    {
        private readonly ILogger&lt;EmailService> _logger;

        public EmailService(ILogger&lt;EmailService> logger)
        {
            _logger = logger;
        }

        public async Task SendEmailAsync(string to, string subject, string body, string mimeType = "plain")
        {
            var email = new MimeMessage();
            email.From.Add(MailboxAddress.Parse("noreply@lycos7560.com"));
            email.To.Add(MailboxAddress.Parse(to));
            email.Subject = subject;

            email.Body = new TextPart(mimeType)
            {
                Text = body
            };

            using var smtp = new SmtpClient();
            try
            {
                await smtp.ConnectAsync("localhost", 포트, SecureSocketOptions.None); 
                await smtp.SendAsync(email);
                await smtp.DisconnectAsync(true);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "메일 전송 실패");
                throw;
            }
        }
    }

}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">namespace "namespace"
{
    public interface IEmailService
    {
        public Task SendEmailAsync(string to, string subject, string body, string mimeType = "plain");
    }
}
</pre>



<figure class="wp-block-image size-full"><img decoding="async" width="391" height="254" src="https://lycos7560.com/wp-content/uploads/2025/07/image-18.png" alt="" class="wp-image-40165" srcset="https://lycos7560.com/wp-content/uploads/2025/07/image-18.png 391w, https://lycos7560.com/wp-content/uploads/2025/07/image-18-300x195.png 300w" sizes="(max-width: 391px) 100vw, 391px" /></figure>



<p></p>
<p>The post <a href="https://lycos7560.com/c/asp-net/mimekit%ea%b3%bc-mailkit-%ed%8c%a8%ed%82%a4%ec%a7%80-%eb%b9%8c%eb%93%9c-%ec%98%a4%eb%a5%98/40164/">MimeKit과 MailKit 패키지 빌드 오류</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/asp-net/mimekit%ea%b3%bc-mailkit-%ed%8c%a8%ed%82%a4%ec%a7%80-%eb%b9%8c%eb%93%9c-%ec%98%a4%eb%a5%98/40164/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ASP.NET Core &#8211; UseStaticFiles</title>
		<link>https://lycos7560.com/c/asp-net/asp-net-core-usestaticfiles/40147/</link>
					<comments>https://lycos7560.com/c/asp-net/asp-net-core-usestaticfiles/40147/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Sun, 13 Jul 2025 04:33:03 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[.NET Development]]></category>
		<category><![CDATA[.NET 개발]]></category>
		<category><![CDATA[404 Not Found]]></category>
		<category><![CDATA[app.UseStaticFiles()]]></category>
		<category><![CDATA[Application Root]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[ASP.NET Core Fundamentals]]></category>
		<category><![CDATA[Backend]]></category>
		<category><![CDATA[Browser Cache]]></category>
		<category><![CDATA[Cache Invalidation]]></category>
		<category><![CDATA[Cache-Control]]></category>
		<category><![CDATA[Caching]]></category>
		<category><![CDATA[Caching Strategy]]></category>
		<category><![CDATA[Configure 메서드]]></category>
		<category><![CDATA[Content Delivery]]></category>
		<category><![CDATA[Content Type]]></category>
		<category><![CDATA[Content-Type]]></category>
		<category><![CDATA[ContentRootPath]]></category>
		<category><![CDATA[CSS]]></category>
		<category><![CDATA[Debugging]]></category>
		<category><![CDATA[Development Environment]]></category>
		<category><![CDATA[Development Purpose]]></category>
		<category><![CDATA[Directory Browse]]></category>
		<category><![CDATA[Endpoint Routing]]></category>
		<category><![CDATA[Environment]]></category>
		<category><![CDATA[Error Handling]]></category>
		<category><![CDATA[File Access]]></category>
		<category><![CDATA[File Extension]]></category>
		<category><![CDATA[file management]]></category>
		<category><![CDATA[File Path]]></category>
		<category><![CDATA[File Serving]]></category>
		<category><![CDATA[File System]]></category>
		<category><![CDATA[FileProvider]]></category>
		<category><![CDATA[Font]]></category>
		<category><![CDATA[Frontend]]></category>
		<category><![CDATA[HTML]]></category>
		<category><![CDATA[HTTP Headers]]></category>
		<category><![CDATA[HTTP Request]]></category>
		<category><![CDATA[HTTP Response]]></category>
		<category><![CDATA[HTTP 요청]]></category>
		<category><![CDATA[HTTP 응답]]></category>
		<category><![CDATA[HTTP 헤더]]></category>
		<category><![CDATA[HttpRequest]]></category>
		<category><![CDATA[HttpResponse]]></category>
		<category><![CDATA[IIS]]></category>
		<category><![CDATA[Image]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Kestrel]]></category>
		<category><![CDATA[Middleware]]></category>
		<category><![CDATA[Middleware Order]]></category>
		<category><![CDATA[Middleware Pipeline]]></category>
		<category><![CDATA[MIME Type]]></category>
		<category><![CDATA[MIME 타입]]></category>
		<category><![CDATA[Optimization]]></category>
		<category><![CDATA[Option Configuration]]></category>
		<category><![CDATA[Path Mapping]]></category>
		<category><![CDATA[Path.Combine]]></category>
		<category><![CDATA[Performance Optimization]]></category>
		<category><![CDATA[Performance Tuning]]></category>
		<category><![CDATA[Permissions]]></category>
		<category><![CDATA[Physical File]]></category>
		<category><![CDATA[PhysicalFileProvider]]></category>
		<category><![CDATA[Production Environment]]></category>
		<category><![CDATA[Program.cs]]></category>
		<category><![CDATA[Request Processing Flow]]></category>
		<category><![CDATA[RequestPath]]></category>
		<category><![CDATA[resource management]]></category>
		<category><![CDATA[Response Headers]]></category>
		<category><![CDATA[Root Folder]]></category>
		<category><![CDATA[Routing Configuration]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Security Vulnerability]]></category>
		<category><![CDATA[Server Configuration]]></category>
		<category><![CDATA[Server Resources]]></category>
		<category><![CDATA[Startup.cs]]></category>
		<category><![CDATA[Static Content]]></category>
		<category><![CDATA[Static File]]></category>
		<category><![CDATA[Static File Hosting]]></category>
		<category><![CDATA[StaticFileOptions]]></category>
		<category><![CDATA[study]]></category>
		<category><![CDATA[URL Mapping]]></category>
		<category><![CDATA[URL Path]]></category>
		<category><![CDATA[URL Redirection]]></category>
		<category><![CDATA[URL 경로]]></category>
		<category><![CDATA[URL 리디렉션]]></category>
		<category><![CDATA[URL 매핑]]></category>
		<category><![CDATA[UseDirectoryBrowser]]></category>
		<category><![CDATA[UseRouting]]></category>
		<category><![CDATA[UseStaticFiles]]></category>
		<category><![CDATA[Virtual Path]]></category>
		<category><![CDATA[Web Application]]></category>
		<category><![CDATA[Web Application Development]]></category>
		<category><![CDATA[Web Cache]]></category>
		<category><![CDATA[Web Content]]></category>
		<category><![CDATA[Web Deployment]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[Web Page]]></category>
		<category><![CDATA[Web Project]]></category>
		<category><![CDATA[Web Publishing]]></category>
		<category><![CDATA[Web Resource]]></category>
		<category><![CDATA[Web Root]]></category>
		<category><![CDATA[Web Security]]></category>
		<category><![CDATA[Web Server]]></category>
		<category><![CDATA[Web Server Configuration]]></category>
		<category><![CDATA[Web Service]]></category>
		<category><![CDATA[Web Standards]]></category>
		<category><![CDATA[WebApplication]]></category>
		<category><![CDATA[WebApplicationBuilder]]></category>
		<category><![CDATA[wwwroot]]></category>
		<category><![CDATA[가상 경로]]></category>
		<category><![CDATA[개발 목적]]></category>
		<category><![CDATA[개발 환경]]></category>
		<category><![CDATA[경로 매핑]]></category>
		<category><![CDATA[공부]]></category>
		<category><![CDATA[권한]]></category>
		<category><![CDATA[기초]]></category>
		<category><![CDATA[디렉터리 브라우징]]></category>
		<category><![CDATA[디버깅]]></category>
		<category><![CDATA[라우팅 구성]]></category>
		<category><![CDATA[루트 폴더]]></category>
		<category><![CDATA[물리적 파일]]></category>
		<category><![CDATA[미들웨어]]></category>
		<category><![CDATA[미들웨어 순서]]></category>
		<category><![CDATA[미들웨어 파이프라인]]></category>
		<category><![CDATA[백엔드]]></category>
		<category><![CDATA[보안]]></category>
		<category><![CDATA[보안 취약점]]></category>
		<category><![CDATA[브라우저 캐시]]></category>
		<category><![CDATA[서버 리소스]]></category>
		<category><![CDATA[서버 설정]]></category>
		<category><![CDATA[성능 최적화]]></category>
		<category><![CDATA[성능 튜닝]]></category>
		<category><![CDATA[애플리케이션 루트]]></category>
		<category><![CDATA[엔드포인트 라우팅]]></category>
		<category><![CDATA[오류 처리]]></category>
		<category><![CDATA[옵션 설정]]></category>
		<category><![CDATA[요청 처리 흐름]]></category>
		<category><![CDATA[운영 환경]]></category>
		<category><![CDATA[웹 개발]]></category>
		<category><![CDATA[웹 루트]]></category>
		<category><![CDATA[웹 리소스]]></category>
		<category><![CDATA[웹 배포]]></category>
		<category><![CDATA[웹 보안]]></category>
		<category><![CDATA[웹 서버]]></category>
		<category><![CDATA[웹 서버 구성]]></category>
		<category><![CDATA[웹 서비스]]></category>
		<category><![CDATA[웹 애플리케이션]]></category>
		<category><![CDATA[웹 애플리케이션 개발]]></category>
		<category><![CDATA[웹 캐시]]></category>
		<category><![CDATA[웹 콘텐츠]]></category>
		<category><![CDATA[웹 퍼블리싱]]></category>
		<category><![CDATA[웹 페이지]]></category>
		<category><![CDATA[웹 표준]]></category>
		<category><![CDATA[웹 프로젝트]]></category>
		<category><![CDATA[응답 헤더]]></category>
		<category><![CDATA[이미지]]></category>
		<category><![CDATA[자원 관리]]></category>
		<category><![CDATA[정적 콘텐츠]]></category>
		<category><![CDATA[정적 파일]]></category>
		<category><![CDATA[정적 파일 호스팅]]></category>
		<category><![CDATA[최적화]]></category>
		<category><![CDATA[캐시 무효화]]></category>
		<category><![CDATA[캐싱]]></category>
		<category><![CDATA[캐싱 전략]]></category>
		<category><![CDATA[콘텐츠 전달]]></category>
		<category><![CDATA[콘텐츠 타입]]></category>
		<category><![CDATA[파일 경로]]></category>
		<category><![CDATA[파일 관리]]></category>
		<category><![CDATA[파일 서빙]]></category>
		<category><![CDATA[파일 시스템]]></category>
		<category><![CDATA[파일 접근]]></category>
		<category><![CDATA[파일 확장자]]></category>
		<category><![CDATA[폰트]]></category>
		<category><![CDATA[프론트엔드]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40147</guid>

					<description><![CDATA[<p>🔥 UseStaticFiles https://learn.microsoft.com/ko-kr/aspnet/core/fundamentals/static-files?view=aspnetcore-8.0 UseStaticFiles는 ASP.NET Core 애플리케이션에서 정적 파일(Static Files)을 제공하기 위해 사용되는 미들웨어입니다. 정적 파일이란 웹 서버가 클라이언트에 그대로 전달하는 파일들로, 웹 페이지를 구성하는 데 필요한 이미지, CSS 파일, JavaScript 파일, 폰트 등을 의미합니다. 1️⃣ UseStaticFiles 미들웨어의 개념 ASP.NET Core는 기본적으로 보안을 위해 웹 루트(wwwroot 폴더) 외부의 파일에 대한 직접적인 웹 접근을 허용하지 않습니다. [&#8230;]</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-usestaticfiles/40147/">ASP.NET Core &#8211; UseStaticFiles</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[				<div class="wp-block-uagb-table-of-contents uagb-toc__align-left uagb-toc__columns-1  uagb-block-39abf3bb      "
					data-scroll= "1"
					data-offset= "30"
					style=""
				>
				<div class="uagb-toc__wrap">
						<div class="uagb-toc__title">
							목차						</div>
																						<div class="uagb-toc__list-wrap ">
						<ol class="uagb-toc__list"><li class="uagb-toc__list"><a href="#usestaticfiles" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> UseStaticFiles</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-usestaticfiles-미들웨어의-개념" class="uagb-toc-link__trigger">1&#x20e3; UseStaticFiles 미들웨어의 개념</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-usestaticfiles의-주요-역할" class="uagb-toc-link__trigger">2&#x20e3; UseStaticFiles의 주요 역할</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-usestaticfiles-사용-방법" class="uagb-toc-link__trigger">3&#x20e3; UseStaticFiles 사용 방법</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#기본-사용법-wwwroot-폴더" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 기본 사용법 (wwwroot 폴더)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#다른-폴더의-정적-파일-제공" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 다른 폴더의 정적 파일 제공</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#디렉터리-브라우징-활성화-권장하지-않음" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 디렉터리 브라우징 활성화 (권장하지 않음)</a></li></ul><li class="uagb-toc__list"><a href="#4-usestaticfiles-미들웨어의-위치" class="uagb-toc-link__trigger">4&#x20e3; UseStaticFiles 미들웨어의 위치</a></ul></ol>					</div>
									</div>
				</div>
			


<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> UseStaticFiles</h2>



<p><a href="https://learn.microsoft.com/ko-kr/aspnet/core/fundamentals/static-files?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/aspnet/core/fundamentals/static-files?view=aspnetcore-8.0</a></p>



<p><code>UseStaticFiles</code>는 ASP.NET Core 애플리케이션에서 <strong>정적 파일(Static Files)을 제공하기 위해 사용되는 미들웨어</strong>입니다. </p>



<p>정적 파일이란 웹 서버가 클라이언트에 그대로 전달하는 파일들로, 웹 페이지를 구성하는 데 필요한 이미지, CSS 파일, JavaScript 파일, 폰트 등을 의미합니다.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">1&#x20e3; UseStaticFiles 미들웨어의 개념</h3>



<p>ASP.NET Core는 기본적으로 보안을 위해 웹 루트(<code>wwwroot</code> 폴더) 외부의 파일에 대한 직접적인 웹 접근을 허용하지 않습니다. </p>



<p><code>UseStaticFiles</code> 미들웨어는 이러한 제한을 해제하고, 웹 서버가 특정 디렉토리(기본값은 <code>wwwroot</code>)에 있는 정적 파일 요청을 처리할 수 있도록 파이프라인에 기능을 추가합니다.</p>



<p>이 미들웨어가 없으면, 브라우저가 <code>/css/site.css</code>나 <code>/images/logo.png</code> 같은 경로로 정적 파일을 요청했을 때, 서버는 해당 파일을 찾지 못하고 404 Not Found 오류를 반환하게 됩니다. </p>



<p><code>UseStaticFiles</code>를 추가함으로써, ASP.NET Core 애플리케이션은 이러한 요청을 가로채어 지정된 위치에서 파일을 찾아 클라이언트에게 응답할 수 있게 됩니다.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2&#x20e3; UseStaticFiles의 주요 역할</h3>



<ul class="wp-block-list">
<li><strong>정적 파일 서빙 활성화</strong>: <br>웹 루트(<code>wwwroot</code>)에 있는 HTML, CSS, JavaScript, 이미지 등 정적 파일을 웹 브라우저가 요청할 수 있도록 허용합니다.</li>



<li><strong>파일 경로 매핑</strong>: <br>들어오는 URL 요청을 서버의 실제 파일 시스템 경로에 매핑합니다.</li>



<li><strong>캐싱 헤더 추가</strong>: <br>효율적인 캐싱을 위해 <code>Cache-Control</code> 같은 HTTP 응답 헤더를 자동으로 추가하여 브라우저가 정적 파일을 효율적으로 캐싱하고, 다음 요청 시 서버에 다시 요청하는 대신 로컬 캐시를 사용하도록 돕습니다.</li>



<li><strong>MIME 타입 추론</strong>: <br>파일 확장자를 기반으로 올바른 MIME 타입(<code>Content-Type</code> 헤더)을 응답에 포함하여 브라우저가 파일을 올바르게 해석하도록 합니다.</li>



<li><strong>디렉터리 브라우징 방지</strong>: <br>기본적으로 보안을 위해 디렉터리 목록을 보여주지 않습니다. (이를 허용하려면 추가 설정 필요)</li>
</ul>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">3&#x20e3; UseStaticFiles 사용 방법</h3>



<p><code>UseStaticFiles</code> 미들웨어는 보통 <code>Program.cs</code> 파일의 <code>Configure</code> 메서드에서 다른 미들웨어들과 함께 구성됩니다.</p>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 기본 사용법 (wwwroot 폴더)</h4>



<p>가장 기본적인 사용법은 웹 루트(<code>wwwroot</code>) 폴더에 있는 정적 파일을 제공하는 것입니다. </p>



<p>이 경우, <code>UseStaticFiles()</code> 메서드를 인자 없이 호출하면 됩니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages(); // 또는 AddControllersWithViews 등

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

// UseStaticFiles 미들웨어를 추가합니다.
// 이는 wwwroot 폴더의 정적 파일을 제공합니다.
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages(); // 또는 app.MapControllerRoute 등

app.Run();</pre>



<ul class="wp-block-list">
<li>위 코드에서 <code>UseStaticFiles()</code>를 호출하면, ASP.NET Core 프로젝트 템플릿에 기본으로 생성되는 <code>wwwroot</code> 폴더의 내용이 웹에 노출됩니다.</li>



<li>예를 들어, <code>wwwroot/css/site.css</code> 파일이 있다면, 브라우저에서 <code>http://localhost:포트번호/css/site.css</code>로 접근할 수 있게 됩니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 다른 폴더의 정적 파일 제공</h4>



<p><code>wwwroot</code> 외의 다른 폴더에 있는 정적 파일을 제공하고 싶을 때는 <code>StaticFileOptions</code>를 구성하여 경로를 지정할 수 있습니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// Program.cs

var builder = WebApplication.CreateBuilder(args);

// ... 서비스 설정 ...

var app = builder.Build();

// ... 기타 미들웨어 설정 ...

// wwwroot 외의 'MyStaticFiles' 폴더에 있는 정적 파일을 제공합니다.
// URL 경로는 `/MyFiles`로 시작합니다.
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
    RequestPath = "/MyFiles" // URL 프리픽스
});

// 기존 wwwroot 폴더도 계속 사용하려면 이 줄도 유지합니다.
app.UseStaticFiles();

app.UseRouting();

// ... 나머지 미들웨어 및 엔드포인트 설정 ...

app.Run();</pre>



<ul class="wp-block-list">
<li><code>FileProvider</code>: <code>PhysicalFileProvider</code>를 사용하여 서버의 실제 파일 시스템 경로를 지정합니다. <br><code>ContentRootPath</code>는 애플리케이션의 기본 디렉토리를 나타냅니다.</li>



<li><code>RequestPath</code>: 이 <code>StaticFileOptions</code> 인스턴스에 의해 제공되는 파일에 접근하기 위한 URL 프리픽스를 정의합니다. <br>위 예시에서는 <code>MyStaticFiles</code> 폴더에 있는 <code>image.png</code> 파일을 <code>http://localhost:포트번호/MyFiles/image.png</code>로 접근할 수 있습니다.</li>



<li><code>UseStaticFiles()</code>를 여러 번 호출하여 여러 정적 파일 소스를 구성할 수 있습니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 디렉터리 브라우징 활성화 (권장하지 않음)</h4>



<p>보안상의 이유로 기본적으로 비활성화되어 있지만, 개발 목적으로 디렉터리 목록을 웹에 노출해야 하는 경우 <code>UseDirectoryBrowser</code> 미들웨어를 함께 사용할 수 있습니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// Program.cs

var builder = WebApplication.CreateBuilder(args);

// 서비스에 디렉터리 브라우징 추가
builder.Services.AddDirectoryBrowser();

var app = builder.Build();

// ... 기타 미들웨어 설정 ...

// 'MyDirectory' 폴더를 웹에 노출하고, 해당 폴더의 내용을 `/MyFolder` URL로 브라우징할 수 있도록 합니다.
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(app.Environment.ContentRootPath, "MyDirectory")),
    RequestPath = "/MyFolder"
});

// 'MyDirectory' 폴더의 정적 파일도 제공합니다.
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(app.Environment.ContentRootPath, "MyDirectory")),
    RequestPath = "/MyFolder"
});

// ... 나머지 미들웨어 및 엔드포인트 설정 ...

app.Run();</pre>



<ul class="wp-block-list">
<li><strong>경고</strong>: <code>UseDirectoryBrowser</code>는 <strong>보안 취약점</strong>이 될 수 있으므로, <strong>운영 환경에서는 절대 사용해서는 안 됩니다.</strong> <br>오직 개발/디버깅 목적으로만 제한적으로 사용해야 합니다.</li>
</ul>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">4&#x20e3; <code>UseStaticFiles</code> 미들웨어의 위치</h3>



<p>미들웨어 파이프라인에서 <code>UseStaticFiles</code>의 위치는 중요합니다.</p>



<ul class="wp-block-list">
<li>일반적으로 <code>UseStaticFiles</code>는 <code>UseRouting</code> 미들웨어 <strong>앞</strong>에 위치하는 것이 좋습니다. <br>이렇게 하면 라우팅 시스템이 복잡한 컨트롤러/엔드포인트 매칭 로직을 시작하기 전에, 정적 파일 요청을 먼저 빠르게 처리하고 응답할 수 있어 성능에 유리합니다.</li>



<li>만약 <code>UseStaticFiles</code>가 <code>UseRouting</code> 뒤에 오면, 정적 파일 요청도 라우팅 시스템의 대상이 되어 불필요한 처리 오버헤드가 발생할 수 있습니다.</li>



<li>하지만, 특정 미들웨어(예: 인증/인가 미들웨어)를 통해 정적 파일 접근을 제어하고 싶다면, 해당 미들웨어 뒤에 <code>UseStaticFiles</code>를 배치할 수도 있습니다. <br>이 경우 정적 파일 요청도 인증/인가 절차를 거치게 됩니다.</li>
</ul>



<p><code>UseStaticFiles</code> 미들웨어는 <strong>ASP.NET Core 웹 애플리케이션에서 정적인 웹 리소스를 효율적이고 안전하게 제공하는 데 필수적인 구성 요소</strong>입니다.</p>



<p></p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-usestaticfiles/40147/">ASP.NET Core &#8211; UseStaticFiles</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/asp-net/asp-net-core-usestaticfiles/40147/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Microsoft.Extensions.Hosting, 호스팅 모델(Hosting Model)</title>
		<link>https://lycos7560.com/c/microsoft-extensions-hosting-%ed%98%b8%ec%8a%a4%ed%8c%85-%eb%aa%a8%eb%8d%b8hosting-model/40109/</link>
					<comments>https://lycos7560.com/c/microsoft-extensions-hosting-%ed%98%b8%ec%8a%a4%ed%8c%85-%eb%aa%a8%eb%8d%b8hosting-model/40109/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Fri, 11 Jul 2025 14:31:34 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[AddHostedService]]></category>
		<category><![CDATA[AddScoped]]></category>
		<category><![CDATA[AddSingleton]]></category>
		<category><![CDATA[AddTransient]]></category>
		<category><![CDATA[appsettings.json]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[async]]></category>
		<category><![CDATA[Await]]></category>
		<category><![CDATA[Background Service]]></category>
		<category><![CDATA[BackgroundService]]></category>
		<category><![CDATA[Best Practices]]></category>
		<category><![CDATA[Build]]></category>
		<category><![CDATA[CancellationToken]]></category>
		<category><![CDATA[Clean Code]]></category>
		<category><![CDATA[Cloud Native]]></category>
		<category><![CDATA[Code Quality]]></category>
		<category><![CDATA[Command Line Arguments]]></category>
		<category><![CDATA[Configuration]]></category>
		<category><![CDATA[ConfigureAppConfiguration]]></category>
		<category><![CDATA[ConfigureLogging]]></category>
		<category><![CDATA[ConfigureServices]]></category>
		<category><![CDATA[Console Application]]></category>
		<category><![CDATA[Console Logger]]></category>
		<category><![CDATA[CreateDefaultBuilder]]></category>
		<category><![CDATA[Cross Platform]]></category>
		<category><![CDATA[Debug Logger]]></category>
		<category><![CDATA[Dependency Injection]]></category>
		<category><![CDATA[Design Patterns]]></category>
		<category><![CDATA[DI]]></category>
		<category><![CDATA[Enterprise Application]]></category>
		<category><![CDATA[Environment Variables]]></category>
		<category><![CDATA[Error Handling]]></category>
		<category><![CDATA[Event Log]]></category>
		<category><![CDATA[Exception Handling]]></category>
		<category><![CDATA[Extensions.Hosting]]></category>
		<category><![CDATA[Framework]]></category>
		<category><![CDATA[Generic Host]]></category>
		<category><![CDATA[Graceful Shutdown]]></category>
		<category><![CDATA[Health Checks]]></category>
		<category><![CDATA[Host]]></category>
		<category><![CDATA[Hosting Model]]></category>
		<category><![CDATA[IHost]]></category>
		<category><![CDATA[IHostBuilder]]></category>
		<category><![CDATA[IHostedService]]></category>
		<category><![CDATA[Infrastructure]]></category>
		<category><![CDATA[Integration Testing]]></category>
		<category><![CDATA[Interlocked]]></category>
		<category><![CDATA[IOptions]]></category>
		<category><![CDATA[Lifecycle Management]]></category>
		<category><![CDATA[Log Provider]]></category>
		<category><![CDATA[Logging]]></category>
		<category><![CDATA[LogLevel]]></category>
		<category><![CDATA[Maintainability]]></category>
		<category><![CDATA[Memory Management]]></category>
		<category><![CDATA[Microservices]]></category>
		<category><![CDATA[Microsoft]]></category>
		<category><![CDATA[Mocking]]></category>
		<category><![CDATA[Options Pattern]]></category>
		<category><![CDATA[Performance]]></category>
		<category><![CDATA[resource management]]></category>
		<category><![CDATA[RunAsync]]></category>
		<category><![CDATA[Scalability]]></category>
		<category><![CDATA[Service Lifetime]]></category>
		<category><![CDATA[Service Registration]]></category>
		<category><![CDATA[ServiceProvider]]></category>
		<category><![CDATA[Software Architecture]]></category>
		<category><![CDATA[SOLID Principles]]></category>
		<category><![CDATA[StartAsync]]></category>
		<category><![CDATA[StopAsync]]></category>
		<category><![CDATA[Structured Logging]]></category>
		<category><![CDATA[Task]]></category>
		<category><![CDATA[Testability]]></category>
		<category><![CDATA[Thread Safety]]></category>
		<category><![CDATA[Timer]]></category>
		<category><![CDATA[Unit Testing]]></category>
		<category><![CDATA[Windows Service]]></category>
		<category><![CDATA[Worker Service]]></category>
		<category><![CDATA[WPF Application]]></category>
		<category><![CDATA[구성 관리]]></category>
		<category><![CDATA[구조화된 로깅]]></category>
		<category><![CDATA[단위 테스트]]></category>
		<category><![CDATA[디자인 패턴]]></category>
		<category><![CDATA[로그 프로바이더]]></category>
		<category><![CDATA[로깅]]></category>
		<category><![CDATA[리소스 관리]]></category>
		<category><![CDATA[마이크로서비스]]></category>
		<category><![CDATA[메모리 관리]]></category>
		<category><![CDATA[명령줄 인수]]></category>
		<category><![CDATA[모범 사례]]></category>
		<category><![CDATA[모킹]]></category>
		<category><![CDATA[백그라운드 서비스]]></category>
		<category><![CDATA[생명 주기 관리]]></category>
		<category><![CDATA[서비스 등록]]></category>
		<category><![CDATA[서비스 생명주기]]></category>
		<category><![CDATA[성능]]></category>
		<category><![CDATA[소프트웨어 아키텍처]]></category>
		<category><![CDATA[스레드 안전성]]></category>
		<category><![CDATA[엔터프라이즈 애플리케이션]]></category>
		<category><![CDATA[예외 처리]]></category>
		<category><![CDATA[오류 처리]]></category>
		<category><![CDATA[옵션 패턴]]></category>
		<category><![CDATA[워커 서비스]]></category>
		<category><![CDATA[윈도우 서비스]]></category>
		<category><![CDATA[유지보수성]]></category>
		<category><![CDATA[의존성 주입]]></category>
		<category><![CDATA[인프라]]></category>
		<category><![CDATA[정상적인 종료]]></category>
		<category><![CDATA[제네릭 호스트]]></category>
		<category><![CDATA[코드 품질]]></category>
		<category><![CDATA[콘솔 애플리케이션]]></category>
		<category><![CDATA[크로스 플랫폼]]></category>
		<category><![CDATA[클린 코드]]></category>
		<category><![CDATA[테스트 용이성]]></category>
		<category><![CDATA[통합 테스트]]></category>
		<category><![CDATA[프레임워크]]></category>
		<category><![CDATA[헬스 체크]]></category>
		<category><![CDATA[호스팅 모델]]></category>
		<category><![CDATA[확장성]]></category>
		<category><![CDATA[환경 변수]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40109</guid>

					<description><![CDATA[<p>개요 Microsoft.Extensions.Hosting 네임스페이스는 .NET 애플리케이션의 호스팅 모델(Hosting Model)을 제공하는 핵심 구성 요소입니다. Host 클래스는 이 모델의 시작점 역할을 하며, 애플리케이션의 생명 주기(lifecycle), 구성(configuration), 의존성 주입(dependency injection, DI), 로깅(logging) 등 다양한 인프라 서비스를 중앙 집중식으로 관리합니다. 이 호스팅 모델은 ASP.NET Core 애플리케이션에서 시작되었지만, 현재는 콘솔 애플리케이션, 백그라운드 서비스, Windows 서비스, WPF 애플리케이션 등 다양한 종류의 .NET [&#8230;]</p>
<p>The post <a href="https://lycos7560.com/c/microsoft-extensions-hosting-%ed%98%b8%ec%8a%a4%ed%8c%85-%eb%aa%a8%eb%8d%b8hosting-model/40109/">Microsoft.Extensions.Hosting, 호스팅 모델(Hosting Model)</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[				<div class="wp-block-uagb-table-of-contents uagb-toc__align-left uagb-toc__columns-1  uagb-block-c19625a0      "
					data-scroll= "1"
					data-offset= "30"
					style=""
				>
				<div class="uagb-toc__wrap">
						<div class="uagb-toc__title">
							목차						</div>
																						<div class="uagb-toc__list-wrap ">
						<ol class="uagb-toc__list"><li class="uagb-toc__list"><a href="#개요" class="uagb-toc-link__trigger">개요</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-host-클래스의-목적과-역할" class="uagb-toc-link__trigger">1. Host 클래스의 목적과 역할</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-host-클래스-핵심-인터페이스-및-메서드" class="uagb-toc-link__trigger">2. Host 클래스 핵심 인터페이스 및 메서드</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-ihostbuilder-인터페이스" class="uagb-toc-link__trigger">(1) IHostBuilder 인터페이스</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-ihost-인터페이스" class="uagb-toc-link__trigger">(2) IHost 인터페이스</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-ihostedservice-인터페이스" class="uagb-toc-link__trigger">(3) IHostedService 인터페이스</a></li></ul><li class="uagb-toc__list"><a href="#3-hostcreatedefaultbuilder-메서드의-이점" class="uagb-toc-link__trigger">3. Host.CreateDefaultBuilder() 메서드의 이점</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#예시-코드-콘솔-애플리케이션에서-host-사용하기" class="uagb-toc-link__trigger">예시 코드: 콘솔 애플리케이션에서 Host 사용하기</a></ul></ul></ol>					</div>
									</div>
				</div>
			


<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">개요</h2>



<p>Microsoft.Extensions.Hosting 네임스페이스는 .NET 애플리케이션의 호스팅 모델(Hosting Model)을 제공하는 핵심 구성 요소입니다. </p>



<p>Host 클래스는 이 모델의 시작점 역할을 하며, 애플리케이션의 생명 주기(lifecycle), 구성(configuration), 의존성 주입(dependency injection, DI), 로깅(logging) 등 다양한 인프라 서비스를 중앙 집중식으로 관리합니다.</p>



<p>이 호스팅 모델은 ASP.NET Core 애플리케이션에서 시작되었지만, 현재는 콘솔 애플리케이션, 백그라운드 서비스, Windows 서비스, WPF 애플리케이션 등 다양한 종류의 .NET 애플리케이션에서 표준적인 방식으로 사용되고 있습니다.</p>



<div style="height:30px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">1. <code>Host</code> 클래스의 목적과 역할</h3>



<p><code>Host</code> 클래스는 특정 애플리케이션 유형에 구애받지 않고, 공통적인 인프라 기능을 쉽게 통합하고 관리할 수 있도록 설계되었습니다.</p>



<ul class="wp-block-list">
<li><strong>생명 주기 관리</strong>: 애플리케이션의 시작(<code>StartAsync</code>) 및 종료(<code>StopAsync</code>)를 관리하고, 이 과정에서 필요한 서비스들이 올바른 순서로 초기화되고 정리될 수 있도록 합니다.</li>



<li><strong>의존성 주입 (DI)</strong>: 애플리케이션 전반에 걸쳐 강력한 DI 컨테이너를 제공하여, 서비스 간의 느슨한 결합을 촉진하고 테스트 용이성을 향상시킵니다.</li>



<li><strong>구성 (Configuration)</strong>: <code>appsettings.json</code>, 환경 변수, 명령줄 인수 등 다양한 소스에서 설정을 로드하고 관리하는 일관된 방법을 제공합니다.</li>



<li><strong>로깅 (Logging)</strong>: 다양한 로깅 프로바이더(콘솔, 파일, 데이터베이스 등)를 쉽게 통합하고, 로깅 레벨을 유연하게 제어할 수 있도록 합니다.</li>



<li><strong>백그라운드 서비스 (IHostedService)</strong>: 장시간 실행되는 백그라운드 작업을 호스트의 생명 주기에 통합하여 관리할 수 있도록 합니다. 이는 API를 제공하지 않고 단순히 작업을 수행하는 서비스에 특히 유용합니다.</li>
</ul>



<div style="height:30px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2. <code>Host</code> 클래스 핵심 인터페이스 및 메서드</h3>



<p><code>Host</code> 클래스 자체보다는, 이와 관련된 <code>IHost</code> 및 <code>IHostBuilder</code> 인터페이스를 이해하는 것이 중요합니다.</p>



<h4 class="wp-block-heading">(1) <code>IHostBuilder</code> 인터페이스</h4>



<p><code>IHostBuilder</code>는 호스트를 <strong>구성(Configure)</strong>하는 데 사용되는 인터페이스입니다. </p>



<p><code>Host.CreateDefaultBuilder()</code> 메서드가 반환하는 객체가 바로 <strong><code>IHostBuilder</code> </strong>타입입니다. </p>



<p>이 빌더를 통해 다음을 정의할 수 있습니다:</p>



<ul class="wp-block-list">
<li><strong><code>ConfigureAppConfiguration</code></strong>: <br>설정 소스(파일, 환경 변수 등)를 추가하거나 커스터마이징합니다.</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">.ConfigureAppConfiguration((context, config) =>
{
    config.AddJsonFile("custom.json", optional: true)
          .AddEnvironmentVariables()
          .AddCommandLine(args);
})</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<ul class="wp-block-list">
<li><strong><code>ConfigureLogging</code></strong>: <br>로깅 프로바이더(콘솔, 디버그, 파일 등)를 추가하고 로깅 필터링 규칙을 정의합니다.</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">.ConfigureLogging((context, logging) =>
{
    logging.AddConsole()
           .AddDebug()
           .SetMinimumLevel(LogLevel.Information);
})</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<ul class="wp-block-list">
<li><strong><code>ConfigureServices</code></strong>: <br>의존성 주입 컨테이너에 애플리케이션의 서비스들을 등록합니다. <br><code>AddSingleton</code>, <code>AddScoped</code>, <code>AddTransient</code> 등의 메서드를 사용하여 서비스의 생명 주기를 지정할 수 있습니다.</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">.ConfigureServices((context, services) =>
{
    services.AddSingleton&lt;ISingletonService, SingletonService>();
    services.AddScoped&lt;IScopedService, ScopedService>();
    services.AddTransient&lt;ITransientService, TransientService>();
    services.AddHostedService&lt;MyBackgroundService>();
})</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<ul class="wp-block-list">
<li><strong><code>UseConsoleLifetime</code></strong>: <br>콘솔 애플리케이션의 경우, <code>Ctrl+C</code>와 같은 콘솔 시그널을 감지하여 호스트를 정상적으로 종료하도록 설정합니다. <br>(이는 <code>Host.CreateDefaultBuilder()</code>에 의해 기본적으로 포함됩니다.)</li>



<li><strong><code>Build()</code></strong>: <br><code>IHostBuilder</code>에 정의된 모든 구성을 기반으로 최종 <code>IHost</code> 인스턴스를 생성합니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading">(2) <code>IHost</code> 인터페이스</h4>



<p><code>IHost</code>는 <strong>빌드된 호스트 인스턴스</strong>를 나타내며, 실제로 애플리케이션을 실행하고 관리하는 데 사용됩니다. </p>



<p>주요 메서드는 다음과 같습니다</p>



<ul class="wp-block-list">
<li><strong><code>Services</code></strong>: <br>등록된 모든 서비스 인스턴스에 접근할 수 있는 <code>IServiceProvider</code>를 제공합니다. <br>이를 통해 런타임에 필요한 서비스를 resolve(가져오기)할 수 있습니다.</li>



<li><strong><code>StartAsync(CancellationToken cancellationToken)</code></strong>: <br>호스트를 비동기적으로 시작합니다. 이 메서드가 호출되면 등록된 모든 <code>IHostedService</code> 인스턴스의 <code>StartAsync</code> 메서드가 호출됩니다.</li>



<li><strong><code>StopAsync(CancellationToken cancellationToken)</code></strong>: <br>호스트를 비동기적으로 종료합니다. 등록된 모든 <code>IHostedService</code> 인스턴스의 <code>StopAsync</code> 메서드가 호출되어 리소스를 정리할 기회를 줍니다.</li>



<li><strong><code>RunAsync(CancellationToken cancellationToken = default)</code></strong>: <br><code>StartAsync</code>를 호출하고, 호스트가 종료될 때까지 대기한 후 <code>StopAsync</code>를 호출합니다. <br>대부분의 콘솔 애플리케이션에서 메인 실행 루프로 사용됩니다.</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading">(3) <code>IHostedService</code> 인터페이스</h4>



<p><code>IHostedService</code>는 호스트의 생명 주기에 통합되어 백그라운드 작업을 수행하는 서비스들을 정의하는 인터페이스입니다. </p>



<p><code>StartAsync</code>와 <code>StopAsync</code> 두 가지 메서드를 구현해야 합니다.</p>



<ul class="wp-block-list">
<li><strong><code>StartAsync(CancellationToken cancellationToken)</code></strong>: <br>호스트가 시작될 때 호출됩니다. 장시간 실행되는 작업을 시작하는 데 사용됩니다.</li>



<li><strong><code>StopAsync(CancellationToken cancellationToken)</code></strong>: <br>호스트가 종료될 때 호출됩니다. <code>StartAsync</code>에서 시작된 작업을 정상적으로 중지하고 리소스를 정리하는 데 사용됩니다.</li>
</ul>



<div style="height:30px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">3. <code>Host.CreateDefaultBuilder()</code> 메서드의 이점</h3>



<p>이 메서드는 많은 &#8220;관습(conventions)&#8221;을 제공하여 애플리케이션 초기 설정을 간소화하는 장점이 있습니다.</p>



<ul class="wp-block-list">
<li><strong>일관성</strong>: <br>모든 .NET 애플리케이션 유형(웹, 콘솔, 백그라운드 서비스)에서 동일한 방식으로 인프라를 설정할 수 있게 하여 코드의 일관성을 유지합니다.</li>



<li><strong>생산성</strong>: <br>설정, 로깅, DI 등 반복적인 초기화 코드를 작성할 필요 없이 바로 비즈니스 로직에 집중할 수 있도록 합니다.</li>



<li><strong>확장성</strong>: <br>기본 설정을 사용하면서도 <code>ConfigureAppConfiguration</code>, <code>ConfigureServices</code> 등의 메서드를 통해 언제든지 필요에 따라 커스터마이징하고 확장할 수 있습니다.</li>



<li><strong>테스트 용이성</strong>: <br>DI 컨테이너를 중심으로 설계되어, 단위 테스트 및 통합 테스트에서 의존성을 쉽게 Mocking하거나 대체할 수 있습니다.<br>참고: Mocking &#8211; 테스트 코드 작성 시 가짜 객체(Mock)를 만들어 실제 객체 대신 사용하는 기법</li>
</ul>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading">예시 코드: 콘솔 애플리케이션에서 <code>Host</code> 사용하기</h4>



<p>콘솔 애플리케이션에서 <code>Host</code> 클래스를 사용하여 백그라운드 서비스(카운터)를 실행하고, 설정 및 로깅을 사용하는 예제</p>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<ul class="wp-block-list">
<li><code>Microsoft.Extensions.Hosting</code> NuGet 패키지를 설치</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">dotnet add package Microsoft.Extensions.Hosting</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<ul class="wp-block-list">
<li><code>appsettings.json</code> 파일을 추가하고 내용을 작성합니다.</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AppConfig": {
    "IntervalSeconds": 2
  }
}</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<ul class="wp-block-list">
<li><code>Services/MyBackgroundService.cs</code> (커스텀 <code>IHostedService</code> 구현):</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration; // IConfiguration 주입을 위해 추가
using System;
using System.Threading;
using System.Threading.Tasks;

namespace MyConsoleApp.Services
{
    // IHostedService를 구현하여 호스트의 생명 주기에 통합될 백그라운드 서비스를 정의합니다.
    public class MyBackgroundService : IHostedService, IDisposable
    {
        private readonly ILogger&lt;MyBackgroundService> _logger;
        private readonly IConfiguration _configuration;
        private Timer? _timer;
        private int _executionCount = 0;
        private int _intervalSeconds;

        public MyBackgroundService(ILogger&lt;MyBackgroundService> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;

            // appsettings.json에서 설정 값 읽기
            _intervalSeconds = _configuration.GetValue&lt;int>("AppConfig:IntervalSeconds", 5); // 기본값 5초
        }

        // 호스트가 시작될 때 호출됩니다.
        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("MyBackgroundService가 시작되었습니다.");

            // 타이머를 설정하여 지정된 간격마다 DoWork 메서드를 호출합니다.
            _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_intervalSeconds));

            return Task.CompletedTask; // 비동기 작업 시작을 알리고 즉시 반환
        }

        // 타이머에 의해 주기적으로 호출되는 실제 작업 메서드
        private void DoWork(object? state)
        {
            var count = Interlocked.Increment(ref _executionCount);
            _logger.LogInformation(
                "MyBackgroundService가 {count}번째 작업을 수행 중입니다. 현재 시간: {time}",
                count, DateTimeOffset.Now);
        }

        // 호스트가 종료될 때 호출됩니다.
        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("MyBackgroundService가 중지되고 있습니다.");

            // 타이머를 중지하고 리소스를 해제합니다.
            // Change(Timeout.Infinite, 0)은 타이머를 비활성화하는 표준 방법입니다.
            _timer?.Change(Timeout.Infinite, 0);

            _logger.LogInformation("MyBackgroundService가 중지되었습니다.");
            return Task.CompletedTask; // 작업 완료를 알립니다.
        }

        // 리소스 해제를 위한 Dispose 메서드 (IHostedService는 IDisposable을 구현하는 경우가 많음)
        public void Dispose()
        {
            _timer?.Dispose();
        }
    }
}</pre>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<ul class="wp-block-list">
<li><code>Program.cs</code> 구현:</li>
</ul>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MyConsoleApp.Services; // MyBackgroundService를 사용하기 위해 추가
using System;
using System.Threading.Tasks;

namespace MyConsoleApp
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            Console.WriteLine("콘솔 애플리케이션 호스트가 시작됩니다. 종료하려면 Ctrl+C를 누르세요.");

            // 1. IHostBuilder 인스턴스 생성: Host.CreateDefaultBuilder()는 기본적인 구성(config, logging, DI)을 설정합니다.
            var builder = Host.CreateDefaultBuilder(args)
                // 2. 서비스 컨테이너 구성: 의존성 주입을 위한 서비스들을 등록합니다.
                .ConfigureServices((hostContext, services) =>
                {
                    // MyBackgroundService를 IHostedService로 등록합니다.
                    // 호스트가 시작될 때 StartAsync가 호출되고, 종료될 때 StopAsync가 호출됩니다.
                    services.AddHostedService&lt;MyBackgroundService>();

                    // 여기에 다른 서비스들도 등록할 수 있습니다. 예:
                    // services.AddSingleton&lt;IMyService, MyService>();
                    // services.AddScoped&lt;IOtherService, OtherService>();
                })
                // 3. 로깅 구성 (선택 사항: CreateDefaultBuilder가 기본 설정 제공하지만, 커스터마이징 가능)
                .ConfigureLogging(logging =>
                {
                    logging.ClearProviders(); // 기본 로깅 프로바이더를 지우고 새로 추가하거나
                    logging.AddConsole();    // 콘솔 로거를 추가합니다.
                    // logging.AddDebug();    // 디버그 로거 추가
                });
                // 4. 기타 구성 (선택 사항: 파일 경로, 환경 등)
                // .UseContentRoot(Directory.GetCurrentDirectory())
                // .UseEnvironment("Development")
                // .ConfigureAppConfiguration((hostingContext, config) => {
                //    config.AddJsonFile("custom.json", optional: true);
                // });

            // 5. IHost 인스턴스 빌드: 구성된 빌더를 바탕으로 실제 호스트를 생성합니다.
            var host = builder.Build();

            // 6. 호스트 실행: 등록된 모든 IHostedService를 시작하고 애플리케이션 종료를 대기합니다.
            // Ctrl+C 시그널을 감지하여 정상적으로 종료됩니다.
            await host.RunAsync();

            Console.WriteLine("콘솔 애플리케이션 호스트가 종료되었습니다.");
        }
    }
}</pre>



<p></p>



<p></p>



<p></p>



<p></p>
<p>The post <a href="https://lycos7560.com/c/microsoft-extensions-hosting-%ed%98%b8%ec%8a%a4%ed%8c%85-%eb%aa%a8%eb%8d%b8hosting-model/40109/">Microsoft.Extensions.Hosting, 호스팅 모델(Hosting Model)</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/microsoft-extensions-hosting-%ed%98%b8%ec%8a%a4%ed%8c%85-%eb%aa%a8%eb%8d%b8hosting-model/40109/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ASP.NET Identity 데이터베이스</title>
		<link>https://lycos7560.com/c/asp-net/asp-net-identity-%eb%8d%b0%ec%9d%b4%ed%84%b0%eb%b2%a0%ec%9d%b4%ec%8a%a4/40160/</link>
					<comments>https://lycos7560.com/c/asp-net/asp-net-identity-%eb%8d%b0%ec%9d%b4%ed%84%b0%eb%b2%a0%ec%9d%b4%ec%8a%a4/40160/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Tue, 08 Jul 2025 23:58:43 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[.Net Core]]></category>
		<category><![CDATA[2단계인증]]></category>
		<category><![CDATA[Access Control]]></category>
		<category><![CDATA[Access Token]]></category>
		<category><![CDATA[Account Lockout]]></category>
		<category><![CDATA[Account Recovery]]></category>
		<category><![CDATA[Account Settings]]></category>
		<category><![CDATA[Admin Panel]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[ASP.NET Identity]]></category>
		<category><![CDATA[AspNetRoleClaims]]></category>
		<category><![CDATA[AspNetRoles]]></category>
		<category><![CDATA[AspNetUserClaims]]></category>
		<category><![CDATA[AspNetUserLogins]]></category>
		<category><![CDATA[AspNetUserRoles]]></category>
		<category><![CDATA[AspNetUsers]]></category>
		<category><![CDATA[AspNetUserTokens]]></category>
		<category><![CDATA[Authentication Filter]]></category>
		<category><![CDATA[Authentication Token]]></category>
		<category><![CDATA[Authorization]]></category>
		<category><![CDATA[Authorization System]]></category>
		<category><![CDATA[Authorize Attribute]]></category>
		<category><![CDATA[Brute Force Protection]]></category>
		<category><![CDATA[C# Programming]]></category>
		<category><![CDATA[Claim Type]]></category>
		<category><![CDATA[Claim Value]]></category>
		<category><![CDATA[Claims-based Authentication]]></category>
		<category><![CDATA[Composite Key]]></category>
		<category><![CDATA[Concurrency Control]]></category>
		<category><![CDATA[Custom Claims]]></category>
		<category><![CDATA[Database Design]]></category>
		<category><![CDATA[Database Tables]]></category>
		<category><![CDATA[Dynamic Permissions]]></category>
		<category><![CDATA[EF Core]]></category>
		<category><![CDATA[Email Confirmation]]></category>
		<category><![CDATA[Email Token]]></category>
		<category><![CDATA[Entity Framework]]></category>
		<category><![CDATA[External Login]]></category>
		<category><![CDATA[Facebook Login]]></category>
		<category><![CDATA[Foreign Key]]></category>
		<category><![CDATA[Google Login]]></category>
		<category><![CDATA[Grant Permission]]></category>
		<category><![CDATA[Group Permissions]]></category>
		<category><![CDATA[Hierarchical Permissions]]></category>
		<category><![CDATA[Identity Core]]></category>
		<category><![CDATA[Identity Framework]]></category>
		<category><![CDATA[JWT Token]]></category>
		<category><![CDATA[JWT토큰]]></category>
		<category><![CDATA[Login Failure]]></category>
		<category><![CDATA[Login Provider]]></category>
		<category><![CDATA[Login System]]></category>
		<category><![CDATA[Microsoft Login]]></category>
		<category><![CDATA[Middleware]]></category>
		<category><![CDATA[Multiple Roles]]></category>
		<category><![CDATA[OAuth]]></category>
		<category><![CDATA[OpenID Connect]]></category>
		<category><![CDATA[Password Hashing]]></category>
		<category><![CDATA[Password Reset]]></category>
		<category><![CDATA[Permission Check]]></category>
		<category><![CDATA[Permission Matrix]]></category>
		<category><![CDATA[Policy-based Authorization]]></category>
		<category><![CDATA[Primary Key]]></category>
		<category><![CDATA[Profile Management]]></category>
		<category><![CDATA[Provider Key]]></category>
		<category><![CDATA[Refresh Token]]></category>
		<category><![CDATA[Relational Database]]></category>
		<category><![CDATA[Role Assignment]]></category>
		<category><![CDATA[Role Hierarchy]]></category>
		<category><![CDATA[Role-based Authorization]]></category>
		<category><![CDATA[Security Filter]]></category>
		<category><![CDATA[Security Policy]]></category>
		<category><![CDATA[Security Stamp]]></category>
		<category><![CDATA[Security System]]></category>
		<category><![CDATA[Session Management]]></category>
		<category><![CDATA[study]]></category>
		<category><![CDATA[Team-based Permissions]]></category>
		<category><![CDATA[Token Management]]></category>
		<category><![CDATA[Two-Factor Authentication]]></category>
		<category><![CDATA[User Authentication]]></category>
		<category><![CDATA[User Dashboard]]></category>
		<category><![CDATA[User Interface]]></category>
		<category><![CDATA[User Management]]></category>
		<category><![CDATA[User Properties]]></category>
		<category><![CDATA[User Registration]]></category>
		<category><![CDATA[User Roles]]></category>
		<category><![CDATA[User Session]]></category>
		<category><![CDATA[User Token]]></category>
		<category><![CDATA[Web Security]]></category>
		<category><![CDATA[계정복구]]></category>
		<category><![CDATA[계정설정]]></category>
		<category><![CDATA[계정잠금]]></category>
		<category><![CDATA[계층적권한]]></category>
		<category><![CDATA[공부]]></category>
		<category><![CDATA[관계형데이터베이스]]></category>
		<category><![CDATA[관리자패널]]></category>
		<category><![CDATA[구글로그인]]></category>
		<category><![CDATA[권한검사]]></category>
		<category><![CDATA[권한관리]]></category>
		<category><![CDATA[권한매트릭스]]></category>
		<category><![CDATA[권한부여]]></category>
		<category><![CDATA[권한어트리뷰트]]></category>
		<category><![CDATA[그룹권한]]></category>
		<category><![CDATA[기본키]]></category>
		<category><![CDATA[기초]]></category>
		<category><![CDATA[다중역할]]></category>
		<category><![CDATA[데이터베이스설계]]></category>
		<category><![CDATA[데이터베이스테이블]]></category>
		<category><![CDATA[동시성제어]]></category>
		<category><![CDATA[동적권한]]></category>
		<category><![CDATA[로그인시스템]]></category>
		<category><![CDATA[로그인실패]]></category>
		<category><![CDATA[로그인제공자]]></category>
		<category><![CDATA[리프레시토큰]]></category>
		<category><![CDATA[마이크로소프트로그인]]></category>
		<category><![CDATA[미들웨어]]></category>
		<category><![CDATA[보안스탬프]]></category>
		<category><![CDATA[보안시스템]]></category>
		<category><![CDATA[보안정책]]></category>
		<category><![CDATA[보안필터]]></category>
		<category><![CDATA[복합키]]></category>
		<category><![CDATA[브루트포스방어]]></category>
		<category><![CDATA[비밀번호재설정]]></category>
		<category><![CDATA[비밀번호해싱]]></category>
		<category><![CDATA[사용자관리]]></category>
		<category><![CDATA[사용자대시보드]]></category>
		<category><![CDATA[사용자세션]]></category>
		<category><![CDATA[사용자속성]]></category>
		<category><![CDATA[사용자역할]]></category>
		<category><![CDATA[사용자인증]]></category>
		<category><![CDATA[사용자인터페이스]]></category>
		<category><![CDATA[사용자토큰]]></category>
		<category><![CDATA[세션관리]]></category>
		<category><![CDATA[액세스토큰]]></category>
		<category><![CDATA[역할계층]]></category>
		<category><![CDATA[역할기반권한]]></category>
		<category><![CDATA[역할할당]]></category>
		<category><![CDATA[외래키]]></category>
		<category><![CDATA[외부로그인]]></category>
		<category><![CDATA[웹보안]]></category>
		<category><![CDATA[이메일인증]]></category>
		<category><![CDATA[이메일토큰]]></category>
		<category><![CDATA[인가시스템]]></category>
		<category><![CDATA[인증토큰]]></category>
		<category><![CDATA[인증필터]]></category>
		<category><![CDATA[접근제어]]></category>
		<category><![CDATA[정책기반권한]]></category>
		<category><![CDATA[제공자키]]></category>
		<category><![CDATA[커스텀클레임]]></category>
		<category><![CDATA[클레임값]]></category>
		<category><![CDATA[클레임기반인증]]></category>
		<category><![CDATA[클레임타입]]></category>
		<category><![CDATA[토큰관리]]></category>
		<category><![CDATA[팀기반권한]]></category>
		<category><![CDATA[페이스북로그인]]></category>
		<category><![CDATA[프로필관리]]></category>
		<category><![CDATA[회원가입]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40160</guid>

					<description><![CDATA[<p>ASP.NET Identity는 .NET 애플리케이션에서 사용자 인증과 권한 관리를 담당하는 시스템입니다. 이 시스템은 7개의 핵심 테이블로 구성되어 있으며, 각각은 특별한 역할을 수행합니다. 🏗️ 전체 구조 개요 📋 1. AspNetUsers &#8211; 사용자 기본 정보 역할: 회원가입한 사용자들의 기본 정보를 저장하는 메인 테이블 주요 필드 설명 필드명 타입 설명 예시 Id GUID 사용자 고유 식별자 a1b2c3d4-e5f6-... UserName string [&#8230;]</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-identity-%eb%8d%b0%ec%9d%b4%ed%84%b0%eb%b2%a0%ec%9d%b4%ec%8a%a4/40160/">ASP.NET Identity 데이터베이스</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>


				<div class="wp-block-uagb-table-of-contents uagb-toc__align-left uagb-toc__columns-1  uagb-block-9e961c7c      "
					data-scroll= "1"
					data-offset= "30"
					style=""
				>
				<div class="uagb-toc__wrap">
						<div class="uagb-toc__title">
							목차						</div>
																						<div class="uagb-toc__list-wrap ">
						<ol class="uagb-toc__list"><li class="uagb-toc__list"><a href="#전체-구조-개요" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3d7.png" alt="🏗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 전체 구조 개요</a><li class="uagb-toc__list"><a href="#1-aspnetusers-사용자-기본-정보" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4cb.png" alt="📋" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 1. AspNetUsers &#8211; 사용자 기본 정보</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#주요-필드-설명" class="uagb-toc-link__trigger">주요 필드 설명</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#보안-관련-필드들" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f512.png" alt="🔒" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 보안 관련 필드들</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#실제-사용-예시" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실제 사용 예시</a></li></ul></li><li class="uagb-toc__list"><a href="#2-aspnetroles-역할-정의" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f465.png" alt="👥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 2. AspNetRoles &#8211; 역할 정의</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#주요-필드" class="uagb-toc-link__trigger">주요 필드</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#일반적인-역할-예시" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3ad.png" alt="🎭" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 일반적인 역할 예시</a></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#3-aspnetuserroles-사용자-역할-연결" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f517.png" alt="🔗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 3. AspNetUserRoles &#8211; 사용자-역할 연결</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#구조" class="uagb-toc-link__trigger">구조</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#관계-예시" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 관계 예시</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#실제-사용법" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4bb.png" alt="💻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실제 사용법</a></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#4-claims-시스템-세부-권한-관리" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3ab.png" alt="🎫" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 4. Claims 시스템 &#8211; 세부 권한 관리</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#클레임-vs-역할-비교" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f50d.png" alt="🔍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 클레임 vs 역할 비교</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#aspnetroleclaims-역할별-클레임" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3f7.png" alt="🏷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> AspNetRoleClaims &#8211; 역할별 클레임</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#aspnetuserclaims-개인별-클레임" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f464.png" alt="👤" class="wp-smiley" style="height: 1em; max-height: 1em;" /> AspNetUserClaims &#8211; 개인별 클레임</a></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#5-aspnetuserlogins-외부-로그인-연동" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f310.png" alt="🌐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 5. AspNetUserLogins &#8211; 외부 로그인 연동</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#구조" class="uagb-toc-link__trigger">구조</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#연동-과정-예시" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f504.png" alt="🔄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 연동 과정 예시</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#실제-구현" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4bb.png" alt="💻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실제 구현</a></li></ul></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#6-aspnetusertokens-인증-토큰-관리" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f39f.png" alt="🎟" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 6. AspNetUserTokens &#8211; 인증 토큰 관리</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#구조" class="uagb-toc-link__trigger">구조</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#토큰-유형별-예시" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f510.png" alt="🔐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 토큰 유형별 예시</a></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#권한-검사-실제-활용법" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 권한 검사 실제 활용법</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-컨트롤러에서-역할-기반-권한-검사" class="uagb-toc-link__trigger">1. 컨트롤러에서 역할 기반 권한 검사</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-클레임-기반-권한-검사" class="uagb-toc-link__trigger">2. 클레임 기반 권한 검사</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-코드에서-동적-권한-검사" class="uagb-toc-link__trigger">3. 코드에서 동적 권한 검사</a></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#실무-시나리오-예시" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3e2.png" alt="🏢" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실무 시나리오 예시</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#회사-내부-시스템-권한-설계" class="uagb-toc-link__trigger">회사 내부 시스템 권한 설계</a></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#테이블-간-관계-요약" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 테이블 간 관계 요약</a><li class="uagb-toc__list"><a href="#핵심-포인트" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 핵심 포인트</a></ul></ul></ul></ul></ul></ul></ul></ul></ol>					</div>
									</div>
				</div>
			


<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="wp-block-image size-full"><img decoding="async" width="1193" height="806" src="https://lycos7560.com/wp-content/uploads/2025/07/image-16.png" alt="" class="wp-image-40161" srcset="https://lycos7560.com/wp-content/uploads/2025/07/image-16.png 1193w, https://lycos7560.com/wp-content/uploads/2025/07/image-16-300x203.png 300w, https://lycos7560.com/wp-content/uploads/2025/07/image-16-768x519.png 768w" sizes="(max-width: 1193px) 100vw, 1193px" /><figcaption class="wp-element-caption">ASP.NET Core Identity 기본 템플릿 Database Diagram</figcaption></figure>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p>ASP.NET Identity는 .NET 애플리케이션에서 사용자 인증과 권한 관리를 담당하는 시스템입니다. </p>



<p>이 시스템은 7개의 핵심 테이블로 구성되어 있으며, 각각은 특별한 역할을 수행합니다.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3d7.png" alt="🏗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 전체 구조 개요</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f464.png" alt="👤" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 사용자 (AspNetUsers)
    <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2195.png" alt="↕" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 다대다 관계
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f465.png" alt="👥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 역할 (AspNetRoles)
    <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2195.png" alt="↕" class="wp-smiley" style="height: 1em; max-height: 1em;" />
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4cb.png" alt="📋" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 권한/정보 (Claims)
<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f510.png" alt="🔐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 로그인 정보 (Logins, Tokens)
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4cb.png" alt="📋" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 1. AspNetUsers &#8211; 사용자 기본 정보</h2>



<p><strong>역할</strong>: 회원가입한 사용자들의 기본 정보를 저장하는 메인 테이블</p>



<h3 class="wp-block-heading">주요 필드 설명</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>필드명</th><th>타입</th><th>설명</th><th>예시</th></tr></thead><tbody><tr><td><code>Id</code></td><td>GUID</td><td>사용자 고유 식별자</td><td><code>a1b2c3d4-e5f6-...</code></td></tr><tr><td><code>UserName</code></td><td>string</td><td>로그인 ID</td><td><code>john_doe</code></td></tr><tr><td><code>Email</code></td><td>string</td><td>이메일 주소</td><td><code>john@example.com</code></td></tr><tr><td><code>PasswordHash</code></td><td>string</td><td>암호화된 비밀번호</td><td><code>AQAAAAEAACcQ...</code></td></tr><tr><td><code>EmailConfirmed</code></td><td>bool</td><td>이메일 인증 완료 여부</td><td><code>true</code></td></tr><tr><td><code>TwoFactorEnabled</code></td><td>bool</td><td>2단계 인증 활성화 여부</td><td><code>false</code></td></tr><tr><td><code>LockoutEnd</code></td><td>DateTime?</td><td>계정 잠금 해제 시간</td><td><code>2024-12-31 23:59:59</code></td></tr><tr><td><code>AccessFailedCount</code></td><td>int</td><td>로그인 실패 횟수</td><td><code>3</code></td></tr></tbody></table></figure>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f512.png" alt="🔒" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 보안 관련 필드들</h3>



<p><strong>SecurityStamp</strong>: 보안이 변경될 때마다 갱신되는 &#8220;도장&#8221;</p>



<ul class="wp-block-list">
<li>비밀번호 변경, 이메일 변경 시 자동 갱신</li>



<li>이전 로그인 세션들을 무효화하는 역할</li>
</ul>



<p><strong>ConcurrencyStamp</strong>: 동시 수정 방지 &#8220;버전 번호&#8221;</p>



<ul class="wp-block-list">
<li>여러 사용자가 같은 계정을 동시에 수정하는 것을 방지</li>
</ul>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실제 사용 예시</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 사용자 계정 잠금 확인
if (user.LockoutEnd.HasValue &amp;&amp; user.LockoutEnd > DateTime.UtcNow)
{
    // 계정이 잠겨있음
    return "계정이 일시적으로 잠겨있습니다.";
}

// 로그인 실패 횟수 증가
user.AccessFailedCount++;
if (user.AccessFailedCount >= 5)
{
    user.LockoutEnd = DateTime.UtcNow.AddMinutes(30);
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f465.png" alt="👥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 2. AspNetRoles &#8211; 역할 정의</h2>



<p><strong>역할</strong>: 사용자들을 그룹으로 분류하는 역할(Role) 정보 저장</p>



<h3 class="wp-block-heading">주요 필드</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>필드명</th><th>설명</th><th>예시</th></tr></thead><tbody><tr><td><code>Id</code></td><td>역할 고유 식별자</td><td><code>role-admin-001</code></td></tr><tr><td><code>Name</code></td><td>역할 이름</td><td><code>Administrator</code></td></tr><tr><td><code>NormalizedName</code></td><td>검색용 정규화된 이름</td><td><code>ADMINISTRATOR</code></td></tr></tbody></table></figure>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3ad.png" alt="🎭" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 일반적인 역할 예시</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 기본 역할들
var roles = new[]
{
    new IdentityRole { Name = "SuperAdmin", NormalizedName = "SUPERADMIN" },
    new IdentityRole { Name = "Admin", NormalizedName = "ADMIN" },
    new IdentityRole { Name = "Manager", NormalizedName = "MANAGER" },
    new IdentityRole { Name = "Employee", NormalizedName = "EMPLOYEE" },
    new IdentityRole { Name = "Customer", NormalizedName = "CUSTOMER" }
};
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f517.png" alt="🔗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 3. AspNetUserRoles &#8211; 사용자-역할 연결</h2>



<p><strong>역할</strong>: 어떤 사용자가 어떤 역할을 가지는지 매핑하는 중간 테이블</p>



<h3 class="wp-block-heading">구조</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>필드명</th><th>설명</th></tr></thead><tbody><tr><td><code>UserId</code></td><td>사용자 ID (외래키)</td></tr><tr><td><code>RoleId</code></td><td>역할 ID (외래키)</td></tr></tbody></table></figure>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 관계 예시</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">사용자 "김철수" (UserId: user123)
├── Admin 역할 (RoleId: role-admin)
└── Manager 역할 (RoleId: role-manager)

사용자 "이영희" (UserId: user456)
└── Employee 역할 (RoleId: role-employee)
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4bb.png" alt="💻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실제 사용법</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 사용자에게 역할 할당
await userManager.AddToRoleAsync(user, "Admin");

// 사용자의 모든 역할 조회
var userRoles = await userManager.GetRolesAsync(user);
// 결과: ["Admin", "Manager"]

// 특정 역할 확인
bool isAdmin = await userManager.IsInRoleAsync(user, "Admin");
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3ab.png" alt="🎫" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 4. Claims 시스템 &#8211; 세부 권한 관리</h2>



<p><strong>클레임(Claim)이란?</strong> 사용자나 역할에 대한 &#8220;주장&#8221; 또는 &#8220;속성&#8221;을 나타내는 이름-값 쌍입니다.</p>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f50d.png" alt="🔍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 클레임 vs 역할 비교</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>구분</th><th>역할 (Role)</th><th>클레임 (Claim)</th></tr></thead><tbody><tr><td><strong>특징</strong></td><td>그룹 단위 분류</td><td>개별 속성/권한</td></tr><tr><td><strong>예시</strong></td><td>&#8220;관리자&#8221;, &#8220;직원&#8221;</td><td>&#8220;게시물삭제권한&#8221;, &#8220;IT부서소속&#8221;</td></tr><tr><td><strong>유연성</strong></td><td>제한적</td><td>매우 유연</td></tr></tbody></table></figure>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3f7.png" alt="🏷" class="wp-smiley" style="height: 1em; max-height: 1em;" /> AspNetRoleClaims &#8211; 역할별 클레임</h3>



<p><strong>역할</strong>: 특정 역할에 속한 모든 사용자가 공통으로 가지는 권한/속성</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>필드명</th><th>설명</th><th>예시</th></tr></thead><tbody><tr><td><code>RoleId</code></td><td>역할 ID</td><td><code>role-admin</code></td></tr><tr><td><code>ClaimType</code></td><td>클레임 유형</td><td><code>Permission</code></td></tr><tr><td><code>ClaimValue</code></td><td>클레임 값</td><td><code>CanManageUsers</code></td></tr></tbody></table></figure>



<p><strong>실제 예시:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// "Admin" 역할에 클레임 추가
var claims = new[]
{
    new Claim("Permission", "CanManageUsers"),
    new Claim("Permission", "CanDeletePosts"),
    new Claim("Permission", "CanViewReports"),
    new Claim("Department", "Management")
};

foreach (var claim in claims)
{
    await roleManager.AddClaimAsync(adminRole, claim);
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f464.png" alt="👤" class="wp-smiley" style="height: 1em; max-height: 1em;" /> AspNetUserClaims &#8211; 개인별 클레임</h3>



<p><strong>역할</strong>: 특정 사용자에게만 부여되는 개별적인 권한/속성</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>필드명</th><th>설명</th><th>예시</th></tr></thead><tbody><tr><td><code>UserId</code></td><td>사용자 ID</td><td><code>user123</code></td></tr><tr><td><code>ClaimType</code></td><td>클레임 유형</td><td><code>SpecialPermission</code></td></tr><tr><td><code>ClaimValue</code></td><td>클레임 값</td><td><code>CanAccessBetaFeatures</code></td></tr></tbody></table></figure>



<p><strong>실제 예시:</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 특정 사용자에게만 베타 기능 접근 권한 부여
var betaClaim = new Claim("Feature", "BetaAccess");
await userManager.AddClaimAsync(user, betaClaim);

// VIP 고객 표시
var vipClaim = new Claim("CustomerLevel", "VIP");
await userManager.AddClaimAsync(user, vipClaim);
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f310.png" alt="🌐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 5. AspNetUserLogins &#8211; 외부 로그인 연동</h2>



<p><strong>역할</strong>: 구글, 페이스북 등 외부 서비스를 통한 로그인 정보 저장</p>



<h3 class="wp-block-heading">구조</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>필드명</th><th>설명</th><th>예시</th></tr></thead><tbody><tr><td><code>LoginProvider</code></td><td>로그인 제공자</td><td><code>Google</code></td></tr><tr><td><code>ProviderKey</code></td><td>제공자 내 사용자 ID</td><td><code>google-user-12345</code></td></tr><tr><td><code>ProviderDisplayName</code></td><td>화면 표시명</td><td><code>구글 계정</code></td></tr><tr><td><code>UserId</code></td><td>연결된 로컬 사용자 ID</td><td><code>user123</code></td></tr></tbody></table></figure>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f504.png" alt="🔄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 연동 과정 예시</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">1. 사용자가 "구글로 로그인" 클릭
2. 구글에서 인증 완료 후 사용자 정보 반환 (ProviderKey: google-12345)
3. AspNetUserLogins 테이블에서 해당 ProviderKey 검색
4. 기존 계정이 있으면 로그인, 없으면 새 계정 생성
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4bb.png" alt="💻" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실제 구현</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 외부 로그인 정보 추가
var loginInfo = new UserLoginInfo("Google", "google-12345", "Google");
await userManager.AddLoginAsync(user, loginInfo);

// 사용자의 모든 외부 로그인 조회
var logins = await userManager.GetLoginsAsync(user);
// 결과: [{ Provider: "Google", Key: "google-12345" }, ...]
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f39f.png" alt="🎟" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 6. AspNetUserTokens &#8211; 인증 토큰 관리</h2>



<p><strong>역할</strong>: 사용자별 각종 보안 토큰 저장 (리프레시 토큰, 인증 코드 등)</p>



<h3 class="wp-block-heading">구조</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>필드명</th><th>설명</th><th>예시</th></tr></thead><tbody><tr><td><code>UserId</code></td><td>사용자 ID</td><td><code>user123</code></td></tr><tr><td><code>LoginProvider</code></td><td>토큰 발급자</td><td><code>MyApp</code></td></tr><tr><td><code>Name</code></td><td>토큰 이름</td><td><code>RefreshToken</code></td></tr><tr><td><code>Value</code></td><td>토큰 실제 값</td><td><code>abc123def456...</code></td></tr></tbody></table></figure>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f510.png" alt="🔐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 토큰 유형별 예시</h3>



<p><strong>1. 리프레시 토큰</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 리프레시 토큰 저장
await userManager.SetAuthenticationTokenAsync(
    user, "MyApp", "RefreshToken", "abc123def456ghi789");
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>2. 이메일 인증 토큰</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 이메일 인증 토큰 생성 및 저장
var emailToken = await userManager.GenerateEmailConfirmationTokenAsync(user);
// 자동으로 AspNetUserTokens에 저장됨
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>3. 비밀번호 재설정 토큰</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 비밀번호 재설정 토큰 생성
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 권한 검사 실제 활용법</h2>



<h3 class="wp-block-heading">1. 컨트롤러에서 역할 기반 권한 검사</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">[Authorize(Roles = "Admin,Manager")]
public class AdminController : Controller
{
    public IActionResult Dashboard()
    {
        return View();
    }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2. 클레임 기반 권한 검사</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">[Authorize(Policy = "CanManageUsers")]
public class UserManagementController : Controller
{
    public async Task&lt;IActionResult> DeleteUser(string userId)
    {
        // 사용자 삭제 로직
        return Ok();
    }
}

// Startup.cs에서 정책 정의
services.AddAuthorization(options =>
{
    options.AddPolicy("CanManageUsers", policy =>
        policy.RequireClaim("Permission", "CanManageUsers"));
});
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">3. 코드에서 동적 권한 검사</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public async Task&lt;IActionResult> SomeAction()
{
    // 현재 사용자가 특정 클레임을 가지고 있는지 확인
    if (User.HasClaim("Permission", "CanDeletePosts"))
    {
        // 게시물 삭제 가능
    }
    
    // 역할 확인
    if (User.IsInRole("Admin"))
    {
        // 관리자 전용 기능
    }
    
    return View();
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3e2.png" alt="🏢" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실무 시나리오 예시</h2>



<h3 class="wp-block-heading">회사 내부 시스템 권한 설계</h3>



<p><strong>1. 역할 구조</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">SuperAdmin (최고 관리자)
├── Admin (일반 관리자)
├── HRManager (인사 관리자)
├── ProjectManager (프로젝트 관리자)
├── Developer (개발자)
├── Designer (디자이너)
└── Intern (인턴)
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>2. 클레임 설계</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 부서별 클레임
ClaimType: "Department"
Values: "Engineering", "Design", "HR", "Marketing"

// 권한별 클레임
ClaimType: "Permission"
Values: "ViewSalary", "EditProject", "DeleteUser", "ViewReports"

// 레벨별 클레임
ClaimType: "Level"
Values: "Senior", "Junior", "Lead"
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>3. 실제 적용</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 사용자 "김개발"의 권한 설정
var developer = await userManager.FindByNameAsync("kim_developer");

// 역할 할당
await userManager.AddToRoleAsync(developer, "Developer");

// 개인 클레임 할당
await userManager.AddClaimAsync(developer, new Claim("Department", "Engineering"));
await userManager.AddClaimAsync(developer, new Claim("Level", "Senior"));
await userManager.AddClaimAsync(developer, new Claim("Permission", "ViewReports"));
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>4. 권한 검사</strong></p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 급여 정보는 HR 부서와 본인만 볼 수 있음
[Authorize]
public async Task&lt;IActionResult> ViewSalary(string userId)
{
    var currentUser = await userManager.GetUserAsync(User);
    
    // 본인 정보이거나 HR 부서인 경우만 허용
    if (currentUser.Id == userId || 
        User.HasClaim("Department", "HR"))
    {
        return View();
    }
    
    return Forbid();
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 테이블 간 관계 요약</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">AspNetUsers (사용자)
    ├── AspNetUserRoles → AspNetRoles (역할)
    ├── AspNetUserClaims (개인 클레임)
    ├── AspNetUserLogins (외부 로그인)
    └── AspNetUserTokens (인증 토큰)

AspNetRoles (역할)
    └── AspNetRoleClaims (역할별 클레임)
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 핵심 포인트</h2>



<ol class="wp-block-list">
<li>역할(Role)은 사용자를 <strong>그룹으로 분류</strong>하는 개념</li>



<li>클레임(Claim)은 <strong>세부적인 권한과 속성</strong>을 정의하는 개념</li>



<li><strong>역할 클레임</strong>은 해당 역할의 모든 사용자에게 적용</li>



<li><strong>사용자 클레임</strong>은 특정 사용자에게만 적용</li>



<li><strong>외부 로그인</strong>으로 여러 인증 방식 통합 가능</li>



<li><strong>토큰</strong>으로 다양한 인증 시나리오 지원</li>
</ol>



<p>이러한 구조를 통해 복잡한 권한 관리와 인증 시스템을 유연하고 안전하게 구현할 수 있습니다.</p>



<p></p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-identity-%eb%8d%b0%ec%9d%b4%ed%84%b0%eb%b2%a0%ec%9d%b4%ec%8a%a4/40160/">ASP.NET Identity 데이터베이스</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/asp-net/asp-net-identity-%eb%8d%b0%ec%9d%b4%ed%84%b0%eb%b2%a0%ec%9d%b4%ec%8a%a4/40160/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ASP.NET Core 파일 처리: FileResult 및 인터페이스</title>
		<link>https://lycos7560.com/c/asp-net/asp-net-core-%ed%8c%8c%ec%9d%bc-%ec%b2%98%eb%a6%ac-fileresult-%eb%b0%8f-%ec%9d%b8%ed%84%b0%ed%8e%98%ec%9d%b4%ec%8a%a4/40149/</link>
					<comments>https://lycos7560.com/c/asp-net/asp-net-core-%ed%8c%8c%ec%9d%bc-%ec%b2%98%eb%a6%ac-fileresult-%eb%b0%8f-%ec%9d%b8%ed%84%b0%ed%8e%98%ec%9d%b4%ec%8a%a4/40149/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Mon, 07 Jul 2025 07:59:28 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[AntivirusScan]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[Audio]]></category>
		<category><![CDATA[AWS S3]]></category>
		<category><![CDATA[AzureBlob]]></category>
		<category><![CDATA[base64]]></category>
		<category><![CDATA[BinaryData]]></category>
		<category><![CDATA[BinaryFile]]></category>
		<category><![CDATA[BlobStorage]]></category>
		<category><![CDATA[ChunkedUpload]]></category>
		<category><![CDATA[ContentType]]></category>
		<category><![CDATA[CSV]]></category>
		<category><![CDATA[Excel]]></category>
		<category><![CDATA[FileAccess]]></category>
		<category><![CDATA[FileAccessMode]]></category>
		<category><![CDATA[FileAnalytics]]></category>
		<category><![CDATA[FileApproval]]></category>
		<category><![CDATA[FileArchive]]></category>
		<category><![CDATA[FileAttributes]]></category>
		<category><![CDATA[FileBackup]]></category>
		<category><![CDATA[FileBuffering]]></category>
		<category><![CDATA[FileCache]]></category>
		<category><![CDATA[FileCDN]]></category>
		<category><![CDATA[FileChecksum]]></category>
		<category><![CDATA[FileCleanup]]></category>
		<category><![CDATA[FileCollaboration]]></category>
		<category><![CDATA[FileCompression]]></category>
		<category><![CDATA[FileConcurrency]]></category>
		<category><![CDATA[FileContentResult]]></category>
		<category><![CDATA[FileController]]></category>
		<category><![CDATA[FileConversion]]></category>
		<category><![CDATA[FileConversionAPI]]></category>
		<category><![CDATA[FileDatabase]]></category>
		<category><![CDATA[FileDirectory]]></category>
		<category><![CDATA[FileDistribution]]></category>
		<category><![CDATA[FileDownload]]></category>
		<category><![CDATA[FileEncryption]]></category>
		<category><![CDATA[FileExpiration]]></category>
		<category><![CDATA[FileExtension]]></category>
		<category><![CDATA[FileExtraction]]></category>
		<category><![CDATA[FileFormat]]></category>
		<category><![CDATA[FileHandler]]></category>
		<category><![CDATA[FileHash]]></category>
		<category><![CDATA[FileHosting]]></category>
		<category><![CDATA[FileIntegrity]]></category>
		<category><![CDATA[FileLocking]]></category>
		<category><![CDATA[FileLogging]]></category>
		<category><![CDATA[FileManagement]]></category>
		<category><![CDATA[FileMetadata]]></category>
		<category><![CDATA[FileMiddleware]]></category>
		<category><![CDATA[FileMode]]></category>
		<category><![CDATA[FileMonitoring]]></category>
		<category><![CDATA[FileNaming]]></category>
		<category><![CDATA[FileOCR]]></category>
		<category><![CDATA[FileOperations]]></category>
		<category><![CDATA[FileOptimization]]></category>
		<category><![CDATA[FileOptions]]></category>
		<category><![CDATA[FilePath]]></category>
		<category><![CDATA[FilePermissions]]></category>
		<category><![CDATA[FilePersistence]]></category>
		<category><![CDATA[FilePreview]]></category>
		<category><![CDATA[FileProgress]]></category>
		<category><![CDATA[FileRepository]]></category>
		<category><![CDATA[FileRequest]]></category>
		<category><![CDATA[FileResizing]]></category>
		<category><![CDATA[FileResponse]]></category>
		<category><![CDATA[FileResult]]></category>
		<category><![CDATA[FileResumeUpload]]></category>
		<category><![CDATA[FileSearch]]></category>
		<category><![CDATA[FileSecurity]]></category>
		<category><![CDATA[FileService]]></category>
		<category><![CDATA[FileShareMode]]></category>
		<category><![CDATA[FileSharing]]></category>
		<category><![CDATA[FileSharingLink]]></category>
		<category><![CDATA[FileSignature]]></category>
		<category><![CDATA[FileSizeLimit]]></category>
		<category><![CDATA[FileStorage]]></category>
		<category><![CDATA[Filestream]]></category>
		<category><![CDATA[FileStreamResult]]></category>
		<category><![CDATA[FileSync]]></category>
		<category><![CDATA[FileSystem]]></category>
		<category><![CDATA[FileTemporaryStorage]]></category>
		<category><![CDATA[FileThumbnail]]></category>
		<category><![CDATA[FileTracking]]></category>
		<category><![CDATA[FileUpload]]></category>
		<category><![CDATA[FileUploadLimit]]></category>
		<category><![CDATA[FileValidation]]></category>
		<category><![CDATA[FileVersioning]]></category>
		<category><![CDATA[FileVirusCheck]]></category>
		<category><![CDATA[FileWatcher]]></category>
		<category><![CDATA[FileWatermark]]></category>
		<category><![CDATA[FileWorkflow]]></category>
		<category><![CDATA[FormFile]]></category>
		<category><![CDATA[GoogleCloudStorage]]></category>
		<category><![CDATA[IDirectoryContents]]></category>
		<category><![CDATA[IFileInfo]]></category>
		<category><![CDATA[IFileProvider]]></category>
		<category><![CDATA[Image]]></category>
		<category><![CDATA[JSON]]></category>
		<category><![CDATA[MemoryStream]]></category>
		<category><![CDATA[MIME]]></category>
		<category><![CDATA[MultipartFormData]]></category>
		<category><![CDATA[PDF]]></category>
		<category><![CDATA[PhysicalFileResult]]></category>
		<category><![CDATA[Streaming]]></category>
		<category><![CDATA[study]]></category>
		<category><![CDATA[TextFile]]></category>
		<category><![CDATA[Video]]></category>
		<category><![CDATA[VirtualFileResult]]></category>
		<category><![CDATA[Word]]></category>
		<category><![CDATA[XML]]></category>
		<category><![CDATA[ZipFile]]></category>
		<category><![CDATA[공부]]></category>
		<category><![CDATA[기초]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40149</guid>

					<description><![CDATA[<p>🔥 ASP.NET Core 파일 처리: FileResult 및 인터페이스 ASP.NET Core 애플리케이션에서 파일은 핵심적인 요소입니다. 클라이언트로부터 파일을 업로드 받거나(Upload), 서버에 있는 파일을 클라이언트에게 제공(Download/Serve)하는 등 다양한 시나리오에서 파일을 효율적이고 안전하게 다루는 것이 중요합니다. 파일을 클라이언트에게 전송하는 주요 FileResult 유형(FileContentResult, VirtualFileResult, PhysicalFileResult)의 차이점과 파일 업로드, 파일 시스템 접근, 환경 정보 제공 등 파일 관련하여 알아야 할 핵심 [&#8230;]</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-%ed%8c%8c%ec%9d%bc-%ec%b2%98%eb%a6%ac-fileresult-%eb%b0%8f-%ec%9d%b8%ed%84%b0%ed%8e%98%ec%9d%b4%ec%8a%a4/40149/">ASP.NET Core 파일 처리: FileResult 및 인터페이스</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[				<div class="wp-block-uagb-table-of-contents uagb-toc__align-left uagb-toc__columns-1  uagb-block-090fc3df      "
					data-scroll= "1"
					data-offset= "30"
					style=""
				>
				<div class="uagb-toc__wrap">
						<div class="uagb-toc__title">
							목차						</div>
																						<div class="uagb-toc__list-wrap ">
						<ol class="uagb-toc__list"><li class="uagb-toc__list"><a href="#aspnet-core-파일-처리-fileresult-및-인터페이스" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core 파일 처리: FileResult 및 인터페이스</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-file-download-및-serving을-위한-fileresult-유형" class="uagb-toc-link__trigger">1&#x20e3; File Download 및 Serving을 위한 FileResult 유형</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#filecontentresult" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> FileContentResult</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#virtualfileresult" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> VirtualFileResult</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#physicalfileresult" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> PhysicalFileResult</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#filestreamresult-스트림-기반-파일-serving" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> FileStreamResult (스트림 기반 파일 Serving)</a></li></ul><li class="uagb-toc__list"><a href="#2-파일-처리-관련-기타-핵심-인터페이스-및-개념" class="uagb-toc-link__trigger">2&#x20e3; 파일 처리 관련 기타 핵심 인터페이스 및 개념</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#iformfile-및-iformfilecollection-파일-업로드" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IFormFile 및 IFormFileCollection (파일 업로드)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#iwebhostenvironment-및-ihostenvironment-환경-정보-및-경로" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IWebHostEnvironment 및 IHostEnvironment (환경 정보 및 경로)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#ifileprovider-추상화된-파일-시스템-접근" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IFileProvider (추상화된 파일 시스템 접근)</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#ifileinfo-및-idirectorycontents-파일디렉토리-메타데이터" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IFileInfo 및 IDirectoryContents (파일/디렉토리 메타데이터)</a></li></ul><li class="uagb-toc__list"><a href="#3-고급-파일-처리-기능" class="uagb-toc-link__trigger">3&#x20e3; 고급 파일 처리 기능</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#파일-업로드-보안-강화" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 업로드 보안 강화</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#파일-시그니처-검증" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 시그니처 검증</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#커스텀-파일-검증-어트리뷰트" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 커스텀 파일 검증 어트리뷰트</a></li></ul><li class="uagb-toc__list"><a href="#대용량-파일-처리-최적화" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 대용량 파일 처리 최적화</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#스트리밍-업로드" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 스트리밍 업로드</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#range-request-지원-부분-콘텐츠-전송" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Range Request 지원 (부분 콘텐츠 전송)</a></li></ul><li class="uagb-toc__list"><a href="#임시-파일-관리-및-정리" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 임시 파일 관리 및 정리</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#임시-파일-자동-정리-서비스" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 임시 파일 자동 정리 서비스</a></li></ul><li class="uagb-toc__list"><a href="#파일-압축-및-최적화" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 압축 및 최적화</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#이미지-자동-리사이징" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 이미지 자동 리사이징</a></li></ul><li class="uagb-toc__list"><a href="#파일-메타데이터-및-추적" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 메타데이터 및 추적</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#파일-메타데이터-모델" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 메타데이터 모델</a></li></ul><li class="uagb-toc__list"><a href="#에러-처리-및-로깅" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 에러 처리 및 로깅</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#포괄적인-파일-에러-처리" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 포괄적인 파일 에러 처리</a></li></ul></li></ul><li class="uagb-toc__list"><a href="#4-파일-처리-시-보안-및-성능-고려사항" class="uagb-toc-link__trigger">4&#x20e3; 파일 처리 시 보안 및 성능 고려사항</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#보안-고려사항" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 보안 고려사항</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#성능-고려사항" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 성능 고려사항</a></li></ul><li class="uagb-toc__list"><a href="#5-프로덕션-환경-설정" class="uagb-toc-link__trigger">5&#x20e3; 프로덕션 환경 설정</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#programcs-설정-예시" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Program.cs 설정 예시</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#로깅-설정-appsettingsjson" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 로깅 설정 (appsettings.json)</a></li></ul><li class="uagb-toc__list"><a href="#6-결론" class="uagb-toc-link__trigger">6&#x20e3; 결론</a></ul></ol>					</div>
									</div>
				</div>
			


<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="wp-block-embed is-type-wp-embed"><div class="wp-block-embed__wrapper">
<blockquote class="wp-embedded-content" data-secret="cjGpApKd9k"><a href="https://lycos7560.com/c/asp-net/%eb%a6%ac%ed%94%8c%eb%a0%89%ec%85%98reflection%ec%9d%84-%ec%82%ac%ec%9a%a9%ed%95%9c-actionresult%eb%a5%bc-%ec%83%81%ec%86%8d%eb%b0%9b%eb%8a%94-%eb%aa%a8%eb%93%a0-%ed%81%b4%eb%9e%98%ec%8a%a4/40153/">리플렉션(Reflection)을 사용한 ActionResult를 상속받는 모든 클래스 찾기</a></blockquote><iframe loading="lazy" class="wp-embedded-content" sandbox="allow-scripts" security="restricted"  title="&#8220;리플렉션(Reflection)을 사용한 ActionResult를 상속받는 모든 클래스 찾기&#8221; &#8212; 어제와 내일의 나 그 사이의 이야기" src="https://lycos7560.com/c/asp-net/%eb%a6%ac%ed%94%8c%eb%a0%89%ec%85%98reflection%ec%9d%84-%ec%82%ac%ec%9a%a9%ed%95%9c-actionresult%eb%a5%bc-%ec%83%81%ec%86%8d%eb%b0%9b%eb%8a%94-%eb%aa%a8%eb%93%a0-%ed%81%b4%eb%9e%98%ec%8a%a4/40153/embed/#?secret=tEHT3JgDc3#?secret=cjGpApKd9k" data-secret="cjGpApKd9k" width="600" height="338" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>
</div></figure>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h1 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f525.png" alt="🔥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> ASP.NET Core 파일 처리: FileResult 및 인터페이스</h1>



<p>ASP.NET Core 애플리케이션에서 파일은 핵심적인 요소입니다. </p>



<p>클라이언트로부터 파일을 업로드 받거나(Upload), 서버에 있는 파일을 클라이언트에게 제공(Download/Serve)하는 등 다양한 시나리오에서 파일을 효율적이고 안전하게 다루는 것이 중요합니다. </p>



<p>파일을 클라이언트에게 전송하는 주요 <strong>FileResult 유형(FileContentResult, VirtualFileResult, PhysicalFileResult)의 차이점</strong>과</p>



<p>파일 업로드, 파일 시스템 접근, 환경 정보 제공 등 파일 관련하여 알아야 할 핵심 인터페이스와 개념들을 정리합니다.</p>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">1&#x20e3; File Download 및 Serving을 위한 FileResult 유형</h2>



<p>ASP.NET Core MVC/API에서 컨트롤러 액션 메서드가 파일을 HTTP 응답으로 스트리밍할 때 사용하는 IActionResult의 파생 클래스들입니다. </p>



<p>파일 데이터를 가져오는 방식과 경로를 해석하는 방식에 따라 구분됩니다.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> FileContentResult</h3>



<p><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.filecontentresult?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.filecontentresult?view=aspnetcore-8.0</a></p>



<p>파일의 내용이 이미 byte[] 배열 형태로 메모리에 로드되어 있을 때 사용됩니다. </p>



<p>웹 서버는 이 바이트 배열을 그대로 클라이언트에게 스트리밍합니다.</p>



<ul class="wp-block-list">
<li><strong>정의</strong>: <br>Microsoft.AspNetCore.Mvc.FileContentResult</li>



<li><strong>데이터 원본</strong>: <br>byte[] (메모리)</li>



<li><strong>메모리 사용</strong>: <br>높음. 파일의 모든 내용이 메모리에 로드되므로, <strong>큰 파일을 처리할 경우 메모리 부족 문제가 발생</strong>할 수 있습니다.</li>



<li><strong>성능</strong>: <br>작은 파일이나 동적으로 생성된 파일에 적합합니다. 대용량 파일에는 비효율적입니다.</li>
</ul>



<p><strong>주요 사용처</strong>:</p>



<ul class="wp-block-list">
<li>동적으로 생성된 파일: <br>이미지 생성 라이브러리(예: System.Drawing.Common, ImageSharp)로 런타임에 생성된 이미지.</li>



<li>데이터베이스에 저장된 파일: <br>파일 데이터가 BLOB(Binary Large Object) 형태로 데이터베이스에 저장되어 있으며, 이를 byte[]로 읽어와야 하는 경우.</li>



<li>작은 파일: <br>메모리 오버헤드가 무시할 만한 수준의 작은 파일.</li>
</ul>



<p><strong>생성자 매개변수</strong>:</p>



<ul class="wp-block-list">
<li><code>byte[] fileContents</code>: 전송할 파일의 바이트 배열 데이터.</li>



<li><code>string contentType</code>: 파일의 MIME 타입 (예: &#8220;image/png&#8221;, &#8220;application/pdf&#8221;, &#8220;text/plain&#8221;).</li>



<li><code>string? fileDownloadName</code> (선택 사항): 브라우저가 파일을 다운로드할 때 제안할 파일 이름. <br>이 매개변수를 지정하면 Content-Disposition 헤더가 attachment로 설정되어 브라우저가 파일을 인라인으로 표시하는 대신 다운로드하도록 유도합니다.</li>
</ul>



<p><strong>예시</strong>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using Microsoft.AspNetCore.Mvc;
using System.Text;

public class DynamicFileController : Controller
{
    // GET: /DynamicFile/GenerateText
    public IActionResult GenerateText()
    {
        var content = "이것은 동적으로 생성된 텍스트 파일입니다. " +
                      "현재 시간: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        byte[] fileBytes = Encoding.UTF8.GetBytes(content);

        return File(fileBytes, "text/plain", "generated_text.txt");
    }

    // GET: /DynamicFile/GetImageFromDatabase
    public IActionResult GetImageFromDatabase()
    {
        // 실제 시나리오에서는 DB에서 byte[]를 읽어옵니다.
        // 여기서는 예시를 위해 더미 바이트 배열을 사용합니다.
        byte[] imageBytes = new byte[] { /* ... PNG 이미지 바이트 데이터 ... */ };

        return File(imageBytes, "image/png", "db_image.png");
    }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> VirtualFileResult</h3>



<p><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.virtualfileresult?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.virtualfileresult?view=aspnetcore-8.0</a></p>



<p><strong>웹 애플리케이션의 가상 경로(Virtual Path)</strong>를 사용하여 파일을 제공할 때 사용됩니다. </p>



<p>ASP.NET Core는 이 가상 경로를 실제 파일 시스템 경로로 변환하여 파일을 찾아 클라이언트에 스트리밍합니다.</p>



<ul class="wp-block-list">
<li><strong>정의</strong>: <br>Microsoft.AspNetCore.Mvc.VirtualFileResult</li>



<li><strong>데이터 원본</strong>: <br>웹 루트(wwwroot)를 기준으로 한 가상 경로</li>



<li><strong>메모리 사용</strong>: <br>낮음. 파일의 내용이 메모리에 한 번에 로드되지 않고, 스트림 방식으로 전송되므로 대용량 파일에도 적합합니다.</li>



<li><strong>성능</strong>: <br>효율적입니다.</li>
</ul>



<p><strong>주요 사용처</strong>:</p>



<ul class="wp-block-list">
<li>wwwroot 내의 정적 파일: 애플리케이션의 wwwroot 폴더 내에 있는 CSS, JavaScript, 이미지 등 정적 파일을 컨트롤러 액션 메서드에서 직접 제공해야 할 때 (예: 특정 인증/권한이 필요한 정적 파일).</li>



<li>StaticFileOptions로 구성된 가상 경로: UseStaticFiles 미들웨어 구성 시 RequestPath를 지정하여 만든 가상 경로에 있는 파일.</li>
</ul>



<p><strong>생성자 매개변수</strong>:</p>



<ul class="wp-block-list">
<li><code>string virtualPath</code>: 웹 루트를 기준으로 한 가상 경로 (예: ~/images/logo.png, /css/site.css).</li>



<li><code>string contentType</code>: 파일의 MIME 타입.</li>



<li><code>string? fileDownloadName</code> (선택 사항): 다운로드 시 파일 이름.</li>
</ul>



<p><strong>예시</strong>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using Microsoft.AspNetCore.Mvc;

public class StaticAssetController : Controller
{
    // GET: /StaticAsset/GetAppStyle
    public IActionResult GetAppStyle()
    {
        // wwwroot/css/app.css 파일을 제공합니다.
        return File("~/css/app.css", "text/css");
    }

    // GET: /StaticAsset/DownloadSampleDocument
    public IActionResult DownloadSampleDocument()
    {
        // wwwroot/downloads/sample.pdf 파일을 다운로드합니다.
        return File("/downloads/sample.pdf", "application/pdf", "SampleDocument.pdf");
    }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> PhysicalFileResult</h3>



<p><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.physicalfileresult?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.physicalfileresult?view=aspnetcore-8.0</a></p>



<p><strong>서버의 실제 파일 시스템 경로(Physical Path)</strong>를 사용하여 파일을 제공할 때 사용됩니다. </p>



<p>웹 애플리케이션의 <strong>루트 디렉토리를 벗어나 서버의 임의의 경로에 있는 파일을 제공해야 할 때 가장 유용</strong>합니다.</p>



<ul class="wp-block-list">
<li><strong>정의</strong>: <br>Microsoft.AspNetCore.Mvc.PhysicalFileResult</li>



<li><strong>데이터 원본</strong>: <br>서버의 실제 파일 시스템 경로 (절대 경로 또는 콘텐츠 루트 기준 상대 경로)</li>



<li><strong>메모리 사용</strong>: <br>낮음. VirtualFileResult와 마찬가지로 스트림 방식으로 전송되어 메모리 효율적입니다.</li>



<li><strong>성능</strong>: <br>효율적입니다.</li>
</ul>



<p><strong>주요 사용처</strong>:</p>



<ul class="wp-block-list">
<li>웹 루트 외부 파일: <strong>사용자가 업로드한 파일, 백업 파일, 또는 보안상의 이유로 wwwroot에 직접 노출되지 않는 폴더에 저장된 파일.</strong></li>



<li>서버의 특정 위치에 있는 파일: 애플리케이션과 독립적으로 관리되는 파일 저장소의 파일.</li>
</ul>



<p><strong>생성자 매개변수</strong>:</p>



<ul class="wp-block-list">
<li><code>string physicalPath</code>: 파일의 절대 경로 (예: &#8220;C:\Uploads\document.pdf&#8221;) 또는 애플리케이션의 콘텐츠 루트에 대한 상대 경로.</li>



<li><code>string contentType</code>: 파일의 MIME 타입.</li>



<li><code>string? fileDownloadName</code> (선택 사항): 다운로드 시 파일 이름.</li>
</ul>



<p><strong>주의사항: 사용자가 physicalPath를 직접 제어할 수 있는 경우 보안 취약점이 될 수 있습니다. 경로 유효성 검사 및 인가 처리가 필수적입니다.</strong></p>



<p><strong>예시</strong>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Hosting; // IWebHostEnvironment를 사용하기 위해 필요

public class UploadedFileController : Controller
{
    private readonly IWebHostEnvironment _env;

    public UploadedFileController(IWebHostEnvironment env)
    {
        _env = env;
    }

    // GET: /UploadedFile/Download/{fileName}
    public IActionResult Download(string fileName)
    {
        // 업로드된 파일이 ContentRootPath/UserUploads 폴더에 저장되어 있다고 가정합니다.
        var filePath = Path.Combine(_env.ContentRootPath, "UserUploads", fileName);

        if (!System.IO.File.Exists(filePath))
        {
            return NotFound();
        }

        // 파일 확장자를 기반으로 MIME 타입 결정 (실제로는 더 견고한 로직 필요)
        string contentType;
        var ext = Path.GetExtension(fileName).ToLowerInvariant();
        if (ext == ".pdf") contentType = "application/pdf";
        else if (ext == ".jpg" || ext == ".jpeg") contentType = "image/jpeg";
        else contentType = "application/octet-stream"; // 기본값: 알 수 없는 파일

        return PhysicalFile(filePath, contentType, fileName);
    }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> FileStreamResult (스트림 기반 파일 Serving)</h3>



<p><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.filestreamresult?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.filestreamresult?view=aspnetcore-8.0</a></p>



<p>FileStreamResult는 Stream 객체로부터 직접 파일을 제공할 때 사용됩니다.</p>



<p>이는 PhysicalFileResult나 VirtualFileResult와 마찬가지로 메모리 효율적이며, </p>



<p>파일이 이미 스트림 형태로 열려 있거나 외부 소스에서 스트림으로 직접 데이터를 읽어올 때 유용합니다.</p>



<ul class="wp-block-list">
<li><strong>정의</strong>: <br>Microsoft.AspNetCore.Mvc.FileStreamResult</li>



<li><strong>데이터 원본</strong>: <br>System.IO.Stream</li>



<li><strong>메모리 사용</strong>: <br>낮음. 파일 내용을 한 번에 메모리에 로드하지 않고 스트림에서 직접 읽어 전송합니다.</li>



<li><strong>성능</strong>: <br>효율적입니다.</li>
</ul>



<p><strong>주요 사용처</strong>:</p>



<ul class="wp-block-list">
<li>네트워크 스트림에서 직접 읽어온 데이터.</li>



<li>압축 파일 내부의 특정 파일을 스트림으로 추출하여 제공할 때.</li>



<li>데이터베이스에서 BLOB 데이터를 Stream으로 직접 가져올 때.</li>



<li>외부 API에서 스트림 형태로 응답을 받을 때.</li>
</ul>



<p><strong>생성자 매개변수</strong>:</p>



<ul class="wp-block-list">
<li><code>Stream fileStream</code>: 전송할 파일의 내용을 담고 있는 스트림.</li>



<li><code>string contentType</code>: 파일의 MIME 타입.</li>



<li><code>string? fileDownloadName</code> (선택 사항): 다운로드 시 파일 이름.</li>
</ul>



<p><strong>예시</strong>:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using Microsoft.AspNetCore.Mvc;
using System.IO;

public class StreamFileController : Controller
{
    // GET: /StreamFile/GetLogFile
    public IActionResult GetLogFile()
    {
        var logFilePath = Path.Combine(AppContext.BaseDirectory, "Logs", "application.log");
        if (!System.IO.File.Exists(logFilePath))
        {
            return NotFound();
        }
        // FileStream을 직접 열어 FileStreamResult로 반환
        var stream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read);
        return File(stream, "text/plain", "application.log");
    }
}
</pre>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">2&#x20e3; 파일 처리 관련 기타 핵심 인터페이스 및 개념</h2>



<p>FileResult 유형 외에도 ASP.NET Core에서 파일을 효율적이고 안전하게 다루기 위해 알아야 할 중요한 인터페이스와 개념들이 있습니다.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IFormFile 및 IFormFileCollection (파일 업로드)</h3>



<p><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.http.iformfile?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.http.iformfile?view=aspnetcore-8.0</a></p>



<p><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.http.iformfilecollection?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.http.iformfilecollection?view=aspnetcore-8.0</a></p>



<p>클라이언트(브라우저)에서 서버로 파일을 전송할 때 사용하는 인터페이스입니다.</p>



<ul class="wp-block-list">
<li><strong>IFormFile</strong>: 단일 업로드 파일을 나타냅니다.
<ul class="wp-block-list">
<li><strong>정의</strong>: Microsoft.AspNetCore.Http.IFormFile</li>



<li><strong>주요 속성</strong>: FileName, ContentType, Length.</li>



<li><strong>주요 메서드</strong>: CopyToAsync(Stream), OpenReadStream().</li>
</ul>
</li>



<li><strong>IFormFileCollection</strong>: 여러 개의 업로드 파일을 나타내는 IFormFile 객체의 컬렉션입니다.
<ul class="wp-block-list">
<li><strong>정의</strong>: Microsoft.AspNetCore.Http.IFormFileCollection</li>



<li><strong>사용 예시</strong>: <code>&lt;input type="file" multiple&gt;</code> 폼 요소로 여러 파일을 받을 때 컨트롤러 액션의 매개변수로 사용됩니다.</li>
</ul>
</li>
</ul>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IWebHostEnvironment 및 IHostEnvironment (환경 정보 및 경로)</h3>



<p><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.hosting.iwebhostenvironment?view=aspnetcore-8.0</a></p>



<p><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.components.routing.ihostenvironmentnavigationmanager?view=aspnetcore-8.0" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.components.routing.ihostenvironmentnavigationmanager?view=aspnetcore-8.0</a></p>



<p>애플리케이션의 호스팅 환경에 대한 정보를 제공하여, 파일 경로를 구성하고 접근할 때 매우 유용합니다. 이들은 DI 컨테이너에 등록되어 있어 생성자를 통해 쉽게 주입받을 수 있습니다.</p>



<ul class="wp-block-list">
<li><strong>IWebHostEnvironment</strong>: 웹 호스팅 환경에 특화된 정보를 제공합니다.
<ul class="wp-block-list">
<li><strong>정의</strong>: Microsoft.AspNetCore.Hosting.IWebHostEnvironment</li>



<li><strong>주요 속성</strong>:
<ul class="wp-block-list">
<li><code>WebRootPath</code>: 웹 루트(wwwroot) 폴더의 물리적 경로. 정적 파일이 위치하는 곳입니다.</li>



<li><code>WebRootFileProvider</code>: WebRootPath에 대한 IFileProvider 인스턴스.</li>
</ul>
</li>



<li><strong>주요 사용처</strong>: 정적 파일 저장 경로, 웹에 노출될 리소스 경로 구성.</li>
</ul>
</li>



<li><strong>IHostEnvironment</strong>: 일반적인 호스팅 환경 정보를 제공합니다. IWebHostEnvironment는 IHostEnvironment를 상속합니다.
<ul class="wp-block-list">
<li><strong>정의</strong>: Microsoft.Extensions.Hosting.IHostEnvironment</li>



<li><strong>주요 속성</strong>:
<ul class="wp-block-list">
<li><code>ContentRootPath</code>: 애플리케이션의 콘텐츠 루트 폴더(프로젝트 파일이 있는 기본 디렉토리)의 물리적 경로.</li>



<li><code>ContentRootFileProvider</code>: ContentRootPath에 대한 IFileProvider 인스턴스.</li>



<li><code>EnvironmentName</code>: 현재 실행 중인 환경의 이름 (예: &#8220;Development&#8221;, &#8220;Production&#8221;, &#8220;Staging&#8221;).</li>
</ul>
</li>



<li><strong>주요 사용처</strong>: 로그 파일, 설정 파일 등 애플리케이션이 사용하는 비-웹 관련 파일 경로 구성.</li>
</ul>
</li>
</ul>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IFileProvider (추상화된 파일 시스템 접근)</h3>



<p>파일 시스템 접근을 추상화하는 핵심 인터페이스입니다. </p>



<p>물리적인 디스크, 메모리, 압축 파일 등 다양한 소스에서 파일을 일관된 방식으로 다룰 수 있게 해줍니다.</p>



<ul class="wp-block-list">
<li><strong>정의</strong>: Microsoft.Extensions.FileProviders.IFileProvider</li>



<li><strong>주요 역할</strong>: 파일 존재 여부 확인, 디렉토리 내용 열거, 파일 내용 스트림 생성 등 파일 시스템 작업을 수행합니다. UseStaticFiles 미들웨어, Razor Pages 등에서 내부적으로 사용됩니다.</li>



<li><strong>주요 메서드</strong>:
<ul class="wp-block-list">
<li><code>GetFileInfo(string subpath)</code>: 지정된 하위 경로의 파일 또는 디렉토리에 대한 IFileInfo 객체를 반환합니다.</li>



<li><code>GetDirectoryContents(string subpath)</code>: 지정된 하위 경로의 디렉토리 콘텐츠(IDirectoryContents)를 반환합니다.</li>



<li><code>Watch(string filter)</code>: 지정된 필터와 일치하는 파일 또는 디렉토리의 변경 사항을 감시합니다 (예: 파일 변경 시 캐시 무효화).</li>
</ul>
</li>



<li><strong>주요 구현체</strong>:
<ul class="wp-block-list">
<li><code>PhysicalFileProvider</code>: 실제 파일 시스템 경로에서 파일을 제공합니다.</li>



<li><code>CompositeFileProvider</code>: 여러 IFileProvider를 결합하여 여러 위치에서 파일을 찾을 수 있게 합니다.</li>
</ul>
</li>
</ul>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IFileInfo 및 IDirectoryContents (파일/디렉토리 메타데이터)</h3>



<p><a href="https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.razor.runtimecompilation.fileproviderrazorprojectitem.fileinfo?view=aspnetcore-8.0#microsoft-aspnetcore-mvc-razor-runtimecompilation-fileproviderrazorprojectitem-fileinfo" target="_blank" rel="noreferrer noopener">https://learn.microsoft.com/ko-kr/dotnet/api/microsoft.aspnetcore.mvc.razor.runtimecompilation.fileproviderrazorprojectitem.fileinfo?view=aspnetcore-8.0#microsoft-aspnetcore-mvc-razor-runtimecompilation-fileproviderrazorprojectitem-fileinfo</a></p>



<p>IFileProvider와 함께 사용되어 파일 시스템 항목의 상세 정보를 제공합니다.</p>



<ul class="wp-block-list">
<li><strong>IFileInfo</strong>: 단일 파일 또는 디렉토리에 대한 메타데이터 (이름, 크기, 수정 시간, 존재 여부 등)를 제공합니다. IFileProvider.GetFileInfo()가 반환합니다.</li>



<li><strong>IDirectoryContents</strong>: 디렉토리 내의 파일 및 서브디렉토리 목록을 나타냅니다. IFileProvider.GetDirectoryContents()가 반환하며, IEnumerable&lt;IFileInfo&gt;를 구현합니다.</li>
</ul>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">3&#x20e3; 고급 파일 처리 기능</h2>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 업로드 보안 강화</h3>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 시그니처 검증</h4>



<p>파일 확장자만으로는 안전하지 않으며, 파일 시그니처(매직 바이트) 검증이 필요합니다.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public static class FileSignatureValidator
{
    private static readonly Dictionary&lt;string, byte[][]> FileSignatures = new()
    {
        { ".jpg", new[] { new byte[] { 0xFF, 0xD8, 0xFF } } },
        { ".jpeg", new[] { new byte[] { 0xFF, 0xD8, 0xFF } } },
        { ".png", new[] { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } },
        { ".pdf", new[] { new byte[] { 0x25, 0x50, 0x44, 0x46 } } },
        { ".gif", new[] { new byte[] { 0x47, 0x49, 0x46, 0x38 } } }
    };

    public static bool IsValidFileSignature(IFormFile file, string[] allowedExtensions)
    {
        if (file.Length == 0) return false;

        var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
        if (!allowedExtensions.Contains(ext) || !FileSignatures.ContainsKey(ext))
            return false;

        using var reader = new BinaryReader(file.OpenReadStream());
        var signatures = FileSignatures[ext];
        var headerBytes = reader.ReadBytes(signatures.Max(s => s.Length));

        return signatures.Any(signature => 
            headerBytes.Take(signature.Length).SequenceEqual(signature));
    }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 커스텀 파일 검증 어트리뷰트</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public class AllowedExtensionsAttribute : ValidationAttribute
{
    private readonly string[] _extensions;

    public AllowedExtensionsAttribute(params string[] extensions)
    {
        _extensions = extensions;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is IFormFile file)
        {
            if (!FileSignatureValidator.IsValidFileSignature(file, _extensions))
            {
                return new ValidationResult("허용되지 않는 파일 형식입니다.");
            }
        }
        return ValidationResult.Success;
    }
}

public class MaxFileSizeAttribute : ValidationAttribute
{
    private readonly int _maxFileSize;

    public MaxFileSizeAttribute(int maxFileSize)
    {
        _maxFileSize = maxFileSize;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is IFormFile file &amp;&amp; file.Length > _maxFileSize)
        {
            return new ValidationResult($"파일 크기는 {_maxFileSize / (1024 * 1024)}MB를 초과할 수 없습니다.");
        }
        return ValidationResult.Success;
    }
}

// 사용 예시
public class FileUploadModel
{
    [AllowedExtensions(".jpg", ".jpeg", ".png", ".pdf")]
    [MaxFileSize(5 * 1024 * 1024)] // 5MB
    public IFormFile UploadedFile { get; set; }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 대용량 파일 처리 최적화</h3>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 스트리밍 업로드</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">[HttpPost]
[DisableFormValueModelBinding]
public async Task&lt;IActionResult> UploadLargeFile()
{
    if (!IsMultipartContentType(Request.ContentType))
        return BadRequest("지원하지 않는 미디어 타입입니다.");

    var boundary = GetBoundary(Request.ContentType);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);
    var section = await reader.ReadNextSectionAsync();

    while (section != null)
    {
        if (ContentDispositionHeaderValue.TryParse(section.ContentDisposition, 
            out var contentDisposition))
        {
            if (HasFileContentDisposition(contentDisposition))
            {
                var fileName = contentDisposition.FileName.Value;
                var filePath = Path.Combine("uploads", fileName);
                
                using var targetStream = new FileStream(filePath, FileMode.Create);
                await section.Body.CopyToAsync(targetStream);
            }
        }
        section = await reader.ReadNextSectionAsync();
    }

    return Ok("파일 업로드 완료");
}

private static bool IsMultipartContentType(string contentType)
{
    return !string.IsNullOrEmpty(contentType) 
           &amp;&amp; contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}

private static string GetBoundary(string contentType)
{
    var elements = contentType.Split(' ');
    var element = elements.Where(entry => entry.StartsWith("boundary=")).First();
    var boundary = element.Substring("boundary=".Length);
    return HeaderUtilities.RemoveQuotes(boundary).Value;
}

private static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
    return contentDisposition != null
           &amp;&amp; contentDisposition.DispositionType.Equals("form-data")
           &amp;&amp; (!StringSegment.IsNullOrEmpty(contentDisposition.FileName)
               || !StringSegment.IsNullOrEmpty(contentDisposition.FileNameStar));
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Range Request 지원 (부분 콘텐츠 전송)</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public IActionResult DownloadWithRangeSupport(string fileName)
{
    var filePath = Path.Combine("uploads", fileName);
    if (!System.IO.File.Exists(filePath))
        return NotFound();

    var fileInfo = new FileInfo(filePath);
    var rangeHeader = Request.Headers["Range"].ToString();

    if (!string.IsNullOrEmpty(rangeHeader))
    {
        // Range 요청 처리
        var range = ParseRangeHeader(rangeHeader, fileInfo.Length);
        if (range.HasValue)
        {
            var (start, end) = range.Value;
            var contentLength = end - start + 1;

            Response.StatusCode = 206; // Partial Content
            Response.Headers.Add("Content-Range", $"bytes {start}-{end}/{fileInfo.Length}");
            Response.Headers.Add("Content-Length", contentLength.ToString());

            var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            stream.Seek(start, SeekOrigin.Begin);
            
            return File(stream, "application/octet-stream", fileName, enableRangeProcessing: true);
        }
    }

    return PhysicalFile(filePath, "application/octet-stream", fileName);
}

private (long start, long end)? ParseRangeHeader(string rangeHeader, long fileLength)
{
    if (!rangeHeader.StartsWith("bytes="))
        return null;

    var range = rangeHeader.Substring(6);
    var parts = range.Split('-');
    
    if (parts.Length != 2)
        return null;

    long start = 0, end = fileLength - 1;

    if (!string.IsNullOrEmpty(parts[0]))
        start = long.Parse(parts[0]);

    if (!string.IsNullOrEmpty(parts[1]))
        end = long.Parse(parts[1]);

    if (start > end || start >= fileLength)
        return null;

    return (start, Math.Min(end, fileLength - 1));
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 임시 파일 관리 및 정리</h3>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 임시 파일 자동 정리 서비스</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public class TempFileCleanupService : BackgroundService
{
    private readonly ILogger&lt;TempFileCleanupService> _logger;
    private readonly string _tempDirectory;

    public TempFileCleanupService(ILogger&lt;TempFileCleanupService> logger, IWebHostEnvironment env)
    {
        _logger = logger;
        _tempDirectory = Path.Combine(env.ContentRootPath, "temp");
        
        // 임시 디렉토리가 존재하지 않으면 생성
        if (!Directory.Exists(_tempDirectory))
        {
            Directory.CreateDirectory(_tempDirectory);
        }
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                CleanupOldTempFiles();
                await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "임시 파일 삭제 실패: {FilePath}", file);
            }
        }
    }
}

// Program.cs에서 서비스 등록
builder.Services.AddHostedService&lt;TempFileCleanupService>();
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 압축 및 최적화</h3>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 이미지 자동 리사이징</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats.Jpeg;

public class ImageProcessingResult : FileStreamResult
{
    public ImageProcessingResult(Stream imageStream, string contentType, int maxWidth = 800, int maxHeight = 600)
        : base(ProcessImage(imageStream, maxWidth, maxHeight), contentType)
    {
    }

    private static Stream ProcessImage(Stream originalStream, int maxWidth, int maxHeight)
    {
        using var image = Image.Load(originalStream);
        
        if (image.Width > maxWidth || image.Height > maxHeight)
        {
            image.Mutate(x => x.Resize(new ResizeOptions
            {
                Size = new Size(maxWidth, maxHeight),
                Mode = ResizeMode.Max
            }));
        }

        var outputStream = new MemoryStream();
        image.SaveAsJpeg(outputStream, new JpegEncoder { Quality = 85 });
        outputStream.Position = 0;
        
        return outputStream;
    }
}

// 사용 예시
public IActionResult GetOptimizedImage(string fileName)
{
    var filePath = Path.Combine("images", fileName);
    if (!System.IO.File.Exists(filePath))
        return NotFound();
        
    var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
    
    return new ImageProcessingResult(stream, "image/jpeg");
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 메타데이터 및 추적</h3>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 파일 메타데이터 모델</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public class FileMetadata
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string OriginalName { get; set; }
    public string StoredName { get; set; }
    public string ContentType { get; set; }
    public long Size { get; set; }
    public string Hash { get; set; } // SHA256 해시
    public DateTime UploadedAt { get; set; } = DateTime.UtcNow;
    public string UploadedBy { get; set; }
    public int DownloadCount { get; set; }
    public DateTime? LastAccessedAt { get; set; }
}

public class SecureFileService
{
    private readonly ILogger&lt;SecureFileService> _logger;
    private readonly string _uploadDirectory;
    private readonly Dictionary&lt;string, FileMetadata> _fileMetadata = new();

    public SecureFileService(ILogger&lt;SecureFileService> logger, IWebHostEnvironment env)
    {
        _logger = logger;
        _uploadDirectory = Path.Combine(env.ContentRootPath, "SecureUploads");
        
        if (!Directory.Exists(_uploadDirectory))
        {
            Directory.CreateDirectory(_uploadDirectory);
        }
    }

    public async Task&lt;FileMetadata> SaveFileAsync(IFormFile file, string userId)
    {
        // 파일 시그니처 검증
        var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".pdf", ".docx", ".xlsx" };
        if (!FileSignatureValidator.IsValidFileSignature(file, allowedExtensions))
        {
            throw new InvalidOperationException("허용되지 않는 파일 형식입니다.");
        }

        var metadata = new FileMetadata
        {
            OriginalName = file.FileName,
            StoredName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}",
            ContentType = file.ContentType,
            Size = file.Length,
            UploadedBy = userId
        };

        var filePath = Path.Combine(_uploadDirectory, metadata.StoredName);
        
        // 파일 저장
        using (var stream = new FileStream(filePath, FileMode.Create))
        {
            await file.CopyToAsync(stream);
        }

        // 파일 해시 계산
        metadata.Hash = await ComputeFileHashAsync(filePath);
        
        // 메타데이터 저장
        _fileMetadata[metadata.Id] = metadata;
        
        _logger.LogInformation("파일 저장 완료: {FileName} ({FileId})", 
            metadata.OriginalName, metadata.Id);
        
        return metadata;
    }

    public async Task&lt;(Stream stream, FileMetadata metadata)> GetFileAsync(string fileId)
    {
        if (!_fileMetadata.TryGetValue(fileId, out var metadata))
        {
            throw new FileNotFoundException("파일을 찾을 수 없습니다.");
        }

        var filePath = Path.Combine(_uploadDirectory, metadata.StoredName);
        if (!System.IO.File.Exists(filePath))
        {
            throw new FileNotFoundException("물리적 파일이 존재하지 않습니다.");
        }

        // 다운로드 통계 업데이트
        metadata.DownloadCount++;
        metadata.LastAccessedAt = DateTime.UtcNow;

        var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
        return (stream, metadata);
    }

    private async Task&lt;string> ComputeFileHashAsync(string filePath)
    {
        using var sha256 = SHA256.Create();
        using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
        var hash = await sha256.ComputeHashAsync(stream);
        return Convert.ToBase64String(hash);
    }

    public bool DeleteFile(string fileId, string userId)
    {
        if (!_fileMetadata.TryGetValue(fileId, out var metadata))
        {
            return false;
        }

        // 권한 확인
        if (metadata.UploadedBy != userId)
        {
            throw new UnauthorizedAccessException("파일 삭제 권한이 없습니다.");
        }

        var filePath = Path.Combine(_uploadDirectory, metadata.StoredName);
        
        try
        {
            if (System.IO.File.Exists(filePath))
            {
                System.IO.File.Delete(filePath);
            }
            
            _fileMetadata.Remove(fileId);
            
            _logger.LogInformation("파일 삭제 완료: {FileName} ({FileId})", 
                metadata.OriginalName, fileId);
            
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "파일 삭제 실패: {FileName} ({FileId})", 
                metadata.OriginalName, fileId);
            return false;
        }
    }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 에러 처리 및 로깅</h3>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 포괄적인 파일 에러 처리</h4>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public class FileController : Controller
{
    private readonly ILogger&lt;FileController> _logger;
    private readonly SecureFileService _fileService;

    public FileController(ILogger&lt;FileController> logger, SecureFileService fileService)
    {
        _logger = logger;
        _fileService = fileService;
    }

    [HttpPost]
    public async Task&lt;IActionResult> Upload(IFormFile file)
    {
        try
        {
            if (file == null || file.Length == 0)
                return BadRequest(new { error = "파일이 선택되지 않았습니다." });

            if (file.Length > 10 * 1024 * 1024) // 10MB
                return BadRequest(new { error = "파일 크기가 10MB를 초과합니다." });

            // 파일명 검증 (경로 조작 공격 방지)
            var fileName = Path.GetFileName(file.FileName);
            if (string.IsNullOrEmpty(fileName) || fileName.Contains(".."))
                return BadRequest(new { error = "유효하지 않은 파일명입니다." });

            var result = await _fileService.SaveFileAsync(file, User.Identity.Name);
            
            _logger.LogInformation("파일 업로드 성공: {FileName}, 사용자: {User}, 파일ID: {FileId}", 
                file.FileName, User.Identity.Name, result.Id);

            return Ok(new { 
                fileId = result.Id, 
                fileName = result.OriginalName,
                size = result.Size,
                message = "파일 업로드 완료" 
            });
        }
        catch (InvalidOperationException ex)
        {
            _logger.LogWarning("파일 업로드 유효성 검사 실패: {FileName}, 오류: {Error}", 
                file?.FileName, ex.Message);
            return BadRequest(new { error = ex.Message });
        }
        catch (IOException ex)
        {
            _logger.LogError(ex, "파일 I/O 오류: {FileName}", file?.FileName);
            return StatusCode(500, new { error = "파일 저장 중 오류가 발생했습니다." });
        }
        catch (UnauthorizedAccessException ex)
        {
            _logger.LogError(ex, "파일 접근 권한 오류: {FileName}", file?.FileName);
            return StatusCode(500, new { error = "파일 접근 권한이 없습니다." });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "예상치 못한 오류: {FileName}", file?.FileName);
            return StatusCode(500, new { error = "파일 처리 중 오류가 발생했습니다." });
        }
    }

    [HttpGet("{fileId}")]
    public async Task&lt;IActionResult> Download(string fileId)
    {
        try
        {
            var (stream, metadata) = await _fileService.GetFileAsync(fileId);
            
            _logger.LogInformation("파일 다운로드: {FileName} ({FileId}), 사용자: {User}", 
                metadata.OriginalName, fileId, User.Identity.Name);

            return File(stream, metadata.ContentType, metadata.OriginalName);
        }
        catch (FileNotFoundException ex)
        {
            _logger.LogWarning("파일 다운로드 실패 - 파일 없음: {FileId}, 사용자: {User}", 
                fileId, User.Identity.Name);
            return NotFound(new { error = "파일을 찾을 수 없습니다." });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "파일 다운로드 오류: {FileId}", fileId);
            return StatusCode(500, new { error = "파일 다운로드 중 오류가 발생했습니다." });
        }
    }

    [HttpDelete("{fileId}")]
    public IActionResult Delete(string fileId)
    {
        try
        {
            var success = _fileService.DeleteFile(fileId, User.Identity.Name);
            
            if (!success)
                return NotFound(new { error = "파일을 찾을 수 없습니다." });

            return Ok(new { message = "파일이 삭제되었습니다." });
        }
        catch (UnauthorizedAccessException ex)
        {
            _logger.LogWarning("파일 삭제 권한 없음: {FileId}, 사용자: {User}", 
                fileId, User.Identity.Name);
            return Forbid("파일 삭제 권한이 없습니다.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "파일 삭제 오류: {FileId}", fileId);
            return StatusCode(500, new { error = "파일 삭제 중 오류가 발생했습니다." });
        }
    }
}
</pre>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">4&#x20e3; 파일 처리 시 보안 및 성능 고려사항</h2>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 보안 고려사항</h3>



<ol class="wp-block-list">
<li><strong>경로 유효성 검사</strong>: <br>PhysicalFileResult나 파일 업로드 시 사용자가 제공한 파일 이름/경로에 대해 경로 조작(Path Traversal) 공격을 방지하기 위해 철저한 유효성 검사를 수행해야 합니다. <br><code>Path.GetFullPath()</code>, <code>Path.GetFileName()</code> 등을 사용하여 안전한 경로를 구성하고, 허용된 디렉토리 내에만 파일을 저장하도록 제한해야 합니다.</li>



<li><strong>MIME 타입</strong>: <br>올바른 Content-Type을 지정하여 브라우저가 파일을 올바르게 해석하고, 잠재적인 스크립트 실행 공격을 방지합니다. <br><code>FileExtensionContentTypeProvider</code>와 같은 클래스를 사용하여 MIME 타입을 자동으로 추론하는 것이 좋습니다.</li>



<li><strong>파일 시그니처 검증</strong>: <br>파일 확장자만으로는 안전하지 않으므로, 파일의 매직 바이트(시그니처)를 검증하여 실제 파일 형식을 확인해야 합니다.</li>



<li><strong>파일 크기 제한</strong>: <br>서비스 거부 공격을 방지하기 위해 업로드 파일 크기를 제한해야 합니다.</li>



<li><strong>바이러스 스캔</strong>: <br>중요한 시스템에서는 업로드된 파일에 대한 바이러스 검사를 수행해야 합니다.</li>
</ol>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 성능 고려사항</h3>



<ol class="wp-block-list">
<li><strong>대용량 파일</strong>: <br>큰 파일은 <code>byte[]</code>로 메모리에 로드하는 <code>FileContentResult</code> 대신, 스트림 기반의 <code>VirtualFileResult</code>, <code>PhysicalFileResult</code>, <code>FileStreamResult</code>를 사용하여 메모리 사용량을 최소화해야 합니다.</li>



<li><strong>캐싱</strong>: <br><code>UseStaticFiles</code> 미들웨어의 <code>StaticFileOptions.OnPrepareResponse</code>를 사용하여 Cache-Control 헤더를 설정하거나, <code>ResponseCaching</code> 미들웨어를 사용하여 캐싱 전략을 최적화할 수 있습니다.</li>



<li><strong>응답 압축</strong>: <br><code>UseResponseCompression</code> 미들웨어를 사용하여 정적 파일 및 동적 응답을 gzip, brotli 등으로 압축하여 네트워크 대역폭 사용을 줄이고 로딩 속도를 향상시킬 수 있습니다.</li>



<li><strong>스트리밍 처리</strong>: <br>대용량 파일 업로드/다운로드 시 전체 파일을 메모리에 로드하지 않고 스트림으로 처리하여 메모리 효율성을 높입니다.</li>



<li><strong>비동기 처리</strong>: <br>파일 I/O 작업은 항상 비동기 메서드(<code>CopyToAsync</code>, <code>ReadAsync</code> 등)를 사용하여 스레드 풀을 효율적으로 활용합니다.</li>
</ol>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">5&#x20e3; 프로덕션 환경 설정</h2>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Program.cs 설정 예시</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">var builder = WebApplication.CreateBuilder(args);

// 서비스 등록
builder.Services.AddControllers();
builder.Services.AddScoped&lt;SecureFileService>();
builder.Services.AddHostedService&lt;TempFileCleanupService>();

// 파일 업로드 설정
builder.Services.Configure&lt;FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 100 * 1024 * 1024; // 100MB
    options.ValueLengthLimit = int.MaxValue;
    options.ValueCountLimit = int.MaxValue;
    options.KeyLengthLimit = int.MaxValue;
});

// 응답 압축 설정
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add&lt;BrotliCompressionProvider>();
    options.Providers.Add&lt;GzipCompressionProvider>();
});

var app = builder.Build();

// 미들웨어 파이프라인
app.UseResponseCompression();
app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        // 정적 파일 캐싱 설정
        ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=31536000");
    }
});

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 로깅 설정 (appsettings.json)</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="json" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="false" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "FileController": "Information",
      "SecureFileService": "Information",
      "TempFileCleanupService": "Information"
    }
  }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">6&#x20e3; 결론</h2>



<p>ASP.NET Core의 이러한 다양한 파일 처리 도구와 개념을 이해하고 적절히 활용하는 것은 견고하고 효율적인 웹 애플리케이션을 구축하는 데 필수적입니다. </p>



<p>특히 보안, 성능, 사용자 경험을 모두 고려한 파일 처리 시스템을 구축하기 위해서는:</p>



<ol class="wp-block-list">
<li><strong>적절한 FileResult 선택</strong>: 파일의 특성과 사용 목적에 따라 최적의 FileResult 유형을 선택</li>



<li><strong>강력한 보안 검증</strong>: 파일 시그니처 검증, 경로 조작 방지, 크기 제한 등 다층적 보안 적용</li>



<li><strong>성능 최적화</strong>: 스트리밍 처리, 캐싱, 압축 등을 통한 성능 향상</li>



<li><strong>포괄적인 에러 처리</strong>: 예상 가능한 모든 오류 상황에 대한 적절한 처리와 로깅</li>



<li><strong>사용자 경험 고려</strong>: Range Request 지원, 진행률 표시, 적절한 피드백 제공</li>
</ol>



<p>이러한 원칙들을 따라 구현하면 안전하고 효율적이며 사용자 친화적인 파일 처리 시스템을 구축할 수 있습니다.</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/asp-net-core-%ed%8c%8c%ec%9d%bc-%ec%b2%98%eb%a6%ac-fileresult-%eb%b0%8f-%ec%9d%b8%ed%84%b0%ed%8e%98%ec%9d%b4%ec%8a%a4/40149/">ASP.NET Core 파일 처리: FileResult 및 인터페이스</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/asp-net/asp-net-core-%ed%8c%8c%ec%9d%bc-%ec%b2%98%eb%a6%ac-fileresult-%eb%b0%8f-%ec%9d%b8%ed%84%b0%ed%8e%98%ec%9d%b4%ec%8a%a4/40149/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Swagger/OpenAPI for .NET</title>
		<link>https://lycos7560.com/c/asp-net/swagger-openapi-for-net/40167/</link>
					<comments>https://lycos7560.com/c/asp-net/swagger-openapi-for-net/40167/#respond</comments>
		
		<dc:creator><![CDATA[lycos7560]]></dc:creator>
		<pubDate>Wed, 18 Jun 2025 07:06:11 +0000</pubDate>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[.NET6]]></category>
		<category><![CDATA[.NET7]]></category>
		<category><![CDATA[.NET8]]></category>
		<category><![CDATA[Accept헤더]]></category>
		<category><![CDATA[API가이드라인]]></category>
		<category><![CDATA[API계약]]></category>
		<category><![CDATA[API명세]]></category>
		<category><![CDATA[API문서화]]></category>
		<category><![CDATA[API버전관리]]></category>
		<category><![CDATA[API설계]]></category>
		<category><![CDATA[API테스트]]></category>
		<category><![CDATA[Application Insights]]></category>
		<category><![CDATA[appsettings]]></category>
		<category><![CDATA[ASP.NET Core]]></category>
		<category><![CDATA[Bearer토큰]]></category>
		<category><![CDATA[CI/CD]]></category>
		<category><![CDATA[Content-Type]]></category>
		<category><![CDATA[CORS]]></category>
		<category><![CDATA[DTO패턴]]></category>
		<category><![CDATA[FluentValidation]]></category>
		<category><![CDATA[FromBody]]></category>
		<category><![CDATA[FromQuery]]></category>
		<category><![CDATA[HTTP메서드]]></category>
		<category><![CDATA[HTTP상태코드]]></category>
		<category><![CDATA[Identity]]></category>
		<category><![CDATA[IdentityServer]]></category>
		<category><![CDATA[Interactive Documentation]]></category>
		<category><![CDATA[JSON Schema]]></category>
		<category><![CDATA[JSON직렬화]]></category>
		<category><![CDATA[JWT인증]]></category>
		<category><![CDATA[NuGet패키지]]></category>
		<category><![CDATA[OAuth2]]></category>
		<category><![CDATA[OpenAPI]]></category>
		<category><![CDATA[OpenAPI Specification]]></category>
		<category><![CDATA[ProducesResponseType]]></category>
		<category><![CDATA[Program.cs]]></category>
		<category><![CDATA[REST API]]></category>
		<category><![CDATA[RESTful설계]]></category>
		<category><![CDATA[Swagger]]></category>
		<category><![CDATA[SwaggerGen]]></category>
		<category><![CDATA[SwaggerUI]]></category>
		<category><![CDATA[Swashbuckle]]></category>
		<category><![CDATA[XML주석]]></category>
		<category><![CDATA[개발도구]]></category>
		<category><![CDATA[개발생산성]]></category>
		<category><![CDATA[개발환경]]></category>
		<category><![CDATA[검색]]></category>
		<category><![CDATA[결과필터]]></category>
		<category><![CDATA[구성관리]]></category>
		<category><![CDATA[권한부여]]></category>
		<category><![CDATA[글로벌필터]]></category>
		<category><![CDATA[기초]]></category>
		<category><![CDATA[네이밍컨벤션]]></category>
		<category><![CDATA[단위테스트]]></category>
		<category><![CDATA[데이터어노테이션]]></category>
		<category><![CDATA[디버깅]]></category>
		<category><![CDATA[라우팅]]></category>
		<category><![CDATA[로깅]]></category>
		<category><![CDATA[리플렉션]]></category>
		<category><![CDATA[린팅]]></category>
		<category><![CDATA[메타데이터추출]]></category>
		<category><![CDATA[모니터링]]></category>
		<category><![CDATA[모델바인딩]]></category>
		<category><![CDATA[문서화자동화]]></category>
		<category><![CDATA[미들웨어]]></category>
		<category><![CDATA[미들웨어파이프라인]]></category>
		<category><![CDATA[백엔드개발]]></category>
		<category><![CDATA[보안설정]]></category>
		<category><![CDATA[사용량추적]]></category>
		<category><![CDATA[상태코드]]></category>
		<category><![CDATA[서비스등록]]></category>
		<category><![CDATA[성능분석]]></category>
		<category><![CDATA[성능최적화]]></category>
		<category><![CDATA[순환참조]]></category>
		<category><![CDATA[스키마생성]]></category>
		<category><![CDATA[스키마필터]]></category>
		<category><![CDATA[스테이징환경]]></category>
		<category><![CDATA[액션메서드]]></category>
		<category><![CDATA[액션필터]]></category>
		<category><![CDATA[에러핸들링]]></category>
		<category><![CDATA[역할기반접근제어]]></category>
		<category><![CDATA[예외처리]]></category>
		<category><![CDATA[오류처리]]></category>
		<category><![CDATA[요청응답]]></category>
		<category><![CDATA[웹API]]></category>
		<category><![CDATA[유효성검사]]></category>
		<category><![CDATA[응답타입]]></category>
		<category><![CDATA[의존성주입]]></category>
		<category><![CDATA[인증통합]]></category>
		<category><![CDATA[인증필터]]></category>
		<category><![CDATA[자동문서화]]></category>
		<category><![CDATA[자동화배포]]></category>
		<category><![CDATA[정렬]]></category>
		<category><![CDATA[정적분석]]></category>
		<category><![CDATA[제네릭타입]]></category>
		<category><![CDATA[지속적통합]]></category>
		<category><![CDATA[컨트롤러]]></category>
		<category><![CDATA[코드리뷰]]></category>
		<category><![CDATA[코드퍼스트]]></category>
		<category><![CDATA[코드품질]]></category>
		<category><![CDATA[클레임기반인증]]></category>
		<category><![CDATA[타입안전성]]></category>
		<category><![CDATA[텔레메트리]]></category>
		<category><![CDATA[통합테스트]]></category>
		<category><![CDATA[트러블슈팅]]></category>
		<category><![CDATA[팀협업]]></category>
		<category><![CDATA[파라미터바인딩]]></category>
		<category><![CDATA[페이징]]></category>
		<category><![CDATA[프로덕션배포]]></category>
		<category><![CDATA[프론트엔드백엔드]]></category>
		<category><![CDATA[필터링]]></category>
		<category><![CDATA[환경변수]]></category>
		<guid isPermaLink="false">https://lycos7560.com/?p=40167</guid>

					<description><![CDATA[<p>📖 Swagger(스웨거)란 무엇인가? Swagger는 현재 OpenAPI Specification (OAS)라는 이름으로 표준화된 RESTful API 명세 및 문서화 도구입니다. 2015년 SmartBear Software에서 OpenAPI Initiative에 기증한 후, 현재는 Linux Foundation 산하에서 관리되고 있습니다. 핵심 개념 🎯 Swagger가 필요한 이유 1. 개발 생산성 향상 2. 팀 협업 효율성 3. 운영 및 테스트 📦 Swashbuckle.AspNetCore 아키텍처 Swashbuckle.AspNetCore는 .NET에서 Swagger/OpenAPI를 구현하는 가장 [&#8230;]</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/swagger-openapi-for-net/40167/">Swagger/OpenAPI for .NET</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></description>
										<content:encoded><![CDATA[				<div class="wp-block-uagb-table-of-contents uagb-toc__align-left uagb-toc__columns-1  uagb-block-aee946f9      "
					data-scroll= "1"
					data-offset= "30"
					style=""
				>
				<div class="uagb-toc__wrap">
						<div class="uagb-toc__title">
							목차						</div>
																						<div class="uagb-toc__list-wrap ">
						<ol class="uagb-toc__list"><li class="uagb-toc__list"><a href="#swagger스웨거란-무엇인가" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4d6.png" alt="📖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Swagger(스웨거)란 무엇인가?</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#핵심-개념" class="uagb-toc-link__trigger">핵심 개념</a></li></ul></li><li class="uagb-toc__list"><a href="#swagger가-필요한-이유" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Swagger가 필요한 이유</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-개발-생산성-향상" class="uagb-toc-link__trigger">1. 개발 생산성 향상</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-팀-협업-효율성" class="uagb-toc-link__trigger">2. 팀 협업 효율성</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-운영-및-테스트" class="uagb-toc-link__trigger">3. 운영 및 테스트</a></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#swashbuckleaspnetcore-아키텍처" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Swashbuckle.AspNetCore 아키텍처</a><li class="uagb-toc__list"><a href="#net-6-프로젝트에서-swagger-구성" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> .NET 6+ 프로젝트에서 Swagger 구성</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-패키지-설치" class="uagb-toc-link__trigger">1. 패키지 설치</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-기본-구성-programcs" class="uagb-toc-link__trigger">2. 기본 구성 (Program.cs)</a></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#swaggergen의-동작-원리" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f50d.png" alt="🔍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> SwaggerGen의 동작 원리</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#생성되는-openapi-문서-예시" class="uagb-toc-link__trigger">생성되는 OpenAPI 문서 예시</a></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#swagger-ui-고급-활용" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4c4.png" alt="📄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Swagger UI 고급 활용</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#주요-기능" class="uagb-toc-link__trigger">주요 기능</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#커스터마이징-옵션" class="uagb-toc-link__trigger">커스터마이징 옵션</a></li></ul></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#실무-필수-확장-기능" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실무 필수 확장 기능</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-xml-주석-통합" class="uagb-toc-link__trigger">1. XML 주석 통합</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-jwt-bearer-인증-통합" class="uagb-toc-link__trigger">2. JWT Bearer 인증 통합</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-api-버전-관리" class="uagb-toc-link__trigger">3. API 버전 관리</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#4-사용자-정의-스키마-필터" class="uagb-toc-link__trigger">4. 사용자 정의 스키마 필터</a></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#성능-최적화-및-모범-사례" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 성능 최적화 및 모범 사례</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#1-프로덕션-환경-고려사항" class="uagb-toc-link__trigger">1. 프로덕션 환경 고려사항</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#2-대용량-api-문서-최적화" class="uagb-toc-link__trigger">2. 대용량 API 문서 최적화</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#3-보안-강화" class="uagb-toc-link__trigger">3. 보안 강화</a></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#트러블슈팅-가이드" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f527.png" alt="🔧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 트러블슈팅 가이드</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#일반적인-문제와-해결책" class="uagb-toc-link__trigger">일반적인 문제와 해결책</a></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#모니터링-및-분석" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4c8.png" alt="📈" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 모니터링 및 분석</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#application-insights-통합" class="uagb-toc-link__trigger">Application Insights 통합</a></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li></ul></li><li class="uagb-toc__list"><a href="#요약-및-베스트-프랙티스" class="uagb-toc-link__trigger"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4cc.png" alt="📌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 요약 및 베스트 프랙티스</a><ul class="uagb-toc__list"><li class="uagb-toc__list"><a href="#핵심-도구-정리" class="uagb-toc-link__trigger">핵심 도구 정리</a><li class="uagb-toc__list"><li class="uagb-toc__list"><a href="#개발팀을-위한-권장사항" class="uagb-toc-link__trigger">개발팀을 위한 권장사항</a></ul></ul></ul></ul></ul></ul></ul></ul></ul></ul></ol>					</div>
									</div>
				</div>
			


<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4d6.png" alt="📖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Swagger(스웨거)란 무엇인가?</h2>



<p><strong>Swagger</strong>는 현재 OpenAPI Specification (OAS)라는 이름으로 표준화된 <strong>RESTful API 명세 및 문서화 도구</strong>입니다. </p>



<p>2015년 SmartBear Software에서 OpenAPI Initiative에 기증한 후, 현재는 Linux Foundation 산하에서 관리되고 있습니다.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">핵심 개념</h3>



<ul class="wp-block-list">
<li><strong>API 명세의 표준화</strong>: REST API의 엔드포인트, 파라미터, 응답 구조를 JSON/YAML 형식으로 정의</li>



<li><strong>Code-First 접근</strong>: 소스코드로부터 API 문서를 자동 생성하여 문서와 구현 간 불일치 방지</li>



<li><strong>Interactive Documentation</strong>: 브라우저에서 직접 API를 테스트할 수 있는 UI 제공</li>
</ul>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Swagger가 필요한 이유</h2>



<h3 class="wp-block-heading">1. 개발 생산성 향상</h3>



<ul class="wp-block-list">
<li><strong>자동 문서화</strong>: 수동으로 API 문서를 작성하고 유지보수하는 비용 절약</li>



<li><strong>실시간 동기화</strong>: 코드 변경 시 문서가 자동으로 업데이트</li>



<li><strong>타입 안전성</strong>: .NET의 강타입 시스템을 활용한 정확한 스키마 생성</li>
</ul>



<h3 class="wp-block-heading">2. 팀 협업 효율성</h3>



<ul class="wp-block-list">
<li><strong>프론트엔드-백엔드 협업</strong>: 명확한 API 계약 정의를 통한 병렬 개발 지원</li>



<li><strong>API 계약 테스트</strong>: 구현 전 API 설계 검증 가능</li>



<li><strong>버전 관리</strong>: API 변경사항 추적 및 하위 호환성 관리</li>
</ul>



<h3 class="wp-block-heading">3. 운영 및 테스트</h3>



<ul class="wp-block-list">
<li><strong>통합 테스트</strong>: CI/CD 파이프라인에서 자동화된 API 테스트</li>



<li><strong>모니터링</strong>: API 사용 패턴 분석 및 성능 모니터링 기반 제공</li>
</ul>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Swashbuckle.AspNetCore 아키텍처</h2>



<p>Swashbuckle.AspNetCore는 .NET에서 Swagger/OpenAPI를 구현하는 가장 널리 사용되는 라이브러리입니다.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>패키지</th><th>역할</th><th>주요 기능</th></tr></thead><tbody><tr><td><code>Swashbuckle.AspNetCore.Swagger</code></td><td>OpenAPI 문서 생성 엔진</td><td>JSON/YAML 형식의 OpenAPI 명세 생성</td></tr><tr><td><code>Swashbuckle.AspNetCore.SwaggerGen</code></td><td>코드 분석 및 메타데이터 추출</td><td>리플렉션을 통한 컨트롤러/액션 분석</td></tr><tr><td><code>Swashbuckle.AspNetCore.SwaggerUI</code></td><td>웹 UI 렌더링</td><td>인터랙티브한 API 문서 및 테스트 인터페이스</td></tr></tbody></table></figure>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> .NET 6+ 프로젝트에서 Swagger 구성</h2>



<h3 class="wp-block-heading">1. 패키지 설치</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group=""># Package Manager Console
Install-Package Swashbuckle.AspNetCore

# .NET CLI
dotnet add package Swashbuckle.AspNetCore
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2. 기본 구성 (Program.cs)</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">var builder = WebApplication.CreateBuilder(args);

// 컨트롤러 서비스 등록
builder.Services.AddControllers();

// Swagger 서비스 등록
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "My API",
        Description = "ASP.NET Core Web API for demonstration",
        Contact = new OpenApiContact
        {
            Name = "Developer Name",
            Email = "developer@example.com"
        }
    });
});

var app = builder.Build();

// 개발 환경에서만 Swagger 활성화
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
        options.RoutePrefix = string.Empty; // 루트 경로에서 Swagger UI 제공
    });
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f50d.png" alt="🔍" class="wp-smiley" style="height: 1em; max-height: 1em;" /> SwaggerGen의 동작 원리</h2>



<p>SwaggerGen은 다음과 같은 과정을 통해 OpenAPI 문서를 생성합니다:</p>



<ol class="wp-block-list">
<li><strong>어셈블리 스캔</strong>: 등록된 컨트롤러와 액션 메서드 탐색</li>



<li><strong>메타데이터 추출</strong>: 라우트, HTTP 메서드, 파라미터, 반환 타입 분석</li>



<li><strong>스키마 생성</strong>: .NET 타입을 JSON Schema로 변환</li>



<li><strong>문서 조합</strong>: OpenAPI 3.0 사양에 맞는 JSON 문서 생성</li>
</ol>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">생성되는 OpenAPI 문서 예시</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">{
  "openapi": "3.0.1",
  "info": {
    "title": "My API",
    "version": "v1"
  },
  "paths": {
    "/api/products": {
      "get": {
        "tags": ["Products"],
        "summary": "제품 목록 조회",
        "responses": {
          "200": {
            "description": "성공",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Product"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Product": {
        "type": "object",
        "properties": {
          "id": { "type": "integer", "format": "int32" },
          "name": { "type": "string", "nullable": true }
        }
      }
    }
  }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="wp-block-image size-full"><img decoding="async" width="1378" height="1299" src="https://lycos7560.com/wp-content/uploads/2025/07/image-19.png" alt="" class="wp-image-40168" srcset="https://lycos7560.com/wp-content/uploads/2025/07/image-19.png 1378w, https://lycos7560.com/wp-content/uploads/2025/07/image-19-300x283.png 300w, https://lycos7560.com/wp-content/uploads/2025/07/image-19-768x724.png 768w" sizes="(max-width: 1378px) 100vw, 1378px" /></figure>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4c4.png" alt="📄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Swagger UI 고급 활용</h2>



<p>Swagger UI는 생성된 OpenAPI 문서를 기반으로 다음 기능을 제공합니다:</p>



<h3 class="wp-block-heading">주요 기능</h3>



<ul class="wp-block-list">
<li><strong>HTTP 메서드별 분류</strong>: GET, POST, PUT, DELETE 등 시각적 구분</li>



<li><strong>스키마 검증</strong>: 요청/응답 데이터 구조 실시간 검증</li>



<li><strong>Try it Out</strong>: 브라우저에서 직접 API 호출 및 결과 확인</li>



<li><strong>모델 정의</strong>: 복잡한 객체 구조의 시각적 표현</li>



<li><strong>인증 통합</strong>: 다양한 인증 방식 지원 (Bearer, API Key 등)</li>
</ul>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">커스터마이징 옵션</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">app.UseSwaggerUI(options =>
{
    options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    options.DocumentTitle = "My API Documentation";
    options.DefaultModelsExpandDepth(2);
    options.DefaultModelRendering(ModelRendering.Model);
    options.DisplayRequestDuration();
    options.EnableDeepLinking();
    options.EnableFilter();
    options.ShowExtensions();
});
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 실무 필수 확장 기능</h2>



<h3 class="wp-block-heading">1. XML 주석 통합</h3>



<p>프로젝트 파일 설정:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="xml" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">&lt;PropertyGroup>
  &lt;GenerateDocumentationFile>true&lt;/GenerateDocumentationFile>
  &lt;NoWarn>$(NoWarn);1591&lt;/NoWarn>
&lt;/PropertyGroup>
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Program.cs 구성:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">builder.Services.AddSwaggerGen(options =>
{
    var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
    
    // 상속된 XML 주석 포함
    options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "MyModels.xml"));
});
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p>컨트롤러 예시:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">/// &lt;summary>
/// 제품 관리 API
/// &lt;/summary>
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    /// &lt;summary>
    /// 제품 목록을 조회합니다.
    /// &lt;/summary>
    /// &lt;param name="pageSize">페이지당 항목 수 (기본값: 10)&lt;/param>
    /// &lt;returns>제품 목록&lt;/returns>
    /// &lt;response code="200">성공적으로 조회됨&lt;/response>
    /// &lt;response code="400">잘못된 요청 파라미터&lt;/response>
    [HttpGet]
    [ProducesResponseType(typeof(IEnumerable&lt;Product>), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task&lt;ActionResult&lt;IEnumerable&lt;Product>>> GetProducts(
        [FromQuery] int pageSize = 10)
    {
        // 구현 로직
    }
}
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">2. JWT Bearer 인증 통합</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\""
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty&lt;string>()
        }
    });
});
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">3. API 버전 관리</h3>



<p>패키지 설치:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="raw" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p>구성:</p>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-Version"),
        new MediaTypeApiVersionReader("ver"));
});

builder.Services.AddVersionedApiExplorer(setup =>
{
    setup.GroupNameFormat = "'v'VVV";
    setup.SubstituteApiVersionInUrl = true;
});

builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions&lt;ConfigureSwaggerOptions>();
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">4. 사용자 정의 스키마 필터</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">public class EnumSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (context.Type.IsEnum)
        {
            schema.Enum.Clear();
            Enum.GetNames(context.Type)
                .ToList()
                .ForEach(name => schema.Enum.Add(new OpenApiString(name)));
        }
    }
}

// 등록
builder.Services.AddSwaggerGen(options =>
{
    options.SchemaFilter&lt;EnumSchemaFilter>();
});
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4ca.png" alt="📊" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 성능 최적화 및 모범 사례</h2>



<h3 class="wp-block-heading">1. 프로덕션 환경 고려사항</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// 조건부 Swagger 활성화
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

// 또는 구성 기반 활성화
if (builder.Configuration.GetValue&lt;bool>("EnableSwagger"))
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
</pre>



<h3 class="wp-block-heading">2. 대용량 API 문서 최적화</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">builder.Services.AddSwaggerGen(options =>
{
    // 불필요한 스키마 제외
    options.SchemaFilter&lt;ExcludeInternalTypesFilter>();
    
    // 문서 압축
    options.EnableAnnotations();
    
    // 메모리 사용량 최적화
    options.UseAllOfToExtendReferenceSchemas();
    options.UseOneOfForPolymorphism();
});
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">3. 보안 강화</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">app.UseSwagger(options =>
{
    // JSON 문서 접근 제한
    options.PreSerializeFilters.Add((swagger, httpReq) =>
    {
        if (!httpReq.Headers.ContainsKey("X-API-Key"))
        {
            throw new UnauthorizedAccessException();
        }
    });
});
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f527.png" alt="🔧" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 트러블슈팅 가이드</h2>



<h3 class="wp-block-heading">일반적인 문제와 해결책</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>문제</th><th>원인</th><th>해결방법</th></tr></thead><tbody><tr><td>XML 주석이 표시되지 않음</td><td>XML 파일 경로 오류</td><td>빌드 출력 디렉토리 확인 및 경로 수정</td></tr><tr><td>복잡한 제네릭 타입 오류</td><td>스키마 생성 실패</td><td>사용자 정의 SchemaFilter 구현</td></tr><tr><td>순환 참조 오류</td><td>모델 간 순환 의존성</td><td>JsonIgnore 또는 DTO 패턴 적용</td></tr><tr><td>인증 테스트 실패</td><td>CORS 또는 인증 설정 문제</td><td>CORS 정책 및 인증 미들웨어 순서 확인</td></tr></tbody></table></figure>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4c8.png" alt="📈" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 모니터링 및 분석</h2>



<h3 class="wp-block-heading">Application Insights 통합</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="csharp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">builder.Services.AddApplicationInsightsTelemetry();

// Swagger 사용량 추적
app.UseSwagger(options =>
{
    options.PreSerializeFilters.Add((swagger, httpReq) =>
    {
        var telemetryClient = httpReq.HttpContext.RequestServices
            .GetRequiredService&lt;TelemetryClient>();
        telemetryClient.TrackEvent("SwaggerAccessed");
    });
});
</pre>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4cc.png" alt="📌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 요약 및 베스트 프랙티스</h2>



<h3 class="wp-block-heading">핵심 도구 정리</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>도구/개념</th><th>설명</th><th>권장 사용 시나리오</th></tr></thead><tbody><tr><td><strong>OpenAPI/Swagger</strong></td><td>REST API 명세 표준</td><td>모든 REST API 프로젝트</td></tr><tr><td><strong>Swashbuckle.AspNetCore</strong></td><td>.NET용 Swagger 구현체</td><td>ASP.NET Core 웹 API</td></tr><tr><td><strong>Swagger UI</strong></td><td>인터랙티브 API 문서</td><td>개발 및 테스트 환경</td></tr><tr><td><strong>SwaggerGen</strong></td><td>코드 기반 문서 생성기</td><td>자동화된 문서 관리</td></tr></tbody></table></figure>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">개발팀을 위한 권장사항</h3>



<ol class="wp-block-list">
<li><strong>개발 초기</strong>부터 Swagger 도입하여 API 설계 단계에서 문서화</li>



<li><strong>XML 주석</strong>을 활용한 상세한 API 설명 작성</li>



<li><strong>DTO 패턴</strong> 적용으로 명확한 API 계약 정의</li>



<li><strong>버전 관리</strong> 전략 수립으로 하위 호환성 보장</li>



<li><strong>보안 설정</strong> 통합으로 실제 운영 환경과 일치하는 테스트 환경 구축</li>



<li><strong>CI/CD 파이프라인</strong>에 OpenAPI 스펙 검증 단계 포함</li>
</ol>



<p>이러한 접근을 통해 Swagger/OpenAPI는 단순한 문서화 도구를 넘어서 API 개발 생명주기 전반을 지원하는 핵심 인프라가 될 수 있습니다.</p>
<p>The post <a href="https://lycos7560.com/c/asp-net/swagger-openapi-for-net/40167/">Swagger/OpenAPI for .NET</a> appeared first on <a href="https://lycos7560.com">어제와 내일의 나 그 사이의 이야기</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://lycos7560.com/c/asp-net/swagger-openapi-for-net/40167/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
