JAVA

Java 연산자와 제어문 핵심 정리 — if/switch, 반복문, 비교 연산 실수 방지

IT Lab 2026. 2. 13. 20:00

Java 17 기준으로 if/switch, for/while 사용법을 실무 관점에서 정리하고, ==/equals, 부동소수점 비교 등 자주 터지는 비교 연산 실수를 한 번에 예방합니다.

 

분기 로직이 늘어나면 “왜 이 케이스만 타지?” 같은 디버깅이 자주 생깁니다. 특히 ==equals()를 헷갈리거나, switch의 fall-through를 놓치면 버그가 조용히 숨어요. 이번 글에서는 Java에서 가장 자주 쓰는 연산자/제어문을 “실수 방지” 관점으로 정리해 봅니다.

핵심 개념: Java if/switch/반복문을 ‘안전하게’ 쓰는 기준

if와 switch 선택 기준 및 분기 흐름을 한눈에 보여주는 다이어그램

 

제어문 자체는 단순하지만, 중요한 건 “코드가 커졌을 때도 의도가 유지되느냐”입니다. 같은 조건 분기라도 if가 더 읽기 쉬운 경우가 있고, 반대로 switch가 더 안전한 경우도 있어요.

if vs switch: 선택 기준이 명확하면 코드가 덜 망가집니다

  • if범위 조건, 복합 조건(AND/OR), **가드 조건(early return)**에 강합니다.
  • switch는 **값 기반 분기(열거형/정수/문자열)**에서 강하고, 케이스가 많아질수록 if-else보다 구조가 안정적입니다.
  • Java 17부터 switchswitch expression(값을 반환)으로도 쓸 수 있어, “분기 후 대입” 패턴이 깔끔해집니다.

switch의 함정: fall-through와 null

  • 전통적인 switch 문(statement)에서는 break를 빼먹으면 다음 케이스로 fall-through 됩니다.
  • switchnull을 허용하지 않습니다. switch (str)에서 str == null이면 NullPointerException이 납니다. (if로 먼저 가드하는 습관이 안전합니다.)
  • Java 14+의 case L -> 형태(arrow label)는 fall-through가 기본적으로 막혀 실수를 줄여줍니다.

(미리보기) switch 패턴 매칭은 “타입 분기”의 가독성을 크게 올립니다

Java 17에서는 instanceof 패턴 매칭이 먼저 들어왔고, switch 패턴 매칭은 Java 21에서 표준화되었습니다.
즉, Java 17 LTS 기준 실무에서는 instanceof (obj instanceof String s)를 주로 쓰고, “나중에 switch로 더 깔끔해진다” 정도로만 알아두면 충분합니다.

for/while: 반복의 목적이 드러나야 유지보수됩니다

  • 반복 횟수가 명확하면 for
  • 종료 조건이 이벤트/상태 기반이면 while
  • 컬렉션 순회는 가능하면 향상된 for-each(또는 Stream)로 의도를 드러내세요.
  • 무한 루프가 필요하면 while (true)를 쓰되, 탈출 조건을 루프 상단에 배치하면 디버깅이 쉬워집니다.

실수하기 쉬운 비교 연산: “같다”는 한 가지가 아닙니다

Java에서 비교는 크게 3가지로 나뉩니다.

비교 대상 올바른 비교 흔한 실수 이유/결과
기본형(primitive) ==, !=, <, > 거의 없음 값 자체를 비교
참조형 객체(Object) equals() (또는 Objects.equals) ==로 비교 ==는 “같은 객체(주소)” 비교
부동소수점(float/double) 오차 허용 비교(epsilon) ==로 비교 0.1 같은 값이 이진 표현에서 정확히 떨어지지 않음

특히 String 비교에서 ==는 “운 좋으면 맞는 것처럼 보이는” 케이스가 있어 더 위험합니다(문자열 풀 때문에). 실무 버그는 대부분 “가끔만 재현되는 비교”에서 나오더라고요.

flowchart TD
  A[비교하려는 값] --> B{"primitive 인가요?"}
  B -- yes --> C[==, <, > 사용]
  B -- no --> D{"부동소수점(double/float) 인가요?"}
  D -- yes --> E[epsilon 비교 사용]
  D -- no --> F[Objects.equals / equals 사용]

비교 대상에 따라 “같음”의 기준이 달라지므로, 먼저 타입을 기준으로 선택하는 게 안전합니다.

코드 예제: if/switch/반복문 + 비교 실수 방지 한 번에 보기 (Java 17)

아래 코드는 그대로 복붙해 실행할 수 있고, 실무에서 자주 쓰는 패턴(가드 조건, switch expression, 안전한 equals, 부동소수점 비교)을 한 파일에 모았습니다.

import java.util.List;
import java.util.Objects;

public class OperatorsAndControlFlowDemo {

    enum Grade { BRONZE, SILVER, GOLD }

    public static void main(String[] args) {
        // 1) if: 가드 조건(early return) 스타일
        System.out.println("discountRate for age=17: " + discountRate(17));
        System.out.println("discountRate for age=70: " + discountRate(70));

        // 2) switch expression: 분기 결과를 값으로 받기
        System.out.println("shippingFee for grade=GOLD: " + shippingFee(Grade.GOLD));

        // 3) switch null 주의: 먼저 가드
        System.out.println("parseCommand(null): " + parseCommand(null));
        System.out.println("parseCommand(\"START\"): " + parseCommand("START"));

        // 4) 반복문: for(횟수) / while(상태)
        sumWithFor(List.of(1, 2, 3, 4, 5));
        pollWithWhile();

        // 5) 비교 연산 실수 방지
        compareStringSafely();
        compareDoubleSafely();
        compareIntegerPitfall();
    }

    // if: 범위/복합 조건에 강함
    static int discountRate(int age) {
        if (age < 0) return 0;           // 가드 조건으로 비정상 입력 차단
        if (age <= 19) return 10;
        if (age >= 65) return 15;
        return 0;
    }

    // switch expression (Java 14+): fall-through 방지 + 결과를 값으로 반환
    static int shippingFee(Grade grade) {
        // grade가 null이면 NPE -> 호출부에서 null을 허용한다면 먼저 체크하는 게 안전
        return switch (grade) {
            case BRONZE -> 3000;
            case SILVER -> 2000;
            case GOLD -> 0;
        };
    }

    // switch는 null을 받을 수 없으니, if로 먼저 가드
    static String parseCommand(String cmd) {
        if (cmd == null || cmd.isBlank()) return "NOOP";

        // 문자열 switch 가능 (Java 7+)
        return switch (cmd) {
            case "START" -> "BEGIN";
            case "STOP" -> "END";
            default -> "UNKNOWN";
        };
    }

    // for-each: 컬렉션 순회 의도를 명확히
    static void sumWithFor(List<Integer> numbers) {
        int sum = 0;
        for (int n : numbers) {
            sum += n;
        }
        System.out.println("sumWithFor: " + sum);
    }

    // while: 상태 기반 반복(예: 폴링/재시도)
    static void pollWithWhile() {
        int attempts = 0;

        while (true) { // 무한 루프는 탈출 조건을 눈에 잘 보이게 두는 게 중요
            attempts++;
            boolean done = attempts >= 3; // 예시: 3번 시도 후 종료

            if (done) {
                System.out.println("pollWithWhile done after attempts=" + attempts);
                break;
            }
        }
    }

    static void compareStringSafely() {
        String a = new String("hello");
        String b = "hello";

        // (X) a == b : 참조 비교라서 false일 수 있음
        // (O) 내용 비교는 equals, null 안전이 필요하면 Objects.equals
        System.out.println("String equals: " + Objects.equals(a, b));
    }

    static void compareDoubleSafely() {
        double x = 0.1 + 0.2;
        double y = 0.3;

        // (X) 부동소수점은 == 비교가 흔히 실패
        System.out.println("double == : " + (x == y));

        // (O) 오차 허용 비교
        double eps = 1e-9;
        System.out.println("double epsilon equals: " + (Math.abs(x - y) < eps));
    }

    static void compareIntegerPitfall() {
        Integer i1 = 127;
        Integer i2 = 127;
        Integer i3 = 128;
        Integer i4 = 128;

        // Integer는 -128~127 범위 캐시로 인해 "가끔" ==가 true가 되어 더 헷갈립니다.
        System.out.println("127 == 127 (Integer): " + (i1 == i2)); // true일 가능성 큼
        System.out.println("128 == 128 (Integer): " + (i3 == i4)); // false일 가능성 큼

        // (O) 래퍼 타입 비교는 equals
        System.out.println("128 equals 128 (Integer): " + i3.equals(i4));
    }
}

 

실무 팁

💡 실무에서는: switch는 “값 분기 + 결과 반환”으로 쓰면 버그가 줄어듭니다

  • switch expression으로 분기 결과를 바로 return/대입하면, 케이스 누락/초기화 누락이 줄어듭니다.
  • 전통 switch 문을 써야 한다면, fall-through가 의도된 경우에만 주석으로 명시해 두세요(리뷰에서 가장 자주 놓칩니다).

💡 실무에서는: 비교는 Objects.equals, 숫자는 “타입에 맞는 전략”을 정해두세요

  • String/DTO 비교를 ==로 하지 않도록 팀 규칙(정적 분석, 코드리뷰 체크리스트)을 두면 효과가 큽니다.
  • 금액/정밀 계산은 double 대신 BigDecimal을 쓰는 쪽이 안전합니다(특히 결제/정산). 부동소수점이 필요한 영역(통계, 그래픽)이라면 epsilon 비교 기준을 상수로 통일해 두세요.

핵심 요약: if는 범위/복합 조건, switch는 값 기반 분기에 강하고 Java 17의 switch expression이 특히 안전합니다.
핵심 요약: 객체 비교는 equals/Objects.equals, 부동소수점은 epsilon 비교를 기본으로 두세요.
핵심 요약: 반복문은 “횟수 vs 상태” 기준으로 고르면 코드 의도가 선명해집니다.

다음 글: #05 클래스와 객체 — 왜 나눠야 할까?