[Info]Tags categorized posts and contents patterns..

[AJAX] Ajax Code E xamples.. [Book] About the book.. [CSS] CSS Code E xamples.. [DB] Sql Code E xamples.. [DEV] All development stor...

2016년 9월 5일 월요일

[JAVA] 20장 스프링을 사용한 원격작업(remoting) 및 웹 서비스 #2..

출처 : Outsider's Dev Story https://blog.outsider.ne.kr/

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.

20.6 JMS

의존하는 통신 프로토콜로 JMS를 사용해서 서비스를 투명하게 노출할 수도 있다. 스프링 프레임워크의 JMS 원격지원은 아주 기본적이고(같은 스레드와 트랜잭션이 아닌 같은 Session에서 주고 받는다.) 이러한 스루풋은 아주 의존적인 구현체가 될 것이다. 이러한 단일 스레ㅇ드이면서 트랜잭션이 아닌 제약사항은 스프링의 JMS 원격 지원에만 적용된다. JMS에 기반한 메시징에 대한 스프링의 더 많은 지원에 대한 내용은 Chapter 22, JMS (Java Message Service)를 봐라.
다음 인터페이스는 서버와 클라이언트가 모두 사용한다.

1
2
3
4
5
6
7
8
Java

package com.foo;

public interface CheckingAccountService {

  public void cancelAccount(Long accountId);
}

다음은 앞의 인터페이스의 간단한 구현체로 서버측에서 사용한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Java

package com.foo;

public class SimpleCheckingAccountService implements CheckingAccountService {

  public void cancelAccount(Long accountId) {
    System.out.println("Cancelling account [" + accountId + "]");
  }
}

이 구성파일은 클라이언트와 서버가 모두 공유하는 JMS-인프라스트럭처 빈을 가진다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
    <property name="brokerURL" value="tcp://ep-t43:61616"/>
  </bean>

  <bean id="queue" class="org.apache.activemq.command.ActiveMQQueue">
    <constructor-arg value="mmm"/>
  </bean>
</beans>


20.6.1 서버측 구성

서버에서는 JmsInvokerServiceExporter를 사용해서 서비스 객체를 노출해야 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <bean id="checkingAccountService"
      class="org.springframework.jms.remoting.JmsInvokerServiceExporter">
    <property name="serviceInterface" value="com.foo.CheckingAccountService"/>
    <property name="service">
      <bean class="com.foo.SimpleCheckingAccountService"/>
    </property>
  </bean>

  <bean class="org.springframework.jms.listener.SimpleMessageListenerContainer">
    <property name="connectionFactory" ref="connectionFactory"/>
    <property name="destination" ref="queue"/>
    <property name="concurrentConsumers" value="3"/>
    <property name="messageListener" ref="checkingAccountService"/>
  </bean>
</beans>


Java

package com.foo;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Server {

  public static void main(String[] args) throws Exception {
    new ClassPathXmlApplicationContext(new String[]{"com/foo/server.xml", "com/foo/jms.xml"});
  }
}


20.6.2 클라이언트측 구성

클라이언트는 합의한 인터페이스 (CheckingAccountService)를 구현할 클라이언트측 프록시를 생성해야 한다. 다음 빈 정의로 생성한 최종 객체(The resulting object created off the back of the following bean definition)를 다른 클라이언트측 객체에 주입할 수 있고 JMS로 서버측 객체 호출에 대한 포워딩은 프록시가 담단한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <bean id="checkingAccountService"
      class="org.springframework.jms.remoting.JmsInvokerProxyFactoryBean">
    <property name="serviceInterface" value="com.foo.CheckingAccountService"/>
    <property name="connectionFactory" ref="connectionFactory"/>
    <property name="queue" ref="queue"/>
  </bean>
</beans>


Java

package com.foo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Client {

  public static void main(String[] args) throws Exception {
    ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] {"com/foo/client.xml", "com/foo/jms.xml"});
    CheckingAccountService service = (CheckingAccountService) ctx.getBean("checkingAccountService");
    service.cancelAccount(new Long(10));
  }
}

Lingo 프로젝트의 지원도 살펴보길 원할 것이다. “ Lingo는 스프링 프레임워크에 기반해서 스프링의 원격 라이브러리가 JMS를 지원하도록 확장한 경량 POJO 기반의 원격 메시징 라이브러리이다. ”

20.7 원격 인터페이스의 자동탐지는 구현되지 않았다

원격 인터페이스를 구현한 인터페이스를 자동으로 탐지하지 않는 주요 이유는 원격 호출자에 너무 많은 연결을 만들지 않도록 하기 위함이다. 대상 객체는 호출자에게 노출하고 싶지 않은 InitializingBean나 DisposableBean 같은 내부 콜백 인터페이스를 구현할 것이다.
대상 객체가 구현한 모든 인터페이스의 프록시를 제공하는 것을 로컬에서는 중요치 않지만 원격서비스를 익스포트하는 경우에는 원격에서 사용할 동작을 가진 특정 서비스 인터페이스를 노출해야 한다. 내부 콜백 인터페이스와 달리 대상 객체는 여러 비즈니스 인터페이스를 구현하고 그 중 하나는 원격으로 노출할 것이다. 그래서 서비스 인터페이스등을 지정해야 한다.
이는 설정의 편리함과 내부 메서드를 실수로 노출할 위험사이의 트레이드오프다. 서비스 인터페이스를 지정하는 것이 항상 많은 노력이 드는 것은 아니고 특정 메서드 노출을 안전하게 제어할 수 있게 해라.

20.8 기술 선택시 고려사항

여기서 언급한 모든 기술은 단점을 가진다. 기술을 선택할 때 요구사항과 노출할 서비스, 네트워크로 보낼 객체를 신중하게 고려해야 한다.
RMI를 사용할 때는 RMI 트래픽을 터널링하지 않고는 HTTP 프로토콜로 객체에 접근할 수 없다. RMI는 상당히 비용이 드는 프로토콜로 네트워크사이에 직렬화가 필요한 복잡한 데이터 모델을 사용할 때 중요한 전체 객체의 직렬화를 지원한다.하지만 RMI-JRMP는 자바 클라이언트에 의존성이 있는 Java-to-Java 원격 솔루션이다.
HTTP기반의 원격이 필요하다면 스프링의 HTTP 인보커가 괜찮지만 스프링 HTTP 인보커도 자바 직렬화에 의존하고 있다. 스프링 HTTP 인보커는 RMI 인보커와 기본 인프라스트럭처를 공유하면서 전송에 HTTP를 사용한다. 스프링 HTTP 인보커는 Java-to-Java 원격에만 제한될 뿐만 아니라 클라이언트와 서버측이 모두 스프링이어야 한다. (클라이언트와 서버가 모두 스프링인 경우 RMI가 아닌 인터페이스에는 스프링의 RMI도 적용한다.)
헤시안과 버랩은 명시적으로 자바가 아닌 클라이언트를 허용하므로 이기종간의 환경에서 작업할 때 중요한 가치를 준다. 하지만 비자바 지원은 아직 제한적이다. 알려진 이슈에는 지연 초기화 컬렉션이 있는 하이버네이트 객체의 직렬화 등이 있다. 이러한 데이터 모델이 있다면 헤시안 대신에 RMI나 HTTP 인코커를 고려해 봐라.
JMS는 서비스 클러스터를 제공하고 로드 밸런싱, 발견(discovery), 자동 장애복구(auto-failover)를 담당하는 JMS 중계자(broker)를 허용할 때 유용하다. 기본적으로 JMS 원격을 사용할 때 자바 직렬화를 사용하지만 JMS 제공자는 네트워크 형식에 따라 다른 기술로 구현한 서버를 사용할 수 있는 XStream같은 다른 메카니즘을 사용할 수 있다.
마지막으로 EJB는 표준 역할(role) 기반의 인증과 인가를 지원하고 원격 트랜잭션 전파(propagation)를 지원해서 RMI보다 뛰어난 이점들이 있다. 스프링 코어가 제공하지는 않지만 보안 컨텍스트 전파를 지원하는 RMI 인보커나 HTTP 인보커를 획득할 수 있다.(서드파티 플러그인이나 커스텀 솔루션을 위한 적절한 훅(hook)을 제공한다.)

20.9 클라이언트에서 RESTful 서비스 접근하기

RestTemplate이 클라이언트가 RESTful 서비스에 접근하는 핵심 클래스다. 개념적으로는 JdbcTemplate과 JmsTemplate이나 스프링 포트폴리오 프로젝트의 템플릿 클래스들과 유사하다. 콜백 메서드를 지정하고 객체를 HTTP 요청 바디로 마샬링하고 응답을 다시 객체로 언마샬링하는데 사용하는 HttpMessageConverter를 구성해서 RestTemplate의 동작을 커스터마이징 할 수 있다. 메시지 형식으로 XML을 사용하는 것이 일반적이므로 스프링은 org.springframework.oxm에 Object-to-XML 프레임워크를 사용하는 MarshallingHttpMessageConverter를 제공한다. MarshallingHttpMessageConverter는 XML을 객체로 매핑하는 기술의 넓은 범위의 선택권을 준다.
이번 섹션에서는 RestTemplate과 관련 HttpMessageConverters를 사용하는 방법을 설명한다.

20.9.1 RestTemplate

자바에서 RESTful 서비스 호출은 보통 Jakarta Commons HttpClient같은 헬퍼 클래스를 사용해서 호출한다. 일반적인 REST 작업에 이 접근은 다음에서 보듯이 너무 저수순이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Java

String uri = "http://example.com/hotels/1/bookings";

PostMethod post = new PostMethod(uri);
String request = // 예약 요청 내용 생성
post.setRequestEntity(new StringRequestEntity(request));

httpClient.executeMethod(post);

if (HttpStatus.SC_CREATED == post.getStatusCode()) {
  Header location = post.getRequestHeader("Location");
  if (location != null) {
    System.out.println("Created new booking at :" + location.getValue());
  }
}

RestTemplate은 6개의 주요 HTTP 메서드에 대응되는 고수준의 메서드를 제공하고 한줄로 다수의 RESTfult 서비스를 호출하고 REST 베스트 프렉틱스를 강제한다.

Table 20.1. RestTemplate 메서드 요약
HTTP 메서드RestTemplate 메서드
DELETEdelete
GETgetForObject
getForEntity
HEADheadForHeaders(String url, String… urlVariables)
OPTIONSoptionsForAllow(String url, String… urlVariables)
POSTpostForLocation(String url, Object request, String… urlVariables)
postForObject(String url, Object request, Class responseType, String… uriVariables)
PUTput(String url, Object request, String…urlVariables)







RestTemplate 메서드의 이름은 작명관례를 따라서 첫 부분은 어떤 HTTP 메서드가 호출되는 지를 나타내고 두번째 부분은 무엇을 반환하는 지를 나타낸다. 예를 들어 getForObject() 메서드는 GET을 수행하고 HTTP 응답을 원하는 객체 타입으로 변환해서 반환한다. postForLocation() 메서드는 POST를 수행하고 전달한 객체를 HTTP 요청으로 변환하고 새로 생성한 객체의 위치를 가리키는 응답 HTTP Location 헤더를 반환한다. HTTP 요청의 예외 처리에서는 RestClientException 타입의 예외가 던져질 것이다. 이 동작은 RestTemplate에 다른 ResponseErrorHandler를 연결해서 변경할 수 있다.
이러한 메서드에 전달하거나 메서드가 반환한 객체들은 HttpMessageConverter 인스턴스를 사용해서 HTTP 메세지로 변환한다. 주요 mime 타입에 대한 컨버터는 기본적으로 등록되어 있지만 messageConverters() 빈 프로퍼티로 자신만의 컨버터를 작성해서 등록할 수도 있다. 템플릿에 등록된 기본 컨버터 인스턴스는 ByteArrayHttpMessageConverter, StringHttpMessageConverter, FormHttpMessageConverter, SourceHttpMessageConverter다. messageConverters()를 사용해서 이 기본값을 덮어쓸 수 있고 MarshallingHttpMessageConverter나 MappingJacksonHttpMessageConverter를 사용한다면 덮어써야 한다.
각 메서드는 String 가변 인자나 Map<String,String> 두가지 형식의 URI 템플릿 인자를 받는다. 예를 들면

1
2
3
4
Java

String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}",
        String.class,"42", "21");

가변 인자를 사용하거나

1
2
3
4
Java

Map<String, String> vars = Collections.singletonMap("hotel", "42");
String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);

Map<String,String>를 사용한다.
RestTemplate 인스턴스를 생성하려면 인자없이 기본 생성자를 호출하면 된다. 이는 HTTP 요청을 생성하는 의존 구현체로 java.net에서 표준 자바 클래스를 사용할 것이다. 그리고 ClientHttpRequestFactory 구현체를 지정해서 오버라이드할 수 있다. 스프링은 요청 생성에 Jakarta Commons HttpClient를 사용하는 CommonsClientHttpRequestFactory 구현체를 제공한다. CommonsClientHttpRequestFactory는 org.apache.commons.httpclient.HttpClient 인스턴스를 사용해서 설정하고 org.apache.commons.httpclient.HttpClient는 자격(credentials) 정보나 연결 풀링 기능을 설정할 수 있다.
Jakarta Commons HttpClient를 직접 사용하는 앞의 예제를 RestTemplate을 사용해서 다음과 같이 다시 작성한다.

1
2
3
4
5
6
7
8
9
Java

uri = "http://example.com/hotels/{id}/bookings";

RestTemplate template = new RestTemplate();

Booking booking = // 예약 객체 생성

URI location = template.postForLocation(uri, booking, "1");

일반적인 콜백 인터페이스는 RequestCallback이고 실행(execute) 메서드가 호출되었을 때 호출한다.

1
2
3
4
5
6
7
Java

public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
         ResponseExtractor<T> responseExtractor,
         String... urlVariables)

// Map<String, String>처럼 urlVariables로 오버로드했다.

RequestCallback 인터페이스는 다음과 같이 정의했다.

1
2
3
4
5
Java

public interface RequestCallback {
  void doWithRequest(ClientHttpRequest request) throws IOException;
}

그리고 요청헤더를 조작하고 요청 바디를 작성할 수 있게 한다. 실행 메서드를 사용하는 경우 리소스 관리는 전혀 걱정한 필요가 없다. 템플릿이 항상 요청을 닫고 오류를 처리할 것이다. 실행메서드를 사용하는 방법과 다른 메서드 인자의 의미는 API 문서를 참고해라.

20.9.1.1 URI 사용하기

주요 HTTP 메서드에 대해 RestTemplate는 첫 인자로 문자열 URI나 java.net.URI를 받을 수 있다.
문자열 URI 방식은 가변 문자열 인자나 Map<String,String>를 템플릿 인자로 받고 URL 문자열이 인코딩되지 않아서 인코딩해야 한다고 가정한다. 예를 들면 다음과 같다.

1
2
3
Java

restTemplate.getForObject("http://example.com/hotel list", String.class);

이는 http://example.com/hotel%20list에 GET 요청을 보낼 것이다. 즉, 입력 URL 문자열이 이미 인코딩되었다면 한번 더 인코딩할 것이다. 예를 들어 http://example.com/hotel%20list는 http://example.com/hotel%2520list가 될 것이다. 두번 인코딩하는 것을 원치 않는다면 URL을 이미 인코딩했고 하나의 (완전히 확장된) URI를 여러번 재사용할 때 유용한 java.net.URI 메서드 방식을 사용해라.
URI 템플릿을 지원하는 URI를 생성하고 인코딩할 때 UriComponentsBuilder 클래스를 사용할 수 있다. 예를 들면 URL 문자열로 시작할 수 있다.

1
2
3
4
5
6
7
8
Java

UriComponents uriComponents =
    UriComponentsBuilder.fromUriString("http://example.com/hotels/{hotel}/bookings/{booking}").build()
        .expand("42", "21")
        .encode();

URI uri = uriComponents.toUri();

아니면 각 URI 컴포넌트를 각각 지정할 수 있다.

1
2
3
4
5
6
7
8
9
Java

UriComponents uriComponents =
    UriComponentsBuilder.newInstance()
        .scheme("http").host("example.com").path("/hotels/{hotel}/bookings/{booking}").build()
        .expand("42", "21")
        .encode();

URI uri = uriComponents.toUri();


20.9.1.2 요청 헤더와 응답 헤더 다루기

앞에서 설명한 메서드에 추가적으로 RestTemplate도 HttpEntity 클래스에 기반한 임의의 HTTP 메서드 실행에 사용할 수 있는 exchange() 메서드를 가진다.
아마 가장 중요한 것중에 하나는 요청 헤더를 추가하고 응답 헤더를 읽는데 exchange() 메서드를 사용할 수 있다는 것이다. 예를 들면 다음과 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("MyRequestHeader", "MyValue");
HttpEntity< ?> requestEntity = new HttpEntity(requestHeaders);

HttpEntity<String> response = template.exchange("http://example.com/hotels/{hotel}",
  HttpMethod.GET, requestEntity, String.class, "42");

String responseHeader = response.getHeaders().getFirst("MyResponseHeader");
String body = response.getBody();

앞의 예제에서 MyRequestHeader 헤더를 담고 있는 요청 엔티티를 먼저 준비한다. 그 다음 응답을 획득하고 MyResponseHeader와 바디를 읽는다.

20.9.2 HTTP 메시지 변환

getForObject(), postForLocation(), put() 메서드에 전달하거나 반환받은 객체는 HttpMessageConverters가 HTTP 요청이나 HTTP 응답으로 변환한다. HttpMessageConverter 인터페이스의 기능은 다음 소스를 보면 알 수 있을 것이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java

public interface HttpMessageConverter<T> {

  // 이 컨버터가 해당 클래스와 미디어타입을 읽을 수 있는지 여부를 나타낸다.
  boolean canRead(Class clazz, MediaType mediaType);

  // 이 컨버터가 해당 클래스와 미디어타입을 쓸(write) 수 있는지 여부를 나타낸다.
  boolean canWrite(Class clazz, MediaType mediaType);

  // 이 컨버터가 지원하는 MediaType 객체 목록을 반환한다.
  List<MediaType> getSupportedMediaTypes();

  // 주어진 입력 메시지에서 해당 타입의 객체를 읽어서 반환한다.
  T read(Class<T> clazz, HttpInputMessage inputMessage) throws IOException,
      HttpMessageNotReadableException;

  // 주어진 출력 메시지에 주어진 객체를 작성한다.
  void write(T t, HttpOutputMessage outputMessage) throws IOException,
      HttpMessageNotWritableException;

}

메인 미디어(mime) 타입에 대한 구현체는 프레임워크가 제공하고 있고 클라이언트측에서는 기본적으로 RestTemplate에 등록되어 있고 서버측에는 AnnotationMethodHandlerAdapter로 등록되어 있다.
HttpMessageConverter의 구현체는 다음 섹션에 나와있다. 모든 컨버터는 기본 미디어 타입을 사용하지만 supportedMediaTypes 빈 프로퍼티를 설정해서 덮어쓸 수 있다.

20.9.2.1 StringHttpMessageConverter

HTTP 요청과 응답에서 문자열을 읽고 쓸 수 있는 HttpMessageConverter 구현체다. 기본적으로 이 컨버터는 모든 텍스트 미디어 타입(text/*)을 지원하고 text/plain의 Content-Type으로 작성한다.

20.9.2.2 FormHttpMessageConverter

HTTP 요청과 응답에서 데이터를 읽고 쓸 수 있는 HttpMessageConverter 구현체다. 기본적으로 이 컨버터는 application/x-www-form-urlencoded 미디어 타입을 읽고 쓴다. 폼(form) 데이터를 읽어서 MultiValueMap<String, String>에 작성한다.

20.9.2.3 ByteArrayMessageConverter

HTTP 요청과 응답에서 바이트 배열을 읽고 쓸 수 있는 HttpMessageConverter 구현체다. 기본적으로 이 컨버터는 모든 미디어 타입(/)을 지원하고 application/octet-stream의 Content-Type으로 작성한다. 이는 supportedMediaTypes 프로퍼티를 설정하고 getContentType(byte[])를 오버라이딩해서 덮어쓸 수 있다.

20.9.2.4 MarshallingHttpMessageConverter

org.springframework.oxm 패키지의 Marshaller와 Unmarshaller 추상화를 사용해서 XML을 읽고 쓸 수 있는 HttpMessageConverter 구현체다. 이 컨버터를 사용하기 전에 Marshaller와 Unmarshaller가 필요하다. Marshaller와 Unmarshaller는 생성자나 빈 프로퍼티로 주입할 수 있다. 기본적으로 이 컨버터는 text/xml와 application/xml를 지원한다.

20.9.2.5 MappingJacksonHttpMessageConverter

Jackson의 ObjectMapper를 사용해서 JSON을 읽고 쓸 수 있는 HttpMessageConverter 구현체다. 필요할 때 Jackson이 제공하는 어노테이션을 사용해서 JSON 매핑을 커스터마이징할 수 있다. 더 세밀한 제어가 필요한 경우 해당 타입에 커스텀 JSON 직렬화/역직렬화(serializers/deserializers)가 필요한 곳에 ObjectMapper 프로퍼티로 커스텀 ObjectMapper를 주입할 수 있다. 기본적으로 이 컨버터는 application/json를 지원한다.

20.9.2.6 SourceHttpMessageConverter

HTTP 요청과 응답에서 javax.xml.transform.Source를 읽고 쓸 수 있는 HttpMessageConverter 구현체다. DOMSource, SAXSource, StreamSource를 지원한다. 기본적으로 이 컨버터는 text/xml과 application/xml를 지원한다.

20.9.2.7 BufferedImageHttpMessageConverter

HTTP 요청과 응답에서 java.awt.image.BufferedImage를 읽고 쓸 수 있는 HttpMessageConverter 구현체다. 이 컨버터는 Java I/O API가 지원하는 미디어타입을 읽고 쓴다.

[JAVA] 20장 스프링을 사용한 원격작업(remoting) 및 웹 서비스 #1..

출처 : Outsider's Dev Story https://blog.outsider.ne.kr/

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.

Part VI. 통합

레퍼런스 문서의 이번 부분에서는 스프링 프레임워크를 다수의 Java EE(혹은 관련된) 기술과 통합하는 내용을 다룬다.
  • Chapter 20, 스프링을 사용한 원격작업(remoting) 및 웹 서비스
  • Chapter 21, 엔터프라이즈 자바빈(EJB) 통합
  • Chapter 22, JMS (Java Message Service)
  • Chapter 23, JMX
  • Chapter 24, JCA CCI
  • Chapter 25, Email
  • Chapter 26, Task Execution and Scheduling
  • Chapter 27, Dynamic language support
  • Chapter 28, Cache Abstraction

20. 스프링을 사용한 원격작업(remoting) 및 웹 서비스

20.1 소개

스프링은 다양한 기술을 사용해서 원격 지원에 대한 통합 클래스를 제공한다. 원격 지원은 일반적인 (스프링) POJO를 구현해서 원격이 가능한 서비스의 배포를 쉽게 한다. 현재 스프링은 다음의 원격 기술을 지원한다.
  • 원격 메서드 호출 (RMI, Remote Method Invocation). RmiProxyFactoryBean와 RmiServiceExporter를 사용하더라도 스프링은 전통전이 두 RMI(java.rmi.Remote인터페이스와 java.rmi.RemoteException)와 RMI 인보커(어떤 자바 인터페이스로도)를 통한 투명한 원격작업을 지원한다.
  • 스프링의 HTTP 인보커. 스프링은 모든 Java 인터페이스를 지원하면서(RMI 인보커처럼) HTTP를 통해 자바 직렬화를 할 수 있는 특수한 원격전략을 제공한다. 이에 대응되는 지원 클래스는 HttpInvokerProxyFactoryBean와 HttpInvokerServiceExporter이다.
  • 해시안(Hessian). 스프링의 HessianProxyFactoryBean와 HessianServiceExporter를 사용해서 Caucho가 제공하는 HTTP기반의 경량 바이너리 프로토콜로 서비스를 투명하게 노출할 수 있다.
  • 버랩(Burlap). 버랩은 Caucho이 XML 기반으로 만든 해시안의 대안이다. 스프링은 BurlapProxyFactoryBean와 BurlapServiceExporter같은 지원 클래스를 제공한다.
  • JAX-RPC. 스프링은 JAX-RPC(J2EE 1.4의 웹 서비스 API)를 사용하는 웹 서비스의 원격 지원을 제공한다.
  • JAX-WS. 스프링은 JAX-WS(JAX-RPC의 후계자로 Java EE 5와 Java 6에서 도입되었다.)를 사용하는 웹 서비스의 원격 지원을 제공한다.
  • JMS. 의존 프로토콜로 JMS를 사용하는 원격은 JmsInvokerServiceExporter와 JmsInvokerProxyFactoryBean 클래스로 지원한다.
스프링의 원격 기능을 설명하면서 다음의 도메인 모델과 서비스를 사용할 것이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Java
public class Account implements Serializable{

  private String name;

  public String getName(){
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

Java
public interface AccountService {
  public void insertAccount(Account account);

  public List<Account> getAccounts(String name);
}

Java
public interface RemoteAccountService extends Remote {
  public void insertAccount(Account account) throws RemoteException;

  public List<Account> getAccounts(String name) throws RemoteException;
}

Java
// 이 구현체는 지금은 아무것도 하지 않는다
public class AccountServiceImpl implements AccountService {
  public void insertAccount(Account acc) {
    // 어떤 작업을 한다...
  }

  public List<Account> getAccounts(String name) {
    // 어떤 작업을 한다...
  }
}

RMI를 사용해서 원격 클라이언트에 서비스를 노출할 것이고 RIM를 사용할 때의 단점을 약간 얘기할 것이다. 그 다음 해시안을 프로토콜로 사용하는 예제를 계속해서 볼 것이다.

20.2 RMI를 사용해서 서비스 노출하기

스프링의 RMI 지원을 사용해서 RMI 인프라를 통해 서비스를 투명하게 노출할 수 있다. 이 설정을 한 후 보안 컨텍스트 전파나 원격 트랜잭션 전파에 대한 지원이 없다는 점을 제외하면 원격 EJB와 유사한 설정을 가진다. RMI 인보커를 사용할 때 추가적인 호출 컨텍스트같은 훅(hook)을 스프링이 제공하므로 여기서 보안 프레임워크나 커스텀 보안 자격(credentials)등을 연결할 수 있다.

20.2.1 RmiServiceExporter를 사용해서 서비스 내보내기(export)

RmiServiceExporter를 사용해서 RMI 객체로 AccountService 객체의 인터페이스를 노출할 수 있다. 이 인터페이스는 RmiProxyFactoryBean으로 접근할 수 있고 전통적인 RMI 서비스인 경우에는 평범한 RMI로 접근할 수 있다. RmiServiceExporter는 RMI 인보커로 RMI가 아닌 서비스의 노출을 명시적으로 지원한다.
물론 서비스를 스프링 컨테이너에 먼저 설정해야 한다.

1
2
3
4
Xml
<bean id="accountService" class="example.AccountServiceImpl">
  <!-- DAO  추가적인 프로퍼티 -->
</bean>

그 다음 RmiServiceExporter로 서비스를 노출해야 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Xml

<bean class="org.springframework.remoting.rmi.RmiServiceExporter">
  <!-- 내보낼 빈과 같은 이름이어야  필요는 없다 -->
  <property name="serviceName" value="AccountService"/>
  <property name="service" ref="accountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
  <!-- 기본값은 1099 -->
  <property name="registryPort" value="1199"/>
</bean>

여기서 볼 수 있듯이 RMI 등록(registry) 포트를 오버라이딩한다. 때로는 어플리케이션 서버도 RMI 등록을 유지하면서 RMI 등록에 간섭하지 않는다는 것을 알고 있다. 게다가 서비스명은 서비스 아래에 바인딩된다. 그러므로 서비스는 'rmi://HOST:1199/AccountService'에 바인딩 될 것이다. 나중에 클라이언트 측 서비스에서 링크에 URL을 사용할 것이다.


Note
servicePort 프로퍼티를 생략했다.(기본값은 0이다) 이는 서비스와의 통신에 익명 포트를 사용한다는 의미이다.


20.2.2 클라이언트 서비스에서의 연결(link)

클라이언트는 account를 관리하는 AccountService를 사용하는 간단한 객체이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

public class SimpleObject {
  private AccountService accountService;

  public void setAccountService(AccountService accountService) {
    this.accountService = accountService;
  }

  // accountService를 사용하는 추가적인 메서드
}

클라이언트의 서비스에서 연결하기 위해 간단한 객체와 약간의 서비스 연결 설정을 가진 분리된 스프링 컨테이너를 생성할 것이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Xml

<bean class="example.SimpleObject">
  <property name="accountService" ref="accountService"/>
</bean>

<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean">
  <property name="serviceUrl" value="rmi://HOST:1199/AccountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>

이것이 클라이언트에서 원격 account 서비스를 지원하는데 필요한 전부이다. 스프링은 투명하게 인보커를 생성하고 RmiServiceExporter로 원격으로 account 서비스를 활성화할 것이다. 클라이언트에서 RmiProxyFactoryBean를 사용해서 이를 연결한다.

20.3 HTTP를 통한 서비스 원격 호출에 해시안과 버랩 사용하기

해시안은 바이너리 HTTP 기반의 원격 프로토콜을 제공한다. 이 프로토콜은 Caucho이 개발했고 해시안의 자세한 내용은 http://www.caucho.com에서 볼 수 있다.

20.3.1 해시안등에 DispatcherServlet 연결하기

해시안은 HTTP로 통신하고 커스텀 서블릿을 사용한다. 스프링 웹 MVC에서 사용하듯이 스프링의 DispatcherServlet 원리를 사용해서 서비스를 노출하는 서비스등을 쉽게 연결할 수 있다. 우선 어플리케이션에 새로운 서블릿을 생성해야 한다.(이는 'web.xml'에서 가져온다.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Xml

<bean id="accountService" class="example.AccountServiceImpl">
  
</bean>

<bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter">
  <property name="service" ref="accountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>

이제 클라이언트에서 서비스를 연결할 준비가 됐다. 서비스에 요청 URL을 매핑하는 핸들러 매핑을 명시적으로 지정하지 않았으므로 BeanNameUrlHandlerMapping을 사용할 것이다. 그러므로 DispatcherServlet의 매핑(위에서 정의했다)의 빈 이름을 나타내는 URL인 'http://HOST:8080/remoting/AccountService'로 서비스를 노출할 것이다.
아니면 루트 어플리케이션 컨텍스트(예를 들면 'WEB-INF/applicationContext.xml')에 HessianServiceExporter를 생성해라.

1
2
3
4
5
6
Xml

<bean name="accountExporter" class="org.springframework.remoting.caucho.HessianServiceExporter">
  <property name="service" ref="accountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>

후자의 경우 동일한 결과가 나오도록 해당 익스포터에 대응되는 서블릿을 'web.xml'에 정의해라. 익스포터는 요청경로 /remoting/AccountService에 매핑된다. 이 서블릿 이름은 대상 익스포터의 빈 이름과 일치해야 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Xml

<servlet>
  <servlet-name>accountExporter</servlet-name>
  <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>accountExporter</servlet-name>
  <url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>


20.3.3 클라이언트측 서비스에서의 연결

HessianProxyFactoryBean를 사용해서 클라이언트측 서비스에서 연결할 수 있다. RMI 예제와 같은 원리를 적용한다. 분리된 빈 팩토리나 어플리케이션을 생성하고 account를 관리하려고 AccountService를 사용하는 SimpleObject인 다음 빈을 정의해라.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Xml

<bean class="example.SimpleObject">
  <property name="accountService" ref="accountService"/>
</bean>

<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean">
  <property name="serviceUrl" value="http://remotehost:8080/remoting/AccountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>


20.3.4 버랩 사용

앞에서 설명한 해시안계열과 정확히 동일하게 설정하고 구성하므로 여기서 버랩(해시안과 동일하지만 XML 기반이다)에 대해서 자세히 얘기하지 않을 것이다. Hessian을 Burlap으로 바꾸면 설정이 완료된다.

20.3.5 해시안이나 버랩으로 노출된 서비스에 HTTP 기본 인증(HTTP basic authentication) 적용하기

해시안과 버랩의 장점 중 하나는 두 프로토콜이 모두 HTTP 기반이므로 HTTP 기본 인증을 쉽게 적용할 수 있다는 점이다. 예를 들어 web.xml 보안 기능으로 일반적인 HTTP 서버 보안 메카니즘을 쉽게 적용할 수 있다. 보통은 여기서 사용자마다 보안 인증서를 사용하기 보다는 Hessian/BurlapProxyFactoryBean 수준(JDBC DataSource와 유사하다)에서 정의되고 공유된 인증서를 사용할 것이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Xml

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
  <property name="interceptors" ref="authorizationInterceptor"/>
</bean>

<bean id="authorizationInterceptor"
    class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
  <property name="authorizedRoles" value="administrator,operator"/>
</bean>

이는 BeanNameUrlHandlerMapping를 명시적으로 언급하고(mention) 이 어플리케이션 컨텍스트에서 언급된 빈을 운영자와 관리자만 호출하도록 하는 인터셉터를 설정하는 예제이다.


Note
물론 이 예제는 유연한 인증 인프라을 보여주지는 않는다. 인증과 관련된 자세한 옵션은 의 스프링 시큐리티 프로젝트를 봐라.


20.4 HTTP 인보커를 사용해서 서비스 노출하기

자신만의 가벼운 직렬화 메카니즘을 사용하는 경량 프로토콜인 버랩이나 해시안과는 반대로 스프링 HTTP 인보커는 HTTP로 서비스를 노출할 때 표준 자바 직렬화 메카니즘을 사용한다. 인자나 반환 타입이 해시안이나 버랩이 사용하는 직렬화 메카니즘을 사용해서 직렬화할 수 없는 복잡한 타입이라면 이는 엄청난 장점이다.(원격 기술을 선택할 때의 고려사항은 다음 섹션에서 설명한다.)
내부적으로 스프링은 HTTP 호출에 J2SE가 제공하는 표준 기술이나 Commons HttpClient를 사용한다. 더 고급이고 사용하기 쉬운 기능이 필요하다면 후자를 사용해라. 자세한 내용은 jakarta.apache.org/commons/httpclient를 참고해라.

20.4.1 서비스 객체 노출하기

서비스 객체에 대한 HTTP 인보커 인프라를 설정하는 방법은 해시안이나 버랩으로 할 때와 아주 유사하다. 해시안 지원이 HessianServiceExporter를 제공하듯이 스프링의 HttpInvoker 지원은 org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter를 제공한다.
스프링 웹 MVC DispatcherServlet에서 AccountService(위에서 나왔던)를 노출하려면 디스패처의 어플리케이션 컨텍스트에 다음 설정을 해야 한다.

1
2
3
4
5
6
Xml

<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
  <property name="service" ref="accountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>

해시안부분에서 설명했듯이 이러한 익스포터(exporter) 정의를 DispatcherServlet의 표준 매핑 기능으로 노출할 것이다.
아니면 루트 어플리케이션 컨텍스트(예를 들면 'WEB-INF/applicationContext.xml'파일에)에 HttpInvokerServiceExporter를 생성해라.

1
2
3
4
5
6
Xml

<bean name="accountExporter" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
  <property name="service" ref="accountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>

추가적으로 'web.xml'의 이 익스포터에 대한 서블릿(대상 익스포터의 빈 이름과 일치하는 서블릿 이름으로)을 정의해라.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Xml

<servlet>
  <servlet-name>accountExporter</servlet-name>
  <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>accountExporter</servlet-name>
  <url-pattern>/remoting/AccountService</url-pattern>
</servlet-mapping>

서블릿 컨테이너 밖에서 돌아가고 Sun의 Java 6를 사용한다면 내장 HTTP 서버 구현체를 사용할 수 있다. 이 예제에서 보듯이 SimpleHttpServerFactoryBean를 SimpleHttpInvokerServiceExporter와 함게 설정할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Xml

<bean name="accountExporter"
    class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter">
  <property name="service" ref="accountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>

<bean id="httpServer"
    class="org.springframework.remoting.support.SimpleHttpServerFactoryBean">
  <property name="contexts">
    <util:map>
      <entry key="/remoting/AccountService" value-ref="accountExporter"/>
    </util:map>
  </property>
  <property name="port" value="8080" />
</bean>


20.4.2 클라이언트에서의 서비스 연결

클라이언트에서 서비스를 연결하는 방법은 헤시안이나 버랩에서 사용한 방법과 아주 유사하다. 스프링이 프록시를 사용해서 호출을 익스포트된 서비스를 가리키는 URL에 대한 HTTP POST 요청으로 변환할 수 있다.

1
2
3
4
5
6
Xml

<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
  <property name="serviceUrl" value="http://remotehost:8080/remoting/AccountService"/>
  <property name="serviceInterface" value="example.AccountService"/>
</bean>

앞에서 얘기했듯이 사용하고자 하는 HTTP 클라이언트를 선택할 수 있다. 기본적으로 HttpInvokerProxy는 J2SE HTTP 기능을 사용하지만 httpInvokerRequestExecutor 프로퍼티를 설정해서 Commons HttpClient를 사용할 수도 있다.

1
2
3
4
5
Xml

<property name="httpInvokerRequestExecutor">
  <bean class="org.springframework.remoting.httpinvoker.CommonsHttpInvokerRequestExecutor"/>
</property>


20.5 웹 서비스

스프링은 표준 자바 웹서비스 API를 완전히 지원한다.
  • JAX-RPC를 사용한 웹서비스 노출
  • JAX-RPC를 사용한 웹서비스 접근
  • JAX-WS를 사용한 웹서비스 노출
  • JAX-WS를 사용한 웹서비스 접근
Note
왜 두 개의 표준 자바 웹서비스 API가 있는가?

JAX-RPC 1.1는 J2EE 1.4의 표준 웹서비스 API이다. 이름에서 알 수 있듯이 수년전에 이미 인기가 없어진 RPC 바인딩에 집중하고 있다. 그 결과 바인딩은 더 유연하지만 상당히 어노테이션 기반인 Java EE 5의 JAX-WS 2.0가 그 자리를 대체했다. JAX-WS 2.1도 Java 6에 포함되었고(구체적으로 말하면 Sun의 JDK 1.6.0_04에 포함되었고 Sun JDK 1.6.0에는 JAX-WS 2.0가 포함되었다.) JDK의 내장 HTTP 서버와 통합되었다.

스프링은 두 표준 자바 웹서비스 API에서 모두 동작한다. Java EE 5 / Java 6에서는 JAX-WS가 좋다. Java 5에서 동작하는 J2EE 1.4 환경에서는 JAX-WS 프로바이터를 연결할 수 있다. Java EE 서버 문서를 확인해 봐라.

스프링 코어의 JAX-RPC와 JAX-WS에 대한 지원에 추가적으로 스프링 포트폴리오에 계약 우선(contract-first)이면서 문서주도의 웹서비스 솔루션인 스프링 웹 서비스 기능도 있다. 현재적이고 경쟁력있는 웹서비스를 만든다면 스프링 웹 비스를 강력히 추천한다.

20.5.1 JAX-RPC를 사용해서 서블릿 기반의 웹 서비스 노출

스프링은 JAX-RPC 서블릿 엔드포인트(endpoint) 구현체(ServletEndpointSupport)를 위한 편리한 기반 클래스를 제공한다. 예제의 AccountService를 노출하려면 스프링의 ServletEndpointSupport 클래스를 확장하고 여기에 비즈니스 로직을 구현하고 비즈니스 계층에 호출을 위임하는 것이 일반적이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Java

/**
 * JAX-RPC를 따르는 RemoteAccountService 구현체로
 * 루트 웹 어플리케이션 컨텍스트에서 AccountService 구현체에 위임한다.
 *
 * JAX-RPC가 전용 엔드포인크 클래스와 동작해야 하므로 이 랩퍼 클래스가 필요하다.
 * 기존에 존재하는 서비스를 익스포트해야 하는 경우 어플리케이션 컨텍스트 접근을 위해
 * ServletEndpointSupport를 확장한 래퍼가 JAX-RPC와 호환되는 가장 간단한 방법이다.
 *
 * 이는 서버측 JAX-RPC 구현체로 등록된 클래스다. Axis에서 이는 배포호출(deployment calls)로
 * 각각 "server-config.wsdd"에서 이뤄진다. 웹서비스 엔진이 이 클래스의 인스턴스 생명주기를
 * 관리한다. 스프링 어플리케이션가 접근할 수 있다.
 */
 import org.springframework.remoting.jaxrpc.ServletEndpointSupport;

public class AccountServiceEndpoint extends ServletEndpointSupport implements RemoteAccountService {

  private AccountService biz;

  protected void onInit() {
    this.biz = (AccountService) getWebApplicationContext().getBean("accountService");
  }

  public void insertAccount(Account acc) throws RemoteException {
    biz.insertAccount(acc);
  }

  public Account[] getAccounts(String name) throws RemoteException {
    return biz.getAccounts(name);
  }
}

스프링 기능에 접근하게 하려면 AccountServletEndpoint가 스프링 컨텍스트와 같은 웹 어플리케이션에서 동작해야 한다. Axis의 경우에는 'web.xml'에 AxisServlet 정의를 복사하고 'server-config.wsdd'에 엔드포인트를 설정해라.(또는 배포도구를 사용해라.) Axis를 사용해서 OrderService를 웹 서비스로 노출한 JPetStore 예제 어플리케이션을 참고해라.

20.5.2 JAX-RPC를 사용해서 웹 서비스에 접근하기

스프링은 JAX-RPC 웹서비스 프록시를 생성하는 두 팩토리 빈 LocalJaxRpcServiceFactoryBean와 JaxRpcPortProxyFactoryBean를 제공한다. LocalJaxRpcServiceFactoryBean는 사용할 JAX-RPC 서비스만 반환할 수 있다. JaxRpcPortProxyFactoryBean는 비즈니스 서비스 인터페이스를 구현한 프록시를 반환할 수 있는 완전히 독립적인 버전이다. 이 예제에서는 이전 섹션에서 노출한 AccountService 엔드포인트에 대한 프록시를 생성하려고 JaxRpcPortProxyFactoryBean를 사용한다. 약간의 코딩을 필요하지만 스프링이 웹서비스를 아주 잘 지원하는 것을 볼 수 있을 것이다. 대부분의 설정은 다른 때와 동일하게 스프링 설정파일에서 이뤄진다.

1
2
3
4
5
6
7
8
9
Xml

<bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
  <property name="serviceInterface" value="example.RemoteAccountService"/>
  <property name="wsdlDocumentUrl" value="http://localhost:8080/account/services/accountService?WSDL"/>
  <property name="namespaceUri" value="http://localhost:8080/account/services/accountService"/>
  <property name="serviceName" value="AccountService"/>
  <property name="portName" value="AccountPort"/>
</bean>

serviceInterface가 클라이언트가 사용할 원격 비즈니스 인터페이스이다. wsdlDocumentUrl는 WSDL 파일의 URL이다. 스프링이 JAX-RPC 서비스를 생성하려면 구동 시 wsdlDocumentUrl가 필요하다. namespaceUri는 .wsdl 파일의 targetNamespace와 대응된다. serviceName은 .wsdl 파일의 서비스명과 대응된다. portName는 .wsdl 파일의 포트명과 대응된다.
웹서비스에 대한 빈 팩토리가 있는 것처럼 이제 웹 서비스 접근은 아주 쉽고 이는 RemoteAccountService 인터페이스로 노출될 것이다. 스프링에서 이를 연결할 수 있다.

1
2
3
4
5
6
Xml

<bean id="client" class="example.AccountClientImpl">
  ...
  <property name="service" ref="accountWebService"/>
</bean>

클라이언트 코드에서 일반적인 클래스인 것처럼 웹서비스에 접근할 수 있다. 다른 점은 RemoteException를 던진다는 것 뿐이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Java

public class AccountClientImpl {

  private RemoteAccountService service;

  public void setService(RemoteAccountService service) {
    this.service = service;
  }

  public void foo() {
    try {
      service.insertAccount(...);
    }
    catch (RemoteException ex) {
      // ouch
    }
  }
}

스프링이 체크드 RemoteException을 대응되는 언체크드 RemoteException로 자동변환해주므로 체크드 RemoteException을 제거할 수 있다. 이를 위해서는 RMI가 아닌 인터페이스도 제공해야 한다. 이제 설정은 다음과 같다.

1
2
3
4
5
6
7
Xml

<bean id="accountWebService" class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
  <property name="serviceInterface" value="example.AccountService"/>
  <property name="portInterface" value="example.RemoteAccountService"/>
  ...
</bean>

serviceInterface는 RMI가 아닌 인터페이스로 바뀐다. RMI 인터페이스는 이제 portInterface 프로퍼티를 사용해서 정의한다. 클라이언트 코드는 이제 java.rmi.RemoteException 처리를 안해도 된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java

public class AccountClientImpl {

  private AccountService service;

  public void setService(AccountService service) {
    this.service = service;
  }

  public void foo() {
    service.insertAccount(...);
  }
}

"portInterface" 부분을 버리고 평범한 비즈니스 인터페이스를 "serviceInterface"로 지정할 수도 있다. 이 경우 자동적으로 JaxRpcPortProxyFactoryBean는 고정된 포트 스텁(stub)없이도 동적 호출을 수행하는 JAX-RPC "동적 호출 인터페이스(Dynamic Invocation Interface)"로 바뀔 것이다. RMI호환의 자바 포트 인터페이스가(예를 들면 대상 웹서비스가 자바가 아닌 경우) 필요없다는 점이 장점이다. 비즈니스 인터페이스만 맞추면 된다. 런타임의 동작은 JaxRpcPortProxyFactoryBean의 javadoc을 참고해라.

20.5.3 JAX-RPC 빈(bean) 매핑 등록하기

Account처럼 복잡한 객체를 네트워크간에 변환하려면 클라이언트측에서 빈 매핑을 등록해야 한다.


Note
Axis를 사용하는 서버측의 빈 매핑 등록은 'server-config.wsdd' 파일에서 보통 이뤄진다.

클라이언트측에서 빈 매핑을 등록할 때 Axis를 사용할 것이므로 프로그래밍적으로 빈 매핑을 등록해야 한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Java

public class AxisPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean {

  protected void postProcessJaxRpcService(Service service) {
    TypeMappingRegistry registry = service.getTypeMappingRegistry();
    TypeMapping mapping = registry.createTypeMapping();
    registerBeanMapping(mapping, Account.class, "Account");
    registry.register("http://schemas.xmlsoap.org/soap/encoding/", mapping);
  }

  protected void registerBeanMapping(TypeMapping mapping, Class type, String name) {
    QName qName = new QName("http://localhost:8080/account/services/accountService", name);
    mapping.register(type, qName,
      new BeanSerializerFactory(type, qName),
      new BeanDeserializerFactory(type, qName));
  }
}


20.5.4 자신만의 JAX-RPC 핸들러 등록하기

이번 섹션에서는 SOAP 메시지를 보내기 전에 커스텀 코드를 실행할 수 있는 웹 서비스 프록시에 자신만의 javax.rpc.xml.handler.Handler를 등록할 것이다. Handler는 콜백 인터페이스다. jaxrpc.jar에서 편리한 기반 클래스를 제공하므로 javax.rpc.xml.handler.GenericHandler를 확장할 것이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java

public class AccountHandler extends GenericHandler {

  public QName[] getHeaders() {
    return null;
  }

  public boolean handleRequest(MessageContext context) {
    SOAPMessageContext smc = (SOAPMessageContext) context;
    SOAPMessage msg = smc.getMessage();
    try {
      SOAPEnvelope envelope = msg.getSOAPPart().getEnvelope();
      SOAPHeader header = envelope.getHeader();
      ...
    }
    catch (SOAPException ex) {
      throw new JAXRPCException(ex);
    }
    return true;
  }
}

이제 AccountHandler를 JAX-RPC 서비스에 등록하면 메시지를 보내기 전에 handleRequest(..)를 호출할 것이다. 스프링이 이러한 핸들러 등록에 선언적인 지원을 하지 않으므로 프로그래밍적인 접근을 취해야 한다. 하지만 스프링에서는 이 목적으로 만들어진 postProcessJaxRpcService(..) 메서드를 오버라이스할 수 있으므로 이 작업을 아주 쉽게 할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java

public class AccountHandlerJaxRpcPortProxyFactoryBean extends JaxRpcPortProxyFactoryBean {

  protected void postProcessJaxRpcService(Service service) {
    QName port = new QName(this.getNamespaceUri(), this.getPortName());
    List list = service.getHandlerRegistry().getHandlerChain(port);
    list.add(new HandlerInfo(AccountHandler.class, null, null));
    logger.info("Registered JAX-RPC AccountHandler on port " + port);
  }
}

마지막으로 팩토리빈을 사용하도록 스프링 설명을 변경하는 것을 잊지 말아야 한다.

1
2
3
4
5
Xml

<bean id="accountWebService" class="example.AccountHandlerJaxRpcPortProxyFactoryBean">
  ...
</bean>


20.5.5 JAX-WS를 사용해서 서블릿기반의 웹서비스 노출하기

스프링은 JAX-WS 서블릿 앤드포인트 구현체에 대한 편리한 기반 클래스 SpringBeanAutowiringSupport를 제공한다. AccountService를 노출하려고 스프링의 SpringBeanAutowiringSupport를 확장해서 비스니스 로직을 작성한다.(보통은 비즈니스 계층에 호출을 위임한다.) 스프링이 관리하는 빈의 의존성 등을 나타내기 위해서 간단히 스프링 2.5의 @Autowired 어노테이션을 사용할 것이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Java

/**
 * JAX-WS 호환 AccountService 구현체로 루트 웹 어플리케이션 컨텍스트에서
 * AccountService 구현체에 위임한다.
 *
 * JAX-WS가 전용 엔드포인트 클래스와 동작해야 하므로 이 랩퍼 클래스가 필요하다.
 * 기존에 존배하는 서비스를 익스포트해야 하면 스프링 빈 자동연결(autowiring)을 위해서
 * SpringBeanAutowiringSupport를 확장한 래퍼가 JAX-WS와 호환이 되는 가장 간단한 방법이다.
 *
 * 서버측 JAX-WS 구현체와 등록된 클래스다. Java EE 5 서버의 경우 이 클래스는 web.xml에
 * 서블릿으로 정의될 것이고 서버는 이 클래스가 JAX-WS 엔드포인트라는 것을 탐지하고 적절하게 반응할
 * 것이다. 보통 서블릿 이름이 지정한 WS 서비스명과 일치해야 한다.
 *
 * 웹 서비스 엔진은 이 클래스 인스턴스의 생명주기를 관리한다.
 * 스프링 빈 참조가 여기서 연결될 것이다.
 */
import org.springframework.web.context.support.SpringBeanAutowiringSupport;

@WebService(serviceName="AccountService")
public class AccountServiceEndpoint extends SpringBeanAutowiringSupport {

  @Autowired
  private AccountService biz;

  @WebMethod
  public void insertAccount(Account acc) {
    biz.insertAccount(acc);
  }

  @WebMethod
  public Account[] getAccounts(String name) {
    return biz.getAccounts(name);
  }
}

스프링의 기능에 접근할 수 있도록 스프링 컨텍스트와 같은 웹 어플리케이션에서 AccountServletEndpoint를 실행해야 한다. JAX-WS 서블릿 엔드포인트 배포의 표준 계약을 사용하는 Java EE 5 환경에서는 이것이 기본값이다. 자세한 내용은 Java EE 5 웹서비스 튜토리얼을 봐라.

20.5.6 JAX-WS를 사용해서 독립(standalone) 웹 서비스 익스포트하기

Sun JDK 1.6의 내장 JAX-WS 프로바이더는 JDK 1.6에 포함된 내장 HTTP 서버를 사용해서 웹서비스를 노출한다. 스프링의 SimpleJaxWsServiceExporter는 스프링 어플리케이션 컨텍스트에서 @WebService 어노테이션이 붙은 모든 빈을 찾아내서 기본 JAX-WS 서버(JDK 1.6 HTTP 서버)로 익스포트한다.
이 시나리오에서 엔드포인트 인스턴스는 스프링 빈으로 정의하고 관리된다. 엔드포인트 인스턴스는 JAX-WS 엔진과 함께 등록될 것이지만 라이프사이클은 스프링 어플리케이션 컨텍스트에 달려있다. 즉, 명시적인 의존성 주입같은 스프링의 기능이 엔드포인트 인스턴스에 적용될 것이다. 물론 @Autowired를 사용한 어노테이션기반 주입도 잘 동작할 것이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Xml

<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter">
  <property name="baseAddress" value="http://localhost:8080/"/>
</bean>

<bean id="accountServiceEndpoint" class="example.AccountServiceEndpoint">
  ...
</bean>
...

AccountServiceEndpoint는 스프링의 SpringBeanAutowiringSupport에서 파생되지만 엔드포인트가 스프링이 완전히 관리하는 빈이므로 꼭 그럴 필요는 없다. 이는 엔드포인트 구현체가 슈퍼클래스를 정의하지 않고 다음과 같이 작성될 것이고 스프링의 @Autowired 설정 어노테이션을 사용할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Java

@WebService(serviceName="AccountService")
public class AccountServiceEndpoint {

  @Autowired
  private AccountService biz;

  @WebMethod
  public void insertAccount(Account acc) {
    biz.insertAccount(acc);
  }

  @WebMethod
  public List<Account> getAccounts(String name) {
    return biz.getAccounts(name);
  }
}


20.5.7 JAX-WS RI의 스프링 지원을 사용한 웹서비스 익스포트

GlassFish 프로젝트의 일부인 Sun의 JAX-WS RI는 JAX-WS Commons 프로젝트의 일부로 스프링을 지원한다. 이는 앞 섹션에서 설명한 독립(standalone) 모드와 유사하게(지금은 서블릿 환경이다.) 스프링이 관리하는 빈으로 JAX-WS 엔드포인트를 정의할 수 있다. 이는 Java EE 5환경에서는 사용할 주 없다. 웹어플리케이션의 일부로 JAX-WS RI를 임베딩한 톰캣같은 EE가 아닌 환경을 위한 것이다.
서블릿에 기반한 엔트포인트 익스포트의 표준 방식과 다른 점은 엔드포인트 인스턴스의 생명주기는 스프링이 관리할 것이고 web.xml에 딱 하나의 JAX-WS 서블릿이 정의되어 있을 것이다. 표준 Java EE 5 방식으로는(위에서 설명한) 서비스 엔드포인트마다 하나의 서블릿 정의를 가질 것이고 각 엔드포인트는 보통 스프링 빈(위에서 처럼 @Autowired를 사용해서)에 위임할 것이다.
자세한 설정과 사용방법은 https://jax-ws-commons.dev.java.net/spring/를 참고해라.

20.5.8 JAX-WS로 웹서비스 접근하기

JAX-RPC 지원과 비슷하게 스프링은 JAX-WS 웹서비스 프록시를 생성하는 두 팩토리 빈 LocalJaxWsServiceFactoryBean와 JaxWsPortProxyFactoryBean를 제공한다. LocalJaxWsServiceFactoryBean는 사용할 JAX-WS 서비스 클래스만을 반환할 수 있고 JaxWsPortProxyFactoryBean는 완전히 독립적인 버전으로(full-fledged version) 비즈니스 서비스 인터페이스를 구현한 프록시를 반환할 수 있다. 이 예제에서 AccountService 엔드포인트에 대한 프록시를 생성하려고 JaxWsPortProxyFactoryBean를 사용한다.

1
2
3
4
5
6
7
8
9
Xml

<bean id="accountWebService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean">
  <property name="serviceInterface" value="example.AccountService"/>
  <property name="wsdlDocumentUrl" value="http://localhost:8888/AccountServiceEndpoint?WSDL"/>
  <property name="namespaceUri" value="http://example/"/>
  <property name="serviceName" value="AccountService"/>
  <property name="portName" value="AccountServiceEndpointPort"/>
</bean>

serviceInterface가 클라이언트가 사용할 비즈니스 인터페이스다. wsdlDocumentUrl는 WSDL 파일의 URL이다. 스프링이 JAX-WS 서비스를 생성도록 구동시에 이 파일이 필요하다. namespaceUri는 .wsdl 파일의 targetNamespace과 대응되고 serviceName는 .wsdl 파일의 서비스명과 대응되고 portName는 .wsdl파일의 포트명과 대응된다.
AccountService 인터페이스로 노출할 빈 팩토리를 가지고 있으므로 이제 웹서비스 접근이 아주 쉽다. 스프링에서 이를 연결할 수 있다.

1
2
3
4
5
6
Xml

<bean id="client" class="example.AccountClientImpl">
  ...
  <property name="service" ref="accountWebService"/>
</bean>

클라이언트 코드에서는 일반적인 클래스인 것처럼 웹 서비스에 접근할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java

public class AccountClientImpl {

  private AccountService service;

  public void setService(AccountService service) {
    this.service = service;
  }

  public void foo() {
    service.insertAccount(...);
  }
}

NOTE: JAX-WS에서는 엔드포인트 인터페이스와 구현 클래스에 @WebService, @SOAPBinding 등의 어노테이션이 붙어야 하므로 앞에서는 약간 간소화되었다. 즉, JAX-WS 엔드포인트 아티팩트(artifacts)로 평번한 자바 인터페이스와 구현클래스를 (쉽게)사용할 수 없다. 그러므로 우선 이러한 클래스에 적절하게 어노테이션을 붙혀야 한다. 요구사항에 대한 자세한 내용은 JAX-WS 문서를 확인해 봐라.