Ga0Lee

[SpringBoot] Pushy ApnsClient 분석 본문

Spring Boot

[SpringBoot] Pushy ApnsClient 분석

가영리 2025. 2. 2. 14:40
 

com.eatthepath.pushy.apns (Pushy 0.15.4 API)

package com.eatthepath.pushy.apns Contains classes and interfaces for interacting with the Apple Push Notification service (APNs). Callers will primarily interact with the ApnsClient class to send push notifications. An ApnsClient maintains a single connec

pushy-apns.org

ApnsClient란?

APNs(Apple Push Notification service) 클라이언트는 푸시 알림을 APNs 게이트웨이에 전송합니다. 

HTTP Client처럼 서버와 HTTP 통신을 위한 라이브러리 또는 도구와 같습니다.

클라이언트는 APNs 서버에 두 가지 방식 중 하나로 인증할 수 있습니다.

  1. TLS 기반 인증
  2. 토큰 기반 인증

TLS 기반 인증

공급자 인증서를 사용하여 공급자 서버와 APNs 간의 보안 연결을 설정합니다.

서버 수준에서 신뢰를 구축하기 때문에 notification에는 payload와 device token만 포함하면 됩니다.

인증 토큰은 포함되지 않아 notification의 크기가 토큰 기반 인증에 비해 상대적으로 작은 편입니다.

그러나 인증서는 하나의 앱에만 적용되므로 여러 앱에 대해 notification을 보내고 싶을 땐 각 앱에 대한 인증서를 만들어야 합니다.

토큰 기반 인증

토큰 기반 인증은 APNs와 stateless한 통신 방식을 제공합니다.

stateless하기 때문에 서버와 관련된 인증서나 기타 정보를 조회할 필요가 없어 인증서 기반 통신보다 빠릅니다.

또한 여러 공급자 서버에서 동일한 토큰을 사용할 수 있으며, 하나의 토큰을 사용해 회사의 모든 앱에 대한 notification을 처리할 수 있습니다.

그러나 토큰 기반 요청은 각 요청마다 토큰이 포함되어야 하므로 인증서 기반 요청보다 notification의 크기가 상대적으로 큰 편입니다.

 

ApnsClient 생성

클라이언트는 ApnsClientBuilder를 사용하여 생성됩니다.

클라이언트를 생성할 때 EventLoopGroup을 지정할 수 있으며, 지정하지 않으면 클라이언트가 자체적으로 단일 스레드 이벤트 루프 그룹을 생성하여 관리합니다.

여러 개의 클라이언트가 동시에 실행될 경우, 공유된 EventLoopGroup을 사용하면 스레드 개수를 제어할 수 있습니다.

아래는 토큰 기반 인증 방식으로 ApnsClient를 생성하는 코드입니다.

new ApnsClientBuilder()
      .setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST)
      .setSigningKey(signingKey)
      .setEventLoopGroup(new NioEventLoopGroup())
      .build();

 

Client 연결 관리

API문서를 보면 아래와 같습니다.

클라이언트는 내부적으로Connection Pool을 관리하며, 필요할 때 자동으로 APNs 서버와 연결을 설정합니다. 따라서
클라이언트를 별도로 시작할 필요가 없으며, 생성되자마자 바로 사용할 수 있습니다.

실제로 ApnsClient 객체를 생성할 때 어떻게 생성하는지 생성자 코드를 확인해 봅시다.

 

ApnsClient 생성자 메서드를 확인해보면, ApnsChannelPool을 사용해 Connection Pool을 생성한 것을 알 수 있습니다.

protected ApnsClient(ApnsClientConfiguration clientConfiguration, EventLoopGroup eventLoopGroup) {
        if (eventLoopGroup != null) {
            this.eventLoopGroup = eventLoopGroup;
            this.shouldShutDownEventLoopGroup = false;
        } else {
            this.eventLoopGroup = new NioEventLoopGroup(1);
            this.shouldShutDownEventLoopGroup = true;
        }

        this.metricsListener = (ApnsClientMetricsListener)clientConfiguration.getMetricsListener().orElseGet(() -> {
            return new NoopApnsClientMetricsListener();
        });
        ApnsChannelFactory channelFactory = new ApnsChannelFactory(clientConfiguration, this.eventLoopGroup);
        ApnsChannelPoolMetricsListener channelPoolMetricsListener = new ApnsChannelPoolMetricsListener() {
            public void handleConnectionAdded() {
                ApnsClient.this.metricsListener.handleConnectionAdded(ApnsClient.this);
            }

            public void handleConnectionRemoved() {
                ApnsClient.this.metricsListener.handleConnectionRemoved(ApnsClient.this);
            }

            public void handleConnectionCreationFailed() {
                ApnsClient.this.metricsListener.handleConnectionCreationFailed(ApnsClient.this);
            }
        };
        this.channelPool = new ApnsChannelPool(channelFactory, clientConfiguration.getConcurrentConnections(), this.eventLoopGroup.next(), channelPoolMetricsListener);
    }

APNs는 HTTP/2 프로토콜을 기반으로 동작하기 때문에 Keep-Alive가 기본적으로 지원됩니다.

따라서 ApnsClientApnsChannelPool을 통해 Connection Pool을 관리하며, 하나의 연결을 유지한 채 여러 개의 notification을 동시에 전송할 수 있습니다.

HTTP/2의 multiplexing 기능을 활용하면, 매번 TCP 연결을 생성하는 것이 아니라 하나의 지속적인 연결을 통해 여러 개의 요청을 동시에 처리할 수 있어 성능이 향상됩니다.

아래의 글은 HTTP/2를 사용하면 어떻게 지속적인 연결이 가능한지에 대해 설명한 글입니다.

 

How HTTP/2 Persistent Connections Help Improve Performance and User Experience | Akamai

By increasing the HTTP/2 persistent connection timeout, Akamai reduced the number of TLS handshakes, which improved performance and user experience.

www.akamai.com

 

알림 전송

API문서를 보면 아래와 같습니다.

클라이언트가 APNs 서버로 보낸 알림은 비동기적으로 처리됩니다.

sendNotification 메서드는 CompletableFuture를 기반으로 한 PushNotificationFuture 객체를 반환하여 비동기적으로 푸시 알림을 전송합니다.

public <T extends ApnsPushNotification> PushNotificationFuture<T, PushNotificationResponse<T>> sendNotification(T notification) {
        PushNotificationFuture<T, PushNotificationResponse<T>> responseFuture = new PushNotificationFuture(notification);
        if (!this.isClosed.get()) {
            long notificationId = this.nextNotificationId.getAndIncrement();
            this.channelPool.acquire().addListener((acquireFuture) -> {
                if (acquireFuture.isSuccess()) {
                    Channel channel = (Channel)acquireFuture.getNow();
                    channel.writeAndFlush(responseFuture).addListener((future) -> {
                        if (future.isSuccess()) {
                            this.metricsListener.handleNotificationSent(this, notificationId);
                        }

                    });
                    this.channelPool.release(channel);
                } else {
                    responseFuture.completeExceptionally(acquireFuture.cause());
                }

            });
            responseFuture.whenComplete((response, cause) -> {
                if (response != null) {
                    if (response.isAccepted()) {
                        this.metricsListener.handleNotificationAccepted(this, notificationId);
                    } else {
                        this.metricsListener.handleNotificationRejected(this, notificationId);
                    }
                } else {
                    this.metricsListener.handleWriteFailure(this, notificationId);
                }

            });
        } else {
            responseFuture.completeExceptionally(CLIENT_CLOSED_EXCEPTION);
        }

        return responseFuture;
    }

메서드 흐름을 정리해보자면 다음과 같다.

  1. sendNotification이 호출되면 PushNotificationFuture 객체를 생성하고 이를 responseFuture에 할당합니다.
  2. channelPool.acquire()를 호출하여 비동기적으로 APNs 서버와의 연결을 위한 채널을 확보합니다.
  3. 채널 확보가 완료되면, channel.writeAndFlush(responseFuture)를 통해 푸시 알림을 비동기적으로 전송합니다.
  4. writeAndFlush() 이후 addListener()를 사용하여 전송 성공 여부를 확인하고, 실패 시 예외를 처리합니다.
  5. channelPool.release(channel)을 호출하여 사용한 채널을 풀에 반환합니다.
  6. responseFuture.whenComplete()를 활용하여 APNs 서버의 응답을 비동기적으로 처리합니다.

이러한 과정을 통해 sendNotification 메서드는 즉시 반환되며, 호출한 측에서는 PushNotificationFuture를 통해 APNs 서버의 응답을 나중에 확인할 수 있습니다. 즉, APNs 서버의 응답을 기다리지 않고도 다음 작업을 수행할 수 있도록 비동기적으로 동작합니다.

 

결론

ApnsClient 내부적으로 채널 풀(ApnsChannelPool)을 관리하므로 여러 개의 ApnsClient 인스턴스를 생성할 필요 없이 하나의 인스턴스를 공유하는 것이 성능적으로 더 효율적입니다. 여러 개의 ApnsClient 인스턴스를 생성하면 각 클라이언트가 별도의 연결 풀을 유지해야 하므로, 오히려 비효율적입니다.