1.1.2 자바 고급 개념
고급 개념
자바의 고급 개념들은 프로그램의 신뢰성, 유지보수성, 성능을 향상시키고, 더 복잡한 문제를 해결할 수 있게 도와줍니다.
예외 처리
자바에서 예외 처리는 프로그램 실행 중에 발생할 수 있는 예상치 못한 상황(예외)에 대응하기 위한 메커니즘입니다. 예외 처리를 통해 프로그램의 비정상 종료를 막고, 예외 상황을 안전하게 처리할 수 있습니다.
try, catch, finally 블록을 사용한 예외 처리
- try 블록: 예외 발생 가능성이 있는 코드를 포함합니다. try 블록 내에서 예외가 발생하면, 즉시 실행이 중단되고 해당 예외를 처리할 수 있는 catch 블록으로 제어가 이동합니다.
- catch 블록: try 블록에서 발생한 예외를 처리합니다. 하나의 try 블록에 여러 개의 catch 블록이 올 수 있으며, 각기 다른 타입의 예외를 처리할 수 있습니다. 예외 타입이 매치되는 catch 블록이 실행됩니다.
- finally 블록: 예외 발생 여부와 상관없이 실행되어야 하는 코드를 포함합니다. 주로 자원을 해제하거나 정리하는 코드를 작성합니다. finally 블록은 선택적으로 사용할 수 있으나, try 블록이 실행된 후에는 반드시 실행됩니다.
try {
// 예외 발생 가능성이 있는 코드
int result = 10 / 0; // ArithmeticException 발생
} catch (ArithmeticException e) {
// ArithmeticException 예외 처리 코드
System.out.println("0으로 나눌 수 없습니다.");
} finally {
// 예외 발생 여부와 관계없이 항상 실행될 코드
System.out.println("예외 처리 완료.");
}
사용자 정의 예외
자바에서는 표준 예외 클래스 외에도 사용자가 직접 예외 클래스를 정의할 수 있습니다. 사용자 정의 예외를 만들기 위해서는 Exception
클래스 또는 RuntimeException
클래스를 상속받아야 합니다. 사용자 정의 예외는 특정 비즈니스 로직에서만 발생하는 예외 상황을 나타낼 때 유용합니다.
사용자 정의 예외 클래스를 만들 때는 주로 다음 두 가지 생성자를 정의합니다:
- 기본 생성자
- 에러 메시지를 매개변수로 받는 생성자
public class MyException extends Exception {
// 기본 생성자
public MyException() {
super();
}
// 에러 메시지를 매개변수로 받는 생성자
public MyException(String message) {
super(message);
}
}
사용자 정의 예외는 특정 조건에서 throw
키워드를 사용하여 명시적으로 발생시킬 수 있습니다.
public void checkAge(int age) throws MyException {
if (age < 18) {
throw new MyException("나이가 18세 미만입니다.");
} else {
System.out.println("나이 확인 완료.");
}
}
이처럼 예외 처리는 프로그램의 안정성과 신뢰성을 높이는 중요한 기능입니다. 사용자 정의 예외를 포함하여 적절한 예외 처리 로직을 구현함으로써, 예외 상황에 대비한 견고한 애플리케이션을 개발할 수 있습니다.
멀티스레딩
멀티스레딩은 프로그램이나 프로세스 내에서 여러 스레드를 동시에 실행하는 기능입니다. 이를 통해 자원을 효율적으로 사용하고, 프로그램의 반응 시간을 개선할 수 있습니다. 하지만 여러 스레드가 동시에 같은 자원에 접근하려고 할 때 동시성 문제가 발생할 수 있습니다.
스레드 생성과 관리
자바에서 스레드를 생성하고 관리하는 방법은 크게 두 가지입니다: Thread
클래스를 상속받거나, Runnable
인터페이스를 구현하는 것입니다.
Thread
클래스 상속public class MyThread extends Thread { public void run() { System.out.println("Thread is running."); } } MyThread t1 = new MyThread(); t1.start();
Thread
클래스를 상속받아run
메소드를 오버라이드하고, 스레드 객체를 생성한 후start
메소드를 호출하여 스레드를 실행합니다.Runnable
인터페이스 구현public class MyRunnable implements Runnable { public void run() { System.out.println("Thread is running."); } } Thread t1 = new Thread(new MyRunnable()); t1.start();
Runnable
인터페이스를 구현하는 클래스를 정의하고, 그 인스턴스를Thread
클래스의 생성자에 전달하여 스레드를 실행합니다.
동시성 문제와 그 해결 방안
동시성 문제는 여러 스레드가 동시에 같은 데이터에 접근할 때 데이터가 예상치 않게 변경되어 일관성을 잃는 문제입니다. 이를 해결하기 위한 몇 가지 방법은 다음과 같습니다:
- 동기화(Synchronization) 자바에서는
synchronized
키워드를 사용하여 특정 객체나 메소드에 대한 접근을 한 스레드씩 순차적으로 제한할 수 있습니다.public synchronized void increment() { // 공유 데이터에 대한 쓰기 작업 }
메소드나 코드 블록에
synchronized
를 사용하여 동시에 하나의 스레드만 접근할 수 있도록 합니다. - 락(Locks)
java.util.concurrent.locks
패키지는 명시적 락을 제공합니다.ReentrantLock
같은 클래스를 사용하면 락을 보다 세밀하게 제어할 수 있습니다.ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 공유 자원에 대한 작업 } finally { lock.unlock(); }
-
동시성 컬렉션
java.util.concurrent
패키지는 동시성 문제를 해결하는 다양한 컬렉션을 제공합니다. 예를 들어,ConcurrentHashMap
,CopyOnWriteArrayList
등은 내부적으로 스레드 안전성을 보장합니다. - 스레드 풀 스레드 풀을 사용하여 생성할 수 있는 스레드의 수를 제한하고, 작업을 효율적으로 관리할 수 있습니다.
ExecutorService
를 사용하여 스레드 풀을 생성하고 관리할 수 있습니다.
멀티스레딩 환경에서 동시성 문제를 관리하는 것은 어플리케이션의 안정성과 성능에 매우 중요합니다. 위에서 소개한 방법들을 적절히 활용하여 동시성 문제를 해결할수 있습니다.
제네릭스
제네릭스는 자바에서 타입(type)을 파라미터로 사용할 수 있게 해주는 기능으로, 컴파일 시간에 타입 안전성을 제공하고, 코드의 재사용성을 향상시킵니다. 제네릭스를 사용하면 다양한 타입의 객체를 다루는 단일 메소드나 클래스를 작성할 수 있습니다.
타입 안전성을 높이고 코드 재사용성을 개선하기 위한 제네릭스 사용법
- 클래스와 인터페이스에 제네릭스 적용하기
클래스나 인터페이스를 정의할 때 타입 파라미터를 사용하여 제네릭스를 적용할 수 있습니다. 이를 통해 인스턴스를 생성할 때 구체적인 타입을 지정할 수 있습니다.
public class Box<T> {
private T t; // T 타입의 객체를 위한 참조 변수
public void set(T t) { this.t = t; } // T 타입의 객체를 설정
public T get() { return t; } // T 타입의 객체를 반환
}
여기서 T
는 타입 파라미터로, 실제 클래스의 인스턴스를 생성할 때 구체적인 타입으로 대체됩니다.
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(10);
Integer someInteger = integerBox.get();
- 메소드에 제네릭스 적용하기
제네릭 메소드를 정의할 때는 메소드 반환 타입 앞에 타입 파라미터를 선언합니다. 이를 통해 다양한 타입에 대해 유연하게 메소드를 사용할 수 있습니다.
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
이 메소드는 어떤 타입의 배열이든 받아서 그 요소를 출력할 수 있습니다.
Integer[] integerArray = { 1, 2, 3, 4, 5 };
String[] stringArray = { "Hello", "World" };
printArray(integerArray);
printArray(stringArray);
- 제네릭 타입 제한하기
특정 타입의 서브클래스만을 타입 파라미터로 받고 싶을 때는 타입 파라미터에 extends 키워드를 사용하여 타입 경계를 지정할 수 있습니다.
public class Box<T extends Number> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
이제 Box
클래스는 Number
클래스 또는 그 서브클래스(Integer
, Double
등)의 인스턴스만을 받을 수 있습니다.
제네릭스를 사용함으로써 타입 안전성을 높이고, 타입 변환에 대한 번거로움 없이 코드를 재사용할 수 있게 됩니다. 컴파일 시간에 타입 체크를 할 수 있기 때문에, 런타임에 발생할 수 있는 ClassCastException
과 같은 예외를 미리 방지할 수 있습니다.
스트림 API와 람다 표현식
자바 8부터 추가된 스트림 API와 람다 표현식은 자바에서 함수형 프로그래밍을 가능하게 하는 중요한 기능들입니다. 이들은 코드를 더 간결하고 읽기 쉽게 만들며, 컬렉션 처리를 위한 풍부한 연산을 제공합니다.
스트림 API
스트림(Stream) API는 데이터 컬렉션 처리를 위한 추상화된 연속된 데이터 항목들의 모임입니다. 스트림 API를 사용하면 데이터를 선언적으로 처리할 수 있으며, 병렬 처리를 지원하여 성능을 향상시킬 수 있습니다.
스트림의 주요 특징:
- 생성: 스트림은 컬렉션, 배열, I/O 자원 등 다양한 데이터 소스로부터 생성될 수 있습니다.
- 중간 연산: 스트림은 필터링, 매핑, 정렬 등 데이터를 변환하는 중간 연산을 지원합니다. 중간 연산은 여러 개를 연결하여 체이닝할 수 있습니다.
- 종단 연산: 결과를 도출하는 연산으로, 스트림을 소비하여 결과를 반환하거나 부작용을 일으킵니다. 예를 들어,
forEach
,collect
,reduce
등이 있습니다.
스트림 API 예제:
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 필터링과 forEach 종단 연산을 사용한 예제
long count = strings.stream().filter(string -> !string.isEmpty()).count();
// 매핑, 정렬, 그리고 출력
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
람다 표현식을 이용한 함수형 프로그래밍
람다 표현식(Lambda Expression)은 간결한 방식으로 익명 함수를 표현할 수 있게 해주는 기능입니다. 람다 표현식을 사용하면 인터페이스의 추상 메소드를 간단히 구현할 수 있으며, 스트림 API와 함께 사용될 때 강력한 데이터 처리 연산을 구현할 수 있습니다.
람다 표현식의 주요 특징:
- 간결성: 람다 표현식은 익명 클래스를 대체할 수 있으며, 코드를 더 간결하게 만들어 줍니다.
- 함수형 인터페이스: 람다 표현식은 단 하나의 추상 메소드를 가진 인터페이스(함수형 인터페이스)에 대한 구현을 제공합니다.
- 유연성: 람다 표현식은 메소드 인자로 전달되거나 변수에 할당될 수 있어, 코드의 유연성을 증가시킵니다.
람다 표현식 예제:
// 람다 표현식을 사용하여 Thread 생성
new Thread(() -> System.out.println("Hello from thread")).start();
// 스트림 API와 함께 사용
strings.stream().filter(s -> !s.isEmpty()).forEach(System.out::println);
스트림 API와 람다 표현식은 자바에서 선언적으로 컬렉션 데이터를 처리하고, 멀티스레딩 처리를 간소화하며, 코드를 더 간결하고 명확하게 만드는 데 도움을 줍니다.
실용적인 자바 개발
자바 개발에 있어 JVM(자바 가상 머신)의 이해는 매우 중요합니다. JVM은 자바 애플리케이션을 실행하는 환경을 제공하며, 자바의 플랫폼 독립성, 메모리 관리, 가비지 컬렉션 등을 담당합니다.
자바 가상 머신(JVM)의 작동 원리
JVM의 작동 원리를 간단히 설명하면 다음과 같습니다:
-
프로그램의 실행: 사용자가 자바 애플리케이션을 실행하면 JVM은 자바 클래스 로더를 사용하여 필요한 클래스들을 로드하고, 바이트코드 검증 과정을 거쳐 안전성을 확인합니다.
-
실행 엔진: 검증된 바이트코드는 실행 엔진에 의해 기계어로 변환되며, 이 기계어는 호스트 시스템의 CPU에서 실행될 수 있습니다. 실행 엔진은 인터프리터와 JIT(Just-In-Time) 컴파일러를 포함하며, 이들은 바이트코드를 효율적으로 실행하기 위해 함께 작동합니다.
-
런타임 데이터 영역: JVM은 메모리 관리를 위해 여러 런타임 데이터 영역을 사용합니다. 이에는 힙(Heap), 스택(Stack), 메소드 영역(Method Area), 프로그램 카운터(Program Counter) 등이 포함됩니다.
- 힙(Heap): 객체와 배열이 할당되는 곳으로, 가비지 컬렉션이 이루어지는 주요 영역입니다.
- 스택(Stack): 스레드마다 런타임 스택이 할당되며, 메소드 호출과 로컬 변수 등을 관리합니다.
- 메소드 영역: 모든 스레드가 공유하는 영역으로, 클래스 데이터, 상수, 정적 변수 등을 저장합니다.
가비지 컬렉션
가비지 컬렉션(Garbage Collection)은 JVM의 메모리 관리 기법 중 하나로, 더 이상 사용되지 않는 객체를 자동으로 검출하고 삭제하여 재사용 가능한 메모리 공간을 확보하는 과정입니다. 가비지 컬렉션은 프로그래머가 직접 메모리를 해제하지 않아도 되므로 메모리 누수를 방지하는 데 큰 도움을 줍니다.
가비지 컬렉션의 주요 알고리즘에는 마크 앤 스위프(Mark and Sweep), 카피(Copy), 마크 앤 컴팩트(Mark and Compact) 등이 있으며, JVM의 구현에 따라 다양한 가비지 컬렉터가 제공됩니다.
JVM의 이해는 자바 애플리케이션의 성능 최적화와 효율적인 메모리 관리에 있어 핵심적인 역할을 합니다. 가비지 컬렉션의 원리를 이해하고 JVM의 설정을 적절히 조정함으로써, 자바 애플리케이션의 성능을 크게 향상시킬 수 있습니다.
source: DevOps/1.Programming_Languages_&_Frameworks/1.1.Java/1.1.2.md