API에서 DSL로

  • 궁극적인 목표: 코드의 가독성과 유지 보수성을 가장 좋게 유지하는 것
  • 깔끔한 API의 의미
    • 코드를 읽는 독자들이 어떤 일이 벌어질지 명확하게 이해할 수 있어야 한다. 이름과 개념을 잘 선택하면 이런 목적을 달성할 수 있다. 어떤 언어를 상요하건 일므을 잘 붙이고 적절한 개념을 사용하는 것은 매우 중요하다.
    • 코드가 간결해야 한다. 불필요한 구문이나 번잡한 준비 코드가 가능한 한 적어야 한다. 깔끔한 API는 언어에 내장된 기능과 거의 구분할 수 없다.
  • 코틀린 DSL도 온전히 컴파일 시접에 타입이 정해진다.

영역 특화 언어(DSL, Domain-Specific Language)

  • 범용 프로그래밍 언어: 컴퓨터가 발명된 초기부터 컴퓨터로 풀 수 있는 모든 문제를 충분히 풀 수 있는 기능을 제공하는 언어
  • 영역 특화 언어: 특정 과업 또는 영역에 초점을 맞추고 그 영역에 필요하지 않는 기능을 없앤 언어
  • 가장 익숙한 DSL은 SQL과 정규식이 있다.
  • DSL이 스스로 제공하는 기능을 제한함으로써 오히려 더 효율적으로 자신의 목표를 달성할 수 있다.
  • DSL은 범용 프로그래밍 언어와 달리 더 선언적이다.
    • 범용 프로그래밍 언어는 보통 명령적이다.
    • 명령적 언어: 어떤 연산을 완수하기 위해 필요한 각 단계를 순서대로 정확히 기술한다.
    • 선언적 언어: 원하는 결과를 기술하기만 하고 그 결과를 달성하기 위해 필요한 세부 실행은 언어를 해석하는 엔진에 맡긴다.
    • 선언적 언어는 실행 엔진이 결과를 얻는 과정을 전체적으로 한꺼번에 최적화하기 때문에 더 효율적인 경우가 있다.
  • DSL의 단점: 범용 언어로 만든 호스트 애플리케이션과 함께 조합하기 어렵다.
    • 이런 문제를 해결하면서 DSL의 다른 이점을 살리는 방법으로 내부 DSL이라는 개념이 유명해지고 있다.

내부 DSL

  • 독립적인 문법 구조를 가진 외부 DSL과는 반대로 내부 DSL은 번용 언어로 작성된 프로그램의 일부며, 범용 언어와 동일한 문법을 사용한다.
  • SQL과 Exposed 프레임워크 비교

DSL의 구조

  • DSL과 일반 API 사이에 잘 정의된 일반적인 경계는 없다.
  • 하지만 DSL에만 존재하는 특징이 한 가지 있다.
  • 전형적인 라이브러리는 여러 메소드로 이뤄지며, 클라이언트는 그런 메소드를 한 번에 하나씩 호출함으로써 라이브러리를 사용한다. 이를 명령-질의 API라고 부른다.
  • 반대로 DSL의 메소드 호출은 DSL 문법에 의해 정해지는 더 커다란 구조에 속한다.
  • 코틀린 DSL에서는 보통 람다를 주첩시키거나 메소드 호출을 연쇄시키는 방식으로 구조를 만든다.
  • 코틀린 DSL을 사용한 그레이들의 의존관계 정의

  • 코틀린테스트의 예

구조화된 API 구축: DSL에서 수신 객체 지정 DSL 사용

수신 객체 지정 람다와 확장 함수 타입

  • 수신 객체 지정 람다를 사용하면, 지정한 객체의 이름과 마침표를 명시하지 않아도 그 인자의 멤버를 바로 사용할 수 있다.

  • 수신 객체 지정 람다에서는 일반 함수 타입 대신, 확장 함수 타입을 사용한다.

  • 수신 객체 지정 람다를 사용하는 대표적인 예로 applywith 라이브러리 함수가 있다.

수신 객체 지정 람다를 HTML 빌더 안에서 사용

  • 코틀린 1.1부터는 @DslMarker 애노테이션을 사용해 중첩된 람다에서 외부 람다의 수신 객체를 접근하지 못하게 제한할 수 있다.

invoke 관례를 사용한 더 유연한 블록 중첩

invoke 관례: 함수처럼 호출할 수 있는 객체

  • operator 변경자가 붙은 invoke 메소드 정의가 들어있는 클래스의 객체를 함수처럼 호출할 수 있다.

invoke 관례와 함수형 타입

  • 일반적인 람다 호출 방식이 실제로는 invoke 관례에 적용한 것에 지나지 않는다.
  • 람다를 함수처럼 호출하면 관례에 따라 invoke 메소드 호출로 변환된다.
  • 장점
    • 복잡한 람다를 여러 메소드로 분리하면서도 여전히 분리 전의 람다처럼 외부에서 호출할 수 있는 객체를 만들 수 있다.
    • 함수 타입 파라미터를 받는 함수에게 그 객체를 전달할 수 있다.

DSL의 invoke 관례: 그레이들에서 의존관계 정의

  • 수신 객체 지정 람다: 람다 안에서 메소드를 호출할 객체를 지정하지 않아도 된다.
  • invoke 관례: 객체를 함수처럼 사용할 수 있게 해준다.