[CleanCode] 7장 - Error Handling
Overview
*프로그램이 잘못될 가능성은 늘 존재한다. 그 원인은 **Error(오류)*이다.
Error & Exception in Runtime
Error (오류)
시스템이 종료되어야 할 수준의 심각한 상황 (프로세스 종료)
ex) OutOfMemoryError, StackOverflowError
Exception (예외)
개발자가 구현한 로직에서 발생하거나 사용자에 의해 발생
Copy data class Person ( val name: String , val age: Int )
// 1. What Exception?
fun main (args: Array < String >) {
var person: Person ? = null
val age: Int ? = person !! .age
println (age)
}
// 2. What Exception?
open class Animal {}
class Dog : Animal () {}
class Cat : Animal () {}
fun main (args: Array < String >) {
val dog = Dog ()
val cat = dog as Cat
}
상황에 맞는 Exception Handle을 해야한다.
Exception Class
Throwable
Error / Exception에 대한 메세지를 담고, Chained Exception에 대한 정보들을 기록
The Throwable
class is the superclass of all errors and exceptions in the Java language. Only objects that are instances of this class (or one of its subclasses) are thrown by the Java Virtual Machine or can be thrown by the Java throw
statement. Similarly, only this class or one of its subclasses can be the argument type in a catch
clause. For the purposes of compile-time checking of exceptions, Throwable
and any subclass of Throwable
that is not also a subclass of either [RuntimeException](https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/RuntimeException.html)
or [Error](https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/Error.html)
are regarded as checked exceptions …
Throwable(String message, Throwable cause)
Copy throw new Exception( "Error Message" )
Copy throw Exception( "Error Message" )
호출한 곳에 Exception 발생 여부 통보
Copy void method () throws Exception1, Exception2 {
//...
}
오류 코드보다 예외를 사용하기
🌱 로직과 Exception 처리를 분리하기
Copy public class DeviceController {
**public void sendShutDown () { **
DeviceHandle handle = getHandle(DEV1) ;
// 디바이스 상태를 점검한다.
if (handle != DeviceHandle . INVALID ) {
// 레코드 필드에 디바이스 상태를 저장한다.
retrieveDeviceRecord(handle) ;
//디바이스가 일시정지 상태가 아니라면 종료한다.
if ( record . getStatus () != DEVICE_SUSPENDED) {
pauseDevice(handle) ;
clearDeviceWorkQueue(handle) ;
closeDevice(handel) ;
} else {
logger . log ( "Device suspended. Unable to shut down" );
}
} else {
logger . log ( "Invalid handle for: " + DEV1 . toString ());
}
}
}
Copy 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()) ; **
...
}
}
Try-Catch-Finally
Copy try {
// Exception 발생이 예상되는 코드 블록
} catch (e: Exception ) {
// Exception이 발생했을 때 실행되는 블록
} finally {
// Exception 발생과 상관없이 무조건 실행되는 블록
}
Copy int c = 4 / 0 ;
// Exception in thread "main" java.lang.ArithmeticException:
Copy int c;
try {
c = 4 / 0 ;
} catch ** (ArithmeticException e) ** {
c = - 1 ; // 예외가 발생하여 수행되는 코드
}
예외가 발생하더라도 반드시 실행되어야 한다면,,
Copy public void shouldBeRun () {
System.out. println ( "ok thanks." );
}
public static void main (String[] args) {
Sample sample = new Sample ();
int c;
try {
c = 4 / 0 ; // exception !!
sample. shouldBeRun ();
} catch (ArithmeticException e) {
c = - 1 ;
}
}
// finally
public static void main (String[] args) {
Sample sample = new Sample ();
int c;
try {
c = 4 / 0 ;
} catch (ArithmeticException e) {
c = - 1 ;
} finally {
sample. shouldBeRun (); // 예외에 상관없이 무조건 수행.
}
}
[Q] try-catch와 return 값
Copy String getTryCatch () {
String word = "do" ;
try {
word = "try" ;
System.out. println (word);
return word;
} catch (Exception e) {
word = "catch" ;
System.out. println (word);
return word;
} finally {
word = "finally"
System.out. println (word);
return word;
}
}
// 최종 리턴값
Copy String getTryCatch () {
String word = "do" ;
try {
word = "try" ;
System.out. println (word);
return word;
} catch (Exception e) {
word = "catch" ;
System.out. println (word);
return word;
} finally {
word = "finally"
System.out. println (word);
// return word;
}
}
// 최종 리턴값
Copy String getTryCatch () {
String arr = { "none" , "none" , "none" }
try {
arr[ 0 ] = "first" ;
return arr;
} catch (Exception e) {
arr[ 1 ] = "second" ;
return arr;
} finally {
arr[ 2 ] = "third"
// return arr[2];
}
}
// 최종 리턴값
// ?
finally 에서의 리턴은 자제하자
파일이 없으면 예외를 던지는지 알아보는 단위 테스트
Copy @Test (expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName () {
sectionStore. retrieveSection ( "invalid - file" );
}
Copy public List < RecordedGrip > retrieveSection (String sectionName) {
// 실제로 구현할 때까지 비어 있는 더미를 반환
return new ArrayList < RecordedGrip >();
}
Copy public List < RecordedGrip > retrieveSection (String sectionName) {
try {
FileInputStream stream = new FileInputStream (sectionName);
} catch (Exception e) {
throw new StorageException ( "retrieval error" , e);
}
return new ArrayList < RecordedGrip >();
}
Copy public List < RecordedGrip > retrieveSection (String sectionName) {
try {
FileInputStream stream = new FileInputStream (sectionName);
stream. close ();
} **catch (FileNotFoundException e) **
throw new StorageException ( "retrieval error" , e);
}
return new ArrayList < RecordedGrip >();
}
Copy try {
loginApiClient. login (request)
} catch (e: LoginException ) {
if (e.errorCode == "INVALID_PASSWORD" ) {
return null
} else {
throw e
}
}
Copy return runCatching {
loginApiClient. login (request)
}. onFailure { e ->
if (e.errorCode != "INVALID_PASSWORD" ) throw e
}. getOrNull ()
Copy @InlineOnly
@SinceKotlin ( "1.3" )
public inline fun < R > runCatching (block: () -> R): Result < R > {
return try {
** Result. success ( block ()) **
} catch (e: Throwable ) {
** Result. failure (e) **
}
}
Exception은 크게 3가지 형태로 처리 가능
회피 : 호출한 쪽으로 그대로 전달 (throw)
Copy // 회피
fun exceptionTest (): Nothing {
throw IOException ( "exception" )
}
복구 : 복구 또는 무시 가능한 경우에 catch 블럭 내에서 처리하거나 예외상황 처리후 에러 코드 반환
Copy // 복구
fun exceptionTest (): String {
return try {
"NoException"
} catch (e: Exception ) {
"Exception"
}
}
전환 : 특정 Exception으로 변환하여 전달 (throw)
Copy // 전환
fun exceptionTest (): String {
return try {
"NoException"
} catch (e: NullPointerException ) {
throw CustomException
}
}
Unchecked Exception을 사용하기
CheckedException
단순 예외, 즉 컴파일 시 발생하는 Exception
UnCheckedException
프로그램 실행시 발생하는 Runtime Exception
RuntimeException을 상속받는 Exception → Unchecked Exception
Kotlin의 모든 Exception은 Runtime Exception을 상속받는다.
UnCheckedException을 사용해보자
Copy class FoolException extends ** RuntimeException ** {
}
public void sayNickname (String nickname) {
if (nickname. equals ( "fool" )) {
throw new FoolException ();
}
System.out. println ( "당신의 별명은 " + nickname + " 입니다." );
}
public static void main (String[] args)
sayNickname ( "fool" ); // Exception in thread "main" FoolException
sayNickname ( "genious" );
}
Checked Exception을 사용해보면?
예측 가능한 CheckedException → 예외처리 강제
Copy class FoolException extends ** Exception ** {
}
Copy public void sayNickname (String nick) {
try {
if (nick. equals ( "fool" )) {
throw new FoolException ();
}
System.out. println ( "당신의 별명은 " + nick + " 입니다." );
} catch (FoolException e) {
System.err. println ( "Fool Exception 발생" );
}
}
sayNickname 메서드에서 예외 발생 & 예외 처리 모두 하는 것?
sayNickname을 호출한 곳에서 예외 처리를 한다면? ✨
Copy // callee
public void sayNickname (String nickname) ** throws FoolException { **
if (nickname. equals ( "fool" )) {
throw new FoolException ();
}
System.out. println ( "당신의 별명은 " + nickname + " 입니다." );
}
// caller
public static void main (String[] args) {
try {
sayNickname ( "fool" );
sayNickname ( "genious" );
} **catch (FoolException e) {
System.err. println ( "Fool Exception 발생" );
} **
}
sayNickname에서 예외 처리 vs main에서 예외처리
Copy sayNickname ( "fool" );
sayNickname ( "genious" );
Exception을 처리하는 위치는 중요하다.
in Clean Code,,
Checked Exception은 비용이 있다!
🌱 Checked Exception은 **OCP(Open-Closed Principle)를 위반한다.**
Copy fun callee (flag: Boolean ) {
if (flag) println ( "호출당했어!" )
}
fun caller (flag: Boolean ) {
callee (flag)
}
출력을 안하는 Exception을 던질 때
하위 단계 코드 변경 → 상위 단계 코드 변경
Copy fun callee (flag: Boolean ) throws NotPrintException {
if (flag) {
println ( "호출당했어!" )
}
else {
throw NotPrintException ()
}
}
fun caller (flag: Boolean ) ** throws NotPrintException ** {
callee (flag)
}
🌱 Checked Exception은 **캡슐화를 깰 수 있다.**
Exception에 의미를 제공하기
🌱 오류가 발생한 **원인과 위치**를 찾을 수 있어야 한다.
message에 유의미한 정보를 담아서 던지기
Logging 가능 → catch 블록에서 오류 기록
Copy // ...
throw new IllegarArgumentException() ;
// ...
throw new InvalidSearchArgumentException( "검색 조건은 빈칸일 수 없습니다." ) ;
Exception Class 정의하기
호출자(Caller)를 고려해서 Exception을 정의할 수 있다.
Copy /**
* + 앱에서 발생하는 클라이언트 및 서버 에러 정보
* + [message] 메세지
* + [cause] 원인이 되는 익셉션
*/
class MyException (
message: String ? = null ,
cause: Throwable ? = null
): Exception ( message , cause ) {
/**
* 에러 코드 [ErrorCode]
*/
var errorCode: Int = ErrorCode.UNKNOWN_ERROR
private set
/**
* 사용자에게 서버 에러 메세지 표시가 필요한 경우 값을 가진다.
*/
var serverAlert: ErrorMessage ? = null
private set
data class ErrorMessage ( val code: Int , val message: String )
companion object {
/**
* 사용자 고지가 필요한 서버 에러 정보를 포함한 Exception 생성
*/
fun alertOf (code: Int , message: String ? = null ): MyException {
return MyException (). apply {
errorCode = ErrorCode.SERVER_ALERT_ERROR
serverAlert = ErrorMessage (code = code, message = message ?: "" )
}
}
/**
* 일반적인 서버 에러로 부터 Exception 생성
*/
fun serverErrorOf (code: Int , message: String ? = null ): MyException {
val error = ErrorCode. parseServerError (code)
return MyException (message). apply {
errorCode = error
}
}
/**
* 원인 익셉션으로 Exception 생성
*/
fun exceptionOf (cause: Exception , message: String ? = null ): MyException {
val error = ErrorCode. parseException (cause)
return MyException (
message = message ?: cause.message,
cause = cause
). apply {
errorCode = error
}
}
}
}
정상 흐름을 정의하기
Copy try {
MealExpenses expenses = expenseReportDAO . getMeals ( employee . getID ());
m_total += expenses . getTotal ();
} catch ( MealExpenseNotFound e) {
m_total += getMealPerDiem() ;
}
특수한 Exception이 있을 상황이 아니다.
Copy MealExpenses expenses = expenseReportDAO . getMeals ( employee . getID ());
** m_total += expenses . getTotal (); **
DAO를 수정 → 항상 MealExpense 객체를 반환
Copy public class PerDiemMealExpenses implements MealExpenses {
public int getTotal () {
// 일일 기본 식비를 더하도록
}
}
Special Case Pattern (특수 사례 패턴)
클래스 or 객체를 조작해서 특수한 사례를 처리
Null을 반환하지도, 전달하지도 마라
Copy 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);
}
}
}
}
직원 리스트를 가져와서 총 급여를 구하는 로직
Copy List < Employee > employees = getEmployees() ;
**if (employees != null ) { **
for ( Employee e : employees) {
totalPay += e . getPay ();
}
}
Copy List < Employee > employees = getEmployees() ;
for ( Employee e : employees) {
totalPay += e . getPay ();
}
// Colections.emptyList() 활용
public List< Employee > getEmployees() {
if ( /* 직원이 없다면 */ ) {
return ** Collections . emptyList (); **
}
}
메서드가 null을 반환하는 방식도 나쁘지만, 메서드에 null을 전달하는건 더 나쁘다.
Copy public double xProjection( Point p1 , Point p2) {
return ( p2 . x - p1 . x ) * 1.5
}
Copy calculator . xProjection ( null , new Point( 2 , 3 ) );
Copy public double xProjection( Point p1 , Point p2) {
if (p1 == null || p2 == null ) {
throw InvalidArgumentException( "Invalid Point" ) ;
}
return ( p2 . x - p1 . x ) * 1.5
}
Copy 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 ;
}
🌱 *애초에 null을 넘기지 못하게 하라*
결론
명시적 Exception은 반드시 이름을 지정하여 catching
어플리케이션에서 구현한 함수 내에서 Exception 발생이 예상되는 경우
최상위 Exception 클래스에서 반드시 catch 처리
Exception을 처리하는 궁극적인 위치가 중요.
Exception 발생시에는 Crashlytics 와 같은 에러 로그 라이브러리 등을 이용하여 개발자가 인지할 수 있도록 하기