From e44436d8f8f5a15efce0ace1549708af1d04d8fa Mon Sep 17 00:00:00 2001 From: jyun-KIM Date: Fri, 5 Jun 2026 02:12:50 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20ch10=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ch10.md | 768 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 768 insertions(+) create mode 100644 docs/ch10.md diff --git a/docs/ch10.md b/docs/ch10.md new file mode 100644 index 0000000..7855839 --- /dev/null +++ b/docs/ch10.md @@ -0,0 +1,768 @@ +https://jyun627.notion.site/chapter-10?source=copy_link + +## 전체 핵심 + +Chapter 10의 주제는 **DSL, Domain-Specific Language**이다. + +DSL은 특정 도메인의 문제를 더 읽기 쉽고 간결하게 표현하기 위한 언어 또는 API 설계 방식이다. 자바에서는 별도의 언어를 새로 만들지 않더라도, **람다 표현식, 메서드 체인, 정적 메서드, 메서드 참조, 스트림, 컬렉터** 등을 조합해서 DSL처럼 읽히는 API를 만들 수 있다. + +즉, 이 장의 핵심은 다음과 같다. + +> 자바 문법 안에서 비즈니스 도메인에 가까운 표현을 만들고, 코드의 가독성·유지보수성·의도를 높이는 방법 + +예를 들어 일반 자바 코드가 다음처럼 보인다면, + +```java +Order order = new Order(); +Trade trade = new Trade(); +trade.setType(Type.BUY); +trade.setStock("AAPL"); +trade.setQuantity(100); +trade.setPrice(150.0); +order.addTrade(trade); +``` + +DSL 스타일 코드는 다음처럼 도메인 문장에 가깝게 표현할 수 있다. + +```java +Order order = forCustomer("BigBank", + buy(100, stock("AAPL"), at(150.0)) +); +``` + +코드가 단순히 짧아지는 것이 아니라, **무엇을 하는 코드인지 도메인 관점에서 바로 읽히는 것**이 목적이다. + +--- + +# 10.1 도메인 전용 언어 + +## 10.1.1 DSL이란? + +**DSL, Domain-Specific Language**는 특정 도메인 문제를 해결하기 위해 설계된 언어이다. + +일반적인 프로그래밍 언어인 Java, Python, C++ 같은 언어는 다양한 문제를 해결할 수 있는 **범용 언어, General-Purpose Language**이다. 반면 DSL은 특정 문제 영역에 집중한다. + +대표 예시는 다음과 같다. + +| DSL | 도메인 | +| ---------- | ----------------- | +| SQL | 데이터베이스 질의 | +| HTML | 웹 문서 구조 | +| CSS | 웹 스타일 | +| 정규표현식 | 문자열 패턴 매칭 | +| Gradle DSL | 빌드 설정 | +| jOOQ | SQL 질의 구성 | +| Cucumber | 테스트 시나리오 | + +DSL의 목적은 **개발자가 도메인 규칙을 더 자연스럽게 표현하게 하는 것**이다. + +--- + +## 10.1.2 DSL의 장점과 단점 + +### 장점 + +### 1. 간결성 + +DSL은 반복적인 보일러플레이트 코드를 줄인다. + +일반 코드에서는 객체 생성, setter 호출, add 호출 등이 길게 이어질 수 있다. DSL을 사용하면 핵심 도메인 규칙만 남길 수 있다. + +```java +order( + buy(100, stock("IBM"), at(125.00)), + sell(50, stock("GOOGLE"), at(375.00)) +); +``` + +이런 코드는 객체 생성 과정이 아니라 **주문 내용 자체**에 집중하게 만든다. + +### 2. 가독성 + +DSL은 도메인 용어를 그대로 코드에 반영한다. + +예를 들어 금융 도메인에서는 `buy`, `sell`, `stock`, `price`, `quantity` 같은 용어가 중요하다. 이런 용어를 코드 구조에 반영하면 비개발자나 도메인 전문가도 코드 흐름을 더 쉽게 이해할 수 있다. + +### 3. 유지보수성 + +비즈니스 로직이 도메인 문장처럼 표현되면, 변경해야 할 부분을 찾기 쉬워진다. + +예를 들어 주문 정책이 바뀌었을 때, 복잡한 객체 생성 코드보다 DSL 형태의 코드가 의도를 더 명확히 드러낸다. + +### 4. 높은 수준의 추상화 + +DSL은 세부 구현을 숨기고, 도메인 개념을 중심으로 코드를 작성하게 한다. + +즉, “객체를 어떻게 만들 것인가?”보다 “어떤 주문을 만들 것인가?”에 집중한다. + +### 5. 관심사 분리 + +DSL을 사용하면 도메인 규칙 표현과 객체 생성 세부 구현을 분리할 수 있다. + +사용자는 다음처럼 도메인을 표현한다. + +```java +buy(100, stock("AAPL"), at(150.0)) +``` + +내부에서는 이 표현을 실제 `Order`, `Trade`, `Stock` 객체로 바꾸는 책임을 DSL 구현부가 가진다. + +--- + +### 단점 + +### 1. DSL 설계가 어렵다 + +DSL은 단순히 메서드 몇 개를 만드는 것이 아니다. + +좋은 DSL은 다음 조건을 만족해야 한다. + +- 읽기 쉬워야 한다. +- 도메인 용어와 잘 맞아야 한다. +- 잘못된 사용을 최대한 막아야 한다. +- IDE 자동완성, 타입 안정성, 컴파일 오류를 잘 활용해야 한다. +- 내부 구현이 과하게 복잡해지면 안 된다. + +### 2. 개발 비용이 든다 + +초기에는 DSL을 설계하고 구현하는 비용이 든다. + +작은 프로젝트에서는 오히려 과한 추상화가 될 수 있다. + +### 3. 추가 계층이 생긴다 + +DSL은 결국 기존 객체 모델 위에 한 계층을 추가하는 방식이다. + +따라서 DSL 계층이 너무 두꺼워지면 디버깅이 어려워지고, 성능이나 구조를 이해하기 어려워질 수 있다. + +### 4. 학습 비용이 있다 + +팀원들이 DSL 문법과 사용 방식을 새로 배워야 한다. + +특히 사내 전용 DSL이라면 외부 자료가 없기 때문에 문서화가 중요하다. + +--- + +## 10.1.3 JVM에서 사용할 수 있는 DSL 해결책 + +DSL은 크게 다음과 같이 나눌 수 있다. + +## 1. 외부 DSL, External DSL + +자바와 별개의 문법을 가진 DSL이다. + +예: + +```sql +SELECT * FROM orders WHERE price > 1000; +``` + +장점은 도메인에 최적화된 문법을 만들 수 있다는 점이다. + +단점은 파서, 문법 해석기, 실행 엔진 등을 직접 구현해야 할 수 있다는 점이다. + +## 2. 내부 DSL, Internal DSL + +자바 문법 안에서 DSL처럼 보이게 API를 설계하는 방식이다. + +예: + +```java +order.forCustomer("BigBank") + .buy(100) + .stock("IBM") + .at(125.00); +``` + +장점은 자바 컴파일러, IDE 자동완성, 리팩터링 도구, 타입 검사 기능을 그대로 사용할 수 있다는 점이다. + +## 3. 다중 DSL + +여러 JVM 언어를 함께 사용하는 방식이다. + +예를 들어 Kotlin, Groovy, Scala는 자바보다 DSL 작성에 유리한 문법을 제공한다. + +Gradle의 Groovy/Kotlin DSL이 대표적이다. + +하지만 이 장에서는 주로 **자바 안에서 DSL을 만드는 방법**에 집중한다. + +--- + +# 10.2 최신 자바 API의 작은 DSL + +자바 8 이후의 여러 API는 이미 DSL적인 성격을 가진다. + +대표적으로 다음이 있다. + +- Stream API +- Collectors +- Comparator +- Optional +- CompletableFuture + +이 API들은 단순한 메서드 모음이 아니라, **특정 작업을 선언형으로 표현하는 작은 DSL**처럼 동작한다. + +--- + +## 10.2.1 Stream API는 컬렉션을 조작하는 DSL + +Stream API는 컬렉션 데이터를 처리하기 위한 내부 DSL로 볼 수 있다. + +기존 반복문 방식은 다음과 같다. + +```java +List result = new ArrayList<>(); + +for (Dish dish : menu) { + if (dish.getCalories() < 400) { + result.add(dish.getName()); + } +} +``` + +Stream API를 사용하면 다음처럼 표현한다. + +```java +List result = menu.stream() + .filter(dish -> dish.getCalories() < 400) + .map(Dish::getName) + .collect(toList()); +``` + +이 코드는 다음 순서로 읽힌다. + +``` +menu에서 stream을 만들고 +칼로리가 400 미만인 dish만 필터링하고 +dish의 이름만 추출해서 +리스트로 수집한다 +``` + +즉, Stream API는 컬렉션 처리 과정을 **명령형이 아니라 선언형**으로 표현한다. + +### Stream API가 DSL처럼 보이는 이유 + +- `filter`, `map`, `sorted`, `limit`, `collect` 같은 도메인화된 연산을 제공한다. +- 메서드 체인으로 데이터 처리 흐름을 문장처럼 표현한다. +- 내부 반복을 사용하므로 개발자가 반복 제어를 직접 작성하지 않아도 된다. +- `Predicate`, `Function`, `Consumer` 같은 함수형 인터페이스와 람다를 활용한다. + +--- + +## 10.2.2 데이터를 수집하는 DSL인 Collectors + +`Collectors`는 Stream의 최종 결과를 다양한 형태로 수집하는 DSL이다. + +예를 들어 다음 코드는 메뉴를 타입별로 그룹화한다. + +```java +Map> dishesByType = + menu.stream() + .collect(groupingBy(Dish::getType)); +``` + +이 코드는 SQL의 `GROUP BY`와 비슷하게 읽힌다. + +```sql +GROUP BY dish.type +``` + +Collectors는 다음과 같은 작업을 표현할 수 있다. + +```java +counting() +summingInt(Dish::getCalories) +averagingInt(Dish::getCalories) +groupingBy(Dish::getType) +partitioningBy(Dish::isVegetarian) +joining(", ") +``` + +예시: + +```java +Map countByType = + menu.stream() + .collect(groupingBy(Dish::getType, counting())); +``` + +의미는 다음과 같다. + +``` +Dish를 type별로 그룹화하고, +각 그룹의 개수를 센다. +``` + +Collectors는 특히 **중첩 조합**이 가능하기 때문에 DSL적인 성격이 강하다. + +```java +Map> mostCaloricByType = + menu.stream() + .collect(groupingBy( + Dish::getType, + maxBy(comparingInt(Dish::getCalories)) + )); +``` + +이 코드는 다음 의미다. + +``` +음식을 타입별로 그룹화한 뒤, +각 타입에서 칼로리가 가장 높은 음식을 찾는다. +``` + +--- + +# 10.3 자바로 DSL을 만드는 패턴과 기법 + +이 절은 Chapter 10의 핵심이다. + +자바로 내부 DSL을 만드는 대표적인 패턴을 다룬다. + +목차 기준으로 다음 기법이 나온다. + +- Method chaining +- Nested function +- Lambda-based function sequencing +- 조합하기 +- DSL에 메서드 참조 사용하기 + +--- + +## 10.3.1 메서드 체인 + +**Method chaining**은 메서드가 자기 자신 또는 다음 빌더 객체를 반환해서 연속 호출을 가능하게 하는 방식이다. + +예: + +```java +Order order = new OrderBuilder() + .forCustomer("BigBank") + .buy(100) + .stock("IBM") + .on("NYSE") + .at(125.00) + .sell(50) + .stock("GOOGLE") + .on("NASDAQ") + .at(375.00) + .end(); +``` + +### 장점 + +메서드 체인은 자연어 문장처럼 읽힌다. + +``` +BigBank 고객을 위한 주문을 만들고, +IBM 주식 100개를 125달러에 사고, +GOOGLE 주식 50개를 375달러에 판다. +``` + +또한 IDE 자동완성을 활용하기 좋다. + +예를 들어 `buy()` 다음에는 `stock()`이 오고, `stock()` 다음에는 `at()`이 오도록 빌더 타입을 나누면 잘못된 순서의 호출을 컴파일 단계에서 막을 수 있다. + +### 단점 + +빌더 클래스가 많아질 수 있다. + +예를 들어 다음과 같은 중간 빌더 객체가 필요할 수 있다. + +```java +OrderBuilder +TradeBuilder +StockBuilder +``` + +즉, 사용자 코드는 깔끔해지지만 내부 구현은 복잡해질 수 있다. + +--- + +## 10.3.2 중첩된 함수 이용 + +**Nested function** 방식은 정적 메서드를 중첩 호출해서 DSL을 만드는 방식이다. + +예: + +```java +Order order = order( + forCustomer("BigBank"), + buy(100, + stock("IBM", on("NYSE")), + at(125.00) + ), + sell(50, + stock("GOOGLE", on("NASDAQ")), + at(375.00) + ) +); +``` + +### 장점 + +도메인 구조를 계층적으로 표현하기 좋다. + +``` +order + ├─ customer + ├─ buy + │ ├─ stock + │ └─ price + └─ sell + ├─ stock + └─ price +``` + +객체 포함 관계가 코드 구조에 직접 드러난다. + +### 단점 + +괄호가 많아진다. + +중첩이 깊어질수록 가독성이 떨어질 수 있다. + +```java +order(forCustomer("BigBank"), buy(100, stock("IBM", on("NYSE")), at(125.00))); +``` + +짧을 때는 간결하지만 복잡한 도메인에서는 괄호 지옥이 될 수 있다. + +--- + +## 10.3.3 람다 표현식을 이용한 함수 시퀀싱 + +이 방식은 람다를 이용해서 객체 설정 과정을 블록 형태로 표현한다. + +예: + +```java +Order order = order(o -> { + o.forCustomer("BigBank"); + + o.buy(t -> { + t.quantity(100); + 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"); + }); + }); +}); +``` + +### 장점 + +도메인 구조가 블록 단위로 표현된다. + +``` +order { + customer + buy { + stock + price + } + sell { + stock + price + } +} +``` + +복잡한 객체 그래프를 만들 때 유리하다. + +또한 람다 내부에서 여러 설정을 순차적으로 할 수 있어 유연하다. + +### 단점 + +코드가 상대적으로 장황해질 수 있다. + +또한 `o`, `t`, `s` 같은 람다 파라미터 이름을 잘못 정하면 가독성이 떨어진다. + +--- + +## 10.3.4 조합하기 + +실제 DSL 설계에서는 한 가지 방식만 고집하지 않는다. + +메서드 체인, 중첩 함수, 람다 방식을 조합할 수 있다. + +예: + +```java +Order order = forCustomer("BigBank", + buy(t -> t.quantity(100) + .stock("IBM") + .on("NYSE") + .at(125.00)), + sell(t -> t.quantity(50) + .stock("GOOGLE") + .on("NASDAQ") + .at(375.00)) +); +``` + +이런 식으로 각 방식의 장점을 섞을 수 있다. + +| 방식 | 장점 | 단점 | +| ----------------- | ------------------------------------ | ------------------------------ | +| Method chaining | 자연어처럼 읽힘, IDE 자동완성 유리 | 빌더 클래스가 많아질 수 있음 | +| Nested function | 구조가 명확함, 정적 팩토리와 잘 맞음 | 괄호가 많아짐 | +| Lambda sequencing | 계층 구조 표현에 유리 | 람다 파라미터가 많아질 수 있음 | +| Mixed style | 유연함 | 일관성이 깨질 수 있음 | + +스터디에서 중요한 포인트는 **DSL의 목적은 문법 기술 자체가 아니라 도메인 표현력 향상**이라는 점이다. + +--- + +## 10.3.5 DSL에 메서드 참조 사용하기 + +메서드 참조는 DSL의 가독성을 더 높일 수 있다. + +예를 들어 Comparator DSL을 보면 다음과 같다. + +```java +inventory.sort(comparing(Apple::getWeight) + .thenComparing(Apple::getColor)); +``` + +이 코드는 다음처럼 읽힌다. + +``` +사과를 무게 기준으로 정렬하고, +무게가 같으면 색상 기준으로 정렬한다. +``` + +람다로 쓰면 다음과 같다. + +```java +inventory.sort(comparing(apple -> apple.getWeight())); +``` + +메서드 참조를 쓰면 더 짧고 도메인 속성이 명확해진다. + +```java +Apple::getWeight +Apple::getColor +Dish::getCalories +Dish::getType +``` + +Collectors에서도 메서드 참조가 자주 쓰인다. + +```java +Map> dishesByType = + menu.stream() + .collect(groupingBy(Dish::getType)); +``` + +DSL에서 메서드 참조를 사용하면 **무엇을 기준으로 처리하는지**가 더 잘 드러난다. + +--- + +# 10.4 실생활의 자바 8 DSL + +이 절에서는 실제 라이브러리에서 DSL이 어떻게 쓰이는지 다룬다. + +목차 기준으로 다음 세 가지가 나온다. + +- jOOQ +- Cucumber +- Spring Integration + +--- + +## 10.4.1 jOOQ + +**jOOQ**는 자바에서 SQL을 타입 안전하게 작성할 수 있게 해주는 DSL이다. + +일반 SQL은 문자열로 작성한다. + +```java +String sql = "SELECT * FROM BOOK WHERE AUTHOR_ID = 1"; +``` + +문제는 문자열 SQL은 컴파일러가 검증하지 못한다는 점이다. + +오타가 있어도 실행 전까지 알기 어렵다. + +jOOQ는 SQL을 자바 코드로 표현한다. + +```java +create.selectFrom(BOOK) + .where(BOOK.AUTHOR_ID.eq(1)) + .fetch(); +``` + +이 코드는 SQL과 비슷하게 읽힌다. + +```sql +SELECT * +FROM BOOK +WHERE AUTHOR_ID = 1 +``` + +### jOOQ가 DSL인 이유 + +- SQL 도메인의 용어를 그대로 사용한다. +- `select`, `from`, `where`, `eq`, `fetch` 같은 메서드가 SQL 문장 구조를 이룬다. +- 자바 타입 시스템을 활용해 오류를 줄인다. +- IDE 자동완성과 리팩터링을 사용할 수 있다. + +즉, jOOQ는 외부 DSL인 SQL을 자바 내부 DSL로 옮긴 사례라고 볼 수 있다. + +--- + +## 10.4.2 큐컴버 + +**Cucumber**는 BDD, Behavior-Driven Development를 위한 테스트 DSL이다. + +테스트 시나리오를 자연어에 가깝게 작성한다. + +```gherkin +Feature: 계좌 이체 + +Scenario: 잔액이 충분한 경우 송금 성공 + Given 사용자의 계좌에 10000원이 있다 + When 사용자가 3000원을 송금한다 + Then 계좌 잔액은 7000원이어야 한다 +``` + +이런 형식은 개발자뿐 아니라 기획자, QA, 도메인 담당자도 이해하기 쉽다. + +자바에서는 각 문장을 실제 테스트 코드와 연결한다. + +```java +@Given("사용자의 계좌에 {int}원이 있다") +public void 계좌에_돈이_있다(int amount) { + account = new Account(amount); +} + +@When("사용자가 {int}원을 송금한다") +public void 송금한다(int amount) { + account.transfer(amount); +} + +@Then("계좌 잔액은 {int}원이어야 한다") +public void 잔액_검증(int expected) { + assertEquals(expected, account.getBalance()); +} +``` + +### Cucumber가 DSL인 이유 + +- 테스트를 도메인 시나리오 형태로 표현한다. +- `Given`, `When`, `Then` 구조로 행동을 명확히 나눈다. +- 기술 구현보다 비즈니스 동작에 집중한다. +- 개발자와 비개발자 간 의사소통 도구가 될 수 있다. + +--- + +## 10.4.3 Spring Integration + +**Spring Integration**은 엔터프라이즈 통합 패턴을 자바 DSL로 표현할 수 있게 해준다. + +예를 들어 파일을 읽고, 필터링하고, 변환하고, 특정 채널로 보내는 흐름을 DSL로 구성할 수 있다. + +개념적으로는 다음과 같은 흐름이다. + +```java +IntegrationFlows.from("inputChannel") + .filter(...) + .transform(...) + .handle(...) + .get(); +``` + +이 코드는 메시지 처리 파이프라인을 표현한다. + +``` +inputChannel에서 메시지를 받고 +조건에 맞는 메시지만 필터링하고 +메시지를 변환한 뒤 +핸들러에서 처리한다 +``` + +### Spring Integration DSL의 의미 + +- 복잡한 메시지 기반 시스템을 선언형으로 표현한다. +- 채널, 필터, 변환기, 핸들러 같은 통합 패턴을 코드 흐름으로 나타낸다. +- XML 설정보다 자바 코드 기반 DSL이 타입 안정성과 자동완성 측면에서 유리하다. + +--- + +# 10.5 마치며 + +Chapter 10의 결론은 다음과 같이 정리할 수 있다. + +## 핵심 요약 + +1. **DSL은 특정 도메인의 문제를 표현하기 위한 언어 또는 API 설계 방식이다.** +2. 자바에서는 별도의 언어를 만들지 않아도 내부 DSL을 만들 수 있다. +3. 람다, 메서드 참조, 정적 메서드, 메서드 체인, 스트림, 컬렉터는 DSL 설계의 핵심 도구가 된다. +4. 좋은 DSL은 코드의 간결성, 가독성, 유지보수성을 높인다. +5. 하지만 DSL 설계에는 비용이 들며, 과도한 추상화는 오히려 복잡도를 높일 수 있다. +6. Stream API, Collectors, Comparator 등 자바 기본 API에도 이미 DSL적인 설계가 녹아 있다. +7. jOOQ, Cucumber, Spring Integration은 실무에서 DSL이 어떻게 활용되는지 보여주는 대표 사례다. + +--- + +- gpt가 알려준 중요 포인트 + ## 1. DSL은 “문법 예쁘게 만들기”가 아니다 + DSL의 목적은 단순히 코드를 짧게 만드는 것이 아니다. + 중요한 것은 다음이다. + ``` + 도메인 의도가 코드에 직접 드러나는가? + ``` + 예를 들어 다음 코드는 짧지만 DSL이라고 보기 어렵다. + ```java + a.b().c().d(); + ``` + 반면 다음 코드는 도메인 의미가 명확하다. + ```java + order.buy(100).stock("IBM").at(125.00); + ``` + DSL은 **읽는 사람이 도메인 흐름을 바로 이해하게 만드는 API 설계**다. + *** + ## 2. 내부 DSL은 자바의 장점을 그대로 활용한다 + 내부 DSL은 자바 코드이므로 다음 장점이 있다. + - 컴파일 타임 타입 검사 + - IDE 자동완성 + - 리팩터링 지원 + - 디버깅 가능 + - 기존 자바 생태계와 통합 가능 + 외부 DSL보다 표현력은 제한될 수 있지만, 실무에서는 내부 DSL이 더 현실적인 경우가 많다. + *** + ## 3. 좋은 DSL은 잘못된 사용을 막아야 한다 + DSL이 단순히 읽기 좋은 것에서 끝나면 부족하다. + 좋은 DSL은 사용자가 잘못된 순서나 잘못된 조합을 사용하지 못하게 설계해야 한다. + 예를 들어 다음 순서는 이상하다. + ```java + order.at(125.00).buy(100).stock("IBM"); + ``` + 가격을 먼저 정하고 매수하는 것은 도메인 흐름상 어색하다. + 빌더 타입을 잘 나누면 다음처럼 자연스러운 순서만 허용할 수 있다. + ```java + order.buy(100) + .stock("IBM") + .at(125.00); + ``` + *** + ## 4. DSL은 팀 단위 합의가 필요하다 + DSL을 만들면 팀원들이 그 DSL을 배워야 한다. + 따라서 다음 기준이 필요하다. + - 정말 반복되는 도메인 로직인가? + - 도메인 용어가 안정적인가? + - DSL을 만들면 코드가 더 읽기 쉬워지는가? + - 내부 구현 복잡도보다 얻는 이점이 큰가? + - 문서화와 예제가 충분한가? + 작은 기능에 무리하게 DSL을 만들면 오히려 유지보수 비용이 커진다.