본문 바로가기

항해 99/Java

Java - 오류 및 예외 처리, 제네릭, Collection

오류 및 예외 처리

  • 오류(Error) : 일반적으로 회복이 불가능한 문제
    • 시스템 레벨 도는 환경적인 이유로 발생
    • 코드의 문제로 발생하는 경우도 있으나, 발생하면 일반적으로 회복이 불가능
    • 에러가 발생한 경우 어떤 에러로 프로그램이 종료되었는지 확인하고 대응
  • 예외(Exception) : 일반적으로 회복이 가능한 문제
    • 회복이 가능하다는 전제는 예외가 발생할 수 있다는 것을 인지하고 대응했을 것
    • 코드레벨에서 할 수 있는 문제사항에 대한 대응은 "예외 처리"에 속함

예외의 종류

  • 코드 실행 관점에서의 예외 종류
    • 컴파일 에러(예외)
      • .java 파일을 .class 파일로 컴파일할 때 발생하는 에러
      • 대부분 자바 프로그래밍 언어의 규칙을 어겨서 발생
      • 없는 클래스 호출, 접근이 불가능한 프로퍼티나 메서드에 접근하는 경우
      • 해결방법은 문법에 맞게 다시 작성하는 것
    • 런타임 에러(예외)
      • 주로 다루게 될 에러(예외)
      • 문법적 오류는 아니라, 컴파일은 잘 되었지만, "프로그램"이 실행 도중 맞닥뜨리게 되는 예외
  • 예외처리 관점에서의 예외 종류
    • 확인된 예외(Checked Exception)
      • 컴파일 시점에 확인하는 예외
      • 반드시 예외 처리를 해줘야 하는 예외
      • 확인된 예외에 대한 예외 처리를 하지 않을 경우 컴파일 에러 발생
    • 미확인된 예외(Unchecked Exception)
      • 런타임 시점에 확인되는 예외
      • 예외 처리가 반드시 필요하지 않은 예외

 

Property(프로퍼티) : 객체의 상태 또는 데이터를 나타내는 필드(멤버 변수)에 대한 접근을 제어하는 메서드(함수)를 의미.

객체의 필드에 직접 접근하는 대신 사용되어, read, write, 기타 동작을 캡슐화하고 제어하는 방법을 제공

 

구성요소

  • Getter 메서드 : 객체의 특정 필드 값을 반환하는 메서드, 주로 필드 값을 읽을 때 사용, getPropertyName 같은 이름으로 명명
  • Setter 메서드 : 객체의 특정 필드에 값을 설정하는 메서드, 필드 값을 변경할 때 사용되며, setPropertyName 같은 이름으로 명명

장점

  • 캡슐화 : 객체의 내부 상태를 보호하고, 외부에서는 메서드를 통해서만 상태를 변경할 수 있게 함(객체의 데이터와 행위를 하나로 묶어 관리할 수 있음)
  • 유효성 검사 : Setter 메서드를 통해 객체의 필드에 설정되는 값을 검사하고, 유효하지 않은 값이 설정되는 것을 방지
  • 추상화 : 사용자는 객체의 내부 구현을 몰라도 Getter와 Setter 메서드를 통해 객체와 상화작용할 수 있음(객체 사용의 복잡성을 줄이고, 사용자에게 간결한 인터페이스 제공)
  • 유연성 : 프로퍼티의 구현을 변경해도 프로퍼티의 인터페이스는 그대로 유지할 수 있어, 코드의 수정 없이 내부 구현을 개선하거나 변경할 수 있음

예시

public class Person {
    private String name; // 'name' 필드는 직접 접근 불가

    // Getter 메서드
    public String getName() {
        return name;
    }

    // Setter 메서드
    public void setName(String name) {
        this.name = name;
    }
}

 

 

 

예외 처리

예외 클래스

package week04.sample01;

// 예외 클래스를 만들어서 예외 정의
public class OurBadException extends Exception {

    public OurBadException() {
        super("위험한 행동을 하면 예외처리를 꼭 해야함!!");
    }
}

 

package week04.sample01;

public class OurClass {
    private final boolean just = true;

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

 

throws throw
메서드 이름 뒤에 붙어 이 메서드가 어떤 예외사항을 던질 수 있는지 알려주는 예약어 메서드 안에서, 실제로 예외 객체를 던질 때 사용하는 예약어
여러 종류의 예외사항을 적을 수 있음 실제로 던지는 예외 객체 하나와 같이 써야 함
  일반 메서드의 return 키워드처럼 throw 아래의 구문들은 실행되지 않고, throw문과 함께 메서드가 종료

 

예외 처리 사용 예시

package week04.sample01;

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

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

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

 

 

 

예외 클래스 구조 이해

Throwable Class

  • 모든 객체의 원형인 Object 클래스에서 시작
  • "문제 상황"을 뜻하는 Throwable 클래스가 Object 클래스를 상속
  • Throwable 클래스의 자식으로 Error와 Exception 클래스가 있음
  • Error 클래스와 Exception 클래스는 각각 IOError 클래스, RutimeException 클래스와 같이 구분하여 처리

Exception 클래스 구조

 

자바의 수많은 에러 구현체

 

Java 예외 리스트

// 참고: https://programming.guide/java/list-of-java-exceptions.html

 

 

Chained Exception, 실제 예외 처리하는 방법

  • 연결된 예외
    • 예외는 다른 예외를 유발할 수 있습니다.
    • 예외 A가 예외 B를 발생시켰다면, 예외 A는 B의 원인 예외
    • 원인 예외를 새로운 예외에 등록한 후 다시 새로운 예외를 발생시키는데, 이를 연결 예외라 함
  • 원인 예외를 다루기 위한 메서드
    • initCause( )
      • 지정한 예외를 원인 예외로 등록하는 메서드
    • getCause( )
      • 원인 예외를 반환하는 메서드
package week04.sample01;

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

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

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

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

// 출력
//Cause by: java.lang.NumberFormatException: 진짜 예외이유

 

실제 예외 처리

1. 예외 복구

public String getDataFromAnohterServer(String dataPath) {
   try {
       return anotherServerClient.getData(dataPath).toString();
   } catch (GetDataException e) {
       return defaultData;
   }
}
  • 실제로 try-catch로 예외 처리 및 프로그램 정상 상태로 복구
  • 가장 기본적인 방식이지만, 현실적으로 복구가 가능한 상황이 아닌 경우가 많거나 최소한의 대응만 가능한 경우가 많기 때문에 자주 사용하지 않음

2. 예외 처리 회피

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

public void someIrresponsibleMethod() throws Exception {
    this.someMethod();
}
  • 관심사를 분리해서 한 레이어에서 처리하기 위해서 에러를 회피해서 그대로 흘려 보내는 경우

3. 예외 전환

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

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

 

 

 

Generic(제네릭) - 타입을 더 유연하게 다루기

  • 제네릭 효용
    • 타입 언어에서 "중복되거나 필요없는 코드를 줄여 주는 것"
    • 그러면서도 타입 안정성을 해치지 않는 것
  • 타입 언어에서의 중복되거나 필요 없는 코드란?
    • 자바는 타입을 지정해줘야 하는 언어라 똑같은 로직을 수행하는 함수를 타입을 지정해야 되는 이유로 세 차례나 구현해야 함

제네릭 문법

package week04.gen;

// 1. 제네릭은 클래스 또는 메서드에 사용 가능
// <>안에 들어가야 할 타입을 명시
public class Generic<T> {
    // 2. 내부 필드에 String
    private T t;

    // 3. method의 return type도 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);
    }
}

 

제네릭 용어 정리

  • Generic<T>의 클래스처럼, 제네릭을 사용한 클래스를 제네릭 클래스라 함
  • 제네릭에서 <> 사이에 들어가는 변수명 T는 타입 변수라고 함
  • Generic 클래스를 원시 타입이라 함

제네릭 제한

  • 객체의  static 멤버에 사용할 수 없음
    • 타입 변수는 인스턴스 변수로 간주되고, 모든 객체에 동일하게 동작해야 하는 static 필드 특성상 사용할 수 없음
  • 제네릭 배열을 생성할 수 없음

제네릭 문법

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


Generic<Long, Integer, String> instance = new Generic();
instance.multiTypeMethod(longVal, intVal);
  • 다형성, 상속과 타입의 관계는 그대로 적용
    • 제네릭 부모 클래스로 제네릭 타입 변수를 지정하고, 그 안에 자식 클래스를 넘기는 것은 가능
public class ParkingLot<T extends Car> { ... }

ParkingLot<BMW> bmwParkingLot = new ParkingLot();
ParkingLot<Iphone> iphoneParkingLot = new ParkingLog(); // error!
  • 와일드 카드를 통해 제네릭의 제한을 구체적으로 정할 수 있음 
    • <? extends T> : T와 그 자손들만 사용 가능
    • <? super T> : T와 그 조상들만 가능
    • <?> : 제한 없음
    • 제한을 하는 이유는 다형성 때문
  • 메서드를 스코프로 제네릭을 별도로 선언 가능
static <T> void sort(List<T> list, Comparator<? super T> c) { ... }
  • 반환 타입 앞에 <> 제네릭을 사용한 경우, 해당 메서드에만 적용되는 제네릭 타입 변수를 선언할 수 있음
  • 타입 변수를 클래스 내부의 인스턴스 변수 취급하기 때문에 제네릭 클래스의 타입 변수를 static 메서드에는 사용할 수 없지만, 제네릭 메서드의 제네릭 타입 변수는 해당 메서드에만 적용되 메서드 하나를 기준으로 선언하고 사용 가능
  • 같은 이름의 변수를 사용했다고 해도 제네릭 메서드의 타입 변수는 제네릭 클래스의 타입 변수와 다름
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는 추상적 자료 구조로서, 순서를 가지고, 일렬로 나열한 원소들의 모임(순서가 있고, 중복을 허용).

  • 검색에는 유리하고, 수정/삭제는 불리한 자료구조

추상적 자료구조인 리스트는 개념적으로 보통 연산들을 지원해야 함

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

Collection 구조

 

 

Wrapper 객체

원시 타입

  • 추상적인 기능을 사용하려거나, 기본형 값 대신 객체로 저장해야 하거나, 객체로의 "기능"이 필요할 대 원시형 값들을 잠시 객체로 만들어 사용

 

 

예외처리 연습 계산기 예외 처리 추가

  • Parser 클래스와 Main 클래스에 예외 처리 구문 추가
package week04.homework;

import java.util.regex.Pattern;

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

    private final Calculator calculator = new Calculator();

    public Parser parseFirstNum(String firstInput) throws Exception {
        // 구현 1.
        if (!Pattern.matches(NUMBER_REG, firstInput)) {
            throw new BadInputException("정수값");
        }
        this.calculator.setFirstNumber(Integer.parseInt(firstInput));

        return this;
    }

    public Parser parseSecondNum(String secondInput) throws Exception {
        // 구현 1.
        if (!Pattern.matches(NUMBER_REG, secondInput)) {
            throw new BadInputException("정수값");
        }
        this.calculator.setSecondNumber(Integer.parseInt(secondInput));

        return this;
    }

    public Parser parseOperator(String operationInput) throws Exception {
        // 구현 1.
        if (!Pattern.matches(OPERATION_REG, operationInput)) {
            throw new BadInputException("연산자");
        }

        switch (operationInput) {
            case "+" -> this.calculator.setOperation(new AddOperation());
            case "-" -> this.calculator.setOperation(new SubstractOperation());
            case "*" -> this.calculator.setOperation(new MultiplyOperation());
            case "/" -> this.calculator.setOperation(new DivideOperation());
        }
        return this;
    }

    public double executeCalculator() {
        return calculator.calculate();
    }
}
package week04.homework;

public class Main {
    public static void main(String[] args) {
        boolean calculateEnded = false;
        // 구현 2.
        while (!calculateEnded) {
            try {
                calculateEnded = CalculatorApp.start();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

 

계산기 코드 파일

homework.zip
0.00MB

 

 

예외 처리문 추가

Exception 클래스

package samples;

public class FoolException extends RuntimeException {
}

 

예외 처리 - throws 미사용

package samples;

public class Sample {
    public void sayNick(String nick) {
        try {
            if ("바보".equals(nick)) {
                throw new FoolException();
            }
            System.out.println("당신의 별명은 " + nick + " 입니다.");
        } catch (FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        sample.sayNick("바보");
        sample.sayNick("야호");
    }
}

 

예외 처리 - throws 사용

package samples;

public class Sample {
    public void sayNick(String nick) throws FoolException {
        if ("바보".equals(nick)) {
            throw new FoolException();
        }
        System.out.println("당신의 별명은 " + nick + " 입니다.");
    }

    public static void main(String[] args) {
        Sample sample = new Sample();
        try {
            sample.sayNick("바보");
            sample.sayNick("야호");
        } catch (FoolException e) {
            System.err.println("FoolException이 발생했습니다.");
        }
    }
}

 

throw와 throws 차이

  • throw : 메서드 내에서 예외를 발생시키는 데 사용 (throw new FoolException)
  • throws : 메서드 선언부에서 사용되며, 해당 메서드가 처리하지 않은 예외를 호출자에게 전달함을 나타냄(public void sayNick(String nick) throws FoolException)

예외 처리 위치는 중요함

프로그래밍할 대 예외 처리 위치는 프로그램의 수행여부를 결정함

  • sayNick 메서드에서 예외 처리를 할 경우 sayNick("바보"), sayNick("야호") 두 문장이 모두 수행됨("바보") 문장 수행 시 Exception이 발생하지만 그 다음 문장도 수행됨
  • main 메서드에서 예외 처리를 한 경우에는 두 번째 문장이 수행되지 않음, 첫 번째 문장에서 예외가 발생하여 catch 문으로 빠져나감

 

트랜잭션(transaction)

하나의 작업 단위를 뜻함

쇼핑몰의 '상품발송' 이라는 트랜잭션에는 다음과 같은 작업들이 있음

  • 포장
  • 영수증 발행
  • 발송

 

운영자는 이 3가지 일 중 하나라도 실패하면 3가지 모두 취소하고 '상품발송'전의 상태로 되돌려야 함(모두 취소하지 않을 경우 데이터 정합성이 크게 흔들리게 됨, 모두 취소하는 행위를 rollback이라 함)

  • 데이터 정합성 : 데이터들의 값이 서로 일관성 있게 일치하는 것

3가지 작업 중 1개라도 실패하면 모든 작업을 취소하기 위한 예외 처리 슈도 코드

 

잘한 경우

상품발송() {
    try {
        포장();
        영수증발행();
        발송();
    }catch(예외) {
        모두취소();  // 하나라도 실패하면 모두 취소한다.
    }
}

포장() throws 예외 {
   ...
}

영수증발행() throws 예외 {
   ...
}

발송() throws 예외 {
   ...
}

 

못한 경우

상품발송() {
    포장();
    영수증발행();
    발송();
}

포장(){
    try {
       ...
    }catch(예외) {
       포장취소();
    }
}

영수증발행() {
    try {
       ...
    }catch(예외) {
       영수증발행취소();
    }
}

발송() {
    try {
       ...
    }catch(예외) {
       발송취소();
    }
}
  • 이 경우 1가지 작업은 예외 발생으로 실패했는데 다른 메서드는 실행될 수 있음

ArithmeticException 예제

package samples;

public class Exception02Main {
    public static void main(String[] args) {
        int num1 = 100;
        int num2 = 0;
        int result = 0;

        // 조건문을 사용한 예외 처리
        if (num2 != 0) {
            result = num1 / num2;
        } else {
            System.out.println("0으로 나눌 수 없습니다.");
        }
        System.out.println("result = " + result);
        System.out.println();

        // try-catch 문을 사용한 예외 처리
        try {
            result = num1 / num2;
            System.out.println("result = " + result);
        } catch (ArithmeticException e) {
            System.out.println("0으로 나누면 발생하는 Exception");
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
    }
}

 

한 번에 여러 개의 예외 처리 예제

package samples;

public class Main {
    public static void main(String[] args) {
        int num1 = 100, num2 = 2, result = 0;
        String str = "Java";
        int[] numbers = new int[10];

        try {
            result = num1 / num2;
            System.out.println("result = " + result);

            int length = str.length();
            System.out.println("length = " + length);

            numbers[10] = 11111;
            System.out.println("numbers = " + numbers[10]);
        } catch (ArithmeticException e) {
            System.out.println("산술 연산 예외 : " + e.getMessage());
        } catch (NullPointerException e) {
            System.out.println("NPE : " + e.getMessage());
        } catch (Exception e) {
            System.out.println("Exception = " + e.getMessage());
            // Exception은 다른 예외보다 상위 클래스에 속해 있으므로 가장 마지막에 작성
        }
    }
}

 

연산자를 이용해 multi-catch를 사용할 수 있음

public class Main {

	public static void main(String[] args) {
		try {
			String str = null;
			str.length();
			int n = 100 / 0;
		} catch (ArithmeticException | NullPointerException | ArrayIndexOutOfBoundsException e) {
			System.out.println(e.getClass());
			System.out.println(e.getMessage());
		}
	}
}

 

try-catch-finally 예제

package samples;

import java.util.InputMismatchException;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {

        System.out.println("# 1 try {} 직전 ");
        
         try {
             System.out.println("# 2 try {} 시작");

             int[] numbers = new int[10];
             numbers[10] = 100;

             System.out.println("# 3 try {} 종료");
         } catch (ArrayIndexOutOfBoundsException e) {
             System.out.println("# 4 catch {}");
             System.out.println("예외 메시지 : " + e.getMessage());
         } finally {
             System.out.println("# 5 finally {}");
         }

        System.out.println("# 6 try 종료 후");
        System.out.println();

        Scanner sc = new Scanner(System.in);
        try {
            System.out.println("정수를 입력하세요.");
            sc.nextLine();
            System.out.println("try 블록 종료");
        } catch (InputMismatchException e) {
            System.out.println("예외 메시지 : " + e.getMessage());
            return;
        } finally {
            System.out.println("finally 수행");
            sc.close();
        }
    }
}

 

 

 

예외 처리 잘하는 9가지 방법

참조 : https://hbase.tistory.com/157

1. 리소스 정리

  • try-catch 구문에서 리소스를 열었다면, finally 블록에서 리소스를 정리하거나 try-with-resources 구문을 이용해야 한다.

 

2. 더 자세한 예외

  • 메서드를 정의할 때, 이 메서드에서 발생할 수 있는 예외를 throw 구문으로 명시하도록 되어 있다(메서드에서 발생할 수 있는 예외는 최대한 자세한 예외를 명시하는 것이 좋다).
    • Exception 클래스로 크게 포함하면 메서드를 호출하는 쪽에서 예외처리하는 코드가 복잡해짐
public void exceptableMethod() throw Exception; // Exception 사용으로 큰 범위로 가져가는 것 보다

public void execptableMethod() throw NumberFormatException; // 자세한 예외를 명시하는 것이 좋음

 

 

3.  javadoc으로 설명하라

  • 메서드가 예외를 발생시킬 수 있는 경우, 어떤 경우에 어떤 예외가 발생하는지 javadoc 문서를 통해서 기술해야 함, throws 절에 최대한 구체적인 예외를 사용하더라도 어떤 상황에서 예외가 발생할 수 있는지 글로 자세히 설명해줘야 메서드를 호출하는 쪽에서 적당한 예외 코드를 작성할 수 있음.
/**
 * 이 메소드는 어떨때 사용하는 메소드입니다.
 *
 * @param input 입력 값
 * @throws MyException 어떤어떤 경우에 이 예외가 발생합니다.
 */
public void myMethod(String input) throws MyException { ... }

 

 

4. 메시지를 자세하게 적는다

  • 사용자 정의 예외를 만들 때, 예외가 발생한 상황에 대한 메시지를 적을 수 있다. 이 때, 어떤 상황에서 예외가 발생했는지 한 두 문장 정도로 간결하게 잘 적어둬야 한다.
  • 단, 예외의 이름에서 어떤 상황인지 알 수 있을 때에는 너무 많은 정보를 메시지에 적지 않아도 된다. 그렇기 때문에 최대한 상황을 잘 설명할 수 있는 예외 이름을 짓는 것이 중요하다.
//자세한 메시지를 생략해도 되는 예
try {

    new Long("xyz");

} catch (NumberFormatException e) {

    System.out.println(e);

}
// 실행 결과
//java.lang.NumberFormatException: For input string: "xyz"

 

 

5. catch 절 순서

  • try-catch 블럭에서 여러 예외가 발생할 수 있는 경우 좀 더 상세한 예외부터 처리해야 함(예외가 상속 관계에 있을 경우 앞쪽 catch 절에서 더 넓은 범위의 예외를 처리하면, 뒤 쪽 catch 절이 쓸모 없어짐)
//올바른 catch 절 순서
try {

    method();

} catch (IllegalArgumentException e) {

    log.error(e);

} catch (Exception e) {

    log.error(e);

}

 

 

6. Throwable은 catch 하지마라

  • Throwable은 모든 예외 클래스의 슈퍼 클래스, catch 절에서 잡아 처리할 수 있으나 예외뿐만 아니라 에러도 잡아서 처리하게 되어 JVM에서 예상치 못한 동작을 할 수 있음.
  • OutOfMemoryError, StackOverflowError 같은 경우 catch 절에서 잡아도 처리할 수 없음.

 

7. 읽고 무시하지 말것

  • 메서드에서 절대 예외가 발생할 수 없는 경우에도 메서드 시그니처를 바꾸지 않으면 나중에 예외가 발생하도록 패치가 될 수도 있음
  • 예외가 발생하지 않는다면 시그니처에서 예외를 제거하는 게 옳음(안 될 경우 로그라도 찍어서 상황을 모니터링할 수 있게 해야 함).
    • 메소드 시그니처(method signature)란 Java 같은 프로그래밍 언어에서 메소드를 정의할 때 사용하는 표현식

 

8. 로그 찍고 다시 던지지 마라

  • 예외를 처리할 때 습관적으로 로그를 남기고, 다시 상위 메서드로 발생한 예외를 던지는 경우 로그가 너무 자주 찍혀서 가독성을 훼손한다
  • 상위로 Throw할 경우 로그를 찍지 말아야 한다. 그 상황에 대한 컨텍스트를 남기고 싶을 경우 예외를 래핑해서 새로운 클래스로 만들고 컨텍스트에 대한 정보를 담아 상위로 던져야 한다.

 

9. 예외를 래핑할 경우 Cause 예외를 담아서 던져라

  • 예외를 래핑해서 사용할 경우 원래 발생했던 예외를 생성자에 넘겨줘야 스택 정보와 메시지, 컨텍스트 정보 등이 상위로 전달된다. 이런 정보가 사라지면 디버깅이 힘들어진다.

예시

try {

    method();
    
} catch (NumberFormatException e) {

    throw new MyException("New Message", e);

}

 

 

 

try-with-resources 구문

  • 자동으로 리소스를 관리할 수 있게 해주는 구문, 리소스를 사용한 후에 명시적으로 닫아줄 필요가 없음
  • java.lang.AutoCloseable 인터페이스나 java.io.Closeable 인터페이스를 구현한 객체를 자동으로 닫아줌
    • 파일, 네트워크 연결, 데이터베이스 연결 등을 안전하고 쉽게 다룰 수 있게 해줌

try-with-resources 기본 구문

try (리소스 선언) {
    // 리소스를 사용하는 코드
} catch (예외 타입 변수명) {
    // 예외 처리 코드
}
  • 리소스 선언 : 하나 이상의 리소스를 선언할 수 있으며, 선언된 리소스는 try 블록이 끝나면 자동으로 닫힘
  • 예외 처리 코드 : try 블록 내에서 발생할 수 있는 예외를 처리

예제 코드

import java.io.IOException; // 입출력간 예외 처리
import java.nio.file.Files;
import java.nio.file.Paths;

public class TryWithResourcesExample {
    public static void main(String[] args) {
        // 파일 경로 지정
        String filePath = "example.txt";

        // try-with-resources 구문을 사용하여 파일을 자동으로 닫음
        try (java.io.BufferedReader reader = Files.newBufferedReader(Paths.get(filePath))) {
            String line = null;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            } // try 블록 종료 시 BufferedReader 객체는 자동으로 닫힘
        } catch (IOException e) { // 파일 읽기 작업 중 발생할 수 있는 예외를 처리
            e.printStackTrace(); // 오류의 원인을 출력
        }
    }
}

//Files.newBufferedReader(Paths.get(filePath)) filePath 경로에 있는 파일로부터 텍스트를 읽기 위한 BufferedReader를 생성
//readline 메서드는 파일의 내용을 한 줄씩 읽음
  • Files.newBufferedReader 메소드를 사용해 BufferedReader 객체를 생성하고, 파일의 내용을 줄 단위로 읽어 콘솔에 출력합니다. try-with-resources 구문 덕분에 BufferedReader는 자동으로 닫히므로, 개발자는 리소스를 명시적으로 관리하는 데 신경 쓸 필요가 없습니다.

 

연결된 예외 예제

public class ChainedExceptionEx {
    //execute 메서드 - install 메서드 호출, 발생할 수 이쓴 예외 처리
    // 예외 발생 시 stackTrace 출력
    public static void execute() {
        try {
            install();
        } catch (InstallException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // install 메서드 - 설치 프로세스를 시뮬레이션
    // startInstall 과 copyFiles 메서드를 순서대로 호출
    // SpaceException, MemoryException 발생 시 이를 InstallException으로 변환 후
    // initCause 메서드를 사용해 원인 예외로 설정하고 변경 된 예외를 다시 던짐 (연쇄 예외 처리의 예)
    // finally 블록에서는 설치 프로세스에 사용된 임시 파일을 삭제하는 deleteTempFiles 메서드 호출
    // 예외의 발생 여부와 관계 없이 정리 작업 보장(finally 블록이라)
    private static void install() throws InstallException {
        try {
            startInstall();
            copyFiles();
        } catch (SpaceException se) {
            InstallException ie = new InstallException("설치 중 예외 발생");
            ie.initCause(se);
            throw ie;
        } catch (MemoryException me) {
            InstallException ie = new InstallException("설치 중 예외 발생");
            ie.initCause(me);
            throw ie;
        } finally {
            deleteTempFiles(); // 설치에 사용된 임시 파일 삭제
        }
    }


    // startInstall 메서드
    // 설치를 시작하는 메서드, 설치에 필요한 공간과 메모리가 충분한지 검사
    // 충분한 공간이 없으면 SpaceException, 충분한 메모리가 없으면 MemoryException 던짐
    // enoughMemory가 false를 반환, MemoryException 발생
    private static void startInstall() throws SpaceException, MemoryException {
        if (!enoughSpace()) {
            throw new SpaceException("설치할 공간이 부족해요.");
        }
        if (!enoughMemory()) {
            throw new MemoryException("메모리가 부족해요.");
            //throw new RuntimeException(new MemoryException("메모리가 부족해요."))
        }
    }


    // methods
    // copyFiles 메소드는 파일 복사를 시뮬레이션
    private static void copyFiles() {
        System.out.println("파일을 복사해요");
    }

    // deleteTempFiles 메소드는 임시 파일을 삭제하는 작업을 시뮬레이션
    // 예외 발생 여부와 관계없이 실행
    private static void deleteTempFiles() {
        System.out.println("임시 파일을 삭제해요");
    }

    // enoughSpace는 항상 true를 반환. 즉, 충분한 공간이 있다고 가정
    private static boolean enoughSpace() {
        return true;
    }

    // enoughMemory는 false를 반환. 즉, 충분한 메모리가 없다고 가정
    private static boolean enoughMemory() {
        return false;
    }

    // 사용자 정의 예외 클래스
    // InstallException은 설치 과정 중 발생하는 일반적인 예외를 나타냄
    static class InstallException extends Exception {
        InstallException(String message) {
            super(message);
        }
    }

    static class SpaceException extends Exception {
        SpaceException(String message) {
            super(message);
        }
    }

    static class MemoryException extends Exception {
        MemoryException(String message) {
            super(message);
        }
    }
}

'항해 99 > Java' 카테고리의 다른 글

WIL - 2  (0) 2024.02.18
페어 프로그래밍 - 코딩 테스트 5  (1) 2024.02.17
페어 프로그래밍 - 코딩 테스트 4  (1) 2024.02.16
페어 프로그래밍 - 코딩 테스트 3  (0) 2024.02.15
페어 프로그래밍 - 코딩 테스트 2  (0) 2024.02.14