폴링(Polling) 사용해서 CompletableFuture 결과 확인하기
📌 서론
이전 글에서 CompletableFuture를 사용해서 서버에서 비동기를 적용한 내용을 작성했다. 그 후에 서버에서 비동기로 처리한 결과를 클라이언트에게 어떻게 알려줄까?라는 게 궁금해져서 해당 내용을 알아보았다.
🔻 CompletableFuture가 무엇인지 궁금하다면? 🔻
CompletableFuture를 활용한 비동기 메일 전송 구현
폴링 (Polling)
비동기 작업에서 발생한 실패나 성공 결과를 클라이언트에게 알리기 위해 서버는 여러가지 방법을 사용할 수 있다. 이 방법들은 애플리케이션의 구조와 클라이언트와의 상호작용 방식에 따라 달라질 수 있다.
폴링은 클라이언트가 비동기 작업의 완료 여부를 주기적으로 서버에 확인(폴링)하는 과정을 말한다. 이 방법에서 클라이언트는 처음 비동기 작업을 요청한 후, 작업의 상태를 확인하기 위해 서버에 반복적으로 요청을 보낸다. 서버는 해당 작업의 상태(진행 중, 성공, 실패)에 대한 정보를 응답으로 제공한다.
(이전 글에서 메일 전송을 비동기로 처리했기 때문에 해당 mapping url을 /mail/status로 예시를 들었다.)
public enum TaskStatus {
COMPLETE, FAILED, IN_PROGRESS
}
private final MailService mailService;
@GetMapping("/mail/status/{taskId}")
public ResponseEntity<?> getMailSendStatus(@PathVariable String taskId) {
// taskId를 사용하여 비동기 작업의 상태 확인
TaskStatus status = mailService.checkTaskStatus(taskId);
// 상태에 따라 다른 응답 반환
if (status == TaskStatus.COMPLETE) {
return ResponseEntity.ok("메일 전송 성공");
} else if (status == TaskStatus.FAILED) {
return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body("메일 전송 실패. 다시 시도해주세요.");
} else {
return ResponseEntity.status(HttpStatus.PROCESSING).body("메일 전송 처리 중");
}
}
응답 반환
조회된 작업 상태(TaskStatus)에 따라 클라이언트에 다른 HTTP 응답을 반환한다. 세 가지 상태를 구분하여 처리한다:
- COMPLETE: 작업이 성공적으로 완료되었을 경우, "메일 전송 성공" 메시지와 함께 HTTP 200(OK) 응답을 반환한다.
- FAILED: 작업이 실패했을 경우, "메일 전송 실패. 다시 시도해주세요." 메시지와 함께 HTTP 417(EXPECTATION FAILED) 응답을 반환한다.
- 그 외 상태(예: IN_PROGRESS): 작업이 아직 처리 중임을 나타내는 "메일 전송 처리 중" 메시지와 함께 HTTP 102(PROCESSING) 응답을 반환한다.
✋ 스탑! 위 코드를 적용하려면 비동기 로직이 시작될때 taskId를 생성해줘야 한다!
taskId는 비동기 작업을 식별하는 고유한 식별자다. 클라이언트가 서버에 비동기 작업을 요청할 때, 서버는 이 작업을 식별하기 위한 고유한 ID를 생성하고 이를 클라이언트에 반환해야 한다. 클라이언트는 이 taskId를 사용하여 작업의 상태를 조회할 수 있다. 이 방식은 비동기 작업의 진행 상태를 추적하고, 작업 완료 후 결과를 확인하는 데 유용하다.
비동기 작업 요청과 taskId 반환 예시
서버가 비동기 작업(예: 메일 전송)을 시작할 때, 작업을 식별할 수 있는 고유한 ID를 생성하고, 이를 클라이언트에게 응답으로 전달한다.
기존에 사용했던 MailService 클래스는 다음과 같았다. (이전 글에 작성된 MailService와 동일하다.)
/**
* mail 전송을 위한 서비스
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class MailService {
private final JavaMailSender emailSender;
@Async
public CompletableFuture<Boolean> sendTemporaryPassword(String to, String temporaryPassword) {
try {
// .. 메일 전송 로직
log.info("{} 로 메일 전송 성공", to);
return CompletableFuture.completedFuture(true);
} catch (Exception e) {
log.error(to + " 로 메일 전송 실패", e);
return CompletableFuture.completedFuture(false);
}
}
}
이 코드를 다음과 같이 수정한다.
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Service
@Slf4j
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender emailSender;
private final ConcurrentHashMap<String, String> taskStatusMap = new ConcurrentHashMap<>();
// 비동기 작업을 시작하고 고유 ID 생성
public String startSendMailProcess(MailRequest mailRequest) {
String taskId = UUID.randomUUID().toString(); // 고유 작업 ID 생성
CompletableFuture.runAsync(() -> {
try {
sendMailInternal(mailRequest.getEmail(), mailRequest.getTemporaryPassword());
taskStatusMap.put(taskId, "SUCCESS");
} catch (Exception e) {
log.error("메일 전송 실패: {}", e.getMessage());
taskStatusMap.put(taskId, "FAILURE");
}
});
return taskId;
}
private void sendMailInternal(String to, String temporaryPassword) {
// 메일 전송 로직 (기존 sendTemporaryPassword 메서드 로직)
log.info("{} 로 메일 전송 성공", to);
}
// 작업 상태 조회
public String checkTaskStatus(String taskId) {
return taskStatusMap.getOrDefault(taskId, "IN_PROGRESS");
}
}
수정된 부분
CompletableFuture.runAsync()의 사용
기존의 @Async 어노테이션 대신 CompletableFuture.runAsync()를 사용하여 비동기 작업을 직접 제어한다. 이 방식을 통해 비동기 작업을 시작하고, 작업의 성공 또는 실패 여부를 ConcurrentHashMap에 저장한다.
고유 작업 ID(taskId) 생성 및 관리
각 비동기 작업을 시작할 때마다 고유한 taskId를 생성하고, 이를 사용하여 작업의 상태를 관리한다. 이 taskId는 작업의 상태를 조회할 때 사용된다.
ConcurrentHashMap을 이용한 작업 상태 관리
비동기 작업의 상태(성공, 실패, 진행 중)를 저장하고 조회하기 위해 ConcurrentHashMap을 사용한다. 이는 멀티 스레드 환경에서의 스레드 안전성을 보장한다.
장점
비동기 작업의 세밀한 제어
CompletableFuture.runAsync()를 사용함으로써 개발자는 비동기 작업의 시작, 완료 및 예외 처리 등을 보다 세밀하게 제어할 수 있다. 이는 비동기 로직의 유연성과 확장성을 향상한다.
작업 상태 추적의 용이성
고유한 taskId를 통해 각 비동기 작업을 식별하고, 그 상태를 추적할 수 있다. 이는 클라이언트가 작업의 진행 상태를 정확하게 알 수 있게 하며, 작업 완료 후 결과 처리를 용이하게 한다.
스레드 안전한 상태 관리:
ConcurrentHashMap의 사용은 멀티 스레드 환경에서 작업 상태의 스레드 안전한 관리를 보장한다. 이는 데이터 일관성과 정확성을 유지하는 데 중요하다.
확장성과 유지보수성의 향상:
비동기 작업의 상태 관리 로직을 명확히 분리함으로써, 코드의 확장성과 유지보수성이 향상된다. 또한, 비동기 작업의 추가, 변경, 제거가 용이해진다.
이러한 변경을 통해, 애플리케이션의 비동기 처리 로직이 더욱 견고하고 유연하게 구성될 수 있으며, 클라이언트와 서버 간의 상호작용이 향상된다.
그리고 기존에 메일 전송을 요청하는 컨트롤러의 코드도 다음과 같이 변경해 준다.
@PostMapping("/sendMail")
public ResponseEntity<?> sendMail(@RequestBody MailRequest mailRequest) {
// 비동기 작업을 시작하고 고유 ID 생성
String taskId = mailService.startSendMailProcess(mailRequest);
// 생성된 taskId를 클라이언트에게 반환
return ResponseEntity.ok().body(Map.of("taskId", taskId, "message", "메일 전송을 시작했습니다. 진행 상태를 확인하세요."));
}
클라이언트는 이 taskId를 저장하고, 작업의 상태를 주기적으로 확인하거나, 필요한 경우 서버에 작업의 상태를 조회할 때 사용한다.
아직 실제 프로젝트에 적용을 하진 못했지만 이런 방법도 있구나 하고 알아가면 좋을 것 같다. 이 방법 외에도 웹 소캣을 이용하는 방법도 있다. 이 부분은 웹 소캣에 대해서 더 알아보고 정리해야겠다.
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
Java 제네릭의 이해: 제네릭 메서드 (0) | 2024.05.23 |
---|---|
중첩 foreach 제거: Function.identity()로 코드 간결화하기 (0) | 2024.05.23 |
CompletableFuture를 활용한 비동기 메일 전송 구현 (1) | 2024.02.06 |
AtomicInteger를 활용한 파일 저장 순서 동기화 문제 해결 (0) | 2024.02.04 |
[JAVA] 코드 최적화를 위한 매직 상수 사용법 (4) | 2023.12.31 |