Skip to content

Conversation

@jsoonworld
Copy link
Member

📄 Work Description

FCM 토큰 재발급 필요 여부를 클라이언트가 판단할 수 있도록, 사용자의 계정 상태 및 푸시 알림 설정을 고려하여 만료 여부를 확인하는 기능을 구현하였습니다.

주요 변경사항:

  • FcmTokenValidateService를 통해 사용자 상태와 푸시 알림 설정을 기반으로 만료 여부 확인
  • 사용자 계정이 비활성화 또는 푸시 알림 비활성화 상태일 경우 false를 반환하도록 처리
  • UserRepository를 통해 사용자를 조회하고, 없는 사용자일 경우 예외 처리
  • 서비스 로직의 명확성을 높이기 위해 사용자 상태 검증 메서드(isUserQualifiedForFcmValidation)를 분리

💬 To Reviewers

  • 사용자 상태 체크 로직(isUserQualifiedForFcmValidation)이 명확하게 분리되었는지 확인 부탁드립니다.
  • 예외 처리 방식이 아닌 상태 반환(true/false) 방식으로 변경되었는데 비즈니스 요구사항과 맞는지 리뷰 부탁드립니다.
  • 테스트 코드에서 계정 탈퇴 및 푸시 알림 비활성화 케이스를 추가했으니 누락된 케이스가 있는지 확인 부탁드립니다.

📷 Screenshot

스크린샷 2025-03-28 오후 8 25 39

⚙️ ISSUE

✅ PR check list

  • Reviewers
  • Assignees
  • Labels

- 메인 메시지 null 여부 및 유효성 검증 에러 추가
- 메시지 포맷 시 파라미터 누락 및 값 누락에 대한 에러 정의
- 공통 인터페이스 ErrorCode 구현
- 메시지 관련 예외 처리를 위한 MessageException 클래스 정의
- BaseException 상속 및 MessageErrorCode 기반 생성자 구현
- 템플릿 내 플레이스홀더를 파라미터로 치환하는 포맷 메서드 정의
- 포맷팅 필요 여부에 따라 파라미터 검증 및 치환 수행
- 누락된 파라미터나 값이 있을 경우 MessageException 발생
- MessageTemplate 인터페이스 구현을 위한 추상 클래스 설계
- 사용자에게 보여줄 메인 메시지를 Enum 형태로 정의
- 각 항목은 포맷팅이 필요 없는 고정 메시지로 구성
- AbstractMessageTemplate 상속을 통해 포맷팅 기능 확장
- 메시지 전송 대상 구분을 위한 타입 정의
- 스크랩한 사용자(SCRAPPED_USER)와 전체 사용자(ALL_USERS) 구분
- 메인/서브 메시지 및 타겟 유형을 조합한 템플릿 타입 정의
- 메시지 종류별로 메인 메시지, 서브 메시지, 대상 사용자 연결
- 각 템플릿 타입에서 메시지 포맷 메서드(main, sub) 제공
- 사용자 이름을 포함하는 메시지 본문(SubMessage) 템플릿 정의
- 포맷팅이 필요한 메시지로 구성되어 파라미터 기반 치환 수행
- AbstractMessageTemplate 상속으로 포맷팅 로직 재사용
- Messages → Message로 클래스명 단수형으로 변경하여 의미 명확화
- 동일한 필드(mainMessage, subMessage)는 유지
- 기존 Messages 클래스 제거 및 새로운 Message 클래스 적용
- 메시지 문자열을 반환하는 value 메서드 정의
- 포맷팅 필요 여부를 판단하는 needsFormatting 메서드 정의
- 파라미터 기반 포맷을 수행하는 format 메서드 포함
- AbstractMessageTemplate, MainMessage, SubMessage 등에서 구현 예정
- Messages 클래스명을 Message로 변경함에 따라 필드 타입 및 import 구문 수정
- 관련 JPA 매핑 어노테이션 유지 (cascade, orphanRemoval 등)
- MessageTemplateType의 main/sub 메시지 포맷 정상 동작 테스트
- 파라미터 누락, 잘못된 키, null 값 등에 대한 예외 케이스 검증
- 포맷이 불필요한 메인 메시지와 여분 파라미터 무시 동작 확인
- AbstractMessageTemplate의 중복 플레이스홀더 처리 테스트 포함
- messageTemplateType 필드 추가 및 Enum 매핑 처리
- formattedMainMessage, formattedSubMessage 필드 추가
- MessageTemplateType 기반 메시지 생성을 위한 of 정적 메서드 구현
- 동일 템플릿 타입 비교를 위한 isSameType 메서드 추가
- MessageTemplateType을 기반으로 Message 객체 생성 기능 테스트
- main/sub 메시지 포맷 정상 동작 여부 검증
- 포맷 파라미터 누락, null 값, 잘못된 키 등의 예외 케이스 검증
- isSameType 메서드를 통한 템플릿 타입 비교 기능 테스트
FCM 메시지 전송 실패에 대한 에러 코드를 FcmErrorCode enum으로 정의하였습니다.
- HttpStatus.INTERNAL_SERVER_ERROR 상태와 함께 실패 메시지 제공
- 공통 인터페이스 ErrorCode 구현
FcmErrorCode를 활용한 FCM 전송 실패 예외 FcmException을 정의하였습니다.
- BaseException을 상속받아 공통 예외 처리 방식 유지
- FcmErrorCode 기반으로 에러 메시지 및 상태 전달
FCM 토큰이 만료되었는지 여부를 판단하는 FcmTokenValidator 인터페이스를 정의하였습니다.
- FcmToken 도메인 객체 기반의 만료 여부 검증 메서드 선언
- 다양한 구현체 확장을 위한 계약 역할 수행
FCM 서버에 검증 메시지를 전송하여 FCM 토큰이 만료되었는지를 확인하는 FirebaseFcmTokenValidator 구현체를 추가하였습니다.
- MessagingErrorCode.UNREGISTERED 에러 시 만료로 간주
- 예외 발생 시 FcmException으로 변환하여 공통 에러 처리 유지
사용자의 FCM 토큰이 만료되었는지 여부를 확인하는 API를 UserController에 구현하였습니다.
- POST /api/v1/users/fcm-tokens/reissue-required 엔드포인트 추가
- 요청 바디로 FcmTokenReissueRequiredRequest 수신
- 결과는 FcmTokenReissueRequiredResponse로 반환하며, 성공 응답 래핑 처리
사용자의 FCM 토큰 재발급 필요 여부를 확인하는 로직을 담당할 FcmTokenValidateService 인터페이스를 정의하였습니다.
- 요청 DTO를 받아 응답 DTO로 결과 반환
- 서비스 로직의 확장성과 테스트 용이성을 고려한 구조
사용자의 FCM 토큰이 만료되었는지 확인하는 FcmTokenValidateServiceImpl 클래스를 구현하였습니다.
- UserRepository를 통해 사용자 조회
- FcmTokenValidator를 통해 토큰 만료 여부 검증
- 결과를 FcmTokenReissueRequiredResponse로 반환
- 사용자 없을 시 USER_NOT_FOUND 예외 처리
UserService에서 FcmTokenValidateService를 사용해 FCM 토큰 만료 여부 확인 기능을 위임 처리하였습니다.
- FcmTokenReissueRequiredRequest를 받아 서비스 호출
- FcmTokenReissueRequiredResponse 반환
사용자 조회 실패 시 예외 처리를 위한 USER_NOT_FOUND 에러 코드를 추가하였습니다.
- HttpStatus.NOT_FOUND 상태와 메시지 구성
- 공통 에러 코드 인터페이스 구현을 위한 포맷 유지
FCM 토큰 재발급 필요 여부를 성공적으로 응답했을 때 사용할 UserSuccessCode를 추가하였습니다.
- HTTP 상태는 200 OK
- 메시지: "FCM 토큰 재발급 여부를 성공적으로 확인했습니다."
User 도메인에 FCM 토큰 만료 여부를 FcmTokenValidator를 통해 확인하는 isFcmTokenExpired 메서드를 추가하였습니다.
- token 필드의 위임 메서드 형태로 구현
- User 내부에서 FCM 토큰 유효성 검증 가능하도록 개선
User 엔티티에 대한 기본 CRUD 작업을 수행할 수 있도록 JpaRepository를 상속한 UserRepository 인터페이스를 정의하였습니다.
- Long 타입의 사용자 ID 기반 조회 지원
- @repository 어노테이션으로 스프링 빈으로 등록
FcmTokenValidator를 이용해 토큰 만료 여부를 확인할 수 있는 isExpiredWith 메서드를 FcmToken VO에 추가하였습니다.
- 외부 유효성 검증 전략을 위임받아 처리 가능
- 테스트 및 검증을 위한 TODO 주석 추가
- value 필드 중복 반환 제거로 정리
사용자의 FCM 토큰 재발급 필요 여부를 확인하기 위한 요청 DTO를 정의하였습니다.
- userId를 필드로 포함
- of 정적 팩토리 메서드를 통해 객체 생성 가능하도록 구성
FCM 토큰 재발급이 필요한지를 클라이언트에 전달하기 위한 응답 DTO를 정의하였습니다.
- reissueRequired 필드를 포함
- 정적 팩토리 메서드 of 제공
불필요한 contextLoads 메서드만 포함된 기본 생성 테스트 클래스 TerningApplicationTests를 삭제하였습니다.
- 불필요한 통합 테스트 실행을 방지하고 명확한 단위 테스트 환경 구성 목적
단위 테스트에서 FCM 토큰 만료 여부를 제어하기 위한 FakeFcmTokenValidator 클래스를 추가하였습니다.
- FcmTokenValidator 인터페이스 구현
- setExpired 메서드를 통해 만료 여부를 임의로 설정 가능
- 테스트 독립성과 예측 가능성 확보
FcmTokenValidateServiceImpl에 대한 단위 테스트를 작성하였습니다.
- FakeFcmTokenValidator를 사용해 만료 여부를 제어
- 토큰이 만료된 경우 true, 그렇지 않으면 false를 반환하는지 확인
- 존재하지 않는 사용자 ID에 대해 예외가 발생하는지 검증
- @DisplayName, @nested를 활용해 테스트 가독성 향상
단위 테스트에서 UserRepository를 대체할 수 있는 인메모리 기반의 UserRepositoryTest 클래스를 구현하였습니다.
- id 자동 생성 및 필드 직접 주입을 통해 테스트 가능하도록 구성
- save, findById, delete 등 기본 CRUD 기능 구현
- Spring Data JPA의 JpaRepository 인터페이스 구현 메서드 중 필요한 부분만 처리
- 실제 DB에 의존하지 않고 사용자 도메인 테스트 가능
- `setUp()`에서 유저 객체를 더 명확히 구분
- `userId`를 잘못된 변수로 재사용하던 부분 수정
- `FcmTokenReissueRequiredRequest` 중복 코드를 하나로 통합
- 잘못된 변수명 수정 및 불필요한 코드 제거
- 테스트의 예외 처리 및 반환 값 검증 부분 수정
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements an FCM token validation API that checks if a token is expired based on the user's account status and push notification settings.

  • Introduces FcmTokenValidateService to determine token validity via Firebase messaging.
  • Refactors the messaging domain by replacing the plural Messages entity with a singular Message entity and adding enums for message templates.
  • Configures Firebase initialization using properties injected via FcmProperties.

Reviewed Changes

Copilot reviewed 42 out of 42 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/main/java/org/terning/notification/domain/Notifications.java Updated to use a singular Message entity instead of Messages.
src/main/java/org/terning/message/domain/enums/SubMessage.java Added enum for detailed sub message templates.
src/main/java/org/terning/message/domain/enums/MessageTemplateType.java Introduced enum tying main and sub messages with target types.
src/main/java/org/terning/message/domain/enums/MessageTargetType.java Defined enum for target user types for messaging.
src/main/java/org/terning/message/domain/enums/MainMessage.java Added enum for main messages with formatting support.
src/main/java/org/terning/message/domain/Messages.java Removed the Messages entity in favor of using the Message entity.
src/main/java/org/terning/message/domain/Message.java New entity encapsulating message details and type matching.
src/main/java/org/terning/message/domain/AbstractMessageTemplate.java Provides common formatting logic with placeholder replacement.
src/main/java/org/terning/message/domain/MessageTemplate.java Interface for message templating.
src/main/java/org/terning/message/common/failure/MessageException.java Exception for message templating errors.
src/main/java/org/terning/message/common/failure/MessageErrorCode.java Enum defining error codes and messages for template failures.
src/main/java/org/terning/fcm/validate/FirebaseFcmTokenValidator.java Implements token validation logic using Firebase messaging.
src/main/java/org/terning/fcm/validate/FcmTokenValidator.java Interface for defining FCM token validation.
src/main/java/org/terning/fcm/config/FcmProperties.java Configured properties for Firebase service key injection.
src/main/java/org/terning/fcm/config/FcmConfig.java Initializes Firebase with JSON credentials from properties.
src/main/java/org/terning/fcm/common/failure/FcmException.java Exception for handling FCM-related failures.
src/main/java/org/terning/fcm/common/failure/FcmErrorCode.java Enum defining error codes and messages for FCM failures.
src/main/java/org/terning/TerningApplication.java Enabled configuration properties for FcmProperties.

…`@GetMapping`이 중복 사용되었던 부분 수정 - `@RequestBody`와 `@PathVariable`의 변수명 중복 해결 - `FcmTokenReissueRequiredRequest` 객체의 생성 로직 개선

- `@PostMapping`과 `@GetMapping`이 중복 사용되었던 부분 수정
- `@RequestBody`와 `@PathVariable`의 변수명 중복 해결
- `FcmTokenReissueRequiredRequest` 객체의 생성 로직 개선
Copy link
Contributor

@junggyo1020 junggyo1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선 Message.of 메서드를 통해 내부적으로 포맷된 메세지를 넘겨받고 있는데요.
포맷에 대한 오류 검증이 외부에 의존하고 있다보니 도메인 레벨에서는 그냥 값이 넘어와버릴 수 있어 불완전하다는 생각이 들었어요. 포맷로직 자체를 내부에서 처리할 수 있도록 고려하면 좋지 않을까하는 생각이 드는데 생각이 궁금합니다.

그리고 햔제 validator에서는 해당 토큰이 만료된 토큰인지만 판단하고 있는데 이후에 토큰의 재발급 조건이 변경될 가능성을 고려하고 계시는 건지 궁금합니다.

마지막으로 재발급이 필요한지 여부를 서버가 판단하고 리턴하는 구조인데, 실제 재발급 API 호출 시점은 어디서 이루어지게 되나요?

고생하셨습니다!!:)

@jsoonworld
Copy link
Member Author

@junggyo1020

우선 Message.of 메서드를 통해 내부적으로 포맷된 메세지를 넘겨받고 있는데요.
포맷에 대한 오류 검증이 외부에 의존하고 있다보니 도메인 레벨에서는 그냥 값이 넘어와버릴 수 있어 불완전하다는 생각이 들었어요. 포맷로직 자체를 내부에서 처리할 수 있도록 고려하면 좋지 않을까하는 생각이 드는데 생각이 궁금합니다.

이전 PR에서 말씀해주신 것처럼, 도메인 내부에서 메시지 포맷까지 처리하는 방향에 저도 공감합니다. 말씀해주신 내용을 바탕으로 이슈로 등록해두었고, 도메인의 책임을 명확히 하기 위해 내부에서 포맷 로직까지 처리하는 방향으로 개선해보려고 합니다 💪

현재는 Message.of()를 호출할 때 외부에서 이미 포맷된 메시지를 주입받고 있기 때문에, 도메인 입장에서는 메시지가 올바르게 포맷되었는지 신뢰할 수 없는 구조입니다. 이로 인해 말씀처럼 불완전한 값이 도메인으로 넘어올 수 있다는 리스크가 있고, 도메인 자체의 무결성을 보장하기 어렵다는 점에 저도 공감했습니다.

말씀해주신 것처럼 Message.of(MessageTemplateType template, Map<String, String> params) 형태로 리팩토링하고, 내부에서 template.main(params)template.sub(params)를 처리하면,

  • 포맷 로직을 외부에 위임하지 않고 메시지가 직접 처리할 수 있고,
  • 도메인 내부에서 포맷 예외도 명확하게 다룰 수 있어
    불변성과 완전성을 갖춘 구조로 개선될 수 있을 것 같습니다.

다만 Message가 템플릿 구조나 파라미터 요구사항을 더 많이 알게 되기 때문에 결합도 증가에 대한 고민도 함께 가져가야 할 것 같고요. 이건 실제 리팩토링을 적용하면서 도메인의 책임 선을 더 명확히 다듬어보면 좋을 것 같습니다.

좋은 피드백 감사합니다! 🙌

@jsoonworld
Copy link
Member Author

@junggyo1020

그리고 햔제 validator에서는 해당 토큰이 만료된 토큰인지만 판단하고 있는데 이후에 토큰의 재발급 조건이 변경될 가능성을 고려하고 계시는 건지 궁금합니다.

피드백 감사합니다! 😊
말씀 주신 부분 공감합니다.

현재 FirebaseFcmTokenValidator에서는 FCM 토큰이 만료되었는지 여부만 판단하고 있는데,
그 이유는 이 API가 로그인 시점에 사용되는 API이기 때문입니다.

소셜 로그인 특성상 이미 회원가입이 완료된 유저가 다시 로그인하는 흐름이라,
해당 시점에서는 "기존에 가지고 있던 FCM 토큰이 만료됐는지" 여부만 판단하면 된다고 생각했습니다.
토큰 자체의 유효성 검증은 회원가입 시 토큰 값을 수신할 때 이미 검증을 마친다고 보고 있어요.

다만, 말씀처럼 재발급 조건이 향후 다양해질 가능성은 충분히 있다고 생각해서,
이를 고려해 FcmTokenValidator는 인터페이스로 분리해두었고,
User 도메인에서도 isFcmTokenExpired(validator)를 통해 검증 책임을 위임하는 구조로 설계했습니다.

향후 토큰 만료 외에도, 예를 들어 일정 기간 미사용, 유저 상태, 앱 버전 등의 조건이 추가될 경우
Validator 내부 로직을 확장하거나 전략 클래스로 분리하는 방식으로 유연하게 대응할 수 있을 것 같아요.

좋은 포인트 짚어주셔서 감사합니다! 🙌

@jsoonworld
Copy link
Member Author

@junggyo1020

마지막으로 재발급이 필요한지 여부를 서버가 판단하고 리턴하는 구조인데, 실제 재발급 API 호출 시점은 어디서 이루어지게 되나요?

좋은 질문 감사합니다! 🙌
말씀 주신 것처럼 현재 구조에서는 ‘재발급 필요 여부’는 서버가 판단하고, 실제 재발급은 클라이언트가 수행하는 흐름입니다.

제가 생각한 전체 플로우는 다음과 같습니다:

  1. 클라이언트가 로그인 요청을 보냅니다.
    이때 이미 가입된 유저라면, 보유하고 있는 FCM 토큰이 유효한지 확인이 필요합니다.

  2. 운영 서버는 로그인 요청을 처리하면서,
    해당 유저의 FCM 토큰이 만료되었는지를 판단하기 위해 알림 서버에 요청을 보냅니다.

  3. 알림 서버는 유저의 ID를 기반으로
    FCM 토큰이 만료되었는지 확인하고, reissueRequired 값을 운영 서버에 응답합니다.

  4. 운영 서버는 이 값을 포함하여 로그인 응답을 클라이언트에 전달합니다.
    응답에는 fcmReissueRequired: true 와 같은 정보가 포함되며,

  5. 클라이언트는 해당 값을 보고 실제로 FCM 토큰을 재발급 받고 서버에 갱신 요청을 수행합니다.

이 구조의 장점은 비즈니스 판단(만료 여부)은 서버가 수행하고,
실제 재발급 책임은 클라이언트가 갖는 역할 분리가 명확하다는 점입니다.
또한, 현재는 단순히 만료 여부만 판단하고 있지만,
앞으로 재발급 조건이 복잡해질 가능성을 고려해 Validator 인터페이스를 두고,
유연하게 조건을 추가할 수 있는 구조로 설계했습니다.

정리하자면, 재발급 API 호출 시점은 클라이언트가 로그인 응답을 받은 직후이며,
reissueRequiredtrue일 때 클라이언트가 새 토큰을 발급받아 별도의 API를 통해 서버에 갱신하게 됩니다.

이 구조가 사용자 경험을 방해하지 않으면서도 안정적으로 토큰 상태를 유지할 수 있어 적절하다고 생각했습니다! 💡

@jsoonworld jsoonworld merged commit 82cd0d2 into develop Mar 29, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[✨ feat] FCM 토큰 유효성 검증 API 구현

3 participants