[ASP.NET Core] Clean Architecture ๋กœ ์ธ์ฆ, ์ธ๊ฐ€(JWT) ์„œ๋น„์Šค ๊ฐœ๋ฐœ

1. ๊ฐœ์š”

Spring Boot๋กœ ์›น ๊ฐœ๋ฐœ๋งŒ ํ•˜๋‹ค๊ฐ€ c# ์œผ๋กœ ํ•  ์ˆ˜๋„..? ์žˆ๊ฒŒ ๋๋Š”๋ฐ c# ์ด๋‚˜ .asp.net core ๊ด€๋ จํ•ด์„  ๊ตญ๋‚ด ์ž๋ฃŒ๊ฐ€ ์ง„์งœ ๋„ˆ์–ด์–ด์–ด๋ฌด ์—†๋‹ค. 

 

๊ฐ์ฒด์ง€ํ–ฅ, DI ๋“ฑ Spring Boot์™€ ๊ต‰์žฅํžˆ ์œ ์‚ฌํ•˜๋‹ค๊ณ ๋Š” ํ•˜์ง€๋งŒ ๋ณด๊ณ  ๋ฐฐ์šธ ์ž๋ฃŒ๋“ค์ด ๋„ˆ๋ฌด ๋ถ€์กฑํ•˜๋‹ค๋Š”๊ฑธ ๋Š๊ผˆ๋‹ค. ์ƒˆ์‚ผ Java ์ƒํƒœ๊ณ„๊ฐ€ ํฌ๊ตฌ๋‚˜ ๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค..

 

 

๊ทธ๋ž˜์„œ c# ๊ณต๋ถ€๋ฅผ ํ•˜๋ฉฐ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๊ณต์œ ํ•˜๊ณ ์ž ๊ธ€์„ ์“ด๋‹ค. 

 

 

์›๋ž˜๋ผ๋ฉด ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑํ•˜๋Š” ๋‚ด์šฉ ๋”ฐ์œˆ(?) ๋ธ”๋กœ๊ทธ์— ์ ˆ๋Œ€ ์“ฐ์ง€ ์•Š์ง€๋งŒ, Java, Spring Boot ๋งŒ ํ•˜๋˜ ๋‚˜์—๊ฒŒ c#์€ ์‹œ์ž‘ ์กฐ์ฐจ ์–ด๋ ค์› ๊ธฐ ๋•Œ๋ฌธ์— ์ƒ์„ธํ•˜๊ฒŒ ๊ธฐ๋กํ•˜๋„๋ก ํ•˜๊ฒ ๋‹ค.

 

 

 

 

 

 

 

 

 

 

 

 

2. Clean Architecture๋ž€?

 

ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ๋ฌด์—‡์ธ์ง€ ์ •์˜ํ•˜๊ธฐ์— ์•ž์„œ, ์•„ํ‚คํ…์ฒ˜๋ž€ ๋ฌด์—‡์ธ๊ฐ€? 

์ผ๋‹จ ์•„ํ‚คํ…์ฒ˜(Architecture)๋ž€ ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ค๊ณ ์ž ํ•˜๋Š” ์„œ๋น„์Šค ํ˜น์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ตฌ์„ฑ์š”์†Œ ๋ฐ ๊ตฌ์„ฑ์š”์†Œ๊ฐ„์˜ ๊ด€๊ณ„, ์™ธ๋ถ€(Client)์™€ ๊ตฌ์„ฑ์š”์†Œ๊ฐ„์˜ ๊ด€๊ณ„ ๋“ฑ์„ ์ •์˜ํ•˜๊ณ  ์„ค๋ช…ํ•˜๋Š” '์„ค๊ณ„๋„' ๊ฐ™์€ ๊ฐœ๋…์ด๋ผ๊ณ  ์ •์˜ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.

 

 

 

 

๊ทธ๋Ÿผ ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋ž€ ๋ฌด์—‡์ผ๊นŒ?

"ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜(Clean Architecture)"๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ์— ์‚ฌ์šฉ๋˜๋Š” ๋””์ž์ธ ํŒจํ„ด ์ค‘ ํ•˜๋‚˜๋กœ, ๋กœ๋ฒ„ํŠธ C. ๋งˆํ‹ด(Robert C. Martin)์ด ์ œ์•ˆํ•œ ์•„ํ‚คํ…์ฒ˜ ์›์น™์ด๋‹ค

 

 

 

ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜์˜ ๊ตฌ์„ฑ ์š”์†Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

  • Adapter(Infrastructure)
    • ์™ธ๋ถ€ ์„ธ๊ณ„(Web, DB, UI)์™€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€์˜ ๋„๋ฉ”์ธ ๋ฐ ๋กœ์ง ์‚ฌ์ด๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ์—ญํ• .
    • ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜์˜ ๊ฒฝ๊ณ„๋ฅผ ํ˜•์„ฑํ•˜๋ฉฐ, ์‹œ์Šคํ…œ์˜ ๋…๋ฆฝ์„ฑ์„ ๋ณด์žฅ
    • in: Controller, Ui
    • out: JpaRepository
  • Application(Use Case)
    • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ์ง„์ž…์ ์„ ์ œ๊ณต
    • ์‹œ์Šคํ…œ์˜ usecase๋ฅผ ์ •์˜ํ•˜๊ณ  ๊ตฌํ˜„ํ•˜๋Š” ์„œ๋น„์Šค๊ฐ€ ์œ„์น˜
    • domain๊ณผ adapter์‚ฌ์ด์— ์ƒํ˜ธ์ž‘์šฉ์„ ์กฐ์ •ํ•˜๊ณ  ํ๋ฆ„์„ ์ œ์–ด
  • Domain
    • ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ตฌํ˜„
    • ๋น„์ฆˆ๋‹ˆ์Šค ๊ฐ์ฒด, ๋„๋ฉ”์ธ ์„œ๋น„์Šค , ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋“ฑ์ด ํฌํ•จ
    • ์™ธ๋ถ€ ์š”์†Œ์— ์˜์กดํ•˜์ง€ ์•Š์œผ๋ฉฐ ์ˆœ์ˆ˜์„ฑ์„ ๋ณด์žฅ

 

 

 

 

 

 

 

 

 

 

 

 

3. ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜์˜ ๊ธฐ๋ณธ ์›๋ฆฌ

ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋Š” ์†Œํ”„ํŠธ์›จ์–ด ์‹œ์Šคํ…œ์„ ์—ฌ๋Ÿฌ ๊ณ„์ธต์œผ๋กœ ๊ตฌ๋ถ„ํ•˜๊ณ , ๊ฐ ๊ณ„์ธต ๊ฐ„์˜ ์˜์กด์„ฑ์„ ์—„๊ฒฉํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ธฐ๋ณธ ์›๋ฆฌ๋กœ ํ•œ๋‹ค.

 

์ด๋Š” '์˜์กด์„ฑ ์—ญ์ „ ์›์น™(Dependency Inversion Principle)'๊ณผ ๋ฐ€์ ‘ํ•˜๊ฒŒ ์—ฐ๊ด€๋˜์–ด ์žˆ๋‹ค.

 

๊ฐ€์žฅ ์ค‘์‹ฌ์—๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ๋‹ด๊ณ  ์žˆ๋Š” ์—”ํ‹ฐํ‹ฐ(Entity)์™€ ์œ ์Šค์ผ€์ด์Šค(Use Cases)๊ฐ€ ์œ„์น˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋“ค์€ ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•˜๋ฉฐ, ์™ธ๋ถ€ ์š”์†Œ์˜ ๋ณ€๊ฒฝ์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค.

 

 

์™œ๋ƒํ•˜๋ฉด ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋Š” ๋‚ด๋ถ€ ๊ณ„์ธต์ด ์™ธ๋ถ€ ๊ณ„์ธต์— ์˜์กดํ•˜์ง€ ์•Š๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ๋…๋ฆฝ์„ฑ์„ ๋ณด์žฅํ•˜๊ณ , ์‹œ์Šคํ…œ์˜ ์œ ์—ฐ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ค.

 

์™ธ๋ถ€๋กœ ๊ฐˆ์ˆ˜๋ก ์ธํ„ฐํŽ˜์ด์Šค ์–ด๋Œ‘ํ„ฐ, ํ”„๋ ˆ์  ํ…Œ์ด์…˜, ์ธํ”„๋ผ์ŠคํŠธ๋Ÿญ์ฒ˜ ๋“ฑ์˜ ๊ณ„์ธต์ด ์œ„์น˜ํ•˜๋ฉฐ, ์ด๋“ค์€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ํ†ต์‹ ์„ ๋‹ด๋‹นํ•œ๋‹ค.

 

 

์ด๋Ÿฌํ•œ ๊ณ„์ธต ๊ตฌ์กฐ๋Š” ์‹œ์Šคํ…œ์˜ ๊ฐ ๋ถ€๋ถ„์„ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋ฉฐ, ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ํ•œ ๋ถ€๋ถ„์— ๊ตญํ•œ๋˜๋„๋ก ํ•œ๋‹ค.

 

 

ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ช‡ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์„ ๋”ฐ๋ผ์•ผ ํ•œ๋‹ค.

 

1. ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ค‘์‹ฌ์œผ๋กœ ์—”ํ‹ฐํ‹ฐ์™€ ์œ ์Šค์ผ€์ด์Šค๋ฅผ ์ •์˜

 

2. ๊ฐ ๊ณ„์ธต์˜ ์ฑ…์ž„๊ณผ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ณ , ๊ณ„์ธต ๊ฐ„์˜ ์˜์กด์„ฑ์„ ์—ญ์ „์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ์„ค๊ณ„ํ•œ๋‹ค. ์ด๋Š” ์ธํ„ฐํŽ˜์ด์Šค์™€ ์ถ”์ƒํ™”๋ฅผ ํ†ตํ•ด ๊ตฌํ˜„ํ•œ๋‹ค.

 

3. ์‹œ์Šคํ…œ์˜ ์™ธ๋ถ€ ์š”์†Œ์™€์˜ ํ†ต์‹ ์„ ๋‹ด๋‹นํ•˜๋Š” ์–ด๋Œ‘ํ„ฐ์™€ ์ธํ”„๋ผ์ŠคํŠธ๋Ÿญ์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์ด ๊ณผ์ •์—์„œ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ ํ”„๋ ˆ์ž„์›Œํฌ์˜ ์˜์กด์„ฑ์€ ์ตœ์†Œํ™”ํ•˜๊ณ , ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ๋กœ์ง๊ณผ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ˜๋‹ค.

 

4. ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๊ฐ ๊ณ„์ธต์˜ ๋…๋ฆฝ์„ฑ๊ณผ ์‹œ์Šคํ…œ์˜ ์ „์ฒด์ ์ธ ์•ˆ์ •์„ฑ์„ ๊ฒ€์ฆํ•œ๋‹ค. ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜๋Š” ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ์„ ๋†’์ด๋Š” ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•œ๋‹ค. 

 

 

 

 

 

 

 

 

 

 

 

 

 

4. ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ ๋ฐ ํšŒ์› ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ

๋“ค์–ด๊ฐ€๊ธฐ ์•ž์„œ ์™„์„ฑ๋œ ํ”„๋กœ์ ํŠธ ์ „์ฒด ํŒจํ‚ค์ง€ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

 

 

 

 

 

 

๋ฃจํŠธ ํ”„๋กœ์ ํŠธ๋Š” ๋นˆ ํ”„๋กœ์ ํŠธ๋กœ ์ƒ์„ฑํ•ด์ค€๋‹ค.

 

 

 

๊ทธ๋ฆฌ๊ณ  ๋‚˜๋จธ์ง€ Application, Domain, Infrastructure, WebAPI ๋Š” ํด๋ž˜์Šค ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(C#)์œผ๋กœ ์ƒ์„ฑํ•ด์ค€๋‹ค.

 

 

 

์ƒ์„ฑ ํ›„ ๋ชจ์Šต์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

 

 

Clean Architecture์— ๋”ฐ๋ผ ๊ฐ ํ”„๋กœ์ ํŠธ์˜ dependency๋ฅผ ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•œ๋‹ค. 

 

application -> domain

infrastrucrue -> application

WebAPI -> application, Infrastructure

 

์œผ๋กœ ์„ค์ •ํ•ด์ค€๋‹ค. Domain์€ ๋‹ค๋ฅธ ๊ณ„์ธต์— ์˜์กด์„ฑ์„ ๊ฐ€์ ธ์„œ๋Š” ์•ˆ ๋œ๋‹ค. 

 

 

์ฐธ๊ณ ๋กœ ์˜์กด์„ฑ ์ถ”๊ฐ€๋Š” ์ข…์†์„ฑ ์šฐํด๋ฆญ ํ›„ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

 

 

 

 

 

InfraStructure ํ”„๋กœ์ ํŠธ => NuGet ์—์„œ efcore์™€ efcore tools, SqlServer ๋ฅผ ์„ค์น˜ํ•ด์ค€๋‹ค. 

Entity Framework Core์˜ ํ•ต์‹ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์ƒํ˜ธ ์ž‘์šฉํ•˜๋Š” ๋ฐ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•œ๋‹ค. Java Spring ์—์„œ JPA์™€ ๊ฐ™์€ ์—ญํ• ์ด๋‹ค.

 

์ถ”๊ฐ€๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ธ์ฝ”๋”ฉ์„ ์œ„ํ•œ BCrtpy๋„ ์„ค์น˜ํ•ด์ฃผ์ž.

 

 

 

 

๊ทธ๋ฆฌ๊ณ  efcore tools๋Š” WebAPI์—๋„ ์„ค์น˜ํ•ด์ฃผ์ž.

 

 

 

 

์˜ˆ์ œ๋กœ ์“ธ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๋ฅผ Domain ํ”„๋กœ์ ํŠธ ์•ˆ์— ๋งŒ๋“ค์–ด์ค€๋‹ค. 

 

- ApplicationUser.cs

namespace Domain.Entities
{
    public class ApplicationUser
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public string? Email { get; set; }
        public string? Password { get; set; }
    }
}

 

 

 

 

infrastructure ์•ˆ์— ๋‹ค์Œ ๋‘ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค. 

 

- AppDbContext.cs

์ด ํด๋ž˜์Šค๋Š” Entity Framework Core์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ์ƒํ˜ธ ์ž‘์šฉ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปจํ…์ŠคํŠธ(Database Context) ์ด๋‹ค. ์ด ํด๋ž˜์Šค๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ”๊ณผ ๋งคํ•‘๋˜๋Š” ์—”ํ‹ฐํ‹ฐ(๋ชจ๋ธ)๋ฅผ ์ •์˜ํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

using Domain.Entities;
using Microsoft.EntityFrameworkCore;

namespace infrastructure.Data
{
    internal class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {
        }

        public DbSet<ApplicationUser> Users { get; set; } // ํ…Œ์ด๋ธ” ๋ช…
    }
}

 

 

 

- ServiceContainer.cs

์ด ํด๋ž˜์Šค๋Š” ์˜์กด์„ฑ ์ฃผ์ž…(Dependency Injection) ์„ค์ •์„ ๋‹ด๋‹นํ•˜๋Š” ์„œ๋น„์Šค ๊ตฌ์„ฑ ํด๋ž˜์Šค

using infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace infrastructure.DependencyInjection
{
    public static class ServiceContainer
    {
        // IServiceCollection์— ์˜์กด์„ฑ ์ฃผ์ž… ๊ตฌ์„ฑ
        public static IServiceCollection InfrastructureServices(this IServiceCollection services, IConfiguration configuration)
        {
            // AddDbContext ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ AppDbContext๋ฅผ DI ์ปจํ…Œ์ด๋„ˆ์— ๋“ฑ๋ก
            services.AddDbContext<AppDbContext>(options =>
                // SQL Server ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •
                options.UseSqlServer(
                    configuration.GetConnectionString("Default"),
                    b => b.MigrationsAssembly(typeof(ServiceContainer).Assembly.FullName)
                ),
                ServiceLifetime.Scoped // // DbContext์˜ ์ƒ๋ช… ์ฃผ๊ธฐ๋ฅผ Scoped๋กœ ์„ค์ • (์š”์ฒญ๋งˆ๋‹ค ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑ)
            );

            // ์˜์กด์„ฑ ๋ฐ˜ํ™˜
            return services;
        }
    }
}

 

 

 

๊ทธ๋ฆฌ๊ณ  appsettings.json์— ConnectionStrings ์„ค์ •์„ ํ•ด์ค€๋‹ค.

{
  // ์ถ”๊ฐ€
  // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด ์„ค์ •
  // Server=local : ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋ฒ„ ์ฃผ์†Œ ์ง€์ •
  // Database=[] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ด๋ฆ„ ์ง€์ •
  // Trusted_Connection=true : true๋กœ ๋˜์–ด ์žˆ์œผ๋ฉด, ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋Œ€์‹  Windows ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—ฐ๊ฒฐ
  // Trust Server Certificate=true : SSL ์ธ์ฆ์„œ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์„œ๋ฒ„ ์ธ์ฆ์„œ๋ฅผ ์‹ ๋ขฐํ•  ๊ฒƒ์ธ์ง€
  "ConnectionStrings": {
    "Default": "Server=(local); Database=JWTWithCleanArchDB; Trusted_Connection=true; Trust Server Certificate=true;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

 

 

 

์†”๋ฃจ์…˜ ๋นŒ๋“œ ํ›„ ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž ์ฝ˜์†”์— Infrastructure ํ”„๋กœ์ ํŠธ ์„ ํƒ ํ›„ ์•„๋ž˜ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅ

add-migration First // First ๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ Migration ์ƒ์„ฑ
update-database

 

(๋งˆ์ด๊ทธ๋ ˆ์ด์…˜(Migration)์„ ์ƒ์„ฑํ•œ๋‹ค๋Š” ๊ฒƒ์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์Šคํ‚ค๋งˆ(๊ตฌ์กฐ) ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ฝ”๋“œ๋กœ ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธ)

 

 

์ฐธ๊ณ ๋กœ ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž ์ฝ˜์†”์€ ๋ณด๊ธฐ => ๋‹ค๋ฅธ ์ฐฝ => ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž ์ฝ˜์†” ์—์„œ ํ™œ์„ฑํ™” ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค. 

 

 

database์— ์—…๋ฐ์ดํŠธ ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ๋กœ๊ทธ๋“ค์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. 

 

 

 

 

 

ํšŒ์› ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ

Application Layer์— Dto์™€ UseCase๋ฅผ ์ƒ์„ฑํ•ด์คฌ๋‹ค.

 

 

IUser UseCase๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” UserRepo๋Š” Infrastructure.Repo ์•ˆ์— ์ƒ์„ฑํ•ด์ฃผ์—ˆ๋‹ค.

 

 

 

์˜์กด์„ฑ ์ฃผ์ž…์„ ์œ„ํ•ด ServiceContainer์— JWT ๊ด€๋ จ ์„ค์ •์„ ํ•ด์ฃผ์—ˆ๋‹ค.

using Application.Contract;
using infrastructure.Data;
using infrastructure.Repo;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace infrastructure.DependencyInjection
{
    public static class ServiceContainer
    {
        // IServiceCollection์— ์˜์กด์„ฑ ์ฃผ์ž… ๊ตฌ์„ฑ
        public static IServiceCollection InfrastructureServices(this IServiceCollection services, IConfiguration configuration)
        {
            // AddDbContext ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ AppDbContext๋ฅผ DI ์ปจํ…Œ์ด๋„ˆ์— ๋“ฑ๋ก
            services.AddDbContext<AppDbContext>(options =>
                // SQL Server ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •
                options.UseSqlServer(
                    configuration.GetConnectionString("Default"),
                    b => b.MigrationsAssembly(typeof(ServiceContainer).Assembly.FullName)
                ),
                ServiceLifetime.Scoped // // DbContext์˜ ์ƒ๋ช… ์ฃผ๊ธฐ๋ฅผ Scoped๋กœ ์„ค์ • (์š”์ฒญ๋งˆ๋‹ค ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑ)
            );

            // JWT
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true, // ํ† ํฐ์˜ ๋ฐœ๊ธ‰์ž(issuer)๋ฅผ ๊ฒ€์ฆ
                    ValidateAudience = true, // ํ† ํฐ์˜ ์ˆ˜์‹ ์ž(audience)๋ฅผ ๊ฒ€์ฆ
                    ValidateLifetime = true, // ํ† ํฐ์˜ ์œ ํšจ ๊ธฐ๊ฐ„์„ ๊ฒ€์ฆ
                    ValidateIssuerSigningKey = true, // ํ† ํฐ์˜ ์„œ๋ช… ํ‚ค๋ฅผ ๊ฒ€์ฆ

                    ValidIssuer = configuration["Jwt:Issuer"], // ๋ฐœ๊ธ‰์ž์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•  ๋•Œ ์‚ฌ์šฉํ•  ๊ฐ’
                    ValidAudience = configuration["Jwt:Audience"], // ์ˆ˜์‹ ์ž์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•  ๋•Œ ์‚ฌ์šฉํ•  ๊ฐ’
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)) // ํ† ํฐ์„ ๊ฒ€์ฆํ•  ๋•Œ ์‚ฌ์šฉํ•  ์„œ๋ช… ํ‚ค
                };
            });

            services.AddScoped<IUser, UserRepo>(); // IUser ์ธํ„ฐํŽ˜์ด์Šค์™€ UserRepo ํด๋ž˜์Šค ๊ฐ„์˜ ์˜์กด์„ฑ์„ DI ์ปจํ…Œ์ด๋„ˆ์— ๋“ฑ๋ก
            // ์˜์กด์„ฑ ๋ฐ˜ํ™˜
            return services;
        }
    }
}

 

 

 

 

 

Repo์—๋Š” ๋กœ๊ทธ์ธ ์‹œ ์ด๋ฉ”์ผ, ์•„์ด๋”” ๊ฒ€์ฆ ํ›„ token์„ ์ƒ์„ฑํ•ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๋กœ์ง๊ณผ, ํšŒ์› ๋“ฑ๋ก ๋กœ์ง์„ ์ž‘์„ฑํ•ด์ฃผ์—ˆ๋‹ค.

using Application.Contract;
using Application.DTOs;
using Domain.Entities;
using infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace infrastructure.Repo
{
    internal class UserRepo : IUser
    {
        private readonly AppDbContext appDbContext;
        private readonly IConfiguration configuration;

        public UserRepo(AppDbContext appDbContext, IConfiguration configuration)
        {
            this.appDbContext = appDbContext;
            this.configuration = configuration;
        }
        public async Task<LoginResponse> LoginUserAsync(LoginDTO loginDTO)
        {
            var getUser = await FindUserByEmail(loginDTO.Email!); // null์ด ์•„๋‹˜์„ ๋ณด์žฅํ•œ๋‹ค๋Š” ์˜๋ฏธ์—์„œ ! ๋ฅผ ๋ถ™์ž„

            if (getUser == null) return new LoginResponse(false, "User not found");

            bool checkPassword = BCrypt.Net.BCrypt.Verify(loginDTO.Password, getUser.Password);
            if (checkPassword)
            {
                return new LoginResponse(true, "Login successfully", GenerateJWTToken(getUser));
            }
            else
            {
                return new LoginResponse(false, "Invalid credentials");
            }
        }

        private string GenerateJWTToken(ApplicationUser getUser)
        {
            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!));
            var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
            var userClaims = new[]
            {
                new Claim(ClaimTypes.NameIdentifier, getUser.Id.ToString()),
                new Claim(ClaimTypes.Name, getUser.Name!),
                new Claim(ClaimTypes.Email, getUser.Email!)
            };

            var token = new JwtSecurityToken(
                issuer: configuration["Jwt:Issuer"],
                audience: configuration["Jwt:Audience"],
                claims: userClaims,
                expires: DateTime.Now.AddDays(5),
                signingCredentials: credentials
                );

            return new JwtSecurityTokenHandler().WriteToken(token);
        }

        private async Task<ApplicationUser> FindUserByEmail(string email) =>
            await appDbContext.Users.FirstOrDefaultAsync(u => u.Email == email);

        public async Task<RegistrationResponse> RegisterUserAsync(RegisterUserDTO registerUserDTO)
        {
            var getUser = await FindUserByEmail(registerUserDTO.Email!);
            if (getUser != null)
                return new RegistrationResponse(false, "User Already exist");

            appDbContext.Users.Add(new ApplicationUser()
            {
                Name = registerUserDTO.Name,
                Email = registerUserDTO.Email,
                Password = BCrypt.Net.BCrypt.HashPassword(registerUserDTO.Password)
            });

            await appDbContext.SaveChangesAsync();
            return new RegistrationResponse(true, "Registration completed");
        }
    }
}

 

 

 

๋งˆ์ง€๋ง‰์œผ๋กœ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ WebAPI.Controllers ํŒจํ‚ค์ง€์— ๋งŒ๋“ค๊ณ , UseCase์ธ IUser๋ฅผ ์ฃผ์ž… ๋ฐ›์•„์ฃผ์ž.

using Application.Contract;
using Application.DTOs;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace WebAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class User : ControllerBase
    {
        private readonly IUser user;

        public User(IUser user)
        {
            this.user = user;
        }

        [HttpPost("login")]
        public async Task<ActionResult<LoginResponse>> LogUserIn(LoginDTO loginDTO)
        {
            var result = await user.LoginUserAsync(loginDTO);
            return Ok(result);
        }

        [HttpPost("register")]
        public async Task<ActionResult<LoginResponse>> RegisterUser(RegisterUserDTO registerUserDTO)
        {
            var result = await user.RegisterUserAsync(registerUserDTO);
            return Ok(result);
        }
    }
}

 

 

 

 

 

Swagger์—์„œ ํšŒ์›๊ฐ€์ž… ํ›„ ๋กœ๊ทธ์ธ ํ•ด๋ณด๋ฉด ํ† ํฐ์ด ์ž˜ ๋‚˜์˜จ๋‹ค.

 

 

 

Swagger์—์„œ ํ† ํฐ์„ ํ—ค๋”์— ๋‹ด์•„ ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด Program.cs์— ์•„๋ž˜ ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค. 

builder.Services.AddSwaggerGen(swagger =>
{
    swagger.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "ASP.NET 8 Web API",
        Description = "Authentication with JWT"
    });

    // To Enable authorization using Swagger (JWT)
    swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
    {
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header
    });

    swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty<string>()
        }
    });
});

 

 

 

 

Authorize ํ•œ ํ›„ ์ „์ฒด ํšŒ์›์„ ๋ฐ˜ํ™˜ํ•˜๋Š” API๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ๋งŒ๋“ค์–ด์ฃผ๊ณ  ํ…Œ์ŠคํŠธํ•ด๋ณด๋ฉด ํ† ํฐ์ด ์—†์„ ๋•Œ 401 ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

[HttpGet("list")]
[Authorize]
public async Task<ActionResult<List<UserResponse>>> GetUserList()
{
	return Ok(user.GetUserList());
}

 

- ํ† ํฐ ์—†์ด ์š”์ฒญ

 

- ํ† ํฐ ๋„ฃ์–ด์„œ ์š”์ฒญ

 

 

 

์—ฌ๊ธฐ์„œ ์•ฝ๊ฐ„์˜ ์‚ฝ์งˆ์„ ํ–ˆ๋Š”๋ฐ, Swagger๋ฅผ ์จ๋ณธ ์‚ฌ๋žŒ์ด๋ผ๋ฉด ์•„๋ž˜ ์‚ฌ์ง„์˜ Authorize ๋ฒ„ํŠผ์— JWT ํ† ํฐ์„ ๋„ฃ์–ด๋ณธ ๊ฒฝํ—˜์ด ์žˆ์„ ๊ฒƒ์ด๋‹ค.

 

 

 

๋ณดํ†ต "Bearer " Prefix์—†์ด ๋„ฃ์—ˆ๋Š”๋ฐ c#์—์„  ๋”ฐ๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ์ง€ ์•Š์•„์„œ ์˜์กด์„ฑ ์ฃผ์ž…ํ•˜๋˜ InfrastructureServices ํด๋ž˜์Šค์— Event๋ฅผ ๊ฑธ์–ด์ฃผ์—ˆ๋‹ค.

 

ํ—ท๊ฐˆ๋ฆด๊นŒ๋ด ์ „์ฒด ์ฝ”๋“œ๋ฅผ ์ฒจ๋ถ€ํ•ด๋’€๋‹ค. 

using Application.Contract;
using infrastructure.Data;
using infrastructure.Repo;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace infrastructure.DependencyInjection
{
    public static class ServiceContainer
    {
        // IServiceCollection์— ์˜์กด์„ฑ ์ฃผ์ž… ๊ตฌ์„ฑ
        public static IServiceCollection InfrastructureServices(this IServiceCollection services, IConfiguration configuration)
        {
            // AddDbContext ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ AppDbContext๋ฅผ DI ์ปจํ…Œ์ด๋„ˆ์— ๋“ฑ๋ก
            services.AddDbContext<AppDbContext>(options =>
                // SQL Server ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •
                options.UseSqlServer(
                    configuration.GetConnectionString("Default"),
                    b => b.MigrationsAssembly(typeof(ServiceContainer).Assembly.FullName)
                ),
                ServiceLifetime.Scoped // // DbContext์˜ ์ƒ๋ช… ์ฃผ๊ธฐ๋ฅผ Scoped๋กœ ์„ค์ • (์š”์ฒญ๋งˆ๋‹ค ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑ)
            );

            // JWT Authentication ์ถ”๊ฐ€
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true, // ํ† ํฐ์˜ ๋ฐœ๊ธ‰์ž(issuer)๋ฅผ ๊ฒ€์ฆ
                    ValidateAudience = true, // ํ† ํฐ์˜ ์ˆ˜์‹ ์ž(audience)๋ฅผ ๊ฒ€์ฆ
                    ValidateLifetime = true, // ํ† ํฐ์˜ ์œ ํšจ ๊ธฐ๊ฐ„์„ ๊ฒ€์ฆ
                    ValidateIssuerSigningKey = true, // ํ† ํฐ์˜ ์„œ๋ช… ํ‚ค๋ฅผ ๊ฒ€์ฆ

                    ValidIssuer = configuration["Jwt:Issuer"], // ๋ฐœ๊ธ‰์ž์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•  ๋•Œ ์‚ฌ์šฉํ•  ๊ฐ’
                    ValidAudience = configuration["Jwt:Audience"], // ์ˆ˜์‹ ์ž์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•  ๋•Œ ์‚ฌ์šฉํ•  ๊ฐ’
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"]!)) // ํ† ํฐ์„ ๊ฒ€์ฆํ•  ๋•Œ ์‚ฌ์šฉํ•  ์„œ๋ช… ํ‚ค
                };

                // Bearer Prefix ์—†์ด ์š”์ฒญ์™”์„ ๋•Œ ์ถ”๊ฐ€ 
                options.Events = new JwtBearerEvents
                {
                    OnMessageReceived = context =>
                    {
                        var token = context.Request.Headers["Authorization"].FirstOrDefault();
                        if (!string.IsNullOrEmpty(token) && !token.StartsWith("Bearer "))
                        {
                            context.Request.Headers["Authorization"] = $"Bearer {token}";
                        }
                        return Task.CompletedTask;
                    }
                };
            });

            services.AddScoped<IUser, UserRepo>(); // IUser ์ธํ„ฐํŽ˜์ด์Šค์™€ UserRepo ํด๋ž˜์Šค ๊ฐ„์˜ ์˜์กด์„ฑ์„ DI ์ปจํ…Œ์ด๋„ˆ์— ๋“ฑ๋ก
            // ์˜์กด์„ฑ ๋ฐ˜ํ™˜
            return services;
        }
    }
}