모던 자바 인 액션 10장

도메인 전용 언어

  • 특정 비즈니스 도메인의 문제를 해결하기 위해 만든 언어
    • 회계 전용 소프트웨어
      • 입출금 내역
      • 계좌
      • 대출 등등등
  • 전문가가 아니라 비전문가가 봐도 이해할 수 있어야 함
    • 문법 자체가 어렵지 않고 쉬운 직관적인 구조로 설계되어야 함
    • 가독성이 좋아야…
  • 장점
    • 간결함
      • 비즈니스 로직을 간편하게 캡슐화하므로 반복을 피할 수 있음
    • 가독성
      • 앞서 말했듯 비 전문가가 봐도 이해할 수 있는 수준
    • 유지보수
      • 잘~ 설계된 DSL로 ‘구현한 코드(구현도 엉망이라면 도루묵)’ 는 유지보수가 쉽다
    • 높은수준의 추상화
      • 도메인과 동일한 레벨의 추상화 수준에서 동작하므로, 도메인의 직접적으로 관련되지 않은 세부 사항을 숨김
    • 집중
      • 개발자가 특정 코드에 집중할 수 있음 –> 생산성 증가
    • 관심사 분리
      • 어플리케이션의 코드와 분리되어 관리가 되므로 좋음 –> 유지보수 && 가독성 증가
  • 단점
    • DSL설계의 어려움
      • ‘잘~ 설계’ 하기가 어려움..
    • 개발 비용
      • 있는거 갖다 쓰는게 아니라 DSL을 추가하는 프로젝트의 경우 비용이 커짐
    • 추가 우회 계층
      • DSL은 추가적인 계층으로 도메인 모델을 감싸며, 이 떄 계층을 최대한 작게 만들어 성능 문제를 회피한다
    • 새로 배워야 하는 언어
      • 써야 할 언어가 하나 더 늘어나는 개념이므로..
    • 호스팅 언어 한계
      • 장황하고 엄격한 문법을 가진 언어를 바탕으로는, 사용자 친화적 DSL을 만들기 힘들다

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

  • 내부 DSL (자바로 구현한 DSL)
    • 기존 자바를 이용하여 만드므로, 러닝커브가 없다
    • 나머지 코드와 함께 DSL을 같이 컴파일 할 수 있다
    • IDE의 지원을 많이 받을 수 있다. 기존 자바로 구현하였으므로, IDE에서 지원해 주는 기능을 100% 사용 가능
    • DSL을 만들 떄, 자바를 이용하여 추가하면 쉽게 DSL을 합칠 수 있다
  • 다중 DSL (자바 호환 언어 DSL)
    • 다중 DSL을 만드려면 선구자가 있어야 한다.
    • 프로젝트 언어의 개수가 2개 이상이므로 빌드 과정을 개선해야 한다
    • 100% 호환의 보장성이 없다
  • 외부 DSL (완벽히 독립된 언어로 구현하는DSL)
    • 프로그래밍 언어를 하나 만드는 수준 –> 이쯤이면 어플리케이션 개발자가 아니지 않나요?
    • 잘만 만들면 무한한 유연성을 가질 수 있다 –> …..음..

최신 자바 API의 작은 DSL

나이순으로 사람을 정렬하는 코드를 구현해보자

Collections.sort(persons, new Comparator<Person>() {
  public int compare() ...
})

…더 이상 자세한 설명은 생략한다

그리고 이제 수없이 반복했던, 람다 표현식으로 이를 표현재 보자

Collections.sort(persons, (p1, p2) -> p1.getAge() - p2.getAge());

// 조금 더 개선하면
Collections.sort(persons, comparing(p -> p.getAge()));

//메소드 참조까지 써보면
Collections.sort(persons, comparing(Person::getAge));

//나이 --> 이름 순으로
Collections.sort(persons, comparing(Person::getAge)
                          .thenComparing(Person::getName));

// List 인터페이스에 추가된 새 sort메소드까지 이용해 주면
persons.sort(comparing(Person::getAge)
                          .thenComparing(Person::getName));

뭐 그렇다고 한다….이게 컬렉션 정렬 도메인의 최소 DSL이라고 한다

스트림 & Collectors

  • 스트림과 Collectors는 DSL이라고 한다.
    • 스트림 (예제 생략)
      • 반복,필터,정렬,변환 등등등…
    • Collectors (예제 생략)
      • 데이터 수집, 그룹화….

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

간단하게 주식을 사고 팔고 하는 예제를 통해, DSL을 만드는 패턴을 몇가지 살펴볼 것이다

  • 레거시 자바의 getter,setter를 이용한 기법
  • 메소드 체인
  • 메소드 중첩
  • 람다 표현식
  • 장단점 짬뽕

이 순서로 살펴보겠다

우선 예제 시작에 앞서 도메인 모델의 코드를 살펴본다.

public class Stock { //주식을 모델링
  private String symbol;
  private String market;
}

public class Trade { // 거래
  public enum Type {BUY,SELL}
  private Type type;

  private Stock stock;
  private int quantity;
  private double price;
}

public class Order {  //주문
  private String customer;
  private List<Trade> trades = new ArrayList<>();
}

레거시 자바의 getter,setter를 이용한 기법

전형적인, 자바를 처음 배우는 사람이 짜는 코드.

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

//trade2.....
order.addTrade(trade2);
//etc...
  • 장점
    • 있……..나?
    • 굳이 생각하자면… 쉽다. 개발자가 보기엔..
      • 근데 저것도 중간에 코드가 하나 빠진다던가, 혹은 중간에 상관없는 다른 구문이 들어간다던가..하면서 마냥 쉽지만은 않을 수 있다.
  • 단점(은 많다)
    • 장황하다
    • 비개발자가 보기엔 좀 어렵다
    • 각 구문의 파편화가 심하다

메소드 체인

DSL에서 가장 흔한 방식

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

(딴건 모르겠고 이시국에 IBM을 사고 구글을 판다는거 보면 주알못인건 확실하다.)

딱 봐도 코드가 개선되었다. 그냥 개발에 1도 모르는 사람이 영어로 읽어도 이해할 수 있는 정도의 레벨이다.

for customer “BigBank”, buy 80 IBM on NYSE at 125.00. AND sell 50 GOOGLE on NASDAQ at 375.00

문법은 하나도 맞지 않으나(사실 모름)….뭐…전달은 된다

하지만… 그냥 남이 만들어놓은 메소드 체인 빌더를 갖다 쓰는 나야 쉽지만, 내가 저 메소드 체인 빌더를 만든다고 생각해보자.

public class MethodChainingOrderBuilder {
  public final Order order = new Order();

  //private 생성자(코드생략)

  public static MethodChainingOrderBuilder forCustomer(String customer) {
    return new MethodChainingOrderBuilder(customer);
  }

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

  public MethodChainingOrderBuilder addTrade(Trade trade) {
    order.addTrade(trade);
    return this;
  }
  public Order end() {
    return order;
  }

}


public class TradeBuilder {
  private final MethodChainingOrderBuilder builder;
  public final Trade trade = new Trade();

  private MethodChainingOrderBuilder(MethodChainingOrderBuilder builder, Trade.Type type, int quantity) {
    // implement...
  }

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

public class StockBuilder {
  private final MethodChainingOrderBuilder builder;
  private final Trade trade;
  private final Stock stock = new Stock();

  public StockBuilder stock(MethodChainingOrderBuilder builder, Trade.Type type, String symbol) {
    // implement...
  }
  
}

public class TradeBuilderWithStock {
  private final MethodChainingOrderBuilder builder;
  private final Trade trade;

  //이하코드 생략...
}
  • 장점
    • 사용할땐 좋다
  • 단점
    • 빌더 구현이 토나온다. 체이닝 되는 메소드 하나마다 클래스를 하나 구현해야 함…..
      • ‘체이닝’ 개념이기에, 상위레벨과 하위레벨을 잇는 메소드들을 각 클래스마다 구현을 할 수밖에 없음…

중첩된 함수 이용

체이닝이 아닌 call depth를 깊게 하여 로직을 구현하는 방식

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

구현코드

  • 장점
    • 메소드의 중첩 방식을 사용함으로써, 객체의 계층구조(주문:거래가 1:N 관계, 거래:주식이 1:1관계라는 그런 도메인 정보)들이 한 눈에 보인다는 것이 장점이다
    • 마찬가지로 좀 길긴 하지만, 적어도 메소드 체이닝처럼 빌더 구현이 길지는 않다.
  • 단점
    • 괄호 중첩이 너무 많아 가독성이 떨어진다.
    • argument가 많으면 많을수록 비즈니스 로직이 장황해진다….
    • 파라미터 목록을 static method에 넘겨줘야 한다는 제약도 있다.

람다 표현식

모던 자바의 알파이자 오메가인 람다 표현식으로 주식거래 주문을 만들어 본다..

솔직히 개인적인 생각으로..가독성이 메소드 체인보다는 좀 못한 것 같다.

다만 충분히 플루언트 방식의 스타일이고…. 들여쓰기에 따라 depth가 나눠져서 도메인 계층구조또한 잘 보이긴 한다.합격(..?)

  • 장점
    • 메소드체인, 메소드중첩 방식의 장점을 잘 섞었다.
  • 단점
    • 사용하는 것이 ‘람다’이지 않는가. 자바8 이상에서만 사용할 수 있는 기법이다.
    • (개인적 생각) 개발 비전문가가 보기엔 메소드 체이닝방식만 못하다.
      • 플루언트하다고 하지만…. 솔직히…?

짬뽕해서 조합하기

총 3개의 기법을 봤는데, 각각의 기법마다 다 장단점이 있다.

적절히 짬뽕해서 섞어써보면…. 뭐 이런 식으로도 쓸 수 있지 않을까?

빌더를 구현하면 다음과 같이 구현할 수가 있다

  • 장점
    • 오…. 한눈에 봐도 읽기가 쉽다
  • 단점
    • 뭐… 딱히 없어보이지만, 얘도 람다의 단점인 자바8의 dependency가 걸리는게 문제 아닐까

DSL에 메소드참조 사용하기

세금 계산과 관련한 최종 값을 계산하는 기능을 구현한다고 하자.

세금 클래스는 이런 식으로 정의될 수 있을 것이다

지방세,부가세….많이 본 세금들

그리고 주문에 세금을 적용하는 메소드를 만든다고 하면, 뭐 이런 식으로 만들어 질 수 있을 것이다.

자, 그럼 이제 이 calculate라는 세금계산 메소드를 사용하는 예제코드를 봐 보자

흠……..ㅠㅠ

argument 에 boolean타입이 세개나 들어가 있다. calculate메소드의 내용을 보지 않고서는 절대 알 수 없는 세금타입이다.

고럼 요런 식으로 세금을 계산하는 코드를 작성해 보자!

깔.끔
호오

이렇게 TaxCalculator를 구현하면 어떤 종류의 세금이 원 가격에 더 붙는지 파악하기가 쉽다.

하지만 몇가지 단점이 보인다

  • 의존성.
    • Tax에 세금의 종류가 생길때마다 TaxCalculator도 코드를 건드려야 하는 의존성이 발생한다
  • 코드의 중복
    • 비슷한 내용의 종류의 메소드가 3개가 중첩되어있다.

메소드(2급시민) 을 일종의 인스턴스(1급시민)으로 올릴 수 있는 모던 자바의 특성을 살려, 메소드 자체를 인스턴스화 해 보자.

그러면 의존성과, 중복성을 모두 다 해결할 수 있을 것이다.

DoubleUnaryOperator

DoubleUnaryOperator라는 함수형 인터페이스를 이용하여, 두개의 taxFunction을 합칠 수 있다.

그럼 구현된 DSL을 사용할 떄, 이런 식으로 사용할 수 있다.

편-안 하다.

실생활의 자바8 DSL

마치며

  • DSL의 주요 기능은 개발자 <–> 도메인 전문가 사이의 간격을 좁히는 것이다. 구현할 순 없어도, 도메인 전문가가 ‘읽을 수 있는’ 수준은 되어야 한다.
  • DSL은 내부적 DSL과 외부적 DSL로 분류할 수 있다.
    • 내부적 DSL : 개발공수가 적다. 단, 어플리케이션 코드의 언어적 특성(단점까지도)제약을 받는다
    • 외부적 DSL : 개발공수가 크다. 단, 높은 유연성을 제공할 수 있다.
  • 스칼라,그루비 등 JVM기반의 언어로 DSL을 만들 수 있다
    • 적재적소에 잘 적용면 내&외부적 DSL의 장점만을 취할 수 있을 것이지만, 그 반대의 경우는 단점만을 취할 수 있겠지…
  • 자바는 특유의 ‘코드가 길어지는’ 특성때문에 내부적DSL로는 적절하지 않은 언어였다. 하지만 8로 올라가면서 많이 개선된 편.
  • 자바로 DSL을 구현할 때, 보통 메소드체인,중첩함수,함수시퀀싱 이렇게 3가지 패턴이 사용된다. 각자의 장단점을 잘 파악하여 사용하자
    • 섞어서 써도 되고요….
  • jOOQ,BDD 프레임워크 큐컴버, 스프링 통합 등의 프레임워크&라이브러리를 DSL을 통해 이용할 수 있다.

Leave a Comment