[Spring 3.2] Interceptor에서 Controller의 어노테이션 체크하기

Published on: 2013. 12. 2. 19:45 by louis.dev

인증이 필요한 서비스를 개발할 경우, Interceptor에서 로그인 유무를 확인하는 로직을 추가하여 각 컨트롤러에 인증 로직을 추가로 작성하지 않도록 할수 있습니다.


만약 이때 어떤 컨트롤러의 메소드는 로그인을 체크하고 어떤 컨트롤러의 메소드에는 로그인을 체크하지 않도록 하려면 어떻게 해야 할까요?


스프링 3.2 부터는 mvc:interceptor 부분에 exclude-mapping 이라는 엘리먼트가 추가되어 기존의 스프링 버전보다 쉽게 인터셉터의 적용을 제외시킬수 있습니다.

<mvc:interceptor> <mvc:mapping path="/**"/> <exclude-mapping path="/info/**"/> <bean class="net.krespo.interceptor.LoginCheckInterceptor" /> </mvc:interceptor>

이렇게 exclude-mapping을 사용하면 LoginCheckInterceptor는 /info/ 로 시작하는 컨트롤러에 인터셉터 적용을 제외 시킬수 있습니다.


그런데 만약 /info/my는 로그인 인터셉터를 수행시키고 /info/test는 로그인 인터셉터를 수행시키려면 어떻게 해야할까요?


물론 exclude-mapping을 여러개 등록하면 되지만 로그인이 필요하지 않은 컨트롤러가 생길때마다 exclude-mapping을 선언하는것이 귀찮게 느껴질겁니다.


이럴때는 임의의 어노테이션을 생성하여 인증이 필요하지 않은 컨트롤러의 메소드에 어노테이션을 붙이고, 어노테이션 유무를 인터셉터에서 확인하여 로그인을 할지 안할지를 하면 좀더 편하게 개발을 진행할 수 있을것 같습니다.


이제부터 본격적으로 인터셉터에서 컨트롤러의 어노테이션을 체크하는 방법을 알아보도록 하겠습니다.


1. 어노테이션 생성

package net.krespo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLoginCheck {
}

NoLoginCheck라는 어노테이션을 생성하였습니다. 이 어노테이션을 사용하여 어노테이션을 메소드에 추가를 했으면 로그인 체크를 하고, 어노테이션이 없으면 로그인 체크를 하지 않도록 할것입니다.


2. 컨트롤러에 어노테이션 추가


@Controller
public class MyController {
	
	@RequestMapping("/my/info")
	@NoLoginCheck 
	public ModelAndView myInfo(){
		...
	}
	
}

3. Interceptor에서 어노테이션 체크하기

public class LoginInterceptor extends WebContentInterceptor {
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {
		//@NoCheckLogin 어노테이션이 컨트롤러에 사용되었는지 체크함
		NoCheckLogin usingAuth = ((HandlerMethod) handler).getMethodAnnotation(NoCheckLogin.class);

		//NoCheckLogin 어노테이션이 없음으로 무조건 로그인 체크
		if(usingAuth == null) {
			//TODO 로그인 체크
		}
		//NoCheckLogin 어노테이션이 없음으로 로그인 체크 하지 않음
		else {
			//TODO 추가작업
		}
	}	
}

가장 중요한 부분은 preHandler 메소드의 handler Object를 HandlerMethod로 타입케스팅 하는 부분입니다. 이렇게 HandlerMethod로 바꾼 handler오브젝트의 getMethodAnnotation을 통해서 앞으로 실행될 컨트롤러의 메소드에 해당 어노테이션이 추가가 되었는지 안되어 있는지를 확인할 수 있습니다.


위 내용은 스프링 3.2에서 확인해 보았습니다. 그러나 스프링 3.0에서는 해당 handler Object를 HandlerMethod의 오브젝트가 아닌 앞으로 실행시킬 컨트롤러오브젝트(위의 예제에서는 LoginController)가 반환이 됨으로 위의 코드는 3.0에서는 사용할 수가 없습니다~(3.1에서는 체크를 해보지 못했습니다. 3.1에서 사용해 보신 분께서는 댓글로 알려주세요~^^)

[Spring 3.2] @ControllerAdvice를 이용한 익셉션 처리

Published on: 2013. 11. 29. 19:23 by louis.dev

스프링 프레임웍 3.2 이상부터 @ControllerAdvice라는 어노테이션이 추가 되었습니다. 이 어노테이션을 사용하면 간단하게 익셉션 발생시 작업을 처리할 수가 있습니다.


방법은 간단합니다. 설정파일에 아래와 같이 설정합니다. 아래의 설정 내용은 따로 설명하지 않겠습니다.

<annotation-driven />
<context:component-scan base-package="net.krespo" >

이후 적당한 위치에 클래스를 하나 생성하여 @ControllerAdvice를 붙여주면 됩니다.

package net.krespo.handler;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;


@ControllerAdvice
public class AnnotationExceptionHandler {
	@ExceptionHandler(Exception.class)
	public void handleException(Exception e) {
		System.out.println("exception");
	}
	
	@ExceptionHandler(RuntimeException.class) 
	public ModelAndView handleRuntimeException(RuntimeException e) {
		ModelAndView mnv = new ModelAndView("exceptionHandler");
		mnv.addObject("data", e.getMessage());
		
		return mnv;
	}
}

클래스에는 @ControllerAdvice를 선언해 주고 각 메소드 마다 @ExceptionHandler라는 어노테이션으로 어떤익셉션을 처리할 것인지를 선언해 주면 됩니다.


익셉션을 처리하는 메소드는 익셉션 파라미터를 받을 수 있습니다. 그래서 개발자가 익셉션에 메세지를 담아서 던지면 해당 메세지를 각 핸들러 메소드에서 확인할수 있습니다.


또한 리턴값으로는 void부터 ModelAndView까지 다양하게 리턴할수 있어 익셉션을 처리하는데 편리하게 사용할 수 있습니다.

[Spring Framework] @RequestParam을 사용하여 List형 데이터 받을때 주의점

Published on: 2013. 11. 14. 14:11 by louis.dev
html페이지로부터 전달되는 파라미터를 받아올때 @RequestParam 어노테이션을 사용해서 httpServletRequest 를 사용하지 않고도 데이터를 전달 받을 수 있다. 만약 페이지에서 name 속성을 동일하게 주어지면 @RequestParam을 이용해서 List형태로 받는것도 가능하다.
<form action="/submit" method="get">
	name=value1 <input type="text" name="value1" />
	name=value1 <input type="text" name="value1" />
	<input type="submit" value="submit" />
</form>
이렇게 textbox를 동일한 이름으로 submit을 하게 되면 파라미터는
value1=1111&value1=2222
처럼 동일한 파라미터 명을 가지게 되고 이 값을 스프링의 Controller에서 List형태로 받으려면
@RequestMapping("/submit")
public String submit(@RequestParam(value="value1", required=true) List<String> values) {
	System.out.println(values.size());  //2가 나오는것을 볼수 있다.
	return null;
}
처럼 사용할수 있다. 그런데 @RequestParam은 ,(comma)형태로 데이터를 보내게되도 ,(comma)를 구분자로하여 데이터를 쪼개서 List형태로 데이터를 변환한다.
//전달된 파라미터가 value1=111,222,333,444 형태라면
@RequestMapping("/submit")
public String submit(@RequestParam(value="value1", required=true) List<String> values) {
	for(String value : values) {
		System.out.println(value);    
	}
	//출력결과는 
	//111
	//222
	//333
	//444
	return null;
}
만약 html 뷰 페이지에서 다음과 같이 추가 버튼을 눌러 동적으로 입력 폼을 추가 하게되게끔 개발을 했다고 하면
<form action="/submit" method="get">
	name=value1 <input type="text" name="value1"/><button type="button" onclick="textbox추가 로직">추가</button>
	<input type="submit" value="submit" />
</form>
Controller에서는 몇개의 textbox가 추가가 될지 모르니 위와같이 @RequestParam을 이용해서 List형태로 받게 될것이다. 이때 textbox가 1개이고(추가버튼을 누르지 않고) 입력된 데이터가 ,(comma)로 구분되는 문자가 들어오게 된다면 우리는 의도했던것과는 다르게 사이즈가 1개이상인(콤마갯수 + 1개) List를 돌려받게 될것이다. 위와같은 문제가 발생될 소지가 있다면 어쩔수 없이 httpServletRequest의 getParameterValues 메소드를 사용하여 직접 받을 수 밖에 없으니 comma에 주의해서 @RequestParam 어노테이션을 사용하자.

[Spring Framework] @Transactional로 구현한 트랜젝션에서 수동 rollback 하기

Published on: 2013. 11. 14. 13:13 by louis.dev
스프링에서 아주 간단하게 트랜젝션을 구현하려면 @Transactional 어노테이션을 통해서 쉽게 트랜젝션을 설정할 수 있습니다. (따로 해당 어노테이션을 사용하기 위한 설정 설명은 하지 않겠습니다.)
@Transactional
public int saveArticle(String title, String content) {
	// 게시글등록 테이블에 데이터를 저장합니다.
	articleDao.setArticle(title, content);
	// 최신글 테이블에 데이터를 저장합니다.
	newArticleDao.setNewArticle(title, content);

	//상태코드 200은 처리완료를 나타냅니다.
	return 200;
}
위의 코드와 같이 메소드 위에 @Transactional을 사용함으로서 saveArticle에 트랜젝션을 걸수가 있습니다.   그런데 위와같은 코드는 한가지 문제가 있습니다.   만약 저장되지 않았을때 상태코드를 500(익셉션 발생)을 내보내고 싶다면 어떻게 해야할까요?
@Transactional
public int saveArticle(String title, String content) {
	try {
		// 게시글등록 테이블에 데이터를 저장합니다.
		articleDao.setArticle(title, content);
		// 최신글 테이블에 데이터를 저장합니다.
		newArticleDao.setNewArticle(title, content);

		//상태코드 200은 처리완료를 나타냅니다.
		return 200;
	} catch(Exception e) {
		e.printStackTrace();
		return 500;
	}
}
위와같이 코딩할수 있습니다.  하지만 이때는 익셉션이 발생하면 try catch로 처리되기 때문에 익셉션이 메소드 밖으로 throw되지 않아 트랜잭션이 실행되지 않습니다. 즉 articleDao.setArticle(title, content); 가 저장되고 난 뒤 newArticleDao.setNewArticle(title, content); 에서 에러가 나면 articleDao.setArticle(title, content);로 저장된 글이 롤백이 되지 않아 article에는 저장되고 newArticle에는 저장이 되지 않게 됩니다. 이때는 Spring Framework의 TransactionAspectSupport 클래스를 통해 rollback을 수동으로 작업해 주면됩니다.
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
public int saveArticle(String title, String content) {
	try {
		// 게시글등록 테이블에 데이터를 저장합니다.
		articleDao.setArticle(title, content);
		// 최신글 테이블에 데이터를 저장합니다.
		newArticleDao.setNewArticle(title, content);

		//상태코드 200은 처리완료를 나타냅니다.
		return 200;
	} catch(Exception e) {
		e.printStackTrace();
  	        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
		return 500;
	}
}
이렇게 작성해 주면 익셉션 발생시 해당메소드를 rollback 시킬수 있게 됩니다. 참고로 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 가 실행될때 바로 rollback이 진행되지는 않습니다. setRollbackOnly 메소드는 속성만 변경을 하는 것이기 때문에 실제 rollback이 일어나는 시점은 commit이 되기 직전에 수행되게 됩니다.

Spring - Spring + iBatis 연동시 iBatis Transaction 실행 안되는 문제

Published on: 2010. 12. 13. 17:56 by louis.dev

프로젝트를 진행하다 Transaction을 수행해야 하는 query가 있었습니다. 그때 저는 iBatis의 Transaction을 통해 다음과 같이 구현하였습니다.
(Spring에서 제공하는 SqlMapClientTemplate를 이용하여 iBatis를 사용했습니다)

SqlMapClient sqlClient = template.getSqlMapClient();
try {
	sqlClient.startTransaction();
	template.insert("turotial.dataInsert", data);
	template.insert("tutorial.updateInfo", data);
	sqlClient.commitTransaction();
	return true;
} catch (SQLException e) {
	e.printStackTrace();
	return false;
} finally {
	sqlClient.endTransaction();
}


위와 같이 insert하고 update부분을 iBatis의 transaction으로 묶으려고 하였습니다.

하지만 우연치 않게 iBatis의 Transaction이 먹히지 않는것을 발견하였습니다. 왜이럴까 고민하다가 Spring에서 제공하는 Transaction을 통해 구현해 보기로 하였고, 결국 Transaction이 제대로 동작하는것을 확인 할 수 있었습니다.

저같은 경우는 Spring project를 할때 db연동에 관련한 모든 bean들을 applicationContext-iBatis.xml에 몰아 둡니다.( 관리하기가 더 편하더라구요.. 개인적 생각입니다.^^)

저는 이 ibatis.xml 에 ibatis를 통해 DB에 접근할 dataSource bean을 설정해 두었습니다.

1. transactionManager와 transactionTemplate을 bean으로 등록합니다.
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
	<property name="jndiName" value="java:comp/env/jdbc/tutorial"/>
</bean>
	
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
	<property name="configLocation" "classpath:sqlMapConfig.xml" />
	<property name="dataSource" ref="dataSource" />
</bean>
	
<bean id="template"
          class="org.springframework.orm.ibatis.SqlMapClientTemplate"
          p:sqlMapClient-ref="sqlMapClient"/>
    
<bean id="transactionManager" "org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
    
<bean id="transactionTemplate"  
      class="org.springframework.transaction.support.TransactionTemplate"
      p:transactionManager-ref="transactionManager"/>


위와 같이 transactionManager와 transactionTemplate bean을 dataSource를 통해 생성합니다.

2. transactionTemplate를 사용할 bean에 property로 설정합니다.
<bean id="tutorialDAO" class="tutorial.TutorialDAOImpl">
	<property name="template" ref="template"/>
	<property name="transactionTemplate" ref="transactionTemplate"/>
 </bean>


3. 해당 DAOImpl 클래스에서 field로 transactionTemplate와 setter method를 구현하여 DI가 되도록 작업해 줍니다.

4. 다음과 같이 transactionTemplate를 통해 transaction을 구현합니다.
public boolean transactionTest(final TutirialVO vo ) throws SQLException{
	boolean result = false;
	result = (Boolean)transactionTemplate.execute( new TransactionCallback() {
		public Object doInTransaction(TransactionStatus status) {
			try {
				template.insert("Tutorial.insertData", vo);
				template.insert("Tutorial.updateData", vo);
				return true;
			}catch (Exception e){
				e.printStackTrace();
				status.setRollbackOnly();
				return false;
			}
		}
	});
	return result;
}

위와 같이 transactionTemplate.execute를 실행하고 전달인자로 TransactionCallBack 인스턴스를 전달하는데, 일반적으로 TransactionCallback이 인터페이스 이기 때문에 new로 객체를 생성하고 doInTransaction method를 구현합니다.