본문 바로가기

Computer Science

[Program Execution Process] 런 타임과 컴파일 타임

728x90
런 타임과 컴파일 타임
  • 프로그래밍에서 소프트웨어 개발은 여러 단계로 나뉘며, 그 중 가장 중요한 두 가지 개념컴파일 타임(Compile Time)과 런타임(Runtime)인데, 이 두 개념은 프로그램의 실행과 오류 처리, 성능 최적화에 밀접한 관련이 있음
  • 고급 언어로 작성된 프로그램 소스 코드(Source Code)라고 하며 컴퓨터는 직접 소스 코드를 실행할 수 없기 때문에, 이를 실행 가능하도록 기계 코드(기계어, Machine Code)로 변환해야 함
    → 이 변환 과정은 컴파일러(Compiler) 또는 인터프리터(Interpreter)라는 프로그래밍 도구를 통해 수행
    • 컴파일러(Compiler) : 전체 소스 코드를 한 번에 번역한 후 실행 파일을 생성
    • 인터프리터(Interpreter) : 코드를 한 줄씩 읽고 즉시 실행
  • 즉, 소스 코드를 입력하는 것만으로는 프로그램을 실행할 수 없고 CPU에서 실행할 수 있는 명령어의 연속(기계 코드)으로 변환하는 과정이 필요
    • 컴파일(Compile) 기계 코드로 변환하는 과정이며, 이 과정에서 발생하는 오류가 컴파일 타임 오류(Compile-time Error)
    • 반면, 런타임(Runtime)프로그램이 실행되는 과정이며, 이 과정에서 발생하는 오류가 런타임 오류(Runtime Error)
  • 컴파일이 성공했다고 해서 프로그램이 반드시 올바르게 실행된다고 보장할 수 없음
    Ex) 실행 도중 잘못된 입력, 논리적 실수, 메모리 부족 등의 이유런타임 오류가 발생할 수 있음

런 타임과 컴파일 타임 비유
  • 소스 코드는 배의 청사진(설계도)과 같고 이는 배를 어떻게 만들어야 하는지 정의한 문서일 뿐임
  • 조선소에서 배를 제작하는 과정에서 설계 오류(예: 도면에 없는 부품)가 발견되면, 제작 과정에서 즉시 중지하게 되는데 이것이 컴파일 타임 오류
  • 즉, 프로그램이 실행되기 전 코드의 문법적 오류나 잘못된 참조 등이 컴파일러에 의해 탐지되는 것
  • 반면, 배가 완성되어 실제로 출항한 후 문제가 발생하면 런타임 오류
    • 배가 바다에서 엔진 결함으로 멈춤 → NullPointerException (null 객체를 참조하는 오류)
    • 배가 무거운 짐을 실어 균형을 잃고 침몰 → StackOverflowError (재귀 호출이 너무 깊어지는 오류)
  • 컴파일 타임 오류 : 프로그램 실행 전에 발생하는 문제
  • 런타임 오류 : 프로그램이 실행된 후 발생하는 문제

소프트웨어 개발의 주요 단계
  • 코딩(Coding) : 개발자가 C, Java, Python, JavaScript 등과 같은 고급 프로그래밍 언어를 사용하여 프로그램의 소스 코드를 작성하는 단계
  • 컴파일(Compile) : 작성된 소스 코드를 컴파일러가 실행 가능한 코드(기계어 코드 또는 바이트코드)로 변환하는 단계
    → 컴파일러가 코드의 구문 오류(Syntax Error)와 타입 오류(Type Error)를 검사하는 단계
  • 실행(Execution) :  변환된 코드(기계어 코드 또는 바이트코드) 실제로 실행되는 단계
    → 프로그램이 런타임 환경에서 동작하며, 예상치 못한 오류(예: 런타임 오류)가 발생할 수 있음

컴파일 타임 (Compile Time)
  • 프로그램의 소스 코드가 실행 가능한 코드(기계어 코드 또는 바이트코드)로 변환되는 시점을 의미
  • 이 과정에서 컴파일러(Compiler)는 소스 코드를 분석하여 구문 오류(Syntax Error), 타입 오류(Type Error) 등을 감지하고, 정적 분석(Static Analysis)을 수행
  • 컴파일 타임에 발생하는 오류프로그램이 실행되기 전에 해결해야 하며, 이를 통해 구문 오류나 타입 오류로 인한 실행 중 문제를 최소화할 수  있음

컴파일러의 역할
  • 컴파일러는 프로그래머가 작성한 소스 코드를 실행 가능한 코드(기계어 코드 또는 바이트코드)로 변환하는 도구
  • 컴파일러는 다음과 같은 주요 작업을 수행
  • 구문 분석(Syntax Analysis)
    • 코드의 문법적 오류를 검사
    • Ex) 잘못된 변수 선언, 세미콜론 누락 등의 구문 오류(Syntax Error) 감지
  • 의미 분석(Semantic Analysis)
    • 코드의 의미적 일관성을 검사
    • Ex) 정의되지 않은 변수 사용, 잘못된 함수 호출, 타입 불일치 등을 확인
  • 타입 체크(Type Checking)
    • 의미 분석의 일부 과정으로, 변수와 함수의 타입이 일관되게 사용되었는지 확인
    • 정적 타입 언어(C, Java 등)에서는 컴파일 타임에 수행되며, 타입 오류가 발견되면 컴파일러가 오류를 출력
  • 코드 최적화(Code Optimization)
    • 실행 속도와 메모리 사용을 개선하기 위해 중간 코드(IR) 또는 기계 코드에서 최적화 수행
    • Ex) 불필요한 연산 제거, 루프 최적화, 함수 인라이닝(Inlining) 등

컴파일 타임의 오류 발생 시점
Scanner scanner = new Scanner(System.in);
String text = scanner.nextLine();
int i = text; // 타입 불일치 오류 (Type mismatch)
  • 위 코드에서 text는 문자열(String)이지만, 이를 정수형(int) 변수 i에 직접 할당하려고 시도했기 때문에 타입 불일치 오류(Type Mismatch Error)가 발생
  • 컴파일러는 이러한 타입 불일치 문제를 컴파일 타임(Compile Time)에 감지하여 오류를 출력
  • 또한, 컴파일 타임은 개발자가 코드를 컴파일할 때 발생하는 과정으로, 컴파일러가 소스 코드를 분석하고 기계어 또는 바이트코드로 변환하는 시점을 의미

컴파일 타임의 입력과 출력
입력(Input)
  • 컴파일 타임 입력은 컴파일러가 소스 코드를 분석하고 변환하는 데 필요한 모든 데이터를 의미
  • 이 입력은 프로그램이 올바르게 변환되도록 돕고, 실행 가능한 파일을 생성하는 데 사용
  • 주요 입력 요소
    • 소스 코드(Source Code) : 개발자가 작성한 프로그램의 코드 파일 (.java, .cs, .py 등)
    • 헤더 파일(Header Files) : C/C++에서 사용하는 추가적인 코드 정보 파일 (.h)
    • 라이브러리(Libraries) : 프로그램이 의존하는 외부 코드 집합 (.dll, .lib 등)
출력(Output)
  • 컴파일 타임 출력은 컴파일러가 소스 코드를 처리한 결과물
  • 실행 가능한 파일뿐만 아니라 바이트코드, 중간 코드, 또는 오류 및 경고 메시지를 포함할 수 있음
  • 주요 출력 요소
    • 기계어 코드(Machine Code) 또는 중간 코드(Intermediate Code)
      • C/C++언어 : 실행 가능한 기계어 코드 (.exe, .o 등)를 생성
      • Java, C# 언어 : 실행을 위한 바이트코드 (.class, .dll 등)를 생성하여 가상 머신(JVM, CLR)에서 실행
    • 오류 메시지(Error Messages) : 컴파일 중 발견된 구문 오류(Syntax Error), 의미 오류(Semantic Error) 등의 메시지
    • 경고 메시지(Warning Messages) : 실행에는 문제가 없지만, 잠재적인 위험이 있는 코드에 대한 경고
  • 컴파일러는 이러한 입력을 처리하여 프로그램의 실행 파일을 생성하고, 컴파일 중 발생한 모든 오류와 경고를 보고

컴파일 타임 오류의 종류
구문 오류(Syntax Errors)
  • 코드의 문법이 잘못되었을 때 발생
  • Ex) 중괄호의 잘못된 사용, 세미콜론 누락, 잘못된 변수 선언 등이 포함
// 세미콜론(;) 누락으로 인한 구문 오류
int x = 10  // 오류 발생
System.out.println(x);
 
의미 오류(Semantic Errors)
  • 코드가 문법적으로는 맞지만, 의미적으로 잘못되었을 때 발생
  • 컴파일 타임에 감지될 수도 있지만, 일부 의미 오류는 실행 시(Run-time)에만 감지될 수도 있음
  • 대표적인 의미 오류
    • 타입 불일치 : 잘못된 타입 할당 (int y = "hello";)
    • 잘못된 연산 : 정수와 문자열을 더하려고 시도 (int z = 10 + "20";)
    • 정의되지 않은 함수 호출 : 존재하지 않는 함수 호출
// 타입 불일치로 인한 의미 오류
int y = "hello";  // 오류 발생: 정수형 변수에 문자열 할당 불가

// 존재하지 않는 함수 호출
printMessage("Hello");  // 오류 발생: printMessage 함수가 정의되지 않음
 

컴파일 타임의 최적화
  • 컴파일 타임에 수행되는 최적화 작업은 프로그램의 실행 속도를 향상시키고, 불필요한 연산을 제거하여 효율적인 실행 코드를 생성하는 데 도움을 줌
  • 컴파일러는 다음과 같은 최적화 작업을 수행할 수 있음
상수 접기(Constant Folding)
  • 컴파일러가 상수 값을 미리 계산하여 런타임에서 불필요한 연산을 줄이는 기법
  • 장점 : 실행 중 불필요한 연산을 줄여 속도를 향상시킴
// 상수 접기 (Constant Folding)
int a = 2 + 3;  // 컴파일러가 미리 계산하여 int a = 5; 로 변환
double b = Math.PI * 2;  // 런타임 계산 없이 상수 값으로 대체 가능
불필요한 코드 제거(Dead Code Elimination)
  • 컴파일러가 절대 실행되지 않는 코드를 감지하여 제거하는 최적화 기법
  • 장점 : 코드 크기를 줄이고, 불필요한 실행을 방지하여 성능을 최적화
// 루프 최적화 (Loop Optimization)
int multiplier = 2;  // 루프 밖으로 이동하여 반복적인 계산 제거
for (int i = 0; i < 1000; i++) {
    int result = i * multiplier;  // 루프 내에서 불필요한 연산 제거
}
루프 최적화(Loop Optimization)
  • 루프 내에서 반복적으로 수행되는 연산을 루프 밖으로 이동시켜 불필요한 계산을 줄이는 기법
  • 장점 : 루프 실행 속도를 빠르게 하고, CPU 연산 부담을 줄일 수 있음
// 루프 최적화 (Loop Optimization)
int multiplier = 2;  // 루프 밖으로 이동하여 반복적인 계산 제거
for (int i = 0; i < 1000; i++) {
    int result = i * multiplier;  // 루프 내에서 불필요한 연산 제거
}

컴파일 타임의 사후 조건
  • 컴파일 타임이 성공적으로 완료되었다는 것은 프로그램이 소스 코드에서 실행 가능한 기계어 코드로 변환되었음을 의미
  • 이 과정에서 컴파일러는 여러 가지 중요한 작업을 수행하며, 그 결과 다음과 같은 사후 조건이 만족되어야 함
구문 및 의미 오류가 모두 제거
  • 컴파일러는 소스 코드에 존재하는 모든 구문(Syntax) 오류와 의미(Semantics) 오류를 감지하고 수정
  • 구문 오류 : 코드 문법에 맞지 않는 부분을 의미(Ex. 변수 타입 불일치)
  • 의미 오류 : 문법적으로는 맞지만 코드가 의도한 대로 동작하지 않는 경우 (Ex.잘못된 변수 비교)
// 구문 오류 예시
int a = "Hello";  // 타입 불일치 (Syntax Error)

// 의미 오류 예시
if (a > b) {  // a와 b가 같아도 비교할 수 없다는 의미 오류
    // 처리 로직
}
실행 가능한 기계어 코드가 생성
  • 컴파일러는 소스 코드를 CPU가 실행할 수 있는 기계어로 변환
  • 이 기계어 코드는 운영 체제에서 실행 가능한 형식이어야 하며, 모든 의존성(라이브러리, 헤더 파일 등)이 적절히 연결된 상태여야 함
    Windows : .exe 파일
    Linux : 실행 가능한 바이너리 파일
모든 의존성(Dependencies)이 적절히 해결
  • 컴파일러는 프로그램이 의존하는 라이브러리와 외부 파일들이 올바르게 연결되었음을 확인
  • 의존성이 해결되지 않으면 컴파일러는 오류를 발생시키고 프로그램은 실행되지 않음
  • 외부 라이브러리가 정상적으로 포함되어 있어야 하며, 의존성이 자동으로 관리되는 경우가 많음
    Ex) Maven, Gradle
컴파일러가 요구하는 최적화가 수행되었다
  • 컴파일러는 프로그램 성능을 향상시키기 위한 다양한 최적화 작업을 수행
  • Ex) 불필요한 연산 제거나 루프 최적화, 인라인 함수 적용 등
    • 인라인 함수 최적화 : 자주 호출되는 함수의 호출을 줄여 성능 향상
    • 루프 최적화 : 불필요한 루프 내 연산 제거
디버깅 정보가 포함
  • 디버깅 모드에서 컴파일을 수행한 경우 디버깅 정보가 포함된 상태로 컴파일이 완료
  • 이 정보는 소스 코드와 기계어 코드 매핑을 통해 오류를 추적하고 디버깅을 쉽게 함
  • Ex) Visual Studio에서 .pdb 파일을 포함하여, 디버깅 정보가 프로그램에 포함
정적 분석 도구를 통한 코드 품질 검증이 완료
  • 컴파일 타임에 정적 분석 도구가 사용되면 코드 품질을 높이기 위한 다양한 분석이 수행
  • SonarQube와 같은 도구를 사용하면 버그, 보안 취약점, 성능 문제 등을 사전에 예방 가능
  • Ex) SonarQube를 사용해 코드 스멜을 감지하고 수정 가능

컴파일링과 인터프리팅
컴파일링(Compiling)의 정의
  • 컴파일링은 소스 코드를 기계어 코드로 변환하는 과정
  • 컴파일된 프로그램은 독립 실행 파일로 생성되어 실행되며, 실행 속도가 빠름
  • 컴파일러는 소스 코드를 한 번에 분석하고 번역하여 실행 파일을 만들고 이후 실행 시 추가적인 번역이 필요하지 않음
  • Ex) C, C++, Rust 등의 언어는 컴파일링을 통해 기계어 코드로 변환
인터프리팅(Interpreting)의 정의
  • 인터프리팅은 소스 코드를 실행하는 동시에 번역하는 과정
  • 인터프리터는 소스 코드를 한 줄씩 읽어 실행하며 코드를 즉시 실행
  • 인터프리팅된 프로그램은 일반적으로 컴파일된 프로그램보다 실행 속도가 느리지만, 코드 수정이 쉽고 디버깅이 용이
  • Ex) Python, Ruby, JavaScript 등의 언어는 인터프리터를 사용하여 실행
컴파일링과 인터프리팅의 차이
특징 컴파일링 인터프리팅
소스 코드 변환 소스 코드가 실행 파일로 변환 소스 코드가 즉시 실행
실행 속도 빠름 상대적으로 느림
오류 감지 실행 전 오류 감지 실행 중 오류 감지
디버깅 디버깅이 어렵고 수정이 복잡함 디버깅과 수정이 쉬움
하이브리드 접근
  • Java와 같은 언어컴파일과 인터프리팅의 하이브리드 접근을 사용
  • Java 프로그램은 먼저 바이트코드(bytecode)로 컴파일된 후바이트코드는 자바 가상 머신(JVM)에서 실행
  • JVM은 바이트코드를 인터프리팅하거나, Just-In-Time(JIT) 컴파일러를 사용하여 즉시 실행 가능
  • 이를 통해 컴파일된 코드의 성능 인터프리팅 코드의 유연성을 모두 제공
  • Ex) Java, C# 등의 언어는 이와 같은 하이브리드 접근을 사용

런 타임 (Run Time)
  • 프로그램이 실제로 실행되는 단계프로그램의 논리적 흐름이 실행 환경에서 수행
  • 런타임 동안 프로그램은 사용자가 입력한 데이터를 처리하고, 결과를 출력하며, 다양한 시스템 자원(파일, 메모리, 네트워크 등)을 활용하여 작업을 수행

런 타임의 중요성
  • 프로그램이 실제로 동작하고 사용자와 상호작용하는 단계성능과 안정성에 직접적인 영향을 미침
  • 런타임 동안 발생하는 오류는 실제 사용자 경험에 큰 영향을 미치며, 프로그램이 예기치 않게 종료되거나 비정상적으로 작동하면 심각한 문제를 초래할 수 있음
  • 따라서 런타임에서의 오류 처리프로그램의 품질과 안정성을 높이는 데 매우 중요

런타임의 오류 발생 시점
Scanner scan = new Scanner(System.in);
String input = scan.nextLine();
int value = Integer.parseInt(input);
  • 위 코드는 Scanner 객체를 이용하여 사용자가 입력한 값을 받고 Integer.parseInt() 메서드를 사용하여 이 문자열을 정수로 변환하려고 시도하지만 사용자가 입력한 값에 따라 결과가 달라짐
  • 사용자가 "123"을 입력하면 Integer.parseInt("123")는 정상적으로 정수 123을 반환
  • 사용자가 "abc"와 같은 문자열을 입력하면 Integer.parseInt("abc")는 NumberFormatException 예외를 발생
  • 이러한 예외는 오직 런타임에 발생하는데, 이는 사용자가 프로그램을 실행하고 실제로 입력한 값에 의해 결정되기 때문
  • 런타임 오류
    • 위 코드에서 Integer.parseInt(input) 메서드는 사용자가 입력한 값이 정수로 변환될 수 있는지 여부에 따라 성공하거나 실패
    • "123"은 성공적으로 변환되지만, "abc"와 같은 문자열은 런타임에서 예외를 발생
  • 런타임에서만 확인 가능
    • 이런 종류의 오류는 프로그램이 실행될 때 즉, 사용자가 입력한 값에 따라 런타임에서만 발생할 수 있으며, 컴파일 타임에서는 발견되지 않음
    • 이 점에서 런타임 오류는 컴파일 타임에 미리 예측하거나 방지할 수 없는 부분
  • 런타임 사용자가 프로그램을 실행하는 시간대이며, 이 시간 동안 사용자의 입력이나 시스템 상태에 따라 다양한 오류가 발생할 수 있음
  • 따라서 런타임 오류는 코드 실행 중에만 발생하며, 이를 처리하기 위해 예외 처리가 필수적

런타임의 입력과 출력
입력(Input)
  • 런타임 입력프로그램이 실행되는 동안 외부로부터 수신하는 모든 데이터를 의미
  • 이러한 입력은 프로그램의 동작을 동적으로 결정하며, 여러 가지 소스에서 발생할 수 있음
    • 사용자 입력 (키보드, 마우스 등)
    • 파일 데이터 (읽기/쓰기)
    • 네트워크 데이터 (HTTP 요청 등)
출력(Output)
  • 런타임 출력프로그램이 처리한 결과를 외부로 내보내는 모든 데이터를 의미
  • 이러한 출력은 프로그램이 사용자 또는 다른 시스템과 상호작용하는 방식으로, 프로그램의 성능과 사용자 경험에 영향을 미침
    • 화면 출력 (UI, 콘솔 등)
    • 파일 저장 (로그 파일, 데이터 파일 등)
    • 네트워크 전송 (HTTP 응답 등)

런타임 오류의 유형
  • 런타임 오류프로그램이 실행되는 동안 발생하며, 프로그램이 예상한 대로 동작하지 않거나 비정상적으로 종료되는 원인이 됨
  • 런타임 오류는 컴파일러에 의해 사전에 감지되지 않기 때문에, 프로그램이 실제로 실행되는 환경에서만 발생
  • 이러한 오류를 이해하고 적절히 처리하는 것프로그램의 안정성과 사용자 경험에 중요한 영향을 미침
0으로 나누기 오류 (Division by Zero)
  • 프로그램이 숫자를 0으로 나누려고 시도할 때 발생하는 오류
  • 수학적으로 정의되지 않은 연산이므로, 프로그램이 비정상적으로 종료될 수 있음
  • 결과 : 프로그램은 ArithmeticException을 발생시키고 종료
    → 대부분의 언어에서 이 오류는 ArithmeticException과 같은 특정 예외로 처리
int a = 10;
int b = 0;
int result = a / b;  // 0으로 나누는 오류 발생
널 포인터 역참조 (Dereferencing a Null Pointer)
  • 프로그램이 null로 설정된 객체를 참조하려고 할 때 발생하는 오류
  • 이는 존재하지 않는 메모리 주소를 참조하려는 시도로 인해 프로그램이 중단될 수 있음
  • 결과 : NullPointerException이 발생하며, 프로그램이 비정상적으로 종료
String str = null;
System.out.println(str.length());  // NullPointerException 발생
메모리 부족 오류 (Out of Memory)
  • 프로그램이 메모리를 과도하게 사용하여 시스템의 가용 메모리가 부족해졌을 때 발생하는 오류
  • 주로 대규모 데이터 처리나 메모리 누수(memory leak)로 인해 발생할 수 있음
  • 결과 : OutOfMemoryError가 발생하며, 프로그램은 메모리 부족으로 인해 작업을 계속할 수 없음
int[] largeArray = new int[Integer.MAX_VALUE];  // 메모리 부족 오류 발생
배열 인덱스 초과 (Array Index Out of Bounds)
  • 배열의 유효 범위를 벗어난 인덱스에 접근하려고 시도할 때 발생하는 오류
  • 결과 : ArrayIndexOutOfBoundsException이 발생하며, 프로그램이 비정상적으로 종료될 수 있음
int[] array = new int[5];
int value = array[10];  // 배열 범위를 벗어난 접근 시도
형식 변환 오류 (Type Conversion Error)
  • 잘못된 형식 간의 변환을 시도할 때 발생하는 오류
  • Ex) 문자열을 정수로 변환하려고 할 때 문자열의 내용이 숫자가 아닌 경우 오류가 발생할 수 있음
  • 결과 : NumberFormatException이 발생하며, 변환이 실패
String str = "abc";
int number = Integer.parseInt(str);  // 형식 변환 오류 발생
파일 또는 네트워크 접근 실패 (File/Network Access Failure)
  • 프로그램이 파일이나 네트워크 자원에 접근하려고 시도할 때 해당 자원이 존재하지 않거나 접근 권한이 없을 때 발생하는 오류
  • 결과: FileNotFoundException 또는 IOException이 발생하며, 자원 접근이 실패
import java.io.*;

public class Main {
    public static void main(String[] args) {
        String path = "non_existent_file.txt";
        try {
            String content = new String(Files.readAllBytes(Paths.get(path)));  // 파일 접근 실패로 인한 오류 발생
        } catch (IOException e) {
            System.out.println(e.getMessage());  // FileNotFoundException이나 IOException 발생
        }
    }
}
스택 오버플로우 (Stack Overflow)
  • 재귀 호출이 너무 깊어져 호출 스택이 가득 찬 경우 발생하는 오류
  • 무한 재귀 호출로 인해 발생
  • 결과: StackOverflowError가 발생하며, 프로그램이 중단
public class Main {
    public static void recursiveFunction() {
        recursiveFunction();  // 무한 재귀 호출로 인해 스택 오버플로우 발생
    }

    public static void main(String[] args) {
        recursiveFunction();
    }
}
데드락 (Deadlock)
  • 두 개 이상의 스레드가 서로 다른 자원의 잠금을 기다리면서 영원히 대기 상태에 빠질 때 발생하는 오류
  • 결과: 프로그램은 멈추고, 스레드는 서로의 잠금을 기다리며 영원히 대기 상태에 머무르게 됨
public class Main {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                synchronized (resource2) {
                    System.out.println("Thread 1");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                synchronized (resource1) {
                    System.out.println("Thread 2");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

런타임 오류 처리 (Runtime Error Handling)
  • 런타임 오류를 처리하는 주요 방법 = 예외 처리 (Exception Handling)
  • 예외 처리는 프로그램이 런타임 오류를 감지하고 이를 적절히 처리하여 비정상 종료를 방지할 수 있도록 도움
예외 처리 블록 (Try-Catch)
  • 예외 처리 블록프로그램이 특정 코드에서 발생할 수 있는 예외를 포착하고 처리할 수 있게 함
  • 이를 통해 프로그램은 비정상 종료를 피하고 사용자에게 오류 메시지를 제공하거나 오류를 수정할 기회를 제공할 수 있음
  • 아래의 예제에서는 ArithmeticException을 사용하여 0으로 나누는 오류를 처리
try {
    int a = 10;
    int b = 0;
    int result = a / b;  // 예외 발생 가능성 (0으로 나누기)
} catch (ArithmeticException e) {
    System.out.println("오류: 0으로 나눌 수 없습니다.");
}
최종 처리 블록 (Finally)
  • finally 블록예외 발생 여부와 관계없이 항상 실행되는 코드를 포함
  • 주로 자원 해제나 정리 작업을 수행하는 데 사용
  • finally 블록은 예외가 발생하더라도 반드시 실행
try {
    // 파일 열기 및 처리
} catch (Exception e) {
    System.out.println("오류 발생: " + e.getMessage());
} finally {
    // 파일 닫기 등 정리 작업
    System.out.println("정리 작업을 수행합니다.");
}
예외 전파 (Exception Propagation)
  • 예외는 발생한 위치에서 처리되지 않으면 호출 스택을 따라 상위 메서드로 전파
  • 이를 통해 상위 메서드에서 예외를 처리하거나, 마지막에 글로벌 예외 처리기에서 처리할 수 있음
  • 아래 예제에서는 methodB에서 발생한 예외가 methodA를 통해 전파되어, main에서 예외가 처리되는 과정을 보여줌
public void methodA() throws Exception {
    methodB();  // 예외 전파
}

public void methodB() throws Exception {
    throw new Exception("예외 발생");
}

public static void main(String[] args) {
    try {
        new ExceptionHandlingExample().methodA();
    } catch (Exception e) {
        System.out.println("메인에서 처리된 예외: " + e.getMessage());
    }
}
사용자 정의 예외 (Custom Exceptions)
  • 특정 도메인에 맞는 사용자 정의 예외를 만들어 프로그램의 오류 처리 로직을 더욱 세분화하고 관리 가능
  • 아래 예제에서는 InvalidTransactionException이라는 사용자 정의 예외를 만들어, 거래 금액이 음수일 경우예외를 발생
// 사용자 정의 예외
public class InvalidTransactionException extends Exception {
    public InvalidTransactionException(String message) {
        super(message);
    }
}

public void processTransaction(double amount) throws InvalidTransactionException {
    if (amount < 0) {
        throw new InvalidTransactionException("거래 금액은 음수일 수 없습니다.");
    }
    // 거래 처리 코드
}

런타임 최적화
  • 런타임에서의 성능 최적화는 프로그램이 사용자에게 더 나은 경험을 제공할 수 있도록 도움
  • 런타임 최적화는 주로 다음과 같은 방식으로 이루어짐
메모리 관리 최적화
  • 가비지 컬렉션(GC)메모리 누수를 방지하는 중요한 메커니즘
  • 그러나 불필요한 객체 생성과 자원의 적절한 해제가 중요
  • 자바에서는 가비지 컬렉션이 자동으로 처리하지만 메모리 누수를 방지하려면 객체가 더 이상 필요 없을 때 참조를 명시적으로 null로 설정하거나, 자원 해제가 필요한 경우 try-with-resources와 같은 방식으로 자원을 관리
알고리즘 최적화
  • 알고리즘 최적화에서 중요한 점은 시간 복잡도뿐만 아니라 공간 복잡도(메모리 사용)를 고려하는 것
  • 알고리즘의 효율성을 판단할 때 O(n log n) 알고리즘이 O(n^2) 알고리즘보다 빠른 경우가 많지만 때로는 메모리 사용량이 증가할 수 있으므로 성능뿐만 아니라 자원 사용도 함께 고려해야 함
    • 이진 탐색(Binary Search)은 정렬된 리스트에서 O(log n) 시간 복잡도를 가지며 매우 효율적
    • 다이나믹 프로그래밍그리디 알고리즘은 문제에 따라 효율적인 해결책을 제공.
I/O 최적화
  • I/O 최적화는 중요하지만 구체적으로 I/O 병목 현상을 피하기 위해 몇 가지 추가적인 최적화 기법을 고려할 수 있음
  • 버퍼링을 사용 : 자주 발생하는 I/O 작업에서는 버퍼링을 통해 성능을 향상시킬 수 있음
    Ex) BufferedReader와 BufferedWriter는 I/O 성능을 크게 개선할 수 있음
  • 비동기 I/O : 파일 시스템이나 네트워크와의 상호작용에서 비동기 방식이나 멀티스레딩을 통해 처리 성능을 향상시킬 수 있음
  • 압축 : 전송되는 데이터를 압축하여 전송 크기를 줄이면 네트워크 성능을 최적화할 수 있음

런 타임의 사후 조건
  • 런 타임이 성공적으로 완료되었다는 것 프로그램이 정상적으로 실행되어 모든 작업을 완료하고, 예상한 대로 종료되었음을 의미
  • 런 타임 동안 발생할 수 있는 다양한 상황을 고려하여, 프로그램이 종료될 때 다음과 같은 사후 조건이 충족되어야 함
모든 입력 데이터가 정상적으로 처리
  • 프로그램이 런타임 동안 받은 모든 입력 데이터가 예상대로 처리되었고, 처리 과정에서 오류나 예외가 발생하지 않았음을 의미
  • 이는 사용자 입력, 파일 입력, 네트워크 데이터 등이 포함
  • Ex) 사용자가 입력한 데이터를 기반으로 보고서가 정상적으로 생성되고 저장된 경우
Scanner scanner = new Scanner(System.in);
System.out.print("이름을 입력하세요: ");
String name = scanner.nextLine();
System.out.println("입력한 이름: " + name);
모든 자원이 적절히 해제
  • 프로그램이 사용한 모든 시스템 자원(메모리, 파일 핸들, 네트워크 소켓 등)이 적절히 해제되었음을 의미
  • 이는 메모리 누수나 자원 고갈을 방지하는 중요한 요소
  • 파일을 사용한 후 파일 핸들이 제대로 닫혔는지 확인
import java.io.*;

try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}
// BufferedReader가 자동으로 닫히므로 자원 해제가 보장됩니다.
프로그램이 정상적으로 종료
  • 프로그램이 런타임 동안 모든 작업을 완료하고, 예외 없이 정상적으로 종료되었음을 의미
  • 정상 종료는 프로그램이 계획한 모든 절차를 완료했음을 나타냄
  • Ex) 프로그램 종료 시 0을 반환하여 정상 종료된 경우
public class Main {
    public static void main(String[] args) {
        System.out.println("프로그램 시작");
        // 기타 로직
        System.exit(0);  // 정상 종료
    }
}
데이터가 안전하게 저장
  • 프로그램이 생성하거나 수정한 모든 데이터가 안전하게 저장되었고, 데이터 손실이나 손상이 발생하지 않았음을 의미
  • 이는 프로그램의 신뢰성을 보장하는 중요한 요소
  • Ex) 파일에 데이터를 저장하고, 데이터 손상이 없도록 보장하는 코드.
import java.io.*;

try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
    writer.write("데이터 저장 완료");
} catch (IOException e) {
    e.printStackTrace();
}
로그와 기록이 완전하게 남음
  • 프로그램의 실행 과정에서 발생한 모든 중요한 이벤트가 로그 파일이나 기록 시스템에 완전히 저장되었음을 의미
  • 이를 통해 프로그램의 동작을 추적하고, 문제가 발생한 경우 원인을 분석 가능
  • Ex) 프로그램에서 로그를 기록하는 코드
import java.util.logging.*;

public class LoggingExample {
    private static final Logger logger = Logger.getLogger(LoggingExample.class.getName());

    public static void main(String[] args) {
        try {
            logger.info("프로그램 시작");
            // 기타 로직
            logger.info("프로그램 종료");
        } catch (Exception e) {
            logger.severe("오류 발생: " + e.getMessage());
        }
    }
}
에러 처리 및 예외 관리가 성공적으로 이루어짐
  • 런타임 동안 발생한 모든 예외 상황이 적절히 처리되었고, 프로그램이 비정상적으로 종료되지 않았음을 의미
  • 예외 처리 메커니즘이 효과적으로 작동하여 시스템의 안정성을 유지할 수 있어야 함
  • Ex) 네트워크 오류가 발생했으나, 이를 처리하고 프로그램이 정상 종료된 경우
try {
    // 네트워크 요청 코드
    throw new IOException("네트워크 오류 발생");
} catch (IOException e) {
    System.out.println("오류 발생: " + e.getMessage());
} finally {
    System.out.println("정상 종료");
}
사용자 인터페이스(UI)가 올바르게 종료
  • 프로그램의 사용자 인터페이스가 올바르게 종료되었고, 모든 UI 요소가 적절히 정리되었음을 의미
  • 이는 특히 GUI 기반 프로그램에서 중요한 요소
  • GUI 애플리케이션에서 창을 닫을 때 발생하는 이벤트 처리
import javax.swing.*;

public class CloseExample {
    public static void main(String[] args) {
        JFrame frame = new JFrame("종료 예시");
        frame.setSize(300, 200);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}
성능 및 응답 시간이 예상 범위 내에서 유지
  • 프로그램이 런타임 동안 예상된 성능을 유지했고, 응답 시간이 허용 가능한 범위 내에서 유지되었음을 의미
  • 이는 사용자 경험을 최적화하는 데 중요한 요소
  • Ex) 웹 애플리케이션의 성능 테스트에서 예상된 시간 내에 요청을 처리한 경우
// 성능 테스트 코드 예시
long startTime = System.currentTimeMillis();
// 처리할 작업
long endTime = System.currentTimeMillis();
System.out.println("응답 시간: " + (endTime - startTime) + "ms");
  • 이러한 사후 조건들이 충족되었다면 런타임이 성공적으로 완료되었다고 할 수 있음
  • 프로그램이 예상대로 동작했으며 사용자가 프로그램을 사용할 때 예상치 못한 오류나 문제를 경험하지 않았음을 나타냄

사용자 경험이란?
  • 사용자가 특정 제품이나 서비스를 사용할 때 겪는 전체적인 경험을 의미
  • 이는 제품을 사용하면서 느끼는 직관성, 만족도, 편리함, 그리고 제품이 제공하는 기능들이 얼마나 효과적으로 작동하는지 등에 영향을 받음
  • 사용자 경험은 단순히 기능만을 의미하는 것이 아니라 사용자가 제품을 처음 접했을 때부터 마지막으로 사용을 마친 후까지의 모든 과정을 포함
  • Ex) 웹사이트에서 로그인할 때
    • 페이지 로딩이 빠르고, 버튼이 직관적으로 배치되어 있다면 사용자 경험이 좋다고 할 수 있음
    • 반대로, 페이지가 너무 느리거나 복잡한 UI 요소들이 있다면 사용자 경험은 나쁘다고 평가될 수 있음
  • 즉, 사용자 경험제품을 사용할 때의 편리함, 효율성, 즐거움 등을 포함하는 개념
  • 이를 개선하기 위해서는 사용자가 불편하거나 혼란을 느끼는 요소들을 찾아내어 해결하는 것이 중요

참고 자료

https://velog.io/@clydehan/%EB%9F%B0-%ED%83%80%EC%9E%84Runtime%EA%B3%BC-%EC%BB%B4%ED%8C%8C%EC%9D%BC-%ED%83%80%EC%9E%84Compile-Time-%EC%99%84%EB%B2%BD%ED%95%98%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0#%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%99%80-%EB%9F%B0%ED%83%80%EC%9E%84%EC%BB%B4%ED%8C%8C%EC%9D%BC-%ED%83%80%EC%9E%84

 

런 타임(Runtime)과 컴파일 타임(Compile Time) 완벽하게 이해하기

런 타임, 컴파일 타임 완벽 정복

velog.io

 

728x90