본문 바로가기
TIL/JAVA

23.05.25

by J1-H00N 2023. 5. 25.

예외 발생시 어떻게 대응할지, 어떻게 사전에 예외처리를 할지

 

1. 예외를 어떻게 정의하는지

// 예외 클래스를 만들어서 예외를 정의
public class BadException extends Exception { // Exception은 java 기본 제공 클래스
    public BadException() {
        super("위험한 행동을 하면 예외처리를 꼭 해야함"); // 출력할 에러 메시지
    }
}

2. 예외가 발생할 수 있음을 알리기

public class UsingClass {
    private final boolean just = true;

    // throws : 던지다 (=예외를 발생시키다)
    public void thisMethodIsDangerous () throws BadException { // throws Exception이어도 작동하지만, 더 구체적으로 나타내기 위해서
        // 위에서 이 메서드가 BadException 예외가 발생할 수 있다고 알려줌

        // custom logic
        if (just) { // 로직에 따라 만약 참이라면
            throw new BadException(); // 위험하다고 알리기
        }
    }
}

throws : 메서드 이름 뒤에서 어떤 예외를 던질 수 있는지 알려주는 예약어/ 여러 예외상황을 적어둘 수 있음

throw : 메서드 안에서 실제로 예외 객체를 던질 때 사용되는 예약어/

return과 같이 throw 아래 구문은 실행되지 않고 throw문과 함께 메서드를 종료시킴

 

3. 사용자가 예외가 발생할 수 있을을 알고 어떻게 예외를 핸들링 하는지

try ~ catch ~ finally

일단 메서드를 시도하고, 예외가 발생하면 잡는다. 그리고 예외가 발생하든 안하든, 로직을 결국엔 수행한다.

public class StudyException {
    public static void main(String[] args) {
        UsingClass usingClass = new UsingClass();
        // usingClass.thisMethodIsDangerous(); // 위험하다고 플래그를 달아놨기 때문에 그냥 사용X

        // try ~ catch ~ finally 구문
        try { // try할 로직 입력
            usingClass.thisMethodIsDangerous();
        } catch (BadException e) { // ()안에는 어떤 예외상황을 캐치할지 적어두는 것 // 아래에서 사용할 수 있도록 객체화
            System.out.println(e.getMessage()); // 오류 메시지 출력
        } finally { // 정상적으로 실행되든 예외가 발생하든 아래 로직은 무조건 실행
            System.out.println("우리는 방금 예외를 handlig 해봤습니다");
        }
        // 위험한 행동을 하면 예외처리를 꼭 해야함 // just = true이므로 무조건 예외 발생해서 메시지 출력
        // 우리는 방금 예외를 handlig 해봤습니다 // 예외 발생 유뮤와 상관없이 출력
    }
}

위에서 모든 예외상황을 캐치하고 싶다면 catch ()에 Exception 변수이름 만 입력해도 된다.

또한, catch는 여러개 적어도 되며, finally는 필수가 아니다.

 

문제의 구체화

  • 가장 추상적인 "문제"
  • 조금 더 구체적인 회복 불가능한 "오류"와 회복 가능한 "예외"
  • 이보다 더 구체적인 사전에 알고있어서 예외처리한 “Checked Exception”과 그러지 못한 “Unchecked Exception”

Throwable Class

  • 모든 클래스의 원형인 Object Class를 상속받음
  • Throwable 클래스의 자식은 Error 클래스와 Exception 클래스가 있다
  • Error 클래스는 IOError, Exception 클래스는 IOException, RuntimeException 등으로 구분된다. (IO = 입출력)
  • RuntimeException을 상속한 예외는 UnCheckedException으로 구현돼있고 상속받지 않은 예외는 IOException을 통해 CheckedException으로 구현돼있다.
  • CheckedException에 속하는 구현체들은 핸들링하지 않으면 컴파일 에러가 발생하는 대신 컴파일 됐다면 복구 가능한 에러다.
  • 아래 이미지에서 NullPointException 등의 예외는 명시적인 처리를 하지 않아도 컴파일 에러가 발생하진 않는다.

아래는 참고용 예외 리스트이고, 어떠한 예외를 내보낼지는 찾아보고 결정해도 늦지 않는다. 찾아도 없다면 구체화해서 직접 정의하고 구현하면 된다.

↓예외 리스트(참고용)

더보기
// 출처 : https://programming.guide/java/list-of-java-exceptions.html

java.io
IOException
CharConversionException
EOFException
FileNotFoundException
InterruptedIOException
ObjectStreamException
InvalidClassException
InvalidObjectException
NotActiveException
NotSerializableException
OptionalDataException
StreamCorruptedException
WriteAbortedException
SyncFailedException
UnsupportedEncodingException
UTFDataFormatException
UncheckedIOException

java.lang

ReflectiveOperationException
ClassNotFoundException
InstantiationException
IllegalAccessException
InvocationTargetException
NoSuchFieldException
NoSuchMethodException
CloneNotSupportedException
InterruptedException

산술 예외
IndexOutOfBoundsException
ArrayIndexOutOfBoundsException
StringIndexOutOfBoundsException
ArrayStoreException
ClassCastException
EnumConstantNotPresentException
IllegalArgumentException
IllegalThreadStateException
NumberFormatException
IllegalMonitorStateException
IllegalStateException
NegativeArraySizeException
NullPointerException
SecurityException
TypeNotPresentException
UnsupportedOperationException

java.net
HttpRetryException
SocketTimeoutException
MalformedURLException
ProtocolException
SocketException
BindException
ConnectException
NoRouteToHostException
PortUnreachableException
UnknownHostException
UnknownServiceException
URISyntaxException

java.text
ParseException
 

java.time
DateTimeException
 

java.time.zone
ZoneRulesException

 

Chained Exception

예외는 다른 예외를 유발할 수 있다. 원인이 되는 예외를 원인 예외라 부른다.

원인 예외를 새로운 예외에 등록한 후 다시 새로운 예외를 발생시키는데, 이를 예외 연결이라고 한다.

 

예외를 연결하는 이유

여러 가지 예외를 하나의 큰 분류의 예외로 묶기 위해서

반드시 명시해야 컴파일 에러가 나지 않는 CheckedException을 UnCheckedException으로 포장하는데 유용해서

 

원인 예외를 다루기 위한 메서드

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

 

예외를 처리하는 3가지 방법

  • 예외 복구하기 
    • try - catch문을 통해 예외를 처리하고 프로그램을 정상 상태로 복구하는 방법
    • 실제로는 복구가 가능하지 않거나 최소한의 대응만 가능한 상황이 많기 때문에 잘 쓰이지는 않음
  • 예외 회피하기
    • 한 레이어에서 처리하기 위해서 에러를 회피해서 그대로 흘려보내는 방법
  • 예외 전환하기
    • 회피하기와 비슷하지만 조금 더 적절한 예외를 던져주는 방법
    • 보통 예외처리에 더 신경쓰고싶은 경우나 RuntimeException처럼 일괄적으로 처리하기 편한 예외로 포장해서 던지고 싶은 경우 사용

예외 복구하기

public String getDataFromAnotherServer(String dataPath) {
	try {
		return anotherServerClient.getData(dataPath).toString();
	} catch (GetDataException e) {
		return defaultData;
	}
}

예외 회피하기

public void someMethod() throws Exception { ... }

public void someIrresponsibleMethod() throws Exception {
	this.someMethod();
}

// 같은 객체 내에서 이러는 경우는 없고 그저 예시

예외 전환하기

public void someMethod() throws IOException { ... }

public void someResponsibleMethod() throws MoreSpecificException {
	try {
		this.someMethod();
	} catch (IOException e) {
		throw new MoreSpecificException(e.getMessage());
	}
}

 

 

Generic

타입 언어에서 중복되거나 필요없는 코드를 줄이면서도, 타입 안정성은 해치지 않는 것이 Generic의 효용이다.

 

Generic이 필요한 이유

아래와 같이 똑같은 로직을 구현해도 매개변수의 타입이 다르면 그만큼 다시 구현해야 한다.

public class Generic {
    public String plusReturnFunction(int a, int b) { ... }

    public String plusReturnFunction(int a, long b) { ... }

    public String plusReturnFunction(int a, String b) { ... }
}

물론 public String plusReturnFunction(Object a, Object b) { . . . }와 같이 구현하면 해결은 가능하다.

하지만 실제로 어떤 값을 입력할지 모르기 때문에 어떤 타입이 들어올지 메서드 내에서 모든 경우의 수를 구현해놔야 한다.

 

public class Generic<T> { ... } // 제네릭 클래스 // T는 타입 변수
// 제네릭 클래스를 원시타입이라고 한다.
Generic<String> stringGeneric = new Generic<>();

제네릭의 제한

- 객체의 스태틱 멤버에 사용할 수 없다. <=  static은 모든 객체에 동일하게 동작해야 하는데 타입 변수는 인스턴스 변수로 간주되기 때문, static은 변수를 생성하지 않아도 사용 할 수 있기 때문

- 제네릭 배열을 생성 할 수 없다.

static T get() { ... } // 에러

static void set(T t) { ... } // 에러

 

타입 변수를 여러개 쓸 수 있다.

public class Generic<T, U, E> {
    public E multiTypeMethod(T t, U u) { ... } // E는 출력 타입으로, 나머지는 입력 타입으로
}


Generic<Long, Integer, String> instance = new Generic();
instance.multiTypeMethod(longVal, intVal);

 

상속과 타입의 관계는 그대로 적용(다형성 적용)

- 대표적으로 부모 클래스로 제네릭 타입변수를 지정하고, 그 안에 자식클래스를 넘기는 것은 잘 동작합니다.

 

와일드카드 : 제네릭의 제한을 구체적으로 정할 수 있음

  • <? extends T> : T와 그 자손들만 사용 가능
  • <? super T> : T와 그 조상들만 가능
  • <?> : 제한 없음
public class ParkingLot<T extends Car> { ... }

ParkingLot<BMW> bmwParkingLot = new ParkingLot(); // BMW는 Car 클래스를 상속받음
ParkingLot<Iphone> iphoneParkingLot = new ParkingLog(); // Iphone은 Phone 클래스를 상속받음 // error!

이렇게 제한을 하는 이유는 다형성 때문입니다. 위의 코드에서, T는 Car의 자손클래스들이라고 정의했기 때문에, 해당 클래스 내부에서 최소 Car 객체에 멤버를 접근하는 코드를 적을 수 있습니다. 반대로 그러한 코드들이 있을 여지가 있기 때문에, Car 객체의 자손이 아닌 클래스는 제한하는 것 이죠

 

제네릭 클래스 뿐만이 아니라 기존 클래스도 메서드 자체만으로 제네릭 메서드로 만들 수 있다.

static <T> void sort(List<T> list, Comparator<? super T> c) { ... }

위와 같이 반환타입 앞에 <> 제네릭을 사용한 경우 해당 메서드에서만 적용되는 제네릭 타입 변수를 선언 할 수 있다.

제네릭 클래스의 타입변수는 스태틱 메서드에서는 스태틱 특성상 사용할 수 없었지만 제네릭 메서드의 타입변수는 메서드 내에서만 적용되기 때문에 메서드 하나를 기준으로 타입 변수를 선언하고 사용 가능하다.

public class Generic<T, U, E> {
	// Generic<T,U,E> 의 T와 아래의 T는 이름만 같을뿐 다른 변수
    static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
}

이 때 위와 같이 제네릭 클래스의 타입변수와 제네릭 메서드의 타입변수는 이름만 같을 뿐 다른 변수다.

 

이제 제네릭에 대해 배웠으니 collection을 다시 돌아볼 시간이다.

List나 Set 등을 생성할 때 

ArrayList<Integer> intList = new ArrayList<Integer>();
Set<Integer> intSet = new HashSet<>();
Map<String, Integer> intMap = new HashMap<>();

위와 같이 이미 제네릭을 썼었다. 당시에는 그냥 collection이 원래 그런가보다 했었는데, 위에서 배운것처럼 <>은 제네릭 타입 변수를 이용한다는 표시이다.

 

Wrapper class

객체 지향 언어를 사용함에 있어 모든 것을 객체로 다뤄야 하지만, 기본형 자료(int, char, float, ...)등은 값 그 자체이기 때문에 메모리를 많이 할당받지 않아서 많이 쓰인다.

java는 기본형의 자료들도 추상화해서 객체화를 해놨다. (소수, 정수, 반올림, ...) 다만 값 그 이상의 의의가 없을 때는 성능상의 이유로 원시값을 그대로 쓸 뿐

그래서 추상적인 기능을 필요로 하거나 객체로서의 기능이 필요할 때 등의 상황에는 잠시 객체화 해서 사용했다.

위 표처럼 기본값을 객체화 한 것들을 Wrapper Class라 부른다.

기본값을 객체화 하는 것을 박싱이라고 하며, 다시 기본값으로 돌리는 것을 언박싱이라고 한다.

이러한 과정을 조금 더 자연스럽게 보여주기 위한 기능인 오토박싱, 오토언박싱이 존재한다.

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

// 위와 같은 과정을 자연스럽게 만들어주는 AutoBoxing, AutoUnBoxing
// JDK 1.5 버전 이상 지원

Character ch = 'X'; // AutoBoxing
char c = ch; // AutoUnBoxing

 

자바 예제 문제 : https://teamsparta.notion.site/Java-b4548d9136e043cf8ef6127f7f5e12ce

 

Java 예제 문제

다음 코드의 출력은 무엇입니까?

teamsparta.notion.site

↓정답

더보기

1 - B : index는 0부터 세니까 index[2]는 세번째를 가리킨다.

2 - 문제 오류

3 - 문제 오류

1 - a

2 - a,b,c,d

1 - 

class Shape {
    void draw() {
        System.out.println("도형을 그립니다.");
    }
}

class Circle extends Shape {
    @Override
    public void draw() {
    	System.out.println("원을 그립니다.")
    }
}

class Rectangle extends Shape {
    @Override
    public void draw() {
    	System.out.println("사각형을 그립니다.")
    }
}

2 -

interface Vehicle {
    void start();

    void stop();
}

class Car implements Vehicle {

    @Override
    public void start() {
        System.out.println("시동을 켭니다.");
    }

    @Override
    public void stop() {
        System.out.println("시동을 끕니다.");
    }
}

class Bicycle implements Vehicle {

    @Override
    public void start() {
        System.out.println("페달을 밟습니다.");
    }

    @Override
    public void stop() {
        System.out.println("페달을 놓습니다.");
    }
}

 

4주차 숙제

 

시작부터 모르는 메서드가 출현했다. 메서드는 Pattern.match()메서드.

주어진 정규 표현식 패턴과 주어진 문자열을 시작부터 비교하여 매치 여부를 확인하는 메서드이다.

일단 이 메서드를 사용하기 위해선 

import java.util.regex.Pattern;

을 통해 사용 가능하고, 패턴 객체는 Pattern.compile()메서드를 통해 생성하고, 이 메서드는 Matcher 타입을 반환한다.

그 객체는 matcher() 메서드를 통해 검사 결과를 Matcher 클래스 변수에 저장하고, 이후 matches()메서드를 통해 문자열이 일치하는지 확인 할 수 있다. Matcher 타입 또한 import를 해야한다. 아래는 예시다.

import java.util.regex.*;

public class Main {
  public static void main(String[] args) {
    String input = "Hello, world!";
    String pattern = "Hello.*";
    
    Pattern compiledPattern = Pattern.compile(pattern);
    Matcher matcher = compiledPattern.matcher(input);
    
    if (matcher.matches()) {
      System.out.println("Pattern matched!");
    } else {
      System.out.println("Pattern did not match.");
    }
  }
}

위 코드는 Hello 패턴이 일치하므로 matcher.matches()가 ture를 반환해 Pattern matched!가 출력된다.

위에서 pattern이 Hello.*이 아닌 Hello 였다면 false를 반환했을 것이다. 따라서 우리는 문자열에 조건을 넣어 어떤 경우에 일치하는지 조건을 추가할 필요가 있다. 이를 위해 필요한게 정규 표현식이다. 나열하자니 너무 길어서 링크를 참조한다.

https://ko.wikipedia.org/wiki/%EC%A0%95%EA%B7%9C_%ED%91%9C%ED%98%84%EC%8B%9D

 

정규 표현식 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 노란색 강조 부분은 다음 정규식을 사용했을 때 매치된 것이다. 정규 표현식(正規表現式, 영어: regular expression, 간단히 regexp[1] 또는 regex, rational expression)[2][3] 또

ko.wikipedia.org

이를 참고해 우리는 숙제 코드에 있는 

private static final String OPERATION_REG = "[+\\-*/]";
private static final String NUMBER_REG = "^[0-9]*$";

두 문자열이 각각 무엇을 의미하는지 알 수 있다. 

첫 번째는  +, -, *, / 중 하나를 선택하면서도 \\를 통해 문자나 숫자, _를 제외했다.

두 번째는 앞 뒤 공백 없이 0부터 9중에 하나를 0번 이상 반복하겠다.

 

매개변수와 비교할 문자열을 비교하고, 어떻게 반환해야할지 고민했었는데, Pattern.matches를 자세히 알고나니 은근히 쉽게 풀렸다.

public Parser parseFirstNum(String firstInput) throws Exception {
    if (!Pattern.matches(NUMBER_REG, firstInput)) {
        throw new BadInputException("정수값");
    }

    this.calculator.setFirstNumber(Integer.parseInt(firstInput));

    return this;
}

 

'TIL > JAVA' 카테고리의 다른 글

23.05.27  (0) 2023.05.27
23.05.26  (0) 2023.05.26
23.05.24  (0) 2023.05.24
23.05.23  (0) 2023.05.23
23.05.22  (0) 2023.05.22