Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

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

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

20.6 JMS

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

다음 인터페이스는 서버와 클라이언트가 모두 사용한다.

package com.foo;

public interface CheckingAccountService {

  public void cancelAccount(Long accountId);
}

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

package com.foo;

public class SimpleCheckingAccountService implements CheckingAccountService {

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

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


<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를 사용해서 서비스 객체를 노출해야 한다.


<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>
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로 서버측 객체 호출에 대한 포워딩은 프록시가 담단한다.


<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>
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 작업에 이 접근은 다음에서 보듯이 너무 저수순이다.

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 메서드
DELETE delete
GET getForObject
  getForEntity
HEAD headForHeaders(String url, String… urlVariables)
OPTIONS optionsForAllow(String url, String… urlVariables)
POST postForLocation(String url, Object request, String… urlVariables)
  postForObject(String url, Object request, Class responseType, String… uriVariables)
PUT put(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 템플릿 인자를 받는다. 예를 들면

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

가변 인자를 사용하거나

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을 사용해서 다음과 같이 다시 작성한다.

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

RestTemplate template = new RestTemplate();

Booking booking = // 예약 객체 생성

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

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

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

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

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

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 문자열이 인코딩되지 않아서 인코딩해야 한다고 가정한다. 예를 들면 다음과 같다.

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 문자열로 시작할 수 있다.

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

URI uri = uriComponents.toUri();

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

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() 메서드를 사용할 수 있다는 것이다. 예를 들면 다음과 같다.

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 인터페이스의 기능은 다음 소스를 보면 알 수 있을 것이다.

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가 지원하는 미디어타입을 읽고 쓴다.

2013/07/05 02:50 2013/07/05 02:50