자원 정리 1
자원 정리를 이해하기 위해 간단한 예제 코드를 만들어보자
package thread.control.network.tcp.autocloseable;
public class CallException extends Exception{
public CallException(String message) {
super(message);
}
}
package thread.control.network.tcp.autocloseable;
public class CloseException extends Exception{
public CloseException(String message) {
super(message);
}
}
package thread.control.network.tcp.autocloseable;
public class ResourceV1 {
private String name;
public ResourceV1(String name) {
this.name = name;
}
public void call() {
System.out.println(name + " call");
}
public void callEx() throws CallException {
System.out.println(name + " callEx");
throw new CallException(name + " ex");
}
public void close() {
System.out.println(name + " close");
}
public void closeEx() throws CloseException {
System.out.println(name + " callEx");
throw new CloseException(name + " ex");
}
}
call(): 정상 로직 호출
callEx() : 비정상 로직 호출 CallException 을 던진다
close() : 정상 종료
closeEx() : 비정상 종료, CloseException을 던진다.
package thread.control.network.tcp.autocloseable;
public class ResourceCloseMainV1 {
public static void main(String[] args) {
try {
logic();
} catch (CallException e) {
System.out.println("CallException 예외 처리");
throw new RuntimeException(e);
} catch (CloseException e) {
System.out.println("CloseException 예외 처리");
throw new RuntimeException(e);
}
}
private static void logic() throws CallException, CloseException {
ResourceV1 resource1 = new ResourceV1("resource1");
ResourceV1 resource2 = new ResourceV1("resource2");
resource1.call();
resource1.callEx(); // CallExcepton
System.out.println(" 자원 정리 "); // 호출안됨
resource2.closeEx();
resource1.close();
}
}
서로 관련된 자원은 나중에 생성한 자원을 먼저 정리해야 한다.
예를 들어서 resource1 을 생성하고, resource1의 정보를 활용해서 resource2를 생성한다면, 닫을 때는 그 반대인 resource2를 먼저 닫고, 그다음에 resource1을 닫아야 한다. 왜냐하면 resource2의 입장에서 resoure1의 정보를 아직 참조하고 있기 때문이다.
이 예제에서는 두 자원이 서로 관련이 없기 때문에 생성과 종료 순서가 크게 상관이 없지만, resource1의 정보를 기반으로 resource2를 생성한다고 가정하겠다.
실행 결과
callEx()를 호출하면서 예외가 발생했다. 예외 때문에 자원정리 코드가 정상 호출되지 않았다.
이 코드는 예외가 발생하면 자원이 정리되지 않는다는 문제가 있다.
자원 정리 2
이번에는 예외가 발생해도 자원을 정리하도록 해보자
package thread.control.network.tcp.autocloseable;
public class ResourceCloseMainV2 {
public static void main(String[] args) {
try {
logic();
} catch (CallException e) {
System.out.println("CallException 예외 처리");
throw new RuntimeException(e);
} catch (CloseException e) {
System.out.println("CloseException 예외 처리");
throw new RuntimeException(e);
}
}
private static void logic() throws CallException, CloseException {
ResourceV1 resource1 = null;
ResourceV1 resource2 = null;
try {
resource1 = new ResourceV1("resource1");
resource2 = new ResourceV1("resource2");
resource1.call();
resource1.callEx(); // CallExcepton
} catch (CallException e) {
System.out.println("ex : " + e);
throw e;
} finally {
System.out.println(" 자원 정리 ");
if(resource2 != null) {
resource2.closeEx(); // CloseExcepton
}
if(resource1 != null) {
resource1.closeEx(); // 이 코드 호출 안됨!
}
}
}
}
실행 결과
null 체크
이번에는 finally 코드 블록을 사용해서 자원을 닫는 코드가 항상 호출되도록 했다.
만약 resouce2 객체를 생성하기 전에 예외가 발생하면 resource2는 null 이 된다. 따라서 null 체크를 해야 한다.
마찬가지로 resource1을 생성하는 중에 예외가 발생한다면 null 체크가 필요하다.
자원 정리 중에 예외가 발생하는 문제
finally 코드 블록은 항상 호출되기 때문에 자원이 잘 정리될 것 같지만, 이번에 자원을 정리하는 중에 finally 코드 블록 안에서 resource2.closeEx()를 호출하면서 예외가 발생하였다. 결과적으로 resource1.closeEx()는 호출되지 않았다.
핵심 예외가 바뀌는 문제
이 코드에서 발생한 핵심적인 예외는 CallException이다. 이 예외 때문에 문제가 된 것이다. 그런데 finally 코드 블록에서 자원을 정리하면서 CloseException 예외가 추가로 발생했다. 예외 때문에 자원을 정리하고 있는데, 자원 정리 중에 또 예외가 발생한 것이다. 이 경우 logic()을 호출한 쪽에서 핵심 예외인 CallException 이 아니라 finally 블록에서 생성된 CloseException을 받게 된다. 핵심예외가 사라진 것이다!
개발자가 원하는 예외는 핵심예외이다. 이 핵심예외를 확인해야 제대로 된 문제를 찾을 수 있다. 자원을 닫는 중 발생한 예외는 부가 예외일 뿐이다.
코드의 문제
- close() 시점에 실수로 예외를 던지면 , 이후 다른 자원을 닫을 수 없는 문제 발생
- finally 블록 안에서 자원을 닫을 때 예외가 발생하면, 핵심 예외가 finally에서 발생한 부가 예외로 바뀌어 버린다. 그리고 핵심 예외가 사라진다.
자원 정리 3
이번에는 자원 정리의 코드에서 try-catch를 사용해서 자원 정리 중에 발생하는 예외를 잡아서 처리해 보자
package thread.control.network.tcp.autocloseable;
public class ResourceCloseMainV3 {
public static void main(String[] args) {
try {
logic();
} catch (CallException e) {
System.out.println("CallException 예외 처리");
throw new RuntimeException(e);
} catch (CloseException e) {
System.out.println("CloseException 예외 처리");
throw new RuntimeException(e);
}
}
private static void logic() throws CallException, CloseException {
ResourceV1 resource1 = null;
ResourceV1 resource2 = null;
try {
resource1 = new ResourceV1("resource1");
resource2 = new ResourceV1("resource2");
resource1.call();
resource1.callEx(); // CallExcepton
} catch (CallException e) {
System.out.println("ex : " + e);
throw e;
} finally {
System.out.println(" 자원 정리 ");
if(resource2 != null) {
try {
resource2.closeEx(); // CloseExcepton 발생
}catch (CloseException e){
// close() 에서 발생한 예외는 버린다. 필요하면 로깅 정도
System.out.println("close ex : " + e);
}
}
try {
if(resource1 != null) {
resource1.closeEx(); // 이 코드 호출 안됨!
}
}catch (CloseException e){
// close() 에서 발생한 예외는 버린다. 필요하면 로깅 정도
System.out.println("close ex : " + e);
}
}
}
}
이전에 발생했던 2문제를 해결했다.
close() 시점에 실수로 예외를 던지면, 이후 다른 자원을 닫을 수 없는 문제 발생
finally 블록 안에서 자원을 닫을 때 예외가 발생하면, 핵심 예외가 finally에서 발생한 부가 예외로 바뀌어 버린다.
그리고 핵심 예외가 사라진다.
핵심적인 문제들은 해결되었지만 코드 부분에서 보면 아쉬운 부분이 많다.
- resource 변수를 선언하면서 동시에 할당할 수 없음(try, finally 코드 블록과 변수 스코프가 다른 문제)
- catch 이후에 finally 호출, 자원 정리가 조금 늦어진다.
- 개발자가 실수로 close()를 호출하지 않을 가능성
- 개발자가 close() 호출 순서를 실수, 보통 자원을 생성한 순서와 반대로 닫아야 함.
자원 정리 4
try-with-resources
package network.tcp.autocloseable;
public class ResourceV2 implements AutoCloseable {
private String name;
public ResourceV2(String name) {
this.name = name;
}
public void call() {
System.out.println(name + " call");
}
public void callEx() throws CallException {
System.out.println(name + " callEx");
throw new CallException(name + " ex");
}
@Override
public void close() throws CloseException {
System.out.println(name + " close");
throw new CloseException(name + " ex");
}
}
- AutoCloseable을 구현했다.
- close()는 항상 CloseException을 던지도록 했다.
package network.tcp.autocloseable;
public class ResourceCloseMainV4 {
public static void main(String[] args) {
try {
logic();
} catch (CallException e) {
System.out.println("CallException 예외 처리");
Throwable[] suppressed = e.getSuppressed();
for (Throwable throwable : suppressed) {
System.out.println("suppressedEx = " + throwable);
}
e.printStackTrace();
} catch (CloseException e) {
System.out.println("CloseException 예외 처리");
e.printStackTrace();
}
}
private static void logic() throws CallException, CloseException {
try (ResourceV2 resource1 = new ResourceV2("resource1");
ResourceV2 resource2 = new ResourceV2("resource2")) {
resource1.call();
resource2.callEx(); // CallException;
} catch (CallException e) {
System.out.println("ex: " + e);
throw e; // CallException;
}
}
}
try-with-resources는 단순하게 close()를 자동 호출해 준다는 정도의 기능만 제공하는 것이 아니다.
고민한 6가지 문제를 모두 해결하는 장치이다.
2가지 핵심 문제
- close() 시점에서 실수로 예외를 던지면 , 이후 다른 자원을 닫을 수 없는 문제 발생
- finally 블록 안에서 자원을 닫을 때 예외가 발생하면, 핵심 예외가 finally에서 부가 예외로 바뀌어 버린다.
- 그리고 핵심예외가 사라진다.
4가지 부가예외
- resource 변수를 선언하면서 동시에 할당할 수 없음(try, finally 코드 블록과 변수 스코프가 다른 문제)
- catch 이후에 finally 호출, 자원 정리가 조금 늦어진다.
- 개발자가 실수로 close()를 호출하지 않을 가능성
- 개발자가 close() 호출 순서를 실수, 보통 자원을 생성한 순서와 반대로 닫아야 함
try with resources 장점
- 리소스 누수 방지 : 모든 리소스가 제대로 닫히도록 보장한다. 실수로 finally 블록을 적지 않거나, finally블록 안에서 자원 해제코드를 누락하는 문제들을 예방할 수 있다.
- 코드 간결성 및 가독성 향상: 명시적이 close() 호출이 필요 없어 코드가 더 간결해지고 읽기 쉬워진다.
- 스코프 범위 한정 : 예를 들어 리소스로 사용되는 resorce1, 2 변수의 스코프가 try 블록 안으로 한정된다.
- 따라서 코드 유지보수가 더 쉬어진다.
- 조금 더 빠른 자원 해제 : 기존에는 try -> catch -> finally로 catch 이후에 자원을 반납했다. Try with resources 구분은 try 블록이 끝나면 즉시 close()를 호출한다.
- 자원 정리 순서: 먼저 선언한 자원을 나중에 정리한다.
- 부가 예외 포함
Try with resources 예외 처리와 부가 예외 포함
try-with-resources를 사용하는 중에 핵심 로직 예외와 자원을 정리하는 중에 발생하는 부가 예외가 모두 발생하면 어떻게 될까?
try-with-resources는 핵심 예외를 반환한다.
부가예외는 핵심 예외 안에 Suppressed로 담아서 반환한다.
개발자가 자원 정리 중에 발생한 부가 예외를 e. getSuppressed()를 통해 활용할 수 있다.
try-with-resources를 사용하면 핵심 예외를 반환하면서, 동시에 부가 예외도 필요하면 확인할 수 있다.
참고로 자바 예외에는 e.addSuppressed(ex)라는 메서드가 있어서 예외 안에 참고할 예외를 담아둘 수 있다. 참고로 이 기능도 try-with-resources와 함께 등장했다
참고자료 : 김영한의 실전 자바 고급 2편 - 네트워크 프로그램 1
'JAVA' 카테고리의 다른 글
[JAVA] 리플렉션 (2) | 2024.11.27 |
---|---|
[JAVA] 자원 정리 (try-catch , try-with-resources) 2 (1) | 2024.11.26 |
[JAVA] Comparable ,Comparator (0) | 2024.11.23 |
[JAVA] 싱글톤 구현 방식 (0) | 2024.11.21 |
[JAVA] iterator 순회 중 만난 ConcurrentModificationException (1) | 2024.11.20 |