language/java

[자바프로그래밍] 05. 배열과 예외처리

렁치 2026. 8. 14. 17:43

오늘은 두 주제를 다뤘다. 여러 값을 묶는 배열과, 실행 중 발생하는 오류에 대응하는 예외 처리다.
자바의 배열은 C와 달리 "객체"라는 점에서 차이가 있고, 예외 처리는 자바가 안정성을 위해 강조하는 기능이다.


1. 배열

자바 배열의 특징
같은 타입의 데이터가 순차 저장된 자료구조로, 인덱스는 0부터 length-1까지다. 자바 배열은 객체이며, 자기 크기를 length라는 속성으로 알고 있다(C 배열이 자기 길이를 모르는 것과 다름).
int[] data = new int[5];      // 선언 + 생성 (크기 5)
int[] data = {1, 2, 3};       // 선언 + 초기화

System.out.println(data.length);  // 배열 크기를 length로 안다

C와 다른 두 가지가 눈에 띄었다.

  • new로 생성하면 자동 초기화된다(숫자 0, boolean false, 참조형 null).
  • data.length로 크기를 알 수 있어, 반복문에서 크기를 따로 안 넘겨도 된다.

C에서 배열 길이를 매번 따로 넘기던 게 번거로웠는데, 자바는 length로 자기 크기를 안다는 점이 확실히 편했다.

배열은 객체라 대입하면 주소를 공유한다
b = a; 로 배열을 대입하면 내용이 복사되는 게 아니라, 두 변수가 같은 배열(같은 주소)을 가리키게 된다. 그래서 b를 고치면 a도 바뀐다. 내용을 진짜 복사하려면 원소를 하나씩 옮기거나 Arrays.copyOf 같은 메서드를 써야 한다.

b = a로 배열을 복사한 줄 알고 b를 고쳤다가 a까지 바뀌어서 당황했다. "배열은 객체라 대입은 주소 공유"라는 걸 그제야 알았다. C에서 배열 이름이 주소였던 것과 같은 맥락이었다.

배열 순회에는 for-each문이 깔끔하다.

int[] num = {1, 2, 3, 4, 5};
int sum = 0;
for (int k : num) sum += k;   // 인덱스 없이 모든 원소를 k로 받음

2차원 배열은 score.length로 행 수를, score[i].length로 i번째 행의 열 수를 구한다. 각 행의 길이가 달라도 되는 비정방형 배열도 가능하다.


2. 예외 처리 (Exception)

컴파일 오류 vs 예외
컴파일 오류는 문법이 틀려 컴파일 단계에서 잡히는 것이고, 예외(Exception)는 문법은 맞지만 실행 중에 터지는 오류다(0으로 나누기, 배열 범위 초과, 잘못된 입력 등). 예외를 처리하지 않으면 프로그램이 강제 종료된다.

자바는 try ~ catch ~ finally로 예외에 대응한다.

try {
    // 예외가 날 수 있는 코드
} catch (예외타입 e) {
    // 예외가 났을 때 처리
} finally {
    // 예외 발생과 무관하게 항상 실행 (생략 가능)
}

자주 만나는 예외들이다.

예외 발생 상황
ArithmeticException 정수를 0으로 나눔
ArrayIndexOutOfBoundsException 배열 범위를 벗어난 인덱스 접근
NumberFormatException 숫자로 못 바꾸는 문자열 변환
NullPointerException null 객체의 멤버 접근
try {
    intArray[i+1] = ...;   // i가 끝에 닿으면 범위 초과
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("배열 인덱스가 범위를 벗어났습니다.");
}

예외 처리의 핵심 가치는 "오류가 나도 프로그램이 죽지 않고 우아하게 대응"하는 것이다. C에서는 배열 범위를 벗어나도 그냥 엉뚱한 메모리를 건드리고 넘어갔는데, 자바는 예외를 던져 명확히 알려주고 잡을 수 있게 해준다는 게 인상적이었다.


실습으로 익히기

입력받은 개수로 배열을 만들고 합·최대·최소·평균을 구하는 전형적인 예다.

int n = scanner.nextInt();
int[] arr = new int[n];        // 입력값으로 배열 크기 결정
int sum = 0;
for (int i = 0; i < arr.length; i++) {
    arr[i] = scanner.nextInt();
    sum += arr[i];
}
int max = arr[0], min = arr[0];
for (int x : arr) {
    if (x > max) max = x;
    if (x < min) min = x;
}
double avg = (double) sum / arr.length;

최댓값·최솟값을 "첫 원소로 초기화한 뒤 더 큰/작은 값으로 갱신"하는 패턴, 평균 계산 시 캐스팅으로 정수 나눗셈을 피하는 점이 핵심이다. 알고리즘 단원에서 본 갱신 패턴과 정수 나눗셈 함정이 자바 실습에서 그대로 반복되는 게 보였다.


오늘 느낀 점

  • 자바 배열이 객체라서 length로 크기를 알고, 대입은 주소를 공유한다는 두 가지가 C와의 핵심 차이였다. 주소 공유를 직접 사고로 겪고서야 제대로 이해했다.
  • 예외 처리가 "오류를 숨기는 게 아니라 의도대로 대응한다"는 점에서, C가 그냥 넘어가던 배열 범위 초과를 자바는 잡아준다는 안정성의 차이를 느꼈다.
  • 최댓값 갱신·정수 나눗셈처럼 알고리즘에서 익힌 패턴이 언어를 옮겨도 그대로 통한다는 걸 또 확인했다.

한 걸음 더

  • 예외에는 컴파일러가 처리를 강제하는 검사 예외(checked, 파일 입출력 등)와, 강제하지 않는 비검사 예외(unchecked, 배열 범위 초과 등)가 있다. 전자는 try-catch나 throws로 반드시 처리해야 컴파일되고, 후자는 안 해도 컴파일은 되지만 안 하면 실행 중 죽는다. 자바가 "오류를 무시하지 못하게" 설계됐음을 보여준다.
  • finally는 예외가 나든 안 나든 반드시 실행되므로, 파일이나 네트워크 연결을 닫는 정리 코드를 두기에 좋다. 다만 요즘은 try-with-resources라는 더 간결한 문법이 그 역할을 대신해, 자원을 자동으로 닫아준다.
  • 예외를 너무 광범위하게(catch (Exception e)) 잡으면 진짜 문제를 가려버릴 수 있다. "예상되는 구체적 예외만 잡고, 모르는 예외는 차라리 드러나게 둔다"가 좋은 예외 처리 원칙이다. 예외 처리는 오류를 숨기는 게 아니라 의도한 대로 대응하는 것이기 때문이다.