예외처리
예외 처리(Exception Handling)는 프로그래밍에서 발생할 수 있는 예상치 못한 상황 또는 오류에 대비하는 방법입니다. 자바에서 예외는 두 가지 종류로 나뉩니다: 확인된 예외(checked exception)와 확인되지 않은 예외(unchecked exception).
- 확인된 예외(checked exception): 컴파일러가 확인할 수 있는 예외로서, 반드시 예외 처리를 해야 합니다. IOException, FileNotFoundException 등이 여기에 속합니다. 이를 처리하지 않으면 컴파일 오류가 발생합니다.
- 확인되지 않은 예외(unchecked exception): 컴파일러가 예외 처리를 강제하지 않는 예외로서, 주로 프로그래머의 실수나 프로그램의 상태에 따라 발생합니다. NullPointerException, ArrayIndexOutOfBoundsException 등이 여기에 속합니다. 이를 처리하지 않아도 컴파일 오류는 발생하지 않지만, 실행 중에 프로그램이 비정상적으로 종료될 수 있습니다.
자바에서 예외 처리는 try, catch, finally 블록을 사용하여 수행됩니다.
- try: 예외가 발생할 수 있는 코드 블록을 포함합니다.
- catch: 예외가 발생했을 때 처리할 코드 블록을 정의합니다.
finally: 예외 발생 여부에 관계 없이 항상 실행되는 코드 블록을 정의합니다.
try { // 예외가 발생할 수 있는 코드 }
catch (예외클래스1 e1) { // 예외 처리 코드 }
catch (예외클래스2 e2) { // 예외 처리 코드 }
finally { // 예외 발생 여부와 관계없이 항상 실행되는 코드 }
또한, throw 키워드를 사용하여 개발자가 직접 예외를 발생시킬 수도 있습니다. 이는 특정 조건이 충족되지 않거나 잘못된 입력 등에 대한 응답으로 사용될 수 있습니다.
예외 처리를 통해 프로그램의 안정성을 높이고, 예외 상황에 대처할 수 있는 기능을 제공할 수 있습니다.
예외 처리 클래스를 만들어서 특정 예외 상황에 대응하는 것은 자바에서 매우 흔한 일입니다. 이를 통해 사용자 정의 예외를 만들어 프로그램의 가독성을 높이고 유지 보수를 용이하게 할 수 있습니다. 여기에는 몇 단계가 있습니다:
- 예외 클래스 생성: Exception 클래스를 확장하여 새로운 예외 클래스를 만듭니다.
- 생성자 정의: 예외 클래스에 필요한 생성자를 정의하여 예외 객체를 초기화합니다.
- 예외 메시지 설정: 예외 객체에 예외 메시지를 설정하여 예외 발생 시 유용한 정보를 전달합니다.
예를 들어, 파일을 열 때 파일이 존재하지 않는 경우 예외를 발생시키는 클래스를 만들어 보겠습니다.
public class FileNotFoundException extends Exception { // 예외 클래스 생성
public FileNotFoundException(String message) { // 생성자 정의 및 예외 메시지 설정
super(message);
}
}
이제 이 예외 클래스를 사용하여 파일을 열 때 파일이 존재하지 않는 경우를 처리할 수 있습니다.
import java.io.File;
public class FileProcessor {
public void openFile(String fileName) throws FileNotFoundException {
File file = new File(fileName);
if (!file.exists()) { // 파일이 존재하지 않는 경우 fileNotFoundException 발생
throw new FileNotFoundException("File not found: " + fileName);
} // 파일이 존재하는 경우 처리 // (파일을 열거나 다른 작업을 수행할 수 있음)
}
}
위의 예제에서 openFile 메서드는 파일이 존재하지 않을 때 FileNotFoundException을 발생시킵니다. 호출하는 쪽에서는 이 예외를 처리하거나 위임할 수 있습니다.
public class Main {
public static void main(String[] args) {
FileProcessor processor = new FileProcessor();
String fileName = "example.txt";
try { processor.openFile(fileName);
}catch (FileNotFoundException e) { // 파일이 존재하지 않는 경우 처리
System.out.println("File not found: " + e.getMessage()); // 예외 처리 코드 추가 가능
}
}
}
이렇게 하면 프로그램이 파일이 존재하지 않는 경우에 대비하여 예외를 처리할 수 있습니다. 이러한 방식으로 사용자 정의 예외 클래스를 만들어 프로그램의 예외 처리를 보다 효과적으로 관리할 수 있습니다.
제네릭
제네릭(Generic)은 자바에서 타입을 파라미터화하는 기능을 말합니다. 이를 통해 클래스, 인터페이스, 메서드를 정의할 때 일반화된 타입을 사용할 수 있습니다. 제네릭을 사용하면 코드의 재사용성과 타입 안정성을 높일 수 있습니다.
제네릭을 사용하는 가장 일반적인 이유는 다음과 같습니다:
- 타입 안정성(Type Safety): 제네릭을 사용하면 컴파일러가 코드를 더 강력하게 검사하여 타입 불일치로 인한 오류를 사전에 방지할 수 있습니다. 즉, 실행 중에 발생할 수 있는 타입 관련 오류를 컴파일 시간에 잡아낼 수 있습니다.
- 코드 재사용성(Reusability): 일반화된 코드를 작성함으로써 여러 종류의 데이터 타입에 대해 동작하는 클래스, 메서드, 인터페이스를 만들 수 있습니다. 이는 코드의 재사용성을 높이고 중복을 줄여줍니다.
제네릭을 사용하는 방법은 다음과 같습니다:
- 클래스나 인터페이스에서 제네릭 타입 선언하기: 제네릭 타입은 클래스나 인터페이스 선언 시에 파라미터로 선언됩니다. 보통 대문자 알파벳으로 표기하며, 클래스 내부에서 해당 타입을 사용할 수 있습니다.
public class MyClass<T> { private T myField; public MyClass(T myField) { this.myField = myField; } public T getMyField() { return myField; } public void setMyField(T myField) { this.myField = myField; } } - 제네릭 메서드 만들기: 클래스나 인터페이스의 일반 메서드처럼, 메서드도 제네릭 타입을 사용할 수 있습니다.
public class Utils { public static <T> T findMax(T[] array) { // 배열에서 최댓값을 찾는 메서드 구현 } } - 제네릭을 사용한 인터페이스 구현하기: 인터페이스에도 제네릭을 사용하여 여러 종류의 데이터 타입을 지원할 수 있습니다.
public interface List<T> {
void add(T element);
T get(int index);
}
제네릭을 사용하면 컴파일 시간에 타입 불일치 관련 오류를 잡을 수 있으므로 프로그램의 안정성을 높이고, 코드의 재사용성을 향상시킬 수 있습니다.
람다 표현식(Lambda Expressions):
람다 표현식은 간단히 말해 익명 함수입니다. 메서드를 하나의 식(expression)으로 표현한 것입니다. 주로 함수형 인터페이스(Functional Interface)의 구현체를 생성할 때 사용됩니다.
예를 들어, 기존의 익명 내부 클래스를 사용하여 Runnable 인터페이스의 구현체를 생성하는 것과 람다 표현식을 사용하는 것을 비교해보겠습니다.
// 익명 내부 클래스를 사용한 Runnable 구현체
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello, world!");
}
};
// 람다 표현식을 사용한 Runnable 구현체
Runnable runnable = () -> System.out.println("Hello, world!");
람다 표현식은 메서드의 매개변수로 전달되거나, 메서드의 반환 값으로 사용될 수 있습니다. 또한 함수형 인터페이스의 메서드를 구현할 때 간결한 방법을 제공합니다.
스트림(Stream):
스트림은 데이터를 처리하는 연속적인 연산(operations)을 나타내는 요소들의 시퀀스입니다. 스트림은 컬렉션(Collection)과 비슷하지만, 컬렉션은 데이터를 저장하고 있지만 스트림은 데이터를 소비(consume)하고 생성할 수 있습니다. 스트림을 이용하면 데이터의 처리 과정을 선언적(Declarative)으로 표현할 수 있습니다.
스트림은 중간 연산(intermediate operations)과 최종 연산(terminal operations)으로 구성됩니다. 중간 연산은 다른 스트림을 반환하고, 최종 연산은 스트림을 소비하고 결과를 생성합니다.
List<String> myList = Arrays.asList("apple", "banana", "orange");
// 중간 연산: 문자열의 길이가 5 이상인 요소만 필터링
Stream<String> filteredStream = myList.stream().filter(s -> s.length() >= 5);
// 최종 연산: 필터링된 요소들을 출력
filteredStream.forEach(System.out::println);
위의 코드에서 filter는 중간 연산이고, forEach는 최종 연산입니다. 중간 연산을 연결하여 복잡한 데이터 처리 파이프라인을 만들 수 있으며, 최종 연산을 통해 결과를 생성합니다.
람다 표현식과 스트림을 함께 사용하면 코드를 더 간결하고 가독성이 높아지며, 병렬 처리(parallel processing)와 같은 성능 향상도 기대할 수 있습니다.