CORS·로깅·설정 관리
곁들여 갖추어야 할 것들입니다.
기능은 아니지만 없으면 안 되는 것들
여기까지 만든 것으로 API 는 동작합니다. 그런데 실제로 올리려면 세 가지가 더 필요합니다. 기능이 아니라 앱이 제대로 돌아가는 데 필요한 것들입니다.
- 설정 — 개발과 운영에서 달라야 하는 값들입니다. 연결 문자열이나 키가 그렇습니다.
- 로깅 — 무슨 일이 있었는지 남깁니다. 2.5의 traceId 를 찾을 곳이 여기입니다.
- CORS — 다른 주소의 화면이 이 API 를 부를 수 있게 합니다.
설정은 겹쳐 읽습니다
한 곳에서만 읽는 것이 아니라 여러 곳을 차례로 읽어 덮어씁니다. 뒤에 읽는 것이 이깁니다.
| 차례 | 어디 | 무엇에 |
|---|---|---|
| 1 | appsettings.json | 어디서나 같은 값 |
| 2 | appsettings.{환경}.json | 개발·운영이 다른 값 |
| 3 | 사용자 비밀 | 개발 기계의 비밀값(저장소에 담기지 않습니다) |
| 4 | 환경 변수 | 운영의 비밀값 |
| 5 | 명령줄 인수 | 그때 한 번만 바꿀 값 |
// appsettings.json { "Site": { "Name": "기본 이름", "PageSize": 10, "Debug": false } } // appsettings.Development.json — 있는 것만 덮어씁니다 { "Site": { "Name": "개발 이름", "Debug": true } }
이 코드는 호스트가 있어야 하므로 브라우저에서 실행할 수 없습니다.
PageSize 가 10으로 남은 것을 보십시오.
Development 파일에 없으므로 앞의 값이 그대로입니다. 파일 전체를 갈아
끼우는 것이 아니라 키 하나하나를 덮습니다.
환경 변수는 밑줄 두 개로 계층을 적습니다. Site:Name 은 Site__Name 입니다. 운영 체제에 따라 콜론을 사용할 수 없어 그렇습니다.
형식으로 받아 사용합니다
c["Site:Name"] 처럼 문자열로 읽어도 되지만, 오타가
나면 null 이 돌아올 뿐 오류가 나지 않습니다. 형식에
담아 받는 편이 낫습니다.
// Program.cs — Build() 앞 builder.Services.Configure<SiteOptions>(builder.Configuration.GetSection("Site")); // 받는 쪽 public class HomeController(IOptions<SiteOptions> site) : Controller { public IActionResult Index() => Content(site.Value.Name); }
이 사이트도 같은 방식입니다. IOptions<DataConfiguration>
처럼 받아 사용합니다.
비밀값은 어느 파일에도 담지 마십시오. 개발 기계에서는 사용자 비밀을 사용합니다. 프로젝트 폴더 밖에 저장되므로 저장소에 담기지 않습니다.
dotnet user-secrets init dotnet user-secrets set "ConnectionStrings:Default" "Server=…;Password=…"
로그는 수준으로 걸러집니다
ILogger<T> 를 받아 찍습니다. 수준이 여섯 가지이고,
설정에 적은 것보다 낮은 것은 나가지 않습니다.
app.MapGet("/log", (ILogger<Program> log) => { var 이름 = "홍길동"; log.LogTrace("추적입니다 {Name}", 이름); log.LogDebug("디버그입니다 {Name}", 이름); log.LogInformation("정보입니다 {Name}", 이름); log.LogWarning("경고입니다 {Name}", 이름); log.LogError("오류입니다 {Name}", 이름); return "찍었습니다"; });
앞의 둘이 나오지 않았습니다. 설정이 Information 이므로 그보다 낮은 Trace·Debug 는 걸러집니다. 수준은 appsettings.json 에서 정하고, 이름별로 다르게 둘 수 있습니다.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning" // 프레임워크 것은 조용히
}
}
}
자리표시자를 사용합니다
"정보입니다 {Name}" 는 문자열 보간이 아닙니다.
앞에 $ 가 없습니다. 값을 따로
넘기면 로그를 받는 쪽이 "메시지" 와 "이름" 을 나누어 담습니다.
log.LogInformation($"정보입니다 {이름}"); // 통째로 글자 하나입니다. // 이름으로 찾을 수 없습니다.
log.LogInformation("정보입니다 {Name}", 이름); // 메시지 틀과 값이 따로 남습니다. // Name = "홍길동" 으로 찾습니다.
콘솔에 찍힌 모양은 같습니다. 차이는 기록을 모아 두는 곳에서 드러납니다. 자리표시자로 적으면 "홍길동에게 있었던 일" 을 골라 볼 수 있고, 보간으로 적으면 글자 검색밖에 되지 않습니다.
그리고 수준에 걸려 나가지 않을 때는 조합조차 하지 않습니다. 보간은 그 자리에서 이미 글자를 만들어 버립니다.
CORS 는 브라우저가 막습니다
https://app.example.com 의 화면이 https://api.example.com 을 부르면 브라우저가 막습니다. 주소가 다르기 때문입니다. 서버가 막는 것이 아닙니다.
builder.Services.AddCors(o => o.AddPolicy("앱", p => p .WithOrigins("https://app.example.com") .WithMethods("GET", "POST") .AllowCredentials())); var app = builder.Build(); app.UseCors("앱");
마지막 두 줄이 이 단원에서 가장 오해가 잦은 자리입니다. 허용하지
않은 출처에도 서버는 200과 본문을 그대로 돌려줍니다. 다만
허락하는 머리글을 붙이지 않으므로 브라우저가 화면에 전달하지 않습니다.
curl 로는 그냥 됩니다.
그러므로 CORS 는 보안 장치가 아닙니다. 남이 못 부르게 하려면 4.2·4.3의 인증과 인가로 막아야 합니다. CORS 는 "브라우저야, 이 화면에는 보여 줘도 된다" 고 알리는 것입니다.
세 번째 묶음의 OPTIONS 는 사전 요청
입니다. 브라우저가 본 요청을 보내기 전에 먼저 물어봅니다. 단순한 GET 이 아니거나
머리글을 붙였을 때 그렇습니다.
모두 허용과 자격 증명은 함께 둘 수 없습니다
p.AllowAnyOrigin().AllowCredentials()
막아 둔 까닭이 있습니다. 아무 출처에나 쿠키를 실어 보내도 된다고 하면, 어느 사이트든 남의 쿠키로 이 API 를 부를 수 있게 됩니다. 자격 증명을 함께 보내야 한다면 출처를 하나하나 적어야 합니다.
UseCors 는 UseAuthorization
보다 앞에 둡니다. 사전 요청에는 자격 증명이 실려 있지 않아, 인가가
먼저 지나면 401 로 막혀 버립니다.
사용자 비밀은 개발 기계에서만
사용자 비밀은 Development 일 때만 읽힙니다. 운영에 올리면 그 값이 없어 앱이 뜨지 않거나 빈 값으로 동작합니다. 운영에서는 환경 변수나 키 보관 서비스를 사용합니다.
이 저장소에도 같은 것이 있습니다. 코드에 없지만 사이트가 도는 데 필요한 값들의 목록과 복구 절차를 SECRETS.md 에 적어 두고, 값 자체는 그 문서에도 적지 않습니다.
직접 해보기
쪽 크기를 개발에서는 5, 운영에서는 20으로 두려 합니다. 어느 파일에 무엇을 적을지 적어보세요.
EF Core 가 SQL 을 모두 찍어 기록이 넘칩니다. 내 코드의 정보는 그대로 두고 EF 것만 줄이려면 어떻게 할지 적어보세요.
UseCors 를 넣었는데 브라우저 콘솔에 여전히 CORS
오류가 납니다. curl 로는 200이 옵니다. 무엇을
살펴야 할지 세 가지 이상 적어보세요.
- 설정은 여러 곳을 겹쳐 읽습니다. 뒤에 읽는 것이 이기고, 없는 키는 앞의 값이 남습니다.
- 비밀값은 파일에 담지 않습니다. 개발은 사용자 비밀, 운영은 환경 변수입니다.
- 로그는
$없는 자리표시자로 적습니다. 값이 따로 남아 찾을 수 있습니다. - 로그 수준은 이름별로 정합니다. 앞에서부터 맞춰 봅니다.
- CORS 는 브라우저가 막는 것이지 보안 장치가 아닙니다. 서버는 200과 본문을 그대로 돌려줍니다.