본문 바로가기

BackEnd/Java

[JAVA] Map과 배열, try-catch와 Character.isDigit() 성능 분석 (+스택 트레이스)

개요

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번째 줄에서 잘못된 값이 넘어가 예외가 발생했다는 것이다. 

가장 마지막 줄 : 예외가 실제로 발생한 지점

그 윗 줄들 : 예외가 전파된 호출 경로

 

결국 스택 트레이스를 보면, 

어디서 예외가 났고, 왜 났고, 어떤 순서로 호출됐는지를 파악할 수 있어 디버깅이 수월해진다.