Exception 클래스의 구현과 AOP 설정을 이용한 예외처리

 

API 서버 개발을 하며 케이스별 에러코드와 에러메시지를 response 값으로 던져주어야 했다.

각각의 API(Controller)에 try catch 로 exception 을 잡아 상황별 에러코드 및 에러메시지를 던지기엔 API 갯수가 너무 많았고, 비효율적(노가다)이라 생각하여

enum과 customized Exception class, AOP 설정을 통해 해결했다.

 

1. ENUM 클래스 작성
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
public static enum APIResult {
        SUCCESS (200"success"),
        FAILURE (999"fail"),  //기타 에러
        E_PARAM_NULL(101"input parameter is null"),  // 필수값 누락 에러
        E_FORMAT(102"input parameter data format error"), // 파싱 에러
        E_NETWORK(104"network error"), //네트워크 에러
        
        private int code;
        private String message;
        
        private APIResult(int _code, String _message) {
            this.code = _code;
            this.message = _message;
        }
        
        public static APIResult search(int code) {
            for (APIResult status : APIResult.values()) {
                if(status.getCode() == code) {
                    return status;
                }
            }
            return null;
        }
      
        public int getCode() {
            return code;
        }
 
        public void setCode(int code) {
            this.code = code;
        }
 
        public String getMessage() {
            return message;
        }
 
        public void setMessage(String message) {
            this.message = message;
        }
}
cs
예외 케이스별로 던져줄 Error Code와 Error Message를 Enum 클래스로 작성.
 

2. customized Exception class 작성

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
package ;
 
public class APIException extends RuntimeException {
 
    private int ERR_CODE = APIResult.FAILURE.getCode();
 
    public APIException()  {
        super();
    }
 
    public APIException(String errDesc) {
        super(errDesc);
    }
 
    public APIException(String errDesc, int code) {
        super(errDesc);
        this.ERR_CODE = code;
    }
 
    public APIException(String errDesc, ConstantsAPI.APIResult e){
        super(errDesc);
        this.ERR_CODE = e.getCode(); 
    }  
 
    public APIException(ConstantsAPI.APIResult e){
        super(e.getMessage());
        this.ERR_CODE = e.getCode(); 
    }
 
    public String getErrorDesc() {
        return this.getMessage();
    }
 
    public int getErrCode() {
        return this.ERR_CODE;
    }  
 
}
 
 
cs

RuntimeException 을 상속받는 Exception 클래스를 작성한다.

생성자 오버로딩을 하여 Exception Message 및 Exception Code를 받을 수 있게 한다.

* Enum 클래스를 통으로 받는 생성자 또한 만들어준다.

ex)

APIException(ConstantsAPI.APIResult e){

//생략

}

 
3. Controller 에서 try catch 처리
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
@RequestMapping(value = "/sample", method={RequestMethod.POST}, produces = "application/json; charset=UTF-8")
public ModelAndView sample(HttpServletRequest request, HttpServletResponse response, @RequestBody Map<String, Object> inParam) throws Exception {
    
    Map<String, Object> result = new HashMap<String, Object>();
    ModelAndView mav = new ModelAndView("jsonView");
    
    try{
        // inParam 유효성 체크
        CommUtil.paramVaildate(new String[] {"tokenId"}, inParam);
        
        sampleService.sampleMethod(inParam);
        
        result.put("result_cd", APIResult.SUCCESS.getCode());
        result.put("result_msg""");
    } catch (APIException ae) {
        result.put("result_cd", ae.getErrCode());
        result.put("result_msg", ae.getMessage());
    } catch (Exception e){
        result.put("result_cd", APIResult.FAILURE.getCode());
        result.put("result_msg", e.getMessage());
    }
    
    mav.addAllObjects(result);
    
    return mav;
}
 
cs
9라인 CommUtil.paramValidate(..); : API 서버의 Controller 이기 때문에 필수값이 제대로 들어왔는지 우선 확인
11라인 sampleService.samplemethod(..); : 서비스 호출(서비스 내부적으로 파싱 및 기타 에러가 나는 경우 try catch로 예외를 잡아 Controller 쪽으로 예외를 미룸..  (생략)
catch문으로 예외를 잡아 error code 및 error message result에 담아 return..

 

* input parameter 유효성 검사

 

향상된 for문을 적절히 사용.

Map에 키가 담겨있지 않거나, 빈 값인 경우 구현해 놓았던 Custom Exception 클래스를 throw.

이때, 위에 작성해 놓은 enum을 활용.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static boolean paramValidate(String[] params, Map<String, Object> inParam) {
        
        boolean _result = true;
        
        try {
            for (String item : params) {
                if(!inParam.containsKey(item)) {
                    throw new APIException(String.format("%s not exist", item), APIResult.E_PARAM_NULL);
                }
                if(String.valueOf(inParam.get(item)).isEmpty) {
                    throw new APIException(String.format("%s is null", item), APIResult.E_PARAM_NULL);
                }
            }
        } catch(APIException e) {
            throw e;
        } catch(Exception e) {
            throw new APIException(APIResult.FAILURE);
        }
        
        return _result;
    }
}
 
cs

 

여기까지만 해도 괜찮지만 Controller 마다 아래의 코드가 들어가는게 마음에 들지 않았다..

1
2
3
4
5
6
7
8
9
try{
 
catch (APIException ae) {
    result.put("result_cd", ae.getErrCode());
    result.put("result_msg", ae.getMessage());        
catch (Exception e){
    result.put("result_cd", APIResult.FAILURE.getCode());
    result.put("result_msg", e.getMessage());
}
cs

 

 

6줄 남짓의 코드를 지우기 위해 

Exception을 공통(일괄?)처리 할 수 있는 @ControllerAdvice 및 @Exceptionhandler 를 사용하여 Exception 처리를 해보았으나..

Exception이 발생하는 경우, log를 찍기 위해 구현해 놓았던 interceptor 가 제대로 동작하지 않는 문제점이 발생.

 

구글링을 해보니, Exception 발생시 interceptor의 postHandle은 타지 않는단다..

 

아래는 구글링으로 찾은 내용..

 

From the javadoc of HandlerInterceptor.postHandle

Intercept the execution of a handler. Called after HandlerAdapter actually invoked the handler, but before the DispatcherServlet renders the view. Can expose additional model objects to the view via the given ModelAndViewDispatcherServlet processes a handler in an execution chain, consisting of any number of interceptors, with the handler itself at the end. With this method, each interceptor can post-process an execution, getting applied in inverse order of the execution chain.

Arguably the MethodArgumentNotValidException is thrown before the method is actually called, it is called in preparing the actual method call. 

Actually the postHandle is only executed after successful execution/invocation of the method.  In case of an exception only the preHandle and afterCompletion methods are called.

>> postHandle은 정상적인 메소드 실행에서만 작동. 예외시 preHandle, afterCompletion 만 호출됨.

 

(출처 : https://stackoverflow.com/questions/20551888/controlleradvice-methods-seem-to-bypass-interceptors)

 

그래서 AOP를 사용해보았다.

 

 

4. AOP 사용

4-1) pom.xml 에 aop 사용을 위한 설정 추가

 

property에 아래의 내용 추가

1
<org.aspectj-version>1.6.10</org.aspectj-version>
cs

 

dependency 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>${org.aspectj-version}</version>
</dependency>
 
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${org.aspectj-version}</version>
</dependency>
 
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjtools</artifactId>
    <version>${org.aspectj-version}</version>
</dependency>
cs

 

4-2) SampleAOP 작성

@Around(pointCut)을 선언

@Around("execution(public org.springframework.web.servlet.ModelAndView com.sample.*Controller.*(..))")

설명 >> public 접근제한자이면서, org.springframework.web.servlet.ModelAndView 리턴타입이면서, 

            com.sample. 밑의 Controller로 끝나는 모든 Class 밑의 모든 메소드(인자값 상관없이)가 실행될 때, 해당 함수(around(..))를 전, 후로 실행...

 Controller(pjp.proceed()) 에서 예외 발생시 AOP에서 error code 및 error message를 넣어 return

 

* advice : @Around, @Before, @After 등..  해당 어노테이션이 붙은 코드의 실행 시점을 의미

* pointCut : 정규식을 사용하여 어떤 joinPoint 를 사용할지 지정

* joinPoint : advice가 적용될 수 있는 메소드(함수)

 

* xml 빈 등록 잘못 할 경우 aop 를 두번 타는 문제가 발생할 수 있으니 주의.

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
package ;
 
import java.util.HashMap;
import java.util.Map;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
 
@Component
@Aspect
public class SampleAOP {
    
    @Around("execution(public org.springframework.web.servlet.ModelAndView com.sample.*Controller.*(..))")
    public ModelAndView around(ProceedingJoinPoint pjp) {
        ModelAndView mav = null;
        
        try {
              mav = (ModelAndView) pjp.proceed();
        } catch(APIException e){
            mav = new ModelAndView("jsonView");
            e.printStackTrace();
            Map<String, Object> result = new HashMap<String, Object>();
 
            result.put("result_cd", e.getErrCode());
            result.put("result_msg", e.getMessage());
            
            mav.addAllObjects(result);
        } catch (Throwable e) {
            mav = new ModelAndView("jsonView");
            e.printStackTrace();
            Map<String, Object> result = new HashMap<String, Object>();
 
            result.put("result_cd"0);
            result.put("result_msg", e.getMessage());
            
            mav.addAllObjects(result);
        }
        return mav;
    }
}
 
cs

 

 

5. Controller 에서 try catch 처리 수정

 

예외처리를 AOP에서 일괄적으로 관리하니 기존의 try catch 를 지워준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 @RequestMapping(value = "/sample", method={RequestMethod.POST}, produces = "application/json; charset=UTF-8")
    public ModelAndView sample(HttpServletRequest request, HttpServletResponse response, @RequestBody Map<String, Object> inParam) throws Exception {
        
        Map<String, Object> result = new HashMap<String, Object>();
        ModelAndView mav = new ModelAndView("jsonView");
        
        // inParam 유효성 체크
        CommUtil.paramVaildate(new String[] {"tokenId"}, inParam);
            
        sampleService.sampleMethod(inParam);
            
        result.put("result_cd", APIResult.SUCCESS.getCode());
        result.put("result_msg""");
        
        mav.addAllObjects(result);
        
        return mav;
    }
 
cs

 

AOP 를 어설프게나마 사용해 보았다는 걸로 만족...

AOP 는 추후 더 공부해서 개념, 설정, 구현 부분을 따로 포스팅 해보겠다.

 

 

 

* 제 경우 기존 프로젝트에 aop 설정이 부분적으로 되어있었습니다.

  위의 설정 및 소스를 그대로 따라할 경우 aop 동작이 제대로 안될 수도 있습니다.

 

반응형

+ Recent posts