일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- Generic
- 제네릭
- namedQuery
- 엔티티직접사용
- JPQL
- fullcalendar
- LIST
- calendar
- JPA
- jQueryUI
- 벌크연산
- 자바서블릿
- jscalendar
- 대량쿼리
- values()
- javaservlet
- fetchjoin
- 제너릭
- jQuery값전달
- springflow
- 페치조인
- 페이징
- 프로젝트생성
- JQuery
- joinfetch
- jQuery값전송
- 스프링데이터흐름
- javascriptcalendar
- paging
- Hibernate
- Today
- Total
가자공부하러!
모던자바인액션(CH10) - 람다를 이용한 도메인 전용 언어(DSL) 본문
목차
요약 및 결론
책 내용
요약 및 결론
자바 8의 람다와 메서드참조 기능의 추가로 DSL 개발 적합도가 얼마나 향상됐는지 보여주기 위한 챕터인듯 했다.
하지만 가장 좋았던건 편하게 쓰기만 하던 메소드 체인 패턴(플루언트 스타일)이 어떻게 구성됐는지 알게된 것이다. 자바 좋다.
책 내용
1. 도메인 전용 언어(Domain Specific Languages)
- DSL은 특정 비즈니스 도메인의 문제를 해결하려고 만들어진 특수 프로그래밍 언어이다.
- 특정 비즈니스 도메인을 인터페이스로 만든 API라고 생각할 수 있다.
- 직역 : 영역 별 언어
1.1. DSL의 장점과 단점
장점과 단점에 대한 이유들과 사례들은 10장이 진행되며 하나씩 나온다.
장점 :
- 간결함 : API는 비즈니스 로직을 간편하게 캡슐화하므로 반복을 피할 수 있고 코드를 간결하게 만들 수 있다.
- 가독성 : 도메인 영역의 용어를 사용하므로 비 도메인 전문가도 코드를 쉽게 이해할 수 있다.
- 유지보수 : 잘 설계된 DSL로 구현한 코드는 유지보수가 쉽다.
- 높은 수준의 추상화 : 도메인의 문제와 직접적으로 관련되지 않은 세부 사항을 숨긴다.
- 집중 : 목적이 명확한 언어이므로 프로그래머가 특정 코드에 집중할 수 있어서 생산성이 좋아진다.
- 관심사 분리 : 애플리케이션과는 독립적으로, 비즈니스 관련된 코드에만 집중하기가 쉽다.
단점
- DSL 설계의 어려움 : 간결하게 제한적인 언어에 도메인 지식을 담는 것은 어렵다.
- 개발 비용 : 코드에 DSL을 추가하는 작업은 초기 프로젝트에 많은 비용과 시간이 소모되는 작업이다. DSL 유지보수와 변경은 프로젝트에 부담을 준다.
- 추가 우회 계층 : DSL은 추가적인 계층으로 도메인 모델을 감싸며 이 때 계층을 최대한 작게 만들어 성능 문제를 회피한다. -> 이게 왜 단점이지?
- 새로 배워야 하는 언어 : 여러 비즈니스 도메인을 다루는 개별 DSL을 사용하는 상황에서 개별 DSL이 독립적으로 진화하게 된다면 복잡해진다.
- 호스팅 언어 한계 : 장황하고 엄격한 문법을 가진 프로그래밍 언어를 기반으로 만든 DSL은 문법 제약 때문에 가독성이 떨어질 수 있다.
1.2. JVM에서 이용할 수 있는 다른 DSL 해결책
내부 DSL : 호스팅 언어를 기반으로 구현
자바 문법 때문에 읽기 쉽고 간단한 DSL을 만드는데 한계가 있었지만 람다 표현식의 등장으로 어느정도 해결되었다.
익명 내부 클래스에는 신호대비 잡음이 있을 수 있는데 람다 표현식 또는 메소드참조로 해결할 수 있다.
@Test public void innerDSLTest() { List<String> numbers = Arrays.asList("one", "two", "three"); //익명 내부 클래스 numbers.forEach(new Consumer<String>() { @Override public void accept(String s) { log.debug(s); } }); //메소드 참조 numbers.forEach(log::debug); }
순수 자바로 DSL을 구현할 때 얻는 장점
- 외부 DSL을 배우기 위한 노력을 하지 않아도 된다.
- 다른 언어의 컴파일러를 이용하거나 외부 DSL 도구를 사용할 필요 없이 자바 코드와 DSL을 한번에 컴파일 할 수 있다.
- 기존 자바 IDE의 자동완성 기능을 그대로 사용할 수 있다.
- 추가로 DSL을 쉽게 기존 코드로 합칠 수 있다.
다중 DSL : 자바가 아니지만 JVM에서 실행됨(스칼라, 그루비 등)
JVM에서 실행되는 언어 중에 문법이 간편하고 제약이 적은 언어가 많다.
Scala에서 내장DSL로 3번 Hello World를 출력하는 프로그램 구현
문법적 잡음이 없음을 확인
// 주어진 함수 f를 주어진 횟수만큼 반복 실행하는 유틸리티 함수 구현 def times(i: Int, f: => Unit): Unit = { f if (i > 1) timesStandard(i - 1, f) } times(3, pringln("Hello World"))
단점
- 새로운 프로그래밍 언어를 배워야만 한다.
- 두 개 이상의 언어가 혼재하므로 여러 컴파일러로 소스를 빌드하도록 빌드 과정을 개선해야 한다.
- JVM에서 실행되는 거의 모든 언어가 자바와 100% 호환을 주장하고 있지만 완벽하지 않을 때가 많다.
- 스칼라와 자바 컬렉션은 호환되지 않으므로 상호 컬렉션을 전달하려면 기존 컬렉션을 대상 언어의 API에 맞게 변환해야 한다.
외부 DSL(Stand Alone) : 호스팅 언어와는 독립적으로 자체의 문법을 가짐
- 시간과 노력이 많이 들어간다.
- 새 언어 파싱 -> 파서 결과 분석 -> 외부 DSL을 실행할 코드 작성
- 외부 DSL의 장점
- 무한한 유연성 : 필요한 특성을 완벽하게 제공하는 언어를 설계할 수 있음
- 기존 어플리케이션과 분리 :DSL과 호스트 언어 사이에 인공계층이 생기므로 단점이 될 수도 있다.
2. 최신 자바 API의 작은 DSL
- 시간과 노력이 많이 들어간다.
자바의 새로운 기능의 장점을 적용한 첫 API는 네이티브 자바 API 자신이다.
람다 표현식과 메소드 참조를 이용해 DSL의 가독성, 재사용성, 결합성이 높아졌다.
2.1. 스트림 API는 컬렉션을 조작하는 DSL
Stream API는 작지만 강력한 내부 DSL : 데이터 조작(필터링, 정렬, 변환, 그룹화 등) 기능 제공
Stream API의 플루언트 형식은 잘 설계된 DSL의 또 다른 특징
- 모든 중간 연산은 게으르며 다른 연산으로 파이프라인될 수 있는 스트림으로 반환된다.
- 최종 연산은 적극적이며 전체 파이프라인이 계산을 일으킨다.
2.2. 데이터를 수집하는 DSL인 Collectors
Collectors는 다중 수준 그룹화를 달성할 수 있도록 합쳐질 수 있다.
@Test public void multipleGrouping() { List<Dish> menuList = Menu.getList(); Map<Dish.CaloricalLevel, Map<Dish.Type, List<Dish>>> collect = menuList.stream().collect(groupingBy(Dish::getCaloricalLevel, groupingBy(Dish::getType))); log.debug("result : {}", collect.toString()); //result : { // FAT={MEAT=[Dish(name=pork, vegeterian=false, calories=800, type=MEAT)]}, // DIET={FISH=[Dish(name=prawns, vegeterian=false, calories=300, type=FISH)], MEAT=[Dish(name=chicken, vegeterian=false, calories=400, type=MEAT)], OTHER=[Dish(name=rice, vegeterian=true, calories=350, type=OTHER), Dish(name=season fruits, vegeterian=true, calories=120, type=OTHER)]}, // NORMAL={FISH=[Dish(name=salmon, vegeterian=false, calories=450, type=FISH)], MEAT=[Dish(name=beef, vegeterian=false, calories=700, type=MEAT)], OTHER=[Dish(name=french fries, vegeterian=true, calories=530, type=OTHER), Dish(name=pizza, vegeterian=true, calories=550, type=OTHER)]}} }
3. 자바로 DSL을 만드는 패턴과 기법
빌더 코드가 너무 많아서 새삼 Lombok의 소중함을 느끼게 됐다.
예제 도메인 모델
- Stock : 주어진 시장에 주식 가격을 모델링하는 순수 자바 빈즈
- Trade : 주어진 가격에서 주어진 양의 주식을 사거나 파는 거래
- Order : 고객이 요청한 한 개 이상의 거래의 주문
3.1. 메서드 체인
주문 빌더 구현
단점 : 빌더 전체를 구현해야 해서 작업이 많아짐
Lombok에 @Builder를 이용하면 단점 해소
//빌더 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 Order end() { return order; } 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; } } //테스트 @Test public void builderTest() { String customerName ="hj"; MethodChainingOrderBuilder.forCustomer(customerName) .buy(10) .stock("RSUPPORT") .on("KOSDAQ") .at(5000) .end(); } //Lombok @Builder를 적용하고 컴파일된 클래스 public static class CustomerBuilder { private int id; private String name; CustomerBuilder() { } public Customer.CustomerBuilder id(final int id) { this.id = id; return this; } public Customer.CustomerBuilder name(final String name) { this.name = name; return this; } public Customer build() { return new Customer(this.id, this.name); } public String toString() { return "Customer.CustomerBuilder(id=" + this.id + ", name=" + this.name + ")"; } }
3.2. 중첩된 함수 이용
함수 안에 함수를 이용해 도메인 모델을 만드는 방법
함수의 중첩 방식이 도메인 객체 계층 구조에 그대로 반영된다는 것이 장점이다.
괄호가 너무 많아 지저분해 보이는게 단점
VO, DTO로 캡슐화 해서 넘기는게 더 좋아보일거라 생각 듬
@Test public void nestedFunctionTest() { Order order = order("BigBank", buy(80, stock("IBM", on("NYSE")), at(125.00)), sell(50, stock("GOOGLE", on("NASDAQ")), at(375.00)) ); log.debug("nested function result : {}", order); }
3.3. 람다 표현식을 이용한 함수 시퀀싱
람다 표현식으로 정의한 함수 시퀀스를 사용하는 DSL 패턴
- 주문(Order) 빌더, 거래(Trade) 빌더, 주식(Stock) 빌더
장점
- 메서드 체인 패턴처럼 플루언트 방식으로 거래 주문을 정의할 수 있다.
- 중첩 함수 형식 처럼 다양한 람다 표현식의 중첩 수준과 비슷하게 계층 구조를 유지한다.
단점 : 예제코드만 해도 정말 장황함
많은 설정 코드가 필요함
DSL 자체가 자바 8 람다 표현식 문법에 의한 잡음의 영향을 받음 ->
//왜 이렇게까지.. @Test public void functionSequencing() { String customer = "SEQBank"; LambdaOrderBuilder lambdaOrderBuilder = new LambdaOrderBuilder(); lambdaOrderBuilder.forCustomer(customer); Order order = LambdaOrderBuilder.order(builder -> { builder.forCustomer(customer); builder.buy(trade -> { trade.quantity(10); trade.price(10.0); trade.stock(stock -> { stock.symbol("IBM"); stock.market("NYSE"); }); }); }); log.debug("order : {}", order); // order : Order[customer=SEQBank, trades=[Trade[type=BUY, stock=Stock[symbol=IBM, market=NYSE], quantity=10, price=10.00] }
3.4. 조합하기
여러 DSL 패턴을 이용해 주식 거래 주문 만들기
@Test public void mixedDSLTest() { String customer = "MIX"; Order order = forCustomer(customer, buy(t -> t.quantity(80) .stock("IBM") .on("NYSE") .at(125.00)), sell(t -> t.quantity(50) .stock("GOOGLE") .on("NASDAQ") .at(375.00))); log.debug("order : {}", order); // order : Order[customer=MIX, trades=[ // Trade[type=BUY, stock=Stock[symbol=IBM, market=NYSE], quantity=80, price=125.00] // Trade[type=SELL, stock=Stock[symbol=GOOGLE, market=NASDAQ], quantity=50, price=375.00] //]] }
3.5. DSL에 메서드 참조 사용하기
메소드 참조는 정말 코드를 간결하게 만들어 준다 멋지다
@Test public void methodReferenceTest() { String customer = "MIX"; Order order = forCustomer(customer, buy(t -> t.quantity(80).stock("IBM").on("NYSE").at(125.00)), sell(t -> t.quantity(50).stock("GOOGLE").on("NASDAQ").at(125.00))); double result1 = new TaxCalculator().withTaxRegional() .withTaxSurcharge() .calculate(order); Order order2 = forCustomer(customer, buy(t -> t.quantity(80).stock("IBM").on("NYSE").at(125.00)), sell(t -> t.quantity(50).stock("GOOGLE").on("NASDAQ").at(125.00))); double result2 = new TaxCalculator().with(Tax::regional) .with(Tax::surcharge) .calculateF(order2); //result1이랑 result2랑 값이 계속 달라서 한참보다보니 계산 함수가 다르다 //calculate(), calculateF() //메소드 이름은 정말 잘 지어야 한다. log.debug("result 1 : {}", result1); log.debug("result 2 : {}", result2); }
4. 실생활의 자바8 DSL
DSL 패턴의 장점과 단점
메서드 체인 : 누가 벌써 이렇게 만들어두면 쓰기만 하는 나는 참 편하다.
- 장점 :
- 메서드 이름이 키워드 인수 역할을 수행
- 선택형 파라미터와 잘 동작한다.
- 정적 메소드를 최소화하거나 없앨 수 있다.
- 문법적 잡음을 최소화 한다.
- 단점 :
- 구현이 장황하다.
- 빌드를 연결하는 접착 코드가 필요하다.
- 들여쓰기 규칙으로만 도메인 객체 계층을 정의한다.
- 장점 :
중첩 함수
- 장점 :
- 구현의 장황함을 줄일 수 있다.
- 함수 중첩으로 도메인 객체 계층을 반영한다.
- 단점 :
- 정적 메서드의 사용이 빈번하다.
- 이름이 아닌 위치로 인수를 정의한다.
- 선택형 파라미터를 처리할 메소드 오버로딩이 필요하다.
- 장점 :
람다를 이용한 함수 시퀀싱
- 장점 :
- 선택형 파라미터와 잘 동작한다.
- 정적 메소드를 최소화하거나 없앨 수 있다.
- 람다 중첩으로 도메인 객체 계층을 반영한다.
- 빌더의 접착 코드가 없다.
- 단점 :
- 구현이 장황하다.
- 람다 표현식으로 인한 문법적 잡음이 DSL에 존재한다.
4.1. jOOQ
- 장점 :
SQL을 구현하는 내부적 DSL
자바에 내장된 형식 안전 언어
예시
//select * from BOOK where BOOK.PUBLISHED_IN = 2006 ORDER BY BOOK.TITLE create.selectFrom(BOOK) .where(BOOK.PUBLISHED_IN.eq(2016)) .orderBy(BOOK.TITLE)
4.2. 큐컴버
다른 BDD(Behavior-driven Development) 프레임워크와 마찬가지로 도메인 전용 스크립팅 언어 명령문을 실행할 수 있는 테스트케이스로 변환한다.
큐컴버는 개발자가 비즈니스 시나리오를 평문 영어로 구현할 수 있도록 도와주는 BDD 도구
예시
//문장은 테스트 케이스의 변수를 캡쳐하는 정규 표현식으로 매칭된다. //테스트 자체를 구현하는 메소드로 문장을 전달한다. Feature: Buy stock Senario: Buy 10 IBM stocks Given the price of a "IBM" stock is 125$ When I buy 10 "IBM" Then the order value should be 1250$
public class BuyStocksSteps { //위 문장이 Given 어노테이션으로 전달되었음 //시나리오의 전제 조건을 정의하는 부분 @Given("^the price of a \"(.*?\" stock is (\\d+)\\$$") public void setUnitPrice(String stockName, int unitPrice) { //단가를 저장하는 세터 } }
4.3. 스프링 통합(Spring Integration)
의존성 주입에 기반한 스프링 프로그래밍 모델을 확장한다.
목표 :
- 복잡한 엔터프라이즈 통합 솔루션을 구현하는 단순한 모델 제공
- 비동기, 메시지 주도 아키텍처를 쉽게 적용할 수 있도록 도움
여러 엔드포인트를 한 개 이상의 메시지 흐름으로 조합해서 통합 과정이 구성된다.
//예제 : https://docs.spring.io/spring-integration/docs/5.1.0.M1/reference/html/java-dsl.html @Configuration @EnableIntegration public class MyConfiguration { @Bean public AtomicInteger integerSource() { return new AtomicInteger(); } @Bean public DirectChannel inputChannel() { return new DirectChannel } @Bean public IntegrationFlow myFlow() { return IntegrationFlows //integerSource를 integrationFlow의 입력으로 사용 .from(integerSource::getAndIncrement, //MessageSource를 폴링하면서 MessageSource가 나르는 데이터를 가져옴 c -> c.poller(Pollers.fixedRate(100))) //inputChannel의 이름만 알고 있으면 모든 컴포넌트로 메시지를 전달할 수 있다. .channel(inputChannel()) .filter((Integer p) -> p > 0) //MessageSource에서 가져온 정수를 문자열로 변환 .transform(Object::toString) .channel(MessageChannels.queue()) .get(); } }
5. 마치며
- DSL의 주요 기능은 개발자와 도메인 전문가 사이의 간격을 좁히는 것이다.
- 애플리케이션의 비즈니스 로직을 구현하는 코드를 만든 사람이 프로그램이 사용될 비즈니스 필드의 전문 지식을 갖추긴 어렵다.
- 개발자가 아닌 사람도 이해할 수 있는 언어로 이런 비즈니스 로직을 구현할 수 있다고 해서 도메인 전문가가 프로그래머가 될 수 있는 것은 아니지만 적어도 로직을 읽고 검증하는 역할은 할 수 있다.
- DSL이랑 코드랑 별 차이 없던데 왜 이런말이 써있는지 잘 모르겠다.
- DSL은 내부DSL, 다중DSL, 외부DSL로 구분할 수 있으며 구분 기준은 호스팅 언어를 기반으로 한 정도이다.
- JVM에서 이용할 수 있는 스칼라, 그루비 등 다른 언어로 다중 DSL을 개발할 수 있다.
- 장점 : 자바보다 유연하며 간결
- 단점 : 자바와 통합할 때 빌드 과정이 복잡해지며 상호 호환성 문제 발생 가능
- 자바의 장황함과 문법적 엄격함 때문에 내부DSL 개발 언어로 적합하지 않다.
- 자바 8의 람다 표현식과 메서드 참조 덕분에 상황이 많이 개선되었다.
플루언트 API (Fluent API)
- https://www.martinfowler.com/bliki/FluentInterface.html
- 내부 DSL 라인을 따라 작업을 수행하는 것
- 메소드 체이닝의 다른 이름인 듯
- Fluent API를 이용한 메소드체이닝 이라는 말이 있었음
- 책 읽다 보니 플루언트 방식이 메소드 체인 보다 상위 개념인듯
###
'공부 > Java' 카테고리의 다른 글
모던자바인액션(CH12) - 새로운 날짜 시간 API (0) | 2020.08.05 |
---|---|
모던자바인액션(CH11) - Optional (0) | 2020.07.22 |
Hibernate에서 uuid와 uuid2의 차이점(스크랩) (0) | 2020.06.16 |
모던자바인액션(CH9) - 스트림과 람다 활용(2) (0) | 2020.06.13 |
모던자바인액션(CH8) - 스트림과 람다 활용(1) (0) | 2020.06.07 |