Backend 세부 설명
작성일: 2025-08
문서 버전: v 1.0
시리즈: 4/4 — Spring Boot 백엔드 아키텍처
1. 백엔드 설계 원칙
본 프로젝트의 백엔드는 최소 범위 원칙을 따른다. 모든 추론·분석은 온디바이스에서 처리하므로, 서버는 다음 네 가지 역할만 담당한다.
| 역할 | 설명 | 우선순위 |
|---|---|---|
| 모델 배포 | TFLite / ONNX 모델 파일 버전 관리 및 배포 | Phase 1 |
| FCM 알림 | 건강 이상 징후 감지 시 푸시 알림 발송 | Phase 1 |
| 통계 집계 | 익명화된 건강 데이터 집계 (품종별 기준값 산출) | Phase 2 |
| 사용자 인증 | 기기 등록, 다기기 프로필 동기화 | Phase 2 |
원칙: 백엔드 없이도 앱의 핵심 기능(촬영·분석·기록)은 완전히 동작해야 한다. 서버는 앱을 보조하는 역할에 그친다.
2. 전체 시스템 구성
┌─────────────────────────────────────────────────────────────────┐
│ Android App │
│ (온디바이스 추론 — 서버 불필요) │
└──────────────────────┬──────────────────────────────────────────┘
│ HTTPS / REST
│
┌────────────▼────────────┐
│ API Gateway │
│ (Spring Cloud Gateway │
│ 또는 Nginx) │
└────────────┬────────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌────▼────┐ ┌─────▼────┐ ┌────▼────┐
│ Model │ │ Stats │ │ Auth │
│ Service │ │ Service │ │ Service │
└────┬────┘ └─────┬────┘ └────┬────┘
│ │ │
┌────▼─────────────▼─────────────▼────┐
│ 공유 인프라 │
│ PostgreSQL │ Redis │ S3 (모델 파일) │
└─────────────────────────────────────┘
│
┌────────────▼────────────┐
│ Firebase Cloud │
│ Messaging (FCM) │
└─────────────────────────┘
3. 기술 스택
| 분류 | 기술 | 버전 | 선택 이유 |
|---|---|---|---|
| 프레임워크 | Spring Boot | 3.3. x | Kotlin 네이티브 지원, 생태계 성숙 |
| 언어 | Kotlin | 2.0. x | Android와 언어 통일, 코루틴 지원 |
| 비동기 | Spring WebFlux (선택적) | - | 모델 다운로드 등 I/O 집약 엔드포인트 |
| ORM | Spring Data JPA + Hibernate | - | 표준, 복잡 쿼리는 QueryDSL 보완 |
| DB | PostgreSQL | 16. x | JSONB 지원, 안정성 |
| 캐시 | Redis | 7. x | 모델 버전 캐싱, FCM 토큰 관리 |
| 파일 저장 | AWS S 3 (또는 MinIO 로컬) | - | 모델 파일 (. tflite, .onnx) 저장 |
| 인증 | JWT + Spring Security | - | Stateless, 모바일 친화적 |
| 알림 | Firebase Admin SDK | - | FCM 푸시 알림 |
| 문서화 | Springdoc OpenAPI 3 | - | Swagger UI 자동 생성 |
| 컨테이너 | Docker + Docker Compose | - | 로컬·서버 환경 통일 |
| 오케스트레이션 | Kubernetes (k 8 s) | - | 프로덕션 배포 (Phase 2) |
| CI/CD | GitHub Actions | - | 자동 빌드·테스트·배포 |
| 모니터링 | Prometheus + Grafana | - | 메트릭 수집·시각화 |
| 로그 | Logback + ELK Stack | - | 구조화 로그, 검색 |
4. 프로젝트 구조
backend/
├── build.gradle.kts
├── settings.gradle.kts
│
├── gateway/ # API Gateway 모듈
│ └── src/main/kotlin/
│ └── GatewayApplication.kt
│
├── model-service/ # 모델 배포 서비스
│ └── src/main/kotlin/
│ ├── ModelServiceApplication.kt
│ ├── controller/
│ │ └── ModelController.kt
│ ├── service/
│ │ ├── ModelService.kt
│ │ └── S3StorageService.kt
│ ├── repository/
│ │ └── ModelVersionRepository.kt
│ └── domain/
│ └── ModelVersion.kt
│
├── stats-service/ # 통계 집계 서비스
│ └── src/main/kotlin/
│ ├── controller/
│ │ └── StatsController.kt
│ ├── service/
│ │ └── StatsAggregationService.kt
│ └── repository/
│ └── HealthStatsRepository.kt
│
├── auth-service/ # 인증 서비스
│ └── src/main/kotlin/
│ ├── controller/
│ │ └── AuthController.kt
│ ├── service/
│ │ ├── AuthService.kt
│ │ └── FcmTokenService.kt
│ └── security/
│ ├── JwtProvider.kt
│ └── SecurityConfig.kt
│
└── common/ # 공통 모듈
└── src/main/kotlin/
├── exception/
│ ├── GlobalExceptionHandler.kt
│ └── ApiException.kt
├── response/
│ └── ApiResponse.kt
└── config/
└── RedisConfig.kt
5. 모델 배포 서비스 (model-service)
5.1 역할
- 앱이 시작될 때 현재 설치된 모델 버전과 서버 최신 버전을 비교
- 신규 버전 존재 시 S 3 다운로드 URL 제공
- 모델 파일 무결성 검증을 위한 SHA-256 체크섬 제공
5.2 도메인 모델
// ModelVersion.kt
@Entity
@Table(name = "model_versions")
data class ModelVersion(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
@Column(nullable = false)
val modelType: String, // "breed_classifier" | "health_scorer"
@Column(nullable = false)
val version: String, // SemVer: "1.2.0"
@Column(nullable = false)
val s3Key: String, // S3 객체 키
@Column(nullable = false)
val checksum: String, // SHA-256
@Column(nullable = false)
val fileSizeBytes: Long,
@Column(nullable = false)
val isActive: Boolean = true, // 현재 배포 버전 여부
@Column(nullable = false)
val releaseNote: String = "",
val createdAt: LocalDateTime = LocalDateTime.now()
)
5.3 API 명세
GET /api/v1/models/latest
앱 시작 시 최신 모델 버전 정보 조회.
Request
GET /api/v1/models/latest
Authorization: Bearer {jwt}
Query Parameters:
- breed_classifier_version: String (현재 설치 버전)
- health_scorer_version: String (현재 설치 버전)
Response
{
"status": "success",
"data": {
"breed_classifier": {
"current_version": "1.0.0",
"latest_version": "1.2.0",
"update_available": true,
"download_url": "https://s3.../models/cat_breed_int8_v1.2.0.tflite",
"checksum": "sha256:a3f2...",
"file_size_bytes": 4194304,
"url_expires_at": "2025-08-01T12:00:00Z"
},
"health_scorer": {
"current_version": "1.0.0",
"latest_version": "1.0.0",
"update_available": false
}
}
}
POST /api/v1/models/upload (관리자 전용)
새 모델 버전 등록.
// ModelController.kt
@RestController
@RequestMapping("/api/v1/models")
class ModelController(
private val modelService: ModelService
) {
@GetMapping("/latest")
fun getLatestVersions(
@RequestParam breedClassifierVersion: String,
@RequestParam healthScorerVersion: String,
@AuthenticationPrincipal user: DevicePrincipal
): ResponseEntity<ApiResponse<ModelUpdateResponse>> {
val response = modelService.checkUpdates(
installedVersions = mapOf(
"breed_classifier" to breedClassifierVersion,
"health_scorer" to healthScorerVersion
)
)
return ResponseEntity.ok(ApiResponse.success(response))
}
@PostMapping("/upload")
@PreAuthorize("hasRole('ADMIN')")
fun uploadModel(
@RequestPart file: MultipartFile,
@RequestPart metadata: ModelUploadRequest
): ResponseEntity<ApiResponse<ModelVersion>> {
val version = modelService.uploadNewVersion(file, metadata)
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(version))
}
}
5.4 서비스 구현
// ModelService.kt
@Service
@Transactional(readOnly = true)
class ModelService(
private val repository: ModelVersionRepository,
private val s3Service: S3StorageService,
private val redis: RedisTemplate<String, String>
) {
companion object {
private const val CACHE_KEY_PREFIX = "model:latest:"
private const val CACHE_TTL_MINUTES = 10L
}
fun checkUpdates(installedVersions: Map<String, String>): ModelUpdateResponse {
return ModelUpdateResponse(
models = installedVersions.entries.associate { (type, installedVer) ->
type to getUpdateInfo(type, installedVer)
}
)
}
private fun getUpdateInfo(modelType: String, installedVersion: String): ModelUpdateInfo {
// Redis 캐시 우선 조회
val cacheKey = "$CACHE_KEY_PREFIX$modelType"
val cached = redis.opsForValue().get(cacheKey)
val latest = cached?.let { objectMapper.readValue<ModelVersion>(it) }
?: repository.findTopByModelTypeAndIsActiveTrueOrderByCreatedAtDesc(modelType)
?.also { version ->
redis.opsForValue().set(
cacheKey,
objectMapper.writeValueAsString(version),
CACHE_TTL_MINUTES, TimeUnit.MINUTES
)
}
?: return ModelUpdateInfo(updateAvailable = false)
val needsUpdate = isNewerVersion(latest.version, installedVersion)
return ModelUpdateInfo(
currentVersion = installedVersion,
latestVersion = latest.version,
updateAvailable = needsUpdate,
downloadUrl = if (needsUpdate) s3Service.generatePresignedUrl(latest.s3Key) else null,
checksum = if (needsUpdate) latest.checksum else null,
fileSizeBytes = if (needsUpdate) latest.fileSizeBytes else null
)
}
// SemVer 비교: "1.2.0" > "1.0.0" → true
private fun isNewerVersion(latest: String, installed: String): Boolean {
val l = latest.split(".").map { it.toInt() }
val i = installed.split(".").map { it.toInt() }
return (0..2).any { idx -> l[idx] > i[idx] }
&& (0 until (0..2).indexOfFirst { l[it] != i[it] }).all { l[it] == i[it] }
}
}
6. 통계 집계 서비스 (stats-service)
6.1 역할
- 앱에서 익명화된 건강 분석 결과를 수집
- 품종별 건강 기준값(Baseline) 산출 → 앱으로 배포
- 이상 패턴 발생 빈도 집계 (연구 목적)
6.2 개인정보 보호 설계
앱에서 서버로 전송하는 데이터는 다음 원칙을 따른다.
전송 O:
- 익명 기기 ID (UUID, 사용자 식별 불가)
- 품종 코드 (문자열)
- 건강 점수 (숫자)
- 행동 태그 목록 (열거형 코드)
- 타임스탬프 (일 단위 버킷 — 정확한 시각 미포함)
전송 X:
- 카메라 영상 / 이미지
- 위치 정보
- 사용자 개인 식별 정보
- 고양이 이름 등 개인화 정보
6.3 API 명세
POST /api/v1/stats/report
분석 결과 익명 리포트 전송 (선택적, 사용자 동의 기반).
Request
{
"device_id": "anon-uuid-1234",
"breed_code": "SCOTTISH_FOLD",
"health_score": 82.5,
"behavior_tags": ["NORMAL", "SLIGHTLY_LOW_ACTIVITY"],
"date_bucket": "2025-08-01"
}
GET /api/v1/stats/baseline/{breedCode}
품종별 건강 기준값 조회 (앱에서 점수 보정에 활용).
Response
{
"status": "success",
"data": {
"breed_code": "SCOTTISH_FOLD",
"sample_count": 1482,
"avg_health_score": 78.3,
"avg_activity_index": 0.42,
"common_behavior_tags": ["NORMAL", "GROOMING"],
"updated_at": "2025-07-31"
}
}
6.4 서비스 구현
// StatsAggregationService.kt
@Service
class StatsAggregationService(
private val statsRepository: HealthStatsRepository
) {
// 매일 새벽 2시 기준값 재계산
@Scheduled(cron = "0 0 2 * * *")
fun recalculateBaselines() {
val breeds = statsRepository.findDistinctBreedCodes()
breeds.forEach { breedCode ->
val stats = statsRepository.aggregateByBreed(breedCode)
statsRepository.upsertBaseline(
BreedBaseline(
breedCode = breedCode,
sampleCount = stats.count,
avgHealthScore = stats.avgScore,
avgActivityIdx = stats.avgActivity,
updatedAt = LocalDate.now()
)
)
}
}
}
7. 인증 서비스 (auth-service)
7.1 인증 흐름
앱 최초 실행
│
▼
POST /api/v1/auth/device/register
{ device_id: UUID, platform: "android", app_version: "1.0.0" }
│
▼
서버: 기기 등록 → JWT (Access + Refresh) 발급
│
▼
앱: JWT 로컬 저장 (DataStore Encrypted)
│
▼
이후 모든 API 요청: Authorization: Bearer {access_token}
│
▼
Access Token 만료 (7일)
│
▼
POST /api/v1/auth/token/refresh
{ refresh_token: "..." }
│
▼
새 Access Token 발급
7.2 JWT 설정
// JwtProvider.kt
@Component
class JwtProvider(
@Value("\${jwt.secret}") private val secret: String,
@Value("\${jwt.access-token-expiry-days:7}") private val accessTokenExpiryDays: Long,
@Value("\${jwt.refresh-token-expiry-days:90}") private val refreshTokenExpiryDays: Long
) {
private val key: SecretKey by lazy {
Keys.hmacShaKeyFor(Base64.getDecoder().decode(secret))
}
fun generateAccessToken(deviceId: String): String =
Jwts.builder()
.subject(deviceId)
.claim("type", "access")
.issuedAt(Date())
.expiration(Date(System.currentTimeMillis()
+ TimeUnit.DAYS.toMillis(accessTokenExpiryDays)))
.signWith(key)
.compact()
fun generateRefreshToken(deviceId: String): String =
Jwts.builder()
.subject(deviceId)
.claim("type", "refresh")
.issuedAt(Date())
.expiration(Date(System.currentTimeMillis()
+ TimeUnit.DAYS.toMillis(refreshTokenExpiryDays)))
.signWith(key)
.compact()
fun validateAndExtractDeviceId(token: String): String =
Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.payload
.subject
}
7.3 FCM 토큰 관리
// FcmTokenService.kt
@Service
class FcmTokenService(
private val redis: RedisTemplate<String, String>,
private val firebaseMessaging: FirebaseMessaging
) {
companion object {
private const val FCM_KEY_PREFIX = "fcm:token:"
}
// 앱에서 FCM 토큰 등록/갱신
fun registerToken(deviceId: String, fcmToken: String) {
redis.opsForValue().set(
"$FCM_KEY_PREFIX$deviceId",
fcmToken,
90, TimeUnit.DAYS // Refresh Token 만료와 동기화
)
}
// 건강 이상 징후 알림 발송
fun sendHealthAlert(deviceId: String, alert: HealthAlert) {
val token = redis.opsForValue().get("$FCM_KEY_PREFIX$deviceId")
?: return // 토큰 없으면 알림 불가 — 무시
val message = Message.builder()
.setToken(token)
.setNotification(
Notification.builder()
.setTitle(alert.title)
.setBody(alert.body)
.build()
)
.putData("alert_type", alert.type.name)
.putData("health_score", alert.score.toString())
.setAndroidConfig(
AndroidConfig.builder()
.setPriority(AndroidConfig.Priority.HIGH)
.build()
)
.build()
try {
firebaseMessaging.send(message)
} catch (e: FirebaseMessagingException) {
if (e.messagingErrorCode == MessagingErrorCode.UNREGISTERED) {
// 만료 토큰 삭제
redis.delete("$FCM_KEY_PREFIX$deviceId")
}
}
}
}
8. 공통 모듈 (common)
8.1 통일된 API 응답 형식
모든 엔드포인트는 동일한 응답 래퍼를 사용한다.
// ApiResponse.kt
data class ApiResponse<T>(
val status: String,
val data: T? = null,
val error: ApiError? = null,
val timestamp: Long = System.currentTimeMillis()
) {
companion object {
fun <T> success(data: T) =
ApiResponse(status = "success", data = data)
fun error(code: String, message: String) =
ApiResponse<Nothing>(
status = "error",
error = ApiError(code, message)
)
}
}
data class ApiError(val code: String, val message: String)
8.2 전역 예외 처리
// GlobalExceptionHandler.kt
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(ApiException::class)
fun handleApiException(e: ApiException): ResponseEntity<ApiResponse<Nothing>> =
ResponseEntity
.status(e.httpStatus)
.body(ApiResponse.error(e.errorCode, e.message ?: "Unknown error"))
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidation(e: MethodArgumentNotValidException): ResponseEntity<ApiResponse<Nothing>> {
val message = e.bindingResult.fieldErrors
.joinToString(", ") { "${it.field}: ${it.defaultMessage}" }
return ResponseEntity
.badRequest()
.body(ApiResponse.error("VALIDATION_ERROR", message))
}
@ExceptionHandler(Exception::class)
fun handleUnexpected(e: Exception): ResponseEntity<ApiResponse<Nothing>> {
log.error("Unexpected error", e)
return ResponseEntity
.internalServerError()
.body(ApiResponse.error("INTERNAL_ERROR", "Internal server error"))
}
}
9. 데이터베이스 설계
9.1 ERD 요약
device_registrations model_versions
┌──────────────────┐ ┌───────────────────────┐
│ id │ │ id │
│ device_id (UK) │ │ model_type │
│ platform │ │ version │
│ app_version │ │ s3_key │
│ registered_at │ │ checksum │
└──────────────────┘ │ file_size_bytes │
│ is_active │
│ release_note │
│ created_at │
└───────────────────────┘
health_stat_reports breed_baselines
┌──────────────────┐ ┌───────────────────────┐
│ id │ │ breed_code (PK) │
│ device_id │ │ sample_count │
│ breed_code │ │ avg_health_score │
│ health_score │ │ avg_activity_index │
│ behavior_tags │◄──────►│ common_behavior_tags │
│ date_bucket │ │ updated_at │
└──────────────────┘ └───────────────────────┘
9.2 주요 인덱스 전략
-- 모델 버전: 타입별 최신 활성 버전 조회 최적화
CREATE INDEX idx_model_versions_type_active
ON model_versions (model_type, is_active, created_at DESC);
-- 통계: 품종별 집계 최적화
CREATE INDEX idx_health_stats_breed_date
ON health_stat_reports (breed_code, date_bucket);
-- 기기 등록: device_id 단건 조회
CREATE UNIQUE INDEX idx_device_registrations_device_id
ON device_registrations (device_id);
10. 설정 파일
10.1 application. yml
# model-service/src/main/resources/application.yml
spring:
application:
name: model-service
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/cathealth}
username: ${DB_USER:cathealth}
password: ${DB_PASSWORD:password}
jpa:
hibernate:
ddl-auto: validate # 프로덕션: validate (Flyway로 마이그레이션)
show-sql: false
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
aws:
s3:
bucket: ${S3_BUCKET:cathealth-models}
region: ${AWS_REGION:ap-northeast-2}
presigned-url-expiry-minutes: 60
jwt:
secret: ${JWT_SECRET}
access-token-expiry-days: 7
refresh-token-expiry-days: 90
firebase:
credentials-path: ${FIREBASE_CREDENTIALS_PATH:firebase-credentials.json}
server:
port: 8081
11. 배포 환경
11.1 Docker Compose (로컬 개발)
# docker-compose.yml
version: "3.9"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: cathealth
POSTGRES_USER: cathealth
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: redis-server --requirepass password
model-service:
build:
context: ./model-service
dockerfile: Dockerfile
ports:
- "8081:8081"
environment:
DB_URL: jdbc:postgresql://postgres:5432/cathealth
DB_USER: cathealth
DB_PASSWORD: password
REDIS_HOST: redis
REDIS_PASSWORD: password
JWT_SECRET: ${JWT_SECRET}
AWS_REGION: ap-northeast-2
depends_on:
- postgres
- redis
stats-service:
build:
context: ./stats-service
dockerfile: Dockerfile
ports:
- "8082:8082"
environment:
DB_URL: jdbc:postgresql://postgres:5432/cathealth
DB_USER: cathealth
DB_PASSWORD: password
REDIS_HOST: redis
depends_on:
- postgres
- redis
auth-service:
build:
context: ./auth-service
dockerfile: Dockerfile
ports:
- "8083:8083"
environment:
DB_URL: jdbc:postgresql://postgres:5432/cathealth
DB_USER: cathealth
DB_PASSWORD: password
REDIS_HOST: redis
REDIS_PASSWORD: password
JWT_SECRET: ${JWT_SECRET}
FIREBASE_CREDENTIALS_PATH: /app/config/firebase-credentials.json
volumes:
- ./config/firebase-credentials.json:/app/config/firebase-credentials.json:ro
depends_on:
- postgres
- redis
volumes:
postgres_data:
11.2 Dockerfile (공통 템플릿)
# Dockerfile (각 서비스 공통 구조)
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY gradlew settings.gradle.kts build.gradle.kts ./
COPY gradle ./gradle
RUN ./gradlew dependencies --no-daemon # 레이어 캐시 활용
COPY src ./src
RUN ./gradlew bootJar --no-daemon
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
# 비루트 사용자 실행 (보안)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 8081
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", \
"-jar", "app.jar"]
11.3 GitHub Actions CI/CD
# .github/workflows/backend-deploy.yml
name: Backend Deploy
on:
push:
branches: [main]
paths:
- 'backend/**'
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: cathealth_test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports: ["5432:5432"]
redis:
image: redis:7-alpine
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Run tests
working-directory: backend
run: ./gradlew test --no-daemon
env:
DB_URL: jdbc:postgresql://localhost:5432/cathealth_test
DB_USER: test
DB_PASSWORD: test
build-and-push:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push Docker images
run: |
docker build -t cathealth/model-service:$ ./backend/model-service
docker push cathealth/model-service:$
12. API 엔드포인트 전체 목록
| 서비스 | Method | Path | 설명 | 인증 |
|---|---|---|---|---|
| auth | POST | /api/v1/auth/device/register |
기기 등록, JWT 발급 | 불필요 |
| auth | POST | /api/v1/auth/token/refresh |
Access Token 갱신 | Refresh Token |
| auth | POST | /api/v1/auth/device/fcm-token |
FCM 토큰 등록 | JWT |
| model | GET | /api/v1/models/latest |
최신 모델 버전 확인 | JWT |
| model | POST | /api/v1/models/upload |
모델 신규 버전 등록 | JWT (Admin) |
| stats | POST | /api/v1/stats/report |
익명 건강 데이터 전송 | JWT |
| stats | GET | /api/v1/stats/baseline/{breedCode} |
품종별 기준값 조회 | JWT |
| stats | GET | /api/v1/stats/summary |
전체 통계 요약 | JWT (Admin) |
13. 보안 체크리스트
| 항목 | 적용 방법 |
|---|---|
| HTTPS 강제 | Nginx/Gateway에서 HTTP → HTTPS 리다이렉트 |
| JWT 비밀키 관리 | 환경변수, AWS Secrets Manager 사용 |
| SQL Injection 방지 | JPA Parameterized Query (직접 쿼리 금지) |
| 민감 데이터 마스킹 | 로그에서 토큰, 패스워드 필드 제외 |
| CORS 설정 | 허용 Origin 명시적 지정 (와일드카드 금지) |
| Rate Limiting | Spring Cloud Gateway 필터로 IP당 요청 제한 |
| 모델 파일 무결성 | SHA-256 체크섬 검증 후 적용 |
| 컨테이너 보안 | 비루트 사용자 실행, 읽기 전용 파일시스템 |
14. 관련 문서
| 문서 | 경로 |
|---|---|
| 전체 기획 개요 | 01_overview.md |
| Android 아키텍처 | 02_android_architecture.md |
| 비전 및 AI 모델 | 03_vision_ai_model.md |
본 문서는 개발 진행에 따라 지속적으로 업데이트됩니다.
C
Contents
