public class DeviceController {
...
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
// Check the state of the device
if (handle != DeviceHandle.INVALID) {
// Save the device status to the record field
retrieveDeviceRecord(handle);
// If not suspended, shut down
if (record.getStatus() != DEVICE_SUSPENDED) {
pauseDevice(handel);
clearDeviceWorkQueue(handle);
closeDevice(handle);
} else {
logger.log("Device suspended. Unable to shut down");
}
} else {
logger.log("Invalid handle for: " + DEV1.toString());
}
}
...
}
public class DeviceController {
...
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutDownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
DeviceHandle handle = getHandle(DEV1);
DeviceRecord record = retrieveDeviceRecord(handle);
pauseDevice(handle);
clearDeviceWorkQueue(handle);
closeDevice(handle);
}
private DeviceHandle getHandle(DeviceID id){
...
throw new DeviceShutDownError("Invalid handle for: " + id.toString());
...
}
}
즉, 사용자가 직접 구현하는 unchecked throwable은 모두 RuntimeException의 하위 클래스여야 한다.
Exception, RuntimeException, Error를 상속하지 않는 throwable을 만들 수도 있지만, 이러한 throwable은 정상적인 사항보다 나을 게 하나도 없으면서 API 사용자를 헷갈리게 할 뿐이므로 절대로 사용하지 말자.
안전적인 소프트웨어를 제작하는 요소로 확인된 예외가 반드시 필요하지는 않다는 사실이 분명해졌다.
C#은 확인된 예외를 지원하지 않는다. 영웅적인 시도에도 불고하고 C++ 역시 확인된 예외를 지원하지 않는다. 파이썬이나 루비도 마찬가지다.
그럼에도 불구하고 C#, C++, 파이썬, 루비는 안정적인 소프트웨어를 구현하기에 무리가 없다.
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {
reportPortError(e);
logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
reportPortError(e);
logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
logger.log("Device response exception");
} finally {
...
}
로그를 찍을 뿐 할 수 있는 일이 없다.
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
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);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
...
}
List<Employee> employees = getEmployees();
if (employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
getEmployees를 설계할 때, 데이터가 없는 경우를 null로 표현했는데, 다른 방법이 있을까?
null을 리턴한다면 이후 코드에서 모두 null체크가 있어야 한다.
List<Employee> employees = getEmployees();
for(Employee e : employees) {
totalPay += e.getPay();
}
public List<Employee> getEmployees() {
if( .. there are no employees .. ){
return Collections.emptyList();
}
}
복수형의 데이터를 가져올 때는 데이터의 없음을 의미하는 컬렉션을 리턴하면 된다.
null 보다 size가 0인 컬렉션이 훨씬 안전하다.
UserLevel userLevel = null;
try {
User user = userRepository.findByUserId(userId);
userLevel = user.getUserLevel();
} catch (UserNotFoundException e) {
userLevel = UserLevel.BASIC;
}
//userLevel을 이용한 처리
호출부에서 예외 처리를 통해 userLevel 값을 처리한다.
코드를 계속 읽어나가면서 논리적인 흐름이 끊긴다.
UserLevel userLevel = userService.getUserLevelOrDefault(userId);
//userLevel을 이용한 처리
public class UserService {
private static final UserLevel USER_BASIC_LEVEL = UserLevel.BASIC;
public UserLevel getUserLevelOrDefault(Long userId) {
try {
User user = userRepository.findByUserId(userId);
return user.getUserLevel();
} catch (UserNotFoundException e) {
return USER_BASIC_LEVEL;
}
}
}
예외 처리를 데이터를 제공하는 쪽에서 처리해 호출부 코드가 심플해진다.
코드를 읽어가며 논리적인 흐름이 끊기지 않는다.
도메인에 맞는 기본 값을 도메인 서비스에서 관리한다.
💡 도메인에 맞는 기본값이 없다면?null 체크 지옥에서 벗어나자
public void registerItem(Item item) {
if (item != null) {
ItemRegistry registry = peristentStore.getItemRegistry();
if (registry != null)
Item existing = registry.getItem(item.getID());
if (existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
null 체크가 빠진 부분이 발생할 수 있다.
peristentStore 에 대한 null체크가 빠져있지만 알아챌 수 없다.
코드 가독성이 현저히 떨어진다.
User user = userRepository.findByUserId(userId);
if (user != null) {
// user를 이용한 처리
}
user를 사용하는 쪽에서 매번 null 체크를 해야한다.
가독성뿐 아니라 안정성도 떨어진다.
User user = userService.getUserOrElseThrow(userId);
// user를 이용한 처리
public class UserService {
private static final UserLevel USER_BASIC_LEVEL = UserLevel.BASIC;
public User getUserOrElseThrow(Long userId) {
User user = userRepository.findByUserId(userId);
if (user == null) {
throw new IllegalArgumentException("User is not found. userId = " + userId);
}
return user;
}
}
데이터를 제공하는 쪽에서 null 체크를 하여, 데이터가 없는 경우엔 예외를 던진다.
호출부에서 매번 null 체크를 할 필요 없이 안전하게 데이터를 사용할 수 있다.
호출부의 가독성이 올라간다.
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
return (p2.x - p1.x) * 1.5;
}
}
// calculator.xProjection(null, new Point(12, 13));
// NullPointerException 발생한다.
null을 리턴하는 것도 나쁘지만 null을 메서드로 넘기는 것은 더 나쁘다.
null을 메서드의 파라미터로 넣어야 하는 API를 사용하는 경우가 아니라면 null을 메서드로 넘기지 마라
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
if(p1 == null || p2 == null) {
throw InvalidArgumentException("Invalid argument for MetricsCalculator.xProjection");
}
return (p2.x - p1.x) * 1.5
}
}
null이 들어오면 unchekced exception을 발생시킨다.
public class MetricsCalculator {
public double xProjection(Point p1, Point p2) {
assert p1 != null : "p1 should not be null";
assert p2 != null : "p2 should not be null";
return (p2.x - p1.x) * 1.5;
}
}
assert를 통해 null이 들어오면 에러를 발생시킨다.
public class MyProjectException extends RuntimeException {
private MyErrorCode errorCode;
private String errorMessage;
public MyProjectException(MyErrorCode errorCode) {
// ..
}
public MyProjectException(MyErrorCode errorCode, String errorMessage) {
// ..
}
}
public enum MyErrorCode {
private String defaultErrorMessage;
INVALID_REQUEST("잘못된 요청입니다."),
DUPLICATE_REQUEST("기존 요청과 중복되어 처리할 수 없습니다."),
// ..
INTERNAL_SERVER_ERROR("처리 중 에러가 발생했습니다.");
}
// 호출부
if (request.getUserName() == null) {
throw new MyProjectException(ErrorCode.INVALID_REQUEST, "userName is null");
}
해당 포스팅은 제로 베이스 클린코드 한달한권을 수강 후 정리한 내용입니다.