Study/Effective-Java

[item#09] try-finally 보다는 try-with-resources 를 사용하라

hongeeii 2023. 11. 27. 18:45
728x90
반응형

try-finally 보다는 try-with-resources 를 사용하라

자바는 close메서드를 직접 달아줘야하는 자원이 많음 => 이걸 놓쳐서 성능문제로 이어지기도 함.

이를 위한 안전망으로 finalizer를 활용하는데 그리 믿을만하지 못함.

// 예제 1
static String firstLineOfFile(String path) throws IOException{
  BufferedReader br = new BufferedReader(new FileReader(path));
  try{
    return br.readLine();
  }finally{
    br.close();
  }
}
// 예제 2 : 자원이 둘 이상인 경우
static void copy(String src, String dst) throws IOEception{
  InputStream in = new FileInputStream(src);
  try{
    OutputStream out = new FileOutputStream(dst);
    try{
      byte[] buf = new byte[BUFFER_SIZE];
      int n;
      while((n = in.read(buf)) >= 0) out.write(buf, 0, n);
    } finally{
      out.close();
    }
   } finally{
    in.close();
   }
}
// 코드가 너무 더러움

위의 예제는 try-finally 문을 제대로 사용한 것이지만 미묘한 결점이 있다.

예외는 try 블록과 finally 블로 모두 발생할 수 있다.

예를 들어, 기기에 물리적 문제가 생기면 firstLineOfFile 메서드 안에서 readLine 메서드가 예외를 던지고,

같은 이유로 close 메서드도 실패함. => 두번째 예외가 첫번째 첫번째 예외를 집어 삼켜 디버깅을 어렵게함(첫번째예외의 로그가 안남음)

해결책 try-with-resource

자바 7에서 생긴 try-with-resource가 이런 문제를 해결해줌.

try-with-resource를 사용하기 위해서는 해당 자원이 AutoCloseable 인터페이스를 구현해야함.

단순히 void를 반환하는 close 메서드 하나만 저으이한 인터페이스임.

// try-with-resource 적용 예제1
static String firstLineOfFile(String path) throws IOException{
  try(BufferedReader br = new BufferedReader(new FileReader(path))){
    return br.readLine();
  }
}
// try-with-resource 적용 예제2
static void copy(String src, String dst) throws  IOException{
  try(InputStream in - new FiileInputStream(src);
      OutputStream out = new FileOutputStream(dst)){
    byte[] buf = new byte[BUFFER_SIZE];
    int n;
    while((n = in.read(buf)) >= 0) out.write(buf, 0, n);
  }
}

try-with-resource 를 사용하므로써 코드도 읽기 수월하고 문제 진단도 훨씬 좋음.

적용예제1을 보면 readLine 과 close를 호출할때 둘다 예외가 발생하면 둘다 예외가 기록됨(숨겨진 예외는 suppressed 를 달고 출력)

또 자바 7에서 추가된 Throwable의 getSuppressed 메서드를 이용하면 프로그램 코드에서 가져올 수도 있음.

한번에 close 하면 안되나?

    static void copy(String src, String dst) throws IOException {
        InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            in.close();
            out.close();
        }
    }

이렇게 사용할 경우 자원의 구멍이 생길 수 있음(leak).

in을 반납할 때 오류가 생겨버리면 out은 반납을 못함.

예외가 먹히는 상황

public class BadBufferedReader extends BufferedReader {
    public BadBufferedReader(Reader in, int sz) {
        super(in, sz);
    }

    public BadBufferedReader(Reader in) {
        super(in);
    }

    @Override
    public String readLine() throws IOException {
        throw new CharConversionException();
    }

    @Override
    public void close() throws IOException {
        throw new StreamCorruptedException();
    }
}
    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BadBufferedReader(new FileReader(path));
        try{
            return br.readLine();
        }finally {
            br.close();
        }
    }

readLine 과 close 둘다 오류가 떨어지는 상황에서 마지막에 발생한 오류만 보이게됨.
image

하지만 try-with-resource로 변경하면 예외가 다 보임.

image

퍼즐러 예외 처리 코드 실수

    static void copy(String src, String dst) throws IOException {
        InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                // TODO 이렇게 하면 되는거 아닌가?
            }
            try {
                in.close();
            } catch (IOException e) {
                // TODO 안전한가?
            }
        }
    }

마지막 close 도 try-catch로 감싸져있으면 괜찮지 않나? => catch로 잡는 예외 외의 다른 예외인 경우에 문제가 생김

오히려 자원 하나당 try로 잡는게 더 안전함.

try-with-resource

try-with-resource 코드는 어떻게 작동할까?

    static String firstLineOfFile(String path) throws IOException {
        try(BufferedReader br = new BadBufferedReader(new FileReader(path))) {
            return br.readLine();
        }
    }

위의 코드를 컴파일 하면 아래와 같은 코드로 컴파일해줌(실제 바이트 코드는 다르게 생김)

    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));

        String var2;
        try {
            var2 = br.readLine();
        } catch (Throwable var5) {
            try {
                br.close();
            } catch (Throwable var4) {
                var5.addSuppressed(var4);
            }
            throw var5;
        }

        br.close();
        return var2;
    }
728x90
반응형