- 참고: 이 글은 iOS의 Push 기능을 사용하기 위한 인증서 및 Firebase Admin SDK Account key 파일이 준비되어있고, 푸시 알림을 수신할 기기의 token 을 알고있다는 가정하에 작성되었습니다.
목차
- (Smartphone) Push Notification 이란?
- Push Notification Flow
- Push Notification Service 를 위한 실시간 연결 상태
- APNs 로 Push Notification 전송 상세 흐름 살펴보기
- Java 에서 APNs 사용하여 Push Notification 전송 기능 구현하기
(Smartphone) Push Notification 이란?
스마트폰의 응용프로그램(App)이 서버로부터 정보를 수신했을때, 이를 사용자에게 실시간 알리는 기술을 의미합니다. 간단하게 App 제공자가 App 설치자에게 App의 변화를 알릴수 있는 실시간 메시지 입니다.
Push Notification Flow
아래 그림은 App Provider가 Push Notification 을 전송하기 까지 전체 과정을 잘 보여주고 있습니다. 먼저 유저 기기가 Apple or Google’s servers 에 Token 을 요청하여 Unique한 값의 Token 을 수신 하면, App 에서 해당 Token 값을 App Provider의 서버에 전송하여 Token 값을 저장하게 됩니다(등록). 그 후 유저 기기에 Push Notification 전송을 할때, Notification Message(Payload) 와 Device Token 값을 Apple or Google’s servers에 전송하면, Apple or Google’s servers 에서 Push Notification을 유저 기기에 전송하게됩니다.
Push Notification Service 를 위한 실시간 연결 상태
Mobile Push notification은 유저가 해당 서비스에 접속하지 않아도 정보를 이용할수 있도록 실시간 성으로 콘텐츠를 전송하는 서비스로, 콘텐츠를 전송하는 서버와 Device가 실시간 연결 상태여야만 가능한 서비스라고 볼수있습니다.
결국 Push Notification 을 수신하는 유저의 Device는 어떤 보안 과정을 거쳐 Device와 Apple or Google’s servers 간에 특정 포트를 통해 통신이 가능한 연결 상태가 성립되어있다고 유추할수 있습니다. (maintain a persistent connection)
Apple의 경우 각각의 iOS Device 는 Apple 이 직접 구현한 XMPP server 에 연결되어있는 XMPP client 가 되며, Push Notification 이 활성화된 iOS Device는 TCP 소켓을 사용해 포트 5223 를 통해 APNS service와 연결됩니다.
- TCP 포트 5223 : APN (Apple 푸시 알림 서비스)과의 통신용
- TCP 포트 2195 : APN에 알림 보내기
- TCP 포트 2196 : APN 피드백 서비스 용
- TCP 포트 443 : 기기가 포트 5223에서 APN에 도달 할 수없는 경우 Wi-Fi의 폴백 전용
APNs 로 Push Notification 전송 상세 흐름 살펴보기
Apple Push Notification(APN) Service 에 대한 Apple의 공식 문서를 보면서 살펴보겠습니다. (참고 : https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html)
1) APN ← → Device 영구적인 TLS 연결
각 Device 는 최초 활성화 시에 OS 에서 제공하는 cryptographic certificate (암호화 인증서) 및 private cryptographic key (개인 암호화 키) 가 Device Keychain 에 저장되어있습니다.
이 인증서와 암호화 키를 기반으로 APN은 아래 그림처럼 Device와의 연결을 인증하고 유효성 검사를 합니다.
이렇게 인증 및 유효성 검사를 완료하면 Device 와 APN 간에 인증되고, 암호화된 영구적인 IP 연결이 자동으로 설정됩니다. (TLS Connecion)
2) Push Notification 을 위한 App 별 Device Token 수신 및 App Provider에 전달
APN 과 Device 간에 TLS Connecion이 설정되면, App 은 APN 에 등록하여 App 별 Device Token을 받을 수 있습니다.
아래와 같이 Device 의 인증서에 포함된 정보를 사용해 Token을 생성 후, 이 Token을 암호화해서 Device에 전달 합니다. 만약 App 이 이미 등록되어 있고 App 별 Device Token 이 변경되지 않은 경우 시스템은 기존의 Token을 App에 신속하게 반환합니다.
Device Token 을 받은 후에는 App Provider 에게 Token을 전달하여, 이후 Push Notification 을 전송할때 활용할수 있도록 합니다.
3) Provider server ← → APN 간 TLS 연결 및 Push Notification 전송
Provider server 에서 Device로 Push Notification 을 전송하기 위해서는 APN server에 연결 후 Push Notification 을 전달하여 합니다.
하지만 인증되지 않은 Provider 가 Device에 함부로 Push Notification 을 전송하는 것을 막기 위해 Provider server 와 APN server 간에 신뢰있는 연결을 위한 2가지 방법을 제공합니다. Provider 는 이 2가지 중 한개를 선택해 APN server와 연결 후 메세지를 전송하면 됩니다.
- a. Token Based Provider to APNs Trust
Token Based 인증은 APNs의 인증 과정을 단순화합니다.
아래 그림과 같이 Provider는 TLS 를 사용해 APN 과 보안 연결을 요청하면, APN 은 APN 인증서를 제공하고 Provider 는 이를 검증하는 과정을 거칩니다.
이 시점에 TLS 연결이 이뤄지고, Provider는 모든 Push Notification 요청에 Token 과 함께 전송하게 됩니다.
- b. Certificate Based Provider to APNs Trust
Certicate Based 연결의 경우 아래 그림과 같이 Provider는 TLS 를 사용해 APN 과 보안 연결을 요청하면, APN 은 APN 인증서를 제공하고 Provider는 이를 검증합니다.
그 후 Provider 는 Apple에서 발급 받은 Provider 인증서를 APN 에 전송하고, 인증서를 수신한 APN은 Provider의 인증서 유효성을 검사하여 이 연결 요청이 합법적인 Provider로 부터 온 요청인지 확인한 후 TLS 연결을 설정합니다.
TLS 연결이 이뤄진 이후에는 Provider는 Certicate Based Push Notification 요청을 APN에 보낼수 있게 됩니다.
Java로 APNs 사용하여 Push Notification 전송 기능 구현하기
*참고: 아래 구현은 기능적인 부분으로 참고만 하시기 바랍니다. 분명 더 좋은 구조로 구현할수 있을 텐데 아직 주니어 개발자라 부족한 부분이 있을수 있습니다.
Push Notification 전송을 위해 ‘pushy’ 라이브러리를 사용하였고, APNs 의 Certificated Based 연결을 통해 기능 구현을 하였습니다. Apple 로 부터 발급받은 Provider 인증서 경로, team id, key id, bundle id 등 연결 인증을 위해 필요한 정보는 아래와 같이 Property Class로 만들었습니다.
// https://mvnrepository.com/artifact/com.turo/pushy
implementation group: ‘com.turo’, name: ‘pushy’, version: ‘0.13.10’
@Data
public abstract class AbstractAPNProperty { private String configPath = "AuthKey_**.p8 file path";
private String teamId = "team id value";
private String keyId = "key id value";
private String bundleId = "app bundle id";
}@Slf4j
@RequiredArgsConstructor
public abstract class AbstractAPNPushService extends AbstractPushService {
private ApnsClient client = null;
private final AbstractAPNProperty abstractApnProperty;
@PostConstruct
public void initialize() {
try (FileInputStream inputStream = new FileInputStream(
new ClassPathResource(abstractApnProperty.getConfigPath()).getFile())) {
this.client = new ApnsClientBuilder()
.setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST)
.setSigningKey(ApnsSigningKey.loadFromInputStream(
inputStream, abstractApnProperty.getTeamId(),
abstractApnProperty.getKeyId()))
.build();
log.debug("apn service application has been initialized");
} catch (IOException | NoSuchAlgorithmException | InvalidKeyException e) {
log.error(e.getMessage(), e);
}
}
public boolean sendPushMessage(final PushRequestForm pushRequestForm) {
if (this.client == null) {
this.initialize();
}
SimpleApnsPushNotification pushNotification =
new SimpleApnsPushNotification(
pushRequestForm.getToken(),
abstractApnProperty.getBundleId(),
new ApnsPayloadBuilder()
.setAlertBody(pushRequestForm.getMessage())
.setAlertTitle(pushRequestForm.getTitle())
.setSound("default")
.buildWithDefaultMaximumLength()
); try {
return client.sendNotification(pushNotification)
.get(10, TimeUnit.SECONDS) //blocking
.isAccepted();
} catch (InterruptedException | ExecutionException | TimeoutException e) {
log.error(e.getMessage(), e);
return false;
}
}
}