DRb 모듈
DRb 모듈
개요
dRuby는 Ruby를 위한 분산 객체 시스템이에요. 순수 Ruby로 작성됐고 자체 프로토콜을 사용해요. TCP 소켓 같은 Ruby 런타임이 제공하는 것 외에는 추가 서비스가 필요 없어요. CORBA, RMI, .NET 같은 다른 분산 객체 시스템에 의존하지도 않고 연동하지도 않아요.
dRuby는 한 Ruby 프로세스 안에서, 다른 Ruby 프로세스(심지어 다른 머신)에 있는 Ruby 객체의 메서드를 호출할 수 있게 해줘요. 객체에 대한 참조는 프로세스 사이에서 전달될 수 있어요. Method 인자와 반환 값은 마샬된(marshalled) 형식으로 덤프되고 로드돼요. 이 모든 것이 원격 메서드의 호출자와 호출 대상 객체 모두에게 투명하게 일어나요.
원격 프로세스의 객체는 로컬에서 DRb::DRbObject 인스턴스로 표현돼요. 이것은 원격 객체에 대한 일종의 프록시 역할을 해요. 이 DRbObject 인스턴스에서 호출되는 메서드는 원격 객체로 전달돼요. 이것은 런타임에 동적으로 배치돼요. CORBA의 IDL 같은 원격 객체를 위한 정적으로 선언된 인터페이스는 없어요.
프로세스에 들어오는 dRuby 호출은 그 프로세스 안의 DRb::DRbServer 인스턴스가 처리해요. 이것이 메서드 호출을 재구성해서 지정된 로컬 객체에서 호출하고, 값을 원격 호출자에게 돌려줘요. 어떤 객체든 dRuby를 통해 호출을 받을 수 있어요. 특별한 인터페이스를 구현하거나 특별한 기능을 mixin할 필요가 없어요. 일반적인 경우에는 객체가 dRuby 호출을 받기 위해 DRbServer에 자신을 명시적으로 등록할 필요도 없어요.
한 프로세스가 다른 프로세스에 대해 dRuby 호출을 하려면, 어떻게든 원격 프로세스의 객체에 대한 초기 참조를 얻어야 해요. 처음에는 호출할 수 있는 원격 객체 참조가 없기 때문에 원격 메서드 호출의 반환 값을 통해서는 얻을 수 없거든요. 이것은 URI로 서버에 접속해서 이뤄져요. 각 DRbServer는 druby://example.com:8787 같은 URI에 바인딩돼요. DRbServer에는 서버의 프론트 객체(front object)로 동작하는 객체를 붙일 수 있어요. DRbObject는 서버의 URI에서 명시적으로 만들어질 수 있어요. 이 DRbObject의 원격 객체는 서버의 프론트 객체가 돼요. 이 프론트 객체는 그다음 DRbServer 프로세스 안의 다른 Ruby 객체에 대한 참조를 돌려줄 수 있어요.
dRuby로 이뤄지는 Method 호출은 프로세스 안에서 이뤄지는 일반 Ruby 메서드 호출과 대체로 똑같이 동작해요. 블록이 있는 Method 호출도 지원되고, 예외를 던지는 것도 지원돼요. 메서드의 표준 오류에 더해, dRuby 호출은 dRuby 특유의 오류 중 하나도 발생시킬 수 있는데, 그것들은 모두 DRb::DRbError의 서브클래스예요.
어떤 종류의 객체든 dRuby 호출의 인자로 전달되거나 반환 값으로 돌아올 수 있어요. 기본적으로 그러한 객체는 로컬 쪽에서 덤프(marshal)되고 원격 쪽에서 로드(unmarshal)돼요. 따라서 원격 쪽은 로컬 객체의 복사본을 받지, 분산 참조를 받는 게 아니에요. 이 복사본에서 호출되는 메서드는 전적으로 원격 프로세스에서 실행되고, 로컬 원본에 전달되지 않아요. 이것은 값에 의한 전달(pass-by-value)과 비슷한 의미를 가져요.
하지만 객체를 마샬할 수 없으면, 그에 대한 dRuby 참조가 대신 전달되거나 반환돼요. 이것은 원격 쪽에서 DRbObject 인스턴스로 나타나요. 이 원격 프록시에서 호출되는 모든 메서드는 로컬 객체로 전달돼요. 이것은 일반 Ruby의 참조에 의한 전달(pass-by-reference)과 비슷한 의미를 가져요.
그 외에는 마샬 가능한 객체를 복사본으로 보내는 대신 DRbObject 참조로 전달하거나 반환하고 싶다는 신호를 보내는 가장 쉬운 방법은 DRb::DRbUndumped mixin 모듈을 include하는 거예요.
dRuby는 블록이 있는 원격 메서드 호출을 지원해요. 블록(그것을 나타내는 Proc 객체)은 마샬할 수 없기 때문에, 블록은 원격이 아니라 로컬 컨텍스트에서 실행돼요. 블록에 yield되는 각 값은 원격 객체에서 로컬 블록으로 전달되고, 각 블록 호출이 반환한 값은 다시 원격 실행 컨텍스트로 전달되어 수집돼요. 그리고 수집된 값들이 마침내 메서드 호출의 반환 값으로 로컬 컨텍스트에 돌아와요.
출처: Ruby 3.3 API
본문
사용 예시
더 많은 dRuby 예시는 전체 dRuby 배포판의 samples 디렉터리를 보세요.
클라이언트/서버 모드의 dRuby
간단한 클라이언트-서버 drb 시스템을 설정하는 예시예요. 서버 코드와 클라이언트 코드를 다른 터미널에서 실행하되, 서버 코드를 먼저 시작하세요.
서버 코드
require 'drb/drb'
# 서버가 접속할 URI
URI="druby://localhost:8787"
class TimeServer
def get_current_time
return Time.now
end
end
# 서버에서 요청을 처리하는 객체
FRONT_OBJECT=TimeServer.new
DRb.start_service(URI, FRONT_OBJECT)
# 앞으로 나가기 전에 drb 서버 스레드가 끝나기를 기다려요
DRb.thread.join
클라이언트 코드
require 'drb/drb'
# 접속할 URI
SERVER_URI="druby://localhost:8787"
# 로컬 DRbServer를 시작해서 콜백을 처리해요.
#
# 이 작은 예시에서는 필요 없지만, dRuby 호출의 인자로
# 마샬할 수 없는 객체를 넘기는 순간부터 필요해져요.
#
# 참고: 효과를 보려면 프로세스당 최소 한 번은 호출해야 해요.
# 프로세스가 fork하면 특히 중요해요.
DRb.start_service
timeserver = DRbObject.new_with_uri(SERVER_URI)
puts timeserver.get_current_time
dRuby 아래의 원격 객체
dRuby 호출에서 객체에 대한 참조를 반환하는 예시예요. Logger 인스턴스는 서버 프로세스에 살아 있어요. 그것들에 대한 참조가 클라이언트 프로세스로 반환되고, 거기서 메서드를 호출할 수 있어요. 이 메서드들은 서버 프로세스에서 실행돼요.
서버 코드
require 'drb/drb'
URI="druby://localhost:8787"
class Logger
# dRuby가 Logger 인스턴스를 복사본이 아니라 dRuby 참조로
# 보내게 해요.
include DRb::DRbUndumped
def initialize(n, fname)
@name = n
@filename = fname
end
def log(message)
File.open(@filename, "a") do |f|
f.puts("#{Time.now}: #{@name}: #{message}")
end
end
end
# 로거를 만들고 가져오는 중앙 객체.
# 생성된 모든 로거에 대한 로컬 참조를 유지해요. 이름으로
# 기존 로거를 찾을 수 있게 하려는 것도 있지만, 로거가
# 가비지 컬렉션되는 것을 막기 위해서예요. dRuby 참조만으로는
# 객체가 가비지 컬렉션되지 않게 막을 수 없어요!
class LoggerFactory
def initialize(bdir)
@basedir = bdir
@loggers = {}
end
def get_logger(name)
if [email protected]_key? name
fname = name.gsub(/[.\/\\\:]/, "_")
@loggers[name] = Logger.new(name, @basedir + "/" + fname)
end
return @loggers[name]
end
end
FRONT_OBJECT=LoggerFactory.new("/tmp/dlog")
DRb.start_service(URI, FRONT_OBJECT)
DRb.thread.join
클라이언트 코드
require 'drb/drb'
SERVER_URI="druby://localhost:8787"
DRb.start_service
log_service=DRbObject.new_with_uri(SERVER_URI)
["loga", "logb", "logc"].each do |logname|
logger=log_service.get_logger(logname)
logger.log("Hello, world!")
logger.log("Goodbye, world!")
logger.log("=== EOT ===")
end
보안
모든 네트워크 서비스처럼 dRuby를 쓸 때도 보안을 고려해야 해요. Ruby 객체에 외부 접근을 허용하면, 외부 클라이언트가 그 객체에 정의된 메서드를 호출할 수 있게 될 뿐만 아니라, 기본적으로 여러분의 서버에서 임의의 Ruby 코드를 실행할 수도 있게 돼요. 다음을 생각해 보세요.
# !!! UNSAFE CODE !!!
ro = DRbObject::new_with_uri("druby://your.server.com:8989")
class << ro
undef :instance_eval # force call to be passed to remote object
end
ro.instance_eval("`rm -rf *`")
instance_eval과 그 친구들이 주는 위험 때문에, DRbServer는 클라이언트를 신뢰할 수 있을 때만 사용해야 해요.
DRbServer는 접근 제어 목록으로 구성해서 지정된 IP 주소로부터의 접근을 선택적으로 허용하거나 거부할 수 있어요. 주 druby 배포판은 이를 위해 ACL 클래스를 제공해요. 일반적으로 이 메커니즘은 좋은 방화벽 대신이 아니라 함께 사용해야 해요.
dRuby 내부 구조
dRuby는 세 가지 주요 구성요소로 구현돼요: 원격 메서드 호출 마샬러/언마샬러, 전송 프로토콜, ID-객체 매퍼. 마지막 두 개는 직접 교체할 수 있고, 첫 번째는 간접적으로 교체할 수 있어서 다른 동작과 능력을 제공할 수 있어요.
원격 메서드 호출의 마샬링과 언마샬링은 DRb::DRbMessage 인스턴스가 수행해요. 이것은 전송 계층으로 보내기 전에 Marshal 모듈로 메서드 호출을 덤프하고, 다른 쪽 끝에서 재구성해요. 보통 이 구성요소를 교체할 필요는 없고, 직접 교체할 방법도 제공되지 않아요. 다만 전송 계층 구현의 일부로 대체 마샬링 방식을 구현하는 것은 가능해요.
전송 계층은 클라이언트와 서버 네트워크 연결을 열고 dRuby 요청을 전달하는 역할을 해요. 보통 내부적으로 DRb::DRbMessage를 사용해서 마샬링과 언마샬링을 관리해요. 전송 계층은 DRb::DRbProtocol이 관리해요. 한 번에 여러 프로토콜을 DRbProtocol에 설치할 수 있는데, 그 사이의 선택은 dRuby URI의 스킴에 따라 결정돼요. 기본 전송 프로토콜은 druby: 스킴으로 선택되고 DRb::DRbTCPSocket이 구현해요. 이것은 통신에 일반 TCP/IP 소켓을 사용해요. UNIX 도메인 소켓을 사용하는 대체 프로토콜은 drb/unix.rb 파일의 DRb::DRbUNIXSocket이 구현하고 drbunix: 스킴으로 선택돼요. HTTP 위의 샘플 구현은 주 dRuby 배포판에 딸린 samples에서 찾을 수 있어요.
ID-객체 매핑 구성요소는 dRuby 객체 id를 그것들이 가리키는 객체로, 그리고 그 반대로 매핑해요. 사용할 구현은 DRb::DRbServer 구성의 일부로 지정할 수 있어요. 기본 구현은 DRb::DRbIdConv가 제공해요. 객체의 ObjectSpace id를 dRuby id로 사용해요. 이것은 그 객체에 대한 dRuby 참조가 객체의 프로세스 수명과 그 프로세스 안에서 객체의 수명 동안에만 의미가 있음을 뜻해요. drb/timeridconv.rb 파일의 DRb::TimerIdConv가 수정된 구현을 제공해요. 이 구현은 dRuby로 내보낸 모든 객체에 대한 로컬 참조를 설정 가능한 시간(기본 10분) 동안 유지해서, 그 시간 동안 가비지 컬렉션되지 않게 해요. 주 dRuby 배포판의 sample/name.rb에 또 다른 샘플 구현이 있어요. 이것은 객체가 자신의 id나 "이름"을 지정할 수 있게 해줘요. 각 프로세스가 같은 dRuby 이름으로 객체를 등록하면 dRuby 참조를 프로세스에 걸쳐 영속적으로 만들 수 있어요.
상수
VERSION
속성
primary_server[RW]— 기본 로컬 dRuby 서버예요.start_service호출이 만든 서버예요.
클래스 메서드
config()— 현재 서버의 구성을 가져와요. 현재 서버가 없으면 기본 구성을 돌려줘요.current_server와DRbServer::make_config참고.current_server()— '현재' 서버를 가져와요. dRuby 서버의 메인 스레드 안에서(보통 서버나 그 객체 중 하나에 대한 원격 호출의 결과로) 실행되는 컨텍스트에서는 그 서버가 현재 서버예요. 그 외에는 primary 서버가 현재 서버예요. 서버를 찾지 못하면DRbServerNotFound오류를 발생시켜요.fetch_server(uri)— 주어진uri로 서버를 가져와요.regist_server와remove_server도 참고.front()— 현재 서버의 프론트 객체를 가져와요. 현재 서버가 없으면DRbServerNotFound오류를 발생시켜요.here?(uri)—uri가 현재 로컬 서버의URI인가요?install_acl(acl)— 기본ACL을acl로 설정해요.DRb::DRbServer.default_acl참고.install_id_conv(idconv)— 기본 id 변환 객체를 설정해요. 객체를DRb참조로/에서 변환하는to_id와to_obj에 응답하는DRb::DRbIdConv같은 인스턴스여야 해요.regist_server(server)—server를DRb에 등록해요. 새DRb::DRbServer가 만들어질 때 호출돼요. primary 서버가 없으면server가 primary 서버가 돼요.remove_server(server)— 등록된 서버 목록에서server를 제거해요.start_service(uri=nil, front=nil, config=nil)— 로컬에서 dRuby 서버를 시작해요. 새 dRuby 서버는 다른 서버가 현재 primary더라도 primary 서버가 돼요.uri는 서버가 바인딩할URI예요. nil이면 기본 로컬 호스트 이름의 임의 포트에 바인딩하고 기본 dRuby 프로토콜을 사용해요.front는 서버의 프론트 객체이고 nil일 수 있어요.config는 새 서버의 구성이고 nil일 수 있어요.stop_service()— 로컬 dRuby 서버를 멈춰요. primary 서버에 동작해요. 현재 실행 중인 primary 서버가 없으면 아무것도 안 해요.thread()— primary 서버의 스레드를 가져와요. primary 서버가 없으면 nil을 돌려줘요.to_id(obj)— 현재 서버를 사용해 객체의 참조 id를 가져와요. 현재 서버가 없으면DRbServerNotFound오류를 발생시켜요.to_obj(ref)— 현재 서버를 사용해 참조를 객체로 변환해요. 현재 서버가 없으면DRbServerNotFound오류를 발생시켜요.uri()— 로컬 dRuby 공간을 정의하는URI를 가져와요. 현재 서버의URI예요.