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

SunYerim·2024년 2월 26일
0

언어

목록 보기
10/11


언어의 주요 목표는 메시지를 명확하고, 안정적인 방식으로 전달하는 것이다.

DSL(도메인 전용 언어)는 특정 도메인을 대상으로 만들어진 특수 프로그래밍 언어이다.

내부적 DSL에서는 적절하게 클래스와 메서드를 노출하는 과정이 필요하다. 외부 DSL은 DSL 문법 뿐 아니라 DSL을 평가하는 파서도 구현해야 한다.

10.1 도메인 전용 언어

DSL은 특정 비즈니스 도메인의 문제를 해결하려고 만든 언어이다.

DSL이란 특정 비즈니스 도메인을 인터페이스로 만든 API라고 생각할 수 있다.

DSL은 범용 프로그래밍 언어가 아니다. 동작과 용어는 특정 도메인에 국한되며, DSL을 사용하면 사용자가 특정 도메인의 복잡성을 더 잘 다룰 수 있다.

저수준 구현 세부 사항 메서드는 클래스의 비공개로 만들어서 저수준 구현 세부 내용은 숨길 수 있다.

아래의 두 가지 필요성을 생각하면서 DSL을 개발해야 한다.

  • 의사 소통의 왕
    • 코드의 의도가 명확히 전달되어야 하며 프로그래머가 아닌 사람도 이해할 수 있어야 한다.
  • 한 번 코드를 구현하지만 여러 번 읽는다.
    • 가독성은 유지보수의 핵심이다.

10.1.1 DSL의 장점과 단점

장점

  • 간결함
    • API는 비즈니스 로직을 간편하게 캡슐화하므로 반복을 피할 수 있고 코드를 간결하게 만들 수 있다.
  • 가독성
    • 도메인 영역의 용어를 사용하므로 비 도메인 전문가도 코드를 쉽게 이해할 수 있다.
  • 유지보수
    • 잘 설계된 DSL로 구현한 코드는 쉽게 유지보수하고 바꿀 수 있다.
  • 높은 수준의 추상화
    • DSL은 도메인과 같은 추상화 수준에서 동작하므로 도메인의 문제와 직접적으로 관련되지 않은 세부 사항을 숨긴다.
  • 집중
    • 비즈니스 도메인의 규칙을 표현할 목적으로 설계된 언어이다. 결론적으로 생산성이 좋아진다.
  • 관심사 분리
    • 관심사의 분리로 유지보수가 쉬운 코드를 구현할 수 있게 된다.

단점

  • DSL 설계의 어려움
    • 간결하게 제한적인 언어에 도메인 지식을 담는 것이 쉬운 작업은 아니다.
  • 개발 비용
    • 코드에 DSL을 추가하는 작업은 초기 프로젝트에 많은 비용과 시간이 소모되는 작업이다. 또한 DSL 유지보수와 변경은 프로젝트에 부담을 주는 요소이다.
  • 추가 우회 계층
    • DSL은 추가적인 계층으로 도메인 모델을 감싸며 이 때 계층을 최대한 작게 만들어 성능 문제를 회피한다.
  • 새로 배워야 하는 언어
    • DSL을 프로젝트에 추가하면서 팀이 배워야 하는 언어가 한 개 더 늘어난다는 부담이 있다.
  • 호스팅 언어 한계
    • 범용 프로그래밍 언어는 장황하고 엄격한 문법을 가졌다. 장황한 프로그래밍 언어를 기반으로 만든 DSL은 문법의 제약을 받고 읽기가 어려워지는데, 람다 표현식은 이 문제를 해결할 도구이다.

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

DSL의 카테고리를 구분하는 갖아 흔한 방법은 내부 DSL, 외부 DSL 을 나누는 것이다. 내부 DSL은 순수 자바 코드 같은 기존 호스팅 언어를 기반으로 구현하는 반면, 외부 DSL은 호스팅 언어와는 독립적으로 자체의 문법을 가진다.

외부와 내부 DSL 중간 카테고리에 해당하는 다중 DSL까지 알아본다.

내부 DSL

내부 DSL이란 자바로 구현한 DSL을 의미한다.

익명 내부 클래스를 람다 표현식으로 바꿀 수 있다.

numbers.forEach(s -> System.out.println(s));

// 메서드 참조로 더 간단하게 만들 수 있다.
numbers.forEach(System.out::println);

사용자가 기술적인 부분을 염두에 두고 있다면 자바를 이용해 DSL을 만들 수 있다.

순수 자바로 DSL을 구현함으로 아래와 같은 장점을 얻을 수 있다.

  • 기존 자바 언어를 이용하면 외부 DSL에 비해 새로운 패턴과 기술을 배워 DSL을 구현하는 노력이 현저하게 줄어든다.
  • 순수 자바로 DSL을 구현하면 나머지 코드와 함께 DSL을 컴파일할 수 있다.

같은 자바 바이트코드를 사용하는 JVM 기반 프로그래밍 언어를 이용함으로 DSL 합침 문제를 해결하는 방법도 있다. 이런 언어를 다중 DSL이라고 부른다.

다중 DSL

DSL은 기반 프로그래밍 언어의 영향을 받으므로 간결한 DSL을 만드는 데 새로운 언어의 특성들이 아주 중요하다.

스칼라 언어를 활용한 예제

→ 자바로는 비슷한 결과를 얻긴 어렵다. 이를 통해 누가 더 DSL 친화적인지를 명확하게 보여준다.

외부 DSL

프로젝트에 DSL을 추가하는 세 번째 옵션은 외부 DSL을 구현하는 것이다. 그러려면 자신만의 문법과 구문으로 새 언어를 설계해야 한다.

외부 DSL을 개발하는 가장 큰 장점은 외부 DSL이 제공하는 무한한 유연성이다.

우리에게 필요한 특성을 완벽하게 제공하는 언어를 설계할 수 있다는 것이 장점이며, 제대로 설계하면 비즈니스 문제를 묘사하고 해결하는 가독성 좋은 언어를 얻을 수 있다,

자바로 개발된 인프라구조 코드와 외부 DSL로 구현한 비즈니스 코드를 명확하게 분리한다는 것도 장점이다. → but, 분리로 인해 DSL과 호스트 언어 사이에 인공 계층이 생기므로 이는 양날의 검과 같다.


10.2 최신 자바 API의 작은 DSL

자바의 새로운 기능의 장점을 적용한 첫 API는 네이티브 자바 API 자신이다.

인터페이스가 정적 메서드와 디폴트 메서드를 가질 수 있다. (추후에 배움.)

람다가 없으면 내부 클래스로 Comparator인터페이스를 구현하여 객체를 정렬할 수 있다.

// 나이를 기준으로 객체를 정렬
Collections.sort(people, (p1, p2) -> p1.getAge() - p2.getAge());

// Comparator. comparing 메서드를 임포트
Collections.sort(persons, comparing(p -> p.getAge()));

// 람다를 메서드 참조로 대신해 코드를 개선
Collections.sort(persons, comparing(Person::getAge));

reverse 메서드를 사용해 역순 정렬도 가능하다.

Collections.sort(persons, comparing(Person::getAge).reverse());

// 이름으로 비교를 수행하는 Comparator 구현해 같은 나이 사람들을 알파벳 순으로 정렬
Collections.sort(persons, comparing(Person::getAge)
													.thenComparing(Person::getName));

// List 인터페이스에 추가된 새 sort 메서드를 이용해 코드를 정리
persons.sort(comparing(Person::getAge)
						.thenComparing(Person::getName));

작은 API는 컬렉션 정렬 도메인의 최소 DSL이다.

람다와 메서드 참조를 이용한 DSL이 코드의 가독성, 재사용성, 결합성을 높일 수 있는지 보여준다.

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

Stream 인터페이스는 네이티브 자바 API에 작은 내부 DSL을 적용한 좋은 예다. Stream은 컬렉션 항목을 필터, 정렬, 변환, 그룹화, 조작하는 작지만 강력한 DSL이다.

Stream 인터페이스를 이용해 함수형으로 코드를 구현하면 쉽고 간결하게 코드를 구현할 수 있다.

//함수형으로 로그 파일의 에러 행을 읽음
List<String> errors = Files.lines(Paths.get(fileName))
													 .filter(line -> line.startsWith("ERROR"))
													 .limit(40)
													 .collect(toList());

스트림 API의 플루언트 형식은 잘 설계된 DSL의 또 다른 특징이다. 모든 중간 연산은 게으르지만 최종 연산은 적극적이다.

10.2.2 데이터를 수집하는 DSL인 Collectors

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

Collector 인터페이스는 데이터 수집을 수행하는 DSL로 간주할 수 있다.

Comparator 인터페이스는 다중 필드 정렬을 지원하도록 합쳐질 수 있으며 Collectors는 다중 수준 그룹화를 달성할 수 있도록 합쳐질 수 있다.

// 두 Comparator를 플루언트 방식으로 연결해서 다중 필드 Comparator를 정의
Comparator<Person> comparator = comparing(Person::getAge).thenComparing(Person::getName);

Collectors API를 이용해 Collectors를 중첩함으로써 다중 수준 Collectors를 만들 수 있다.

Collector<? super Car, ?, Map<Brand, Map<Color, List<Car>>>> 
	carGroupingCollector = 
		groupingBy(Car::getBrand, groupingBy(Car::getColor));

특히 셋 이상의 컴포넌트를 조합할 때는 보통 플루언트 형식이 중첩 형식에 비해 가독성이 좋다.

GroupingBuilder는 유연한 방식으로 여러 그룹화 작업을 만든다.

Collector<? super Car, ?, Map<Brand, Map<Color, List<Car>>>> 
	carGroupingCollector = 
		groupingOn(Car::getColor).after(Car::getBrand).get()

위 코드는 중첨된 그룹화 수준에 반대로 그룹화 함수를 구현해야 하므로 유틸리티 사용 코드가 직관적이지 않다.


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

(해당 절의 대부분은 예제코드와 관련된 설명으로 다소 설명이 부족할 수 있으니 책을 참고하도록 권장합니다.)

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

10.3.1 메서드 체인

해당 방법을 이용하면 한 개의 메서드 호출 체인으로 거래 주문을 정의할 수 있다.

// 메서드 체인으로 주식 거래 주문 만들기
Order order = forCustomer("BigBank")
				.buy(80)
				.stock("IBM")
				.on("NYSE")
				.at(125.00)
		.....
				.end();

빌더를 구현해야 한다는 것이 메서드 체인의 단점이다. 상위 수준의 빌더를 하뒤 수준의 빌더와 연결할 많은 접착 코드가 필요하다. 도메인의 객체의 중첩 구조와 일치하게 들여쓰기를 강제하는 방법이 없다는 것도 단점이다.

10.3.2 중첩된 함수 이용

메서드 체인에 비해 함수의 중첩 방식이 도메인 객체 계층 구조에 그대로 반영된다는 것이 장점이다.

그렇지만 문제점이 있는데, 결과 DSL에 더 많은 괄호를 사용해야 한다는 사실이다. 더불어 인수 목록을 정적 메서드에 넘겨줘야 한다는 제약도 있다.

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

람다 표현식으로 정의한 함수 시퀀스를 사용한다.

해당 DSL을 만들려면 람다 표현식을 받아 실행해 도메인 모델을 만들어 내는 여러 빌더를 구현해야 한다. DSL 구현에서 했던 방식과 마찬가지로 이들 밀더는 메서드 체인 패턴을 이용해 만들려는 객체의 중간 상태를 유지한다.

10.3.4 조합하기

하나의 DSL에 한 개의 패턴만 사용할 수 있다는 규칙은 없다. 새로운 DSL을 개발해 메서드를 정의할 수 있다.

DSL 패턴을 혼용하여 가독성 있는 DSL을 만들 수도 있지만 결점도 있다. 결과 DSL이 여러 가지 기법을 혼용하고 있으므로 한 가지 기법을 적용한 DSL에 비해 사용자가 DSL을 배우는데 오랜 시간이 걸린다.

메서드 참조를 이용하면 많은 DSL의 가독성을 높일 수 있다.

10.3.5 DSL에 메서드 참조 사용하기

메서드 참조는 읽기 쉽고 코드를 간결하게 만든다.


10.4 실생활의 자바 8 DSL

세 가지 유명한 자바 라이브러리에 지금까지 살펴본 패턴이 사용되고 있는지 살펴본다.

10.4.1 jOOQ

SQL은 DSL을 가장 흔히, 광범위하게 사용하는 분야이다. jOOQ는 SQL을 구현하는 내부적 DSL로 자바에 직접 내장된 형식 안전 언어다.

SELECT * FROM BOOK
WHERE BOOK.PUBLISHED_IN = 2016
ORDER BY BOOK.TITLE

jOOQ DSL을 이용해 위 질의를 아래처럼 구현할 수 있다.

create.selectFrom(BOOK)
			.where(BOOK.PUBLISHED_IN.eq(2016))
			.orderBy(BOOK.TITLE)

스트림 API와 조합해 사용할 수 있다는 것이 jOOQ DSL의 또 다른 장점이다.

해당 기능 덕분에 SQL 질의 실행으로 나온 결과를 한 개의 플루언트 구문으로 데이터를 메모리에서 조작할 수 있다!

10.4.2 큐컴버

동작 주도 개발은 테스트 주도 개발의 확장으로 다양한 비즈니스 시나리오를 구조적으로 서술하는 간단한 도메인 전용 스크립팅 언어를 사용한다.

큐컴버는 이들 명령문을 실행할 수 있는 테스트 케이스로 변환한다. 결과적으로 이 개발 기법으로 만든 스크립트 결과물은 실행할 수 있는 테스트임과 동시에 비즈니스 기능의 수용 기준이 된다.

큐컴버는 전제 조건 정의(Given), 시험하려는 도메인 객체의 실질 호출(When), 테스트 케이스의 결과를 확인하는 어설션(Then) 의 개념을 사용한다.

10.4.3 스프링 통합

유명한 엔터프라이즈 통합 패턴을 지원할 수 있도록 의존성 주입에 기반한 스프링 프로그래밍 모델을 확장한다.

목표는 복잡한 엔터프라이즈 통합 솔루션을 구현하는 단순한 모델을 제공하고 비동기, 메시지 주도 아키텍처를 쉽게 적용할 수 있도록 돕는 것이다.

profile
내 안에 있는 힘을 믿어라.

0개의 댓글