IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
메인 컨텐츠로 가기

한국 developerWorks  >  오픈 소스 | Information Management | 자바 | 웹 개발  >

Google Web Toolkit, Apache Derby, Eclipse를 사용하여 Ajax 애플리케이션 구현하기, Part 3: 커뮤니케이션(Communication) (한글)

Remote Procedure Calls (RPC)와 Google Web Toolkit

developerWorks
문서 옵션

JavaScript가 필요한 문서 옵션은 디스플레이되지 않습니다.


제안 및 의견
피드백

난이도 : 중급

Noel Rappin, Senior Software Engineer, Motorola, Inc.

2007 년 4 월 17 일

지난 시간 두 편의 기술자료에서는 Google Web Toolkit (GWT)과 Apache Derby 관계형 데이터베이스를 사용하여 웹 애플리케이션을 구현하는 방법에 대해 배웠습니다. Part 1에서는 GWT를 사용하여 Slicr라고 하는 피자 배달 시스템용 프론트엔드를 구현하는 방법을 설명했고, Part 2에서는 Derby를 사용하는 관계형 데이터베이스의 생성 데모와 데이터베이스 행을 자바™ 객체로 변환하는 방법을 설명했습니다. 이제 더욱 재미있는 부분으로 들어가고자 합니다. 세 번째 기술자료에서는 클라이언트와 서버가 서로 통신하게 하는 방법을 설명합니다. GWT 내에서 Remote Procedure Call (RPC)을 사용하여 자바 메소드 호출만큼 간단하게 서버에서 데이터를 가져옵니다.

Ajax의 "A"

Asynchronous JavaScript + XML (Ajax)-실행 웹 애플리케이션과 전통적인 웹 애플리케이션의 주요한 차이점은 비동기성(asynchronous)이다. Ajax 애플리케이션에서는 브라우저가 전체 페이지를 완전히 리프레시(refresh) 할 필요 없이 페이지의 특정 부분만 업데이트 할 수 있다. 이렇게 간단한 트릭을 통해 대화식의(interactive) 사용자 경험이 가능하며, 간단한 웹 페이지가 데스크탑 애플리케이션과 훨씬 더 비슷하게 작동한다.

개발자 관점에서 볼 때, 이러한 비동기식 작동에는 두 가지 핵심 컴포넌트가 있다.

  • XMLHttpRequest 객체는 브라우저에 의해 정의된 JavaScript 객체로서 웹 페이지가 백그라운드 쓰레드에서 HTTP 요청을 보내고 응답을 받을 수 있다. 전형적인 페이지 요청과는 달리, 이 호출은 사용자의 사용 환경(User Experience)을 인터럽트 하지 않고, 브라우저는 응답을 기다리는 동안 중지되지 않는다.
  • 응답이 완료된 후에 콜백이 실행된다. 이 콜백은 JavaScript Document Object Model (DOM)을 사용하여 새로운 데이터에 기반한 페이지의 엘리먼트들을 조작한다. 신선한 무엇인가가 스크린에 펼쳐지고, 사용자는 행복해 진다.
Ajax 리소스 센터에서는 Ajax 프로그래밍 모델과 관련한 기술자료, 튜토리얼, 포럼, 블로그, wiki, 이벤트, 뉴스 등이 제공된다.

기본적인 흐름은 다음과 같다. 서버로 호출하고, 응답이 돌아오고, 그 응답에 기반하여 페이지에 액션이 수행된다. 여기에서 기억해야 할 중요한 사항은 브라우저에서 페이지를 변경할 때 일반적으로 기다리고 리프레시 할 필요 없이 뒤에서 이 모든 일이 발생한다는 점이다.

XMLHttpRequest/DOM 메소드와 관련하여 한 가지 문제가 있다. 각 브라우저는 한 가지를 위해 관련 JavaScript 객체들을 다르게 실행한다. 다르게는, 데이터를 서버로 가져가는 것이 어색한 것처럼, 응답을 유용한 데이터로 변환하는 것도 그럴 수 있다. 결과적으로, 모든 Ajax 프레임웍은 RPC의 생성, 호출, 관리와 관련된 일정한 종류의 단순화 래퍼를 갖고 있다. GWT도 예외는 아니다.

GWT는 Enterprise JavaBeans (EJB) 기술을 연상할 수도 있지만, 다행히도 훨씬 단순한 다중 인터페이스 인프라스트럭처로 RPC를 관리한다. 여러분은 시스템이 만들어 내는 원격 호출 리스트를 정의해야 한다. GWT는 데이터를 변환하여 서버로 보내고, 서버 호출을 만들고, 리턴된 데이터를 클라이언트 데이터로 변환한다. 여러분은 원격 호출이 완전히 리턴된 후에 무엇을 해야 할 지를 정의한다. 일반적인 자바 메소드 호출만큼 쉬운 것은 아니지만, 어렵지도 않다.

Slicr 애플리케션에서 변경해야 할 것은 서버 데이터베이스에서 토핑의 초기 리스트를 가져오는 것이다. 이들을 클라이언트 코드로 하드 코딩 하지 않는다. 이것은 RPC 호출을 만드는데 전체 과정 중 단순한 예제이다. 이 예제를 쉽게 실행하려면, Hosted Mode에서 실행한다. Hosted Mode에서, GWT는 원격 호출을 자동으로 시뮬레이트 한다. Eclipse나 IntelliJ IDEA 같은 통합 개발 환경(IDE)를 사용한다면 Hosted Mode에서 작업이 더욱 쉬워진다.

주: 본 시리즈 마지막 기술자료에서는 정상적인 서블릿 환경에서 서버 측을 실행하는 방법을 설명하겠다.

서비스 정의하기

GWT 프로시저 호출에서 많은 작업들이 두 개의 클래스에서 발생한다. 서버 측에서는, RemoteServiceServlet의 서브클래스를 정의한다. 이 클래스에서, 서버 데이터를 조작하고 값을 클라이언트로 리턴한다. (또는 예외를 던질 수도 있지만, 이 상황에서는 긍정적인 측면만 보도록 하자.) 클라이언트 측에서는, AsyncCallback 인터페이스를 실행하는 클래스를 정의한다. 이 클래스에서, 서버가 실행될 때 데이터(또는 예외)를 사용하여 클라이언트 페이지가 무엇을 하는지를 정의한다. 이러한 두 개의 클래스 외에 GWT가 클라이언트 측 클래스와 서버 측 클래스를 바인딩 할 수 있도록 글루 코드를 작성해야 한다. 비교적 많은 글루 코드가 있다. 글루 코드는 두 개의 다른 인터페이스와 몇 개의 클라이언트 측 코드와 하나 또는 두 개의 설정으로 구성된다. 하지만 걱정하지 말라. 대부분의 코드는 공통적인 것이고, 단계를 따라가면 괜찮을 것이다.

우선 서버 측면부터 시작한다. 목표는 이전 기술자료의 끝 부분에서 데이터베이스에 두었던 모든 토핑 리스트를 생성하는 것이다. 서버는 이전 기술자료(Listing 1 참조)에서 ObjectFactory를 사용한다.


Listing 1. 토핑 서비스 실행
                
public class ToppingServiceImpl extends RemoteServiceServlet 
		implements ToppingService {

	public static final String DRIVER = "org.apache.derby.jdbc.EmbeddedDriver";

	public static final String PROTOCOL = "jdbc:derby:slicr;";

	public List getAllToppings() {
		try {
			Class.forName(DRIVER).newInstance();
			Connection con = DriverManager.getConnection(PROTOCOL);
			Statement s = con.createStatement();
			ResultSet rs = s.executeQuery(
			        "SELECT * FROM toppings order by name");
			return ObjectFactory.convertToObjects(rs, Topping.class);
		} catch (Exception e) {
			e.printStackTrace();
			return new ArrayList();
		} finally {
			try {
				DriverManager.getConnection("jdbc:derby:;shutdown=true");
			} catch (SQLException ignore) {}
		}
	}

}

이 코드에 대해 알아야 할 것은 아무것도 없다는 것이다. GWT 원격 서비스에 대한 요구 사항은 RemoteServiceServlet을 확장하고 여기에서 두 개의 패러그래프로 생성할 인터페이스를 실행하는 것이다.

주: 대부분의 GWT 문서에서 설명하듯, 먼저 인터페이스를 만들어야 한다. 이것은 문제가 되지 않는다. 구체적인 코드를 먼저 사용하면 더 명확해 진다고 생각한다.

그 무엇보다도, 이 코드는 이 전 글의 ToppingTestr 예제와 같은 기능을 갖고 있다. 이것은 GWT 내에서 사용할 용도로 재패키징 되었다. 여러분은 Derby 데이터베이스로 호출하고, 객체 팩토리를 사용하여 토핑 객체들을 생성하고, 이들을 리턴한다.

글루 코드

인터페이스들을 정의해야 한다. 첫 번째는 Listing 1에 사용된 ToppingService 인터페이스이다. 매우 단순하다. (Listing 2)


Listing 2. 첫 번째 관련 인터페이스, ToppingService 정의하기
                public interface ToppingService extends RemoteService {
	public List getAllToppings();
}

여러분이 했던 일은 실제 구체적인 클래스의 메소드에 사용했던 것과 같은 서명을 사용하는 것이다. 주요한 제약 조건은 여러분의 인터페이스가 com.google.gwt.user.client.rpc.RemoteService;를 확장해야 한다는 점이다. 또한, 여러분의 매개변수와 리턴 값은 GWT가 직렬화 할 수 있는 유형이어야 한다. 그러한 유형 리스트가 Part 2에 나와있다. 하지만, 단 하나의 서비스 인터페이스 버전을 갖는 것으로는 충분하지 않다. 비동기식 버전의 인터페이스도 정의해야 한다. (Listing 3)


Listing 3. 비동기식 버전의 인터페이스 정의하기
                public interface ToppingServiceAsync {
	public void getAllToppings(AsyncCallback callback);
}

비동기식 버전의 서비스 인터페이스는 위에 설명했던 기본 버전에서 파생된다. 두 가지 버전이 같은 패키지에 있어야 하며, 그 패키지는 GWT 클라이언트 코드에 나타나야 한다. (필자는 com.ibm.examples.client를 사용했다.) 비동기식 버전에 있는 클래스 이름은 끝에 추가된 Async 스트링을 가진 원래의 인터페이스의 이름이다. 원래의 인터페이스에 있는 각각의 메소드의 경우, 비동기식 버전은 void로 수정된 리턴 유형과 매칭 메소드와 AsyncCallback 유형의 추가 매개변수를 갖고 있어야 한다. 클라이언트 측 코드는 AsyncCallback을 사용하여 서버 응답 시 작동한다.

여러분은 하나의 서비스 인터페이스에 원하는 만큼 많은 메소드를 가질 수 있다. 이들이 모두 비동기식 버전에 형제(sibling)를 갖고 있고, 모두 같은 원격 서비스 클래스에서 구현된다면 말이다. 서비스를 구성하는 방법은 미학적인 측면을 고려해서 내린 결정이다. 비록 서비스 메소드가 서버 측에 공통 데이터를 공유하는 것이 더욱 가치가 있는데도 말이다.

한 가지 더, 여러분의 서버 코드가 등록될 것이다. 다음 라인을 Slicr.gwt.xml 파일에 추가한다.

<servlet path="/toppings" class="com.ibm.examples.server.ToppingServiceImpl"/>

위 라인은 구체적인 원격 서비스 클래스의 완전한 전체 이름을 이 서비스에 대한 URL이 되는 경로 이름과 쌍을 맞춘다. 여러분이 기억을 하고 나중에도 일관성을 유지한다면 원하는 이름을 사용할 수 있다. 기술적으로, Hosted Mode에서 애플리케이션을 실행한다면 .xml 파일에서 이 라인만 추가해야 한다. 웹 개발 시, web.xml 파일에 이와 비슷한 무엇인가가 필요하다.

단일 호출에 일관성을 유지해야 할 것들이 많이 있다. 인터페이스, 비동기식 인터페이스, 실제 구현, 모듈 파일이 그것이다. 하나를 소실하거나, 부정확한 매치는 서비스를 실제로 실행할 때 에러가 생길 수 있다. 이 글을 쓰고 있는 현재, 적어도 한 개의 IDE(IntelliJ IDEA)는 이 모든 아이템들을 일관성 있는 상태로 유지할 수 있는 에디터를 지원한다. 여러분이 Eclipse를 사용한다면 Googlipse 플러그인 역시 이와 비슷한 지원을 한다.

클라이언트 측 호출

서버 측을 관리하면서, 클라이언트가 프로시저 호출(procedure call)을 할 차례이다. 기본 개념은 어떤 원격 서비스를 호출하고 있는지를 알려주는 것이다. AsyncCallback 객체를 하늘로 보낸다. 결국, GWT는 이것을 여러분에게 다시 보내고, 여러분은 그 결과에 대해 작업할 수 있다. Listing 2는 설정과 호출용 코드이다. 이 메소드는 Part 1Slicr 클래스에 있는 것이다. 특히, 이 메소드에 대한 호출은 토핑 패널을 추가했던 Slicr.onModuleLoad()의 라인을 대체한다.


Listing 4. 설정과 호출
                
public void callForToppings() {
	ToppingServiceAsync toppingService = 
		(ToppingServiceAsync) GWT.create(ToppingService.class);
	ServiceDefTarget target = (ServiceDefTarget) toppingService;
	String relativeUrl = GWT.getModuleBaseURL() + "toppings";
	target.setServiceEntryPoint(relativeUrl);
	toppingService.getAllToppings(new ToppingCallback());
}

각 라인은 GWT 호출에 있어서 중요한 단계이다. 다음은 체크리스트이다.

  1. 비동기식 인터페이스의 인스턴스를 만든다. 일반적으로, 인터페이스의 인스턴스를 만들 수 없다. 이것은 GWT.create() 호출이 들어오는 장소이다. GWT 클래스는 여러 유틸리티들이 들어있는 곳이다. 이 경우, 여러분은 주어진 인터페이스를 실행하는 프록시 객체를 생성할 수 있다. 전개 시, 이 메소드에 대한 인자는 변수 보다는 리터럴(literal)이 되어야 한다. 변수는 Hosted Mode에서 작동되기 때문에 조심해야 한다.
  2. GWT.create()가 리턴하는 프록시 객체는 또 다른 인터페이스인 ServiceDefTarget을 실행한다. 한 줄 또는 두 줄로, 그 인터페이스에 메소드가 필요하다.
  3. 메시지를 보내야 하는 URL을 결정한다. 이 URL은 두 개의 컴포넌트를 갖고 있다.
    • 시스템용 기본 URL인 GWT.getModuleBaseURL()을 호출하면 얻을 수 있다.
    • 서블릿을 .xml 파일에 추가했을 때 경로로 사용되었던 것과 동일한 스트링.
  4. 전체 URL이 생겼다면, 이 특별한 URL이 setServiceEntryPoint() 메소드를 호출함으로써 특정 서비스로 갈 장소라는 것을 GWT에게 알릴 수 있다. 이 인터페이스에는 관련 게터(getter) 메소드도 들어있다. 이 시점에서, 서비스는 클라이언트 페이지에서 완전히 사용할 수 있게 된다.
  5. 마지막으로, 서비스가 이것을 정의한 것처럼 서비스로 실제 호출을 할 수 있다. (ToppingCallback 객체의 경우는 예외이다. 이것은 다음 섹션에서 설명하겠다.)

여러분은 클라이언트 페이지에서 한번 서비스(이 예제의 처음 네 줄)만 시작해야 한다. 서비스가 생성되고 이 엔트리 포인트로 연결된 후에, 같은 서비스 객체를 재사용 하여 서버로 다중 호출을 한다. 여러분은 또한 헬퍼(helper) 메소드를 생성하여 서버 객체의 기본적인 초기화를 단순화 할 수 있다.

비동기성(asynchrony)의 장점

전에 언급했듯이, GWT RPC와 일반 메소드 호출과의 큰 차이점은 메소드 호출이 언제 완료되는지 또는 완료되는지의 여부를 알 수 없다는 점이다. 웹 사용자들은 코너에 있는 선택 박스를 리프레시 하는 동안 전체 페이지를 동결시킬 필요가 없기 때문에, GWT 프로그램은 원격 호출을 한다는 점이다. 결국, 서버는 응답을 보충하고, 해당 지점에서만GWT 코드는 여러분이 계획했던 놀라운 일을 수행한다.

이러한 유형의 환경에서 백그라운드 쓰레드를 트래킹 하는 것은 도전이 될 수 있다. 하지만, 여러분의 경우에, GWT는 위에서 설명했던 메커니즘으로 대부분의 작업을 수행하고, 여러분이 이미 보았던 AsyncCallback 인터페이스를 수행한다. GWT는 여러 쓰레드를 처리하는 오버헤드를 효과적으로 줄일 수 있다.

AsyncCallback 인터페이스는 두 개의 메소드, OnSuccess(Object obj)OnFailure(Throwable t)를 정의한다. 여러분은 이 두 개를 실행하는 클래스를 정의해야 한다. 클래스의 인스턴스를 생성하여, 원격 호출을 할 때 비동기식 서비스 메소드로 전달한다. (Listing 4) 결국, 서버 측 리소스는 완전하고, 두 개의 메소드들 중 하나가 호출된다. 성공 메소드에 대한 인자는 인터페이스와 실행에서의 호출의 리턴 값이다. 이 경우, 토핑 리스트가 된다. 여러분은 이것을 예정된 유형으로 던지고 새로운 데이터를 사용하여 작업을 하나. GWT는 Java 1.5를 곧 지원할 예정이며, 많은 캐스팅(casting)의 필요도 줄어들 것이다. 서버 측 코드가 실패하면, 실패 메소드가 예외와 함께 인자로서 호출된다. 이제 여러분은 자유롭게 이러한 예외에 대응할 수 있다.

Listing 5의 코드는 Listing 4에서 언급한 ToppingCallback 클래스를 보여주고 있다. 앞서 언급했듯이, 여러분은 익명의 내부 클래스(anonymous inner classes)로서 정의된 AsyncCallback 클래스를 자주 보게 될 것이다. 이것을 피하는 것이 좋다. 이 클래스에 이름이 있고, 완전히 다른 코드 블록의 중간에 배치되어 있지 않다면 여러분의 코드는 훨씬 더 읽기 쉽고 테스트도 쉬울 것이다.


Listing 5. ToppingCallback 클래스
                
public class ToppingCallback implements AsyncCallback {

	public void onFailure(Throwable caught) {
	    GWT.log("Error ", caught);
		caught.printStackTrace();
	}

	public void onSuccess(Object result) {
		List allToppings = (List) result;
		VerticalPanel toppings = new VerticalPanel();
		toppings.add(new HTML("<h2>Toppings</h2>"));
		Grid topGrid = new Grid(allToppings.size() + 1, 3);
		topGrid.setText(0, 0, "Topping");
		topGrid.setText(0, 1, "Left");
		topGrid.setText(0, 2, "Right");
		for (int i = 0; i < allToppings.size(); i++) {
			Topping t = (Topping) allToppings.get(i);
			Button button = new Button(t.getName() );
			CheckBox leftCheckBox = new CheckBox();
			CheckBox rightCheckBox = new CheckBox();
			clearables.add(leftCheckBox);
			clearables.add(rightCheckBox);
			button.addClickListener(new ToppingButtonListener(leftCheckBox,
					rightCheckBox));
			topGrid.setWidget(i + 1, 0, button);	
			topGrid.setWidget(i + 1, 1, leftCheckBox);
			topGrid.setWidget(i + 1, 2, rightCheckBox);
		}
		toppings.add(topGrid);
		panel.add(toppings, DockPanel.EAST);
	}
	
}

ToppingCallback 클래스의 인스턴스는 비동기식 인터페이스에 의해 보내진다. GWT는 이것을 먼저 뒤에서 관리하고, 해당 호출이 서버에 이루어지고, 서버 호출의 결과가 콜백 객체에 주어지고, 마지막으로 알맞은 응답 메소드가 실행된다.

이 경우, 실패 옵션은 기술하기가 더 쉽다. 서버 측 메소드가 어떤 이유로든 예외를 던지면, 콜백의 컨트롤은 onFailure() 메소드로 전달한다. 이 메소드에 대한 인자는 예외 또는 서버 측이 리턴 했던 기타 객체이다. 이 인자는 가능하다면, 실패에 대해 성의 있게 응답할 수 있는 기회를 준다. 필자는 GWT.log() 메소드를 사용하는데, 이것은 인자로서 스트링과 Throwable을 취한다. 이 메소드는 로그 메시지를 GWT Hosted Mode용 쉘 윈도우에 프린트 한다. 마찬가지로, 이것은 Hosted Mode에서만 실행한다. Web Mode에서, 이 메소드는 무시된다. GWT.log() 메소드는 개발하는 동안 트래킹을 수행한다. 여러분은 아마도 이 메소드를 사용하여 리프레시 할 스크린의 일부에 디폴트 메시지 또는 에러 메시지를 두거나, 전체 브라우저를 에러 스크린으로 리다이렉션(redirection) 하게 될 것이다.

본 시리즈의 첫 번째 기술자료를 읽었다면, onSuccess() 코드는 원래 버전의 buildToppingTypePanel() 매소드와 거의 동일하다는 것을 바로 알 수 있을 것이다. 그의 차이는 미미하다. 어레이 대신에 리스트를 순환하고, 이는 루프 컨트롤에 미미한 변화를 만들어 낸다. 필자는 토핑의 가격을 디스플레이에 추가했으며, 그 역시 데이터베이스에 필요하다.

이전 버전의 코드는 호출하는 코드로 toppings 패널을 리턴할 수 있었고, 호출하는 코드는 toppings 패널을 부모 패널에 추가할 수 있었다. 이 코드에서, 여러분은 값을 리턴할 수 없기 때문에, 새롭게 생성된 토핑 패널을 이 메소드의 일부로서 부모에 추가해야 한다. 이것이 실행될 때, Pizza 패널이 처음 나타나게 된다. GWT가 서버 호출을 시뮬레이트 하느라 약간 지연된 후에, Toppings 패널이 나타난다. (그림 1)


그림 1. New Topping 패널
New Topping 패널

이 작은 예제에서 조차도, 비동기식 구조로 옮길 때 코드의 전체 구조에 심각한 영향을 미친다. 하위 패널들이 메인 패널에 추가되는 순서는 부모 안의 각 하위 패널의 최종 모양을 결정하는데 중요했다. RPC 호출로 진행해 가면서, 순서대로 생성되는 하위 패널에 더 이상 의존할 수 없다. 패널의 초기 생성과 이들 안에 위젯을 삽입하는 것을 분리해야 한다. (이렇게 하면 새로운 데이터가 추가될 때 깜박거림이 줄어드는 효과가 있다.) 일반적인 목적은 비동기식 콜백 내에 코드를 가능한 단순히 유지하고, 서로서로 연결되지 않도록 하는 것이다. 상호 의존성이 있는 쓰레드들은 코드의 복잡성만 매우 늘릴 뿐이다.

소셜 북마크

mar.gar.in mar.gar.in
digg Digg
del.icio.us del.icio.us
Slashdot Slashdot

다음 편 예고

이 글에서, GWT를 사용하여 RPC를 생성하는 과정을 설명했다. 이제 여러분에게는 GWT 애플리케이션이 생겼다. 하지만, 그 애플리케이션은 개발 머신의 Hosted Mode에서만 실행된다. 이것을 보다 큰 무대에서 실행하려면, Web Mode로 옮겨서 서블릿 환경 안에서 웹 애플리케이션을 전개하도록 한다. 다음 시리즈를 기대해주기 바란다.

기사의 원문보기



참고자료

교육

제품 및 기술 얻기

토론


필자소개

Noel Rappin, Ph.D.는 Georgia Institute of Technology의 Graphics, Visualization, Usability Center 소속이며 Motorola의 시니어 소프트 엔지니어이다. wxPython in Action과 Jython Essentials를 공동 집필했다. Noel의 블로그는 10printhello.blogspot.com이다.




기사에 대한 평가


보다 나은 서비스를 제공하기 위함이오니 잠시 짬을 내어 이 양식을 제출하여 주십시오.



아니오잘 모르겠음
 


 


12345
 



위로


Cloudscape, IBM, and the IBM logo are a registered trademarks of IBM in the United States, other countries or both. Java is a trademark of Sun Microsystems in the United States, other countries, or both. 기타 회사, 제품, 및 서비스명은 다른 상표나 서비스 마크일 수 있습니다.

developerWorks 콘텐트를 다른 사이트에 전재하기:
developerWorks 콘텐트에 대한 저작권은 IBM에 있습니다. IBM의 서면 허가나 원본 저자의 허락이 없이는 전재를 금합니다. 저희 콘텐트를 전재하시려면 IBM developerWorks 담당자 에게 문의하십시오.

    IBM 소개개인정보 보호정책문의