Developer: Java
  다운로드
Oracle SOA Suite (OWSM, JDeveloper 포함)
샘플 코드
  태그
soa, java, All

데코레이터 패턴을 이용한 웹 서비스 개발 환경의 단순화

복잡한 코딩/테스팅/적용 사이클을 거치지 않고 웹 서비스에 계층화된 기능을 신속하게 구현하는 방법을 배워 보십시오.

저자 - Jason Jones Oracle ACE Director

게시일: 2007년 12월

Oracle Fusion Middleware는 Java, BPEL, ESB, PHP 등 웹 서비스 구현을 위한 다양한 대안을 제공합니다. Database Native Web Services 덕분에 데이터베이스 환경에서도 다양한 선택의 여지를 가질 수 있게 되었습니다. 오늘날 대부분의 기업들이 이기종 IT 인프라스트럭처를 운영하고 있습니다. 기업이 구축한 SOA 기반 서비스는 다양한 플랫폼, 툴, 프로그래밍 언어, 벤더의 조합으로 구현됩니다. 이로 인해 전체 환경에 공통적인 웹 서비스 정책을 적용하는 것이 매우 어려운 문제로 대두되고 있습니다.

따라서 하부 시스템을 수정하지 않고도 전체 웹 서비스의 정책을 일관성 있게 수정하고 개선할 수 있는 방안이 요구됩니다. 이러한 문제는 Gang of Four (GoF) Decorator Pattern에서 제시된 내용과 매우 긴밀한 연관성을 갖습니다. 본 문서는 Oracle Web Services Manager(OWSM, Oracle SOA Suite에 포함되어 있습니다)를 이용하여 웹 서비스를 "장식(decorate)"하는 방법을 설명하고 있습니다.

The Decorator Pattern

GoF 데코레이터 패턴은 오브젝트 지향형 개발 환경에서 기존 오브젝트를 변경하지 않고 오브젝트의 행동 양식(behavior)에 변화를 주기 위해 사용되는 디자인 패턴입니다. 이를 위해 기존 오브젝트를 데코레이터로 래핑(wrapping)하는 작업이 수행됩니다. 데코레이터는 기존 오브젝트와 동일한 인터페이스를 갖지만, 그 행동 양식과 입출력은 다르게 정의됩니다. 데코레이터는 하부의 오브젝트에 모든 작업을 위임하는 것이 일반적이지만, 특정 조건을 만족하지 못하는 경우 데코레이터 레벨에서 실행 중인 작업을 중단시킬 수도 있습니다.

데코레이터 패턴에 대한 기본적인 설명은 이 정도로 간략하게 마무리하겠습니다. 자세한 정보가 필요하신 경우 관련 서적 Design Patterns: Elements of Reusable Object-Oriented Software를 참고하시기 바랍니다.

Oracle Web Services Manager

데코레이터는 OWSM의 동작 방식을 이해할 수 있는 편리한 방편을 제공합니다. OWSM에서 정책은 중앙 집중적으로 정의됩니다. 이러한 정책들은 PreRequest, Request, Response, PostResponse 등의 4가지 파이프라인(pipeline)으로 분류됩니다. 각각의 정책 파이프라인은 정책 단계(policy step)에 의해 구성됩니다. 이 모든 구성 요소들은 OWSM 콘솔 상에서 개발 가능합니다. 파이프라인은 메시지 로깅과 같은 간단한 기능을 구현하고 있을 수 있습니다. 실제로 오라클이 제공하는 기본적인 정책은 메시지 로깅만을 위한 단순한 Request/Response 파이프라인으로 구성되어 있습니다.

이러한 정책들은 하부 웹 서비스에 대한 데코레이터로서 기능합니다. 정책은 하부 서비스를 전혀 수정하지 않은 상태에서, 클라이언트의 관점에서 바라본 서비스의 행동 양식을 수정하는데 활용됩니다.

OWSM은 정책을 서비스로 유연하게 매핑할 수 있는 환경을 제공합니다. OWSM의 정책은 개별 서비스 단위로 수동으로 매핑되거나, Java EE Servlet Filter와 유사한 형태의 URL 패턴으로 적용될 수 있습니다. 또 파이프라인 템플릿(pipeline template)을 이용하면 각 서비스 또는 URL의 셋업 작업을 반복하지 않고도 일련의 정책을 쉽게 적용할 수 있습니다.

OWSM 아키텍처

OWSM 정책 및 서비스 등록 정보는 데이터베이스를 기반으로 하는 OWSM Policy Manager에 중앙 집중적으로 저장됩니다. 정책과 서비스는 웹 기반 인터페이스를 통해 구현됩니다. OWSM의 정책은 중앙 집중적으로 구현, 저장되지만, 여러 실행 지점(enforcement point)에 개별적으로 적용이 가능합니다. "실행 지점"은 OWSM Policy Manager와의 커뮤니케이션을 통해 현재 정책 및 서비스 등록 정보를 가져 옵니다. 실행 지점은 게이트웨이와 에이전트의 두 가지 형태를 취합니다.

OWSM Gateway는 Oracle Application Server와 같은 Java EE 서버를 기반으로 실행되는 HTTP 프록시 서버입니다. 게이트웨이는 웹 서비스 요청을 수신하고, 정책을 적용하고(이 과정에서 메시지 처리가 수행됩니다), 다시 요청을 전달하는 프록시로서 기능합니다. 필요한 경우, 동기식 서비스에도 OWSM Gateway를 적용하는 것이 가능합니다. 게이트웨이는 정책 적용을 위한 가장 유용한 대안을 제공합니다. 웹 서비스 클라이언트 또는 서비스의 코드/설정 변경이 전혀 불필요하며, 단지 클라이언트가 호출하는 URL만 변경해 주면 됩니다. 예를 들어 http://jj620.ciscoinc.com:7777/orabpel/default/SampleService/1.0 URL을 호출하는 대신 http://jj620.ciscoinc.com:7777/gateway/services/SampleService URL을 호출하도록 변경할 수 있습니다.

OWSM Agent는 게이트웨이와 동일한 기능을 수행하지만 서비스 또는 클라이언트 애플리케이션에 직접 설치된다는 차이를 갖습니다. 에이전트는 "인-프로세스(in-process)" 방식으로 실행되며 웹 서비스 클라이언트 또는 서비스의 변경 작업이 필요합니다. 하지만 현재 Oracle Application Server Web 서비스 스택을 사용 중인 고객이라면 이 작업을 매우 쉽게 처리할 수 있습니다. 예를 들어 오라클 웹 서비스 클라이언트의 설정을 변경하려면 아래의 내용을 client-webservices.xml 파일에 추가하기만 하면 됩니다:

<runtime enabled="owsm">
<owsm init-home="C:\java\oc4j_101330\owsm\config\interceptors\C0003005"/> </runtime>

OWSM 사용자들이 알고 있어야 할 마지막 컴포넌트가 OWSM Monitor입니다. 이 컴포넌트는 실행 지점(enforcement point)으로부터 정보를 수집합니다. 수집된 정보는 성능, 에러, 통계 관리 등의 목적으로 취합되어 기록, 보고됩니다.

간단한 파이프라인

이제 Oracle Internet Directory(OID)와 같은 LDAP 디렉토리의 컨텐트를 기반으로 사용자를 인증하는 파이프라인을 구현해 보기로 합시다. 웹 서비스는 특정 권한을 갖는 엔드 유저에 의해서만 호출될 수 있다고 가정합니다. 가장 일반적인 방법은 액세스 레벨(read-only, edit, manage, admin 등)에 따라 LDAP 그룹을 관리하는 것입니다. 사용자들은 접근을 허용 받기 전에 이 그룹에 대한 확인 절차를 거칩니다.

이러한 인증 방식의 구현을 위해 클라이언트가 WS-Security SOAP Header에 저장된 인증 정보를 전달하도록 구현할 수 있습니다. 그 예가 아래와 같습니다:

<env:Envelope>
  <env:Header>
    <wsse:Security>
      <wsse:UsernameToken>
        <wsse:Username>jjones</wsse:Username>
        <wsse:Password>test</wsse:Password>
      </wsse:UsernameToken>
    </wsse:Security>
  </env:Header>
  <env:Body>
    ...
  </env:Body>
</env:Envelope>

OWSM에서는 이 인증 정보를 처리하기 위한 파이프라인을 구현하고 LDAP 디렉토리에서 확인하도록 할 수 있습니다. 먼저 앞에서 등록한 서비스에 정책을 할당해야 합니다. 이를 위해 네비게이션 바의 Manage Policies 링크를 클릭하고 방금 생성한 실행 지점의 옆에 위치한 Policies 링크를 클릭합니다.

다음으로, 방금 등록한 서비스의 편집 아이콘 옆에 위치한 edit를 클릭합니다. 이제 디폴트 파이프라인이 입력되었습니다.

다음 단계에서는 "Extract Credentials"를 추가해야 합니다. 이 단계에서 SOAP 메시지의 인증 정보를 가져오는 작업이 수행됩니다. Request 파이프라인의 log 단계에서 Add Step Below를 클릭합니다. New Step 대화상자에서 Extract Credentials을 선택하고 Ok를 클릭합니다.

인증 정보 추출 과정에서는 기본적으로 HTTP Basic 인증 정보를 확인하도록 설정됩니다. 여기서 HTTP를 WS-BASIC으로 변경하면 WS-Security UsernameToken 헤더를 대신 조회하도록 할 수 있습니다. 사용자 인증 정보의 추출 작업을 완료했다면 다음에는 인증 절차를 추가해야 합니다. 방금 생성한 Extract Credential 단계에서 Add Step Below 링크를 클릭하고 Ldap Authorize 단계를 선택한 다음 OK를 누릅니다. 지금까지 구현된 Request 파이프라인이 아래와 같습니다:

이 인증 정보 추출 과정에서는 기본적으로 WS-Security UsernameToken 헤더로부터 인증 정보를 조회하도록 설정됩니다. 사용자 인증 정보의 추출 작업을 완료했다면 다음에는 인증 절차를 추가해야 합니다. 방금 생성한 Extract Credential 단계에서 Add Step Below 링크를 클릭합니다. Ldap Authorize 단계를 선택한 다음 Ok를 누릅니다. 지금까지 구현된 Request 파이프라인이 아래와 같습니다:

마지막으로 Authorize 단계를 설정해야 합니다. Authorize 단계의 Configure 링크를 클릭하고 로컬 LDAP 서버를 참조하도록 속성을 편집합니다. LDAP baseDN은 사용자와 그룹을 모두 포함하는 컨테이너로 설정되어야 합니다(예: "dc=ciscoinc,dc=com") ServiceRoles는 서비스 호출이 허용된 LDAP 그룹을 포함하는 CSV 포맷의 목록으로 작성됩니다. 마지막으로 LDAPAdminDN에 입력된 내용을 지우고 LDAPAdminLoginEnabled를 false로 설정합니다.

Ok를 누르고 다시 Next > Save를 누릅니다. 이제 Commit을 눌러 정책을 저장합니다. 이것으로 사용자 인증 정보를 이용한 웹 서비스의 데코레이션 작업을 완료하였습니다.

커스텀 단계의 작성

OWSM은 웹 서비스의 보안 및 모니터링을 위한 다양한 단계(step)들을 기본적으로 제공하고 있습니다. 이 요소를 애플리케이션 아키텍처의 핵심 기반으로 활용할 수도 있습니다. Java EE 개발자들이라면 Java 서브렛 필터에 익숙할 것입니다. 이 필터들은 Java 서브렛, JSP, 웹 애플리케이션 프레임워크의 데코레이션을 위해 사용됩니다. OWSM 커스텀 단계(custom step)는 SOAP 헤더와 본문에 대한 접근 권한을 가집니다. 여기서 페이로드를 변환하거나 XML 엘리먼트를 추가, 수정, 삭제할 수 있습니다.

이번 섹션에서는 GUID(Globally Unique Identifier)를 포함하는 커스텀 SOAP 헤더를 추가하는 커스텀 단계를 구현해 보겠습니다. GUID는 길이가 긴 랜덤 숫자 또는 문자열로 구성되는 식별자입니다. 확률적으로는 GUID가 중복될 가능성이 존재하지만, 가능한 수의 조합을 생각할 때 실제로 이러한 경우가 발생할 가능성은 전혀 없다고 보아도 무방합니다. 또 많은 경우, MAC 주소 또는 IP 주소를 이용하여 GUID를 설정하는 방법이 사용되기도 합니다. 추가해야 할 헤더의 예가 아래와 같습니다:

<env:Envelope>
  <env:Header>
    ...
    <otn:GUID>
      B72C0BF6-F795-5A04-4D2F-AA24F2DD9888
    </otn:GUID>
    ...
  </env:Header>
  <env:Body>
    ...
  </env:Body>
</env:Envelope>

GUID는 매우 유용하게 활용됩니다. 그 가장 일반적인 예로 트러블슈팅 작업을 생각할 수 있습니다. 하나의 웹 서비스 메시지가 여러 컴포넌트들을 거치면서 각각의 컴포넌트가 관리하는 로그에 저장되는 경우가 많습니다. 이때 메시지 ID를 로그에 함께 남겨 둔다면 grep과 같은 커맨드 라인 도구를 이용하여 쉽게 트러블슈팅 작업을 수행할 수 있을 것입니다. 다행스럽게도 OWSM은 내부적으로 GUID를 기본 생성합니다. 여기에서는 이 정보를 SOAP 헤더에 추가하기로 합니다.

그 첫 번째 단계로 Oracle JDeveloper에서 새로운 프로젝트를 생성합니다. 그리고 프로젝트 속성으로 이동하여 Libraries 옵션을 클릭합니다. Classpath에 아래의 JAR 파일들을 추가합니다:

$SOA_HOME/owsm/lib/coresv-4.0.jar
$SOA_HOME/owsm/lib/extlib/axis.jar
$SOA_HOME/owsm/lib/extlib/saaj.jar

When you're done it should look approximately like this:

이제 com.cfluent.policysteps.sdk.AbstractStep을 확장하는 새로운 Java 클래스를 생성합니다. 이때 JDeveloper에서 기본적으로 요구되는 메소드가 구현되어 있지 않다는 에러 메시지를 표시하게 됩니다. Quick Fix를 클릭하고 Implement Methods...를 선택합니다 . 이것으로 IResult execute(IMessageContext context) 메소드를 위한 스텁(stub)이 구현되었습니다. execute 메소드는 커스텀 단계가 파이프라인으로 적용될 때 OWSM 엔진에 의해 실행됩니다. 여기서 OWSM에게 작업 진행 여부를 통보하기 위해서는 IResult의 인스턴스를 반환해야 합니다. 일반적으로 execute 메소드는 IResult.SUCCEEDED를 반환하여 OWSM에 파이프라인의 처리를 계속하라는 신호를 보냅니다.

이 메시지와 관련 메타데이터는 IMessageContext의 커스텀 단계로 전달됩니다. 이 단계에서는 일반적으로, SOAP 메시지를 가져오기 위해 context.getRequestMessage() 또는 context.getResponseMessage()가 호출됩니다. 또 메시지의 처리를 위해 아래와 같이 com.cfluent.pipelineengine.container.MessageContext에 IMessageContext를 전달해야 합니다.

&
...
String stage = context.getProcessingStage(); // processing stage indicates if this is a request or a response
MessageContext msgContext = (MessageContext)context;
SOAPMessage message = null;
if (IMessageContext.STAGE_PREREQUEST.equals(stage) || IMessageContext.STAGE_REQUEST.equals(stage)) {
  message = msgContext.getRequest(); // getting the Axis message
} else if (IMessageContext.STAGE_RESPONSE.equals(stage) || IMessageContext.STAGE_POSTRESPONSE.equals(stage)) {
  message = msgContext.getRequest(); // getting the Axis message
} 
...

다음 단계에서는 SOA 헤더를 메시지에 추가합니다. 이를 위해, 먼저 SOAPEnvelope의 핸들을 가져와야 합니다. (여기서 "Axis" SOAP Envelope이 사용됨에 유의하십시오.)

아래 코드에서 몇 가지 참고가 필요한 부분을 설명합니다. 먼저, SOAPEnveleope와 그 밖의 엘리먼트들을 Axis 버전으로 전달해야 합니다. Axis 버전만이 페이로드의 수정을 허용하기 때문입니다. 두 번째로, 페이로드를 수정한 다음에는 setDirty(true)를 호출하여 페이로드가 변경되었다는 사실을 OWSM 엔진에 알려야 합니다. 필자의 경험에 의하면, 이 메소드는 자식 엘리먼트(child element)들을 재귀적으로 "dirty"로 마킹하지 않는 것으로 보입니다. 이러한 사실을 확인해 주는 문서는 아직 확인하지 못했지만, 여러 레벨에 대해 변경 작업을 수행했다면 각 레벨의 엘리먼트에 대해 별도로 setDirty(true)를 호출할 필요가 있어 보입니다.

...
// now get the GUID and build a SOAPElement
try {
  SOAPEnvelope env = message.getAxisMessage().getSOAPEnvelope();
  // next build header element with a name of GUID
  SOAPHeaderElement element =
    (SOAPHeaderElement)env.getHeader().addChildElement("GUID", "otn", "http://otn.oracle.com/owsmarticle");
  element.addTextNode(context.getGUID()); // add the guid that is generated by OWSM

  env.setDirty(true); // need to let OWSM know that we've changed the payload.
} catch (Exception e) {
  logger.log(Level.SEVERE, "exception occurred while adding GUID header", e);
}
...

마지막으로 성공 메시지를 로그에 기록하고 SUCCEEDED IResult를 반환합니다. 로깅 작업은 별도의 OWSM 라이브러리를 통해 수행되어야 합니다. 이 단계가 여러 대의 머신에서 수행될 수 있기 때문에 별도의 로그 파일 또는 System.out/err에 로그를 기록하는 것은 바람직하지 않습니다. OWSM이 제공하는 ILogger 클래스는 Log4J 또는 Java에 내장된 로깅 클래스와 매우 유사한 방식으로 동작합니다. 마지막으로 수퍼 클래스에서 AbstractStep.createMethod(...)를 호출하여 OWSM에 반환할 결과를 생성합니다.

...
logger.log(Level.INFO, "successfully added GUID {" + context.getGUID() + "} to SOAP message.");
return createResult(IResult.SUCCEEDED);
...

이 단계의 전체 코드는 샘플 코드에서 확인할 수 있습니다.

커스텀 단계의 패키징

커스텀 단계를 적용(deploy)하기 위해서는 두 가지 아티팩트(artifact)를 준비해야 합니다. 그 하나가 위에서 준비한 클래스의 jar 파일이고, 또 하나는 OWSM을 위한 커스텀 스텝 템플릿(custom step template)입니다. 스텝 템플릿은 사용되는 클래스, 관리자에게 제공되는 정보, 단계의 설정에 사용되는 속성 등을 기술하는 XML 디스크립터 파일입니다. 스텝 템플릿의 예가 아래와 같습니다:

<csw:StepTemplate
  ..
  name="GUIDStep" 
  ...
>
  <csw:Description>Custom step that adds GUID SOAP header</csw:Description>
  <csw:Implementation>oracle.otn.guidstep.GUIDStep</csw:Implementation>

  <csw:PropertyDefinitions>
    <csw:PropertyDefinitionSet name="Basic Properties">
      <csw:PropertyDefinition name="Enabled" type="boolean">
        <csw:Description>If set to true, this step is enabled</csw:Description>
        <csw:DefaultValue>
          <csw:Absolute>true</csw:Absolute>
        </csw:DefaultValue>
      </csw:PropertyDefinition>
    </csw:PropertyDefinitionSet>
  </csw:PropertyDefinitions>
</csw:StepTemplate>

커스텀 단계의 적용

커스텀 단계를 적용하기 위해서는 먼저 jar 파일을 적용한 후 스텝 템플릿을 업로드해야 합니다. jar 파일을 적용하는 방법은 간단합니다. $OWSM_HOME/lib/custom 디렉토리에 파일을 복사하기만 하면 됩니다. OWSM 단계가 여러 게이트웨이, 에이전트에 동시 적용될 수 있으므로, 이 작업을 정책 파이프라인이 적용되는 모든 서버에서 반복적으로 수행해야 합니다. 이 작업을 마친 후에는 애플리케이션 서버를 재시작합니다.

다음으로 정책 템플릿을 업로드할 차례입니다. Web Services Manager 콘솔에 로그인한 다음 구현된 실행 지점(enforcement point) 옆에 위치한 Steps 링크를 누릅니다. Add New Step 버튼을 누른 다음 XML 스텝 템플릿을 업로드합니다.

이 작업 역시 각각의 실행 지점에 대해 수행해 주어야 합니다. 이제 이 단계를 정책 파이프라인에 추가할 수 있습니다.

커스텀 단계의 개발을 위한 팁

커스텀 단계의 개발 과정에서 코딩-적용-테스팅 사이클이 작업 진행의 걸림돌이 될 수 있습니다. 일반적으로 커스텀 단계를 변경하고 나면 새로운 클래스를 가져오기 위해 OC4J를 재시작해야 합니다. Oracle SOA Suite가 완전하게 설치된 환경이라면 OC4J 재시작 과정에서 많은 지연이 발생할 수 있습니다.

이 과정을 단축하기 위해 Java Platform Debugging Architecture(JPDA)를 활용할 수 있습니다. JDPA를 이용하면 JDeveloper에서 OC4J에 직접 연결할 수 있으며, 이를 통해 몇 가지 이점을 기대할 수 있습니다. 먼저, 커스텀 단계를 개별 코드 단위로 실행하면서 변수 값을 확인하는 것이 가능합니다. 두 번째로(그리고 가장 중요한 장점으로), JDeveloper에서 코드를 변경한 후 핫-스왑 방식으로 코드를 적용할 수 있습니다. JDeveloper는 JVM에 코드를 적용한 후 적용된 코드를 바로 이용합니다. 이러한 방식은 jar 파일을 구현하고, 커스텀 lib 디렉토리로 전송하고, 서버를 재시작하는 번거로운 절차에 비했을 때 훨씬 효율적입니다.

위 방법을 적용하려면, 먼저 OC4J가 JPDA 디버거에 대해 리스닝을 수행하도록 설정해야 합니다:

  1. Enterprise Manager(http://{hostname}:{port}/em)를 열고 oc4j_soa 인스턴스를 클릭합니다.
  2. Administration탭을 클릭합니다.
  3. Server Properties를 클릭합니다.
  4. 커맨드 라인 옵션에서 아래 라인을 추가합니다: -Xrunjdwp:transport=dt_socket,server=y,address=9901,suspend=n
  5. Apply를 클릭하고 OC4J 서버를 재시작합니다.

다음으로, JDeveloper 프로젝트에 대해 원격 디버깅을 설정해 줍니다:
  1. 프로젝트를 마우스 오른쪽 버튼으로 클릭하고 Project Properties를 선택합니다.
  2. Run/Debug 옵션을 클릭합니다.
  3. 디폴트 실행 설정을 클릭하고 Edit...를 누릅니다.
  4. Launch Settings 옵션에서 Remote Debugging and Profiling 체크박스를 체크합니다.
  5. Debug > Remote 옵션을 선택합니다.
  6. 아래와 같이 옵션을 입력합니다:

  7. OK를 누릅니다.
프로젝트를 마우스 오른쪽 버튼으로 클릭하여 OC4J 인스턴스에 연결하고 Start Remote Debugger를 선택합니다:

이제 JDeveloper가 OC4J에 연결되었습니다. 코드에 브레이크포인트를 삽입하고 서비스를 실행하면 브레이크포인트에서 실행이 일시 중단됩니다. 또 변경 사항을 적용한 후 "Alt-Shift-F9"(컴파일)를 누르면, 코드가 JVM에 적용되고 바로 테스트를 재수행할 수 있습니다.

이 단계의 전체 코드는 샘플 코드에서 확인할 수 있습니다.

결론

웹 서비스를 개발하는 과정은 매우 복잡할 수 있습니다. 또 재활용 가능한 공통 코드와 추상화된 기능을 기반으로 웹 서비스를 설계하는 작업은 더욱 복잡합니다. 데코레이터 패턴을 이용하면 복잡한 코딩/테스팅/적용 사이클을 거치지 않고 웹 서비스에 계층화된 기능을 유연하게 구현할 수 있습니다. 커스텀 단계를 개발하고 정책에 적용함으로써, 보다 단순화된 환경에서 구체적인 코드가 아닌 비즈니스 기능에 집중하면서 웹 서비스 프로젝트를 진행할 수 있을 것입니다.


Jason Jones (jason.jones@zirous.com)는 오라클 파트너 업체인 Zirous에서 근무하는 시스템 아키텍트입니다. 그는 Enterprise Java, SOA 솔루션을 전문 분야로 하고 있으며 오라클의 SOA 고객 자문 위원회 위원으로 위촉되기도 하였습니다. 제이슨은 오라클 ACE 디렉터이며 Java, SOA, 오라클 등을 주제로 한 블로그를 운영하고 있습니다.

E-mail this page
Printer View Printer View