switch가 식(expression)이 된 순간 — 연산자에서 제어 흐름까지
예전 Java에서 switch를 쓰면 이랬다:
String result;
switch (day) {
case 1: result = "월"; break;
case 2: result = "화"; break;
// ... break 빼먹으면 다음 case로 흘러내림
}
변수 선언 → switch → case마다 대입 → break. 이게 값 하나를 결정하는 데 너무 많은 의식이다. Java 14부터 switch가 값을 반환하는 식이 됐다 — String result = switch(day) { case 1 -> "월"; ... }; 한 줄로. break도 없고, 흘러내림도 없다. (JEP-361)
이 글은 이 변화의 배경이 되는 연산자 함정(&& vs &, 문자열 +)부터, switch expression과 pattern matching for switch까지 풀어간다.
연산자 — 자주 틀리는 세 가지
단락 평가(short-circuit evaluation) — && vs &
논리 연산자 &&와 ||는 단락 평가를 수행한다. (JLS §15.23, §15.24) 좌변만으로 결과가 확정되면 우변을 아예 실행하지 않는다.
쉽게 말하면: "왼쪽만 봐도 답이 나오면 오른쪽은 안 본다"는 것이다.
null 체크를 생각해 보자. s != null이 false면 — s가 null이라는 뜻이니, s.length()를 부르면 NullPointerException이 난다. 하지만 &&는 왼쪽이 false면 오른쪽을 실행 자체를 안 한다. 그래서 null 검사와 길이 검사를 한 줄에 안전하게 쓸 수 있다.
// Java 25
String s = null;
// 단락 평가: s == null이면 우변 평가 안 함 → NullPointerException 발생 안 함
if (s != null && s.length() > 0) {
System.out.println("유효한 문자열");
} else {
System.out.println("null이거나 빈 문자열");
}
null이거나 빈 문자열
반면 &와 |는 비트 연산자다 — 논리형에 쓰면 양쪽을 모두 평가한다(단락 없음).
// Java 25 — 위험: &는 단락하지 않음
if (s != null & s.length() > 0) { // s == null이어도 s.length() 실행 → NPE!
}
&&를 쓸 의도로&를 쓰는 것이 논리 연산 관련 가장 흔한 버그다. IDE에서 경고하지만, 코드 리뷰에서도 자주 놓친다.
문자열 결합 연산자 + — 편리하지만 느린 이유
Java에서 +는 숫자 덧셈과 문자열 결합을 모두 수행한다. (JLS §15.18.1)
// Java 25
String name = "Java";
int version = 21;
String result = "Hello, " + name + " " + version; // "Hello, Java 25"
이 코드는 실제로 StringBuilder를 생성하여 append를 반복 호출한 뒤 toString으로 변환한다 — 루프 안에서 쓰면 매 반복마다 새 StringBuilder 객체와 임시 문자열을 만든다.
// Java 25 — 루프 안의 + 연산 (성능 문제)
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // 매번 새 StringBuilder + 새 String 생성
}
// 올바른 방식
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result2 = sb.toString();
단일 표현식에서의
+연산은 Java 9+부터 컴파일러가invokedynamic으로 최적화한다. (JEP-280) 따라서String s = a + b + c;는 효율적이다. 문제는 루프 안에서의+=다 — 이 경우 매번 새 객체를 생성한다.
삼항 연산자 — ? :의 타입 규칙
// Java 25
int x = 5;
String result = x > 0 ? "양수" : "음수 또는 0";
삼항 연산자의 두 피연산자 타입이 다를 때, Java는 결과 타입을 통합한다. (JLS §15.25) 이 규칙이 직관과 다를 때가 있다:
// Java 25 — 삼항 연산자 타입 함정
Object obj = true ? Integer.valueOf(1) : "hello";
// 결과 타입: Integer와 String의 공통 조상인 Object/Serializable/Comparable
// 예상과 다를 수 있음
// 더 위험한 경우
int a = 1;
long b = 2L;
var result = true ? a : b; // result는 int가 아니라 long
switch — 문(statement)에서 식(expression)으로의 진화
전통적 switch 문
Java의 switch는 C 계열 언어의 switch와 같은 구조로 시작했다 — 정수/enum/String 값에 따라 분기하고, break로 빠져나온다. break를 빼먹으면 fall-through(다음 case로 넘어감)가 발생한다.
// Java 25 — 전통적 switch 문 (fall-through 함정)
int day = 3;
String type;
switch (day) {
case 1:
case 2:
case 3:
case 4:
case 5:
type = "평일"; // case 1~5가 모두 여기로 fall-through
break;
case 6:
case 7:
type = "주말";
break;
default:
type = "알 수 없음";
}
break를 빼먹으면 의도치 않게 다음 case가 실행된다. 이것이 전통적 switch의 가장 큰 버그 원인이었다.
switch expression (Java 14+, JEP-361)
Java 14부터 switch가 값을 반환하는 식이 됐다. (JEP-361) 화살표(->) 케이스 라벨을 쓰고, 각 케이스가 값을 반환한다. fall-through가 없다.
// Java 25 — switch expression
int day = 3;
String type = switch (day) {
case 1, 2, 3, 4, 5 -> "평일";
case 6, 7 -> "주말";
default -> "알 수 없음";
};
System.out.println(type); // 평일
핵심 차이:
case 1, 2, 3, 4, 5— 쉼표로 여러 값을 나열 (Java 14+)->— 화살표 케이스.break불필요, fall-through 없음switch가 값을 반환 (세미콜론으로 문장 종료)- 여러 줄 실행이 필요하면
{ }블록 +yield:
// Java 25 — yield로 블록에서 값 반환
String type = switch (day) {
case 1, 2, 3, 4, 5 -> {
String msg = "평일 " + day;
yield msg; // 블록에서 값 반환
}
case 6, 7 -> "주말";
default -> "알 수 없음";
};
yield는 Java 13에서 도입된 키워드다 (yield는 제한된 식별자 — 변수명으로 쓸 수 있으나 switch expression의 yield와 혼동되므로 권장하지 않음).
switch expression의 exhaustiveness
switch expression은 모든 가능한 값을 처리해야 한다 — 누락 시 컴파일 에러. (JLS §15.28)
// Java 25 — 컴파일 에러: default 없고 모든 case를 안 덮음
// enum이나 sealed class가 아닌 int는 default가 필수
String type = switch (day) {
case 1 -> "월";
// 컴파일 에러: switch expression이 모든 값을 처리하지 않음
};
이 제약 덕분에 새 case를 잊는 버그를 컴파일 타임에 잡을 수 있다. enum과 sealed class에서 특히 강력하다.
Pattern matching for switch (Java 21, JEP-441)
Java 21에서 switch는 단순 값 비교를 넘어 타입 패턴(type pattern)으로 분기할 수 있게 됐다. (JEP-441) 이것이 switch를 근본적으로 바꾼다.
// Java 25 — pattern matching for switch
Object obj = "Hello, Java 25";
String result = switch (obj) {
case Integer i -> "정수: " + i;
case String s -> "문자열: " + s;
case Double d -> "실수: " + d;
case null -> "null입니다";
default -> "기타: " + obj;
};
System.out.println(result); // 문자열: Hello, Java 25
전통적 instanceof 체인과의 비교
// Java 25 — 전통적 방식 (장황함)
String describe(Object obj) {
if (obj instanceof Integer) {
Integer i = (Integer) obj; // 명시적 캐스트
return "정수: " + i;
} else if (obj instanceof String) {
String s = (String) obj; // 명시적 캐스트
return "문자열: " + s;
} else if (obj == null) {
return "null입니다";
} else {
return "기타: " + obj;
}
}
// Java 25 — switch pattern matching (간결)
String describe(Object obj) {
return switch (obj) {
case Integer i -> "정수: " + i;
case String s -> "문자열: " + s;
case null -> "null입니다";
default -> "기타: " + obj;
};
}
null 케이스 — 직접 처리 가능
전통적 switch에서 selector가 null이면 NullPointerException이 발생했다. pattern matching switch에서는 case null으로 직접 처리할 수 있다.
// Java 25 — null 케이스 직접 처리
String result = switch (obj) {
case String s when s.isEmpty() -> "빈 문자열"; // guard (when절)
case String s -> "문자열: " + s;
case Integer i when i > 100 -> "큰 정수: " + i; // guard
case Integer i -> "정수: " + i;
case null -> "null입니다"; // null 직접 처리
default -> "기타";
};
when 절(guard) — 패턴에 조건 추가
when 키워드로 case 패턴에 추가 조건을 건다. (JEP-441)
// Java 25 — when 절로 정제
case String s when s.length() > 10 -> "긴 문자열";
case String s -> "짧은 문자열";
when 절이 없으면 모든 String이 같은 case로 처리된다. when이 있으면 위에서부터 순서대로 평가한다.
sealed class와 switch — 완전성 보장
sealed class와 pattern matching switch의 조합이 가장 강력하다:
// Java 25 — sealed class + switch
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
// default 불필요! sealed class의 모든 허용 타입을 처리했으므로
};
}
sealed class는 허용된 하위 타입을 컴파일 타임에 알 수 있으므로, default 없이도 exhaustiveness가 보장된다. 새 하위 타입을 추가하면 switch가 컴파일 에러를 발생시킨다 — 누락된 case를 즉시 발견할 수 있다.
반복문 — enhanced for와 labeled break
Java의 반복문은 for, enhanced for(for-each), while, do-while이다. 핵심은 enhanced for와 중첩 루프에서의 labeled break다.
enhanced for (for-each)
// Java 25
int[] nums = { 1, 2, 3, 4, 5 };
for (int n : nums) {
System.out.print(n + " ");
}
1 2 3 4 5
enhanced for는 배열과 Iterable에 작동한다. 인덱스가 필요 없을 때 권장된다. 인덱스가 필요하면 전통적 for를 쓴다.
labeled break / continue — 중첩 루프 탈출
// Java 25 — labeled break로 중첩 루프 탈출
int[][] matrix = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
int target = 5;
boolean found = false;
search:
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] == target) {
System.out.println("찾음: (" + i + ", " + j + ")");
found = true;
break search; // 외부 루프까지 한 번에 탈출
}
}
}
labeled break는 Java에서 유일하게 레이블(
label:)을 쓰는 곳이다.goto가 없는 Java에서 다중 루프 탈출을 위한 안전한 대안이다. 다만 남용하면 흐름을 따라가기 어려워지므로, 메서드로 추출하여return으로 대체하는 것이 더 깔끔할 수 있다.
실습 — switch expression과 pattern matching 체감하기
사전: Java 25 설치 확인.
// Java 25 — SwitchDemo.java
public class SwitchDemo {
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
public static void main(String[] args) {
Object[] items = { 42, "Hello", 3.14, null, new Circle(5.0) };
for (Object item : items) {
String desc = switch (item) {
case Integer i when i > 100 -> "큰 정수: " + i;
case Integer i -> "정수: " + i;
case String s when s.isBlank() -> "빈 문자열";
case String s -> "문자열: " + s;
case Double d -> "실수: " + d;
case Circle c -> "원(반지름=" + c.radius() + ")";
case Rectangle r -> "사각형(" + r.width() + "x" + r.height() + ")";
case null -> "null";
default -> "알 수 없음: " + item;
};
System.out.println(desc);
}
}
}
java SwitchDemo.java
정수: 42
문자열: Hello
실수: 3.14
null
원(반지름=5.0)
확인할 것: 하나의 switch expression이 타입 패턴(Integer, String, Double, Circle), guard(when), null 케이스, default를 모두 처리한다. instanceof 체인이나 방대한 if-else 없이 타입 안전한 분기가 이루어진다.
요약 — 이 글의 결론
&&와&는 다르다. 논리 연산에는 단락 평가(&&)를, 비트 연산에만&를 쓴다. 혼동하면 NPE가 발생한다.- 루프 안의
+=문자열 결합은 매 반복마다 새 객체를 만든다. 루프에서는StringBuilder를 쓴다. - switch는 식(expression)이 됐다. Java 14+에서 값을 반환하고, fall-through가 없으며, 모든 값을 처리해야 한다(exhaustiveness). 이 제약이 누락된 case를 컴파일 타임에 잡는다.
- pattern matching for switch(Java 25)는 타입 안전한 분기를 가능하게 한다.
instanceof체인과 명시적 캐스트가 사라진다.when절로 조건을 추가하고,case null로 NPE를 원천 차단한다. - sealed class + switch가 최강의 조합이다. 모든 하위 타입을 컴파일 타임에 알 수 있으므로,
default없이도 완전성이 보장된다. 새 타입 추가 시 switch가 컴파일 에러로 알려준다.
생각해 볼 문제
boolean result = obj instanceof String s && s.length() > 0;에서s의 스코프는 어디까지인가? (flow typing을 조사해 보자.)- switch expression에서
case Integer i와case int i의 차이는 무엇인가? 둘 다 컴파일되는가? - 삼항 연산자
char c = true ? 'a' : 65;의 결과 타입은 무엇인가? (JLS §15.25의 타입 규칙을 읽어보자.) - enhanced for 루프 내에서
ConcurrentModificationException이 발생하는 조건은 무엇인가? 이를 방지하려면 어떤 컬렉션을 써야 하는가? var x = switch (day) { case MONDAY -> 1; default -> "Monday"; };의 결과 타입은 무엇으로 추론되는가? 컴파일이 될까?
참고
- JLS §15 - Expressions - 접근 2026-07-10
- JLS §15.28 - Switch Expressions - 접근 2026-07-10
- JEP-361: Switch Expressions - 접근 2026-07-10
- JEP-441: Pattern Matching for switch - 접근 2026-07-10
- JEP-280: Indify String Concatenation - 접근 2026-07-10
'Develop Artifacts > Java' 카테고리의 다른 글
| Java - 06. inheritance and polymorphism (0) | 2026.07.12 |
|---|---|
| Java - 05. class and object (0) | 2026.07.12 |
| Java - 04. array and string (0) | 2026.07.12 |
| Java - 02. types (0) | 2026.07.10 |
| Java - 01. intro (0) | 2026.07.10 |
