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

표준 방식이 아닌 방법으로 가장 쉽고 용이한 방식으로 JRA(Java REST Annotation) 설정을 이용하여 RESTful 서비스를 작성할 수 있게 한다.

Java REST Annotation의 특징 을 살펴보면 다음과 같다.
  • Web Service로 노출시킬 서비스 인터페이스 클래스의 각 method별로 JRA 설정을 하여 RESTful Web Service를 구현한다.
  • 인터페이스 클래스의 메소드 별로 @Get/@Post/@Put/@Delete와 @HttpResource Annotation을 설정한다. HttpResource의 location 속성 정보가 RESTful Web Service로 접근하는 path 정보가 된다.
  • 인터페이스 클래스 메소드가 리턴 타입이 존재하는 경우 @WebResult Annotation을 설정한다.(리턴 타입이 void 인 경우 불필요함)
다음은 HTTP Binding(JRA)를 활용하여 RESTful 서비스 구현 시 Server와 Client 단에서 어떻게 사용해야 하는지에 대한 사용법이다.

Server Configuration

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

Samples

다음은 Movie Service의 인터페이스 클래스 정의에 대한 예제이다. 서버 구동을 위해서 Anyframe에서 제공하는 JaxWsServer 클래스를 이용하고 있다.
  • Interface Class
  • 다음은 Movie Service의 인터페이스 클래스를 작성한 MovieService.java 의 일부이다. Annotation 설정에 유의하도록 한다.
    import org.codehaus.jra.Get;
    import org.codehaus.jra.Post;
    import org.codehaus.jra.HttpResource;
    
    @WebService(targetNamespace = "http://anyframe.sample.movie.restful.httpbinding.jra")
    public interface MovieService {
        @Get
        @HttpResource(location = "/movies")
        @WebResult(name = "Movies")
        public Movies findMovieListAll() throws Exception;
    
        @Get
        @HttpResource(location = "/movies/{movieId}")
        @WebResult(name = "Movie")
        public Movie findMovie(FindMovie findMovie) throws Exception;
    
        @Post
        @HttpResource(location = "/movies")
        public void createMovie(@WebParam(name = "Movie")
        Movie movie) throws Exception;
            중략...
    
  • Test case
  • 다음은 서버 사이드의 서비스를 Web Services로 노출시키는 서버를 구동하는 코드를 작성한 HttpBindingJRATest.java 의 일부이다. setUp() 메소드 내에서 JaxWsServer 를 생성시킨 후, 서버의 정보로 인터페이스 클래스, 구현 클래스의 인스턴스, Web Services 주소를 설정해준다. 이때 서버 정보로 Binding Id를 HTTP Binding ID로 설정하고 있음에 유의하도록 한다.
    상위 테스트케이스 클래스인 RemotingTestCase 의 setUp() 메소드에서 Server 의 start() 메소드가 호출되면서 실제로 구동된다.
    public class HttpBindingJRATest 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);
            serverInfo.setWrapped(false);
            this.getServer().setServerInfo(serverInfo);
            super.setUp();
        }
           중략...
    [참고] Spring Configuration XML Server 설정 이용
    HTTP Binding(JRA)을 이용하여 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의 인터페이스 클래스에서 목록조회 시 리턴 값으로 사용하는 Movies.java 의 일부이다. Movies 클래스 정의 시 작성한 @XmlRootElement(name = "Movies") Annotation 설정에 유의하도록 한다. JAXB를 이용하여 서비스 결과 XML을 JavaBeans 객체로 변환 시 이 Annotation 정보를 이용한다. 특히 목록 조회 결과 시 사용되는 리턴 값은 내부 멤버 변수로 Collection 객체를 정의하여 새로운 Movies 객체를 작성함에 유의하도록 한다.
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement(name = "Movies")
    public class Movies {
        private Collection<Movie> movies;
    
        public Collection<Movie> getMovie() {
            return movies;
        }
    
        public void setMovie(Collection<Movie> movies) {
            this.movies = movies;
        }
            중략...
    
    다음은 JAXB를 사용하기 위해서 필요한 jaxb.index 파일이다. Movie와 Movies 클래스를 정의해 놓는다.
    Movie
    Movies
    
    다음은 XML Namespace가 정의된 XML 데이터를 JavaBeans 객체로 변경하기 위해서 필요한 package-info.java 파일이다. JRA를 이용하여 RESTful Service를 구현하면 서비스의 리턴 XML 데이터는 서비스 인터페이스 클래스의 패키지명의 반대 형태로 Namespace가 정의되어 온다. (ex."http://jra.httpbinding.restful.movie.sample.anyframe/")
    이 Namespace를 맞춰서 JavaBeans 객체를 생성하려고 하면 에러가 나게 되므로, 현재 Movie와 Movies 클래스의 패키지명으로 Movie Service의 인터페이스 클래스 선언부에 @WebService(targetNamespace = "http://anyframe.sample.movie.restful.httpbinding.jra")와 같이 작성해주도록 한다. 그리고 anyframe.sample.movie.restful.httpbinding.jra 패키지 하위에 package-info.java 파일을 작성해놓음으로써 클라이언트 사이드에서 JAXB를 통해 바이딩할 때 Namespace "http://anyframe.sample.movie.restful.httpbinding.jra"를 인식시키도록 한다.
    @javax.xml.bind.annotation.XmlSchema(
            namespace = "http://anyframe.sample.movie.restful.httpbinding.jra", 
            elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) 
    package anyframe.sample.movie.restful.httpbinding.jra;
    
  • Test Case
  • 다음은 HttpClient 클래스를 사용하여 RESTful Web Services로 노출된 Movie Service에 접근하는 클라이언트 코드를 작성한 HttpBindingJRATest.java 의 일부이다.
    testXXX() 메소드 내에서 Get/Post/Put/Delete Method 중 어느 것을 사용할 것인지 정하여 접근하고자 하는 Web Services 주소와 함께 정의하고, HttpClient 객체를 생성하여 위에서 정의한 Method를 실행시킨다. 이때 클라이언트는 서버의 Movie Service 인터페이스 클래스를 이용하지 않고 URL 정보를 이용하여 접근한다.
    RESTful WebService 실행 결과로 받은 XML 데이터를 JavaBeans 객체로 변경하여 메소드 동작이 올바른지 테스트해본다.
    public class HttpBindingJRATest extends RemotingTestCase {
        // ==============================================================
        // ====== TestCase methods ======================================
        // ==============================================================    
         
        /**
         * [Flow #-1] Positive Case : Get method로 Movie Service의 전체 목록 조회 기능을 
         *			    호출하여 XML data를 리턴받고
         *            JAXB를 사용하여 Movies 객체로 전환하여 사용한다.  
         *            @Get, @HttpResource, @WebResult annotation을 이용하여
         *            findMovieListAll method를 RESTful한 Web Service로 노출하여 Client가 
         *			   호출가능하도록 한다.
         *            (ex. @Get
         *                 @HttpResource(location = "/movies")
         *                 @WebResult(name = "Movies")
         *                 public Movies findMovieListAll() 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.jra");
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
    
            Movies movies =
                (Movies) 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를 사용하여 Movie 객체로 전환하여 사용한다.  
         *             @Get, @HttpResource, @WebResult annotation을 이용하여
         *            findMovie method를 RESTful한 Web Service로 노출하여 Client가
    	 *			    호출가능하도록 한다.
         *            (ex. @Get
         *                 @HttpResource(location = "/movies/{movieId}")
         *                 @WebResult(name = "Movie")
         *                 public Movie findMovie(FindMovie findMovie) 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.jra");
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            Movie movie =
                (Movie) unmarshaller.unmarshal(new InputSource(new StringReader(
                    response)));
    
            // 2. check the movie information
            assertEquals("The Sound Of Music", movie.getTitle());
            assertEquals("Robert Wise", movie.getDirector());
        }
    
        /**
         * [Flow #-4] Positive Case : Post method로 Movie Id가 "003"인 신규 Movie를
         *			    생성하는 기능을 호출한다.
         *            @Post, @HttpResource annotation을 이용하여
         *            createMovie method를 RESTful한 Web Service로 노출하여 Client가 
         *			    호출가능하도록 한다.
         *            (ex. @Post
         *            @HttpResource(location = "/movies")
         *            public void createMovie(@WebParam(name = "Movie") Movie movie) throws Exception;
         *            )
         * @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/jra/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();
            }
            중략...
    

유의 사항

위에서 단일항목을 Post Method 방식으로 생성하는 방법과 유사하게 Collection 항목을 Post Method 방식으로 보내어 생성해 낼 수 있다.

Samples

Post 방식으로 보내고자 하는 Collection 항목을 다음과 같이 작성한다. 이때 Movie를 소문자로 시작하여 작성함에 유의하도록 한다. 왜냐하면 기본적으로 RESTful Web Service 사용 시 Collection 데이터 내부 항목에 대해서 소문자로 XML 내용이 채워지게 되기 때문이다.
  • Collection 데이터 작성
  • 다음은 생성하고자 하는 Customer에 대한 Collection 데이터를 작성한 create_movies.txt 의 일부이다.
    Movies 태그 내 하위 태그로 movie 태그가 소문자로 작성되어 있음에 주의한다.
    <Movies xmlns="http://anyframe.sample.movie.restful.httpbinding.jra">
    	<movie xmlns="http://anyframe.sample.movie.restful.httpbinding.jra">
    		<movieId>005</movieId>
    		<title>Life Is Beautiful</title>
    		<director>Roberto Benigni</director>
    	</movie>
    	<movie xmlns="http://anyframe.sample.movie.restful.httpbinding.jra">
    		<movieId>006</movieId>
    		<title>Life Is sad</title>
    		<director>no director</director>
    	</movie>		
    </Movies>
    
    만약, 대문자 Movie로 작성하고자 한다면 아래와 같이 Movies 클래스의 멤버변수인 movies에 @XmlElement Annotation 설정을 추가해주는데 이때 Movies 목록 조회 시 대문자 Movie와 소문자 movie 내용이 XML 상에 중복하게 나오게 된다. 단, JAXB 데이터 바인딩 이후에는 내용 중복없이 사용된다.
    다음은 Movie Service의 인터페이스 클래스에서 목록조회 시 리턴 값으로 사용하는 Movies.java 의 일부이다. Movies 클래스 내 멤버 변수로 선언한 movies 변수 정의 시 작성한 @XmlElement(name="Movie") Annotation 설정에 유의하도록 한다.
    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement(name = "Movies")
    public class Movies {
    @XmlElement(name="Movie")
        private Collection<Movie> movies;
    
        public Collection<Movie> getMovie() {
            return movies;
        }
    
        public void setMovie(Collection<Movie> movies) {
            this.movies = movies;
        }
            중략...
    
  • Test Case
  • 다음은 HttpClient 클래스를 사용하여 RESTful Web Services로 노출된 Movie Service에 접근하는 클라이언트 코드를 작성한 HttpBindingJRATest.java 의 일부이다.
    testCreateMovies() 메소드 내에서 Post Method를 사용하여 Movie Collection 목록을 생성해내도록 한 후, Get Mothod를 사용하여 Movie 목록을 확인한다.
    RESTful WebService 실행 결과로 받은 XML 데이터를 JavaBeans 객체로 변경하여 메소드 동작이 올바른지 테스트해본다.
    public class HttpBindingJRATest extends RemotingTestCase {
        // ==============================================================
        // ====== TestCase methods ======================================
        // ==============================================================    
        /**
         * [Flow #-7] Positive Case : Post method로 Movie Id가 "005", "006"인 신규 Movie 목록을 
    	 *							    생성하는 기능을 호출한다.
         *                            @Post, @HttpResource annotation을 이용하여
         *                            createMovies method를 RESTful한 Web Service로 노출하여 
         *							  Client가 호출가능하도록 한다.
         *                            (ex. @Post
         *                                 @HttpResource(location = "/movielist")
         *                                 public void createMovies(@WebParam(name = "Movies")
         *                                   Movies movies) throws Exception;
         *                            ) 
         * @throws Exception
         *         throws exception which is from service                            
         */     
        public void testCreateMovies() throws Exception {
            // 1. create movie
            String inputFile =
                this.getClass().getClassLoader().getResource(
                    "webservices/restful/httpbinding/jra/create_movies.txt")
                    .getFile();
            File input = new File(inputFile);
            PostMethod post =
                new PostMethod("http://localhost:9002/movieservice/movielist");
            RequestEntity entity =
                new FileRequestEntity(input, "text/xml; charset=ISO-8859-1");
            post.setRequestEntity(entity);
            HttpClient httpclient = new HttpClient();
    
            try {
                assertEquals(200, httpclient.executeMethod(post));
            } catch (Exception e) {
                fail();
            } finally {
                post.releaseConnection();
            }
    
            // 2. find movies
            GetMethod get =
                new GetMethod("http://localhost:9002/movieservice/movies");
            HttpClient httpclientforlist = new HttpClient();
            String response = "";
            try {
                assertEquals(200, httpclientforlist.executeMethod(get));
                response = get.getResponseBodyAsString();
                System.out.println("movies reponse:"+response);
            } catch (Exception e) {
                fail();
            } finally {
                get.releaseConnection();
            }
            
            JAXBContext jaxbContext =
                JAXBContext
                    .newInstance("anyframe.sample.movie.restful.httpbinding.jra");
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
    
            Movies movies =
                (Movies) unmarshaller.unmarshal(new InputSource(new StringReader(
                    response)));
    
            // 3. check the new movie information
            Collection col = movies.getMovie();
            Iterator itr = col.iterator();
            while(itr.hasNext())
            {
                Movie movie = (Movie)itr.next();
                if(movie.getMovieId().equals("005"))
                {
                    assertEquals("Life Is Beautiful", movie.getTitle());
                    assertEquals("Roberto Benigni", movie.getDirector());                
                }
                else if(movie.getMovieId().equals("006"))
                {
                    assertEquals("Life Is sad", movie.getTitle());
                    assertEquals("no director", movie.getDirector());                
                }
            }
        }
            중략...
    

Resources