Java 17
do-while, 무한 루프, break/continue로 반복 제어하기
do-while, 무한 루프, break, continue가 모두 반복문 안에서 흐름을 바꾸는 말이라 차이가 섞인다.
근거 · 교안 p119-p123
1장: do-while: 일단 실행하고 나서 판단하기
솔라는 키보드 위에서 손가락을 잠시 멈췄다. 화면에는 몇 줄 안 되는 코드가 깜빡이고 있었다. ‘1부터 100 사이의 숫자를 입력받으세요.’ 간단한 목표였지만, 솔라의 미간은 살짝 찌푸려져 있었다. 뭔가 매끄럽지 않았다.
“됐어.”
나지막한 혼잣말과 함께 실행 버튼을 눌렀다. 프로그램은 곧바로 ‘숫자를 입력하세요:’라는 메시지를 띄웠다. 솔라는 시험 삼아 ‘200’을 입력하고 엔터 키를 쳤다.
유효하지 않은 숫자입니다. 다시 입력하세요.
숫자를 입력하세요:
의도대로 작동했다. 이번엔 ‘50’을 입력했다. 프로그램은 아무런 메시지 없이 깔끔하게 종료되었다. 완벽한 성공처럼 보였다. 하지만 솔라는 여전히 찜찜한 표정으로 자신의 코드를 노려보았다.
// 솔라가 작성한 코드
Scanner scanner = new Scanner(System.in);
System.out.println("숫자를 입력하세요:");
int number = scanner.nextInt(); // (A)
while (number < 1 || number > 100) {
System.out.println("유효하지 않은 숫자입니다. 다시 입력하세요.");
System.out.println("숫자를 입력하세요:");
number = scanner.nextInt(); // (B)
}
System.out.println("입력된 숫자: " + number);
“흐음….”
코드를 가만히 들여다보던 솔라의 콧잔등에 주름이 잡혔다. 방문을 열고 들어오던 루나가 그 모습을 보고 피식 웃었다.
“뭘 그렇게 심각하게 보고 있어?”
“언니, 이것 좀 봐봐. 프로그램은 잘 돌아가는데, 코드가 마음에 안 들어.”
솔라가 의자를 돌려 화면을 가리켰다.
“사용자한테 숫자를 입력받아서, 그게 1에서 100 사이가 아니면 다시 입력받게 하는 거야. 그런데 ‘숫자를 입력하세요’랑 ‘scanner.nextInt()’ 이 코드가 (A)에도 있고, (B)에도 있어. 똑같은 코드를 두 번이나 써야 하는 게 너무 이상해.”
솔라의 말대로였다. 반복문에 들어가기 전에 조건을 검사하기 위해 어쩔 수 없이 입력을 한 번 받고, 조건이 틀렸을 경우 반복문 안에서 또 입력을 받아야 하는 구조였다.
“반복문은 반복되는 걸 묶으려고 쓰는 거 아니었어? 그런데 반복문 바깥에도 똑같은 게 튀어나와 있으니… 뭔가 잘못 쓰는 기분이야.”
이게 솔라가 느낀 위화감의 정체였다. 모든 반복문은 조건을 먼저 검사하고 실행된다고 생각했기에, 첫 검사를 위한 ‘준비물’을 루프 밖에서 미리 챙겨야만 했다.
루나는 고개를 끄덕이며 솔라의 모니터 옆에 작은 메모지 한 장을 붙였다. 그리고는 펜으로 두 개의 상자를 그렸다.
“이 첫 번째 상자가 ‘숫자 입력받기’ 동작이라고 해보자. 그리고 두 번째 상자는 ‘숫자가 1에서 100 사이인지 검사하기’ 동작이야.”
루나는 솔라를 쳐다보며 물었다.
“솔라 네가 하려는 일은, 이 두 동작 중에 뭐가 반드시 먼저 일어나야 해?”
“그야 당연히… 숫자를 입력받아야 검사를 할 수 있지.”
솔라가 즉시 대답했다. 너무나 당연한 순서였다.
“맞아. 그런데 네가 쓴 while문은 어떤 순서로 동작하지? 입구에서부터 검사부터 하려고 하잖아.”
루나가 펜으로 솔라의 코드에서 while 키워드를 가리켰다. 마치 클럽 입구에서 신분증부터 검사하는 문지기 같았다. 하지만 문제는, 검사해야 할 신분증(사용자 입력값) 자체가 클럽(반복문) 안으로 들어가야만 받을 수 있다는 점이었다. 그래서 어쩔 수 없이 밖에서 한 번, 안에서 또 한 번 받는 비효율이 생긴 것이다.
“그러네… while문은 ‘선(先) 검사, 후(後) 실행’이니까. 나는 ‘선 실행, 후 검사’가 필요한 거였어.”
솔라는 무릎을 탁 쳤다. 문제의 핵심을 스스로 짚어낸 순간이었다. 일단 저지르고, 그다음에 그 결과가 괜찮은지 따져 묻는 흐름이 필요했다.
“바로 그거야. 그럴 때를 위해 문지기가 더 너그러운 반복문이 하나 있어.”
루나는 솔라의 코드 아래에 새로운 구조를 타이핑해주었다.
// 루나가 제안한 코드
int number; // 변수 선언만 먼저
do {
System.out.println("숫자를 입력하세요:");
number = scanner.nextInt();
if (number < 1 || number > 100) {
System.out.println("유효하지 않은 숫자입니다.");
}
} while (number < 1 || number > 100);
System.out.println("입력된 숫자: " + number);
do-while 이라는 낯선 형태였다. while이 맨 끝에 붙어 있고, 그 앞에 do 라는 단어가 중괄호 블록을 이끌고 있었다.
솔라는 잠시 두 코드를 비교해 보더니 눈을 동그랗게 떴다.
“어! 이러면 숫자 입력받는 코드가 한 번만 쓰이네!”
do 블록 안의 코드는 조건 검사 없이 일단 한 번 실행된다. 그리고 블록의 끝에 다다라서야 while의 조건을 만나 다음 반복을 할지 말지를 결정한다. 솔라가 원했던 ‘선 실행, 후 검사’의 흐름과 정확히 일치했다.
솔라는 자신의 기존 코드를 지우고 do-while 문으로 다시 작성했다. 훨씬 간결하고 명확해졌다. 이제는 ‘최소한 한 번은 입력을 받아야 한다’는 프로그램의 목적이 코드 구조에 그대로 드러났다.
“알겠다. while이나 for는 조건이 처음부터 틀리면 한 번도 실행 안 될 수 있지만, do-while은 무슨 일이 있어도 일단 한 번은 실행을 보장해주는 거구나.”
솔라는 만족스럽게 중얼거렸다. 똑같이 반복을 제어하는 문법이라도, 그 목적과 흐름에 따라 이렇게 다른 도구를 써야 한다는 사실이 분명하게 다가왔다. 불필요한 중복 코드가 사라진 화면이 아까보다 훨씬 똑똑해 보였다.
“그럼 이제 반복문은… 조건이 맞을 때만 도는 것, 그리고 일단 돌고 나서 생각하는 것. 이 두 종류가 있는 거네. 어쨌든 둘 다 언젠가는 멈추는 거고.”
자신감에 찬 솔라의 말에, 루나는 대답 대신 의미심장한 미소만 살짝 지었다.
“정말 그럴까? 때로는 영원히 멈추지 않는 반복이 필요할 때도 있는데.”
2장: 무한 루프: 멈추지 않는 반복의 이유
솔라는 의자 등받이에 몸을 깊게 기댔다. 화면에는 간결해진 do-while 코드가 만족스럽게 떠 있었지만, 그녀의 머릿속은 조금 전 루나 언니가 던진 한마디로 가득 차 있었다.
‘때로는 영원히 멈추지 않는 반복이 필요할 때도 있는데.’
말도 안 돼. 반복문은 정해진 일을 하고 끝내기 위해 쓰는 거 아닌가? 시작이 있으면 끝이 있어야지. 영원히 멈추지 않는 프로그램은 그냥 오류, 즉 버그일 뿐이라고 생각했다. 솔라는 자신의 생각을 증명이라도 하듯, 새 파일을 열고 키보드 위로 손을 가져갔다. 사용자가 ‘종료’라고 입력하기 전까지, 입력된 말을 앵무새처럼 따라 하는 프로그램을 만들어보기로 했다.
“좋아. ‘종료’라는 말이 입력되지 않는 ‘동안’ 계속 반복하면 되겠지.”
솔라는 while 문을 이용해 코드를 짜기 시작했다. 하지만 곧 손가락이 멈칫했다.
// 솔라의 고민
String command = ""; // (A) 처음에 뭐라고 해야 하지?
while (!command.equals("종료")) { // (B) 여기서 검사하려면...
System.out.print("말씀하세요: ");
command = scanner.nextLine();
System.out.println("앵무새: " + command);
}
또다시 미묘한 위화감이 발목을 잡았다. while 문은 (B)에서 조건을 먼저 검사한다. 그러려면 command라는 변수가 반복문에 진입하기 전에 이미 어떤 값을 갖고 있어야 했다. 솔라는 어쩔 수 없이 (A)처럼 command 변수를 빈 문자열 ""로 초기화했다. 틀린 코드는 아니었지만, 어딘가 부자연스러웠다. 이 반복의 진짜 조건은 루프 안에서 사용자의 입력을 받아야만 알 수 있는데, 그걸 흉내 내기 위해 바깥에서 억지로 값을 만들어주는 느낌이었다.
“결국 모든 반복문은 어떻게든 끝날 조건을 가지고 시작해야 하는구나.”
솔라가 혼잣말로 결론을 내리는 순간, 루나가 조용히 다가와 솔라의 모니터 옆에 놓인 스탠드 조명을 툭 켰다. 작은 LED 램프에 불이 들어오며 책상을 환하게 비췄다.
“솔라, 그 조명은 언제 꺼져?”
“응? 스위치를 누르면 꺼지지.”
“그럼 스위치를 누르기 전까지는?”
“계속 켜져 있겠지. 내가 끌 때까지는.”
솔라는 대답하고 나서야 언니의 질문 의도를 어렴풋이 눈치챘다. 루나는 이번엔 솔라의 노트북 화면 구석에서 깜빡이는 와이파이 아이콘을 가리켰다.
“저 아이콘은? ‘앞으로 5분만 더 신호를 찾고 종료합니다’ 라고 말하던?”
“아니, 그냥 계속 신호를 찾잖아. 내가 와이파이를 끄거나 컴퓨터를 끌 때까지.”
스탠드 조명, 와이파이 아이콘, 스마트폰의 잠금 화면, TV의 메뉴 화면… 주위를 둘러보니 ‘언젠가 끝나겠지’라는 막연한 예상과 달리, 명확한 종료 시점 없이 외부의 명령을 무한히 기다리는 것들로 가득했다. 그것들은 버그가 아니었다. 원래 그렇게 만들어진 것들이었다.
“네가 만든 앵무새 프로그램도 마찬가지 아닐까? 몇 번 반복하고 끝날지 미리 정해져 있지 않아. 사용자가 ‘종료’라고 말하기 전까지는 영원히 대기해야 해.”
루나는 솔라의 코드 아래에 극단적으로 단순한 코드를 한 줄 적었다.
while (true) {
// ...
}
while (true). 조건 자리에 true가 떡하니 박혀 있었다. 거짓이 될 가능성이 아예 없는, 영원한 참. 이 반복문은 절대로 스스로 멈출 수 없었다.
“이게… 된다고? 이건 그냥 고장 난 거 아니야?”
솔라의 목소리에 의심이 가득했다. 루프는 항상 종료 조건이 있어야 한다는 그녀의 믿음이 정면으로 공격받는 순간이었다.
“한번 시험해볼까?”
루나의 제안에 솔라는 반신반의하며 짧은 코드를 작성했다.
int i = 0;
while (true) {
System.out.println("멈추지 않는 기차 " + i);
i++;
}
실행 버튼을 누르자, 솔라의 눈이 휘둥그레졌다. 콘솔 창은 미친 듯한 속도로 숫자를 찍어내며 스크롤되기 시작했다.
멈추지 않는 기차 0
멈추지 않는 기차 1
멈추지 않는 기차 2
…
멈추지 않는 기차 5841
멈추지 않는 기차 5842
프로그램은 멈출 기미가 보이지 않았다. 마치 브레이크가 파괴된 기차 같았다. 솔라는 황급히 IDE의 빨간색 ‘중지’ 버튼을 눌러 프로그램을 강제로 종료했다. 그제야 광란의 질주가 멈췄다. 잠시 동안 노트북 팬이 윙 하고 도는 소리가 방 안을 채웠다.
“와… 진짜로 영원히 도네.”
솔라는 짧은 탄성을 내뱉었다. 이것은 버그가 아니었다. 명백히 의도된 동작이었다. 종료 조건이 없는 게 아니라, ‘종료하지 않는 것’이 조건인 셈이었다.
“이제 알겠어. 종료 조건이 프로그램 코드 ‘안’에 정해져 있는 게 아니라, 스위치를 누르거나, 중지 버튼을 누르는 것처럼 프로그램 ‘밖’에서 신호를 줄 때까지 계속 대기하는 상태를 만드는 거구나.”
솔라는 자신의 앵무새 프로그램을 다시 쳐다봤다. while (!command.equals("종료")) 라고 썼던 코드가 어색하게 느껴졌다. 이 프로그램의 본질은 ‘종료 조건의 만족’이 아니라 ‘무한한 대기’에 더 가까웠다.
솔라는 자신의 코드를 지우고 while (true)를 사용해 다시 작성했다.
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("말씀하세요: ");
String command = scanner.nextLine();
System.out.println("앵무새: " + command);
}
코드가 훨씬 간결하고 솔직해졌다. ‘이 반복은 외부의 개입이 없다면 끝나지 않습니다’ 라고 말하는 듯했다. 이제 이 프로그램은 사용자의 입력을 무한히 기다릴 것이다.
그런데 바로 그 순간, 새로운 의문이 고개를 들었다.
“좋아, 이제 무한히 반복하는 앵무새는 만들었어. 그런데… 사용자가 ‘종료’라고 입력하면 어떻게 이 무한의 고리를 끊지? 아까처럼 맨날 빨간 버튼을 누를 순 없잖아.”
스스로 멈출 의지가 없는 반복문. 하지만 프로그램 안에서, 특정 조건이 만족되었을 때 이 반복을 우아하게 탈출시킬 방법이 반드시 필요했다. 솔라는 while (true) 블록 안을 물끄러미 바라보았다. 이 견고한 감옥을 부술 비상 탈출구가 어디엔가 있을 것 같았다.
3장: break: 반복문에서 ‘탈출!’
솔라는 자신이 만든 앵무새 프로그램의 콘솔 창을 멍하니 바라보고 있었다. while(true)로 만든 무한의 고리는 견고했다.
말씀하세요: 안녕
앵무새: 안녕
말씀하세요: 오늘 날씨 어때?
앵무새: 오늘 날씨 어때?
솔라는 키보드로 ‘종료’라고 타자를 치고 엔터 키를 눌렀다.
말씀하세요: 종료
앵무새: 종료
앵무새는 ‘종료’라는 단어마저 충실하게 따라 할 뿐, 멈출 기미가 보이지 않았다. 프로그램의 의도대로라면 저 말을 듣는 순간 대화를 끝내야 했다. 솔라는 한숨을 쉬며 IDE의 붉은색 중지 버튼을 눌렀다. 스스로 멈출 방법이 없는 반복문은 마치 출구 없는 방과 같았다.
“어떻게 빠져나가야 하지?”
솔라는 while(true) 블록 안에 코드를 추가하기 시작했다. 사용자의 입력이 ‘종료’와 같다면, 무언가 조치를 취해야 했다.
while (true) {
System.out.print("말씀하세요: ");
String command = scanner.nextLine();
if (command.equals("종료")) {
// 여기서 뭘 해야 반복이 멈출까?
}
System.out.println("앵무새: " + command);
}
하지만 if문의 중괄호 안을 채울 마땅한 명령어가 떠오르지 않았다. 반복문의 조건 자체가 영원한 true이니, 조건식을 바꿀 수도 없는 노릇이었다. 솔라는 잠시 고민하다가, 문득 switch-case문에서 봤던 키워드를 떠올렸다.
“혹시… break?”
switch문에서 하나의 case가 실행된 후, 원치 않게 다음 case로 넘어가는 것을 막기 위해 썼던 명령어였다. ‘흐름을 깨뜨린다’는 어감이 지금 상황에 어울리는 것 같았다. 하지만 그건 switch문 안에서나 쓰는 특별한 규칙이 아닐까? 반복문에서 사용하면 어떻게 될지 확신이 서지 않았다.
그때, 솔라의 고민을 들여다보던 루나가 나지막이 말했다.
“비상 탈출 스위치 같은 게 필요하겠네.”
루나는 책상 위 포스트잇에 네모난 상자를 하나 그렸다. 상자 위에는 while(true)라고 적었다. 그리고 상자 안에는 빙글빙글 도는 화살표를 그려 넣어, 코드의 흐름이 계속해서 맴도는 모습을 표현했다.
“이 반복문은 스스로 나올 문이 없는 방이야. 조건이 항상 ‘참’이라서, 정상적인 방법으로는 저 화살표를 멈출 수가 없어.”
루나는 솔라의 코드를 가리켰다.
“그런데 네가 if (command.equals("종료")) 라고 쓴 저 부분. 저게 바로 비상 상황을 감지하는 센서야. ‘종료’ 신호가 들어오면, 문이 없더라도 벽을 부수고 나와야지.”
루나는 펜으로 상자의 한쪽 벽을 가리키며 말했다.
“네가 떠올린 break가 바로 그 벽을 부수는 망치야.”
루나는 빙글빙글 돌던 화살표가 if 조건을 만나는 지점에서, 상자 벽을 뚫고 밖으로 뻗어 나가는 새로운 화살표를 그려 넣었다. 그리고 그 화살표 위에 ‘break’라고 적었다.
“break는 자기가 속한 가장 가까운 반복문이나 switch문의 벽을 깨고 즉시 탈출하는 명령어거든. ‘흐름을 깨뜨린다’고 생각한 게 정확해.”
‘가장 가까운 반복문의 벽을 깨고 탈출한다.’ 솔라는 그 말을 곱씹으며 코드를 수정했다. 반신반의하던 마음에 확신이 들기 시작했다.
while (true) {
System.out.print("말씀하세요: ");
String command = scanner.nextLine();
if (command.equals("종료")) {
break; // 반복문을 탈출!
}
System.out.println("앵무새: " + command);
}
System.out.println("앵무새가 잠들었습니다.");
탈출이 성공했는지 확인하기 위해, 솔라는 반복문이 끝난 직후 “앵무새가 잠들었습니다.”라는 메시지를 출력하도록 코드를 추가했다. 만약 break가 정말로 반복문을 완전히 끝낸다면, 저 메시지가 화면에 나타날 것이다.
솔라는 침을 꿀꺽 삼키고 프로그램을 실행했다.
말씀하세요: 안녕
앵무새: 안녕
말씀하세요: 종료
엔터 키를 누르는 순간, 더 이상 ‘앵무새:’라는 메시지는 나타나지 않았다. 대신, 그토록 기다렸던 문장이 콘솔 창에 찍혔다.
앵무새가 잠들었습니다.
프로그램은 오류 없이 깔끔하게 종료되었다. break는 정말로 while(true)라는 견고한 감옥의 벽을 부수고 흐름을 밖으로 끄집어낸 것이었다.
“와… 진짜 되네. break는 반복문 자체를 그냥 끝내버리는 거구나.”
솔라는 감탄했다. break가 단순히 이번 차례를 건너뛰는 정도일 거라고 막연히 추측했던 것이 틀렸다. 이것은 ‘건너뛰기’가 아니라 ‘완전한 중단’이었다. 특정 조건을 만족했을 때, 남은 반복 횟수나 반복문의 다른 코드들을 모조리 무시하고 즉시 빠져나오는 비상 탈출 장치. 무한 루프와 break는 떼려야 뗄 수 없는 한 쌍처럼 느껴졌다.
“좋아, 그럼 ‘종료’는 break로 해결했어. 그런데 만약 사용자가 아무것도 입력하지 않고 엔터만 치면 어떡하지?”
솔라의 머릿속에 새로운 질문이 떠올랐다.
말씀하세요:
앵무새:
현재 코드는 빈 줄마저 그대로 따라 한다. 보기 좋지 않다.
“이럴 땐 ‘앵무새:’ 부분을 실행하지 않고 그냥 건너뛰어서 다시 ‘말씀하세요:’라고 물어보면 좋겠어. 그렇다고 break를 쓸 수는 없잖아? 그럼 프로그램이 아예 끝나버리니까.”
반복을 끝내고 싶지는 않지만, 이번 한 번만 살짝 건너뛰고 싶은 상황. 솔라는 break만큼 강력하지는 않지만, 더 유연하게 흐름을 제어할 또 다른 도구가 필요하다는 것을 직감했다.
4장: continue: 이번 반복만 ‘건너뛰기’
솔라는 자신의 앵무새 프로그램을 다시 실행했다. ‘종료’ 명령어를 만나면 비상 탈출하는 break 덕분에 프로그램은 이제 제법 똑똑해졌다. 하지만 솔라는 곧 새로운 허점을 발견했다. 그녀는 ‘말씀하세요:’ 프롬프트에 아무것도 입력하지 않고 그냥 엔터 키를 눌러보았다.
말씀하세요:
앵무새:
콘솔 창에는 앵무새가 빈 줄을 따라 하는 어색한 결과가 출력되었다. 보기 좋지 않았다. 프로그램이 끝나서는 안 되지만, 이런 무의미한 입력은 무시하고 다시 질문을 던져야 했다. 솔라는 코드에 if 문을 추가해 빈 문자열 입력을 감지하도록 했다. 그러나 막상 그 안을 어떻게 채워야 할지 막막했다.
// 솔라의 코드
while (true) {
System.out.print("말씀하세요: ");
String command = scanner.nextLine();
if (command.equals("종료")) {
break; // 이건 반복문을 완전히 끝낸다.
}
if (command.isEmpty()) {
// 여기에 뭘 써야 '이번만' 건너뛸 수 있지?
// break를 쓰면 프로그램이 끝나버릴 텐데...
}
System.out.println("앵무새: " + command);
}
break는 비상 탈출용 망치였다. 지금 필요한 건 망치가 아니었다. 반복이라는 게임은 계속하되, 이번 판만 무효로 하고 다음 판으로 넘어가는 규칙이 필요했다. break와 비슷하지만, 효과는 전혀 다른 어떤 제어문이 있을 거라고 막연히 짐작할 뿐이었다.
그때 루나가 솔라의 옆으로 다가와 책상 위에 놓인 숫자 카드 뭉치를 집어 들었다. 1부터 10까지 숫자가 적힌 카드였다.
“간단한 게임 하나 하자. 내가 카드를 1부터 10까지 차례대로 넘길 거야. 너는 그 숫자가 짝수일 때만 숫자를 외쳐. 홀수면 아무 말도 하지 않는 거야.”
“좋아.”
솔라는 고개를 끄덕였다. 루나는 첫 번째 카드를 뒤집었다. ‘1’이었다. 솔라는 입을 다물었다. 루나는 다음 카드를 넘겼다. ‘2’가 나오자 솔라는 “이!” 하고 외쳤다. 다음 카드 ‘3’에는 다시 침묵. ‘4’에는 “사!” 하고 외쳤다. 게임은 카드 10까지 순식럽게 진행되었다.
“방금 네가 한 일을 생각해 봐.”
게임이 끝나자 루나가 물었다.
“내가 ‘3’이나 ‘5’ 같은 홀수 카드를 보여줬을 때, 넌 어떻게 했어?”
“아무것도 안 했지. 그냥 다음 카드를 기다렸어.”
“게임을 그만뒀어? ‘이제 안 해!’ 하고 자리를 떴어?”
“아니. 게임은 계속했지. 그냥 이번 차례에 외칠 필요가 없어서 건너뛴 거야.”
솔라는 대답을 하다가 문득 깨달았다. 바로 이것이었다. 반복이라는 게임의 판을 깨는 것이 아니라, 현재의 차례만 건너뛰고 다음으로 넘어가는 것.
“네가 말한 ‘건너뛰는’ 행동, 그게 바로 continue 야.”
루나는 포스트잇 한 장을 가져와 어제 그렸던 break의 그림 옆에 나란히 붙였다. while 루프를 상징하는 네모 상자와 그 안에서 맴도는 화살표를 똑같이 그렸다.
“break는 이 상자의 벽을 부수고 밖으로 탈출하는 망치였지.”
루나는 break 그림의 탈출 화살표를 콕 찍었다. 그리고는 새 포스트잇에 다른 종류의 화살표를 그리기 시작했다.
“continue는 망치가 아니야. 이건 순간이동 장치에 가까워.”
루나는 if 조건에 걸렸을 때, 반복문 안의 남은 코드(System.out.println)를 건너뛰고 곧장 루프의 맨 처음으로 돌아가는 화살표를 그렸다. 마치 뱀사다리 게임에서 사다리를 타고 위로 훌쩍 올라가 버리는 모습과 같았다.
“continue를 만나면, 그 아래에 있는 코드들은 모두 무시돼. 그리고 반복문은 즉시 다음 회차를 준비하러 가. for문이라면 증감식을 실행하고, while문이라면 다시 조건 검사로 돌아가지. 어쨌든 반복을 끝내진 않아.”
break는 완전한 탈출. continue는 현재 회차만 건너뛰기. 둘 다 반복의 흐름을 제어하지만, 그 목적지가 완전히 달랐다. break의 목적지는 반복문 ‘밖’이었고, continue의 목적지는 반복문의 ‘다음 시작점’이었다.
솔라는 방금 했던 카드 게임을 코드로 옮겨보면 그 차이가 더 명확해질 것 같았다.
for (int i = 1; i <= 10; i++) {
if (i % 2 != 0) { // 홀수이면,
continue; // 아래 코드를 실행하지 말고 다음 숫자로 넘어가!
}
System.out.println("짝수 발견: " + i);
}
코드를 실행하자 콘솔에는 짝수만 정확히 출력되었다. continue 문이 홀수가 나올 때마다 System.out.println 코드가 실행될 기회를 박탈하고 루프의 다음 회차로 강제 이동시킨 것이다. break를 썼다면 ‘짝수 발견: 2’가 출력된 후, i가 3일 때 반복문 자체가 끝나버렸을 것이다.
이제 앵무새 프로그램의 빈칸을 채울 수 있었다.
// 솔라가 완성한 코드
if (command.isEmpty()) {
continue; // '앵무새:' 출력을 건너뛰고 다음 입력을 받으러 간다.
}
솔라는 자신 있게 continue;를 써넣고 프로그램을 다시 실행했다.
말씀하세요: 안녕
앵무새: 안녕
말씀하세요: (엔터)
말씀하세요: (질문이 다시 나온다!)
말씀하세요: 잘 가
앵무새: 잘 가
말씀하세요: 종료
앵무새가 잠들었습니다.
완벽했다. 빈 줄을 입력하자 앵무새는 아무런 대답 없이 침묵했고, 프로그램은 곧바로 다음 입력을 요구했다. 반복은 끝나지 않았지만, 불필요한 작업은 건너뛰었다.
솔라는 화면에 완성된 코드를 보며 만족스럽게 미소 지었다. do-while로 일단 실행하고, while(true)로 무한히 기다리며, break로 탈출하고, continue로 건너뛴다. 이제 반복의 흐름을 마음대로 주무를 수 있는 여러 개의 도구를 손에 쥔 기분이었다.
“좋아, 이제 도구는 다 모인 것 같네.”
솔라가 중얼거렸다.
“그런데 언니, 만약 ‘1부터 100까지 짝수의 합을 구하라’는 문제가 있다면 어떤 도구를 써야 제일 좋을까? for문에 continue를 쓰는 방법도 있고, if문으로 짝수일 때만 더하는 방법도 있겠지. 무한 루프랑 break로도 어떻게든 만들 수는 있을 것 같고… 상황에 따라 가장 좋은 전략은 어떻게 고르는 거야?”
개별 도구의 사용법은 익혔지만, 실제 문제를 앞에 두고 최적의 도구 조합을 선택하는 것은 또 다른 차원의 문제였다. 솔라의 시선은 새로운 과제를 향하고 있었다.
5장: 반복 제어, 최적의 전략을 선택하다
솔라의 책상 위, 노트북 화면이 세 개의 창으로 나뉘어 있었다. 각 창에는 똑같은 목적을 가진 코드가 조금씩 다른 모습으로 담겨 있었다. ‘1부터 사용자가 입력한 숫자 n까지의 짝수 합 구하기’. 세 코드 모두 정확한 답을 내놓았다. 하지만 솔라의 표정은 개운하지 않았다. 마치 똑같은 목적지로 가는 세 개의 다른 지도를 들고 어느 길로 가야 할지 몰라 망설이는 사람 같았다.
첫 번째 코드는 가장 평범하고 익숙한 방식이었다. for 반복문으로 1부터 n까지 모든 숫자를 확인하며, if문으로 짝수일 때만 합계에 더했다. 두 번째 코드는 for문은 같았지만 if문의 조건이 반대였다. 홀수이면 continue를 사용해 아래의 덧셈 코드를 건너뛰었다. 세 번째 코드는 조금 억지스러웠다. while(true) 무한 루프 안에서 숫자를 하나씩 늘리다가, 숫자가 n보다 커지면 break로 탈출하는 방식이었다.
솔라는 턱을 괸 채 세 개의 코드를 번갈아 노려보았다.
“전부 답은 똑같이 나오는데… 그럼 아무거나 써도 되는 건가? 어떤 게 더 좋은 코드인지 기준이 없어.”
이것이 솔라가 마주한 새로운 혼란이었다. do-while, while(true), break, continue. 강력한 도구들의 사용법은 익혔지만, 막상 문제 앞에 서니 어떤 연장을 들어야 할지 판단이 서지 않았다. 다들 비슷하게 반복의 흐름을 바꾸는 것 같아 보였다.
그때 방에 들어온 루나가 나란히 떠 있는 세 개의 코드 창을 흥미롭게 들여다보았다.
“모두 같은 역으로 가는 지하철 노선도를 보고 있는 것 같네. 1호선, 2호선, 3호선. 전부 목적지에 도착은 시켜주지.”
루나는 의자를 끌어와 솔라 옆에 앉았다. 그리고는 첫 번째 코드 창을 손가락으로 가리켰다.
// 1번 전략: 짝수만 '골라내기'
int sum = 0;
for (int i = 1; i <= n; i++) {
if (i % 2 == 0) { // 짝수이면,
sum += i; // 더한다.
}
}
“이 코드는 우리에게 어떤 이야기를 들려주는 것 같아? 이 코드의 목소리로 말해본다면.”
“음… ‘1부터 n까지 모든 숫자를 하나씩 살펴볼게. 그중에서 짝수인 애들만 골라서 더할 거야.’ 라는 느낌?”
솔라가 대답했다. 루나는 고개를 끄덕이고 두 번째 코드 창으로 손가락을 옮겼다.
// 2번 전략: 홀수는 '무시하기'
int sum = 0;
for (int i = 1; i <= n; i++) {
if (i % 2 != 0) { // 홀수이면,
continue; // 그냥 지나가.
}
sum += i; // 홀수가 아니라면 더한다.
}
“이건? 목소리가 좀 다른가?”
솔라는 잠시 생각에 잠겼다. 미묘한 차이가 느껴졌다.
“얘는… ‘1부터 n까지 일단 다 보긴 할 건데, 혹시 홀수면? 에이, 넌 관심 없으니까 그냥 넘어가자! 그리고 나머지만 더할게.’ 약간 이런 느낌이야. 적극적으로 짝수를 찾는 게 아니라, 귀찮은 홀수를 먼저 쳐내는 데 관심이 있는 것 같아.”
“정확해. 마지막 세 번째는?”
루나가 세 번째 코드 창을 가리켰다.
// 3번 전략: '일단 계속 가다가' 멈추고 더하기
int sum = 0;
int i = 1;
while (true) {
if (i > n) {
break; // n을 넘으면 탈출.
}
if (i % 2 == 0) {
sum += i;
}
i++;
}
“이건 좀… 수다스러워. ‘일단 끝없이 달려볼게! 아, 근데 지금 n을 넘었나? 넘었으면 그만하자. 안 넘었으면 혹시 짝수인지 볼까? 짝수면 더하고. 이제 다음 숫자로 가자!’ 라고 말하는 것 같아.”
이야기를 만들어내고 보니, 세 코드의 성격이 뚜렷하게 보였다. 1번은 문제의 요구사항인 ‘짝수 합계’를 가장 직접적으로 표현했다. 2번은 ‘특정 조건을 제외한다’는 의도가 강할 때 어울렸다. 3번은 반복의 핵심 조건(n까지)이 루프의 시작점이 아닌 내부에 흩어져 있어 코드를 읽는 사람이 이야기를 파악하기 가장 어려웠다.
“알겠다…!”
솔라는 손뼉을 쳤다.
“단순히 작동하느냐 마느냐의 문제가 아니었어. 코드가 얼마나 ‘솔직하게’ 자기 의도를 드러내느냐의 문제구나. 1번은 ‘짝수를 더한다’는 목적이 명확하고, 2번은 ‘홀수는 제외한다’는 필터링의 목적이 뚜렷해. 3번은 굳이 이렇게 할 이유가 없어. for문이 훨씬 더 이야기를 간단하게 만드니까.”
문제를 해결하는 방법은 여러 가지일 수 있지만, 그중에서도 문제의 본질을 가장 명확하고 간결하게 표현하는 방법이 좋은 전략이라는 사실을 깨달은 것이다. do-while은 ‘최소 한 번 실행’이라는 이야기가 필요할 때, while(true)와 break는 ‘외부 신호로 종료되는 무한 대기’라는 이야기가 필요할 때 가장 빛을 발하는 도구였다.
솔라는 자신이 만들었던 앵무새 프로그램을 떠올렸다. 그 프로그램은 이제 개별 기능의 집합이 아닌, 하나의 잘 짜인 이야기로 보였다.
‘사용자의 입력을 무한히 기다리되(while-true), 만약 빈 입력이면 이번 회차는 건너뛰고(continue), ‘종료’라는 특정 신호를 받으면 대화를 완전히 끝낸다(break).’
각 제어문은 저마다의 목적을 가지고 명확한 역할을 수행하고 있었다.
“그럼 이제 이 도구들로 더 복잡한 이야기도 만들 수 있겠어.”
솔라는 비어 있는 새 파일 탭을 클릭했다. 그녀의 머릿속에는 새로운 프로그램의 구조가 그려지고 있었다. 사용자의 입력을 받아 유효성을 검사하고, 특정 조건에서만 연산을 수행하며, 원할 때 종료할 수 있는 간단한 계산기 프로그램.
솔라는 키보드에 손을 얹고 주석을 달기 시작했다. 코드가 아닌, 프로그램의 시나리오를 먼저 쓰는 중이었다.
// 1. "계산할 첫 번째 숫자를 입력하세요." -> 최소 한 번은 물어봐야 하니 do-while
// 2. 연산자를 입력받고, 두 번째 숫자를 입력받는다.
// 3. 계산 결과를 보여준다.
// 4. "더 계산하시겠습니까?" -> 이 모든 과정을 계속 반복할지 결정. while(true)와 break 조합이 좋겠다.
// 5. 만약 연산자가 이상하면? -> "잘못된 연산자" 메시지만 보여주고 다시 입력받기. continue!
이제는 코드를 작성하기 전에, 어떤 흐름 제어 도구가 어떤 이야기의 장면에 가장 어울리는지 먼저 생각하게 되었다. 손에 쥔 망치, 드라이버, 렌치의 용도를 명확히 구분하고, 설계도에 맞춰 최적의 연장을 선택하는 목수처럼. 솔라의 손가락이 자신감 있게 움직이기 시작했다. 반복문을 제어하는 것은, 결국 코드의 흐름으로 가장 명료한 이야기를 만드는 일이었다.