Repository navigation
Decision Log
donghoong2 edited this page Oct 28, 2026
·
3 revisions
develop
04c0e10(2026-10-27) 기준
위키의 사례 기록과 공통 페이지에 흩어진 결정을 표 하나로 모은 색인이다. 같은 결정이 여러 곳에 적혀 있으면 한 줄로 합쳤다. 결정을 뒤집었더라도 옛 결정은 지우지 않고 대체됨 → 새 결정 ID로 남긴다. 주제별 8개 절로 나눴고, ID는 절 순서대로 붙였다.
근거 열은 그 결정의 맥락·검토한 선택지·결과가 적힌 사례 기록을 가리킨다. 사례 기록이 없으면 관련 공통 페이지를 걸고, 지금 코드가 어떻게 동작하는지가 근거인 결정은 레포 문서(README와 docs/)라고 적었다. 형식은 Microsoft Code-With Engineering Playbook의 Decision Log를, 상태 구분은 Michael Nygard의 ADR을 따랐다.
상태는 다음 넷 중 하나다.
| 상태 | 뜻 |
|---|---|
| 채택 | 지금 유효한 결정이다. 괄호 안에 이 결정이 밀어낸 옛 방식을 적기도 한다 |
| 대체됨 → D-xxx | 한때 채택했으나 다른 결정으로 바뀌었다. 화살표가 바꾼 결정을 가리킨다 |
| 폐기 | 검토하거나 시도했으나 채택하지 않았다 |
| 제안 | 방향은 정했으나 아직 적용하지 않았다 |
ID는 한 번 붙이면 바꾸지 않는다. 새 결정은 주제에 맞는 절의 표에 넣고, 번호는 로그 전체에서 가장 큰 번호 다음을 쓴다.
| ID | 결정 | 상태 | 시점 | 근거 |
|---|---|---|---|---|
| D-001 | AI 리포트를 사용자에게 바로 주지 않고 등록 손해사정사의 검수를 거치는 B2B2C 구조로 한다 | 채택(처음 구상한 B2C 직접 제공을 대체) | 기획 단계 | 프로젝트 개요 |
| D-002 | 사고 입력부터 OCR 트리거 발행까지를 백엔드가 맡는다 | 채택(처음엔 FastAPI가 업로드·접수를 받았다) | 2026-06-20 | 프로젝트 개요 |
| D-003 | 매칭(제안·상담 결정)을 별도 컨텍스트로 떼지 않고 report·chat에 둔다 |
채택 | — | 레포 문서 |
| D-004 | 검수 요청에서 status를 빼고, 다음 상태를 서버가 현재 상태에서 파생한다 |
채택(요청의 status 필드를 대체) |
— | 레포 문서 |
| D-005 | 분석 처리 상태(AnalysisState)를 저장하지 않고 조회 때마다 계산한다. 판정 우선순위는 VO(ReportAnalysis)가 갖고 NEEDS_REUPLOAD를 AI 초안 검사보다 앞에 둔다 |
채택 | #247 (2026-08-15), #251 (2026-08-16) | AI 워커가 남긴 상태를 소비하지 않아 리포트가 무음으로 멈춤 |
| D-006 | AI 초안(reports)과 손해사정사별 검수본(report_reviews)을 테이블로 나눠 경쟁 검수를 격리한다. 식별자 없는 목록은 text[]로, 원소마다 상태가 있는 쟁점은 엔티티로 둔다 |
채택 | — | 레포 문서 |
| D-007 | 자식 Repository를 없애고 Root가 자식을 소유하며(cascade·orphanRemoval), 자식 목록은 clear()/addAll() 대신 제자리에서 갱신한다 |
채택(자식 Repository 직접 조작과 clear()/addAll()을 대체) |
— | 레포 문서 |
| D-008 | 상담 수락은 여러 Aggregate를 한 트랜잭션으로 바꾸고(통계는 위임, 알림은 커밋 후 이벤트), 상담 거절의 "남은 상담" 판단에는 리포트 행 비관락을 건다 | 채택(한 트랜잭션 한 Aggregate 원칙의 예외) | — | 레포 문서 |
| D-009 | 손해사정사 평점·후기 수·상담 완료 수를 adjuster_profiles에 비정규화하고 쓰지 않는 컬럼은 정리한다 |
채택 | #301 (2026-09-13) | 읽는 곳은 다섯인데 쓰는 곳이 없던 비정규화 컬럼으로 사정사 평점이 항상 0점 |
| D-010 | AI 소유 실패 저널을 @Subselect 읽기 전용 엔티티로 매핑해 기동 시 스키마 검증에서 뺀다 |
채택 | #247 (2026-08-15) | AI 워커가 남긴 상태를 소비하지 않아 리포트가 무음으로 멈춤 |
| D-129 | 사정사 집계 컬럼은 단일 진입점에서 이벤트마다 원천 테이블 전량 재집계로 덮어쓴다(증분 아님) | 채택 | #305 (2026-09-13) | 읽는 곳은 다섯인데 쓰는 곳이 없던 비정규화 컬럼으로 사정사 평점이 항상 0점 |
| D-130 | 상담 완료 수의 트리거는 검수 등록이 아니라 상담 수락으로 한다 | 채택 | #305 (2026-09-13) | 읽는 곳은 다섯인데 쓰는 곳이 없던 비정규화 컬럼으로 사정사 평점이 항상 0점 |
| D-131 | 집계 갱신 전 사정사 프로필 행을 비관적 잠금으로 직렬화한다 | 채택 | #305 (2026-09-13) | 읽는 곳은 다섯인데 쓰는 곳이 없던 비정규화 컬럼으로 사정사 평점이 항상 0점 |
| D-132 | 검수 대기 목록은 상세와 같은 노출 정책(AWAITING_INSPECTION·AWAITING_ADOPTION)만 보여 준다 | 채택 | #151 (2026-07-25) | 검수 대기 숫자가 화면마다 다르고 목록에서 누르면 404 |
| D-133 | '검수 대기'는 사정사별 개인 큐로 정의하고(두 상태 − 내 보류 − 내 SENT·COUNSELING 검수) 모든 화면에 같은 기준을 적용한다 | 채택(D-134를 대체) | #186 (2026-07-31) | 검수 대기 숫자가 화면마다 다르고 목록에서 누르면 404 |
| D-134 | 보류 제외는 검수 대기 화면에만 두고 홈은 전역 풀로 둔다 | 대체됨 → D-133 | #185 (2026-07-31) | 검수 대기 숫자가 화면마다 다르고 목록에서 누르면 404 |
| ID | 결정 | 상태 | 시점 | 근거 |
|---|---|---|---|---|
| D-011 | 패키지를 4계층 대신 컨텍스트별 5개 패키지(controller/dto/entity/repository/service)의 실용적 레이어드 구조로 둔다 |
채택(4계층 구조를 대체) | 2026-07-02 | 레포 문서 |
| D-012 | 조회에 native query를 쓰지 않는다. 동적 조회는 QueryDSL, 단순 조회는 파생 쿼리·JPQL로 하고, 불가피하면 사유 주석을 단 문서화된 예외로 둔다 | 채택 | #100 (2026-07-13) | 레포 문서 |
| D-013 | 컨트롤러가 ResponseEntity 대신 ApiResponse<T>를 직접 반환하고, 200이 아닌 성공 상태는 @ResponseStatus로 선언한다 |
채택(ResponseEntity 래핑을 대체) |
#321 (2026-09-27) | OpenAPI 응답 상태코드가 전부 200으로 문서화됨 |
| D-014 | DTO에 @Schema required/nullable을 명시해 OpenAPI를 API 명세의 단일 기준으로 삼고, springdoc이 쓰는 정적 매퍼(Json31.mapper())에 snake_case를 직접 설정한다 |
채택 | #197 (2026-08-06), #248·#249 (2026-08-15) | OpenAPI 프로퍼티명이 camelCase로 문서화돼 실제 응답과 어긋남 |
| D-015 | 포맷은 Spotless가 자동으로 고치고 Checkstyle은 판단이 필요한 규칙만 경고한다 | 채택 | — | 레포 문서 |
| D-016 | 릴리즈 태깅을 semantic-release로 자동화한다. 커밋 type이 버전을 정하고 첫 태그 v0.1.0만 손으로 찍는다 |
채택 | #192 (2026-08-06) | 레포 문서 |
| D-017 | 검수 PATCH는 보낸 필드만 반영하는 부분 수정으로 한다. 쟁점은 GET이 내려준 review_issue_id로 upsert하고 제외는 EXCLUDED 상태로 표현한다 |
채택(전체 덮어쓰기를 대체) | — | 레포 문서 |
| D-018 | 보류를 추가 전용 멱등 POST로 하고, 멱등 생성은 UNIQUE 제약 + REQUIRES_NEW 별도 빈 + 재조회로 처리한다 |
채택(PATCH 토글과 native ON CONFLICT를 대체) |
— | 레포 문서 |
| D-019 | 분석 처리 상태를 소유자 전용 폴링 엔드포인트와 목록·상세의 평면 필드로 노출한다 | 채택 | #247 (2026-08-15) | AI 워커가 남긴 상태를 소비하지 않아 리포트가 무음으로 멈춤 |
| D-020 | 알림 타입 이름에 도메인 접두어를 붙인다(REPORT_NEEDS_REUPLOAD) |
채택 | #251 (2026-08-16) | AI 워커가 남긴 상태를 소비하지 않아 리포트가 무음으로 멈춤 |
| D-126 | 4xx 프레임워크 예외를 포괄 예외 핸들러에서 떼어 원래 상태코드(404·405·400·403)로 응답한다 | 채택 | #145 (2026-07-25) | 서버 오류는 0건인데 500이 144건이던 포괄 예외 핸들러 문제 |
| D-127 | 매핑 없는 경로의 404는 actuator 경로만 로그를 남기지 않고, 나머지는 메서드·경로를 WARN으로 남긴다 | 채택(무로그 404를 대체) | #166 (2026-07-25) | 서버 오류는 0건인데 500이 144건이던 포괄 예외 핸들러 문제 |
| D-128 | 표준 MVC 예외는 ResponseEntityExceptionHandler 확장으로 한 번에 처리한다 | 제안 | #145 (2026-07-24) | 서버 오류는 0건인데 500이 144건이던 포괄 예외 핸들러 문제 |
| D-135 | 응답의 지역 필드는 단일 문자열로 통일하되 /users/me만 배열로 예외를 둔다 |
채택 | #155 (2026-07-25) | 프론트 계약이 어긋나 기능이 조용히 깨진 두 번 |
| D-136 | 가입 요청 이름은 name을 정식 키로 받고 구 키 nickname은 @JsonAlias로 계속 받는다 |
채택 | #189 (2026-08-03) | 프론트 계약이 어긋나 기능이 조용히 깨진 두 번 |
| ID | 결정 | 상태 | 시점 | 근거 |
|---|---|---|---|---|
| D-021 | 토큰을 응답 바디로 내려 Authorization 헤더로 받고, 쿠키로 옮긴 뒤에도 Bearer 헤더를 폴백으로 함께 받는다 |
대체됨 → D-022 | 초기 ~ PR #169 | k6 부하테스트 401 폭주를 VU별 토큰 재주입으로 해결 |
| D-022 | 인증은 HttpOnly access_token 쿠키(Path /) 한 경로로만 받고, refresh_token 쿠키는 Path /auth로 좁혀 재발급·로그아웃 요청에만 싣는다 |
채택 | PR #169, 2026-07-10 | k6 부하테스트 401 폭주를 VU별 토큰 재주입으로 해결 |
| D-023 | Spring oauth2Login 대신 프론트가 넘긴 인가코드를 백엔드가 REST로 직접 교환하고, 신규 회원은 5분짜리 가입 티켓을 받아 /auth/register에서 생성한다 |
채택 | — | 레포 문서 |
| D-024 | refresh 회전을 GET → 비교 → DEL → SET 개별 연산으로 한다 | 대체됨 → D-025 | 초기 | Refresh 토큰 동시 재발급 경쟁을 Lua 원자 CAS로 해결 |
| D-025 | refresh 회전을 Lua 스크립트 원자적 CAS(rotate)로 하고 불일치하면 탈취 의심으로 키를 지운다. 재발급할 때 role은 DB에서 다시 읽는다 |
채택 | 2026-07-10 | Refresh 토큰 동시 재발급 경쟁을 Lua 원자 CAS로 해결 |
| D-026 | access 토큰 블랙리스트는 회원 탈퇴 때만 userId 단위로 아웃박스를 거쳐 등록하고 로그아웃 때는 등록하지 않는다. 조회는 Redis 장애 시 fail-open이다 | 채택 | — | 회원 탈퇴의 dual-write를 트랜잭셔널 아웃박스로 해결 |
| D-027 | 익명 인증을 꺼서 401과 403의 의미를 지키고, 쿠키 인증이라 CORS를 allowCredentials(true) + allowedOriginPatterns 목록으로 구성한다 |
채택 | — | 레포 문서 |
| D-028 | 개발용 로그인 백도어를 여러 겹으로 막는다(prod 기동 차단, dev는 시크릿이 없으면 기동 실패, 시크릿 상수 시간 비교, 권한 상승 차단) | 채택 | 부하테스트 준비 | k6 부하테스트 401 폭주를 VU별 토큰 재주입으로 해결 |
| D-029 | PII 컬럼을 AES-256-GCM 봉투 암호화로 저장하고, 조회 조건으로 쓰는 값은 HMAC 블라인드 인덱스를 따로 둔다 | 채택 | #221, V37·V38 | 스키마 미지정 마이그레이션이 배포를 깨뜨려 core 스키마로 통일 |
| D-030 | prod는 KMS 키 경로만 허용하고, 안전하지 않은 설정(raw 키, dev-login 활성, Apple 키 누락)은 기동 단계에서 막는다 | 채택 | #221 | 레포 문서 |
| D-031 | pgAudit을 세션 단위로 켜 전체 READ를 감사하고, 대상은 OCR 결과처럼 마스킹돼 저장되는 테이블로 고른다 | 대체됨 → D-032 | — | pgAudit 감사 로그 범위를 두 번 고친 일 |
| D-032 | pgAudit은 객체 단위로 걸고, 대상은 "유출되면 원문이 나가는" 회원·리포트·청구 테이블로 고른다 | 채택 | — | pgAudit 감사 로그 범위를 두 번 고친 일 |
| ID | 결정 | 상태 | 시점 | 근거 |
|---|---|---|---|---|
| D-033 | 회원 탈퇴 후처리를 서비스 메서드 안에서 Redis에 직접 쓴다 | 대체됨 → D-034 | 초기 | 회원 탈퇴의 dual-write를 트랜잭셔널 아웃박스로 해결 |
| D-034 | DB 밖 부수효과를 트랜잭셔널 아웃박스에 적재하고 스케줄러가 FOR UPDATE SKIP LOCKED(D-012의 문서화된 예외)로 폴링해 처리한다. 탈퇴 때 요청 스레드에서는 쿠키 만료만 한다 |
채택 | — | 회원 탈퇴의 dual-write를 트랜잭셔널 아웃박스로 해결 |
| D-035 | 릴레이는 실패한 한 건만 표시하고 배치 전체를 롤백하지 않는다 | 채택 | — | 레포 문서 |
| D-036 | OCR 트리거 브로커를 EC2에 직접 띄운 Kafka(Docker, KRaft)로 한다 | 대체됨 → D-037 | 초기 | OCR 트리거 브로커를 Kafka에서 MSK를 거쳐 SQS로 전환 |
| D-037 | 브로커를 AWS MSK Provisioned(IAM 인증)로 옮긴다 | 대체됨 → D-038 | PR #205 | OCR 트리거 브로커를 Kafka에서 MSK를 거쳐 SQS로 전환 |
| D-038 | OCR 트리거를 AWS SQS Standard 큐로 보낸다. 상시 브로커 대신 관리형 큐를 고르고, 전환 시점에 DLQ는 넣지 않는다 | 채택 | #208 (2026-08-09) | OCR 트리거 브로커를 Kafka에서 MSK를 거쳐 SQS로 전환 |
| D-039 | 큐 이름 설정(SQS_OCR_QUEUE_NAME)에 폴백 기본값을 두지 않아, 빠지면 기동이 실패한다 |
채택 | #208 | OCR 트리거 브로커를 Kafka에서 MSK를 거쳐 SQS로 전환 |
| D-040 | 아웃박스 클래스는 OcrOutbox*로 리네임하고 테이블 kafka_outbox_events는 그대로 둔다 |
채택 | 2026-08-13 | 스키마 미지정 마이그레이션이 배포를 깨뜨려 core 스키마로 통일 |
| D-041 | AI 워커의 실패·차단·재업로드 신호를 조회 시점이 아니라 스케줄러 스윕에서 알림으로 바꾸고, 상태마다 스윕러와 통지 시각 컬럼을 따로 둔다 | 채택 | #247, #248, #251 (V41~V43) | AI 워커가 남긴 상태를 소비하지 않아 리포트가 무음으로 멈춤 |
| D-042 | 알림 생성과 FCM 발송은 AFTER_COMMIT 리스너에서 한다 | 채택 | — | 레포 문서 |
| D-043 |
reports.status 값은 DB 제약 없이 Java enum에서만 강제하고, 새 값은 백엔드를 AI 워커보다 먼저 배포한다 |
채택 | #251 | AI 워커가 남긴 상태를 소비하지 않아 리포트가 무음으로 멈춤 |
| D-044 | 채팅을 SockJS·@MessageMapping·CONNECT 프레임 Bearer 인증으로 만든다(학습용 프로토타입) |
대체됨 → D-045, D-046 | 초기 | 레포 문서 |
| D-045 | 메시지 전송은 REST로 하고 WebSocket은 수신 전용으로 쓴다. 팬아웃은 내장 SimpleBroker + Redis pub/sub relay로 하고, 보낸 서버도 직접 브로드캐스트하지 않고 relay를 거친다 |
채택 | — | 레포 문서 |
| D-046 | WebSocket 핸드셰이크를 access_token 쿠키로 인증하고, SUBSCRIBE 거절은 연결을 끊는 ERROR 대신 MESSAGE 프레임으로 알린다 |
채택 | — | 채팅 구독 거절이 WebSocket 연결 전체를 끊던 문제 |
| D-047 | 읽음 상태를 방의 참여자별 커서 컬럼 두 개로 둔다 | 채택 | V24 | 레포 문서 |
| D-048 | 메시지당 첨부 하나를 단일 컬럼 네 개로 저장한다 | 대체됨 → D-049 | V24 | 레포 문서 |
| D-049 | 첨부를 attachments jsonb 배열로 저장한다 |
채택 | V25 | 레포 문서 |
| D-050 | 첨부 타입을 Content-Type 화이트리스트로만 검사한다 | 대체됨 → D-051 | 초기 | 레포 문서 |
| D-051 | 첨부는 서버를 거쳐 받아 매직바이트와 선언 타입이 맞는지 확인하고, private S3 key만 저장해 5분짜리 presigned GET으로 내준다 | 채택 | — | 레포 문서 |
| D-052 | 메시지 이력을 (created_at, id) 커서 페이지네이션으로 준다 |
채택 | — | 레포 문서 |
| D-053 | 채팅 메시지의 오프라인 FCM 푸시는 범위 밖으로 둔다 | 채택 | — | 레포 문서 |
| ID | 결정 | 상태 | 시점 | 근거 |
|---|---|---|---|---|
| D-054 | 저장소를 RDS for PostgreSQL 16 + pgvector 하나로 하고, 벡터는 halfvec(1024) 타입에 HNSW 인덱스로 둔다 |
채택 | — | 레포 문서 |
| D-055 | 벡터 타입을 vector(1024)로 한다 |
대체됨 → D-054 | 초기 설계 | 레포 문서 |
| D-056 | 키워드 검색에 ParadeDB pg_search(BM25)를 쓴다 |
폐기 | 초기 설계 | 레포 문서 |
| D-057 | 스토리지는 gp3, 가용 영역은 Single-AZ로 시작한다 | 채택 | — | 레포 문서 |
| D-058 | 세 팀(백엔드·AI 엔진·약관 청커)이 public 스키마 하나를 같이 쓴다 |
대체됨 → D-059 | 초기 | 레포 문서 |
| D-059 | 물리 DB는 하나로 두고 core/ai/corpus 스키마와 소유 롤로 나눈다. 크로스 스키마 접근은 최소 GRANT로 열고 SQL에 스키마를 명시한다 |
채택 | — | 레포 문서 |
| D-060 | 마이그레이션에서 스키마를 지정하지 않고 CREATE TABLE을 쓴다 |
대체됨 → D-061 | #223 이전 | 스키마 미지정 마이그레이션이 배포를 깨뜨려 core 스키마로 통일 |
| D-061 | 새 테이블·인덱스 마이그레이션은 core.를 명시한다 |
채택 | #223 | 스키마 미지정 마이그레이션이 배포를 깨뜨려 core 스키마로 통일 |
| D-062 | V40으로 public에 남은 테이블을 core로 옮기고, JDBC URL에 currentSchema=core,public을 고정한다 |
채택 | #244 | 스키마 미지정 마이그레이션이 배포를 깨뜨려 core 스키마로 통일 |
| D-063 | prod Flyway는 체크섬 검증을 켜고 clean을 막고 baseline을 끈다. 마이그레이션 번호가 겹치면 나중에 머지되는 PR이 번호를 바꾼다 | 채택 | PR #249·#251 | AI 워커가 남긴 상태를 소비하지 않아 리포트가 무음으로 멈춤 |
| D-064 | 사건번호를 당일 카운터 행(ON CONFLICT DO UPDATE)으로 빈틈없는 연번으로 발급한다 |
대체됨 → D-065 | V14 | 사건번호 발급 락 경합으로 처리량 붕괴 |
| D-065 | 사건번호를 날짜 + 랜덤 코드로 만들고 UNIQUE로 확인해 충돌 시 재시도한다 | 대체됨 → D-066 | #309 | 사건번호 발급 락 경합으로 처리량 붕괴 |
| D-066 | 사건번호를 전역 시퀀스 core.report_case_seq의 nextval로 발급한다(yyyyMMdd-NNNNNN, 번호 빈틈 허용) |
채택 | V47 | 사건번호 발급 락 경합으로 처리량 붕괴 |
| ID | 결정 | 상태 | 시점 | 근거 |
|---|---|---|---|---|
| D-067 | 앱 EC2를 private subnet에 두고 접속은 SSM으로만 하며, S3 Gateway VPC Endpoint로 S3 트래픽을 NAT에서 뺀다 | 채택 | 초기 인프라 | 레포 문서 |
| D-068 | 보안그룹 인바운드는 IP가 아니라 보안그룹 참조로 연다. DB도 Public Access 없이 이 방식으로 5432만 연다 | 채택 | 초기 인프라 | 레포 문서 |
| D-069 | 앱의 AWS 자격증명은 DefaultCredentialsProvider(IAM Role)에 맡기고, CI는 OIDC 임시 자격증명을 쓴다 |
채택 | 초기 인프라, CI/CD 구축 | 레포 문서 |
| D-070 | prod 배포는 SSH나 repository_dispatch 대신 SSM send-command로 하고, 배포 compose 파일은 레포가 단일 진실원이라 CD가 매번 덮어쓴다 |
채택 | CI/CD 구축 | 배포는 초록불인데 컨테이너가 계속 재시작되던 일 |
| D-071 | dev도 SSM + docker compose로 배포한다 | 대체됨 → D-099, D-108 | #332 이전 | Kubernetes |
| D-072 | 모니터링 인스턴스 동기화를 compose 파일을 base64로 실은 SSM 페이로드로 한다 | 대체됨 → D-073 | #328 이전 | 레포 문서 |
| D-073 | 모니터링 인스턴스가 해당 커밋의 레포 tarball을 직접 받아 동기화하고, 이 배포는 deploy/monitoring/** 경로가 바뀔 때만 돈다 |
채택 | #214, #328, #343 | 레포 문서 |
| D-074 | JVM 힙을 MaxRAMPercentage=75와 컨테이너 메모리 상한으로 묶고, HikariCP 풀 크기는 15로 둔다 |
채택 | 프로덕션 하드닝, 2026-08-15 | HikariCP 풀 확대로는 풀리지 않은 RDS CPU 한계 |
| D-075 | actuator를 관리 포트 9292로 분리하고, 외부에는 health·info만 연다 | 채택 | 프로덕션 하드닝 | GitOps 이전 중에 드러난 Traefik 서비스 타입 오류와 외부에 열린 actuator |
| D-125 | GitHub App 개인 키를 레포마다 따로 두지 않고 조직 시크릿 하나(가시성 selected)로 모으며, Client ID는 레포 변수로 둔다 | 채택(레포별 개인 키를 대체) | 2026-10-13 | GitHub App 키를 지워 dev 배포가 멈추고 조직 시크릿으로 통합 |
| ID | 결정 | 상태 | 시점 | 근거 |
|---|---|---|---|---|
| D-076 | Prometheus·Loki·Tempo·Grafana를 직접 운영한다. 보존 기간은 3일, Prometheus 저장 크기 상한은 8GB다 | 채택 | #88 | 레포 문서 |
| D-077 | 관측 스택을 앱 EC2에 함께 올린다 | 대체됨 → D-078 | #88 | 레포 문서 |
| D-078 | 관측 스택을 별도 모니터링 인스턴스로 분리한다 | 채택 | #214 | 레포 문서 |
| D-079 | cAdvisor를 v0.49.1에서 v0.55.1로 올려 컨테이너 메트릭 소실을 해결한다 | 대체됨 → D-081 | 클러스터 이주 이전 | 타깃은 UP인데 컨테이너 메트릭이 비어 있던 일 |
| D-080 | 앱 호스트를 정적 스크랩한다(backend·node-exporter·cAdvisor·redis-exporter) | 대체됨 → D-081 | #328 이전 | 레포 문서 |
| D-081 | 클러스터의 kube-prometheus-stack이 중앙 Prometheus로 remote_write한다(컨테이너 지표는 kubelet cAdvisor) | 채택 | #328 | Kubernetes |
| D-082 | 모든 메트릭에 application 공통 태그를 붙이고 percentiles-histogram은 필요한 타이머에만 켠다 |
채택 | PR #132 | 레포 문서 |
| D-083 | 로그는 구조화 JSON으로 stdout에 남겨 Alloy가 Loki로 보낸다. 바인드 파라미터는 로깅하지 않고, OTLP 로그 export는 쓰지 않으며, Loki 라벨은 저카디널리티(service)만 두고 traceId는 derived field로 잇는다 |
채택 | #88, 프로덕션 하드닝 | 레포 문서 |
| D-084 | 트레이싱 의존성을 스타터 대신 세 조각으로 명시한다 | 채택 | 트레이싱 도입 | Spring Boot 4에 트레이싱을 붙이다 세 번 조용히 실패한 일 |
| D-085 | traceId를 커스터마이저로 JSON 로그에 직접 넣는다 | 폐기 | 트레이싱 도입 | Spring Boot 4에 트레이싱을 붙이다 세 번 조용히 실패한 일 |
| D-086 | dev 트레이스 샘플링을 1.0(전량)으로 둔다 | 대체됨 → D-087 | 트레이싱 도입 | 레포 문서 |
| D-087 | Tempo OOM 이후 dev 샘플링을 0.5로 낮추고 Tempo 메모리 상한을 1GB로 올린다 | 채택 | 2026-09-16 | 레포 문서 |
| D-088 | 슬로우쿼리를 Hibernate 슬로우쿼리 로그와 JDBC 쿼리 스팬 두 경로로 본다 | 채택 | #324, PR #325 | 레포 문서 |
| D-089 | 알림은 severity별 2채널로 보내고 분류되지 않은 알림은 critical로 본다 | 채택 | #88 | 레포 문서 |
| D-090 | k6 결과를 Prometheus remote-write로 기존 Grafana에 넣는다 | 대체됨 → D-091 | 부하테스트 준비 | 레포 문서 |
| D-091 | k6 결과는 Datadog으로 분리한다 | 채택 | 부하테스트 준비 | 레포 문서 |
| D-092 |
POST /reports 쓰기 경로를 구간별로 계측한다(report.create.*, 커넥션 획득 p95) |
채택 | #306 | 사건번호 발급 락 경합으로 처리량 붕괴 |
| D-093 | 스트레스 한계를 넘기려고 HikariCP 풀을 15에서 30으로 늘린다 | 폐기(병목은 풀이 아니라 RDS CPU였다. 풀 크기는 D-074 유지) | 2026-09 스트레스 테스트 | HikariCP 풀 확대로는 풀리지 않은 RDS CPU 한계 |
| D-094 | 200 RPS 목표는 RDS 스펙 상향을 선결 조건으로 둔다 | 채택 | 2026-09 스트레스 테스트 | HikariCP 풀 확대로는 풀리지 않은 RDS CPU 한계 |
| D-095 | SLO는 성능 지표와 오류율 가드레일을 한 쌍으로 정의한다 | 채택 | 부하테스트 준비 | HikariCP 풀 확대로는 풀리지 않은 RDS CPU 한계 |
| D-096 | k6는 토큰을 VU 변수에 두고 이터레이션마다 쿠키를 다시 심는다 | 채택 | 부하테스트 준비 | k6 부하테스트 401 폭주를 VU별 토큰 재주입으로 해결 |
| D-097 | 시드 러너는 도메인 서비스를 거치지 않고 엔티티만 저장하며, 리포트 단위로 별도 Bean에서 커밋하고 (유저 × 풀) 단위로 멱등하게 만든다. 시나리오 러너는 프로파일(!test & !prod)과 프로퍼티 두 겹으로 켠다 |
채택 | 부하테스트 준비 | 레포 문서 |
| D-098 | DB 버퍼 풀을 줄이고 시드 기본 유저 수를 100에서 600으로 늘려 디스크 I/O를 재현한다 | 채택 | PR #327 | HikariCP 풀 확대로는 풀리지 않은 RDS CPU 한계 |
| ID | 결정 | 상태 | 시점 | 근거 |
|---|---|---|---|---|
| D-099 | dev 배포를 EC2 docker compose에서 Kubernetes로 옮기되(prod는 compose 유지), 관리형 EKS 대신 기존 EC2 위에 클러스터를 직접 세운다 | 채택 | 2026-10-12 ~ 2026-10-14 | Kubernetes |
| D-100 | k3s로 클러스터를 세운다 | 대체됨 → D-101 | 2026-10-14 이전 | Kubernetes |
| D-101 | 실데이터를 올리기 전에 kubeadm으로 다시 설치하고, etcd 시크릿 암호화는 설치 전에 켠다 | 채택 | 2026-10-14 | Kubernetes |
| D-102 | CNI로 Flannel을 쓴다 | 대체됨 → D-103 | kubeadm 초기 | CNI를 Flannel에서 Calico를 거쳐 Cilium으로 전환 |
| D-103 | NetworkPolicy가 필요해져 CNI를 Calico(eBPF 데이터플레인)로 바꾼다 | 대체됨 → D-104 | 2026-10-14 이전 | CNI를 Flannel에서 Calico를 거쳐 Cilium으로 전환 |
| D-104 | 플로우 단위 관측(Hubble)을 위해 CNI를 Cilium(eBPF, kube-proxy 대체)으로 바꾼다 | 채택 | 2026-10-14 | CNI를 Flannel에서 Calico를 거쳐 Cilium으로 전환 |
| D-105 | Ingress는 Traefik을 NodePort로 두어 ALB가 포트 하나만 알게 한다 | 채택 | 2026-10-14 | GitOps 이전 중에 드러난 Traefik 서비스 타입 오류와 외부에 열린 actuator |
| D-106 | RDS·S3·SQS는 클러스터에 올리지 않고 AWS 관리형으로 둔다 | 채택 | 2026-10-14 | Kubernetes |
| D-107 | GPU 노드는 같은 인스턴스를 stop/start한다(Karpenter·Cluster Autoscaler 미채택) | 채택 | 2026-10-14 | Kubernetes |
| D-108 | dev 배포를 Argo CD GitOps로 바꾼다. CI는 매니페스트 레포의 이미지 태그(커밋 해시)만 커밋하고, 배포 후 /actuator/info의 app.commit으로 반영을 확인한다(verify-deploy) |
채택 | #332 (PR #340) | 배포는 초록불인데 컨테이너가 계속 재시작되던 일 |
| D-109 | 클러스터 매니페스트는 소스 레포와 분리된 매니페스트 레포에만 두고, 환경은 브랜치가 아니라 폴더로 나눈다 | 채택 | #332, #338, #339 | 클러스터 상태가 레포 밖에 있어서 잃어버린 것들 |
| D-110 | Argo CD Sync 정책을 리소스 성격별로 다르게 둔다(Cilium·Argo CD 자신은 수동 Sync) | 채택 | 2026-10-14 | Helm 3과 4의 렌더링 차이를 리소스 단위 대조로 잡고 애드온을 Argo CD로 이전 |
| D-111 | 비밀값은 Secrets Manager에 두고 External Secrets Operator로 k8s Secret에 동기화한다 | 채택 | 2026-10-14 | Kubernetes |
| D-112 | ECR pull 토큰을 손으로 만들어 둔다 | 대체됨 → D-113 | 2026-10-14 이전 | Kubernetes |
| D-113 | ECR pull 토큰을 ESO의 ECR 토큰 생성기로 주기 발급한다 | 채택 | 2026-10-14 | Kubernetes |
사례 기록 하나에 결정이 여럿 걸려 있으면 한 줄에 모았다. 모든 사례 기록이 이 표에 한 번 이상 나온다.
성능 · 부하테스트
데이터 · API 계약
- 500 144건과 포괄 예외 핸들러
- 평점이 항상 0점
- 검수 대기 숫자 불일치
- 프론트 계약 드리프트
- core 스키마 통일
- 리포트 무음 정지
- OpenAPI camelCase 드리프트
- OpenAPI 상태코드 드리프트
인증 · 메시징
배포 · 관측성
Kubernetes
Kubernetes
레퍼런스