while 반복과 break·continue
조건이 참인 동안 같은 코드를 되풀이하는 while과 do-while을 익히고, break와 continue로 반복을 멈추거나 한 회차를 건너뛰는 방법을 배웁니다.
앞 글에서 나이 범위의 경계값을 확인할 때는 값 하나마다 프로그램을 다시 실행했습니다. 13, 14, 120, 121을 확인하려면 명령을 네 번 입력해야 했습니다. 같은 코드를 조건이 만족되는 동안 되풀이하게 하는 문장이 반복문이고, 그중 가장 기본이 되는 것이 while입니다.
while은 반복 횟수가 미리 정해져 있지 않을 때 특히 잘 어울립니다. "입력이 남아 있는 동안", "종료 명령을 만나기 전까지"처럼 계속할지를 그때그때의 상태로 판단하는 경우입니다. 이 글에서는 while과 do-while이 어떤 순서로 실행되는지 따라가 보고, break와 continue로 흐름을 바꾸는 방법을 익힌 뒤, 실행 인자로 받은 여러 회원 정보를 한 번에 처리하는 프로그램을 만듭니다. 반복문에서 가장 흔한 실수인 '끝나지 않는 반복'을 피하는 방법도 함께 다룹니다.
while이 실행되는 순서
while은 괄호 안의 조건이 참이면 본문을 실행하고, 다시 조건으로 돌아가 검사하는 일을 되풀이합니다. 조건이 거짓이 되는 순간 본문을 건너뛰고 반복문 다음 문장으로 넘어갑니다. 다음 프로그램은 count를 0부터 하나씩 늘리며 출력합니다.
public final class WhileProgress {
public static void main(String[] args) {
int count = 0;
while (count < 3) {
System.out.println("count=" + count);
count++;
}
System.out.println("done=" + count);
}
}count=0
count=1
count=2
done=3조건은 본문을 실행하기 전에 매번 검사됩니다. 본문을 한 번 실행하는 것을 반복의 한 회차라고 하는데, 조건을 검사할 때마다 count가 얼마였는지 적어 보면 출력이 왜 이렇게 나오는지 분명해집니다.
| 조건 검사 | count | count < 3 | 그다음 일 |
|---|---|---|---|
| 1번째 | 0 | 참 | count=0 출력, count가 1이 됨 |
| 2번째 | 1 | 참 | count=1 출력, count가 2가 됨 |
| 3번째 | 2 | 참 | count=2 출력, count가 3이 됨 |
| 4번째 | 3 | 거짓 | 반복을 끝내고 done=3 출력 |
조건은 네 번 검사했지만 본문은 세 번만 실행됐습니다. 네 번째 검사에서 조건이 거짓이 되어 본문으로 들어가지 않고 반복이 끝났기 때문입니다. 반복이 끝난 뒤의 count가 2가 아니라 3인 것도 같은 이유입니다. 조건을 거짓으로 만든 값이 3이므로, done=3은 반복이 정확히 그 값에서 멈췄다는 것을 보여 줍니다.
이 반복은 세 부분이 맞물려야 제대로 동작합니다. 시작값 count = 0, 계속할 조건 count < 3, 그리고 본문에서 조건 쪽 변수를 바꾸는 count++입니다. 시작값이 처음부터 조건을 만족하지 않으면 본문은 한 번도 실행되지 않고, 시작값이나 조건의 숫자를 잘못 쓰면 한 번 더 돌거나 덜 돕니다. 마지막 count++가 빠지면 더 심각한 문제가 생기는데, 바로 다음 절에서 보겠습니다.
상태가 바뀌지 않으면 반복은 끝나지 않는다
본문이 조건에 쓰인 변수를 바꾸지 않으면 조건은 매번 같은 값을 냅니다. 조건이 계속 참이면 반복은 끝나지 않는데, 이를 무한 반복(infinite loop)이라고 합니다. 진짜 무한 반복을 실행하면 터미널이 멈춘 것처럼 보이므로, 다음 프로그램에는 네 번째 회차에서 실행을 강제로 멈추는 감시용 변수 watchdog를 넣었습니다. count를 바꾸지 않는 결함은 그대로 두었습니다.
public final class NonProgressingWhile {
public static void main(String[] args) {
int count = 0;
int watchdog = 0;
while (count < 3) {
System.out.println("count=" + count);
watchdog++;
if (watchdog == 4) {
throw new IllegalStateException("count did not progress");
}
}
}
}count=0
count=0
count=0
count=0
Exception in thread "main" java.lang.IllegalStateException: count did not progress
at NonProgressingWhile.main(NonProgressingWhile.java:10)본문이 count를 한 번도 바꾸지 않았으므로, 조건을 검사할 때마다 count는 0이고 count < 3은 계속 참입니다. 네 번째 회차에서 watchdog가 4가 되자 throw 문이 실행 중 오류를 일으켜 프로그램을 멈췄습니다. 마지막 두 줄은 처음 만나는 Java 오류에서 본 실행 중 오류와 같은 모양으로, 오류 메시지와 오류가 난 위치(10번째 줄)를 알려 줍니다. throw와 예외는 뒤에서 자세히 배우므로, 지금은 '여기서 프로그램을 멈춘다'는 뜻으로만 읽으면 됩니다.
watchdog는 문제를 보여 주려고 넣은 안전장치일 뿐 해결책이 아닙니다. 해결은 앞 절처럼 본문에서 count++로 조건 쪽 상태를 움직이는 것입니다. 실습하다가 감시 장치가 없는 무한 반복을 실행했다면, 터미널에서 Ctrl+C를 눌러 프로그램을 강제로 끝내면 됩니다.
변수를 바꾸기는 하지만 엉뚱한 방향으로 바꾸는 경우도 조심해야 합니다. count++ 대신 count--를 쓰면 count는 0, -1, -2로 작아지기만 하므로 count < 3은 계속 참입니다. 그래서 반복문을 쓸 때는 조건에 쓰인 변수가 매 회차 조건을 거짓으로 만드는 쪽으로 움직이는지 확인해야 합니다.
더 깊이: count--는 정말 영원히 돌까
엄밀히 말하면 count-- 버전은 영원히 돌지 않습니다. int는 -2147483648보다 작은 값을 담을 수 없어서, 그 값에서 1을 더 빼면 가장 큰 값인 2147483647로 넘어가 버립니다. 이를 오버플로(overflow)라고 하는데, 이 순간 count < 3이 거짓이 되어 반복이 끝납니다. 실제로 세어 보면 본문이 2,147,483,649번, 약 21억 번 실행된 뒤입니다. 하지만 이것은 의도한 종료가 아니라 우연이므로 무한 반복과 똑같이 버그로 다뤄야 합니다. 오버플로는 변수 스코프와 숫자 형변환에서 자세히 다룹니다.
do-while은 본문을 먼저 한 번 실행한다
while은 본문보다 조건을 먼저 검사하므로, 처음부터 조건이 거짓이면 본문을 한 번도 실행하지 않습니다. 반대로 do-while은 본문을 먼저 실행하고 나서 조건을 검사합니다. 그래서 조건이 처음부터 거짓이어도 본문은 최소 한 번 실행됩니다. 두 반복문에 똑같이 거짓인 조건을 주고 비교해 보겠습니다.
public final class PreAndPostCheck {
public static void main(String[] args) {
int remaining = 0;
while (remaining > 0) {
System.out.println("while=" + remaining);
}
do {
System.out.println("do-while=" + remaining);
} while (remaining > 0);
}
}do-while=0remaining이 0이므로 remaining > 0은 처음부터 거짓입니다. while은 조건부터 검사해 본문을 건너뛰었고, 그래서 while=로 시작하는 줄은 출력되지 않았습니다. do-while은 조건을 보기 전에 본문을 실행해 do-while=0을 한 번 출력했고, 그 뒤 조건이 거짓이라 끝났습니다. do-while은 맨 끝의 while (remaining > 0) 뒤에 세미콜론을 붙여야 한다는 점도 기억해 두세요. 빠뜨리면 ';' expected 컴파일 오류가 납니다.
'최소 한 번 실행'이 실제로 필요할 때만 do-while을 고릅니다. 메뉴를 먼저 보여 준 뒤 계속할지 묻는 프로그램처럼, 본문을 한 번 실행해야 조건을 판단할 재료가 생기는 경우가 그렇습니다. 반대로 '처리할 입력이 없으면 아무것도 하지 않는다'가 맞는 일괄 처리에 do-while을 쓰면, 입력이 하나도 없을 때도 본문이 한 번 실행되어 있지도 않은 입력을 처리하려 들게 됩니다.
do-while의 조건이 본문 블록 바깥에 있다는 점도 주의해야 합니다. 본문 중괄호 안에서 선언한 변수를 조건에 쓰면 cannot find symbol 컴파일 오류가 나므로, 조건에 필요한 변수는 do 앞에서 선언합니다. 변수를 쓸 수 있는 범위는 변수 스코프와 숫자 형변환에서 자세히 다룹니다.
아래 그림은 두 반복문이 조건을 처음 검사하는 위치와, count가 늘어날 때와 늘어나지 않을 때의 흐름을 단계별로 비교합니다.
첫 실행 위치와 count의 진전을 따로 확인한다
while/do-while의 첫 검사 위치와 count 진전 또는 무한 반복을 대비합니다. 조건·본문·증가가 만드는 실제 상태 전환을 순서대로 확인할 수 있습니다.
FLOWCHART · LOOP PROGRESS
첫 실행 위치와 count의 진전을 따로 확인한다
while은 조건을 먼저 검사하고 do-while은 본문을 먼저 실행합니다. 본문의 count 예제는 count++를 실행해야 count < 3이 거짓이 되어 종료됩니다.
반복 종료 계약 첫 검사 위치와 매 회 상태 진전을 분리해 봅니다.
while은 조건부터
while은 본문 전에 조건을 검사하므로 처음부터 false면 본문을 0회 실행합니다.do-while은 본문부터
do-while은 본문 뒤에서 조건을 검사하므로 처음 조건이 false여도 본문을 최소 1회 실행합니다.종료 가능한 전체 계약
초기 상태, 조건, 본문, 상태 진전, 조건 복귀가 한 세트입니다. 이 count 예제에서는 본문이 조건에 쓰인 count를 증가시켜야 종료됩니다.
성공 trace
count++가 매 회 실행되면0→1→2→3→done으로 진전되어count < 3을 빠져나갑니다.진전 누락 trace
count++가 없으면0→0→…으로 같은 상태를 반복하며 watchdog 같은 외부 안전장치 없이는 종료되지 않습니다.
- 조건·본문·조건 복귀
- 상태 진전이 누락된 미종료 trace
- 종료 방향의 상태 진전
while과 do-while의 차이는 첫 검사 위치이고, 이 count 예제의 종료 조건은 증가한 count가 3에 도달하는 것입니다. break나 return으로 끝나는 다른 반복에는 별도의 종료 경로가 있습니다.
이 예의 조건
- count=0에서 count<3을 반복하는 증가 있음·없음의 두 조건을 공통 관찰 단계마다 비교합니다.
- while은 조건을 먼저 검사하고 do-while은 본문을 먼저 실행합니다. 첫 조건이 false이면 while 본문은 0회이며 do-while은 최소 1회입니다.
- remaining=0으로 시작하는 원 본문의 WhileOrDoWhile 예제는 독립 첫 진입 비교이며 이 count 반복의 후속 실행으로 연결하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 첫 실행 위치와 count의 진전을 따로 확인한다 while과 do-while의 첫 검사 위치 및 상태 진전에 따른 종료 흐름 FLOWCHART · LOOP PROGRESS 첫 실행 위치와 count의 진전을 따로 확인한다 while은 조건을 먼저 검사하고 do-while은 본문을 먼저 실행합니다. 본문의 count 예제는 count++를 실행해야 count < 3이 거짓이 되어 종료됩니다. 반복 종료 계약 첫 검사 위치와 매 회 상태 진전을 분리해 봅니다. while은 조건부터 while 은 본문 전에 조건을 검사하므로 처음부터 false면 본문을 0회 실행합니다. do-while은 본문부터 do-while 은 본문 뒤에서 조건을 검사하므로 처음 조건이 false여도 본문을 최소 1회 실행합니다. 종료 가능한 전체 계약 초기 상태, 조건, 본문, 상태 진전, 조건 복귀가 한 세트입니다. 이 count 예제에서는 본문이 조건에 쓰인 count를 증가시켜야 종료됩니다. 성공 trace count++ 가 매 회 실행되면 0→1→2→3→done 으로 진전되어 count < 3 을 빠져나갑니다. 진전 누락 trace count++ 가 없으면 0→0→… 으로 같은 상태를 반복하며 watchdog 같은 외부 안전장치 없이는 종료되지 않습니다. 조건·본문·조건 복귀 상태 진전이 누락된 미종료 trace 종료 방향의 상태 진전 while 과 do-while 의 차이는 첫 검사 위치이고, 이 count 예제의 종료 조건은 증가한 count가 3에 도달하는 것입니다. break나 return으로 끝나는 다른 반복에는 별도의 종료 경로가 있습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 진입과 두 조건 count=0에서 증가 있음·없음 모두 반복에 진입하고 검사 위치를 구분합니다. |
|
|
| 본문 뒤 재검사 둘 다 현재 0을 출력하지만 증가 여부에 따라 다음 count가 달라집니다. |
|
|
| 각 종료와 정체 증가 경로는 3에서 끝나지만 진전 없는 경로는 0을 계속 출력합니다. |
|
|
첫 실행 위치와 count의 진전을 따로 확인한다
while/do-while의 첫 검사 위치와 count 진전 또는 무한 반복을 대비합니다. 조건·본문·증가가 만드는 실제 상태 전환을 순서대로 확인할 수 있습니다.
FLOWCHART · LOOP PROGRESS
첫 실행 위치와 count의 진전을 따로 확인한다
while은 조건을 먼저 검사하고 do-while은 본문을 먼저 실행합니다. 본문의 count 예제는 count++를 실행해야 count < 3이 거짓이 되어 종료됩니다.
반복 종료 계약 첫 검사 위치와 매 회 상태 진전을 분리해 봅니다.
while은 조건부터
while은 본문 전에 조건을 검사하므로 처음부터 false면 본문을 0회 실행합니다.do-while은 본문부터
do-while은 본문 뒤에서 조건을 검사하므로 처음 조건이 false여도 본문을 최소 1회 실행합니다.종료 가능한 전체 계약
초기 상태, 조건, 본문, 상태 진전, 조건 복귀가 한 세트입니다. 이 count 예제에서는 본문이 조건에 쓰인 count를 증가시켜야 종료됩니다.
성공 trace
count++가 매 회 실행되면0→1→2→3→done으로 진전되어count < 3을 빠져나갑니다.진전 누락 trace
count++가 없으면0→0→…으로 같은 상태를 반복하며 watchdog 같은 외부 안전장치 없이는 종료되지 않습니다.
- 조건·본문·조건 복귀
- 상태 진전이 누락된 미종료 trace
- 종료 방향의 상태 진전
while과 do-while의 차이는 첫 검사 위치이고, 이 count 예제의 종료 조건은 증가한 count가 3에 도달하는 것입니다. break나 return으로 끝나는 다른 반복에는 별도의 종료 경로가 있습니다.
이 예의 조건
- count=0에서 count<3을 반복하는 증가 있음·없음의 두 조건을 공통 관찰 단계마다 비교합니다.
- while은 조건을 먼저 검사하고 do-while은 본문을 먼저 실행합니다. 첫 조건이 false이면 while 본문은 0회이며 do-while은 최소 1회입니다.
- remaining=0으로 시작하는 원 본문의 WhileOrDoWhile 예제는 독립 첫 진입 비교이며 이 count 반복의 후속 실행으로 연결하지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 첫 실행 위치와 count의 진전을 따로 확인한다 while과 do-while의 첫 검사 위치 및 상태 진전에 따른 종료 흐름 FLOWCHART · LOOP PROGRESS 첫 실행 위치와 count의 진전을 따로 확인한다 while은 조건을 먼저 검사하고 do-while은 본문을 먼저 실행합니다. 본문의 count 예제는 count++를 실행해야 count < 3이 거짓이 되어 종료됩니다. 반복 종료 계약 첫 검사 위치와 매 회 상태 진전을 분리해 봅니다. while은 조건부터 while 은 본문 전에 조건을 검사하므로 처음부터 false면 본문을 0회 실행합니다. do-while은 본문부터 do-while 은 본문 뒤에서 조건을 검사하므로 처음 조건이 false여도 본문을 최소 1회 실행합니다. 종료 가능한 전체 계약 초기 상태, 조건, 본문, 상태 진전, 조건 복귀가 한 세트입니다. 이 count 예제에서는 본문이 조건에 쓰인 count를 증가시켜야 종료됩니다. 성공 trace count++ 가 매 회 실행되면 0→1→2→3→done 으로 진전되어 count < 3 을 빠져나갑니다. 진전 누락 trace count++ 가 없으면 0→0→… 으로 같은 상태를 반복하며 watchdog 같은 외부 안전장치 없이는 종료되지 않습니다. 조건·본문·조건 복귀 상태 진전이 누락된 미종료 trace 종료 방향의 상태 진전 while 과 do-while 의 차이는 첫 검사 위치이고, 이 count 예제의 종료 조건은 증가한 count가 3에 도달하는 것입니다. break나 return으로 끝나는 다른 반복에는 별도의 종료 경로가 있습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 진입과 두 조건 count=0에서 증가 있음·없음 모두 반복에 진입하고 검사 위치를 구분합니다. |
|
|
| 본문 뒤 재검사 둘 다 현재 0을 출력하지만 증가 여부에 따라 다음 count가 달라집니다. |
|
|
| 각 종료와 정체 증가 경로는 3에서 끝나지만 진전 없는 경로는 0을 계속 출력합니다. |
|
|
break와 continue로 흐름 바꾸기
반복문 본문 안에서 흐름을 바꾸는 문장이 두 가지 있습니다. break는 반복문 전체를 즉시 끝내고 반복문 다음 문장으로 이동합니다. continue는 이번 회차의 남은 본문만 건너뛰고 다음 조건 검사로 돌아갑니다. 다음 프로그램은 value를 1씩 늘려 가며 짝수는 건너뛰고, 5보다 큰 값을 만나면 반복을 끝냅니다.
public final class LoopControl {
public static void main(String[] args) {
int value = 0;
while (value < 8) {
value++;
if (value % 2 == 0) {
continue;
}
if (value > 5) {
break;
}
System.out.println(value);
}
System.out.println("stoppedAt=" + value);
}
}1
3
5
stoppedAt=7회차마다 value가 어떻게 바뀌고 어느 문장이 실행되는지 따라가 보겠습니다.
| 회차 | value++ 뒤의 값 | 짝수인가 | 5보다 큰가 | 결과 |
|---|---|---|---|---|
| 1 | 1 | 아니요 | 아니요 | 1 출력 |
| 2 | 2 | 예 | 검사 안 함 | continue |
| 3 | 3 | 아니요 | 아니요 | 3 출력 |
| 4 | 4 | 예 | 검사 안 함 | continue |
| 5 | 5 | 아니요 | 아니요 | 5 출력 |
| 6 | 6 | 예 | 검사 안 함 | continue |
| 7 | 7 | 아니요 | 예 | break |
짝수인 회차에서는 continue가 아래의 if와 println을 건너뛰고 곧바로 조건 value < 8로 돌아갑니다. 7이 되는 회차에서는 짝수 검사를 통과한 뒤 value > 5가 참이 되어 break가 실행됩니다. 이때 value < 8은 아직 참이었으므로, 이 반복은 조건이 아니라 break 때문에 끝났습니다. break가 없었다면 value가 8이 된 뒤 조건이 거짓이 되어 끝났을 것입니다. 마지막 줄 stoppedAt=7은 반복이 끝난 순간의 값을 보여 줍니다.
continue보다 먼저 상태를 바꾸는 이유
LoopControl에서 value++가 본문 맨 앞에 있는 데는 이유가 있습니다. 증가를 본문 맨 끝으로 옮기면 어떻게 되는지 보겠습니다.
int value = 1;
while (value < 8) {
if (value % 2 == 0) {
continue;
}
System.out.println(value);
value++;
}이 코드는 1을 출력하고 value를 2로 늘립니다. 그런데 다음 회차에서 2는 짝수이므로 continue가 실행되고, 그 아래에 있는 value++까지 건너뜁니다. value는 계속 2에 머물고 조건 value < 8도 계속 참이므로, 프로그램은 1 하나만 출력한 채 끝나지 않습니다.
그래서 반복 상태를 바꾸는 문장은 continue보다 앞에 두거나, 본문의 모든 경로에서 실행되게 해야 합니다. 다음 글에서 배울 for는 증가식을 머리 부분에 따로 적고 continue 뒤에도 그 증가식을 실행하므로, 이런 실수가 구조적으로 줄어듭니다.
가장 가까운 반복 하나에만 적용된다
break와 continue는 자신을 감싸는 가장 가까운 반복문 하나에만 적용됩니다. 반복문 안에 또 반복문이 있으면 안쪽 반복만 끝나거나 건너뛰고, 바깥 반복은 계속됩니다. 중첩 반복과 바깥 반복까지 한 번에 끝내는 방법은 다음 글 for 반복과 중첩 반복에서 다룹니다.
반복문 안에 switch 문이 있을 때도 조심해야 합니다. switch 문·switch 식과 삼항 연산자에서 본 것처럼 switch 문 안의 break는 가장 가까운 switch를 끝냅니다. 그래서 반복문 안의 switch 문에서 쓴 break는 반복을 끝내지 못하고 switch만 빠져나옵니다.
실행 인자를 하나씩 처리하는 while
이제 배운 것을 모아 회원 정보 여러 개를 한 번에 처리해 보겠습니다. 실행 명령의 클래스 이름 뒤에 email:age 모양의 실행 인자를 여러 개 주면, 하나씩 읽어 형식과 나이 범위를 검사하고 통과한 것만 저장한 것으로 셉니다. quit을 만나면 그 뒤의 인자는 읽지 않고 끝냅니다. 나이 범위는 앞 글에서 정한 14세 이상 120세 이하를 그대로 씁니다.
나이 부분이 정수가 아니면 Integer.parseInt가 실행 중 오류를 냅니다. 이를 복구하는 try/catch는 입력 검증과 재입력 반복에서 배우므로, 여기서는 나이 부분이 늘 정수라고 가정하고 반복의 흐름에 집중합니다. 이 글에서는 실행 인자 하나하나를 토큰(token)이라고 부릅니다. email:age 모양의 토큰을 이메일과 나이로 나누는 데는 다음 문자열 메서드를 씁니다. 문자열의 위치는 0부터 셉니다.
token.equals("quit")은 토큰의 내용이quit과 같은지 비교합니다.token.indexOf(':')는 처음 나오는:의 위치를 돌려주고,:가 없으면 -1을 돌려줍니다.java:30에서는 4입니다.token.length()는 문자열의 길이입니다. 정확히는 UTF-16 코드 단위의 개수인데, 이 예제처럼 영문과 숫자만 있는 문자열에서는 눈에 보이는 글자 수와 같습니다.token.substring(0, separator)는 0번부터separator바로 앞까지를 잘라java를 만듭니다. 시작 위치는 포함하고 끝 위치는 포함하지 않습니다.token.substring(separator + 1)은:다음부터 끝까지인30을 만듭니다.
이 메서드들로 형식 검사를 만들 수 있습니다. separator <= 0은 :가 없거나(-1) 맨 앞에 있어서(0) 이메일 부분이 빈 경우를 거르고, separator == token.length() - 1은 :가 맨 끝에 있어서 나이 부분이 빈 경우를 거릅니다. 각 메서드의 정확한 정의는 indexOf 설명과 substring 설명에서 확인할 수 있습니다.
public final class BatchWhile {
public static void main(String[] args) {
int index = 0;
int savedCount = 0;
int totalAge = 0;
while (index < args.length) {
String token = args[index];
index++;
if (token.equals("quit")) {
break;
}
int separator = token.indexOf(':');
if (separator <= 0 || separator == token.length() - 1) {
System.out.println("skip.format=" + token);
continue;
}
String email = token.substring(0, separator);
int age = Integer.parseInt(token.substring(separator + 1));
if (age < 14 || age > 120) {
System.out.println("skip.range=" + token);
continue;
}
savedCount++;
totalAge += age;
System.out.println("saved=" + email + ":" + age);
}
System.out.println("count=" + savedCount);
System.out.println("total=" + totalAge);
}
}saved=java:30
skip.format=bad
skip.range=sql:0
saved=loops:45
count=2
total=75토큰 하나를 처리한 뒤의 상태를 표로 적으면 출력의 각 줄이 어디서 나왔는지 알 수 있습니다.
| 읽은 토큰 | 읽은 뒤 index | savedCount | totalAge | 결과 |
|---|---|---|---|---|
java:30 | 1 | 1 | 30 | 저장하고 saved=java:30 출력 |
bad | 2 | 1 | 30 | :가 없어 continue |
sql:0 | 3 | 1 | 30 | 나이가 범위 밖이라 continue |
loops:45 | 4 | 2 | 75 | 저장하고 saved=loops:45 출력 |
quit | 5 | 2 | 75 | break로 반복 종료 |
bad에는 :가 없어서 indexOf가 -1을 돌려주었고, sql:0은 형식은 맞지만 나이 0이 14보다 작습니다. 두 경우 모두 continue로 다음 토큰으로 넘어갔고, savedCount와 totalAge는 바뀌지 않았습니다. quit에서는 break가 실행되었으므로 위치 5에 있는 ignored:50은 읽지도 않았습니다. 반복이 끝난 뒤 출력한 count=2와 total=75는 저장된 java:30과 loops:45만 센 결과입니다.
index++를 읽은 직후에 두는 이유
이 프로그램에서 가장 중요한 줄은 index++의 위치입니다. 토큰을 읽자마자 index를 늘렸기 때문에, 그 아래에서 continue로 빠지든 끝까지 저장하든 다음 회차는 항상 다음 토큰을 읽습니다. 만약 index++를 본문 맨 끝, 저장 코드 뒤로 옮기면 어떻게 될까요? java:30은 정상 처리되지만, bad에서는 continue가 index++를 건너뛰기 때문에 다음 회차에 다시 bad를 읽습니다. 그래서 프로그램은 skip.format=bad를 끝없이 출력합니다. 앞에서 본 continue 함정이 실제 프로그램에서 일어나는 모습입니다.
반복 불변식으로 반복 점검하기
BatchWhile이 어떤 입력에서도 올바르게 동작한다고 어떻게 확신할 수 있을까요? 한 가지 방법은 조건을 검사하기 직전마다 늘 성립해야 하는 사실을 정해 두고, 본문이 그 사실을 깨뜨리지 않는지 확인하는 것입니다. 이런 사실을 반복 불변식(loop invariant)이라고 합니다. BatchWhile의 불변식은 다음 세 가지입니다.
index는 지금까지 읽은 토큰의 수이고, 0 이상args.length이하입니다.savedCount는 검사를 통과해 저장한 토큰의 수입니다.totalAge는 저장한 토큰의 나이만 더한 값입니다.
처음 조건을 검사할 때는 세 변수가 모두 0이므로 불변식이 성립합니다. 본문은 토큰 하나를 읽고 index를 1 늘리는데, 본문에 들어왔다는 것은 index < args.length였다는 뜻이므로 1을 늘려도 args.length를 넘지 않습니다. 그리고 저장할 때만 savedCount와 totalAge를 함께 바꿉니다. 형식이나 범위 검사에 실패해 continue하는 경로는 두 값을 건드리지 않습니다. 그러니 어느 경로로 회차를 마치든 다음 조건 검사 때 불변식은 다시 성립합니다. 앞의 상태 표에서 각 행이 세 사실을 모두 만족하는지 직접 확인해 보세요.
불변식은 반복이 끝나는 이유도 알려 줍니다. index는 매 회차 정확히 1씩 늘어나고 args.length를 넘을 수 없으므로, 반복은 많아야 args.length번 돌고 끝납니다. 출력이 기대와 다를 때도 불변식이 쓸모 있습니다. 회차마다 세 값을 출력해 보고 불변식이 처음으로 깨진 회차를 찾으면, 그 회차의 본문에 버그가 있습니다. 불변식으로 반복이 맞는지와 언제 끝나는지를 따지는 방법은 불변조건·종료와 반례 찾기에서 더 자세히 다룹니다.
아래 그림은 토큰 하나를 처리하는 회차가 어느 경로로 흘러가는지와, 그동안 불변식이 어떻게 유지되는지를 단계별로 보여 줍니다. 그림의 count와 total은 코드의 savedCount와 totalAge입니다.
배치 while의 진행·continue·break 상태 계약
토큰마다 index를 먼저 증가시키고 quit/continue/누적 분기로 이동하는 배치입니다. index·count·total 불변식과 미처리 입력을 단계로 따라가기 좋습니다.
FLOWCHART · LOOP INVARIANT
배치 while의 진행·continue·break 상태 계약
token을 고른 즉시 index를 증가시켜 모든 경로의 진행을 보장하고, 실패는 누적값을 보존한 채 continue하며 quit만 남은 입력을 미도달로 남깁니다.
loop-head invariant 0 ≤ index ≤ args.length이며 count와 total은 유효하게 저장한 항목만 누적합니다.
남은 token 확인
index < args.length가 false면 입력을 모두 확인한 것이므로 최종count와total을 출력합니다.선택 즉시 위치 진전
token = args[index]직후index++를 실행합니다. 따라서 continue와 break를 포함한 모든 처리 경로에서 입력 위치가 진전됩니다.quit은 break
quit이면 반복을 즉시 끝내며 뒤의ignored:50같은 남은 token은 NOT REACHED, 즉 미도달 상태가 됩니다.format·range 실패는 continue
separator 형식이 잘못됐거나 나이가 14–120 밖이면
count와total을 바꾸지 않고 다음 loop head로 돌아갑니다.정수 입력 전제
이 절에서는 나이 문자열이 정수라고 가정합니다. 숫자 변환 실패 복구는
ch3-2에서 다루므로 현재 continue 경로가 아닙니다.유효 token만 저장
검사를 통과한 token만
count++와total += age를 함께 실행한 뒤 다음 loop head로 돌아갑니다.
- 조건·선택·판정 흐름
- 누적값을 보존하는 continue
- 유효 token의 count·total 갱신
배치 반복의 안전성은 continue 위치가 아니라 그 전에 index가 진전됐는지, 그리고 실패 경로가 누적값을 바꾸지 않았는지로 판단합니다. 그림의 count·total은 코드의 savedCount·totalAge입니다.
이 예의 조건
- 나이는 정수 문자열이며 변환 실패 복구는 별도 장에서 다룹니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 배치 while의 진행·continue·break 상태 계약 배치 while의 loop-head 불변식과 continue, save, break 제어 흐름 FLOWCHART · LOOP INVARIANT 배치 while의 진행·continue·break 상태 계약 token을 고른 즉시 index를 증가시켜 모든 경로의 진행을 보장하고, 실패는 누적값을 보존한 채 continue하며 quit만 남은 입력을 미도달로 남깁니다. loop-head invariant 0 ≤ index ≤ args.length 이며 count와 total은 유효하게 저장한 항목만 누적합니다. 남은 token 확인 index < args.length 가 false면 입력을 모두 확인한 것이므로 최종 count 와 total 을 출력합니다. 선택 즉시 위치 진전 token = args[index] 직후 index++ 를 실행합니다. 따라서 continue와 break를 포함한 모든 처리 경로에서 입력 위치가 진전됩니다. quit은 break quit 이면 반복을 즉시 끝내며 뒤의 ignored:50 같은 남은 token은 NOT REACHED, 즉 미도달 상태가 됩니다. format·range 실패는 continue separator 형식이 잘못됐거나 나이가 14–120 밖이면 count 와 total 을 바꾸지 않고 다음 loop head로 돌아갑니다. 정수 입력 전제 이 절에서는 나이 문자열이 정수라고 가정합니다. 숫자 변환 실패 복구는 ch3-2 에서 다루므로 현재 continue 경로가 아닙니다. 유효 token만 저장 검사를 통과한 token만 count++ 와 total += age 를 함께 실행한 뒤 다음 loop head로 돌아갑니다. 조건·선택·판정 흐름 누적값을 보존하는 continue 유효 token의 count·total 갱신 배치 반복의 안전성은 continue 위치가 아니라 그 전에 index가 진전됐는지, 그리고 실패 경로가 누적값을 바꾸지 않았는지로 판단합니다. 그림의 count·total은 코드의 savedCount·totalAge입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 항목 선택과 진전 loop head에서 token=args[index]를 고른 직후 index++합니다. |
|
|
| 분기별 누적 quit은 break, 형식·범위 오류는 continue합니다. |
|
|
| 유효 입력과 종료 14≤age≤120인 항목만 count++와 total+=age를 수행합니다. |
|
|
배치 while의 진행·continue·break 상태 계약
토큰마다 index를 먼저 증가시키고 quit/continue/누적 분기로 이동하는 배치입니다. index·count·total 불변식과 미처리 입력을 단계로 따라가기 좋습니다.
FLOWCHART · LOOP INVARIANT
배치 while의 진행·continue·break 상태 계약
token을 고른 즉시 index를 증가시켜 모든 경로의 진행을 보장하고, 실패는 누적값을 보존한 채 continue하며 quit만 남은 입력을 미도달로 남깁니다.
loop-head invariant 0 ≤ index ≤ args.length이며 count와 total은 유효하게 저장한 항목만 누적합니다.
남은 token 확인
index < args.length가 false면 입력을 모두 확인한 것이므로 최종count와total을 출력합니다.선택 즉시 위치 진전
token = args[index]직후index++를 실행합니다. 따라서 continue와 break를 포함한 모든 처리 경로에서 입력 위치가 진전됩니다.quit은 break
quit이면 반복을 즉시 끝내며 뒤의ignored:50같은 남은 token은 NOT REACHED, 즉 미도달 상태가 됩니다.format·range 실패는 continue
separator 형식이 잘못됐거나 나이가 14–120 밖이면
count와total을 바꾸지 않고 다음 loop head로 돌아갑니다.정수 입력 전제
이 절에서는 나이 문자열이 정수라고 가정합니다. 숫자 변환 실패 복구는
ch3-2에서 다루므로 현재 continue 경로가 아닙니다.유효 token만 저장
검사를 통과한 token만
count++와total += age를 함께 실행한 뒤 다음 loop head로 돌아갑니다.
- 조건·선택·판정 흐름
- 누적값을 보존하는 continue
- 유효 token의 count·total 갱신
배치 반복의 안전성은 continue 위치가 아니라 그 전에 index가 진전됐는지, 그리고 실패 경로가 누적값을 바꾸지 않았는지로 판단합니다. 그림의 count·total은 코드의 savedCount·totalAge입니다.
이 예의 조건
- 나이는 정수 문자열이며 변환 실패 복구는 별도 장에서 다룹니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 배치 while의 진행·continue·break 상태 계약 배치 while의 loop-head 불변식과 continue, save, break 제어 흐름 FLOWCHART · LOOP INVARIANT 배치 while의 진행·continue·break 상태 계약 token을 고른 즉시 index를 증가시켜 모든 경로의 진행을 보장하고, 실패는 누적값을 보존한 채 continue하며 quit만 남은 입력을 미도달로 남깁니다. loop-head invariant 0 ≤ index ≤ args.length 이며 count와 total은 유효하게 저장한 항목만 누적합니다. 남은 token 확인 index < args.length 가 false면 입력을 모두 확인한 것이므로 최종 count 와 total 을 출력합니다. 선택 즉시 위치 진전 token = args[index] 직후 index++ 를 실행합니다. 따라서 continue와 break를 포함한 모든 처리 경로에서 입력 위치가 진전됩니다. quit은 break quit 이면 반복을 즉시 끝내며 뒤의 ignored:50 같은 남은 token은 NOT REACHED, 즉 미도달 상태가 됩니다. format·range 실패는 continue separator 형식이 잘못됐거나 나이가 14–120 밖이면 count 와 total 을 바꾸지 않고 다음 loop head로 돌아갑니다. 정수 입력 전제 이 절에서는 나이 문자열이 정수라고 가정합니다. 숫자 변환 실패 복구는 ch3-2 에서 다루므로 현재 continue 경로가 아닙니다. 유효 token만 저장 검사를 통과한 token만 count++ 와 total += age 를 함께 실행한 뒤 다음 loop head로 돌아갑니다. 조건·선택·판정 흐름 누적값을 보존하는 continue 유효 token의 count·total 갱신 배치 반복의 안전성은 continue 위치가 아니라 그 전에 index가 진전됐는지, 그리고 실패 경로가 누적값을 바꾸지 않았는지로 판단합니다. 그림의 count·total은 코드의 savedCount·totalAge입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 항목 선택과 진전 loop head에서 token=args[index]를 고른 직후 index++합니다. |
|
|
| 분기별 누적 quit은 break, 형식·범위 오류는 continue합니다. |
|
|
| 유효 입력과 종료 14≤age≤120인 항목만 count++와 total+=age를 수행합니다. |
|
|
어떤 반복을 고를까
| 하고 싶은 일 | 알맞은 형태 |
|---|---|
| 입력이 남아 있는 동안만 처리 | while (index < args.length) |
| 본문을 먼저 한 번 실행한 뒤 계속할지 판단 | do-while |
| 종료 명령을 만날 때까지 계속 | while (true) 안에서 break |
| 잘못된 항목 하나만 건너뛰기 | continue |
| 남은 반복 전체를 즉시 끝내기 | break |
while (true)는 조건이 늘 참이므로, 반복을 끝내려면 본문 안에서 break나 메서드를 끝내는 return을 실행해야 합니다. 종료 사유가 본문 여러 곳에서 생길 때는 편리하지만, 반복이 언제 끝나는지 알려면 본문의 모든 break를 찾아 읽어야 합니다. 그래서 while (index < args.length)처럼 계속할 조건을 머리에 직접 쓸 수 있다면 그쪽이 읽기 쉽습니다. BatchWhile도 입력이 바닥나는 평범한 경우는 조건으로 끝나고, quit이라는 특별한 경우만 break로 끝납니다.
정리
while은 조건을 먼저 검사하므로 본문이 0번 실행될 수 있고,do-while은 본문을 먼저 실행하므로 최소 한 번 실행됩니다.- 반복이 끝나려면 본문이 조건에 쓰인 변수를 조건이 거짓이 되는 방향으로 바꿔야 합니다.
break는 가장 가까운 반복문을 끝내고,continue는 이번 회차의 남은 본문을 건너뜁니다. 상태를 바꾸는 문장은continue보다 앞에 둡니다.- 반복 불변식은 조건을 검사하기 직전마다 성립하는 사실입니다. 반복이 맞게 동작하는지, 언제 끝나는지 따질 때 씁니다.
연습 문제
- 1부터 숫자를 하나씩 늘리며 홀수만 합계에 더하는
OddSumLimit을 작성하세요. 짝수는continue로 건너뛰고, 합계가 30을 넘으면break로 끝낸 뒤 마지막 숫자와 합계를 출력합니다. 매 회차 숫자가 반드시 늘어나야 합니다. BatchWhile을java -cp out BatchWhile a:20 :30 b:15 quit c:40으로 실행하면 무엇이 출력될까요? 반복이 끝났을 때index는 얼마일까요?- 다음 코드의 출력을 실행하기 전에 예측해 보세요.
public class DoOnce {
public static void main(String[] args) {
int n = 5;
do {
System.out.println("do-while: " + n);
n -= 2;
} while (n > 5);
int m = 5;
while (m > 5) {
System.out.println("while: " + m);
m -= 2;
}
System.out.println("n=" + n + ", m=" + m);
}
}해설 보기
1번. 숫자를 늘리는 number++를 continue보다 앞에 두는 것이 핵심입니다.
public final class OddSumLimit {
public static void main(String[] args) {
int number = 0;
int sum = 0;
while (true) {
number++;
if (number % 2 == 0) {
continue;
}
sum += number;
if (sum > 30) {
break;
}
}
System.out.println("number=" + number);
System.out.println("sum=" + sum);
}
}number=11
sum=36합계는 1, 4, 9, 16, 25로 늘어나다가 11을 더한 순간 36이 되어 30을 넘습니다. 한계 검사가 더한 뒤에 있으므로 결과는 '30을 처음 넘긴 합계'입니다. 30을 넘기 직전에 멈추고 싶다면 더하기 전에 if (sum + number > 30)으로 검사하고 break해야 합니다. 그러면 number=11, sum=25가 출력되는데, 이는 문제의 요구가 달라진 것이므로 어느 쪽이 필요한지 먼저 정해야 합니다.
2번. 출력은 다음과 같고, 반복이 끝났을 때 index는 4입니다.
saved=a:20
skip.format=:30
saved=b:15
count=2
total=35:30은 :가 맨 앞(위치 0)에 있어 이메일 부분이 비었으므로 separator <= 0에 걸립니다. b:15의 나이 15는 14 이상이라 저장됩니다. quit은 위치 3에 있고, 읽자마자 index++가 실행된 뒤 break하므로 index는 4로 끝납니다. c:40은 읽지 않았습니다.
3번. 출력은 다음과 같습니다.
do-while: 5
n=3, m=5do-while은 조건을 보기 전에 본문을 실행하므로 do-while: 5를 출력하고 n을 3으로 줄입니다. 그다음 3 > 5가 거짓이라 끝납니다. while은 5 > 5부터 거짓이라 본문을 한 번도 실행하지 않으므로 m은 5 그대로입니다.
다음 글
for 반복과 중첩 반복에서는 시작값, 조건, 변화를 한 줄에 모은 for를 배웁니다. 이 글의 일괄 처리도 향상된 for로 더 짧게 바꿔 봅니다.