1. 예외 처리의 좋은 예를 먼저 살펴보자.
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());
}
}
1) 예외를 상위로 전파
- tryToShutDown() 에서 예외를 던지고, sendShutDown() 에서 처리합니다.
- 예외가 발생한 위치와 처리 위치가 분리되어 있어, 코드의 책임이 명확합니다.
2) 로깅 처리
- 예외가 발생했을 때, 로그로 남기고 있습니다.
- 예외 상황을 추적할 수 있습니다.
2. Exception
1) Exception을 상속하면 checked Exception.
- 명시적인 예외 처리가 필요하다.
- 예 : IOException, SQLException
2) RuntimeException을 상속하면 Unchecked Exception
- 명시적인 예외 처리가 필요하지 않습니다.
- 예 : NullPointException, IllegalArgumentException, IndexOutOfBoundException
3. Checked Exception은 쓰지 말자
1) 나쁜 예시
// 상위 메서드에서 finally catch
void highLevel() {
try {
middleLevel();
} catch (IOException e) {
System.out.println("처리 불가: " + e.getMessage());
}
}
// 중단 단계 메서드도 throws 선언 필요
void middleLevel() throws IOException {
lowLevel();
}
// 하위 메서드에서 checked exception을 throw
void lowLevel() throws IOException {
throw new IOException("Disk error");
}
- 위 코드를 보면 IOException이 lowLevel에서 발생하지만, middleLevel도 throw를 써야 합니다. 그런데 highLevel에서만 실제로 처리됩니다.
- 만약 lowLevel의 예외 타입이 변경되면 middleLevel, highLevel 모두 변경을 유발할 수 있습니다.
- 코드의 유연성과 유지보수성이 떨어집니다.
2) 예외 변환(Wrapper) 패턴
void highLevel() {
try {
middleLevel();
} catch (RuntimeException e) {
System.out.println("처리 불가: " + e.getMessage());
}
}
// checked exception을 unchecked로 변환
void middleLevel() {
try {
lowLevel();
} catch (IOException e) {
throw new RuntimeException("저수준 오류", e);
}
}
void lowLevel() throws IOException {
throw new IOException("Disk error");
}
- 중간 단계에서 예외를 unchecked로 변환하여, 상위 메서드가 하위 예외의 세부사항에 의존하지 않게 합니다.
- 예외가 의미 있는 수준에서만 처리되며, 시그니처 오염이 없습니다.
3) 결론
- checked exception은 복구 가능한 상황에서만 제한적으로 사용합니다.
- 불필요한 경우에는 uncheckd exception을 사용하거나 예외 변환 패턴을 활용하는 것이 더 좋은 설계 입니다.
- 이렇게 하면 상위 코드가 하위 세부 구현에 의존하지 않으므로 OCP 위배 가능성을 줄일 수 있습니다.
4. 예외 변환(Wrapper) 다른 케이스
1) 기존 코드의 문제점
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", e);
}
- 예외가 여러 개로 분리되어 있어, 포트 열기와 관련된 모든 예외를 각각 처리해야 합니다.
- 새로운 예외가 생기면 try-catch 블록에 또 추가해야 해서 코드가 계속 길어집니다.
- 비슷한 예외 처리 코드가 반복되어 중복이 많아집니다.
- 상위 코드가 하위(ACMEPort)에서 발생하는 구체적인 예외를 모두 알아야 하므로, 결합도가 높아집니다. (유지보수 어려움)
2) 개선된 코드
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 | ATM1212UnlockedException | GMXError e) {
// 예외 추상화
throw new PortDeviceFailure(e);
}
}
// ..
}
- 예외 추상화 : LocalPort에서 하위 예외(DeviceResponseException, ATM1212UnlockedException, GMXError)를 하나의 상위 예외 PortDeviceFailure로 감쌉니다.
- 중복 제거 및 코드 간결화 : catch 블록이 하나로 줄어들었고, 새로운 하위 예외가 추가되어도 LocalPort 내부에서만 처리하면 되니, 상위 코드에는 영향이 없습니다.
- 결합도 감소, 유지보수성 향상 : 상위 코드가 하위 라이브러리(ACMEPort)의 세부 변경에 영향을 받지 않고, 예외 처리 정책을 LocalPort 클래스 내부에서 통일적으로 관리할 수 있습니다.
- OCP 준수 : 하위 라이브러리 예외가 바뀌어도, LocalPort만 수정하면 되고, 상위 코드는 그대로 둘 수 있습니다. 즉, 확장에는 열려 있고, 변경에는 닫혀 있는 구조가 됩니다.
5. 실무 예외 처리 패턴
1) getOrElse
- 예외 대신 기본값을 리턴합니다. null이 아닌 기본값, 도메인에 맞는 기본 값
- null을 리턴한다면 이후 코드에서도 모두 null 체크가 있어야 하므로 디버깅이 어렵습니다.
- null 보다 size가 0인 컬렉션을 사용하는 것이 안전하고, 빈 컬렉션이 for문에 들어가면 안전하게 종료됩니다.
2) getOrElseThrow
- 기본 값이 없다면 null 대신 예외를 던집니다.
- 데이터를 제공하는 쪽에서 null 체크를 하여, 데이터가 없을 경우엔 예외를 던집니다. 그러면 호출부에서 매번 null 체크를 할 필요 없이 안전하게 데이터를 사용할 수 있습니다.
- null을 리턴하는 것도 나쁘지만 null을 메서드로 넘기는 것은 더 나쁘다. null을 메서드의 파라미터로 넣어야 하는 API를 사용하는 경우가 아니면 null을 메서드로 넘기지 않아야 합니다.
2-1) 예제 1
// 좋은 예 - throw new ~ 사용
// null이 들어오면 unchecked exception을 발생시킵니다.
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;
}
}
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보다 예외를 던지는 방법을 훨씬 많이 사용합니다.
2-2) 예제 2
//호출부
if (request.getUserName() == null) {
throw new MyProjectException(ErrorCode.INVALID_REQUEST, "userName is null");
}
public class MyProjectException extends RuntimeException {
private MyErrorCode errorCode;
private String errorMessage;
public MyProjectException(MyErrorCode errorCode) {
//
}
public MyProjectException(MyErrorCode errorCode, String errorMessage) {
//
}
}
// enum에 메세지를 넣으면 공통적으로 사용하는 표준 메세지를 관리할 수 있습니다.
// 개발자가 실수로 메세지를 빼먹거나, 표현이 다르게 들어가도 표준 메세지 확인이 가능합니다.
// 여러 곳에서 같은 에러코드를 사용할 때, 매번 메세지를 입력하면 코드 중복이 발생합니다.
// enum에 메시지를 두면 나중에 다국어 메세지로 확장하기 좋습니다.
public enum MyErrorCode {
private String defaultErrorMessage;
INVALID_REQUEST("잘못된 요청입니다."),
DUPLICATED_REQUEST("기존 요청과 중복되어 처리할 수 없습니다."),
// ...
INTERNAL_SERVER_ERROR("처리 중 에러가 발생했습니다.");
}
- 에러 로그에서 stacktrace 해봤을 때, 우리가 발생시킨 예외라는 것을 바로 인지할 수 있습니다.
- 다른 라이브러리에서 발생한 에러와 섞이지 않습니다. IllegalArgumentException을 던지는 것보다 우리 예외로 던지는게 어느 부분에서 에러가 났는지 파악하기가 용이합니다.
[참고]
- enum에 메세지를 넣으면 공통적으로 사용하는 표준 메세지를 관리할 수 있습니다.
- 개발자가 실수로 메세지를 빼먹거나, 표현이 다르게 들어가도 표준 메세지 확인이 가능합니다.
- 여러 곳에서 같은 에러코드를 사용할 때, 매번 메세지를 입력하면 코드 중복이 발생합니다.
- enum에 메시지를 두면 나중에 다국어 메세지로 확장하기 좋습니다.
// 기본 메시지 사용
throw new MyProjectException(ErrorCode.INVALID_REQUEST);
// 상세 메시지 추가
throw new MyProjectException(ErrorCode.INVALID_REQUEST, "userName is null");
- 이렇게 하면, 기본 메시지 + 상세 메시지 조합도 가능합니다.
728x90
'Software Tech > Spring (feat.JAVA)' 카테고리의 다른 글
[클린코드] 5. 클래스 (1) | 2025.05.20 |
---|---|
[클린코드] 4. 경계 & 단위테스트 (2) | 2025.05.19 |
[클린코드] 2. 주석 & 형식 맞추기 & 객체와 자료구조 (1) | 2025.05.17 |
[클린코드] 1. 깨끗한 코드와 함수 (1) | 2025.05.15 |
[AOP] Aspect & Logging (feat. SpringBoot) (0) | 2025.05.12 |