예외 처리와 C·Java·Python 코드 판독

프로그래밍 언어·객체지향 · 예외·실행 결과

이 페이지의 목차

핵심 요약

컴파일·실행·논리 오류와 예외 전파를 이해하고 C·Java·Python 코드 결과를 체계적으로 판독한다.

본문

프로그램 오류의 분류

오류발생 시점·특징예시
구문·컴파일 오류번역 단계에서 발견괄호 누락, 존재하지 않는 변수
링크 오류목적 모듈 결합 단계함수 선언은 있으나 정의를 찾지 못함
실행 오류·예외실행 중 발생0 나눗셈, 잘못된 인덱스, 파일 없음
논리 오류실행은 되지만 결과가 잘못됨경계 조건 오류, 잘못된 공식
도식
소스 코드
   │
   ├─ 컴파일 오류 ─► 수정
   ▼
목적 코드·바이트코드
   │
   ├─ 링크·로딩 오류 ─► 수정
   ▼
실행
   ├─ 예외·실행 오류 ─► 처리 또는 종료
   └─ 논리 오류 ─────► 테스트·디버깅으로 발견

컴파일러와 인터프리터

  • 컴파일러: 프로그램 단위로 번역해 목적 코드나 중간 표현을 만든다.
  • 인터프리터: 실행 과정에서 소스 또는 중간 표현을 해석한다.
  • 현대 언어 구현은 컴파일과 인터프리트, JIT를 함께 사용할 수 있으므로 둘을 완전히 배타적인 언어 분류로 단정하지 않는다.
도식
C: 소스→전처리 → 컴파일 → 어셈블 → 링크 → 실행파일Java: 소스→javac → 바이트코드 → JVM 해석·JIT → 실행Python 구현 예: 소스→바이트코드 → 가상머신 실행

Java 예외 처리

JAVA
try {
    int value = Integer.parseInt(text);
    System.out.println(100 / value);
} catch (NumberFormatException e) {
    System.out.println("숫자 형식 오류");
} catch (ArithmeticException e) {
    System.out.println("0으로 나눌 수 없음");
} finally {
    System.out.println("정리 작업");
}
도식
try 진입
  │
  ├─ 정상 완료 ────────────────┐
  │                            ▼
  └─ 예외 발생 → 일치 catch 실행
                               │
                               ▼
                          finally 실행
                               │
                               ▼
                       다음 문장 또는 전파
  • throw: 예외 객체를 직접 발생
  • throws: 메서드 선언에서 호출자에게 전파 가능성을 표시
  • 검사 예외와 비검사 예외는 컴파일러의 처리 강제 여부가 다르다.
  • finally는 일반적으로 정상·예외 경로 모두에서 실행되지만 프로세스 강제 종료 같은 특수 상황은 예외가 될 수 있다.

자원은 try-with-resources를 사용하면 역순으로 자동 닫을 수 있다.

Python 예외 처리

PYTHON
try:
    value = int(text)
    result = 100 / value
except ValueError:
    print("숫자 형식 오류")
except ZeroDivisionError:
    print("0으로 나눌 수 없음")
else:
    print(result)
finally:
    print("정리 작업")
  • raise: 예외 발생
  • except: 예외 처리
  • else: try에서 예외가 없을 때 실행
  • finally: 정리 코드
  • 너무 넓은 예외를 잡고 아무 작업도 하지 않으면 오류 원인을 숨길 수 있다.

C의 오류 처리

C에는 Java·Python과 같은 언어 차원의 예외 구문이 없다. 일반적으로 반환값, 출력 매개변수, errno, 오류 코드를 사용한다.

C
int divide(int a, int b, int *result) {
    if (result == NULL || b == 0) {
        return 0;
    }
    *result = a / b;
    return 1;
}

호출자는 반환값을 반드시 확인해야 한다.

코드 실행 결과를 푸는 순서

예시·텍스트
① 언어와 버전·전제 확인
② 변수의 자료형·초기값 기록
③ 함수 호출마다 새 지역 범위 표시
④ 괄호·우선순위·단락평가 적용
⑤ 반복별 변수 변화를 표로 작성
⑥ 객체 별칭과 변경 가능성 확인
⑦ 예외 발생 지점과 catch·finally 흐름 표시
⑧ 출력 시점과 형식을 순서대로 기록

C 코드 판독 예시

C
#include <stdio.h>

int f(int x) {
    static int total = 0;
    total += x;
    return total;
}

int main(void) {
    printf("%d ", f(2));
    printf("%d", f(3));
    return 0;
}

출력은 2 5이다. 정적 지역변수 total은 함수 호출이 끝나도 값이 유지된다.

C 코드 문제에서는 평가 순서와 부수효과가 문제되는 정의되지 않은 동작을 정상 계산 문제처럼 풀지 않는다.

Java 코드 판독 예시

JAVA
class Parent {
    int value = 1;
    int get() { return value; }
}

class Child extends Parent {
    int value = 2;
    @Override int get() { return value; }
}

Parent p = new Child();
System.out.print(p.value + " ");
System.out.print(p.get());

대표적인 결과는 1 2이다.

  • 필드 접근은 선언 타입 기준의 숨김 규칙에 영향을 받는다.
  • 재정의된 인스턴스 메서드는 실제 객체 타입에 따라 실행된다.

Python 코드 판독 예시

PYTHON
def append_value(value, items=[]):
    items.append(value)
    return items

print(append_value(1))
print(append_value(2))

기본 인수 객체는 함수 정의 시 한 번 생성되므로 출력은 [1], [1, 2]가 된다. 일반적으로는 None을 기본값으로 사용해 호출마다 새 리스트를 만든다.

디버깅과 테스트

디버깅은 오류 원인을 찾아 수정하는 과정이다.

  • 브레이크포인트와 한 단계 실행
  • 변수·호출 스택 확인
  • 로그와 추적 출력
  • 최소 재현 예제 작성
  • 정상·경계·예외 입력 테스트
  • 가정 하나씩 검증

오류가 나타난 지점과 잘못된 상태가 처음 만들어진 지점을 구분해야 한다.

예외 전파와 catch 순서

예외는 현재 블록에 일치하는 처리기가 없으면 호출 스택을 따라 바깥으로 전파된다. 상속 관계의 예외를 여러 개 잡을 때는 구체적인 하위 예외를 먼저 둔다.

JAVA
try {
    readFile();
} catch (java.io.FileNotFoundException e) {
    // 더 구체적인 처리
} catch (java.io.IOException e) {
    // 더 넓은 처리
}

상위 예외를 먼저 잡으면 뒤의 하위 예외 처리기는 도달할 수 없어 컴파일 오류가 된다.

finally의 return 함정

finally에서 return하면 try나 catch가 반환하려던 값 또는 전파하려던 예외를 덮어쓸 수 있다.

JAVA
static int f() {
    try { return 1; }
    finally { return 2; }
}

이 메서드는 2를 반환하지만 오류 원인을 숨길 수 있으므로 finally에서는 정리만 수행하고 반환·예외 덮어쓰기를 피하는 것이 좋다. Python도 finally의 return이 기존 반환이나 예외를 가릴 수 있다.

자원 닫기 순서와 억제 예외

Java의 try-with-resources는 선언의 역순으로 자원을 닫는다.

JAVA
try (A a = new A(); B b = new B()) {
    work();
} // b.close() 후 a.close()

본문 예외와 close 예외가 함께 발생하면 본문 예외가 주 예외로 전파되고 닫기 예외는 suppressed exception으로 기록될 수 있다.

Python의 with는 컨텍스트 관리자의 __enter__와 __exit__를 이용한다. __exit__가 참을 반환하면 해당 예외를 처리한 것으로 간주해 전파를 막을 수 있다.

C 동작 분류

분류의미예시
정의되지 않은 동작표준이 결과를 요구하지 않음부호 있는 정수 오버플로, 해제 메모리 역참조
구현 정의 동작구현이 한 선택을 정하고 문서화일반 char의 부호 여부
미지정 동작허용된 여러 결과 중 하나, 문서화 의무 없음일부 평가 순서 선택

정의되지 않은 동작을 특정 출력으로 묻는 문제는 타당하지 않다. 문제에서 구현·비트 폭·언어 버전을 명시했는지도 확인한다.

예외 연결과 원인 보존

Python에서는 raise NewError() from original로 새 예외와 원래 원인을 연결할 수 있다. Java에서는 예외 생성자의 cause나 initCause를 이용할 수 있다. 단순히 새 예외만 던져 원인을 버리면 장애 분석이 어려워질 수 있다.

복습 문제

답을 생각한 뒤 정답과 근거를 펼쳐 보세요.

01

Java에서 상위 예외 catch를 하위 예외 catch보다 먼저 두면 왜 문제가 되는가?

정답과 근거

정답: 뒤의 하위 예외 처리기가 도달 불가능해지기 때문이다.

상위 타입 catch가 이미 하위 예외까지 모두 잡으므로 Java 컴파일러가 오류로 판단한다.

02

try { return 1; } finally { return 2; }의 반환값은 무엇이며 왜 권장되지 않는가?

정답과 근거

정답: 2

finally의 반환이 기존 반환과 예외를 덮어쓸 수 있어 원인을 숨기므로 정리 코드만 두는 것이 좋다.

03

try-with-resources의 두 자원 A, B는 어떤 순서로 닫히는가?

정답과 근거

정답: B 후 A

자원은 선언의 역순으로 닫힌다.

04

C의 일반 char 부호 여부는 정의되지 않은 동작과 구현 정의 동작 중 어느 것인가?

정답과 근거

정답: 구현 정의 동작

구현은 선택한 부호 규칙을 문서화해야 한다.

05

Python의 raise NewError() from original은 무엇을 보존하는가?

정답과 근거

정답: 새 예외와 원래 예외의 인과관계

원인 체인을 유지하면 상위 계층에서 의미 있는 예외로 바꾸면서도 근본 원인을 추적할 수 있다.