일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 로버트마틴
- 큐
- 자바
- Spring
- JDBC
- Collection
- JavaScript
- exception
- Rails
- 겨울카카오인턴
- Python
- lambda calculus
- tcp
- Collections
- Network
- DesignPattern
- 함수형 프로그래밍
- 디자인패턴
- Java
- 프로그래머스
- 백준
- 파이썬
- Eclipse
- 람다 칼큘러스
- design-pattern
- solid
- Pattern
- functional programming
- javscript
- 스택
- Today
- Total
개발자 노트
java - exception 처리 (코드) 본문
상위 주제
제가 잘못 사용했던 냄새나는 코드를 준비해보았습니다.
상황
주문 번호를 받아 해당 주문서 내용을 반환해주는 기능입니다.
만약 클라이언트가 요청하는 주문 번호가 존재하지 않으면 error를 발생시키고 해당 주문이 없음을 전해주어야 하지요.
코드
Controller
package com.company.exceptiontest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Controller {
private final Service service;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public Controller(Service service) {
this.service = service;
}
public String orderInfo(Long id) {
try {
OrderSheet orderSheet = service.searchOrderSheet(id);
return String.valueOf(orderSheet);
} catch (CustomException e) {
logger.info("controller - orderInfo 메서드: 주문서 정보 요청 중 error 발생!");
e.printStackTrace();
return e.getMessage();
}
}
}
controller에선 주문서 정보를 string으로 전달해주고요, error가 발생했을 경우엔 잡아서 log를 찍은 다음 error message를 사용자에게 전달해줍니다.
Service
package com.company.exceptiontest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Service {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public Service(Repository repository) {
this.repository = repository;
}
private final Repository repository;
public OrderSheet searchOrderSheet(Long id) throws CustomException {
try {
return repository.findById(id);
} catch (CustomException e) {
logger.info("주문서 정보 요청 중 error 발생하였음!");
throw new CustomException("주문서 정보 요청 중 실패하였습니다. 재시도 후에도 문제될 경우 연락 부탁드립니다.");
}
}
}
에러가 발생했다면 고객이 볼 수 있는 error message를 담아 던지죠. error message는 orderId를 찾지 못했다라기 보다는 일반적으로 작성하였습니다.
여기선 repository를 찾는 로직밖에 없지만, 현업에서는 db connection error가 발생할 수도 있고 다른 로직에서 또 에러가 발생할 수 있거든요. 그리고 사용자에게 전달할 코드이므로 서버 내부 에러의 구체적인 내용을 전달하면 안되겠죵.
Repository
package com.company.exceptiontest;
import java.util.List;
public class Repository {
public OrderSheet findById(Long orderId) throws CustomException {
if (orderId == 0) {
throw new CustomException("해당 id가 존재하지 않습니다. id: " + orderId);
}
return new OrderSheet(List.of(new Product("안경"), new Product("물")));
}
}
repository쪽이므로 어떤 id가 없는지 확인할 수 있게 메세지를 담아 exception을 던집니다.
간단히 하기 위해 id = 0을 전달받은 경우 exception을 던지도록 하였습니다.
CustomException
package com.company.exceptiontest;
public class CustomException extends Exception {
public CustomException(String message) {
super(message);
}
}
checked exception으로 구현되어 있습니다.
OrderSheet
import java.util.List;
public class OrderSheet {
private final List<Product> products;
public OrderSheet(List<Product> products) {
this.products = products;
}
public List<Product> getProducts() {
return products;
}
@Override
public String toString() {
return "OrderSheet{" +
"products=" + products +
'}';
}
}
주문서 class입니다. 간단히 만들기 위해 상품정보만 담고 있습니다.
Product
public class Product {
private final String name;
public Product(String name) {
this.name = name;
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
'}';
}
}
ErrorHandling
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ErrorHandling {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public void run() {
Controller controller = getController();
String orderSheetInfo = controller.orderInfo(0L);
logger.info(orderSheetInfo);
logger.info("실행 끝");
}
private Controller getController() {
Repository repository = new Repository();
Service service = new Service(repository);
return new Controller(service);
}
}
의존성을 주입해주고, 프로그램 실행 흐름을 책임지는 클래스입니다.
App (main)
public class App {
public static void main(String[] args) {
ErrorHandling errorHandling = new ErrorHandling();
errorHandling.run();
}
}
실행 결과
1) 일반 흐름의 실행결과
15:12 INFO c.c.exceptiontest.ErrorHandling - OrderSheet{products=[Product{name='안경'}, Product{name='물'}]}
15:12 INFO c.c.exceptiontest.ErrorHandling - 실행 끝
Process finished with exit code 0
상품 정보를 잘 보여주네용^^
2) exception 실행 결과
15:30 INFO com.company.exceptiontest.Service - 주문서 정보 요청 중 error 발생하였음!
15:30 INFO com.company.exceptiontest.Controller - controller - orderInfo 메서드: 주문서 정보 요청 중 error 발생!
15:30 INFO c.c.exceptiontest.ErrorHandling - 주문서 정보 요청 중 실패하였습니다. 재시도 후에도 문제될 경우 연락 부탁드립니다.
15:30 INFO c.c.exceptiontest.ErrorHandling - 실행 끝
com.company.exceptiontest.CustomException: 주문서 정보 요청 중 실패하였습니다. 재시도 후에도 문제될 경우 연락 부탁드립니다.
at com.company.exceptiontest.Service.searchOrderSheet(Service.java:21)
at com.company.exceptiontest.Controller.orderInfo(Controller.java:16)
at com.company.exceptiontest.ErrorHandling.run(ErrorHandling.java:12)
at com.company.App.main(App.java:8)
Process finished with exit code 0
client 메시지는 잘 전달해줬는데요; 도대체 어떤 문제인지 잘 모르겠습니다. 문제는 다음과 같습니다.
- 사용자가 어떤 주문서 번호를 요청했길래 이런 에러가 발생했는지 모릅니다.
- 어떤 코드라인에서 exception이 발생했는지를 모릅니다.
at com.company.exceptiontest.Service.searchOrderSheet(Service.java:16)
16번 라인은 다시 던지는 부분입니다. repository.findById 쪽 에러를 나타내지 않죠. - exception이 씹혔습니다. 어떤 아이디를 입력해서 에러가 발생했는지 repository쪽에서 message를 담아 알려줬는데 stackTrace엔 출력되지 않았죠.
- exception 메세지가 중복으로 출력되었습니다. 주문서 정보 요청 중 error 발생을 중복으로 호출했죠.
- try - catch문이 많아 가독성이 떨어집니다.
총체적 난국입니다. 이런 상황에서 에러를 찾으면
- break point를 걸어서 어디서 에러가 발생하는지 확인하고
- 그 에러의 근처에서 런타임 값을 확인하죠.
환경에 따라 결과가 다르다면 코드 곳곳에 log를 찍고 문제되는 환경에 배포하면서 에러를 확인해야하지요 ㅠㅠ
잘못된 코드는 많네요...
Service에서 exception을 잡아 다시 던졌을 때 error를 담아 던지지 않음
이 때문에 stackTrace에 실제로 에러가 발생한 위치가 누락되었죠.
이를 개선한다면 다음과 같습니다.
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
// exception을 담을 수 있는 생성자 선언
public CustomException(String message, Throwable e) {
super(message, e);
}
}
package com.company.exceptiontest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Service {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public Service(Repository repository) {
this.repository = repository;
}
private final Repository repository;
public OrderSheet searchOrderSheet(Long id) throws CustomException {
try {
return repository.findById(id);
} catch (CustomException e) {
logger.info("주문서 정보 요청 중 error 발생하였음!");
throw new CustomException("주문서 정보 요청 중 실패하였습니다. 재시도 후에도 문제될 경우 연락 부탁드립니다.", e); //exception 객체를 전달한다.
}
}
}
service에서 위 exception을 담아 던졌다면 다음처럼 출력됩니다.
com.company.exceptiontest.CustomException: 주문서 정보 요청 중 실패하였습니다. 재시도 후에도 문제될 경우 연락 부탁드립니다.
at com.company.exceptiontest.Service.searchOrderSheet(Service.java:21)
at com.company.exceptiontest.Controller.orderInfo(Controller.java:16)
at com.company.exceptiontest.ErrorHandling.run(ErrorHandling.java:12)
at com.company.App.main(App.java:8)
Caused by: com.company.exceptiontest.CustomException: 해당 id가 존재하지 않습니다. id: 0
at com.company.exceptiontest.Repository.findById(Repository.java:8)
at com.company.exceptiontest.Service.searchOrderSheet(Service.java:17)
... 3 more
이젠 service class의 몇번째 코드 라인에서 exception이 발생했는지 명확히 출력되고, repository의 exception도 씹히지 않게 되죠.
service에서 exception log를 찍고 다시 던짐
이 때문에 service, controller에서 중복으로 메세지를 찍었네요.
checked exception으로 선언
생각해보면 checked exception일 필요가 있을까요?? method를 call한 client 측에서 exception을 처리하리라 기대하는 건데요.
exception을 잘 처리할 수 있는 상황이 얼마나 될까요? 방금처럼 없는 id라고하면 없는 id를 뭐 만들어서 전달해줄까요? 이상하죠??
단지 고객에게 없는 주문서라고 알리고, 주문서 번호를 확인한 후 다시 입력해달라고 바랄 수 밖에요. 그래서 개인적으로는 checked exception을 쓸 상황은 매우 적다고 볼 수 있습니다.
checked exception으로 선언된 것 때문에 try - catch를 쓸 수 밖에 없으며, OCP 위배의 원인이 되기도 합니다.
따라서 이는 unchecked exception으로 선언하는 것이 더 적절하리라 판단됩니다.
일반적인 Exception class
너무 일반적이에요; 그래서 controller에서는 어떤 종류의 Exception인지 확인할 수 없기 때문에 exception 객체의 message를 전달할 수 밖에 없습니다.
그 exception 객체의 message는 service에서 생성해주고요.
그런데 각자의 역할이 적절한가요? 고객에게 message를 어떻게 보내줄지는 controller의 역할입니다. service는 business logic을 수행의 책임이 있을 뿐이죠. 그런데 service에서 표현 계층의 로직이 들어가버렸습니다.
따라서 문제를 특정지을 수 있는 exception class를 선언하고 controller에서는 해당 exception class를 catch했을 경우에 대해 메세지를 전달할 수 있도록 해야 합니다.
e.printStackTrace
System.out.println
보다 logger를 사용하는 이유는 출력에 대한 기능이 많기 때문입니다. 그런데 e.printStackTrace를 찍다니요.
stderr로 출력하길 바라는 마음에서 e.printStackTrace를 찍은 것이라면 logger에서 logger.error의 출력 방향을 stdout에서 stderr로 변경해야하는 일이구요,
단지 stackTrace를 보고 싶은 마음이라면 logger.error(message, e); 로 exception 객체를 담아주면 되는 일입니다.
따라서 이를 리팩토링을 해보겠습니다.
리팩토링
Controller
package com.company.exceptiontest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Controller {
private final Service service;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public Controller(Service service) {
this.service = service;
}
public String orderInfo(Long id) {
try {
OrderSheet orderSheet = service.searchOrderSheet(id);
return String.valueOf(orderSheet);
} catch (OrderSheetNotFoundException e) {
logger.info("주문서 정보 요청 중 error 발생!", e);
return "해당 주문서 번호는 존재하지 않습니다. 올바른 주문서 번호일 경우 연락 주시기 바랍니다.";
}
}
}
OrderSheet가 존재하지 않는 오류 유형을 잡고, 해당 유형에 따른 메세지를 클라이언트에게 적절히 전달하였습니다.
또한, stackTrace를 logger를 이용하여 출력하였죠.
package com.company.exceptiontest;
public class Service {
public Service(Repository repository) {
this.repository = repository;
}
private final Repository repository;
public OrderSheet searchOrderSheet(Long id) {
return repository.findById(id);
}
}
service측에선 exception을 catch를 한다 하여도 할 수 있는게 없습니다. 더욱이 클라이언트에게 보내는 메세지의 역할도 controller에게 넘어갔기 때문이죠.
그리고 unchecked exception이기 때문에 try catch로 잡을 건지, method signature로 exception을 나타내줄지는 선택사항이 됩니다.
코드도 깔끔해졌죠.
Repository
package com.company.exceptiontest;
import java.util.List;
public class Repository {
public OrderSheet findById(Long orderId) {
if (orderId == 0) {
throw new OrderSheetNotFoundException("해당 id가 존재하지 않습니다. id: " + orderId);
}
return new OrderSheet(List.of(new Product("안경"), new Product("물")));
}
}
CustomException대신 OrderSheeetNotFoundException을 던져줍니다. 보다 명확해졌죠. 그리고 unchecked exception이기 때문에 method signature에 exception type이 사라졌습니다.
OrderSheetNotFoundException
package com.company.exceptiontest;
public class OrderSheetNotFoundException extends RuntimeException {
public OrderSheetNotFoundException(String message) {
super(message);
}
}
exception을 담는 생성자는 만들진 않았습니다. 이제 service에선 catch를 하지 않기 때문이죠. exception handling은 최종 위치인 controller에서만 하기 때문에 exception이 씹힐 일이 없습니다.
추후에 exception을 service layer에서 handling할 일이 있다면 그때 아래의 코드를 추가 해주시면 됩니다.
public OrderSheetNotFoundException(String message, Throwable e) {
super(message, e);
}
그 외 클래스들은 모두 동일합니다. 이제, 리팩토링을 바탕으로 출력결과를 확인해볼까요?
실행결과
1) 일반 흐름의 실행결과
16:12 INFO c.c.exceptiontest.ErrorHandling - OrderSheet{products=[Product{name='안경'}, Product{name='물'}]}
16:12 INFO c.c.exceptiontest.ErrorHandling - 실행 끝
Process finished with exit code 0
2) exception 실행 결과
16:12 INFO com.company.exceptiontest.Controller - 주문서 정보 요청 중 error 발생!
com.company.exceptiontest.OrderSheetNotFoundException: 해당 id가 존재하지 않습니다. id: 0
at com.company.exceptiontest.Repository.findById(Repository.java:8)
at com.company.exceptiontest.Service.searchOrderSheet(Service.java:12)
at com.company.exceptiontest.Controller.orderInfo(Controller.java:16)
at com.company.exceptiontest.ErrorHandling.run(ErrorHandling.java:12)
at com.company.App.main(App.java:8)
16:12 INFO c.c.exceptiontest.ErrorHandling - 해당 주문서 번호는 존재하지 않습니다. 올바른 주문서 번호일 경우 연락 주시기 바랍니다.
16:12 INFO c.c.exceptiontest.ErrorHandling - 실행 끝
Process finished with exit code 0
크~
- 어떤 부분에서 에러가 발생했는지 stack trace에 명확히 나오네요. stack trace도 잘리지 않았구요.
- 에러를 유발했던 런타임 값도 무엇인지 알 수 있습니다.
- 그리고 사용자에게 맞는 메세지도 잘 전달해주었습니다.
위 정보라면 적어도 log를 찍어가며 어떤 위치에 에러가 발생했는지, 그때의 값이 무엇인지 찾아가는 수고를 충분히 덜어낼 수 있습니다!!
'이것저것' 카테고리의 다른 글
Logging AOP 구현 - 1 (0) | 2021.09.19 |
---|---|
Exception handling - java의 Exception처리(이론) (0) | 2021.08.08 |
exception handling 여정 - Redirection (0) | 2021.07.03 |
키보드로 입력한 값이 콘솔로 출력될 때까지 (0) | 2021.07.03 |
Exception handling 여정 (0) | 2021.07.03 |