Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,37 +1,74 @@
package com.comatching.notification.global.config;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;

import org.springframework.beans.factory.annotation.Value;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.StringUtils;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class FCMConfig {

private final ResourceLoader resourceLoader;

// 운영: base64 인코딩된 서비스 계정 JSON 을 env 로 주입받는다.
// (.env 파일이 멀티라인 값을 지원하지 않아 JSON 원문 대신 base64 한 줄로 넣는다)
@Value("${fcm.service-account-b64:}")
private String serviceAccountB64;

// 로컬 폴백: 기존과 같은 classpath 파일.
@Value("${fcm.credentials:classpath:serviceAccountKey.json}")
private String credentialsLocation;

// 운영(aws 프로파일)에서만 true. 키가 없으면 기동을 깨서 배포 시점에 드러낸다.
@Value("${fcm.required:false}")
private boolean required;

@PostConstruct
public void init() {
try {
try (InputStream serviceAccount = openCredentials()) {
if (serviceAccount == null) {
if (required) {
throw new IllegalStateException("FCM 서비스 계정 키가 없다 (env FCM_SERVICE_ACCOUNT_B64 미설정)");
}
log.warn("FCM 키 없음 - 푸시 비활성화 상태로 기동");
return;
}

InputStream serviceAccount = new ClassPathResource("serviceAccountKey.json").getInputStream();
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();

if (FirebaseApp.getApps().isEmpty()) {
FirebaseApp.initializeApp(options);
log.info("🔥 FirebaseApp Initialized");
}
} catch (IOException | IllegalArgumentException e) {
// 키가 "있는데" 깨진 것(base64 오류·JSON 파싱 실패)은 환경 불문 잘못된 상태다. 삼키지 않는다.
throw new IllegalStateException("FirebaseApp 초기화 실패", e);
}
}

} catch (Exception e) {
log.error("❌ FirebaseApp Init Failed", e);
private InputStream openCredentials() throws IOException {
if (StringUtils.hasText(serviceAccountB64)) {
return new ByteArrayInputStream(Base64.getDecoder().decode(serviceAccountB64.trim()));
}
Resource resource = resourceLoader.getResource(credentialsLocation);
return resource.exists() ? resource.getInputStream() : null;
}
}
}
7 changes: 7 additions & 0 deletions notification/src/main/resources/application-aws.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,10 @@ comatching:
# 비우면 내부 API 전체가 401 로 닫힌다(fail-closed).
internal:
service-token: ${INTERNAL_SERVICE_TOKEN:}


# FCM 서비스 계정 키. 이미지에 굽지 않고 .env.prod 의 env 로 주입받는다.
# required: 키가 없으면 기동을 깨서 배포 시점에 드러낸다(kafka.admin.fail-fast 와 같은 철학).
fcm:
service-account-b64: ${FCM_SERVICE_ACCOUNT_B64:}
required: true
7 changes: 7 additions & 0 deletions notification/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,10 @@ comatching:
# 비우면 내부 API 전체가 401 로 닫힌다(fail-closed).
internal:
service-token: ${INTERNAL_SERVICE_TOKEN:}


# FCM 서비스 계정 키. 이미지에 굽지 않고 .env.prod 의 env 로 주입받는다.
# required: 키가 없으면 기동을 깨서 배포 시점에 드러낸다(kafka.admin.fail-fast 와 같은 철학).
fcm:
service-account-b64: ${FCM_SERVICE_ACCOUNT_B64:}
required: true
Comment on lines +65 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

application.yml 은 프로파일과 무관하게 항상 로드되는 base 설정이라, 여기서 fcm.required: true 를 켜면 local 을 포함한 모든 프로파일에서 fail-fast 가 적용됩니다. FCMConfig.init() 에서 required 가 true 인 상태로 base64 env 도, classpath 의 serviceAccountKey.json 도 없으면 IllegalStateException 이 던져져 기동 자체가 깨지는데, PR 설명의 "로컬 환경에서는 classpath 자격 증명 파일을 폴백으로 사용", "운영 환경의 키 누락 시에만 기동 실패"라는 의도와 정반대입니다. FCMConfig.java 의 주석("운영(aws 프로파일)에서만 true")과 @Value("${fcm.required:false}") 기본값과도 어긋나고, application-aws.yml 에 이미 같은 값을 넣어둔 게 완전히 무의미해집니다.

base 는 false 로 두고 application-aws.yml 에서만 true 로 덮어써야 의도한 계층 구조가 성립합니다.

Suggested change
# FCM 서비스 계정 키. 이미지에 굽지 않고 .env.prod 의 env 로 주입받는다.
# required: 키가 없으면 기동을 깨서 배포 시점에 드러낸다(kafka.admin.fail-fast 와 같은 철학).
fcm:
service-account-b64: ${FCM_SERVICE_ACCOUNT_B64:}
required: true
# FCM 서비스 계정 키. 이미지에 굽지 않고 .env.prod 의 env 로 주입받는다.
# required 는 운영(application-aws.yml)에서만 true 로 켠다. 로컬은 classpath 폴백을 사용한다.
fcm:
service-account-b64: ${FCM_SERVICE_ACCOUNT_B64:}
required: false

Loading