도메인 전용 언어(DSL)은 내부적, 외부적 DSL가 존재한다.
스트림 API의 특성인 메서드 체인을 보통 자바의 루프 복잡합 제어와 비교해 유창함을 의미하는 플루언트 스타일(fluent style)이라고 부른다.
도메인 전용 언어
DSL이란 특정 비즈니스 도메인을 인터페이스로 만든 API라고 생각할 수 있다.
DSL 두 가지 필요성
의사 소통의 왕 : 우리의 코드의 의도가 명확히 전달되어야 하며 프로그래머가 아닌 사람도 이해할 수 있어야 한다. 이런 방식으로 코드가 비즈니스 요구사항에 부합하는지 확인 할 수 있다.
한 번 코드를 구현하지만 여러 번 읽는다. : 가독성은 유지보수의 핵심이다. 즉 항상 우리의 동료가 쉽게 이해할 수 있도록 코드를 구현해야 한다.
DSL의 장점과 단점
장점
간결함 : API는 비즈니스 로직을 간편하게 캡슐화하므로 반복을 피할 수 있고 코드를 간결하게 만들 수 있다.
가독성 : 도메인 영역의 용어를 사용하므로 비 도메인 전문가도 코드를 쉽게 이해할 수 있다. 결과적으로 다양한 조직 구성원 간에 코드와 도메인 영역이 공유 될 수 있다.
유지보수 : 잘 설계된 DSL로 구현한 코드는 쉽게 유지보수하고 바꿀 수 있다. 유지보수는 비즈니스 관련 코드 즉 가장 비번히 바뀌는 애플리케이션 부분에 특히 중요하다.
높은 수준의 추상화 : DSL은 도메인과 같은 추상화 수준에서 동작하므로 도메인의 문제와 직접적으로 관련되지 않은 세부 사항을 숨긴다.
집중 : 비즈니스 도메인의 규칙을 표현할 목적으로 설계된 언어이므로 프로그래머가 특정 코드에 집중할 수 잇다. 결과적으로 생산성이 좋아진다.
관심사분리(separation of concerns) : 지정된 언어로 비즈니스 로직을 표현함으로 애프리케이션의 인프라구조와 관련된 문제와 독립적으로 비즈니스 관련된 코드에서 집중하기가 용이하다. 결과적으로 유지보수가 쉬운 코드를 구현한다.
단점
DSL 설계의 어려움 : 간결하게 제한적인 언어에 도메인 지식을 담는 것이 쉬운 작업은 아니다.
개발 비용 : 코드에 DSL을 추가하는 작업은 초기 프로젝트에 많은 비용과 시간이 소모되는 작업이다. 또한 DSL 유지보수와 변경은 프로젝트에 부담을 주는 요소다.
추가 우회 계층 : DSL은 추가적인 계층으로 도메인 모델을 감싸며 이 때 계층을 최대한 작게 만들어 성능 문제를 회피한다.
새로 배워야 하는 언어 : 요즘에는 한 프로젝트에도 여러가지 언어를 사용하는 추세다. 하지만 DSL을 프로젝트에 추가하면서 팀이 배워야 하는 언어가 한 개 더 늘어난다는 부담이 있다. 여러 비즈니스 도메인을 다루는 개별 DSL을 사용하는 상황이라면 이들을 유기적으로 동작하도록 합치는 일은 쉬운 일이 아니다. 개별 DSL이 독립적으로 진화할 수 있기 때문이다.
호스팅 언어 한계 : 일부 자바 같은 범용 프로그래밍 언어는 장황하고 엄격한 문법을 가졌다. 이런 언어로는 사용자 친화적 DSL을 만들기가 힘들다. 사실 장황한 프로그래밍 언어를 기반으로 만든 DSL은 성가신 문법의 제약을 받고 읽기가 어려워진다. 자바 8의 람다 포현식은 이문제를 해결할 강력한 새 도구다.
JVM에서 이용할 수 있는 다른 DSL 해결책
내부 DSL
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 -> Systemo.out.println(s));
다음처럼 메서드 참조로 더 간단하게 만들 수 있다.
Numbers.forEach(System.out::println);
자바로 DSL 구현함으로 다음과 같은 장점을 얻을 수 있다.
기존 자바 언어를 이용하면 외부 DSL에 비해 새로운 패턴과 기술을 배워 DSL을 구현하는 노력이 현저하게 줄어든다.
순수 자바로 DSL을 구현하면 나머지 코드와 함께 DSL을 컴파일할 수 있다. 따라서 다른 언어의 컴파일러를 이용하거나 외부 DSL을 만드는 도구를 사용할 필요가 없으므로 추가로 비용이 들지 않는다.
여러분의 개발 팀이 새로운 언어를 배우거나 또는 익숙하지 않고 복잡한 외부 도구를 배울 필요가 없다.
DSL 사용자는 기존의 자바 IDE를 이용해 자동 완성, 자동 리팩터링 같은 기능을 그대로 즐길 수 있다. 최신 IDE는 다른 유명한 JVM 언어도 지원하지만 자바 만큼의 기능을 지원하진 못한다.
한 개의 언어로 한 개의 도메인 또는 여러 도메인을 대응하지 못해 추가로 DSL을 개발해야 하는 상황에서 자바를 이용한다면 추가 DSL을 쉽게 합칠 수 있다.
다중 DSL
JVM 기반 프로그래밍 언어를 이용함으로 DSL 합침 문제를 해결 하는 것을 다중 DSL이라고 한다.
하지만 이 접근 방법은 다음과 같은 불편함도 초래한다.
새로운 프로그래밍 언어를 배우거나 또는 팀의 누군가가 이미 해당 기술을 가지고 있어야 한다. 멋진 DSL을 만들려면 이미 기존 언어의 고급 기능을 사용할 수 있는 충분한 지식이 필요하기 때문이다.
두 개 이상의 언어가 혼재하므로 여러 컴파일러로 소스를 빌드하도록 빌드 과정을 개선해야 한다.
마지막으로 JVM에서 실행되는 거의 모든 언어가 자바와 백 퍼센트 호환을 주장하고 있지만 자바와 호환성이 완벽하지 않을 때가 많다. 이런 호환성 때문에 성능이 손실될 때도 있다. 예를 들어 스칼라와 자바 컬렉션은 서로 호환되지 않으므로 상호 컬렉션을 전달하려면 기존 컬렉션을 대상 언어의 API에 맞게 변환해야 한다.
외부 DSL
외부 DSL을 개발하는 가장 큰 장점은 외부 DSL이 제공하는 무한한 유연성이다.
인프라구조 코드와 외부 DSL로 구현한 비즈니스 코드를 명확하게 분리한다는 것도 장점이다.
하지만 이 분리로 인해 DSL과 호스트 언어 사이에 인공 계층이 생기므로 이는 양남의 검이다.
최신 자바 API의 작은 DSL
사람(Persons)을 가르키는 객체 목록을 가지고 잇는데 사람의 나이를 기준으로 객체를 정렬한다고하자.
Collection.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());
정적으로 Comparaotr.comparing 메서드를 임포트해 다음처럼 구현할 수 있다.
Collections.sort(persons, comparing(p -> p.getAge()));
메서드 참조로 개선도 가능하다.
Collections.sort(persons, comparing(Person::getAge));
reverse를 사용해 역순으로 정렬도 가능하다.
Collections.sort(persons, comparing(Person::getAge).reverse();
스트림 API는 컬렉션을 조작하는 DSL
예제 10-1 반복 형식으로 예제 로그 파일에서 여러 행을 읽는 코드
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("ERROC")) {
errors.add(line);
errorCount++;
}
line = bufferedRead.readLine();
}
문제가 분리되지 않아 가독성과 유지보수성 모두 저하되었다.
Stream 인터페이스를 이용해 함수형으로 코드를 구현하면 더 쉽고 간결하게 코드를 구현할 수 있다.
List<String> errors = Files.readAllLines(Paths.get(fileName)) // 파일을 열어서 문자열 스트림을 만듬
.filter(line -> line.startsWith("ERROR")) // "ERROR"로 시작하는 행을 필터링
.limit(40) // 결과를 첫 40행으로 제한
.collect(toList()); // 결과 문자열을 리스트로 수집
데이터를 수집하는 DSL인 Collectors
유연한 그룹화 컬렉터 빌더
import static java.util.stream.Collectors.groupingBy
public class GrouppingBuilder<T, D, K> {
private final Collector<? super T, ?, Map<K, D>> collector;
private GroupingBuilder(Collector<? supter T, ?, Map<k, D>> collector) {
this.collector = collector;
}
public Collector<? super t, ?, Map<K, D>> get() {
return collector;
}
public <J> GroupingBuilder<T, Map<k, D>, J>
after(Function<? super T, ? extends J> classifier) {
return new GroupingBuilder<>(groupingBy(classifier, collector));
}
public static <T, D, K> GroupingBuilder<T, List<T>, K>
groupOn(Function<? super T, ? extends k> classifier) {
return new GroupingBuilder<>(groupingBy(classifier));
}
}
플루언트 형식 빌더에 어떤 문제가 있는지 확인해 보자.
Collector<? Super Car, ?, Map<Brand, Map<Color, List<Car>>>>
carGroupingCollector =
groupOn(Car::getColor).after(Car::getBrand).get()
유틸리티 사용 코드가 직관적이지 않다.
자바로 DSL을 만드는 패턴과 기법
도메인 객체의 API를 직접 이용해 주식 거래 주문을 만든다.
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);
메서드 체인
메서드 체인으로 주식 거래 주문 만들기
Order order = forCustomer( “BigBank” )
.buy( 80 )
.stock( “IBM”)
.on( “NYSE” )
.at( 125.00 )
.sell( 50 )
.stock( “GOOGLE’ )
.on( “NASDAQ” )
.at( 375.00 )
.end();
메서든 체인 DSL을 제공하는 주문 빌더
public class MethodChainingOrderBuilder {
public final Order order = new Order(); // 빌더로 감싼 주문
private MethodChainingOrderBuilders(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); // 주식을 사는 TrandeBuilder 만들기
}
public TradeBuilder sell(int quantity) {
return new TradeBuilder(this, Trade.Type.SELL, quantity); // 주식을 파는 TradeBuilder 만들기
}
public MethodChainingOrderBuilder addTrade(Trade trade) {
order.addTrade(trade); // 주문에 주식 추가
return this; // 유연하게 추가 주문을 만들어 추가할 수 있도록 주문 빌더 자체를 반환
}
public Order end() {
return order; // 주문 만들기 종료하고 반환
}
}
주문 빌더의 buy(), sell() 메서드는 다른 주문을 만들어 추가할 수 있도록 자신을 만든다.
public class TradeBuilder {
private final MethodChainingOrderBuilder builder;
public final Trade trade = new Trade();
private TradeBuilder(MethodChainingOrderBuilder builder,
Trade.Type type, int quantity) {
this.builder = builder;
this.setType(type);
trade.setQuantity(quantity);
}
public StockBuilder stock(String symbol) {
return new StockBuilder(builder, trade, symbol);
}
}
Stock 클래스의 인스턴스를 만드는 TradeBuilder의 공개 메서드를 이용해야 한다.
public 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 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가 끝날 때까지 다른 거래를 플루언트 방식으로 추가할 수 있다.
빌더를 구현해야 한다는 것이 메서드 체인의 단점이다. 상위 수준의 빌더를 하위 수준의 빌더와 연결할 접착 많은 접착 코드가 필요하다. 도메인의 객체의 중첩 구조와 일치하게 들여쓰기를 강제하는 방법이 없다는 것도 단점이다.
중첩된 함수 이용
다른 함수 안에 함수를 이용해 도메인 모델을 만든다.
중첩된 함수로 주식 거래 만들기
Order order = order("BigBank",
buy(80,
stock("IBM", on("NYSE")), at(125.00))),
sell(50,
stock("GOOGLE", on("NASDAQ")), at(375.00))
);
중첩된 함수 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;
}
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;
}
}
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;
}
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;
}
}
이 방식에도 문제점이 있다. 결과 DSL에 더 많은 괄호를 사용해야 한다는 사실과 인수 목록을 정적 메서드에 넘겨줘야 한다는 제약도 있다. 인수의 의미가 이름이 아니라 위치에 의해 정의 되었다. NestedFunctionOrderBuilder의 at(), on() 메서드에서 했던 것처럼 인수의 역할을 확실하게 만드는 여러 더미 메서드를 이용해 마지막 문제를 조금은 해결할 수 있다.
람다 표현식을 이용한 함수 시퀀싱
함수 시퀀싱으로 주식 거래 주문 만들기
Order order = 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을 만들려면 람다 표현식을 받아 실행해 도메인 모데을 만들어 내는 여러 빌더를 구현해야 한다.
메서드 체인 패턴에는 주문을 만드는 최상위 수준의 빌더를 가졌지만 이번에는 Consumer 객체를 빌더가 인수로 받음으로 DSL 사용자가 람다 표현식으로 인수를 구현할 수 있게 했다.
함수 시퀀싱 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 forCustoomer(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<TradeBulder> 람다 표현식을 받는다.
이 람다 표현식을 실행하면 다음처럼 주식 매수, 주식 매도 거래가 만들어진다.
public 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 class StockBuilder {
private Stock stock = new Stock();
public void symbol(String symbol) {
stock.setSymbol(symbol);
}
public void market(String market) {
stock.setMarket(market);
}
}
이 패턴은 두 가지 장점이 있다. 메서드 체인 패턴처럼 플루언트 방식으로 거래 주문을 정의할 수 있다. 또한 중첩 함수 형식처럼 다양한 람다 표현식의 중첩 수준과 비슷하게 도메인 객체의 계층 구조를 유지한다.
많은 설정 코드가 필요하며 DSL 자체가 자바 8람다 표현식 문법에 의한 잡음의 영향을 받는다는 것이 이 패턴의 단점이다.
조합하기
여러 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(125.00)) );
여러 형식을 혼합한 DSL을 제공하는 주문 빌더
public class MixedBuilder {
public static Order forCustomer(String customer, TradeBulder... 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 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);
}
}
public 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;
}
}
DSL에 메서드 참조 사용하기
주문 총 합에 0개 이상의 세금을 추가해 최종값을 계산하는 기능을 추가한다.
주문의 총 합에 적용할 세금
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 (useSurchange) value = Tax.surchange(value);
return value;
}
지역 세금과 추가 요금을 적용하고 일반 세금은 뺀 주문의 최종값을 계산할 수 있다.
double value = calculate(order, true, false, true);
불리언 변수의 올바른 순서를 기억하기도 어려우며 어떤 세금이 적용되었는지도 파악하기 어렵다. 불리언 플래그를 설정하는 최소 DSL을 제공하는 TaxCalculator를 이용하는 것이 더 좋다.
적용할 세금을 유창하게 정의하는 세금 계산기
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 withTaxSurchage() {
useSurcharge = true;
return this;
}
public double calculate(Order order) {
return calculate(order, useRegional, useGeneral, useSurcharge);
}
}
다음 코드는 TaxCalculator 지역 세금과 추가 요금은 주문에 추가하고 싶다는 점을 명확하게 보여준다.
double value = new TaxCalculator().withTaxReginonal()
.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 클래스를 통해 모든 세금 설정이 적용된다. 이 함수의 시작값은 확인 함수다. 처음 시점에서는 세금이 적용되지 않았으므로 최종값은 총합과 같다. With() 메서드로 새 세금이 추가되면 현재 세금 계산 함수에 이 세금이 조합되는 방식으로 한 함수에 모든 추가된 세금이 적용된다. 마지막으로 주문을 calculate() 메서드에 전달하면 다양한 세금 설정의 결과로 만들어진 세금 계산 함수가 주문의 합계에 적용된다. 리팩터링한 TaxCalculator는 다음과 같다.
double value = new TaxCalculator().with(Tax::regional)
.with(Tax::surcharge)
.calculate(order);
실생활의 자바 8 DSL
패턴 이름 | 장점 | 단점 |
메서드 체인 | 메서드 이름이 키워드 인수 역할을 한다. | 구현이 장황하다. |
선택형 파라미터와 잘 동작한다. | 빌드를 연결하는 접착 코드가 필요하다. | |
DSL 사용자가 정해진 순서로 메서드를 호출 하도록 강제할 수 있다. | 들여쓰기 규칙으로만 도메인 객체 계증을 정의한다. | |
정적 메서드를 최소화하거나 없앨 수 있다. | ||
문법적 잡음을 최소화한다. | ||
중첩 함수 | 구현의 장황함을 줄일 수 있다. | 정적 메서드의 사용ㅇ 빈번하다. |
함수 중첩으로 도메인 객체 계증을 반영한다. | 이름이 아닌 위치로 인수를 정의한다. | |
선택형 파라미터를 처리할 메서드 오버로딩이 필요하다. | ||
람다를 이용한 함수 시퀀싱 | 선택형 파라미터와 잘 동작한다. | 구현이 장황하다. |
정적 메서드를 최소화하거나 없앨 수 있다. | 람다 표현식으로 인한 문법적 잡음이 DSL에 존재한다. | |
람다 충점으로 도메인 객체 계증을 반영한다. | ||
빌더의 접착 코드가 없다. |
세 가지의 유명한 자바 라이브러리에 지금까지 살펴본 패턴이 얼마나 사용되고 있는지 살펴보면서 배운 내용을 정리하자. SQL 매핑 도구, 동작 주도(behavior-driven)개발 프레임워크, 엔터프라이즈 통합 패턴(Enterpricse Integration Patterns)을 구현하는 도구 세가지 자바 라이브러리를 확인한다.
jOOQ
SQL을 구현하는 내부적 DSL로 자바에 직접 내장된 형식 안전 언어다.
SELECT * FROM BOOK
WHERE BOOK.PUBLISHED_IN = 2016
ORDER BY BOOK.TITLE
다음처럼 구현할 수 있다.
create.selectFrom(Book)
.where(BOOK.PUBLISHED_IN.eq(2016))
.orderby(BOOK.TITLE)
스트림 API를 조합해 사용할 수 있다는 것이 jOOQ DSL의 장점이다.
jOOQ DSL을 이용해 데이터베이스에서 책 선택
Class.forName("org.h2.Driver");
try (Connection c = getConnection("jdbc:h2:~/sql-goodies-with-mapping", "sa", "") { // SQL 데이터베이스 연결 만들기
DSL.using(c)
.select(BOOK.AUTHOR, BOOK.TITLE) // 만들어진 데이터베이스 연결을 이용해 jOOQ SQL 문 시작
.where(BOOK.PUBLISHED_IN.eq(2016))
.orderBy(BOOOK.TITLE)
.fetch() // jOOQ DSL로 SQL문 정의
.stream() // 데이터베이스에서 데이터 가져오기 jOOQ문은 여기서 종료
.collect(groupingBy( // 스트림 API로 데이터베이스에서 가져온 데이터 처리 시작
r -> r.getValue(BOOK.AUTHOR),
LinkedHashMap::new,
mapping(r -> r.getValue(BOOK.TITLE), toList())))
.forEach((author, titles) -> // 저자의 이름 목록과 각 저자가 집필한 책들을 출력
System.out.println(author + " is author of " + titles));
};
큐컴버
동작 주도 개발(Behavior-driven developments)(BDD)은 테스트 주도 개발의 확장으로 다양한 비즈니스 시나리오를 구조적으로 서술하는 간단한 도메인 전용 스크립팅 언어를 사용한다.
큐컴버는 세 가지로 구분되는 개념을 사용한다. 전제 조건 정의(Given), 시험하려는 도메인 객체의 실질 호출(When), 테스트 케이스의 결과를 확인하는 어선셜(assertion)(Then)
큐펌버 어노테이션을 이용해 테스트 시나리오 구현
public class BuyStocksSteps {
private Map<String, Integer> stockUnitPrices = new HashMap<>();
private Order order = new Order();
@Given("^the price of a \"(.*?)\" stock is (\\d+)\\$$"); // 시나리오의 전체 조건인 주식 단가 정의
public void setUnitPrice(String stockName, int unitPrice) {
stockUnitValues.put(stockName, unitPrice); // 주식 단가 저장
}
@When("^I buy (\\d+) \"(.*?)\"$") // 테스트 대상인 도메인 모델에 행할 액션 정의
public void buyStocks(int quantity, String stockName) {
Trade trade = new Trade(); // 적절하게 도메인 모델 도출
trade.setType(trade.Type.BUY);
Stock stock = new Stock();
stock.setSymbol(stockName);
trade.setStock(stock);
trade.setPrice(stockUnitPrices.get(stockName));
trade.setQuantity(quantity);
order.addTrade(trade);
}
@Then("the order value should be(\\d+)\\$$")
public void checkOrderValue(int experctedValue) { // 예상되는 시나리오 결과 정의
assertEquals(expectedValue, order.getValue()); // 테스트 어설션 확인
}
}
형식 표기법을 이용해 테스트 시나리오를 다시 구현해보자.
public class BuyStocksSteps implements cucumber.api.java8.En {
private Map<String, Integer> stockUnitPrices = new HashMap<>();
private Order order =new Order();
public BuyStockSteps() {
Given("^the price of a \"(.*?\" stock is (\\d+\\$$", (String stockName, int unitPrice) -> {
stockUnitValues.put(stockName, unitPrice);
});
// ... When과 Then 람다는 편의상 생략
}
}
코드가 단순해진다는 장점이 있다.
스프링 통합
스프링 통합(Spring Integration)은 유명한 엔터프라이즈 통합 패턴을 지원할 수 있도록 의존성 주입에 기반한 스프링 프로그래밍 모델을 확장한다.
스프링 통합은 채널, 엔트포인트(endpoints), 폴러(pollers), 채널 인터셉터(channel interceptors)등 메시지 기반의 애플리케이션에 필요한 가장 공통 패턴을 모두 구현한다.
스프링 통합 DSL을 이용해 스프링 통합 흐름 설정하기
@Configuration
@EnableIntegeration
public class MyConfiguration {
@Bean
public MessageSource<?> integerMessageSource() {
MethodInvokingMessageSource source = new MethodInvokingMessageSource(); // 호출시 AtomicInteger를 증가시키는 새 MessageSource를 생성
source.setObject(new AtomicInteger());
source.setMethodName("getAndIncrement");
return source;
}
@Bean
public DirectChannel inputChannel() {
return new DirectChannel(); // MessageSource에서 도착하는 데이터를 나르는 채널
}
@Bean
public IntegrationFlow myFlow() {
return IntegerationFlows
.from(this.integerMessageSource(), // 기존에 정의한 MessageSource를 IntegrationFlow의 입력으로 사용
c -> c.poller(Pollers.fixedRate(10))) // MessageSource를 폴링하면서 MessageSource가 나르는 데이터를 가져옴
.channel(this.inputChannel())
.filter((Integer p) -> p % 2 == 0) // 짝수만 거름
.transform(Object::toString) // MessageSource에서 가져온 정수를 문자열로 변환
.channel(MessageChannels.queue("queueChannel")) // queueChannel을 IntegrationFlow의 결과로 설정
.get(); // IntegrationFlow 만들기를 끝나고 변환
}
}
inputChannel 이름만 알고 있다면 이 API를 이용해 플로 내의 모든 컴포넌트로 메시지를 전달할 수 있다.
@Bean
public IntegrationFlow myFlow() {
return flow -> flow.filter((Intger p) -> p % 2 == 0)
.transform(Object::toString)
.handle(Systeomout::println);
}
스프링 통합 DSL에서 가장 널리 사용하는 패턴은 메서드 체인이다. 최상위 수준의 객체를 만들때는 함수 시퀀싱과 람다 표현식을 사용한다.
'Programming > Java' 카테고리의 다른 글
EPISODE 12. 모던 자바 인 액션(CHAPER 12 새로운 날짜와 시간 API) (0) | 2023.07.21 |
---|---|
EPISODE 11. 모던 자바 인 액션(CHAPTER 11 null 대신 Optional 클래스) (0) | 2023.07.20 |
EPISODE 9. 모던 자바 인 액션(CHAPTER 9 리팩터링, 테스팅, 디버킹) (0) | 2023.07.17 |
EPISODE 8. 모던 자바 인 액션(CHAPTER 8 컬렉션 API 개선) (0) | 2023.07.14 |
EPISODE 7. 모던 자바 인 액션(CHAPTER 7 벙렬 데이터 처리와 성능) (0) | 2023.07.13 |