9oj0e / pathorder_server

3 stars 4 forks source link

feat : 실시간 주문 push #87

Closed 9oj0e closed 1 month ago

9oj0e commented 1 month ago

실시간 푸쉬 기능 구현

비교

sse comparison

WebSocket

실시간 양방향 구조라는 장점을 가지지만, 클라이언트가 서버에서 push받으면 되는구조에서는, 불필요하다고 판단.

Pooling

주기적으로 새로고침해서, 전체 페이지를 다시 그려내면 되지만, 서버의 부하가 심하다고 판단.

Server-sent event

Spring이 이미 가지고있는 라이브러리이며, eventstream이 연결되면, 서버-> 클라이언트 단방향 통신이 형성되어, 필요한 정보만 간략하게 받을 수 있음.

구현

Client Side

@RequiredArgsConstructor
@Service
public class StoreSseService {
    private final StoreSseRepository storeSSERepository;
    private static final long TIMEOUT = 60 * 1000;
    private static final long RECONNECTION_TIMEOUT = 1000L;
    // OnCompletion: Emitter 가 완료될 때(모든 데이터가 성공적으로 전송된 상태)
    // OnTimeout: Emitter 가 타임아웃 되었을 때(지정된 시간동안 어떠한 이벤트도 전송되지 않았을 때)

    public SseEmitter createConnection(int storeId) {
        SseEmitter emitter = new SseEmitter(TIMEOUT);
        storeSSERepository.save(storeId, emitter);
        createEvent(storeId, "실시간 Push 서비스 연결 완료"); // EventStream 생성
        return emitter;
    }

    public void createOrderNotification(int orderId, int storeId) {
        if (storeSSERepository.findById(storeId).isPresent()) {
            createEvent(storeId, orderId + "번 주문 알림. 새로고침을 눌러 주문을 확인해주세요.");
        }
    }

    public void createEvent(int storeId, String data) {
        Optional<SseEmitter> opEmitter = storeSSERepository.findById(storeId);
        if (opEmitter.isPresent()) {
            SseEmitter emitter = opEmitter.get();
            SseEmitter.SseEventBuilder event = SseEmitter.event()
                    .name("sse")
                    .data(data)
                    .reconnectTime(RECONNECTION_TIMEOUT);
            try {
                emitter.send(event);
                emitter.onTimeout(() -> storeSSERepository.deleteById(storeId));
                emitter.onCompletion(() -> storeSSERepository.deleteById(storeId));
            } catch (Exception e) {
                storeSSERepository.deleteById(storeId);
                emitter.completeWithError(e);
            }
        }
    }
}
@RequiredArgsConstructor
@RestController
public class StoreSseController {
    private final HttpSession session;
    private final StoreSseService storeSSEService;

    @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter connect() {
        SessionStore sessionStore = (SessionStore) session.getAttribute("sessionStore");
        if (sessionStore == null) {
            return null;
        } else {
            return storeSSEService.createConnection(sessionStore.getId());
        }
    }
}

클라이언트쪽 로직이 모두 구현되었으면, 이제, 주문하기 로직에서 해당 sse Service를 건드려주면 된다.

@PostMapping("/api/users/{userId}/orders") // 주문하기
public ResponseEntity<?> order(@PathVariable String userId, @RequestBody UserRequest.OrderDTO reqDTO) {
    UserResponse.OrderDTO respDTO = userService.createOrder(reqDTO);
    storeSseService.createOrderNotification(respDTO.getId(), respDTO.getStoreId()); // 이 부분

    return ResponseEntity.ok(new ApiUtil<>(respDTO));
}
Hyeonjeong-JANG commented 1 month ago

수고하셨어요.