자바/모던 자바 인 액션

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

Rudtjs 2022. 12. 27. 10:26

도메인 전용 언어

  • DLS은 특정 비즈니스 도메인의 문제를 해결하려고 만든 언어다. 예를 들어 회계전용 소프트웨어 애플리케이션을 개발한다고 가정, 이 상황에서 비즈니스 도메인에는 통장 입출금 내역, 계좌와 같은 개념이 포함된다.
  • DSL은 평문 언어가 아니며, 도메인 전문가가 저수준 비즈니스 로직을 구현하기 위해 사용하는것이 아니다. 다음 뚜 가지 필요성을 생각하며 DSL을 개발해야 한다.
    • 작성자 코드의 의도가 명확히 전달되어야 한다.
    • 가독성이 좋게 코드를 작성해야 한다.

DSL의 장점과 단점

  • DSL의 장점
    • 간결함: API는 비즈니스 로직을 간편하게 캡슐화 하여 코드가 간결해지며 중복을 줄일 수 있다. 
    • 가독성: 도메인 영역의 용어를 사용하므로 비 도메인 전문가도 코드를 쉽게 이해할 수 있다.
    • 유지보수: 잘 설계된 DSL은 도메인과 같은 추상화 수준에서 동작하므로 도메인의 문제와 직접적으로 관련되지 않은 세부 사항을 숨긴다.
    • 높은 수준의 추상화: DSL은 도메인과 같은 추상화 수준에서 동작하므로 도메인의 문제와 직접적으로 관련되지 않은 세부 사항을 숨긴다.
    • 관심사 분리: 지정된 언어로 비즈니스 로직을 표한함으로 애플리케이션의 인프라 구조와 관련된 문제, 독립적으로 비즈니스 관련된 코드에 집중하기가 용이하다.
  • DSL의 단점
    • 설계의 어려움: 간결하게 제한적인 언어에 도메인 지식을 담는 것이 쉬운 작업이 아니다.
    • 개발 비용:  코드에 DSL을 추가하는 작업은 초기 프로젝트에 많은 비용과 시간이 소모되는 작업이다. 또한 DSL 유지보수와 변경은 프로젝트에 부담을 주는 요소이다.
    • 새로 배워야 하는 언어: 요즘 추세는 하나의 프로젝트에도 여러가지 언어를 사용한다. DSL을 프로젝트에 추가하며 배워야 하는 언어가 늘어난다는 부담이 생긴다. (실무에서도 이런 비슷한 이유로 신기술을 도입하지 않았다.)
    • 호스팅 언어 한계: 자바 같은 언어는 장황하고 엄격한 문법을 가졌다. 이런 언어의 특성상 사용자 친화적 DSL을 만들기가 힘들다. Java8의 람다 표현식은 이 문제를 해결할 강력한 도구이다.

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

DSL의 카테고리를 구분하는 가장 흔한 방법은 내부 DSL과 외부 DSL을 나누는 것이다.

 

내부 DSL

  • 기존 자바 언어를 이용해 외부 DSL에 비해 새로운 패턴과 기술을 배워 DSL을 구현하는 노력이 현저히 줄어든다.
  • 순수 자바로 DSL을 구현하면 나머지 코드와 함께 DSL을 컴파일할 수 있다. 따라서 다른 언어의 컴파일러를 이용하거나 외부 DSL을 만드는 도구를 사용할 필요가 없으므로 추가 비용이 발생하지 않는다.
  • 개발 팀이 새로운 언어를 배우거나 또는 익숙하지 않고 복잡한 외부 도구를 배울 필요가 없다.
  • DSL 사용자는 기존의 자바 IDE를 이용해 자동 완성, 리팩터링과 같은 기능을 사용할 수 있다.
  • 한 개의 언어로 하나 또는 여러 도메인을 대응하지 못해 추가 DSL을 개발해야 하는 상황에서 자바를 이용하여 추가 DSL을 쉽게 합칠 수 있다.

외부 DSL

  • 외부 DSL은 쉽게 제어 범위를 벗어날 수 있으며 처음 설계한 목적을 벗어날 수 있다는 문제점이 있다.
  • 자신만의 문법과 구문으로 새 언어를 설계하고 DSL을 실행할 코드를 만들어야 한다.
  • 이런 문제점들이 있지만 사용하는 큰 이유는 외부 DSL이 제공하는 무한한 유연성 때문이다.

 

최신 자바 API의 작은 DSL

Collections.sort(persons, new Comparator<Person>() {
  public int compare() ...
})
 
  • Java8 이전에는 위와 같이 익명 클래스를 활용하여 구현해야 했다.
  • 내부 클래스를 간단한 람다 표현식으로 바꿀수 있다.
Collections.sort(persons, (p1, p2) -> p1.getAge() - p2.getAge())
 
  • 위와 같은 구현은 코드를 간결하게 만들었지만 정적 유틸리티 메서드를 활용하여 좀 더 가독성 있게 개선할 수 있다.
Collections.sort(persons, comparing(p -> p.getAge()))
 
  • 람다를 메서드 참조로 변경하여 코드를 개선할 수 있다.
// 나이 정렬 후 이름 정렬
Collections.sort(persons, comparing(Person::getAge)
                          .thenComparing(Person::getName))

이 예시에서도 DSL이 코드의 가독성, 재사용성, 결합성을 높일수 있는지 보여준다.

 

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

Stream 인터페이스를 이용하여 코드를 쉽게 구현할 수 있다.

List<String> errors = Files.lines(Paths.get(fileName))
	.filter(line -> line.startWith("ERROR"))
	.limit(40)
	.collect(toList());