티스토리 뷰

푸시 서비스는 메시지를 구글 클라우드 서ㅓ에서 구글 Play 스토어가 설치된 단말기로 보내주는 방식이다. 

이 푸시 서비스를 단말에서 연결을 유지하고 있기 때문에 사용하는 각각은 앱의 구글 클라우드 서버에서 직접 연결하지 않는다. 

 

만, 이 구글 서비스를 이용하지 않고 직접 구현하려면 단말에서 서버로 연결을 유지하며ㅓ 동시에 연결을 지속적으로 유지해야 한다. 이는 polling 매커니즘을 구현하여 일정 시간 간격으로 연결이 끊어졌는 지 확인해야 하는데, 이러한 매커니즘은 휴대폰의 리소스를 많이 잡아 먹는 문제가 있다. 

 

다음은 안드로이드에서 제공하는 FCM 푸시 메시지 처리 과정을 순서대로 나타낸 것이다. 

  1. 단말은 자신을 클라우드 서버에 등록하고 서버로부터 등록 ID 를 받는다.
  2. 등록 ID는 메시지 전송을 담당할 애플리케이션 서버로 보낸 후 메시지를 기다린다.
  3. 보내려는 메시지는 애플리케이션 서버에서 클라우드에 접속한 후 전송한다.
  4. 클라우드 서버로 전송된 메시지는 대상 단말에 보내진다.

 

예제를 한번 확인해보자.


우선, 새 프로젝트를 생성하고 해당 프로젝트에세 FCM Push 서비스를 사용하기 위해서는 여기에서 푸시 정보를 새로 등록해줘야 한다.

새 프로젝트 생성을 마치면, 해당 FCM 프로젝트에 사용할 Android 프로젝트를 등록해줘야 한다. 

등록할 프로젝트의 패키지 이름을 작성한 뒤, 

json 형식의 FCM Service 파일을 다운받아 위 디렉토리에 추가해주면 된다.

 

이제 FCM Push Service를 사용하기 위해 build.gradle을 아래와 같이 수정한다. google-services의 버전은 위 SDK 설정 페이지에 나와있다.

 

◎build.gradle

buildscript {
    repositories {
        // Check that you have the following line (if not, add it):
        google()  // Google's Maven repository

    }
    dependencies {
        // Add this line
        classpath 'com.google.gms:google-services:4.3.10'

    }
}
plugins {
    id 'com.android.application'
}

apply plugin: 'com.google.gms.google-services'

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.example.samplepush"
        minSdk 16
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.4.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    implementation platform('com.google.firebase:firebase-bom:29.3.1')
    implementation 'com.google.firebase:firebase-messaging'
    implementation 'com.google.firebase:firebase-analytics'
}


이제 FCM Push를 사용하기 위한 기본 설정이 끝났다. FCM을 사용하려면 앱 프로젝트 안에 두 개의 서비스를 만들어야 한다. 

 

New - Service - Service 를 클릭하여 MyFirebaseMessagingService 를 생성한다. 

 

◎MyFirebaseMessagingService.java

package com.example.samplepush;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

import java.util.Map;

public class MyFirebaseMessagingService extends FirebaseMessagingService {
    private static final String TAG = "FMS";

    public MyFirebaseMessagingService() {
    }

    @Override
    // 새로운 토큰을 확인했을 때 호출되는 메서드
    public void onNewToken(String token) {
        super.onNewToken(token);

        Log.d(TAG, "onNewToken 호출됨 : " + token);
    }

    @Override
    // 새로운 메시지를 받았을 때 호출되는 메서드
    // 푸시 메시지를 받았을 때, 그 내용을 확인한 후 엑티비티 쪽으로 보내는 메서드 호출
    public void onMessageReceived(@NonNull RemoteMessage message) {
        super.onMessageReceived(message);

        Log.d(TAG, "onMessageReceived 호출됨.");

        // 어디에서 메시지를 전송한 것인지 발신자 코드를 확인할 수 있다.
        String from = message.getFrom();
        Map<String, String> data = message.getData();
        String contents = data.get("contents");

        Log.d(TAG, "from : " + from + ", contents : " + contents);

        sendToActivity(getApplicationContext(), from, contents);
    }

    // 엑티비티 쪽으로 데이터를 보내기 위해 인텐트 객체를 만들고 startActivity() 메서드 호출
    private void sendToActivity(Context context, String from, String contents) {
        Intent intent = new Intent(context, MainActivity.class);
        intent.putExtra("from", from);
        intent.putExtra("contents", contents);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);

        context.startActivity(intent);
    }

}

처음 서비스 생성 시 상속하고 있던 Service 객체를 FirebaseMessagingService로 변경하고, onBind() 메서드를 삭제했다. 

FirebaseMessagingService 객체도 마찬가지로 서비스 클래스이며 푸시 메시지를 전달 받는 역할을 담당한다. 

 

구글 클라우드 서버에서 보내오는 메시지는 이 클래스에서 받을 수 있으며, 메시지가 도착하면 onMessageReseived() 메서드가 호출된다. 

 

onNewToken() 메서드는이 앱이 Firebase 서버에 등록되었을 때 호출된다. 파라미터로 전달받는 토큰의 정보는 이 앱의 등록 id를 의미하며, 해당 id는 이 단말로 메시지를 전달하고 싶은 쪽에서 이 등록 id를 사용할 수 있다. 

 

이제 Manifest 파일에서 이 서비스가 인텐트 필터를 가지도록 수정해야 한다.

 

◎AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.samplepush">

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

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.SamplePush">
        <service
                android:name=".MyFirebaseMessagingService"
                android:enabled="true"
                android:exported="true"
                android:stopWithTask="false">
                <intent-filter>
                    <action android:name="com.google.firebase.MESSAGING_EVENT"/>
                </intent-filter>

        </service>

        <activity
                android:name=".MainActivity"
                android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

FirebaseMessagingService에서 참조할 regID와 푸시 메시지를 화면에 띄우기 위한 MainActivity를 구성한다.

 

◎MainActivity.java

package com.example.samplepush;

import android.content.Intent;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.installations.FirebaseInstallations;
import com.google.firebase.messaging.FirebaseMessaging;

public class MainActivity extends AppCompatActivity {

    TextView textView;
    TextView textView2;

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

        textView = findViewById(R.id.textView);
        textView2 = findViewById(R.id.textView2);

        // 등록 ID 확인을 위한 리스너
        FirebaseMessaging.getInstance().getToken()
                .addOnCompleteListener(new OnCompleteListener<String>() {
                    @Override
                    public void onComplete(@NonNull Task<String> task) {
                        if (!task.isSuccessful()) {
                            Log.w("Main", "토큰 가져오는 데 실패함", task.getException());
                            return;
                        }

                        String newToken = task.getResult();
                        println("등록 id : " + newToken);
                    }
                });

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String instanceId = String.valueOf(FirebaseInstallations.getInstance().getId());

                println("확인된 인스턴스 id : " + instanceId);
            }
        });
    }

    public void println(String data) {
        textView2.append(data + "\n");
        Log.d("FMS", data);
    }

    @Override
    protected void onNewIntent(Intent intent) {
        println("onNewIntent 호출됨");
        if (intent != null) {
            processIntent(intent);
        }

        super.onNewIntent(intent);
    }

    private void processIntent(Intent intent) {
        String from = intent.getStringExtra("from");
        if (from == null) {
            println("from is null.");
            return;
        }

        String contents = intent.getStringExtra("contents");
        println("DATA : " + from + ", " + contents);
        textView.setText("[" + from + "]로부터 수신한 데이터 : " + contents);
    }
}

FirebaseMessaging.getInstance() 메서드를 호출하여 해당 인스턴스를 참조한 뒤, addOnSuccessListener() 메서드를 이용하여 리스너를 등록해야 등록 ID가 반환되었을 때 onComplete() 메서드가 호출되어 ID 값을 확인할 수 있다. 

 

또한 MainActivity에서는 MyFirebaseMessagingService가 보낸 Intent객체를 받아서 해당 푸시 메시지를 화면에 띄워주도록 구성한다.


이제 푸시 메시지를 보내줄 새로운 앱을 하나 구성한다. 이는 웹 통신으로 reqeustData를

https://fcm.googleapis.com/fcm/send

 에 보내주기만 하면 되는 거라 앱을 구성해도 되고, java나 다른 언어를 사용하여 구성하여도 무방하다. 보통은 서버단에서 해당 푸시를 보내줘야 하기 때문에 어플로 구성하지는 않는 거 같다.

 

대충 메시지를 입력하고 전송 버튼을 눌러 구글 클라우드 서버에 전송하는 앱을 구성을 하려고 한다. 

http 통신을 위한 volley 라이브러리를 추가한다.

 

◎build.gradle

// https://mvnrepository.com/artifact/com.android.volley/volley
implementation group: 'com.android.volley', name: 'volley', version: '1.2.1'

 

MainActivity는 아래와 같이 구성한다.

 

◎MainActivity.java

package com.example.samplepushsend;

import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import com.android.volley.*;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;
import org.json.JSONArray;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.Map;

public class MainActivity extends AppCompatActivity {

    EditText editText;
    TextView textView;

    static RequestQueue requestQueue;
    static String regId = "eU0FbkOWRCyz5MrdrJuWle:APA91bG9Be8epmtXTOJrEJtankeK5nbzOVaTUHVbt6UQjcmgmb4aeFGBk7Nkn47HI0TNR7zCp3rQzMKEhBO9Vy9Sj2I4j4CruJZ94yKAbcC2Nf0kn7VRcxxHKmrBPrj7wwj7xY-P_Yb8";

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

        editText = findViewById(R.id.editText);
        textView = findViewById(R.id.textView);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String input = editText.getText().toString();
                send(input);
            }
        });

        if (requestQueue == null) {
            requestQueue = Volley.newRequestQueue(getApplicationContext());
        }

    }

    public void send(String input) {
        JSONObject requestData = new JSONObject();

        try {
            requestData.put("priority", "high");

            JSONObject dataObj = new JSONObject();

            dataObj.put("contents", input);
            requestData.put("data", dataObj);
            JSONArray idArray = new JSONArray();

            idArray.put(0, regId);
            requestData.put("registration_ids", idArray);
        } catch (Exception e) {
            e.printStackTrace();
        }
        sendData(requestData, new SendResponseListener() {

            @Override
            public void onRequestCompleted() {
                println("onRequestCompleted() 호출됨.");
            }

            @Override
            public void onRequestStarted() {
                println("onRequestStarted() 호출됨.");
            }

            @Override
            public void onRequestWithError(VolleyError error) {
                println("onRequestWithError() 호출됨.");
            }
        });
    }

    public interface SendResponseListener {
        public void onRequestStarted();
        public void onRequestCompleted();
        public void onRequestWithError(VolleyError error);
    }

    public void sendData(JSONObject requestData, final SendResponseListener listener) {
        JsonObjectRequest request = new JsonObjectRequest(

            Request.Method.POST, "https://fcm.googleapis.com/fcm/send", requestData, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                listener.onRequestCompleted();
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                listener.onRequestWithError(error);
            }
        }) {
            @Override
            protected Map<String, String> getParams() throws AuthFailureError {
                Map<String, String> params = new HashMap<String, String>();
                return params;
            }

            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {
                Map<String, String> headers = new HashMap<String, String>();
                headers.put("Authorization", "key=AAAAcigofdU:APA91bHYKr-nb8x2T-JhnneiTfla_VFDz75HXCYa4JowTXhLlk748awqkt13ke1nN625QM2mVl3UdZXTpWmxhKWJUsP9YT78QgzBEyw25DEIZxUOeiWDhymguo7BQMzecN6E3-nGGmeB");
                return headers;
            }

            @Override
            public String getBodyContentType() {
                return "application/json";
            }
        };

        request.setShouldCache(false);
        listener.onRequestStarted();
        requestQueue.add(request);
    }

    public void println(String data) {
        textView.append(data + "\n");
    }

}

전송하기 버튼을 누르면 입력 상자에 담겨있는 데이터를 담아 send() 메서드를 호출한다. 

 

send() 는 JsonObject를 하나 생성하여 푸시 메시지로 보낼 데이터들을 담으며, 이 때 푸시 메시지를 받을 앱의 RegID를 registration_ids key에 담아 보내는데, 보통의 경우는 푸시 메시지를 받을 어플에서 메시지를 보내는 어플에게 regID를 보내주지만 따로 해당 로직을 구현하지 않았기 때문에 정적으로 선언해주었다. 

 

JsonObject를 생성한 뒤 sendData() 메서드를 호출하여 구글 FCM 서비스로 JsonObject를 전송하게 된다. 

 

이 때 생성한 인터페이스인

SendResponseListener

 를 재정의하여 인자로 넣어주는데, 해당 인터페이스의 콜백 메서드들은 http Response의 상태에 따라 호출된다.

 

다음으로 JsonObjectRequest객체의 header에 해당 FCM service의 Authorization Key를 넣어 요청을 보내주기만 하면 끝이 난다. 


Comments