diff --git a/.gitignore b/.gitignore index 357cdbe..8d814c1 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ build/ ### VS Code ### .vscode/ -application.properties \ No newline at end of file +application.properties +firebase +firebase/make-delivery-firebase-adminsdk-8jura-1d0b64450e.json \ No newline at end of file diff --git a/pom.xml b/pom.xml index e031007..6ecde20 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,12 @@ spring-boot-starter-validation + + com.google.firebase + firebase-admin + 6.8.1 + + diff --git a/src/main/java/com/flab/makedel/config/SpringAsyncConfig.java b/src/main/java/com/flab/makedel/config/SpringAsyncConfig.java new file mode 100644 index 0000000..0b4ee54 --- /dev/null +++ b/src/main/java/com/flab/makedel/config/SpringAsyncConfig.java @@ -0,0 +1,44 @@ +package com.flab.makedel.config; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor.AbortPolicy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/* + 스프링의 @Async를 사용할 때 비동기처리를 새로운 스레드 풀에서 해주기 위한 설정입니다. + 이 설정이 없다면 SimpleAsyncTaskExecutor를 사용하는데 이는 새로운 비동기 작업을 + 스레드 풀에서 처리하는 것이 아니라 새로운 스레드를 매번 생성하여 작업을 수행시킵니다. + 또한 스레드 관리를 직접 할 수 없어 위험할 수 있습니다. + 따라서 밑에 설정에서 스레드 풀을 빈으로 설정해서 @Async 로직이 수행될 때 + 이 스레드 풀을 이용하도록 설정해줍니다. + */ + +@Configuration +@EnableAsync +public class SpringAsyncConfig { + + private static final int CORE_POOL_SIZE = 5; + private static final int MAX_POOL_SIZE = 500; + private static final int QUEUE_CAPACITY = 0; + private static final int KEEP_ALIVE_SECONDS = 60; + private static final String NAME_PREFIX = "springAsyncTask-"; + + @Bean(name = "springAsyncTask") + public Executor threadPoolTaskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.setCorePoolSize(CORE_POOL_SIZE); + taskExecutor.setMaxPoolSize(MAX_POOL_SIZE); + taskExecutor.setQueueCapacity(QUEUE_CAPACITY); + taskExecutor.setThreadNamePrefix(NAME_PREFIX); + taskExecutor.setWaitForTasksToCompleteOnShutdown(false); + taskExecutor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS); + taskExecutor.setAllowCoreThreadTimeOut(true); + taskExecutor.setRejectedExecutionHandler(new AbortPolicy()); + return taskExecutor; + } + +} diff --git a/src/main/java/com/flab/makedel/controller/RiderController.java b/src/main/java/com/flab/makedel/controller/RiderController.java index 5e485fe..ab809b2 100644 --- a/src/main/java/com/flab/makedel/controller/RiderController.java +++ b/src/main/java/com/flab/makedel/controller/RiderController.java @@ -2,8 +2,13 @@ import com.flab.makedel.annotation.LoginCheck; import com.flab.makedel.annotation.LoginCheck.UserLevel; +import com.flab.makedel.dto.PushMessageDTO; import com.flab.makedel.dto.RiderDTO; +import com.flab.makedel.service.PushService; import com.flab.makedel.service.RiderService; +import com.google.firebase.messaging.FirebaseMessagingException; +import java.io.IOException; +import java.time.LocalDateTime; import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; @@ -45,5 +50,5 @@ public void acceptStandbyOrder(@PathVariable long orderId, public void finishDeliveringOrder(@PathVariable long orderId, RiderDTO rider) { riderService.finishDeliveringOrder(orderId, rider); } - + } diff --git a/src/main/java/com/flab/makedel/controller/StoreController.java b/src/main/java/com/flab/makedel/controller/StoreController.java index 864eee0..1276cf4 100644 --- a/src/main/java/com/flab/makedel/controller/StoreController.java +++ b/src/main/java/com/flab/makedel/controller/StoreController.java @@ -7,6 +7,8 @@ import com.flab.makedel.annotation.LoginCheck.UserLevel; import com.flab.makedel.dto.StoreDTO; import com.flab.makedel.service.StoreService; +import com.google.firebase.messaging.FirebaseMessagingException; +import java.io.IOException; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/flab/makedel/dao/CartItemDAO.java b/src/main/java/com/flab/makedel/dao/CartItemDAO.java index 9433be5..d50c881 100644 --- a/src/main/java/com/flab/makedel/dao/CartItemDAO.java +++ b/src/main/java/com/flab/makedel/dao/CartItemDAO.java @@ -17,9 +17,7 @@ exec 후에 오류가 나지 않은 부분은 실행된다. exec 이전에 command queue에 적재하는 도중 실패하는 경우 (command 문법오류,메모리 부족오류등, 다른 클라이언트에서 command날려 atomic보장이 안되는 경우) 에는 exec하면 전부 discard된다. - (실험해보니 multi 후 트랜잭션중 다른 스레드에서 command를 날리면 discard된다. - 오히려 다른스레드에서 그 키에 command 날린 것만 반영이 되고 원래트랜잭션은 discard된다. 아마도 - 원래 트랜잭션은 어차피 처리가 안되고 discard되니 다른 스레드에서 날린 command는 유효하게 처리하는거같다.) + (실험해보니 multi 후 트랜잭션중 다른 스레드에서 command를 날리면 discard된다.) (레디스 2.6.5이후로 트랜잭션시작 후 오류가 있으면 exec될 때 전부 discard된다.) 트랜잭션 명령어들은 exec되기 위해 큐에서 기다리는데 discard를 이용해 실행을 하지 않을 수 있다. 트랜잭션의 locking은 watch를 이용한 optimistic locking이다. watch로 어떠한 키를 감시하고 diff --git a/src/main/java/com/flab/makedel/dto/PushMessageDTO.java b/src/main/java/com/flab/makedel/dto/PushMessageDTO.java new file mode 100644 index 0000000..13e2496 --- /dev/null +++ b/src/main/java/com/flab/makedel/dto/PushMessageDTO.java @@ -0,0 +1,21 @@ +package com.flab.makedel.dto; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PushMessageDTO { + + private final String title; + + private final String content; + + private final OrderReceiptDTO orderReceipt; + + private final String createdAt; + + public static final String RIDER_MESSAGE_TITLE = "배차 요청"; + public static final String RIDER_MESSAGE_CONTENT = "근처 가게에서 주문이 승인된 후 배차 요청이 도착했습니다. 승인하시겠습니까?"; + +} diff --git a/src/main/java/com/flab/makedel/service/PushService.java b/src/main/java/com/flab/makedel/service/PushService.java new file mode 100644 index 0000000..0afd629 --- /dev/null +++ b/src/main/java/com/flab/makedel/service/PushService.java @@ -0,0 +1,56 @@ +package com.flab.makedel.service; + +import com.flab.makedel.dao.DeliveryDAO; +import com.flab.makedel.dto.PushMessageDTO; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Value; +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; +import org.springframework.stereotype.Service; + +@Service +@Log4j2 +public class PushService { + + private final String firebaseConfigPath; + private final DeliveryDAO deliveryDAO; + + public PushService(@Value("${firebase.config.path}") String firebaseConfigPath, + DeliveryDAO deliveryDAO) { + this.firebaseConfigPath = firebaseConfigPath; + this.deliveryDAO = deliveryDAO; + } + + @PostConstruct + public void init() throws IOException { + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(GoogleCredentials + .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())) + .build(); + if (FirebaseApp.getApps().isEmpty()) { + FirebaseApp.initializeApp(options); + } + } + + @Async("springAsyncTask") + public void sendMessages(List messages) { + FirebaseMessaging.getInstance().sendAllAsync(messages); + } + + +} diff --git a/src/main/java/com/flab/makedel/service/RiderService.java b/src/main/java/com/flab/makedel/service/RiderService.java index dba3cc6..9a55b74 100644 --- a/src/main/java/com/flab/makedel/service/RiderService.java +++ b/src/main/java/com/flab/makedel/service/RiderService.java @@ -2,9 +2,14 @@ import com.flab.makedel.dao.DeliveryDAO; import com.flab.makedel.dto.OrderDTO.OrderStatus; +import com.flab.makedel.dto.PushMessageDTO; import com.flab.makedel.dto.RiderDTO; import com.flab.makedel.mapper.OrderMapper; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,6 +20,7 @@ public class RiderService { private final DeliveryDAO deliveryDAO; private final OrderMapper orderMapper; + private final PushService pushService; public void registerStandbyRiderWhenStartWork(RiderDTO rider) { deliveryDAO.insertStandbyRiderWhenStartWork(rider); @@ -36,4 +42,18 @@ public void finishDeliveringOrder(long orderId, RiderDTO rider) { deliveryDAO.insertStandbyRiderWhenStartWork(rider); } + public void sendMessageToStandbyRidersInSameArea(String address, PushMessageDTO pushMessage) { + Set tokenSet = deliveryDAO.selectStandbyRiderTokenList(address); + List messages = tokenSet.stream().map(token -> Message.builder() + .putData("title", pushMessage.getTitle()) + .putData("content", pushMessage.getContent()) + .putData("orderReceipt", pushMessage.getOrderReceipt().toString()) + .putData("createdAt", pushMessage.getCreatedAt()) + .setToken(token) + .build()) + .collect(Collectors.toList()); + + pushService.sendMessages(messages); + } + } diff --git a/src/main/java/com/flab/makedel/service/StoreService.java b/src/main/java/com/flab/makedel/service/StoreService.java index 391b2d1..f3d2332 100644 --- a/src/main/java/com/flab/makedel/service/StoreService.java +++ b/src/main/java/com/flab/makedel/service/StoreService.java @@ -3,9 +3,13 @@ import com.flab.makedel.dto.OrderDTO.OrderStatus; import com.flab.makedel.dto.OrderDetailDTO; import com.flab.makedel.dto.OrderReceiptDTO; +import com.flab.makedel.dto.PushMessageDTO; import com.flab.makedel.dto.StoreDTO; import com.flab.makedel.mapper.OrderMapper; import com.flab.makedel.mapper.StoreMapper; +import com.google.firebase.messaging.FirebaseMessagingException; +import java.io.IOException; +import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -20,6 +24,7 @@ public class StoreService { private final StoreMapper storeMapper; private final OrderMapper orderMapper; private final DeliveryService deliveryService; + private final RiderService riderService; public void insertStore(StoreDTO store) { storeMapper.insertStore(store); @@ -69,6 +74,18 @@ public void approveOrder(long orderId) { orderMapper.approveOrder(orderId, OrderStatus.APPROVED_ORDER); OrderReceiptDTO orderReceipt = orderMapper.selectOrderReceipt(orderId); deliveryService.registerStandbyOrderWhenOrderApprove(orderId, orderReceipt); + riderService.sendMessageToStandbyRidersInSameArea(orderReceipt.getStoreInfo().getAddress(), + getPushMessage(orderReceipt)); + } + + private PushMessageDTO getPushMessage(OrderReceiptDTO orderReceipt) { + return PushMessageDTO.builder() + .title(PushMessageDTO.RIDER_MESSAGE_TITLE) + .content(PushMessageDTO.RIDER_MESSAGE_TITLE) + .createdAt(LocalDateTime.now().toString()) + .orderReceipt(orderReceipt) + .build(); + } }