FCM를 사용하여 Android 폰으로 알림 전송하기 (feat. Spring Boot, Android Studio)

728x90

 



개요


본 글에서는 Spring Boot와 Android Studio를 사용하여 서버에서 안드로이드 스마트폰으로 알림을 전송하는 간단한 웹 어플리케이션을 만들어 본다. 

글을 쓰게 된 계기


서버를 만들다 보면 웹 뿐만 아니라 앱에 알림을 보내는 기능을 만들어보고 싶은 때가 있다. FCM은 Android와 IOS에 통합적으로 알림을 보낼 수 있는 서비스를 제공하므로 FCM을 이용하면 간편하게 알림을 보낼 수 있다. 문제는 Firebase는 입문하는 과정에서 정보를 얻기가 참 어렵다는 점이다. 공문서를 뒤져봐도 공문서 내용의 Android 버전이 예전 버전인 경우가 많아 바로 적용하기 어려운 부분이 있었다. 

결국 알림을 원하는 대로 전송할 수 있었지만, 정보를 찾기 어려워서 상당한 시간을 소요한 것에 아쉬움이 느껴졌었다. 비슷한 경험을 하실 분들에게 도움이 되고자 글을 남긴다.

Requirement

1. Firebase 계정

Firebase 계정이 있어야 한다. 계정이 없다면 Firebase 계정을 생성하자. 구글 id가 있다면 쉽게 가입할 수 있다.

2. Spring Boot 

Spring Boot로 Dependency 설정 및 Configuration을 통해 외부 API를 사용해본 적이 있다면 충분히 따라할 수 있다.

본 글은 Spring Boot 3 버전을 사용한다.

3. Android Studio (Java)


Android Studio를 사용한 적이 있다면 구현이 좀 더 수월할 것이다. 하지만 Android Studio 사전 지식이 없더라도 Java만 알고 있다면 충분히 따라할 수 있다. UI는 작성하지 않고 그저 알림만 받는 어플리케이션이기 때문에 아주 약간의 XML 수정만 필요하기 때문이다.

본 글에서는 Android Studio Giraffe 2022.3.1 Patch를 사용한다. 


Firebase 

프로젝트 생성

firebase 콘솔로 이동하여 프로젝트를 생성한다. 



테스트 용도로 만드는 프로젝트기 때문에 구글 애널리틱스는 끄고 프로젝트 만들기 실행

 



잠시 대기 후 프로젝트가 생성 완료되면 아래와 같은 화면에 진입한다.

 

 

프로젝트 설정


프로젝트 개요 옆에 있는 톱니바퀴 모양을 눌러 프로젝트 설정으로 들어간다.

Server Side Setting - Firebase Admin SDK


로컬 코드에서 Firebase 기능을 사용하려면 Firebase Admin SDK가 필요하다. 대부분의 cloud 서비스가 그렇듯이 Firebase에서도 클라우드 접근을 위한 API Key 생성을 할 수 있다. 다음과 같이 실행한다.

프로젝트 설정 -> 서비스 계정 -> Firebase Admin SDK -> 새 비공개 키 생성


비공개 키를 생성하면 자동으로 .json 확장자의 파일이 다운로드된다. 해당 정보는 Firebase에 접근할 수 있는 권한을 부여하므로 **절대 공개 저장소에 저장하지 않도록 한다.**

Client Side Setting - 


안드로이드 디바이스에서 Firebase를 연결하려면 Firebase 앱을 생성하여 google-service.json을 다운로드 받아야 한다.

프로젝트 개요 화면에서 앱을 추가한다. 아래 화면에서 안드로이드 모양 버튼을 누른다.

 



패키지 이름을 임의로 정하여 등록한다. 다만 해당 패키지 이름은 추후 Android Studio에서 프로젝트를 생성할 때 **동일하게 패키지 이름을 사용해야한다.**

 



구성 파일을 다운로드하여 저장해둔다. **해당 파일은 공개 저장소에 올라가지 않도록 주의한다.**

 


Firebase SDK 추가 부분은 일단 넘어가고 설정을 완료한다.

 


앱 생성을 완료하면 프로젝트 설정에 들어갔을 때 아래와 같이 '내 앱'이 추가된 것을 확인할 수 있다.

 

 

Server side - 알림 전송 코드 작성

프로젝트 생성 및 json 파일 옮기기

spring initializer로 프로젝트 생성


spring boot 프로젝트를 생성한다.

admin sdk를 프로젝트 폴더에 포함시키기


"Server Side Setting - Firebase Admin SDK"에서 다운로드 받은 json파일을 프로젝트 루트 디렉토리에 포함시킨다. 

본 글쓴이는 파일명을 "sample-firebase-admin-sdk.json"으로 교체했다.



(만약 git을 사용한다면, 해당 파일을 .gitignore에 반드시 포함시키자)

Configuration

build.gradle


build.gradle에 Firebase 의존성을 추가한다.

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'com.google.firebase:firebase-admin:9.1.1'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}



application.properties


application.properties에 아래와 같이 설정을 추가한다. 

firebase.config에는 현재 루트 디렉토리에 있는 (Firebase 콘솔에서 다운로드 받은) firebase admin sdk의 파일명을 그대로 작성한다.

project id는 Firebase 콘솔에서 프로젝트 설정으로 들어가면 확인할 수 있다.

# Firebase setting
firebase.config=sample-firebase-admin-sdk.json
firebase.project.id=sample-7667c



Firebase config class


conguration 파일을 생성한다.

 

@Configuration
public class FirebaseConfig {

    @Value("${firebase.config}")
    private String firebaseConfig;
    @Value("${firebase.project.id}")
    private String firebaseProjectId;

    @PostConstruct
    public void initialize() {
        try {
            InputStream serviceAccount = new FileInputStream(firebaseConfig);
            FirebaseOptions options = new FirebaseOptions.Builder()
                    .setCredentials(GoogleCredentials.fromStream(serviceAccount))
                    .setProjectId(firebaseProjectId)
                    .build();

            if (FirebaseApp.getApps().isEmpty()) {
                FirebaseApp.initializeApp(options);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 



application.properties에 있는 변수를 Spring의 Value 애노테이션을 사용하여 가져온다.

해당 실습은 Firebase 설정을 위해 파일을 통째로 가져와야 하므로 FileInputStream을 사용하여 통째로 정보를 가져온다. GoogleCredentials를 사용하면 stream을 통해 Credential을 추출할 수 있고, 추출한 Credential을 포함한 Firebase option을 통해 FirebaseApp을 초기화하여 Spring 프로젝트에서 사용할 수 있다.

Service 코드 생성


Firebase message 전송을 위한 서비스 코드를 작성한다.

 

@Service
public class FirebaseService {

    public String sendNotification(String content, String token) {
        try {
            Message message = Message.builder()
                    .setToken(token)
                    .putData("title", content)
                    .build();

            String response = FirebaseMessaging.getInstance().send(message);
            // return response if firebase messaging is successfully completed.
            return response;

        } catch (FirebaseMessagingException e) {
            e.printStackTrace();
            return "Failed";
        }
    }
}

 


Message 전송을 위해 Message 인스턴스를 생성할 때 토큰을 넣어줘야 하는데 이 토큰이 무엇을 의미하는지 아래 Client side 구현에서 자세히 알아볼 것이다. ("Client side - 토큰 생성"의 "토큰 생성 코드 작성" 부분 참고)

FirebaseMessaging이 성공적으로 메시지 전송을 마치면 전송 결과과 response로 돌아온다.

이렇게 서비스 코드 작성을 완료하면 해당 알림 전송 기능이 필요한 곳에 "sendNotification" 메서드를 삽입하여 사용하면 된다.

Client side - 토큰 생성

프로젝트 생성 및 json 파일 옮기기

프로젝트 생성


android studio을 실행하여 새 Activity를 생성한다.



이 때 주의할 점은 Empty Views Activity를 선택해야 한다는 점이다. Empty Activity를 선택하면 Java를 사용할 수 없게 된다.



프로젝트 생성 시 패키지이름은 Firebase 콘솔에서 사용한 패키지 이름과 동일하게 작성해야 한다.

 



language를 java로 선택하고 API는 임의로 API 22를 선택하여 진행한다. Build configuration 언어는 Groovy DSL을 선택한다.

프로젝트 생성이 완료되면 아래와 같이 MainActivity와 Manifest가 자동적으로 생성되어있을 것이다.

 

앱 Firebase SDK 옮기기


Firebase 콘솔에서 앱을 생성할 때 다운로드 받은 "google-service.json"파일을 프로젝트 폴더에 옮겨야 한다.

아래처럼 IDE의 디렉토리 뷰를 프로젝트로 전환하고 app 폴더 아래에 다운로드 받았던 "google-service.json"을 넣는다.

Configuration


client side에서는 dependency 설정이 좀 더 까다롭다. build.gradle에 project레벨이 있고, app 레벨이 있는데 두 군데 모두 dependency를 추가해줘야 한다. 

다시 디렉토리 뷰를 android로 교체하여 진행하면 build.gradle을 찾기 수월할 것이다.

 




Firebase 앱 생성시 안내문에 나온대로 실행하면 된다.

 

 

build.gradle(prject 레벨)

plugin에 google-services 의존성을 추가한다.

plugins {
	id 'com.android.application' version '8.1.1' apply false
	id 'com.google.gms.google-services' version '4.3.15' apply false
}

build.gradle(app 레벨)


plugin에 google-services를 추가한다.

plugins {
    id 'com.android.application'
    id 'com.google.gms.google-services'
}



dependency에 firebase-bom 의존성을 추가해준다.

여기에 추가로 우리는 Firebase의 messaging 기능을 사용할 것이므로 firebase-messaging 의존성도 추가해줘야 한다.

둘 다 추가하면 아래와 같이 될 것이다.

 

dependencies {
    implementation platform('com.google.firebase:firebase-bom:32.2.3')
    implementation 'com.google.firebase:firebase-messaging:23.2.1'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

 

AndroidManifest.xml 파일 설정


AndroidManifest.xml의 manifest 내부에 아래 코드를 추가하여 인터넷 사용을 허용하게 한다.

<uses-permission android:name="android.permission.INTERNET"/>

 

 

토큰 생성 코드 작성


특정 안드로이드 디바이스에 FCM을 통한 알림을 보내려면 해당 디바이스의 토큰을 서버에서 알고 있어야 한다. 

FirebaseMessaging을 사용하면 FCM 메시지에 필요한 토큰을 생성할 수 있다. 다음과 같이 onCreate() 메서드에 작성하여 앱이 생성될 때 즉시 Firebase messaging token을 생성하도록한다.

 

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FirebaseMessaging.getInstance().getToken()
                .addOnCompleteListener(task -> {
                    if (!task.isSuccessful()) {
                        Log.w("FCM Log", "Fetching FCM registration token failed", task.getException());
                        return;
                    }
                    String token = task.getResult();
                    Log.d("FCM Log", "Current token: " + token);
                });
    }
}



토큰 생성 Test


중간 테스트로 토큰이 정상적으로 생성되는지 테스트 해본다.

Android studio로 android device에 연결한 후 실행을 한다. (virtual device를 생성해도 되지만, 알림 기능 테스트인 만큼 physical device에 연결하여 테스트하는 것을 추천한다.)

앱을 실행하면 UI는 특별히 건든 것이 없으므로 화면 중앙에 Hello world!가 뜰 것이다. 

더 중요한 것은 토큰이 생성 되는지 여부다. Android studio의 Logcat을 확인하여 Token이 정상적으로 생성되었는지 확인한다. 아래와 같이 log가 기록되었다면 토큰이 정상적으로 생성된 것이다.

Client side - Message receive 코드 작성하기

Service 코드 생성


서버 쪽에서 FCM을 통해 메시지를 전송하므로 클라이언트 쪽에서는 FCM을 통해 메시지를 받는 코드가 필요하다. 아래와 같이 MyFirebaseMessagingService 를 만들어 코드를 작성한다.

각 코드에 대한 설명은 코드 아래에서 설명한다.

public class MyFirebaseMessagingService extends FirebaseMessagingService {

    private static final String NOTIFICATION_CHANNEL_ID = "sample.noti.app";
    private static final String NOTIFICATION_CHANNEL_NAME = "Notification";
    private static final String NOTIFICATION_CHANNEL_DESCRIPTION = "notification channel";

    @Override
    public void onNewToken(@NonNull String token) {
        Log.d("FCM Log", "Refreshed token: " + token);
    }

    @Override
    public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        if (remoteMessage.getData().size() > 0) {
            String content = remoteMessage.getData().get("content");
            showNotification(content);
        }
    }

    private void showNotification(String content) {
        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel(notificationManager);
        }

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
                .setAutoCancel(true)
                .setDefaults(Notification.DEFAULT_ALL)
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setContentTitle("A new post you might be interested in.")
                .setContentText(content)
                .setContentInfo("Info");

        notificationManager.notify(new Random().nextInt(), notificationBuilder.build());
    }

    private void createNotificationChannel(NotificationManager manager) {
        NotificationChannel notificationChannel = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationChannel = new NotificationChannel(
                    NOTIFICATION_CHANNEL_ID,
                    NOTIFICATION_CHANNEL_NAME,
                    NotificationManager.IMPORTANCE_DEFAULT
            );
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationChannel.setDescription(NOTIFICATION_CHANNEL_DESCRIPTION);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            manager.createNotificationChannel(notificationChannel);
        }
    }
}



0. NOTIFICATION CHANNEL 전역 변수


Android의 Oreo 이상 버전에서는 Notification Channel을 만들어 알림을 관리하게 되는데, 이 채널을 만들기 위해 채널 id와 채널 이름이 필요하다. (Description은 없어도 상관없다.) 채널 생성에 필요한 변수인데, 개발자가 임의로 혹은 내부 컨벤션에 따라 작성하면 된다.

1. onNewToken()


토큰이 새로 생성되었을 때 로그를 남기는 역할을 한다.

2. onMessageReceived()


FCM을 통해 디바이스에 메시지가 도착하면 동작하는 메서드다. 여기서는 메시지의 데이터("content")를 꺼내서 showNotification()메서드에 넘겨주는 역할을 한다.

3. showNotification()

실질적으로 디바이스에 알림을 뜨게 하는 역할을 한다.

NotificationCompat을 통해 Notification을 생성하고 Notification Manager로 넘겨서 알림을 동작시킨다. 

setContentTitle()에는 디바이스 알림 탭의 윗 부분에 뜨는 문구를 설정하게 해준다. 여기선 임의로 새 글 알림을 알리는 메시지 문구를 작성했다.

setContentTxt()에는 디바이스 알림 탭 아랫 부분에 뜨는 문구다. 서버에서 보낸 알림 메시지가 뜨게 된다.

4. createNotificationChannel()

앞서 언급했듯이 Android Oreo 이상 버전에서는 Notification Channel을 만들어 알림을 관리하게 된다. Notifcation Channel을 만들기 위한 설정 코드라고 보면 된다.

AndroidManifest.xml 수정


MyFirebaseMessagingService를 service 태그를 하용하여 Manifest에 등록해야 한다. 아래 코드를 manifest 태그 안에 추가한다.

 

<service android:name=".MyFirebaseMessagingService" android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
</service>

 



Messaging 전송 Test


여태 작성한 코드를 바탕으로 테스트를 진행한다.

Server side - test용 controller 생성


test를 하기 앞서 test 용도의 controller를 생성한다. 보통 알림 기능은 서비스의 다른 기능과 결합하여 함게 동작하기 때문에 실제 서비스에서는 이런 controller 생성이 필요없다. 해당 controller는 오로지 테스트 용도로만 활용되기 때문에 이렇게 분리하여 서술하였다. 테스트가 완료되어 정상동작이 확인되면 삭제해도 무방하다. 

Dependency 추가


@RestController를 사용하기 위해 build.gradle에 Spring web 의존성을 추가한다.

Dto를 손쉽게 만들기 위해 lombok 의존성도 추가해준다.

 

테스트 용도 Dto 생성

Token과 content를 보내기 위한 Dto를 생성한다.

 

@Getter
public class NotiDto {
    private String token;
    private String content;
}

 

테스트 용도 Controller 생성



아래와 같이 @RestController를 사용하여 서버에서 알림 요청을 임의로 일으키기 위한 코드를 작성한다.

 

@RestController
@RequestMapping("/")
@AllArgsConstructor
@Slf4j
public class FcmTestController {

    private final FirebaseService firebaseService;

    @PostMapping
    public void createNotification(@RequestBody NotiDto notiDto){
        String response = firebaseService.sendNotification(notiDto.getToken(), notiDto.getContent());
        log.info(response);
    }

}

 



이전에 작성한 service의 메서드를 여기서 불러와 사용하면 된다.

@Slf4j도 추가하여 response를 log에 남겨서 확인하도록 한다.

POSTMAN으로 테스트


이제 본격적으로 만든 앱을 테스트 할 시간이다.

안드로이드 앱을 실행하고, 서버도 실행한 후 POSTMAN을 동작시켜 POST 요청을 보내도록 하자. 보내는 데이터는 아래처럼 token과 content를 JSON 형식으로 보내면 된다.

token에는 Android Studio의 logcat에서 보여지는 token을 그대로 사용하면 된다.

 



send를 누르고 잠시 기다리면 휴대폰 디바이스에 알림이 도착할 것이다. 처음 보내는 요청이라면 메시징이 전달되는데에 시간이 조금 소요될 수 있다. (1분 정도 걸리는 경우도 있었다.)

디바이스에 아래처럼 알림이 도착하면 성공한 것이다.

 

글을 마치며


실제로는 이처럼 Android studio에서 log를 확인하여 직접 token을 입력하여 요청을 보내는 일은 없을 것이다. 단순히 FCM이 동작하는지 확인하기 위한 코드기 때문에 실제 앱에서는 적용하기 어려운 부분이 있다.

실제 앱처럼 동작하게 하려면 앱이 생성되는 동시에 서버에 token을 전달하여 사용자 정보와 함께 DB에 저장하면 된다. 문제는 token이 생성됨과 동시에 서버에 해당 token 정보를 날리려면 서버의 ip에 직접 찌르는 요청을 보내야 한다.

아래 ChatGPT의 설명을 확인해보자.

 


이처럼 Android 앱에서 작성된 코드의 localhost는 당연히 디바이스 내부 통신을 가리키는 것이라서 서버에 접근하려면 localhost가 아닌 서버의 ip에 데이터를 전송해야한다.

문제는 외부 기기가 서버 컴퓨터의 ip에 접근하려면 window port의 방화벽 설정을 바꾸는 작업이 필요하다는 것이다. 테스트를 위해 컴퓨터의 방화벽을 건드는 것은 여간 찜찜한 일이 아닐 것이다. 

그럴 바에 cloud 서비스에 직접 배포하여 테스트하는 것이 나을 수 있다. 

AWS에 서버를 배포하여 Android에서 생성된 토큰을 전달하여 DB에 기록하고 서버 측에서 알림이 생성되면 DB에서 사용자의 디바이스 토큰을 추출하여 디바이스에 알림을 보내는 것까지 실습해 볼 것이다. 

또한 알림 서버를 배포하려면 Firebase admin SDK가 필요한데, 이 SDK는 공개 저장소에 올라가면 안 되므로 AWS Secret Manager를 통해 .json 파일을 관리하는 방법까지 알아볼 것이다.