이 노트는 Java의 래퍼 숫자를 값처럼 읽다가 참조 비교·null unboxing·숫자 승격·부재 의미를 놓치는 경계를 설명합니다. 식을 읽을 때 먼저 정적 타입과 필요한 변환을 적고, 그 다음 null·equals·Optional의 도메인 계약을 확인합니다.
래퍼 숫자의 참조 동일성과 == 비교 조건
int와 Integer 사이를 오갈 때 컴파일러가 boxing·unboxing을 자동으로 넣을 수 있지만, 두 Integer에 ==를 쓰면 먼저 값이 아니라 참조가 같은지 비교합니다. 그래서 127처럼 일부 상수 표현식의 boxing에서 참조 공유가 보장되는 값은 숫자 비교처럼 보일 수 있습니다. 더 큰 값의 참조 공유는 구현이 확장할 수 있으므로 “128 이상이면 반드시 다른 참조”라고 단정하지 말고, 숫자를 비교할 때는 기본형 변환이나 equals의 타입 계약을 의도에 맞게 선택합니다.
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // 이 상수 표현식에서는 true
Integer n = null;
System.out.println(n == null); // true
// n == 0 은 unboxing 중 NullPointerException
실행문은 main 같은 메서드 안에 놓습니다. n == null은 참조 비교이고 n == 0은 숫자 비교를 위해 n의 기본형 값을 필요로 합니다. null을 자동으로 0으로 바꾸지 않습니다. 실행 추적은 Integer n=null → n==null에서는 참조 비교가 끝나지만, n==0에서는 비교를 위해 unbox하는 순간 예외가 발생하는 순서입니다. 따라서 ==의 결과만 외우기보다 컴파일러가 어느 피연산자를 기본형으로 변환하는지 확인해야 합니다.
산술식 피연산자 변환과 unboxing 실패 지점
Integer left = null; Long right = 2L; left + right를 보면 결과 타입을 long으로 정하기 전에 피연산자의 숫자 값을 꺼내야 합니다. left의 unboxing에서 예외가 나므로 실제 덧셈은 수행되지 않습니다. 래퍼를 혼합한 조건 연산자에서도 타입 규칙에 따라 예상하지 못한 unboxing이 생길 수 있습니다.
| 식 | 비교·계산의 의미 | 주의점 |
|---|---|---|
| Integer == Integer | 참조 동일성 | 숫자 값 비교가 아님 |
| Integer == int | unboxing 후 숫자 비교 | null 실패 |
| Objects.equals(a,b) | null-safe 객체 equals | 다른 래퍼 타입의 수학적 비교 아님 |
| Integer(1).equals(Long(1)) | 서로 다른 래퍼 타입 | false |
| map.get(key) + 1 | 부재값 unboxing 가능 | 먼저 누락 정책 필요 |
표의 Integer(1)·Long(1)은 값 개념을 뜻하며 실제 코드는 valueOf 등을 사용합니다. Objects.equals는 null 안전성만 제공하므로 범위·형식·숫자 타입 통일은 따로 해야 합니다. 실무 선택은 “null이면 실패인가, 정상 부재인가, 0인가”를 API 반환형과 오류 채널에 먼저 표현하는 것입니다. map.get(key)+1 같은 식은 map 부재와 저장된 null을 같은 unboxing 실패로 만들 수 있으므로 containsKey·명시적 결과 타입·도메인 오류 중 하나를 선택합니다. boxing을 반복하는 합산 코드는 객체 할당·참조 교체 비용을 만들 수 있어 원시형이 적합한지 실제 할당 프로파일로 확인합니다.
0·정상 부재·조회 실패의 결과 구분
재고가 0인 것과 재고 정보가 없는 것, DB 조회가 실패한 것은 같지 않습니다. 모든 부재를 기본 0으로 바꾸면 품절·미등록·장애가 섞입니다. 외부 입력에서 null을 허용하는지, 생략이 기존값 유지인지, 0이 실제 값인지 먼저 정합니다.
그림을 글로 읽기
- DB·API의 숫자 결과
- 값·정상 부재·조회 실패 분리
- 숫자 타입·범위 확정
- 기본형 또는 도메인 계산
- DB·API의 숫자 결과 → 값·정상 부재·조회 실패 분리: 계약 해석
- 값·정상 부재·조회 실패 분리 → 숫자 타입·범위 확정: 값이 있는 경우
- 숫자 타입·범위 확정 → 기본형 또는 도메인 계산: unboxing 전 조건 충족
nullable Integer를 반환하는 API는 호출자가 null인지 먼저 분기한 뒤 숫자 계산으로 넘어가야 한다는 계약입니다. Optional<Integer>나 OptionalInt는 정상적으로 값이 없는 경우를 반환형에 드러내지만, 필드·직렬화·컬렉션에서 항상 최선인 것은 아니므로 프레임워크 지원·비용·사용 위치를 함께 봅니다. 정상 부재만 Optional.empty로 표현하고, 오류를 같은 empty로 바꾸어 숨기지 않습니다.
Optional 기본값 함수의 실행 시점
optional.orElse(expensiveDefault())는 optional에 값이 있어도 메서드 인자 평가로 expensiveDefault가 실행됩니다. orElseGet(() -> expensiveDefault())는 값이 없을 때 supplier를 실행합니다. 기본값 함수가 외부 조회·로그·자격 발급을 하면 단순 성능 문제가 아니라 불필요한 부수 효과가 됩니다.
값이 없으면 거절해야 하는 API는 임의 기본값을 만들기보다 명확한 도메인 오류로 처리할 수 있습니다. DB NULL·JSON null·필드 생략·기본값의 왕복 의미도 테스트합니다. 모든 숫자가 int 범위에 들어가는지와 overflow·반올림 문제는 boxing과 별도입니다.
컴파일 타입과 실행 오류의 분리 확인
래퍼-래퍼·래퍼-기본형·null·서로 다른 래퍼·조건식·overload의 작은 예를 만들어 컴파일 타입과 결과를 대조합니다. Optional에 값이 있을 때와 없을 때 기본값 함수 호출 횟수를 검사합니다. 기본 PATH의 javac는 JDK를 찾지 못했지만, 이후 Homebrew OpenJDK 21.0.12.1을 찾아 scripts/VerifyJavaStudy.java로 상수 boxing·null unboxing·다른 래퍼 equals·지연 기본값 예제를 확인했습니다. 모든 overload·조건식·외부 DB 부재 변환까지 실행한 것은 아닙니다. 추가 검증에서는 컴파일 경고와 실제 예외 위치를 함께 기록하고, orElse의 기본값 함수가 값 존재 시에도 호출되는지 부수 효과 카운터로 확인합니다. 이 범위 밖의 overload·조건식 결과는 별도 작은 예제로 확정해야 합니다.