반응형
예외처리 방식
- 오류 코드를 리턴하지 말고, 예외를 던져라 (당연)
예전 에러코드로 반환하는 방식
- 오류가 발생한 부분에서 예외를 던진다. (별도의 예외 처리가 필요하다면 checked exception을 사용한다. 또는 throws 명시)
- checked exception을 사용하지 않는다면, 메서드 선언부에 throws를 명시
- 예외를 처리할 수 있는 곳에서 catch하여 처리
*문제점 *
- 특정 메서드에서 checked exception을 throw하고 상위 메서드에서 catch 한다면 모든 중간 메서드들은 throws를 해야 한다.
- OCP 위배 : 상위 메서드에서 하위 메서드의 디테일에 대해 알아야 한다.
- 필요한 경우 checked exception을 사용하지만, 일반적으로 득보다 실이 많다.
Checked vs Unchecked Exception
Exception을 상속하면 checked exception의 명시적인 예외처리가 필요하다. (IOException 등)
RuntimeException을 상속하면 UncheckedException 명시적인 예외처리가 필요하지 않다. (NullPointerException, IllegalArgumentException 등)
Error 클래스를 상속해 하위 클래스를 만드는 일은 자제하자.
Exception 잘쓰기
private DeviceHandle getHandle(DeviceId id){
throw new DeviceShutDownError("Invalid handle for: " + id.toString());
}
- 예외에 메시지를 담자
- 오류가 발생한 원인과 위치를 찾기 쉽도록, 예외를 던질 때 전후 상황을 충분히 덧붙인다.
- 실패한 연산 이름과 유형 등 정보를 담아 예외를 던진다.
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber){
innerPort = new ACMEPort(portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
}...
}
...
}
- 예외를 감싸는 클래스를 만든다. (exception wrapper)
- open시 발생하는 checked exception을 감싸도록 클래스를 만든다.
실무 예외 처리 패턴
getOrElse (기본값 O)
- 예외 대신 기본 값을 리턴
List<Employee> employees = getEmployees();
for (Employee e : employees) {
totalPay += e.getPay();
}
public List<Employee> getEmployees() {
if ( no employees...) {
return Collections.emptyList();
}
}
- null이 아닌 기본 값 - 컬렉션에선 emptyList를 제공한다. null 대신 size가 0인 빈 컬렉션을 사용하자.
UserLevel userLevel = null;
try {
User user = userRepository.findByUserId(userId);
userLevel = user.getUserLevel();
} catch (UserNotFoundException e) {
userLevel = UserLevel.BASIC;
}
- 도메인에 맞는 기본 값 - 컬렉션을 사용하지 않을 경우 기본 값을 처리 해주자.
- 하지만, 코드 흐름상 가독성이 좋지 않다. 다른 방법을 사용하자.
public class UserService {
private static final UserLevel USER_BASIC_LEVEL = UserLevel.BASIC;
public UserLevel getUserLevelOrDefault(Long userId){
try {
User user = userRepository.findByUserId(userId);
userLevel = user.getUserLevel();
} catch (UserNotFoundException e) {
userLevel = UserLevel.BASIC;
}
}
}
- 예외 처리를 데이터를 제공하는 쪽에서 처리해 호출부 코드가 심플해진다.
getOrElseThrow (기본 값X)
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = persistentStore.getItemRepository();
}
if (registry != null){
...
}
}
- null 체크가 빠진 부분이 발생할 수 있다. persistentStore에 대한 null 체크가 빠져있지만 알아챌 수 없다. (BAD)
- null을 메서드로 넘기는 것은 더 나쁜 상황. 특수한 상황이 아니라면 사용하지 말자.
if (user == null) {
throw new IllegalArgumentException("USER IS NOTFOUND. userId = " + userId);
}
- null 대신 예외를 던진다. (기본 값이 없다면)
assert p1 != null : "p1 should not be null";
- assert를 사용해서 null이 들어오면 에러를 발생 시킨다.
커스텀 예외 처리
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum ErrorCode {
INVALID_REQUEST("잘못된 요청입니다."),
INTERNAL_ERROR("알 수 없는 에러가 발생했습니다."),
ENTITY_NOT_FOUND("데이터를 찾을 수 없습니다.");
private String message;
}
- 자주 사용하는 에러 코드를 enum 타입으로 생성
public class HouseUtilsException extends RuntimeException {
private ErrorCode errorCode;
private String message;
public HouseUtilsException(ErrorCode errorCode) {
this(errorCode, errorCode.getMessage());
}
public HouseUtilsException(ErrorCode errorCode, String customMessage) {
super(customMessage);
this.errorCode = errorCode;
this.message = customMessage;
}
}
- RuntimeException을 상속 받아 unchecked exception을 사용 한다.
@Service
@AllArgsConstructor
public class ApartmentService {
private final ApartmentRepository apartmentRepository;
@Transactional
public Long getPriceOrThrow(Long apartmentId) {
return apartmentRepository.findById(apartmentId)
.orElseThrow(() -> new HouseUtilsException(ErrorCode.ENTITY_NOT_FOUND))
.getPrice();
}
}
- 커스텀 예외 처리를 사용
에러 로그에서 stacktrace 했을 때 우리가 발생시킨 예외라는 것을 바로 인지
에러 파악에 용이하다.
오픈소스 Exception
)
)
unchecked exception을 사용하자
본 포스팅은 Clean Code (Robert C. Martin), 제로베이스-한달한권(클린코드)를 참고하여 작성하였습니다.
반응형