(스프링) 15장 원격 서비스 이용하기

1. 스프링 리모팅 개요


리모팅(Remoting)은 클라이언트 애플리케이션과 서비스 간의 대화다. 클라이언트 측에서 애플리케이션의 범위에 없는 어떤 기능이 필요해지면 클라이언트 애플리케이션이 그 기능을 제공할 수 있는 다른 시스템에 접촉을 한다. 원격 애플리케이션은 그 기능을 원격 서비스를 통해 노출시킨다.

다른 애플리케이션과의 통신은 클라이언트 애플리케이션에서 호출하는 원격 프로시저 호출(RPC, Remote Procedure Call)로 시작된다. 표면적으로 RPC는 로컬 객체에 있는 메소드 호출과 유사하다. 즉, 원격호출과 로컬호출 둘 다 호출되는 프로시저가 완료될 때까지 호출코드에서 실행을 블록하는 동기식 작업이다.

<RPC와 기타원격 기술 비교>

  • 원격 메소드 호출(RMI) : 방화벽과 같은 네트워크 제약이 고려 대상이 아닐 경우, 자바 기반 서비스에 접속하거나 노출시킬 때
  • Hessian 또는 Burlap : 네트워크 제약이 고려 대상일 경우, HTTP를 거쳐 자바 기반 서비스에 접속하거나 노출시킬 때, Hessian은 바이너리 프로토콜이고, Burlap은 XML기반이다.
  • HTTP호출자 : 네트워크 제약이 고려대상이고 XML이나 독자적인 직렬화를 통한 자바 직렬화를 원하는 경우, 스프링기반 서비스에 접속하거나 서비스를 노출 시킬 때
  • JAX-RPC와 JAX-WS : 플랫폼 중립적인 SOAP기반 웹 서비스에 접속하거나 서비스를 노출 시킬 때

모든 모델에서 서비스는 애플리케이션에 스프링 관리 빈으로 구성될 수 있는데, 이것은 프록시 팩토리 빈을 이용해 원격 서비스를 마치 로컬 객체들인 것처럼 다른 빈의 프로퍼티에 연결할 수 있기 때문에 가능한 일이다. 



2. RMI활용


스프링은 RMI서비스를 로컬 자바빈즈(JavaBeans)인 것처럼 스프링 애플리케이션에 연결할 수 있게 해주는 프록시 팩토리 빈을 제공함으로써 RMI모델을 단순화한다.
스프링은 또한 스프링관리 빈을 RMI서비스로 전환하는 작업을 시간적으로 단축시키는 원격 익스포터를 제공한다.

스프링에서 RMI서비스 구성하기

스프링은 RMI 서비스를 발행하는 좀 더 쉬운 방법을 제공한다. 스프링을 이용하면 RemoteException을 던지는 메소드를 갖는 RMI종속적인 클래스를 작성하는 대신, 제공할 서비스의 기능을 수행하는 POJO를 작성하기만 하면 된다. 나머지는 스프링이 처리한다.


<SpitterService는 Spitter 애플리케이션의 서비스 계층을 정의한다.>

public interface SpitterService{
    List<Spittle> getRecentSpittles(int count);
    void saveSpittle(Spittle spittle);
    void saveSpitter(Spitter spitter);
   Spitter getSpitter(long id);
    void startFollowing(Spitter follower, Spitter followee);
    ...
}

만일 전통적인 RMI를 사용하여 이 서비스를 노출했다면, SpitterService와 SpiterServiceImpl에 있는 모든 메소드는 java.rmi.RemoteException을 던져야 한다.  
하지만 스프링의 RmiServiceExporter를 사용하여 RMI서비스로 변환 시켰으므로 기존 구현 내용은 잘 동작한다. 


 RmiServiceExporter는 모든 스프링 관리 빈을 RMI서비스로 익스포트한다.

다음 그림에서 볼 수 있듯이 RmiServiceExporter는 빈을 어댑터 클래스 안에 래핑하는 방식으로 작동한다.
그 다음, 이 어댑터 클래스는 RMI레지트리에 바인딩되어 서비스클래스(여기서는 SpitterServiceImpl)에 대한 요청을 프록시한다.

RmiServiceExporter를 이용해 SpitterServiceImpl을 RMI 서비스로 노출시키는 가장 간단한 방법은 @Bean으로 스프링에 설정하는 것이다.

@Bean
public RmiServiceExporter rmiExporter(SpitterService spitterService){
   RmiServiceExporter rmiExporter=new RmiServiceExporter();
   rmiExporter.setService(spitterService);
   rmiExporter.setServiceName("SpitterService");
   rmiExporter.setServiceInterface(SpitterService.class);
   return rmiExporter;
}

--> 여기에서 spitterService 빈은 service 프로퍼티에 할당되어 있는데, 이는 RmiServiceExporter가 이 빈을 RMI서비스로 익스포트한다는 것을 나타낸다. serviceName프로퍼티는 RMI서비스를 명명하고 serviceInterface프로퍼티는 그 서비스가 구현하는 인터페이스를 지정한다.

기본적으로 RmiServiceExporter는 로컬 머신의 1099번 포트에 RMI레지스트리 바인드를 시도한다. 그리고 해당 포트에서 RMI레지스트리가 발견되지 않으면 RmiServiceExporter가 RMI레지스트리 하나를 시작한다. RMI레지스트리를 다른 포트나 호스트에 있는 RMI레지스트리에 바인드하려면, registryPort프로퍼티와 registryHost프로퍼티를 이용해 설정하면 된다.

예)
rmiExporter.setRegistryHost("rmi.spitter.com");
rmiExporter.setRegistryPort(1199);


2.2 RMI 서비스 와이어링


전통적으로, RMI 클라이언트는 RMI레지스트레이서 서비스를 검색하려면 RMI API의 Naming클래스를 사용해야 한다. 

try{
   String serviceUrl="rmi:/spitter/SpitterService";
   SpitterService spitterService=(SpitterService)Naming.lookup(serviceUrl);
 ....
}
catch(RemoteException e){...}
catch(NotBoundException e){...}
catch(MalformedURLException e){...}

이 코드는 두 가지 문제가 발생한다.
  • 일반적인 RMI 검색(lookup)은 처리되거나 다시 던져야만 하는 세 가지 검사형예외(RemoteException, NotBoundException, MalformedURLException) 중 하나를 일으킬 수 있다. 
  • Spitter 서비스를 필요로하는 모든 코드는 해당 서비스 자체에 대한 레퍼런스를 흭득해야 한다. 이는 배관용(pluumbing)코드이며 클라이언트 기능과 직접적인 관련이 없다.
훨씬 더 나쁜점은 이 코드가 DI원칙을 정면으로 위반한다는 사실이다. 클라이언트 코드는 Spitter서비스를 검색하고, 이 서비스는 RMI서비스이므로 다른 소스에서 SpitterService의 다른 구현을 제공하기 위한 기회가 없다.
이상적으로 빈이 서비스 자체를 검색하게 하지 않고, SpitterService객체를 필요로하는 어떤 빈에든 그 객체를 주입할 수 있어야 한다. DI를 이용하면 SpitterService의 모든 클라이언트는 SpitterService가 어디에서 왔는지 알 필요가 없다.

스프링의 RmiProxyFactoryBean은 RMI서비스에 대한 프록시를 생성하는 팩토리 빈이다.

@Bean
public RmiProxyFactoryBean spitterService(){
  RmiProxyFactoryBean rmiProxy=new RmiProxyFactoryBean();
  rmiProxy.setServiceUrl("rmi://localhost/SpitterServie");
  rmiProxy.setServiceInterface(SpitterService.class);
  return rmiProxy;
}



--->그림) RmpProxyFactoryBean은 클라이언트를 대신해 원격 RMI서비스에 말을 거는 프록시 객체를 생성한다. 클라이언트는 해당 서비스의 인터페이스를 통해 그 원격 서비스가 마치 로컬 POJO인 것처럼 프록시에 말을 건다.



예를들어, 클라이언트가 Spitter서비스를 이용하여 주어진 사용자에 대한 Spitter목록을 조회한다고 가정하자. 다음과 @Autowired를 이용하여 서비스 프록시를 클라이언트에 연결한다.

@Autowired
SpitterService spitterService;


그러면 마치 로컬 빈인 것처럼 서비스의 메소드를 호출한다.
public List<Spittle> getSpittles(String userName){
 Spitter spitter=spitterService.getSpitter(userName);
 return spitterService.getSpittlesForSpitter(spitter);
}



-->이런방식의 RMI서비스에 접속하는것이 좋은 이유는 클라이언트 코드는 RMI서비스를 처리한다는 사실조차 몰라도되기 때문이다. 그것이 어디에서 오는지 전혀 관심없이 주입을 통해 SpitterService객체를 받기만 한다.

더욱이 프록시는 서비스에 의해 던져질 가능성이 있는 모든 RemoteException을 잡아 그것들을 마음놓고 무시해 버릴 수 있도록 런타임 예외로 다시 던진다.


RMI는 원격 서비스와 통신하는 훌륭한 방법이기는 하지만 한계도 있다. RMI는 방화벽을 넘어 작업하는데 어려움이 있다. 그 이유는 RMI가 통신을 위해 대체로 방화벽이 허용하지 않을 임의의 포트를 사용하기 때문이다. 
또 한가지 고려사항은 RMI가 자바 기반이라는 사실이다. 이는 클라이언트와 서비스 둘다 자바로 작성돼야 한다는 것을 의미한다. 그리고 RMI가 자바의 직렬화를 사용하므로 네트워크를 통해 전송되는 객체들의 타입이 호출의 양편에서 모두 정확히 동일해야 한다.





3. Hessian과 Burlap을 이용한 리모트 서비스 노출


Hessian과 Burlap은 HTTP를 통한 가벼운 원격 서비스를 가능하게 한다. 이 솔루션들은 각각 API와 통신 프로토콜 둘 모두를 가능한 한 간단하게 유지함으로써 웹 서비스를 단순화하는 것이 목표다.

Hessian은 RMI처럼 클라이언트와 서비스간에 바이너리 메시지를 이용해 통신한다. 그러나 RMI와 달리 PHP,Python,C++,C#등 자바외의 언어에 이식된다.

Burlap은 XML기반 리모팅 기술로 ,XML을 파싱할 수 있는 언어라면 어떤 언어로든 자동적으로 이식된다. 그리고 Burlap은 XML이므로 Hessian의 바이너리 포맷에 비해 사람이 훨씬 쉽게 읽을 수 있따.

Hessian메시지는 바이너리이므로 좀 더 대역폭에 친화적이다(즉, 전송데이터의 크기가 작다) 하지만 사람의 읽기에 편한점이 더 중요하거나(디버깅 목적으로) Hessian구현체가 없는 언어로 통신하는 경우에는 Burlap의 XML메시지가 더 좋을 것이다.


3.1 Hessian과 Burlap을 이용한 빈 기능 노출


*Hessian 서비스 익스포트 하기

RMI와 마찬가지로 Spitter서비스를 Hessian서비스로 노출 시키기 위해서는 익스포터 빈을 설정해야 한다. 그 빈은 HessianServiceExporter이 된다.

그러나 익스포트 하는 방식은 다르다.



--->HessianServiceExporter은 Hessian요청을 받아서 그 요청을 POJO에 대한 호출로 변환하는 방식으로, POJO를 Hessian서비스로 익스포트하는 스프링 MVC컨트롤러다.


@Bean
public HessianServiceExporter hessianExportedSpitterService(SpitterSerivce service)
{
   HessianServiceExporter exporter=new HessainServiceExporter();
   exporter.setService(service);
   exporter.setServiceInterface(SpitterService.class);
  return exporter;
}


-->RmiServiceExporter에서와 달리 serviceName 프로퍼티는 설정할 필요없다. RMI에서 serviceName프로퍼티는 서비스를 RMI레지스트리에 등록하는데 이용되는 프로퍼티다.
Hessian은 레지스트리를 갖지 않으므로 Hessian서비스에 이름을 부여할 필요가 ㅇ벗다.


Hessian 컨트롤러 구성하기

RmiServiceExporter와 HessianServiceExporter의 또 한가지 주된 차이점은 Hessian이 HTTP기반이므로 HessianServiceExporter가 스프링 MVC컨트롤러로 구현된다는 것이다.
그래서 익스포트된 Hessian서비스를 사용하려면 두 가지 추가적인 설정단계를 거쳐야 한다.
  • web.xml에 스프링 DispatcherServlet을 설정하고 애플리케이션을 웹 애플리케이션으로 적용한다.
  • Hessian서비스 URL을 적절한 Hessian서비스 빈으로 디스패치하도록 스프링 설정 파일에서 URL핸들러를 설정한다.

<servlet-mapping>
  <servlet-name>spitter</servlet-name>
  <url-pattern>*.service</url-pattern>
</servlet-mapping>

----->DispatcherServlet은 *.service URL을 흭득하는 서블릿 매핑이다.


WebApplicationInitializer를 구현하여 자바로 DispatcherServlet을 설정한다면, DispatcherServlet을 컨테이너에 추가할 때 얻은 ServletRegistraion.Dynamic에 매핑 시 URL패턴에 추가한다.

ServletRegistraion.Dynamic dispatcher=container.addServlet(
      "appServlet", new DispatcherServlet(dispatcherServletContext));
   dispatcher.setLoadOnStrartup(1);
   dispatcher.addMapping("/");
dispatcher.addMapping("*.service");



AbstractDispatcherServletInitializer 또는 AbstractAnnotaionConfigDispatcherServletInitializer를 확장하여 DispatcherServlet을 설정한다면 getServletMappings()오버라이딩시 매핑한다.

@Override
 protected String[] getServletMappings(){
  return new String[] {"/", "*.service"};
}

이렇게 구성되어 있으면 URL이 .service로 끝나는 모든 요청은 DispatcherServlet에 전달된다. 그러면 그 요청이 해당 URL에 매핑되어 있는 Controller로 넘어간다. 

어떻게 요청이 hessianSpitterService로 간다고 알 수 있을까? 그것은 DispatcherServlet이 hessianSpitterService로 보내도록 URL매핑을 구성할 것이기 때문이다. 다음의 SimplUrlHandlerMapping이 URL매핑을 처리한다.

@Bean
public HandlerMapping hessianMapping(){
  SimpleUrlHandlerMapping mapping=new SimpleUrlHandlerMapping();
  Properties mappings=new Properties();
  mapping.setProperty("/spitter.service", "hessianExportedSpitterSerivce");
  mapping.setMappings(mappings);
  return mapping;
}


Burlap 서비스 익스포트 하기

BurlapServiceExporter는 Hessian 바이너리 메시지 대신 Burlap의 XML기반 메시지를 처리한다는 점을 제외하고는 모든 측면에서 HessianServiceExporter와 동일하다.

@Bean
public BurlapServiceExporter  burlapExportedSpitterService(SpitterService service){
 BurlapServiceExporter exporter=new BurlapServiceExporter();
 exporter.setService(service);
 exprter.setServiceInterface(SpitterService.class);
 return exporter;
}

두 작업이 동일하다는데에 URL핸들러와 DispatcherServlet을 설정해야 한다는 것도 동일하다.



3.2 Hessian/Burlap 서비스에 액세스하기



--> 위 그림이 RmiProxyFactoryBean 대신 Hessian/Burlap Factory Bean으로 바뀌고 JRMP메시지 대신 HTTP로 바뀐다.

이렇게하면 클라이언트가 서비스의 구현에 대해 알 필요가 없으므로 RMI클라이언트르 Hessian 클라이언트로 교체하는 경우, 클라이언트의 자바 코드에 어떤 변경도 가할 필요 없이 매우 쉽게 한다는 장점이 있다.


Hessian과 Burlap은 둘 다 HTTP를 기반으로 하므로 RMI 같은 방화벽 문제를 겪지 않는다. 그러나 RMI는 RPC메시지로 전송되는 객체들을 직렬화하는 것에 관한 한 Hessian이나 Burlap보다 우위에 있다. Hessian과 Burlap이 모두 독자적인 직렬화 메커니즘을 쓰는 반면, RMI는 자바의 직렬화 메커니즘을 쓴다. 

그러나 이 둘의 장점을 모은 해결책이 있다. HTTP를 통해 RPC를 제공하는 한편, 동시에 자바 직렬화를 사용하는 스프링의 HTTP호출자가 있다.


4. 스프링의 HttpInvoker 사용하기


4.1 빈을 HTTP서비스로 익스포트하기

Spitter서비스를 HTTP호출자 기반으로 익스포트 하기 위해서는 HttpInvokerServiceExporter빈을 설정해야 한다.

@Bean
public HttpInvokerServiceExporter httpExportedSpitterService(SpitterService service)
{
   HttpInvokerServiceExporter exporter=new HttpInvokerServiceExporter();
   exporter.setService(service);
   exporter.setServiceInterface(SpitterService.class);
   return exporter;
}


--->HessianServiceExporter과 똑같으므로 저 부분만 HttpInvokerServiceExporter로 바뀐다.


4.2 HTTP를 거쳐 서비스에 액세스하기


이부분도 RMI Proxy가 아닌 HttpInvokerProxy로 바뀌고 JRMP메시지 대신 HTTP로 바뀌므로 똑같다.




그러나... HttpInvoker에는 주의를 기울여야 하는 한가지가 있다.
바로 HttpInvoker가 오로지 스프링 프레임워크에 의해서만 제공되는 리모팅 솔루션이란 점이다. 이는 클라이언트와 서비스 양쪽이 모두 스프링을 사용할 수 있는 애플리케이션이어야 한다는 것을 의미한다. 여기에는 또한 적어도 지금으로서는 클라이언트와 서비스 둘 다 자바기반이어야 한다는 제한도 내포되어 있다. 그리고 자바 직렬화가 사용되므로 양쪽 모두 같은 버전의 클래스를 갖고 있어야 한다.


5. 웹 서비스의 발행과 소비


서비스 지향 아키텍처(SOA, Service-Oriented Architecture)의 중심에 놓여있는 것은 각 애플리케이션 마다 동일한 기능을 다시 구현하는 대신, 애플리케이션들이 일련의 공통된 핵심 서비스에 의거하도록 설계될 수 있고, 또 설계돼야 한다는 생각이다.


5.1 스프링을 사용할 수 있는 JAX-WS 엔드포인트 생성




댓글

이 블로그의 인기 게시물

(네트워크)폴링방식 vs 롱 폴링방식

(ElasticSearch) 결과에서 순서 정렬

(18장) WebSocekt과 STOMP를 사용하여 메시징하기