자바 연산자와 제어문 - if switch 식 for while 그리고 break continue (자바 초급 3단원)
이 단원에서 배우는 것
2단원에서 연체일과 연체료를 변수에 담았다. 하지만 값을 담기만 해서는 프로그램이 아니다. "7일까지는 하루 100원, 그 뒤로는 200원"처럼 조건에 따라 다르게 계산하고, "연체일 수만큼" 반복해야 한다. 이번 단원이 그 두 가지를 담당한다.
도서 대출 시스템의 연체료 정책을 계속 예제로 쓴다. 정책은 이렇게 정한다. 연체 0일은 0원, 1~7일은 하루 100원, 8일부터는 하루 200원이다.
- 비교·논리·복합대입 연산자를 쓰고, 단축 평가(short-circuit)가 언제 이득이 되는지 설명한다.
if사슬과switch문을 구분해 쓰고, Java 14 부터 정식이 된switch식으로 값을 바로 받는다.for·while을 상황에 맞게 고르고,break·continue·라벨로 흐름을 끊는다.
왜 필요한가
조건문 없이는 정책을 표현할 수 없다
연체료 정책은 한 줄짜리 수식이 아니다. 구간마다 단가가 다르고, 회원 등급마다 한도가 다르고, 예외 조건이 붙는다. 현실의 규칙은 거의 항상 이런 모양이라서, 조건 분기를 어떻게 쓰느냐가 코드의 읽히는 정도를 그대로 결정한다.
그런데 자바의 조건문에는 두 세대가 섞여 있다. 예전부터 있던 if 문과 switch 문은 "실행"만 하고 값을 돌려주지 않는다. 그래서 결과를 담을 변수를 미리 만들고, 각 가지에서 그 변수에 대입하는 형태가 된다.
String grade; // 값이 없는 상태로 먼저 선언
if (overdueDays == 0) {
grade = "정상";
} else if (overdueDays <= 7) {
grade = "경고";
}
// else 를 빼먹으면 grade 가 초기화되지 않아 컴파일 에러
이 형태의 문제는 가지가 늘어날수록 "이 변수가 모든 경로에서 채워지는가"를 사람이 확인해야 한다는 점이다. Java 14 에서 정식이 된 switch 식은 이 검사를 컴파일러에게 넘긴다. 초급 단계에서 두 형태를 같이 배워 두면, 남의 코드를 읽을 때와 새로 쓸 때 각각 다른 도구를 쓸 수 있다.
반복문 없이는 누적을 못 한다
연체료를 구간별로 계산하려면 하루씩 훑으면서 단가를 더해야 한다. 수식 하나로 접을 수도 있지만, 정책이 바뀌었을 때 고치기 쉬운 쪽은 대개 반복문 쪽이다. 반복은 자바에서 for, while, do-while 세 가지 형태로 쓴다. 배열을 훑는 향상된 for 는 4단원에서 배열을 배우고 나서 다룬다.
문법과 예제
연산자 정리
| 분류 | 연산자 | 주의할 점 |
|---|---|---|
| 산술 | + - * / % | 정수끼리 / 는 몫만 남는다(2단원) |
| 비교 | == != > >= < <= | 참조형에 == 를 쓰면 주소 비교다(7단원) |
| 논리 | && || ! | 단축 평가한다 |
| 비트/비단축 논리 | & | ^ ~ << >> >>> | boolean 에 쓰면 단축 평가를 안 한다 |
| 복합 대입 | += -= *= /= %= | 안에 형변환이 숨어 있다 |
| 증감 | ++ -- | 앞에 붙는지 뒤에 붙는지로 결과가 달라진다 |
| 삼항 | 조건 ? A : B | 중첩하면 급격히 안 읽힌다 |
public class Operators {
public static void main(String[] args) {
int x = 5;
System.out.println(x++ + " " + x); // 5 6 (쓰고 나서 증가)
int y = 5;
System.out.println(++y + " " + y); // 6 6 (증가하고 나서 씀)
boolean isMember = false;
int borrowed = 3;
System.out.println(isMember && borrowed < 5); // false
System.out.println(isMember & borrowed < 5); // false (오른쪽도 평가함)
int overdueDays = 12;
String label = overdueDays > 0 ? "연체" : "정상";
System.out.println(label); // 연체
}
}
&& 는 왼쪽이 false 면 오른쪽을 아예 실행하지 않는다. 이게 단순한 성능 얘기가 아니라 안전장치로 쓰인다. 7단원에서 title != null && title.length() > 0 같은 관용구를 보게 되는데, 순서를 바꾸면 NullPointerException 이 난다.
if 사슬
public class OverdueGrade {
public static void main(String[] args) {
int overdueDays = 12;
String grade;
if (overdueDays == 0) {
grade = "정상";
} else if (overdueDays <= 7) {
grade = "경고";
} else if (overdueDays <= 30) {
grade = "연체";
} else {
grade = "대출정지";
}
System.out.println(overdueDays + "일 -> " + grade); // 12일 -> 연체
}
}
순서가 중요하다. 위에서부터 검사해 처음 걸리는 가지 하나만 실행되므로, overdueDays <= 30 을 overdueDays <= 7 보다 위에 두면 "경고" 가 영원히 나오지 않는다. 이런 실수를 하고도 컴파일러는 아무 말 안 한다.
switch 문과 switch 식
값이 딱 떨어지는 몇 개 중 하나일 때는 switch 가 if 사슬보다 읽기 쉽다. int 계열, char, String, 그리고 열거형에 쓸 수 있다. 예전 문법은 이렇게 생겼다.
String memberType = "STUDENT";
int loanLimit;
switch (memberType) {
case "STUDENT":
loanLimit = 5;
break;
case "STAFF":
loanLimit = 10;
break;
default:
loanLimit = 3;
}
System.out.println(loanLimit); // 5
Java 14 부터 정식이 된 화살표 switch 식은 같은 일을 이렇게 쓴다. break 가 필요 없고, 결과를 바로 변수에 담는다.
public class LoanLimit {
public static void main(String[] args) {
String memberType = "STUDENT";
int loanLimit = switch (memberType) {
case "STUDENT" -> 5;
case "STAFF" -> 10;
case "PROFESSOR" -> 20;
default -> 3;
};
System.out.println("대출 한도: " + loanLimit); // 대출 한도: 5
int month = 4;
String season = switch (month) {
case 12, 1, 2 -> "겨울";
case 3, 4, 5 -> "봄";
case 6, 7, 8 -> "여름";
default -> {
String s = "가을";
yield s; // 블록에서 값을 내보낼 때는 yield
}
};
System.out.println(season); // 봄
}
}
세 가지가 달라졌다. (가) case A, B -> 로 여러 값을 묶는다. (나) break 가 없고 아래로 흘러내리지 않는다. (다) 식이므로 모든 경우가 값을 내야 한다. 그래서 default 를 빠뜨리면 컴파일 에러가 나고, 이게 앞서 말한 "모든 경로에서 값이 채워지는가"를 컴파일러가 대신 검사해 주는 지점이다.
Java 21 에서는 여기에 패턴 매칭이 더해져 case Integer i -> 처럼 타입으로 분기하고 case null 을 따로 받을 수 있다. Java 17 에서는 미리보기 기능이라 --enable-preview 없이는 못 쓴다. 이 커리큘럼은 위의 형태까지만 쓴다.
for 와 while
반복 횟수가 미리 정해져 있으면 for, 조건이 만족될 때까지 도는 것이면 while 이 자연스럽다. 연체료 누적은 "연체일 수만큼"이므로 for 다.
public class LateFeeLoop {
public static void main(String[] args) {
int overdueDays = 12;
int fee = 0;
for (int day = 1; day <= overdueDays; day++) {
if (day <= 7) {
fee += 100;
} else {
fee += 200;
}
}
System.out.println("연체료 " + fee + "원"); // 연체료 1700원
}
}
for (초기화; 조건; 증감) 의 세 칸은 각각 한 번, 매회 시작 전, 매회 끝난 뒤에 실행된다. day 는 for 안에서만 살아 있고 밖에서는 쓸 수 없다. 이건 실수를 막아 주는 장치다.
int remain = 5;
while (remain > 0) {
System.out.print(remain + " ");
remain--; // 이 줄을 빠뜨리면 무한 루프
}
System.out.println(); // 5 4 3 2 1
int attempt = 0;
do {
attempt++;
} while (attempt < 0);
System.out.println(attempt); // 1 (조건이 거짓이어도 한 번은 돈다)
break, continue, 라벨
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) continue; // 짝수는 건너뛴다
if (i > 7) break; // 7 을 넘으면 반복 자체를 끝낸다
System.out.print(i + " ");
}
System.out.println(); // 1 3 5 7
break 는 가장 안쪽 반복문 하나만 빠져나온다. 중첩 반복에서 바깥까지 한 번에 끊고 싶으면 라벨을 쓴다. 책장과 칸을 훑다가 조건을 만나면 전체를 멈추는 상황이 그렇다.
public class ShelfScan {
public static void main(String[] args) {
outer:
for (int shelf = 1; shelf <= 3; shelf++) {
for (int slot = 1; slot <= 3; slot++) {
if (shelf * slot > 4) break outer;
System.out.print(shelf + "-" + slot + " ");
}
}
System.out.println(); // 1-1 1-2 1-3 2-1 2-2
}
}
라벨은 강력하지만 흐름을 따라가기 어렵게 만든다. 중첩이 두 겹을 넘어가면 라벨을 쓰기 전에 안쪽을 메서드로 빼는 쪽을 먼저 고려한다. 5단원에서 그 방법을 배운다.
실무에서 자주 틀리는 것
1. switch 문에서 break 를 빠뜨린다
예전 switch 문은 break 를 만날 때까지 아래 case 로 계속 흘러내린다. 문법 오류가 아니라 의도된 기능이라서 컴파일러가 막지 않는다.
int grade = 3;
switch (grade) {
case 3:
System.out.println("case 3");
case 4:
System.out.println("case 4 (fallthrough)");
break;
default:
System.out.println("default");
}
// 출력:
// case 3
// case 4 (fallthrough)
이 사고를 원천적으로 없애는 방법이 화살표 switch 다. 새로 쓰는 코드는 화살표 형태를 기본으로 하고, 콜론 형태는 남의 코드를 읽을 때만 쓴다고 정해 두면 편하다.
2. 실수를 == 로 비교한다
2단원에서 본 부동소수점 오차가 조건문에서 그대로 사고가 된다.
double a = 0.1 + 0.2;
System.out.println(a == 0.3); // false
System.out.println(Math.abs(a - 0.3) < 1e-9); // true
"금액이 정확히 0원일 때" 같은 조건을 double 로 판정하면 어느 날 통과하지 않는다. 조건에 실수 == 가 보이면 일단 의심한다.
3. 조건문 뒤에 세미콜론을 찍는다
int overdueDays = 12;
if (overdueDays > 0); // 여기 세미콜론
{
System.out.println("연체"); // 조건과 무관하게 항상 실행된다
}
컴파일도 되고 실행도 된다. if 의 본문이 "아무것도 안 함"이 되고, 아래 중괄호는 그냥 블록이라 무조건 실행된다. for, while 뒤에 찍으면 무한 루프가 된다. IDE 경고를 켜 두면 잡히지만, 경고를 끄고 사는 프로젝트에서는 실제로 나온다.
4. 복합 대입 연산자가 형변환을 숨긴다
a += b 는 a = a + b 와 같지 않다. 명세상 a = (a의 타입)(a + b) 이고, 축소 형변환이 자동으로 끼어든다.
int days = 10;
// days = days + 1.5; // 컴파일 에러: double 을 int 에 담을 수 없다
days += 1.5; // 통과한다
System.out.println(days); // 11 (11.5 가 잘렸다)
드러나 있으면 에러로 막아 주는 것을, += 로 쓰면 조용히 통과시킨다. 정수 변수에 실수를 누적하는 코드는 매 회차 소수점을 버리므로 결과가 크게 어긋난다.
5. switch 대상이 null 이면 터진다
Java 17 까지 switch 의 대상 문자열이 null 이면 default 로 가지 않고 NullPointerException 이 난다. default 가 있으니 안전하다고 생각하기 쉬운 지점이다.
String memberType = null;
// switch (memberType) { ... } // NullPointerException
String safe = (memberType == null) ? "UNKNOWN" : memberType;
int limit = switch (safe) {
case "STUDENT" -> 5;
default -> 3;
};
System.out.println(limit); // 3
Java 21 부터는 case null -> 를 직접 쓸 수 있어 이 처리가 필요 없어진다.
스스로 확인하기
- 아래
for문의 출력은 무엇인가? 그리고1 2 3 4 5가 나오도록 고쳐라.for (int i = 1; i < 5; i++); System.out.print(i + " "); - 회원 등급 문자열(
"VIP","NORMAL", 그 외)에 따라 연체료 감면율을 각각 0.5, 0.2, 0.0 으로 정하는 코드를 화살표switch식으로 쓰라. - 연체일이 1부터 20일까지일 때, 이 단원의 정책으로 계산한 연체료가 처음으로 3000원을 넘는 날은 며칠째인가?
for와break로 구하는 코드를 쓰고 답을 적어라.
정답
- 컴파일 에러다.
for뒤의 세미콜론 때문에System.out.print는 반복문 밖의 독립된 문장이 되고, 그 자리에서i는 이미 범위를 벗어나 존재하지 않는다. 세미콜론을 없애고 본문을 중괄호로 묶으면 된다.
원래 조건for (int i = 1; i < 6; i++) { System.out.print(i + " "); }i < 5로는 4까지만 나오므로i < 6또는i <= 5로 바꿔야1 2 3 4 5가 된다. String memberGrade = "NORMAL"; double discountRate = switch (memberGrade) { case "VIP" -> 0.5; case "NORMAL" -> 0.2; default -> 0.0; }; System.out.println(discountRate); // 0.2- 19일째다. 7일까지 700원이고 8일째부터 하루 200원이므로
700 + (day - 7) * 200이 3000을 넘는 첫 날을 찾으면 된다. 18일째가 2900원, 19일째가 3100원이다.int fee = 0; int answer = -1; for (int day = 1; day <= 20; day++) { fee += (day <= 7) ? 100 : 200; if (fee > 3000) { answer = day; break; } } System.out.println(answer + "일째, " + fee + "원"); // 19일째, 3100원
다음 단원에서는 책 한 권이 아니라 여러 권을 다룬다. 배열을 배우고, 배열을 훑는 향상된 for 를 여기에 얹는다.