Skip to content

[RELEASE] 1.4.10 - #632

Merged
GiJungPark merged 100 commits into
mainfrom
dev
Sep 16, 2026
Merged

GiJungPark merged 100 commits into
mainfrom
dev

Conversation

@GiJungPark

Copy link
Copy Markdown
Member

Related Issue

Key Changes

인증·인가와 공통 오류 응답 정비

  • 기존 사용자 ID 로그인 API는 경로와 응답 계약을 유지한 채 Deprecated 처리하고 인증 패키지로 이동했습니다.
  • Apple·Kakao 외부 연동, Refresh Token 삭제, 로그아웃 디바이스 정리 책임을 각 서비스와 클라이언트로 분리하고 외부 호출 타임아웃·오류 변환을 보완했습니다.
  • JWT가 지원하지 않는 Subject를 거부하고, 실제 토큰을 사용하는 Controller 인증 테스트를 확장했습니다.
  • 요청 검증, 404·405·406·415, 인가 및 예상하지 못한 오류를 공통 COMMON-* 코드로 응답하도록 정리했습니다.
  • 관련 커밋: a33a881b, 43be287e, 6bffdb26

컬렉션 기능 추가

  • 컬렉션 생성·조회·수정·삭제와 좋아요 등록·취소·목록 API를 추가했습니다.
  • 공개 범위와 양방향 차단 정책을 적용하고, 회원 탈퇴 시 컬렉션을 삭제하지 않고 알 수 없는 사용자에게 이관합니다.
  • 컬렉션 작품 순서는 collection_novel.display_order로 보존하며, 같은 작품과 같은 사용자의 중복 좋아요는 DB 제약조건으로 방지합니다.
  • 컬렉션 좋아요는 네이티브 upsert로 멱등 처리하고 기존 좋아요 시각을 보존합니다.
  • 관련 커밋: a7ced573, fe85ce0a

완결·휴재 복귀 알림 기능 추가

  • 사용자별 작품 알림 구독 등록·조회·일괄 삭제 API를 추가했습니다.
  • 구독은 사용자·작품·알림 유형 조합으로 유일하며 발송 완료 여부를 is_sent로 관리합니다.
  • 기존 알림 목록 응답에 nullable novelId를 추가해 작품 알림의 이동 대상을 전달합니다.
  • 실제 알림 생성과 발송 완료 처리는 어드민 서버가 담당하는 계약입니다.
  • 관련 커밋: b899358d

조회 정확성·성능과 클라이언트 계약 보완

  • 작품 상세·서재·취향 추천의 작품 평점과 평점 수를 novel_statistics에서 조회하고 회원 탈퇴 시 통계 기여분을 차감합니다.
  • 서재 작품 응답에 작가명을 추가하고 작품 검색의 최근 검색어 저장 여부를 선택할 수 있게 했습니다.
  • 노벨 탐색의 다중 키워드 조건을 OR로 수정하고 피드 ETC 필터와 인기 피드의 작품 연결 조건을 보완했습니다.
  • 비공개 타 사용자 프로필 조회를 정상 응답으로 복원했습니다.
  • 관련 커밋: 729192b3, 0ba234ef, d81a0c03

빌드·문서·로깅 구조 정비

  • REST Docs 테스트로 OpenAPI 명세를 생성하고 개발 배포 이미지의 Swagger UI에 포함하도록 구성했습니다.
  • 요청 본문 민감정보를 마스킹하고 응답은 구조만 기록하며, 로그를 JSON 비동기 stdout 단일 경로로 전환했습니다.
  • wss-common과 wss-service-support를 루트 source set으로 이동해 Gradle 단일 모듈로 통합하고 Java 17 toolchain을 명시했습니다.
  • 관련 커밋: 094b2843, bd7fb6c1, 54b179fd

To Reviewers

머지·배포 순서

  • main과 dev가 분기돼 있으며 현재 병합 시 FeedCustomRepositoryImplTest.java에서 add/add 충돌이 발생합니다. 충돌을 해결해 최신 main을 dev에 반영한 뒤 최종 검증해야 합니다.
  • main push 즉시 운영 CD가 시작되므로 아래 운영 DB 작업과 로그 수집 경로 확인을 PR 머지 전에 완료해야 합니다.
  • 운영 스키마는 ddl-auto: none이므로 아래 DDL은 자동 적용되지 않습니다.
  • 아래 SQL은 MySQL 8.0 기준입니다. 실행 전 스냅샷/백업과 실제 테이블 engine·charset·collation·PK 타입을 확인하고, 이미 존재하는 객체에는 CREATE/ALTER를 재실행하지 않습니다.
  • 애플리케이션 롤백 시 신규 테이블과 nullable notification.novel_id는 구버전에서 참조하지 않으므로 스키마를 즉시 제거하지 않고 이전 이미지만 재배포합니다.

1. 실행 전 운영 DB 상태 확인

아래 결과를 먼저 기록합니다.

SELECT VERSION() AS mysql_version,
       @@session.time_zone AS session_time_zone,
       @@global.time_zone AS global_time_zone;

SELECT table_name, engine, table_collation
FROM information_schema.tables
WHERE table_schema = DATABASE()
  AND table_name IN (
      'user',
      'novel',
      'notification',
      'notification_type',
      'novel_statistics',
      'collection',
      'collection_novel',
      'collection_like',
      'novel_notification_subscription'
  )
ORDER BY table_name;

SELECT COUNT(*) AS unknown_user_count
FROM user
WHERE user_id = -1;

SELECT COUNT(*) AS missing_novel_statistics_count
FROM novel n
LEFT JOIN novel_statistics ns ON ns.novel_id = n.novel_id
WHERE ns.novel_id IS NULL;

SELECT notification_type_id,
       notification_type_name,
       notification_type_image
FROM notification_type
WHERE notification_type_name IN ('완결 알림', '휴재 복귀 알림');
  • unknown_user_count는 반드시 1이어야 합니다. 값이 없으면 collection.user_id FK 정책을 확정한 뒤 센티넬 사용자를 먼저 준비해야 합니다.
  • missing_novel_statistics_count는 0이어야 합니다.
  • 컬렉션 또는 작품 알림 테이블이 이미 존재하면 아래 CREATE를 실행하지 말고 SHOW CREATE TABLE 결과를 최종 정의와 비교합니다.

2. 컬렉션 테이블 생성

운영에 세 테이블이 모두 없을 때 아래 순서로 실행합니다.

CREATE TABLE collection (
    collection_id           BIGINT      NOT NULL AUTO_INCREMENT,
    user_id                 BIGINT      NOT NULL,
    name                    VARCHAR(20) NOT NULL,
    description             VARCHAR(60) NULL,
    is_public               BOOLEAN     NOT NULL,
    representative_novel_id BIGINT      NOT NULL,
    created_date            DATETIME(6) NOT NULL,
    modified_date           DATETIME(6) NOT NULL,
    PRIMARY KEY (collection_id),
    CONSTRAINT fk_collection_user
        FOREIGN KEY (user_id) REFERENCES user (user_id),
    INDEX idx_collection_user_created
        (user_id, created_date, collection_id)
);

CREATE TABLE collection_novel (
    collection_novel_id BIGINT      NOT NULL AUTO_INCREMENT,
    collection_id       BIGINT      NOT NULL,
    novel_id            BIGINT      NOT NULL,
    display_order       INT         NOT NULL,
    created_date        DATETIME(6) NOT NULL,
    modified_date       DATETIME(6) NOT NULL,
    PRIMARY KEY (collection_novel_id),
    CONSTRAINT uk_collection_novel_collection_novel
        UNIQUE (collection_id, novel_id),
    CONSTRAINT fk_collection_novel_collection
        FOREIGN KEY (collection_id) REFERENCES collection (collection_id),
    CONSTRAINT fk_collection_novel_novel
        FOREIGN KEY (novel_id) REFERENCES novel (novel_id),
    INDEX idx_collection_novel_collection_display_order
        (collection_id, display_order)
);

CREATE TABLE collection_like (
    collection_like_id BIGINT      NOT NULL AUTO_INCREMENT,
    user_id            BIGINT      NOT NULL,
    collection_id      BIGINT      NOT NULL,
    created_date       DATETIME(6) NOT NULL,
    modified_date      DATETIME(6) NOT NULL,
    PRIMARY KEY (collection_like_id),
    CONSTRAINT uk_collection_like_user_collection
        UNIQUE (user_id, collection_id),
    CONSTRAINT fk_collection_like_collection
        FOREIGN KEY (collection_id) REFERENCES collection (collection_id),
    INDEX idx_collection_like_user_created
        (user_id, created_date, collection_like_id)
);
  • collection.is_public에는 DB 기본값을 두지 않습니다.
  • collection.representative_novel_id와 collection_like.user_id에는 FK를 두지 않습니다.
  • collection_like의 감사 시각은 DB NOW(6)로 기록되므로 DB 세션과 애플리케이션 JVM의 시간대를 Asia/Seoul로 맞춥니다.

3. 작품 알림 구독 테이블 생성

저장소의 기존 DDL은 목록 조회 조건인 is_sent가 인덱스에서 빠져 있습니다. 아래처럼 엔티티와 실제 조회 조건에 맞춘 최종 정의를 적용합니다.

CREATE TABLE novel_notification_subscription (
    novel_notification_subscription_id BIGINT NOT NULL AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    novel_id BIGINT NOT NULL,
    notification_type VARCHAR(30) NOT NULL,
    is_sent BOOLEAN NOT NULL DEFAULT FALSE,
    created_date DATETIME NOT NULL,
    modified_date DATETIME NOT NULL,
    PRIMARY KEY (novel_notification_subscription_id),
    CONSTRAINT uk_novel_notification_subscription_user_novel_type
        UNIQUE (user_id, novel_id, notification_type),
    CONSTRAINT fk_novel_notification_subscription_user
        FOREIGN KEY (user_id) REFERENCES user (user_id) ON DELETE CASCADE,
    CONSTRAINT fk_novel_notification_subscription_novel
        FOREIGN KEY (novel_id) REFERENCES novel (novel_id) ON DELETE CASCADE,
    INDEX idx_novel_notification_subscription_user_type_sent_id
        (user_id, notification_type, is_sent, novel_notification_subscription_id),
    INDEX idx_novel_notification_subscription_novel_type
        (novel_id, notification_type)
);

4. 기존 notification 테이블 확장

테이블 크기와 실행 계획을 확인하고 컬럼·인덱스·FK를 분리해 적용합니다. FK 추가는 테이블 재구성과 쓰기 잠금 가능성이 있으므로 저트래픽 시간대에 실행합니다.

ALTER TABLE notification
    ADD COLUMN novel_id BIGINT NULL AFTER feed_id;

ALTER TABLE notification
    ADD INDEX idx_notification_novel_id (novel_id),
    ALGORITHM=INPLACE,
    LOCK=NONE;

ALTER TABLE notification
    ADD CONSTRAINT fk_notification_novel
        FOREIGN KEY (novel_id)
        REFERENCES novel (novel_id)
        ON DELETE SET NULL;

5. 작품 통계 누락·불일치 보정

1단계 조회의 missing_novel_statistics_count가 0이 아니거나 원천 user_novel과 통계값이 다르면, 저트래픽 시간대에 아래 보정 쿼리를 한 번 실행합니다.

INSERT INTO novel_statistics (
    novel_id,
    average_rating,
    rating_sum,
    rating_count,
    popularity
)
SELECT
    n.novel_id,
    CAST(
        ROUND(COALESCE(AVG(NULLIF(un.user_novel_rating, 0)), 0), 3)
        AS DECIMAL(4, 3)
    ),
    CAST(
        ROUND(COALESCE(SUM(NULLIF(un.user_novel_rating, 0)), 0), 1)
        AS DECIMAL(19, 1)
    ),
    COUNT(NULLIF(un.user_novel_rating, 0)),
    COUNT(CASE
        WHEN un.is_interest = TRUE OR un.status <> 'QUIT' THEN 1
    END)
FROM novel n
LEFT JOIN user_novel un ON un.novel_id = n.novel_id
GROUP BY n.novel_id
ON DUPLICATE KEY UPDATE
    average_rating = VALUES(average_rating),
    rating_sum = VALUES(rating_sum),
    rating_count = VALUES(rating_count),
    popularity = VALUES(popularity);

6. 작품 알림 타입 시드

어드민이 실제 작품 알림 발송을 시작하기 전까지 적용합니다. <...>는 확정된 S3 아이콘 경로로 치환한 뒤 실행하며, 기존 행이 있으면 INSERT하지 않습니다.

INSERT INTO notification_type (
    notification_type_name,
    notification_type_image
)
SELECT '완결 알림', '<완결 알림 S3 아이콘 경로>'
WHERE NOT EXISTS (
    SELECT 1
    FROM notification_type
    WHERE notification_type_name = '완결 알림'
);

INSERT INTO notification_type (
    notification_type_name,
    notification_type_image
)
SELECT '휴재 복귀 알림', '<휴재 복귀 알림 S3 아이콘 경로>'
WHERE NOT EXISTS (
    SELECT 1
    FROM notification_type
    WHERE notification_type_name = '휴재 복귀 알림'
);
  • 어드민은 발송 완료 후 novel_notification_subscription.is_sent = TRUE로 갱신하고, 앱 내 알림 생성 시 notification.novel_id를 채워야 합니다.

7. 적용 후 검증

SHOW CREATE TABLE collection;
SHOW CREATE TABLE collection_novel;
SHOW CREATE TABLE collection_like;
SHOW CREATE TABLE novel_notification_subscription;
SHOW CREATE TABLE notification;

SELECT COUNT(*) AS unknown_user_count
FROM user
WHERE user_id = -1;

SELECT COUNT(*) AS missing_novel_statistics_count
FROM novel n
LEFT JOIN novel_statistics ns ON ns.novel_id = n.novel_id
WHERE ns.novel_id IS NULL;

SELECT index_name,
       GROUP_CONCAT(column_name ORDER BY seq_in_index) AS indexed_columns
FROM information_schema.statistics
WHERE table_schema = DATABASE()
  AND table_name IN (
      'collection',
      'collection_novel',
      'collection_like',
      'novel_notification_subscription',
      'notification'
  )
GROUP BY table_name, index_name
ORDER BY table_name, index_name;

운영·호환성 확인

  • 새 logback-spring.xml은 파일 로그를 제거하고 JSON stdout만 출력합니다. 운영 deploy.sh가 awslogs 또는 동등한 수집 경로를 사용하고, 기존 평문 기반 CloudWatch 필터가 JSON에 맞게 갱신됐는지 확인해야 합니다.
  • dev/prod가 동일한 websoso/websoso-image:latest 태그를 사용하므로 운영 CD 동안 dev push를 중단하고 배포 전후 이미지 digest를 기록해 주세요.
  • 작품 알림 발송은 어드민 서버가 담당하므로 DB 스키마와 is_sent 계약을 어드민 배포 일정과 맞춰야 합니다.
  • NotificationItem.novelId, 서재 작품의 author는 추가 필드입니다. 클라이언트는 novelId = null을 허용해야 합니다.
  • 요청 검증 및 404·405·406·415·500 응답이 더는 401 AUTH-001로 변환되지 않습니다. 클라이언트의 401 재로그인 분기가 상태 코드와 COMMON-* 오류 코드 변경을 수용하는지 확인해 주세요.
  • 현재 dev에서 ./gradlew build -x test는 성공했습니다. ./gradlew test는 792개 중 13개가 실패했으며, 12개는 로컬 MySQL 부재로 인한 컨텍스트 실패이고 1개는 main에도 동일한 RecentSearchServiceTest 기존 NPE입니다. main 병합 충돌 해결 후 운영과 같은 설정에서 두 명령을 다시 실행해야 합니다.

References

GiJungPark and others added 30 commits July 31, 2026 13:59
UserController에 남아 있던 POST /users/login 처리를 AuthController로 옮겨 인증 관련 엔드포인트를 인증 패키지에 모은다.

요청 경로와 요청·응답 형식은 그대로 유지해 기존 클라이언트 계약에는 변화가 없다.
사용자 ID만으로 Access Token을 발급하는 POST /users/login과 AuthApplication.login을 Deprecated로 표시하고 사유를 문서화한다.

신규 클라이언트는 소셜 로그인으로 인증하고 Access Token 만료 시 POST /reissue로 재발급받아야 하며, 기존 클라이언트의 미사용이 확인되면 별도 이슈에서 제거한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit ed075b2)
[HOTFIX] 인기 피드 소설 정보 null 반환 방지
[REFACTOR] Refresh Token 저장소 접근 책임 정리
[REFACTOR] Deprecated 리소스 인가 구조 제거
[TEST] 인증 Controller 테스트에 Access Token 적용 기반 마련
[REFACTOR] Apple 외부 연동 책임 정리
[TEST] 소셜 로그인 및 로그아웃 인증 흐름 보완
[REFACTOR] GET /novels/taste 응답 지연 개선
ljy1348 and others added 26 commits August 14, 2026 20:58
동기 appender는 요청 스레드가 직접 인코딩·write·flush를 수행해 16스레드
부하 실측에서 요청당 평균 538us, p99 7,209us가 로깅에 소요됐다. 로그 전송이
정체되면 파이프 버퍼 포화로 요청 스레드가 최대 501ms까지 멈추기도 했다.

AsyncAppender를 적용해 평균 37us, p99 8.8us로 낮추고, 큐가 가득 차면 로그를
버리되 요청 처리는 막지 않도록 neverBlock을 켠다. INFO 로그까지 임의로
버리지 않도록 discardingThreshold는 0으로 둔다.

컨테이너 stdout은 Docker awslogs 드라이버가 수집하므로 파일 appender는
중복이라 제거한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
[CHORE] 요청·응답 바디 로깅 및 민감정보 마스킹 적용
[CHORE] 멀티모듈 구조를 단일 모듈로 통합
[REFACTOR] 공통 API 오류 응답 코드 체계를 도입
[FIX] 피드 ETC 필터 조건 수정
[FIX] 노벨 전체 탐색 다중 키워드 조건을 OR로 변경
[FEAT] 서재 노벨 정보에 작가명 추가
[FIX] 노벨 통계 조회 시 통계 테이블 데이터 사용
@GiJungPark
GiJungPark marked this pull request as ready for review September 16, 2026 06:16
@GiJungPark
GiJungPark merged commit 8568e8b into main Sep 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants