GPath

GPath (GPath)

중첩된 구조 데이터의 일부분을 지정해서 찾아내고 싶을 때가 있어요. Groovy는 그런 일을 위한 경로 표현 언어를 통합해 두었는데, 그게 바로 GPath예요. XML에 대한 XPath의 목적·범위와 비슷하다고 생각하면 돼요. GPath는 XML을 다룰 때 자주 쓰이지만, 사실 어떤 객체 그래프(object graph)에도 적용돼요.

출처: Groovy 공식 문서 - Semantics (GPath)

본문

GPath 표현식 (GPath expressions)

GPath는 Groovy에 통합된 경로 표현 언어로, 중첩된 구조 데이터의 일부분을 식별할 수 있게 해 줘요. 이런 점에서 XML을 위한 XPath와 목적과 범위가 비슷해요. GPath는 XML 처리 맥락에서 자주 쓰이지만, 실제로는 어떤 객체 그래프에도 적용돼요. XPath가 파일 시스템 같은 경로 표기, 즉 슬래시 /로 구분된 트리 계층을 사용한다면, GPath는 객체 탐색을 위해 점-객체 표기(dot-object notation)를 사용해요.

예를 들어, 관심 있는 객체나 요소로 가는 경로를 지정할 수 있어요.

  • a.b.c → XML에서는 a 안의 b 안에 있는 모든 c 요소를 산출해요.
  • a.b.c → POJO에서는 a의 모든 b 프로퍼티에 대한 c 프로퍼티를 산출해요(대략 JavaBeans의 a.getB().getC()와 비슷해요).

두 경우 모두 GPath 표현식은 객체 그래프에 대한 질의(query)로 볼 수 있어요. POJO의 경우 객체 그래프는 대부분 프로그램이 객체 인스턴스화와 합성을 통해 만드는 것이고, XML 처리의 경우 객체 그래프는 XML 텍스트를 파싱한 결과예요. 대개 XmlParserXmlSlurper 같은 클래스로 파싱하죠. Groovy에서 XML을 소비하는 더 깊은 내용은 Processing XML을 참고하세요.

Tip: XmlParserXmlSlurper로 만든 객체 그래프를 질의할 때, GPath 표현식은 @ 표기법으로 요소에 정의된 속성을 참조할 수 있어요.

  • a["@href"] → 맵 같은 표기법: 모든 a 요소의 href 속성
  • a.'@href' → 프로퍼티 표기법: 이를 표현하는 또 다른 방식
  • a.@href → 직접 표기: 또 다른 대안 방식

객체 탐색 (Object navigation)

간단한 객체 그래프(java reflection으로 얻은 것)에 대한 GPath 표현식 예시를 볼게요. aMethodFoo라는 메서드가 있는 클래스의 비정적(non-static) 메서드 안에 있다고 가정해 보세요.

void aMethodFoo() { println "This is aMethodFoo." } (0)

다음 GPath 표현식은 그 메서드의 이름을 가져와요.

assert ['aMethodFoo'] == this.class.methods.name.grep(~/.*Foo/)

더 정확히는, 위 GPath 표현식은 String 리스트를 만들어요. 각각은 this에 존재하는 메서드 중 이름이 Foo로 끝나는 메서드의 이름이에요.

이제 그 클래스에 다음 메서드들도 정의되어 있다고 할게요:

void aMethodBar() { println "This is aMethodBar." } (1)
void anotherFooMethod() { println "This is anotherFooMethod." } (2)
void aSecondMethodBar() { println "This is aSecondMethodBar." } (3)

그러면 다음 GPath 표현식은 **(1)**과 **(3)**의 이름을 가져오지만, **(2)**나 **(0)**은 가져오지 않아요.

assert ['aMethodBar', 'aSecondMethodBar'] as Set == this.class.methods.name.grep(~/.*Bar/) as Set

표현식 분해 (Expression Deconstruction)

이 표현식 this.class.methods.name.grep(~/.*Bar/)을 분해해서 GPath가 어떻게 평가되는지 감을 잡아 볼게요.

  • this.class — 프로퍼티 접근자로, 자바에서 this.getClass()에 해당하며 Class 객체를 산출해요.
  • this.class.methods — 프로퍼티 접근자로, 자바에서 this.getClass().getMethods()에 해당하며 Method 객체 배열을 산출해요.
  • this.class.methods.name — 배열의 각 요소에 프로퍼티 접근자를 적용하고 결과 목록을 만들어요.
  • this.class.methods.name.grep(…)this.class.methods.name이 산출한 목록의 각 요소에 grep 메서드를 호출하고 결과 목록을 만들어요.

Warning: this.class.methods 같은 하위 표현식은 배열을 산출해요. 자바에서 this.getClass().getMethods()를 호출하면 그렇게 나오니까요. GPath 표현식에는 s가 리스트를 의미한다거나 하는 관례 같은 건 없어요.

GPath 표현식의 강력한 기능 하나는, 컬렉션에 대한 프로퍼티 접근이 컬렉션의 각 요소에 대한 프로퍼티 접근으로 변환되고 그 결과가 컬렉션으로 모인다는 거예요. 그래서 this.class.methods.name 표현식은 자바에서 다음과 같이 표현할 수 있어요.

List<String> methodNames = new ArrayList<String>();
for (Method method : this.getClass().getMethods()) {
   methodNames.add(method.getName());
}
return methodNames;

컬렉션이 있는 곳에서는 배열 접근 표기법을 GPath 표현식에서 쓸 수도 있어요.

assert 'aSecondMethodBar' == this.class.methods.name.grep(~/.*Bar/).sort()[1]

Note: GPath 표현식에서 배열 접근은 0부터 시작해요.

XML 탐색을 위한 GPath (GPath for XML navigation)

XML 문서와 다양한 형태의 GPath 표현식이 있는 예시를 볼게요.

def xmlText = """
              | <root>
              |   <level>
              |      <sublevel id='1'>
              |        <keyVal>
              |          <key>mykey</key>
              |          <value>value 123</value>
              |        </keyVal>
              |      </sublevel>
              |      <sublevel id='2'>
              |        <keyVal>
              |          <key>anotherKey</key>
              |          <value>42</value>
              |        </keyVal>
              |        <keyVal>
              |          <key>mykey</key>
              |          <value>fizzbuzz</value>
              |        </keyVal>
              |      </sublevel>
              |   </level>
              | </root>
              """
def root = new XmlSlurper().parseText(xmlText.stripMargin())
assert root.level.size() == 1 (1)
assert root.level.sublevel.size() == 2 (2)
assert root.level.sublevel.findAll { it.@id == 1 }.size() == 1 (3)
assert root.level.sublevel[1].keyVal[0].key.text() == 'anotherKey' (4)
  • (1) root 아래에 level 노드가 하나 있어요.
  • (2) root/level 아래에 sublevel 노드가 두 개 있어요.
  • (3) id 속성 값이 1인 sublevel 요소가 하나 있어요.
  • (4) root/level 아래 두 번째 sublevel 요소의 첫 번째 keyVal 요소에 있는 key 요소의 텍스트 값은 'anotherKey'예요.

XML을 위한 GPath 표현식의 더 자세한 내용은 XML 사용자 가이드에 있어요.

더 알아보기 (Learn more)