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

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

Grails 마스터하기: Ajax를 가미한 다 대 다 관계

developerWorks
문서 옵션

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

영어원문

영어원문


제안 및 의견
피드백

난이도 : 초급

Scott Davis, 편집장, AboutGroovy.com

옮긴이: 박찬욱 dwkorea@kr.ibm.com

2008 년 7 월 01 일

다 대 다(m:m) 관계는 웹 애플리케이션에서 신중하게 다뤄야 합니다. Grails 마스터하기 의 이번 회에서 Scott Davis는 어떻게 하면 Grails로 성공적인 다 대 다 관계를 구현할 수 있는지 보여줍니다. GORM(Grails Object Relational Mapping) API와 백엔드 데이터베이스를 다루는 방법도 함께 살펴보겠습니다. 또한 Ajax(Asynchronous JavaScript + XML)를 일부 사용해 사용자 인터페이스를 효율적으로 개선하는 방법도 함께 알아보려 합니다.

소프트웨어 개발은 실제 세계를 코드로 모델링하는 작업이다. 예를 들어, 책에는 지은이와 출판사가 있다. Grails 애플리케이션에서 이를 각각 도메인 클래스로 만들 수 있다. GORM은 각 클래스에 대응되는 데이터베이스 테이블을 생성하고, 스캐폴딩(scaffolding)은 공짜로 기본적인 생성/읽기/수정/삭제(CRUD)에 대한 웹 인터페이스를 만들어 준다.

진행 차례에 따라 다음 단계는 이 클래스들 사이에서 관계를 정의하는 작업이다. 일반적인 출판사는 책을 한 권 이상 출판하므로 출판사와 책 사이의 관계가 일 대 다(1:m)가 된다는 건 직관적으로 알 수 있다. 즉 한 Publisher가 여러 권의 Books를 출판할 수 있다. Publisher 클래스에 static hasMany = [books:Book] 코드를 넣어 일 대 다 관계를 생성하면 된다. Book 클래스에 static belongsTo = Publisher를 넣으면 연쇄적인 수정과 삭제가 일어나는 또 다른 관계 영역이 추가된다. Publisher가 삭제되면, 이와 관계를 맺고 있는 모든 Books 역시 지워진다.

이 연재에 대해

Grails는 현대적인 웹 개발 프레임워크로 스프링과 하이버네이트 같은 친숙한 자바 기술을 '설정보다는 관례' 같은 최근 실천법을 합친 것이다. Grails는 그루비로 작성되어 Grails를 이용하면 기존 자바 코드와 매끄럽게 통합하면서 스크립팅 언어의 유연성과 동적인 특성을 추가로 얻을 수 있다. Grails를 배우고 나면 웹 개발이 예전처럼 보이지 않을 것이다.

일 대 다 관계는 기본적인 데이터베이스 내에서 쉽게 모델링이 된다. 각 테이블은 주 키로 사용하는 id 필드를 가지고 있다. GORM이 book 테이블에 publisher_id 필드를 추가할 때, 두 개 테이블 사이에 일 대 다 관계가 성립된다. 프론트엔드에서도 Grails는 자연스럽게 일 대 다 관계를 처리한다. 새로운 Book을 생성했을 때 자동으로 생성되는(스캐폴딩되는) HTML 폼은 드롭다운 콤보 박스를 제공해 기존 Publisher 목록에서 하나를 선택할 수 있도록 제한한다. 이번 연재의 첫 번째 글에서부터 일 대 다 관계의 예제를 봐왔을 것이다.

이제 조금 더 복잡한 관계인 다 대 다 관계를 집중해서 볼 시간이다. BookAuthor 사이의 관계는 BookPublisher 간의 관계처럼 쉽게 모델링을 할 수 없다. 책 한 권에 지은이가 여러 명일 수 있고, 또한 한 지은이가 책 여러 권을 집필할 수 있다. 이것이 전통적인 다 대 다 관계다. 실 세계를 모델링하는 관점에서 보면 다 대 다 관계는 매우 일반적이다. 한 사람은 예금 계좌 여러 개를 가질 수 있고, 예금 계좌 하나를 많은 사람이 관리할 수 있다. 한 컨설턴트가 여러 프로젝트에서 일할 수 있고, 한 프로젝트에서 여러 컨설턴트를 고용할 수 있다. 이번 글에서는 Grails에서 다 대 다 관계를 구현하는 방법을 살펴보고, 이번 연재를 통해 개발해 나가고 있는 여행 계획 애플리케이션에 다 대 다 관계를 구축해볼 생각이다. 그렇지만 여행 계획 프로그램으로 시선을 돌리기 전에, 중요한 요점을 이해하는 데 도움을 주기 위해 조금 시간이 걸리더라도 책 예제를 함께 따라 해 볼 것이다.

세 번째 클래스

데이터베이스에서 세 개 테이블이 다 대 다 관계를 표현한다. 이미 알고 있듯이 두 개는 일반 테이블(BookAuthor)이며, 세 번째 테이블은 조인 테이블(BookAuthor)이다. Book이나 Author에 외래 키를 추가하는 것이 아니라, GORM은 BookAuthor 조인 테이블에 book_idauthor_id를 추가한다. 조인 테이블은 다수의 지은이와 책의 관계뿐 아니라 한 명의 지은이와 책에 대한 관계 또한 영속성을 유지하게 해준다. 또한 다수의 책을 쓴 다수의 지은이 또한 표현할 수 있다. 한 권의 책은 무제한의 지은이를 갖게 될 수 있고, 한 명의 지은이 또한 무제한의 책을 갖게 될 수 있는 진정한 무제한의 유연성을 얻게 된다.

Dierk Koening이 내게 다음과 같이 말한 적이 있다. "두 개의 객체가 단순한 다 대 다 관계를 공유한다고 생각한다면, 도메인을 완벽하게 보지 못하는 겁니다. 나름대로 생명 주기가 있고, 속성으로 발견되기를 기다리는 세 번째 객체가 있습니다." 실제로 BookAuthor의 관계는 간단한 조인 테이블을 능가한다. 예를 들어, Dierk는 Groovy in Action(Manning Publictions, 2007년 1월)의 주 지은이다. 주 지은이임은 AuthorBook 사이의 관계에 대한 필드로 표현될 수 있다. 그러나 여기서 다양한 현상이 존재한다. 지은이는 특정 순서에 따라서 표지에 나열된다. 각 지은이는 책의 특정 장에 기여를 하게 되고, 각 지은이는 자신들의 기여도에 따라 다른 보상을 받게 될 수 있다. 위 예에서 보았듯이 AuthorBook 사이의 관계는 원래 의도와는 일정 부분 미묘한 차이를 보인다. 실제 세계에서 각 지은이는 명시적인 조항을 갖도록 책과의 관계에 대해 자세히 기술된 계약에 서명을 한다. 아마도 첫 번째 클래스인 Contract 클래스는 BookAuthor 간의 더 자연스러운 관계를 표현하도록 생성되었을 것이다.

간단히 말해, 이 말은 다 대 다 관계가 마치 실제로는 두 개의 일 대 다 관계로 보일 수 있음을 의미한다. 두 클래스가 다 대 다 관계를 공유하는 것처럼 보인다면, 두 개의 일 대 다 관계를 유지하는 세 번째 클래스를 정의할 수 있게 더 상세하게 파고들어 가면, 세 번째 클래스를 명확하게 정의할 수 있다.




위로


항공노선과 공항 모델링하기

이제 여행 계획 프로그램으로 돌아가서, 어떤 다 대 다 관계가 숨어 있는지 도메인 모델을 다시 살펴보자. 첫 번째 글에서, 우리는 Listing 1에 나오는 Trip 클래스를 만들었다.


Listing 1. Trip 클래스
                
class Trip { 
  String name
  String city
  Date startDate
  Date endDate
}

두 번째 글에서 우리는 Listing 2에 나오는 것처럼 단순한 일 대 다 관계를 설명하는 데 함께 사용한 Airline 클래스를 추가했다.


Listing 2. Airline 클래스
                
class Airline { 
  static hasMany = [trip:Trip]
  String name
  String frequentFlier
}

이런 연습용(toy) 클래스들은 그 당시 그만의 목적을 가지고 사용됐지만, 엄격한 도메인 모델로 유지되지는 않았다. 이제 기존 클래스를 보완하고, 좀 더 견고한 클래스로 증축할 시간이다.

당시에는 그 방법이 정확한 것으로 알고 있었기 때문에 그 방법으로 Trip 클래스를 만들었다. "나는 시카고 여행을 계획하고 있다" 또는 "나는 다음 달 15일에서 20일까지 뉴욕시로 갈 예정이다"와 같은 말을 했다. city, startDateendDateTrip 클래스의 자연스러운 속성으로 보였다. 그렇지만 다시 한 번 살펴보면, Trip에는 아마도 더 많은 내용이 포함되어 있을 것이다.

나는 유나이티드 에어라인(United Airlines)의 허브 도시인 콜로라도 주 덴버에 산다. 이 말은 언제나 최종 목적지까지 직항으로 날아갈 수 있다는 말이지만, 두 군데 이상을 여행해야 할 때도 종종 있다. 여행은 "나는 월요일부터 금요일까지 수업을 하기 위해 보스톤으로 날아갈 것이다. 동부 해안에 있는 중, 토요일에는 컨퍼런스 발표 때문에 워싱턴 D.C로 이동할 필요가 있다. 일요일 오후에는 집으로 돌아온다"처럼 계획하기에 따라 여러 도시를 포함할 수도 있다. 운 좋게 특정 도시로 가는 직항편을 찾더라도, 다른 도시로는 바로 날아갈 수 없을지도 모르고, 우리 여행에는 여전히 하나 이상의 비행(왕복 비행)을 포함할 것이다. 하나의 Trip 클래스는 다수의 Flight를 갖게 될 수 있다. Listing 3은 TripFlight 간의 관계를 정의한다.


Listing 3. TripFlight 사이의 1 대 다 관계
                
class Trip{
  static hasMany = [flights:Flight]
  String name
}

class Flight{
  static belongsTo = Trip
  String flightNumber
  Date departureDate
  Date arrivalDate
}

belongsTo 필드 관계에 대한 설정은 Trip이 삭제되면 관계를 맺고 있는 Flight 또한 전부 삭제된다는 점을 기억하자. 항공 교통량 제어 시스템을 구축한다면, 아마도 다른 아키텍처 디자인을 만들고 싶었을 것이다. 또는 공통의 비행기를 공유할 수 있는 다수의 승객 관리 시스템을 구축하고자 했다면(하나의 Flight가 여러 Passengers를 가질 수 있고, 한 Passengers가 여러 Flights를 갖게 될 수도 있다) 한 비행기에 특정 승객의 비행을 고정시켰다면 문제가 됐을 것이다. 그러나 나는 전 세계의 수많은 승객을 위해 매일 뜨는 수천 개의 비행을 모델링하지는 않을 것이다. 우리처럼 단순한 상황에서는 더욱이 모든 Flight가 하나의 Trip을 나타내게 된다. 만약 Trip이 우리에게 중요하지 않다면, 매번 Flight에 포함해 표현할 수도 있다.

이제 Airline 클래스와 함께 행동하는 것은 무엇인가? Trip 하나는 다른 Airlines를 여러 개 포함할 수 있고, 한 Airline도 여러 개의 다른 Trips를 사용할 수 있다. 이 두 클래스 상호간에는 명백하게 다 대 다 관계이지만, Listing 4에서 볼 수 있듯이, FlightAirlines를 추가하기에 적절한 위치처럼 보인다. 단일 Flight는 하나 이상의 Airline과 절대로 관계를 맺을 수 없지만, 하나의 Airline은 여러 Flight를 포함할 수 있다.


Listing 4. AirlineFlight 관계 맺기
                
class Airline{
  static hasMany = [flights:Flight]
  String name
  String iata
  String frequentFlier
}

class Flight{
  static belongsTo = [trip:Trip, airline:Airline]
  String flightNumber
  Date departureDate
  Date arrivalDate
}

여기서 몇 가지 알아야 할 것이 있다. 무엇보다 먼저, Flight에 있는 belongsTo 필드는 단일 값에서 값에 대한 해시맵(hashmap)으로 교체됐다는 점이다. 하나의 TripFlights를 여러 개 포함할 수 있을 뿐만 아니라 하나의 Airline 역시 여러 개의 Flights를 포함할 수 있다.

다음으로 우리는 Airlineiata 필드를 추가했다. 이 필드는 IATA(International Air Transport Association) 코드를 의미한다. IATA는 각 비행기에 대한 유일한 코드(United Airlines는 UAL, Continental은 COA, Delta는 DAL 등)를 제공한다.

마지막으로 우리가 했던 또 다른 아키텍처 정의를 알아야 한다. 이 시점에서는 Airlines와 비행 빈도 기록간의 관계를 포함하게 된다. 시스템에는 한 명의 사용자만 있다고 가정했기 때문에, Airline 클래스의 속성으로 FrequentFlier를 사용한 것은 적절하게 유용한 방법이다. Airline당 하나 이상의 비행 빈도 기록을 가질 수 없기 때문에, 가장 간단하게 사용할 수 있는 방법이 된다. 만약 여행 계획 애플리케이션의 요구사항이 바뀌어 다수의 사용자를 지원해야 한다면, 또 다른 다 대 다 관계가 나타나는 것을 볼 수 있다. 한 승객은 여러 비행 기록을 갖게 될 수 있고, 하나의 Airline 또한 비행 기록을 여러 건 갖게 될 수 있다. 이 관계를 관리하는 조인 테이블 생성은 적절한 행동이라고 생각할 수 있다. 지금까지는 간단한 해결책을 고수해 왔지만, 마음속으로는 요구사항이 변경했을 때 리팩터링해야 하는 지점으로 FrequentFlier를 기억해두자.




위로


도시 또는 공항?

이제 City를 추가하면 다시 복잡한 상황으로 돌아가게 된다(물론 그렇지 않을 수도 있다). 비록 "나는 시카고로 비행한다"라고 했지만, 기술적으로는 공항으로 날아가는 것이다. 내가 시카고 O'Hare로 날아간 것인가 아니면 Midway 공항으로 날아간 것인가? 뉴욕으로 날아갔을 때, 그곳은 LaGuardia인가 또는 JFK인가? 단순히 City 필드 대신 Airport 클래스를 필요로 한다는 게 명백하다. Listing 5는 Airport 클래스를 보여준다.


Listing 5. Airport 클래스
                
class Airport{
  static hasMany = [flights:Flight]
  String name
  String iata
  String city
  String state
  String country
}

Listing 4에서 iata 필드가 돌아온 것을 볼 수 있다. DEN은 Denver International Airport를, ORD는 Chicago O'Hare, MDW는 Chicago Midway 등을 의미한다. State 클래스를 생성해서 간단하게 1 대 다 관계로 설정하고 싶을 수도 있고 심지어 city, state, country를 캡슐화하는 Location을 생성하고 싶을 수도 있다. 현재 어렵게 진행하고 있는 프로젝트를 끝내고 독자들이 직접 해볼 수 있도록 이 부분은 남겨둘 생각이다.

이제 우리는 Listing 6에서 볼 수 있듯이, Flight 클래스에 Airport를 추가할 것이다.


Listing 6. FlightAirport의 관계
                
class Flight{
  static belongsTo = [trip:Trip, airline:Airline]
  String flightNumber
  Date departureDate
  Airport departureAirport
  Date arrivalDate
  Airport arrivalAirport
}

그렇지만 이번에 나는 암시적으로 belongsTo 필드를 사용하는 것보다 명시적으로 departureAirportarrivalAirport를 생성했다. 사용자 인터페이스에서는 차이를 느낄 수 없지만(필드들은 콤보 박스를 사용해 표현될 것이다), 클래스들 간의 관계는 미묘하지만 중대한 차이가 생긴다. Airport 삭제는 관계를 맺고 있는 Flight를 연속적으로 삭제를 하지 않는 반면, Trip이나 Airline은 삭제될 것이다. 나는 다양한 클래스의 관계를 다양한 방법으로 표현할 수 있는 방안을 소개할 생각이다. 현실에서는 클래스들이 엄격한 참조 무결성(referential integrity) 유지(다른 말로 하면, 모든 참조를 연속적으로 삭제하는)를 원하는지, 느슨한 관계를 허용하는지에 대한 결정까지도 할 수도 있다.




위로


실용적인 다 대 다 관계 살펴보기

현실에서 적절한 객체 모델은 실제 업무 모델링을 위한 합리적인 방안이 된다. 나는 1년 동안 비행기를 여러 차례 타고, 여러 곳의 공항으로 날아가는 여행을 몇 차례 한다. 이러한 많은 관계 모두 Flight에 묶인다.

아무런 일도 하지는 않았지만, Listing 7에 나오는 MySQL의 show tables 명령어를 실행해 데이터베이스 내부를 살펴보면 보고자 했던 테이블을 볼 수 있다.


Listing 7. 보이지 않는 곳에서 생성된 테이블
                
mysql> show tables;
+----------------+
| Tables_in_trip |
+----------------+
| airline        | 
| airport        | 
| flight         | 
| trip           | 
+----------------+

Airline, Airport, Trip 테이블에 있는 칼럼은 대응되는 도메인 클래스의 필드에 모두 일치된다. flight 테이블은 조인 테이블이며, 다른 테이블 간의 복잡한 관계를 표현한다. Listing 8은 Flight 테이블에 있는 필드를 보여준다.


Listing 8. Flight 테이블에 있는 필드
                
mysql> desc flight;
+----------------------+--------------+------+-----+
| Field                | Type         | Null | Key |
+----------------------+--------------+------+-----+
| id                   | bigint(20)   | NO   | PRI |
| version              | bigint(20)   | NO   |     |
| airline_id           | bigint(20)   | YES  | MUL |
| arrival_airport_id   | bigint(20)   | NO   | MUL |
| arrival_date         | datetime     | NO   |     |
| departure_airport_id | bigint(20)   | NO   | MUL |
| departure_date       | datetime     | NO   |     |
| flight_number        | varchar(255) | NO   |     |
| trip_id              | bigint(20)   | YES  | MUL |
+----------------------+--------------+------+-----+


그림 1에서는 스캐폴딩으로 생성된 HTML 페이지가 관련된 모든 테이블에 대한 콤보 박스를 제공하는 것을 보여준다.


그림 1. Flight에 추가된 스캐폴딩으로 생성된 HTML 페이지
Flight에 추가된 스캐폴딩으로 생성된 HTML 페이지



위로


사용자 인터페이스 미세 조정

지금까지 다 대 다 관계에 대한 토의의 논점은 클래스와 데이터베이스 테이블의 관계 모델링 방법이었다. 독자들이 과학만큼이나 예술적인 면도 볼 수 있기를 바란다. Grails 개발자들처럼 하면, 관계의 행위와 부작용을 정교하게 정제하는 많은 이점을 얻을 수 있다. 마찬가지로 사용자 인터페이스로 관심을 돌려 다 대 다 관계의 화면 표현을 정교하게 조정할 수 있는 세밀한 방법을 찾아볼 것이다.

바로 앞 절에서 보았듯이, Grails는 기본적으로 일 대 다 관계를 보여주는 데 선택 필드(select field)를 사용한다. 이는 시작으로는 나쁘지 않지만, 다른 환경에서 다른 HTML 제어를 사용하고 싶을 수도 있다. 단지 현재 값에 해당하는 필드만 선택해 화면에 표현하고, 가능한 모든 값을 보려면 리스트를 드롭다운(drop-down)해야만 한다. 물론 화면이 크지 않다면 가장 좋은 선택이 되겠지만, 가능한 모든 값을 선택할 수 있도록 만드는 것이 더 좋은 해결책일 때도 있다. 라디오 버튼은 선택할 수 있는 모든 값을 보여주기는 하지만, 하나의 값만 선택할 수 있는 제한이 있다. 체크 박스는 선택 가능한 모든 값을 보여주고, 다중 선택을 허용한다.

위에서 다룬 모든 제어 방법은 제한된 선택 값들을 보여주기에는 좋지만, 사용할 수 있는 값이 수백, 수천 가지이면 허용 범위를 넘어서게 된다. 예를 들어, 최종 사용자에게 낱말 단위로 대략 650개의 항공사 모두를 보여줄 필요가 있다면, 그 어떤 표준 HTML 제어 방법으로도 이 엄청난 크기에 손을 댈 수 없을 것이다. 이제부터가 개발자들의 판단이 시작하는 시점이다. 이번 애플리케이션에서 우리는 650개 항공사를 전부 표현하길 원하지 않는다. 아마도 내 평생 동안 열두 군데 이하의 항공사를 이용하게 될 것이다. 대부분의 경우 선택 필드를 사용해 항공사 선택 화면을 보여주는 것으로 충분하다.

Grails가 Airline에 대한 선택 필드를 생성하는 방법을 보려면, grails generate-views Flight를 입력하자. grails-app/views/flight/create.gsp를 살펴보자. 선택 필드에 대해서는 <g:select>를 사용한 단 한 줄의 코드가 생성된다(Grails TagLibs에 대해 다시 살펴보려면, 지난 글을 참조하자). Listing 9는 실제로 사용한 <g:select> 필드를 보여준다.


Listing 9. 실제 사용한 <g:select> 필드
                
<g:select optionKey="id" 
          from="${Airline.list()}" 
          name="airline.id" 
          value="${flight?.airline?.id}" ></g:select>

Listing 10에서 볼 수 있는 것처럼, 어떻게 화면에 보이는지 확인하려면 웹 브라우저에서 View > Source를 선택하자.


Listing 10. 화면에 보이는 선택 필드
                
<select name="airline.id" id="airline.id" >
<option value="1" >UAL - United Airlines</option>
<option value="2" >DAL - Delta</option>
<option value="3" >COA - Continental</option>
</select>

<g:select> 태그의 optionKey 속성은 '일(1)' 측면의 클래스 필드가 관계를 맺는 다른 측면의 '다(m)' 측면의 필드에 있는 값을 받을 수 있도록 정한다. Airline 테이블의 주 키(airline.id)는 Flight 테이블에서 외래 키 역할을 한다. 선택 필드에서 airline.id가 선택 값이란 걸 기억하자(Airline.toString() 메서드를 호출해 화면에 값을 출력한다). 옵션 정렬 순서를 변경하고 싶다면, GORM이 Airline.list()에서 Airline.listOrderByIata()Airline.listOrderByName()을 호출하도록 바꿀 수 있으며, 다른 필드에서도 이와 같이 하면 된다.




위로


대규모 옵션을 제어하도록 Ajax 사용하기

기본적인 선택 제어는 현실적인 개수의 항공사를 보여주는 상황에서는 합리적인 선택이다. 불행하게도, 공항이 문제가 될지도 모른다. 나는 주어진 기간에 40 또는 50개의 다른 공항을 여행할 수 있다. 내 경험상, 선택 필드에서 15 또는 20보다 조금 더 많은 값을 제공하는 일은 더 성가신 일의 시작이 된다.

운 좋게도 공항에 대한 IATA 코드는 산업 전체에 광범위하게 쓰인다. 비행기를 검색했을 때 코드를 확인할 수 있다. 심지어 티켓에서도 확인할 수 있다. IATA 코드 유형에 대한 사용자의 질문은 수백 개의 가능한 항공사에 대한 코드를 스크롤해 찾아보게 만드는 방법이 합리적이다.

이번 글을 시작하면서 소개했던 Book의 예로 돌아가서 생각해보자. Amazon.com은 홈페이지에서 저장해 놓은 모든 책을 보여주는 하나의 엄청난 규모의 선택 필드를 제공하는가? 아니다. 원한다면 책 제목, 지은이, 또는 ISBN(International Standard Book Number)까지 입력할 수 있는 텍스트 필드를 제공한다. 우리의 여행 계획 애플리케이션에서 공항 코드를 다루는 데 같은 기술을 사용할 것이다.

선택 필드에서 텍스트 필드로 제어를 변경하는 것은 매우 쉬운 일이다. 그러나 기술적인 해결책을 계속 하기 전에, 나는 의미론적인 접근을 위해 잠시 시간을 사용하길 원한다. iata 필드는 자유스런 폼인 텍스트 필드임에도 불구하고, 사용자가 입력한 모든 값을 단순하게 다 받아들일 수는 없다(애플리케이션이 이름에 대한 철자를 틀렸다고 해서 우리를 혼내지는 않지만, IATA 코드를 잘못 입력했을 때는 경고를 줄 필요는 있다). 잘못된 값이 입력됐을 때마다 벌을 받으면서, 전체 HTML 폼을 반복 전송하는 것만큼 부끄러운 일이 없기 때문에 나는 이벤트 발생 즉시 피드백 받기를 원한다.

그렇기 때문에 나는 단일 필드 유효성을 검증하려고 서버로 전체 폼을 주고 받는다거나 고객이 매번 이용 가능한 수천 개의 항공 IATA 코드를 다운로드하기를 원하지 않는다. 해결 방법은 서버에 데이터를 유지하고 전체 폼에 대한 코스 그레인드(coarse-grained, 역주: 대상을 큰 단위로 분리하여 구성함) 요청보다는 개별 필드에 대한 파인 그레인드(fine-grained, 역주: 대상을 잘게 쪼개 구성함) HTTP 요청을 만드는 것이다. 이 기술을 Ajax 요청이라 한다(Ajax에 대한 소개는 참고자료를 참조하자).

Ajax를 사용할 수 있는 Grails 애플리케이션이 되도록 하려면, AirportController가 Ajax 요청을 수용할 수 있도록 일부 수정이 필요하며, 뷰에서도 Ajax 요청을 생성할 수 있도록 일부 수정이 필요하다. AirportController부터 시작해보자.

AirportController는 이미 개별 Airport를 보여줄 뿐만 아니라, Airport들의 리스트를 반환하는 클로저가 스캐폴드되어 있다. 그렇지만 기존 클로저는 HTML로 값을 반환한다. 우리는 로(raw) 데이터를 반환하는 새로운 클로저를 추가할 것이다. 한 가지 방법은 화면과 대응되는 POGO를 직렬화하는 것이지만 고객은 웹 브라우저를 사용한다. 슬프게도 그루비(Groovy)가 아니라 자바스크립트가 웹 브라우저와 소통할 수 있는 언어다(모질라 재단, 듣고 있는 건가?).

Ajax의 x가 의미하듯이 XML을 반환할 수 있다. AirportControllergrails.converters 패키지를 임포트(import)한다면, Listing 11에서 볼 수 있는 것처럼 XML 반환은 한 줄로 처리되는 일이다.


Listing 11. 컨트롤러에서 XML 반환하기
                
import grails.converters.*

class AirportController {
  def scaffold = Airport
  
  def getXml = {
    render Airport.findByIata(params.iata) as XML
  }  
}

이 해결 방법의 한 가지 문제점은 XML이 그루비보다 자바스크립트에 좀 더 네이티브하다는 점이다. GORM과 같은 객체-관계형 매퍼의 장점은 근본적으로 제공하지 않는 포맷(관계형 데이터베이스에 저장된) 데이터를 그루비로 간단하게 전환이 가능하다는 점이다. 이런 작동과 비슷하게 자바스크립트에서는 그루비 데이터를 JSON(JavaScript Object Notation, 참고자료 참조)으로 변환할 수 있다. 고맙게도 동일하게 한 줄의 코드 변환 작업으로 XML을 JSON으로 만들 수 있다. Listing 12에서는 getJson 클로저에 일부 에러 처리를 추가했고, 이는 getXml 클로저와 똑같은 기능을 한다.


Listing 12. 컨트롤러에서 JSON 반환하기
                
def getJson = {
  def airport = Airport.findByIata(params.iata)
  
  if(!airport){
    airport = new Airport(iata:params.iata, name:"Not found")
  }
  
  render airport as JSON
}

웹 브라우저에 http://localhost:9090/trip/airport/getJson?iata=den을 입력해 JSON 전송 작업을 확인하자. Listing 13에서 볼 수 있는 응답을 받게 될 것이다(JSON 응답을 보려면 브라우저에서 View > Source를 선택하면 된다).


Listing 13. JSON 응답
                
{"id":1,"class":"Airport","city":
   "Denver","country":"US","iata":
"DEN","name":"Denver International Airport","state":"CO"}

Airline 리스트를 반환하려면, render Airline.list() as JSON으로 매우 간단하게 처리할 수 있다.

이제 JSON을 만들 수 있으니, 이를 사용할 수 있도록 집어넣을 시간이다. departureAirport에 대한 기존 <g:select>를 주석 처리하고, Listing 14에 나오는 네 줄의 코드로 대체하자.


Listing 14. 선택 필드를 텍스트 필드로 교체하기
                
<div id="departureAirportText">[Type an Airport IATA Code]</div>
<input type="hidden" name="departureAirport.id" value="-1" 
   id="departureAirport.id"/>          
<input type="text" name="departureAirportIata" id="departureAirportIata"/>
<input type="button" value="Find" onClick="get('departureAirport')"/>

첫 번째 줄은 읽기 전용 화면 출력 영역이다. 여기서 id를 가지고 있다는 점을 기억하자. ID는 전체 HTML DOM(Document Object Model)에서 유일해야만 한다. 잠시 후에 JSON 호출 결과를 작성하는 데 departureAirportText를 처리해 사용할 생각이다.

<div>는 폼이 전송될 때 서버로 보내지 않고, input과 select처럼 폼을 제어하는 데 사용한다. hidden 텍스트 필드는 전체 폼이 서버로 다시 전송될 때 Airportid를 저장할 수 있는 기회를 준다.

departureAirportIata로 명명된 텍스트 필드에 사용자가 IATA 코드를 입력하게 된다. ID와 name 모두 동일한 값을 입력하는 것은 그다지 DRY(Don't Repeat Yourself)하지 않은 것처럼 보이지만, HTML 메커니즘은 이 기준을 요구한다. 폼이 전송될 때 서버로 다시 돌아가는 것은 name 값이다. ID는 getJson 클로저를 호출할 때 조건으로 사용하게 된다.

마지막 줄에 있는 버튼은 클릭할 때 get으로 명명된 자바스크립트 함수를 호출한다. get 함수에 대한 구현은 잠시 후에 보여줄 것이다. 우선은 그림 2에서 볼 수 있듯이 새로운 폼을 소개할 것이다.


그림 2. 새로 변경된 폼




위로


Ajax 호출에 Prototype 사용하기

Grails는 Prototype(참고자료 참조)이라는 자바스크립트 라이브러리를 사용한다. Prototype은 모든 주요 브라우저에 대한 호환성을 보장하며 Ajax 호출을 하는 가장 일반적인 방법이다. get 함수는 일찍 브라우저에 입력할 수 있는 단순한 URL을 구축해주며, 서버 백엔드로는 비동기 호출을 가능케 해준다. 호출이 성공(HTTP 200이 반환됨)하면, update 함수가 호출된다. Listing 15는 Ajax 호출에 Prototype을 사용한 예다.


Listing 15. Ajax 호출에 Prototype 사용하기
                
<g:javascript library="prototype" />     
<script type="text/javascript">
  function get(airportField){
    var baseUrl = "${createLink(controller:'airport', action:'getJson')}"
    var url = baseUrl + "?iata=" + $F(airportField + "Iata")
    new Ajax.Request(url, {
      method: 'get',
      asynchronous: true,
      onSuccess: function(req) {update(req.responseText, airportField)}
    })
  }
  
  ...
</script> 

update 함수는 JSON 호출 결과를 읽어, <div>의 화면 표현을 갱신하고, airport의 주 키 값을 찾은 경우 hidden 필드 값을 갱신하고, 찾지 못했을 때는 -1로 값을 갱신한다. Listing 16은 update 함수를 보여준다.


Listing 16. JSON 데이터로 필드 값 갱신하기
                
function update(json, airportField){
  var airport = eval( "(" + json + ")" )
  var output = $(airportField + "Text")
  output.innerHTML = airport.iata + " - " + airport.name
  var hiddenField = $(airportField + ".id")
  airport.id == null ? hiddenField.value = -1 : hiddenField.value = airport.id
}

그림 3은 두어 번 성공한 Ajax 호출 후 Flight가 어떻게 보이는지를 보여준다.


그림 3. Ajax 호출로 값을 받아온 폼




위로


클라이언트 측 유효성 검증

마지막으로 크게 어렵지 않은 작업으로 클라이언트 측 유효성 검증을 해서 departureAirportarrivalAirport에 부적절한 값이 들어갔을 때 서버로 전송되지 않도록 해보자(선택 필드, 라디오 버튼 집합, 체크 박스 집합에서 사용자가 선택했을 때 부적절한 값이 들어가는 것은 완전히 불가능하다. 사용자가 자유롭게 폼에 글자를 입력하도록 했기 때문에, 사용자가 입력한 값의 질에 대해 조금 더 걱정할 필요가 있는 것이다).

g:form 태그에 onSubmit을 추가하자.

<g:form action="save" method="post" onsubmit="return validate()" >

validatetrue를 반환하면, 폼은 서버로 폼 내용을 전송한다. validatefalse를 반환한다면, 전송 요청은 취소된다.


Listing 17. validate 함수
                
function validate(){
  if( $F("departureAirport.id") == -1 ){
    alert("Please supply a valid Departure Airport")
    return false
  }
  
  if( $F("arrivalAirport.id") == -1 ){
    alert("Please supply a valid Arrival Airport")
    return false
  }
  
  return true
}

선택 필드를 텍스트 필드로 변경하는 데 일이 조금 더 필요하다는 생각이 든다면 그 생각에 동의한다. 나는 이 변경을 자체적으로 더 쉽게 만들 수는 없다(하지만 나는 최종 사용자가 더 쉽게 이 변경을 할 수 있도록 시도하고 있다). 이 점은 고려해보자. 대부분의 초기화 작업을 대신 해주도록 Grails가 제공하는 스캐폴딩은 때때로 우리가 하고 싶은 상세한 튜닝을 고려하지 않는다. 스캐폴딩은 제품 개발 완료를 의미하지 않는다. 우리가 퍼즐의 흥미로운 부분에 좀 더 집중할 수 있게 지겹고 사소한 모든 일에서 빠져 나올 수 있는 방법을 의미한다.




위로


결론

다 대 다 관계의 단순한 메커니즘에서 한 발 더 나아가서, 독자들이 생성의 미묘한 차이에 대한 무엇인가를 집어내길 바란다. 연쇄적인 삭제를 원할 때도 있고 아닐 때도 있다. 두 클래스 간의 간단한 다 대 다 관계를 가지고 생각하고 있을 때, 아마도 세 번째 클래스가 우리의 발견을 기다리고 있을 것이다.

프리젠테이션 측에서 선택 필드는 다 대 다 관계의 기본적인 방법이 되지만 유일한 방법은 아니다. 라디오 버튼과 체크 박스 또한 선택할 수 있는 값이 몇 가지 없는 경우 고려해볼 대안이 된다. 선택할 수 있는 값이 많을 때는 텍스트 필드나 Ajax로 문제를 해결할 수 있다.

다음 달에는 비행 정보를 구글 맵과 연동할 수 있는 방법을 보여줄 생각이다. 또한 Grails 서비스에 대해 이야기기해볼 것이다. 자 그럼 다시 만날 때까지 Grails 마스터하기를 즐겨보자.



참고자료

교육

제품 및 기술 얻기
  • Grails: 최신 Grails를 다운로드하자

토론


필자소개

Scott Davis 사진

Scott Davis는 국제적으로 유명한 저자이자 강사, 소프트웨어 개발자다. 저서로는 Groovy Recipes: Greasing the Wheels of Java, GIS for Web Developers: Adding Where to Your Application, The Google Maps API, JBoss At Work가 있다.




기사에 대한 평가


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



 


 


 


이 문서 북마킹 하기

mar.gar.in mar.gar.in naver naver eolin eolin del.icio.us del.icio.us





위로


Java and all Java-based trademarks are trademarks of Sun Microsystems, Inc. in the United States, other countries, or both. 기타 회사, 제품, 및 서비스명은 다른 상표나 서비스 마크일 수 있습니다.

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