클린 코드(로버트 C. 마틴 저)를 참고하여 작성하였습니다. 현재 회사에서 개발하다 든 생각을 간략히 남기기 위함이고, 개발 환경은 스프링 및 웹스퀘어를 사용하고 있습니다. 글에 대한 피드백이 있다면 댓글로 남겨주시면 감사하겠습니다.
___
보통 웹개발을 하면서 가장 중요하다고 할 수 있는 것 중 하나는 오류[1]에 대한 처리다. 즉, 사용자가 겁을 먹을 수 있기 때문에(?) 직접 404나 500 오류 메세지[2]를 보지 않도록 설정 혹은 처리해주어야 한다. 특히 500의 경우 그 원인이 다양하기에[3] 세밀한 처리를 할 수록 좋다.
RDB를 사용하는 대부분의 개발이 그렇겠지만 현재 회사에서 개발하고 있는 화면 역시 여러 테이블에서 데이터를 조회해온다. 이를 테면 사용자가 부서 정보(PK)로 부서를 조회하면, 화면에서는 부서 정보와 함께 해당 부서의 사원 목록을 조회해오는 식[4]이다. 요청 한 번에 부서 정보 및 사원 정보를 다 가져오는데, 사원 정보를 가져오기 위해선 부서 정보가 필요한, 아래와 같은 코드였다.
/*
* 컨트롤러는 화면, 서비스 및 매퍼는 도메인 기준으로 생성
*/
@Autowired
EmployeeService employeeService;
@Autowired
DeptService deptService;
@GetMapping("")
public Map<String, Object> selectEmployeeListByDept(Dept dept) {
Result result = new Result();
Map<String, Object> map = new HashMap<String, Object>();
dept = deptService.selectOne(dept);
employee = employeeService.selectListByDept(getEmployeeByDept(dept)); // NullPointerException 야기
// 생략
return result;
}
private Employee getEmployeeByDept(Dept dept) {
Employee employee = new Employee();
employee.setDeptCd(dept.getDeptCd());
return employee;
}
위에도 적혀 있듯 만약 조회 조건에 따른 부서 정보가 없다면, 직원 리스트를 조회하는 메소드(selectListByDept)에서 NullPointerException[5]이 발생하게 된다. 즉 아마 스택 트레이스 윗 부분이 아래와 같을 것이다.
com.test.web.TestController.getEmployeeByDept
com.test.web.TestController.selectEmployeeListByDept
그래서 만약 조회된 부서정보가 없다면 예외를 발생시켜 주는 코드를 아래와 같이 추가했다.
if (dept == null) {
throw new TestException("부서 정보가 존재하지 않습니다.");
}
이는 실제로 프로젝트 개발 가이드 문서에 Exception 처리에 대하여 try-catch의 사용을 자제하고, 사용할 경우 exception을 throw하라고 명시되어 있기 때문이었는데, 만약 개발 가이드를 보지 않았다면 예외를 던지지 않고 if-else문을 통해 오류에 대한 처리를 했을 수도 있었을 것 같다.
한편, 클린 코드에서도 이 주제와 관련해 오류 코드보다 예외를 사용하라고 언급하고 있다. 그 이유는 오류 처리 코드가 원래 코드에서 분리되면 코드가 깔끔해지기 때문이다. 실제로 책에 언급되어 있는 예를 통해 비교해보도록 하자.
/*
* 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 원칙을 미묘하게 위반한다.
* 자칫하면 아래와 같이 if문에서 명령을 표현식으로 사용하기 쉬운 탓이다.
*/
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
logger.log("page deleted");
} else {
logger.log("configKeys not deleted");
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("delete failed");
return E_ERROR;
}
/*
* 아래와 같이 try/catch 블록은 별도 함수로 추출해내는 것이 좋다.
* 함수는 한 가지 작업만 해야 하고, 오류 처리도 한 가지 작업이다.
* 즉, 아래와 같이 함수에 키워드 try가 있다면 함수는 try문으로 시작해 catch/finally문으로 끝나야 한다.
*/
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page pages) throws Exception {
deletePages(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
위의 delete 함수는 모든 오류를 처리하고, 실제로 페이지를 제거하는 함수는 deletePageAndAllReferences다. 이렇게 정상 동작과 오류 처리 동작을 구분한다면 코드를 이해하고 수정하기 쉬워진다.
___
1. 개발자가 해결할 수 있다는 점에서 에러 대신 오류라는 표현을 사용했다. 자바 개념서에 에러(Error)와 예외(Exception)에 대한 부분은 항상 언급되는데 에러는 개발자가 잡을 수 없다.
- Error: An Error indicates serious problem that a reasonable application should not try to catch.
- Exception: Exception indicates conditions that a reasonable application might try to catch.
2. 정확히 말하면 HTTP 상태 코드다. 메서드가 서버에게 무엇을 해야 하는지 말해주는 것처럼, 상태 코드는 클라이언트에게 무엇이 일어났는지 말해준다.
- 400~499: 클라이언트 에러 상태 코드
- 500~599: 서버 에러 상태 코드
3. NullPointerException, IndexOutOfBoundException, ArithmeticException 등 RuntimeException을 상속한 모든 클래스들이 포함된다.
4. 부서 테이블이 사원 테이블의 부모 테이블이라고 할 수 있다.
___
참고자료
댓글