클린코드 5장(객체와 자료구조), 6장 (오류처리)

JAVA/클린코드|2020. 6. 29. 11:25

 

객체와 자료구조


  • 객체에서 자료를 세세하게 공개하는 것 보다 추상화를 통해 표현하는 것이 더 좋다.
  • 객체는 동작을 공개하고 자료를 숨긴다.
  • 복잡한 시스템을 짜다보면 새로운 함수가 아니라 새로운 자료타입이 필요한 경우가 발생하는데 이 때는 클래스와 객체 지향 기법이 적합하다. 하지만 새로운 함수가 필요하다면 절차지향 코드와 자료구조 형태가 더 적합한 코드이다.

 

 

 

 

오류처리


 

  • 오류를 일일히 처리하는 것보다 차라리 예외를 던저버리는게 더 깔끔하다.
  • 확인된 예외를 처리하기 위해서 하위 메소드에서 throws를 하게되면 상위 메소드에서 이 예외에 대한 명시가 되어야 하기 때문에 하위의 예외 때문에 상위 메소드가 수정되어야 하는 불상사가 발생하기 때문에 수정에 닫혀 있어야 하다는 OCP규칙을 위반한 것이다.
  • 예외에 의미있는 내용을 함께 던져서 의미 파악에 도움이 되게 하라.
  • 에러가 발생하였을 때 null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 일감을 더 주게 되는 문제가 있다.
  • 에러를 모두 한 곳에서 처리하게 되면 아래와 같이 귀찮게 된다. 이를 매번 open()을 사용하는 곳에서 처리하게 되면 엄청나게 귀찮게 되고 의존성이 있어 하나의 오류가 늘어나게 되면 모든 사용하는 곳에서 처리를 해줘야 한다. 그래서 이를 별도의 클래스를 만들어서 클래스 내부에서 innerPort.open()을 실행시키게 해서 그 곳에서만 에러 내용을 처리해주고 정재된 Exception만 다시 던지게 하는 것도 방법이다.
// 문제
public void open() {
	try { 
		innerPort.open();
	} catch (DeviceResponseException e) {
		...
  } catch (NetworkErrorException e) { 
		...
  } catch (BindingException e) {
    ....
  } catch ....

} 


// 감싸기 클래스 사용
public class LocalPort {

  private ACMEPort innerPort;

  public void open() {
		try { 
			innerPort.open();
		} catch (DeviceResponseException e) {
			// 내부에서 약속된 하나의 에러 패턴을 사용.
			throw new PortDeviceFailuer(e);
	  } catch (NetworkErrorException e) { 
			throw new PortDeviceFailuer(e);
	  } catch (BindingException e) {
			throw new PortDeviceFailuer(e);
	  } catch ....
	  }
	}
}

 

 

 

 

결론

에러처리를 잘못하면 코드가 굉장히 더러워진다. 실제로 많이 경험해봤고 아직도 어렵다. 중요한건 내부에서 발생한 에러로 인해 상위에서 호출하는 함수의 코드가 변해야 하는 OCP 위반 사항을 발생시키지 않도록 하는 것 같다.

댓글()

생성한 Custom validation으로 에러메시지 출력하기

web/Spring|2018. 12. 25. 00:45

바로 직전 https://wedul.tistory.com/562?category=595982 에서 Custom validation을 만들어서 입력된 값에 validation을 체크하는 방법을 알아봤다.

그럼 이 validation체크를 통해서 front에 상황에 맞는 에러를 보내줄 수 있도록 조치를 취해보자.

우선 @valid 처리를 했었던 컨트롤러에서 에러 메시지를 수집해야한다. 


1. Controller

Spring에서 Validation 작업을 진행할 시 validation에 문제가 발생하면 에러 내용을 묶어서 BindingResult로 처리할 수 있도록 제공해준다. 이를 사용하기 위해서 parameter로 BindingResult값을 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * 회원가입
 *
 * @param reqDto
 * @return
 * @throws Exception
 */
@RequestMapping("/join")
public ResponseEntity<?> join(@Valid UserDto reqDto, BindingResult bindingResult) throws Exception {
    // check constraint rules
    this.checkConstraintRule(bindingResult);
 
    return ResponseEntity.ok(userService.insertUser(reqDto));
}
cs


2. 에러 처리

BindingResult에 validation을 체크하고 발생한 에러들에 대한 내용을 하나씩 뽑아서 국제화 메시지로 변경해주고 \n으로 데이터를 묶어서 view에 전달할 수 있도록 데이터를 바꿔 준다. 공통적으로 사용하것이기 때문에 공통 Controller 클래스를 하나 만들어서 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.wedul.common.controller;
 
import com.wedul.common.error.BadRequestException;
import com.wedul.common.util.MessageBundleUtil;
import lombok.AllArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.springframework.validation.BindingResult;
 
import java.util.stream.Collectors;
 
/**
 * wedulpos
 *
 * @author wedul
 * @since 2018-12-24
 **/
@AllArgsConstructor
public class BaseController {
 
  private final MessageBundleUtil messageBundleUtil;
 
  protected void checkConstraintRule(BindingResult bindingResult) throws BadRequestException {
    String msg = null;
    if (bindingResult.hasErrors()) {
       msg = bindingResult.getFieldErrors()
              .stream()
              .map(error -> messageBundleUtil.getMessage(error.getDefaultMessage()))
              .collect(Collectors.joining("\n"));
    }
 
    if(StringUtils.isNotBlank(msg)) {
      throw new BadRequestException(msg);
    }
 
    return;
  }
 
}
 
cs


3. 에러 핸들링

나는 에러를 에러 코드와 메시지로 전달해주는 방식을 좋아한다. 사실 다른 정보를 다 전달해줘봐야 프론트에서 처리하기도 어렵고 나머지는 로그로써 확인하는게 더 편하다. 그래서 전달하는 값을 정제하기 위해서 @ControllerAdvice를 통해 출력되는 에러를 재정의 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package com.wedul.common.config;
 
import com.wedul.common.enums.EnumErrorType;
import com.wedul.common.error.BadRequestException;
import com.wedul.common.error.ForbiddenException;
import com.wedul.common.error.NotFoundException;
import com.wedul.common.error.InternalServerException;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
 
/**
 * 에러 유형을 나타내는 Config
 *
 * @author wedul
 * @Date 2017. 07. 09
 */
@ControllerAdvice
public class ExceptionConfig {
 
  @Data
  @Builder
  private static class ErrorResponse {
    private int errCode;
    private String msg;
  }
 
  @ExceptionHandler({Exception.class})
  @ResponseBody
  public ErrorResponse errorHandler(Exception ex) {
    if(ex instanceof BadRequestException) {
      return this.getError(400, ex);
    } else if(ex instanceof ForbiddenException) {
      return this.getError(403, ex);
    } else if(ex instanceof NotFoundException) {
      return this.getError(404, ex);
    } else if(ex instanceof InternalServerException) {
      return this.getError(500, ex);
    } else {
      return ErrorResponse.builder().errCode(500).msg(ex.getMessage()).build();
    }
  }
 
  /**
   * 기본 에러 내용 출력
   *
   * @param errorCode
   * @param ex
   * @return
   */
  private ErrorResponse getError(int errorCode, Exception ex) {
    String message = ex.getMessage();
    if(StringUtils.isBlank(message)) {
      message = EnumErrorType.getErrorMsg(errorCode);
    }
 
    return ErrorResponse.builder().errCode(errorCode).msg(message).build();
  }
 
  /**
   * Error code 만들기
   *
   * @return String
   * @date 2017. 7. 9.
   * @author wedul
   */
  private String makeErrorCode(Exception ex) {
    StackTraceElement[] ste = ex.getStackTrace();
    StringBuffer sb = new StringBuffer();
    StackTraceElement[] arrayOfStackTraceElement1;
    int j = (arrayOfStackTraceElement1 = ste).length;
    for (int i = 0; i < j; i++) {
      StackTraceElement el = arrayOfStackTraceElement1[i];
      String className = el.getClassName();
      if (className.startsWith("com.wedul.wedulpos")) {
        sb.append(className.substring(className.lastIndexOf("."+ 1).toUpperCase()).append("[");
        sb.append(el.getLineNumber()).append("]");
        break;
      }
    }
    if (StringUtils.isBlank(sb.toString())) {
      return ex.getStackTrace()[0].getClassName();
    }
    return sb.toString();
  }
}
 
cs


4. 테스트 진행

1) @NotBlank, @Email 확인


2) Custom Validation인 password check



소스코드 : https://github.com/weduls/wedulpos_boot

참고 : https://meetup.toast.com/posts/147

댓글()
  1. 이정욱 2019.08.12 18:53 댓글주소  수정/삭제  댓글쓰기

    정말 깔끔하고 좋은 코드 감사합니다.

Promise에서 Unhandled Rejection 설명

web/node.js|2018. 11. 9. 23:16

Rejection 프로미스에서 발생하는 에러를 칭한다. Es6에서는 비동기 동작의 상태 표현으로 "pending", "fulfilled", "rejected" 세가지가 정의 되었다.


pending 비동기 작업중인 것을 나타내고 fulfilled 비동기 동작이 완료된것을 표현한다. 그리고 rejected 비동기가 실패한 것을 표현해준다. 

promise에서 작업이 성공할 경우에는 resolve 실행하고 실패할 경우에는 reject 수행한다.


그럼 Promise에서 에러가 발생했을 때는 어떻게 처리하는게 좋을 ?

대부분은 에러가 발생했을 때는 promise chaining 사용해서 .catch(err => { } ); 잡을 있다.


그럼 이런 경우에도 정상적으로 잡힐까?


1
2
new Promise((_, reject) => reject(new Error('woops'))).
  catch(error => { console.log('caught', err.message); });
cs


잡히지 않고 결국 Unhandled 에러가 발생한다. 그럴까? 대부분은 error 잡힐꺼 같은데 안잡히는 이유가 몰까? 이유는 우리가 잡으려고 했던 error 위에서 발생하는 것이기 때문에 reject catch 설정을 해줘야 문제없이 처리가 가능하다.


1
2
3
4
5
6
7
8
9
new Promise((_, reject) => reject(new Error('woops'))).
  catch(error => new Promise((resolve, reject) => {
    sentry.captureMessage(error.message, function(error) {
      if (error) {
        return reject(error);
      }
      resolve();
    });
  }));
cs


비동기 상태에서 에러처리는 참 쉽지 않다.


댓글()

규칙 58 복구가능 상태에는 점검지정 예외를 사용하고, 프로그래밍 오류에는 실행시점 예외를 이용하라.

JAVA/Effective Java|2018. 5. 29. 23:53

자바에는 몇 가지에 throwable을 제공한다.

점검지정 예외 (checked error)
컴파일 시점에 예외가 발생하는 부분으로 컴파일 시에 에러를 처리하는 코드를 삽입하지 않으면 컴파일이 되지 않는다.


1
2
3
4
5
6
7
8
9
10
public void ioOperation(boolean isResourceAvailable) {
  try {
    if (!isResourceAvailable) {
      throw new IOException();
    }
  } catch(IOException e) {
    // Handle caught exceptions.
  }
}
 
cs




unchecked error
컴파일 시점에 체크되지 않는 에러
실생시점 예외(runtime exception)와 오류(error)은 모두 unchecked error에 속한다.



실행시점 예외
NullPointerException, ArrayIndexOutOfBoundException와 같이 실행시간에 발생되는 에러



오류
OutofMemoryError ThreadDeath 같은 에러
catch 블럭으로 잡아도 대응 방법이 없다.




이런 예외들을 어떻게 기준을 잡고 처리해야 하는지 알아보자.

unchecked error의 기본적인 규칙은 caller 측에서 복구할것으로 여겨지는 상황에 대해서는 점검지점 예외를 사용해야한다.

-> 점검지점 예외를 사용한다는 것은 API 사용자에게 복구할 권한을 준다는 것이다. 그러므로 점검지점 예에서 발생한 에러는 catch에서 무조건 처리를 해야한다.


프로그래밍 오류를 표현할 때는 실행시점 예외를 사용하라.

-> 실행시점에 발생하는 대다수의 에러는 규칙위반으로 발생한다. 예를 들어 배열의 인덱스를 넘어가는 접근을 할때 발생하는 ArrayIndexOutOfBoundException이 대표적인 예이다.


예외 정보 문자열을 파싱해서 원하는 정보를 얻어서 처리하는 것은 굉장히 위험한 행동이다.

-> 예외정보는 각 버전 별로 변경될 수 있으며, 문자열에 경우 내용이 변하거나 없어질 수있다. 또한 정확한 예외에 대한 명세를 한 클래스 자체가 거의 없기 때문에 더욱더 위험하다.



결론을 말하면 복구 가능한경우에는 try-catch, throws를 사용하여 점검가능한 예외를 사용하고, 그것이 아닌 경우에는 실행시점 예외를 사용하라.

출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙58 인용.

댓글()