배울 내용
- 도메인 전용 언어(domain-specific languages, DSL)이란 무엇이고, 어떤 형식으로 구성되는가?
- DSL을 API에 추가할 때의 장단점
- JVM에서 활용할 수 있는 자바 기반 DSL을 깔끔하게 만드는 대안
- 최신 자바 인터페이스와 클래스에 적용된 DSL에서 배움
- 효과적인 자바 기반 DSL을 구현하는 패턴과 기법
- 이들 패턴들은 자바 라이브러리와 도구에서 얼마나 흔히 사용되는가?
서론
많은 개발자들이 프로그래밍 언어도 결국 '언어' 라는 사실을 잊곤 합니다. 언어라는 것은 전달하고자 하는 바를 명확하고 안정적인 방식으로 전달하는 것입니다. 프로그래밍 언어도 결국 언어라는 사실은 프로그램도 결국 사람들에게 이해하기 쉽고 명확한 형태로 만들어져야 한다는 것을 의미합니다.
이해하기 쉬운 코드는 개발 관계자들이 자연스럽게 개발 프로세스에 참여하도록 도와주고 결국 개발 생산성이 높아질 수 있습니다. 특히 도메인 전문가가 소프트웨어 개발 프로세스에 참여할 수 있고 비즈니스 관점에서 소프트웨어가 제대로 확인할 수 있습니다.
여기에에 특정 비즈니스 로직을 이해하기 쉽게 코드로 표현할 수 있는 방법이 있습니다.
DSL
- 도메인 전용 언어라는 뜻으로 애플리케이션의 비즈니스 로직을 표현하는 것이 목적
- DSL은 특정 도메인을 대상으로 만들어진 특수 프로그래밍 언어
- 메이즌, 앤트 등이 DSL에 속하고 빌드라는 과정을 표현하는 DSL로 간주할 수 있음
- HTML도 웹 페이지의 구조를 정의하도록 특화된 언어
DSL을 활용하는 예시를 간단히 살펴봅시다.
데이터베이스
자바로 데이터베이스를 구현했다고 했을 때, 개발자는 데이터베이스 내부에서 주어진 레코드가 저장되어야 하는 장소, 테이블의 인덱스 구성 방법, 병렬 트랜잭션을 어떻게 처리할지를 계산하는 등의 많은 코드를 작성하게 될 것입니다.
아마 저 기능들을 코드로 작성하기 위해서는 상당히 많은 시스템 접근 코드(디스크 I/O, 네트워크, 버퍼 등)를 다루어야 할 것이고, 이런 것들은 애플리케이션 수준이 아니기 때문에 경험이 부족한 프로그래머의 경우 구현하기 어려울 수 있습니다.
위의 상황들을 고려해야 하는 코드 말고, 단순히 SQL처럼 SELECT name FROM menu WHERE calorie < 400 으로 코드를 표현할 수 없을까 하는 의문이 생길 수 있습니다. 이것이 바로 DSL을 활용한 예시라고 할 수 있습니다.
자바는 아니지만 DSL을 이용해 데이터베이스를 조작하자는 의미로 통합니다. 기술적으로 이런 종류의 DSL을 외부적이라고 하는데, 그 이유는 데이터베이스가 텍스트로 구현된 SQL 표현식을 파싱하고 평가하는 API를 제공하는 것이 일반적이기 때문입니다.
자바의 Stream api
Stream api는 Java의 데이터 처리 및 조작이라는 비즈니스 로직을 다루기 위해 설계된 내부 DSL 입니다.
Stream api를 사용하면 기존의 자바 코드를 간결하게 표현할 수 있었습니다.
코드가 간결해졌다고 느껴지는 이유는 스트림 api 의 특성인 메서드 체인 때문입니다.
메서드 체인을 플루언트 스타일이라고도 부르고 이런 스타일이 DSL에 쉽게 적용될 수 있습니다.
이제 DSL에 대해서 좀 더 깊이있게 배워봅시다.
10.1 도메인 전용 언어
DSL이란 특정 비즈니스 도메인의 문제를 해결하기 위해 만든 언어입니다.
DSL은 특정 비스니스 도메인을 인터페이스로 만든 API라고도 생각할 수 있습니다.
DSL은 범용 프로그래밍 언어가 아니고 특정 도메인에 국한되어 오직 놓여있는 문제에만 집중할 수 있습니다.
그렇기 떄문에 DSL을 이용하면 사용자가 특정 도메인의 복잡성을 좀 더 잘 다룰 수 있게 되는 것입니다.
저수준 구현 세부 사항 메서드는 클래스의 비공개로 만들어서 저수준 구현 세부 내용은 숨길 수 있습니다.
10.1.1 DSL의 장점과 단점
DSL은 코드의 비즈니스 의도를 명확하게 하고 가독성을 높인다는 점에서 장점이 되지만, DSL 구현은 코드이므로 올바로 검증하고 유지보수해야 하는 책임이 따릅니다. 그래서 DSL의 장점과 비용을 모두 고려해서 DSL을 추가하는 것이 투자대비 긍정적인 결과를 가져올 수 있을 것인지 따져볼 수 있어야 합니다.
DSL은 우선 다음과 같은 장점을 제공합니다.
- 간결함
- API는 비즈니스 로직을 간편하게 캡슐화해서 반복을 피할 수 있음
- 가독성
- 비 전문가도 코드를 쉽게 이해할 수 있음
- 유지보수
- 잘 설계된 DSL로 구현한 코드는 쉽게 유지보수하고 바꿀 수 있음
- 유지보수는 빈번하게 변경되는 비즈니스 로직 부분에 매우 중요함
- 높은 수준의 추상화
- DSL은 도메인과 직접적으로 관련없는 문제를 뒤로 숨겨줌
- 집중
- 특정 비즈니스 로직에 집중해서 만들었기 때문에 사용하는 개발자들은 해당 비즈니스 로직에만 집중 가능
- 관심사 분리
- 지정된 언어로 비즈니스 로직을 표현함으로 애플리케이션의 인프라구조와 관련된 문제와 독립적으로 비즈니스 관 련된 코드에서 집중하기가 용이함.
다음은 DSL의 단점입니다.
- DSL 설계의 어려움
- 간결하게 제한적인 언어에 도메인 지식을 담는 것이 쉬운 작업이 아님
- 개발 비용
- 코드에 DSL을 추가하는 작업은 초기 프로젝트에 많은 비용과 시간이 소모되는 작업
- DSL의 유지보수와 변경 또한 프로젝트에 부담이 됨
- 추가 우회 계층
- DSL은 추가적인 계층으로 도메인 모델을 감싸며 이 때 계층을 최대한 작게 만들어 성능 문제를 회피
- 새로 배워야 하는 언어
- 요즘은 한 프로젝트에 여러가지 언어를 사용해야 하는 추세
- 하지만 DSL을 프로젝트에 추가하면서 팀이 배워야 하는 언어가 한 개 더 늘어나게 됨
- 호스팅 언어 한계
- 일부 자바 같은 범용 프로그래밍 언어는 장황하고 엄격한 문법을 가져 사용자 친화직 DSL을 만들기 힘듦
- 자바 8의 람다식이 이런 자바의 문제를 해결할 강력한 도구
이제 자바 8에 추가된 기능을 이용해서 가독성 좋고 사용하기 쉬은 DSL을 개발하는데 필요한 패턴과 전략을 살펴보기로 합시다. 또 DSL말고 다른 해결책이 없는지도 한번 알아봅시다!
10.2 최신 자바 API의 작은 DSL
앞서 말씀드린 대로 자바 Stream 인터페이스는 네이티브 자바 API에 작은 내부 DSL이라고 볼 수 있습니다.
Stream은 컬렉션의 항목을 필터, 정렬, 변환, 그룹화, 조작하는 DSL 입니다.
전통적인 자바 문법만을 사용하여 로그 파일을 가져와 ERROR라는 단어로 시작하는 파일의 첫 40행을 수집하는 작업을 수행한다고 가정해 봅시다.
List<String> errors = new ArrayList<>();
int errorCount = 0;
BufferedReader bufferedReader
= new BufferedReader(new FileReader(fileName));
String line = bufferedReader.readLine();
while (errorCount < 40 && line != null) {
if(line.startsWith("ERROR")){
errors.add(line);
errorCount++;
}
line = bufferedReader.readLine();
}
해당 코드에는 가독성과 유지보수성 관점에서 문제가 되는 코드입니다.
전통적인 자바 언어로만 코드를 작성해야 하다보니 해당 코드가 해결해야 하는 문제(File 관련 문제, 실제 로직 처리 문제 등)들에 대해 분리되지 않은 형태를 보입니다.
같은 의무를 지닌 코드가 여러 행에 분산
File 관련 문제
- FileReader 생성
- 파일이 종료되었는지 확인하는 while 루프의 두 번째 조건
- 파일의 다음 행을 읽는 while 루프의 마지막 행
실제로 원하는 작업을 수행해야 하는 코드
- errorCount 변수를 초기화 하는 코드
- while 루프의 첫 번째 조건
- Error을 로그에서 발견하면 카운터를 증가시키는 행
함수 하나가 하는 일이 여러개라서 전통적인 자바 문법을 이용하는 경우에는 어쩔 수 없는 문제인거 같아 보입니다.
하지만 저희는 Stream 인터페이스를 이용해서 함수형으로 코드를 좀 더 쉽고 간편하게 작성할 수 있습니다!
List<String> errors = Files.lines(Paths.get(fileName))
.filter(line -> line.startsWith("ERROR"))
.limit(40)
.collect(toList());
이렇게 스트림을 통해 함수형으로 코드를 작성하게 되면 앞서 보았던 코드에 분산되어 있는 문제들을 한 곳으로 집중시킬 수 있고, 무엇보다 가독성과 유지보수성이 올라가게 됩니다.
10.2.2 데이터를 수집하는 DSL인 Collectors
Stream 인터페이스가 데이터 리스트를 조작하는 DSL로 간주할 수 있음을 여러번 확인 했습니다.
앞장에서 배웠던 Collectors 인터페이스 또한 데이터를 수집한다는 문제에서 하나의 DSL로 간주될 수 있습니다.
Collectors를 이용해 데이터를 수집, 그룹화, 파티션 할 수 있음을 앞장에서 알아봤었습니다.
이번 절에서 주목해서 지켜볼 점은 Collectors가 다중 필드 그룹화를 처리하는 방식입니다.
비교를 위해 Comparator 인터페이스를 이용해 다중 필드 정렬을 해보겠습니다.
Comparator<Person> comparator =
comparing(Person::getAge).thenComparing(Persion::getName);
Comparator 인터페이스의 경우 플루언트 형식으로 연결할 수 있게 만들어져 있습니다.
그에 반해 Collectors 인터페이스의 경우는 Collectors를 중접해서 다중 필드 그룹화가 가능하도록 만들어져 있습니다.
Collector<? super Car, ?, Map<Brand, Map<Color, List<Car>>>>
carGroupingCollector = groupingBy(Car::getBrand, groupingBy(Car::getColor));
사실 여러 필드를 가지고 작업하는 경우에도 가독성 측면에서 플루언트 형식으로 만들면 좀 더 좋았을 수도 있을 것 같습니다.
하지만 왜 Java API 설계자들은 플루언트 형식이 아닌 중첩 형식을 제공했을까요?
다음 절에서 한번 왜 설계자들이 내부 설계를 이렇게 할 수 밖에 없었는지 자바로 DSL을 만들어 보면서 그 이유도 한번 생각해보고, DLS을 구현하는 유용한 패턴과 기법도 한번 알아봅시다!
10.3 자바로 DSL을 만드는 패턴과 기법
우선 특정 도메인 모델부터 정의해 봅시다.
DSL이라는 것은 특정 도메인 모델에 특화된 언어이므로 대상이 되는 도메인 모델이 있어야 합니다.
예제 도메인은 총 3개의 클래스로 구성되어 있습니다.
Stock : 주어진 시장에 주식 가격을 모델링하는 순수 자바 빈즈
Trade : 주어진 가격에서 주어진 양의 주식을 사거나 파는 거래
Order : 고객이 요청한 한 개 이상의 거래의 주문
@Setter @Getter
@ToString
public class Order {
private String customer;
private List<Trade> trades = new ArrayList<>();
public void addTrade( Trade trade ) {
trades.add( trade );
}
public double getValue() {
return trades.stream().mapToDouble( Trade::getValue ).sum();
}
}
@Getter @Setter
public class Stock {
private String symbol;
private String market;
}
@Getter @Setter
public class Trade {
public enum Type {BUY, SELL}
private Type type;
private Stock stock;
private int quantity;
private double price;
public double getValue() {
return quantity * price;
}
}
DSL을 패턴을 익히기 전에 전통적인 자바 문법만을 가지고 주식 거래를 위한 주문 객체를 하나 만들어 봅시다.
Order order = new Order();
order.setCustomer("Big Bank");
Trade trade1 = new Trade();
trade1.setType(Trade.Type.BUY);
Stock stock1 = new Stock();
stock1.setSymbol("IBM");
stock1.setMarket("NYSE");
trade1.setStock(stock1);
trade1.setPrice(125.00);
trade1.setQuantity(80);
order.addTrade(trade1);
Trade trade2 = new Trade();
trade2.setType(Trade.Type.BUY);
Stock stock2 = new Stock();
stock2.setSymbol("GOOGLE");
stock2.setMarket("NASDAQ");
trade2.setStock(stock2);
trade2.setPrice(375.00);
trade2.setQuantity(50);
order.addTrade(trade2);
처음 프로그래밍 언어를 접할 때 작성하는 코드의 전형적인 모습인 것 같습니다...
하지만 좀 더 나은 프로그래머가 되기 위해 저렇게 장황하고 비개발자인 도메인 전문가가 이해하기 힘든 코드 말고,
DSL 패턴을 적용해서 우리 이외에 다른 개발 이해관계자들도 알아보기 쉬운 코드로 바꿔봅시다!
10.3.1 메서드 체인
메서드 체인은 DSL 에서 가장 흔하게 사용되는 방식입니다.
이 방법을 사용하게 되면 한번의 메서드 호출 체인으로 거래 주문을 정의할 수 있습니다.

코드가 한눈에 들어오고 무엇보다 각 메서드가 어떤 의도를 가지는지 뚜렷하게 드러납니다.
해당 DSL을 구현하기 위해서는 플루언트 API로 도메인 객체를 만드는 몇 개의 빌더를 구현해야 합니다.
우선 메서드 체인을 만들기 위한 최상위 수준의 빌더를 하나 만들어 봅시다.
public class MethodChainingOrderBuilder {
public final Order order = new Order();
private MethodChainingOrderBuilder(String customer) {
order.setCustomer(customer);
}
public static MethodChainingOrderBuilder forCustomer(String customer) {
return new MethodChainingOrderBuilder(customer);
}
------------------------------------------------------------------------
//주문 빌더는 buy,sell을 통해 다른 주문을 추가할 수 있음
public TradeBuilder buy(int quantity) {
return new TradeBuilder(this, Trade.Type.BUY, quantity);
}
public TradeBuilder sell(int quantity) {
return new TradeBuilder(this, Trade.Type.SELL, quantity);
}
------------------------------------------------------------------------
private MethodChainingOrderBuilder addTrade(Trade trade) {
order.addTrade(trade);
return this;
}
public Order end() {
return order;
}
}
자 이제 Order를 감싸는 최상위 빌더를 만들어 보았으니 하나의 주문에 여러개의 거래를 추가하기 위한 빌더를 만들어 봅시다.
public static class TradeBuilder {
private final MethodChainingOrderBuilder builder;
public final Trade trade = new Trade();
private TradeBuilder(MethodChainingOrderBuilder builder, Trade.Type type, int quantity) {
this.builder = builder;
trade.setType(type);
trade.setQuantity(quantity);
}
//빌더를 이어나가기 위한 TradeBuilder의 public 메서드
public StockBuilder stock(String symbol) {
return new StockBuilder(builder, trade, symbol);
}
}
이제 메서드 체인을 계속 이어나기 위한 StockBuilder라는 것을 만들어 보겠습니다.
public static class StockBuilder {
private final MethodChainingOrderBuilder builder;
private final Trade trade;
private final Stock stock = new Stock();
private StockBuilder(MethodChainingOrderBuilder builder, Trade trade, String symbol) {
this.builder = builder;
this.trade = trade;
stock.setSymbol(symbol);
}
public TradeBuilderWithStock on(String market) {
stock.setMarket(market);
trade.setStock(stock);
return new TradeBuilderWithStock(builder, trade);
}
}
StockBuilder는 주식의 시장을 지정하고, 거래에 주식을 추가하고, 최종 빌더를 반환하는 on() 메서드가 정의되어 있습니다.
public static class TradeBuilderWithStock {
private final MethodChainingOrderBuilder builder;
private final Trade trade;
public TradeBuilderWithStock(MethodChainingOrderBuilder builder, Trade trade) {
this.builder = builder;
this.trade = trade;
}
public MethodChainingOrderBuilder at(double price) {
trade.setPrice(price);
return builder.addTrade(trade);
}
}
TradeBuilderWithStock는 거래되는 주식의 단위 가격을 설정한 다음 원래 주문 빌더를 반환합니다.
플루언트 형식을 통해 주문 객체 생성
Order order = forCustomer("Big Bank")
.buy(80)
.stock("IBM")
.on("NYSE")
.at(125.00)
.sell(50)
.stock("GOOGLE")
.on("NASDAQ")
.at(375.00)
.end();
메서드 체인을 이용한 플루언트 형식의 장/단
장점
- 사용자 정의에 따라 유연하고 가독성 좋게 코드 작성 가능
- 개발자가 미리 정의해둔 방식으로 사용 방식 유도 가능
- 주문에 사용한 파라미터가 빌더 내부로 국한
- 정적 메서드의 사용을 최소하하고 메서드의 명이 파라미터의 이름을 대신하도록 만듦으로써 가독성이 높음
단점
- 여러개의 빌더를 구현해야 함
- 상위 수준의 빌더를 하위 수준의 빌더와 연결하기 위해 코드를 작성해야 함
- 도메인의 객체의 중첩 구조와 일치하게 들여쓰기를 강제하는 방법이 없음
10.3.2 중첩된 함수 이용
중첩된 함수 DSL 패턴은 이름에서 알 수 있듯이 다른 함수 안에 함수를 이용해 도메인 모델을 만드는 것입니다.

public class NestedFunctionOrderBuilder {
public static Order order(String customer, Trade... trades) {
Order order = new Order();
order.setCustomer(customer);
Stream.of(trades).forEach(order::addTrade);
return order;
}
public static Trade buy(int quantity, Stock stock, double price) {
return buildType(quantity, stock, price, Trade.Type.BUY);
}
public static Trade sell(int quantity, Stock stock, double price) {
return buildType(quantity, stock, price, Trade.Type.SELL);
}
private static Trade buildType(int quantity, Stock stock, double price, Trade.Type buy) {
Trade trade = new Trade();
trade.setQuantity(quantity);
trade.setType(buy);
trade.setStock(stock);
trade.setPrice(price);
return trade;
}
//의미 부여를 위한 더미 메서드
public static double at(double price) {
return price;
}
public static Stock stock(String symbol, String market) {
Stock stock = new Stock();
stock.setSymbol(symbol);
stock.setMarket(market);
return stock;
}
//의미 부여를 위한 더미 메서드
public static String on(String market) {
return market;
}
}
이 방법의 장점은 메서드 체인과 메서드의 중첩 방식이 도메인 객체 계층 구조(주문은 한 개 이상의 거래를 포함하고
각 거래는 한 개의 주식을 참조)에 그대로 반영이 된다는 것입니다.
하지만 이 방식에도 단점이 있습니다.
- 결과 생성 시 더 많은 괄호 생성
- 도메인 객체에 선택 사항 필드가 존재하는 경우, 인수를 생략할 수 있으므로 이를 위한 여러 메서드 오버라이드 구현 필요
- 파라미터의 의미가 이름이 아니라 위치에 의해 정의됨
- 해당 예제에서는 파라미터의 역할을 확실하게 만들기 위해 여러 더미 메서드를 이용(at, on)
10.3.3 람다 표현식을 이용한 함수 시퀀싱
다음 DSL 패턴은 람다 표현식으로 정의한 함수 시퀀스를 이용합니다.

이런 DSL을 만들기 위해서는 람다 표현식을 파라미터로 받아 도메인 모델을 만들어 내는 여러 빌더를 구현해야 합니다.
이번 패턴은 Consumer 객체를 파라미터로 설정해서 사용자가 람다 표현식을 이용해서 파라미터를 구현할 수 있게 합니다.
public class LambdaOrderBuilder {
private Order order = new Order();
public static Order order(Consumer<LambdaOrderBuilder> consumer) {
LambdaOrderBuilder builder = new LambdaOrderBuilder();
consumer.accept(builder);
return builder.order;
}
public void forConsumer(String customer) {
order.setCustomer(customer);
}
public void buy(Consumer<TradeBuilder> consumer) {
trade(consumer, Trade.Type.BUY);
}
public void sell(Consumer<TradeBuilder> consumer) {
trade(consumer, Trade.Type.SELL);
}
private void trade(Consumer<TradeBuilder> consumer, Trade.Type type) {
TradeBuilder builder = new TradeBuilder();
builder.trade.setType(type);
consumer.accept(builder);
order.addTrade(builder.trade);
}
public static class TradeBuilder {
private Trade trade = new Trade();
public void quantity(int quantity) {
trade.setQuantity(quantity);
}
public void price(double price) {
trade.setPrice(price);
}
public void stock(Consumer<StockBuilder> consumer) {
StockBuilder builder = new StockBuilder();
consumer.accept(builder);
trade.setStock(builder.stock);
}
}
public static class StockBuilder {
private Stock stock = new Stock();
public void symbol(String symbol) {
stock.setSymbol(symbol);
}
public void market(String market) {
stock.setMarket(market);
}
}
}
주문 빌더의 buy,sell 메서드는 두 개의 Consumer<TradeBuilder> 람다 표현식을 파라미터로 받습니다.
이 람다 표현식이 실행되면 Stock 매수, Stock 매도 거래가 만들어 집니다.
TradeBuilder는 세 번째 빌더의 Consumer 즉 거래된 주식을 파라미터로 받습니다.
이 패턴의 장점은 메서드 체인 패턴처럼 플루언트 방식으로 주문을 만들 수 있고, 중첩 함수 형식처럼 다양한 람다 표현식의 중첩 수준과 비슷하게 도메인 객체의 계층 구조를 유지할 수 있습니다.
하지만 많은 코드 설정과 자바 8 람다 표현식 문법에 의한 부정적인 영향 또한 받을 수 있겠습니다...
10.3.4 조합하기
이제 이 3가지 패턴을 한번 조합해봅시다!

플루언트 방식을 사용한 메서드 체인 패턴과 중첩된 함수 패턴 그리고 람다 기법을 혼용한 결과입니다.
public class MixBuilder {
public static Order forCustomer(String customer,
TradeBuilder... builders) {
Order order = new Order();
order.setCustomer(customer);
Stream.of(builders).forEach(b -> order.addTrade(b.trade));
return order;
}
public static TradeBuilder buy(Consumer<TradeBuilder> consumer) {
return buildTrade(consumer, Trade.Type.BUY);
}
public static TradeBuilder sell(Consumer<TradeBuilder> consumer) {
return buildTrade(consumer, Trade.Type.SELL);
}
private static TradeBuilder buildTrade(Consumer<TradeBuilder> consumer,
Trade.Type buy) {
TradeBuilder builder = new TradeBuilder();
builder.trade.setType(buy);
consumer.accept(builder);
return builder;
}
// 메서드 체인 패턴을 구현하여 플루언트 API 제공
public static class TradeBuilder {
private Trade trade = new Trade();
public TradeBuilder quantity(int quantity) {
trade.setQuantity(quantity);
return this;
}
public TradeBuilder at(double price) {
trade.setPrice(price);
return this;
}
public StockBuilder stock(String symbol) {
return new StockBuilder(this, trade, symbol);
}
}
// 메서드 체인 패턴을 구현하여 플루언트 API 제공
public static class StockBuilder{
private final TradeBuilder builder;
private final Trade trade;
private final Stock stock = new Stock();
private StockBuilder(TradeBuilder builder, Trade trade, String symbol) {
this.builder = builder;
this.trade = trade;
stock.setSymbol(symbol);
}
public TradeBuilder on(String market) {
stock.setMarket(market);
trade.setStock(stock);
return builder;
}
}
public static void main(String[] args) {
Order order =
forCustomer("Big Bank",
buy(t -> t.quantity(80)
.stock("IBM")
.on("NYSE")
.at(125.00)),
sell(t -> t.quantity(50)
.stock("GOOGLE")
.on("NASDAQ")
.at(125.00)));
}
}
앞서 살펴본 세 가지 패턴을 혼용해 가독성 있는 DSL을 만들어 보았습니다.
하지만 다른 세 가지 패턴과 마찬가지로 이 방법도 단점이 존재합니다.
여러 가지 기법이 혼용되어 있어 사용자가 DSL을 익히는 데 시간이 걸립니다.
10.3.5 DSL에 메서드 참조 사용하기
이번에는 앞서 살펴본 도메인 모델인 주식 거래 모델에 기능을 추가해 보겠습니다.
주문의 총 합에 세금을 추가해 최종 가격을 계산하는 기능을 추가해 보려고 합니다.
public class Tax {
public static double regional(double value) {
return value * 1.1;
}
public static double general(double value) {
return value * 1.3;
}
public static double surcharge(double value) {
return value * 1.05;
}
}
어떤 세금을 적용할 것인지는 불리언 변수를 통해 구별해 내는 방법을 사용할 수도 있겠습니다.
public static double calculate(Order order, boolean useRegional, boolean useGeneral, boolean useSurcharge) {
double value = order.getValue();
if (useRegional) {
value = Tax.regional(value);
}
if (useGeneral) {
value = Tax.general(value);
}
if (useSurcharge) {
value = Tax.surcharge(value);
}
return value;
}
=> double value = calculate(order,true,false,true);
하지만 이런식으로 코드를 작성하는 것은 가독성 측면에서 좋지 않습니다.
불리언 변수의 순서를 기억해야 하며, 어떤 세금이 적용되었는지 한 눈에 알아보기 힘듦니다.
그래서 플루언트 방식으로 불리언 플래그를 설정할 수 있는 DSL을 만드는 것이 더 좋아보입니다!
public class TaxCalculator {
private boolean useRegional;
private boolean useGeneral;
private boolean useSurcharge;
public TaxCalculator withTaxRegional() {
useRegional = true;
return this;
}
public TaxCalculator withTaxGeneral() {
useGeneral= true;
return this;
}
public TaxCalculator withTaxSurcharge() {
useSurcharge = true;
return this;
}
public double calculate(Order order) {
return calculate(order, useRegional, useGeneral, useSurcharge);
}
}
TaxCalulator는 지역 세금과 추가 요금은 주문에 추가하고 싶다는 점을 명확하게 보여줄 수 있습니다.
double value = new TaxCalculator().withTaxRegional()
.withTaxSurcharge()
.calculate(order);
코드가 좀 장황해 보입니다.
그리고 도메인의 각 세금에 해당하는 불리언 필드가 필요하므로 확장성도 제한적입니다.
자바의 함수형 기능을 이용해서 더 간결하고 유연한 방식으로 같은 가독성을 달성해 봅시다.
TaxCalculator를 리팩터링 해봅시다.
public class TaxCalculator {
public DoubleUnaryOperator taxFunction = d -> d; //주문에 적용된 모든 세금을 계산하는 함수
public TaxCalculator with(DoubleUnaryOperator f) { // 새로운 세금 계산 함수를 얻어서 파라미터로 전달된 함수와 현재 함수를 함침
taxFunction = taxFunction.andThen(f);
return this;
}
public double calculate(Order order) {
return taxFunction.applyAsDouble(order.getValue()); //주문의 총 합에 세금 계산 함수를 적용해 최종 주문값을 계산
}
}
이 기법은 주문의 총 합에 적용할 함수 한 개의 필드만 필요로 하며 TaxCalculator 클래스를 통해 모든 세금 설정이 적용됩니다.
double value = new TaxCalculator().with(Tax::regional)
.with(Tax::surcharge)
.calculate(order);
이렇게 with이라는 메서드에 새 세금이 추가되어도 적용 방식만 함수로 넘겨주면 되므로 매우 유연해 졌습니다.
그리고 메서드 참조를 이용했기 때문에 읽기 쉽고 간결한 코드가 되었습니다!
정리
전 이번 챕터를 공부하면서 왜 제가 자바로 프로그래밍을 하면서 어려움을 느꼈는지 알게되었던거 같아요.
테스트를 작성할 때나, JPA를 사용할 때나, 빌드 도구를 사용하기 위해 Gradle을 사용했을 때 등등...
자바의 전통적인 방식만을 사용해서 코딩을 했던 저였기에, 그리고 자바 8 이후의 문법들을 등한시 했던 저에게 요즘 개발 라이브러리 및 프레림워크, 개발 관련 도구들이 참 어렵게 다가왔었습니다.
하지만 이제 스트림, 람다, 메서드 참조, 그리고 DSL을 배우게 되면서 자바로 이루어진 개발 도구들에 대해 좀 더 이해할 수 있게 되었던거 같아요. 그리고 추상화라는 것에 좀 더 집중하게 되면서 사고가 이전과 달리 많이 확장되었음을 느낍니다...
좀 더 나은 개발자가 되기 위해 좀 더 노력해보죠!
'도서 > 모던 자바 인 액션' 카테고리의 다른 글
| 모던 자바 인 액션 - 7장 - 병렬 데이터 처리와 성능 (0) | 2023.08.10 |
|---|---|
| 모던 자바 인 액션 - 6장 - 스트림으로 데이터 수집 (0) | 2023.08.05 |
| 모던 자바 인 액션 - 5장 다양한 스트림 활용 (0) | 2023.08.02 |
| 모던 자바 인 액션 - 4장 스트림 활용 (0) | 2023.07.31 |
| 모던 자바 인 액션 - 2장 동작의 파라미터화 (1) | 2023.07.28 |