제7장 - 오류처리
in Book on Clean Code
목차
로버트 C 마틴 - 클린코드 7장 정리
오류 처리
오류처리 코드로 인해 프로그램 논리를 이해하기 어려워지면 안된다.
오류 코드보다 예외를 사용하라.
물론 Optional을 사용하면 더 쉽게 구현할 수 있다. 억지로 예시를 만들기 위해 아래와 같이 로직을 작성하였다.
예외 코드를 반환
public class TestServiceImpl implements TestService {
public void isDuplicatedMember(Long memberId) {
int count = memberRepository.countMemberById(memberId);
if (count == 0) {
// 이후 정상 로직
} else {
logger.log("이미 등록된 사용자입니다.");
}
}
}
예외 던지기
public class TestServiceImpl implements TestService {
public void isDuplicatedMember(Long memberId) {
try {
tryCheckDuplication(memberId);
} catch (DuplicatedException e) {
// 별도의 로직
}
}
private void tryCheckDuplication(Long memberId) throws DuplicatedException {
int count = countMember(memberId);
//...
}
private int countMember(Long memberId) {
int count = memberRepository.countMemberById(memberId);
if (count > 0) {
throw new DuplicatedException("이미 등록된 회원입니다.");
}
return count;
}
}
위 두 예시코드를 작성해보니 확실히 두 번째 예시가 예외처리코드와
메인 로직이 분리가 되어있어 한 눈에 코드를 파악하기가 좋았다.
Try-Catch-Finally
try블록은 트랜잭션과 비슷하다. try블록에서 무슨일이 생기든지 catch블록은
프로그램 상태를 일관성 있게 유지해야 한다. 그러므로 예외가 발생할 코드를 짤 때는
Try-Catch-Finally 문으로 시작하는 편이 낫다.
Unchecked 에외를 사용하라.
📖 Java의 Unchecked, Checked Exception 정리내용
[ Checked 예외의 단점 ]
throws “Checked 예외”가 명시되어있는 메소드를 호출하는 경우, 하위 메소드를
호출하는 상위 메소드 또한 throws “Checked 예외”를 작성해줘야 한다.
그렇지 않을 경우 IDE에서 Add exception to method signature 라는 경고문구가 나온다.
위에서 확인했듯이 Checked Exception은 상위 메소드와 굉장히 의존적이다.
따라서 OOP의 원칙중 OCP를 위배하게 된다. 만약 C()
메서드에서 다른 Checked 오류를
던진다고 가정해보자. 이렇게 되면 함수에 throws 절을 수정해야 한다.
또한 변경한 메서드 C를 호출하는 A,B 모두 throws 절을 수정해줘야 한다.
결과적으로 최하위 단계에서 최상위 단계까지 연쇄적인 수정이 일어난다…!
이렇게 Checked 예외는 OCP를 위배하며, 캡슐화를 꺠버리는 단점을 가지고 있다.
[ Unchecked 예외를 사용하면? ]
Unchecked 예외를 사용하여 예외처리를 진행하면 하위 메서드가 상위 메서드에
의존적이지 않음을 확인할 수 있다. 만약 다른 Unchecked 예외로 변경하면??
상위 메서드에 영향을 미치지 않음을 확인할 수 있다.
이번 장에서는 왜 Checked Exception 보다는 Unchecked Exception을 사용 해야하는
이유를 잘 설명해준거 같다.
wrapper 클래스를 정의하라.
정상 흐름을 정의하라.
try {
Employee em = repository.findById(employee.getId());
m_total += em.getSalary();
} catch (temporaryWorkerException e) {
m_total += 0;
}
위 코드는 회사내 정규직의 총 급여를 계산하는 임시 코드이다.
계약직일 경우 총 급여에 포함하지 않기 위해 0원을 더한다.
만약 위의 코드처럼 특수한 상황을 처리하지 않으면 아래처럼 코드가 더 간결해질 것이다.
Employee em = repository.findById(employee.getId());
m_total += em.getSalary();
위 코드를 위해서는 한 가지 더 처리를 해줘야한다.
고용형태가 계약직일 경우에는 0원을 반환하는 로직이다.
public class Employee {
private WorkType workType;
private int salary;
public int getSalary() {
if (this.workType == WorkType.TEMPORARY) {
return 0;
}
return this.salary;
}
}
이를 특수 사례 패턴(Special Case Pattern) 이라고 부른다.
클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식이다. 해당 패턴을 사용하면,
클래스나 객체가 예외적인 상황을 캡슐화해서 처리하기 때문에 클라이언트 코드가 예외적인
상황을 처리할 필요가 없어진다.
null을 반환하지 마라.
null을 반환하고 싶은 경우 아래의 방법을 고려해보자.
- 별도의 예외 던지기
- 특수 사례 객체를 반환
대다수의 경우에는 특수 사례 객체를 반환하는 것이 좋다고 한다.
아래의 예시는 사용자가 작성한 게시물을 가져오는 간단한 예시다.
아래 코드에서 사용자가 등록한 게시물이 없을 경우 NullPointException이 터질것이다. 따라서 별도의 예외 처리 로직이 필요하다.
개선 전
articles.size()
를 호출할 때List<Article> articles = testService.getArticles(1);
System.out.println(articles.size()); //=> NullPointException
public class TestServiceImpl implements TestService {
public List<Article> getArticles(Long memberId) {
return repository.findAllById(memberId);
}
}
반면 개선 후 코드에서는 사용자의 게시물이 없을 때 빈 컬렉션을 반환했기 때문에
개선 후
0
이라는 값을 출력할 것이다.List<Article> articles = testService.getArticles(1);
System.out.println(articles.size()); //=> 0
public class TestServiceImpl implements TestService {
public List<Article> getArticles(Long memberId) {
List<Articel> articles = repository.findAllById(memberId);
if (articles == null) {
return Collections.emptyList();
}
return articles;
}
}
null을 전달하지 마라.
메서드에서 null을 반환하는 방식도 나쁘지만 메서드로 null을 전달하는 방식은 더 나쁘다.
정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피한다.