프로그램에서 발생할 것 같은 문제를 정의하고 대응하는 것
회복 가능 여부를 기준으로 오류와 예외를 구분
회복 가능 여부란?
예외가 발생할 수 있다는 것을 인지하고 그에 따른 대응을 할 수 있는 것
- 오류(Error)
- 일반적으로 회복이 불가능한 문제
- 시스템 레벨 또는 환경적인 이유로 발생
- 어떠한 에러로 프로그램이 종료되었는지를 확인하고 대응
- 예외(Exception)
- 일반적으로 회복이 가능한 문제
- 코드 레벨에서 가능한 대응을
예외 처리
라고 칭함
- 코드 실행 관점의 예외 종류
- 컴파일 에러
-.java
파일을.class
파일로 컴파일할 때 발생
- 대부분은 틀린 문법으로 인해 발생
- 틀린 문법을 수정하여 해결
- 런타임 에러
- 프로그램 실행 중에 발생
- 예외 처리 관점의 예외 종류
- 확인된 예외 (Checked Exception)
- 컴파일 시점에 확인하는 예외로서 반드시 예외 처리 필요
⚠️ 인지한 특정 문제에 대해 예외 처리가 완료되었는지 확인(Check)할 수 있는 예외
⚠️ 예외 처리를 완료하지 않았다면 컴파일 에러 발생- 미확인된 예외 (Unchecked Exception)
- 런타임 시점에 확인되는 예외
- 예외 처리가 반드시 필요하지 않음
- 문제에 대한 예외가 Exception 클래스에 없거나 더 구체화하고 싶다면 별도의 예외 클래스에서 정의할 수 있음
class OurBadException extends Exception { public OurBadException() { super("위험한 행동을 하면 예외처리를 꼭 해야합니다!"); } }
- 문제가 발생할 수도 있다는 것을 해당 메소드와 로직에서
throw
,throws
를 사용하여 알려야함
- throws
- 메소드 이름 뒤에 붙이며, 어떤 예외 사항이 발생할 수 있는지 알려주는 예약어
- 여러 종류의 예외 사항을 적을 수 있음
- throw
- 메소드 내에서 실제로 예외 객체를 던질 때 사용
- 예외 객체 앞에 불임
- 일반 메소드의 return 키워드처럼 throw 아래 구문들은 실행되지 않으며, throw문과 함께 메소드가 종료됨
class OurClass { private final Boolean just = true; // throws를 사용하여 OurBadException이 발생할 수 있다는 것을 명시 public void thisMethodIsDangerous() throws OurBadException { if (just) { // 예외가 발생했을 때, throw를 사용하여 예외 객체를 던짐 throw new OurBadException(); } } }
메소드를 확인하여 위험하면(throws가 있다면) try-catch-(finally)를 사용하여 핸들링이 필요함
try
와catch
는 필수지만finally
는 생략 가능try
는 예외가 발생할 수 있지만 실행을 시도해야하는 코드를 담음catch
는 예외가 발생한 것을 잡아낸다면 어떻게 처리할 지에 대한 코드를 담으며, 여러 개 사용 가능try
의 코드를 실행하던 중 예외가 발생하면, 즉시try
의 실행을 멈추고catch
의 코드를 실행함finally
는 예외의 발생 유무와 상관없이 실행되어야 할 코드를 담음public class StudyException { public static void main(String[] args) { OurClass ourClass = new OurClass(); try { // 예외가 발생할 수 있지만 실행을 시도해야하는 코드 ourClass.thisMethodIsDangerous(); } catch (OurBadException e) { // 예외를 잡아낸 즉시 실행해야 하는 코드 System.out.println(e.getMessage()); } finally { // 예외 발생 여부와 상관없이 항상 실행되어야 하는 코드 System.out.println("우리는 방금 예외를 handling 했습니다!"); } } }
- 모든 객체의 원형인
Object
클래스에서 시작- 문제 상황을 뜻하는
Throwable
클래스가Object
클래스를 상속Throwable
의 자식 클래스로 에러(Error
)클래스와 예외(Exception
) 클래스 존재- 에러(
Error
)클래스와 예외(Exception
) 클래스는 각각IOException
클래스,RuntimeException
클래스와 같이 구분하여 처리
- 예외는 다른 예외를 유발할 수 있음
- 예외 A가 예외 B를 발생시켰다면, 예외 A는 예외 B의 원인
- 원인 예외를 새로운 예외에 등록 후, 새로운 예외를 발생시키는 것을
예외 연결
이라 칭함
- 여러 가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위함
- checked exception을 unchecked exception으로 포장(wrapping)하는데 유용하게 사용
- initCause()
- 지정한 예외를 원인 예외로 등록
- getCause()
- 원인 예외를 반환
// 모든 예외 클래스는 존재한다고 가정 // 여러 가지 예외를 하나의 큰 분류의 예외로 묶기 public class Main { public static void main(String[] args) { try { install(); } catch (InstallException e) { System.out.println("원인 예외 : " + e.getCause()); // 4. 원인 예외를 출력(SpaceException) e.printStackTrace(); // 5. 예외가 발생한 곳을 추적 } } public static void install() throws InstallException { try { throw new SpaceException("설치할 공간이 부족합니다."); } catch (SpaceException e) { InstallException ie = new InstallException("설치중 예외발생"); // 1. InstallException 발생 ie.initCause(e); // 2. InstallException의 원인으로 SpaceException 등록 throw ie; // 3. 예외 객체를 던짐 } catch (MemoryException e) { // ... } } } // 출력 예상 // 원인 예외 : SpaceException: 설치할 공간이 부족합니다. // InstallException: 설치중 예외발생 // SpaceException: 설치할 공간이 부족합니다. // 단순히 '설치 공간이 부족'이 아닌 // '설치 중 -> 설치 공간 부족'이기 때문에 어디서 예외가 발생했는지 추적에 용이
// 모든 예외 클래스는 존재한다고 가정 // checked exception을 unchecked exception으로 포장 public class Main { public static void main(String[] args) { install(); } public static void install() { // 원래대로라면 IOException은 checked exception이기 때문에 예외처리가 필수이지만 // unchecked exception인 RuntimeException으로 포장했기 때문에 예외처리를 하지 않아도 됨 // 또한, public RuntimeException(Throwable cause) {} 이기 때문에 // 예외가 발생했을 때 원인이 IOException인 것을 확인할 수 있음 throw new RuntimeException(new IOException("설치할 공간이 부족합니다.")); } }
중복되거나 필요 없는 코드를 줄여줌
- 클래스 또는 메소드에 사용할 수 있으며, 클래스 이름 뒤의
<>
안에 들어가야 할 타입 변수 지정
- 어떤 것을 사용해도 상관없지만, 암묵적으로 T, U, V, E를 사용
- 선언해둔 타입 변수는 해당 클래스 내에서 특정한 타입이 들어갈 자리에 대신 들어감
// 예제에선 타입 변수로 T를 사용 public class Generic<T> { // 메소드의 데이터 타입, 매개변수의 데이터 타입 등에 사용 private T t; public T get() { return this.t; } public void set(T t) { this.t = t; } public static void main(String[] args) { // 인스턴스를 만들 때 타입 변수에 들어갈 실제 값 입력 Generic<String> stringGeneric = new Generic<>(); stringGeneric.set("Hello World"); String tValueTurnOutWithString = stringGeneric.get(); System.out.println(tValueTurnOutWithString); } }
- 제네릭을 사용한 클래스는 제네릭 클래스라 부르며 원시 타입임
<>
사이에 들어가는 변수를 타입 변수라고 칭함- 제네릭 배열은 생성 불가
- 객체의 static 멤버에 사용할 수 없음
- 타입 변수는 인스턴스 변수로 간주되기 때문에 모든 객체에 동일하게 동작해야하는 static 필드에 사용 불가
static T get() { ... } // 에러 static void set(T t) { ... } // 에러
- 다수의 타입 변수 사용 가능
public class Generic<T, U, E> { public E multiTypeMethod(T t, U u) { ... } } Generic<Long, Integer, String> instance = new Generic(); instance.multiTypeMethod(longVal, intVal);
- 타입 변수에 지정할 수 있는 종류를 제한할 수 있음
// Car의 자손만 지정 가능 class sportCar<T extends Car> { ArrayList<T> list = new ArrayList<>(); }
// 인터페이스를 구현한 타입으로 지정할 때도 동일하게 extends 사용 interface Speed {} class sportCar<T extends Speed> {}
// Car의 자손이면서 Speed 인터페이스도 구현해야한다면 // & 를 사용하여 연결 class sportCar<T extends Car & Speed> {}
- 와일드카드를 통해 제네릭의 제한을 구체적으로 설정 가능
-<? extends T>
- T와 그 자손들만 가능
-<? super T>
- T와 그 조상들만 가능
-<?>
- 제한 없음
-&
사용 불가
// 어떤 타입의 매개변수가 들어와도 매개 값을 출력 public static <T, U> void printStr(T text1, U text2){ System.out.println(text1 + " " + text2); }
- 반환 타입 앞에 제네릭을 사용한 경우, 해당 메소드에서만 적용되는 제네릭 타입 변수를 선언할 수 있음
public class CheckOne<T> { // 제네릭 클래스 내부에 선언 public static void method1(T t) { ... } // 컴파일 에러 발생 } public class CheckTwo { // 일반 클래스 내부에 선언 public static <T> void method2(T t) { ... } }
CheckOne
의 제네릭 타입 변수는 인스턴스가 생성될 때 결정이 되는데,static
메소드는 인스턴스 생성과 관계가 없음- 그렇게 때문에
CheckOne
이 생성되는 시점에 제네릭 타입을 메소드가 매개변수로 받아낼 수 없기 때문에 컴파일 에러 발생
CheckTwo
의 제네릭 타입 변수는method2
에만 적용되기 때문에 호출되는 시점에 타입이 결정되어 컴파일 에러가 발생하지 않음- 그렇기 때문에
static
메소드지만 제네릭을 사용할 수 있음public class CheckThree<T> { public static <T> void method3(T t) { ... } }
CheckThree
와method3
에서 같은 이름의 타입 변수를 사용했지만 다른 변수CheckThree
에 사용된 타입변수는 인스턴스가 생성될 때마다 지정되기 때문에 인스턴스 변수로 간주되고,method3
에 사용된 타입변수는 지역 변수로 간주