첫 번째 글에서는 TsgcHTTPRESTServer와 그 요청 처리 방식을 소개했습니다. 이번 글에서는 동작하는 엔드포인트를 실제로 고객 앞에 내놓을 수 있는 것으로 바꿔주는 세 가지 동반 컴포넌트를 다룹니다. 사용자 저장소, 멀티 테넌시, 그리고 운영팀이 첫날부터 요구할 메트릭입니다.
셋 모두 별개의 컴포넌트입니다. 필요한 것만 생성해서 서버에 할당하면 되고, 할당하지 않은 것은 요청당 포인터 검사 한 번의 비용만 듭니다.
TStringList 대신 사용자 저장소
TsgcHTTPServer_Users는 솔트가 적용된 반복 해시 비밀번호로 계정을 보관합니다. 중요한 점은 연결에 필요한 코드가 거의 없다는 것입니다. 서버의 인증 옵션에 할당하기만 하면 HTTP Basic 관문이 알아서 자격 증명을 이 저장소에서 조회합니다.
uses
sgcHTTP_REST_Server, sgcHTTP_REST_Server_Users;
FUsers := TsgcHTTPServer_Users.Create(self);
FServer := TsgcHTTPRESTServer.Create(self);
FServer.Authentication.Users := FUsers;
이것이 전부입니다. Basic 인증이 동작하는 데 OnAuthentication 핸들러는 필요 없으며, 관문이 조회와 해시 비교를 수행합니다.
계정은 AddUser로 추가하며, 생성된 사용자 Id를 반환합니다. 사용자명이 비어 있거나 이미 사용 중이면 빈 문자열을 반환합니다.
FUsers.AddUser('alice', 'secret123', 'admin,reader');
FUsers.AddUser('bob', 'secret456', 'reader');
나머지 인터페이스는 예상하는 그대로이며, 모든 조회는 스레드 안전합니다.
if FUsers.ValidateCredentials('alice', 'secret123') then
...
FUsers.SetPassword('bob', 'newsecret');
FUsers.EnableUser('bob', False);
FUsers.DeleteUser('bob');
역할
역할은 계정에 쉼표로 구분된 목록으로 저장되며, 읽고 검사하는 헬퍼가 함께 제공됩니다. 핸들러에서의 역할 검사는 호출 한 번입니다.
FUsers.AddRole('bob', 'admin');
if not FUsers.UserHasRole(vUser, 'admin') then
begin
AResponseInfo.ResponseNo := 403;
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ContentText := '{"error":"forbidden"}';
Exit;
end;
해싱
비밀번호는 기본적으로 SHA-512로 10000회 반복 해싱되며, 각 계정은 자신만의 무작위 솔트를 가집니다. 계정별 솔트 덕분에 동일한 비밀번호 두 개도 서로 다른 해시가 되므로, 이 설정을 건드릴 일은 거의 없습니다.
FUsers.Hashing.Algorithm := uhaSHA512;
FUsers.Hashing.Iterations := 10000;
Salt 속성은 이와는 다른 것으로, 주의 깊게 읽어볼 가치가 있습니다. 이것은 계정별 솔트 위에 모든 해시에 섞이는 선택적 페퍼이며, 의도적으로 기본값이 비어 있습니다. 생성 시점에 만들어낸 값은 다음 실행 때 달라지고, 그러면 올바른 비밀번호가 모두 거부되면서 이유를 설명해 주는 오류도 나오지 않습니다. 사용자 저장소 자체가 아닌 다른 곳, 예를 들어 환경 변수나 키 볼트에서 값을 가져올 수 있을 때만 설정하세요. 자신이 보호하는 해시 옆에 함께 보관되는 페퍼는 아무런 도움이 되지 않습니다. 이 값을 바꾸면 이미 저장된 모든 비밀번호가 무효가 됩니다.
영속성
저장소는 기본적으로 메모리에 있습니다. 파일을 지정하면 재시작 후에도 유지됩니다.
FUsers.Storage.StorageType := ustFile;
FUsers.Storage.FileName := 'sgcRESTUsers.dat';
FUsers.Storage.EncryptAtRest := True;
FUsers.Storage.EncryptionKey := GetKeyFromEnvironment;
FUsers.Storage.AutoSaveSeconds := 30;
FUsers.LoadUsers;
AutoSaveSeconds를 설정하면 저장소가 주기적으로 스스로 기록합니다. 그렇지 않으면 원하는 시점에 SaveUsers나 SaveToFile을 호출하세요. 이미 여러분의 데이터베이스에 있는 저장소라면 StorageType을 ustCustom으로 설정하고 대신 이벤트에 응답하면 됩니다. 이때 OnValidateCredentials가 최종 권한을 가집니다.
procedure TForm1.UsersValidateCredentials(Sender: TObject;
const aUsername, aPassword: string; var Valid: Boolean);
begin
Valid := MyDatabase.CheckLogin(aUsername, aPassword);
end;
관리 화면을 만든다면 중요한 세부 사항이 하나 있습니다. GetUserByIndex는 레코드를 돌려주기 전에 PasswordHash와 Salt를 비우므로, 목록 조회 라우트가 실수로라도 자격 증명을 유출할 수 없습니다. FindUser는 그렇게 하지 않습니다. 인증 게이트 자체가 사용하는 조회이기 때문에, 자격 증명 필드까지 포함해 저장된 그대로 레코드를 반환합니다. 여기서는 필요한 필드만 읽어서 쓰고, 레코드 전체를 응답 본문으로 직렬화하는 일은 절대 하지 마십시오.
멀티 테넌시
TsgcHTTPServer_Tenancy는 요청마다 하나의 질문에 답합니다. 이 요청은 어떤 고객을 위한 것인가? 핸들러가 실행되기 전에 테넌트 문자열을 결정하고, 서버는 읽기 전용 Tenant 속성으로 이를 노출합니다.
FTenancy := TsgcHTTPServer_Tenancy.Create(self);
FTenancy.Resolution := trHeader;
FTenancy.HeaderName := 'X-Tenant-Id';
FTenancy.DefaultTenant := 'public';
FServer.Tenancy := FTenancy;
결정 모드는 다섯 가지입니다.
| Resolution | 출처 | 설정 속성 |
|---|---|---|
trNone | 사용 안 함, Tenant는 항상 비어 있음 | — |
trHost | 호스트 이름 | HostSuffix |
trPath | 요청 경로의 한 세그먼트 | PathSegmentIndex |
trHeader | 요청 헤더 | HeaderName |
trJWTClaim | 베어러 토큰의 클레임 | ClaimName |
trHost를 사용하고 HostSuffix를 .example.com으로 설정하면 acme.example.com에 대한 요청은 acme로 결정됩니다. trPath에 PathSegmentIndex가 0이면 /acme/api/orders도 동일하게 결정됩니다. 설정한 출처에서 아무것도 얻지 못하면 DefaultTenant가 사용됩니다.
핸들러에서 읽는 것은 그저 속성 접근입니다.
procedure TForm1.ServerCommandGet(AContext: TIdContext;
ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
AResponseInfo.ResponseNo := 200;
AResponseInfo.ContentType := 'application/json';
AResponseInfo.ContentText := '{"tenant":"' + FServer.Tenant + '"}';
end;
Tenant는 OnBeforeCommand, OnCommandGet, OnCommandOther 안에서 유효하며 스레드 지역 값이므로, 여러 테넌트를 동시에 처리하는 바쁜 서버에서도 서로 섞이는 일이 없습니다.
trJWTClaim에 대해서는 분명히 해둘 점이 하나 있습니다. 클레임은 서명을 검증하지 않고 토큰 페이로드에서 읽습니다. 테넌트는 라우팅 힌트일 뿐이기 때문입니다. 서명은 JWT 인증을 활성화했을 때 여전히 검사됩니다. 테넌트 자체를 신원의 증거로 취급하지 마세요.
다섯 가지 모드 중 어느 것도 맞지 않는다면, OnResolveTenant가 모든 출처를 한꺼번에 넘겨주고 판단을 여러분에게 맡깁니다.
procedure TForm1.TenancyResolveTenant(Sender: TObject;
const aHost, aPath, aHeaderValue, aJWTPayload: string; var aTenant: string);
begin
aTenant := LookupTenantForHost(aHost);
end;
메트릭과 헬스
TsgcHTTPServerStats는 서버가 한 일을 집계하고 두 개의 엔드포인트로 공개할 수 있습니다. 둘 다 개별적으로 활성화하기 전까지는 꺼져 있으므로, 폼에 컴포넌트를 올려놓았다는 이유만으로 무언가 접근 가능해지는 일은 없습니다.
FStats := TsgcHTTPServerStats.Create(self);
FStats.Endpoints.Metrics.Enabled := True;
FStats.Endpoints.Health.Enabled := True;
FServer.ServerStats := FStats;
카운터는 코드에서도 읽을 수 있어서 내부 상태 화면을 만들 때 유용합니다.
lblRequests.Caption := IntToStr(FStats.TotalRequests);
lblErrors.Caption := IntToStr(FStats.Status5xx);
lblLatency.Caption := IntToStr(FStats.LatencyAvgMs) + ' ms';
lblUptime.Caption := IntToStr(FStats.UptimeSeconds) + ' s';
/metrics는 중간에 익스포터 없이 Prometheus 텍스트 노출 형식으로 직접 응답합니다. 엔드포인트별 카운터도 포함되며, 서로 다른 경로는 256개로 카디널리티가 제한되어 있어 경로에 id가 들어가는 라우트가 시리즈 수를 폭증시킬 수 없습니다. 제한을 넘는 것은 모두 other로 묶입니다.
# HELP sgc_server_endpoint_requests_total Requests per endpoint
# TYPE sgc_server_endpoint_requests_total counter
sgc_server_endpoint_requests_total{endpoint="/api/orders"} 3120
sgc_server_endpoint_requests_total{endpoint="/api/users"} 845
방화벽, 속도 제한기, 서킷 브레이커, API 키 관리자 컴포넌트가 연결되어 있으면 각각의 메트릭도 같은 출력에 추가됩니다.
FStats.RateLimiter := FRateLimiter;
FStats.Firewall := FFirewall;
/health는 간결한 JSON 문서로 응답합니다. status는 ok를 반환하고, 서킷 브레이커가 연결되어 있고 열린 브레이커가 있으면 degraded를 반환하므로 로드 밸런서 프로브로 바로 사용할 수 있습니다.
마지막으로, 수치를 직접 어딘가로 보내고 싶다면 OnStats가 전체 통계 객체와 함께 발생합니다.
procedure TForm1.StatsEvent(Sender: TObject;
const aStats: TsgcHTTPServerStats);
begin
MyTelemetry.Send(aStats.TotalRequests, aStats.LatencyAvgMs);
end;
모두 조합하기
셋을 모두 연결한 서버는 열댓 줄 정도이며, 각 컴포넌트는 서로 독립적으로 유지됩니다.
FStats := TsgcHTTPServerStats.Create(self);
FStats.Endpoints.Health.Enabled := True;
FStats.Endpoints.Metrics.Enabled := True;
FTenancy := TsgcHTTPServer_Tenancy.Create(self);
FTenancy.Resolution := trHeader;
FUsers := TsgcHTTPServer_Users.Create(self);
FUsers.Storage.StorageType := ustFile;
FUsers.Storage.FileName := 'users.dat';
FUsers.LoadUsers;
FServer := TsgcHTTPRESTServer.Create(self);
FServer.ServerStats := FStats;
FServer.Tenancy := FTenancy;
FServer.Authentication.Users := FUsers;
FServer.Authentication.Enabled := True;
FServer.Port := 5876;
FServer.Active := True;
다음 글에서는 이 서버 앞에 OpenAPI 계약을 두어 라우팅, 검증, 문서가 모두 명세에서 나오도록 합니다.
sgcWebSockets 다운로드 페이지에서 최신 빌드를 받으세요.
