was를 만들면서 403 에러처리를 하기 위해서
이번에 처음으로 예외를 직접 만들고 try catch로 잡아주었다.
public class InvalidContentTypeException extends RuntimeException{
public InvalidContentTypeException() {
}
public InvalidContentTypeException(String message) {
super(message);
}
}
public class InvalidHttpMethodException extends RuntimeException{
public InvalidHttpMethodException() {
}
public InvalidHttpMethodException(String message) {
super(message);
}
}
public class InvalidPostRequestException extends RuntimeException{
public InvalidPostRequestException() {
}
public InvalidPostRequestException(String message) {
super(message);
}
}
public class NonexistentFileException extends RuntimeException {
public NonexistentFileException() {
}
public NonexistentFileException(String message) {
super(message);
}
}
이렇게 예외를 직접 만드는 건, 구체적으로 어떤 이유에서 문제가 생겼는지 확인하기 위해서다.
public void run() {
logIp();
ResponseManager handler = new ResponseManager();
ErrorManager errorManager = new ErrorManager();
try {
executeSafely(respondToHttpRequest(handler));
} catch (InvalidContentTypeException | InvalidHttpMethodException
| InvalidPostRequestException | NonexistentFileException e) {
executeSafely(handleError(errorManager));
}finally {
closeConnection();
}
}
@NotNull
private Action handleError(ErrorManager errorManager) {
return () -> errorManager.respondToError(out);
}
@NotNull
private Action respondToHttpRequest(ResponseManager handler) {
return () -> {
HttpRequest httpRequest = convertToHttpRequest(in);
handler.respondTo(httpRequest, out);
};
}
public void executeSafely(Action action){
try {
action.excute();
}catch (IOException e){
}
}
//try-catch가 이중으로 반복돼서 가독성이 떨어졌다.
//이에 try-catch를 추상화했다.
@FunctionalInterface
public interface Action {
public void excute() throws IOException;
}
예외가 터진다.
그러면, try -catch에서 catch 부분에서 예외를 잡고
그때 에러를 처리한다. 이 경우에는 화면에 403에러를 띄워주는 방식을 택했다.
이번에 예외처리를 하면서
예외처리의 best pratice가 무엇일지 궁금했다.
예전에 호눅스랑 jk가 catch부분에서는 try부분과는 다른 흐름이 나오도록 코드를 구성해야 한다는 말을 한 적이 있다.
막연하게 느껴지는 부분이다. 이에 공부를 해봤다.
예외란 무엇일까?
실생활의 예로 이해를 해보자.
쿠팡에서 물건을 주문했는데, 배송 중에 문제가 발생했다고 해보자.
이런 경우, 물건 배송을 맡은 회사는 이 문제를 해결하기 위한 조치를 할 것이다. 물건을 다시 보내면서, 물건의 배송 경로에 문제가 있는지를 확인하고 문제가 있다면 고칠 것이다.
자바에서도 코드는 error를 경험할 수 있다. 예외를 처리하는 좋은 방법은 프로그램을 re-route(아마 여기서는 새로운 방향으로 가게 한다는 뜻 같다)해서, 유저들에게 좋은 경험(positive exeperice)를 제공해야 한다.
Baeldung의 Exception Handling in Java글을 추가로 정리한다.
우리는 아무런 문제가 없는 환경을 상정하고 코드를 작성한다.
파일 시스템에는 항상 원하는 파일이 있고, 네트워크는 매번 안정적이며, jvm은 언제든 메모리가 충분하다.
하지만, 실제 파일시스템과 네트워크, jvm은 매번 문제를 겪을 가능성이 있다. 우리 코드의 wellbeing(정상적인 작동)은 우리가 이러한 unhappy paths(비정상적인 경로들)을 얼마나 잘 다루는지에 따라 달라진다.
public static List<Player> getPlayers() throws IOException {
Path path = Paths.get("players.dat");
List<String> players = Files.readAllLines(path);
return players.stream()
.map(Player::new)
.collect(Collectors.toList());
}
위 코드는 IOException을 직접 처리하지 않고, call stack에 넘긴다. 이상적인 환경에서는 이 코드는 문제없이 작동한다.
현실세계는 다르다. 만약, players.dat라는 파일이 없다면 NoSuchFileException같은 문제가 터진다. 우리는 이상적인 세계에서 살고 있지 않다. 즉, 언제든 문제가 생길 수 있다는 가정을 하고 이에 대한 계획을 마련해야 한다.
1)Checked exceptions
이 예외는 자바 컴파일러가 우리로 하여금 처리하도록 강제하는 예외다. call statck에 예외를 던지거나, 우리가 직접 처리를 해야 한다.
오라클 문서에 따르면, 우리는 caller of our method가 합리적으로 회복될 수 있을 것이라고 예상할 때 checked exception을 사용해야 한다. 대표적인 사례로는 IOException과 ServletException이 있다.
2)Unchecked Exceptions
Unchecked exceptions는 자바 컴파일러가 처리를 강제하지 않는 예외다. 우리가 RuntimeException를 상속하는 예외들을 만들었을 경우 모두 여기에 해당하게 된다. 그렇지 않으면, checked exception이다.
대표적인 예외로는 NullPointerException, IllegalArgumentException, and SecurityException가 있다.
1)Throw하기
private HttpRequest convertToHttpRequest(InputStream in) throws IOException {
HttpRequestParser parser = new HttpRequestParser();
HttpRequest httpRequest = parser.parseRequestMessage(in);
HttpRequestValidator validator = new HttpRequestValidator();
validator.validate(httpRequest);
return httpRequest;
}
예외를 던지면, 이 메서드를 호출하는 곳에서 예외를 catch해줘야 한다.
2)try catch
public void run() {
logIp();
ResponseManager handler = new ResponseManager();
ErrorManager errorManager = new ErrorManager();
try {
executeSafely(respondToHttpRequest(handler));
} catch (InvalidContentTypeException | InvalidHttpMethodException
| InvalidPostRequestException | NonexistentFileException e) {
executeSafely(handleError(errorManager));
}finally {
closeConnection();
}
}
예외가 발생했을 때 어떠한 동작을 실행하도록 한 코드다.
예외가 발생했을 때는 새로운 예외를 던져주거나(예외의 범위를 좁히기 위해서) 회복을 시키는 방법도 있다.
사실 이게 왜 회복을 하는 사례인지 잘 이해가 안돼서 gpt에게 물어보니
여기서 "복구(recover)"는 예외가 발생했을 때 프로그램이 중단되지 않고, 대신 안전한 방법으로 계속 실행될 수 있도록 하는 과정을 의미합니다.안전한 기본값(여기서는 0)을 반환합니다: 파일이 없거나 다른 이유로 점수를 읽어올 수 없는 경우, 메소드는 안전한 기본값인 0을 반환하여, 호출자가 이 메소드의 결과를 사용하여도 프로그램이 예상 가능한 방식으로 동작하도록 합니다. 이로써 프로그램은 예외적인 상황에도 불구하고 안정적으로 계속 실행될 수 있습니다.
라는 답이 나왔다. 내 생각에는 예외가 터져서 프로그램이 종료되지 않도록 로직을 구현한거 같다. 예전에 jk가 예외를 던지는 대신 빈값을 반환하는 방식으로 프로그램이 종료되지 않게끔 로직을 구현할 수 있다는 말을 한 적이 있다. 그런 사례가 아닐까?
public int getPlayerScore(String playerFile) {
try {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException noFile) {
throw new IllegalArgumentException("File not found");
}
}
public int getPlayerScore(String playerFile) {
try {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch ( FileNotFoundException noFile ) {
logger.warn("File not found, resetting score.");
return 0;
}
}
아래 방식은 logger로 기록을 남기고 0을 반환하도록 한 것이다.
3)finally
예외가 발생하든 말든, 반드시 실행해야 하는 코드가 있다면 finally를 쓰면 된다. 위 코드에서 closeConnection();했던 것처럼 말이다.
4)try-with-resource
이 방식을 쓰면 리소스를 클로즈하지 않아도 된다. 대신, AutoCloseable를 상속받은 클래스들을 사용할 때만 가능하다
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile))) {
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException e ) {
logger.warn("File not found, resetting score.");
return 0;
}
}
1)Swallowing Exceptions
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (Exception e) {} // <== catch and swallow
return 0;
}
catch에 어떤 흐름이 없는 경우다. 결국, 이슈를 처리하지 않고 다른 코드가 해당 이슈를 계속 맞이하도록 하는 것이다. 만약, 특정한 checked exception이 일어나지 않을 것이라고 확신할 수도 있다. 그런 경우라 해도 각주로 우리가 의도적으로 exception을 ate(먹었다? 무시한다?가 어울릴듯하다)고 아래처럼 명시적으로 알려주는 게 좋다.
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
// this will never happen
}
}
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
logger.error("Couldn't load the score", e);
return 0;
}
}
위 사례처럼 단순히 예외를 출력하기만 하는 것도 좋지 않다. 차라리 로거를 쓰는 것이 더 낫다.
새로운 예외를 던지느라, 중요한 정보를 놓칠 수도 있다.
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
throw new PlayerScoreException();
}
}
여기서는 IOException이 에러의 원인이라는 주요 정보를 놓칠 수 있다.
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
throw new PlayerScoreException(e);
}
}
이런식으로 생성자를 이용해 메시지를 전달해주는 것이 좋다.
public int getPlayerScore(String playerFile) {
try {
// ...
} catch ( IOException io ) {
throw new IllegalStateException(io); // <== eaten by the finally
} finally {
throw new OtherException();
}
}
이런식으로 finally에서 예외를 새롭게 던져주는 것도 좋지 않다. catch블록에서 발생한 예외보다 나중에 던져진 예외가 더 우선권을 갖기 때문이다. try블록에서 발생한 예외를 지워버릴 수 있는 것이다.
** 예외를 새롭게 던지는 경우는 언제여야 할까?
You can manually throw an exception using the throw keyword. This is generally used when your code cannot recover from an error or when it encounters an unusual state.
라는 답을 봤다. 즉, 코드가 에러로부터 복구하기 어렵거나 unusal한 상태에 마주했을 때 예외를 던져주는 것 같다.
Java Exception Handling & Best Practices
여기에 예외처리에 관한 베스트프랙티스가 있어서 정리한다.
1.구체적인 예외를 사용해라. 제너럴한 예외 대신 구체적인 예뢰를 catch해야 더 좋은 복구 프로세스를 제공할 수 있다.
2.예외를 삼키지 마라. catch블록을 비워두지 말아라.
4.추상화에 적합한 예외를 던져라. 만약, 주문처리를 하는 메서드에서 파일을 읽어야 하는데 그 파일을 발견할 수 없는 것이라면, FileNotFoundException같은 낮은 레벨의 예외가 아니라 OrderProcessingException같은 높은 레벨의 예외를 던져야 한다.
**이 부분이 이해가 안돼서 gpt에게 물어보니
추상화 누수 방지: 낮은 수준의 예외를 그대로 노출함으로써 내부 구현의 세부 사항이 외부로 노출되는 것을 방지합니다. 이를 통해 내부 구현을 자유롭게 변경할 수 있으며, 사용자는 메서드의 추상화 수준에만 집중할 수 있습니다.
사용성 향상: 메서드의 사용자는 더 이해하기 쉽고 관련성 있는 예외 메시지를 받게 됩니다. 이는 디버깅과 오류 처리를 보다 용이하게 합니다.
일관성 유지: 같은 추상화 수준에서 비슷한 유형의 문제가 발생했을 때 일관된 방식으로 예외를 처리할 수 있습니다.
try {
// 파일 읽기 시도
} catch (FileNotFoundException e) {
throw new OrderProcessingException("주문 처리 중 문제가 발생했습니다.", e);
}
라는 답이 나왔다.
5.리소스를 사용하면 항상 정리해야 한다.
6.Checked Exceptions를 무시하면 안된다.
7.finally블록에서 예외를 던지지 마라.
8.메서드가 예외를 던지면, Javadoc @throws tag.로 문서화해라.
9.커스터마이즈된 예외를 써라. 자바에서 제공하는 예외가 충분하지 않다면 직접 예외를 만들어라.
Java Exception handling best practices이 글의 내용도 정리한다.
1)로그할 때 주의가 필요하다.
보호가 필요한 데이터가 로그 파일에 쓰여지지 않도록 해야 한다.
2)예외를 버리지 말고, 최고한 예외의 이름과 메시지를 로그로 남겨라. 그렇게 해야 로그에 남긴 예외로부터 의미있는 정보를 추출할 수 있다.
3)리소스를 직접 닫지 말고 try-with-resource를 써라
4)