어떤 기능을 수행하는 메서드가 있고 메서드 전체 코드에서 일부 영역이 또 다른 메서드에서 반복해서 등장한다면 변경되지 않는 공통된 부분을 템플릿으로 분리하고, 기능에 따라 변경되는 부분은 전략 패턴으로 분리해 낼 수 있음을 배웠다. 이 때 일정한 패턴을 가지는 템플릿 메서드에서 변경되는 코드를 콜백해 주는데 콜백을 위한 객체를 전달해 주는 여러가지 방법이 존재한다. 최근에는 람다식을 많이 사용하는 것 같지만 경우에 따라서는 Functional Interface를 구현한 독립적인 클래스를 선언하고 해당 객체를 전달할 수도 있다. 회사에서 종종 이 둘 중 어떤 것을 사용할지 논쟁이 있었던 것 같다. 그래서 둘 간에 어떤 장단점이 있고 어떤 경우에 람다식을 선택하면 좋은지 개인적으로 정리해 본다.
먼저 람다식을 사용하면 람다식을 정의하고 있는 메서드 뿐 아니라 람다식 외부 객체의 멤버에 직접 접근할 수 있어 편리하다. 반면에 람다식 내부에서 외부 참조가 많아지면 의존관계가 눈에 잘 보이지 않는 단점도 있다. 혹 확장이 일어나는 성격의 코드인데 람다식 안에 코드를 마구 추가하다보면 스파게티 코드가 되기 쉽상인 것 같다. 익명 내부 클래스를 사용해도 동일하다.
public class Client {
private int member;
public void function() {
template(arg -> {
...
log.info("arg = {}, member = {}", arg, member);
});
}
public void template(Consumer<String> callback) {
...
String arg = ...;
callback.accept(arg);
}
}
람다식 대신 Functional 인터페이스를 구현한 분리된 클래스를 사용하면 클래스 생성자로 필요한 객체 참조를 전달해 주어야 하고 전달 받은 객체를 사용할 때까지 멤버 변수에 저장하고 있어야 하는 귀찮음이 존재한다. 반면에 클래스 정의에 의존하는 객체들이 명시적으로 드러나는 장점이 있다.
@FunctionalInterface
public interface ConsumerInterface {
void accept(String arg);
}
public class MyConsumer implements ConsumerInterface{
private int member;
public MyConsumer(int member) {
this.member = member;
}
@Override
public void accept(String arg) {
log.info("arg = {}, member = {}", arg, member);
}
}
public class Client {
private int member;
public void function() {
template(new MyConsumer(member));
}
public void template(ConsumerInterface callback) {
...
String arg = ...;
callback.accept(arg);
}
}
회사에서 이런 논쟁이 몇 번 있었다. ‘간편하게 외부의 변수들을 참조할 수 있는 람다식을 사용하자’라는 의견과 ‘확실히 클래스로 분리해서 의존관계를 분명히 하자’라는 의견이 대립되었다. 이번 학습과 나눔을 통해 람다식에서 참조하는 외부 레퍼런스가 많은 경우에는 별도의 클래스로 확실히 분리하는 게 좋겠다는 개인적인 결론을 내리게 되었다. 생성자를 통해 의존관계에 있는 객체를 전달하고 멤버 변수로 저장하고 있는 거추장스러운 코드 작성을 해야하지만 오히려 외부 의존관계가 분명하게 드러남으로 부가적인 참조 문제들을 명시적으로 인지하게 될 수도 있다는 생각이 든다. 람다식을 잘 활용하려면 람다식이 하는 일이 응집성이 높은 한 가지 일이 되도록 하고 작은 코드 베이스와 적은 외부 참조를 유지하는게 좋다는 생각이 든다.