SPMS_API/SPMS.API/Documents/ServerDevelopmentGuide.md

326 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 1. 기술 스택 및 환경
| 기술 | 스택 |
|:----------|:------------------------------------|
| Framework | .NET 9.0 / ASP .NET Core |
| Database | MariaDB (Server Version AutoDetect) |
| ORM | Entity Framework Core (Code-First) |
| API Docs | Swagger (OpenAPI) |
| Cache | Redis |
| MQ | RabbitMQ |
| Logging | Serilog |
| Testing | xUnit, Moq |
---
# 2. 코드 컨벤션
## 2.1. 명명 규칙
- PascalCase (대문자 시작): 클래스명, 메서드명, 프로퍼티명, 파일명, public 필드
- camelCase (소문자 시작): 로컬 변수, 매개 변수
- _camelCase (언더바 시작): private 필드
- Interface 는 반드시 접두사 `I` 를 붙인다.
- 비동기 메서드는 접미사 `Async` 를 붙인다.
## 2.2. 문법 규칙
- `var` 사용: 자료형을 명확하게 우변에서 확인 가능할 경우에는 var를 적극 사용한다.
- 리턴 타입이 뭔지 모르는 경우에는 명시를 해준다.
```csharp
var user = new User(); // 가능
var count = GetCount(); // 불가능
int count = GetCount(); // 리턴 타입을 명시해줄 것
```
- 비동기(Asnyc/Await) 동작: I/O 작업(DB, API호출, 파일)은 **무조건 비동기**로 작성한다.
- 비동기 동작을 하는 메서드의 이름 뒤에는 `Asnyc` 를 붙인다.
- void 대신 `Task` 또는 `Task<T>`를 반환한다.
- LINQ: 가독성을 해치지 않는 선에서 적극 사용하되, 복잡한 쿼리는 쿼리 구문이나 분할 작성 할 것
- 주석 및 문서화 (Swagger): API 정의서와의 동기화를 위해 Controller와 DTO의 모든 Public 멤버에는 반드시 XML 주석(`/// <summary>`)을 작성해야 한다.
-
---
# 3. 아키텍쳐 및 개발 표준
## 3.1. 개요
- 의존성의 방향은 항상 외부에서 내부로만 향한다.
- 프로젝트 분리: 프로젝트는 클린 아키텍쳐를 기반으로 하여 물리적으로 4개의 .csproj 로 분리해 참조 규칙을 **강제**한다.
<br><br>
- 클린 아키텍쳐 샘플 이미지
![CleanArchitecture.png](CleanArchitecture.png)
## 3.2. 계층 구조 (Layers)
- 계층의 정의는 내부에서 외부 순서로 정의한다.
### 3.2.1. 계층 (Layers)
#### 1. Domain (Entities, Enterprise Business Rules)
- 역할: 비즈니스의 핵심 개념, 기업 업무 규칙(Entity)을 정의한다.
- 특징: 외부 라이브러리 (DB, Web, 등)에 대한 의존성이 **없어야** 한다. 순수한 C# 클래스로만 구성된다.
- 요소: Entites, Value Objects, Enums, Domain Exceptions, Repository Interfaces (선택)
#### 2. Application (Use cases, Application Business Rules)
- 역할: 애플리케이션의 비즈니스 로직을 처리하고 도메인과 외부를 연결하는 오케스트레이션 담당한다.
- 특징: 도메인 계층에만 의존하며, 인터페이스를 정의하고 구현은 외부 계층(Infra Structure)에 맡긴다.
- 요소: Service Interfaces, DTOs, Mappers, Validators, Service, Implementations (pure 로직)
#### 3. Infra Structure (Interface Adapters)
- 역할: 애플리케이션 계층에서 정의한 인터페이스를 실제로 구현한다. DB, 외부 API, 파일 시스템 등 외부 세계와 통신한다.
- 특징: 애플리케이션과 도메인을 참조하고 다양한 라이브러리를 사용한다.
- 요소: Repository 구현체, DB Context, 외부 API Client, Migrations
#### 4. Presentation (Frameworks & Drivers)
- 역할: 사용자의 요청(HTTP)을 받아 애플리케이션 계층으로 전달하고 결과를 반환한다.
- 특징: 로직을 가지지 않으며 애플리케이션에 의존한다.
- 요소: Controller, Middlewares, Filters, Program.cs (DI 설정)
### 3.2.2. 프로젝트 참조 규칙
- 프로젝트 내에서 참조 설정을 시스템 상에서 해줘서 파일들 간의 물리적인 연결 고리를 룰에 맞게 설정해 줘야 한다.
- Domain: 참조 없음
- Application: Domain
- Infra Structure: Application, Domain
- Presentation: Application, Infra Structure
## 3.3. 개발 상세 가이드
### 3.3.1. API 프로토콜 및 데이터 규격
- 클라이언트와 서버 간의 모든 통신은 아래 정의된 Header와 Body 규격을 준수해야 한다.
#### 1. 요청 규격 (Request)
- 클라이언트는 API 호출 시, HTTP Header에 다음 필수 메타데이터를 포함해야 한다.
- 메타데이터들은 로그 컨텍스트 식별을 위해 평문으로 전송한다.
| 헤더 명 | 설명 및 용도 | 예시 |
| --- |-------------------------------------| --- |
| Content-Type | 전송 데이터 포맷 | application/octet-stream |
| Authorization | JWT 인증 토큰<br>- L2 등급 이상 필수 | Bearer abcdefg… |
| X-API-KEY | 프로젝트 식별 및 데이터 격리를 위한 고유 키 (테넌트 식별자) | spms_api_custom_key |
| X-Request-ID | 트랜잭션 추적 ID<br>- 매요청 마다 고유값 생성<br>- 재사용 금지 | abcdefg-12aa… |
#### 2. 응답 규격 (Response)
- 서버는 비즈니스 로직의 결과와 상관 없이 항상 아래의 공통 JSON포맷으로 응답한다.
- 단, Body의 내용물인 data 필드는 보안 등급에 따라 암호화될 수 있다.
```json
{
"req_id": "abcdefg-12aa…", "result": "success", "code": "0000", "msg": "Response Success MESSAGE", "data": { ... } }
``` - req_id: 요청 헤더의 X-Request-ID를 그대로 반환 (없을 경우 서버 생성), 비동기 응답 식별용
- result: API 호출 결과에 따라 성공(success) / 실패(fail) 여부 반환
- code: API 명세서에 선언된 코드 값 반환 (성공시 0000)
- msg: 현재 동작에 대한 결과 메시지 반환
- data: 현재 동작에 대한 데이터 반환 (E2EE 적용시 암호화된 문자열)
#### 3. 데이터 매핑 원칙
- Entity 노출 금지 (Layer: Domain)
- DB 테이블과 매핑되거나 핵심 로직을 가진 객체로, **절대** Controller 밖으로 노출하지 않는다.
- DTO 필수 (Layer: Application): 데이터 전송을 위한 껍데기 객체
- Controller는 반드시 `Request DTO` 를 받고 `Response DTO` 를 반환해야 한다.
- API 스펙 변경이 도메인 모델에 영향을 주어서는 안되며 그 반대의 경우도 마찬가지이다.
- Body 암호화 정책
- [3.3.8. 보안 정책](#338-데이터-보안-및-암호화)에 의거 **L3**등급 이상의 data 필드는 암호화된다.
### 3.3.2. 비즈니스 로직과 의존성 주입 (DI)
#### 1. 비즈니스 로직 위치
- 단순 데이터 조회 / 저장
- Repository (Layer: Infra Structure)에서 담당하되, 내부 구현 로직이 없어야 한다.
- 업무 규칙 (유효성 검사, 계산, 흐름 제어)
- 반드시 Service (Layer: Application) 또는 Entity (Layer: Domain) 내부에 위치해야 한다.
- Controller
- 요청을 받고 Service를 호출하고 결과를 DTO로 변환하는 역할만 수행한다.
#### 2. 의존성 역전 원칙 (DIP)
- Interface 정의(Layer: Application)
- 특정한 동작에 대한 정의서를 Application 계층에서 선언한다.
- 예: 유저 정보를 저장하는 기능의 필요 = IUserRepository
- 구현(Layer: Infra Structure)
- 해당 동작의 특별한 동작 구현을 Infra Structure 계층에서 구현한다.
- 예: 저장 기능을 EF Core로 구현 = UserRepository
- 주입 (Layer: Presentation)
- Program.cs 에서 DI를 통해 둘을 연결해준다.
- 예: builder.Services.AddScoped<IUserRepository, UserRepository>();
### 3.3.3. 데이터베이스 접근 (EF Core)
1. Code-First
- C# 엔티티 코드가 메인이 되며 Migration 명령어로 DB를 업데이트 한다.
2. Fluent API 사용
- Entity 클래스의 순수성을 위해 [Key], [Table] 과 같은 어트리뷰트 대신 DbContext 내 OnModelCreating 또는 별도의 IEntityTypeConfiguration 파일에서 설정한다.
3. Production 배포 전략
- 개발/스테이징 환경에서는 자동 마이그레이션을 허용하나, 운영(Production) 환경에서는 절대 Database.Migrate() 자동 실행을 금지한다.
4. Script Migration
- 반드시 dotnet ef migrations script 명령어로 SQL 스크립트를 생성하여, DBA 또는 관리자의 검수를 거친 후 수동/CI 파이프라인을 통해 적용한다.
### 3.3.4. 유효성 관리
- FluentValidation 사용: 잦은 if문 대신 `AbstractValidator<T>` 를 상속 받은 별도 클래스로 분리한다.
- 동작: Controller 진입 전 또는 Service 실행 시점에 자동 검증한다.
- 위치: SPMS.Application/Validators
### 3.3.5. 트랜잭션 관리
- 서비스 단위: 하나의 Service 메서드가 하나의 트랜잭션 단위가 된다.
- 원자성 (Atomicity): 원자적인 작업 단위로 모든 작업이 성공적으로 완료되지 않는다면, 즉 하나라도 실패하면 전체 롤백한다.
### 3.3.6. 예외 처리
- Global Handling: 개별 메서드에서 try-catch로 에러를 삼키지 않는다.
- Custom Exception: 비즈니스 로직 에러는 의도적으로 `throw new BusinessExeption(”msg”)`을 발생시킨다
- Middleware: 전역 미들웨어에서 에러를 잡아 공통 포맷으로 변환하여 응답한다.
### 3.3.7. 인증 및 접근 제어
- 시스템 접근 권한은 Who, Where, How often 의 3중 체계로 검증한다.
#### 1. 인증 - JWT
- Stateless 구조를 위해 JWT 방식을 표준으로 한다.
- 구현
- Authorization 헤더에 Bearer Token 을 담아 전송하며 만료 시 Refresh Token 으로 갱신한다.
#### 2. 인가 - 역할 기반 엑세스 제어 (RBAC)
- 사용자의 Role(Admin, Manager, User) 에 따라 API 접근 권한을 엄격히 분리한다.
- 구현
- Controller 상단에 `[Authorize(Roles="...")]` 어트리뷰트를 명시해 코드 레벨에서 권한을 강제한다.
#### 3. 네트워크 접근 제어
- API Key가 탈취되더라도 허용되지 않은 IP에서의 접근을 원천 차단한다.
- 구현
- 모든 API요청 시 미들웨어에서 클라이언트 IP를 추출한다.
- ServiceIP 테이블에 등록된 IP 대역인지 검증하고 불일치 시 403 Forbidden 처리한다.
#### 4. API 속도 제한
- Dos 공격 및 루프로 인한 과부하 방지
- AspNetCoreRateLimit 을 사용하여 IP 주소 또는 API Key 별로 초당/분당 요청 쿼터를 제한한다.(초과시 429 Too Many Requests)
### 3.3.8. 데이터 보안 및 암호화
- 데이터의 생명 주기 (저장, 전송) 전반에 걸쳐 데이터 등급에 따른 암호화 정책을 적용한다.
#### 1. 데이터 등급 분류
| 등급 | 암호화 방식 | 대상 |
| --- | --- | --- |
| L1 (Public) | 평문 | 공지사항, 일반 리소스 |
| L2 (Authenticated) | HTTPS + JWT | 일반 서비스 데이터 |
| L3 (Sensitive) | HTTPS + E2EE + AES-256 저장 | API Key, 토큰 |
| L4 (Critical) | HTTPS + E2EE + BCrypt 저장 | 비밀번호 |
#### 2. 암호화 - 저장
- 양방향 암호화: 외부 연동에 필요한 민감 정보는 AES-256 (CBC/GCM) 알고리즘으로 암호화해 DB에 저장한다.
- 단방향 해시: 비밀번호 등 복호화가 불필요한 정보는 BCrypt 알고리즘(Salt 포함)을 사용해 해싱 저장한다. (절대 평문 저장 금지)
- Key 관리: 암호화 키는 소스 코드가 아닌 환경 변수 또는 Key Vault로 관리한다.
#### 3. 암호화 - 전송
- 적용 대상 : L3, L4 등급의 데이터 전송 시 필수 적용
- 알고리즘: AES-256 (데이터 암호화) + RSA-2048 (키 교환) 하이브리드 방식
- 구현 프로세스
- **[요청 시: Client → Server]**
1. 암호화 값 생성: Client 는 요청마다 새로운 일회용 대칭키(AES Key) 와 IV 를 생성한다.
2. 데이터 암호화: 요청 본문을 생성한 Key와 IV로 암호화 한다.
3. 키 암호화: 생성한 AES Key를 서버의 공개키로 암호화한다. (RSA)
4. 패킹 구조 (Parsing 효율성을 위해 고정 길이 필드 우선 배치)
- JSON 포맷 사용 금지: 페이로드의 효율성과 구조 은닉을 위해 바이트 배열로 직렬화하여 전송한다.
- 구조: `[ Encrypted AES Key (256 Bytes) ] + [ IV (16 Bytes) ] + [ Encrypted Body (Variable) ]`
- Body가 없는 단순 조회 요청이라도 Key와 IV는 **필수 전송**한다.(Body = 0 Byte)
- 파싱 전략
- Server는 수신된 바이너리 스트림의 첫 256바이트를 잘라 RSA 복호화하여 AES Key 획득
- 다음 16바이트를 잘라 IV 획득
- 나머지 바이트를 AES 복호화해 원본 Body 획득
- **[응답 시: Server → Client]**
1. 키 재사용 : 응답 시에는 키 교환을 하지 않고, 요청 패킷에서 획득한 AES Key를 그대로 재사용한다.
2. 새로운 IV 생성: 보안 강화를 위해 응답을 위한 새로운 IV를 새로 생성한다.
3. 패킹 구조 (Parsing 효율성을 위해 고정 길이 필드 우선 배치)
- 구조: `[ IV (16 Bytes) ] + [ Encrypted Body (Variable) ]`
- Client 는 자신이 보냈던 Key와 응답에 포함된 IV 를 사용해 복호화 한다.
4. 복호화: Client는 자신이 갖고 있던 AES Key와 응답 패킷의 IV를 사용해 본문을 복호화한다.
### 3.3.9. 보안 감사 및 시큐어 코딩
- 이미 정의된 네트워크/암호화 보안 외에 Application 계층에서 발생할 수 있는 취약점을 방어하고 추적성을 확보한다.
#### 1. 보안 감사 로그
- 저장소: SystemLog 테이블 (비동기 저장)
- 기록 대상
- 인증 실패: 로그인 n회 연속 실패, 비인가 프로젝트 키 접근 시도
- 권한 위반: 401 Unauthorized, 403 Forbidden 응답 발생한 모든 요청
- 중요한 변경: 관리자에 의한 데이터 변경, 데이터 삭제 행위
- 로그 마스킹
- 헤더 및 개인정보는 로그 저장 시 반드시 마스킹 처리를 한다.
- 비밀번호, 암호화 키 원문은 어떠한 경우에도 로그 파일이나 콘솔에 평문으로 출력하지 않는다.
#### 2. 시큐어 코딩
- SQL Injection 방지: Raw Query 사용을 금지하며 반드시 EF Core(ORM) 메서드나 Parameterized Query 만 사용한다.
- XSS(Cross-Site Scripting) 방지
- BO 에서 HTML 입력을 허용해야 하는 경우 `<script>`, `<iframe>` 등 위험 태그를 제거 후 저장한다. (라이브러리: HtmlSanitizer)
- 그 외 모든 입/출력 데이터는 기본적으로 HTML Encode 처리한다.
### 3.3.10. 웹 보안 및 헤더 정책
- [3.3.1의 요청 규격](#331-api-프로토콜-및-데이터-규격)과 달리 서버가 BO 등 브라우저 클라이언트 보호를 위해 응답시 자동으로 주입하는 보안 헤더 정책이다.
- 미들웨어에서 일괄 처리해서 진행한다.
1. HTTPS 강제 (HSTS)
- 중간자 공격 방지 및 프로토콜 보안 강화
- 설정: Strict-Transport-Security: max-age=31536000; includeSubDomains
2. CORS
- `AllowAnyOrigin(*)` 설정을 **절대 금지**한다.
- 설정: 화이트리스트에 등록된 도메인(Admin URL, Partner URL)의 요청에 대해서만 허용 응답을 반환한다.
3. 보안 헤더 자동 주입
- 모든 API 응답에 아래 헤더를 강제로 포함시켜 브라우저의 보안 기능을 활성화한다.
| 헤더 명 | 방어 목적 | 설정 값 |
| ----------------------- | -------------------------------------- | ------------------- |
| X-Frame-Options | 클릭재킹(Clickjacking) 및 `<iframe>` 임베딩 차단 | DENY |
| X-XSS-Protection | 브라우저 내장 XSS 필터 강제 활성화 | 1; mode=block |
| X-Content-Type-Options | MIME 타입 스니핑 차단 (악성 파일 실행 방지) | nosniff |
| Content-Security-Policy | 승인된 소스 외의 스크립트/리소스 로딩 차단 | default-src 'self'; |
### 3.3.11. 핵심 기술 구현 전략
- 주요 난제인 대량 트래픽 처리와 멀티 테넌시 격리를 위해 아래 4가지를 강제한다.
#### 1. 멀티 테넌시 격리
- Context Caching
- 미들웨어는 X-API-KEY 검증 시 매번 DB를 조회하지 않고, Redis 캐시를 우선 조회하여 ProjectID를 식별한다.
- Global Query Filter
- 식별된 ProjectID를 EF Core의 Global Query Filter에 주입하여, 개발자의 실수로 인한 타 프로젝트 데이터 조회를 원천 차단한다.
#### 2. 멱등성 보장
- 중복 발송 방지
- 네트워크 타임아웃으로 인한 재시도로 중복 실행을 막기 위해, 요청 진입 시 Redis에 X-Request-ID를 키로 조회한다.
- Atomic Check
- 이미 처리 중이거나 완료된 ID라면 작업을 즉시 폐기(Discard)한다
#### 3. 대량 처리 최적화
- Bulk Insert
- 대량의 데이터 저장시 단건 Loop Insert를 금지하며, EF Core Bulk Extensions 를 사용하여 단일 커넥션으로 처리한다.
- MQ QoS (Rabbit MQ)
- Worker 서비스는 RabbitMQ의 Prefetch Count를 적정값으로 설정하여 메모리 과부하 없이 안정적으로 메시지를 소비해야 한다.
#### 4. 데드 토큰 자동 정리
- Worker 서비스가 발송 결과를 처리할 때 APNs/FCM 으로부터 Unregistered(앱 삭제) 또는 BadDeviceToken 응답을 받으면 즉시 해당 토큰을 DB에서 물리 삭제하여 비용 낭비를 방지한다.
### 3.3.12. API 버전 관리 (Versioning Strategy)
- URI Versioning: 모든 API 엔드포인트는 URI 경로에 버전을 명시하여, 클라이언트의 강제 업데이트 없이도 구버전을 지원해야 한다.
- 규칙: `GET /api/v1/users` (메이저 버전만 명시)
- 정책: Breaking Change(필드 삭제, 타입 변경 등)가 발생할 경우에만 v2로 버전을 올리며, 기존 v1은 최소 N개월간 유지(Deprecated) 후 종료한다.
### 3.3.13. 시스템 회복 탄력성 (Resilience & Fault Tolerance)
- 외부 시스템(DB, Redis, FCM, APNs) 장애가 전체 시스템 중단으로 전파되는 것을 막기 위해 Polly 라이브러리를 사용하여 아래 정책을 적용한다.
- Retry (재시도): 일시적인 네트워크 오류 발생 시, Exponential Backoff(지수 대기: 2초, 4초, 8초...) 방식으로 최대 3회 재시도한다.
- Circuit Breaker (차단기): 특정 외부 시스템(예: FCM)이 연속 N회 실패 시, 즉시 회로를 차단(Open)하여 대기 시간 없이 Fail-Fast 처리하고 시스템 리소스를 보호한다.
(일정 시간 후 재접속 시도)
- Timeout: 모든 외부 호출에는 반드시 제한 시간(Timeout)을 설정한다.
### 3.3.14. 상태 모니터링 (Health Checks)
- 로드밸런서 및 모니터링 시스템이 서버의 생존 여부를 판단할 수 있도록 표준 엔드포인트를 제공한다.
- Endpoint: `GET /health`
- 검사 항목
- Liveness: 서버 프로세스가 떠 있는가? (단순 Ping)
- Readiness: DB, Redis, RabbitMQ와 정상적으로 연결되어 트래픽을 받을 준비가 되었는가?
- 응답: 모든 연결 정상 시 200 OK, 하나라도 실패 시 503 Service Unavailable 반환.
## 3.4. 폴더 및 네임스페이스 구조
```
📂 SPMS.Solution
├── 📂 SPMS.Domain
│ ├── Entities (User.cs, Project.cs)
│ ├── Enums (UserRole.cs)
│ └── Exceptions (UserNotFoundException.cs)
├── 📂 SPMS.Application
│ ├── Interfaces (IUserService.cs, IUserRepository.cs)
│ ├── DTOs (Req, Res)
│ ├── Services (UserService.cs - 인터페이스 구현 아님, 로직 담당)
│ └── Mappers (MappingProfile.cs)
├── 📂 SPMS.Infrastructure
│ ├── Persistence (AppDbContext.cs)
│ ├── Repositories (UserRepository.cs - IUserRepository 구현)
│ └── ExternalServices (EmailService.cs)
└── 📂 SPMS.API (ASP.NET Core Web API)
├── Controllers (UsersController.cs)
├── Middlewares
└── Program.cs
```