요즘 많이 사용되는 AOP Logging. 현재 작업 중인 프로젝트에서도 사용중인데, 최근 기능을 추가하면서 문제가 발생했다.


● 문제발생

현재 사용중인 프로젝트에서도 AspectLogging 기법을 사용했다. 그런데, class cast를 하는 과정에서 지속적으로

Exception in thread "main" java.lang.ClassCastException: $Proxy11 cannot be cast to classA

라고 하는 문제가 발생하면서 로깅클래스에서 동작이 멈추는 증상이 나타났다. 물론…
AOP측에서 이에 대한 예외처리를 제대로 했으면 별문제가 없었겠지만,


● 문제요인

문제가 발생하는 부분을 유심히 살펴봤다. 스프링의 ReloadableResourceBundleMessageSource를 확장extends하여 간단한 메소드를 추가한 ResourceBundleMessageSource클래스로 class casting을 하는 부분에서 문제가 발생하고 있었다.


● 힌트

이 문제가 왜 생겼는지 인터넷 검색에 들어간다.

그러다가 발견한 힌트!

classcastexception-proxy-cannot-be-cast-to-using-aop - Stackoverflow

제일 마지막 부분에

a good article about proxy creation in Spring

이 글을 보고 깨달음을 얻었다.


● 해결책

  1. BundleMessageSource 라고 하는 인터페이스를 만들고, ResourceBundleMessageSource 클래스에서 implements 로 구현한다고 선언.
  2. ResourceBundleMessageSource 를 class casting 하는 부분을 BundleMessageSource 로 변경하였다.
  3. OK~!

● 정리

… proxy에서 효과적인 캐스팅 방식은, ‘클래스 캐스팅을 사용할 경우 인터페이스를 선언하여 구현하고 인터페이스로 캐스팅하는 것’ 이다.

얼마 전에 JSON을 사용해서 AJAX 통신 API를 만드는 과정에서 삽질을 한 적이 있다. 기본만 제대로 살폈으면 쉽게 넘어갈 수 있는 일이었는데 나의 얼렁뚱땅 대충대충 ‘필요하면 그때그때 찾아서 쓰면 되지 뭐.’ 라는 안일함이 하루의 시간을 날려먹는 상황을 낳고 말았다. 방법은 정말 간단했다.

헤맸던 소스: Before Source

  • ● TestForm.java

    @Data
    @NoArgsConstructor
    @ToString
    public class TestForm {
      private String id;
      private String name;
      private List<TestTag> testTags;
    
      @Data
      @NoArgsConstructor
      @ToString
      public class TestTag {
          private String id;
          private String tag;
      }
    }
    
  • ● TestController.java

    @Controller
    public class TestController {
      private static final Logger logger = LoggerFactory.getLogger(TestController.class);
    
      @RequestMapping(value="/test", method=RequestMethod.GET)
      public void test(@RequestBody TestForm form, ModelMap map) {
          logger.debug("TestForm : {}", form);
      }
    }
    
  • ● Form JSON

      var form = {
          id: "123",
          name: "123",
          testTags: [{id: "1111", tag: "2222"}]
      };
    
      $.ajax({
          url: "http://localhost:8080/test",
          method: "get",
          type: "json",
          data: form,
          success: function(data) {
              console.log(data);
          }
      });
    

    여러가지 시도를 해봤지만, form의 데이터를 TestController의 test에서 제대로 받아들이지 못하는 문제로 골머리를 썩었다(지금 생각해보면 나의 무식함에 부끄럽지만).

해결 코드: After Source

  • ● TestForm.java

    @Data
    @NoArgsConstructor
    @ToString
    public class TestForm {
      private String id;
      private String name;
      private List<TestTag> testTags;
    
      @Data
      @NoArgsConstructor
      @ToString
      public static class TestTag {
          private String id;
          private String tag;
      }
    }
    
  • ● TestController.java

    @Controller
    public class TestController {
      private static final Logger logger = LoggerFactory.getLogger(TestController.class);
    
      @RequestMapping(value="/test", method=RequestMethod.POST)
      public void test(@RequestBody TestForm form, ModelMap map) {
          logger.debug("TestForm : {}", form);
      }
    }
    
  • ● Form JSON

      var form = {
          id: "123",
          name: "123",
          testTags: [{id: "1111", tag: "2222"}]
      };
    
      $.ajax({
          url: "http://localhost:8080/test",
          method: "post",
          type: "json",
          contentType: "application/json",
          data: JSON.stringify(form),
          success: function(data) {
              console.log(data);
          }
      });
    

Before Source와 After Source의 차이를 눈치챘는가? ㅡ_-)?
RequestMethod가 GET에서 POST로 변경되었다. 이에 대한 설명을 해본다. 토비의 스프링에 @RequestBody, @ResponseBody 를 살펴보기 바란다.

@RequestBody, @ResponseBody

최근 개발하고 있는 방식은 대부분이 프론트엔드와 백엔드를 분리하여 개발을 하고 있다. 프론트엔드의 AJAX요청은 대부분 JSON으로 되어 있고, 이에 맞춰 백엔드에서도 JSON 형태로 응답을 해주는 방식을 취하게 된다. 스프링에서는 이와 관련된 @MVC 관련 애노테이션과 설정을 통해 기능을 제공하고 있다.

  • ● @RequestBody

    이 애노테이션이 붙은 파라미터에는 HTTP 요청의 본문body 부분이 그대로 전달된다.
    AnnotationMethodHandlerAdapter에는 HttpMessageConverter 타입의 메시지 변환기message converter가 여러 개 등록되어 있다. @RequestBody가 붙은 파라미터가 있으면 HTTP 요청의 미디어 타입과 파라미터의 타입을 먼저 확인한다(servlet-context.xml 에서 <annotation-drvien> 태그 내에 선언하는 <message-converter> 에서 확인). 메시지 변환기 중에서 해당 미디어 타입과 파라미터 타입을 처리할 수 있다면, HTTP 요청의 본문 부분을 통째로 변환해서 지정된 메소드 파라미터로 전달해준다.

    내가 헤매던 부분이 바로 이부분이었다. ㅡ_-);;JSON 메시지 변환기에는 MappingJackson2HttpMessageConverter를 사용했다. @RequestBody 애노테이션은 요청에서 Body부분을 살펴 요청된 데이터를 추출하여 파라미터로 변환해주는데, ‘GET’ 메소드 요청의 경우에는 HTTP Body에 요청이 전달되는 것이 아니라, URL의 파라메터로 전달(ex: http://localhost:8080/test?id=123&name=123&testTag=…) 형식으로 전달되기 때문에 @RequestBody로 받으려고 해도 서로 다른 곳을 보며 데이터가 없다는 결과를 던질 수밖에 없다(이 부분도 로그에 대해서 상세하게 설정해서 살펴보면서 확인한 결과. 로그! 개발 중에 문제가 되는 요인들을 찾기 위해서 관심을 가지자).

  • ● @ResponseBody

    @ResponseBody는 @RequestBody와 비슷한 방식으로 동작한다. @ResponseBody가 메소드 레벨에서 부여되면 메소드가 리턴하는 오브젝트는 뷰를 통해 결과를 만들어내는 모델로 사용하는 대신, 메시지 컨버터를 통해 바로 HTTP 응답의 메시지 본문으로 변환된다.

    간단히 이야기 하자면, 요청한 형태에 맞춰서 메시지 변환기를 통해 결과값을 반환한다. ‘콩심은 데 콩나고 팥 심은데 팥난다.’ 랄까? ContentNegotiatingViewResolver 와는 동작방식이 좀 다르다. ContentNegotiatingViewResolver는 등록되어 있는 ViewResolver중에서 controller 메소드의 리턴값을 통해 등록된 ViewResolver 중에서 적합한 형태로 처리해서 반환하는 반면, @ResponseBody는 @RequestBody가 선택한 형식으로 결과값을 변환하여 반환한다고 보면 된다.

  • ● MessageConverter 메시지 변환기의 종류는 Spring API 문서를 참고하자.

정리

해당하는 애노테이션들이 어떻게 동작하는지 내가 했던 인터넷 설정들이 어떻게 반응하는지를 제대로 이해했다면, 별다른 삽질없이 조용히 넘어갈 수 있던 문제였는데, 쉬운 문제였다. 하아!!
요즘 들어서 부쩍 ‘기본을 탄탄히 갖춰야겠다.’라는 생각을 하게되는 일들이 많아지고 있다.

끊임없이 공부하고 공부하라!

  • ● 이와 관련된 내용들은 ‘[토비의 스프링]에서 [스프링 @MVC]’ 관련 내용을 상세하게 설명되어 있다.


<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

<head>

    <title th:text="${title}">Report mail</title>

    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

</head>

<body>

    <h1>Send Test Report Email</h1>

    <table>

        <thead>

        </thead>

        <tbody>

        <colcolgroup>

        <col style="width: 100px;"/>

        <col/>

        </colcolgroup>

            <tr>

                <td th:text="#{mail.th.project.id}">Proejct</td>

                <td th:text="${projectId}">Project</td>

            </tr>

            <tr>

                <td th:text="#{mail.th.sub.project.id}">Sub Project</td>

                <td th:text="${subProjectId}">Sub Project</td>

            </tr>

            <tr>

                <td th:text="#{mail.th.duration}">Duration</td>

                <td th:text="${duration}">Duration</td>

            </tr>

            <tr>

                <td th:text="#{mail.th.format}">Report Format</td>

                <td th:text="${format}">Document</td>

            </tr>

            <tr>

                <td th:text="#{mail.th.start.datetime}">Start Datetime</td>

                <td th:text="${startDateTime}"></td>

            </tr>

            <tr>

                <th th:text="#{mail.th.end.datetime}">End Datetime</th>

                <td th:text="${endDateTime}"></td>

            </tr>

        </tbody>

    </table>

</body>

</html> 

위의 템플릿 코드는 그대로 둔 채,

<!-- Thymeleaf template -->

<dependency>

    <groupId>org.thymeleaf</groupId>

    <artifactId>thymeleaf</artifactId>

    <version>2.0.17</version>

</dependency>

<dependency>

    <groupId>org.thymeleaf</groupId>

    <artifactId>thymeleaf-spring3</artifactId>

    <version>2.0.17</version>

</dependency>

아래의 오류가 발생한다. 

ERROR: org.thymeleaf.TemplateEngine - [THYMELEAF][http-bio-8080-exec-7] Exception processing template "report/testReportMail": Exception evaluating SpringEL expression: "title" (report/testReportMail:4)

thymeleaf 의 version을 2.0.17 에서 2.0.16으로 변경해보자.

정상동작한다. 

현재 사용하고 있는 스프링은 '3.2.0.RELEASE'


이메일 발송용 템플릿 엔진으로 사용하려고 Thymeleaf 를 찾았다.

전 프로젝트에서는 StringTemplate3를 사용했었는데, StringTemplate4로 바뀌면서 그 사용법이 많이 바뀐 탓에 새로 익혀야할 것 같아 찾다보니 몇몇 사람들의 추천하는 글을 보고는 덜컥 시도를 해본다.

ㅡ_-);; 주된 삽질의 끝은 오탈자였다. 하아...

Spring에서 사용하는 예제 : http://www.thymeleaf.org/springmail.html

<!-- THYMELEAF: Template Resolver for email templates --> 

<bean id="emailTemplateResolver" class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver"> 

  <property name="prefix" value="mail/" /> 

  <property name="templateMode" value="HTML5" /> 

  <property name="characterEncoding" value="UTF-8" /> 

  <property name="order" value="1" /> 

</bean> 


<!-- THYMELEAF: Template Resolver for webapp pages   --> 

<!-- (we would not need this if our app was not web) --> 

<bean id="webTemplateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver"> 

  <property name="prefix" value="/WEB-INF/templates/" /> 

  <property name="templateMode" value="HTML5" /> 

  <property name="characterEncoding" value="UTF-8" /> 

  <property name="order" value="2" /> 

</bean> 


<!-- THYMELEAF: Template Engine (Spring3-specific version) --> 

<bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine"> 

  <property name="templateResolvers"> 

    <set> 

      <ref bean="emailTemplateResolver" /> 

      <ref bean="webTemplateResolver" /> 

    </set> 

  </property> 

</bean> 

<!-- THYMELEAF: View Resolver - implementation of Spring's ViewResolver interface --> 

<!-- (we would not need this if our app was not web)                              --> 

<bean id="viewResolver" class="org.thymeleaf.spring3.view.ThymeleafViewResolver"> 

  <property name="templateEngine" ref="templateEngine" /> 

  <property name="characterEncoding" value="UTF-8" /> 

</bean> 



내가 적용한 코드

<!-- THYMELEAF: Template Resolver for email templates -->

    <bean id="emailTemplateResolver" class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver">

        <property name="prefix" value="META-INF/template/mail" />

        <property name="suffix" value=".html"/>

        <property name="templateMode" value="HTML5" />

        <property name="characterEncoding" value="UTF-8" />

        <property name="order" value="1" />

        <!-- Template cache is true by default. Set to false if you want -->

        <!-- templates to be automatically updated when modified.        -->

        <property name="cacheable" value="true" />

    </bean>

    

    <!-- THYMELEAF: Template Engine (Spring3-specific version) -->

    <bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine">

        <property name="templateResolvers">

          <set>

            <ref bean="emailTemplateResolver" />

          </set>

        </property>

    </bean>

이렇게 설정을 해놓으니 계속 

org.thymeleaf.exceptions.TemplateInputException: Error resolving template "email-test", template might not exist or might not be accessible by any of the configured Template Resolvers

TemplateEngine에서 템플릿 파일을 찾지 못한다는 오류를 뿌린다. ㅡ_-);;

2시간 정도 삽질을 한 결과를 알아냈다.

<bean id="emailTemplateResolver" class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver">

        <property name="prefix" value="META-INF/template/mail" />

...

</bean>

의 항목을

<bean id="emailTemplateResolver" class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver">

        <property name="prefix" value="META-INF/template/mail/" />

...

</bean>

으로 바꾸니까 된다.

미묘한 차이인데, 발견했는가? ㅡ_-)?

예제의 '/' 하나를 무시한 결과가 나의 3시간 삽질의 차이를 낳았다. Orz.

### 개발환경

* Hibernate 4.2.0.Final


### 문제발생 원인

* @Entity 선언 객체를 정렬하려는 영도로 interface Compare<T>를 구현한 후

 int compareTo(Object o) 에서 사용하려는 비교값 필드로 order 을 정의했다.

@Entity

@Table(name="SAMPLE")

public class Sample implements Serialize, Compare<Sample> {

@Id

@GeneratedValue(strategy=GenerationType.AUTO)

private Long id;


     @Getter

private Long order;


@Override

public int compareTo(Sample o) {

return getOrder().compareTo(o.getOrder());

}

}


  이렇게 정의된 엔티티 객체의 테이블이 생성되지 않는 문제가 발생해서, 이를 해결하기 위해서 여러가지를 시도해봤다.  복사해서 새로운 엔티티 객체를 만들어보고, 객체의 참조관계를 변경해보고, 클래스명을 바꿔보고 하이버네이트 버전을 변경해봤지만 증상은 동일했다.

  이를 확인하기 위해서 구현하는 과정을 한단계한단계 되짚어보았다. ㅠㅅ-) 그러다가 발견했다.

  새로 만들어서 id 값을 넣었을 떄까지는 이상없이 생성이 되다가,

private Long order;

이 필드를 넣는 순간부터 구현되지 않는 것을 발견했다.


### 해결방법

* order -> seq 로 변경

필드명을 seq로 변경하고 나니... 정상적으로 테이블이 생성되는 것을 확인했다.

### 보충 설명

다른 분이 알려주신 것을 보고 생각했다.

'아, order가 hibernate와 관련된 예약어가 아니라 database와 관련된 예약어...였어.'

Entity 객체의 필드명을 지정할 때 Database의 예약어를 사용하지 않도록 하자. 반드시 도메인의 필드명으로 사용해야 한다면, @Column(name="")을 이용하여 테이블의 컬럼명을 다른 것으로 변경하자.



+ Recent posts