개요
https://0htmdwns.tistory.com/13
[JAVA] Map에서 value값에 대응하는 key값 찾는 방법? - 백준1620번
백준 1620번 문제https://www.acmicpc.net/submit/1620/94655619 개요첫째 줄에 도감에 수록되어 있는 포켓몬의 개수 N과 맞춰야 하는 문제의 개수 M이 주어진다.둘째 줄부터 N개의 줄에 포켓몬의 번호가 1번인
0htmdwns.tistory.com
위 글을 작성하던 중, Map과 배열의 성능 차이, try-catch와 Character.isDigit()으로 입력값을 처리했을 때의 성능 차이가 궁금해 비교해보려 한다.
1-1. 배열, try-catch로 입력 값을 처리한 경우
import java.io.*;
import java.util.*;
public class Main {
public static void main(String [] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
String input [] = br.readLine().split(" ");
//입력 받을 포켓몬 수
int countPokemon = Integer.parseInt(input[0]);
//맞춰야할 정답 수
int countQuestion = Integer.parseInt(input[1]);
//번호와 포켓몬을 저장할 맵.
Map <String, Integer> Encyclopedia = new HashMap<>();
//번호에 대응하는 이름 저장할 배열
String [] numberToName = new String[countPokemon + 1];
for(int i = 1; i <= countPokemon; i++){
String pokemon = br.readLine();
Encyclopedia.put(pokemon, i);
numberToName[i] = pokemon;
}
//포켓몬 이름을 입력한 경우, 해당 도감 번호 출력
for(int i = 0; i < countQuestion; i++){
String inputQuestion = br.readLine();
//숫자 처리
try{
int number = Integer.parseInt(inputQuestion);
bw.write(numberToName[number] + "\n");
}
catch(NumberFormatException e){ //문자 처리
bw.write(Encyclopedia.get(inputQuestion) +"\n");
}
}
bw.flush();
bw.close();
br.close();
}
}

1-2. 배열, Character.isDigit()로 입력 값을 처리한 경우
import java.io.*;
import java.util.*;
public class Main {
public static void main(String [] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
String input [] = br.readLine().split(" ");
//입력 받을 포켓몬 수
int countPokemon = Integer.parseInt(input[0]);
//맞춰야할 정답 수
int countQuestion = Integer.parseInt(input[1]);
//번호와 포켓몬을 저장할 맵.
Map <String, Integer> Encyclopedia = new HashMap<>();
//번호에 대응하는 이름 저장할 배열
String [] numberToName = new String[countPokemon + 1];
for(int i = 1; i <= countPokemon; i++){
String pokemon = br.readLine();
Encyclopedia.put(pokemon, i);
numberToName[i] = pokemon;
}
for(int i = 0; i < countQuestion; i++){
String inputQuestion = br.readLine();
if(Character.isDigit(inputQuestion.charAt(0))){
int number = Integer.parseInt(inputQuestion);
bw.write(numberToName[number] + "\n");
}
else{
bw.write(Encyclopedia.get(inputQuestion) + "\n");
}
}
bw.flush();
bw.close();
br.close();
}
}

2-1. Map, try-catch로 입력 값을 처리한 경우
import java.io.*;
import java.util.*;
public class Main {
public static void main(String [] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
String input [] = br.readLine().split(" ");
//입력 받을 포켓몬 수
int countPokemon = Integer.parseInt(input[0]);
//맞춰야할 정답 수
int countQuestion = Integer.parseInt(input[1]);
Map <String, Integer> Encyclopedia = new HashMap<>(); //번호와 포켓몬을 저장할 맵.
Map <Integer, String> numberToName = new HashMap<>(); //번호에 대응하는 이름 저장할 맵
for(int i = 1; i <= countPokemon; i++){
String pokemon = br.readLine();
Encyclopedia.put(pokemon, i);
numberToName.put(i, pokemon);
}
for(int i = 0; i < countQuestion; i++){
String inputQuestion = br.readLine();
try{
int number = Integer.parseInt(inputQuestion);
bw.write(numberToName.get(number) + "\n");
}
catch(NumberFormatException e){
bw.write(Encyclopedia.get(inputQuestion) + "\n");
}
}
bw.flush();
bw.close();
br.close();
}
}

2-2. Map, Character.isDigit()로 입력 값을 처리한 경우
import java.io.*;
import java.util.*;
public class Main {
public static void main(String [] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
String input [] = br.readLine().split(" ");
//입력 받을 포켓몬 수
int countPokemon = Integer.parseInt(input[0]);
//맞춰야할 정답 수
int countQuestion = Integer.parseInt(input[1]);
Map <String, Integer> Encyclopedia = new HashMap<>(); //번호와 포켓몬을 저장할 맵.
Map <Integer, String> numberToName = new HashMap<>(); //번호에 대응하는 이름 저장할 맵
for(int i = 1; i <= countPokemon; i++){
String pokemon = br.readLine();
Encyclopedia.put(pokemon, i);
numberToName.put(i, pokemon);
}
for(int i = 0; i < countQuestion; i++){
String inputQuestion = br.readLine();
if(Character.isDigit(inputQuestion.charAt(0))){
int number = Integer.parseInt(inputQuestion);
bw.write(numberToName.get(number) + "\n");
}
else{
bw.write(Encyclopedia.get(inputQuestion) + "\n");
}
}
bw.flush();
bw.close();
br.close();
}
}

성능 비교
| 접근 방법 | 메모리 | 시간 |
| 배열, try-catch | 98,316KB | 712ms |
| 배열, Character.isDigit() | 46,788KB | 576ms |
| Map, try-catch | 105,796KB | 732ms |
| Map, Character.isDigit() | 53,864KB | 480ms |
왜 try-catch보다 Character.isDigit()의 경우가 훨씬 뛰어난 성능을 보였을까?
1. 시간 차이 : try-catch는 예외 처리 비용이 크다.
- 자바에서 예외(Exception)는 정말로 예외적인 상황에서만 써야한다.
- try블록 내부에서 parseInt()가 실패하면, JVM은 스택 트레이스를 생성하고 예외 객체를 만들어서 catch로 넘겨줘야 한다.
- 이 과정이 무겁고 느림. 특히 catch가 자주 발생할 수록 시간이 기하급수적으로 늘어난다.
- 즉 Integer.parseInt("Pikachu") 같은 문자가 들어오면 예외가 발생하고, JVM이 복잡한 내부 처리를 하여 시간이 증가한다.
2. 메모리 차이 : 예외 객체 + 스택 트레이스 = 메모리 낭비
- try-catch 방식은 숫자가 아닌 입력마다 NumberFormatException 객체가 만들어진다.
- 자바의 예외는 내부적으로 스택 트레이스라는 디버깅 정보를 포함하는데, 이게 많은 메모리를 차지함.
- 입력의 수만큼 예외 객체가 생기고, 이 객체들이 메모리에 쌓이는데
- 이후 쓸모없어진 객체들을 JVM 내부에서 자동으로 메모리에서 제거(Garbage Collector)해줘야 하는 소요가 생긴다.
반면 Character.isDigit()은?
- char 하나만 검사 → 가볍고 빠름
- 예외 발생 없음 → 추가 객체 생성 X
- CPU, 메모리 모두 절약됨.
try-catch는 "예외 상황"에서만 쓰는 것이 원칙이다.
입력의 50% 이상이 예외라면, try-catch를 쓰는 건 적절하지 않은 방법인 것이다.
스택 트레이스(Stack Trace)란?
JVM에서 스택 트레이스는 예외(Exception)가 발생했을 때
그 예외가 어디서 발생했고, 어떤 경로로 호출됐는지를 보여주는 호출 경로(Call Stack) 기록이다.
정의
- 예외가 발생했을 때, 현재까지 쌓여 있는 메서드 호출 정보를 위에서부터 아래까지 출력한 것.
- 즉, 프로그램이 예외가 발생하기까지 어떤 함수가 어떤 함수를 호출했는지를 추적할 수 있는 디버깅 정보이다.
왜 메모리를 많이 쓸까?
- 예외가 발생하면 JVM은 해당 스택 정보 전체를 메모리에 저장하고,
- Exception 객체 안에 StackTraceElement[] 배열로 보관한다.
- 이 Exception 객체는:
- 각 메서드 이름
- 클래스 이름
- 파일 이름
- 줄 번호
- 를 다 포함하기 때문에 많은 문자열 객체 + 배열 공간이 필요하다.
- 즉, try-catch가 많고, 예외가 자주 발생하면 메모리 사용량이 증가하는 것이다.
예시 코드를 실행하면,
public class ExStackTrace {
public static void main(String[] args) {
method1();
}
public static void method1() {
method2();
}
public static void method2() {
int x = Integer.parseInt("not_a_number"); // 여기서 예외 발생
}
}
이런 오류(스택 트레이스)가 출력된다.
Exception in thread "main" java.lang.NumberFormatException: For input string: "not_a_number"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:588)
at java.base/java.lang.Integer.parseInt(Integer.java:685)
at ExStackTrace.method2(ExStackTrace.java:11)
at ExStackTrace.method1(ExStackTrace.java:7)
at ExStackTrace.main(ExStackTrace.java:3)
스택 트레이스를 보는 방법
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:588)
at java.base/java.lang.Integer.parseInt(Integer.java:685)
- 이 부분은 Java 내부 클래스가 예외를 발생시킨 코드 위치를 의미한다.
- Integer.parseInt()를 호출했는데, 내부적으로 오류가 발생한 것이다.
at ExStackTrace.method2(ExStackTrace.java:11)
at ExStackTrace.method1(ExStackTrace.java:7)
at ExStackTrace.main(ExStackTrace.java:3)
- 여기는 직접 작성한 사용자 코드에서 예외가 전달된 호출 순서를 보여준다.
- main() → method1() → method2() → Integer.parseInt() 순으로 호출 되었고
- 결국 method2()의 11번째 줄에서 잘못된 값이 넘어가 예외가 발생했다는 것이다.
가장 마지막 줄 : 예외가 실제로 발생한 지점
그 윗 줄들 : 예외가 전파된 호출 경로
결국 스택 트레이스를 보면,
어디서 예외가 났고, 왜 났고, 어떤 순서로 호출됐는지를 파악할 수 있어 디버깅이 수월해진다.
'BackEnd > Java' 카테고리의 다른 글
| [JAVA] Map.put()의 반환 규칙 : null을 반환하는 이유(putIfAbsent 메서드) (1) | 2025.08.12 |
|---|---|
| [JAVA] Stream API - 가독성 좋은 간결한 프로그래밍 (1) | 2025.05.22 |
| [JAVA] 자동 형변환 (char + char 연산) (0) | 2025.05.14 |