스타일 가이드

스타일 가이드 (Style Guide)

Groovy를 새로 시작하는 자바 개발자는 아마 자바를 머릿속에 품은 채 출발할 거예요. 그런 분들이 한 기능씩 Groovy를 익혀 가면서 더 능숙해지고, 더 'Groovy다운' 코드를 쓰게 되는 과정을 돕는 게 이 문서의 목적이에요. 자주 쓰이는 Groovy 문법 스타일과 새로운 연산자, 클로저 같은 기능들을 짚어 드릴게요.

참고로 이 가이드는 완전한 문서는 아니에요. 빠르게 훑어볼 수 있는 입문서 정도로 생각해 주시고, 문서에 기여하고 싶다면 더해 주시면 좋겠습니다.

출처: Groovy 공식 문서 - Style guide

본문

1. 세미콜론 없이 쓰기 (No semicolons)

C/C++/C#/Java 출신이라면 세미콜론에 너무 익숙해서 어디에나 찍게 돼요. 게다가 Groovy가 자바 문법의 99%를 지원하다 보니, 자바 코드를 그대로 붙여 넣다 보면 세미콜론이 쌓이기 쉽죠.

하지만 세미콜론은 Groovy에서 생략 가능해요. 빼도 되고, 오히려 뺴는 편이 더 관용적(idiomatic)이에요.

2. return 키워드는 선택 (Return keyword optional)

Groovy에서는 메서드 본문에서 마지막으로 평가된 표현식이 return 키워드 없이도 반환돼요. 특히 짧은 메서드나 클로저에서는 생략하는 편이 깔끔해요.

String toString() { return "a server" }
String toString() { "a server" }

하지만 변수를 쓰는데 그 변수가 두 줄에 걸쳐 두 번 보이는 경우에는 오히려 지저분해 보일 수 있어요.

def props() {
    def m1 = [a: 1, b: 2]
    m2 = m1.findAll { k, v -> v % 2 == 0 }
    m2.c = 3
    m2
}

이런 경우에는 마지막 표현식 앞에 줄바꿈을 넣거나, 명시적으로 return을 쓰는 편이 가독성에 나을 수 있어요. 저도 상황에 따라 return을 쓸 때도 있고 안 쓸 때도 있는데, 취향의 영역인 경우가 많아요. 다만 클로저 안에서는 보통 생략하는 편이에요. 그러니까 키워드가 선택 사항이더라도, 쓰는 게 가독성을 해친다고 생각되면 안 써도 된다는 뜻이에요.

한 가지 주의할 점이 있어요. def 키워드로 선언한 메서드에서는 마지막 표현식이 때때로 반환되는 일을 볼 수 있는데, 놀랄 수 있으니 void나 구체적인 타입을 명시하는 쪽을 보통 선호해요. 위 예시에서 마지막 문장인 m2를 깜빡 빠뜨렸다고 생각해 보면, 마지막 표현식은 m2.c = 3이 되고 결국 기대한 맵이 아니라 3이 반환돼요.

if/else, try/catch 같은 문장도 마지막 표현식을 평가해서 값을 반환할 수 있어요.

def foo(n) {
    if(n == 1) {
        "Roshan"
    } else {
        "Dawrani"
    }
}

assert foo(1) == "Roshan"
assert foo(2) == "Dawrani"

3. def와 타입 (Def and type)

타입에 대해 말하다 보면, def와 타입을 같이 쓰는 개발자들을 자주 보게 돼요. 그런데 여기서 def는 중복이에요. def를 쓸지, 타입을 쓸지 하나만 골라 주세요.

그러니까 이렇게 쓰지 말고:

def String name = "Guillaume"

이렇게 쓰는 게 맞아요.

String name = "Guillaume"

Groovy에서 def를 쓰면 실제 타입은 Object가 돼요. 그래서 def로 선언한 변수에는 어떤 객체든 넣을 수 있고, def를 반환 타입으로 선언한 메서드도 어떤 객체든 반환할 수 있어요.

타입 없는 파라미터를 선언할 때 def를 쓸 수는 있는데, 꼭 필요하지는 않아요. 그래서 보통 생략하는 편이에요. 이렇게 쓰는 대신:

void doSomething(def param1, def param2) { }

이렇게 쓰는 걸 선호해요:

void doSomething(param1, param2) { }

다만 이 문서의 마지막 섹션에서 언급하듯이, 메서드 파라미터에는 타입을 붙여 주는 편이 코드 문서화에 좋고, IDE의 코드 완성이나 정적 타입 검사·정적 컴파일 기능을 활용하는 데도 도움이 돼요.

def가 중복이라 피해야 할 또 다른 곳은 생성자예요.

class MyClass {
    def MyClass() {}
}

대신 그냥 def를 빼면 돼요:

class MyClass {
    MyClass() {}
}

4. 기본은 public (Public by default)

Groovy는 기본적으로 클래스와 메서드를 public으로 간주해요. 그래서 모든 곳에 public 수식어를 붙일 필요가 없어요. public이 아닌 경우에만 접근 제한 수식어를 붙이면 돼요.

이렇게 쓰는 대신:

public class Server {
    public String toString() { return "a server" }
}

더 간결한 이 형태를 선호해요:

class Server {
    String toString() { "a server" }
}

'package-scope' 접근성이 궁금할 수 있는데, Groovy에서 public을 생략할 수 있다는 점 때문에 이 스코프는 기본 지원되지 않아요. 다만 그 접근성을 쓰게 해 주는 특별한 Groovy 애노테이션이 있어요:

class Server {
    @PackageScope Cluster cluster
}

5. 괄호 생략 (Omitting parentheses)

Groovy는 최상위 표현식에서 괄호를 생략할 수 있어요. println 명령이 대표적이에요.

println "Hello"
method a, b

이런 형태가 아니라:

println("Hello")
method(a, b)

클로저가 메서드 호출의 마지막 파라미터인 경우, each{} 같은 반복 메커니즘을 쓸 때처럼, 클로저를 닫는 괄호 밖에 두거나 괄호 자체를 생략할 수 있어요.

list.each( { println it } )
list.each(){ println it }
list.each  { println it }

빈 괄호는 그냥 문법적 노이즈일 뿐이니, 항상 세 번째 형태를 선호해 주세요. 더 자연스러워요.

다만 중첩 메서드 호출을 하거나 파라미터가 없는 메서드를 호출할 때처럼 괄호가 필요한 경우도 있어요.

def foo(n) { n }
def bar() { 1 }

println foo 1 // won't work
def m = bar   // won't work

6. 클래스도 일급 시민 (Classes as first-class citizens)

Groovy에서는 .class 접미사가 필요 없어요. 자바의 instanceof와 비슷한 느낌이에요. 예를 들면:

connection.doPost(BASE_URI + "/modify.hqu", params, ResourcesResponse.class)

아래에서 다룰 GStrings와 일급 시민을 활용하면 이렇게 쓸 수 있어요.

connection.doPost("${BASE_URI}/modify.hqu", params, ResourcesResponse)

7. Getter와 Setter (Getters and Setters)

Groovy에서는 getter와 setter가 모여 '프로퍼티(property)'를 이뤄요. 그래서 이 프로퍼티에 접근·설정하는 간단한 표기법을 제공해요. 자바식으로 getter/setter를 호출하는 대신, 필드에 접근하듯 쓸 수 있어요.

resourceGroup.getResourcePrototype().getName() == SERVER_TYPE_NAME
resourceGroup.resourcePrototype.name == SERVER_TYPE_NAME

resourcePrototype.setName("something")
resourcePrototype.name = "something"

Groovy로 빈(bean)을 작성할 때, 흔히 POGO(Plain Old Groovy Objects)라고 부르는 그런 객체를 만들 때는 필드와 getter/setter를 직접 만들 필요 없이 Groovy 컴파일러가 대신 만들어 줘요.

이렇게 쓰는 대신:

class Person {
    private String name
    String getName() { return name }
    void setName(String name) { this.name = name }
}

그냥 이렇게 쓰면 돼요:

class Person {
    String name
}

보시다시피 접근 제한 없이 홀로 선언된 '필드'는 Groovy 컴파일러가 private 필드와 getter/setter를 자동 생성해 줘요. 이런 POGO를 자바에서 쓸 때도 getter와 setter가 물론 존재하고, 평소처럼 사용할 수 있어요.

컴파일러가 기본 getter/setter 로직을 만들긴 하지만, 그 getter/setter에 추가로 뭔가 더 하거나 다르게 하고 싶다면 직접 제공해도 돼요. 그러면 컴파일러는 기본 생성 로직 대신 여러분의 로직을 사용해요.

8. 이름 붙은 파라미터와 기본 생성자로 빈 초기화 (Initializing beans with named parameters and the default constructor)

이런 빈이 있다고 할게요:

class Server {
    String name
    Cluster cluster
}

각 setter를 문장마다 하나씩 설정하는 방식 대신:

def server = new Server()
server.name = "Obelix"
server.cluster = aCluster

기본 생성자와 이름 붙은 파라미터(named parameters)를 쓸 수 있어요. 이때는 먼저 생성자가 호출되고, 그다음에 setter들이 맵에 지정된 순서대로 호출돼요.

def server = new Server(name: "Obelix", cluster: aCluster)

9. 같은 빈에 반복 작업할 때는 with()와 tap() (Using with() and tap() for repeated operations on the same bean)

기본 생성자와 이름 붙은 파라미터는 새 인스턴스를 만들 때 유용해요. 그런데 다른 곳에서 받은 인스턴스를 갱신할 때는 어떨까요? 매번 'server'라는 접두어를 반복해야 할까요? 아닙니다. Groovy가 모든 종류의 객체에 더해 주는 with()tap() 메서드 덕분에 반복하지 않아도 돼요.

server.name = application.name
server.status = status
server.sessionCount = 3
server.start()
server.stop()

이런 형태가 아니라:

server.with {
    name = application.name
    status = status
    sessionCount = 3
    start()
    stop()
}

Groovy의 다른 클로저와 마찬가지로, 마지막 문장이 반환 값으로 간주돼요. 위 예시에서는 stop()의 결과가 그 값이에요. 들어온 객체를 그대로 반환하는 빌더로 쓰려면 tap()도 있어요.

def person = new Person().with {
    name = "Ada Lovelace"
    it // Note the explicit mention of it as the return value
}

이런 형태가 아니라:

def person = new Person().tap {
    name = "Ada Lovelace"
}

참고로 tap() 대신 with(true)를, with() 대신 with(false)를 쓸 수도 있어요.

10. equals와 == (Equals and ==)

자바의 ==는 사실 Groovy의 is() 메서드에 해당하고, Groovy의 ==는 똑똑한 equals()예요. 객체의 참조 자체를 비교하고 싶다면 == 대신 a.is(b)를 써야 해요. 반면 평소의 equals() 비교를 하려면 Groovy의 ==를 선호하세요. 왼쪽이든 오른쪽이든 null이어도 NullPointerException을 피해 주거든요.

이렇게 쓰는 대신:

status != null && status.equals(ControlConstants.STATUS_COMPLETED)

이렇게 쓰세요:

status == ControlConstants.STATUS_COMPLETED

11. GStrings (보간, 여러 줄) (GStrings (interpolation, multiline))

자바에서는 문자열과 변수를 이어 붙일 때 큰따옴표를 열고 닫고, 플러스 기호, 그리고 줄바꿈의 \n을 자주 쓰게 돼요. 보간 문자열(interpolated strings)을 GString이라고 부르는데, 이걸 쓰면 훨씬 보기 좋고 입력하기도 덜 고통스러워요.

throw new Exception("Unable to convert resource: " + resource)

이런 형태가 아니라:

throw new Exception("Unable to convert resource: ${resource}")

중괄호 안에는 변수뿐 아니라 어떤 표현식이든 넣을 수 있어요. 단순 변수나 변수.프로퍼티 같은 경우에는 중괄호까지 빼도 돼요.

throw new Exception("Unable to convert resource: $resource")

심지어 ${-> resource }처럼 클로저 표기법을 쓰면 그 표현식을 지연 평가(lazily evaluate)할 수도 있어요. GString이 String으로 강제 변환될 때 클로저를 평가해서 반환 값의 toString() 표현을 얻게 돼요. 예를 들면:

int i = 3

def s1 = "i's value is: ${i}"
def s2 = "i's value is: ${-> i}"

i++

assert s1 == "i's value is: 3" // eagerly evaluated, takes the value on creation
assert s2 == "i's value is: 4" // lazily evaluated, takes the new value into account

자바에서 문자열과 이어 붙인 표현식이 길어질 때가 있어요:

throw new PluginException("Failed to execute command list-applications:" +
    " The group with name " +
    parameterMap.groupname[0] +
    " is not compatible group of type " +
    SERVER_TYPE_NAME)

이럴 때는 \ 연속(continuation) 문자를 쓸 수 있어요. 참고로 이건 여러 줄 문자열은 아니에요.

throw new PluginException("Failed to execute command list-applications: \
The group with name ${parameterMap.groupname[0]} \
is not compatible group of type ${SERVER_TYPE_NAME}")

또는 세 개의 따옴표로 감싼 여러 줄 문자열을 쓰면 돼요.

throw new PluginException("""Failed to execute command list-applications:
    The group with name ${parameterMap.groupname[0]}
    is not compatible group of type ${SERVER_TYPE_NAME)}""")

여러 줄 문자열 왼쪽에 나타나는 들여쓰기는 그 문자열에 .stripIndent()를 호출해서 제거할 수도 있어요.

또한 Groovy에서 작은따옴표와 큰따옴표의 차이를 기억해 주세요. 작은따옴표는 변수 보간 없이 항상 Java String을 만들고, 큰따옴표는 보간 변수가 있으면 Java String 또는 GString을 만들어요. 여러 줄 문자열은 따옴표를 세 개 겹칠 수 있는데, GString은 큰따옴표를, 그냥 String은 작은따옴표를 세 개 쓰면 돼요.

정규식 패턴을 써야 한다면 "슬래시(slashy)" 문자열 표기법을 쓰세요.

assert "foooo/baaaar" ==~ /fo+\/ba+r/

슬래시 표기법의 장점은 백슬래시를 이중 이스케이프할 필요가 없다는 거예요. 그래서 정규식 작업이 조금 더 간단해져요.

마지막으로, 문자열 상수가 필요할 때는 작은따옴표 문자열을, 문자열 보간에 의존할 때는 큰따옴표 문자열을 쓰는 편을 선호해 주세요.

12. 데이터 구조의 네이티브 문법 (Native syntax for data structures)

Groovy는 리스트, 맵, 정규식, 값 범위 같은 데이터 구조를 위한 네이티브 문법 구문을 제공해요. Groovy 프로그램에서 이걸 꼭 활용하세요. 그 네이티브 구문의 예시를 몇 가지 볼게요:

def list = [1, 4, 6, 9]

// by default, keys are Strings, no need to quote them
// you can wrap keys with () like [(variableStateAcronym): stateName] to insert a variable or object as a key.
def map = [CA: 'California', MI: 'Michigan']

// ranges can be inclusive and exclusive
def range = 10..20 // inclusive
assert range.size() == 11
// use brackets if you need to call a method on a range definition
assert (10..<20).size() == 10 // exclusive

def pattern = ~/fo*/

// equivalent to add()
list << 5

// call contains()
assert 4 in list
assert 5 in list
assert 15 in range

// subscript notation
assert list[1] == 4

// add a new key value pair
map << [WA: 'Washington']
// subscript notation
assert map['CA'] == 'California'
// property notation
assert map.WA == 'Washington'

// matches() strings against patterns
assert 'foo' ==~ pattern

13. Groovy 개발 키트 (The Groovy Development Kit)

데이터 구조에 이어서, 컬렉션을 순회해야 할 때 Groovy는 자바의 핵심 데이터 구조를 장식한 다양한 추가 메서드를 제공해요. each{}, find{}, findAll{}, every{}, collect{}, inject{} 같은 것들이 있죠. 이런 메서드들은 언어에 함수형 느낌을 더하고, 복잡한 알고리즘을 더 쉽게 다룰 수 있게 해 줘요. 언어의 동적 특성 덕분에 다양한 타입에 수많은 새 메서드가 장식(decoration)을 통해 적용돼요. String, Files, Streams, Collections 등에 아주 유용한 메서드가 많으니 다음 링크를 참고하세요:

http://groovy-lang.org/gdk.html

14. switch의 힘 (The power of switch)

Groovy의 switch는 보통 프리미티브와 그와 유사한 것만 받는 C 계열 언어들보다 훨씬 강력해요. Groovy의 switch는 거의 모든 종류의 타입을 받아요.

def x = 1.23
def result = ""
switch (x) {
    case "foo": result = "found foo"
    // lets fall through
    case "bar": result += "bar"
    case [4, 5, 6, 'inList']:
        result = "list"
        break
    case 12..30:
        result = "range"
        break
    case Integer:
        result = "integer"
        break
    case Number:
        result = "number"
        break
    case { it > 3 }:
        result = "number > 3"
        break
    default: result = "default"
}
assert result == "number"

더 일반적으로는, isCase() 메서드를 가진 타입이라면 그 값이 case에 해당하는지 결정할 수도 있어요.

15. import 별칭 (Import aliasing)

자바에서 java.util.Listjava.awt.List처럼 서로 다른 패키지에 같은 이름의 클래스 두 개를 써야 할 때, 하나는 import 하고 다른 하나는 완전한 이름(fully-qualified name)을 써야 해요. 또 긴 클래스 이름을 코드에서 여러 번 쓰다 보면 장황해지고 코드가 덜 명확해지기도 해요. 이런 상황을 개선하려고 Groovy는 import 별칭(aliasing)을 지원해요:

import java.util.List as UtilList
import java.awt.List as AwtList
import javax.swing.WindowConstants as WC

UtilList list1 = [WC.EXIT_ON_CLOSE]
assert list1.size() instanceof Integer
def list2 = new AwtList()
assert list2.size() instanceof java.awt.Dimension

메서드를 정적으로 import 할 때도 별칭을 쓸 수 있어요.

import static java.lang.Math.abs as mabs
assert mabs(-4) == 4

16. Groovy 진실 (Groovy Truth)

모든 객체는 boolean 값으로 '강제 변환(coerced)'될 수 있어요. null, void, 0과 같은 값, 또는 비어 있는 것은 false로 평가되고, 그렇지 않으면 true로 평가돼요.

이렇게 쓰는 대신:

if (name != null && name.length > 0) {}

이렇게만 해도 돼요:

if (name) {}

컬렉션도 마찬가지예요. 그래서 while(), if(), 삼항 연산자, 엘비스 연산자(아래 참고) 같은 곳에서 여러 단축 표현을 쓸 수 있어요. 심지어 클래스에 boolean asBoolean() 메서드를 추가해서 Groovy 진실을 직접 정의하는 것도 가능해요!

17. 안전한 그래프 탐색 (Safe graph navigation)

Groovy는 객체 그래프를 안전하게 탐색하도록 . 연산자의 변형을 지원해요. 자바에서 그래프 깊숙한 곳의 노드가 궁금하고 그 사이의 null을 확인해야 한다면, 이런 복잡한 if나 중첩 if를 쓰게 되기 쉽죠.

if (order != null) {
    if (order.getCustomer() != null) {
        if (order.getCustomer().getAddress() != null) {
            System.out.println(order.getCustomer().getAddress());
        }
    }
}

?. 안전 역참조(safe dereference) 연산자를 쓰면 이런 코드를 이렇게 단순화할 수 있어요.

println order?.customer?.address

호출 체인 전체에서 null이 검사되고, 어떤 요소가 null이면 NullPointerException이 발생하지 않으며, 뭔가 null이면 결과 값도 null이 돼요.

18. assert (Assert)

파라미터, 반환 값 등을 검사하려면 assert 문을 쓸 수 있어요. 자바의 assert와 달리 Groovy의 assert는 활성화하지 않아도 동작해요. 그래서 assert는 항상 검사돼요.

def check(String name) {
    // name non-null and non-empty according to Groovy Truth
    assert name
    // safe navigation + Groovy Truth to check
    assert name?.size() > 3
}

또 Groovy의 "Power Assert" 문이 제공하는 멋진 출력도 볼 수 있을 거예요. assert 되는 각 하위 표현식의 여러 값들을 그래프로 보여 주거든요.

19. 기본 값용 엘비스 연산자 (Elvis operator for default values)

엘비스 연산자는 기본 값을 다룰 때 유용한 특별한 삼항 연산자 단축 표현이에요. 우리는 이런 코드를 자주 쓰게 되죠:

def result = name != null ? name : "Unknown"

Groovy 진실 덕분에 null 검사는 그냥 name으로 줄일 수 있어요. 그리고 더 나아가 어차피 name을 반환하니, 이 삼항 표현식에 name을 두 번 반복하는 대신 물음표와 콜론 사이의 내용을 엘비스 연산자로 제거할 수 있어요. 그래서 위 코드는 이렇게 됩니다:

def result = name ?: "Unknown"

20. 어떤 예외든 잡기 (Catch any exception)

try 블록 안에서 던져진 예외의 타입을 별로 신경 쓰지 않는다면, 그중 아무거나 잡을 수 있고 잡는 예외의 타입을 생략하면 돼요.

이렇게 예외를 잡는 대신:

try {
    // ...
} catch (Exception t) {
    // something bad happens
}

이렇게 아무거나('any'나 'all'처럼, 뭐든 떠오르는 이름으로) 잡으면 돼요:

try {
    // ...
} catch (any) {
    // something bad happens
}

Note: 여기서 잡히는 건 모든 Exception이지 Throwable이 아니라는 점을 기억하세요. 정말 "모든 것"을 잡아야 한다면, Throwable을 잡고 싶다고 명시적으로 말해야 해요.

21. 선택적 타입 지정에 대한 조언 (Optional typing advice)

마지막으로 언제, 어떻게 선택적 타입 지정을 쓸지에 대한 몇 마디로 마무리할게요. Groovy는 명시적 강타입을 쓸지, def를 쓸지 스스로 결정하게 해 줘요. 저는 아주 단순한 경험칙을 하나 가지고 있어요. 쓰는 코드가 공개 API처럼 다른 사람이 사용하게 될 코드라면 항상 강타입을 쓰는 쪽이 좋아요. 계약(contract)을 더 강하게 만들고, 전달되는 인자 타입 실수를 피하게 해 주고, 문서화도 더 잘 되고, IDE의 코드 완성에도 도움이 되거든요. 코드가 나만 쓰는 코드라면, 예를 들어 private 메서드라거나 IDE가 타입을 쉽게 추론하는 상황이라면, 어느 쪽을 쓸지 더 자유롭게 결정해도 돼요.

더 알아보기 (Learn more)