본문 바로가기

국비 교육

2020.9.14일자 수업 : 예외 처리, 파일 입출력

 실습 - 예외 처리 

git/eomcs-java-project/mini-pms-29

여태까지는 숫자나 Date를 입력할 때 적절한 형식을 입력하지 않으면 프로그램이 종료됐다. 이제는 잘못된 값을 입력하더라도 프로그램이 중단되지 않도록 예외 처리를 해줄 것이다.

 

훈련 목표

execute() 메서드를 실행하는 와중에 예외가 발생할 수 있는 부분을 찾아 예외 처리를 해준다.

 

1단계 : 실행하여 예외가 발생할 수 있는 부분과 코드를 찾는다. execute가 실행되는 와중에 번호를 입력해야할 때 숫자가 아닌 문자를 입력하면 다음과 같은 에러가 발생한다. 숫자를 입력해야하는 상황이 다양하기 때문에 각각의 parseInt() 메서드에 예외 처리를 해주는 것보다는 App에서 execute() 부분에 예외 처리를 해주는 것이 더 편할 것이다.

Exception in thread "main" java.lang.NumberFormatException: For input string: "d"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.base/java.lang.Integer.parseInt(Integer.java:652)
	at java.base/java.lang.Integer.parseInt(Integer.java:770)
	at com.eomcs.util.Prompt.inputInt(Prompt.java:16)
	at com.eomcs.pms.handler.BoardAddCommand.execute(BoardAddCommand.java:22)
	at com.eomcs.pms.App.main(App.java:108)

2단계 : execute() 실행 부분을 try 블록 안에 넣고, catch 블록에 예외가 발생할 경우 실행할 코드를 집어넣는다. 어떤 종류의 예외가 발생했고, 원인은 무엇인지에 대한 정보를 출력하게 한다.

          default:
            Command command = commandMap.get(inputStr);
            if (command != null) {
              try {
                command.execute();
              
              } catch (Exception e) {
              System.out.printf("명령 처리 중 오류 발생: %s\n%s\n",
                  e.getClass().getName(),
                  e.getMessage());
              }
            } else {
              System.out.println("실행할 수 없는 명령입니다.");
            }
            
            // 예외 발생 시 출력되는 줄
            // 명령 처리 중 오류 발생: java.lang.NumberFormatException
            // For input string: "d"

 예외 처리 

git/eomcs-java-basic/src/main/java com.eomcs.exception

예외 처리 문법이 필요한 이유

예외 처리 문법이 없었을 때는 특별한 값을 지정하여 그 값을 리턴함으로써 예외를 전했다. 

  public static int compute(String op, int a, int b) {
    switch (op) {
      case "+":
        return a + b;
      case "-":
        return a - b;
      case "*":
        return a * b;
      case "/":
        return a / b;
      case "%":
        return a % b;
      default:
        // 만약 유효한 연산자가 아닐 경우 계산 결과는?
        // => 보통 리턴 값으로 알린다.
        return -1;
    }
  }

그러나 이렇게 처리할 경우, 문제점이 두가지 있다.

  • 실제로 정상적인 리턴값으로 -1이 나오는 경우에도 에러가 떠버린다.
public static void main(String[] args) {
    int result = Calculator.compute("-", 6, 7);

    if (result == -1) {
      System.out.println("유효하지 않은 연산자입니다!");
    } else {
      System.out.println(result);
    }
  }
  • 아예 다른 종류의 예외가 존재했을 때, JVM이 프로그램의 실행을 종료할 가능성도 있다.
  public static void main(String[] args) {
    Scanner keyScan = new Scanner(System.in);
    while (true) {
      System.out.print("입력> ");
      String op = keyScan.next();
      if (op.equalsIgnoreCase("quit"))
        break;
      int v1 = keyScan.nextInt();
      int v2 = keyScan.nextInt();

      int result = Calculator2.compute(op, v1, v2);
      if (result == -1212121212) {
        System.out.println("유효하지 않은 연산자입니다!");
      } else {
        System.out.println(result);
      }
    }
    keyScan.close();
  }

 

따라서 예외 처리 문법은

  • 리턴값으로 오류를 알리는 방식의 한계를 극복하기 위해

  • JVM의 실행 종료를 방지하기 위해

고안되었다.

 


예외 처리 문법

  • 예외가 발생했을 때 예외를 client에게 알려주는 문법
    => throw new [Throwable 객체]
    Throwable 나 그 하위 객체가 아닌 것을 던질 수 없다.
  • 예외를 받았을 때 이를 처리하는 문법
    => try {} catch (Throwable 객체) {}
    try 코드를 실행하는 과정에서 던져진 예외 객체를 catch의 파라미터로 받는다.
    catch의 파라미터는 Throwable 나 그 하위 클래스 타입이어야한다.
    catch 블록에는 예외에 대한 조치를 수행하는 코드를 둔다.
public class Exam0110 {

  static void m() {
    // throw new String("예외가 발생했습니다!"); // 컴파일 오류!
    throw new RuntimeException("예외가 발생했습니다!");
  }

  public static void main(String[] args) {

    try {
      m();
    } catch (RuntimeException e) {
      System.out.println(e.getMessage());
    }

    System.out.println("시스템을 종료합니다.");

  }

}

 


예외를 던지는 방법

 

Throwable 클래스에는 Error, Exeption 이라는 서브 클래스가 두 개 있다. 

Exception

Exception은 Application 개발 중에 던지는 예외로 "application 예외"라고 부른다.

적절한 조치를 취한 후 계속 시스템을 실행하게 만들 수 있다.

ex) 배열의 인덱스가 무효한 오류, I/O 오류, SQL 오류, Parse 오류, 데이터 포맷 오류 등

 

 

Exception를 던진다면 반드시 메서드 선언부에 던지는 예외를 선언해야한다. 선언부에 작성하는 객체도 무조건 Throwable의 하위 객체여야한다.

=> 메서드명() throws Exception {}

  static void m1() throws Exception {
    throw new Exception(); // OK!
  }
  
    // Exception 예외를 던질 경우 반드시 메서드 선언부에 표시해야 한다.
  static void m2() { // 컴파일 오류!
    throw new Exception();
  }

  // 메서드의 throws 에 선언할 수 있는 클래스는 Throwable 타입만 가능한다.
  static void m3() throws String {
    throw new String(); // 컴파일 오류!
    // throw 로 던질 수 있는 객체는 오직 java.lang.Throwable 타입만 가능하다.
  }

여러 개의 오류를 던진다면 선언부에 여러개의 오류를 선언해야한다.

   static void m2() throws FileNotFoundException, RuntimeException {
    int a = 100;
    if (a < 0)
      throw new FileNotFoundException(); // OK!
    else
      throw new RuntimeException(); // OK!
  }

Runtime Exception은 Exception의 서브 클래스이지만 메서드 선언부에 예외를 던진다고 표시하지 않아도 된다.

스텔스 모드(비유!) 를 지원하기 위해 만든 예외이다.

스텔스
명사 : 은밀함, 또는 그렇게 움직임
형용사 : (보통 명사 앞에 수식) 은신하는
public class Exam0220 {

  static void m() throws RuntimeException {
    throw new RuntimeException(); // OK!
  }

  static void m2() {
    throw new RuntimeException();
  }
}

Error

JVM이 던지는 예외로 "System 예외"라고 부른다. 이것은 개발자가 사용하는 것이 아니다.

ex) 스택 오버 플로우 오류, VM 관련 오류, AWT 윈도우 관련 오류, 스레드 종료 오류 등

이 오류가 발생하면 현재의 시스템을 즉시 백업하고 실행을 멈춰야한다. JVM에서 오류가 발생한 경우에는 계속 실행해봐야 소용이 없다.

근본적으로 문제 해결이 안된다.

Error는 메서드의 선언부에 어떤 오류를 던지는지 선언해도 되고 안해도 된다.

public class Exam0211 {

  // Error 계열의 예외를 던져서는 안되지만,
  // 혹 던진다고 한다면
  // 다음과 같이 메서드 선언부에 던지는 예외를 표시(선언)해도 되고,
  static void m1() throws Error {
    throw new Error();
    // OK! 하지만 이 계열의 클래스를 사용하지 말라!
    // 왜? JVM 관련 오류일 때 사용하는 클래스이다.
  }

  // 또는 다음과 같이 선언하지 않아도 된다.
  static void m2() {
    throw new Error();
  }
  // 즉 Error 계열의 예외를 던질 경우, 메서드 선언부에 표시하는 것은 선택사항이다.

  public static void main(String[] args) {}

}

공통 분모를 사용하여 퉁치는 방법

메서드에서 발생하는 예외의 공통 수퍼 클래스를 지정하여 여러개의 예외를 한번에 메서드 선언부에 선언할 수 있다. 그러나 호출자에게 각각 어떤 오류가 발생하는지 정확하게 알려주는 것 유지보수에 도움이 된다.

public class Exam0320 {

  static void m1(int i) throws Exception {
    if (i == 0)
      throw new Exception();
    else if (i == 1)
      throw new RuntimeException();
    else if (i == 2)
      throw new SQLException();
    else
      throw new IOException();
  }

 static void m2(int i) throws Exception, RuntimeException, SQLException, IOException {
    if (i == 0)
      throw new Exception();
    else if (i == 1)
      throw new RuntimeException();
    else if (i == 2)
      throw new SQLException();
    else
      throw new IOException();
  }
}

던져진 예외를 처리하는 방법

던져진 예외를 client가 처리하지 않다면, 더욱 상위 client에게 위임할 수 있다. 그렇게 계속 위임하다보면 main 메서드에서도 위임하게 되는데, 그럼 결국 JVM에게 예외가 던져지게 되고, 바로 프로그램을 중단한다. 그러므로 반드시 어느 순간에서는 처리를 해줘야한다.

 

이것을 어느 시점에서 처리하기 위해 try{} catch (Throwable) {} 블록들을 사용한다. try 블록 안에는 예외가 발생할 수 있는 코드를 두고, catch 블록에는 예외가 발생하면 적절한 조치를 수행할 코드를 둔다. catch 블록의 파라미터는 Throwable 타입의 객체가 들어갈 수 있으며 결과적으로는 try 블록을 실행하는 동안 발생한 예외 객체가 파라미터로 들어간다.

 

catch가 받는 예외가 종류별로 여러개가 있다면 한 try 블록에 대하여 각 종류의 예외를 파라미터로 둔 catch문을 여러 개 둘 수 있다. 이렇게 코드를 작성하면 예외가 발생했을 경우 catch 블록들을 위에서부터 차례대로 파라미터의 타입을 검사한다.

package com.eomcs.exception.ex3;

import java.io.IOException;
import java.sql.SQLException;

public class Exam0430 {

  static void m(int i) throws Exception, RuntimeException, SQLException, IOException {
    if (i == 0)
      throw new Exception();
    else if (i == 1)
      throw new RuntimeException();
    else if (i == 2)
      throw new SQLException();
    else if (i == 3)
      throw new IOException();

  }

  public static void main(String[] args) {

    try {
      m(4);
      System.out.println("실행 성공!");
      
    } catch (IOException e) {
      System.out.println("IOException 발생");

    } catch (SQLException e) {
      System.out.println("SQLException 발생");

    } catch (RuntimeException e) {
      System.out.println("RuntimeException 발생");

    } catch (Exception e) {
      System.out.println("기타 Exception 발생");
    }
  }

}

위에서부터 차례로 검증해나가기 때문에 슈퍼 클래스의 파라미터를 서브 클래스의 파라미터보다 위에 두면 어떤 예외 객체도 서브 클래스 예외를 파라미터로 둔 catch블록에는 도달할 수가 없다. 따라서 컴파일 오류가 뜨므로, 서브클래스부터 차례로 나열해야한다.

  public static void main(String[] args) {
    try {
      m(1);

    } catch (Exception e) {

    } catch (IOException e) {

    } catch (SQLException e) {

    } catch (RuntimeException e) {
    
    }
  }
  
  // 컴파일 오류!!

한 catch 블록에서 여러 종류의 예외를 처리하고 싶다면 다음과 같이 or 라는 뜻의 | 를 사용하여 작성할 수가 있다.

  public static void main(String[] args) {
    try {
      // try 블록에서 예외가 발생할 수 있는 메서드를 호출한다.
      m(1);

    } catch (RuntimeException | SQLException | IOException e) {
      // OR 연산자를 사용하여 여러 개의 예외를 묶어 받을 수 있다.
      //
    } catch (Exception e) {

    }
  }

시스템 에러에 대한 안내 메세지를 출력하고 싶은 경우가 아니라면 가능한 Error 계열의 시스템 예외를 받지 않는게 좋다. Error은 시스템 예외로 당장 프로그램이 정상적으로 실행될 수 없는 상태이기 때문이다. 따라서 시스템을 멈추지 않고서는 정상적인 복구가 불가능하다.

package com.eomcs.exception.ex3;

import java.io.IOException;
import java.sql.SQLException;

public class Exam0470 {

  static void m(int i) throws Exception, RuntimeException, SQLException, IOException {
    if (i == 0)
      throw new Exception();
    else if (i == 1)
      throw new RuntimeException();
    else if (i == 2)
      throw new SQLException();
    else if (i == 3)
      throw new IOException();
    else if (i < 0)
      throw new Error(); // 시스템 오류가 발생하다고 가정하자!
  }

  public static void main(String[] args) {
    try {
      // try 블록에서 예외가 발생할 수 있는 메서드를 호출한다.
      m(-1);

    } catch (Exception e) {
      System.out.println("애플리케이션 예외 발생!");

    } catch (Error e) {
      System.out.println("시스템 예외 발생!");
    }
  }
}

마찬가지로 Exception과 Error를 둘 다 받을 수도 있는 Throwable 변수를 사용해서도 안된다. 

package com.eomcs.exception.ex3;

import java.io.IOException;
import java.sql.SQLException;

public class Exam0471 {

  static void m(int i) throws Exception, RuntimeException, SQLException, IOException {
    if (i == 0)
      throw new Exception();
    else if (i == 1)
      throw new RuntimeException();
    else if (i == 2)
      throw new SQLException();
    else if (i == 3)
      throw new IOException();
    else if (i < 0)
      throw new Error(); // 시스템 오류가 발생하다고 가정하자!
  }

  public static void main(String[] args) {
    try {
      m(-1);

    } catch (Throwable e) {
      System.out.println("애플리케이션 예외 발생!");
    }
  }
}

finally

  • try {} catch(Throwable) {} finally {}
  • try {} finally {}  - 예외 처리를 하지 않는 블록(client에게 위임한다.)

정상적으로 실행하든, 아니면 예외가 발생하여 catch 블록을 실행하든 finally 블록은 무조건 실행한다. 즉 예외 처리 록을 나가기 직전에 실행된다. 그래서 이 블록에는 try 에서 사용한 자원을 해제시키는 코드를 주로 둔다. 

자원
파일, DB 커넥션, 소켓 커넥션, 대량의 메모리 등
package com.eomcs.exception.ex3;

import java.io.IOException;
import java.sql.SQLException;

public class Exam0510 {

  static void m(int i) throws Exception, RuntimeException, SQLException, IOException {
    if (i == 0)
      throw new Exception();
    else if (i == 1)
      throw new RuntimeException();
    else if (i == 2)
      throw new SQLException();
    else if (i == 3)
      throw new IOException();
  }

  public static void main(String[] args) {
    try {
      m(0);
      System.out.println("try");

    } catch (RuntimeException | SQLException | IOException e) {
      System.out.println("catch 1");

    } catch (Exception e) {
      System.out.println("catch 2");

    } finally {
      System.out.println("finally");
    }
    
    try {
      m(1);

    } finally {
      // try 블록을 나가기 전에 무조건 실행해야 할 작업이 있다면
      // catch 블록이 없어도 finally 블록만 사용할 수 있다.
      System.out.println("마무리 작업 실행!");
    }
  }

}

우리가 Scanner(System.in) 객체를 생성하여 사용할 때, 단기간 내에 종료되는 간단한 프로그램이라면 종료와 함께 스캐너와 키보드와의 연결이 자동으로 끊어진다. JVM이 종료되면 OS는 JVM이 사용한 모든 자원을 자동으로 회수하기 때문이다.

 

그러나 365일 24시간 내내 실행되는 프로그램을 작성했다면, 키보드를 입력을 사용하지 않을 때는 다른 프로그램에서 사용이 가능하도록 스캐너와 연결된 키보드를 풀어줘야한다. 이것을 자원 해제라고 부른다.

 

그런데 만약 자원을 해제하기 전에 예외가 발생하여 메서드에서 빠져나오게 되면 자원을 해제할 수가 없다. 따라서 이러한 경우를 위해 finally 블록 안에서 close()를 호출하여 예외가 발생해도 자원을 해제하고 메서드를 빠져나올 수 있게 할 수가 있다.

public class Exam0620 {

  public static void main(String[] args) {
    Scanner keyScan = null;
    try {
      keyScan = new Scanner(System.in);

      System.out.print("입력> ");
      int value = keyScan.nextInt();
      System.out.println(value * value);

    } finally {
      // 이렇게 정상적으로 실행되든 예외가 발생하든 상관없이
      // 자원해제 같은 일은 반드시 실행해야 한다.
      keyScan.close();
      System.out.println("스캐너 자원 해제!");
    }
  }

}

try에서 keyScan을 선언하면 try 블록 외부인 finally블록에서는 접근할 수 없기 때문에 밖에서 선언해줘야한다.

 

매번 finally에서 자원을 해제시키는 과정이 귀찮다면, 다음과 같이 try () 소괄호 안에 해제하고자 하는 자원을 넣어주면 try 중괄호가 끝나면 자동해제가 되게 할 수 있다. 대신 소괄호 안에 들어가는 것은 무조건 변수 선언이 와야하고,  그 중에서도 AutoCloseable 인터페이스의 구현체만 올 수 있다.

 

여러 자원을 선언할 경우 마지막 선언문 뒤에는 세미콜론을 찍지 않아도 된다.

    try (Scanner keyScan = new Scanner(System.in); // OK!
        FileReader in = new FileReader("Hello.java") // OK!
        // String s = "Hello"; // 컴파일 오류!
        // if (true) {} // 컴파일 오류!
    ) {
      System.out.print("입력> ");
      int value = keyScan.nextInt();
      System.out.println(value * value);
    }

 

변수 선언 문장이 와야하고, 이미 선언된 변수를 둘 수 없다.

  public static void main(String[] args) throws Exception {
    B obj2 = null;

    try (
        obj2 = new B(); // 컴파일 오류!
        // 변수 선언은 반드시 괄호 안에 해야 한다.
    ) {
      System.out.println("try 블록 실행...");
    }
  }

try 소괄호에 있는 자원이 해제되는 시점은 예외가 발생하든 아니든, try 중괄호를 나가기 직전에 실행된다. 즉 catch 블록을 실행하기전에 실행된다.

public class Exam0641 {
  
  static class B implements AutoCloseable {
    
    public void m(int value) throws Exception {
      if (value < 0) {
        throw new Exception("음수입니다!");
      }
      System.out.println("m() 호출!");
    }
    
    @Override
    public void close() throws Exception {
      System.out.println("close() 호출");
    }
  }
  
  public static void main(String[] args) throws Exception {
    try (B obj = new B()) {
      System.out.println("try 블록 실행 시작");
      obj.m(-100);
      System.out.println("try 블록 실행 종료");
    } catch (Exception e) {
      System.out.println("예외 발생! : " + e.getMessage());
    }
  }
}

// 결과!!
// try 블록 실행 시작
// close() 호출
// 예외 발생! : 음수입니다!

예외 던지고 받기

다음은 try / catch 블록이 있는 곳까지 예외를 던지는 과정을 보여준다. 상황은 다음과 같다.

  • main -> m1 -> m2 -> m3 -> m4 이 순으로 메서드를 호출하고 있다.
  • m4에서 예외를 처음으로 던진다.
  • main에서 예외를 처리할 것이다.

m4에서 발생된 예외는 m4를 호출한 m3로 던져지고, m3에서 받은 예외는 처리되지 않고 다시 m3를 호출한 m2로 던져질 것이다. 따라서 예외는 m4 -> m3 -> m2 -> m1 -> main  이 순으로 던져진다.

이 과정에서 예외를 던진 모든 메서드 선언부에 예외를 선언해주어야한다.

package com.eomcs.exception.ex4;

public class Exam0120 {

  static void m1() throws Exception {
    m2();
  }

  static void m2() throws Exception {
    m3();
  }

  static void m3() throws Exception {
    m4();
  }

  static void m4() throws Exception {
    throw new Exception("m4()에서 예외 발생!");
  }

  public static void main(String[] args) {
    try {
      m1();
    } catch (Exception e) {
      System.out.println(e.getMessage());
    }
  }
}

이에 반해서 스텔스 모드(?)로 던져질 수 있는 RuntimeException 객체는 메서드에 따로 선언되지 않아도 client로 던져질 수 있다. 위의 사례와 똑같은 과정을 밟고 있으나 메서드에 예외가 선언되지 않아 티가 잘 나지 않는다.

package com.eomcs.exception.ex4;

public class Exam0130 {

  static void m1() {
    m2();
  }

  static void m2() {
    m3();
  }

  static void m3() {
    m4();
  }

  static void m4() {
    throw new RuntimeException("m4()에서 예외 발생!");
  }

  public static void main(String[] args) {
    try {
      m1();
    } catch (RuntimeException e) {
      // m4() 에서 발생된 예외가 여기까지 도달한다.
      System.out.println(e.getMessage());
    }
  }
}

RuntimeException 계열의 예외는 굳이 throws 문장을 선언하지 않아도 되지만, read()를 호출하는 개발자에게 어떤 예외가 발생할 수 있는지 명확하게 제시해주는 것이 유지보수에 도움이 되기 때문에 메서드 선언부에 발생되는 예외를 명시하는 것이 좋다.

 

 그런데 client에서 try/catch로 예외를 처리하려고 할 때, 호출한 메서드에서 RuntimeException을 던진다는 의미에 대해 직관적으로 이해하기는 어렵다. 그 예외가 어떻게 발생하는 지, 무슨 의미인지 단번에 알기 어렵다는 뜻이다.

package com.eomcs.exception.ex5;

import java.sql.Date;
import java.util.Scanner;

public class Exam0120 {

  static Board read() throws RuntimeException {
    try (Scanner keyScan = new Scanner(System.in)) {
      Board board = new Board();
      
      System.out.print("번호> ");
      board.setNo(Integer.parseInt(keyScan.nextLine()));
      
      System.out.print("제목> ");
      board.setTitle(keyScan.nextLine());
      
      System.out.print("내용> ");
      board.setContent(keyScan.nextLine());
      
      System.out.println("등록일> ");
      board.setCreatedDate(Date.valueOf(keyScan.nextLine()));
      
      return board;
    }
  }
  
  public static void main(String[] args) {
    try {
      Board board = read();
      System.out.println("---------------");
      System.out.printf("번호: %d\n", board.getNo());
      System.out.printf("제목: %s\n", board.getTitle());
      System.out.printf("내용: %s\n", board.getContent());
      System.out.printf("등록일: %s\n", board.getCreatedDate());
      
    } catch (RuntimeException e) { // ? 이게 무슨 종류의 예외지? 어떻게 발생된 예외지?
      System.out.println(e.getMessage());
      System.out.println("게시물 입력 중에 오류 발생!");
      // e.printStackTrace();
    }
  }
}

이럴 경우에는 RuntimeException을 상속받아 좀 더 알아보기 쉬운 이름을 가진 예외 클래스를 만들고 그것을 던지게 하는 것이 좋다. 그렇게 하기 위해서는 RuntimeException 객체를 예외가 발생한 메서드 안에서 받고 catch 블록에서 이를 상속받은 새로운 예외 객체를 던져야한다.

package com.eomcs.exception.ex5;

import java.sql.Date;
import java.util.Scanner;

public class Exam0130 {
  
  static Board read() throws BoardException {
    try (Scanner keyScan = new Scanner(System.in)) {
      Board board = new Board();

      System.out.print("번호> ");
      board.setNo(Integer.parseInt(keyScan.nextLine()));

      System.out.print("제목> ");
      board.setTitle(keyScan.nextLine());

      System.out.print("내용> ");
      board.setContent(keyScan.nextLine());

      System.out.print("등록일> ");
      board.setCreatedDate(Date.valueOf(keyScan.nextLine()));

      return board;
    } catch (Exception 원본오류) {
      throw new BoardException("게시물 입력 도중 오류 발생!", 원본오류);
    }
  }
  
  public static void main(String[] args) {
    try {
      Board board = read();
      System.out.println("---------------------");
      System.out.printf("번호: %d\n", board.getNo());
      System.out.printf("제목: %s\n", board.getTitle());
      System.out.printf("내용: %s\n", board.getContent());
      System.out.printf("등록일: %s\n", board.getCreatedDate());

    } catch (BoardException e) {
      System.out.println(e.getMessage());
//      e.printStackTrace();
    }
  }
}

 


 실습 - 파일 입출력 

 

git/eomcs-java-project/mini-pms-30-a

 

지금까지는 생성되는 모든 데이터를 컬렉션 객체에 저장했고 이것은 RAM에만 저장되어 프로그램이 종료되거나 컴퓨터를 끄면 데이터가 지워지는 문제가 있었다. 이번에는 프로그램을 종료하더라도 데이터가 지워지지 않도록 데이터를 외부 저장장치(하드 디스크, SSD 등)에 옮겨 저장하려고 한다. 따라서 파일 입출력 API를 통해 각종 데이터를 csv 파일로 저장하고, 파일에서 데이터를 읽도록 바꿀 것이다.

 

파일 입출력 API

데이터를 파일로 입출력하는 다양한 도구(*클래스*, *인터페이스*)를 제공하는 자바 API이다.

 

CSV(Comma-Seperated Values)형 파일

  • 몇가지 필드를 콤마(,)로 구분한 텍스트 파일이며 확장자는 `.csv` 를 사용한다.
  • MIME 형식은 `text/csv` 로 표현한다. 
  • 각 레코드(한 단위의 값)는 한 줄의 문자열로 표현하며 한 줄은 줄바꿈 기호(CRLF)로 구분한다.
  • 레코드를 구성하는 필드는 콤마(,)로 구분한다.
  • 각 필드는 큰 따옴표를 쳐도 되고 안쳐도 된다.
  • 마지막 줄은 줄바꿈 기호가 없어도 된다.
aaa, bbb, ccc (CRLF)
aaa, bbb, ccc (CRLF)
"aaa", "bbb", "ccc"

참고 : https://tools.ietf.org/html/rfc4180

 

MIME(Multi Internet Mail Extension)
다중목적을 지닌 인터넷 메일 익스텐션
쉽게 말하면 파일 변환을 뜻한다.
MIME는 이메일과 함께 동봉할 파일을 텍스트 문자로 전환해서 이메일 시스템을 통해 전달하기 위해 개발되었기 때문에 이름에 Internet Mail Extension 이지만 현재는 웹을 통해서 여러형태의 파일 전달하는데 쓰이고 있다.
"지금너에게 text를 보낼 건데 csv 형식이야" = test/csv
"지금너에게 어플리케이션 파일을 보낼건데 excel이야" = application/excel

 

레코드(Record)
레코드는 컴퓨터 과학에서 한 단위의 정보를 가리키는 용어를 말한다.

학생정보, 성적정보, 도서정보, 주문정보, 결제정보.....등등이 될 수 있다.
객체 지향 프로그래밍에서 레코드는 보통 클래스로 정의된다.

 

 

훈련 목표

새로 생성된 게시글을 csv 파일로 저장한다.

 

1단계 : saveBoards() 스태틱 메서드를 App 클래스에 정의하고 main 메서드의 가장 마지막 코드로 메서드를 호출한다. saveBoard() 메서드는 다음과 같이 구성된다.

  • File 객체 생성(파일 위치는 ./board.csv)

  • FileWriter 객체 생성 - 생성자 호출 과정에서 IOException 예외를 던지고 있으므로 try/catch 블록 안에 생성

  • boardList에 있는 요소에 대한 정보를 하나씩 파일에 출력 FileWriter의 wrtie(String) 메서드 호출

  • 파일 출력 도구의 임시메모리(버퍼)에 잔류하는 데이터까지 모두 출력되도록 close() 메서드 호출

 public static void saveBoards() {
    System.out.println("[게시글 저장]");
    
    // 파일 객체 생성
    File file = new File("./board.csv");
    
    try {
      // 데이터를 파일에 출력할 때 사용할 도구 생성
      FileWriter out = new FileWriter(file);
      
      // 각각 게시글 파일로 출력한다. 
      for (Board board : boardList) {
        String record = String.format("%d,%s,%s,%s,%s,%d\n",
            board.getNo(),
            board.getTitle(),
            board.getContent(),
            board.getWriter(),
            board.getRegisteredDate().toString(),
            board.getViewCount());
        out.write(record); // 번호,제목,내용,작성자,작성일,조회수CRLF
      }
      
      // 파일 출력 도구 닫기
      // => 이 과정에서 파일 출력 도구의 임시 메모리(Buffer)에 잔류하는 데이터를 마무리로 완전히 출력한다.
      out.close();
    } catch (IOException e) {
      System.out.println("파일 출력 중 오류 발생!");
    }
  }

이렇게 하면 프로젝트 파일에 있는 board.csv 파일이 생성되고, 안에 정보가 차례대로 입력된다.

 

2단계 : close() 메서드가 예외가 발생해도 온전히 실행될 수 있도록 메서드를 finally 블록을 만들어 그 안에 두고, FileWriter 변수의 선언도 try 블록 밖으로 뺀다. close()도 마찬가지로 실행 과정에서 IOException 예외가 발생할 수 있기때문에 try/catch를 만들어주지만, 예외가 발생한다고 해도 딱히 처리할 것이 없기 때문에 catch 블록안에는 아무것도 두지 않아도 된다.

public static void saveBoards() {
    System.out.println("[게시글 저장]");
    
    File file = new File("./board.csv");
    // 밖에서 선언
    FileWriter out = null;
    try {
      // 데이터를 파일에 출력할 때 사용할 도구
      out = new FileWriter(file);
      
      // 각각 게시글 파일로 출력한다. 
      for (Board board : boardList) {
        String record = String.format("%d,%s,%s,%s,%s,%d\n",
            board.getNo(),
            board.getTitle(),
            board.getContent(),
            board.getWriter(),
            board.getRegisteredDate().toString(),
            board.getViewCount());
        out.write(record); // 번호,제목,내용,작성자,작성일,조회수CRLF
      }
      
      // 파일 출력 도구 닫기
      // => 이 과정에서 파일 출력 도구의 임시 메모리(Buffer)에 잔류하는 데이터를 마무리로 완전히 출력한다.
    } catch (IOException e) {
      System.out.println("파일 출력 중 오류 발생!");
    } finally {
      try {
        out.close();
      } catch (IOException e) {

      }
    }
  }