Java 16
while 문, 조건에 따라 반복하고 입력으로 종료하기
for와 while이 둘 다 반복문이라면 왜 while은 초기식과 변화식이 밖으로 흩어져 있는지 모르겠다.
근거 · 교안 p115-p118
1장: while과 for, 반복의 목적이 다르다
솔라의 노트북 화면에는 두 개의 코드 블록이 나란히 떠 있었다. 왼쪽과 오른쪽, 둘 다 콘솔 창에 ‘Hello’라는 단어를 다섯 번 출력하는 똑같은 일을 했다. 하지만 어쩐지 솔라의 미간은 펴지지 않았다. 같은 일을 하는데도 코드의 모양새가 마음에 들지 않았다. 특히 오른쪽 코드는 뭔가 중요한 부품들이 제자리를 찾지 못하고 흩어져 있는 것처럼 보였다.
“언니, 이거 좀 봐.”
마침 방에 들어온 루나에게 솔라가 화면을 가리켰다.
“왼쪽은 for문이고 오른쪽은 while문. 결과는 똑같은데, while문은 왜 이렇게 어수선하지? 변수 i를 처음 만드는 부분, i가 5보다 작은지 확인하는 조건, i를 1씩 늘리는 변화식이 전부 따로 놀고 있잖아. 둘 다 똑같은 반복문이라면서.”
루나는 말없이 화면을 들여다보았다. 솔라가 작성한 코드는 군더더기 없이 깔끔했다.
// for 문
for (int i = 0; i < 5; i++) {
System.out.println("Hello");
}
// while 문
int i = 0;
while (i < 5) {
System.out.println("Hello");
i++;
}
“그러게. 정말 흩어져 있는 것처럼 보이네.” 루나는 솔라의 관찰에 동의했다. “솔라, 전에 for문 괄호 안에 있는 세 부분이 한꺼번에 있어서 오히려 헷갈린다고 한 적 있었지?”
“응. 처음엔 그랬지. 초기식, 조건식, 변화식이 한 줄에 다 들어있어서 정신없었어. 하지만 이제는 그게 ‘정해진 횟수’를 반복하기 위한 하나의 세트라는 걸 알아. 시작점, 종료 조건, 보폭까지. 횟수 제어에 필요한 모든 게 한곳에 모여 있으니 오히려 파악하기 편하더라고.”
솔라의 말대로였다. for문은 반복 횟수를 제어하는 데 필요한 장치들을 괄호 () 안에 단단히 묶어두고 있었다.
“바로 그거야.” 루나가 for문의 첫 줄을 손가락으로 가리켰다. “for문의 가장 중요한 임무는 ‘정해진 횟수만큼 정확하게 반복하는 것’이야. 그래서 횟수를 세기 위한 변수를 만들고(int i = 0), 반복을 계속할지 검사하고(i < 5), 횟수를 하나씩 세는(i++) 모든 장치를 한곳에 모아둔 거지.”
루나의 손가락이 이번엔 오른쪽 while문으로 옮겨갔다.
“그럼 while문의 괄호 안을 볼까? 여기엔 뭐가 남았지?”
“조건식. i < 5 이거 하나밖에 없네.”
솔라는 자기가 뱉은 말을 곱씹었다. for문의 괄호 안은 세 개의 구획으로 나뉜 북적이는 제어실 같았다. 반면 while문의 괄호 안은 덩그러니 조건문 하나만 놓인 텅 빈 공간처럼 느껴졌다. 어수선하게 흩어져 있다고 생각했던 초기식(int i = 0;)과 변화식(i++;)은 아예 괄호 바깥으로 밀려나 있었다.
“while문의 유일한 규칙은 아주 단순해.” 루나가 말했다. “‘괄호 안의 조건이 참(true)인 동안, 중괄호 {} 안의 코드를 계속 반복한다.’ 이게 전부야.”
솔라는 잠시 생각에 잠겼다. 유일한 규칙. 그렇다면 while문에게 i를 0으로 만드는 것이나 1씩 증가시키는 것은 부차적인 문제라는 뜻일까?
“while문은 지금 i가 몇인지, 앞으로 몇 번을 더 반복해야 하는지엔 관심이 없어. 오직 ‘그래서 지금 i < 5라는 조건이 참이야, 거짓이야?’ 이것만 물어볼 뿐이야. int i = 0;은 첫 번째 질문에 답하기 위한 준비물이고, i++;는 언젠가 이 질문을 끝내기 위한 장치일 뿐이지.”
그 말을 듣고 다시 코드를 보자, 흩어져 있던 조각들이 다른 그림으로 맞춰지기 시작했다. for문은 ‘5회’라는 횟수를 완수하기 위해 모든 제어 장치를 한 손에 쥐고 있는 지휘자 같았다. 시작, 과정, 변화 모두 그의 통제 아래 있었다.
반면 while문은 문지기 같았다. 문지기는 통행증이 유효한지만 확인할 뿐, 그 사람이 어디서 와서 어디로 가는지, 몇 번이나 이 문을 지나갔는지는 신경 쓰지 않는다. while (i < 5)라는 조건이 바로 그 통행증이었다. 통행증이 유효하면 문을 열어주고({} 안의 코드 실행), 유효하지 않으면 문을 닫을 뿐이다.
“아…!” 솔라의 입에서 작은 감탄사가 터져 나왔다. “for문은 ‘횟수’가 중요하고, while문은 ‘조건’이 중요한 거구나. 그래서 for는 횟수 제어 세트가 묶여있고, while은 조건식만 덩그러니 남겨서 그것만 계속 확인하는 거였어.”
어수선하고 제멋대로 흩어져 있다고 생각했던 구조가 아니었다. 목적이 달랐기 때문에 설계가 달라진 것이었다. while문의 구조는 횟수가 아닌, 오직 ‘조건의 참/거짓’ 상태에만 집중하기 위한 필연적인 선택이었다.
이제 솔라는 while문이 조건이 참인 동안 계속 반복하고, 거짓이 되면 멈춘다는 단순한 규칙을 몸으로 이해할 수 있었다. 두 반복문은 생김새만 다른 게 아니라, 반복을 대하는 근본적인 관점 자체가 달랐다.
그런데 한 가지 의문이 새로 떠올랐다.
“언니, 그럼 이렇게 ‘Hello’를 다섯 번 찍는 것처럼 횟수가 처음부터 정해진 일에는 for문이 더 깔끔하고 안전한 거 아니야? while문은 굳이 이렇게 쓸 필요가 없어 보이는데. 이 흩어진 구조가 for문보다 더 강력해지는 특별한 상황이 있다는 뜻인가?”
2장: 흩어진 코드가 만드는 강력한 조건
솔라의 질문이 끝나자, 루나는 대답 대신 솔라의 노트북으로 손을 뻗었다. 그리고는 while문 코드 블록에서 i++;라고 적힌 한 줄을 블록 지정하더니 망설임 없이 삭제 키를 눌렀다.
화면의 코드는 아주 조금 바뀌었을 뿐이지만, 솔라는 그 변화가 일으킬 결과를 즉시 알아차렸다.
// while 문 (수정 후)
int i = 0;
while (i < 5) {
System.out.println("Hello");
}
“이러면… 영원히 안 끝나.” 솔라가 중얼거렸다. 변수 i는 처음에 0이 되고, while문은 조건 i < 5를 확인한다. 0은 5보다 작으니 참이다. ‘Hello’가 출력된다. 그리고 다시 조건을 확인한다. i는 여전히 0이다. 또 참이다. ‘Hello’가 출력된다. i의 값을 바꿔주던 i++ 코드가 사라졌으니, i < 5라는 조건은 영원히 참으로 유지될 것이다.
“맞아.” 루나가 말했다. “while문은 조건이 거짓이 되지 않는 한 절대 스스로 멈추지 않아. for문처럼 ‘정해진 횟수’라는 안전장치가 없지. 조건을 거짓으로 만드는 건 전적으로 우리 책임이야.”
루나는 방금 삭제했던 코드가 있던 자리를 가리켰다. “조금 전까지는 우리가 i++ 코드를 넣어서 다섯 번 만에 반복을 끝냈어. 반복을 멈추게 할 신호를 우리가 직접 만든 거지. 그런데 만약 그 신호가 우리 코드 안이 아니라, 코드 밖에서 온다면 어떨까? 예를 들면, 프로그램을 사용하는 사람에게서.”
솔라의 눈이 반짝였다. ‘흩어진 구조가 더 강력해지는 특별한 상황’이라는 질문에 대한 실마리 같았다.
“사용자한테서 신호를 받는다고?”
“응. 이런 프로그램을 만든다고 생각해 봐.” 루나가 새로운 과제를 제시했다. “사용자에게 숫자를 계속 입력받는 거야. 그러다가 사용자가 숫자 9를 입력하면, 그때 프로그램을 종료하는 거지. 몇 번 만에 9가 입력될지는 아무도 몰라.”
반복 횟수가 정해져 있지 않은 상황. for문으로 만들기에는 껄끄러운 문제였다. 솔라는 새 파일을 열고 코드를 짜기 시작했다. 일단 사용자 입력을 받아야 하니 Scanner부터 만들었다.
import java.util.Scanner;
public class StopSignal {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// ... 여기에 반복문 ...
}
}
이제 반복문이 들어갈 차례. while문을 쓰는 게 맞을 것 같았다. 조건은 ‘입력된 숫자가 9가 아닐 동안’이 되어야 한다. 솔라는 일단 변수 이름을 input으로 정하고 while문의 괄호 안을 채웠다.
while (input != 9)
코드를 적고 나자마자 문제가 보였다. input이라는 변수는 while문이 처음 시작될 때부터 값을 가지고 있어야 조건 검사가 가능하다. 하지만 input에 들어갈 값은 사용자가 입력해야만 알 수 있다.
“아, while문을 시작하기 전에 먼저 한 번은 입력을 받아야 하네.”
솔라는 while문 위쪽에 코드를 추가했다.
System.out.print("숫자를 입력하세요 (종료는 9): ");
int input = scanner.nextInt(); // 첫 번째 입력
while (input != 9) {
// ...
}
첫 번째 관문은 통과했다. 이제 while문의 {} 안을 채울 차례. 만약 사용자가 9가 아닌 다른 숫자를 입력했다면, 그 숫자를 일단 처리해야 한다. 그리고… 그 다음 입력을 또 받아야 한다. 그래야 다음번 반복에서 새로운 input 값으로 input != 9 조건을 다시 검사할 수 있으니까.
솔라는 키보드를 두드려 나머지 코드를 완성했다.
System.out.print("숫자를 입력하세요 (종료는 9): ");
int input = scanner.nextInt(); // 초기식
while (input != 9) { // 조건식
System.out.println("당신이 입력한 숫자는 " + input + "입니다.");
System.out.print("다음 숫자를 입력하세요 (종료는 9): ");
input = scanner.nextInt(); // 변화식
}
System.out.println("프로그램을 종료합니다.");
코드를 완성하고 나자 솔라는 잠시 멍하니 화면을 바라보았다. 자신이 그토록 이상하게 생각했던 while문의 ‘흩어진’ 구조가 눈앞에 그대로 펼쳐져 있었다.
- 초기식 (
int input = scanner.nextInt();): 반복문을 시작하기 전, 조건(input != 9)을 처음으로 검사하기 위해 루프 바깥에 존재해야만 했다. - 변화식 (
input = scanner.nextInt();): 반복이 계속될지 끝날지를 결정할 다음 조건을 만들기 위해 루프 안쪽 마지막에 존재해야만 했다.
이건 임의적인 설계나 어수선한 배치가 아니었다. ‘언제 끝날지 모르는 외부 신호를 기다리는’ 상황을 해결하기 위한 가장 논리적이고 필연적인 구조였다. for문처럼 제어 요소들을 한곳에 묶어둘 수 없는 이유가 명확해졌다. for문의 변화식 i++는 예측 가능하게 자동으로 변하지만, 이 while문의 변화식 input = scanner.nextInt()는 언제 어떤 값으로 바뀔지 아무도 예측할 수 없기 때문이다.
“흩어져 있는 게 아니었어.” 솔라가 입을 열었다. “각자 있어야 할 정확한 자리에 있었던 거야. 바깥에 있는 초기식은 문지기에게 첫 통행증을 보여주는 것과 같고, 안쪽에 있는 변화식은 다음 검문을 위해 새 통행증을 발급받는 거였어.”
어지럽게 흩어져 있던 부품들이 제자리를 찾아 하나의 강력한 엔진으로 조립되는 느낌이었다. while문의 힘은 정해진 횟수를 깔끔하게 반복하는 데 있는 것이 아니었다. 언제 올지 모르는 ‘종료 신호’를 끈기 있게 기다리는 유연함에 있었다.
솔라는 방금 자신이 만든 프로그램을 실행하고 여러 숫자를 입력했다. 5, 12, -100… 프로그램은 끈질기게 다음 숫자를 요구했다. 그러다 9를 입력하자, 루프는 거짓이 된 조건을 확인하고는 미련 없이 “프로그램을 종료합니다.”라는 마지막 메시지를 남기고 멈췄다.
모든 것이 명확해졌다. 하지만 완성된 코드를 다시 보니, 새로운 의문이 고개를 들었다.
“언니, 그런데 System.out.print랑 scanner.nextInt()를 루프 바깥에서 한 번, 안에서 또 한 번, 총 두 번이나 똑같이 썼네. 물론 이유는 알겠는데… 뭔가 좀 비효율적인 것 같기도 하고. 이 ‘입력 대기 루프’를 좀 더 세련되게 만드는 방법은 없을까?”
3장: 원하는 때 종료하는 입력 대기 루프
솔라의 손가락이 노트북 트랙패드 위에서 멈칫했다. 화면에는 방금 전 자신이 완성한 ‘입력 대기 루프’ 코드가 떠 있었다. 프로그램은 완벽하게 작동했지만, 솔라의 눈에는 두 군데의 똑같은 코드 덩어리가 계속 거슬렸다. while 문에 진입하기 위해 루프 바깥에서 입력을 받는 부분, 그리고 루프를 계속 돌리기 위해 루프 안쪽에서 다시 입력을 받는 부분.
“이 중복, 어떻게든 합칠 수 있을 것 같은데….”
솔라는 혼잣말을 하며 코드 수정에 들어갔다. 일단 루프 바깥에 있던 첫 번째 입력 코드를 과감하게 지웠다.
// 수정 시도 1
Scanner scanner = new Scanner(System.in);
// System.out.print("숫자를 입력하세요 (종료는 9): "); // 삭제
// int input = scanner.nextInt(); // 삭제
while (input != 9) {
// ...
}
당연히 input 변수가 선언된 적이 없다는 컴파일 에러가 발생했다. “아, 변수는 미리 만들어둬야지.” 솔라는 while 문 위쪽에 int input;을 추가했다. 하지만 이번엔 변수가 초기화되지 않았다는 에러가 떴다. while 문이 조건(input != 9)을 검사하려면 input이 맨 처음에 어떤 값을 가지고 있어야만 했다.
“그럼 아무 값이나 넣어주면 되잖아? 어차피 바로 덮어쓸 테니.” 솔라는 int input = 0; 이라고 코드를 수정했다. 에러는 사라졌다. 만족스러운 미소를 지으며 프로그램을 실행했다. 하지만 결과는 엉뚱했다.
당신이 입력한 숫자는 0입니다.
다음 숫자를 입력하세요 (종료는 9):
프로그램은 사용자에게 입력을 받기도 전에, 초기값으로 설정한 0을 멋대로 처리해버렸다. 명백히 잘못된 동작이었다. 중복을 피하려다 로직이 망가진 셈이다.
그때까지 조용히 지켜보던 루나가 입을 열었다. “지금 문제는 루프의 조건을 확인하는 시점과, 우리가 확인하고 싶은 값을 얻는 시점이 맞지 않아서 생기는 거야.”
루나는 솔라의 코드를 손가락으로 짚었다. “while은 몸통 {}을 실행하기 전에 조건을 검사해. 그런데 우리가 진짜 검사하고 싶은 건 사용자가 입력한 값이잖아. 그건 몸통 안에서 일어나고.”
“맞아. 그래서 밖에서 한 번, 안에서 한 번, 두 번이나 물어볼 수밖에 없었어. 이 타이밍 문제를 해결할 방법은 없을까?” 솔라의 목소리에는 아쉬움이 묻어났다.
“구조를 약간 비틀어보면 어떨까?” 루나는 솔라의 노트북 옆 빈 메모장을 가져왔다. 그리고 이렇게 적었다.
while (true) {
// 1. 일단 안으로 들어온다.
// 2. 안에서 신호를 확인하고,
// 3. 여기서 빠져나갈지 결정한다.
}
while (true). 솔라는 그 코드를 보자마자 몸을 움찔했다. “무한 루프잖아! 위험한 거 아니야?”
“탈출구만 확실하다면, 가장 강력한 무기가 될 수 있어.” 루나는 메모장에 break;라는 단어를 추가했다. “우리가 원하는 정확한 타이밍에 반복을 깨고 나올 수 있는 비상 탈출 장치지.”
이 새로운 구조는 솔라의 머릿속에 있던 while문의 개념을 뒤흔들었다. 지금까지는 ‘조건이 참인 동안 반복한다’는 문지기의 역할에만 집중했다. 하지만 while(true)는 아예 문을 활짝 열어두고, ‘들어온 손님을 내보낼지 말지’를 안에서 결정하는 방식이었다.
“이 구조를 써서 새로운 프로그램을 만들어보자.” 루나가 제안했다. “사용자가 숫자를 계속 입력하면 그 합계를 구하는 프로그램이야. 단, 사용자가 숫자 대신 ‘q’라는 글자를 입력하면 입력을 멈추고 그때까지의 총합을 출력하는 거지.”
이번엔 종료 신호가 숫자가 아니라 문자였다. 이전보다 조금 더 복잡한 문제였지만, 새로운 구조를 시험해 보기엔 안성맞춤이었다. 솔라는 다시 키보드에 손을 올렸다.
먼저 합계를 저장할 변수 sum을 만들고 0으로 초기화했다. 그리고 루나가 알려준 대로 과감하게 while (true)로 반복문을 시작했다.
Scanner scanner = new Scanner(System.in);
int sum = 0;
while (true) {
// ...
}
이제 루프의 몸통을 채울 차례다. 중복이고 뭐고 따질 필요 없이, 그냥 안으로 들어왔다. 가장 먼저 할 일은 사용자에게 입력을 받는 것이다. 종료 신호가 ‘q’라는 문자일 수 있으므로, 이번에는 nextInt()가 아니라 nextLine()으로 입력을 한 줄 통째로 받아야 했다.
while (true) {
System.out.print("더할 숫자를 입력하세요 (종료는 q): ");
String input = scanner.nextLine();
// ...
}
입력을 받았으니, 이제 탈출할 시간인지 확인해야 한다. 사용자가 입력한 문자열이 “q”와 같다면, break를 호출해서 루프를 깨고 나가면 된다.
if (input.equals("q")) {
break; // 탈출!
}
만약 탈출 조건에 해당하지 않는다면? 그건 사용자가 숫자를 입력했다는 뜻이다. 그럼 입력받은 문자열을 Integer.parseInt()를 이용해 숫자로 바꾸고, sum 변수에 더해주면 된다.
솔라는 이 모든 조각을 하나로 합쳐 코드를 완성했다.
import java.util.Scanner;
public class SumUntilQ {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int sum = 0;
while (true) {
System.out.print("더할 숫자를 입력하세요 (종료는 q): ");
String input = scanner.nextLine();
if (input.equals("q")) {
break;
}
sum += Integer.parseInt(input);
}
System.out.println("입력된 숫자의 총합: " + sum);
}
}
프로그램을 실행하자, 솔라가 원했던 그림이 펼쳐졌다.
더할 숫자를 입력하세요 (종료는 q): 10
더할 숫자를 입력하세요 (종료는 q): 25
더할 숫자를 입력하세요 (종료는 q): 7
더할 숫자를 입력하세요 (종료는 q): q
입력된 숫자의 총합: 42
코드는 군더더기 없이 깔끔했다. 입력받는 코드는 루프 안에 단 한 번만 존재했다. 불필요한 초기값 처리도 없었다. 루프 안의 논리는 ‘입력받기 → 종료 조건 확인하기 → 데이터 처리하기’라는 자연스러운 순서로 흘러갔다.
“이제 알겠어.” 솔라가 완성된 코드를 보며 말했다. “while문은 반복 횟수가 정해지지 않았을 때 쓰는 거였어. 특히 사용자의 입력을 기다리는 이런 상황에서 말이야. while(true)와 break를 함께 쓰니까, 언제 끝날지 모르는 반복을 내가 원하는 시점에, 아주 깔끔하게 끝낼 수 있네.”
더 이상 while문의 구조가 흩어져 보이지 않았다. 오히려 for문으로는 흉내 낼 수 없는 유연함과 강력함이 느껴졌다. 횟수 제어라는 족쇄를 벗어 던지고, 오직 ‘조건’이라는 열쇠 하나로 반복의 흐름을 자솔라재로 설계하는 것. 그것이 while문이 가진 진짜 힘이었다. 솔라는 이제 어떤 상황에서 for를 써야 하고, 언제 while이 더 적합한 도구인지 자신 있게 판단할 수 있게 되었다.