자바 RMI (Remote Method Invovation) 사용하기 서창근 <chang@webdox.co.kr> 2001년 3월 7일 개요 분산시스템 이러한 시스템에서 중요한 것은 바로 시스템에 소속되어 있는 컴퓨터 간의 통신이다. 더욱 자세하게 말하면 각 컴퓨터에서 실행되고 있는 프로그램, 또는 프로세스들이 서로 통신이 가능해야 한다는 것이다. 이 프로그램 사이의 통신을 가능하게 하는 것 중 현재 UNIX에서 가장 많이 사용되고 있는 통신 프로토콜이 바로 RPC (Remote Procedure Call)이다. RPC는 네트워크에 연결되어 있는 다른 컴퓨터에 존재하는 함수를 사용자가 눈치채지 못하도록 실행해 줌으로써 분산시스템의 구현을 가능하게 했다. 그러나, RPC는 객체지향 개발개념을 구현하지는 않으며 단지 다른 컴퓨터에 저장되어 있는 순처적인 언어로 개발된 애플리케이션의 함수를 부를 수 있도록 해주는 역할을 할 따름이다. RMI (Remote Method Invocation) 란? 왜 RMI를 사용하는가? 지금부터 자바 RMI의 기본적인 사용 방법을 예제를 통하여 살펴보고 더 이상 소켓 프로그래밍에 구애받지 않는 더욱 강력하고 세련된 개발자가 되어보도록 하자. 특히 자바 RMI는 EJB의 통신 중추이므로 RMI를 이해하고 있으면 EJB를 공부하는데도 더욱 수월하다. 현재 EJB를 공부하고 있거나 나중에 배우기를 원하는 독자들에겐 RMI의 기본 개념의 이해는 필수적이라 하겠다. RMI에 관해서 그렇다면 도대체 RMI에서 어떻게 한 객체가 다른 컴퓨터에 존재하는 객체에게 메세지를 전송할 수 있을까? 가장 기본적인 문제는 그 객체가 다른 객체의 존재를 알아야 하며, 그 객체에게 메세지를 전송하는 방법을 알아야 하고, 그리고 마지막으로 그 객체로부터 메세지의 답변을 받아야 한다는 것이다. 이 과정에서 수반되는 문제는 메세지를 전송하고 답변을 받을때 어떠한 종류의 데이터를 주고 받아야 하는가이다. RMI는 위에 나열된 문제를 두 가지의 방법으로 해결한다. 첫째가 스텁(stub)이며 둘째가 parameter marshalling 이다. 스텁(stub) 과 Parameter Marshalling 스텁이 하는 중요한 일은 두 가지가 있다. 첫째는 당연히 서버에 존재하는 메쏘드를 불러줘야 하고 둘째는 그 메쏘드의 인자를 일정한 포맷으로 바꿔주는 것이다. 이렇게 인자를 일정한 포맷으로 바꿔주는 일을 parameter marshalling 이라고 한다. 즉, 인자를 조율하고 정리하는 역할을 하는 것이다. 왜 이런 parameter marshalling 이 필요한 것일까? 컴퓨터개론을 공부해 본 독자들은 알겠지만 모든 컴퓨터시스템에서 데이터 표현방법이 약간씩 다르다는 것을 알 것이다. 예를 들어 어떤 시스템은 big-endian으로 표현하고 어떤 시스템은 little-endian으로 표시한다. 정수가 차지하는 메모리의 양도 다를 수 있다. 자바는 플랫폼과는 독립적으로 작동하는 언어이다. 만약 한 객체가 윈도우 시스템에서 작동하고 다른 객체는 유닉스 환경에서 작동하고 있다면 이러한 데이터의 구현 방법을 통일해 줄 필요가 있다. 특히 쓰리티어시스템(three-tier system)의 경우 데이터베이스 서버, 애플리케이션 서버, 그리고 클라이언트가 일일히 다른 플랫폼으로 구현될 수도 있으므로 데이터의 구현방법을 일관적으로 표현하는 일은 아주 중요하다. 그 일이 바로 paramter marshalling 이다. Parameter marshalling 이 끝났으면 스텁은 서버에 존재하는 객체의 메쏘드를 부르게 된다. 이때 스텁이 서버에 보내는 정보는 서버 객체의 아이디, 실행해야할 메쏘드, 그리고 변환된 인자이다. 클라이언트 쪽에서 스텁이 대리인의 역할을 하듯 서버쪽에서도 이러한 대리인의 역할을 하는 존재가 있는데 수신자(receiver) 또는 스켈레톤(skeleton, 주: 해골 또는 골격이라고 부르기엔 좀 너무하다는 생각이 들었다.) 이라고도 한다. 수신자가 하는 역할은 스텁의 반대라고 생각하면 된다. 조율된 인자들을 다시 그 서버에 맞는 형태로 바꿔준 뒤 스텁이 보낸 아이디의 객체가 갖고 있는 알맞는 메쏘드를 실행시킨다. 그리고 실행이 끝났으면 결과를 다시 parameter marshalling 을 통하여 변환한 뒤, 다시 스텁에게 보내준다. 스텁은 이 결과를 받아서 다시 시스템에 맞게 변환시킨 뒤, 원래 메쏘드를 불렀던 객체에게 결과를 넘겨준다. 물론 결과가 예외일 수도 있다. 동적 클래스 로딩 (Dynamic Class Loading) Computer remote_comp = new Computer(); 문제는 여기서 getType() 메쏘드를 불렀을 때 돌아오는 값이 어떤 숫자나 문자가 아니라 서버에 존재하는 객체라는 것이다. 그리고 클라이언트는 이 새로운 원격객체의 someMethod()라는 메쏘드를 불러주고 있다. 물론 클라이언트는 함수값으로 돌아오는 객체가 ComputerType이라는 객체라는 것은 알지만 실제로 돌아오는 객체는 ComputerType의 서브클래스일 수도 있다. 게다가 이 새로운 객체가 서버에 정의돼 있는 새로운 예외객체를 필요로 할 수도 있다. 물론 이 새로운 객체의 someMethod()라는 메쏘드를 부르려면 클라이언트가 이 새로운 객체에 대해서 알고 있어야 한다. 이 문제를 해결하기 위해 클라이언트에 이 객체를 정의해주고 저장하는 방법도 있고 서버와 동시에 일일히 클라이언트를 업데이트 해주는 것도 방법이겠지만 새로운 종류의 ComputerType을 서버에 정의할 때마다 일일이 클라이언트를 업데이트 할 수는 없다. 그래서 필요한 것이 바로 동적 클래스 로딩이다. 동적 클래스 로딩은 이렇게 클라이언트가 알지 못하는 객체가 필요할 경우 서버에서 그때 그때 필요할 때마다 클라이언트로 그 필요한 객체의 스텁클래스를 불러오는 것을 말한다. 자바의 애플릿과 비슷한 개념이라고 생각하면 되겠다. 위의 예제의 경우 자동으로 ComputerType의 서브클래스의 스텁클래스가 클라이언트로 로딩되어 someMethod()를 부르는것을 가능케 한다. 하지만 이렇게 다른 컴퓨터에서 네트워크를 통하여 스텁클래스를 불러올 경우 보안의 문제가 발생할 수 있으므로 실제로 이러한 시스템을 가동시킬 땐 꼭 보안에 신경을 써서 자바 보안 매니저 (Java Security Manager)등을 사용하는 것이 좋다. 자바 RMI 프로그래밍 서버 쪽 프로그래밍 interface Seat extends Remote 여기서 주의해야 할 것은 모든 원격 인터페이스는 Remote 클래스를 상속하여야 하고 원격적으로 공개되는 메쏘드는 모두 RemoteException 예외클래스를 던져야 한다는 것이다. 이 인터페이스를 하나의 자바파일로 만들어서 서버와 클라이언트 양쪽에 저장하면 되겠다. 이렇게 인터페이스를 정의했으면 다음엔 실제 객체를 구현해야 한다. 여기선 다른 메쏘드는 배제하고 isReserved() 메쏘드만 구현해 보기로 하겠다. import java.rmi.Naming; public class SeatImpl extends UnicastRemoteObject implements
Seat String query = "SELECT reserved FROM seats WHERE plane = ? AND seat_no = ?"; PreparedStatement ps = connection.prepareStatement(query); ResultSet rs = ps.executeQuery(); boolean reserved = rs.getBoolean("reserved"); return(reserved); public static void main(String args[]) { 여기서 주의해야 할 것은 바로 서버객체가 UnicastRemoteObject 클래스를 상속한다는 것이다. 이 클래스가 바로 서버객체를 원격객체로 만들어주는 클래스가 되겠다. UnicastRemoteObject 를 사용하면 꼭 서버객체가 TCP/IP로 접근이 가능하여야 한다. 그 위에 정의했던 Seat 이라는 인터페이스를 구현하는 것도 잊지 말자. 다음으로 해야 할 것이 객체의 이름을 RMI Registry에 등록하는 것이다. JNDI (Java Naming and Directory Interface)의 일환이기도 한데 클라이언트에 있는 객체에게 서버객체의 존재를 알려주는 역할을 한다. 즉, 서버객체를 어떠한 이름하에 등록하여 클라이언트의 객체가 그 이름으로 원격객체를 찾을 수 있도록 해주는 것을 말한다. 클라이언트는 이 이름으로 서버객체를 찾으며 찾았을 경우 그 이름에 해당하는 서버객체의 스텁클래스가 클라이언트로 전송되어 서버객체의 메쏘드를 부르는 것을 가능케 한다. 특히 JNDI는 EJB에서도 사용되므로 개념을 이해해두면 EJB를 공부하기도 편하다. 위의 예제를 보면 seat이라는 이름 하나만 등록한 것을 알 수 있다. 실제로 비행기 좌석 예약 시스템을 만든다고 하자. SeatImpl 이라는 서버객체가 만약에 좌석 하나하나를 구현한다고 하면 그 좌석 하나하나를 다 등록할 수는 없는 일이다. 왜냐하면 이 이름은 클라이언트에게 서버객체의 존재를 알게 해주는 것이므로 클라이언트가 어떠한 메쏘드를 부르기 전에 벌써 등록이 되어 있어야 한다. 하지만 여기서 Naming.bind() 메쏘드의 인자 중 하나가 바로 SeatImpl 객체이다. 그렇다면 좌석 하나하나의 객체를 다 생성해서 그 이름들을 따로 등록해야 한다는 예기가 된다. 특히 좌석의 데이터가 데이터베이스에 저장되어 있을 경우 그것을 일일히 동적으로 불러와야 하는데 그렇다면 클라이언트가 메쏘드를 부르기 전에 그 이름을 등록한다는 것은 불가능하다. 이것은 상당히 비효율적인 개발방법이다. 여기선 SeatImpl 객체가 한 비행기의 모든 좌석을 구현하게 개발하는 것이 더욱 효율적이며 RMI Registry도 더 간단해 진다. seat 이라는 이름을 통하여 클라이언트에게 서버객체의 존재를 알려주고 SeatImpl 이라는 서버객체가 따로 각 좌석의 데이터를 데이터베이스로부터 불러오는 것이 더 효율적인 것이다. 게다가 RMI Registry는 모든 객체들이 공유하는 서비스이므로 한 프로그램이 다 독차지해서도 안된다. 클라이언트쪽 프로그래밍 import java.rmi.Naming; public class Reservation public boolean isReserved(int seat_numer) throws Exception { boolean result; try { // 원격객체를 찾는데 성공했으면 메쏘드를 부르자 return result; 먼저 클라이언트 프로그램이 실행되기 시작될 때 자바 보안매니저를 꼭 실행시키도록 하자. 만약 클라이언트가 자바 애플릿일 경우엔 자동으로 보안이 실행되므로 상관없지만 자바 애플리케이션이라면 꼭 실행시켜주도록 한다. 그리고 서버객체의 이름을 찾을때 URL을 사용하는것을 기억하도록 하자. "rmi://서버이름/등록된 객체이�" 의 형식이면 되겠다. 그리고 예외클래스는 꼭 처리하도록 해야한다. RMI가 네트워크를 통해서 이루어지므로 어떠한 에러가 발생할지 모르기 때문이다. RMI의 실행 서버 2. rmic 를 이용하여 스텁클래스 생성 3. rmiregistry 실행 4. 서버 프로그램 실행 위에 나열한 것과 같이 실행하면 RMI를 사용할 수 있게 된다. 일단 인터페이스와 서버 프로그램을 자바 컴파일러로 컴파일 다. 처음에 서술했듯이 클라이언트객체가 서버객체를 사용하려면 스텁클래스가 있어야 한다. 이 스텁클래스를 생성하는 것이 rmic 프로그램이 되겠다. rmic 로 생성된 스텁클래스가 동적 클래스로딩을 통해서 클라이언트가 필요로할 때 클라이언트로 옮겨 가게 되는 것이다. rmic 는 다음과 같이 실행시키면 된다. rmic SeatImpl 만약 자바 1.1 객체를 지원해야 한다면 -v1.2 옵션을 사용하면 된다. 클래스 이름은 패키지 이름까지 다 사용하여야 한다. 만약에 SeatImpl 클래스가 com.myairline 이라는 패키지의 일부라면 rmic com.myairline.SeatImpl 이라고 해야 한다. rmic 를 실행시키면 앞에서 말한 바와 같이 스텁 클래스와 스켈레톤 클래스가 SeatImpl_Stub.class, SeatImpl_Skel.class 라는 이름으로 생성된다. 하지만 스켈레톤 클래스는 자바 2에선 더이상 사용하지 않으므로 무시해도 좋다. 이렇게 스텁을 만들었으면 다음은 rmiregistry 를 실행시키는 일만이 남았다. 윈도우즈 서버에서는 start rmiregistry 유닉스나 리눅스에선
이렇게 rmiregistry 가 실행 됐으면 서버 프로그램을 실행시켜주면 되겠다. 여기서 주의해야 할것은 서버프로그램을 서비스 프로그램으로 실행시켜야 한다는 것이다. 즉, rmiregistry 와 같은 방법으로 말이다. 위의 예제에선 start java SeatImpl 또는
클라이언트 2. 보안정책을 정의하는 파일 만들기 3. 클라이언트 프로그램의 실행 서버와 마찬가지로 클라이언트 객체의 자바 파일과 인터페이스 파일을 자바 컴파일러로 컴파일 해준다. 여기서 인터페이스 파일은 서버에서 컴파일한 것과 같은 파일이어야 한다. 컴파일이 성공했으면 다음은 보안정책을 설정해주는 파일을 만들어야 한다. 파일이름은 아무래도 상관없다. 여기선 client_security.policy 라고 하겠다. 위에서 자바 보안매니저를 실행시킨 것을 기억할 것이다. 자바 보안매니저는 원래 어떠한 네트웍 연결도 허용하지 않는다. 그러므로 이러한 보안정책을 설정해 주어야 하는 것이다. 보안정책 파일엔 다음과 같이 해주면 되겠다. grant 여기선 RMI가 포트번호 1024에서 65535 사이의 아무 포트나 사용할 수 있도록 하였다. RMI의 기본설정은 1099이다. 보안정책 파일엔 이외에 다른 많은 정보가 들어갈 수 있는데 정책파일과 보안 매니저에 관한 자세한 정보는 다른 글들을 참조하길 바란다. 이렇게 보안정책이 설정되었으면 프로그램을 실행시키는 일만이 남았다. 프로그램은 다음과 같이 실행시키면 된다. 보안정책 파일을 지정해 주는 것을 잊지 않도록 하자. java Reservation -Djava.security.policy=client_security.policy 동적 클래스로딩에 관해 주의할점 위에서 언급한대로 클라이언트 객체는 원격 인터페이스를 통하여, 즉 스텁클래스를 통하여 서버객체의 메쏘드를 부르게 된다. 다시 말하면 클라이언트는 원격 인터페이스에 공개되어 있는 메쏘드밖에 부를 수 없다는 말이다. 서버객체 내에 다른 여러가지 메쏘드가 있을 수 있다. 하지만 원격 인터페이스에 공개되어 있지 않으면 클라이언트 객체는 그 메쏘드를 부를 수 없다. 예를 들어 클라이언트 객체가 서버객체의 어떠한 메쏘드를 불렀는데 그 값으로 원격객체를 상속한 어떠한 객체가 돌아왔다고 하자. 그리고 원격객체는 원격 인터페이스를 구현하지만 그 원격객체를 상속한 객체는 원격 인터페이스를 구현하지 않는다고 하자. 그렇다면 클라이언트 객체가 받은 이 객체는 어떠한 메쏘드의 실현이 가능할까? 답은 수퍼클래스가 구현한 원격 인터페이스에 공개되어 있는 메쏘드 뿐이다. 상속한 클래스가 다른 어떤 메쏘드를 정의했을지라도 그것은 클라이언트가 실행시킬 수 없다. 이것은 그 객체가 메쏘드의 인자로 사용되던 돌아오는 값으로 사용되던 마찬가지다. 그렇다면 원격 인터페이스를 구현하지 않는 다른 클래스들은 어떻게 될까? 예를 들어 서버객체의 메쏘드를 부를때 String 클래스를 인자로 사용했다고 하자. 이럴 경우 String 클래스는 Remote 인터페이스를 구현하지 않으므로 실제론 객체가 복사가 되어 서버로 보내지게 된다. 만약 서버가 String 객체의 어떠한 메쏘드를 사용한다고 해도 객체 자체가 복사가 되어 보내졌으므로 문제가 없다. 간단히 말하면 원격 인터페이스를 구현하는 객체가 인자나 메쏘드 값으로 보내질 경우 스텁클래스만이 보내지고 원격 인터페이스를 구현하지 않는 객체를 보낼 경우 객체 자체가 복사되어 보내진다는 얘기가 되겠다. 마치며 |