얼마 전에 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]’ 관련 내용을 상세하게 설명되어 있다.


미투데이 따라집기!!!

me2daytest(1).JPG

 

테이블 생성 SQL

  1. create table me2daytest(
        pid number primary key,
        pname varchar2(20),
        particle varchar2(150),
        pwdate date );

 

index2.jsp

  1. <%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>
    <html>
    <head>
    <script language="javascript" src="jslb_ajax.js" >
    </script> 
    <script language="javascript">
    <!--
        //콜백함수(수신시에 실행된다)
        function on_loaded(oj){
            //응답을 취득
            var res = oj.responseText

            //응답된 문자열을 DIV로 출력
            document.getElementById("list").innerHTML = res
        }

        function result(oj){
            //응답을 취득
            var res = oj.responseText
       
            //응답된 문자열을 DIV로 출력
            document.getElementById("result").innerHTML = res
    }

        //입력한 내용을 바로 업데이트 시킴.
        function input(oj){
            if (oj.form.article.value.length === 0) return
            sendRequest(result, '&name='+oj.form.name.value+'&article='+oj.form.article.value, 'POST', 'input.jsp', true, true)
            sendRequest(on_loaded, '', 'POST', 'list.jsp', true, true)
            oj.form.article.value='';
        }

        function on_formLoad(oj){
            sendRequest(on_loaded, '', 'POST', 'list.jsp', true, true)
        }


    //-->
    </script>
    <meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
    <title>허니몬의 me2day 떼쳇 따라잡기</title>
    </head>
        <body>
        <h3>사용법 : 이름 입력하고 내용 입력하고 Enter 혹은 글올리기 클릭하면 됨!!</h3>
        <h3>여러분은 낙장불입의 세계에 한발 들어섰습니다.</h3>
        <hr>
        <form>
        <table>
        <tr>
        <th>이름</th><th>내용</th><th><!-- 스크립트 값이 들어가야하려나? document.write(var); -->
        <div id='result'></div>
        </th>
        </tr>
        <tr>
       
        <td><input type='text' name='name' /></td><td><input type='text' name='article' size='50' maxlength='50' onkeyup='on_formLoad(this)'/></td><td><input type='button' value='글 올리기' onclick='input(this)' /></td>
       
        </tr>
        </table>
        </form>
        화면출력!!
        <div id='list'></div>
        </body>
    </html>

 

input.jsp

  1. <%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>
    <jsp:useBean id="data" class="data.DataProc" scope="page"/>
    <%
        String name = request.getParameter("name");
        String article = request.getParameter("article");
       
        data.insertData(name,article);
        out.println("처리완료");
    %>

 

list.jsp

  1. <%@ page language="java" contentType="text/html; charset=EUC-KR" pageEncoding="EUC-KR"%>
    <%@ page import='java.util.*, data.*' %>
    <jsp:useBean id='data' class='data.DataProc' scope='page' />
    <%
        Vector result = data.list();

        if( result.isEmpty() == true) {
            out.println("입력된 데이터가 없습니다.");
        } else {
    %>
        <table border='1'>
    <%
            //vector는 0부터 시작하는 거였구나... 그걸 몰랐다!!
            for( int i = 0; i < result.size(); i++){
                DataBean b = (DataBean)result.elementAt(i);
                String name = b.getPname();
                String article = b.getParticle();
                String wdate = b.getPwdate();
    %>
            <tr>
            <td><%= name %></td><td><%= article %></td><td><%= wdate %></td>
            </tr>
    <%
            }
    %>
        </table>
    <%
           
        }
    %>

 

DataBean.jsp

  1. package data;

    public class DataBean {
        private String pname="";
        private String particle="";
        private String pwdate="";
       
        public DataBean(){}
       
        public DataBean(String pname, String particle, String pwdate){
            this.pname = pname;
            this.particle = particle;
            this.pwdate = pwdate;
        }

        public String getPname() {
            return pname;
        }

        public void setPname(String pname) {
            this.pname = pname;
        }

        public String getParticle() {
            return particle;
        }

        public void setParticle(String particle) {
            this.particle = particle;
        }

        public String getPwdate() {
            return pwdate;
        }

        public void setPwdate(String pwdate) {
            this.pwdate = pwdate;
        }
       
    }

 

DataProc.java

  1. package data;

    import java.sql.Connection;
    import java.sql.ResultSet;
    import java.sql.Statement;
    import java.util.Vector;

    import javax.naming.InitialContext;
    import javax.sql.DataSource;

    public class DataProc {
        DataSource ds;
       
        public DataProc(){
            try {
                InitialContext context = new InitialContext();
                ds = (DataSource)context.lookup("java:comp/env/jdbc/oracle");
            } catch(Exception e){
                e.printStackTrace();
            }
        }
       
        public void insertData(String name, String article){
            Statement stat;
            Connection con;
            int pid = checkpid();
           
            String sql = "INSERT INTO me2daytest values(" + pid + ",'" + name + "', '"+ article +"', sysdate)";
            try{
                con = ds.getConnection();
                stat = con.createStatement();
                stat.executeUpdate(sql);
                stat.close();
                con.close();
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
       
        public Vector list(){
            Connection con;
            Statement stat;
            Vector result = new Vector();
           
            String sql = "SELECT pname, particle, pwdate FROM me2daytest ORDER BY pwdate desc";
            try {
                con = ds.getConnection();
                stat = con.createStatement();
                ResultSet rs = stat.executeQuery(sql);
                while( rs.next() ){
                    String name = rs.getString(1);
                    String article = rs.getString(2);
                    String wdate = rs.getString(3);
                    DataBean bean = new DataBean(name, article, wdate);
                    result.add(bean);
                }
                rs.close();
                stat.close();
                con.close();
            } catch(Exception e){
                e.printStackTrace();
            }
            return result;   
        }
       
        public int checkpid(){
            Connection con;
            Statement stat;
            int pid=0;
           
            String sql = "SELECT max(pid) FROM me2daytest";
            try {
                con = ds.getConnection();
                stat = con.createStatement();
                ResultSet rs = stat.executeQuery(sql);
                while( rs.next() ){
                    pid = rs.getInt(1);
                }
                rs.close();
                stat.close();
                con.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return pid+1;
        }
    }

 

문제점 :

  • 다른 사람이 입력한 뒤에 Reload가 되지 않는다. 이건 어떻게 해야하지? ㅡㅅ-)? 원격인터페이스를 써줘야할까? EJB 무상태 세션빈?
  • 본문 입력시 입력하지 않고 입력했을 때 javascript로 거절하도록 한다.
  • 페이지 나누기 기능이 필요할 듯 하다.
  • 미관상 이쁘지 않다.

이 글은 스프링노트에서 작성되었습니다.

+ Recent posts