본문 바로가기

프로그래밍/Spring

Custom Annotation을 이용한 객체 검증 작업..(3)

이제 마지막으로 이렇게 검증 작업을 할때 에러 메시지를 출력하는 것과 어떤 기준으로 에러 메시지를 골라서 출력하는지에 대해 설명하고자 한다. Spring에서 객체 Binding 관련 에러 메시지를 관리할때는 일종의 규칙이 존재한다. 예를 들어 이제껏 설명한 예였던 입력한 값을 객체에 Binding 하면서 데이터 중복을 체크하는 과정에서의 에러 메시지에 대한 내용을 얘기해보자. 데이터 중복에 대한 메시지는 여러 형태를 가질수 있다. 단순히 "이미 입력된 값입니다" 도 있을수 있고 "입력한 이름은 이미 입력된 값입니다" 이렇게 나타낼수도 있다. 하지만 그렇다 해도 중복 에러에 대한 에러코드는 변함이 없어야 한다. "이미 입력된 값입니다"의 에러코드는 ExistCheck이고 "입력한 이름은 이미 입력된 값입니다"의 에러코드는 ExistCheckName으로 만들면 메시지 종류가 늘어날때마다 늘 새로운 에러코드를 만들어야 하기때문에 나중에는 에러코드 자체를 정하기 위해 머리를 쥐어 싸매야 할것이다. 그래서 기본적인 에러 코드는 하나 정해놓고 체크해야 할 항목이나 조건에 따라 기존 에러 코드에서 확장해서 나가는 식으로 가는 것이 좋다. 그러나 확장 자체를 피할수는 없다. 어차피 메시지 자체가 새로 늘어나는것은 피할수가 없으니까..단지 확장이 되더라도 기본적인 코드값에서 체계적인 확장이 되어 나가는 것이 중요하다.

 

이런 관점에서 Spring의 에러코드 매핑은 나름 좋다고 생각한다(나름..이라고 말하는 것은 내 기준에서 좋다는 의미이지 지금부터 말하는 내용이 모두에게 또는 모든 상황에서 최상의 해결책은 아닐수도 있기 때문이다) Spring에서는 MessageCodeResolver를 통해 다음의 에러코드에 대한 확장체계와 우선 순위를 갖는다

 

우선순위 형식 예시(@ExistCheck annotation을 사용하는 경우)
1 에러코드.모델이름.필드이름 ExistCheck.board.title
2 에러코드.필드이름 ExistCheck.title
3 에러코드.클래스이름 ExistCheck.BoardModel
4 에러코드 ExistCheck

 

Spring에서는 검증작업을 할때 문제가 발생하면 에러코드를 지정해서 출력할수 있다. 그러나 우리는 annotation을 통한 검증이기 때문에 에러코드를 지정해서 출력할수 없다. 그래서 annotation을 이용한 검증의 경우 사용한 annotation 이름이 에러코드가 된다. 즉 @NotNull annotation을 이용한 경우 NotNull이 에러코드가 되는것이다. 그래서 위에서 예시를 만들때 우리가 앞에서 만든 ExistCheck annotation에 대한 에러 코드로 예시를 들은것이다.

 

@ExistCheck로  입력한 값을 객체에 Binding하면서 중복체크 할때 에러가 발생할경우 어떻게 에러 메시지를 선정하는지 보자. 앞에서 설명할때 다음과 같은 Controller 함수가 있을 것이다.

 

@RequestMapping(value = "insertBoardValid", method=RequestMethod.POST)
public String insertBoardValidJob(@ModelAttribute @Valid BoardModel board, BindingResult result)

 

이 코드는 Controller에서 사용자가 입력한 값을 BoardModel 클래스의 객체인 board 변수에 Binding을 하며 이 과정에서 validation 작업을 거치는 것을 의미한다. BoardModel 클래스 소스에는 멤버변수 title에 대해서는 @ExistCheck annoation이 붙어 있기 때문에 title 변수에 binding 하는 과정에서 @ExistCheck가 하는 역할인 입력값의 중복 체크를 하게 될 것이고 기존에 입력된 값이 있으면 에러가 발생할 것이다. 그럼 이렇게 에러가 발생할 경우 메시지를 어떻게 찾는가? 위의 도표대로 우선순위별로 에러메시지를 출력한다. Spring에서 에러 메시지를 담고 있는 프로퍼티 파일에 에러코드.모델이름.필드이름 으로 구성된 key(여기서는 ExistCheck.board.title)를 찾게 되면 그 key 에 대한 메시지를 출력하게 된다. 그러나 그런 key를 발견하지 못할 경우 다음 우선순위인 에러코드.필드이름으로 구성된 key(여기서는 ExistCheck.title)를 프로퍼티 파일에서 찾아서 만약 key가 존재하면 그 key에 대한 메시지를 출력한다. 그러나 그런 key를 발견하지 못할 경우 다음 우선순위인 에러코드.클래스이름으로 구성된 key(여기서는 ExistCheck.BoardModel)를 프로퍼티 파일에서 찾아서 만약 그 key가 존재하면 그 key에 대한 메시지를 출력한다. 그러나 그런 key를 발견못하면 마지막으로 에러코드가 key(여기서는 annotation 이름은 ExistCheck)를 프로퍼티 파일에서 찾아서 해당 메시지를 출력한다.

 

그러면 메시지 프로퍼티 파일은 어떻게 구성하는지 살펴보자. 메시지 프로퍼티 파일은 일반 텍스트 파일이나 한글 사용시엔 유니코드로 변환이 되어야 한다. 자바에서 제공하는 native2ascii 유틸리티로 파일을 변환시켜주거나 또는 이클립스에서 사용할수 있는 프로퍼티 파일 제작 플러그인(PropEdit)을 이용해서 만들어주어야 한다. 예시에서는 편의상 그냥 한글로 쓰지만 실제 프로퍼티 파일을 열어보면 한글로 표시되지 않는다. 메시지 프로퍼티 파일은 다음의 형식으로 만들면 된다.

 

 

ExistCheck = 중복된 값입니다

ExistCheck.title = 입력하신 {2}값은 이미 등록되어 있는 값입니다.

# ExistCheck.title = {0},{1},{2},{3},{4},{5},{6},{7},{8},{9}

 

 

BoardModel 클래스 객체로 사용자가 입력한 값을 Binding 하는 과정에서 @ExistCheck annotation을 이용하여 중복 체크를 할 경우 BoardModel 클래스의 title 필드에 @ExistCheck annotation을 설정했기 때문에 ExistCheck.title인 key를 갖고 있는 메시지를 찾을 것이다. 현재는 ExistCheck.title key가 있기 때문에 메시지를 출력할 것이다. 메시지 형태는 이전 포스트에서 캡춰 화면으로 보여줬던 형태로 출력하고 있다. 근데 여기 보면 {2}가 있다. 여기에는 지정된 값이 나오게 된다. 이전 포스트에서 {2} 부분에 "제목" 이란 글자가 대신 나타나고 있다. 그러면 이 규칙은 어떻게 지정이 될까? 위에 보면 #으로 주석처리한 ExistCheck.title key가 있다. 이 주석처리를 빼고 기존 것에 #을 붙여 주석 처리한후 나타나는 메시지를 보면 다음과 같이 나타난다

 

title,TITLE,제목,BOARD,{4},{5},{6},{7},{8},{9}

 

{0} 위치에는 title, {1} 위치에는 TITLE 이런식으로 {3}까지 나오며 {4} 부터는 그냥 출력하고 있다. 그러면 이렇게 출력하는 기준은 무엇일까? {0}은 해당 annotation이 붙은 멤버변수명을 출력한다. BoardModel 클래스의 멤버변수 title에 @ExistCheck이 붙어있다. 그래서 title을 출력해준것이다. {1} 부터는 해당 annotation에 설정되는 값들을 알파벳순으로 보여주고 있다. @ExistCheck를 보면 tableName, columnName, fieldName 에 사용자가 지정한 값이 할당된다. 그리고 이전 포스트를 보면 BoardModel 클래스 안에서 @ExistCheck annotation을 다음과 같이 사용했다

 

@NotNull
@Size(min=1)
@ExistCheck(tableName="BOARD", columnName="TITLE", fieldName="제목")
String title;

 

위에서 보여준 메시지에서 나타난 값들과 변수명과 값들을 매핑해보자. 알파벳순으로 보여주고 있다는 것을 알수 있다.

 

이제 이 메시지 프로퍼티 파일을 셋팅하는 내용에 대해 말하고자 한다. 이 부분에서 좀 셋팅이 오래 걸린 부분이기도 했는데 왜 그랬는지는 나중에 밝히겠다. 위에서 MessageCodeResolver를 통해 에러코드가 정의된다고 했다. 그럼 이렇게 에러코드가 지정이 되면 그 에러코드에 대한 메시지는 어디서 찾는걸까? 그건 Spring의 MessageSourceResolver에 해당 에러코드가 거쳐서 최종적인 메시지로 얻어지게 된다. 정리하자면 우리가 이용하고자 하는 메시지가 있는 메시지 프로퍼티 파일을 MessageSourceResolver에 셋팅한후 MessageCodeResolver를 통해 얻어진 에러코드들이 이 MessageSourceResolver를 거쳐서 최종 메시지로 바뀌게 되는 것이다. 이렇게 메시지 프로퍼티 파일을 이용해서 MessageSource를 구현할때는 ResourceBundleMessageSource 클래스를 사용한다. 그러나 이 클래스는 메시지 프로퍼티 파일이 변경될때는 변경사항을 자동 반영을 해주지 않기 때문에 이럴 경우엔 ReloadableResourceBundleMessageSource 클래스를 사용해야 한다. 

 

바로 이것땜에 셋팅에 시간이 걸렸는데 원래 이것에 대한 예제를 만들 당시엔 ResourceBundleMessageSource를 사용하려 했다. 근데 메시지 프로퍼티 파일 위치를 임의의 위치를 줄 수가 없었다(즉 /WEB-INF/messages/messages 같이 클래스 패스와는 무관한 그런 위치를 사용할 수가 없었다) 이런저런 시도를 계속 하다가 결국 이 부분은 ReloadableResourceBundleMessageSource클래스를 사용하는 것으로 해결이 되었다. 출처는 잘 모르겠으나 어딘가 포스팅에서 올린 글을 보면 ReloadableResourceBundleMessageSource 클래스를 사용하는 것은 문제가 있다고 되어 있으나 무슨 문제인지는 구체적으로 언급이 없었다. 그래서 만약 문제가 생길 경우는 메시지 프로퍼티의 파일 위치를 클래스 패스쪽으로 바꾸고 ResourceBundleMessageSource를 사용해보길 바란다. 셋팅은 다음과 같이 했다.

 

    
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
   <property name="basenames">
	   <list>
		   <value>/WEB-INF/messages/messages</value>
	   </list>
   </property>
   <property name="cacheSeconds" value="5"/>
</bean>

 

속성을 보면 basenames 란 속성이 있는데 이 속성에 읽어야 할 메시지 프로퍼티 파일을 지정한다. 메시지 프로퍼티가 여러개 있을 수 있기 때문에 <list>를 통해 여러개를 입력받게끔 되어 있다. 그리고 위에서 설명했다시피 ReloadableResourceBundleMessageSource는 주기적으로 메시지 프로퍼티 파일의 변경여부를 확인해서 이를 적용하도록 되어있다. 그래서 cacheSeconds 속성을 통해 몇 초 간격으로 이를 체크하는지를 지정할수 있다. 그리고 이 2개의 클래스는 지역 Locale 정보를 이용해서 그에 맞는 메시지를 출력한다. 위의 예를 통해 설명하자면 웹브라우저를 이용해 한국(Locale.KOREAN)에서 접속했을때는 먼저 /WEB-INF/messages/messages_ko.properties 파일을 찾는다는 것이다. Locale.ENGLISH 지역이라면 /WEB-INF/messages/messages_en.properties 파일을 찾게 된다. 만약 이렇게 지역에 특성화된 파일이 없을 경우 /WEB-INF/messages/messages.properties 파일을 읽어서 메시지를 구성하게 된다.

 

이상과 같이 Custom Annotation을 이용한 검증 작업을 살펴보았다. 일반적인 웹페이지를 이용한 작업에서 이런 서버용 검증 Annotation을 만들면 한결 작업을 수월하게 할 수 있는 장점이 있다. 한번 활용해보길 바란다.