HTTP Binding(Naming Convention) 활용한 RESTful 서비스 구현

표준 방식이 아닌 방법으로 가장 쉽고 용이한 방식으로 Naming Convention 기반의 매핑 방식을 이용하여 RESTful 서비스를 작성할 수 있게 한다.

Naming Convention 기반 매핑의 특징 을 살펴보면 다음과 같다.
  • Web Service로 노출시킬 서비스 인터페이스 클래스의 각 method를 특정 Naming Convention에 맞춰서 정의하면 부가 Annotation이나 Configuration 없이 손쉽게 RESTful Web Service를 구현할 수 있다.
  • 기본 CRUD에 대한 메소드 Naming Convention이 존재한다.(아래 표 참조)
메소드 Naming Convention
매핑 결과
Collection<Movie> getMovies() HTTP GET /movies
Movie getMovie(movieId) HTTP GET /movies/{movieId}
void addMovie(Movie movie)
*createXXX도 동일함
HTTP POST /movies
void updateMovie(String movieId, Movie movie) HTTP PUT /movies/{movieId}
void deleteMovie(String movieId)
*removeXXX도 동일함
HTTP DELETE /movies/{movieId}

다음은 HTTP Binding(Naming Convention)를 활용하여 RESTful 서비스 구현 시 Server와 Client 단에서 어떻게 사용해야 하는지에 대한 사용법이다.

Server Configuration

Movie Service를 JAX-WS Frontend를 사용하여 RESTful Web Services로 노출시켜보도록 한다.

Samples

다음은 Movie Service의 인터페이스 클래스 정의에 대한 예제이다. 서버 구동을 위해서 Anyframe에서 제공하는 JaxWsServer 클래스를 이용하고 있다.
  • Interface Class
  • 다음은 Movie Service의 인터페이스 클래스를 작성한 MovieService.java 의 일부이다. Annotation 설정에 유의하도록 한다. 특히, XML 데이터를 서버에 전달하여 객체로 변경시킬 때 JAXB를 사용하므로 addMovie와 updateMovie 메소드에 @WebParam Annotation 속성 name 값을 Movie로 설정해놓아야 하는 것을 기억한다.
    @WebService(
      targetNamespace = "http://anyframe.sample.movie.restful.httpbinding.namingconvention")
    public interface MovieService {
    
        public Collection<Movie> getMovies() throws Exception;
    
        public Movie getMovie(String movieId) throws Exception;
    
        public void addMovie(@WebParam(name = "Movie")
        Movie movie) throws Exception;
    
        public void updateMovie(String movieId, @WebParam(name = "Movie")
        Movie movie) throws Exception;
    
        public void deleteMovie(String movieId) throws Exception;
            중략...
    
  • Test case
  • 다음은 서버 사이드의 서비스를 Web Services로 노출시키는 서버를 구동하는 코드를 작성한 HttpBindingNamingConventionTest.java 의 일부이다. setUp() 메소드 내에서 JaxWsServer 를 생성시킨 후, 서버의 정보로 인터페이스 클래스, 구현 클래스의 인스턴스, Web Services 주소를 설정해준다. 이때 서버 정보로 Binding Id를 HTTP Binding ID로 설정하고 있음에 유의하도록 한다.
    상위 테스트케이스 클래스인 RemotingTestCase 의 setUp() 메소드에서 Server 의 start() 메소드가 호출되면서 실제로 구동된다.
    public class HttpBindingNamingConventionTest extends RemotingTestCase {
        // ==============================================================
        // ====== TestCase 수행에 필요한 사전 작업 정의 ====================
        // ==============================================================    
        public void setUp() throws Exception {
            this.setServer(new JaxWsServer());
            ServerInfo serverInfo =
                new ServerInfo(MovieService.class, new MovieServiceImpl(),
                    "http://localhost:9002/movieservice/");
            // Use the HTTP Binding which understands the
            // Java Rest Annotations
            serverInfo.setBindingId(HttpBindingFactory.HTTP_BINDING_ID);
            this.getServer().setServerInfo(serverInfo);
            super.setUp();
        }
           중략...
    [참고] Spring Configuration XML Server 설정 이용
    HTTP Binding(Naming Convention)을 이용하여 RESTful 서비스를 구현할 때 Spring 설정 파일을 이용하여 서버를 구동시킬 수 있다. 다음은 Spring Configuration XML 예시이다.
    <jaxws:endpoint id="movieService" 
                    implementor="#anyframe.sample.movie.jaxws.MovieService" 
                    bindingUri="http://apache.org/cxf/binding/http"
                    address="/movieservice">
    </jaxws:endpoint>                
    중략...

Client Configuration

Web Services에 접근하기 위한 클라이언트를 작성한다. HttpClient 혹은 URL 클래스를 이용하여 RESTful WebService에 접근하도록 한다.

Samples

다음은 org.apache.commons.httpclient.HttpClient를 이용하여 RESTful Web Services로 노출된 Movie Service에 접근하는 예제이다.
이때, RESTful WebService의 결과 값이 XML 형태로 리턴되기 때문에 실제 클라이언트 코드에서 JavaBeans 객체로 변경하여 사용해야 하는 작업이 추가된다. XML을 JavaBeans 객체로 변경하는 일을 JAXB를 통해서 수행하고 있다.
  • JAXB 활용
  • JAXB를 활용하여 서비스 결과 XML을 JavaBeans로 변경시키기 위해서는 다음과 같이 각 JavaBeans 객체에 Annotation을 설정하고 jaxb.index 파일을 추가 생성시켜줘야 한다.

    다음은 Movie Service의 인터페이스 클래스에서 파라미터와 리턴 값으로 사용하는 Movie.java 의 일부이다.
    Movie 클래스 정의 시 작성한 @XmlRootElement(name = "Movie") Annotation 설정에 유의하도록 한다. JAXB를 이용하여 서비스 결과 XML을 JavaBeans 객체로 변환 시 이 Annotation 정보를 이용한다.
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement(name = "Movie")
    public class Movie implements Serializable {
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
            중략...
    
    다음은 Movie Service의 인터페이스 클래스에서 목록조회 메소드인 getMovies()의 리턴 타입이 Collection<Movie> 인 경우, 추가 작성해야 하는 GetMoviesResponse.java 의 일부이다.
    현재 Wrapped mode로 서버를 설정하여 사용하고 있으므로 메소드 명으로 wrapping된 XML 결과 값이 리턴된다. @XmlRootElement(name = "getMoviesResponse") Annotation 설정에 유의하도록 한다. JAXB를 이용하여 서비스 결과 XML을 JavaBeans 객체로 변환 시 이 Annotation 정보를 이용한다.
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "", propOrder = {"movies" })
    @XmlRootElement(name = "getMoviesResponse")
    public class GetMoviesResponse {
    
        @XmlElement(name = "return", required = true)
        private Collection<Movie> movies;
        public Collection<Movie> getMovie() {
            return movies;
        }
        public void setMovie(Collection<Movie> movies) {
            this.movies = movies;
        }
            중략...
    
    다음은 Movie Service의 인터페이스 클래스에서Movie 조회 메소드인 getMovie()의 리턴 타입이 <Movie> 인 경우, 추가 작성해야 하는 GetMovieResponse.java 의 일부이다.
    현재 Wrapped mode로 서버를 설정하여 사용하고 있으므로 메소드 명으로 wrapping된 XML 결과 값이 리턴된다. @XmlRootElement(name = "getMovieResponse") Annotation 설정에 유의하도록 한다. JAXB를 이용하여 서비스 결과 XML을 JavaBeans 객체로 변환 시 이 Annotation 정보를 이용한다.
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlType(name = "", propOrder = {"movie" })
    @XmlRootElement(name = "getMovieResponse")
    public class GetMovieResponse {
    
        @XmlElement(name = "return", required = true)
        private Movie movie = null;
        public Movie getMovie() {
            return movie;
        }
        public void setMovie(Movie movie) {
            this.movie = movie;
        }
            중략...
    
    다음은 JAXB를 사용하기 위해서 필요한 jaxb.index 파일이다. Movie와 GetMoviesResponse 그리고 GetMovieResponse 클래스를 정의해 놓는다.
    Movie
    GetMoviesResponse
    GetMovieResponse
    
    다음은 XML Namespace가 정의된 XML 데이터를 JavaBeans 객체로 변경하기 위해서 필요한 package-info.java 파일이다. HTTP Binding(Naming Convention) 방식을 이용하여 RESTful Service를 구현하면 서비스의 리턴 XML 데이터는 서비스 인터페이스 클래스의 패키지명의 반대 형태로 Namespace가 정의되어 온다. (ex."http://namingconvention.httpbinding.restful.movie.sample.anyframe/")
    이 Namespace를 맞춰서 JavaBeans 객체를 생성하려고 하면 에러가 나게 되므로, 현재 Movie 클래스의 패키지명으로 Movie Service의 인터페이스 클래스 선언부에 @WebService(targetNamespace = "http://anyframe.sample.movie.restful.httpbinding.namingconvention")와 같이 작성해주도록 한다. 그리고 anyframe.sample.movie.restful.httpbinding.namingconvention 패키지 하위에 package-info.java 파일을 작성해놓음으로써 클라이언트 사이드에서 JAXB를 통해 바이딩할 때 Namespace "http://anyframe.sample.movie.restful.httpbinding.namingconvention"를 인식시키도록 한다.
    @javax.xml.bind.annotation.XmlSchema(
            namespace = "http://anyframe.sample.movie.restful.httpbinding.namingconvention", 
            elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) 
    package anyframe.sample.movie.restful.httpbinding.namingconvention;
    
  • Test Case
  • 다음은 HttpClient 클래스를 사용하여 RESTful Web Services로 노출된 Movie Service에 접근하는 클라이언트 코드를 작성한 HttpBindingNamingConventionTest.java 의 일부이다.
    testXXX() 메소드 내에서 Get/Post/Put/Delete Method 중 어느 것을 사용할 것인지 정하여 접근하고자 하는 Web Services 주소와 함께 정의하고, HttpClient 객체를 생성하여 위에서 정의한 Method를 실행시킨다. 이때 클라이언트는 서버의 Movie Service 인터페이스 클래스를 이용하지 않고 URL 정보를 이용하여 접근한다.
    RESTful WebService 실행 결과로 받은 XML 데이터를 JavaBeans 객체로 변경하여 메소드 동작이 올바른지 테스트해본다.
    public class HttpBindingNamingConventionTest extends RemotingTestCase {
        // ==============================================================
        // ====== TestCase methods ======================================
        // ==============================================================       
        /**
         * [Flow #-1] Positive Case : Get method로 Movie Service의 전체 목록 조회 기능을 호출하여 
         *            XML data를 리턴받고 JAXB를 사용하여 GetMoviesResponse 객체로 전환하여 사용한다.  
         *            Naming Convention 규칙: 
         *            Collection<"resource class name"> get+"The plural of resource class name"() 
         *                            
         *            getMovies method를 RESTful한 Web Service로 노출하여 Client가 호출가능하도록 한다.
         *            (ex. public Collection<Movie> getMovies() throws Exception; )
         * @throws Exception
         *         throws exception which is from service                            
         */         
        public void testFindMovieListAll() throws Exception {
            // 1. find movie
            GetMethod get = new GetMethod("http://localhost:9002/movieservice/movies");
            HttpClient httpclient = new HttpClient();
            String response = "";
            try {
                assertEquals(200, httpclient.executeMethod(get));
                response = get.getResponseBodyAsString();
                System.out.println(response);
            } catch (Exception e) {
                fail();
            } finally {
                get.releaseConnection();
            }
    
            JAXBContext jaxbContext =
             JAXBContext.newInstance("anyframe.sample.movie.restful.httpbinding.namingconvention");
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
    
            GetMoviesResponse movies = (GetMoviesResponse) unmarshaller.unmarshal
                    (new InputSource(new StringReader(response)));
    
            // 2. check the movie information
            assertEquals(2, movies.getMovie().size());
        }
    
        /**
         * [Flow #-3] Positive Case : Get method로 Movie Id가 "001"인 Movie를 조회하는 기능을 호출하여 
         *            XML data를 리턴받고 JAXB를 사용하여 GetMovieResponse 객체로 전환하여 사용한다.  
         *            Naming Convention 규칙: 
         *            "resource class name" get+"resource class name"(Object id) 
         *                            
         *            getMovie method를 RESTful한 Web Service로 노출하여 Client가 호출가능하도록 한다.
         *            (ex. public Movie getMovie(String movieId) throws Exception;)
         * @throws Exception
         *         throws exception which is from service                            
         */       
        public void testFindMovie() throws Exception {
            // 1. find movie
            GetMethod get=new GetMethod("http://localhost:9002/movieservice/movies/001");
            HttpClient httpclient = new HttpClient();
            String response = "";
            try {
                assertEquals(200, httpclient.executeMethod(get));
                response = get.getResponseBodyAsString();
                System.out.println("find: " + response);
            } catch (Exception e) {fail();
            } finally { get.releaseConnection();}
    
            JAXBContext jaxbContext = JAXBContext
                    .newInstance("anyframe.sample.movie.restful.httpbinding.namingconvention");
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            GetMovieResponse movie = (GetMovieResponse) unmarshaller.unmarshal(
                  new InputSource(new StringReader(response)));
    
            // 2. check the movie information
            assertEquals("The Sound Of Music", movie.getMovie().getTitle());
            assertEquals("Robert Wise", movie.getMovie().getDirector());
        }
    
        /**
         * [Flow #-4] Positive Case : Post method로 Movie Id가 "003"인 신규 Movie를 생성하는 .
         *            기능을 호출한다
         *            Naming Convention 규칙: 
         *            void add or create + "resource class name"("resource class" obj) 
         *                            
         *            addMovie method를 RESTful한 Web Service로 노출하여 Client가 호출가능하도록 한다.
         *            (ex. public void addMovie(@WebParam(name = "Movie")  Movie movie) ...
         * @throws Exception
         *         throws exception which is from service                            
         */    
        public void testCreateMovie() throws Exception {
            // 1. create movie
            String inputFile = this.getClass().getClassLoader().getResource(
             "webservices/restful/httpbinding/namingconvention/create_movie.txt").getFile();
            File input = new File(inputFile);
            PostMethod post =
                new PostMethod("http://localhost:9002/movieservice/movies");
            RequestEntity entity =
                new FileRequestEntity(input, "text/xml; charset=ISO-8859-1");
            post.setRequestEntity(entity);
            HttpClient httpclient = new HttpClient();
            String response = "";
    
            try {
                assertEquals(200, httpclient.executeMethod(post));
                response = post.getResponseBodyAsString();
                System.out.println("create: " + response);
            } catch (Exception e) {
                fail();
            } finally {
                post.releaseConnection();
            }
            중략...
    

Resources

  • 다운로드
  • 샘플 테스트 코드를 포함하고 있는 anyframe-remotingtest-src.zip 파일을 다운받은 후, 테스트 환경 설정 을 참조하여 위에서 제시한 예제 코드를 실행해 볼 수 있다. 이때 해당 프로젝트 내의 src/test/java 소스폴더 하위의 anyframe.core.remoting.webservices.restful.httpbinding.HttpBindingNamingConventionTest 클래스를 JUnit Test Framework을 이용하여 수행시키도록 한다.
    Name
    Download
    anyframe-remotingtest-src.zip
    Download

  • 참고자료