2011년 2월 21일 월요일

CXF 서비스 호출 시 발생하는 예외 처리 - Spring AOP Annotation

Spring에서 Annotation기반 AOP를 이용하려면 스프링 설정을 아래와 같이 지정해야 한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:aop="http://www.springframework.org/schema/aop"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

  <aop:aspectj-autoproxy />
  
</beans>

아래의 소스 코드는 CXF의 서비스 호출 시 에러 처리에 관련된 Aspect 소스 코드다.
아래의 소스 코드에서 @Service나 @Component 스테레오 타입이 지정되도록 해야한다.

package com.archnal.sample.cxf;

import javax.ws.rs.core.Response;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;

import com.archnal.sample.exception.BaseException;
import com.archnal.sample.exception.StatusCode;

@Aspect()
@Service("restfulServiceAspect")
public class RestfulServiceAspect {

  @Around("execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))")
//  @Pointcut("execution(public * com.archnal.sample.jaxrs.*RestfulServiceImpl.*(..))")
  public Object handleRestfulService(ProceedingJoinPoint joinpoint) {
    System.out.println("around: " + joinpoint.getSignature().getName());
    
    Response response = null;
    try {
      Object[] args = joinpoint.getArgs();
      Object entity = joinpoint.proceed(args);
      
      return entity;
//      response = Response.ok(entity).build();
//      response = (Response)joinpoint.proceed(args);
      
    } catch(BaseException ex) {
      System.out.println("baseException: " + ex);
      response = Response.serverError().entity(create(ex)).build();
    } catch(Throwable ex) {
      System.out.println("throwable: " + ex);
      response = Response.serverError().entity(create(ex)).build();
    }
    return response;
  }
  
  @AfterThrowing(pointcut="execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))", 
      throwing="e")
  public Object handleException(JoinPoint joinPoint, Throwable e) {
    System.out.println("afterThrowing: " + joinPoint.getSignature().getName());
    System.out.println("e: " + e);
    return Response.serverError().entity(create(e));
  }
  
  @Before("execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))")
  public void before(JoinPoint joinPoint) {
    System.out.println("before: " + joinPoint.getSignature().getName());
  }
  
  @After("execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))") 
  public void after(JoinPoint joinPoint) {
    System.out.println("after: " + joinPoint.getSignature().getName());
  }
  
  @AfterReturning(pointcut="execution(public * com.archnal.sample.jaxrs.*ResponseServiceImpl.*(..))", returning="result") 
  public void afterReturning(JoinPoint joinPoint, Object result) {
    System.out.println("afterReturning: " + joinPoint.getSignature().getName());
    System.out.println("result: " + result);
  }
  
  private ResponseResult create(BaseException ex) {
    ResponseResult header = new ResponseResult();
    header.setSuccess(false);
    header.setStatusCode(ex.getStatusCode().getCode());
    header.setMessage(ex.getMessage());
    
    return header;
  }
  
  private ResponseResult create(Throwable th) {
    if(th.getClass().isAssignableFrom(BaseException.class)) {
      return create((BaseException) th);
    }
    
    ResponseResult header = new ResponseResult();
    header.setSuccess(false);
    header.setStatusCode(StatusCode.UNKNOWN.getCode());
    header.setMessage(th.getMessage());
    
    return header;
    
  }
  

}


위에 메서드를 호출하면 에러가 발생하는 경우에 @AfterThrowing 으로 처리가 가능할까 싶어서 테스트를 해 보았으나,
예외가 발생하는 경우에 @AfterThrowing이 지정된 메서드를 호출하지만 메서드가 호출 된 이후에 Aspect는 여전히 원래 발생한 Throwable을 잡아서 제거하지 않고 던져 버린다. 따라서 서비스 Invoker에서는 여전히 에러가 발생한 것으로 인식하여 장황한 stack trace를 화면에 출력한다.
위의 예제에서는 에러가 발생하는 경우 잡아채서 클라이언트에게 serverError(500) 의 상태코드로 전달하면서 에러상태 코드를 전달하려는 의도로 작성된 코드다.
이런 요구사항에 @AterThrowing은 에러를 여전히 던지기 때문에 적합하지 않다. 실제로 @AfterThrowing에서 리턴하는 Response는 아무런 역할을 하지 못한다. 위의 소스 코드에서 처럼 @Around 에서 원래 메서드를 호출하고 에러가 발생하는 경우에 Response를 생성하여 리턴하도록 해야한다.
하지만 이런 방식에서도 문제가 하나 있다. Restful서비스의 원래 메서드가 Response를 리턴하도록 되어 있어야 한다. 일반 Pojo 클래스를 리턴하는 경우에는 ClassCastException이 발생한다. 이처럼 에러를 일괄적으로 처리하기 위해서는 꼭 Response가 아니더라도 ResponseMessage 같은 일관된 메시지 형태의 객체를 리턴해야만 ClassCastException이 발생하지 않는다.

위의 Aspect가 적용된 상태에서 서비스를 호출하였을 때 정상적인 경우에 아래와 같이 화면에 출력된다.
before: get
around: get
after: get
afterReturning: get
result: org.apache.cxf.jaxrs.impl.ResponseImpl@170a5a3

에러가 발생하는 경우에는 아래와 같이 화면에 출력된다.

before: get
around: get
after: get
afterThrowing: get
e: com.archnal.sample.exception.BaseException: category: BIZ_LOGIC_ERROR, status: USER_NOT_FOUND, code: E_102
afterReturning: get
result: org.apache.cxf.jaxrs.impl.ResponseImpl@15f8a0a

뭔 얘기를 하는건지 나만 알 수 있는 말을 적은 거 같다. 아는 사람은 알겠지 뭐~~

댓글 없음:

댓글 쓰기