실행 방법입니다. 외부 jar 는 필요 없습니다.
cd java-src\extension\16_async_job
javac -encoding UTF-8 *.java
java Main --demo # 7단계 시나리오 자동 실행, 약 2초인자 없이 java Main 만 실행하면 18086 포트에 서버만 띄우고 대기합니다.
JobManager — 상태·진행률·중복 방지·협력적 취소public synchronized Job submit(String owner, int total) {
String activeId = activeOwner.get(owner);
if (activeId != null) {
Job active = jobs.get(activeId);
if (active != null && isActive(active.status)) {
throw new IllegalStateException("이미 실행 중: " + activeId);
}
}
Job job = new Job(owner, total);
jobs.put(job.id, job);
activeOwner.put(owner, job.id);
pool.execute(() -> run(job)); // 큐 초과 시 RejectedExecutionException
return job;
}
private void run(Job job) {
job.status = "RUNNING";
for (int i = 0; i < job.total; i++) {
if (job.cancelRequested) {
job.status = "CANCELLED";
return;
}
// 실제로는 여기서 엑셀 한 행, 발송 한 건을 처리한다
job.progress.incrementAndGet();
}
job.status = "DONE";
}submit 은 synchronized 로 감싸 같은 owner 의 중복 확인과 등록 사이에 다른 요청이 끼어들지 못하게 합니다. run 은 매 반복마다 취소 플래그를 확인해 협력적으로 멈춥니다.
Main — 202 제출, 상태 조회, SSE, 취소, 결과 다운로드static void handleSubmit(HttpExchange ex) throws IOException {
Map<String, String> q = parseQuery(ex.getRequestURI().getRawQuery());
try {
JobManager.Job job = jobManager.submit(
q.getOrDefault("owner", "anon"),
Integer.parseInt(q.getOrDefault("total", "10")));
sendJson(ex, 202, "{\"id\":\"" + job.id + "\",\"statusUrl\":\"/jobs/" + job.id + "\"}");
} catch (IllegalStateException dup) {
sendJson(ex, 409, "{\"error\":\"" + esc(dup.getMessage()) + "\"}");
} catch (RejectedExecutionException full) {
sendJson(ex, 503, "{\"error\":\"작업 큐가 가득 찼습니다\"}");
}
}SSE 엔드포인트는 sendResponseHeaders(200, 0) 으로 길이를 0 으로 줘서 청크 전송을 사용하고, 상태가 끝날 때까지 50ms 간격으로 진행률을 흘려보냅니다.
ex.getResponseHeaders().add("Content-Type", "text/event-stream; charset=UTF-8");
ex.getResponseHeaders().add("X-Accel-Buffering", "no");
ex.sendResponseHeaders(200, 0);
OutputStream os = ex.getResponseBody();
while (true) {
os.write(("data: " + jobJson(job) + "\n\n").getBytes(StandardCharsets.UTF_8));
os.flush();
if (끝난_상태) break;
Thread.sleep(50);
}
os.close();--demo 로 실행하면 서버를 띄운 뒤 HttpClient 로 7단계를 자동 실행하고 스스로 종료합니다.
[1] 제출 -> 202 {"id":"4f1945d4","statusUrl":"/jobs/4f1945d4"}
[2] 폴링 1 -> {"status":"RUNNING","processed":7,"total":15,...}
[3] SSE data: {"status":"DONE","processed":15,"total":15,...} ← 마지막 이벤트
[4] 즉시 취소 후 상태 -> {"status":"CANCELLED","processed":0,...}
[5] 중복 제출 -> 409 {"error":"이미 실행 중인 작업이 있습니다: ..."}
[6] 결과 다운로드 -> OK: kim 작업 15건 처리 완료 (jobId=4f1945d4)
[7] 데모 종료, 서버 완전 종료 진행
서버 완전 종료 완료수동으로 확인하려면 서버를 띄운 뒤 아래처럼 호출합니다.
curl -X POST "http://localhost:18086/jobs?owner=kim&total=20"
curl "http://localhost:18086/jobs/{id}"
curl -N "http://localhost:18086/jobs/{id}/events"