2022.07.28/ 자바 내용/예외처리

Jimin·2022년 7월 28일
0

비트캠프

목록 보기
11/60
post-thumbnail

알고리즘

  • for문을 이용한 덧셈은 한 줄의 곱셈으로 변형할 수 있다.
    -> 이는 시간 복잡도를 상수로 만들어준다.

예외 처리: 발생 → 보고 → 처리

예외 처리 문법

try{
	예외가 발생할 수 있는 코드
} catch(예외 정보를 받을 파라미터){
	예외 처리 코드
}

예외 처리 방법 - 둘 중 하나임

  1. 직접 처리
  • 예외가 발생할 수 있는 메소드 내부에서 try-catch 실행
void m() {
	try{
    	예외를 발생시키는 코드
    }catch(Throwable ex){
    	예외 처리 코드
    }
}
  1. 위임
  • 예외를 처리하지 않고, 예외 표시를 하여 m()을 호출한 쪽에서 try-catch를 사용해 예외를 처리하도록 유도한다.
  • 예외 표시는 메소드 명 옆에 throws 를 붙여서 표시한다.
  • 예외 타입에는 Throwable 클래스 타입(서브 클래스)이 들어가야 한다.
void m() throws 예외타입{ 
	예외를 발생시키는 코드
}

예외를 처리하는 방법1 - 직접 처리

- 리턴값 이용

  • 예외가 발생했을 때 시스템을 멈추지 않는 방법
  • 리턴 값을 통해서 예외 상황을 호출자에게 알린다.
  • 리턴 값을 검사하여 예외 상황에 대해 조치를 취한다.
  • 예외 정보를 받을 파라미터: java.lang.Throwable 타입

1단계 - 예외가 발생하는 Prompt 클래스에서 예외를 처리하기

  • Prompt 클래스 변경
    • inputInt() 메서드에 예외 처리 문법 적용
    • 예외가 발생하면 리턴 값으로 예외 상황을 알린다.
      → 리턴 값으로 사용자가 입력할 것 같지 않은 아주 독특한 값 리턴, -1, -12121212, ...
    • 예외 정보를 즉시 출력한다.
      • 따라서, 호출자가 원하는 방식으로 출력하지 못하는 경우가 발생할 수 있다.
public static int inputInt(String title) {
    System.out.print(title);
    String str = keyboardInput.nextLine();
    try {
      return Integer.parseInt(str); 
    }catch(Throwable ex) {
      // 예외처리:
      // => 예외가 발생했음을 알리는 메시지를 출력한다.
      System.out.println("숫자가 아닙니다.");
      // 예외 처리를 한 후에도 메서드는 값을 리턴해야 한다.
      // 그런 사용자가 입력할 법한 값을 리턴하면 호출하는 쪽에서는 예외가 발생했는지 모른다.
      // 그래서 다음과 같이 사용자가 입력할 것 같지 않은 아주 독특한 값을 리턴한다.
      return -1121212;
    }
  }
  • 예외를 발생시킬 수 있는 메서드를 호출할 때 항상 그 리턴 값을 검사한다.
 int mainMenuNo = Prompt.inputInt("메뉴를 선택하세요[1..6](0: 종료) ");
      // 메뉴 번호를 잘못 입력하는 상황이 발생했을 때, inputInput() 리턴 값 12121212를 리턴할 것이다.
      // 이 경우를 고려해서 처리한다.
      if(mainMenuNo == -12121212) {
        continue;
      }

→ 예외가 발생했음을 알리는 메시지를 출력한다.

// Prompt 클래스
System.out.println("숫자가 아닙니다.");

위와 같이 예외가 발생한 지점(Prompt 클래스)에서 출력을 하는 방식호출자(BoardHandler)의 예외 처리를 방해할 수 있다.
(메뉴 번호가 옳지 않습니다만 출력하고 싶은데, 숫자가 아닙니다 까지 출력하고 있다.)

⇒ 따라서, 호출한 쪽에서 출력을 하든 무시를 하든 적절하게 처리하라고 단순히 리턴값을 통해 예외 상황만 알리는것이 좋다.


예외를 처리하는 방법2 - 위임하기

  • 예외가 발생했을 때 시스템을 멈추지 않는 방법
  • 예외가 발생하는 메서드를 호출할 쪽에서 처리하기
  • 프로그램 동작에 따라 예외처리 코드를 두는 곳이 달라진다.
  • 존재하는 번호외에 다른 번호 입력시, 해당 메뉴나 게시판이 없다고 뜬다.
  • 숫자 이외의, 문자열 입력하면 옳지 않은 값이라고 뜬다.

1단계 - main 메뉴를 잘못 입력 했을 때 발생한 예외를 처리한다.

  • App 클래스 변경
    • main 메뉴 번호를 잘못 입력했을 때 예외를 처리한다.
    • 메뉴 번호 입력 받는 부분에 try-catch 적용
try {
        int mainMenuNo = Prompt.inputInt("메뉴를 선택하세요[1..6](0: 종료) ");

        switch (mainMenuNo) {
          case 0: break loop;
          case 1: // 게시판
            boardHandler.execute();
            break;
          case 2: // 독서록
            readingHandler.execute();
            break;
          case 3: // 방명록
            visitHandler.execute();
            break;
          case 4: // 공지사항
            noticeHandler.execute();
            break;
          case 5: // 일기장
            diaryHandler.execute();
            break;
          case 6: // 회원
            memberHandler.execute();
            break;
          default: System.out.println("메뉴 번호가 옳지 않습니다!");
        } // switch
      }catch(Throwable ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }

2단계 - 게시판 메뉴를 잘못 입력 했을 때 발생한 예외를 처리한다.

  • BoardHandler 클래스 변경
    • 게시판 메뉴 번호를 잘못 입력 했을 때 예외를 처리한다.
    • execute() 메서드에 try-catch 적용
try {
        int menuNo = Prompt.inputInt("메뉴를 선택하세요[1..5](0: 이전) ");
        displayHeadline();

        switch (menuNo) {
          case 0: return;
          case 1: this.onList(); break;
          case 2: this.onDetail(); break;
          case 3: this.onInput(); break;
          case 4: this.onDelete(); break;
          case 5: this.onUpdate(); break;
          default: System.out.println("메뉴 번호가 옳지 않습니다!");
        }

        displayBlankLine();

      }catch(Throwable ex) {
        System.out.printf("예외 발생: %s\n", ex.getMessage());
      }

3단계 - 게시판 조회, 변경, 삭제 메뉴를 잘못 입력 했을 때 발생한 예외를 처리한다.

  • BoardHandler 클래스 변경
    • onDetail(), onUpdate(), onDelete() 에도 적용
      • 이 메서드에서 예외가 발생했을 때 처리
      • 이 메서드들에 try-catch 적용
  • 잘못된 값을 입력 받았을 때 올바른 입력 값을 받을 때까지 입력을 받고 싶다면, while문으로 감싸줘야 한다.
    → while문으로 감싸주지 않으면 try-catch문이 작동하더라도 오류를 잡고 그냥 종료해버린다.
  • 예외 처리 코드는 프로그램 동작에 따라 적용이 달라진다.
  • try-catch문으로 묶을 때는 정상적으로 작동하는 코드 전체를 묶어주는 것이 나중에 유지보수에 좋다.
  • catch() 소괄호 안에는 꼭 Throwable 타입이 들어와야 한다.
    • 더 자세하게, Throwable 타입의 하위클래스 타입인 NUllpoint exception같은 건 되는데, Object와 같이 더 포괄적인 것은 안된다.
    • 빵을 Throwable이라고 할 때, 구체적인 빵 종류는 들어갈 수 있지만, 빵 상자는 못 들어간다고 생각하면 쉽다.
  • onDetail() in BoardHandler class
private void onDetail() {
    System.out.printf("[%s 상세보기]\n", this.title);

    int boardNo=0;
    while(true) {
      try {
        boardNo = Prompt.inputInt("조회할 게시글 번호? ");
        break;
      }catch(Throwable ex0) {
        System.out.println("입력값이 옳지 않습니다!");
      }
      ...
    }
  }
  • onUpdate() in BoardHandler class
private void onUpdate() {
    System.out.printf("[%s 변경]\n", this.title);
    int boardNo=0;
    while(true) {
      try {
        boardNo = Prompt.inputInt("변경할 게시글 번호? ");
        break;
      }catch(Throwable ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }
    ...
}
  • onDelete() in BoardHandler class
private void onDelete() {
    System.out.printf("[%s 삭제]\n", this.title);
    int boardNo=0;
    while(true) {
      try {
        boardNo = Prompt.inputInt("삭제할 게시글 번호? ");
        break;
      }catch(Throwable ex) {
        System.out.println("입력 값이 옳지 않습니다!");
      }
    }
   ...
}


오버라이딩 메서드 호출

  • Object 클래스
    • toString => "클래스명@해시값" 문자열로 출력
Object obj;
obj = new Object();
obj.toString();ObjecttoString() 메소드
  • Object 클래스 ← Member 클래스 (상속)
    • toString => "클래스명@해시값" 문자열로 출력
Member m = new Member(); 
m.toString();ObjecttoString() 메소드
  • Object 클래스 ← String 클래스 (상속)
    • toString() => 오버라이딩, "저장된 문자열"
String s = new String("Hello");
s.toString();StringtoString() 메소드
  • Object 클래스 레퍼런스에 String 인스턴스 주소 값 넣기
Object x = new String("Hello");
x.toString();
x.charAt(0); ← 컴파일 오류가 난다.
x.toString();
  • 주의! 단, 밑의 코드는, Object의 메서드만 가능하다.
    → 상위 클래스 타입을 갖는 레퍼런스 변수에 하위 클래스 타입의 인스턴스 주소값을 넣는 것
Object x = new String("Hello");
  • Object 레퍼런스에 String 인스턴스 주소를 넣어준 것
x.toString();

↑ x가 실제 가르키는 클래스(String class type)에서부터 메소드를 찾아 올라간다.
String 클래스가 toString()메소드를 오버라이딩 했기 때문에 Object의 메소드가 덮어씌워져서,
String클래스의 메소드가 쓰여지는 것이다.

x.charAt(0); ← 컴파일 오류가 난다.
  • 컴파일러는 레퍼런스 변수의 타입에 존재하는 메서드만 호출이 가능하다.
  • 따라서 레퍼런스 변수 타입에 존재하지 않은 메서드를 호출하면 컴파일 에러가 나게 된다.
  • 컴파일러는 x의 변수에 어떤 객체(인스턴스)가 저장되었는지 관심 없다.
x.toString(); ← 컴파일 OK!
  • 컴파일만 넘긴다면, JVM은 레퍼런스가 실제 가르키는 클래스에서부터 메서드를 찾아 올라간다.결론적으로 위의 x가 가르키는 곳은 String 것이다!

ObjectList list = new ObjectList(); // 추상 클래스가 아니어서 객체로 직접 사용 가능!
    list.add("홍길동");
    list.add("임꺽정");
    list.add("유관순");
    list.add(null); //목록에 null을 넣을 수 있다.
    list.add("안중근");

    Object obj = list.get(0); //  실제 get()이 리턴하는 것은 String 객체이다.
    System.out.println(obj.toString()); 

    System.out.println(list.get(0)); // 따로 레퍼런스 변수 안만들고 바로 넣어줘도 된다.
  • ObjectList 클래스는 추상 클래스가 아니어서 객체로 만들어서 직접 사용가능하다.
  • Object 타입인 목록에 null 값을 넣을 수 있다.
  • list 인덱스 0에 "홍길동"을 넣었기 때문에 실제 get()이 리턴하는 것은 String 객체이다.
  • obj가 실제 가르키는것은 String 클래스이다. obj는 Object 클래스 타입이었던 것이 맞지만, String클래스가 오버라이딩 했기 때문에 String클래스에서 가져온다.
  • 컴파일러: 현재 레퍼런스 변수가 가르키는 클래스 타입에 사용하려는 메소드가 존재하는 지 여부만 따진다.
    ⇒ 원래 Object에도 toString()이 있기 때문에 컴파일러에게 안걸리는 것이다.
    ⇒ 하지만, obj.char(0) 이 메소드는 컴파일 오류가 난다. Object 클래스에 이 메소드가 존재하지 않기 때문이다.
  • JVM: 컴파일러 실행 후에, 실행을 담당, 인스턴스 타입부터 따져서 올라간다.
    → String 클래스는 toString() 메소드를 오버라이딩 했기 때문에 String 클래스의 toString() 메소드를 가져온다.
    → 만약 String 클래스가 오버라이딩 안했으면 Object 클래스의 toString()이 가져와졌을 것이다.
ObjectList list = new ObjectList();
    list.add("홍길동");
    list.add("임꺽정");
    list.add("유관순");
    list.add(null); //목록에 null을 넣을 수 있다.
    list.add("안중근");

    Object obj = list.get(0);
    System.out.println(obj.toString()); 

    System.out.println(list.get(0));
    obj = list.get(1);
    System.out.println(obj.toString());
    System.out.println(list.get(1)); // 쓸데 없이 변수에 담을 필요 없다. 바로println에게 넘겨주면 된다. 

    obj = list.get(2);
    System.out.println(obj.toString());

println():

  • 파라미터 값이 유효한 인스턴스 주소라면 println() 메소드 내부에서 알아서 toString()을 호출하여 String 타입으로 변환시킨 후 그 리턴값을 출력한다.
    ⇒ 따라서 굳이 toString()을 명시적으로호출할 필요가 없다.
System.out.println(list.get(4).toString());
System.out.println(list.get(4);
⇒ 그래서 이 코드에서 toString() 없어도 되는 것!
  • 파라미터 값이 null이라면 그대로 Null을 출력한다.

Q. 생각해볼 문제!

  • Q. get()이 목록에서 null을 리턴한다면, 예외 상황인가 아니면 그냥 정상적인 상황인가?

3번 인덱스에 null이 들어 있기 때문에 꺼낸 값도 null이다.

System.out.println(list.get(3)); 

100번 인덱스는 유효한 인덱스가 아니기 때문에 get()메소드가 null을 리턴한다.

System.out.println(list.get(100));

Ans. 결론!

  • null을 리턴한다고 해서 무조건 예외상황이 아니다.
  • 의도적으로 null을 저장해놓고 꺼낼 수 있기 때문이다.
  • 따라서 인덱스를 잘못 지정한 경우와 구분해야한다!
  • 인덱스를 잘못 지정했으면, 예외상황이지만, 그밖에는 정상적인 상황이다.
    결론적으로 예외 상황인지 아닌지는 알 수 없다.
  • 따로 구분하지 않았기 때문이다.
  • 리턴값으로는 정상과 예외 상황을 표현할 수 없다.
    ⇒ 리턴값으로는 두 상황을 구분할 수 없다.

!해결책!

  • 정상적인 경우는 정상적으로 해당 값을 리턴한다.
  • 예외 상황일 경우에는 예외 정보를 던진다. ⇒ 예외를 발생시킨다.


예외 상황 알리기

throw 예외 정보를 담은 객체;
  • 예외 정보를 담은 객체? java.lang.Throwable객체를 말한다.
  • Throwable의 서브 클래스도 가능!
    → 빵이 가능하므로 소보루 빵, 단팥빵 다 가능행

  • 위에 밑 줄 친애(throws) 붙은 메소드 호출한 곳에서는 강제로 무조건 예외처리(try-catch로) 해줘야함.
  • 예외가 JVM까지 타고 올라가면 프로그램이 강제 종료된다.
  • 정교하게 통제 ⇒ 리턴값이 예외 상황인지 아닌지를 정확히 구분한다.

예외를 발생시키는 방법

  • 비정상적인 상황을 알릴 때(리턴값으로 예외인지 아닌지를 구분할 수 없을 때) 리턴 값 대신 예외를 발생시키기
  • 메서드 안에서 발생한 예외를 호출자에게 위임하는 방법
  • 메서드에서 발생한 예외를 try-catch로 처리하는 방법
  • 여러 메서드에서 발생한 예외를 한 메서드로 몰아서 처리하는 방법

1단계 - 예외 상황이 발생했을 때 특정값을 리턴 값을 리턴하는 대신, 오류 정보를 던진다.

  • ObjectList 클래스 변경
    • get() 변경: 오류 상황일 때(인덱스가 무효할 때) 예외를 발생시킨다.
    • remove() 변경: 오류 상황일 때(인덱스가 무효할 때) 예외를 발생시킨다.
    • 이 메서드를 호출하는 쪽에서 좀 더 정교하게 제어할 수 있도록 도와준다.
    • 얘를 바꿨으면, BoardList에서 오류가 난다.
      예외를 던지는 메소드를 호출할 때는 무조건 try-catch문으로 감싸줘야한다.
  • get() in ObjectList class
  public Object get(int index) throws Throwable{  // 단, 메서드 선언부에 어떤 예외를 던지는지 표시해야 한다!!
    if(index <0 || index >= size) {
      throw new Throwable("인덱스가 무효합니다!");
    }
    return elementData[index];
  }
  • 예외를 보고하는 메서드인 경우
    • 메서드 선언부에 어떤 예외를 보고하는지 표시해야한다.
  • 인덱스가 무효하면 예외를 발생시킨다.
    • 예외 정보를 객체에 담아서 호출한 쪽으로 던진다.
    • 예외 정보는 던질 수 있는 객체에 담아야 한다!
      • 던질 수 있는 객체? -> jaba.lang.Throwable 객체!
        ⇒ 그래서 클래스 이름이 Throwable 이다.
    • 단, 메서드 선언부에 어떤 예외를 던지는지 표시해야 한다.
  • remove() in ObjectList class
public boolean remove(int index)throws Throwable {
    if(index <0 || index >= size) {
      // 인덱스가 무효할 떄 false 를 리턴하는대신,
      // 예외정보를 호출자에게 던진다.
      // 예외 상황을 호출자에게 보고한다.
      throw new Throwable("인덱스가 무효합니다!");
    }
    for(int i=index+1;i<size;i++) {
      elementData[i-1]=elementData[i];
    }
    elementData[--size] = null;
    return true;
  }

2단계 - 예외 발생시키는 메서드를 호출 했을 때 대응 방안! -> 메서드에 대해 예외 처리 코드를 추가한다.

  • BoardList 클래스변경
    • 메서드에서 예외를 던졌을 때 직접 처리하는 대신 호출자(BoardHandler)에게 위임한다.
  • MemeberList 클래스변경
    • 메서드에서 예외를 던졌을 때 직접 처리하는 대신 호출자(BoardHandler)에게 위임한다.
  • BoardHandler에게 위임하는 이유? 얘가 UI를 처리하는 애니까!
  • get() in BoardList class
  @Override
  public Board get(int boardNo) throws Throwable{
    for (int i = 0; i < size(); i++) {
      Board board = (Board)super.get(i);
      if (board.no== boardNo) {
        return board;
      }
    }
    return null;
  }
  
@Override
  public boolean remove(int boardNo) throws Throwable{
    for (int i = 0; i < size(); i++) {
      Board board = (Board)super.get(i);
      if (board.no == boardNo) {
        return super.remove(i);
      }
    }
    return false;
  }
  • 수퍼 클래스의 get()메서드를 호출했을 때 예외가 발생하면, 서브 클래스의 get()메서드에서 처리할 상황이 아니다.
  • 서브 클래스의 get()을 호출한 쪽에 보고하는 것이 더 낫다.
  • 이럴 경우, 서브클래스에서 try-catch문을 써저 오류를 잡지 말고 메서드 선언부에 발생되는 예외를 표시해서 이 메서드를 호출하는 곳에 오류를 위임하자!

3단계 - XxxList가 던진 예외를 XxxHandler에서 처리한다.

  • ObjectList(BoardList, MemberList)의 get(), remove() 메서드를 호출할 때 try-catch 로 처리한다.
  • BoardHandler 클래스 변경
    • onDetail(), onUpdate(), onDelete() 메서드 각각에서 예외를 처리한다.
  • MemberHandler 클래스 변경
    • onDetail(), onUpdate(), onDelete() 메서드 각각에서 예외를 처리한다.
      • 메서드 안의 코드를 통째로 try{}안에 둔다.
  • onDetail() in BoardList
try {
	Board board = this.boardList.get(boardNo);
    if (board == null) {
        System.out.println("해당 번호의 게시글이 없습니다!");
        return;
     }
      System.out.printf("번호: %d\n", board.no);
      System.out.printf("제목: %s\n", board.title);
      System.out.printf("내용: %s\n", board.content);
      System.out.printf("조회수: %d\n", board.viewCount);
      System.out.printf("작성자: %s\n", board.writer);
      Date date = new Date(board.createdDate);
      System.out.printf("등록일: %tY-%1$tm-%1$td %1$tH:%1$tM\n", date);
      // 정상적일 때 여기까지 진행한다.
    }catch(Throwable ex) {
      System.out.printf("예외 발생: %s\n", ex.getMessage());
    }

4단계 - 메서드에서 발생한 예외를 한 곳에서 처리한다.

  • BoardHandler 클래스 변경
    • onDetail(), onUpdate(), onDelete() 메서드에서 예외를 처리하는 대신,
      이 메서드들을 호출하는 execute() 메서드에서 예외를 한꺼번에 몰아 처리한다.
  • MemberHandler 클래스 변경
    • onDetail(), onUpdate(), onDelete() 메서드에서 예외를 처리하는 대신,
      이 메서드들을 호출하는 execute() 메서드에서 예외를 한꺼번에 몰아 처리한다.
  • execute() in BoardHandler class
public void execute() {
    while (true) {
      System.out.printf("%s:\n", this.title);
      System.out.println("  1: 목록");
      System.out.println("  2: 상세보기");
      System.out.println("  3: 등록");
      System.out.println("  4: 삭제");
      System.out.println("  5: 변경");
      System.out.println();

      try {
        int menuNo = Prompt.inputInt("메뉴를 선택하세요[1..5](0: 이전) ");
        displayHeadline();

        switch (menuNo) {
          case 0: return;
          case 1: this.onList(); break;
          case 2: this.onDetail(); break;
          case 3: this.onInput(); break;
          case 4: this.onDelete(); break;
          case 5: this.onUpdate(); break;
          default: System.out.println("메뉴 번호가 옳지 않습니다!");
        }
        
        displayBlankLine();
      }catch(Throwable ex) {
        System.out.printf("예외 발생: %s\n", ex.getMessage());
      }
    } // 게시판 while
  }
profile
https://github.com/Dingadung

0개의 댓글