Groovy 연산자

Groovy 연산자

이 장에서는 Groovy 프로그래밍 언어의 연산자에 대해 다뤄요.

출처: Groovy 공식 문서

본문

1. 산술 연산자

Groovy는 수학이나 Java 같은 다른 언어에서 익숙한 일반적인 산술 연산자를 지원해요. Java의 산술 연산자는 모두 지원됩니다. 다음 예시들을 통해 하나씩 살펴볼게요.

1.1. 일반 산술 연산자

Groovy에서 사용 가능한 이항(binary) 산술 연산자는 다음과 같아요.

연산자 용도 비고
+ 덧셈
- 뺄셈
* 곱셈
/ 나눗셈 정수 나눗셈에는 intdiv()를 쓰세요. 나눗셈의 반환 타입에 대한 자세한 내용은 정수 나눗셈 섹션을 참고하세요
% 나머지
** 거듭제곱 연산의 반환 타입에 대한 자세한 내용은 거듭제곱 섹션을 참고하세요

이 연산자들의 사용 예시를 몇 개 볼게요.

assert  1  + 2 == 3
assert  4  - 3 == 1
assert  3  * 5 == 15
assert  3  / 2 == 1.5
assert 10  % 3 == 1
assert  2 ** 3 == 8

1.2. 단항 연산자

+- 연산자는 단항 연산자로도 사용할 수 있어요.

assert +3 == 3
assert -4 == 0 - 4

assert -(-1) == 1  (1)

(1) — 단항 마이너스를 적용할 표현식을 괄호로 둘러싸는 사용법에 유의하세요.

단항 산술 연산자로는 ++(증가)와 --(감소) 연산자가 있는데, 전위(prefix)와 후위(postfix) 표기 모두 사용할 수 있어요.

def a = 2
def b = a++ * 3             (1)

assert a == 3 && b == 6

def c = 3
def d = c-- * 2             (2)

assert c == 2 && d == 6

def e = 1
def f = ++e + 3             (3)

assert e == 2 && f == 5

def g = 4
def h = --g + 1             (4)

assert g == 3 && h == 4

(1) — 후위 증가는 표현식이 평가되고 b에 할당된 후에 a를 증가시켜요

(2) — 후위 감소는 표현식이 평가되고 d에 할당된 후에 c를 감소시켜요

(3) — 전위 증가는 표현식이 평가되고 f에 할당되기 전에 e를 증가시켜요

(4) — 전위 감소는 표현식이 평가되고 h에 할당되기 전에 g를 감소시켜요

Boolean에 대한 단항 not 연산자는 조건 연산자 섹션을 참고하세요.

1.3. 할당 산술 연산자

앞에서 본 이항 산술 연산자들은 할당 형태로도 사용할 수 있어요.

  • +=
  • -=
  • *=
  • /=
  • %=
  • **=

실제로 사용해 볼게요.

def a = 4
a += 3

assert a == 7

def b = 5
b -= 3

assert b == 2

def c = 5
c *= 3

assert c == 15

def d = 10
d /= 2

assert d == 5

def e = 10
e %= 3

assert e == 1

def f = 3
f **= 2

assert f == 9

2. 관계 연산자

관계 연산자는 객체들을 비교해서, 두 객체가 같거나 다른지, 또는 하나가 다른 것보다 크거나 작거나 같은지 알 수 있게 해 줘요.

사용 가능한 연산자는 다음과 같아요.

연산자 용도
== 같음
!= 다름
< 작음
<= 작거나 같음
>
>= 크거나 같음
=== 동일(identical) (Groovy 3.0.0부터)
!== 동일하지 않음 (Groovy 3.0.0부터)

이 연산자를 사용한 간단한 숫자 비교 예시를 몇 개 볼게요.

assert 1 + 2 == 3
assert 3 != 4

assert -2 < 3
assert 2 <= 2
assert 3 <= 4

assert 5 > 1
assert 5 >= -2

===!==는 모두 지원되는데, 각각 is() 메서드를 호출하는 것, 그리고 is() 호출을 부정하는 것과 같아요.

import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode
class Creature { String type }

def cat = new Creature(type: 'cat')
def copyCat = cat
def lion = new Creature(type: 'cat')

assert cat.equals(lion) // Java 논리적 동일성
assert cat == lion      // Groovy 축약 연산자

assert cat.is(copyCat)  // Groovy 동일성(identity)
assert cat === copyCat  // 연산자 축약형
assert cat !== lion     // 부정된 연산자 축약형

3. 논리 연산자

Groovy는 boolean 표현식을 위한 세 가지 논리 연산자를 제공해요.

  • &&: 논리 "and"
  • ||: 논리 "or"
  • !: 논리 "not"

다음 예시로 확인해 볼게요.

assert !false           (1)
assert true && true     (2)
assert true || false    (3)

(1) — "not" false는 true

(2) — true "and" true는 true

(3) — true "or" false는 true

3.1. 우선순위

논리 "not"은 논리 "and"보다 우선순위가 높아요.

assert (!false && false) == false   (1)

(1) — 여기서 단언은 참입니다(괄호 안의 표현식이 false이므로). "not"이 "and"보다 우선순위가 높아서 첫 번째 "false" 항에만 적용되기 때문이죠. 그렇지 않았다면 "and"의 결과에 적용되어 true로 만들고 단언이 실패했을 거예요.

논리 "and"는 논리 "or"보다 우선순위가 높아요.

assert true || true && false        (1)

(1) — 여기서 단언은 참입니다. "and"가 "or"보다 우선순위가 높아서 "or"이 마지막에 실행되고, 참인 인자를 하나 가지므로 true를 반환하거든요. 그렇지 않았다면 "and"가 마지막에 실행되어 거짓인 인자를 하나 가지므로 false를 반환하고 단언이 실패했을 거예요.

3.2. 단락 평가(Short-circuiting)

논리 || 연산자는 단락 평가를 지원해요. 왼쪽 피연산자가 true이면 결과가 어차피 true임을 알기 때문에 오른쪽 피연산자르 평가하지 않아요. 오른쪽 피연산자는 왼쪽 피연산자가 false일 때만 평가됩니다.

논리 && 연산자도 마찬가지예요. 왼쪽 피연산자가 false이면 결과가 어차피 false임을 알기 때문에 오른쪽 피연산자를 평가하지 않아요. 오른쪽 피연산자는 왼쪽 피연산자가 true일 때만 평가됩니다.

boolean checkIfCalled() {   (1)
    called = true
}

called = false
true || checkIfCalled()
assert !called              (2)

called = false
false || checkIfCalled()
assert called               (3)

called = false
false && checkIfCalled()
assert !called              (4)

called = false
true && checkIfCalled()
assert called               (5)

(1) — 호출될 때마다 called 플래그를 true로 설정하는 함수를 만들어요

(2) — 첫 번째 경우, called 플래그를 리셋한 후 ||의 왼쪽 피연산자가 true이면 함수가 호출되지 않음을 확인해요. ||가 오른쪽 피연산자 평가를 단락시키기 때문이죠

(3) — 두 번째 경우, 왼쪽 피연산자가 false이므로 함수가 호출돼요. 우리 플래그가 이제 true라는 사실이 그걸 나타내요

(4) — &&도 마찬가지로, 왼쪽 피연산자가 false일 때 함수가 호출되지 않는 것을 확인해요

(5) — 하지만 왼쪽 피연산자가 true이면 함수가 호출돼요

4. 비트 및 비트 시프트 연산자

4.1. 비트 연산자

Groovy는 네 가지 비트 연산자를 제공해요.

  • &: 비트 "and"
  • |: 비트 "or"
  • ^: 비트 "xor"(배타적 "or")
  • ~: 비트 부정

비트 연산자는 byte, short, int, long, BigInteger 타입의 인자에 적용할 수 있어요. 인자 중 하나가 BigInteger면 결과는 BigInteger 타입이고, 그게 아니고 인자 중 하나가 long이면 결과는 long 타입이에요. 그 외에는 결과가 int 타입입니다.

int a = 0b00101010
assert a == 42
int b = 0b00001000
assert b == 8
assert (a & a) == a                     (1)
assert (a & b) == b                     (2)
assert (a | a) == a                     (3)
assert (a | b) == a                     (4)

int mask = 0b11111111                   (5)
assert ((a ^ a) & mask) == 0b00000000   (6)
assert ((a ^ b) & mask) == 0b00100010   (7)
assert ((~a) & mask)    == 0b11010101   (8)

(1) — 비트 and

(2) — 비트 and는 공통 비트를 반환해요

(3) — 비트 or

(4) — 비트 or는 모든 '1' 비트를 반환해요

(5) — 마지막 8비트만 확인하려고 마스크를 설정해요

(6) — 자기 자신에 대한 비트 배타적 or는 0을 반환해요

(7) — 비트 배타적 or

(8) — 비트 부정

기본형 타입의 내부 표현이 Java 언어 명세를 따른다는 점을 알아두는 게 좋아요. 특히 기본형 타입은 부호가 있으므로, 비트 부정을 할 때는 필요한 비트만 뽑기 위해 항상 마스크를 쓰는 게 좋아요.

Groovy에서 비트 연산자는 오버로드할 수 있어요. 즉 어떤 종류의 객체에 대해서도 그 연산자의 동작을 정의할 수 있다는 뜻이에요.

4.2. 비트 시프트 연산자

Groovy는 세 가지 비트 시프트 연산자를 제공해요.

  • <<: 왼쪽 시프트
  • >>: 오른쪽 시프트
  • >>>: 부호 없는 오른쪽 시프트

세 연산자 모두 왼쪽 인자가 byte, short, int, long 타입일 때 적용할 수 있어요. 처음 두 연산자는 왼쪽 인자가 BigInteger일 때도 적용할 수 있죠. 왼쪽 인자가 BigInteger면 결과는 BigInteger 타입이고, 그게 아니고 왼쪽 인자가 long이면 결과는 long 타입이에요. 그 외에는 결과가 int 타입입니다.

assert 12.equals(3 << 2)           (1)
assert 24L.equals(3L << 3)         (1)
assert 48G.equals(3G << 4)         (1)

assert 4095 == -200 >>> 20
assert -1 == -200 >> 20
assert 2G == 5G >> 1
assert -3G == -5G >> 1

(1) — 결과 타입을 확인하기 위해 == 대신 equals 메서드를 사용했어요

Groovy에서 비트 시프트 연산자는 오버로드할 수 있어요. 즉 어떤 종류의 객체에 대해서도 그 연산자의 동작을 정의할 수 있다는 뜻이에요.

5. 조건 연산자

5.1. Not 연산자

"not" 연산자는 느낌표(!)로 표현되며, 기반 boolean 표현식의 결과를 뒤집어요. 특히 not 연산자를 Groovy 진리(Groovy truth)와 결합할 수 있어요.

assert (!true)    == false                      (1)
assert (!'foo')   == false                      (2)
assert (!'')      == true                       (3)

(1) — true의 부정은 false

(2) — 'foo'는 비어 있지 않은 문자열이라 true로 평가되므로, 부정은 false를 반환해요

(3) — ''는 빈 문자열이라 false로 평가되므로, 부정은 true를 반환해요

5.2. 삼항 연산자

삼항 연산자는 if/else 분기로 변수에 어떤 값을 할당하는 것과 동등한 축약 표현식이에요.

이것 대신:

if (string!=null && string.length()>0) {
    result = 'Found'
} else {
    result = 'Not found'
}

이렇게 쓸 수 있어요.

result = (string!=null && string.length()>0) ? 'Found' : 'Not found'

삼항 연산자는 Groovy 진리와도 호환되므로, 더 단순하게 만들 수 있어요.

result = string ? 'Found' : 'Not found'

5.3. 엘비스 연산자

"엘비스 연산자(Elvis operator)"는 삼항 연산자를 줄인 거예요. 표현식이 false에 가까운 값(Groovy 진리 기준)으로 평가될 때 '합리적인 기본값'을 반환하는 경우에 유용해요. 간단한 예는 이렇게 생겼어요.

displayName = user.name ? user.name : 'Anonymous'   (1)
displayName = user.name ?: 'Anonymous'              (2)

(1) — 삼항 연산자에서는 할당하려는 값을 반복해야 해요

(2) — 엘비스 연산자에서는 검사하는 값이 false에 가깝지 않으면 그 값이 사용돼요

엘비스 연산자를 쓰면 조건과 긍정 반환 값 둘 다에서 검사하는 표현식을 중복할 필요가 없어져서, 코드의 장황함을 줄이고 리팩터링 시 오류 위험도 낮춰줘요.

5.4. 엘비스 할당 연산자

Groovy 3.0.0은 엘비스 할당 연산자를 도입했어요. 예를 들어:

import groovy.transform.ToString

@ToString(includePackage = false)
class Element {
    String name
    int atomicNumber
}

def he = new Element(name: 'Helium')
he.with {
    name = name ?: 'Hydrogen'   // 기존 엘비스 연산자
    atomicNumber ?= 2           // 새 엘비스 할당 축약형
}
assert he.toString() == 'Element(Helium, 2)'

6. 객체 연산자

6.1. 안전 내비게이션 연산자

안전 내비게이션(Safe Navigation) 연산자는 NullPointerException을 피하는 데 사용돼요. 보통 객체에 대한 참조가 있을 때, 객체의 메서드나 프로퍼티에 접근하기 전에 그게 null이 아닌지 확인해야 할 때가 있어요. 이를 피하려고 안전 내비게이션 연산자는 예외를 던지는 대신 그냥 null을 반환해요. 이렇게요.

def person = Person.find { it.id == 123 }    (1)
def name = person?.name                      (2)
assert name == null                          (3)

(1) — findnull 인스턴스를 반환할 거예요

(2) — null-safe 연산자를 사용하면 NullPointerException을 막아줘요

(3) — 결과는 null이에요

6.2. 직접 필드 접근 연산자

보통 Groovy에서 이렇게 코드를 쓰면:

class User {
    public final String name                 (1)
    User(String name) { this.name = name}
    String getName() { "Name: $name" }       (2)
}
def user = new User('Bob')
assert user.name == 'Name: Bob'              (3)

(1) — 공개 필드 name

(2) — name에 대한 커스텀 문자열을 반환하는 getter

(3) — getter를 호출해요

user.name 호출은 같은 이름의 프로퍼티에 대한 호출을 일으켜요. 즉 여기서는 name의 getter를 호출하는 거죠. getter를 호출하는 대신 필드를 가져오고 싶다면, 직접 필드 접근 연산자를 사용할 수 있어요.

assert user.@name == 'Bob'                   (1)

(1) — .@를 쓰면 getter 대신 필드를 강제로 사용해요

6.3. 메서드 포인터 연산자

메서드 포인터 연산자(.&)는 메서드에 대한 참조를 변수에 저장해 나중에 호출하는 데 쓸 수 있어요.

def str = 'example of method reference'            (1)
def fun = str.&toUpperCase                         (2)
def upper = fun()                                  (3)
assert upper == str.toUpperCase()                  (4)

(1) — str 변수는 String을 담고 있어요

(2) — str 인스턴스의 toUpperCase 메서드에 대한 참조를 fun이라는 변수에 저장해요

(3) — fun은 일반 메서드처럼 호출할 수 있어요

(4) — 결과가 str에 직접 호출한 것과 같다는 걸 확인할 수 있어요

메서드 포인터를 쓰면 여러 장점이 있어요. 먼저, 그런 메서드 포인터의 타입은 groovy.lang.Closure이므로 클로저가 쓰이는 어떤 곳에서든 사용할 수 있어요. 특히 전략 패턴의 필요에 맞춰 기존 메서드를 변환하는 데 적합합니다.

def transform(List elements, Closure action) {                    (1)
    def result = []
    elements.each {
        result << action(it)
    }
    result
}
String describe(Person p) {                                       (2)
    "$p.name is $p.age"
}
def action = this.&describe                                       (3)
def list = [
    new Person(name: 'Bob',   age: 42),
    new Person(name: 'Julia', age: 35)]                           (4)
assert transform(list, action) == ['Bob is 42', 'Julia is 35']    (5)

(1) — transform 메서드는 리스트의 각 요소를 가져와 action 클로저를 호출하고 새 리스트를 반환해요

(2) — Person을 받아 String을 반환하는 함수를 정의해요

(3) — 그 함수에 메서드 포인터를 만들어요

(4) — 디스크립터를 모으고 싶은 요소들의 리스트를 만들어요

(5) — Closure가 기대되는 곳에 메서드 포인터를 사용할 수 있어요

메서드 포인터는 수신자와 메서드 이름에 바인딩돼요. 인자는 런타임에 해석되는데, 즉 같은 이름의 메서드가 여러 개 있어도 문법은 동일하고, 호출할 적절한 메서드의 해석만 런타임에 이루어진다는 뜻이에요.

def doSomething(String str) { str.toUpperCase() }    (1)
def doSomething(Integer x) { 2*x }                   (2)
def reference = this.&doSomething                    (3)
assert reference('foo') == 'FOO'                     (4)
assert reference(123)   == 246                       (5)

(1) — String 인자를 받는 오버로드된 doSomething 메서드를 정의해요

(2) — Integer 인자를 받는 오버로드된 doSomething 메서드를 정의해요

(3) — 인자 타입을 지정하지 않고 doSomething에 단일 메서드 포인터를 만들어요

(4) — String으로 메서드 포인터를 쓰면 doSomethingString 버전이 호출돼요

(5) — Integer로 메서드 포인터를 쓰면 doSomethingInteger 버전이 호출돼요

Java 8 메서드 참조 기대치와 맞추기 위해, Groovy 3 이상에서는 생성자에 대한 메서드 포인터를 얻기 위해 메서드 이름으로 new를 사용할 수 있어요.

def foo  = BigInteger.&new
def fortyTwo = foo('42')
assert fortyTwo == 42G

또한 Groovy 3 이상에서는 클래스의 인스턴스 메서드에 대한 메서드 포인터를 얻을 수 있어요. 이 메서드 포인터는 그 메서드를 호출할 수신자 인스턴스인 추가 매개변수를 하나 취합니다.

def instanceMethod = String.&toUpperCase
assert instanceMethod('foo') == 'FOO'

역호환성을 위해, 이 경우 호출에 올바른 매개변수를 우연히 갖는 정적 메서드가 인스턴스 메서드보다 우선합니다.

6.4. 메서드 참조 연산자

Groovy 3+의 Parrot 파서는 Java 8+ 메서드 참조 연산자를 지원해요. 메서드 참조 연산자(::)는 함수형 인터페이스를 기대하는 컨텍스트에서 메서드나 생성자를 참조하는 데 쓸 수 있어요. 이는 Groovy의 메서드 포인터 연산자가 제공하는 기능과 다소 겹칩니다. 실제로 동적 Groovy에서 메서드 참조 연산자는 그저 메서드 포인터 연산자의 별칭이에요. 정적 Groovy에서는 그 연산자가 Java가 같은 컨텍스트에서 생성할 바이트코드와 비슷한 바이트코드를 생성해요.

지원되는 다양한 메서드 참조 케이스를 보여주는 예시가 다음 스크립트에 있어요.

import groovy.transform.CompileStatic
import static java.util.stream.Collectors.toList

@CompileStatic
void methodRefs() {
    assert 6G == [1G, 2G, 3G].stream().reduce(0G, BigInteger::add)                           (1)

    assert [4G, 5G, 6G] == [1G, 2G, 3G].stream().map(3G::add).collect(toList())              (2)

    assert [1G, 2G, 3G] == [1L, 2L, 3L].stream().map(BigInteger::valueOf).collect(toList())  (3)

    assert [1G, 2G, 3G] == [1L, 2L, 3L].stream().map(3G::valueOf).collect(toList())          (4)
}

methodRefs()

(1) — 클래스 인스턴스 메서드 참조: add(BigInteger val)은 BigInteger의 인스턴스 메서드예요

(2) — 객체 인스턴스 메서드 참조: add(BigInteger val)은 객체 3G의 인스턴스 메서드예요

(3) — 클래스 정적 메서드 참조: valueOf(long val)은 클래스 BigInteger의 정적 메서드예요

(4) — 객체 정적 메서드 참조: valueOf(long val)은 객체 3G의 정적 메서드예요(어떤 이들은 일반적인 상황에서 나쁜 스타일로 여겨요)

지원되는 다양한 생성자 참조 케이스를 보여주는 예시가 다음 스크립트에 있어요.

@CompileStatic
void constructorRefs() {
    assert [1, 2, 3] == ['1', '2', '3'].stream().map(Integer::valueOf).collect(toList())  (1)

    def result = [1, 2, 3].stream().toArray(Integer[]::new)                           (2)
    assert result instanceof Integer[]
    assert result.toString() == '[1, 2, 3]'
}

constructorRefs()

(1) — 클래스 생성자 참조

(2) — 배열 생성자 참조

7. 정규식 연산자

7.1. 패턴 연산자

패턴 연산자(~)는 java.util.regex.Pattern 인스턴스를 만드는 간단한 방법을 제공해요.

def p = ~/foo/
assert p instanceof Pattern

보통 패턴 연산자를 slashy 문자열 안의 표현식과 함께 사용하지만, Groovy에서는 어떤 종류의 String과도 쓸 수 있어요.

p = ~'foo'                                                        (1)
p = ~"foo"                                                        (2)
p = ~$/dollar/slashy $ string/$                                   (3)
p = ~"${pattern}"                                                 (4)

(1) — 작은따옴표 문자열 사용

(2) — 큰따옴표 문자열 사용

(3) — dollar-slashy 문자열은 슬래시와 달러 기호를 이스케이프 없이 쓸 수 있게 해 줘요

(4) — GString도 사용할 수 있어요!

Pattern, Find, Match 연산자와 대부분의 String 형태를 쓸 수 있지만, 그렇지 않으면 필요한 이스케이프 요구 사항을 기억하지 않아도 되도록, 대부분의 경우 slashy 문자열을 사용할 것을 권장해요.

7.2. Find 연산자

패턴을 만드는 대신, find 연산자 =~를 사용해 java.util.regex.Matcher 인스턴스를 직접 만들 수 있어요.

def text = "some text to match"
def m = text =~ /match/                                           (1)
assert m instanceof Matcher                                       (2)
if (!m) {                                                         (3)
    throw new RuntimeException("Oops, text not found!")
}

(1) — =~는 오른쪽의 패턴을 사용해 text 변수에 대한 매처(matcher)를 만들어요

(2) — =~의 반환 타입은 Matcher예요

(3) — if (!m.find(0))을 호출하는 것과 동등해요

Matcher는 그 find 메서드 호출로 boolean으로 강제 변환되므로, =~ 연산자는 Perl의 =~ 연산자를 술어로 쓸 때(if, ?: 등에서)의 간단한 사용법과 일관됩니다. 지정된 패턴의 매치를 순회하려는 의도(while 등)라면 매처에서 find()를 직접 호출하거나 iterator DGM을 사용하세요.

7.3. Match 연산자

match 연산자(==~)는 find 연산자의 약간 변형된 것으로, Matcher가 아니라 boolean을 반환하고 입력 문자열의 엄격한 매칭을 요구해요.

m = text ==~ /match/                                              (1)
assert m instanceof Boolean                                       (2)
if (m) {                                                          (3)
    throw new RuntimeException("Should not reach that point!")
}

(1) — ==~는 주제를 정규식과 매칭하지만, 매칭은 엄격해야 해요

(2) — 그래서 ==~의 반환 타입은 boolean이에요

(3) — if (text ==~ /match/)를 호출하는 것과 동등해요

7.4. Find vs Match 연산자 비교

보통 match 연산자는 패턴이 단일 정확 매칭과 관련될 때 사용되고, 그 외에는 find 연산자가 더 유용할 수 있어요.

assert 'two words' ==~ /\S+\s+\S+/
assert 'two words' ==~ /^\S+\s+\S+$/         (1)
assert !(' leading space' ==~ /\S+\s+\S+/)   (2)

def m1 = 'two words' =~ /^\S+\s+\S+$/
assert m1.size() == 1                          (3)
def m2 = 'now three words' =~ /^\S+\s+\S+$/    (4)
assert m2.size() == 0                          (5)
def m3 = 'now three words' =~ /\S+\s+\S+/
assert m3.size() == 1                          (6)
assert m3[0] == 'now three'
def m4 = ' leading space' =~ /\S+\s+\S+/
assert m4.size() == 1                          (7)
assert m4[0] == 'leading space'
def m5 = 'and with four words' =~ /\S+\s+\S+/
assert m5.size() == 2                          (8)
assert m5[0] == 'and with'
assert m5[1] == 'four words'

(1) — 동등하지만, 명시적인 ^$는 필요 없으므로 권장되지 않아요

(2) — 앞 공백 때문에 매치 없음

(3) — 매치 1개

(4) — ^$는 정확 매칭이 필요함을 나타내요

(5) — 매치 0개

(6) — 매치 1개, 첫 단어에서 탐욕적으로 시작

(7) — 매치 1개, 앞 공백 무시

(8) — 매치 2개

8. 기타 연산자

8.1. 스프레드 연산자

스프레드-닷(Spread-dot) 연산자(*.)는 보통 그냥 스프레드 연산자라고 부르는데, 집계 객체의 모든 항목에 어떤 동작을 호출할 때 사용해요. 각 항목에 동작을 호출하고 결과를 리스트로 모으는 것과 동등합니다.

class Car {
    String make
    String model
}
def cars = [
       new Car(make: 'Peugeot', model: '508'),
       new Car(make: 'Renault', model: 'Clio')]       (1)
def makes = cars*.make                                (2)
assert makes == ['Peugeot', 'Renault']                (3)

(1) — Car 항목들의 리스트를 만들어요. 리스트는 객체들의 집계예요.

(2) — 리스트에 스프레드 연산자를 호출해 각 항목의 make 프로퍼티에 접근해요

(3) — make 항목들의 모음에 해당하는 문자열 리스트를 반환해요

cars*.make 표현식은 cars.collect{ it.make }와 동등해요. Groovy의 GPath 표기법은 참조하는 프로퍼티가 포함 리스트의 프로퍼티가 아닐 때 축약을 허용하는데, 그 경우 자동으로 스프레드됩니다. 앞서 언급한 경우 cars.make 표현식을 쓸 수 있지만, 명시적 스프레드-닷 연산자를 유지하는 게 종종 권장됩니다.

스프레드 연산자는 null-safe예요. 즉 컬렉션의 요소가 null이면 NullPointerException을 던지는 대신 null을 반환한다는 뜻이에요.

cars = [
   new Car(make: 'Peugeot', model: '508'),
   null,                                              (1)
   new Car(make: 'Renault', model: 'Clio')]
assert cars*.make == ['Peugeot', null, 'Renault']     (2)
assert null*.make == null                             (3)

(1) — 요소 중 하나가 null인 리스트를 만들어요

(2) — 스프레드 연산자를 사용하면 NullPointerException던지지 않아요

(3) — 수신자도 null일 수 있고, 그 경우 반환 값은 null이에요

스프레드 연산자는 Iterable 인터페이스를 구현하는 어떤 클래스에서든 사용할 수 있어요.

class Component {
    Integer id
    String name
}
class CompositeObject implements Iterable<Component> {
    def components = [
        new Component(id: 1, name: 'Foo'),
        new Component(id: 2, name: 'Bar')]

    @Override
    Iterator<Component> iterator() {
        components.iterator()
    }
}
def composite = new CompositeObject()
assert composite*.id == [1,2]
assert composite*.name == ['Foo','Bar']

그 자체로 집계를 포함하는 데이터 구조의 집계를 다룰 때는 스프레드-닷 연산자를 여러 번 호출해요(여기서는 cars*.models*.name).

class Make {
    String name
    List<Model> models
}

@Canonical
class Model {
    String name
}

def cars = [
    new Make(name: 'Peugeot',
             models: [new Model('408'), new Model('508')]),
    new Make(name: 'Renault',
             models: [new Model('Clio'), new Model('Captur')])
]

def makes = cars*.name
assert makes == ['Peugeot', 'Renault']

def models = cars*.models*.name
assert models == [['408', '508'], ['Clio', 'Captur']]
assert models.sum() == ['408', '508', 'Clio', 'Captur'] // flatten one level
assert models.flatten() == ['408', '508', 'Clio', 'Captur'] // flatten all levels (one in this case)

컬렉션의 컬렉션에는 스프레드-닷 연산자 대신 collectNested DGM 메서드 사용을 고려해 보세요.

class Car {
    String make
    String model
}
def cars = [
   [
       new Car(make: 'Peugeot', model: '408'),
       new Car(make: 'Peugeot', model: '508')
   ], [
       new Car(make: 'Renault', model: 'Clio'),
       new Car(make: 'Renault', model: 'Captur')
   ]
]
def models = cars.collectNested{ it.model }
assert models == [['408', '508'], ['Clio', 'Captur']]

8.1.1. 메서드 인자 스프레딩

메서드 호출의 인자를 리스트에서 찾아 메서드 인자에 맞춰야 하는 상황이 있을 수 있어요. 그런 상황에서는 스프레드 연산자를 사용해 메서드를 호출할 수 있어요. 예를 들어 다음 메서드 시그니처가 있다고 상상해 보세요.

int function(int x, int y, int z) {
    x*y+z
}

그리고 다음 리스트가 있으면:

def args = [4,5,6]

중간 변수를 정의하지 않고도 메서드를 호출할 수 있어요.

assert function(*args) == 26

일반 인자와 스프레드 인자를 섞는 것도 가능합니다.

args = [4]
assert function(*args,5,6) == 26

8.1.2. 스프레드 리스트 요소

리스트 리터럴 안에서 사용되면 스프레드 연산자는 스프레드 요소의 내용이 리스트에 인라인된 것처럼 동작해요.

def items = [4,5]                      (1)
def list = [1,2,3,*items,6]            (2)
assert list == [1,2,3,4,5,6]           (3)

(1) — items는 리스트예요

(2) — addAll을 호출하지 않고 items 리스트의 내용을 list에 직접 넣고 싶어요

(3) — items의 내용이 list에 인라인됐어요

8.1.3. 스프레드 맵 요소

스프레드 맵 연산자는 스프레드 리스트 연산자와 비슷한 방식으로 동작하지만 맵용이에요. 다음 예시처럼 맵의 내용을 다른 맵 리터럴에 인라인할 수 있게 해 줍니다.

def m1 = [c:3, d:4]                   (1)
def map = [a:1, b:2, *:m1]            (2)
assert map == [a:1, b:2, c:3, d:4]    (3)

(1) — m1은 인라인하고 싶은 맵이에요

(2) — *:m1 표기법을 사용해 m1의 내용을 map에 스프레드해요

(3) — mapm1의 모든 요소를 담고 있어요

스프레드 맵 연산자의 위치도 중요해요. 다음 예시처럼요.

def m1 = [c:3, d:4]                   (1)
def map = [a:1, b:2, *:m1, d: 8]      (2)
assert map == [a:1, b:2, c:3, d:8]    (3)

(1) — m1은 인라인하고 싶은 맵이에요

(2) — *:m1 표기법으로 m1의 내용을 map에 스프레드하지만, 스프레드 후에d를 재정의해요

(3) — map은 기대했던 모든 키를 담고 있지만, d는 재정의됐어요

8.2. 범위 연산자

Groovy는 범위(Range) 개념을 지원하며, 객체의 범위를 만들기 위한 표기법(..)을 제공해요.

def range = 0..5                                    (1)
assert (0..5).collect() == [0, 1, 2, 3, 4, 5]       (2)
assert (0..<5).collect() == [0, 1, 2, 3, 4]         (3)
assert (0<..5).collect() == [1, 2, 3, 4, 5]         (4)
assert (0<..<5).collect() == [1, 2, 3, 4]           (5)
assert (0..5) instanceof List                       (6)
assert (0..5).size() == 6                           (7)

(1) — 지역 변수에 저장된 간단한 정수 범위

(2) — 경계를 모두 포함하는 IntRange

(3) — 상한이 제외된 IntRange

(4) — 하한이 제외된 IntRange

(5) — 하한과 상한이 모두 제외된 IntRange

(6) — groovy.lang.RangeList 인터페이스를 구현해요

(7) — 즉 그에 대해 size 메서드를 호출할 수 있다는 뜻이에요

범위 구현은 가벼워서 하한과 상한만 저장합니다. next()previous() 메서드로 범위의 다음/이전 항목을 결정할 수 있는 어떤 Comparable 객체에서든 범위를 만들 수 있어요. 예를 들어 문자 범위는 이렇게 만들 수 있어요.

assert ('a'..'d').collect() == ['a','b','c','d']

8.3. 우주선 연산자

우주선(Spaceship) 연산자(<=>)는 compareTo 메서드에 위임해요.

assert (1 <=> 1) == 0
assert (1 <=> 2) == -1
assert (2 <=> 1) == 1
assert ('a' <=> 'z') == -1

8.4. 첨자 연산자

첨자(Subscript) 연산자는 할당의 왼쪽에 있느냐 오른쪽에 있느냐에 따라 getAt 또는 putAt의 축약 표기예요.

def list = [0,1,2,3,4]
assert list[2] == 2                         (1)
list[2] = 4                                 (2)
assert list[0..2] == [0,1,4]                (3)
list[0..2] = [6,6,6]                        (4)
assert list == [6,6,6,3,4]                  (5)

(1) — getAt(2) 대신 [2]를 쓸 수 있어요

(2) — 할당의 왼쪽에 있으면 putAt을 호출할 거예요

(3) — getAt도 범위를 지원해요

(4) — putAt도 마찬가지예요

(5) — 리스트가 변경돼요

첨자 연산자는 getAt/putAt의 커스텀 구현과 함께 쓰면 객체를 분해(destructuring)하는 편리한 방법이 돼요.

class User {
    Long id
    String name
    def getAt(int i) {                                             (1)
        switch (i) {
            case 0: return id
            case 1: return name
        }
        throw new IllegalArgumentException("No such element $i")
    }
    void putAt(int i, def value) {                                 (2)
        switch (i) {
            case 0: id = value; return
            case 1: name = value; return
        }
        throw new IllegalArgumentException("No such element $i")
    }
}
def user = new User(id: 1, name: 'Alex')                           (3)
assert user[0] == 1                                                (4)
assert user[1] == 'Alex'                                           (5)
user[1] = 'Bob'                                                    (6)
assert user.name == 'Bob'                                          (7)

(1) — User 클래스는 커스텀 getAt 구현을 정의해요

(2) — User 클래스는 커스텀 putAt 구현을 정의해요

(3) — 샘플 사용자를 만들어요

(4) — 인덱스 0으로 첨자 연산자를 쓰면 사용자 id를 가져올 수 있어요

(5) — 인덱스 1로 첨자 연산자를 쓰면 사용자 이름을 가져올 수 있어요

(6) — putAt에 위임하는 덕분에 첨자 연산자로 프로퍼티에 쓸 수 있어요

(7) — 정말 name 프로퍼티가 바뀌었는지 확인해요

8.5. 안전 인덱스 연산자

Groovy 3.0.0은 안전 인덱싱(safe indexing) 연산자, 즉 ?.와 비슷한 ?[]를 도입했어요. 예를 들어:

String[] array = ['a', 'b']
assert 'b' == array?[1]      // get using normal array index
array?[1] = 'c'              // set using normal array index
assert 'c' == array?[1]

array = null
assert null == array?[1]     // return null for all index values
array?[1] = 'c'              // quietly ignore attempt to set value
assert null == array?[1]

def personInfo = [name: 'Daniel.Sun', location: 'Shanghai']
assert 'Daniel.Sun' == personInfo?['name']      // get using normal map index
personInfo?['name'] = 'sunlan'                  // set using normal map index
assert 'sunlan' == personInfo?['name']

personInfo = null
assert null == personInfo?['name']              // return null for all map values
personInfo?['name'] = 'sunlan'                  // quietly ignore attempt to set value
assert null == personInfo?['name']

8.6. 소속 연산자

소속(Membership) 연산자(in)는 isCase 메서드를 호출하는 것과 동등해요. List의 문맥에서는 contains를 호출하는 것과 동등한데, 다음 예시처럼요.

def list = ['Grace','Rob','Emmy']
assert ('Emmy' in list)                     (1)
assert ('Alex' !in list)                    (2)

(1) — list.contains('Emmy') 또는 list.isCase('Emmy')를 호출하는 것과 동등해요

(2) — 소속 부정은 !list.contains('Emmy') 또는 !list.isCase('Emmy')를 호출하는 것과 동등해요

8.7. 동일성 연산자

Groovy에서 ==로 동등성을 검사하는 것은 Java에서 같은 연산자를 쓰는 것과 달라요. Groovy에서는 equals를 호출해요. 참조 동일성을 비교하려면 다음 예시처럼 is를 사용해야 해요.

def list1 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3']        (1)
def list2 = ['Groovy 1.8','Groovy 2.0','Groovy 2.3']        (2)
assert list1 == list2                                       (3)
assert !list1.is(list2)                                     (4)
assert list1 !== list2                                      (5)

(1) — 문자열 리스트를 만들어요

(2) — 같은 요소를 포함하는 또 다른 문자열 리스트를 만들어요

(3) — ==를 사용하면 객체 동등성을 검사해요. Java의 list1.equals(list2)와 동등해요

(4) — is를 사용하면 참조가 서로 다르다는 것을 확인할 수 있어요. Java의 list1 == list2와 동등해요

(5) — === 또는 !==(Groovy 3.0.0부터 지원·권장)를 사용하면 참조가 다르거나 같은지도 확인할 수 있어요. Java의 list1 == list2list1 != list2에 해당해요

8.8. 강제 변환 연산자

강제 변환(Coercion) 연산자(as)는 캐스팅의 변형이에요. 강제 변환은 객체를 한 타입에서 다른 타입으로 할당에 호환되지 않더라도 변환해요. 예시를 볼게요.

String input = '42'
Integer num = (Integer) input                      (1)

(1) — StringInteger에 할당할 수 없으므로, 런타임에 ClassCastException을 일으킬 거예요

이는 강제 변환을 사용해 고칠 수 있어요.

String input = '42'
Integer num = input as Integer                      (1)

(1) — StringInteger에 할당할 수 없지만, as를 쓰면 Integer강제 변환돼요

객체가 다른 것으로 강제 변환될 때, 대상 타입이 소스 타입과 같지 않으면 강제 변환은 객체를 반환해요. 강제 변환 규칙은 소스와 대상 타입에 따라 달라지며, 변환 규칙을 찾을 수 없으면 강제 변환은 실패할 수 있어요. 커스텀 변환 규칙은 asType 메서드로 구현할 수 있어요.

class Identifiable {
    String name
}
class User {
    Long id
    String name
    def asType(Class target) {                                              (1)
        if (target == Identifiable) {
            return new Identifiable(name: name)
        }
        throw new ClassCastException("User cannot be coerced into $target")
    }
}
def u = new User(name: 'Xavier')                                            (2)
def p = u as Identifiable                                                   (3)
assert p instanceof Identifiable                                            (4)
assert !(p instanceof User)                                                 (5)

(1) — User 클래스는 User에서 Identifiable로의 커스텀 변환 규칙을 정의해요

(2) — User의 인스턴스를 만들어요

(3) — User 인스턴스를 Identifiable로 강제 변환해요

(4) — 대상은 Identifiable의 인스턴스예요

(5) — 대상은 더 이상 User의 인스턴스가 아니에요

8.9. 다이아몬드 연산자

다이아몬드(Diamond) 연산자(<>)는 Java 7의 같은 이름의 연산자와 호환성을 지원하기 위해 추가된 문법 설탕 전용 연산자예요. 제네릭 타입이 선언에서 추론되어야 함을 나타내는 데 사용돼요.

List<String> strings = new LinkedList<>()

동적 Groovy에서는 이것이 전혀 사용되지 않아요. 정적으로 타입 검사되는 Groovy에서는, Groovy 타입 검사기가 이 연산자의 유무와 상관없이 타입 추론을 수행하므로 이것도 선택적이에요.

8.10. 호출 연산자

호출(Call) 연산자 ()call이라는 이름의 메서드를 암시적으로 호출하는 데 사용돼요. call 메서드를 정의하는 어떤 객체든 .call 부분을 생략하고 호출 연산자를 사용할 수 있어요.

class MyCallable {
    int call(int x) {           (1)
        2*x
    }
}

def mc = new MyCallable()
assert mc.call(2) == 4          (2)
assert mc(2) == 4               (3)

(1) — MyCallablecall이라는 이름의 메서드를 정의해요. java.util.concurrent.Callable을 구현할 필요는 없다는 점을 유의하세요

(2) — 고전적인 메서드 호출 문법으로 그 메서드를 호출할 수 있어요

(3) — 또는 호출 연산자 덕분에 .call을 생략할 수 있어요

9. 연산자 우선순위

아래 표는 모든 groovy 연산자를 우선순위 순서로 나열한 거예요.

레벨 연산자 이름
1 new () 객체 생성, 명시적 괄호
() {} [] 메서드 호출, 클로저, 리터럴 리스트/맵
. .& .@ 멤버 접근, 메서드 클로저, 필드/속성 접근
?. * *. *: 안전 역참조, 스프레드, 스프레드-닷, 스프레드-맵
~ ! (type) 비트 부정/패턴, not, 타입캐스트
[] ?[] ++ -- 리스트/맵/배열 (안전) 인덱스, 후위 증가/감소
2 ** 거듭제곱
3 ++ -- + - 전위 증가/감소, 단항 플러스, 단항 마이너스
4 * / % 곱셈, 나눗셈, 나머지
5 + - 덧셈, 뺄셈
6 << >> >>> .. ..< <..< <.. 왼쪽/오른쪽(부호 없는) 시프트, 포함/제외 범위
7 < <= > >= in !in instanceof !instanceof as 미만/이하, 초과/이상, in, not in, instanceof, not instanceof, 타입 강제 변환
8 == != <=> === !== 같음, 다름, 비교, 동일, 동일하지 않음
=~ ==~ 정규식 find, 정규식 match
9 & 이진/비트 and
10 ^ 이진/비트 xor
11 ` `
12 && 논리 and
13 `
13.5 ==> 논리 함의
14 ? : 삼항 조건
?: 엘비스 연산자
15 = **= *= /= %= += -= <<= >>= >>>= &= ^= ` = ?=`

10. 연산자 오버로딩

Groovy는 여러 연산자를 오버로드해서 여러분 자신의 클래스에서 사용할 수 있게 해 줘요. 이 간단한 클래스를 고려해 볼게요.

class Bucket {
    int size

    Bucket(int size) { this.size = size }

    Bucket plus(Bucket other) {                     (1)
        return new Bucket(this.size + other.size)
    }
}

(1) — Bucketplus()라는 특별한 메서드를 구현해요

plus() 메서드를 구현하는 것만으로 Bucket 클래스는 + 연산자와 함께 사용할 수 있게 돼요. 이렇게요.

def b1 = new Bucket(4)
def b2 = new Bucket(11)
assert (b1 + b2).size == 15                         (1)

(1) — 두 Bucket 객체를 + 연산자로 더할 수 있어요

모든 (비-비교기) Groovy 연산자는 여러분 자신의 클래스에서 구현할 수 있는 대응 메서드를 가져요. 요구 사항은 메서드가 public이고 올바른 이름을 가지며 올바른 수의 인자를 갖는 것뿐이에요. 인자 타입은 연산자의 오른쪽에서 지원하려는 타입에 따라 달라져요. 예를 들어 다음 문장을 지원하려면

assert (b1 + 11).size == 15

다음 시그니처로 plus() 메서드를 구현하면 돼요.

Bucket plus(int capacity) {
    return new Bucket(this.size + capacity)
}

다음은 연산자와 그 대응 메서드의 전체 목록이에요.

연산자 메서드 연산자 메서드
+ a.plus(b) a[b] a.getAt(b)
- a.minus(b) a[b] = c a.putAt(b, c)
* a.multiply(b) a in b b.isCase(a)
/ a.div(b) << a.leftShift(b)
% a.mod(b) >> a.rightShift(b)
** a.power(b) >>> a.rightShiftUnsigned(b)
` ` a.or(b) ++
& a.and(b) -- a.previous()
^ a.xor(b) +a a.positive()
as a.asType(b) -a a.negative()
a() a.call() ~a a.bitwiseNegate()

더 알아보기