푸시 서비스 사용하기
안드로이드 스마트폰을 사용하다 보면 Play 스토어나 앱 마켓에서 설치한 앱의 업데이트가 있다는 알람이 종종 울린다. 단말의 위쪽 부분에 보이는 상태 바 부분에 메시지가 표시되면 사용자는 업데이트 여부를 결정하게 된다.
업데이트 메시지는 어떻게 표시되는 걸까?
단말로 메시지를 보내는 방법에는 세 가지 방법이 있다.
단순 SMS를 이용한 알림 -> 간단하지만 비용이 발생할 수 있다.
- 앱과 서버를 연결한 알림 -> 앱에서 서버와의 연결을 만들어놓고 폴링하는 과정이 필요하며 백그라운드 서비스를 이용해 연결을 유지 해야 하므로 간단하지 않다.
- 구글의 푸시 서비스(FCM)를 사용하여 알림 -> 구글의 클라우드 서비스를 사용해 메시지 전송 방식을 최적화한 서비스 방식 -> 앱에서 서버로 직접 연결할 필요가 없으며 단말의 내부 연결을 공유하여 메시지를 수신하는 방식
세 번째 푸시 서비스는 FCM(Firebase Cloud Messaging) 이라고 불린다.
FCM은 앱 사용자에게 포그라운드 상태 (앱 실행 중)나 백그라운드(앱 실행 x) 상태나 알림을 보낼 수 있다.
푸시 서비스란 ‘업데이트가 있습니다.’ 와 같은 메시지를 구글 클라우드 서버에서 구글 Play 스토어가 설치된 단말기로 보내는 방식이다.
이 푸시 서비스를 사용하는 각각의 앱은 구글 클라우드 서버에 직접 연결하지 않는다. 그 이유는 단말에서 연결을 유지하고 있기 때문이다.
만약 위 구글 서비스를 사용하지 않고 직접 구현하려면 단말에서 서버로 연결을 유지하면서 동시에 연결을 지속적으로 유지해야 한다. 따라서 일정 시간 간격으로 연결이 끊어졌는지 검사하는 Polling 메커니즘을 구현해야한다
하지만 폴링 기능을 구현하게 되면 그 과정에서 단말의 하드웨어 리소스나 전원을 많이 소모하는 문제가 발생하게 된다. 결국 구글에서 제공하는 FCM을 사용하는 것이 효과적으로 푸시 메시지를 보낼 수 있는 방법이 된다.
FCM 푸시 메시지 처리 과정
1. 단말은 자신을 클라우드 서버에 등록하고 서버로부터 등록 id를 받는다. 2. 등록 ID는 메시지 전송을 담당할 애플리케이션 서버로 보낸 후 메시지를 기다린다. 3. 보내려는 메시지는 애플리케이션 서버에서 클라우드에 접속한 후 전송한다. 4. 클라우드 서버로 전송된 메시지는 대상 단말에 보내진다.
기본적으로 푸시 메시지를 받으려면 클라우드 서버에 자신의 단말이 등록되어 있어야 한다. 단말 등록 과정에서 등록 id를 받게 되고 푸시 메시지를 보내는 쪽에서 상대방의 등록 id를 사용해야 메시지를 보낼 수 있다. 등록 id는 전송 허가서와 같은 역할을 하는셈이다.
보통 웹 서버에 푸시 메시지를 전송하는 기능을 추가하여 구성하는 경우가 많다.
이 어플리케이션 서버는 직접 단말로 메시지를 보낼 수 없으므로 클라우드 서버를 통해 보내게 된다. 즉, 어플리케이션 서버 -> 클라우드서버 -> 단말 형태를 띠게 된다.
위 과정은 다음과 같은 두 가지 내용을 알아야한다.
1. 어플리케이션 서버에 저장된 단말의 등록 id 2. 어플리케이션 서버에서 클라우드 서버로 접속하기 위한 인증 정보
첫 번째는 단말의 등록 id이다.
푸시 메시지를 보내기 위해서는 어플리케이션 서버가 어떤 단말로 메시지를 보내 줄 것인지에 대한 정보가 필요하게 된다.
이 등록 id는 등록한 단말별로 고유한 값이 되므로 어플리케이션 서버에 저장되며, 클라우드 서버가 메시지를 보내주어야 할 단말을 구분하는데 사용된다.
두 번째로 클라우드 서버로 접속하기 위한 인증 정보이다.
메시지를 전송할 때도 아무나 접속하여 단말 메시지를 보내면 안되므로 애플리케이션 서버가 API 키라는 고유한 값을 포함하여 메시지를 보내도록 한다.
이 인증 정보는 어떤 사람이 어떤 서비스에 사용하는지 구별하기 위한 것이므로 개발자가 만드는 애플리케이션을 FCM 사이트에 등록해야 사용할 수 있다.
예제
FCM 설정 페이지에서 프로젝트 만들기
FCM을 사용하려면 FCM 설정 페이지에서 프로젝트를 새로 만들어야 한다.
FCM은 여러 구글 서비스 중 하나인 Firebase에 통합되어 있어 Firebase 개발자 콘솔 페이지에서 새로운 프로젝트를 만들거나 설정할 수 있다.
아래 홈페이지에 들어가 프로젝트를 만들어주자.
안드로이드 아이콘을 클릭
다운로드를 해서 프로젝트의 app 폴더에 넣어준다.
이렇게 하면 성공적으로 프로젝트가 만들어진다.
인제 FCM 사용을 위해 추가 설정을 해주자.
build.gradle(Project:SamplePath)
1
2
3
dependencies {
classpath 'com.google.gms:google-services:4.3.10'
}
위의 클래스 패치를 추가해 준다.
build.gradle(Module:SamplePath)
1
2
3
4
5
plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
}
1
2
3
4
dependencies {
implementation platform('com.google.firebase:firebase-bom:29.0.4')
implementation 'com.google.firebase:firebase-analytics'
implementation 'com.google.firebase:firebase-messaging:21.1.0'
플러그인과 의존성을 추가해준다.
위의 29.0.4 같은 숫자들은 프로젝트를 언제 만들었는지에 따라 달라지므로 개발자 콘솔 페이지에 있는 설명글을 확인해서 넣어주자.
이렇게 하면 FCM를 사용할 때 필요한 기본적인 설정이 끝난다.
FCM을 사용하려면 앱 프로젝트 안에 두 개의 서비스를 만들어야 한다.
첫 번째 서비스 파일을 만들어 보자.
프로젝트 창의 왼쪽 프로젝트 영역에서 app를 선택한 후 오른쪽 마우스를 클릭 [New -> Service -> Service] 메뉴를 누른다. 대화 상자가 표시되면 Class Name 입력 상자에 MyFirebaseMessagingService를 입력한다.
MyFirebaseMessagingService.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.techtown.push;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import java.util.Map;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
public MyFirebaseMessagingService() {
}
@Override
public void onNewToken(String token){ // 새로운 토큰을 확인했을 때 호출되는 메서드
super.onNewToken(token); // token은 앱의 등록 id를 의미
Log.e("FMS", "onNewToken 호출됨: " + token);
}
@Override // 푸시 메시지를 받았을 떄 그 내용 확인한 후 액티비티 쪽으로 보내는 메서드 호출
public void onMessageReceived(RemoteMessage remoteMessage){ // 새로운 메시지를 받았을 때 호출되는 메서드
Log.d("FMS", "onMessageReceived 호출됨.");
String from = remoteMessage.getFrom();
Map<String, String> data = remoteMessage.getData();
String contents = data.get("contents");
Log.d("FMS", 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);
}
}
위와 같이 수정한다.
구글 클라우드 서버에서 보내오는 메시지는 위 클래스에서 받을 수 있으며, 메시지가 도착하면 onMessageReceived
메서드가 자동으로 호출된다.
따라서 onMessageReceived
메서드를 재정의하면 구글 클라우드 서버에서 보내오는 메시지를 받아서 처리할 수 있다.
onNewToken
메서드는 이 앱이 Firebase 서버에 등록되었을 때 호출된다.
AndroidManifest.xml 파일을 열어 인텐트 필터를 갖도록 설정해주고 인터넷 권한을 추가해주자.
AndroidManifest.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.techtown.push">
<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>
인제 소스 코드에 단말 등록 기능을 추가하자.
MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package org.techtown.push;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.iid.FirebaseInstanceIdReceiver;
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);
FirebaseMessaging.getInstance().getToken() // 등록 id 확인을 위한 리스너 설정
.addOnCompleteListener(new OnCompleteListener<String>() {
@Override
public void onComplete(@NonNull Task<String> task) {
if(!task.isSuccessful()){
Log.d("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 view) {
Task<String> instanceId = FirebaseInstallations.getInstance().getId()
.addOnCompleteListener(new OnCompleteListener<String>() {
@Override
public void onComplete(@NonNull Task<String> task) {
if (task.isSuccessful()) {
Log.d("Installations", "Installation ID: " + task.getResult());
println("확인된 인스턴스 id: " + task.getResult());
} else {
Log.e("Installations", "Unable to get Installation ID");
}
}
});
}
});
}
public void send(String input){
}
@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("form is null");
return;
}
// 보낸 데이터는 contents 키를 사용해서 확인
String contents = intent.getStringExtra("contents");
textView.setText("[" + from + "]로부터 수신한 데이터: " + contents);
}
public void println(String data){
textView2.append(data + "\n");
}
}}
책에서는 인스턴스 등록 id 값을 확인할 때 FirebaseInstnaceId.getInstance().getId() 메서드를 사용 하라고 했는데 위의 메서드가 자꾸 인식 오류가 났다.
원인을 알아 본 결과 위의 방식은 예전 방식이여서 지원하지 않는다고 한다.. -_-
따라서 FirebaseInstallations.getInstance().getId()
로 확인해야 한다.
이렇게 하면 푸시 메시지를 받는 앱의 기능이 완성 되었다.
메시지 전송 앱 만들기
SamplePushSend라는 새 프로젝트를 만들어 준다.
메시지 전송할때 Volley 라이브러리를 사용할 것이므로 아래와 같이 라이브러리를 추가한다.
build.gradle(Module: SamplePushSend.app)
1
2
dependencies {
implementation 'com.android.volley:volley:1.2.0'
MainActivity.java를 수정해주자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package org.techtown.push.send;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.android.volley.AuthFailureError;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
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 = "Input Your Installation ID";
@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 객체 생성
JSONObject requestData = new JSONObject();
try {
requestData.put("priority", "high"); // 옵션 추가
// 전송 할 데이터 추가
JSONObject dataObj = new JSONObject();
dataObj.put("contents", input);
requestData.put("data", dataObj);
// 푸시 메시지를 수신할 단말 등록 ID를 JSONArray에 추가한 후 requestData 객체에 추가
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) {
// Volley 요청 객체를 만들고 요청을 위한 데이터 설정, 두 번째 매개 변수는 클라우드 서버의 요청 주소
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= Input Your API KEY");
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");
}
}
getHeader
메서드 안의 key값은 FCM 개발자 콘솔 페이지에서 확인할 수 있다.
프로젝트 설정 -> 서버 키 복사해서 넣으면 된다.
regId 변수의 값은 Firebase 클라이언트 식별자 id 값이다.
마지막으로 인터넷 권한을 추가해주면 끝난다.
AndroidManifest.xml
1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.techtown.push.send">
<uses-permission android:name="android.permission.INTERNET"/>