website: 배포 이력 조회 전용 어드민 API 추가
CD가 매 배포마다 남기는 기록을 어드민 화면에서 볼 수 있게 조회 API를 하나 붙인 PR이다.
CD 워크플로우의 record-deploy-status job은 배포할 때마다 infra/logs/deploy-history/{env}.jsonl에 한 줄씩 결과를 남기고 있었다. sha, outcome, 마이그레이션 적용 내역, 기대/실제 마이그레이션 개수 같은 정보가 이미 쌓이고 있는데, 이걸 확인하려면 서버에 직접 들어가서 로그 파일을 열어봐야 했다. #451 인프라 대시보드 작업의 첫 단계로 "가시성" 확보가 필요했고, 그중에서도 이미 쌓여있는 이 배포 기록을 API로 꺼내주는 게 가장 손대기 쉬운 부분이었다.
범위를 정할 때 신경 쓴 부분은 이걸 조회 전용으로 딱 잘라놓는 것이었다. 대시보드라고 하면 재배포 버튼이나 롤백 트리거 같은 걸 같이 떠올리기 쉬운데, 이번 PR에서는 그런 실행/제어 기능은 아예 손대지 않았다. 커밋 메시지에도, PR 본문에도 이 부분을 명시해뒀다. 나중에 실행 기능을 붙일 때 이 API에 사이드이펙트가 없다는 걸 전제로 짤 수 있게 하려는 목적도 있었다.
DeployHistoryService를 짜면서 가장 먼저 고민한 건 파일이 없는 경우였다. docker-compose가 infra/logs/deploy-history 디렉터리를 stage/prod 컨테이너에만 읽기 전용으로 마운트해두기 때문에, 로컬 개발 환경에는 이 디렉터리 자체가 존재하지 않는다. 이 상황을 에러로 처리하면 로컬에서 이 API를 건드릴 때마다 500이 뜨게 되는데, 실제로는 "아직 배포 기록이 없다"는 것과 다를 게 없는 상태다. 그래서 Files.isReadable 체크로 걸러서 빈 리스트를 돌려주는 쪽으로 정리했다.
한 줄씩 JSON을 파싱하는 부분에서도 비슷한 판단을 했다. jsonl 포맷이다 보니 CD가 남기는 형식이 바뀌는 과도기에 깨진 줄이 하나 섞여 들어올 수 있는데, 그 한 줄 때문에 전체 파싱이 실패해서 나머지 정상 기록까지 못 보여주는 건 원치 않았다. 그래서 파싱 실패한 줄은 그냥 건너뛰고 나머지는 그대로 살리게 했다:
try {
records.add(objectMapper.readValue(line, DeployRecord.class));
} catch (IOException e) {
// 한 줄이 깨져도 나머지 유효한 기록까지 통째로 못 보여주면 안 된다
}
컨트롤러 쪽은 크게 복잡할 게 없었다. env는 stage/prod로 화이트리스트를 걸어서 잘못된 값이 오면 400을 던지게 했고, limit은 최소 1, 최대 50으로 캡을 씌웠다. 무제한으로 열어두면 나중에 파일이 커졌을 때 부담이 될 수 있어서 상한을 둔 것이고, @PreAuthorize("hasRole('ADMIN')")로 어드민 권한 없이는 접근 못 하게 막았다.
DeployRecord에서 눈에 띄는 필드가 expectedMigrationCount와 actualMigrationCount다. 이 둘이 다르면 배포된 앱 버전이 기대하는 마이그레이션 개수와 DB에 실제 적용된 개수가 어긋난 상태라는 뜻인데, 예를 들어 배포가 실패해서 앱은 롤백됐는데 마이그레이션은 이미 적용돼서 그대로 남아있는 경우가 여기 해당한다. outcome도 confirmed, rolled_back, rollback_failed, manual_intervention_needed, migration_check_blocked, build_failed까지 CD 파이프라인에서 실제로 나올 수 있는 상태들을 그대로 옮겨왔다. 이 값들은 shared/types/deploy-history.ts에 타입으로 박아뒀는데, FE-BE 계약 파일이라 화면 작업은 이 PR 범위 밖이지만 응답 모양은 미리 확정해두는 게 나중에 화면 붙일 때 왔다 갔다 하지 않을 것 같았다.
테스트는 서비스 쪽에 파일 없음, 정렬 순서, count 불일치 시 inSync false 처리, 깨진 줄 스킵, limit 초과 시 자르기, env별 파일 안 섞이는지까지 6가지 케이스를 @TempDir로 실제 파일을 써가며 검증했고, 컨트롤러 쪽은 인증 없이 401, env 검증, limit 캡, 기본 파라미터 동작을 MockMvc로 확인했다. 커밋 자체는 한 번에 올라갔지만, 로컬 파일 부재 케이스와 파싱 실패 케이스를 미리 테스트로 짜두고 나니 구현에서 빠뜨릴 뻔한 예외 상황들을 놓치지 않을 수 있었다.
merge 전에 stage 환경에 workflow_dispatch로 실제 배포를 한 번 돌려서 CD가 남긴 로그를 이 API로 실제로 읽어와지는지 확인했다(CD run 31072110467). jsonl 파일 포맷을 코드로만 짐작하고 넘어가지 않고 실제 운영 환경에서 나온 로그로 검증한 게 이번 작업에서 제일 중요했던 확인 절차였다.