[chore] prod 배포 v1.0.8 - #211
Merged
Merged
Conversation
* refactor: 검색 인덱스 생성을 도메인 밖으로 옮긴다 Message·Conversation 이 BlindIndexer 를 알지 않게 한다. applySearchIndex 가 인덱서 대신 완성된 인덱스 값을 받으므로, 엔티티는 "검색 인덱스 값을 보관한다"만 알고 그것이 HMAC 인지 무엇인지는 모른다. domain -> global.crypto import 가 사라진다. 인덱스를 만드는 두 리스너는 JPA 라이프사이클 훅 + HMAC 계산이라 도메인이 아니므로 conversation/search 로 옮긴다. @EntityListeners 는 클래스 참조라 패키지가 달라도 그대로 동작한다. * refactor: 검색 추상화에서 Port 네이밍을 걷어낸다 ConversationSearchPort -> ConversationSearcher, BlindIndexConversationSearch -> BlindIndexConversationSearcher 로 바꾸고 주입 필드명 searchPort 와 "포트 구현" 표현도 함께 정리한다. 헥사고날 용어를 쓰지 않는다. * refactor: ErrorCode 에서 HTTP 상태 코드를 떼어낸다 ErrorCode 는 도메인이 그대로 참조하는데(Nickname·ConversationTitle·FcmToken· ExcludedEmotionTypes·Card·Conversation) HttpStatus 를 필드로 들고 있어서, 닉네임 검증 규칙까지 스프링 웹 타입에 묶여 있었다. ErrorCode 는 에러의 성격만 ErrorKind 로 말하고, HTTP 상태 코드로의 번역은 웹 계층의 httpStatusOf 가 맡는다. when 이 exhaustive 라 ErrorKind 를 추가하면 상태 코드를 정하지 않고는 컴파일되지 않는다. 에러 코드 36개의 상태 코드는 변경 전과 전부 동일하다. * refactor: 백오피스 집계 서비스가 웹 응답 DTO 를 만들지 않게 한다 UsageStatsService·QualityStatsService 가 admin/controller/dto 의 Response 를 직접 조립해 반환하고 있었다. 서비스가 웹 응답 포맷을 알면 필드명·Swagger 스키마가 바뀔 때마다 집계 코드가 따라 바뀐다. 두 서비스는 UsageStats·QualityStats(및 DailyActivity·DailyGeneration·EmotionCount)를 반환하고, Response 로의 변환은 컨트롤러가 한다. ConversationUsage 가 이미 쓰던 방식과 같다. EmotionCountResponse 가 card/repository 의 EmotionCountProjection 을 알던 것도 없앤다. 컨트롤러 DTO 가 서비스를 건너뛰고 리포지토리 프로젝션을 직접 참조하고 있었다. TokenUsageResponse 는 tokenlimit 의 TokenUsage 대신 컨트롤러에서 값을 받아 만든다. * refactor: 모듈이 다른 모듈의 리포지토리를 직접 잡지 않게 한다 admin·monitoring·tokenlimit·notification·card 가 남의 모듈 리포지토리를 그대로 주입받아 쓰고 있었다. 이러면 쿼리 하나를 고칠 때 어느 모듈이 깨지는지 알 수 없고, 주인 모듈이 자기 저장 구조를 바꿀 자유를 잃는다. 읽기는 주인 모듈이 창구를 연다. - conversation/service/ConversationReadService - card/service/CardReadService - monitoring/service/GenerationLogReadService - member 는 이미 있는 MemberService 에 countSignupsBetween 을 더한다 카드 생성이 대화방 상태를 바꾸던 부분은 conversation/service/ConversationCardStateService 로 옮긴다. 어느 상태에서 어느 상태로 갈 수 있는지(SKIPPED 포함 CAS 대상)를 카드 모듈이 알고 있을 이유가 없다. 대화방 소유·종료 판정도 대화 모듈이 한 번만 정의한다. 인터페이스 프로젝션(EmotionCountProjection·ConversationSenderCountProjection)도 모듈 밖으로 내보내지 않는다. 프로젝션은 쿼리 모양에 딸린 것이라 밖으로 새면 쿼리를 못 바꾼다. * test: 계층 의존 방향과 검색 인덱스 채우기를 테스트로 고정한다 LayerDependencyTest 는 이번에 고친 규칙들을 코드로 남긴다. 도메인이 인프라를 모른다, 서비스가 컨트롤러를 모른다, 컨트롤러가 리포지토리를 건너뛰지 않는다, 모듈이 남의 리포지토리를 직접 잡지 않는다, global 이 기능 모듈을 모른다, 에러 코드가 HTTP 를 모른다, Port·Adapter 네이밍을 쓰지 않는다. 새 의존성 없이 소스의 import 만 본다. SearchIndexFillIntegrationTest 는 메시지를 저장하는 세 경로(사용자 메시지, 캐릭터 댓글· 티키타카, 유저 답글 재응답)와 제목 rename 에서 인덱스가 실제로 채워지는지 확인한다. 리스너를 도메인 밖으로 옮긴 뒤에도 그 보장이 그대로여야 한다. 두 테스트 모두 규칙을 일부러 되돌려 실제로 실패하는 것을 확인했다. @EntityListeners 참조는 규칙에서 예외로 둔다. @convert 와 같은 JPA 선언이고 엔티티가 리스너를 호출하는 것이 아니다(근거는 EncryptedStringConverter KDoc). global/exception 이 스프링 웹을 모르도록 GlobalExceptionHandler 와 httpStatusOf 를 global/web 으로 옮긴다. * refactor: 읽기 창구 패턴을 member 에도 맞추고 대화 설정을 밖으로 흘리지 않는다 D 에서 세운 규칙(다른 모듈이 읽어갈 때는 주인 모듈의 XxxReadService 를 거친다)을 member 만 지키지 않고 있었다. MemberService 에 쓰기와 밖에서 읽는 것이 섞여 있어, getById·search·getStats·countSignupsBetween 을 MemberReadService 로 옮긴다. MemberService 는 create·updateNickname·withdraw 만 남고 읽기는 창구에 위임한다. QualityStatsService 가 ConversationProperties.pendingGenerationTimeout 을 직접 읽어 '막힌 PENDING' 기준 시각을 계산하고 있었다. 그 기준은 대화 모듈이 정하는 값이라, 부르는 쪽이 계산하면 정리 스케줄러가 쓰는 기준과 조용히 어긋날 수 있다. countCommentsStuckBefore(before) 를 countStuckPendingComments() 로 바꿔 대화 모듈이 자기 기준으로 판단하게 한다. * chore: 쓰이지 않는 import 를 제거한다 리팩터링으로 참조가 사라졌는데 남아 있던 것들이다. ktlint 1.8 의 standard 룰셋에는 no-unused-imports 가 없어서 빌드가 잡아주지 못한다. * refactor: 서비스를 읽기·쓰기가 아니라 목적으로 가른다 XxxReadService 는 기술 축이라, 새 기능마다 "이건 읽기인가"를 물어야 하는데 유스케이스 서비스에도 조회가 있어서 답이 갈렸다. 실제로 member 만 읽기·쓰기 축으로, 나머지는 모듈 안·밖 축으로 갈라져 있었다. 목적으로 가르면 이름이 곧 배치 규칙이 된다. "무엇을 위한 기능인가"만 물으면 된다. - ConversationReadService(15) 를 셋으로 나눈다 - ConversationStatsService: 얼마나 있는지 센다(백오피스 집계·모니터링 게이지) - ConversationCardGenerationService: 카드 생성이 대화방에 요구하는 것 (기존 ConversationCardStateService 를 개명하고 카드 생성용 조회 셋을 흡수) - ConversationService: 대화방 삭제(카드 삭제에 딸린 것 포함)와 미종료 방 주인 조회 - CardReadService -> CardStatsService - GenerationLogReadService -> GenerationLogStatsService - MemberReadService 는 없앤다. getById·search 는 회원 유스케이스라 MemberService 로 돌리고, 집계 둘만 MemberStatsService 로 뺀다 getOwnedConversation 이 ConversationService 와 조회 창구 양쪽에 중복돼 있었다. ConversationService 것을 공개해 하나로 만들고 카드 쪽은 위임한다. LayerDependencyTest 에 이름 규칙을 더한다. Read·WriteService 로 되돌아가면 실패한다. 되돌려서 실제로 잡히는 것을 확인했다. * fix: 자체 리뷰에서 나온 세 건을 고친다 1. LayerDependencyTest 가 소스를 하나도 못 찾으면 실패시킨다. MAIN_SOURCE 가 상대경로라 작업 디렉터리가 프로젝트 루트가 아니면 빈 리스트가 되는데, 그때 규칙 여덟 개가 전부 "위반 0건"으로 통과해 버린다. 경로를 깨뜨려 확인했고, 고친 뒤에는 여덟 개가 모두 실패한다. 2. ErrorKind 와 ErrorCode 의 KDoc 링크가 옮기기 전 위치(global.response.httpStatusOf)를 가리키고 있었다. 실제 위치는 global.web 이다. KDoc 링크는 깨져도 컴파일이 통과한다. 3. 쓰이지 않는 import 세 개를 지운다. getOwnedConversation 을 위임으로 바꾸고 deleteForCardRemoval 을 옮기면서 생긴 것들이다. * fix: 리뷰 반영, 검색 인덱스 규칙과 인덱싱 책임을 바로잡는다 1. LayerDependencyTest 가 #191 이 지목한 바로 그 위반을 못 잡고 있었다. OUTER_LAYERS 에 crypto 가 없어서 domain -> global.crypto 가 규칙 밖이었다. EncryptedStringConverter 를 남기려고 뺀 것인데 같은 패키지의 BlindIndexer 까지 함께 빠졌다. 앞서 "되돌려서 잡히는지" 확인할 때 repository import 로 시험해 다른 위반을 검증하고 넘어간 것이 원인이다. crypto 를 OUTER_LAYERS 에 넣고, @convert 도 @EntityListeners 와 같은 JPA 선언이므로 Converter 를 예외에 더한다. #191 이전 모양으로 되돌려 실제로 실패하는 것을 확인했다. 2. applySearchIndex 가 완성된 값을 받으면서 리스너가 conversation.title?.value 를 알게 됐다. 인덱싱 대상이 바뀌면 엔티티와 리스너를 같이 고쳐야 하고 한쪽만 고치면 조용히 어긋난다. dev 시절 리스너는 그것을 몰랐으므로 결합이 없어진 게 아니라 옮겨간 것이었다. (String) -> String? 을 받는다. BlindIndexer import 는 그대로 사라지고, 무엇을 인덱싱할지는 엔티티가, 어떻게 만들지는 리스너가 각자 안다. 3. 그 메서드는 internal 로 좁힌다. "직접 부를 일은 없다"를 KDoc 으로 부탁하던 자리를 컴파일러가 대신 막는다. 부르는 쪽은 같은 모듈의 리스너뿐이다.
* fix: 백오피스 배지가 좁은 칸에서 줄바꿈되지 않게 한다
상태(진행중, 삭제됨)와 카드(생성) 배지가 두 줄로 접혀 행 높이가 들쭉날쭉했다.
Badge 는 어느 자리에서도 줄바꿈될 이유가 없으므로 기본 클래스에 넣는다.
* feat: 새벽 알림이 나갔는지 백오피스에서 대화방별로 본다
지금까지 FCM 발송 이력을 어디에도 남기지 않아 "알림이 나갔는지"를 서버 로그로만 확인할
수 있었다. 로그는 회원 단위 집계라 "이 방 때문에 대상이었는가"를 답하지 못한다.
notification_log 에 알림 1건이 대화방 하나에 대해 어떻게 끝났는지 남긴다. 발송은 회원
단위인데 표는 대화방 단위라, 발송 한 번이 방 여러 개를 커버하면 방마다 한 줄씩 남기고
같은 결과를 적는다.
결과를 성공, 실패 둘로 두지 않는다. 알림을 끈 회원(NO_DEVICE)과 같은 회원의 다른 방으로
이미 나가서 건너뛴 것(SKIPPED)은 실패가 아닌데, 셋을 뭉치면 백오피스에서 '안 갔다'로만
보여 대응할 것과 아닌 것이 구분되지 않는다. 기록이 아예 없으면 그 회차에 대상이 아니었다는
뜻이라 표에서 빈칸이다.
04:30 리마인더 조회가 회원 id 대신 (대화방, 회원) 을 돌려주게 바꿨다. 회원당 한 번만
보내는 중복 제거는 발송하는 쪽으로 옮겼다.
* fix: 표 헤더와 생성일도 줄바꿈되지 않게 한다
알림 컬럼 두 개를 더하면서 폭이 밀려 헤더가 접혔다("05:00 알/림", "캐릭/터").
헤더 라벨은 어느 표에서도 접힐 이유가 없으므로 TableHead 기본 클래스에 넣는다.
같은 이유로 좁아진 생성일도 "2026. 08. / 27. 12:12" 로 어색하게 끊겼다. 날짜와 시각을
한 줄로 두고, 넘치면 이미 있는 가로 스크롤이 받는다.
* fix: 알림 발송 결과를 회원별로 정확히 기록한다
CodeRabbit 리뷰에서 나온 두 건이다. 둘 다 발송 이력이 사실과 다르게 남는 문제라,
"이 방 때문에 알림이 갔는가"를 보여주려던 목적 자체를 깨뜨린다.
1. 04:30 리마인더가 전원에게 한 번에 보내고 그 합계 하나를 모든 방에 적어 넣었다.
한 명이라도 성공하면 기기가 없던 회원의 방까지 '발송'으로 남는다. 회원마다 따로
보내 자기 결과로 기록한다. FCM 왕복이 늘지만 새벽 배치이고 대상은 수십 명 단위다.
2. CardCreatedNotifier 가 예외를 PushSendResult.none() 으로 삼켜서, 토큰 조회나 정리가
끊긴 장애가 '기기 없음'(알림을 끈 회원이라는 정상 상태)으로 기록됐다. none() 과
구분되는 failed() 를 두고 그것을 돌려준다. 배치 집계에서도 실패가 세어진다.
NotificationLogRecorderTest 를 새로 두어 결과 네 가지의 매핑을 고정한다. 세 항목 모두
되돌려서 실제로 실패하는 것을 확인했다.
* refactor: 자체 리뷰에서 나온 네 건을 고친다
1. admin 이 notification 리포지토리를 직접 잡고 있었다. #208 이 "모듈은 다른 모듈의
리포지토리를 직접 잡지 않는다"를 규칙으로 넣는데, dev 의 기존 위반 19건은 그 PR 이
전부 고치고 이 PR 만 20번째를 더하고 있었다. #208 의 규칙 파일을 이 브랜치에 붙여
실제로 걸리는 것을 확인했다.
#208 이 세운 목적 축대로 notification/service/NotificationLogStatsService 를 둔다.
엔티티도 밖으로 내보내지 않고 결과값만 돌려준다.
2. 회원마다 따로 보내도록 바꾸면서 중간에 터지면 부분 상태로 끝나게 됐는데, KDoc 은
전원에게 한 번에 보내던 때의 설명("예외가 나가도 그 회차가 끝날 뿐이다") 그대로였다.
실제 동작을 적고 테스트로 고정한다.
3. 대화방별 사용량 표가 빈 값을 알림 칸은 하이픈, 나머지 칸은 em dash 로 그려 한 행에
두 기호가 나란히 보였다. em dash 를 쓰지 않기로 했으므로 하이픈으로 맞춘다.
4. 새로 쓴 주석에 남은 가운뎃점 하나를 고친다.
* style: 손보는 파일의 em dash 와 가운뎃점을 걷어낸다
두 문자를 쓰지 않기로 했고, 기존 코드는 그 파일을 손볼 때 함께 고치기로 했다. 이 PR 이
건드리는 11개 파일이 대상이다.
가운뎃점은 자리마다 쉼표인지 '와/과'인지 달라 기계적으로 바꾸지 않고 표현마다 정했다.
em dash 는 대부분 뒤에 완결된 문장이 오는 자리라 마침표로 끊었고, 그렇게 해서 어색해진
다섯 곳은 문장을 다시 썼다.
DailyAutoCardScheduler 의 "5 · LLM 생성" 하나는 남긴다. Grafana 대시보드의 실제 제목을
그대로 인용한 것이라(deploy/monitor/grafana/dashboards/llm-domain.json) 고치면 참조가
가리키는 곳과 어긋난다.
|
Warning Review limit reachedNext included review available in 32 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (83)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📌 개요
dev 에 쌓인 2건을 prod 로 올린다. 마이그레이션 1건(신규 테이블) 있고, API 는 하위 호환이다.
#208 은 내부 구조만 바꾼다. #210 이 사용자에게 닿는 동작을 하나 바꾸므로 아래에 따로 적는다.
1. 04:30 리마인더가 회원마다 따로 나간다
전에는 대상 전원의 토큰을 모아 FCM 을 한 번 불렀다. 이제 회원마다 한 번씩 부른다.
바꾼 이유는 발송 결과를 대화방별로 남기기 위해서다. 한 번에 몰아 보내면 결과가 전원분 합계로만 돌아와서, 한 명이라도 성공하면 기기가 없던 회원의 방까지 '발송' 으로 기록된다.
대상=7)라 부담은 없다고 봤다대상=N명과성공=M건이 크게 벌어지면 이 경우다2. 백오피스 '대화방별 사용량' 에 알림 칸 두 개가 생긴다
04:30 알림,05:00 알림컬럼이 붙는다. 값은 다섯 가지다.배포 직후에는 전부 빈칸이다. 이력을 남기기 시작한 것이 이번 배포라 과거 회차의 기록이 없다. 다음 새벽 배치(04:30, 05:00)부터 채워진다. 표 설명에도 그 사실을 적어 뒀다.
3. 백오피스 표의 줄바꿈이 사라진다
진행중,삭제됨,생성배지와 헤더가 좁은 칸에서 두 줄로 접히던 것을 고쳤다. 기능 변화는 없다.🗄️ 마이그레이션
V36__notification_log.sql신규 테이블 하나뿐이다. 기존 테이블과 데이터는 건드리지 않는다.빈 테이블 생성이라 즉시 끝나고, 실패해도 기존 스키마에 영향이 없다.
🌐 API
GET /api/admin/conversation-usage응답에reminderNotification,cardNotification두 필드 추가(nullable). 기존 필드는 그대로라 클라이언트 영향 없음ErrorCode에서HttpStatus를 떼어냈지만 에러 코드 36개의 상태 코드가 변경 전과 전부 동일한지 프로그램으로 대조해 확인했다🔎 배포 후 확인
notification_log테이블이 생성됐는지미종료 대화방 리마인더 완료: 대상=N명, 대화방=M개, 성공=..., 실패=...에서 대상과 성공이 크게 벌어지지 않는지💬 참고
이번 배포에 사용자 대상 신규 기능은 없다. 3번(줄바꿈)만 백오피스 화면에 보이고, 1번은 발송 방식이 바뀌었을 뿐 받는 알림은 같다.