예외처리에 대한 생각.

올바른 예외처리란?

공부는 과거에 토비의 스프링을 읽으면서 공부했습니다. 예외처리 관련해서는 이 책을 보고 공부했기 때문에 대부분 이 내용 중에 머리속에 있는 내용을 토대로 작성될 것 같습니다. 문제시 삭제하겠습니다.

자바를 맨 처음 배울 때 throw가 뭔지 몰랐다. try & catch는 명시적으로 시도해보고, 에러나면 잡는다.
잡는 코드도 있고 처리해야할 방향도 있다. 그러나 throw는 던진다는데 도대체 어디로 던진다는 것인가 이런 고민을 했었던 때가 있다.

지금도 코드 작성 할 때마다 테스트코드를 작성하지만 예외처리 또한 고민하게 된다.(솔직하게 귀찮은 작업 하지만 중요한 작업 이라고 생각한다.)

보통 IDE가 에외처리가 발생할 것 같으면 try, catch를 제안한다. 그럼 쉽게 알트 + 엔터 눌러주고 try, catch 자동완성 되면

try{

/** 코드 작성 내용 */

}catch (Exception e){
e.printStackTrace();
}

이런 상황이 오는데 이렇게 넘어가버리면 나중에 문제가 발생해도 무슨 문제가 발생하는지 모른다. 이 뿐만 아니라 (exception e) 해놓고 e.printStackTrace 이것도 마찬가지이다. 이렇게 해놓으면 계속 로그 찍히는걸 감시해야하는 상황이 발생한다. 그렇다면 어떻게 해야할까?

예외 Error, Exception, RuntimeException과 같은 애들이 있다. Error의 경우 안드로이드에서 많이 뱉어내는 OOM(Out of memory — 사이즈가 큰 이미지를 불러 올 때 자주 발생시킴)것이 있다. 그냥 서버가 죽어버릴 것이다(안드로이드가 죽는 것처럼) 이러한 것은 달빅이든 버츄어 머신이든 일으키는 것이므로, 예외처리보다는 별다른 코드를 작성한다던지 다른 작업들을 해줘야 한다.

예외에는 체크에러와 언체크에러로 나뉜다. 체크에러는 말그대로 예상될 에러를 체크해주는 것이고 언체크 에러는 예외처리를 강제하지 않는 것이라고 생각하면된다. 친구 중 하나는 매일 나에게 물어보는 것이 NullPointerException이다. 버그를 잡아달라면 항상 이거.. 이거는 그냥 자신이 짠 코드를 차례대로 디버깅해보면 되는데.. 이 외에도 문법이 적절하지 않는 것 등이다. 참고로 안드로이드는 찾기가 힘든 것이 xml에 정의된 레이아웃의 아이디를 잘못 지정해서 이를 참조할 경우에 나는 에러가 많이 발생한다. 레이아웃의 동작해야할 컴포넌트를 모두 명명해줘야하니 중복되는 것도 발생하는 경우도 있고.. 이럴경우 많이 발생한다.
 
결론적으로 말하면 개발자가 코드를 잘못 작성하면 이런 언체크 에러를 발생한다. 체크 에러는 시스템에서 강제하기 때문에 자연스럽게 커버가 가능하다. 하지만 불필요한 예외처리까지 해줘야하기 때문에 문제가 많다.

어떻게 해야 좋은 예외처리를 만들 수 있을까.

제가 배우고 여러가지 비교해보면서 생각한 결과 방법은 다음과 같습니다. 
1. 예외를 일으켰을 때 다시 재 작동하도록 해주는 처리
글로벌 시대에 맞추어 애플리케이션이 전 세계적으로 퍼져나간다면 Amazon Web Service같은 전 세계적인 클라우드를 쓰지 않는 한 멀리서 접속하는 클라이언트의 네트워크 상황은 좋지 않을 수 밖에 없습니다. 이러한 경우 IOException이 발생하게 되는데, 이 때 재 접속 하도록 만들어 주는 겁니다. 예시 코드는 다음과 같습니다.

//최대 접속 요청 회수 지정
int repeat = 5;

while (repeat --> 0){
try{
// 작업
System.out.printf("connecting\n");
}catch(Exception e){
//대기
}finally{
// 접속 관련 또는 작업 정리
}
}
//실패시 예외 발생 처리하는 클래스로 넘김
throw new failedConnectionException();

다음과 같이 최대횟수만큼 반복적으로 시도하게해서 예외상황에서도 복구할 수 있도록 해주는 작업을 더했습니다.

2. 1번은 이상적이지만 매우 번거로운 방법이라고 생각합니다. 2번은 제가 자주 사용하는 방법입니다. ( 예시는 스프링 프레임워크 코드입니다.) 제가 작성한 코드의 일부를 발췌해왔습니다. 유저가 회원가입을 하려고 할 때 email이 중복되어 에러를 발생시키는 상황을 가정하였습니다. (email을 pk로 지정하였습니다.)

/**
* 유저 중복 예외 처리 클래스
*/
@Getter
public class UserDuplicatedException extends RuntimeException {

private String email;

public UserDuplicatedException(String email) {
this.email = email;
}

}

이 부분은 예외처리를 담당할 클래스 입니다.

/**
* 데이터베이스에 User정보를 저장한다
*
@param dto
*
@return
*/
public User createUser(UserDto.Create dto) {

User user = modelMapper.map(dto, User.class);

String email = dto.getEmail();

/** 유저가 존재할 경우 예외처리 */
if(repository.findByEmail(email) != null){
throw new UserDuplicatedException(email);
}

/** 가입 날짜, 업데이트 날짜 삽입 */
Date now = new Date();
user.setJoined(now);
user.setUpdated(now);

return repository.save(user);
}

이 부분은 예외를 발생시키는 부분입니다. JPA repository에서 유저를 생성하기 전에 email이 이미 존재하는지 체크하여 throw로 위의 예외 클래스로 던지는 부분입니다.

/**
* 유저 중복 예외 처리
*
@param exception
*
@return
*/
@ExceptionHandler(UserDuplicatedException.class)
public ResponseEntity userDuplicatedExceptionHandler(UserDuplicatedException exception){
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setMessage("[" + exception.getEmail()+ "] 중복된 userName 입니다");
errorResponse.setCode("duplicated.username.exception");
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

이 방법은 Runtime Exception을 예로 들었습니다. 제가 작성한 코드 방법은 서비스 부분에서 에러가 발생하면 예외를 던지고 그 예외를 스프링에서 제공하는 예외핸들러를 통해서 잡아서 유저에게 어떤 점이 잘못되었는지 명시적으로 알려주는 방법입니다. 단순히 로그를 찍거나, 무시시키거나 하는 방법이 아닌 클라이언트에서 어떤 문제가 발생하는지 알 수 있으므로 명확하고 간결하다고 생각합니다. 또한 공통적으로 예외 클래스들을 묶어 놓으면 협업자들 끼리도 어떤 곳에서 에러가 발생하겠다는 것을 명시적으로 알 수 있으므로 깔끔한 코드라고 생각합니다.

과거의 나에게.

과거에 적었던 내용인데 정말 이상하게 글을 적어놨군.. 결국 retry 시키거나 해당 에러 내용을 내려줘 클라이언트에게 인지시켜주고 또 다른 액션을 요구하거나 인데.. 가장 베스트는 몇회 시도 시켜주고 안되면 사용자에게 액션을 취하게하는게 올바른 것 같습니다. rx를 사용해서 코드적인 면으로 봤을 때는 실패 놨을 때와 성공 했을 때 method callback을 넘겨 받아서 처리해주면 깔끔할 것이라 생각합니다. 예를 들어 onSuccess나 onError처럼