본문 바로가기
그룹 스터디 공부(IT 서적)/모던 자바 인 액션

chapter 10 람다를 이용한 도메인 전용 언어

by hanyugyeong 2023. 8. 10.
반응형
SMALL

이 장의 내용

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

10.1 도메인 전용 언어 

DSL은 특정 비즈니스 도메인의 문제를 해결하기 위해 만든 언어로, 자바에서는 클래스와 메서드를 이용하여 도메인을 표현합니다. DSL은 해당 도메인을 인터페이스로 만든 API와 같은 역할을 합니다.

DSL은 특정 도메인에 국한된 용어와 동작을 갖는 범용 프로그래밍 언어가 아닙니다. DSL은 해당 도메인의 문제 해결에만 집중하며, 이를 통해 사용자가 해당 도메인의 복잡성을 더 잘 다룰 수 있도록 합니다. 이러한 특징을 통해 사용자 친화적인 DSL을 만들 수 있습니다.

DSL은 평문 영어가 아닌 특정 도메인에 국한된 용어와 동작을 갖는 언어입니다. DSL은 프로그래머가 아닌 사람도 이해할 수 있는 코드를 작성하고, 가독성을 유지하여 유지보수를 쉽게 할 수 있도록 합니다. 이러한 특징으로 인해 DSL은 의사소통의 왕이며, 코드를 한 번 구현하지만 여러 번 읽게 될 때 가독성이 유지되도록 만들어야 합니다.

 

10.1.1 DSL의 장점과 단점 

DSL 구현은 코드이므로 올바른 검증하고 유지보수해야하는 책임이 따른다.

DSL은 다음과 같은 장점을 제공합니다.      

  • 간결함 : 비즈니스 로직을 캡슐화하여 코드를 간결하게 만들 수 있습니다.
  • 가독성 : 도메인 용어를 사용해서 비 도메인 전문가도 쉽게 이해할 수 있습니다.
  • 유지보수 : 잘 설계된 DSL로 구현한 코드는 쉽게 유지보수하고 변경할 수 있습니다.
  • 높은 수준의 추상화 : DSL은 도메인과 같은 추상화 수준에서 동작하므로 세부 사항을 숨길 수 있습니다.
  • 집중 : 비즈니스 도메인의 규칙을 표현할 목적으로 설계되어 프로그래머가 특정 코드에 집중할 수 있고, 생산성이 향상됩니다.
  • 관심사분리 : 지정된 언어로 비즈니스 로직을 표현함으로 애플리케이션의 인프라 구조와 관련된 문제와 독립적으로 비즈니스 관련된 코드에서 집중하기가 용이하며, 유지보수가 쉬운 코드를 구현할 수 있습니다.

반면, DSL로 인해 다음과 같은 단점도 발생합니다.

  • DSL 설계의 어려움 : 간결하게 제한적인 언어에 도메인 지식을 담는 것이 쉬운 작업은 아닙니다.
  • 개발 비용 : DSL을 추가하는 작업은 초기에 많은 비용과 시간이 소요되며, 또한 DSL의 유지보수와 변경은 프로젝트에 부담을 줍니다.
  • 추가 우회 계층 : DSL은 도메인 모델을 감싸기 위해 추가적인 계층을 만들어야 합니다. 이때 가능한 한 계층을 작게 유지하여 성능 문제를 회피합니다.
  • 새로 배워야 하는 언어 : DSL을 추가하면서 새로 배워야 하는 언어가 생기며, 여러 비즈니스 도메인을 다루는 개별 DSL을 사용하는 경우 이들을 유기적으로 동작하도록 합치는 일은 쉽지 않습니다. 이는 개별 DSL이 독립적으로 진화할 수 있기 때문입니다.
  • 호스팅 언어 한계 : 일부 자바 같은 범용 프로그래밍 언어는 사용자 친화적인 DSL을 만들기 어렵습니다. 이러한 언어로 만든 DSL은 장황하고 엄격한 문법을 가지며, 성가신 문법의 제약을 받아 읽기가 어려워집니다. 하지만 자바 8의 람다 표현식은 이러한 문제를 해결할 수 있는 강력한 새 도구입니다.

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

DSL의 카테고리를 구분하는 가장 흔한 방법은 내부 DSL과 외부 DSL로 나누는 것입니다. 내부 DSL은 기존 호스팅 언어를 기반으로 구현하며, 외부 DSL은 자체적인 문법을 갖는 독립적인 언어입니다.

JVM으로 인해 내부 DSL과 외부 DSL의 중간에 해당하는 DSL인 다중 DSL이 생겨날 수 있습니다. 다중 DSL은 자바가 아니지만 JVM에서 실행되며, 스칼라나 그루비와 같이 더 유연하고 표현력이 강력한 언어들을 의미합니다.

 

내부 DSL

자바는 예전에는 읽기 쉽고 간단하며 표현력 있는 DSL을 만드는데 한계가 있었지만, 람다 표현식이 등장하면서 이 문제가 어느 정도 해결되었습니다. 람다를 적극적으로 활용하면 익명 내부 클래스를 사용하는 것보다 간결한 DSL을 만들 수 있으며, 이를 통해 신호 대비 잡음 비율을 적정 수준으로 유지할 수 있습니다.

다음은 자바 7 문법으로 문자열을 출력하고, 자바 8의 새 forEach 메서드를 이용하는 예제로 신호 대비 잡음 비율이 무엇을 의미하는지 확인하는 코드입니다. 

List<String> numbers = Arrays.asList("one", "two", "three");
numbers.forEach(new Consumer<String>() {
   @Override
   public void accept(String s) {
      System.out.println(s);
   }
});

위 코드 예제에서 다음처럼 익명 내부 클래스를 람다 표현식으로 바꿀 수 있습니다.

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

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

자바로 DSL을 구현하면 다음과 같은 장점을 얻을 수 있습니다.                                                                                                  

  • 기존 자바 언어를 이용하면 새로운 패턴과 기술을 배워 DSL을 구현하는 노력이 줄어듭니다.
  • 순수 자바로 DSL을 구현하면 컴파일할 때 다른 언어의 컴파일러를 사용할 필요가 없으므로 추가 비용이 들지 않습니다.
  • 새로운 언어나 외부 도구를 배울 필요 없이 자바를 이용해 DSL을 구현할 수 있습니다.
  • 자바 IDE를 사용해 자동 완성, 자동 리팩터링 등의 기능을 사용할 수 있습니다.
  • 자바로 구현한 DSL은 추가 DSL을 쉽게 합칠 수 있습니다.

DSL 합침 문제를 해결하는 방법으로, 같은 자바 바이트코드를 사용하는 JVM 기반 프로그래밍 언어를 이용하는 것이 있습니다. 이러한 언어는 다중 DSL이라고 불립니다         

다중 DSL

현재 JVM에서 실행되는 언어는 100개가 넘으며, 스칼라, 루비, JRuby, Jython, 코틀린, 실론 등이 유명한 언어 중 하나입니다. 이들은 자바보다 젊으며 제약을 줄이고, 간편한 문법을 지향하도록 설계되었습니다. DSL은 기반 프로그래밍 언어의 영향을 받으므로, 새로운 언어의 특성들이 간결한 DSL을 만드는 데 아주 중요합니다. 특히, 스칼라는 커링, 임의 변환 등 DSL 개발에 필요한 여러 특성을 갖췄습니다.                                                                                                                                 

외부 DSL

외부 DSL을 구현하는 것은 새로운 문법과 구문을 갖는 언어를 설계하고 파싱, 분석, 실행 코드를 만드는 큰 작업입니다. 이 방법은 일반적인 작업이 아니며, 기술적인 어려움과 제어 범위를 벗어나는 문제가 발생할 수 있습니다. ANTLR 같은 자바 기반 파서 생성기를 사용하면 도움이 될 수 있습니다.

외부 DSL 개발의 큰 장점은 무한한 유연성으로, 필요한 특성을 완벽하게 제공하는 언어를 설계할 수 있다는 것입니다. 하지만 외부 DSL로 구현한 비즈니스 코드와 자바로 개발된 인프라구조 코드를 분리하는 것은 인공 계층을 만들어내어 양날의 검이 될 수 있습니다.

10.2 최신 자바  API의 작은 DSL 

Java 8에서는 람다와 메서드 참조 등의 새로운 기능을 활용해 네이티브 자바 API도 DSL을 개발할 수 있게 되었습니다. Comparator 인터페이스에 새 메서드가 추가되어 람다 표현식이 재사용 가능한 Comparator 객체를 생성하는 데 사용될 수 있으며, 불필요한 무명 내부 클래스를 사용하지 않아도 됩니다. 이를 통해 네이티브 자바 API의 재사용성과 메서드 결합도를 높일 수 있습니다. 인터페이스가 정적 메서드와 디폴트 메서드를 가질 수 있다는 사실도 배우게 됩니다.

사람을 가리키는 객체 목록을 가지고 있고, 사람의 나이를 기준으로 객체를 정렬해야하는 상황에서 람다가 없으면 내부 클래스로 Comparator 인터페이스를 구현해야 합니다.

Collections.sort(persons, new Comparator<Person>() {
   public int compare(Person p1, Person p2) {
      return p1.getAge() - p2.getAge();
   }
});

// 내부 클래스를 람다 표현식으로 바꿀 수 있습니다.
Collections.sort(people, (p1, p2) -> p1.getAge() - p2.getAge());

람다 표현식으로 Comparator 인터페이스를 구현하는 것이 좋지만, 자바에서는 Comparator 객체를 더 가독성 있게 구현하기 위해 Comparator 인터페이스에 정적 유틸리티 메서드를 제공합니다. 이러한 정적 메서드는 Comparator.comparing 메서드와 같이 인터페이스에 포함되어 있으며, 위의 예제를 더 간결하게 구현할 수 있도록 도와줍니다.      

Collections.sort(persons, comparing(p -> p.getAge());

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

// reverse 메서드를 이용해서 나이 역순으로 정렬
Collections.sort(persons, comparing(Person::getAge).revers());

// 이름으로 비교를 수행하는 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 인터페이스는 작은 내부 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();
}

같은 의무를 지닌 코드가 여러 행에 분산되어 있다. 

  • FileReader가 만들어짐 
  • 파일이 종료되었는지 확인하는 while 루프의 두 번째 조건 
  • 파일의 다음 행을 읽는 while 루프의 마지막 행 

마찬가지로 첫 40행을 수집하는 코드도 세 부분으로 흩어져있다.

  • errorCount 변수를 초기화하는 코드 
  • while 루프의 첫 번째 조건 
  • "Error"을 로그에서 발견하면 카운터를 증가시키는 행 

코드에 에러 처리를 생략하더라도, 코드가 장황하고 의도를 파악하기 어려울 때 가독성과 유지보수성이 떨어집니다. 문제가 잘 분리되지 않아서 관련 코드가 여러 행에 걸쳐 분산되어 있습니다.
예를 들어, FileReader 생성, 파일 종료 확인, 파일의 다음 행 읽기와 같은 코드가 분산되어 있습니다. 또한, 첫 40행을 수집하는 코드도 초기화, while 루프 조건, "Error" 발견 시 카운터 증가 등 세 부분으로 나뉘어 있어 코드의 구조를 이해하기 어렵습니다.
Stream 인터페이스를 이용해 함수형으로 코드를 구현하면 더 쉽고 간결하게 구현할 수 있습니다.

List<String> errors = Files.lines(Paths.get(fileName)) // 파일을 열어서 문자열 스트림을 만듦
                           .filter(line -> line.startsWith("ERROR")) // "ERROR"로 시작하는 행 필터링
                           .limit(40) // 결과를 첫 40행으로 제한
                           .collect(toList()); // 결과 문자열을 리스트로 수집

스트림 API는 잘 설계된 DSL의 특징을 갖고 있으며, 플루언트 형식을 사용합니다. 중간 연산들은 게으른 방식으로 작동하며, 파이프라인에 포함될 수 있는 스트림을 반환합니다. 반면, 최종 연산은 적극적으로 동작하여 전체 파이프라인의 계산을 실행합니다.

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

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

 

예제 도메인 모델은 세 가지로 구성된다. 첫 번째는 주어진 시장에 주식 가격을 모델링하는 순수 자바 빈즈다.

public class Stock {

  private String symbol;
  private String market;

  public String getSymbol() {
    return symbol;
  }

  public void setSymbol( String symbol ) {
    this.symbol = symbol;
  }

  public String getMarket() {
    return market;
  }

  public void setMarket( String market ) {
    this.market = market;
  }

  @Override
  public String toString() {
    return String.format("Stock[symbol=%s, market=%s]", symbol, market);
  }

}

두 번째는 주어진 가격에서 주어진 양의 주식을 사거나 파는 거래(trade)다. 

public class Trade {

  public enum Type {
    BUY,
    SELL
  }

  private Type type;
  private Stock stock;
  private int quantity;
  private double price;

  public Type getType() {
    return type;
  }

  public void setType(Type type) {
    this.type = type;
  }

  public int getQuantity() {
    return quantity;
  }

  public void setQuantity(int quantity) {
    this.quantity = quantity;
  }

  public double getPrice() {
    return price;
  }

  public void setPrice(double price) {
    this.price = price;
  }

  public Stock getStock() {
    return stock;
  }

  public void setStock(Stock stock) {
    this.stock = stock;
  }

  public double getValue() {
    return quantity * price;
  }

  @Override
  public String toString() {
    return String.format("Trade[type=%s, stock=%s, quantity=%d, price=%.2f]", type, stock, quantity, price);
  }

}

고객이 요청한 한 개 이상의 거래의 주문이다. 

public class Order {

  private String customer;
  private List<Trade> trades = new ArrayList<>();

  public void addTrade( Trade trade ) {
    trades.add( trade );
  }

  public String getCustomer() {
    return customer;
  }

  public void setCustomer( String customer ) {
    this.customer = customer;
  }

  public double getValue() {
    return trades.stream().mapToDouble( Trade::getValue ).sum();
  }

  @Override
  public String toString() {
    String strTrades = trades.stream().map(t -> "  " + t).collect(Collectors.joining("\n", "[\n", "\n]"));
    return String.format("Order[customer=%s, trades=%s]", customer, strTrades);
  }

}

도메인 모델은 직관적이다. 주문을 의미하는 객체를 만드는 것은 조금 귀찮은 작업이다. 

BigBank라는 고객이 요청한 두 거래를 포함하는 주문을 만들어보자.

Order order = new Order();
    order.setCustomer("BigBank");

    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);

    System.out.println("Plain:");
    System.out.println(order);
  }

  public void methodChaining() {
    Order order = forCustomer("BigBank")
        .buy(80).stock("IBM").on("NYSE").at(125.00)
        .sell(50).stock("GOOGLE").on("NASDAQ").at(375.00)
        .end();

    System.out.println("Method chaining:");
    System.out.println(order);

10.3.1 매서드 체인  

한 개의 메서드 호출 체인으로 거래 주문을 정의할 수 있다. 

   Order order = forCustomer("BigBank")
        .buy(80)
        .stock("IBM")
        .on("NYSE")
        .at(125.00)
        .sell(50)
        .stock("GOOGLE")
        .on("NASDAQ")
        .at(375.00)
        .end();

최상위 수준 빌더를 만들고 주문을 감싼 다음 한 개 이상의 거래를 주문에 추가할 수 있어야 한다. 

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); //고객의 주문을 만드는 정적 팩토리 메서드 
  }

  public TradeBuilder buy(int quantity) {
    return new TradeBuilder(this, Trade.Type.BUY, quantity); //주식을 사는 TradeBuilder 만들기 
  }

  public TradeBuilder sell(int quantity) {
    return new TradeBuilder(this, Trade.Type.SELL, quantity); //주식을 파는 TradeBuilder 만들기 
  }

  private MethodChainingOrderBuilder addTrade(Trade trade) {
    order.addTrade(trade); //주문에 주식 추가 
    return this; //유연하게 추가 주문을 만들어 추가할 수 있도록 주문 빌더 자체를 반환
  }
  
  public Order end(){
  	return order; //주문 만들기를 종료하고 반환
  }

}

 주문 빌더의 buy(), sell() 메서드는 다른 주문을 만들어 추가할 수 있도록 자신을 만들어 반환한다.

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);
    }

  }

빌더를 Stock 클래스의 인스턴스를 만드는 TradeBuilder의 공개 메서드를 이용해야 한다.

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);
    }

  }

StockBuilder는 주식의 시장을 지정하고, 거래에 주식을 추가하고, 최종 빌더를 반환하는 on() 메서드 한 개를 정의한다. 

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 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은 거래되는 주식의 단위 가격을 설정한 다음 원래 주문 빌더를 반환한다. MethodChainingOrderBuilder가 끝날 때까지 다른 거래를 플루언트 방식으로 추가할 수 있다. 

10.3.2 중첩된 함수 이용

중첩된 함수 DSL 패턴은 이름에서 알 수 있듯이 다른 함수 안에 함수를 이용해 도메인 모델을 만듭니다.

다음 예제는 이 접근 방법을 적용한 DSL의 구현 코드입니다.

Order order = order("BigBank", buy(80, 
                                   stock("IBM", on("NYSE")), at(125.00)),
                               sell(50, 
                                    stock("GOOGLE", on("NASDAQ")), at(375.00))
                   );

다음 예제는 사용자에게 API를 제공할 수 있음을 보여주는 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 buildTrade(quantity, stock, price, Trade.Type.BUY); // 주식 매수 거래 만들기
   }
   
   public static Trade sell(int quantity, Stock stock, double price) {
      return buildTrade(quantity, stock, price, Trade.Type.SELL); // 주식 매도 거래 만들기
   }
   
   private static Trade buildTrade(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;
   }
   
   private 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;
   }
}

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

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

이 형식의 DSL을 이용해 기존 주식 거래 예제의 거래를 정의할 수 있다. 

Order order = LambdaOrderBuilder.order( o -> {
      o.forCustomer( "BigBank" );
      o.buy( t -> {
        t.quantity(80);
        t.price(125.00);
        t.stock(s -> {
          s.symbol("IBM");
          s.market("NYSE");
        });
      });
      o.sell( t -> {
        t.quantity(50);
        t.price(375.00);
        t.stock(s -> {
          s.symbol("GOOGLE");
          s.market("NASDAQ");
        });
      });
    });

DSL을 만들려면 람다 표현식을 받아 실행해 도메인 모델을 만들어 내는 여러 빌더를 구현해야 한다. 

DSL 구현해서 했던 방식과 마찬가지로 이들 빌더는 메서드 체인 패턴을 이용해 만들려는 객체의 중간 상태를 유지한다.

메서드 체인 패턴에는 주문을 만드는 최상위 수준의 빌더를 가졌지만 이번에는 Consumer 객체를 빌더가 인수로 받음으로 DSL 사용자가 람다 표현식으로 인수를 구현할 수 있게 했다. 

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; // OrderBuilder의 Consumer를 실행해 만들어진 주문을 반환
  }

  public void forCustomer(String customer) {
    order.setCustomer(customer); // 주문을 요청한 고객 설정
  }

  public void buy(Consumer<TradeBuilder> consumer) {
    trade(consumer, Trade.Type.BUY); // 주식 매수 주문을 만들도록 TradeBuilder 소비
  }

  public void sell(Consumer<TradeBuilder> consumer) {
    trade(consumer, Trade.Type.SELL); // 주식 매도 주문을 만들도록 TradeBuilder 소비
  }

  private void trade(Consumer<TradeBuilder> consumer, Trade.Type type) {
    TradeBuilder builder = new TradeBuilder();
    builder.trade.setType(type);
    consumer.accept(builder); //TradeBuilder로 전달할 람다 표현식 실행
    order.addTrade(builder.trade);//TradeBuilder의 Consumer를 실행해 만든 거래를 주문에 추가 
  }

주문 빌더의 buy(),sell 매서드는 두 개의 Consumer<TradeBuilder> 람다 표현식을 받는다. 

이 람다 표현식을 실행하면 다음처럼 주식 매수, 주식 매도 거래가 만들어진다. 

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);
    }

  }

마지막으로 TradeBuilder는 세 번째 빌더의 Consumer 즉 거래된 주식을 받는다. 

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);
    }

  }

10.3.4 조합하기 

새로운 DSL을 개발해 주식 거래 주문을 정의할 수 있다. 

여러 DSL 패턴을 이용해 주식 거래 주문 만들기 

 

1) 여러 DSL 패턴을 이용해 주식 거래 주문 만들기 

 Order order =
        forCustomer("BigBank", //최상위 수준 주문의 속성을 지정하는 중첩함수
            buy(t -> t.quantity(80) //한 개의 주문을 만드는 람다 표현식
                .stock("IBM") // 거래 객체를 만드는 람다 표현식 바디의 매서드 체인 
                .on("NYSE")
                .at(125.00)),
            sell(t -> t.quantity(50)
                .stock("GOOGLE")
                .on("NASDAQ")
                .at(375.00)));

2) 여러 형식을 혼합한 DSL을 제공하는 주문 빌더 

public class MixedBuilder {

  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;
  }

  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);
    }

  }

}

핼퍼 클래스 TradeBuilder와 StockBuilder는 내부적으로 메서드 체인 패턴을 구현해 플루언트 API를 제공한다. 

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);
    }

    public StockBuilder stock(String symbol) {
      return new StockBuilder(builder, trade, symbol);
    }

  }
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);
    }

  }

10.4 실생활의 자바 8 DSL 

10.3절에서는 자바로 DSL을 개발하는데 사용할 유용한 패턴을 살펴봤고 각각의 장단점을 확인했습니다. 다음 그림에서는 지금 가지 배운 내용을 요약합니다.

세 가지 유명한 자바 라이브러리에서 사용되는 패턴을 살펴보았습니다. SQL 매핑 도구, 동작 주도 개발 프레임워크, 엔터프라이즈 통합 패턴 구현 도구를 구현하는 도구 세 가지 자바 라이브러리를 확인합니다.

10.4.1 jOOQ

JOOQ는 자바 내부적 DSL로 SQL을 구현하며 형식 안전을 제공합니다. 데이터베이스 스키마 역공학을 통해 소스코드 생성기를 사용하고, 자바 컴파일러가 SQL 구문의 형식을 확인할 수 있습니다. 이를 통해 데이터베이스 스키마 탐색이 가능해집니다.

// SQL 이용
SELECT * FROM BOOK
WHERE BOOK.PUBLISHED_IN = 2016
ORDER BY BOOK.TITLE

// JOOQ DSL 이용
create.selectFrom(BOOK)
      .where(BOOK.PUBLISHED_IN.eq(2106))
      .orderBy(BOOK.TITLE)

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

Class.forName("org.h2.Driver");
try (Connection c = 
        getConnection("jdbc:h2:~/sql-goodies-with-mapping", "sa", "")) {
     DSL.using(c)
        .select(BOOK.AUTHOR, BOOK.TITLE)
        .where(BOOK.PUBLISHED_IN.eq(2016))
        .orderBy(BOOK.TITLE)
      .fetch()
      .stream()
      .collect(groupingBy(
         r -> r.getValue(BOOK.AUTHOR),
         LinkedHashMap::new,
         mapping(r -> r.getValue(BOOK.TITLE), toList())))
            forEach((author, titles) ->
         System.out.println(author + " is author if " + titles));
     }

JOOQ DSL은 메서드 체인 패턴을 사용하여 잘 만들어진 SQL 질의 문법을 구현합니다. 이 패턴은 선택적 파라미터 허용과 미리 정해진 순서로 특정 메서드 호출을 가능하게 함으로써 필수적입니다.

10.4.2 큐컴버

BDD는 테스트 주도 개발의 확장으로, 비즈니스 시나리오를 구조적으로 서술하며 도메인 전문가와 프로그래머 간의 간격을 줄입니다. 큐컴버는 BDD 프레임워크로 평문 영어로 시나리오를 구현할 수 있게 돕습니다.

큐컴버는 BDD 프레임워크 중 하나로, 명령문을 전체 조건 정의(Given), 시험하려는 도메인 객체의 실질 호출(When), 테스트 케이스의 결과를 확인하는 어설션(Then)으로 분류하여 테스트 케이스를 작성합니다.

큐컴버의 DSL은 아주 간단하지만 외부적 DSL과 내부적 DSL이 어떻게 효과적으로 합쳐질 수 있으며 람다와 함께 가독성 있는 함축된 코드를 구현할 수 있는지를 잘 보여줍니다.

10.4.3 스프링 통합

스프링 통합은 엔터프라이즈 통합 패턴을 지원하며, 복잡한 통합 솔루션 구현을 단순화하고 비동기 및 메시지 주도 아키텍처를 적용하는 데 도움을 줍니다. 이는 경량 원격, 메시징, 스케쥴링을 지원하며, 유창한 DSL을 통해 설정을 제공합니다. 스프링 통합은 메시지 기반 애플리케이션에 필요한 공통 패턴을 구현하고, 가독성을 높이기 위해 엔드포인트를 동사로 구현하며, 메시지 흐름을 통한 통합 과정을 구성합니다. 

 

반응형
LIST