본문 바로가기

게시판 탐색 이력과 작업 큐

두 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를 출력합니다.

lab/StaleForwardHistoryBug.java
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로 이 전제를 검사해야 합니다.

src/BrowserHistory.java
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 경로를 추적할 수 있습니다.

top부터 읽는 두 이력과 새 방문의 초기화

BrowserHistory의 실제 호출 순서대로 back, current, forward를 추적하면 D 방문이 C 경로를 비우고 마지막 두 번의 back은 A로 돌아갑니다.

HISTORY · STATE TRACE

top부터 읽는 두 이력과 새 방문의 초기화

새 visit는 앞으로 이력을 비웁니다. back·current·forward를 호출 직후 상태로 비교합니다.

새 방문의 폐기와 이동 거부를 구분하는 이력 원main의C방문/첫backB/새D방문/마지막두backA네주요상태입니다. D에서forwardfalse는같은D로돌아오는거부루프이며두back전이를구분합니다. 모든목록top부터/current별도/forward[B,D]최종을보존합니다. B·C 방문 뒤 back=[B,A] / current=C forward=[] 첫 back 뒤 back=[A] / current=B forward=[C] visit(D) 뒤 back=[B,A] / current=D forward=[] / C경로 폐기 back 두 번 뒤 back=[] / current=A forward=[B,D] visit(D) / forward 비움 forward()=false / 변화없음 실선:원main의 상태변경/거부 · 목록은top부터 C경로폐기 후두back의 최종은A/forward[B,D]
새 방문의 폐기와 이동 거부를 구분하는 이력 원main의C방문/첫backB/새D방문/마지막두backA네주요상태입니다. D에서forwardfalse는같은D로돌아오는거부루프이며두back전이를구분합니다. 모든목록top부터/current별도/forward[B,D]최종을보존합니다. B·C 방문 뒤 back=[B,A] / current=C forward=[] 첫 back 뒤 back=[A] / current=B forward=[C] visit(D) 뒤 back=[B,A] / current=D forward=[] / C경로 폐기 back 두 번 뒤 back=[] / current=A forward=[B,D] forward()=false / 변화없음 실선:back() 두 번 각 목록은 top부터 나열 current는 두Stack 밖의 값
top부터 읽는 두 이력과 새 방문의 초기화
main의 생성·호출 순서back · top부터currentforward · top부터
new History("A")[]A[]
visit("B")[A]B[]
visit("C")[B, A]C[]
첫 back()[A]B[C]
visit("D")[B, A]DC 경로 삭제: []
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]를 보존합니다.
4back=[B,A]
5current=C
8back=[A]
9current=B
12back=[B,A]
13current=D
15C 경로 폐기
17back=[]
18current=A
20visit(D)
21forward 비움
22forward()=false
23변화 없음
24실선: 원래 main의 상태 변경/거부 · 목록은 top부터
25C 경로 폐기 후 두 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,A]
  • current: C
  • forward: []
  • 방문은 이전 현재를 back에 보관한다.
첫 back

B로 돌아가 C를 forward에 넣습니다.

  • back: [A]
  • current: B
  • forward: [C]
  • 현재 위치의 소유가 두 방향 이력 사이에서 이동한다.
새 D 방문

앞으로 갈 C 경로를 폐기합니다.

  • back: [B,A]
  • current: D
  • forward: [] / forward()=false
  • 새 분기는 이전 미래 이력을 유지하지 않는다.
두 back

B를 거쳐 A에 도달합니다.

  • back: []
  • current: A
  • forward: [B,D]
  • 역방향 두 이동은 최신 D 경로를 앞 이력에 쌓는다.

top부터 읽는 두 이력과 새 방문의 초기화

방문·뒤로·새 방문·두 번 뒤로 뒤 forward 거부까지 두 스택과 current가 바뀝니다. 같은 영역에서 스택 top과 폐기된 C 경로를 추적할 수 있습니다.

top부터 읽는 두 이력과 새 방문의 초기화

BrowserHistory의 실제 호출 순서대로 back, current, forward를 추적하면 D 방문이 C 경로를 비우고 마지막 두 번의 back은 A로 돌아갑니다.

HISTORY · STATE TRACE

top부터 읽는 두 이력과 새 방문의 초기화

새 visit는 앞으로 이력을 비웁니다. back·current·forward를 호출 직후 상태로 비교합니다.

새 방문의 폐기와 이동 거부를 구분하는 이력 원main의C방문/첫backB/새D방문/마지막두backA네주요상태입니다. D에서forwardfalse는같은D로돌아오는거부루프이며두back전이를구분합니다. 모든목록top부터/current별도/forward[B,D]최종을보존합니다. B·C 방문 뒤 back=[B,A] / current=C forward=[] 첫 back 뒤 back=[A] / current=B forward=[C] visit(D) 뒤 back=[B,A] / current=D forward=[] / C경로 폐기 back 두 번 뒤 back=[] / current=A forward=[B,D] visit(D) / forward 비움 forward()=false / 변화없음 실선:원main의 상태변경/거부 · 목록은top부터 C경로폐기 후두back의 최종은A/forward[B,D]
새 방문의 폐기와 이동 거부를 구분하는 이력 원main의C방문/첫backB/새D방문/마지막두backA네주요상태입니다. D에서forwardfalse는같은D로돌아오는거부루프이며두back전이를구분합니다. 모든목록top부터/current별도/forward[B,D]최종을보존합니다. B·C 방문 뒤 back=[B,A] / current=C forward=[] 첫 back 뒤 back=[A] / current=B forward=[C] visit(D) 뒤 back=[B,A] / current=D forward=[] / C경로 폐기 back 두 번 뒤 back=[] / current=A forward=[B,D] forward()=false / 변화없음 실선:back() 두 번 각 목록은 top부터 나열 current는 두Stack 밖의 값
top부터 읽는 두 이력과 새 방문의 초기화
main의 생성·호출 순서back · top부터currentforward · top부터
new History("A")[]A[]
visit("B")[A]B[]
visit("C")[B, A]C[]
첫 back()[A]B[C]
visit("D")[B, A]DC 경로 삭제: []
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]를 보존합니다.
4back=[B,A]
5current=C
8back=[A]
9current=B
12back=[B,A]
13current=D
15C 경로 폐기
17back=[]
18current=A
20visit(D)
21forward 비움
22forward()=false
23변화 없음
24실선: 원래 main의 상태 변경/거부 · 목록은 top부터
25C 경로 폐기 후 두 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,A]
  • current: C
  • forward: []
  • 방문은 이전 현재를 back에 보관한다.
첫 back

B로 돌아가 C를 forward에 넣습니다.

  • back: [A]
  • current: B
  • forward: [C]
  • 현재 위치의 소유가 두 방향 이력 사이에서 이동한다.
새 D 방문

앞으로 갈 C 경로를 폐기합니다.

  • back: [B,A]
  • current: D
  • forward: [] / forward()=false
  • 새 분기는 이전 미래 이력을 유지하지 않는다.
두 back

B를 거쳐 A에 도달합니다.

  • back: []
  • current: A
  • forward: [B,D]
  • 역방향 두 이동은 최신 D 경로를 앞 이력에 쌓는다.

이 예제의 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()를 확인하므로 빈 상태에서 작업 메서드를 호출하지 않습니다.

src/PrinterRetryQueue.java
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 변화가 핵심입니다.

broken은 세 번 실패하고 두 번만 다시 대기한다

notes는 한 번에 완료되고 broken은 실패 횟수 0, 1, 2로 꺼내져 세 번째 실패 뒤 dead 목록으로 이동합니다.

QUEUE · RETRY LIMIT

broken은 세 번 실패하고 두 번만 다시 대기한다

attempts는 꺼내기 전의 누적 실패 수입니다. 세 번째 실패는 재삽입 대신 dead로 이동합니다.

작업별 세 실패와 두 재삽입의 소유 이동 notes성공done과broken처리를별도영역에보입니다. broken꺼낼때attempts0/1/2이고실패뒤1/2는pending재삽입,3은dead입니다. 실선은각작업의소유이동이며세번째재삽입은없습니다. 원실제장치실행0. notes의 처리 poll:attempts0 print 성공 done에 기록 done=[notes] 다시pending에 넣지 않음 broken의 세 실패 poll:attempts0 첫실패 / 새attempts1 뒤에 재삽입 pending broken(1) 둘째실패 / 새attempts2 뒤에 다시 재삽입 pending broken(2) 셋째실패 / 새attempts3 dead로 옮김 dead=[broken] 다시pending에 넣지 않음 실선:작업별실패뒤소유이동 최초시도1+재시도2=실패3 원main notes완료뒤 broken 처리
작업별 세 실패와 두 재삽입의 소유 이동 notes성공done과broken처리를별도영역에보입니다. broken꺼낼때attempts0/1/2이고실패뒤1/2는pending재삽입,3은dead입니다. 실선은각작업의소유이동이며세번째재삽입은없습니다. 원실제장치실행0. notes의 처리 poll:attempts0 print 성공 done에 기록 done=[notes] 다시pending에 넣지 않음 broken의 세 실패 poll:attempts0 첫실패 / 새attempts1 뒤에 재삽입 pending broken(1) 둘째실패 / 새attempts2 뒤에 다시 재삽입 pending broken(2) 셋째실패 / 새attempts3 dead로 옮김 dead=[broken] 다시pending에 넣지 않음 실선:작업별실패뒤소유이동 최초시도1+재시도2=실패3 원main notes완료뒤 broken 처리
  1. notes
    attempts=0

    판정과 이번 조치
    모의 print 성공
    done.add("notes")
    조치 뒤 대기·결과
    pending=[broken(0)]
    done=[notes]
    dead=[]
  2. broken
    attempts=0

    판정과 이번 조치
    첫 실패: 0 → 1
    offer로 뒤에 재삽입
    조치 뒤 대기·결과
    pending=[broken(1)]
    done=[notes]
    dead=[]
  3. broken
    attempts=1

    판정과 이번 조치
    둘째 실패: 1 → 2
    offer로 뒤에 재삽입
    조치 뒤 대기·결과
    pending=[broken(2)]
    done=[notes]
    dead=[]
  4. broken
    attempts=2

    판정과 이번 조치
    셋째 실패: 2 → 3
    dead.add("broken")
    조치 뒤 대기·결과
    pending=[]
    done=[notes]
    dead=[broken]

broken(n)의 n은 꺼내기 전까지 누적된 실패 횟수 attempts입니다. 처음 시도와 재시도 두 번이 모두 실패합니다. 이 코드는 이름으로 성공 여부를 판정하며 실제 인쇄를 하지 않습니다. 중간 상태는 소스 추적이고 최종 출력은 done=[notes], dead=[broken]입니다.

이 예의 조건

  • 모의 출력 상태이며 실제 프린터 장치 실행은 0이다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2notes 성공의 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에 보관합니다.

  • notes attempts: 0
  • done: [notes]
  • 완료 작업은 pending으로 재삽입하지 않는다.
두 재삽입

broken의 첫/둘째 실패 뒤 attempts가 1/2인 작업을 다시 큐에 넣습니다.

  • 첫 실패 뒤: pending broken(1)
  • 둘째 실패 뒤: pending broken(2)
  • 재시도 예산이 남을 때만 작업 소유를 큐로 돌린다.
마지막 실패

세 번째 실패 뒤 attempts가 3이 된 작업을 dead로 옮깁니다.

  • dead: [broken]
  • 재삽입: 없음
  • 실패 수: 3 / 재시도 2
  • 최초 시도와 재시도 횟수를 구분하여 종료한다.

broken은 세 번 실패하고 두 번만 다시 대기한다

notes 성공 뒤 broken 세 실패가 재삽입 두 번과 dead 이동을 만듭니다. pending/처리중/done/dead 소유와 attempts 변화가 핵심입니다.

broken은 세 번 실패하고 두 번만 다시 대기한다

notes는 한 번에 완료되고 broken은 실패 횟수 0, 1, 2로 꺼내져 세 번째 실패 뒤 dead 목록으로 이동합니다.

QUEUE · RETRY LIMIT

broken은 세 번 실패하고 두 번만 다시 대기한다

attempts는 꺼내기 전의 누적 실패 수입니다. 세 번째 실패는 재삽입 대신 dead로 이동합니다.

작업별 세 실패와 두 재삽입의 소유 이동 notes성공done과broken처리를별도영역에보입니다. broken꺼낼때attempts0/1/2이고실패뒤1/2는pending재삽입,3은dead입니다. 실선은각작업의소유이동이며세번째재삽입은없습니다. 원실제장치실행0. notes의 처리 poll:attempts0 print 성공 done에 기록 done=[notes] 다시pending에 넣지 않음 broken의 세 실패 poll:attempts0 첫실패 / 새attempts1 뒤에 재삽입 pending broken(1) 둘째실패 / 새attempts2 뒤에 다시 재삽입 pending broken(2) 셋째실패 / 새attempts3 dead로 옮김 dead=[broken] 다시pending에 넣지 않음 실선:작업별실패뒤소유이동 최초시도1+재시도2=실패3 원main notes완료뒤 broken 처리
작업별 세 실패와 두 재삽입의 소유 이동 notes성공done과broken처리를별도영역에보입니다. broken꺼낼때attempts0/1/2이고실패뒤1/2는pending재삽입,3은dead입니다. 실선은각작업의소유이동이며세번째재삽입은없습니다. 원실제장치실행0. notes의 처리 poll:attempts0 print 성공 done에 기록 done=[notes] 다시pending에 넣지 않음 broken의 세 실패 poll:attempts0 첫실패 / 새attempts1 뒤에 재삽입 pending broken(1) 둘째실패 / 새attempts2 뒤에 다시 재삽입 pending broken(2) 셋째실패 / 새attempts3 dead로 옮김 dead=[broken] 다시pending에 넣지 않음 실선:작업별실패뒤소유이동 최초시도1+재시도2=실패3 원main notes완료뒤 broken 처리
  1. notes
    attempts=0

    판정과 이번 조치
    모의 print 성공
    done.add("notes")
    조치 뒤 대기·결과
    pending=[broken(0)]
    done=[notes]
    dead=[]
  2. broken
    attempts=0

    판정과 이번 조치
    첫 실패: 0 → 1
    offer로 뒤에 재삽입
    조치 뒤 대기·결과
    pending=[broken(1)]
    done=[notes]
    dead=[]
  3. broken
    attempts=1

    판정과 이번 조치
    둘째 실패: 1 → 2
    offer로 뒤에 재삽입
    조치 뒤 대기·결과
    pending=[broken(2)]
    done=[notes]
    dead=[]
  4. broken
    attempts=2

    판정과 이번 조치
    셋째 실패: 2 → 3
    dead.add("broken")
    조치 뒤 대기·결과
    pending=[]
    done=[notes]
    dead=[broken]

broken(n)의 n은 꺼내기 전까지 누적된 실패 횟수 attempts입니다. 처음 시도와 재시도 두 번이 모두 실패합니다. 이 코드는 이름으로 성공 여부를 판정하며 실제 인쇄를 하지 않습니다. 중간 상태는 소스 추적이고 최종 출력은 done=[notes], dead=[broken]입니다.

이 예의 조건

  • 모의 출력 상태이며 실제 프린터 장치 실행은 0이다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2notes 성공의 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에 보관합니다.

  • notes attempts: 0
  • done: [notes]
  • 완료 작업은 pending으로 재삽입하지 않는다.
두 재삽입

broken의 첫/둘째 실패 뒤 attempts가 1/2인 작업을 다시 큐에 넣습니다.

  • 첫 실패 뒤: pending broken(1)
  • 둘째 실패 뒤: pending broken(2)
  • 재시도 예산이 남을 때만 작업 소유를 큐로 돌린다.
마지막 실패

세 번째 실패 뒤 attempts가 3이 된 작업을 dead로 옮깁니다.

  • dead: [broken]
  • 재삽입: 없음
  • 실패 수: 3 / 재시도 2
  • 최초 시도와 재시도 횟수를 구분하여 종료한다.

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장에서 별도로 추가합니다.

app/PostTaskQueueCli.java
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 유무를 따라가기 좋은 사례입니다.

네 명령을 순서대로 실행하고 Print에서만 출력한다

두 Add가 조회수 상태를 채운 뒤 Publish가 map을 공개하고 마지막 Print가 두 누적 상태를 한 줄로 출력합니다.

QUEUE · COMMAND TRACE

네 명령을 순서대로 실행하고 Print에서만 출력한다

앞에서 꺼낸 명령이 같은 상태를 차례로 갱신합니다. 출력은 마지막 Print 한 번입니다.

두 등록·공개·출력의 같은 상태 갱신원Addmap40/Addqueue50/Publishmap/Print를같은스레드FIFO로처리합니다. 실선은명령완료후다음실행이고첫세stdout없음/마지막한줄출력/Print상태변경없음을보존합니다.
Add(map,40) 실행viewCount map=40published는 비어 있음출력 없음
연결 관계Add(map,40) 실행 · viewCount map=40 · published는 비어 있음 · 출력 없음 → Add(queue,50) 실행 · viewCount map40/queue50 · stdout없음
Add(queue,50) 실행viewCount map40/queue50stdout없음
연결 관계Add(queue,50) 실행 · viewCount map40/queue50 · stdout없음 → Publish(map) 실행 · published map=true · viewCount는그대로
Publish(map) 실행published map=trueviewCount는그대로
연결 관계Publish(map) 실행 · published map=true · viewCount는그대로 → Print 실행 · 두Map을한줄로출력 · 상태 변경 없음
Print 실행두Map을한줄로출력상태 변경 없음

실선:같은스레드 FIFO 실행각poll 뒤execute를 마침첫3명령은 stdout없음

  1. Add("map", 40)

    명령이 바꾸는 상태
    viewCount={map=40}
    표준 출력
    없음
  2. Add("queue", 50)

    명령이 바꾸는 상태
    viewCount=
    {map=40, queue=50}
    표준 출력
    없음
  3. Publish("map")

    명령이 바꾸는 상태
    published={map=true}
    표준 출력
    없음
  4. 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 상태 변경 없음을 보존합니다.
9stdout 없음
12viewCount는 그대로
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)를 처리합니다.

  • viewCount: map40,queue50
  • published: 비어 있음
  • stdout: 없음
  • 앞 명령의 저장 상태를 다음 명령이 이어받는다.
공개 명령

Publish(map)이 공개 상태를 바꿉니다.

  • published: map=true
  • viewCount: 그대로
  • stdout: 없음
  • 메타 상태와 조회 수 갱신은 별개다.
마지막 Print

두 Map을 한 줄로 출력합니다.

  • 출력 횟수: 1줄
  • 저장 상태: 변경 없음
  • 조회 명령은 앞 명령의 누적 결과를 관찰한다.

네 명령을 순서대로 실행하고 Print에서만 출력한다

FIFO의 두 Add·Publish·Print가 같은 State를 변경하고 마지막에만 출력합니다. 명령별 상태 변화와 stdout 유무를 따라가기 좋은 사례입니다.

네 명령을 순서대로 실행하고 Print에서만 출력한다

두 Add가 조회수 상태를 채운 뒤 Publish가 map을 공개하고 마지막 Print가 두 누적 상태를 한 줄로 출력합니다.

QUEUE · COMMAND TRACE

네 명령을 순서대로 실행하고 Print에서만 출력한다

앞에서 꺼낸 명령이 같은 상태를 차례로 갱신합니다. 출력은 마지막 Print 한 번입니다.

두 등록·공개·출력의 같은 상태 갱신원Addmap40/Addqueue50/Publishmap/Print를같은스레드FIFO로처리합니다. 실선은명령완료후다음실행이고첫세stdout없음/마지막한줄출력/Print상태변경없음을보존합니다.
Add(map,40) 실행viewCount map=40published는 비어 있음출력 없음
연결 관계Add(map,40) 실행 · viewCount map=40 · published는 비어 있음 · 출력 없음 → Add(queue,50) 실행 · viewCount map40/queue50 · stdout없음
Add(queue,50) 실행viewCount map40/queue50stdout없음
연결 관계Add(queue,50) 실행 · viewCount map40/queue50 · stdout없음 → Publish(map) 실행 · published map=true · viewCount는그대로
Publish(map) 실행published map=trueviewCount는그대로
연결 관계Publish(map) 실행 · published map=true · viewCount는그대로 → Print 실행 · 두Map을한줄로출력 · 상태 변경 없음
Print 실행두Map을한줄로출력상태 변경 없음

실선:같은스레드 FIFO 실행각poll 뒤execute를 마침첫3명령은 stdout없음

  1. Add("map", 40)

    명령이 바꾸는 상태
    viewCount={map=40}
    표준 출력
    없음
  2. Add("queue", 50)

    명령이 바꾸는 상태
    viewCount=
    {map=40, queue=50}
    표준 출력
    없음
  3. Publish("map")

    명령이 바꾸는 상태
    published={map=true}
    표준 출력
    없음
  4. 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 상태 변경 없음을 보존합니다.
9stdout 없음
12viewCount는 그대로
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)를 처리합니다.

  • viewCount: map40,queue50
  • published: 비어 있음
  • stdout: 없음
  • 앞 명령의 저장 상태를 다음 명령이 이어받는다.
공개 명령

Publish(map)이 공개 상태를 바꿉니다.

  • published: map=true
  • viewCount: 그대로
  • stdout: 없음
  • 메타 상태와 조회 수 갱신은 별개다.
마지막 Print

두 Map을 한 줄로 출력합니다.

  • 출력 횟수: 1줄
  • 저장 상태: 변경 없음
  • 조회 명령은 앞 명령의 누적 결과를 관찰한다.

이 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입니다.

exercise/BrowserBackTwiceSolution.java
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로 소유권을 옮겨야 합니다.