이제는 없으면 안되는 firebase.
웹에서 작성한 알림메세지를 서버로 전송하여 각각의 디바이스로 뿌려주는 간단한 코드를 작성하고자 한다.
대략 플로우는 이런식이다.
Web ( notification UI/UX ) -> Spring Boot( WAS ) -> Firebase -> Android ( device )
Firebase를 경유 해야하기 때문에 폐쇄망에서는 Firebase를 구축하거나,
방화벽 허용 등으로 해결해야 된다.
https://console.firebase.google.com/
우선 Firebase -> console 로 이동해서 프로젝트 생성을 해준다.
그 후에 사진과 같은 방법으로 비공개 키를 생성해준다.
사용자 인증할 때 필요한 친구이고,
여기서 sdk를 다운받아서 프로젝트를 연결 시켜준다.
화면에 보이듯이, SDK 안내 보기를 누르면 쉽게 설명해주기 때문에 따로 설명은 필요 없을것같다.
그 다음 Spring Boot에서 설정을 시작해보자.
SDK를 제대로 연결했다면 Spring Boot 폴더에 .json 파일이 들어가 있을것이다.
<dependency>
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>7.3.0</version>
</dependency>
maven으로 작업했기때문에 pom.xml에 작성해주고,
패키징은 단순 보내기 목적만 달성할 요량이므로 간단하게 만들어준다.
우선 initalize를 먼저 작성해주는 것이 좋을거같다.
FirebaseConfig
@Configuration
public class FirebaseConfig {
private final Logger logger = LoggerFactory.getLogger(FirebaseConfig.class);
@Value("${firebase-sdk-path}") // your firebase sdk path
private String firebaseSdkPath;
@PostConstruct
public void initialize() {
try {
ClassPathResource resource = new ClassPathResource(firebaseSdkPath);
InputStream serviceAccount = resource.getInputStream();
// 2021.06.23 FirebaseOptions 생성자가 Deprecated 되었기 때문에 builder 수정.
// 2022.01.04 공식홈페이지에서 제대로 수정됨
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
FirebaseApp.initializeApp(options);
} catch (FileNotFoundException e) {
logger.error("Firebase ServiceAccountKey FileNotFoundException" + e.getMessage());
} catch (IOException e) {
logger.error("FirebaseOptions IOException" + e.getMessage());
}
}
}
나는 .yml 파일에 firebase-sdk-path 변수에 path를 저장해두었다.
그리고 보낼형식을 만들어주는 model를 작성한다.
@Builder
@AllArgsConstructor
@Data
public class FcmMessage {
private boolean validate_only;
private Message message;
@Builder
@AllArgsConstructor
@Data
public static class Message {
private Notification notification;
private String token;
}
@Builder
@AllArgsConstructor
@Data
public static class Notification {
private String title;
private String body;
private String image;
}
}
lombok 어노테이션은 개인 취향
그다음 Service 작성
@Slf4j
@Service
public class PushNotificationService {
private static final String PROJECT_ID = /* your project id */;
private static final String BASE_URL = "https://fcm.googleapis.com";
private static final String FCM_SEND_ENDPOINT = "/v1/projects/" + PROJECT_ID + "/messages:send";
private static final String MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
private static final String[] SCOPES = { MESSAGING_SCOPE };
public static final String MESSAGE_KEY = "message";
}
일단 상수 값들을 넣어준다. firebase 홈페이지에 있는 project_id ( 또는 json파일 읽어보면 적혀있다. ) 를 적어준다.
ENDPOINT가 왜 이렇지하고 생각 될 수 있는데
firebase가 기존 HTTP 에서 HTTP v1 으로 마이그레이션을 진행했다.
작성당시에는 버그가 어마어마하게 많았는데
주기적으로 업데이트 하는 듯 하다.
( 참고로 google 답지않게 공식 페이지의 description이 최악이였다.. )
private static HttpURLConnection getConnection() throws IOException {
// [START use_access_token]
URL url = new URL(BASE_URL + FCM_SEND_ENDPOINT);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setRequestProperty("Authorization", "Bearer " + getAccessToken());
httpURLConnection.setRequestProperty("Content-Type", "application/json; UTF-8");
return httpURLConnection;
// [END use_access_token]
}
private static String getAccessToken() throws IOException {
//21.6.23 아직까지도 공식 홈페이지에서 Deprecated 된 해당 문장을 수정하지 않고있다.
//22.01.04 공식 홈페이지에서 제대로 수정이 되었다.
GoogleCredentials googleCredential = GoogleCredentials
.fromStream(new ClassPathResource("/*your json file*/").getInputStream())
.createScoped(Arrays.asList(SCOPES));
googleCredential.refreshIfExpired();
return googleCredential.getAccessToken();
}
private static String inputstreamToString(InputStream inputStream) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
Scanner scanner = new Scanner(inputStream);
while (scanner.hasNext()) {
stringBuilder.append(scanner.nextLine());
}
return stringBuilder.toString();
}
우선, Firebase와 통신하기 위해 connection 을 열어주고, 서비스를 이용 하기위해
token 값을 받아주는 메소드를 먼저 작성해준다.
inputstreamToString은 메세지를 보내고나서 return값을 connection에서 읽어오기 위해 필요하다.
public static void sendMessage(JsonObject fcmMessage) throws IOException {
HttpURLConnection connection = getConnection();
connection.setDoOutput(true);
DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
outputStream.writeBytes(fcmMessage.toString());
outputStream.flush();
outputStream.close();
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
String response = inputstreamToString(connection.getInputStream());
System.out.println("Message sent to Firebase for delivery, response:");
System.out.println(response);
} else {
System.out.println("Unable to send message to Firebase:");
String response = inputstreamToString(connection.getErrorStream());
System.out.println(response);
}
}
firebase와 통신하고, return값을 받는 메소드
이제 메세지에 무엇이 들어가는지, 누구에게 보내는지 등을 특정해줘야 한다.
private static JsonObject buildNotificationMessage(String title, String body) {
JsonObject jNotification = new JsonObject();
jNotification.addProperty("title", title);
jNotification.addProperty("body", body);
JsonObject jMessage = new JsonObject();
jMessage.add("notification", jNotification);
/*
firebase
1. topic
2. token
3. condition -> multiple topic
*/
jMessage.addProperty("topic", "news");
//jMessage.addProperty("token", /* your test device token */);
JsonObject jFcm = new JsonObject();
jFcm.add(MESSAGE_KEY, jMessage);
return jFcm;
}
firebase에서 누군가를 특정할 수 있는 방법은 크게 topic과 token이 있다.
사실 다른 사람들은 어떻게 구분해서 쓰는지 모르겠지만
나같은 경우는
topic은 grouping 해서 보내는 값이기 때문에,
특정 이벤트나 공지로 쓰고 있고
token은 각각 개개인에게 보낼 수 있게 되어있기 때문에,
사용자의 event로 무언가 알림이 갈 일이 생기면 쓰고 있다.
condiction은 조건식 조합법이다.
자세한건 firebase 홈페이지에서 확인 가능하다.
public static void sendCommonMessage(String title, String body) throws IOException {
JsonObject notificationMessage = buildNotificationMessage(title, body);
System.out.println("FCM request body for message using common notification object:");
prettyPrint(notificationMessage);
sendMessage(notificationMessage);
}
private static void prettyPrint(JsonObject jsonObject) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
System.out.println(gson.toJson(jsonObject) + "\n");
}
prettyPrint는 로그용.
이제 웹에서 오는 신호를 받기 위해 Controller를 작성하면 서버단은 끝이다.
@RestController
@Slf4j
public class PushApiController {
/**
* 푸시 알람 요청
* @param title
* @param body
* @throws IOException
*/
@PostMapping("/fcm")
public ResponseEntity<?> reqFcm(
@RequestParam(required = true) String title,
@RequestParam(required = true) String body
) {
log.info("** title : {}",title);
log.info("** body : {}",body);
CommResponse res = new CommResponse();
try {
PushNotificationService.sendCommonMessage(title, body);
res.setCdResult(Constant.SUCCESS);
} catch(Exception e) {
log.error(e.getMessage());
res.setCdResult(Constant.FAILED);
res.setMsgResult("처리중 에러 발생");
return new ResponseEntity<>(res, HttpStatus.INTERNAL_SERVER_ERROR);
}
return ResponseEntity.ok(res);
}
}
그럼 API Test를 해보자.
여담이지만 PostMan 쓰다가 API Tester 로 넘어왔는데 괜찮은거같다. 크롬 확장프로그램.
200값이 return 되었으며 정상적으로 메세지가 보내진것이 확인된다.
아직 모바일쪽은 설명안했지만 알람 테스트용으로 스샷을 찍었다.
title과 body가 잘 들어간 푸쉬 확인가능 !
모바일 소스는 여기
2021.12.29 - [프로그래밍 공부/Android] - [firebase] notification / message
'프로그래밍 공부 > Spring Boot' 카테고리의 다른 글
[Slack] slack bot message (0) | 2022.10.19 |
---|---|
[java] spring boot oauth2 refresh token 고민 (0) | 2022.07.13 |
[Kotlin] java POI 를 이용한 write / download (0) | 2022.03.08 |
[Spring Boot/ldaps] AD 연동 (0) | 2020.07.24 |
[GraphQL] Spring Boot + 그래프QL 사용하기 (CRUD) (4) | 2019.07.08 |