Skip to content
This repository has been archived by the owner on Aug 13, 2022. It is now read-only.

[#46] 비동기적인 Push 알람 서비스 구현 #48

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from

Conversation

tjdrnr0557
Copy link
Collaborator

@tjdrnr0557 tjdrnr0557 commented Nov 2, 2020

결론적으로 파이어베이스에서 제공해주는 sendAllAsync, sendAsync등의 메소드를 사용하면 어플리케이션 레벨에서 스레드 풀을 새로 띄우고 비동기 로직을 구현해주지 않아도 비동기로 푸쉬 알람을 보내는게 가능합니다.

FCM 공식 Document를 읽어보며 sendAsync함수를 이용해 비동기적으로 푸쉬 메세지를 보낼 수 있다는 것은 알고 있었으나 어플리케이션 차원에서 스프링을 이용해 어떻게 비동기로 로직을 구현할까? 라는 의문이 들어 스레드 풀을 커스터마이징하고 @Async를 이용하여 구현해봤습니다.

@tjdrnr0557 tjdrnr0557 self-assigned this Nov 2, 2020
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core pool size와 max pool size를 다르게 해주신 이유는 어떤 상황을 가정해서 다르게 주신 것일까요?

Copy link
Collaborator Author

@tjdrnr0557 tjdrnr0557 Nov 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core pool size는 스레드 풀 안에 유지되는 스레드 개수이고
max pool size는 작업 큐에 QUEUE_CAPACITY만큼 꽉찼을 시, 작업을 처리하려면 스레드의 개수가 늘어나야하는데 스레드의 개수가 최대로 몇개까지 늘어날 수 있는 사이즈입니다. 즉 스레드 풀에서 관리하는 최대 스레드의 개수입니다.

만약 core pool size를 높게 둔다면 스레드를 여러개만들어 두니 바로 바로 처리할 수 있지만 스레드를 많이 만들어 두는 것 자체가 메모리 낭비일 수 있으므로 작게 두었습니다.
max pool size는 푸쉬알람이 한번에 몇십개를 보내야 한다면 스레드의 개수가 많이 늘어날 수 있으니 높게 설정해두었습니다.

이 둘을 다르게 설정해준 이유는 한번에 갑자기 많은 푸쉬알람을 보내야 할 때 core pool size의 스레드 개수가 알람을 보내고 response가 올떄 까지 blocking이 되면 작업 큐에 있는 작업들이 처리되지 못하여 core pool size의 스레드를 할당받지 못하고 기다리게 될 것이므로 max pool size만큼 스레드를 늘려 푸쉬 알람을 보내기 위해서 입니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스레드를 생성하고 소멸시키면서 생기는 오버헤드보다는 메모리를 적게 사용하는 것을 선택하신 것 같네요~
그러면 메모리에 얼마나 차이가 있을지는 혹시 계산해보셨을까요?

Copy link
Collaborator Author

@tjdrnr0557 tjdrnr0557 Nov 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

푸쉬메세지를 보낼 때 Runtime 클래스를 이용해 메모리 사용량을 계산해보았습니다.
Max Pool을 500으로 지정해놓았을때랑 5로 지정해 놓았을 때 푸쉬메세지를 보내보면 스레드를 495개 더 만드는데에 메모리 차이는 70MB정도의 차이가 있었습니다. (매번 다르게 나오는데 이 부분은 정확히 왜 계속 다르게 나오는지 모르겠습니다)
푸쉬메세지가 한번에 폭증하는 것을 병목없이 보내주는데 70MB를 더 쓰는 것이 더 효율적이라고 생각했습니다.
만약 큐에 쌓아놓고 푸쉬메세지요청이 계속 더 들어온다면 병목이 계속 생길 수 있다고 생각하기 때문입니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

매번 다르게 나와서 정확한 수치 측정은 불가능하나

Maxpool을 500으로 잡았을때와 5로 잡았을때 메모리 차이는 50~100MB정도의 차이는 꾸준히 있었습니다.

ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. QUEUE_CAPACITY 가 상당히 낮은데 어떤 이유로 이렇게 산정하셨나요?
  2. QueueCapacity는 크기에 따른 각 어떤 장단점이 있을까요?

Copy link
Collaborator Author

@tjdrnr0557 tjdrnr0557 Nov 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


core_pool_size만큼 스레드 풀은 스레드를 만들어놓고 스레드를 처리하다 더 많은 작업량이 들어오면 작업 큐에 넣어놓고 작업 큐는 대기하고 있고 core_pool_size의 스레드들이 작업은 완료하면 작업 큐에서 한개씩 스레드를 할당해서 스레드를 재사용합니다.

만약 푸쉬알람이 폭증해서 동시에 몇십개를 보내야 하는 상황에서 QUEUE_CAPACITY가 20이고 core_pool_size로 정해놓은 스레드들이 3이라면 3개의 스레드가 푸쉬알람을 보내고 response를 받고 이 일 처리를 끝내고 이 3개의 스레드로 20개의 작업큐의 작업을 해야 하므로 작업큐에서도 blocking이 있을것이고 core_pool_size만큼의 스레드들도 푸쉬알람을 보내고 response까지 받을때까진 blocking이 있을것이므로 작업큐에서 대기하는 작업들은 굉장히 늦게 처리될 것입니다.

따라서 queue_capactiy를 아예 낮게 두어 core_pool_size의 스레드가 꽉차고 queue_capacity도 꽉찬다면 작업 큐에서 계속 작업들이 기다리는 것을 방지하고자 아예 max_pool_size를 높게 두어 한번에 몇십개씩 푸쉬알람을 보내야 할 때 스레드의 개수를 늘려 푸쉬알람 병목을 대비하였습니다.


기존 스레드풀의 core_pool_size만큼의 스레드가 모두 일을하고있고 작업큐에 작업이 쌓였다면
기존 core_pool_size만큼의 스레드가 제할일을 끝내고 작업큐에 남아있는 작업들에 할일을 끝낸 스레드를 할당합니다.
기존 스레드들은 계속 재활용되며 작업큐의 작업들에 스레드를 할당합니다.
따라서 queue_capacity가 높다면 시간은 조금 오래걸리더라도 새로운 스레드를 생성하지 않고 재활용 하기 때문에 새로운 스레드를 만드는데 메모리를 더 쓰지 않고 재활용하므로 메모리면에서 효율적입니다.

하지만 queue_capacity가 낮다면 작업큐까지 다 꽉차버리면 max_pool_size까지 스레드를 필요한만큼 늘려버리기 때문에 바로바로 일처리를 할 수 있고 병목이 생길 확률이 줄어듭니다. 시간은 적게 걸리지만 새로운 스레드들을 생성하기 때문에 새로운 스레드를 만드는데 메모리를 더 쓰게 될 것입니다.

taskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
taskExecutor.setThreadNamePrefix(NAME_PREFIX);
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 무한루프를 돌거나 무한 block에 걸려있는 스레드가 있는 상황도 가정하신걸까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설정을 false로 두어야 blocking이 되어도 shutdown이 가능 할 것 같습니다. 수정하였습니다

taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
taskExecutor.setThreadNamePrefix(NAME_PREFIX);
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
taskExecutor.setRejectedExecutionHandler(new AbortPolicy());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbortPolicy로 설정해주신 이유가 있으실까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abort policy는 최대 스레드 크기까지 쓰고 작업큐까지 꽉차면 RejectedExecutionException 예외를 던집니다. 이 예외를 잡아서 직접 대응해줘야합니다. 만약 푸쉬 알람을 보내다 실패한다면 이 예외를 푸쉬 알람을 보내려고 한 사용자에게 throw하여 알려야 한다고 생각하여 abortpolicy로 설정하였습니다.


@Bean(name = "springAsyncTask")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자바에서 기본으로 제공하는 factory 메소드가 있습니다. 혹시 커스터마이징을 의도하셨다면 이유가 있으실까요?

Copy link
Collaborator Author

@tjdrnr0557 tjdrnr0557 Nov 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newFixedThreadPool은 스레드에 할당된 시간 제한이 무제한으로 설정되어 있으므로
아무런 작업 없이 대기하고 있는 스레드도 아무작업하지 않는데도 메모리를 잡아먹고 있으므로 적용할 수 없다고 생각했습니다.
시간 제한을 두어야 풀의 스레드 개수가 코어크기를 넘어설 때 제거할 수 있습니다. 물론 제거하고나면 나중에 다시 스레드를 생성해야하는 단점은 있습니다. 또한 newFixedThreadPool은 크기가 제한되지 않은 LinkedBlockingQueue를 사용하므로 푸쉬알람이 많이오면 큐에 쌓아두기만 하여 병목이 생길 수 있습니다.

또한 newCachedThreadPool은 스레드 풀의 최대 크기가 Integer.MAX_VALUE로 지정되어있어 적용하기 알맞지 않다고 생각했습니다. 왜냐하면 푸쉬알람이 아무리 많이와도 스레드의 개수를 무작정 많이 늘려도 모두 처리할 수 없기 때문에 적당한 크기의 최대 스레드 개수를 지정하기 위해 커스터마이징을 하게 되었습니다. 하지만 synchronousQueue를 이용하여 스레드를 큐에 넣지 않고 바로 스레드로 작업을 넘겨주기 때문에 푸쉬알람이 수십개가 한번에 온다면 max_pool_size를 늘려놓고 이 방식을 쓰는 것도 대안이 될 수 있습니다.

푸쉬 알람 보내는 것을 비동기 작업으로 처리할 것이기 때문에 newCachedThreadPool이 알맞긴 하지만 시간제한을 30초로 두고 직접 코어 사이즈나 큐개수등을 제한하기 위해 커스터마이징을 하게되었습니다.

스레드 풀에서 처리하는 것이 아니라 새로운 스레드를 매번 생성하여 작업을 수행시킵니다.
또한 스레드 관리를 직접 할 수 없어 위험할 수 있습니다.
따라서 밑에 설정에서 스레드 풀을 빈으로 설정해서 @Async 로직이 수행될 때
이 스레드 풀을 이용하도록 설정해줍니다.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -82,7 +83,7 @@
@PostMapping("/{storeId}/orders/{orderId}/approve")
@LoginCheck(userLevel = UserLevel.OWNER)
public void approveOrder(@PathVariable long orderId, @PathVariable long storeId,
@CurrentUserId String ownerId) {
@CurrentUserId String ownerId) throws FirebaseMessagingException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 프로젝트에서 파이어베이스를 사용한다는 것은 프로젝트 내부의 관심사이기에 외부로 파이어베이스 관련 예외를 전파하는게 아닌 다른 방식으로 처리해주시면 좋을 것 같습니다~

private final String createdAt;

public static final String RIDER_MESSAGE_TITLE = "배차 요청";
public static final String RIDER_MESSAGE_CONTENT = "근처 가게에서 주문이 승인된 후 배차 요청이 도착했습니다. 승인하시겠습니까?";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 이 어플리케이션을 베트남에서도 서비스하게 된다면 이 상수가 큰 걸림돌이 되지 않을까요? 이럴 땐 어떻게 해야할까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resources 디렉토리 안에 베트남의 언어로 출력할 메세지를 작성하고 resource bundle로 묶습니다. 그 다음 ResourceBundleMessageSoure빈이 리소스 번들을 메세지 소스로 읽어와서 이를 Local.VIETNAM 이런식으로 출력해줄 수 있습니다.

public class PushService {

@Value("${firebase.config.path}")
private String FIREBASE_CONFIG_PATH;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 이 변수는 static, 상수가 아니기 때문에 카멜케이스를 지켜주셔야할 것 같습니다~
  2. @Value도 생성자 주입이 가능합니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하였습니다. @Value도 생성자 주입이 가능한지는 몰랐네요.

.build())
.collect(Collectors.toList());

BatchResponse response = FirebaseMessaging.getInstance().sendAll(messages);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 로컬변수는 사용하는 곳이 없는 것 같네요~

@@ -34,6 +35,8 @@ public Executor threadPoolTaskExecutor() {
taskExecutor.setQueueCapacity(QUEUE_CAPACITY);
taskExecutor.setThreadNamePrefix(NAME_PREFIX);
taskExecutor.setWaitForTasksToCompleteOnShutdown(false);
taskExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
taskExecutor.setAllowCoreThreadTimeOut(true);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 메소드를 통해 풀 내부의 모든 스레드가 시간 제한에 걸리도록 하여 특정 크기의 스레드와 크기가 제한된 작업 큐를 사용하는 스레드 풀에서 처리할 작업이 없을 때 스레드가 점차 사라지도록 하였습니다.

private static final int KEEP_ALIVE_SECONDS = 30;
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 500;
private static final int QUEUE_CAPACITY = 0;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

queue capacity가 양수이면 LinkedBlockingQueue로 지정되고

0이하라면 SynchronousQueue를 이용합니다.

LinkedBlockingQueue는 큐의 크기가 제한되지 않는 큐입니다. 크기를 수동으로 설정을 할 수도 있습니다. 하지만 큐에 작업이 계속 쌓이게 되면 푸쉬 알람에 병목이 생길 수 있습니다.

SynchronousQueue는 큐라기보다는 작업이 들어오면 그것을 스레드에 넘겨주는 역할을 합니다. 큐에 쌓지 않고 바로 프로듀서에서 생성한 작업을 컨슈머인 스레드에게 직접 전달합니다. 쉬고 있는 스레드에게 처리할 작업을 직접 넘겨주므로 효율적입니다. 또한 대기중인 스레드가 없는 상태에서 스레드의 개수가 max pool size보다 작다면 새로운 스레드를 생성해 동작시킵니다. 따라서 푸쉬알람이 폭증해도 대비할 수 있습니다. 하지만 정해둔 max pool size보다 커진다면 abort policy를 이용해 푸쉬 알람 보내는 것을 거부하고 exception을 던집니다.

@@ -55,7 +59,7 @@ public void sendMessageToStandbyRidersInSameArea(String address, PushMessageDTO
.build())
.collect(Collectors.toList());

BatchResponse response = FirebaseMessaging.getInstance().sendAll(messages);
FirebaseMessaging.getInstance().sendAll(messages);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거를 비동기로 처리해준다면 @Async는 붙이지 않아도 되지 않을까요?

try {
FirebaseMessaging.getInstance().sendAll(messages);
} catch (FirebaseMessagingException e) {
throw new IOException(e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 checked exception을 사용하신 이유가 있으실까요?

Copy link
Collaborator Author

@tjdrnr0557 tjdrnr0557 Nov 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checked exception은 catch로 잡든 throw로 던지든 반드시 예외처리를 해줘야합니다. 예외처리를 강제로 시키고 이 소스 코드를 같이 개발하는 개발자들에게 이 예외는 꼭처리해야한다고 명확하게 알려주기 위해 checked exception을 사용했습니다. 또한 다시 ioexception으로 감싸서 던져서 사용자가 푸쉬메세지를 보내는데 실패한다면 사용자에게도 알려줄 수 있도록 다시 throw하였습니다.

@@ -47,7 +47,7 @@ public void init() throws IOException {
}

public void sendMessageToStandbyRidersInSameArea(String address, PushMessageDTO pushMessage)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다시 보니 이 클래스의 이름은 PushService인데 메소드 이름에 StandbyRidersInSameArea가 붙어있습니다. 순수하게 푸시 로직만 들어있는게 아니라 라이더 관련 책임도 들어있네요. 그러면 두 책임간의 결합도가 높은게 아닐까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

도메인 관계를 잘못 설정하고 있었네요. RiderService가 Pushservice에 의존하여 메세지를 보내게 변경하였습니다.

private static final int QUEUE_CAPACITY = 3;
private static final int KEEP_ALIVE_SECONDS = 30;
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 500;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 이렇게 높혀주신 이유가 있으실까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

라이더들에게 지역별로 푸쉬 알람을 보내긴 하지만 갑자기 푸쉬 알람이 폭증할 가능성이 있기 때문에 max_pool_size를 늘려서 푸쉬 알람 보내는 기능에 병목이 일어날 확률이 적어지도록 만들었습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 값은 성능테스트하면서 조절하는게 일반적인데요~ 추후에 한번 테스트해보시면서 조절해봐도 좋을 것 같네요

Copy link
Member

@f-lab-dev f-lab-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좀 아깝긴 하지만 @Async 관련 설정 추가하신건 이제 필요가 없어졌으니 지워야하지 않을까요? 😢

@@ -20,6 +20,7 @@
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.scheduling.annotation.Async;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 커밋은 별 의미가 없는 것 같은데 추가하신 이유가 있으실까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

뭔가 오류가 있었던것 같습니다.

@tjdrnr0557 tjdrnr0557 changed the title [#46] Push 알람 서비스 구현 [#46] 동시성을 고려한 Push 알람 서비스 구현 Nov 6, 2020
@tjdrnr0557 tjdrnr0557 changed the title [#46] 동시성을 고려한 Push 알람 서비스 구현 [#46] 스레드풀을 커스텀하고 @Async를 이용해 Push 알람 서비스 구현 Nov 6, 2020
@tjdrnr0557 tjdrnr0557 changed the title [#46] 스레드풀을 커스텀하고 @Async를 이용해 Push 알람 서비스 구현 [#46] 비동기적인 Push 알람 서비스 구현 Nov 6, 2020
@tjdrnr0557 tjdrnr0557 added the complete but merge hold 어떤 이유 때문에 이 PR을 보류합니다. label Jan 1, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
complete but merge hold 어떤 이유 때문에 이 PR을 보류합니다.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants