게시판 탐색 이력과 작업 큐
두 Stack으로 게시판 탐색 이력을 이동하고 FIFO Queue로 알림 작업을 처리하며 실패 복구 위치까지 설계합니다.
자료 구조는 실제 기능의 상태 전이를 설명할 때 가장 잘 이해됩니다.
브라우저 이력은 현재 페이지와 back·앞으로 Stack 사이를 이동하고, 프린터는 제출 순서대로 Queue에서 작업을 꺼냅니다.
두 예제 모두 “어느 끝에서 넣고 빼는가”와 실패 시 원소가 어느 구조에 남는가를 명시합니다.
Stack은 나중에 넣은 원소를 먼저 꺼내는 LIFO이고, 이 절의 Queue는 먼저 넣은 원소를 먼저 꺼내는 FIFO입니다. 같은 ArrayDeque라도 push/pop은 앞쪽 한 끝을 사용하고, offer/poll은 뒤에 넣고 앞에서 꺼냅니다. 클래스 이름만 보고 순서를 판단하지 않고 사용하는 연산의 끝을 확인합니다.
forward 이력 초기화
A→B→C 방문 뒤 back으로 B에 왔다가 D를 새로 방문하면 C로 가는 앞으로 경로는 더 이상 유효하지 않습니다.
다음 잘못된 구현은 앞으로 Stack에 C를 남깁니다. 실제 코드는 이동하지 않고 invalid-forward=C를 출력합니다.
import java.util.ArrayDeque;
import java.util.Deque;
public final class StaleForwardHistoryBug {
public static void main(String[] args) {
Deque<String> back = new ArrayDeque<>(), forward = new ArrayDeque<>();
String current = "A";
back.push(current);
current = "B";
back.push(current);
current = "C";
forward.push(current);
current = back.pop();
back.push(current);
current = "D";
System.out.println("current=" + current + ", invalid-forward=" + forward.peek());
}
}새 분기가 생기면 forward.clear()가 visit 동작에 포함되어야 합니다. 이 코드는 단일 스레드 예제이며 원자적 갱신이나 실패 시 롤백을 구현하지 않습니다.
Stack 연산만 맞아도 기능 상태 기계 규칙을 빠뜨리면 결과는 틀립니다.
현재 페이지는 Stack 밖의 명시적 상태로 배치
back을 누르면 현재 페이지를 앞으로 스택에 push하고 back 스택에서 새 current를 pop합니다.
앞으로 이동할 때는 반대로 현재 페이지를 back 스택에 push한 뒤 앞으로 스택에서 다음 페이지를 꺼냅니다.
빈 Stack이면 current를 바꾸지 않고 false를 반환합니다.
이력의 home과 방문할 page는 null이 아닌 페이지 이름이라는 전제입니다. ArrayDeque는 null 원소를 허용하지 않지만 current는 별도 필드이므로 visit(null) 자체가 곧바로 거부되지는 않습니다. 이 호출은 current를 null로 바꾼 뒤 다음 back()에서 push(current)를 실패하게 합니다. 외부 입력을 받도록 확장한다면 생성자와 visit의 상태 변경 전에 Objects.requireNonNull로 이 전제를 검사해야 합니다.
import java.util.ArrayDeque;
import java.util.Deque;
public final class BrowserHistory {
public static void main(String[] args) {
History h = new History("A");
h.visit("B");
h.visit("C");
h.back();
h.visit("D");
System.out.println(h);
System.out.println("forward=" + h.forward());
h.back();
h.back();
System.out.println(h);
}
private static final class History {
private final Deque<String> back = new ArrayDeque<>(), forward = new ArrayDeque<>();
private String current;
History(String home) {
current = home;
}
void visit(String page) {
back.push(current);
current = page;
forward.clear();
}
boolean back() {
if (back.isEmpty()) return false;
forward.push(current);
current = back.pop();
return true;
}
boolean forward() {
if (forward.isEmpty()) return false;
back.push(current);
current = forward.pop();
return true;
}
public String toString() {
return "back=" + back + ", current=" + current + ", forward=" + forward;
}
}
}top부터 읽는 두 이력과 새 방문의 초기화
방문·뒤로·새 방문·두 번 뒤로 뒤 forward 거부까지 두 스택과 current가 바뀝니다. 같은 영역에서 스택 top과 폐기된 C 경로를 추적할 수 있습니다.
BrowserHistory의 실제 호출 순서대로 back, current, forward를 추적하면 D 방문이 C 경로를 비우고 마지막 두 번의 back은 A로 돌아갑니다.
HISTORY · STATE TRACE
top부터 읽는 두 이력과 새 방문의 초기화
새 visit는 앞으로 이력을 비웁니다. back·current·forward를 호출 직후 상태로 비교합니다.
| main의 생성·호출 순서 | back · top부터 | current | forward · top부터 |
|---|---|---|---|
new History("A") | [] | A | [] |
visit("B") | [A] | B | [] |
visit("C") | [B, A] | C | [] |
첫 back() | [A] | B | [C] |
visit("D") | [B, A] | D | C 경로 삭제: [] |
forward() → false | [B, A] | D | [] |
둘째 back() | [A] | B | [D] |
셋째 back() | [] | A | [B, D] |
new History("A")- back · top부터
[]- current
A- forward · top부터
[]
visit("B")- back · top부터
[A]- current
B- forward · top부터
[]
visit("C")- back · top부터
[B, A]- current
C- forward · top부터
[]
- 첫
back() - back · top부터
[A]- current
B- forward · top부터
[C]
visit("D")- back · top부터
[B, A]- current
D- forward · top부터
- C 경로 삭제:
[]
forward()→false- back · top부터
[B, A]- current
D- forward · top부터
[]
- 둘째
back() - back · top부터
[A]- current
B- forward · top부터
[D]
- 셋째
back() - back · top부터
[]- current
A- forward · top부터
[B, D]
두 이력은 맨 왼쪽이 push/pop하는 top입니다. 각 행은 호출 직후의 소스 추적입니다. 실제 출력은 D 방문 뒤 상태, forward=false, 마지막 A 상태의 세 줄이며, back() 반환값은 출력하지 않습니다.
이 예의 조건
- 스택은 top부터 표시하며 forward 거절은 상태를 바꾸지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원래 main의 C 방문/첫 back B/새 D 방문/마지막 두 back A의 네 주요 상태입니다. D에서 forward false는 같은 D로 돌아오는 거부 루프이며 두 back 전이를 구분합니다. 모든 목록은 top부터 표시하고 current는 별도이며 최종 forward [B,D]를 보존합니다. |
| 4 | back=[B,A] |
| 5 | current=C |
| 8 | back=[A] |
| 9 | current=B |
| 12 | back=[B,A] |
| 13 | current=D |
| 15 | C 경로 폐기 |
| 17 | back=[] |
| 18 | current=A |
| 20 | visit(D) |
| 21 | forward 비움 |
| 22 | forward()=false |
| 23 | 변화 없음 |
| 24 | 실선: 원래 main의 상태 변경/거부 · 목록은 top부터 |
| 25 | C 경로 폐기 후 두 back의 최종은 A/forward [B,D] |
| 구분 | 내용 |
|---|---|
| 설명 | 원래 main의 C 방문/첫 back B/새 D 방문/마지막 두 back A의 네 주요 상태입니다. D에서 forward false는 같은 D로 돌아오는 거부 루프이며 두 back 전이를 구분합니다. 모든 목록은 top부터 표시하고 current는 별도이며 최종 forward [B,D]를 보존합니다. |
| 전체 읽기 정보 | top부터 읽는 두 이력과 새 방문의 초기화 top부터 읽는 두 이력과 새 방문의 초기화 BrowserHistory의 실제 호출 순서대로 back, current, forward를 추적하면 D 방문이 C 경로를 비우고 마지막 두 번의 back은 A로 돌아갑니다. HISTORY · STATE TRACE top부터 읽는 두 이력과 새 방문의 초기화 새 visit는 앞으로 이력을 비웁니다. back·current·forward를 호출 직후 상태로 비교합니다. top부터 읽는 두 이력과 새 방문의 초기화 main의 생성·호출 순서 back · top부터 current forward · top부터 new History("A") [] A [] visit("B") [A] B [] visit("C") [B, A] C [] 첫 back() [A] B [C] visit("D") [B, A] D C 경로 삭제: [] forward() → false [B, A] D [] 둘째 back() [A] B [D] 셋째 back() [] A [B, D] new History("A") back · top부터 [] current A forward · top부터 [] visit("B") back · top부터 [A] current B forward · top부터 [] visit("C") back · top부터 [B, A] current C forward · top부터 [] 첫 back() back · top부터 [A] current B forward · top부터 [C] visit("D") back · top부터 [B, A] current D forward · top부터 C 경로 삭제: [] forward() → false back · top부터 [B, A] current D forward · top부터 [] 둘째 back() back · top부터 [A] current B forward · top부터 [D] 셋째 back() back · top부터 [] current A forward · top부터 [B, D] 두 이력은 맨 왼쪽이 push/pop 하는 top입니다. 각 행은 호출 직후의 소스 추적입니다. 실제 출력은 D 방문 뒤 상태, forward=false , 마지막 A 상태의 세 줄이며, back() 반환값은 출력하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| B/C 방문 후 현재 C와 과거 두 위치가 있습니다. |
|
|
| 첫 back B로 돌아가 C를 forward에 넣습니다. |
|
|
| 새 D 방문 앞으로 갈 C 경로를 폐기합니다. |
|
|
| 두 back B를 거쳐 A에 도달합니다. |
|
|
top부터 읽는 두 이력과 새 방문의 초기화
방문·뒤로·새 방문·두 번 뒤로 뒤 forward 거부까지 두 스택과 current가 바뀝니다. 같은 영역에서 스택 top과 폐기된 C 경로를 추적할 수 있습니다.
BrowserHistory의 실제 호출 순서대로 back, current, forward를 추적하면 D 방문이 C 경로를 비우고 마지막 두 번의 back은 A로 돌아갑니다.
HISTORY · STATE TRACE
top부터 읽는 두 이력과 새 방문의 초기화
새 visit는 앞으로 이력을 비웁니다. back·current·forward를 호출 직후 상태로 비교합니다.
| main의 생성·호출 순서 | back · top부터 | current | forward · top부터 |
|---|---|---|---|
new History("A") | [] | A | [] |
visit("B") | [A] | B | [] |
visit("C") | [B, A] | C | [] |
첫 back() | [A] | B | [C] |
visit("D") | [B, A] | D | C 경로 삭제: [] |
forward() → false | [B, A] | D | [] |
둘째 back() | [A] | B | [D] |
셋째 back() | [] | A | [B, D] |
new History("A")- back · top부터
[]- current
A- forward · top부터
[]
visit("B")- back · top부터
[A]- current
B- forward · top부터
[]
visit("C")- back · top부터
[B, A]- current
C- forward · top부터
[]
- 첫
back() - back · top부터
[A]- current
B- forward · top부터
[C]
visit("D")- back · top부터
[B, A]- current
D- forward · top부터
- C 경로 삭제:
[]
forward()→false- back · top부터
[B, A]- current
D- forward · top부터
[]
- 둘째
back() - back · top부터
[A]- current
B- forward · top부터
[D]
- 셋째
back() - back · top부터
[]- current
A- forward · top부터
[B, D]
두 이력은 맨 왼쪽이 push/pop하는 top입니다. 각 행은 호출 직후의 소스 추적입니다. 실제 출력은 D 방문 뒤 상태, forward=false, 마지막 A 상태의 세 줄이며, back() 반환값은 출력하지 않습니다.
이 예의 조건
- 스택은 top부터 표시하며 forward 거절은 상태를 바꾸지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원래 main의 C 방문/첫 back B/새 D 방문/마지막 두 back A의 네 주요 상태입니다. D에서 forward false는 같은 D로 돌아오는 거부 루프이며 두 back 전이를 구분합니다. 모든 목록은 top부터 표시하고 current는 별도이며 최종 forward [B,D]를 보존합니다. |
| 4 | back=[B,A] |
| 5 | current=C |
| 8 | back=[A] |
| 9 | current=B |
| 12 | back=[B,A] |
| 13 | current=D |
| 15 | C 경로 폐기 |
| 17 | back=[] |
| 18 | current=A |
| 20 | visit(D) |
| 21 | forward 비움 |
| 22 | forward()=false |
| 23 | 변화 없음 |
| 24 | 실선: 원래 main의 상태 변경/거부 · 목록은 top부터 |
| 25 | C 경로 폐기 후 두 back의 최종은 A/forward [B,D] |
| 구분 | 내용 |
|---|---|
| 설명 | 원래 main의 C 방문/첫 back B/새 D 방문/마지막 두 back A의 네 주요 상태입니다. D에서 forward false는 같은 D로 돌아오는 거부 루프이며 두 back 전이를 구분합니다. 모든 목록은 top부터 표시하고 current는 별도이며 최종 forward [B,D]를 보존합니다. |
| 전체 읽기 정보 | top부터 읽는 두 이력과 새 방문의 초기화 top부터 읽는 두 이력과 새 방문의 초기화 BrowserHistory의 실제 호출 순서대로 back, current, forward를 추적하면 D 방문이 C 경로를 비우고 마지막 두 번의 back은 A로 돌아갑니다. HISTORY · STATE TRACE top부터 읽는 두 이력과 새 방문의 초기화 새 visit는 앞으로 이력을 비웁니다. back·current·forward를 호출 직후 상태로 비교합니다. top부터 읽는 두 이력과 새 방문의 초기화 main의 생성·호출 순서 back · top부터 current forward · top부터 new History("A") [] A [] visit("B") [A] B [] visit("C") [B, A] C [] 첫 back() [A] B [C] visit("D") [B, A] D C 경로 삭제: [] forward() → false [B, A] D [] 둘째 back() [A] B [D] 셋째 back() [] A [B, D] new History("A") back · top부터 [] current A forward · top부터 [] visit("B") back · top부터 [A] current B forward · top부터 [] visit("C") back · top부터 [B, A] current C forward · top부터 [] 첫 back() back · top부터 [A] current B forward · top부터 [C] visit("D") back · top부터 [B, A] current D forward · top부터 C 경로 삭제: [] forward() → false back · top부터 [B, A] current D forward · top부터 [] 둘째 back() back · top부터 [A] current B forward · top부터 [D] 셋째 back() back · top부터 [] current A forward · top부터 [B, D] 두 이력은 맨 왼쪽이 push/pop 하는 top입니다. 각 행은 호출 직후의 소스 추적입니다. 실제 출력은 D 방문 뒤 상태, forward=false , 마지막 A 상태의 세 줄이며, back() 반환값은 출력하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| B/C 방문 후 현재 C와 과거 두 위치가 있습니다. |
|
|
| 첫 back B로 돌아가 C를 forward에 넣습니다. |
|
|
| 새 D 방문 앞으로 갈 C 경로를 폐기합니다. |
|
|
| 두 back B를 거쳐 A에 도달합니다. |
|
|
이 예제의 ArrayDeque는 push와 pop을 맨 앞에서 수행하며, 디버깅 출력도 top부터 나열합니다.
사용자는 현재 페이지와 이동 성공 여부를 봅니다.
방문할 때 앞으로를 비우는 규칙이 분기 이력을 일관되게 만듭니다.
원 main의 마지막은 back=[], current=A, forward=[B, D]입니다. 여기서 forward()를 세 번 더 누르면 성공 여부는 true, true, false이고 current는 B, D, D입니다. 세 번째는 빈 앞으로 스택이므로 두 이력과 current를 그대로 유지합니다. 새 방문에서 앞으로를 비우는 것과, 빈 스택에서 이동을 거부하는 것은 서로 다른 상태 전이입니다.
프린터 Queue의 실패 작업 소유권
poll 후 인쇄가 실패하면 작업은 대기 중에서 이미 사라졌습니다.
재시도하려면 실패 작업을 재시도 Queue에 넣거나 성공 후에만 제거하는 peek/remove 패턴을 사용합니다.
후자는 같은 실패 작업이 뒤 작업을 영원히 막을 수 있어 최대 재시도와 dead-letter 정책이 필요합니다.
다음 예제는 대기 중에서 꺼낸 작업이 실패하면 attempts를 늘려 뒤에 다시 넣고, 세 번째 실패에서는 dead letter로 보냅니다. print는 이름으로 성공 여부를 판정하는 모의 함수이며 실제 인쇄를 수행하지 않습니다.
성공 작업은 done 목록에 기록합니다.
poll은 앞 작업을 제거해 반환하므로 그 직후에는 지역 변수 job이 처리 대상을 들고 있습니다. 성공하면 done, 아직 실패 한도 미만이면 pending의 뒤, 한도에 도달하면 dead 중 한 곳으로 결과를 넘깁니다. 여기서 dead letter는 자동 재시도를 끝낸 작업의 보류 목록이며, 예제의 dead에는 작업 이름만 저장합니다. 빈 큐의 poll은 null을 반환하지만 이 단일 스레드 반복문은 먼저 isEmpty()를 확인하므로 빈 상태에서 작업 메서드를 호출하지 않습니다.
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public final class PrinterRetryQueue {
public static void main(String[] args) {
Deque<Job> pending = new ArrayDeque<>();
pending.add(new Job("notes", 0));
pending.add(new Job("broken", 0));
List<String> done = new ArrayList<>(), dead = new ArrayList<>();
while (!pending.isEmpty()) {
Job job = pending.poll();
if (print(job)) {
done.add(job.name());
continue;
}
Job retried = new Job(job.name(), job.attempts() + 1);
if (retried.attempts() >= 3) dead.add(job.name());
else pending.offer(retried);
}
System.out.println("done=" + done + ", dead=" + dead);
}
private static boolean print(Job job) {
return !job.name().equals("broken");
}
private record Job(String name, int attempts) {}
}broken은 세 번 실패하고 두 번만 다시 대기한다
notes 성공 뒤 broken 세 실패가 재삽입 두 번과 dead 이동을 만듭니다. pending/처리중/done/dead 소유와 attempts 변화가 핵심입니다.
notes는 한 번에 완료되고 broken은 실패 횟수 0, 1, 2로 꺼내져 세 번째 실패 뒤 dead 목록으로 이동합니다.
QUEUE · RETRY LIMIT
broken은 세 번 실패하고 두 번만 다시 대기한다
attempts는 꺼내기 전의 누적 실패 수입니다. 세 번째 실패는 재삽입 대신 dead로 이동합니다.
notesattempts=0- 판정과 이번 조치
- 모의
print성공done.add("notes") - 조치 뒤 대기·결과
pending=[broken(0)]done=[notes]dead=[]
brokenattempts=0- 판정과 이번 조치
- 첫 실패:
0 → 1offer로 뒤에 재삽입 - 조치 뒤 대기·결과
pending=[broken(1)]done=[notes]dead=[]
brokenattempts=1- 판정과 이번 조치
- 둘째 실패:
1 → 2offer로 뒤에 재삽입 - 조치 뒤 대기·결과
pending=[broken(2)]done=[notes]dead=[]
brokenattempts=2- 판정과 이번 조치
- 셋째 실패:
2 → 3dead.add("broken") - 조치 뒤 대기·결과
pending=[]done=[notes]dead=[broken]
broken(n)의 n은 꺼내기 전까지 누적된 실패 횟수 attempts입니다. 처음 시도와 재시도 두 번이 모두 실패합니다. 이 코드는 이름으로 성공 여부를 판정하며 실제 인쇄를 하지 않습니다. 중간 상태는 소스 추적이고 최종 출력은 done=[notes], dead=[broken]입니다.
이 예의 조건
- 모의 출력 상태이며 실제 프린터 장치 실행은 0이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | notes 성공의 done과 broken 처리를 별도 영역에 보입니다. broken을 꺼낼 때 attempts는 0/1/2이고 실패 뒤 1/2는 pending 재삽입, 3은 dead입니다. 실선은 각 작업의 소유 이동이며 세 번째 재삽입은 없습니다. 원래 실제 장치 실행은 0. |
| 8 | 다시 pending에 넣지 않음 |
| 11 | 첫 실패 |
| 12 | 새 attempts 1 |
| 15 | 둘째 실패 |
| 16 | 새 attempts 2 |
| 19 | 셋째 실패 |
| 20 | 새 attempts 3 |
| 23 | 다시 pending에 넣지 않음 |
| 24 | 실선: 작업별 실패 뒤 소유 이동 |
| 25 | 최초 시도 1+재시도 2=실패 3 |
| 26 | 원래 main의 notes 완료 뒤 broken 처리 |
| 구분 | 내용 |
|---|---|
| 설명 | notes 성공의 done과 broken 처리를 별도 영역에 보입니다. broken을 꺼낼 때 attempts는 0/1/2이고 실패 뒤 1/2는 pending 재삽입, 3은 dead입니다. 실선은 각 작업의 소유 이동이며 세 번째 재삽입은 없습니다. 원래 실제 장치 실행은 0. |
| 전체 읽기 정보 | broken은 세 번 실패하고 두 번만 다시 대기한다 broken은 세 번 실패하고 두 번만 다시 대기한다 notes는 한 번에 완료되고 broken은 실패 횟수 0, 1, 2로 꺼내져 세 번째 실패 뒤 dead 목록으로 이동합니다. QUEUE · RETRY LIMIT broken은 세 번 실패하고 두 번만 다시 대기한다 attempts는 꺼내기 전의 누적 실패 수입니다. 세 번째 실패는 재삽입 대신 dead로 이동합니다. notes attempts=0 판정과 이번 조치 모의 print 성공 done.add("notes") 조치 뒤 대기·결과 pending=[broken(0)] done=[notes] dead=[] broken attempts=0 판정과 이번 조치 첫 실패: 0 → 1 offer 로 뒤에 재삽입 조치 뒤 대기·결과 pending=[broken(1)] done=[notes] dead=[] broken attempts=1 판정과 이번 조치 둘째 실패: 1 → 2 offer 로 뒤에 재삽입 조치 뒤 대기·결과 pending=[broken(2)] done=[notes] dead=[] broken attempts=2 판정과 이번 조치 셋째 실패: 2 → 3 dead.add("broken") 조치 뒤 대기·결과 pending=[] done=[notes] dead=[broken] broken(n) 의 n은 꺼내기 전까지 누적된 실패 횟수 attempts 입니다. 처음 시도와 재시도 두 번이 모두 실패합니다. 이 코드는 이름으로 성공 여부를 판정하며 실제 인쇄를 하지 않습니다. 중간 상태는 소스 추적이고 최종 출력은 done=[notes], dead=[broken] 입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 정상 작업 notes를 꺼내 성공 후 done에 보관합니다. |
|
|
| 두 재삽입 broken의 첫/둘째 실패 뒤 attempts가 1/2인 작업을 다시 큐에 넣습니다. |
|
|
| 마지막 실패 세 번째 실패 뒤 attempts가 3이 된 작업을 dead로 옮깁니다. |
|
|
broken은 세 번 실패하고 두 번만 다시 대기한다
notes 성공 뒤 broken 세 실패가 재삽입 두 번과 dead 이동을 만듭니다. pending/처리중/done/dead 소유와 attempts 변화가 핵심입니다.
notes는 한 번에 완료되고 broken은 실패 횟수 0, 1, 2로 꺼내져 세 번째 실패 뒤 dead 목록으로 이동합니다.
QUEUE · RETRY LIMIT
broken은 세 번 실패하고 두 번만 다시 대기한다
attempts는 꺼내기 전의 누적 실패 수입니다. 세 번째 실패는 재삽입 대신 dead로 이동합니다.
notesattempts=0- 판정과 이번 조치
- 모의
print성공done.add("notes") - 조치 뒤 대기·결과
pending=[broken(0)]done=[notes]dead=[]
brokenattempts=0- 판정과 이번 조치
- 첫 실패:
0 → 1offer로 뒤에 재삽입 - 조치 뒤 대기·결과
pending=[broken(1)]done=[notes]dead=[]
brokenattempts=1- 판정과 이번 조치
- 둘째 실패:
1 → 2offer로 뒤에 재삽입 - 조치 뒤 대기·결과
pending=[broken(2)]done=[notes]dead=[]
brokenattempts=2- 판정과 이번 조치
- 셋째 실패:
2 → 3dead.add("broken") - 조치 뒤 대기·결과
pending=[]done=[notes]dead=[broken]
broken(n)의 n은 꺼내기 전까지 누적된 실패 횟수 attempts입니다. 처음 시도와 재시도 두 번이 모두 실패합니다. 이 코드는 이름으로 성공 여부를 판정하며 실제 인쇄를 하지 않습니다. 중간 상태는 소스 추적이고 최종 출력은 done=[notes], dead=[broken]입니다.
이 예의 조건
- 모의 출력 상태이며 실제 프린터 장치 실행은 0이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | notes 성공의 done과 broken 처리를 별도 영역에 보입니다. broken을 꺼낼 때 attempts는 0/1/2이고 실패 뒤 1/2는 pending 재삽입, 3은 dead입니다. 실선은 각 작업의 소유 이동이며 세 번째 재삽입은 없습니다. 원래 실제 장치 실행은 0. |
| 8 | 다시 pending에 넣지 않음 |
| 11 | 첫 실패 |
| 12 | 새 attempts 1 |
| 15 | 둘째 실패 |
| 16 | 새 attempts 2 |
| 19 | 셋째 실패 |
| 20 | 새 attempts 3 |
| 23 | 다시 pending에 넣지 않음 |
| 24 | 실선: 작업별 실패 뒤 소유 이동 |
| 25 | 최초 시도 1+재시도 2=실패 3 |
| 26 | 원래 main의 notes 완료 뒤 broken 처리 |
| 구분 | 내용 |
|---|---|
| 설명 | notes 성공의 done과 broken 처리를 별도 영역에 보입니다. broken을 꺼낼 때 attempts는 0/1/2이고 실패 뒤 1/2는 pending 재삽입, 3은 dead입니다. 실선은 각 작업의 소유 이동이며 세 번째 재삽입은 없습니다. 원래 실제 장치 실행은 0. |
| 전체 읽기 정보 | broken은 세 번 실패하고 두 번만 다시 대기한다 broken은 세 번 실패하고 두 번만 다시 대기한다 notes는 한 번에 완료되고 broken은 실패 횟수 0, 1, 2로 꺼내져 세 번째 실패 뒤 dead 목록으로 이동합니다. QUEUE · RETRY LIMIT broken은 세 번 실패하고 두 번만 다시 대기한다 attempts는 꺼내기 전의 누적 실패 수입니다. 세 번째 실패는 재삽입 대신 dead로 이동합니다. notes attempts=0 판정과 이번 조치 모의 print 성공 done.add("notes") 조치 뒤 대기·결과 pending=[broken(0)] done=[notes] dead=[] broken attempts=0 판정과 이번 조치 첫 실패: 0 → 1 offer 로 뒤에 재삽입 조치 뒤 대기·결과 pending=[broken(1)] done=[notes] dead=[] broken attempts=1 판정과 이번 조치 둘째 실패: 1 → 2 offer 로 뒤에 재삽입 조치 뒤 대기·결과 pending=[broken(2)] done=[notes] dead=[] broken attempts=2 판정과 이번 조치 셋째 실패: 2 → 3 dead.add("broken") 조치 뒤 대기·결과 pending=[] done=[notes] dead=[broken] broken(n) 의 n은 꺼내기 전까지 누적된 실패 횟수 attempts 입니다. 처음 시도와 재시도 두 번이 모두 실패합니다. 이 코드는 이름으로 성공 여부를 판정하며 실제 인쇄를 하지 않습니다. 중간 상태는 소스 추적이고 최종 출력은 done=[notes], dead=[broken] 입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 정상 작업 notes를 꺼내 성공 후 done에 보관합니다. |
|
|
| 두 재삽입 broken의 첫/둘째 실패 뒤 attempts가 1/2인 작업을 다시 큐에 넣습니다. |
|
|
| 마지막 실패 세 번째 실패 뒤 attempts가 3이 된 작업을 dead로 옮깁니다. |
|
|
Queue 뒤에 재삽입하므로 실패 작업 사이에 다른 작업이 처리될 수 있습니다.
순서 절대 보장이 요구되면 정책이 달라집니다.
자료 구조 선택은 전달 의미와 함께 설계해야 합니다.
최대 시도 3번은 최초 시도 1번과 재시도 2번입니다. 처음 Job의 attempts=0은 아직 한 번도 실패하지 않았다는 뜻이고, 실패 직후 1·2·3으로 올라갑니다. 세 번째 실패에서 dead로 옮기는 조건이므로 attempts=3인 작업을 다시 인쇄하는 경로는 없습니다.
다른 대기 순서로 따라가기
새 빈 큐에 broken(0), notes(0)을 이 순서로 넣었다고 합시다. broken의 첫 실패 뒤 notes가 앞에 남으므로 notes가 먼저 완료되고 broken(1)이 다음에 꺼내집니다. 최종 done=[notes], dead=[broken]은 같지만, 원 main의 처음 notes 성공 다음 broken 실패와 실행 순서는 다릅니다. 이것은 이름으로 실패를 정하는 유한 모의 실행이며 인쇄 장치의 중복 출력·프로세스 중단 복구는 포함하지 않습니다.
큐에 등록한 명령의 단일 스레드 실행
실제 스레드를 도입하기 전에 Queue 소비 규칙을 단일 스레드로 확인합니다.
등록, 공개, 출력 명령을 FIFO로 처리하고 각 명령 결과를 누적 상태에서 확인합니다.
동시성은 18장에서 별도로 추가합니다.
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.Map;
public final class PostTaskQueueCli {
public static void main(String[] args) {
Deque<Command> queue = new ArrayDeque<>();
queue.offer(new Add("map", 40));
queue.offer(new Add("queue", 50));
queue.offer(new Publish("map"));
queue.offer(new Print());
State state = new State();
while (!queue.isEmpty()) {
queue.poll().execute(state);
}
}
private interface Command {
void execute(State s);
}
private record Add(String title, int viewCount) implements Command {
public void execute(State s) {
s.viewCount.put(title, viewCount);
}
}
private record Publish(String title) implements Command {
public void execute(State s) {
if (!s.viewCount.containsKey(title)) throw new IllegalArgumentException(title);
s.published.put(title, true);
}
}
private record Print() implements Command {
public void execute(State s) {
System.out.println("viewCount=" + s.viewCount + ", published=" + s.published);
}
}
private static final class State {
final Map<String, Integer> viewCount = new LinkedHashMap<>();
final Map<String, Boolean> published = new LinkedHashMap<>();
}
}네 명령을 순서대로 실행하고 Print에서만 출력한다
FIFO의 두 Add·Publish·Print가 같은 State를 변경하고 마지막에만 출력합니다. 명령별 상태 변화와 stdout 유무를 따라가기 좋은 사례입니다.
두 Add가 조회수 상태를 채운 뒤 Publish가 map을 공개하고 마지막 Print가 두 누적 상태를 한 줄로 출력합니다.
QUEUE · COMMAND TRACE
네 명령을 순서대로 실행하고 Print에서만 출력한다
앞에서 꺼낸 명령이 같은 상태를 차례로 갱신합니다. 출력은 마지막 Print 한 번입니다.
실선:같은스레드 FIFO 실행각poll 뒤execute를 마침첫3명령은 stdout없음
Add("map", 40)- 명령이 바꾸는 상태
viewCount={map=40}- 표준 출력
- 없음
Add("queue", 50)- 명령이 바꾸는 상태
viewCount={map=40, queue=50}- 표준 출력
- 없음
Publish("map")- 명령이 바꾸는 상태
published={map=true}- 표준 출력
- 없음
Print()- 명령이 바꾸는 상태
- 상태 변경 없음
- 표준 출력
viewCount={map=40, queue=50},published={map=true}
처음에는 두 Map이 비어 있습니다. poll().execute(state)가 명령 하나를 같은 스레드에서 끝낸 뒤 다음 명령을 꺼냅니다. 마지막 셀의 두 부분은 실제로 한 줄의 출력이며, 두 Map의 나열은 이 코드의 LinkedHashMap 삽입 순서입니다.
이 예의 조건
- 동일 스레드 FIFO이며 각 execute를 마친 뒤 다음 poll한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원래 Add(map, 40)/Add(queue, 50)/Publish(map)/Print를 같은 스레드의 FIFO로 처리합니다. 실선은 명령 완료 후 다음 실행이고 첫 세 명령의 stdout 없음/마지막 한 줄 출력/Print 상태 변경 없음을 보존합니다. |
| 9 | stdout 없음 |
| 12 | viewCount는 그대로 |
| 14 | 두 Map을 한 줄로 출력 |
| 16 | 실선:같은 스레드 FIFO 실행 |
| 17 | 각 poll 뒤 execute를 마침 |
| 18 | 첫 3명령은 stdout 없음 |
| 구분 | 내용 |
|---|---|
| 설명 | 원래 Add(map, 40)/Add(queue, 50)/Publish(map)/Print를 같은 스레드의 FIFO로 처리합니다. 실선은 명령 완료 후 다음 실행이고 첫 세 명령의 stdout 없음/마지막 한 줄 출력/Print 상태 변경 없음을 보존합니다. |
| 전체 읽기 정보 | 네 명령을 순서대로 실행하고 Print에서만 출력한다 네 명령을 순서대로 실행하고 Print에서만 출력한다 두 Add가 조회 수 상태를 채운 뒤 Publish가 map을 공개하고 마지막 Print가 두 누적 상태를 한 줄로 출력합니다. QUEUE · COMMAND TRACE 네 명령을 순서대로 실행하고 Print에서만 출력한다 앞에서 꺼낸 명령이 같은 상태를 차례로 갱신합니다. 출력은 마지막 Print 한 번입니다. Add("map", 40) 명령이 바꾸는 상태 viewCount={map=40} 표준 출력 없음 Add("queue", 50) 명령이 바꾸는 상태 viewCount= {map=40, queue=50} 표준 출력 없음 Publish("map") 명령이 바꾸는 상태 published={map=true} 표준 출력 없음 Print() 명령이 바꾸는 상태 상태 변경 없음 표준 출력 viewCount={map=40, queue=50}, published={map=true} 처음에는 두 Map이 비어 있습니다. poll().execute(state) 가 명령 하나를 같은 스레드에서 끝낸 뒤 다음 명령을 꺼냅니다. 마지막 셀의 두 부분은 실제로 한 줄의 출력이며, 두 Map의 나열은 이 코드의 LinkedHashMap 삽입 순서입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 등록 Add(map, 40) 뒤 Add(queue, 50)를 처리합니다. |
|
|
| 공개 명령 Publish(map)이 공개 상태를 바꿉니다. |
|
|
| 마지막 Print 두 Map을 한 줄로 출력합니다. |
|
|
네 명령을 순서대로 실행하고 Print에서만 출력한다
FIFO의 두 Add·Publish·Print가 같은 State를 변경하고 마지막에만 출력합니다. 명령별 상태 변화와 stdout 유무를 따라가기 좋은 사례입니다.
두 Add가 조회수 상태를 채운 뒤 Publish가 map을 공개하고 마지막 Print가 두 누적 상태를 한 줄로 출력합니다.
QUEUE · COMMAND TRACE
네 명령을 순서대로 실행하고 Print에서만 출력한다
앞에서 꺼낸 명령이 같은 상태를 차례로 갱신합니다. 출력은 마지막 Print 한 번입니다.
실선:같은스레드 FIFO 실행각poll 뒤execute를 마침첫3명령은 stdout없음
Add("map", 40)- 명령이 바꾸는 상태
viewCount={map=40}- 표준 출력
- 없음
Add("queue", 50)- 명령이 바꾸는 상태
viewCount={map=40, queue=50}- 표준 출력
- 없음
Publish("map")- 명령이 바꾸는 상태
published={map=true}- 표준 출력
- 없음
Print()- 명령이 바꾸는 상태
- 상태 변경 없음
- 표준 출력
viewCount={map=40, queue=50},published={map=true}
처음에는 두 Map이 비어 있습니다. poll().execute(state)가 명령 하나를 같은 스레드에서 끝낸 뒤 다음 명령을 꺼냅니다. 마지막 셀의 두 부분은 실제로 한 줄의 출력이며, 두 Map의 나열은 이 코드의 LinkedHashMap 삽입 순서입니다.
이 예의 조건
- 동일 스레드 FIFO이며 각 execute를 마친 뒤 다음 poll한다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 원래 Add(map, 40)/Add(queue, 50)/Publish(map)/Print를 같은 스레드의 FIFO로 처리합니다. 실선은 명령 완료 후 다음 실행이고 첫 세 명령의 stdout 없음/마지막 한 줄 출력/Print 상태 변경 없음을 보존합니다. |
| 9 | stdout 없음 |
| 12 | viewCount는 그대로 |
| 14 | 두 Map을 한 줄로 출력 |
| 16 | 실선:같은 스레드 FIFO 실행 |
| 17 | 각 poll 뒤 execute를 마침 |
| 18 | 첫 3명령은 stdout 없음 |
| 구분 | 내용 |
|---|---|
| 설명 | 원래 Add(map, 40)/Add(queue, 50)/Publish(map)/Print를 같은 스레드의 FIFO로 처리합니다. 실선은 명령 완료 후 다음 실행이고 첫 세 명령의 stdout 없음/마지막 한 줄 출력/Print 상태 변경 없음을 보존합니다. |
| 전체 읽기 정보 | 네 명령을 순서대로 실행하고 Print에서만 출력한다 네 명령을 순서대로 실행하고 Print에서만 출력한다 두 Add가 조회 수 상태를 채운 뒤 Publish가 map을 공개하고 마지막 Print가 두 누적 상태를 한 줄로 출력합니다. QUEUE · COMMAND TRACE 네 명령을 순서대로 실행하고 Print에서만 출력한다 앞에서 꺼낸 명령이 같은 상태를 차례로 갱신합니다. 출력은 마지막 Print 한 번입니다. Add("map", 40) 명령이 바꾸는 상태 viewCount={map=40} 표준 출력 없음 Add("queue", 50) 명령이 바꾸는 상태 viewCount= {map=40, queue=50} 표준 출력 없음 Publish("map") 명령이 바꾸는 상태 published={map=true} 표준 출력 없음 Print() 명령이 바꾸는 상태 상태 변경 없음 표준 출력 viewCount={map=40, queue=50}, published={map=true} 처음에는 두 Map이 비어 있습니다. poll().execute(state) 가 명령 하나를 같은 스레드에서 끝낸 뒤 다음 명령을 꺼냅니다. 마지막 셀의 두 부분은 실제로 한 줄의 출력이며, 두 Map의 나열은 이 코드의 LinkedHashMap 삽입 순서입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 두 등록 Add(map, 40) 뒤 Add(queue, 50)를 처리합니다. |
|
|
| 공개 명령 Publish(map)이 공개 상태를 바꿉니다. |
|
|
| 마지막 Print 두 Map을 한 줄로 출력합니다. |
|
|
이 main은 예외를 잡아 복구하지 않습니다. 별도 입력으로 빈 State에 Publish("map"), Print()만 넣으면 첫 poll은 Publish를 큐에서 꺼내고, 등록된 map이 없어 execute에서 예외가 납니다. Print는 큐에 남아 있지만 다음 반복으로 진행하지 않으므로 출력하지 않습니다. 두 Map은 여전히 비어 있고 실패 Publish는 자동으로 다시 넣어지지 않습니다. “큐에서 꺼냄”, “명령 성공”, “사용자에게 결과 출력”을 같은 사건으로 취급하지 않아야 실패 처리 위치를 정할 수 있습니다.
연습 문제
History에 A, B, C, D를 방문하고 두 번 back 하세요.
current는 B, 앞으로 pop 순서는 C 다음 D여야 합니다.
직접 Stack 두 개로 확인합니다.
이어 같은 두 번의 back 직후 B에서 E를 새로 방문한다면 back·current·forward가 각각 무엇이 되는지도 예측하세요. 새 분기에서 C와 D의 이동 경로가 유지되는지 판단하는 추가 문제입니다.
정답과 해설
방문마다 기존 current를 back에 push합니다.
back 두 번은 D와 C를 차례로 앞으로에 push하므로 top은 C입니다.
import java.util.ArrayDeque;
import java.util.Deque;
public final class BrowserBackTwiceSolution {
public static void main(String[] args) {
Deque<String> back = new ArrayDeque<>(), forward = new ArrayDeque<>();
String current = "A";
for (String page : new String[] {"B", "C", "D"}) {
back.push(current);
current = page;
forward.clear();
}
for (int i = 0; i < 2; i++) {
forward.push(current);
current = back.pop();
}
System.out.println(
"current=" + current + ", next=" + forward.pop() + ", then=" + forward.pop());
}
}출력은 current=B, next=C, then=D입니다.
Stack의 top이 최근 이동 대상을 나타냅니다.
해설의 forward.pop() 두 번은 다음 대상 순서를 확인하려고 Stack에서 직접 꺼낸 것입니다. History.forward()를 두 번 호출한 실행과는 달라서 이 해설 코드의 current는 B로 남습니다. 실제 이동 메서드는 현재 페이지도 바꾸고 반대쪽 Stack에도 넣습니다.
추가 문제의 출발 상태는 back=[A], current=B, forward=[C, D]입니다. visit("E")는 B를 back의 맨 앞에 넣고 current를 E로 바꾼 뒤 forward를 비웁니다. 따라서 back=[B, A], current=E, forward=[]이며 C와 D는 앞으로 이동할 대상에서 사라집니다.
브라우저와 프린터 예제의 공통점은 원소 순서보다 상태 전이 규칙이 먼저라는 것입니다.
새 방문은 앞으로를 폐기하고, 작업 실패는 재시도 또는 dead-letter로 소유권을 옮겨야 합니다.