아래의 코드는 자바 개발하며 흔히 볼 수 있는 entity 객체다.

 
public class TestEntity {
    private String name;
    private String description;
    private Long sequence;
    public Long getSequence() {
        return sequence;
    }
    public void setSequence(Long sequence) {
        this.sequence = sequence;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
}

지금까지 개발해 오면서 클래스를 만들고 멤버변수들을 만들고 난 뒤 getter와 setter는 IDE가 알아서(?) 만들어 주기에 크게 신경쓰지 않고 있었다. 최근 자바스크립를(정확하게는 jQuery를) 한창 하면서, 그리고 또 팀에 새로 오신분의 코딩 스타일을 보면서 setter에 대해 Builder Pattern과 비슷한 다음의 형태로 바꾸는 것에 대해 생각해 보게 되었다.

//in TestEntity
 public TestEntity setSequence(Long sequence) {
        this.sequence = sequence;
        return this;
 }


이런 식으로 setter를 구성할 경우(특히 테스트케이스를 만들 때) 어딘가 지저분한 느낌이었던 코드 일부가 깔끔해 지는 걸 볼 수 있었다.


//before
TestEntity entity = new TestEntity();
entity.setSequence(1);
entity.setName("devjerry");
entity.setDescription("now in tistory");

//after
TestEntity entity = new TestEntity()
                                   .setSequence(1)
                                   .setName("devjerry")
                                   .setDescription("now in tistory");


그럼 왜 이러한 형태가 일반적으로 쓰이지 않는 것일까? (IDE가 자동으로 생성해 주는 패턴이 아닌 것으로 보아..)

구글링을 통해 다음의 페이지를 찾을 수 있었다.

(당연하게도 내가 생각한건 이미 다른사람들이 다 생각을 마친 뒤인 경우가 많다.)


http://stackoverflow.com/questions/31584/design-java-and-returning-self-reference-in-setter-methods


대충 답변을 보니 사용 상 큰 문제는 없는 것 같았다. 그리고 이러한 방법을 Fluent Interface 라고 하는 것을 알 수 있었다.


위키를 참조한 결과, Fluent Interface의 경우 logging, debugging, 그리고 subclass 만들기의 복잡함 정도의 불편함이 있다고 한다. Subclass 만드는 게 좀 걸리긴 하지만.. 나머지 경우는 실제 프로그래밍에 있어서 큰 문제가 되지는 않기에 한정적인 환경에서는 한번 제대로 써보는 것도 나쁘지 않다는 생각이 든다.


Fluent Interface에 대해 간단히 정리하며 이 생각은 우선은 마치겠다. ( 나중에 또 할지 모르지만.. )


이하는 http://en.wikipedia.org/wiki/Fluent_interface 위키 페이지를 대충 정리한 것이다.


Fluent Interface는 (as first coined by Eric Evans and Martin Fowler) 가독성 향상에 목적을 둔 객체 지향 API 구현 방법이다.

A fluent interface is normally implemented by using method cascading (concretely method chaining) to relay the instruction context of a subsequent call (but a fluent interface entails more than just method chaining). Generally, the context is

  • defined through the return value of a called method
  • self-referential, where the new context is equivalent to the last context
  • terminated through the return of a void context.


History

The term "fluent interface" was coined in late 2005, though this overall style of interface dates to the invention of method cascading in Smalltalk in the 1970s, and numerous examples in the 1980s. The most familiar is the iostream library in C++, which uses the << or >> operators for the message passing, sending multiple data to the same object and allowing "manipulators" for other method calls. Other early examples include the Garnet system (from 1988 in Lisp) and the Amulet system (from 1994 in C++) which used this style for object creation and property assignment.


Problems


1. Debugging & error reporting


Fluent Interface형태의 api를 사용할 경우, chained 선언을 한줄에 붙여 쓰게 되면 debugger로 chain 중간에 breakpoint를 잡기 어렵다는 단점이 있다. 해당 라인에 breakpoint를 잡더라도 Step을 몇번 더 타고 들어가야 하기에 디버깅이 불편하다.

또다른 문제로, 특히 chained 선언시 동일 메소드가 여러번 호출 되는 경우, 어느 메소드가 익셉션의 원인인지 명확하게 알기 어렵다. 이러한 문제는 아래와 같이 chained선언을 여러 줄에 걸쳐 줄바꿈을 통해 선언하면 조금은 해결할 수 있다.(However, some debuggers always show the first line in the exception backtrace, although the exception has been thrown on any line.)

java.nio.ByteBuffer.
    allocate(10).
    rewind().
    limit(100);

2. Logging


또다른 이슈는 로깅하는 데 있다.

ByteBuffer buffer = ByteBuffer.allocate(10).rewind().limit(100);

예를 들어, 위 선언에서 rewind이후의 buffer상태를 로깅하고 싶다면 아래와 같이 fluent call을 깨뜨려야만 가능하다.

ByteBuffer buffer = ByteBuffer.allocate(10).rewind();
log.debug("First byte after rewind is " + buffer.get(0));
buffer.limit(100);

3. Subclasses

Strongly typed languages (C++, Java, C#, etc.) 에서는 subclass구현 시, parent class에서 fluent interface로 구현된 method를 모두 override하여 return type을 바꾸어 주어야 하는 불편함이 있다.

class A {
    public A doThis() { ... }
}
class B extends A{
    public B doThis() { super.doThis(); } // return type to B.
    public B doThat() { ... }
}
...
A a = new B().doThat().doThis(); // It works even without overriding A.doThis().
B b = new B().doThis().doThat(); // It would fail without overriding A.doThis().


Override 없이 subclass를 구현하기 위해서는 parent class를 둘로 나누어 fluent interface method를 모아 abstract class로 빼는 방법을 생각할 수 있다. ( the class A with no content (it would only contain constructors if those were needed). )


abstract class AbstractA<T extends AbstractA<T>> {
    @SuppressWarnings("unchecked")
    public T doThis() { ...; return (T)this; }
}   
class A extends AbstractA<A> {}
 
class B extends AbstractA<B> {
    public B doThat() { ...; return this; }
}
 
...
B b = new B().doThis().doThat(); // Works!
A a = new A().doThis();          // Also works.

이를 확장하면 sub-subclasses도 아래와 같이 선언할 수 있다.

abstract class AbstractB<T extends AbstractB<T>> extends AbstractA<T> {
    @SuppressWarnings("unchecked")
    public T doThat() { ...; return (T)this; }
}
class B extends AbstractB<B> {}
 
abstract class AbstractC<T extends AbstractC<T>> extends AbstractB<T> {
    @SuppressWarnings("unchecked")
    public T foo() { ...; return (T)this; }
}
class C extends AbstractC<C> {}
...
C c = new C().doThis().doThat().foo(); // Works!
B b = new B().doThis().doThat();       // Still works.



살짝 다른 이야기로, stackoverflow 페이지의 첫 답변으로 달린 http://tech.puredanger.com/java7#chained (void 형태의 메소드가 항상 클래스를 리턴하도록 만들어 주도록 하자는 Java7 제안) 에 대한 이야기 중 아래의 이야기는 인상적이었다.


 This is "magic" behaviour, and all magic is bad.


나는 비슷한 이유로 선언하지 않아도 자동으로 getter/setter등을 만들어주는 롬복(http://projectlombok.org/) 의 적용은 별로 선호하지 않는 편이다.





'Java' 카테고리의 다른 글

Framework이란?  (0) 2014.08.30
Posted by 제리곰
,