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) : 실행에는 문제가 없지만, 잠재적인 위험이 있는 코드에 대한 경고
- 기계어 코드(Machine Code) 또는 중간 코드(Intermediate Code)
- 컴파일러는 이러한 입력을 처리하여 프로그램의 실행 파일을 생성하고, 컴파일 중 발생한 모든 오류와 경고를 보고
컴파일 타임 오류의 종류
구문 오류(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 요소들이 있다면 사용자 경험은 나쁘다고 평가될 수 있음
- 즉, 사용자 경험은 제품을 사용할 때의 편리함, 효율성, 즐거움 등을 포함하는 개념
- 이를 개선하기 위해서는 사용자가 불편하거나 혼란을 느끼는 요소들을 찾아내어 해결하는 것이 중요
참고 자료
런 타임(Runtime)과 컴파일 타임(Compile Time) 완벽하게 이해하기
런 타임, 컴파일 타임 완벽 정복
velog.io
728x90
'Computer Science' 카테고리의 다른 글
[Software Development] 워터폴 방법론과 애자일 방법론 (0) | 2025.02.19 |
---|---|
[Software Architecture] 도메인 주도 설계(Domain-Driven Design, DDD) (0) | 2025.02.19 |
[Third Party] 서드 파티의 개념 (1) | 2024.12.16 |
[Cloud Service] 클라우드 서비스의 개념 (3) | 2024.12.15 |
[Software Engineering] CBD 방법론 (1) | 2024.12.03 |