[알고리즘] 14. 예외 케이스 처리법

[알고리즘] 14. 예외 케이스 처리법

지난 글에서는 정렬 기준 만들기에 대해 정리했습니다. 이번 글에서는 알고리즘 문제를 풀 때 자주 틀리는 원인 중 하나인 예외 케이스 처리법에 대해 알아보겠습니다.

알고리즘 문제는 예제 입력만 맞는다고 끝이 아닙니다. 채점에는 다양한 입력이 들어오기 때문에, 평범한 경우뿐 아니라 가장 작거나 큰 값, 중복, 음수, 0, 빈 값 같은 상황도 고려해야 합니다.

예외 케이스는 일반적인 흐름에서는 잘 보이지 않지만, 정답 여부를 크게 좌우하는 특수한 입력 상황입니다.

1. 예외 케이스란?

예외 케이스는 코드가 일반적인 입력에서는 잘 동작하지만, 특정 입력에서는 잘못된 결과를 낼 수 있는 상황을 말합니다.

예를 들어 배열에서 최댓값을 찾는 문제를 생각해보겠습니다.

int[] numbers = {-5, -2, -9};

int max = 0;

for (int i = 0; i < numbers.length; i++) {
    if (numbers[i] > max) {
        max = numbers[i];
    }
}

System.out.println(max);

이 코드는 모든 값이 음수일 때 잘못된 결과를 냅니다. 배열 안에는 0이 없는데도 max가 0으로 남기 때문입니다.

올바르게 처리하려면 배열의 첫 번째 값을 기준으로 시작하는 것이 안전합니다.

int[] numbers = {-5, -2, -9};

int max = numbers[0];

for (int i = 1; i < numbers.length; i++) {
    if (numbers[i] > max) {
        max = numbers[i];
    }
}

System.out.println(max);

출력 결과는 다음과 같습니다.

-2
예외 케이스는 코드의 빈틈을 찾는 테스트입니다.

2. 예외 케이스를 확인해야 하는 이유

알고리즘 문제에서는 예제 입력이 항상 모든 상황을 보여주지 않습니다. 예제는 문제 이해를 돕기 위한 일부 데이터일 뿐입니다.

그래서 예제는 맞았는데 제출하면 틀리는 경우가 자주 발생합니다. 이때 원인은 대부분 아래와 같습니다.

  • 가장 작은 입력을 고려하지 않음
  • 가장 큰 입력을 고려하지 않음
  • 0이나 음수를 처리하지 않음
  • 중복 값을 고려하지 않음
  • 배열 인덱스 범위를 잘못 처리함
  • 정답이 없는 경우를 고려하지 않음
  • int 범위를 넘어가는 값을 고려하지 않음

문제를 풀 때는 코드 작성 후 예외 케이스를 따로 만들어 확인하는 습관이 필요합니다.


3. 최소 입력 확인하기

가장 먼저 확인할 예외 케이스는 최소 입력입니다. N의 최솟값이 1이라면 데이터가 하나만 들어오는 상황을 반드시 생각해야 합니다.

예를 들어 배열에서 인접한 두 값의 차이를 구하는 문제라면, 값이 하나뿐인 경우를 조심해야 합니다.

int[] numbers = {10};

for (int i = 0; i < numbers.length - 1; i++) {
    System.out.println(numbers[i + 1] - numbers[i]);
}

배열에 값이 하나뿐이면 비교할 다음 값이 없습니다. 이런 문제에서는 N이 1일 때 무엇을 출력해야 하는지 문제 조건을 확인해야 합니다.

입력 크기가 가장 작을 때도 코드가 자연스럽게 동작하는지 확인해야 합니다.

4. 최대 입력 확인하기

최소 입력만큼 중요한 것이 최대 입력입니다. 입력 크기가 커질수록 시간복잡도와 자료형 문제가 발생할 수 있습니다.

예를 들어 N이 100,000인데 이중 반복문을 사용하면 연산 횟수가 매우 커집니다.

for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        // 처리
    }
}

이 코드는 시간복잡도가 O(N²)입니다. N이 100,000이면 대략 10,000,000,000번에 가까운 반복이 발생할 수 있습니다.

따라서 최대 입력을 보고 현재 풀이가 제한 시간 안에 가능한지 확인해야 합니다.

N O(N²) 판단
100 10,000 대체로 가능
1,000 1,000,000 상황에 따라 가능
100,000 10,000,000,000 대부분 위험

5. 값이 하나만 있는 경우

배열이나 문자열 문제에서는 값이 하나만 들어오는 경우를 자주 놓칩니다.

예를 들어 배열에서 최댓값과 최솟값을 동시에 찾는 문제라면, 값이 하나뿐일 때 최댓값과 최솟값은 같은 값입니다.

int[] numbers = {7};

int max = numbers[0];
int min = numbers[0];

for (int i = 1; i < numbers.length; i++) {
    if (numbers[i] > max) {
        max = numbers[i];
    }

    if (numbers[i] < min) {
        min = numbers[i];
    }
}

System.out.println("최댓값: " + max);
System.out.println("최솟값: " + min);

출력 결과는 다음과 같습니다.

최댓값: 7
최솟값: 7

초기값을 배열의 첫 번째 값으로 잡으면 값이 하나뿐인 경우에도 안전하게 동작합니다.


6. 0 처리하기

숫자 처리 문제에서는 0을 반드시 조심해야 합니다. 특히 자리수 개수를 구하거나, while문으로 숫자를 나누는 문제에서 자주 실수합니다.

int num = 0;
int count = 0;

while (num > 0) {
    count++;
    num = num / 10;
}

System.out.println(count);

위 코드는 0의 자릿수를 0으로 출력합니다. 하지만 숫자 0은 한 자리 숫자입니다.

따라서 0은 따로 처리해야 합니다.

int num = 0;
int count = 0;

if (num == 0) {
    count = 1;
} else {
    while (num > 0) {
        count++;
        num = num / 10;
    }
}

System.out.println(count);

출력 결과는 다음과 같습니다.

1
숫자 문제에서는 0이 입력될 수 있는지 먼저 확인해야 합니다.

7. 음수 처리하기

음수는 최댓값, 최솟값, 합계, 정렬, 나머지 연산 문제에서 자주 예외가 됩니다.

특히 최댓값을 구할 때 max를 0으로 시작하면 음수 배열에서 틀릴 수 있습니다.

int[] numbers = {-10, -3, -7};

int max = numbers[0];

for (int i = 1; i < numbers.length; i++) {
    if (numbers[i] > max) {
        max = numbers[i];
    }
}

System.out.println(max);

출력 결과는 다음과 같습니다.

-3

음수가 들어올 수 있는 문제에서는 초기값을 임의로 0으로 두지 않는 것이 좋습니다.

상황 안전한 초기값
최댓값 찾기 배열의 첫 번째 값
최솟값 찾기 배열의 첫 번째 값
합계 구하기 0
개수 세기 0

8. 중복 값 처리하기

정렬, 순위, 빈도수 계산 문제에서는 중복 값이 중요한 예외 케이스가 됩니다.

예를 들어 가장 많이 나온 값을 찾는 문제에서 등장 횟수가 같은 값이 여러 개라면 어떻게 처리해야 할까요? 문제에서 작은 값을 출력하라고 할 수도 있고, 먼저 등장한 값을 출력하라고 할 수도 있습니다.

입력: 1 1 2 2 3
1의 빈도수: 2
2의 빈도수: 2

이런 경우에는 문제 조건을 확인해야 합니다.

  • 빈도수가 같으면 작은 값을 선택하는가?
  • 빈도수가 같으면 큰 값을 선택하는가?
  • 먼저 등장한 값을 선택하는가?
  • 중복이면 별도의 값을 출력하는가?

중복 값이 있는 경우는 문제마다 처리 기준이 다를 수 있으므로 조건을 정확히 읽어야 합니다.


9. 정답이 없는 경우

탐색 문제에서는 정답이 존재하지 않는 경우를 반드시 고려해야 합니다.

예를 들어 배열에서 target 값을 찾는 문제를 보겠습니다.

int[] numbers = {1, 3, 5, 7};
int target = 4;

boolean found = false;

for (int i = 0; i < numbers.length; i++) {
    if (numbers[i] == target) {
        found = true;
        break;
    }
}

if (found) {
    System.out.println("찾았습니다.");
} else {
    System.out.println("없습니다.");
}

출력 결과는 다음과 같습니다.

없습니다.

정답이 없을 수 있는 문제에서는 found 같은 변수를 사용해 결과를 구분하는 방식이 자주 사용됩니다.

탐색 문제에서는 찾은 경우와 찾지 못한 경우를 모두 처리해야 합니다.

10. 인덱스 범위 확인하기

배열과 문자열 문제에서 가장 흔한 오류는 인덱스 범위를 벗어나는 것입니다.

int[] arr = {10, 20, 30};

System.out.println(arr[3]);

위 코드는 오류가 발생합니다. 배열의 길이는 3이지만 사용할 수 있는 인덱스는 0, 1, 2뿐입니다.

사용 가능 인덱스: 0, 1, 2
잘못된 인덱스: 3

반복문에서는 보통 아래처럼 작성해야 안전합니다.

for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}

문자열도 마찬가지입니다. 마지막 문자는 length()가 아니라 length() - 1 위치에 있습니다.

String word = "java";

char last = word.charAt(word.length() - 1);

System.out.println(last);

출력 결과는 다음과 같습니다.

a

11. 빈 문자열과 공백 처리하기

문자열 문제에서는 빈 문자열이나 공백이 예외가 될 수 있습니다.

String text = "";

System.out.println(text.length());

출력 결과는 0입니다.

0

빈 문자열에서 첫 번째 문자를 가져오려고 하면 오류가 발생합니다.

String text = "";

System.out.println(text.charAt(0));

따라서 문자열 길이를 먼저 확인해야 합니다.

String text = "";

if (text.length() > 0) {
    System.out.println(text.charAt(0));
} else {
    System.out.println("빈 문자열입니다.");
}

공백 문자열도 조심해야 합니다.

String text = "   ";

System.out.println(text.length());
System.out.println(text.trim().length());

공백을 제거한 뒤 비어 있는지 확인해야 하는 문제라면 trim()을 사용할 수 있습니다.


12. int 범위 초과 확인하기

큰 수를 더하거나 곱하는 문제에서는 int 범위를 넘을 수 있습니다. 이때는 long을 사용해야 합니다.

예를 들어 1부터 N까지의 합을 구하는 공식은 다음과 같습니다.

N * (N + 1) / 2

N이 크면 int 범위를 넘을 수 있으므로 long으로 계산하는 것이 안전합니다.

int n = 100000;

long sum = (long) n * (n + 1) / 2;

System.out.println(sum);

여기서 중요한 부분은 (long) n입니다. 계산이 시작되기 전에 long으로 바꿔야 안전합니다.

큰 수 계산에서는 결과만 long에 담는 것이 아니라, 계산 과정부터 long으로 처리해야 합니다.

13. 나눗셈에서 0 확인하기

나눗셈이 있는 문제에서는 나누는 값이 0이 될 수 있는지 확인해야 합니다.

int a = 10;
int b = 0;

System.out.println(a / b);

정수를 0으로 나누면 오류가 발생합니다. 따라서 나누기 전에 b가 0인지 확인하는 것이 안전합니다.

int a = 10;
int b = 0;

if (b != 0) {
    System.out.println(a / b);
} else {
    System.out.println("0으로 나눌 수 없습니다.");
}

문제 조건에서 0이 들어오지 않는다고 보장한다면 따로 처리하지 않아도 되지만, 보장되지 않는다면 반드시 확인해야 합니다.


14. 정렬 후 첫 번째와 마지막 값 확인하기

정렬을 사용한 문제에서는 첫 번째 값과 마지막 값을 자주 사용합니다. 이때 배열이 비어 있거나 길이가 1인 경우를 조심해야 합니다.

int[] numbers = {5};

Arrays.sort(numbers);

int min = numbers[0];
int max = numbers[numbers.length - 1];

System.out.println(min);
System.out.println(max);

배열 길이가 1이면 min과 max가 같은 값이 됩니다. 이 자체는 문제가 아니지만, 두 개 이상의 값을 비교해야 하는 문제라면 별도 처리가 필요할 수 있습니다.

값이 1개일 때
최솟값 = 최댓값

정렬 후 인덱스를 사용할 때는 배열 길이를 먼저 생각하는 습관이 좋습니다.


15. 예외 케이스 직접 만들어보기

예외 케이스는 문제를 풀고 나서 직접 만들어보는 것이 좋습니다. 아래처럼 입력을 다양하게 바꿔 테스트할 수 있습니다.

확인할 상황 예시 입력
값이 하나뿐인 경우 1개 데이터
모든 값이 같은 경우 5 5 5 5
모든 값이 음수인 경우 -5 -2 -9
0이 포함된 경우 0 1 2 3
정답이 없는 경우 target이 존재하지 않음
최댓값 근처 입력 N이 제한 조건의 최댓값

처음에는 번거롭게 느껴질 수 있지만, 예외 케이스를 직접 확인하면 틀리는 횟수를 크게 줄일 수 있습니다.


16. 예외 케이스 체크리스트

알고리즘 문제를 제출하기 전에 아래 내용을 빠르게 확인해보면 좋습니다.

  • N이 가장 작은 경우도 동작하는가?
  • N이 가장 큰 경우 시간복잡도가 괜찮은가?
  • 값이 1개만 들어와도 처리되는가?
  • 0이 들어올 수 있는가?
  • 음수가 들어올 수 있는가?
  • 중복 값이 들어올 수 있는가?
  • 정답이 없는 경우를 처리했는가?
  • 배열이나 문자열의 마지막 인덱스를 안전하게 처리했는가?
  • 합계나 곱셈 결과가 int 범위를 넘지 않는가?
  • 나눗셈에서 0으로 나누는 상황은 없는가?
제출 전 체크리스트를 한 번만 확인해도 많은 실수를 줄일 수 있습니다.

17. 정리

이번 글에서는 예외 케이스 처리법에 대해 정리했습니다.

  • 예외 케이스는 일반적인 입력이 아닌 특수한 입력 상황이다.
  • 최소 입력과 최대 입력을 반드시 확인해야 한다.
  • 값이 하나뿐인 경우, 0, 음수, 중복 값은 자주 등장하는 예외 케이스다.
  • 탐색 문제에서는 정답이 없는 경우를 처리해야 한다.
  • 배열과 문자열에서는 인덱스 범위 초과를 조심해야 한다.
  • 큰 수 계산에서는 int 대신 long을 고려해야 한다.
  • 나눗셈에서는 0으로 나누는 상황이 있는지 확인해야 한다.

알고리즘 문제에서 예외 케이스를 잘 처리한다는 것은 단순히 실수를 줄이는 것 이상입니다. 문제를 더 정확히 이해하고, 코드가 다양한 상황에서도 안정적으로 동작하게 만드는 과정입니다.

좋은 풀이는 예제만 맞는 코드가 아니라, 숨어 있는 예외 상황까지 버티는 코드입니다.

다음 글 예고

다음 글부터는 자료구조 영역으로 들어갑니다. 다음 글에서는 스택에 대해 알아보겠습니다.

스택의 개념, push와 pop, 후입선출 구조, 괄호 검사 문제까지 예제로 정리해보겠습니다.