Rath World Notes by Jang-Ho Hwang

3Oct/09Off

훌륭한 POJO 도우미인 Project Lombok

Posted by Jang-Ho Hwang

며칠전에 발견한 멋진 POJO 도우미인 Project Lombok을 소개한다.

사실, 내가 따로 소개할 필요를 느끼지 못하겠다. 스크린캐스트 영상을 보면 누구라도 감 잡고, 바로 이용할 수 있기 때문이다.

그래도 링크 클릭하기 귀찮은 사람들을 위해 정리를 하자면

Lombok은 Java 1.5 부터 포함된 apt(annotation processing tool)를 이용해 만든 POJO 도우미다. 그저 lombok.jar (343KB)를 내려받아 더블클릭하여 실행하면 바로 이클립스 플러그인으로 설치된다. (Manifest에 Main-Class 속성이 지정되어 인스톨러 GUI가 바로 실행되고, OSX일 경우 이클립스의 위치까지 바로 제안해준다, 343KB 속에는 여러가지 PNG 리소스와 빠른 설치를 위한 DLL까지 포함되어있다)

진정 설치하는데 10초도 안걸린다.

Lombok을 쉽게 이해하려면 Setter, Getter 생성기라고 보면 된다. 그 외에도 편리한 기능들을 제공하지만 말이다.

public class Person {
private final int id;
private String name;
private String mobile;
private Date birthday;
}

위와 같은 POJO가 있다고 가정해보자. 이클립스에서 Getter/ Setter/ Constructor/ equals()/ hashcode()/ toString() 를 만들려면  Source 메뉴에서 각 메뉴를 일일히 클릭해서 엔터를 치는 수고를 해야 한다. 물론 이 작업을 다 하는데는 10초정도면 될 것이다. (10초는 주의력이 분산되기에 충분한 시간이다) 그런데 Person에 필드가 하나 추가되면? Quick-Fix를 눌러 setter/getter를 생성해줘야 한다. 그런데 final int로 선언되어있던 id가 String으로 바뀌면? Refactor 메뉴를 또 한번 눌러줘야하는 수고를 해야 한다. toString(), hashCode(), equals()도 다시 건드려줘야 한다.

게다가 setter/getter 말고 조금 특수한 getter들이 추가되었다면 어떨까. 예를 들어 getBirthdayAsString()를 추가했다고 치자. 지금은 메서드 7개와 생성자 1개밖에 안되지만 필드가 10개쯤 된다면 수많은 setter/getter들 사이에서 내가 직접 추가한 메서드가 무엇인지 파악하기 힘들다. 소스코드가 얼마나 긴지..

그래서 태어난 Lombok.

import lombok.Data;

public @Data class Person {
private final int id;
private String name;
private String mobile;
private Date birthday;
}

축하한다. 그대는 final 필드를 파라미터로 받는 생성자와 Setter/Getter를 모두 다 가졌다. 뿐만아니라 디버그를 위한 toString 메서드도 가졌고 Joshua Bloch이 추천하는 equals(), hashCode()도 가졌다. 새로운 필드가 추가되거나 필드 형식이 바뀌더라도 아무런 추가 작업이 필요없다.

그런데 import lombok을 해서 lombok.jar에 의존성이 생기지 않았냐고? No. lombok은 컴파일 타임 유틸리티이다. lombok.jar를 classpath에 넣고 Person.java 를 컴파일 하는 순간 lombok의 역할은 끝이다.

테스트 삼아 위의 Person.java를 컴파일 해보겠다.

$ javac -cp lombok.jar Person.java

컴파일이 됐다. 그럼 javap로 Person.class에 어떤 메서드들이 들어있는지 확인해보자.

$ javap Person
Compiled from "Person.java"
public class Person extends java.lang.Object{
public int getId();
public java.lang.String getName();
public void setName(java.lang.String);
public java.lang.String getMobile();
public void setMobile(java.lang.String);
public java.util.Date getBirthday();
public void setBirthday(java.util.Date);
public java.lang.String toString();
public int hashCode();
public boolean equals(java.lang.Object);
public Person(int);
}

모두 가졌다. 컴파일 타임에 Person 클래스가 확장됐다. 물론 Lombok은 이클립스 플러그인도 제공하고 있기 때문에 (lombok.jar를 더블클릭하여 설치를 누른순간 이미 그대의 이클립스에 설치됐다) @Data를 통해 Person.java를 만들고나면 Outline 에서도 생성된 모든 메서드를 볼 수 있다.

그러나 @Data 만으로는 불충분하다. 그래서 클래스 레벨의 @Data 말고도 개별적으로 @Setter, @Getter, @ToString, @EqualsAndHashCode를 지정할 수도 있게 해준다. @Data 어노테이션은 transient를 제외한 모든 필드에 대해 @Setter, @Getter를 붙이고 (final 필드면 @Getter만) @ToString, @EqualsAndHashCode 를 클래스에 붙여주는 *통합본*일 뿐이다.

그 외에도 아래와 같은 재미난 어노테이션들을 제공한다.

  • @NonNull -  멤버 변수에 붙여줄 경우, 그 필드를 꼭 받도록 생성자가 수정된다. 그리고 setXXX로 null을 넘겨줄 경우 if (xxx==null) 체크 코드가 들어가서 throw new NullPointerException("XXX") 를 던져준다. 버그 방지용으로 안성맞춤이다.
  • @Cleanup - 내가 아주 즐겨 쓰는 유틸리티이다. 로컬 변수 앞에 붙여 쓸 수 있으며, 해당 변수 scope의 끝까지 자동으로 try { } 로 묶어주고 finally 블럭에서 field.close() 를 넣어준다. 이것은 컴파일 타임에 close()를 호출하도록 하는 것이므로 별도의 interface를 필요로 하지 않는다. 만약 destroy() 를 불러야하는 경우라면 @Cleanup("destroy") 라고 써주면 된다. 단, try 블럭에서 예외가 발생했고 cleanup 메서드에서 예외가 발생할 경우 cleanup 메서드에서 발생한 예외는 완전히 무시된다. 그러므로 @Cleanup에 완전히 의존하는 것은 위험하다.
  • @Synchronized - static 용, instance 용 lock 오브젝트를 자동으로 생성해주고 어노테이션을 메서드에 적용할 경우, 메서드 바디를 synchronized 키워드로 감싸준다. 내겐 별로 쓸모가 없지만, 주목할만한 점은 new Object() 가 serialize 되지 않는 것을 고려하여 instance용 lock 오브젝트를 new Object[0]로 선언해준다는 것이 특이사항이다. 별 생각없이 new Object()로 lock를 선언했다가 serialize 안되서 짜증나는 경우를 겪었던 사람이라면 이 @Synchronized 키워드의 센스를 인정할 수 있을 것이다. 만약 별도의 lock 객체를 생성해서 몇몇 메서드에게만 적용하고 싶다면 (read-lock 같은 경우를 위해) @Synchronized("lock-object-name") 형식으로 쓰면 된다. 대신 lock-object-name은 프로그래머가 직접 필드를 선언해야만 하는데, 그 이유는 나중에 버그 찾기 힘들어서라고!
  • @SneakyThrows - 이것 또한 내가 좋아하는 것이다. 절대 일어나지 않을 checked exception을 씹어 먹어주는 편리한 녀석이다. 이클립스에서 checked exception을 Quick-Fix로 고치면 자동으로 e.printStackTrace()를 붙여주지만, 정말로 정말로 일어나지 않을 exception이라면 // TODO 와 스택트레이스 지우는 것조차도 귀찮은 일이니까. String.getBytes("UTF-8")이 대표적인 예인데, JVM 스펙에 따르면 UTF-8 인/디코더의 존재여부는 MUST BE 임에도 불구하고 우리는 IOException 을 잡아줘야 하기 때문. 그럴때 @SneakyThrows(UnsupportedEncodingException.class)를 해주면 코드가 예뻐진다. 단, 예외를 씹어먹는 부분이 Lombok.sneakyThrow(e) 라서 런타임 시 lombok.jar에 의존성을 가지게 되는 문제가 있다.

큰 기쁨을 주는 툴이 아닐 수 없다. apt를 정말 멋지게 활용한 프로젝트가 아닐 수 없다. 게다가 javac와 묶여서 apt 커멘드를 실행할 일조차 없게 해주다니, 정말 멋지지 않은가?

이제 LISP의 defmacro가 부럽지 않을 것만 같다.