모던 자바 인 액션 - 10장 람다를 이용한 도메인 전용 언어

Modern Java In Action 정리 - 람다를 이용한 도메인 전용 언어

  • 도메인 전용 언어(Domain-Specific Languages, DSL)란 무엇이며 어떤 형식으로 구성되는가 ?
  • DSL을 API에 추가할 때의 장단점
  • JVM에서 활용할 수 있는 자바 기반 DSL을 깔끔하게 만드는 대안
  • 최신 자바 인터페이스와 클래스에 적용된 DSL에서 배움
  • 효과적인 자바 기반 DSL을 구현하는 패턴과 기법
  • 이들 패턴을 자바 라이브러리와 도구에서 얼마나 흔히 사용하는가 ?

모던 자바 인 액션 책을 보고 정리한 글입니다.

도메인 전용 언어

  • DSL은 특정 비즈니스 도메인의 문제를 해결하려고 만든 언어다. 예를 들어 회계전용 소프트웨어 애플리케이션을 개발한다고 가정, 이 상황에서 비즈니스 도메인에는 통장 입출금 내역, 계좌 와 같은 개념이 포함
    이런 문제를 표현할 수 있는 DSL을 만들수 있다. (DDD의 유비쿼터스 언어와 유사한것 같다.)
  • DSL 이란 특정 비즈니스 도메인을 인터페이스로 만든 API라고 생각할 수 있다.
  • DSL은 평문 언어가 아니며, 도메인 전문가가 저수준 비즈니스 로직을 구현하기 위해 사용하는것이 아니다. 다음 뚜 가지 필요성을 생각하며 DSL을 개발해야 한다.
  • 의사소통의 왕
    • 코드의 의도가 분명히 전달되어야 하며 개발자가 아닌 사람들도 이해할수 있어야 한다.
  • 한 번 코드를 구현하지만 여러 번 읽는다.
    • 가독성은 유지보수의 핵심이다. 항상 다른 개발자 또는 팀원들이 이해할 수 있도록 코드를 구현해야한다.

DSL의 장점과 단점

  • DSL은 만병 통치약이 아니다. DSL은 코드의 비즈니스 의도를 분명히 드러내고 가독성을 높인다는점에서 약이 되지만 그에 따른 유지보수의 책임이 생긴다.
  • DSL의 장점
    • 간결함
      • API는 비즈니스 로직을 간편하게 캡슐화 하여 코드가 간결해지며 중복을 줄일수 있다.
    • 가독성
      • 도메인 영역의 용어를 사용하므로 비 도메인 전문가도 코드를 쉽게 이해할 수 있다.
    • 유지보수
      • 잘 설계된 DSL로 구현한 코드는 쉽게 유지보수하고 바꿀수 있다. 유지보수는 비즈늬스 관련 코드, 즉 가장 빈번히 바뀌는 애플리케이션 부분에 특히 중요하다.
    • 높은 수준의 추상화
      • DSL은 도메인과 같은 추상화 수준에서 동작하므로 도메인의 문제와 직접적으로 관련되지 않은 세부 사항을 숨긴다.
    • 집중
      • 비즈니스 도메인의 규칙을 표현할 목적으로 설계된 언어이므로 프로그래머가 특정 코드에 집중할 수 있다.
    • 관심사분리
      • 지정된 언어로 비즈니스 로직을 표한함으로 애플리케이션의 인프라 구조와 관련된 문제, 독립적으로 비즈니스 관련된 코드에 집중하기가 용이하다.
  • DSL의 단점
    • DSL 설계의 어려움
      • 간결하게 제한적인 언어에 도메인 지식을 담는 것이 쉬운 작업은 아니다.
    • 개발 비용
      • 코드에 DSL을 추가하는 작업은 초기 프로젝트에 많은 비용과 시간이 소모되는 작업이다. 또한 DSL 유지보수와 변경은 프로젝트에 부담을 주는 요소이다.
    • 추가 우회 계층
      • DSL은 추가적은 계층으로 도메인 모델을 감싸며 이 때 계층을 최대한 작게 만들어 성능 문제를 회피한다.
    • 새로 배워야 하는 언어
      • 요즘 추세는 하나의 프로젝트에도 여러가지 언어를 사용한다. DSL을 프로젝트에 추가하며 배워야 하는 언어가 늘어난다는 부담이 생긴다.
    • 호스팅 언어 한계
      • 자바 같은 언어는 장황하고 엄격한 문법을 가졌다. 이런 언어의 특성상 사용자 친화적 DSL을 만들기가 힘들다. Java8의 람다 표현식은 이 문제를 해결할 강력한 도구이다.

JVM에서 이용할 수 있는 다른 DSL 해결책

  • 내부 DSL (순수 자바로 구현한 DSL)
    • 기존 자바 언어를 이용해 외부 DSL에 비해 새로운 패턴과 기술을 배워 DSL을 구현하는 노력이 현저히 줄어든다.
    • 순수 자바로 DSL을 구현하면 나머지 코드와 함께 DSL을 컴파일 할 수 있다. 따라서 다른 언어의 컴파일러를 이용하거나 외부 DSL을 만드는 도구를 사용할 필요가 없으므로 추가 비용이 발생하지 않는다.
    • 개발 팀이 새로운 언어를 배우거나 또는 익숙하지 않고 복잡한 외부 도구를 배울 필요가 없다.
    • DSL 사용자는 기존의 자바 IDE를 이용해 자동 완성, 리팩터링과 같은 기능을 사용할 수 있다.
    • 한개의 언어로 하나 또는 여러 도메인을 대응하지 못해 추가 DSL을 개발해야 하는 상황에서 자바를 이용하여 추가 DSL을 쉽게 합칠수 있다.
  • 다중 DSL
    • 새로운 프로그래밍 언어를 배우거나 또는 팀의 누군가가 리딩 할 수 있어야 한다.
    • 두 개 이상의 언어가 혼재하므로 여러 컴파일러로 소스를 빌드 하도록 빌드 과정의 개선이 필요하다.
    • JVM에서 실행되는 거의 모든 언어가 자바와 호환된다고 하지만 모든 것이 호환되지 않을 때가 있다.
  • 외부 DSL
    • 자신만의 문법과 구문으로 새로운 언어를 설계해야 한다.
    • 외부 DSL을 개발하는 가장 큰 장점은 외부 DSL이 제공하는 무한한 유연성 때문이다.

최신 자바 API의 작은 DSL

  • 자바의 새로운 기능의 장점을 적용한 첫 API는 네이티브 자바 API 자신이다.
  • 사람들을 가지고 있는 리스트에서 나이순으로 객체를 정렬하는 예제를 보자.
1
2
3
Collections.sort(persons, new Comparator<Person>() {
  public int compare() ...
})
  • Java8 이전에는 위와 같이 익명 클래스를 활용하여 구현해야 했다.
  • 내부 클래스를 간단한 람다 표현식으로 바꿀수 있다.
1
Collections.sort(persons, (p1, p2) -> p1.getAge() - p2.getAge())
  • 위와 같은 구현은 코드를 간결하게 만들었지만 정적 유틸리티 메서드를 활용하여 좀 더 가똑성 있게 개선할 수 있다.
1
2
// Comparator.comparing
Collections.sort(persons, comparing(p -> p.getAge()))
  • 람다를 메서드 참조로 변경하여 코드를 개선할 수 있다.
1
2
3
4
5
6
7
8
9
10
// Comparator.comparing
Collections.sort(persons, comparing(Person::getAge))

Collections.sort(persons, comparing(Person::getAge).reverse()) // 역순 정렬

Collections.sort(persons, comparing(Person::getAge)
                          .thenComparing(Person::getName)) // 나이 정렬 후 이름 정렬

persons.sort(comparing(Person::getAge)
                          .thenComparing(Person::getName)) // List 에 추가된 sort 메서드 이용하여 마지막 리팩토링
  • 이 작은 예시는 컬렉션 정렬 도메인의 최소 DSL이다. 작은 영역에 국한된 예제지만 이미 람다와 메서드 참조를 이용한 DSL이 코드의 가독성, 재사용성, 결합성을 높일수 있는지 보여준다.

스트림 API는 컬렉션을 조작하는 DSL

  • Stream 인터페이슨느 네이티브 자바 API에 작은 내부 DSL을 적용한 좋은 예다.

    데이터를 수집하는 DSL인 Collectors

  • Stream 인터페이스를 데이터 리스트를 조작하는 DSL 로 간주할 수 있음을 확인했다.

자바로 DSL을 만드는 패턴과 기법

  • DSL은 특정 도메인 모델에 적용할 친하적이고 가독성 높은 API를 제공한다.

메서드 체인을 이용

  • 메서드 체인을 이용하여 DSL을 구현해보자.
  • 플루언트 API로 도메인 객체를 만드는 몇개의 빌더를 구현해야 한다.
  • 최상위 빌더를 만들고 도메인 객체를 감싼 다음 빌더를 구가할 수 있는 구조가 되어야 한다.
  • 빌더를 구현해야하는것이 메서드 체인 방법의 단점이다.
  • 상위 수준의 빌더를 하위 수준의 빌더와 연결할 많은 접착 코드가 필요하다.

중첩된 함수 이용

  • 중첩된 함수 DSL 패턴은 이름에서도 알 수 있듯이 다른 함수 안에 함수를 이용해 도메인 모델을 만든다.
1
2
3
4
5
Order order = order("BigBank", 
                      buy(80, stock("IBM", on("NYSE")), at(125.00)),
                    sell(50, 
                      stock(...))
                    );
  • 메서드 체인에 비해 함수의 중첩 방식이 도메인 객체 계층 구조에 그대로 반영된다는 것이 장점이다.
  • 결과 DSL에 더 많은 괄호를 사용해야 한다는 것이 단점이다.

람다 표현식을 이용한 함수 시퀀싱

  • 다음 DSL 패턴은 람다 표현식으로 정의한 함수 시퀀스를 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
Order order = order(0 -> {
  o.forCustomer(...);
  o.buy(t -> {
    t.quantity(80);
    t.price(132);
    ...
  });
  o.sell(t -> {
    t.quantity(2);
    t.price(12);
    ...
  })
})
  • 이런 DSL 을 만드려면 람다 표현식을 받아 실행해 도메인 모델을 만들어 내는 여러 빌더를 구현해야 한다.
  • DSL 구현에서 했던 방식과 마찬가지로 이들 빌더는 메서드 체인 패턴을 이용해 만들려는 객체의 중간 상태를 유지한다.

조합하기

  • 지금까지 살펴본 세가지 DSL 패턴 각자가 장단점을 갖고 있다.
  • 세가지 DSL 패턴을 혼용하여 가독성 있는 DSL을 만드는 방법 을 사용할 수 있다.