본문 바로가기

증감·비교·논리·대입 연산자

값을 바꾸는 연산과 값을 만드는 연산을 구분하고, 비교·논리 연산자와 단축 평가로 회원가입 조건을 안전하게 계산하는 방법을 배웁니다.

산술 연산과 문자열 결합에서 쓴 +나 /는 값을 계산해 내놓을 뿐, 피연산자로 쓴 변수는 건드리지 않습니다. 이번 글의 연산자 중에는 그렇지 않은 것이 섞여 있습니다. count++는 값을 하나 내놓으면서 count에 저장된 값도 바꾸고, active = true는 변수에 값을 저장하면서 그 값을 식의 결과로도 내놓습니다. 반면 age >= 30이나 active == true는 변수를 읽기만 하고 boolean 값 하나를 만듭니다.

그래서 이런 연산자가 들어간 식을 읽을 때는 두 가지를 따로 따라가야 합니다. 하나는 식이 계산해 내놓는 값, 곧 식의 결과이고, 다른 하나는 식을 실행한 뒤 변수에 남은 값입니다. 식이 결과를 만드는 일 말고 변수 같은 상태를 바꾸는 것을 부수 효과(side effect)라고 부릅니다. 아래 그림은 후위 증가, 대입, 비교를 실행 전 상태, 식의 결과, 실행 뒤 상태로 나누어 보여 줍니다.

연산식의 결과와 저장 상태를 따로 추적한다

후위증가·대입·비교의 이전 상태/식 결과/이후 상태를 명시합니다. 세 독립 연산을 같은 단계에서 비교하면 값 생성과 상태 변경 차이가 선명합니다.

Java 연산식의 결과와 저장 상태 전이 비교

STATE MACHINE · EXPRESSION CONTRACT

연산식의 결과와 저장 상태를 따로 추적한다

같은 식 안에서도 계산된 값과 변수에 남은 상태는 다를 수 있다. 이전 상태, 식 결과, 이후 상태, 저장 상태의 변경 여부를 나눠 읽는다.

연산식의 결과와 저장 상태를 따로 추적한다 memberCount 후위 증가, boolean 대입, boolean 비교를 이전 상태와 식 결과와 이후 상태로 나누어 저장 상태의 변경 여부를 비교한다. 후위 증가 memberCount++ 이전 값을 결과로 사용 memberCount = 0 이전 상태 식 결과 = 0 memberCount = 1 이후 · 값 변경 대입 active = true 값 저장과 결과 생성 active = false 이전 상태 식 결과 = true active = true 이후 · 값 변경 비교 active == true 상태를 읽어 비교 active = false 이전 상태 비교 결과 = false active = false 이후 · 그대로 이전 상태 → 식의 결과 → 이후 상태 · 비교는 저장 상태를 바꾸지 않습니다
연산식의 결과와 저장 상태를 따로 추적한다 후위증가0→1/결과0,대입false→true/결과true,비교false유지/결과false. 본문과 바로 아래 설명이 원래 코드·수치를 보완합니다. memberCount++ 식 결과 0 이전 저장 상태 0 이후 저장 상태 1 변경 active = true 식 결과 true 이전 저장 상태 false 이후 저장 상태 true 변경 active == true 식 결과 false 이전 저장 상태 false 이후 저장 상태 false 유지
memberCount++
이전 상태 0 · 식 결과 0 · 이후 상태 1 · 상태 변경
active = true
이전 상태 false · 식 결과 true · 이후 상태 true · 상태 변경
active == true
이전 상태 false · 비교 결과 false · 이후 상태 false · 상태 유지

후위 증가의 식 결과가 이전 값이라는 사실과, 식 평가 뒤 저장 상태가 증가했다는 사실을 한 문장으로 섞지 않는다.

이 예의 조건

  • 세 연산은 각각 제시된 초기 상태에서 실행합니다.
관계도의 보충 설명
원 도표의 설명과 주석
구분내용
전체 읽기 정보연산식의 결과와 저장 상태를 따로 추적한다 Java 연산식의 결과와 저장 상태 전이 비교 STATE MACHINE · EXPRESSION CONTRACT 연산식의 결과와 저장 상태를 따로 추적한다 같은 식 안에서도 계산된 값과 변수에 남은 상태는 다를 수 있다. 이전 상태, 식 결과, 이후 상태, 저장 상태의 변경 여부를 나눠 읽는다. memberCount++ 이전 상태 0 · 식 결과 0 · 이후 상태 1 · 상태 변경 active = true 이전 상태 false · 식 결과 true · 이후 상태 true · 상태 변경 active == true 이전 상태 false · 비교 결과 false · 이후 상태 false · 상태 유지 후위 증가의 식 결과가 이전 값이라는 사실과, 식 평가 뒤 저장 상태가 증가했다는 사실을 한 문장으로 섞지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
이전 상태

증가·대입·비교의 저장 상태를 먼저 둡니다.

  • 증가: memberCount=0
  • 대입·비교: active=false
  • 초기 상태를 식의 결과와 구별해 읽습니다.
식 결과

후위 증가는 이전 값, 대입은 새 값, 비교는 boolean을 만듭니다.

  • memberCount++: 결과 0
  • 대입 / 비교: true / false
  • 같은 형태의 식도 결과 생성 규칙이 다릅니다.
이후 상태

저장된 상태의 변경 여부를 확인합니다.

  • 증가: memberCount=1
  • 대입 / 비교: active=true / false 유지
  • 비교는 값을 읽기만 하므로 상태를 바꾸지 않습니다.

연산식의 결과와 저장 상태를 따로 추적한다

후위증가·대입·비교의 이전 상태/식 결과/이후 상태를 명시합니다. 세 독립 연산을 같은 단계에서 비교하면 값 생성과 상태 변경 차이가 선명합니다.

Java 연산식의 결과와 저장 상태 전이 비교

STATE MACHINE · EXPRESSION CONTRACT

연산식의 결과와 저장 상태를 따로 추적한다

같은 식 안에서도 계산된 값과 변수에 남은 상태는 다를 수 있다. 이전 상태, 식 결과, 이후 상태, 저장 상태의 변경 여부를 나눠 읽는다.

연산식의 결과와 저장 상태를 따로 추적한다 memberCount 후위 증가, boolean 대입, boolean 비교를 이전 상태와 식 결과와 이후 상태로 나누어 저장 상태의 변경 여부를 비교한다. 후위 증가 memberCount++ 이전 값을 결과로 사용 memberCount = 0 이전 상태 식 결과 = 0 memberCount = 1 이후 · 값 변경 대입 active = true 값 저장과 결과 생성 active = false 이전 상태 식 결과 = true active = true 이후 · 값 변경 비교 active == true 상태를 읽어 비교 active = false 이전 상태 비교 결과 = false active = false 이후 · 그대로 이전 상태 → 식의 결과 → 이후 상태 · 비교는 저장 상태를 바꾸지 않습니다
연산식의 결과와 저장 상태를 따로 추적한다 후위증가0→1/결과0,대입false→true/결과true,비교false유지/결과false. 본문과 바로 아래 설명이 원래 코드·수치를 보완합니다. memberCount++ 식 결과 0 이전 저장 상태 0 이후 저장 상태 1 변경 active = true 식 결과 true 이전 저장 상태 false 이후 저장 상태 true 변경 active == true 식 결과 false 이전 저장 상태 false 이후 저장 상태 false 유지
memberCount++
이전 상태 0 · 식 결과 0 · 이후 상태 1 · 상태 변경
active = true
이전 상태 false · 식 결과 true · 이후 상태 true · 상태 변경
active == true
이전 상태 false · 비교 결과 false · 이후 상태 false · 상태 유지

후위 증가의 식 결과가 이전 값이라는 사실과, 식 평가 뒤 저장 상태가 증가했다는 사실을 한 문장으로 섞지 않는다.

이 예의 조건

  • 세 연산은 각각 제시된 초기 상태에서 실행합니다.

관계도의 보충 설명

원 도표의 설명과 주석
구분내용
전체 읽기 정보연산식의 결과와 저장 상태를 따로 추적한다 Java 연산식의 결과와 저장 상태 전이 비교 STATE MACHINE · EXPRESSION CONTRACT 연산식의 결과와 저장 상태를 따로 추적한다 같은 식 안에서도 계산된 값과 변수에 남은 상태는 다를 수 있다. 이전 상태, 식 결과, 이후 상태, 저장 상태의 변경 여부를 나눠 읽는다. memberCount++ 이전 상태 0 · 식 결과 0 · 이후 상태 1 · 상태 변경 active = true 이전 상태 false · 식 결과 true · 이후 상태 true · 상태 변경 active == true 이전 상태 false · 비교 결과 false · 이후 상태 false · 상태 유지 후위 증가의 식 결과가 이전 값이라는 사실과, 식 평가 뒤 저장 상태가 증가했다는 사실을 한 문장으로 섞지 않는다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
이전 상태

증가·대입·비교의 저장 상태를 먼저 둡니다.

  • 증가: memberCount=0
  • 대입·비교: active=false
  • 초기 상태를 식의 결과와 구별해 읽습니다.
식 결과

후위 증가는 이전 값, 대입은 새 값, 비교는 boolean을 만듭니다.

  • memberCount++: 결과 0
  • 대입 / 비교: true / false
  • 같은 형태의 식도 결과 생성 규칙이 다릅니다.
이후 상태

저장된 상태의 변경 여부를 확인합니다.

  • 증가: memberCount=1
  • 대입 / 비교: active=true / false 유지
  • 비교는 값을 읽기만 하므로 상태를 바꾸지 않습니다.

세 식은 서로 이어지는 코드가 아니라, 각각 memberCount가 0이고 active가 false인 상태에서 따로 실행한다고 가정합니다. 결과와 남은 상태의 짝이 세 식에서 모두 다르다는 점에 주목하세요. 후위 증가 memberCount++는 결과로 이전 값 0을 내놓지만 변수는 1이 됩니다. 대입 active = true는 결과도 true이고 변수도 true로 바뀝니다. 비교 active == true는 결과로 false를 만들 뿐 변수는 그대로 false입니다. 이 글은 이 차이를 하나씩 풀어 가고, 마지막에는 비교·논리 연산자로 회원가입 조건을 계산합니다.

증가·감소 연산자는 언제 값을 내놓을까

++는 변수에 1을 더하고 --는 1을 뺍니다. 그런데 이 연산자는 값을 바꾸는 동시에 식의 결과도 내놓기 때문에, 그 결과를 다른 곳에 쓰면 바꾸기 전 값과 바꾼 뒤 값 중 무엇이 쓰이는지가 문제가 됩니다. 다음 코드는 회원 수를 1 늘리면서 그 식의 결과를 printedCount에 받습니다. 실행하기 전에 두 줄의 출력을 먼저 예상해 보세요.

lab/PostIncrementTrap.java
public final class PostIncrementTrap {
    public static void main(String[] args) {
        int memberCount = 0;
        int printedCount = memberCount++;

        System.out.println("printed=" + printedCount);
        System.out.println("stored=" + memberCount);
    }
}
printed=0
stored=1

int printedCount = memberCount++; 한 줄은 다음 순서로 실행됩니다.

순서하는 일memberCount식의 결과
1memberCount의 현재 값 0을 읽어 둔다00
2읽은 값에 1을 더해 memberCount에 저장한다10
3식의 결과 0을 printedCount에 대입한다10

이렇게 변수 뒤에 붙는 후위(postfix) 증가는 식의 결과로 증가하기 전의 값을 내놓습니다. 변수는 이미 1이 되었지만 printedCount가 받는 것은 1단계에서 읽어 둔 0입니다. 그래서 첫 줄은 printed=0, 둘째 줄은 stored=1입니다.

변수 앞에 붙는 전위(prefix) 증가 ++memberCount는 순서가 반대입니다. 먼저 1을 더해 저장하고, 저장한 새 값을 식의 결과로 내놓습니다. 위 코드의 memberCount++를 ++memberCount로 바꾸면 printed=1과 stored=1이 출력됩니다. --도 마찬가지여서 후위 count--는 줄이기 전 값을, 전위 --count는 줄인 뒤 값을 결과로 내놓습니다.

증가 식을 다른 식에 섞지 않고 단독 문장으로 쓰면 결과를 받는 곳이 없으므로 전위와 후위의 차이가 사라집니다. 아래 두 줄은 첫 줄을 ++memberCount;로 바꿔도 똑같이 동작하고, 늘어난 값을 출력합니다.

memberCount++;
System.out.println(memberCount);

증가를 대입이나 메서드 인자 전달과 한 식에 섞으면 읽는 사람이 매번 전위인지 후위인지 따져야 합니다. 그래서 이 교재의 예제는 상태를 바꾸는 증가를 이처럼 별도 문장으로 둡니다.

후위 증가의 실행 순서를 알면 다음 코드가 왜 1이 아니라 0을 출력하는지도 설명할 수 있습니다.

lab/SelfPostIncrement.java
public final class SelfPostIncrement {
    public static void main(String[] args) {
        int count = 0;
        count = count++;
        System.out.println("count=" + count);
    }
}
count=0

오른쪽 count++는 결과로 0을 내놓으면서 count를 1로 바꿉니다. 그다음 대입이 실행되어 식의 결과 0을 count에 다시 저장하므로, 1로 늘었던 값이 0으로 덮어써집니다. 값을 1 늘리려면 count++;와 count = count + 1; 중 하나만 씁니다.

증감 연산자는 값이 허용 범위 안에 있는지 검사하지 않는다는 점도 알아 두세요. 남은 가입 정원처럼 0 아래로 내려가면 안 되는 값이라도 --는 0을 그대로 -1로 만듭니다. 그런 규칙은 다음 글에서 배울 if로 직접 확인해야 합니다.

=와 ==를 바꿔 쓰면 생기는 일

=는 오른쪽 값을 왼쪽 변수에 저장하는 대입 연산자이고, ==는 두 값이 같은지 비교하는 연산자입니다. 기호가 비슷해서 비교하려던 자리에 =를 하나만 쓰는 실수가 흔합니다. 다음 코드는 active가 참인지 확인하려다 true를 대입해 버린 경우입니다.

lab/AssignmentInsteadOfComparison.java
public final class AssignmentInsteadOfComparison {
    public static void main(String[] args) {
        boolean active = false;

        System.out.println("expression=" + (active = true));
        System.out.println("stored=" + active);
    }
}
expression=true
stored=true

대입도 결과를 내놓는 식입니다. 대입식의 결과는 방금 저장한 값이므로 (active = true)는 true가 되고, 그 과정에서 active도 true로 바뀝니다. 그래서 첫 줄만 보면 비교가 성공한 것 같지만, 실제로는 원래 값이 무엇이었든 true로 덮어쓴 것입니다. 이 식이 if (active = true)처럼 조건 자리에 있었다면 조건은 언제나 참이 되고, 비활성 회원도 활성 회원처럼 처리됩니다.

다행히 Java에서 이 실수가 컴파일까지 통과하는 경우는 좁습니다. if의 조건은 반드시 boolean이어야 하고, Java는 정수를 boolean으로 바꿔 주지 않기 때문입니다. 그래서 int 변수로 같은 실수를 하면 컴파일러가 막아 줍니다.

int count = 0;
if (count = 1) {
    System.out.println("one");
}

이 코드를 컴파일하면 대입식의 결과인 int 값을 조건에 쓸 수 없다는 오류가 납니다. 메시지의 핵심 줄만 옮기면 다음과 같습니다.

error: incompatible types: int cannot be converted to boolean

결국 조용히 통과하는 대표적인 경우는 boolean 변수에 대입할 때인데, 여기에도 실수할 여지를 없애는 방법이 있습니다. boolean은 그 자체가 참·거짓 값이므로 if (active == true) 대신 if (active), if (active == false) 대신 if (!active)라고 쓰면 됩니다. 비교 연산자를 아예 쓰지 않으니 =를 잘못 칠 일이 없고 코드도 짧아집니다.

문자열은 사정이 다릅니다. 두 문자열의 내용이 같은지 확인할 때는 ==가 아니라 equals 메서드를 씁니다. ==가 문자열에서 무엇을 비교하는지는 참조를 배우는 기본형과 참조형 복사에서 설명합니다.

조건을 boolean 값으로 만들기

회원가입을 판정하려면 '나이가 양수인가', '120세 이하인가'처럼 참이나 거짓으로 답하는 값이 필요합니다. 비교 연산자는 두 값을 비교해 이런 boolean 값을 만듭니다. 아래 표의 결과는 age가 45일 때의 값입니다.

연산자뜻예결과
<작다age < 18false
<=작거나 같다age <= 120true
>크다age > 0true
>=크거나 같다age >= 18true
==같다age == 45true
!=다르다age != 45false

조건 여러 개를 묶을 때는 논리 연산자를 씁니다. &&(AND)는 양쪽이 모두 참일 때만 참이고, ||(OR)는 한쪽이라도 참이면 참입니다. !(NOT)는 참과 거짓을 뒤집습니다. 가능한 경우를 모두 적으면 다음과 같습니다.

aba && ba || b!a
truetruetruetruefalse
truefalsefalsetruefalse
falsetruefalsetruetrue
falsefalsefalsefalsetrue

다음 프로그램은 회원 한 명에 대한 조건을 하나씩 boolean 변수에 담아 출력합니다.

src/MemberConditions.java
public final class MemberConditions {
    public static void main(String[] args) {
        int age = 45;
        int minimumAge = 18;
        boolean emailPresent = true;

        boolean positive = age > 0;
        boolean withinLimit = age <= 120;
        boolean valid = emailPresent && positive && withinLimit;
        boolean active = age >= minimumAge;
        boolean needsAttention = !valid || !active;

        System.out.println("positive=" + positive);
        System.out.println("withinLimit=" + withinLimit);
        System.out.println("valid=" + valid);
        System.out.println("active=" + active);
        System.out.println("needsAttention=" + needsAttention);
    }
}
positive=true
withinLimit=true
valid=true
active=true
needsAttention=false

positive와 withinLimit는 비교 하나씩의 결과입니다. 45는 0보다 크고 120 이하이므로 둘 다 참입니다. valid는 세 조건을 &&로 이었으므로 셋이 모두 참이어야 참이 되는데, 여기서는 모두 참입니다. active는 45가 최소 가입 나이 18 이상인지 비교한 결과입니다. 마지막 needsAttention은 '유효하지 않거나 활성이 아니면 확인이 필요하다'는 규칙을 !와 ||로 옮긴 것입니다. valid와 active가 모두 참이라 !valid와 !active가 둘 다 거짓이므로 결과는 false입니다.

조건에 이름을 붙여 두면 같은 규칙을 여러 곳에서 다시 쓸 수 있습니다. 출력할 때도, 다음 글에서 if로 분기할 때도 valid라는 이름 하나면 됩니다. 긴 조건은 아래처럼 의미 단위로 나누어 이름을 붙이면 각 줄이 무엇을 확인하는지 한눈에 보입니다.

boolean withinRange = age >= 1 && age <= 120;
boolean acceptable = emailPresent && withinRange;
boolean active = acceptable && age >= minimumAge;

둘째 줄과 셋째 줄은 바로 앞 줄에서 만든 이름을 재료로 씁니다. 그래서 active가 예상과 달리 false로 나오면 withinRange와 acceptable을 차례로 출력해 보는 것만으로 어느 단계에서 조건이 거짓이 됐는지 찾을 수 있습니다. 세 조건을 한 줄에 모두 적었다면 식을 다시 쪼개야 알 수 있는 정보입니다.

범위는 비교 두 개를 &&로 잇는다

age > 0 && age <= 120은 '0보다 크고 120 이하', 즉 1부터 120까지의 정수를 뜻합니다. 양 끝인 1과 120이 범위에 들어가는지는 연산자가 정합니다. 120을 넣으려면 <=를 써야 하고, <를 쓰면 120이 빠집니다. 이렇게 끝값 하나를 놓치는 실수는 경계값으로 조건문 점검하기에서 따로 연습합니다.

수학 시간에 쓰던 대로 0 < age <= 120이라고 한 번에 쓸 수는 없습니다. Java는 이 식을 왼쪽부터 계산해 0 < age의 결과인 boolean을 먼저 만들고, 그 boolean을 120과 비교하려다 실패합니다. 컴파일 오류 메시지도 <=의 왼쪽(first type)이 boolean이고 오른쪽(second type)이 int라서 비교할 수 없다고 알려 줍니다. 메시지의 핵심 줄만 옮기면 다음과 같습니다.

error: bad operand types for binary operator '<='
  first type:  boolean
  second type: int

&&는 ||보다 먼저 묶인다

연산자가 여러 개인 식은 우선순위가 높은 연산자부터 묶입니다. 2장에서 쓰는 연산자를 먼저 묶이는 것부터 나열하면 다음과 같습니다. 표에서 위에 있는 줄일수록 먼저 묶입니다.

순서연산자
1++, --, !, 형변환 (double) 같은 단항 연산자
2*, /, %
3+, -
4<, <=, >, >=
5==, !=
6&&
7||
8? : (삼항 연산자)
9=, +=, -= 같은 대입 연산자

이 순서 덕분에 지금까지 본 식은 괄호 없이도 의도대로 묶였습니다. 형변환은 단항 연산자라서 앞 글의 (double) totalAge / memberCount는 나눗셈보다 형변환이 먼저 일어납니다. 산술이 비교보다 먼저이므로 totalAge / memberCount >= 30은 나눗셈을 끝낸 뒤 30과 비교합니다. 비교가 &&보다 먼저이므로 age >= 1 && age <= 120은 비교 두 개를 계산한 다음 &&로 잇습니다. 대입은 맨 마지막이라서 boolean valid = ...는 오른쪽 전체를 계산한 뒤에 저장됩니다.

자주 틀리는 곳은 &&와 ||가 섞인 식입니다. &&가 먼저 묶이기 때문에, 다음 두 식은 괄호 하나 때문에 결과가 달라집니다.

lab/LogicalPrecedence.java
public final class LogicalPrecedence {
    public static void main(String[] args) {
        boolean admin = true;
        boolean member = false;
        boolean emailPresent = false;

        boolean withoutParentheses = admin || member && emailPresent;
        boolean withParentheses = (admin || member) && emailPresent;

        System.out.println("withoutParentheses=" + withoutParentheses);
        System.out.println("withParentheses=" + withParentheses);
    }
}
withoutParentheses=true
withParentheses=false

괄호가 없는 admin || member && emailPresent는 admin || (member && emailPresent)로 묶입니다. admin이 참이므로 이메일이 없어도 전체가 참입니다. '관리자나 회원이면서 이메일도 있어야 한다'는 뜻이었다면 (admin || member) && emailPresent라고 써야 하고, 이메일이 없으니 결과는 거짓입니다. 우선순위를 외워서 괄호를 줄이기보다, &&와 ||가 섞이면 괄호로 의도를 적어 두는 편이 안전합니다.

단축 평가: 결과가 정해지면 오른쪽은 계산하지 않는다

&&는 왼쪽이 거짓이면 오른쪽이 무엇이든 전체가 거짓입니다. 그래서 Java는 이때 오른쪽 식을 아예 계산하지 않습니다. 마찬가지로 ||는 왼쪽이 참이면 전체가 참이므로 오른쪽을 계산하지 않습니다. 이렇게 결과가 이미 정해졌을 때 나머지를 건너뛰는 동작을 단축 평가(short-circuit evaluation)라고 합니다.

이 성질은 위험한 계산을 막는 데 쓰입니다. 산술 연산과 문자열 결합에서 정수를 0으로 나누면 ArithmeticException이 발생하는 것을 보았는데, 회원이 한 명도 없을 때 평균 나이를 구하려 하면 바로 그 상황이 됩니다. 다음 코드는 회원 수가 0이 아닌지 먼저 확인한 뒤에 평균을 비교합니다.

src/ShortCircuitGuard.java
public final class ShortCircuitGuard {
    public static void main(String[] args) {
        int totalAge = 120;
        int memberCount = 0;

        boolean averageAtLeastThirty = memberCount != 0
            && (totalAge / memberCount) >= 30;

        System.out.println("averageAtLeastThirty=" + averageAtLeastThirty);
    }
}
averageAtLeastThirty=false

왼쪽 memberCount != 0이 거짓이므로 &&는 오른쪽 (totalAge / memberCount) >= 30을 계산하지 않고 전체 결과를 false로 정합니다. 나눗셈이 실행되지 않았으니 예외도 나지 않습니다. 회원이 있을 때만 오른쪽까지 계산하므로, 이 식은 0으로 나누는 일 없이 평균이 30 이상인지 판정합니다.

같은 두 조건이라도 순서를 바꾸면 결과가 달라집니다. 아래 식에서는 나눗셈이 왼쪽에 있어 가장 먼저 계산됩니다.

boolean broken = (totalAge / memberCount) >= 30
    && memberCount != 0;

memberCount가 0이므로 확인 조건에 도달하기도 전에 예외가 발생하고, 실행 결과의 첫 줄에 Exception in thread "main" java.lang.ArithmeticException: / by zero가 출력됩니다. 결과를 만들기는커녕 프로그램이 거기서 멈춥니다.

이처럼 &&의 왼쪽에 '오른쪽을 계산해도 안전한가'를 확인하는 조건을 두는 방식을 가드(guard)라고 부릅니다. 단축 평가를 계산량을 줄이는 최적화 정도로만 생각하면 조건의 순서를 무심코 바꾸기 쉽습니다. 하지만 가드가 있는 식에서는 순서가 곧 안전입니다. 아래 그림에서 왼쪽 가드의 결과에 따라 오른쪽 나눗셈이 실행되는지 따라가 보세요.

단축 평가 가드가 위험한 나눗셈을 막는다

왼쪽 nonzero 가드가 오른쪽 나눗셈의 도달을 막습니다. 검사→평가 또는 건너뜀→결과/예외의 순서를 따라가기 좋습니다.

Java AND 단축 평가와 0 나눗셈 가드 흐름

FLOWCHART · SHORT-CIRCUIT GUARD

단축 평가 가드가 위험한 나눗셈을 막는다

AND의 왼쪽 조건은 전체 결과뿐 아니라 오른쪽 식을 실행할지도 결정한다. 0이 아닌지 먼저 확인해야 정수 나눗셈이 안전하게 열린다.

단축 평가 가드가 위험한 나눗셈을 막는다 memberCount가 0이면 AND 오른쪽 나눗셈을 평가하지 않고 false가 되며, 0이 아닐 때만 평균을 계산하는 안전 흐름과 나눗셈을 먼저 둔 예외 흐름을 비교한다. totalAge = 120 평균 조건 평가 시작 memberCount != 0 ? 왼쪽 조건 · 0 확인 오른쪽 식 실행 안 함 전체 false · 예외 없음 false totalAge / memberCount 오른쪽 평균식 평가 true 비교 결과 반환 평균 ≥ 30의 true / false 위험한 순서 · 나눗셈 먼저 totalAge / memberCount memberCount가 0인 경우 count = 0 ArithmeticException 가드 전에 예외 발생 왼쪽 조건이 false이면 오른쪽 식을 실행하지 않습니다 이 예제는 정수 나눗셈을 안전하게 실행하도록 순서를 정합니다.
단축 평가 가드가 위험한 나눗셈을 막는다 왼쪽count!=0 참일때만오른쪽평균비교;거짓이면미실행false;역순count0예외. 본문과 바로 아래 설명이 원래 코드·수치를 보완합니다. 안전한 순서 위험한 순서 · count=0 참 거짓 totalAge = 120 AND 평가 시작 memberCount != 0 ? 오른쪽 평균 평가 totalAge / memberCount >= 30 비교 결과 true 또는 false 오른쪽 미실행 전체 false · 예외 없음 가드 전에 정수 나눗셈 Arithmetic Exception 발생 가드까지 도달 못함
  1. 왼쪽 가드부터 평가

    memberCount != 0이 오른쪽 나눗셈을 실행해도 되는지 결정합니다.

  2. false이면 중단

    memberCount == 0이면 오른쪽은 실행되지 않음이고 전체 결과는 false입니다.

  3. true일 때만 평균 계산

    totalAge / memberCount >= 30을 평가해 최종 boolean을 만듭니다.

  4. 역순은 안전하지 않음

    나눗셈을 먼저 두고 count가 0이면 가드에 도달하기 전에 ArithmeticException이 발생합니다.

&&의 조건 순서는 결과뿐 아니라 오른쪽 식의 실행 여부를 결정한다.

이 예의 조건

  • totalAge=120인 정수 나눗셈의 안전한 && 식을 추적합니다. 원 main은 memberCount=0이고, count≠0은 같은 식의 조건부 비교입니다.
  • 나눗셈을 앞에 둔 broken 식은 독립 반례이며 위 관계도와 표의 원문 표에 보존합니다. 안전한 식의 뒤 동작으로 연결하지 않습니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
10totalAge
15평균 ≥ 30의 true
18totalAge
원 도표의 설명과 주석
구분내용
전체 읽기 정보단축 평가 가드가 위험한 나눗셈을 막는다 Java AND 단축 평가와 0 나눗셈 가드 흐름 FLOWCHART · SHORT-CIRCUIT GUARD 단축 평가 가드가 위험한 나눗셈을 막는다 AND의 왼쪽 조건은 전체 결과뿐 아니라 오른쪽 식을 실행할지도 결정한다. 0이 아닌지 먼저 확인해야 정수 나눗셈이 안전하게 열린다. 왼쪽 가드부터 평가 memberCount != 0 이 오른쪽 나눗셈을 실행해도 되는지 결정합니다. false이면 중단 memberCount == 0 이면 오른쪽은 실행되지 않으며 전체 결과는 false 입니다. true일 때만 평균 계산 totalAge / memberCount >= 30 을 평가해 최종 boolean을 만듭니다. 역순은 안전하지 않음 나눗셈을 먼저 두고 count가 0이면 가드에 도달하기 전에 ArithmeticException 이 발생합니다. && 의 조건 순서는 결과뿐 아니라 오른쪽 식의 실행 여부를 결정한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
안전한 식의 입력

totalAge=120과 memberCount를 안전한 식에 전달합니다.

  • 원 main: count=0
  • 조건부 비교: count≠0
  • 평가 순서: count 가드 → 평균식
  • 같은 안전한 식의 왼쪽 가드가 나눗셈 실행 여부를 먼저 결정합니다.
왼쪽 가드 판정

memberCount!=0을 먼저 평가하고 오른쪽 도달 여부를 정합니다.

  • count=0: 왼쪽 false · 오른쪽 미평가
  • count≠0: 왼쪽 true · 오른쪽 평가 가능
  • 변수 상태: 읽기만 수행 · 변경 없음
  • count=0의 실행은 나눗셈 전에 false로 끝나고, 0이 아닌 조건만 오른쪽으로 갑니다.
조건별 결과 반환

원 main은 예외 없이 false이고, 0이 아닌 조건은 평균 비교 결과를 반환합니다.

  • 원 main count=0: 전체 false · ArithmeticException 없음
  • 조건부 count≠0: 120/count 정수 나눗셈 후 평균≥30의 true/false
  • 출력 원 main: averageAtLeastThirty=false
  • 가드 통과 조건에서만 평균식을 평가하므로 0으로 나누지 않고 각 조건의 boolean 결과를 반환합니다.

단축 평가 가드가 위험한 나눗셈을 막는다

왼쪽 nonzero 가드가 오른쪽 나눗셈의 도달을 막습니다. 검사→평가 또는 건너뜀→결과/예외의 순서를 따라가기 좋습니다.

Java AND 단축 평가와 0 나눗셈 가드 흐름

FLOWCHART · SHORT-CIRCUIT GUARD

단축 평가 가드가 위험한 나눗셈을 막는다

AND의 왼쪽 조건은 전체 결과뿐 아니라 오른쪽 식을 실행할지도 결정한다. 0이 아닌지 먼저 확인해야 정수 나눗셈이 안전하게 열린다.

단축 평가 가드가 위험한 나눗셈을 막는다 memberCount가 0이면 AND 오른쪽 나눗셈을 평가하지 않고 false가 되며, 0이 아닐 때만 평균을 계산하는 안전 흐름과 나눗셈을 먼저 둔 예외 흐름을 비교한다. totalAge = 120 평균 조건 평가 시작 memberCount != 0 ? 왼쪽 조건 · 0 확인 오른쪽 식 실행 안 함 전체 false · 예외 없음 false totalAge / memberCount 오른쪽 평균식 평가 true 비교 결과 반환 평균 ≥ 30의 true / false 위험한 순서 · 나눗셈 먼저 totalAge / memberCount memberCount가 0인 경우 count = 0 ArithmeticException 가드 전에 예외 발생 왼쪽 조건이 false이면 오른쪽 식을 실행하지 않습니다 이 예제는 정수 나눗셈을 안전하게 실행하도록 순서를 정합니다.
단축 평가 가드가 위험한 나눗셈을 막는다 왼쪽count!=0 참일때만오른쪽평균비교;거짓이면미실행false;역순count0예외. 본문과 바로 아래 설명이 원래 코드·수치를 보완합니다. 안전한 순서 위험한 순서 · count=0 참 거짓 totalAge = 120 AND 평가 시작 memberCount != 0 ? 오른쪽 평균 평가 totalAge / memberCount >= 30 비교 결과 true 또는 false 오른쪽 미실행 전체 false · 예외 없음 가드 전에 정수 나눗셈 Arithmetic Exception 발생 가드까지 도달 못함
  1. 왼쪽 가드부터 평가

    memberCount != 0이 오른쪽 나눗셈을 실행해도 되는지 결정합니다.

  2. false이면 중단

    memberCount == 0이면 오른쪽은 실행되지 않음이고 전체 결과는 false입니다.

  3. true일 때만 평균 계산

    totalAge / memberCount >= 30을 평가해 최종 boolean을 만듭니다.

  4. 역순은 안전하지 않음

    나눗셈을 먼저 두고 count가 0이면 가드에 도달하기 전에 ArithmeticException이 발생합니다.

&&의 조건 순서는 결과뿐 아니라 오른쪽 식의 실행 여부를 결정한다.

이 예의 조건

  • totalAge=120인 정수 나눗셈의 안전한 && 식을 추적합니다. 원 main은 memberCount=0이고, count≠0은 같은 식의 조건부 비교입니다.
  • 나눗셈을 앞에 둔 broken 식은 독립 반례이며 위 관계도와 표의 원문 표에 보존합니다. 안전한 식의 뒤 동작으로 연결하지 않습니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
10totalAge
15평균 ≥ 30의 true
18totalAge
원 도표의 설명과 주석
구분내용
전체 읽기 정보단축 평가 가드가 위험한 나눗셈을 막는다 Java AND 단축 평가와 0 나눗셈 가드 흐름 FLOWCHART · SHORT-CIRCUIT GUARD 단축 평가 가드가 위험한 나눗셈을 막는다 AND의 왼쪽 조건은 전체 결과뿐 아니라 오른쪽 식을 실행할지도 결정한다. 0이 아닌지 먼저 확인해야 정수 나눗셈이 안전하게 열린다. 왼쪽 가드부터 평가 memberCount != 0 이 오른쪽 나눗셈을 실행해도 되는지 결정합니다. false이면 중단 memberCount == 0 이면 오른쪽은 실행되지 않으며 전체 결과는 false 입니다. true일 때만 평균 계산 totalAge / memberCount >= 30 을 평가해 최종 boolean을 만듭니다. 역순은 안전하지 않음 나눗셈을 먼저 두고 count가 0이면 가드에 도달하기 전에 ArithmeticException 이 발생합니다. && 의 조건 순서는 결과뿐 아니라 오른쪽 식의 실행 여부를 결정한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
안전한 식의 입력

totalAge=120과 memberCount를 안전한 식에 전달합니다.

  • 원 main: count=0
  • 조건부 비교: count≠0
  • 평가 순서: count 가드 → 평균식
  • 같은 안전한 식의 왼쪽 가드가 나눗셈 실행 여부를 먼저 결정합니다.
왼쪽 가드 판정

memberCount!=0을 먼저 평가하고 오른쪽 도달 여부를 정합니다.

  • count=0: 왼쪽 false · 오른쪽 미평가
  • count≠0: 왼쪽 true · 오른쪽 평가 가능
  • 변수 상태: 읽기만 수행 · 변경 없음
  • count=0의 실행은 나눗셈 전에 false로 끝나고, 0이 아닌 조건만 오른쪽으로 갑니다.
조건별 결과 반환

원 main은 예외 없이 false이고, 0이 아닌 조건은 평균 비교 결과를 반환합니다.

  • 원 main count=0: 전체 false · ArithmeticException 없음
  • 조건부 count≠0: 120/count 정수 나눗셈 후 평균≥30의 true/false
  • 출력 원 main: averageAtLeastThirty=false
  • 가드 통과 조건에서만 평균식을 평가하므로 0으로 나누지 않고 각 조건의 boolean 결과를 반환합니다.

단축 평가 때문에 조심할 점도 있습니다. 조건식 안에 부수 효과가 있으면 그 부수 효과가 일어날지가 왼쪽 조건에 따라 달라집니다. 예를 들어 age >= 18 && adultCount++ < 100이라고 쓰면 18세 미만 회원에서는 adultCount++가 실행되지 않습니다. 증가가 일어났는지를 코드만 보고 알기 어려우므로, 상태를 바꾸는 코드는 조건식 밖의 별도 문장으로 둡니다.

더 깊이: &와 |는 양쪽을 모두 계산한다

boolean 값에는 &와 |도 쓸 수 있습니다. 결과는 &&, ||와 같지만 단축 평가를 하지 않고 언제나 양쪽을 모두 계산합니다. 그래서 &&를 &로 잘못 치면 가드가 사라집니다.

boolean result = memberCount != 0 & (totalAge / memberCount) >= 30;

memberCount가 0이면 이 식은 왼쪽이 거짓인데도 오른쪽 나눗셈을 계산해 ArithmeticException을 던집니다. 오른쪽을 반드시 실행해야 하는 특별한 이유가 없다면 조건에는 &&와 ||를 씁니다. 같은 &, |를 정수에 쓰면 비트 단위로 계산하는 별개의 연산이 됩니다.

복합 대입 연산자

변수의 현재 값에 무언가를 더하거나 곱해서 같은 변수에 다시 저장하는 일은 아주 흔합니다. totalAge = totalAge + 30처럼 변수 이름을 두 번 쓰는 대신 totalAge += 30이라고 줄여 쓸 수 있는데, 이런 연산자를 복합 대입 연산자라고 합니다. 사칙연산과 나머지에 각각 +=, -=, *=, /=, %=가 있습니다.

src/CompoundAssignment.java
public final class CompoundAssignment {
    public static void main(String[] args) {
        int totalAge = 40;

        totalAge += 30;
        totalAge -= 10;
        totalAge *= 2;
        totalAge /= 4;

        System.out.println("total=" + totalAge);
    }
}
total=30

totalAge += 30은 '현재 값을 읽고, 30을 더해서, 같은 변수에 다시 저장하라'는 뜻입니다. 네 문장을 지나며 totalAge가 바뀌는 과정은 다음과 같습니다.

문장계산실행 뒤 totalAge
int totalAge = 40;처음 값40
totalAge += 30;40 + 3070
totalAge -= 10;70 - 1060
totalAge *= 2;60 × 2120
totalAge /= 4;120 ÷ 430

int 변수에 쓴 /=가 정수 나눗셈이라는 점도 그대로입니다. 120은 4로 나누어떨어져서 30이 되었지만, 121이었다면 소수 부분을 버린 30이 저장됩니다.

더 깊이: 복합 대입에 숨은 형변환

a += b를 a = a + b로 읽어도 대부분은 맞지만, 둘이 완전히 같지는 않습니다. Java 언어 명세는 복합 대입을 '계산한 결과를 왼쪽 변수의 타입으로 형변환해서 저장하는 것'으로 정의합니다. int끼리는 결과도 int라서 차이가 없지만, byte처럼 작은 타입에서는 이 숨은 형변환이 드러납니다.

lab/CompoundNarrowing.java
public final class CompoundNarrowing {
    public static void main(String[] args) {
        byte small = 120;
        small += 10;
        System.out.println("small=" + small);
    }
}
small=-126

Java는 byte 값이 들어간 덧셈을 int로 계산하므로 small + 10의 결과는 int 130입니다. 그래서 small = small + 10;이라고 쓰면 int를 byte에 넣다가 값을 잃을 수 있다며 컴파일러가 거부합니다(possible lossy conversion from int to byte). 그런데 small += 10;은 숨은 (byte) 형변환 덕분에 그대로 컴파일됩니다. byte에는 -128부터 127까지만 담을 수 있어서 130은 256을 뺀 -126으로 바뀝니다. 컴파일러가 막아 주던 손실이 복합 대입에서는 조용히 일어나는 것입니다. 형변환과 값의 넘침은 변수 스코프와 숫자 형변환에서 자세히 다룹니다.

명령행 인자로 회원 상태 계산하기

이제 회원가입 입력을 받아 판정값을 계산해 보겠습니다. 아직 분기는 하지 않고 valid와 active 두 값만 출력합니다. 앞 글의 마지막 예제처럼 명령행에 적는 실행 인자 args로 이메일, 나이, 최소 가입 나이를 받고, Integer.parseInt로 뒤의 두 값을 int로 바꿉니다. 이번 예제는 인자 세 개가 모두 있고 두 숫자가 int 범위의 정수 문자열이라고 가정합니다. 인자 개수는 앞 글처럼 args.length로 확인할 수 있지만 조건 계산에 집중하려고 생략했고, 숫자가 아닌 문자열을 처리하는 방법은 3장의 입력 검증과 재입력 반복에서 다룹니다.

이메일은 email.isBlank()로 검사합니다. isBlank()는 문자열이 비어 있거나 공백 문자로만 이루어져 있으면 true를 돌려주므로, 앞에 !를 붙인 !email.isBlank()는 '공백이 아닌 글자가 하나라도 있다'는 뜻이 됩니다. 이 검사는 @가 있는지 같은 이메일 형식까지는 확인하지 않습니다. 정확한 정의는 String API의 isBlank 설명에서 확인할 수 있습니다.

src/MemberFlags.java
public final class MemberFlags {
    public static void main(String[] args) {
        String email = args[0];
        int age = Integer.parseInt(args[1]);
        int minimumAge = Integer.parseInt(args[2]);

        boolean emailPresent = !email.isBlank();
        boolean ageValid = age >= 1 && age <= 120;
        boolean minimumAgeValid = minimumAge >= 1 && minimumAge <= 120;
        boolean valid = emailPresent && ageValid && minimumAgeValid;
        boolean active = valid && age >= minimumAge;

        System.out.println("valid=" + valid);
        System.out.println("active=" + active);
    }
}

javac -d out src/MemberFlags.java로 컴파일하고 두 가지 입력으로 실행한 결과입니다. >로 시작하는 줄이 입력한 명령이고, 그 아래 줄들이 출력입니다.

> java -cp out MemberFlags [email protected] 45 60
valid=true
active=false

> java -cp out MemberFlags [email protected] 700 60
valid=false
active=false

첫 실행에서는 이메일과 두 숫자가 모두 범위 안에 있으므로 valid가 참입니다. 하지만 45세는 최소 가입 나이 60세에 못 미치므로 active는 거짓입니다.

두 번째 실행에서는 나이 700이 1–120을 벗어나 valid가 거짓이 되고, &&의 왼쪽이 거짓이므로 active도 거짓입니다. 이때 오른쪽 비교 age >= minimumAge는 단축 평가 때문에 계산되지도 않습니다. 그 비교만 따로 했다면 700은 60보다 크므로 참이 나왔을 것입니다. 만약 active를 age >= minimumAge 하나로만 정했다면 나이 700인 입력도 활성 회원이 되었을 것입니다. valid를 함께 묶었기 때문에 유효하지 않은 입력은 최소 나이와 상관없이 활성 회원이 될 수 없습니다.

정리

  • ++, --, 대입처럼 변수를 바꾸는 식은 식의 결과와 실행 뒤 변수의 값을 따로 따라가야 합니다. 후위 x++는 바꾸기 전 값을, 전위 ++x는 바꾼 뒤 값을 결과로 내놓습니다.
  • =는 대입이고 ==는 비교입니다. boolean은 if (active), if (!active)처럼 비교 연산자 없이 조건에 씁니다.
  • 비교 연산자는 boolean을 만들고 &&, ||, !가 조건을 묶습니다. &&가 ||보다 먼저 묶이므로 둘을 섞어 쓸 때는 괄호로 의도를 적습니다.
  • &&와 ||는 결과가 정해지면 오른쪽을 계산하지 않습니다. 이 단축 평가를 이용해 왼쪽에 가드를 두고, 조건식 안에는 부수 효과를 넣지 않습니다.
  • a += b는 a = a + b와 거의 같지만, 결과를 a의 타입으로 형변환하는 단계가 숨어 있습니다.

다음 글에서는 이렇게 만든 boolean 값에 따라 회원을 저장할지 오류를 출력할지를 if로 실제로 나눕니다.

연습 문제

문제 1: 부분 조건과 최종 조건 함께 출력하기

이메일이 비어 있지 않고, 나이가 18세 이상 120세 이하이며, 로그인 실패 횟수가 2 이하일 때만 eligible=true가 되는 SignupFlags를 작성하세요. age는 50, failedLoginCount는 3으로 두고 각 부분 조건과 최종 조건을 모두 출력합니다.

해설 보기
src/SignupFlags.java
public final class SignupFlags {
    public static void main(String[] args) {
        String email = "[email protected]";
        int age = 50;
        int failedLoginCount = 3;

        boolean emailPresent = !email.isBlank();
        boolean ageAllowed = age >= 18 && age <= 120;
        boolean loginAttemptsAllowed = failedLoginCount <= 2;
        boolean eligible = emailPresent
            && ageAllowed
            && loginAttemptsAllowed;

        System.out.println("emailPresent=" + emailPresent);
        System.out.println("ageAllowed=" + ageAllowed);
        System.out.println("loginAttemptsAllowed=" + loginAttemptsAllowed);
        System.out.println("eligible=" + eligible);
    }
}
emailPresent=true
ageAllowed=true
loginAttemptsAllowed=false
eligible=false

이메일과 나이는 조건을 만족하지만 실패 횟수 3이 2를 넘으므로 loginAttemptsAllowed가 거짓이고, &&로 이은 eligible도 거짓이 됩니다. 최종 결과만 출력했다면 세 조건 중 무엇이 실패했는지 알 수 없었을 것입니다. 연습할 때는 이렇게 부분 조건에 이름을 붙여 함께 출력하고, 실제 프로그램에서는 다음 글의 if로 실패 이유를 골라 사용자에게 안내합니다.

문제 2: 전위와 후위 증가 계산하기

다음 세 줄을 실행한 뒤 x, y, z의 값을 구하고, 각 값이 그렇게 되는 이유를 설명하세요. 줄마다 증가 연산자가 식에 내놓는 값과, 그 줄이 끝난 뒤 x에 남는 값을 나누어 적어 보면 헷갈리지 않습니다.

int x = 3;
int y = x++ * 2;
int z = ++x * 2;
해설 보기

x는 5, y는 6, z는 10입니다. 둘째 줄의 후위 x++는 증가하기 전 값 3을 결과로 내놓고 x를 4로 바꾸므로 y는 3 × 2인 6입니다. 셋째 줄의 전위 ++x는 먼저 x를 5로 바꾸고 그 새 값을 결과로 내놓으므로 z는 5 × 2인 10입니다. 이렇게 한 줄에 증가와 계산을 섞으면 따져야 할 것이 많아지므로, 실제 코드에서는 증가를 별도 문장으로 빼는 편이 읽기 쉽습니다.

문제 3: ||로 만든 가드

회원이 없을 때도 안전하게 '평균 나이가 30 미만인가'를 판정하려고 memberCount == 0 || totalAge / memberCount < 30이라고 썼습니다. memberCount가 0일 때 이 식이 예외 없이 계산되는 이유와 결과를 설명하세요.

해설 보기

memberCount가 0이면 왼쪽 memberCount == 0이 참입니다. ||는 한쪽만 참이어도 전체가 참이므로 오른쪽 나눗셈을 계산하지 않고 결과를 true로 정합니다. 나눗셈이 실행되지 않으니 0으로 나누는 예외도 생기지 않습니다.

&& 가드가 왼쪽에 '오른쪽을 계산해도 되는 조건'을 두었다면, || 가드는 왼쪽에 '오른쪽을 계산하면 안 되는 상황'을 둡니다. 다만 이 식은 회원이 없는 경우도 '평균 30 미만'과 같은 쪽으로 분류합니다. 그렇게 분류해도 되는지는 연산자가 아니라 요구사항이 정할 문제입니다.