Categories
Daily Development

훌륭한 POJO 도우미인 Project Lombok

며칠전에 발견한 멋진 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가 부럽지 않을 것만 같다.

Categories
Daily Development

귀찮을 땐 reflection이 제 맛

아무리 IDE가 좋아졌다할지라도 라디오 버튼, 체크 박스, 콤보 박스, 텍스트 필드가 적절히 섞여 30여개의 입력 필드가 한 화면에 펼쳐져 있다면.. 유쾌하지 않다.

여기에 한 필드라도 고쳐지면 ‘Save’ 버튼이 활성화되어야 하고 그렇지 않다면 비활성화 되어야 한다는 요구가 들어온다면?

htm_20040809011613070000070100-001.jpg

부들부들.

리플렉션의 이름으로 그대를 처단하겠다.


 1
 2   private void installModifyListeners() {
 3     ActionListener al = new ActionListener() {
 4       @Override
 5       public void actionPerformed(ActionEvent e) {
 6         fireModified("various buttons");
 7       }
 8     };
 9     KeyListener kl = new KeyAdapter() {
10       @Override
11       public void keyReleased(KeyEvent e) {
12         fireModified("various fields");
13       }
14     };
15     
16     Field[] fields = this.getClass().getDeclaredFields();
17     for(Field f : fields) {
18       try {
19         Object o = f.get(this);
20         if( o instanceof JComponent ) {          
21           try {
22             Method m = o.getClass().getMethod("addActionListener", ActionListener.class);
23             if( m!=null ) {
24               m.invoke(o, al);
25               continue;
26             }
27             
28           } catch( NoSuchMethodException e ) {}
29         }
30         
31         if( o instanceof JTextComponent ) {          
32           try {
33             Method m = o.getClass().getMethod("addKeyListener", KeyListener.class);
34             if( m!=null ) 
35               m.invoke(o, kl);
36           } catch( NoSuchMethodException e ) {}
37         }
38       } catch (Exception e) {
39         System.err.println(e);
40       }
41     }
42   }

이 글은 스프링노트에서 작성되고 살짝 고쳐졌습니다.

Categories
Daily Development

Eclipse SWT & JFace API 문서 합본

몇해전 어느날, jmsn-swt를 만진 이후로 나로부터 버림받은 SWT.. 

 

프리랜서 프로젝트 하나를 진행중이라 swing질을 하고 있는데, 이녀석은 어떻게 된일인지 아직도 Generic 지원할 생각이 없다.  그나마 하나 찾은게 JTree 클래스에 있는 Enumeration<TreePath> getExpandedDescendants( TreePath ) 뿐이다. DefaultMutableTreeNode의 set/getUserObject 정도는 제네릭 지원해줘도 되는거 아닌가. 

그렇게  투덜거리다 미투데이에 투덜거리는 글을 올렸고, 친구들이 SWT/JFace로 넘어오라는 댓글들에 힘입어,

The Definitive Guide to SWT and JFace 

를 질렀습니다! (하지만 정작 SWT/JFace 에도 Generic 지원은 없음; )

 

그런데 SWT 랑 JFace API 문서만 보려니 어딨는지 모르겠어서..

cvs -d :pserver:anonymous@dev.eclipse.org/cvsroot/eclipse co org.eclipse.jface 
cvs -d :pserver:anonymous@dev.eclipse.org/cvsroot/eclipse co org.eclipse.swt 
vim build.xml, … <javadoc … excludepackagenames=”org.eclipse.swt.internal.*” … >

한 곳으로 묶어봤습니다.

 

이에 공유합니다.

다운로드는 여기(4.8M)서.

Categories
Daily Development

When you pass an object via AMF3 with Red5, be careful.

Flash나 Flex를 프론트 엔드로 쓰는 네트워크 프로그램을 개발한다면 Red5를 사용해볼 것을 추천한다. Red5는 FMS(Flash Media Server)의 자바 구현체이며 오픈소스이다. 많은 사람들이 red5를 오디오나 비디오를 스트리밍할 용도로 많이 사용하고 있는데, 나는 그런 것에는 별로 관심이 없고 그저 AMF를 쓸 수 있기 때문에 사용한다.

AMF는 Active Message Format의 약자이며 위키백과 페이지에서 볼 수 있듯이 대부분의 언어에서 사용할 수 있도록 오픈소스로 개발된 라이브러리들이 많다. AMF는 그저 serialize 포맷의 하나다. Java의 ObjectOutputStream/ObjectInputStream, ruby의 marshal load/dump 처럼 ActionScript 에서 int, string, array, dictionary, object 들을 serialize 하는데 사용하는 것이다. 그나저나 Flash 9 부터 포함되기 시작한 성능 좋은 AVM2 위에서는 AMF3가 들어갔는데 이전 버전의 AMF(version 0)보다 빨라졌다. (AMF1 이나 AMF2 같은 것은 존재한 적이 없다.)

아래 James Ward의 벤치마크 결과는 text based format과 binary based format 과의 치사한 비교이긴 하지만, 괜시리 내 마음을 끌기에는 충분했다.

아이고 치사하다. 아무튼 Red5에서도 AMF3를 지원하므로 POJO를 flash side로 보낼 수 있다.

  1. package a.b.c;
  2. import java.io.Serializable;
  3. import java.util.List;
  4. import java.util.Map;
  5. public class Melong implements java.io.Serializable {
  6. private String a;
  7. private String b;
  8. private List<Integer> c;
  9. private Map<Integer, String> d;
  10. public String getA() { return a; }
  11. public String getB() { return b; }
  12. public List<Integer> getC() { return c; }
  13. public Map<Integer> getD() { return d; }
  14. }

물론 getter/setter 메서드들은 만들지 않아도 된다. 그저 public variable들만 선언해도 무난히 전송된다. Flex의 NetConnection으로 객체를 집어 던질때처럼 registerClassAlias 같은 것으로 등록해줄 필요도 없다. 대신 이 객체를 받아먹을 flex 사이드의 클래스 선언 위에 살포시..

  1. package qqq.www.eee {
  2. [RemoteClass(alias=”a.b.c.Melong”)]
  3. public class Melong {
  4. }
  5. }

RemoteClass alias로 red5 사이드의 fqdn을 넣어주기만 하면 된다.

그런데 오늘 포스팅의 목적은 이것이 아니다.  메서드 하나 잘못 추가했다가 패닉할 수 있다. 자, 여러분이 서버 사이드에서 사용할 목적으로 d Map의 모든 values를 뽑아오는 유틸리티 메서드를 만들었다고 치자. 구현은 다음과 같을 것이다.

  1. public class Melong implements java.io.Serializable {
  2. [생략]
  3. private Map<Integer, String> d;
  4. public List<String> getAllValuesFromD() {
  5. return new ArrayList<String>(this.d.values());
  6. }
  7. }

이렇게 getAllValuesFromD 란 메서드를 구현한 다음, 이 객체를 flash에 전송해본다. 어랏. 이상한 예외가 발생하네 -_-?

  1. org.red5.server.net.rtmp.codec.RTMPProtocolEncoder – Error encoding object:
    java.lang.NullPointerException
    at org.red5.io.object.Serializer.serializeField(Serializer.java:317)
    at org.red5.io.amf3.Output.writeObject(Output.java:483)
    at org.red5.io.object.Serializer.writeObjectType(Serializer.java:274)

거참, 불친절하기도 하다. NullPointerException이 발생했다는데 힌트가 하나도 없다 stacktrace 최상단을 보면 serializeField 란 메서드에서 났다고 한다. 허허.. 요즘 세상에 기본 컬렉션이나 primitive type들도 serialize가 안되는건가. ㄱ- 게다가 이 오류가 발생하면 flex side에 어떠한 신호도 보내주지 않아 로그를 주시하고 있지 않았다면 더욱 황당한 일일 것이다.

이것때문에 1시간 넘게 삽질에 삽질을 거듭했었다. 원인은.. red5의 미숙한 배려였다. 필드를 전송할 때 reflection api로 get으로 시작하는 모든 메서드를 불러와서 get을 떼고 나머지 문자열의 맨 앞 글자를 대문자로 만든 뒤 getField를 해서 null check 없이 다음 프로세스를 처리하는 것이다. 만약 getD() 라는 메서드를 서버 사이드에 구현해뒀다면 getD의 실행결과가 amf3로 전달될 것이다. 하지만 getAllValuesFromD 메서드를 구현한 뒤에는, 황당하게도 어떠한 메시지도 없이 위와 같은 예외를 뱉어버린다. encoding object에 null pointer exception 이라니.. 개발자로 하여금 serializable을 다시 확인하게 하기만 한다.

절대로 절대로 유틸리티성 메서드를 AMF로 전송할 클래스에 선언하지 마라.

이 오류를 뱉는 것은 red5 0.8RC1 포함한 이전 버전에서만 생긴다. 물론 svn trunk를 이용하면 이 오류가 발생하지 않는다. (지금은 RC3 까지 나온 상태이다) 하지만 깔끔하게 처리된 것은 결코 아니다. 오늘 svn update 한 red5 에서 실행해본 결과 값을 잘 전달하고, red5는 어떠한 오류도 뱉지 않지만 flex side에서 오류가 발생한다. flex의 Melong 클래스에 allValuesFromD 란 필드가 없다고. 이것은 당황스럽게 보일 수 있겠지만 대단히 편리한 수단이 될 수도 있다. 만약 Melong 플렉스 클래스에 public var allValuesFromD:Array 를 선언해뒀다면 서버 사이드의 getAllValuesFromD 메서드 실행 결과가 allValuesFromD:Array 필드에 바로바로 박혀주기 때문.

이 사건을 긍정적으로 살펴보면, 모델 클래스에는 절대로 로직 넣지 말라는 조언 정도로 볼 수 있겠다. 메서드가 getter 형태를 띌 경우 remote field에 바로 대입해주는 것은 얼핏보면 편리해보이지만 규칙이 없고 잘 정리된 문서가 없기 때문에 개발자를 혼란에 빠트릴 위험이 많기 때문이다.

이 글은 스프링노트에서 작성되었습니다.