
Java
0. 들어가며

IDE 환경에서 코딩 테스트 문제를 풀다가, 숫자로 이루어진 문자열을 정수형으로 변환해 주는 valueOf() 메서드에 대해 IntelliJ가 경고를 띄워준 적이 있습니다. IDE가 parseInt()로 바꿀 수 있다고 추천해주더라고요..! “어? 둘 다 같은 거 아닌가?”, “어차피 int로 변환하는 건데 뭐가 다른 거지?”라는 생각이 들었습니다. 처음에는 단순 스타일 차이라고 생각했지만, 내부 구현 코드까지 살펴보니 명확한 차이가 존재한다는 것을 알게 되었습니다.
기업 코딩 테스트의 경우 대부분 프로그래머스 환경을 사용합니다. 프로그래머스에서는 IDE처럼 별도의 경고나 조언을 제공하지 않기 때문에, 이런 차이를 인지하지 못한 채 넘어가기 쉽습니다. 게다가 숫자로 이루어진 문자열을 정수형으로 변환하는 상황은 코딩 테스트에서 자주 등장하는 패턴이기도 합니다.
IDE에서 문제를 풀다가 우연히 발견한 이 작은 경고. 이번 글에서는 valueOf()와 parseInt()의 차이점, 그리고 어떤 상황에서 무엇을 사용하는 것이 더 적절한지 정리해보려고 합니다. 그럼 함께 살펴보시죠 :>
1. 개념 정리
이런 변환 메서드는 대부분의 타입에 존재합니다. 정수형 변환에는 Integer, Long, Short, Byte 등 다양한 타입이 존재하고, 실수형 변환 역시 Float, Double과 같이 여러 타입이 있습니다. 이들 대부분은 parseXxx()와 valueOf() 메서드를 함께 제공하며, 각각 primitive 타입과 Wrapper 객체를 반환한다는 공통적인 구조를 가지고 있습니다. (단, Character는 예외적으로 parseXxx() 메서드를 제공하지 않습니다.) 정리한 표는 아래와 같습니다.

이번 글에서는 가장 대표적으로 사용되는 "Integer"를 기준으로 정리해보겠습니다.
1.1 parseInt()에 대해서
/**
* Parses the string argument as a signed decimal integer. The
* characters in the string must all be decimal digits, except
* that the first character may be an ASCII minus sign {@code '-'}
* ({@code '\u005Cu002D'}) to indicate a negative value or an
* ASCII plus sign {@code '+'} ({@code '\u005Cu002B'}) to
* indicate a positive value. The resulting integer value is
* returned, exactly as if the argument and the radix 10 were
* given as arguments to the {@link #parseInt(java.lang.String,
* int)} method.
*
* @param s a {@code String} containing the {@code int}
* representation to be parsed
* @return the integer value represented by the argument in decimal.
* @exception NumberFormatException if the string does not contain a
* parsable integer.
*/
public static int parseInt(String s) throws NumberFormatException {
return parseInt(s,10);
}
Java Integer 클래스 내부의 코드를 보면 위와 같이 구현되어 있는 것을 확인할 수 있습니다. 즉, Integer.parseInt(String s)는 primitive(원시형) int를 바로 반환하는 메서드입니다.
객체 생성 없이 바로 int를 반환합니다. 그래서 다음과 같은 특징이 있습니다.
- 불필요한 boxing/unboxing이 없다.
- 의도가 더 명확하다.
- 코딩 테스트에서 가장 많이 쓰이는 방식이다.
IntelliJ가 valueOf() 대신 parseInt()를 추천하는 이유도 바로 이 때문입니다.
“primitive가 필요한데 굳이 wrapper 객체를 반환하는 메서드를 쓰지 마세요.”
즉, 오류라기보다는 더 적절한 메서드를 쓰라는 권고입니다.
그런데, 위의 내부 구현 코드를 살펴보면 parseInt(String s) 메서드는 내부적으로 parseInt(s, 10)을 호출하는 것을 확인할 수 있습니다. 즉, 두 번째 파라미터로 기본값 10을 전달하여 10진수 기준으로 문자열을 해석하도록 되어 있습니다. parseInt(String s, int radix)의 시그니처는 다음과 같습니다.
public static int parseInt(String s, int radix) throws NumberFormatException {...}
여기서 radix에는 우리가 흔히 알고 있는 진법의 수를 넣어주면 됩니다. 그러면 문자열 s를 해당 radix 진법의 숫자로 해석한 뒤, 그 값을 10진수 int 타입으로 변환하여 반환합니다. 기본적으로 parseInt(String s)는 내부에서 radix = 10을 사용하므로, 10진법으로 구성된 문자열에 대해서는 우리가 일반적으로 기대하는 정수 값이 그대로 반환됩니다.
정리하자면, parseInt(String s, int radix)는 문자열을 지정된 진법(radix) 기준으로 해석하여, 그 결과를 10진수 int 타입으로 반환하는 메서드라고 이해할 수 있습니다.
1.2 valueOf()에 대해서
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
/**
* Returns an {@code Integer} object holding the
* value of the specified {@code String}. The argument is
* interpreted as representing a signed decimal integer, exactly
* as if the argument were given to the {@link
* #parseInt(java.lang.String)} method. The result is an
* {@code Integer} object that represents the integer value
* specified by the string.
*
* <p>In other words, this method returns an {@code Integer}
* object equal to the value of:
*
* <blockquote>
* {@code new Integer(Integer.parseInt(s))}
* </blockquote>
*
* @param s the string to be parsed.
* @return an {@code Integer} object holding the value
* represented by the string argument.
* @exception NumberFormatException if the string cannot be parsed
* as an integer.
*/
public static Integer valueOf(String s) throws NumberFormatException {
return Integer.valueOf(parseInt(s, 10));
}
Integer.valueOf(String s)는 Integer 객체를 반환하는 메서드입니다. 즉, 반환 타입은 int가 아니라 Integer입니다. 그런데, 코테를 보다 보면 이런 코드, 한 번쯤 써보셨을 겁니다. (저도 최근에 실제로 이렇게 사용했습니다…ㅠ)
int n = Integer.valueOf("123");
겉으로 보기에는 전혀 문제없어 보입니다. 실제로도 정답 처리에는 아무 영향이 없습니다. 그런데 내부적으로는 어떤 일이 일어나고 있을까요?
Integer.valueOf(String s)의 구현을 다음과 같은 동작 순서를 가질 것입니다.
- parseInt(s, 10) 호출 → int 반환
- Integer.valueOf(int) 호출 → Integer 객체 반환
그런데 우리가 변수 타입을 int로 선언했기 때문에,
int n = Integer.valueOf("123");
이 코드에서는 자동 언박싱(unboxing)이 발생합니다. 결국 실제 흐름은 이렇게 됩니다.
- parseInt() → int 생성
- valueOf() → Integer 객체 생성 (또는 캐시 반환)
- .intValue() 호출 → 다시 int로 변환
객체 단계를 한 번 거쳤다가 다시 primitive로 돌아오는 구조입니다. 여기서 한 가지 오해가 생길 수 있습니다. “그럼 성능 차이가 꽤 있지 않나요?” 사실 거의 없습니다. 그 이유는 다음과 같습니다.
- valueOf()는 내부적으로 parseInt()를 사용합니다.
- 작은 정수(-128 ~ 127)는 캐시된 객체 재사용합니다.
- JVM이 이런 패턴을 매우 잘 최적화합니다.
즉, 코딩 테스트에서 시간 초과가 날 정도의 차이는 아닙니다. 다만, primitive 타입이 목적이라면 굳이 객체 단계를 거칠 필요는 없다는 점에서 parseInt()가 더 자연스럽고 명확한 선택이었던 것이죠. 그래서 IDE에서 경고를 줬던 것입니다.
추가적으로 숫자 비교 연산을 하다가 아주 몇 번은 Integer 객체끼리 "==" 비교 연산을 하다가 테스트 케이스에서는 맞는데, 코드 제출 후 틀린 상황이 종종 있었습니다. 왜 틀렸는지에 대해서 그때는 그냥 .equals()를 활용해서 넘어갔었는데, Integer.valueOf(int) 내부를 보고 나서 그 이유를 깨달았습니다.
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
기본적으로 -128 ~ 127 범위의 값은 "캐시"됩니다. 즉,
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
이 경우 a와 b는 같은 객체를 가리킵니다. 하지만,
Integer a = Integer.valueOf(128);
Integer b = Integer.valueOf(128);
이 경우는 매번 새로운 객체가 생성됩니다.
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false
즉, 값의 범위에 따라서 작은 값인 경우에는 캐시된 객체를 활용하고, 범위를 넘어가는 값들에는 새로운 객체를 생성하는 로직이 내부적으로 숨어있었습니다. 그래서 "==" 비교 연산에서 값에 따라 어떤 조건문은 통과하고, 어떤 조건문에서는 통과하지 못하는 문제가 생겼던 것입니다.
그럼 왜 이런 차이가 생길까요?
- "=="는 값 비교가 아니라 참조(주소) 비교
- 작은 숫자는 캐시 재사용 → 같은 객체
- 큰 숫자는 새 객체 생성 → 다른 객체
따라서, Wrapper 객체 비교에서는 반드시 equals()를 사용해야 합니다. 그렇다면 int로 받으면 괜찮을까? 그렇습니다.
int a = Integer.valueOf(128);
int b = Integer.valueOf(128);
System.out.println(a == b); // true
이 경우에는 자동 언박싱이 일어나므로 a와 b는 primitive int 값이 됩니다. primitive끼리는 값 비교이므로 객체 캐시와는 전혀 관련이 없습니다. 만약, int자료형이 아닌 Integer로 받았다면, 해당 코테 문제는 틀렸겠죠...?
2. 정리하며
지금까지 parseInt() 메서드와 valueOf() 메서드에 대해서 알아보았습니다. 그동안은 큰 고민 없이 사용해 왔지만, 이렇게 내부 구현까지 정리해 보니 상황에 맞는 메서드를 선택하는 기준이 조금 더 명확해진 것 같습니다. 다시 정리해 보자면, Integer.valueOf(String)는 내부적으로 parseInt()를 호출한 뒤, 그 결과를 Integer 객체로 감싸서 반환합니다.
- parseInt()로 문자열을 int로 변환
- valueOf(int)로 Integer 객체 생성 (또는 캐시 반환)
이때 중요한 차이가 발생합니다.
- 작은 숫자(-128 ~ 127)는 캐시된 객체 재사용
- 그 외의 숫자는 새로운 객체 생성
- Wrapper 객체끼리 "==" 비교하면 참조 비교가 되므로 위험
- primitive(int)로 받으면 자동 언박싱이 일어나 값 비교가 되어 안전
int형 변수에 값을 받는다면, 두 메서드는 결과 값 자체는 동일합니다. 그래서 코딩 테스트에서 비교 연산 부분이 아니고선, valueOf()를 사용했다고 해서 오답이 되는 일은 전혀 없다고 생각합니다. 다만, 목적에 따라 선택을 잘해야겠죠?
- int 자료형 변수가 필요하다 → parseInt()가 더 적절
- Integer 객체 변수가 필요하다 → valueOf() 사용
IntelliJ의 경고 역시 “잘못됐다”는 의미가 아니라, “지금 상황에서는 parseInt()가 더 적절합니다”라는 제안에 가까웠던 것 같습니다. 저 역시 단순한 숫자 변환 목적이라면 parseInt()를 사용하는 방향으로 정리하게 되었습니다. 작은 차이처럼 보이지만, 이런 디테일을 이해하고 사용하는 것이 코드를 더 명확하게 만들어준다고 생각합니다. 여러분들도 이런 디테일한 부분들을 잘 챙겨가셨으면 좋겠습니다!
3. Reference
- 내 코드
- GPT 선생님
- 내부 구현 코드
'Developer > Java' 카테고리의 다른 글
| [Java] 순열과 조합을 구현하기 위한 Next Permutation 알고리즘에 대해서 (2) | 2026.03.04 |
|---|---|
| [Java] 우선순위 큐(Priority Queue)와 정렬(Order) (0) | 2025.10.05 |