[Java] 자바 문법 종합반 4주차

Yuri·2025년 1월 3일

Java

목록 보기
12/13
post-thumbnail

예외처리

오류(Error) vs 예외(Exception)

  • 오류(Error): 일반적으로 회복이 불가능한 문제
    • 이는 시스템 레벨에서, 또는 주로 환경적인 이유로 발생
    • 코드의 문제로 발생하는 경우도 있지만, 일단 발생하는 경우 일반적으로 회복이 불가능하다.
    • 에러가 발생한 경우 우리는 어떠한 에러로 프로그램이 종료되었는지를 확인하고 대응한다.
  • 예외(Exception): 회복이 가능한 문제
    • 회복이 가능하다는 전제는, "그 예외가 발생할 수 있다는 것을 인지하고, 대응"
    • 현실적으로 코드 레벨에서 할 수 있는 문제 상황에 대한 대응은 "예외 처리"에 속한다.

예외의 종류

▶︎ 코드 실행 관점에서 예외의 종류

  • 컴파일 에러
    • .java 파일을 .class 파일로 컴파일할 때 발생하는 에러
    • 자바 프로그래밍의 언어의 규칙을 지키지 않았기 때문에 발생
    • 들어 있지 않은 클래스를 호출하거나, 접근이 불가능한 프로퍼티나 메소드에 접근하는 경우 발생
    • 컴파일 에러가 발생하는 경우 해결 방법은 문법에 맞게 다시 작성하는 것
  • 런타임 에러
    • 우리가 주로 다루게 될 에러(예외)
    • 문법적인 오류는 아니라서, 컴파일은 잘 되었지만 "프로그램"이 실행 도중 맞닥뜨리게 되는 예외

▶︎ 예외처리 관점에서 예외의 종류

  • 확인된 예외(Checked Exception): 컴파일 시점에 확인하는 예외 → 반드시 예외 처리를 해줘야하는 예외
    🤔 Checked Exception 에 대한 처리가 바로 개발자가 주로 다루게 될 예외처리이다.

  • 미확인된 예외(Unchecked Exception): 런타임 시점에 확인되는 예외, 예외처리가 반드시 필요하지 않은 예외

💡 컴파일이란? 프로그래밍 언어로 작성한 코드가 컴퓨터가 이해할 수 있는 언어로 변환되는 것

예외 발생과 try-catch, finally, throw

1. 예외를 어떻게 정의하고,
2. 예외가 발생 할 수 있음을 알리고, → 예외가 발생할 수 있음을 논리적 flag를 세워둔다. 
3. 사용자는 예외가 발생할 수 있음을 알고 예외를 핸들링하는지

예외 정의하기

어떠한 상황(정의)에 의해 프로그램에서 예외가 발생할 수 있음을 알리고 예외가 발생하면 이러한 처리를 한다(핸들링)

  • OurBadException.java: 개발자가 직접 예외 클래스를 정의할 수 있다. → 사용자 정의 Exception
  • OurClass.java: 메서드에서 정의된 예외 클래스를 인스턴스화하여 사용할 수 있다.

▶︎ OurBadException.java

package week04.sample01;

// 예외 클래스를 만들어서 예외를 정의 : 사용자 정의 Exception
public class OurBadException extends Exception {
    public OurBadException() {
        super("위험한 행동을 하면 예외처리를 꼭 해야함!");
    }

}

▶︎ OurClass.java

package week04.sample01;

public class OurClass {
    private final boolean just = true;

    // throws : 던지다! (= 예외를 던지다 / 발생시키다)
    public void thisMethodIsDangerous() throws OurBadException {
        // custom logic
        if (just) {
            throw new OurBadException();
        }
    }
}

▶︎ StudyException.java

package week04.sample01;

public class StudyException {
    public static void main(String[] args) {
        OurClass ourClass = new OurClass();

        // try ~ catch ~ finally 구문
        // try: 시도하다
        // catch: 잡다 (붙잡다)
        // finally: 마침내

        // 일단 try ~~ 그리고, 예외가 발생하면 그걸 잡아 (catch)
        // 그리고, 정상적으로 수행되든, 예외가 발생하든 마침내 수행해야 하는 로직을 'finally' 수행해

        try {
            // 일단 실행!
            ourClass.thisMethodIsDangerous();
        } catch (OurBadException e) {
            System.out.println(e.getMessage());
        } finally {
            // 무조건 여기는 거쳐요!
            System.out.println("우리는 방금 예외를 handling 했습니다. 정상처리되든, 오류가 발생하든 여기를 반드시 거쳐요");
        }

    }
}
  • throws: 메서드 이름 뒤에 붙어 이 메서드가 어떠한 예외사항을 던질 수 있는지 알려주는 예약어이다.
  • throw: 메서드 안에서 실제로 예외 객체를 던질 때 사용, 일반 메서드의 return 키워드처럼 throw 아래의 구문들은 실행되지 않고, throw 문과 함께 메서드가 종료된다.

▶︎ 용어 정리

  • 예외가 발생하는 상황을 인지하고, 어떠한 에러인지 정의했습니다.
  • 우리는 메서드를 선언할 때 예외가 발생하는 위험한 메서드라는 것을 알렸다.
  • 확인된 예외(checked exception)를 정의하고 알렸으니, 이 메서드를 사용할 때 예외처리를 하지 않으면 컴파일 에러가 발생함!

예외 클래스 구조 이해하기

프로그래밍하다 보면 "문제"상황에 직면하고, 회복 가능 여부에 따라 ❌(Error), ⭕️(Exception) 으로 나누고, 예외와 에러를 컴파일/런타임과 같은 여러 기준에 따라 구체적으로 정의함

결론적으로 우리가 맞닥뜨리는 "문제"상황을 추상화, 일반화, 구체화해서 정의

  • 추상적인 "문제" → 일반적인 "오류", "예외" → 구체적인 "Checked Exception", "Unchecked Exception"
  • Checked Exception: 사전에 확인이 가능하여 사전에 처리하지 않으면 컴파일 시점에 에러가 발생함

▶︎ 예외 또한 "객체"다

이러한 에러 객체들을 구현하는 것 자체가 조금 전에 언급된 언어 설계 차원에서의 에러 대응 프로세스이기도 하기 때문에 당연히 이 문제들을 추상화해서 객체로 만들어 주는 것은 자바 언어 차원에서 지원해 준다.

Throwable Class

  • 모든 객체의 원형인 Object 클래스를 상속받는다.

  • Throwable 클래스의 자식으로 에러(Error)와 예외(Exception)가 있다.

  • 에러 클래스와 예외 클래스는 각각 IOError 클래스와 RuntimeException 클래스와 같이 구분하여 처리된다.

  • CheckedExceptionUncheckedException을 쉽게 구분하는 방법

    • RuntimeException을 상속한 예외들은 Unchecked Exception, 반대로 상속하지 않은 예외들은 CheckedException 으로 구현되어 있다.

▶︎ Exception 클래스 구조

  • 즉, NullPointException, ArrayIndexOutOfBoundsException 등 예외 구현체들은 명시적인 에러처리를 하지 않아도 컴파일 에러가 발생하지 않는다. (RuntimeException을 상속받고 있다.)

✨ 이미 수많은 구현체들이 있고, 거의 대부분의 상황에 맞는 에러들은 이미 구현되어 있다. 명시적으로 어떠한 에러를 내보낼지는 찾아보고 결정하면 된다. 만약, 찾아봤는데 알맞은 에러가 없다면 특정 에러를 더 구체화해서 직접 예외 클래스를 "정의", "구현"하면 된다.

Chained Exception

연결된 예외(Chained Exception)

  • 예외는 다른 예외를 유발할 수 있다.
  • 예외 A가 예외 B를 발생시켰다면, 예외 A는 B의 원인이다.
  • 원인 예외는 새로운 예외를 등록한 후 다시 새로운 예외를 발생시키는데 이를 예외 연결이라고 한다.

▶︎ 원인 예외를 다루기 위한 메소드

  • initCause()
    • 지정한 예외를 원인 예외로 등록하는 메소드
  • getCause()
    • 원인 예외를 반환하는 메소드

🤔 왜 예외를 연결하죠?

  • 예외를 연결하는 이유는 여러 가지 예외를 하나의 큰 분류의 예외로 묶어서 다루기 위함
  • checkedException 을 uncheckedException으로 포장(wrapping) 하는데 유용하게 사용되기도 한다.

▶︎ 연결된 예외 예시

// 연결된 예외 
public class Main {

    public static void main(String[] args) {
        try {
            // 예외 생성
            NumberFormatException ex = new NumberFormatException("가짜 예외이유");

            // 1. initCause: 원인 예외 설정(지정한 예외를 원인 예외로 등록)
            ex.initCause(new NullPointerException("진짜 예외이유"));

            // 예외를 직접 던집니다.
            throw ex;
        } catch (NumberFormatException ex) {
            // 예외 로그 출력
            ex.printStackTrace();
            // 2. getCause(): 예외 원인 조회 후 출력
            ex.getCause().printStackTrace();
        }

        // checked exception 을 감싸서 unchecked exception 안에 넣습니다.
        throw new RuntimeException(new Exception("이것이 진짜 예외 이유 입니다."));
    }
}

ex.printStackTrace()로 예외 로그를 콘솔에 출력하면 예외 ex의 메세지와 ex가 원인 예외로 지정한 NullPointerException 의 메세지까지 함께 출력된다. → 에러 원인에 대한 추적이 가능하다.

실질적 예외 처리 방법

예외복구, 예외 처리 회피, 예외 전환

  1. 예외 복구
    try-catch로 예외를 처리하고 프로그램을 정상상태로 복구함
public String getDataFromAnotherServer(String dataPath) {
		try {
				return anotherServerClient.getData(dataPath).toString();
		} catch (GetDataException e) {
				return defaultData;
		}
}
  • 가장 기본적인 방식이지만 복구가 가능한 상황이 아닌 경우가 많아 최소한의 대응만 가능한 경우가 많다.
  1. 예외 처리 회피하기
public void someMethod() throws Exception { ... }

public void someIrresponsibleMethod() throws Exception {
		this.someMethod();
}
  • 예외의 처리를 예외를 호출한 곳으로 흘려보낸다.
  1. 예외 전환하기
public void someMethod() throws IOException { ... }

public void someResponsibleMethod() throws MoreSpecificException {
		try {
			this.someMethod();
		} catch (IOException e) {
			throw new MoreSpecificException(e.getMessage());
		}
}
  • 조금 더 적절한(상세한) 예외로 바꿔서 던져주는 경우
  • 예외처리를 더 신경 쓰고 싶은 경우나, 오히려 RuntimeException처럼 일괄적으로 처리하기 편한 예외로 바꿔서 던지고 싶은 경우 사용

제네릭(Generic)

gereralized: 일반화된, genetic: 유전적인
타입 문법이기 때문에 특정 타입을 지정해줘야 하는데(강타입) 이로 인한 문제가 발생할 수 있다.

  • 강타입 언어(자바) - 약타입 언어 (자바스크립트)
  • 자바에서도 Object를 사용하여 타입 지정을 회피할 수 있지 않을까?
    🤨 a 객체와 b 객체의 단항연산자를 사용할 수 없다. 또한 오히려 메서드의 구현부 {} 안에 모든 경우의 수에 대한 처리를 명시해야 한다. → 타입 안정성을 침해하는 행위

하지만, 타입없이 쓰고 싶은데! : 제네릭을 사용해보자

package week04.gen;

// 1. 제네릭은 클래스 또는 메서드에 사용 가능하다.
// <> 안에 들어가야 할 타입을 명시
// T (타입변수) 는 규약 (컨벤션)
public class Generic<T> {
    // 2. main 안 Generic<String>에 의해 내부 필드는 String 타입
    private T t;
    // 3. 메서드의 리턴 타입도 String 으로 대체된다.
    public T get() {
        return this.t;
    }

    public void set(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
        // 4.
        Generic<String> stringGeneric = new Generic<>();
        // 5.
        stringGeneric.set("Hello World");

        String tValueTurnOutWithString = stringGeneric.get();

        System.out.println(tValueTurnOutWithString);
    }
}

제네릭 문법

public class Generic<T> { ... }

Generic<String> stringGeneric = new Generic<>();
  • Generic<T>의 클래스처럼, 제네릭을 사용한 클래스를 제네릭 클래스라고 한다.
  • 제네릭에서 <>사이에 들어가는 변수명 T는 타입 변수라고 한다.
  • Generic 클래스를 원시 타입이라고 한다.

▶︎ 제네릭의 제한
1. 객체의 static 멤버에 사용할 수 없다.
(타입변수는 인스턴스 변수로 간주된다.)
2. 제네릭 배열을 생성할 수 없다.

▶︎ 제네릭 문법

  1. 다수의 타입변수를 사용할 수 있다.
public class Generic<T, U, E> {
    public E multiTypeMethod(T t, U u) { ... }
}


Generic<Long, Integer, String> instance = new Generic();
instance.multiTypeMethod(longVal, intVal);
  1. 다형성 즉 상속과 타입의 관계는 그대로 적용된다.
  • 대표적으로 부모 클래스로 제네릭 타입변수를 지정하고, 그 안에 자식 클래스를 넘기는 것은 잘 동작한다.
  1. 와일드카드를 통해 제네릭의 제한을 구체적으로 정할 수 있다.
public class ParkingLot<T extends Car> { ... }

ParkingLot<BMW> bmwParkingLot = new ParkingLot();
ParkingLot<Iphone> iphoneParkingLot = new ParkingLog(); // error!
  • 와일드카드
1. <? extends T>: T와 그 자손들만 사용 가능
2. <? super T>: T와 그 조상들만 가능
3. <?>: 제한 없음

이렇게 제한하는 이유는 다형성 때문이다. 제네릭 클래스 안의 로직은 다형성을 염두에 두고(부모 클래스의 메서드를 사용) 개발되었기 때문이다.

  1. 메서드 스코프(범위)로 제네릭을 별도로 선언할 수 있다.
    : 제네릭 클래스에서만 제네릭을 사용가능한 것이 아니라 제네릭 메서드로도 선언이 가능하다.

    출처: Head First Java

📌 타입변수를 클래스 내부의 인스턴스 변수 취급하기 때문에 제네릭 클래스의 타입변수를 static 메서드에서는 사용할 수 없었지만, 제네릭 메서드의 제네릭 타입 변수는 해당 메서드에만 적용되기 때문에 메서드 하나를 기준으로 선언하고 사용할 수 있다.

  • 같은 이름의 변수를 사용했다고 해도 제네릭 메서드의 타입변수는 제네릭 클래스의 타입변수와 다르다.
public class Generic<T, U, E> {
	// Generic<T,U,E> 의 T와 아래의 T는 이름만 같을뿐 다른 변수
    static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
}

👉 제네릭 클래스에서의 <T>와 제네릭 메서드 sort에서의 <T>는 관계가 없다! (<T>가 관여하는 범위가 다르다.)

Collection

Collection으로 돌아온 이유는 사실 우리가 쓰는 컬렉션에는 제네릭이 포함되어있다. → 자료구조 (List, Array, Map)

List, 배열 예시

리스트는 추상적 자료구조로서 (이를 실체화한게 ArrayList, LinkedList), 순서를 가지고, 일렬로 나열한 원소들의 모임이다. 순서가 있고, 중복을 허용한다는 점에서 집합(Set)과 구별된다.

▶︎ 추상적 자료구조인 리스트는 개념적으로 보통 다음 연산들을 지원한다.

* 빈 리스트를 만드는 연산
* 리스트가 비어있는지 확인하는 연산
* 리스트의 앞에 원소를 삽입하는 연산
* 리스트의 뒤에 원소를 삽입하는 연산
* 리스트의 제일 첫 원소를 알아보는 연산
* 리스트의 첫 원소를 제외한 나머지 리스트를 알아보는 연산

리스트는 사실 인터페이스다.

ArrayList, LinkedList는 이러한 리스트 인터페이스를 구현(implements)한 클래스로 리스트에 작성된 메서드를 모두 오버라이딩하여 완성시킨다.

배열은 프로그래밍 언어에서 지원하는 자료형
int[] arr = {1,2,3};
배열은 첫 칸의 주소를 알고있으면 인덱스를 통해 쉽게 조회가 가능하다. 하지만 순서를 가지고 있기 때문에 추가와 삭제가 매우 느리다.
→ 검색에는 유리하고, 수정/삭제는 불리한 자료구조다.

Collection의 구조


출처 : https://www.javatpoint.com/collections-in-java
자바에서는 Computer Science의 자료구조를 구체화하여 객체 클래스로 제공하고 있으며 개발자는 해당 클래스를 이용해 알고리즘을 구현할 수 있다.

Wrapper 클래스

원시 타입(Primitive Data Type)과 Wrapper 클래스

원시 타입래퍼 클래스
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

래퍼 클래스는 기본형 타입이 가지고 있는 기능을 개선하여 여러 기능을 추가했다.

기본형 타입은 실제로 "값" 만 가지고 있다. 하지만 자주 쓰이는 정수, 실수, 문자 값들까지 모두 객체로 관리하면 메모리를 많이 차지하게 되므로 성능·비용문제까지 발생한다. 그래서 객체 대신 원시 타입을 사용한다.

Boxing / Unboxing

Integer num = new Integer(17);  // Boxing
int n = num.intValue(); // UnBoxing

Character ch = 'X'; // AutoBoxing
char c = ch; // AutoUnBoxing
profile
안녕하세요 :)

0개의 댓글