[클린코드] 7장 오류 처리

wlsh44·2022년 10월 9일
0

클린코드

목록 보기
6/8

오류 코드보다 예외를 사용하라

예외 처리를 오류 코드로 하게 되면 리턴 타입이 오류 코드이거나, 리턴하는 모든 객체가 오류 코드에 관한 맴버 변수를 항상 들고 있어야 한다.
그리고 항상 리턴하는 경우 받는 쪽에서 오류 코드를 계속 확인해야 되고 순식간에 코드가 지저분해진다.

public StateCode func() {
	Obj obj = func1();
    if (obj.getStateCode() != FUNC1_ERROR) {
    	Obj2 obj2 = func2();
        if (obj2.getStateCode() != FUNC2_ERROR) {
        	Obj3 ojb3 = func3();
            if (obj3.getStateCode() != FUNC3_ERROR) {
            	// 정상 흐름
            } else {
            	log.error("func3 에러");
                return StateCode.FUNC_ERROR;
            }
        } else {
            log.error("func2 에러");
            return StateCode.FUNC_ERROR;
        }
    } else {
        log.error("func1 에러");
        return StateCode.FUNC_ERROR;
    }
	return StateCode.FUNC_SUCCESS;
}

조금 극단적이긴 하지만 이런 함수가 func1, func2, func3 등 에서도 계속 발생할 수 있다.
그렇기 때문에 try/catch 를 잘 이용해서 오류 처리를 해야 하며, 오류 코드는 예외 객체 안으로 넣어줘 메세지를 컨트롤 하는 방식을 사용하는 것이 좋다.

public void func() {
	try {
    	Obj obj = func1();
    catch (CustomException e) {
    	log.error(e.getMessage());
    }
}

private Obj func1() throws CustomException {
	// something...
    throw new CustomException(ErrorCode.FUNC1_ERROR);
    // something...
}

private Obj func2() throws CustomException {
	// something...
    throw new CustomException(ErrorCode.FUNC2_ERROR);
    // something...
}

Unchecked 예외를 사용하라

자바에는 Checked ExceptionUnchecked Exception 이 존재한다.
예전부터 이에 관해서 많은 논쟁이 있었고 최근 들어서는 Unchecked Exception 을 사용해야 한다는 쪽으로 결론이 났다.

Checked Exception 을 사용하면 OCP를 위반할 가능성이 높다.
최하위 함수에서 예외를 호출하면 상위 함수에서 catch를 만날 때까지 중간에 있는 모든 함수가 throws 를 추가해야 한다.
이렇게 되면 모든 함수가 최하위 함수에서 발생하는 일을 알아야 하므로 캡슐화가 깨지게 된다.

따라서 정말 Checked Excpetion 이 필요한 경우가 아니라면 Unchecked Exception 을 사용해야 한다.
만약 Checked Excpetion 이 필요한 메서드를 사용할 경우, 이를 Unchecked Excpetion 으로 예외 전환 방식을 사용하여 처리한다.

Checked Exception은 왜 존재할까?

여담이지만 Checked ExceptionUnchecked Exception 를 공부할 때 왜 Checked Exception 이 존재하는지 궁금했었다.
또 그 둘을 나누는 기준에 대해서도 의문이었다.

Checked Exception 을 통해서 서비스를 좀 더 안정적으로 만들 수 있다는 것은 당연하다.
하지만 개발을 하면서 가장 많이 겪는 예외는 아마도 UnChecked ExceptionNullPointerException 일 것이다.
그렇다면 진짜 안정적인 서비스를 만들기 위해서 NullPointerException 와 같은 예외도 CheckedExcepion 으로 만들어 오브젝트에 접근하는 모든 코드에 대해 오류 처리를 하면 되지 않을까 라는 생각을 했다.

그러다가 CheckedExcepion 에 어떤 예외가 있는지 궁금해져 찾아본 후 약간의 답을 얻었다.

대표적인 CheckedExcepion 으로는

  • SQLExcepion
  • IOException
  • ClassNotFoundException
  • FileNotFoundException

등이 있다.

이 예외들의 공통점은 개발자가 컨트롤 할 수 없는 부분이라는 것이다.
해당 예외들은 코드 내에서 발생하는 것이 아니라 외부의 원인으로 발생한다.
예를 들어 SQLExcepion 같은 경우, db 서버에서 에러가 발생하고, FileNotFoundException 같은 경우 os에서 해당 파일을 찾지 못했기 때문에 발생하는 에러이다.

하지만 NullPointerException, IndexOutOfBoundsException 같은 경우, 개발자에 의해 발생하는 에러이며, 개발자가 처리만 잘 한다면 절대 일어나지 않는 예외다.
그렇기 때문에 개발자가 코드로 다룰 수 없는 예외를 처리하기 위해 Checked Exception 이라는 개념이 만들어지지 않았나 라는 생각을 하게 되었다.

예외의 의미를 제공하라

오류 메세지에 정보를 담아 예외와 함께 던진다.
실패한 연산 이름과 실패 유형도 언급한다.
catch블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

오류 정보를 귀찮아서 대충 담았다가 나중에 로깅할 때 피눈물 보지 말자. 😂

호출자를 고려해 예외 클래스를 정의하라

어떤 오류인지 파악하기 위해 오류를 분류하는 것도 중요하지만 개발을 위해 설계적으로 본다면 오류를 잡아내는 방법이 제일 중요하다.

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);
}

책에 있는 예제이다.
과연 port.open() 을 호출하는 메서드에서 어떤 예외가 발생했는지 다 알아야 할까?
만약 port 내부가 수정이 되어 오류가 바뀌거나 추가되면 위 메서드도 수정을 해야되기 때문에 OCP를 지킬 수 없게 된다.

따라서 해당 예외들을 하나의 유형으로 전환 시켜주는 방식으로 해결한다.

try {
	port.open();
} catch (PortDeviceException e) {
	reportPortError(e);
    logger.log(e.getMessage(), e);
}

public class LocalPort {
	private ACMEPort innerPort;
    
	public void open() {
    	try {
        	innerPort.open();
    	} catch (DeviceResponseException e) {
            throw new PortDeviceException(e);
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceException(e);
        } catch (GMXError e) {
            throw new PortDeviceException(e);
        }
    }
}

정상 흐름을 정의하라

catch 에 비즈니스 로직이 들어가는 방식은 좋지 않다.

try {
	Integer num = obj.getInteger();
    total += num;
} catch (NullPointerException e) {
	total += 0;
}

와 같은 방식이 아닌 특수 상황을 캡슐화를 통해 객체 내부에서 처리해, 클라이언트가 이 상황을 모르게 하는 방식으로 차리한다.

Integer num = obj.getInteger();
total += num;

public class Obj {
	private Integer num;
	public Integer getInteger() {
    	return num == ? 0 : num;
    }
}

이런 방식을 특수 사례 패턴 이라고 한다.

null을 반환하지 마라

앞서도 언급했지만 개발자가 자주 접하는 예외 중 하나가 NullPointerException 이다.
이를 통제하기 위해 자바8 부터 Optional 등을 제공하지만 그보다 먼저 해야 할 행동이 null 을 반환하지 않는 것이다.

언제나 개발에서 실수는 일어나는 법이고 null 을 리턴하게 되면 그런 실수가 발생할 가능성이 아주 높아진다.

특히나 자바에서 Collecion, List 와 같은 객체들은 자체적으로 isEmpty() emptyList() 같은 메서드를 제공하기 때문에 null 을 리턴하는 것보다 빈 Collection, List 객체를 리턴하는 것이 좋다.

느낀 점

개발을 하면서 예외 처리에 대한 중요성을 점점 느껴가던 도중에 이 부분을 읽어서 굉장히 좋았다.
특히나 크롤링과 파싱을 하는 프로젝트에서 언제 데이터의 포맷이 바뀔 지 알 수 없어 예외 처리에 대한 중요성을 더욱 깨닫고 최대한 처리를 해주려고 노력을 하고 있다.
또 어떤 방식으로 예외에 오류 정보를 담을지 고민을 정말 많이 했었다.
사실 이 글을 쓰는 시점에 이미 14장까지 다 읽은 상태에서 14장 코드를 따라하며 리팩토링을 해봤는데 예외 처리에 대한 방식에 대해 조금 더 배울 수 있었던 것 같다.

profile
정리정리

0개의 댓글