Effective Java Item 09

parrineau·2022년 7월 12일
0

EffectiveJava

목록 보기
9/14

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

안녕하세요, 이번 포스팅은 저에게는 약간 생소한 주제입니다.
바로 try-with-resources 입니다.

마찬가지 여담으로 ㅎㅎㅎ...
올해 초부터 개발바닥이라는 유튜브를 즐겨 보고있습니다.

향로님과 호돌맨님께서 세계 최초 예능지향 Dev Entertaintment 토크쇼를 진행하시는데요, 두분 모두 우아한형제들에서 일하시다가 인프런, 반려생활에 CTO로 재직 중이십니다.

아주 흥미로운 썸네일로 유혹하여 안볼 수 없는 호돌맨님의 센스란...

출근길이나, 퇴근길 혹은 남는 시간에 보신다면 아주 유익한 정보를 습득하실 수 있을 것 같습니다.


이전 Item 7 (소멸자)와 관련이 있는 내용인데요, 바로 close 입니다.

InputStream에서 close를 클라이언트가 하지않을 때, 안전망으로 finalizer를 구현했다고 했는데요.

자바 라이브러리에서는 close 메서드를 직접 호출하여 닫아줘야 하는 것들이 많습니다.

바로 InputStream, OutputStream, java.sql.Connection 등이 있습니다.

하지만, 앞서 포스팅한 것 처럼 finalizer는 너무 믿을만하지 않습니다.
(신명나게 까댔던 기억이 납니다...)

전통적으로, DB 커넥션을 예시로 들어도 try-finally가 쓰였습니다.

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);

            rs = pstmt.executeQuery();
            if (rs.next()) {
                Member member = new Member();
                member.setMemberId(rs.getString("member_id"));
                member.setMoney(rs.getInt("money"));
                return member;
            } else {
                throw new NoSuchElementException("member not found memberId=" + memberId);
            }
        } catch (SQLException e) {
            log.error("error", e);
            throw e;
        } finally {
            close(con, pstmt, rs);
        }
    }

제 Github의 일부 내용인데요. (고대 유물 냄새가 납니다.)

이런식으로 예외가 발생하거나, 메서드에서 반환되는 경우를 포함해 자원이 제대로 닫힘을 보장하는 try-finally가 많이 사용되었습니다.

책의 예시를 들어보겠습니다.

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

PATH의 파일을 읽어, 첫 줄을 반환하는 간단한 예시입니다.

하지만, 여기서 자원을 하나 더 사용한다면 어떨까요?

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

try... try... 2중 try문은 18중 for문보다 복잡해 보입니다.
(예외처리를 생각해야 할 것이 많기 때문...)

위의 두 예시 모두 생각하기 어려운 결점이 있습니다.
예외는 try와 finally에서 모두 발생할 수 있습니다. 예를 들어, firstLineOfFile의 readLine 메서드가 예외를 던졌다고 가정합시다.

catch 문도 없고, 바로 무조건적으로 호출되는 finally 문으로 이동합니다.
그럼 같은 이유로 close 메서드도 실패할 것입니다.

여기서 문제가 발생하는데, 두 번째 close 예외가 readLine 메서드를 삼켜버리기 때문에, 스택 추적에 몹시 어려움을 겪고, 디버깅이 힘들어집니다.

오늘따라 가져오는 이미지가 크네요 ㅎㅎ..


이런 문제는 자바 7에서 제공된 try-with-resources 덕에 편안해졌습니다.
(개비스x의 편안~ 이미지를 넣으려고 했다가 너무 뇌절일 것 같아서...)

이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 합니다. item 8에도 언급을 드렸던 것 같습니다.

그저 void를 반환하는 close 메서드 하나만 덩그러니 정의한 인터페이스입니다.

만약, 닫아야 하는 자원을 뜻하는 클래스를 작성한다면 반드시 AutoCloseable을 구현하라고 합니다.

다음은, 첫 번째 예시를 try-with-resources를 사용해 재작성한 예시입니다.

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

try-with-resources는 try의 자원을 target으로 지정 후, try 구문이 끝나면 자동으로 try 자원을 회수합니다.

이제부터 강점이 드러나는데, 두 번째 예시에 try-with-resources을 적용해보겠습니다.

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

차이점을 눈치 채셨나요? 바로 try 구문이 하나로 통합되었고, 또한 코드도 아주 직관적입니다.


firstLineOfFile 메서드를 다시 보겠습니다.
readLine과 close 호출 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록됩니다.
(애초에 close 예외의 원인은 readLine부터 발생할 것이기 때문)

이것도 완전히 숨겨진것이 아닌, 스택 추적 내역에 (supressed)라는 문구를 달고 출력됩니다.

또한, 자바 7에서 Throwable에 추가된 getSupressed 메서드를 이용하면 프로그램 코드에서 가져올 수도 있습니다.

    public final synchronized Throwable[] getSuppressed() {
        if (suppressedExceptions == SUPPRESSED_SENTINEL ||
            suppressedExceptions == null)
            return EMPTY_THROWABLE_ARRAY;
        else
            return suppressedExceptions.toArray(EMPTY_THROWABLE_ARRAY);
    }
    private void printStackTrace(PrintStreamOrWriter s) {
        // Guard against malicious overrides of Throwable.equals by
        // using a Set with identity equality semantics.
        Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<>());
        dejaVu.add(this);

        synchronized (s.lock()) {
            // Print our stack trace
            s.println(this);
            StackTraceElement[] trace = getOurStackTrace();
            for (StackTraceElement traceElement : trace)
                s.println("\tat " + traceElement);

            // Print suppressed exceptions, if any
            for (Throwable se : getSuppressed())
                se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);

            // Print cause, if any
            Throwable ourCause = getCause();
            if (ourCause != null)
                ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
        }
    }

try-finally 처럼 try-with-resources에서도 catch 절을 쓸 수 있습니다.
catch 절 덕분에, try 문을 더 중첩하지 않고도 다수의 예외를 처리할 수 있습니다.

책의 예시는, firstLineofFile 메서드를 살짝 수정하여, 파일을 열거나 데이터를 읽지 못했을 때 예외를 던지는 대신, 기본값을 반환합니다.

    static String firstLineOfFile3(String path) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            return br.readLine();
        } catch (IOException e) {
            return defaultVal;
        }
    }

회고

try-with-resources는 솔직히 처음 보는 방식이고, 책에 나오는 이중 try 문도 되게 생소한 예시였습니다.

하지만, 직관적으로 이중 try 문은 많은 문제가 있을것이라 생각했고, 바로 해결법(try-with-resources)이 나와 이해하기 쉬웠고, 신기했습니다.

아무래도.. 조만간 깃허브의 코드도 수정을 하면 좋지 않을까... 싶습니다 ㅎㅎ

profile
방황하는 귀여운 개발자

0개의 댓글