devseop08 님의 블로그
[API] 람다와 스트림: Mordern Java - 10. 람다를 이용한 도메인 전용 언어 본문
- 프로그래밍 언어의 주요 목표는 메시지를 명확하고, 안정적인 방식으로 전달하는 것
- 무엇보다 의도가 명확하게 전달돼야 한다.
- 개발팀과 도메인 전문가가 공유하고 이해할 수 있는 코드는 생산성과 직결
- 도메인 전용 언어로 애플리케이션의 비즈니스 로직을 표현함으로 이 문제를 해결 가능
- DSL은 범용이 아니라 특정 도메인을 대상으로 만들어진 특수 프로그래밍 언어다.
- Maven, Gradle, Ant => 빌드 과정을 표현하는 DSL
- HTML은 웹 페이지 구조를 정의하도록 특화된 언어
- SQL 언어같은 종류의 DSL을 외부적이라 하는데, 이는 데이터베이스가 텍스트로 구현된 SQL 표현식을 파싱하고 평가하는 API를 제공하는 것이 일반적이기 때문이다.
menu.stream()
.filter(d -> d.getCalories() < 400)
.map(Dish::getName)
.forEach(System.out::println)- 위 예제의 DSL은 외부적이 아니라 내부적이다.
- 내부적 DSL에서는 SQL 구문처럼 애플리케이션 수준의 기본값이 자바 메서드가 사용할 수 있도록 데이터베이스를 대표하는 한 개 이상의 클래스 형식으로 노출된다.
- 내부적 DSL에서는 유창하게 코드를 구현할 수 있도록 적절하게 클래스와 메서드를 노출하는 과정이 필요하다.
- 외부 DSL은 DSL 문법 뿐 아니라 DSL을 평가하는 파서도 구현해야 한다.
- 10.1 도메인 전용 언어
- 자바에서는 DSL을 만들기 위해 도메인을 표현할 수 있는 클래스와 메서드 집합이 필요하다.
- DSL이란 특정 비즈니스 도메인을 인터페이스로 만든 API라고 생각할 수 있다.
- DSL은 범용 프로그래밍 언어가 아니다.
- 도메인 전문가가 저수준 비즈니스 로직을 구현하도록 만드는 것은 DSL의 역할이 아니다.
- DSL 만들 시 고려 사항
- 코드의 의도가 명확히 전달, 프로그래머가 아닌 사람도 이해할 수 있어야 한다.
- 가독성, 동료가 쉽게 이해할 수 있도록 코드를 구현해야 한다.
10.1.1 DSL의 장점과 단점
- DSL 장점
- 간결함
- 가독성
- 유지보수
- 높은 수준의 추상화
- 집중
- 관심사 분리
- DSL 단점
- DSL 설계의 어려움
- 개발 비용
- 추가 우회 계층: DSL은 추가적인 계층으로 도메인 모델을 감싸며 이 때 계층을 최대한 작게 만들어 성능 문제를 회피해야 한다.
- 새로 배워야 하는 언어
- 호스팅 언어 한계 : 일부 자바 같은 범용 프로그래밍 언어로는 사용자 친화적인 DSL을 만들기가 힘들다.
10.1.2 JVM에서 이용할 수 있는 다른 DSL 해결책
- DSL 구분
- 내부 DSL: 순수 자바 코드와 같은 기존 호스팅 언어를 기반으로 구현
- 외부 DSL: 호스팅 언어와는 독립적으로 자체 문법을 갖는다.
- 다중 DSL: JVM으로 인해 내부 DSL과 외부 DSL의 중간 카테고리에 해당하는 DSL, 스칼라나 그루비처럼 자바가 아니지만 JVM에서 실행되며 더 유연하고 표현력이 강력한 언어가 있다.
내부 DSL
자바로 구현한 DSL
람다를 적극적으로 활용하면 익명 내부 클래스를 사용해 DSL을 구현하는 것보다 장황함을 크게 줄여 신호 대비 잡을 비율을 적정 수준으로 유지하는 DSL을 만들 수 있다.
순수 자바로 DSL을 구현함으로써 얻을 수 있는 장점
- 기존 자바 언어를 이용하면 외부 DSL에 비해 새로운 패턴과 기술을 배워 DSL을 구현하는 노력이 현저하게 줄어든다.
- 순수 자바로 DSL을 구현하면 나머지 코드와 함께 DSL을 컴파일할 수 있다. 외부 DSL을 만드는 도구를 사용할 필요가 없으므로 추가 비용이 들지 않는다.
- 새로운 언어를 배우거나 또는 익숙하지 않고 복잡한 외부 도구를 배울 필요가 없다.
- DSL 사용자는 기존의 자바 IDE를 이용해 자동 완성, 자동 리팩터링 같은 기능을 그대로 즐길 수 있다.
- 한 개의 언어로 한 개의 도메인 또는 여러 도메인을 대응하지 못해 추가로 DSL을 개발해야 하는 상황에서 자바를 이용한다면 추가 DSL을 쉽게 합칠 수 있다.
다중 DSL
자바 외의 JVM에서 실행되는 언어들
스칼라, 루비, 그루비, 코틀린, ,실론
DSL은 기반 프로그래밍언의 영향을 받으므로 간결한 DSL을 만드는 데 새로운 언어의 특성들이 아주 중요하다.
스칼라는 커링, 임의 변환 등 DSL 개발에 필요한 여러 특성을 갖췄다.
DSL에 친화적인 다른 JVM 언어로 DSL을 구현하는 경우의 불현함
- 새로운 프로그래밍 언어 습득 필요
- 두 개 이상의 언어가 혼재하므로 여러 컴파일러로 소스를 빌드하도록 빌드 과정 개선 필요
- JVM에서 실행되는 언어가 자바와 호환성이 완벽하지 않을 때가 많다.
외부 DSL
자신만의 문법과 구문으로 새 언어를 설계해야 한다.
새 언어를 파싱하고 파서의 결과를 분석하고 외부 DSL을 실행할 코드를 만들어야 한다.
외부 DSL을 개발하는 가장 큰 장점은 외부 DSL이 제공하는 무한한 유연성이다.
제대로 언어를 설계하면 비즈니스 문제를 묘사하고 해결하는 가독성 좋은 언어를 얻을 수 있다.
10.2 최신 자바 API의 작은 DSL
자바의 새로운 기능의 장점을 적용한 첫 API는 네이티브 자바 API 자신이다.
자바 8 이전의 네이티브 자바 API는 이미 한 개의 추상 메서드를 갖는 인터페이스를 갖고 있었지만 익명 내부 클래스를 구현하려면 불필요한 코드가 추가돼야 한다.
람다와 메서드 참조가 등장하면서 전혀 다른 패러다임으로 전환되었다.(특히 DSL 관점에서 말이다.)
자바 8의 Comparator 인터페이스에 새 메서드 추가되었는데, 인터페이스가 정적 메서드와 디폴트 메서드를 가질 수 있게 됐다.
자바 8 이전 Comparator 인터페이스 사용: 익명 객체 생성하여 사용
Collections.sort(persons, new Comparator<Person>(){
public int compare(Person p1, Person p2){
return p1.getAge() - p2.getAge();
}
});- 자바 8 이후 람다를 이용한 Comparator 인터페이스 사용
Collections.sort(persons, (p1, p2) -> p1.getAge() - p2.getAge());- Comparator 인터페이스의 정적 유틸리티 메서드를 이용해 Comparator 타입 객체 생성
static import Comparator.comparing
Collections.sort(persons, comparing(p -> p.getAge()));- 메서드 참조 이용
Collections.sort(persons, comparing(Person::getAge));- Comparator 타입 객체에 추가 메서드 적용
Collections.sort(persons, comparing(Person::getAge).reverse());Collections.sort(persons, comparing(Person::getAge)
.thenComparing(Person::getName));persons.sort(comparing(Person::getAge).thenComparing(Person::getName))10.2.1 스트림 API는 컬렉션을 조작하는 DSL
Stream 인터페이스는 네이티브 자바 API에 작은 DSL을 적용한 좋은 예
Stream은 컬렉션의 항목을 필터, 정렬, 변환, 그룹화, 조작하는 작지만 강력한 DSL
스트림 API의 플루언트 형식은 잘 설계된 DSL의 또 다른 특징이다.
- 모든 중간 연산은 게으르며 다른 연산으로 파이프라인될 수 있는 스트림으로 반환된다.
- 최종 연산은 적극적이며 전체 파이프라인이 계산을 일으킨다.
10.2.2 데이터를 수집하는 DSL인 Collectors
Stream 인터페이스를 데이터 리스트를 조작하는 DSL로 간주할 수 있음을 확인
Collector 인터페이스는 데이터 수집을 수행하는 DSL로 간주 가능
Collectors 클래스에서 제공하는 정적 팩토리 메서드를 이용해 필요한 Collector 타입의 객체를 만들고 합칠 수 있다.
Comparator 인터페이스는 다중 필드 정렬을 지원하도록 합쳐질 수 있으며, Collector는 다중 수준 그룹화를 달성할 수 있도록 합쳐질 수 있다.
Comparator를 플루언트 방식으로 연결해서 다중 필드 Comparator를 선언
Comparator<Person> comparator =
comparing(Person::getAge).thenComparing(Person::getName);- Collectors API를 이용해 Collector를 중첩함으로써 다중 수준 Collector를 만들 수 있다.
Collector<? super Car, ?, Map<Brand, Map<Color, List<Car>>>> carGroupCollector
= groupingBu(Car::getBrand, groupingBy(Car::getColor));- 보통 셋 이상의 컴포넌트를 조합할 때는 보통 플루언트 형식이 중첩 형식에 비해 가독성이 좋다.
- 플루언트 형식으로 Collector를 연결하지 않고 Collector 생성을 Collectors 클래스의 여러 정적 메서드로 중첩함으로써 안쪽 그룹화가 처음 평가되고 코드에서는 반대로 가장 나중에 등장하게 된다.
10.3 자바로 DSL을 만드는 패턴과 기법
주식 거래 도메인 전용 언어로 주식 거래 도메인 모델에 사용할 API를 만들어보자.
먼저 주식거래 도메인 모델을 정의하자
주식 거래 도메인 모델은 세 가지로 구성: 주식(Stock), 거래(Trade), 주문(Order)
주식 종목을 모델링하는 순수 자바 빈즈 Stock
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;
}
}- 주어진 주식 가격에서 주어진 양의 주식을 사거나 파는 거래를 모델링하는 순수 자바 빈즈 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;
}
}- 고객이 요청한 한 개 이상의 거래를 담은 주문을 모델링하는 순수 자바 빈즈 Order
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();
}
}- 도메인 객체의 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");
trade1.setStock(stock2);
trade1.setPrice(375.00);
trade1.setQuantity(50);
order.addTrade(trade2);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();- 위의 메서드 체인이 동작하게 하기 위해선 플루언트 API로 객체를 만드는 몇 개의 빌더를 만들어야 한다.
- 메서드 체인 DSL을 제공하는 주문 빌더
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);
}
public MethodChainingOrderBuilder addTrade(Trade trade) {
order.addTrade(trade);
return this;
}
public TradeBuilder sell(int quantity) {
return new TradeBuilder(this,Trade.Type.SELL, quantity);
}
public Order end() {
return order;
}
}- 거래 빌더
public class TradeBuilder {
private final MethodChainingOrderBuilder builder;
public Trade trade = new Trade();
public 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 class StockBuilder {
private final MethodChainingOrderBuilder builder;
private final Trade trade;
private final Stock stock = new Stock();
public 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);
}
}- 실제 주식 거래 빌더
public class TradeBuilderWithStock {
private 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);
}
}코드에서 볼 수 있듯이 MethodChainingOrderBuilder가 끝날 때까지 다른 거래를 플루언트 방식으로 추가할 수 있다.
여러 빌드 클래스 특히 거래 빌더를 따로 만듦으로써 사용자가 미리 지정된 절차에 따라 플루언트 API의 메서드를 호출하도록 강제한다.
이 접근 방법은 정적 메서드 사용을 최소화하고 메서드 이름이 인수의 이름을 대신하도록 만듦으로 이런 형식의 DSL의 가독성을 개선하는 효과를 더한다.
이런 기법을 적용한 플루언트 API에는 분법적 잡음이 최소화된다.
메서드 체인을 이용한 DSL 패턴은 빌더를 구현해야 한다는 것이 단점이다.
상위 수준의 빌더를 하위 수준의 빌더와 연결할 많은 접착 코드가 필요하다.(결합도가 높다)
도메인의 객체 중첩 구조와 일치하게 들여쓰기를 강제하는 방법이 없다는 것도 단점이다.
10.3.2 중첩된 함수 이용
- 중첩된 함수 DSL 패턴은 다른 함수 안에 함수를 이용해 도메인 모델을 만든다.
Order anotherOrder = 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 type) {
Trade trade = new Trade();
trade.setQuantity(quantity);
trade.setStock(stock);
trade.setPrice(price);
trade.setType(type);
return trade;
}
public static Stock stock(String symbol, String market){
Stock stock = new Stock();
stock.setMarket(market);
stock.setSymbol(symbol);
return stock;
}
public static String on(String market) {
return market;
}
public static double at(double price) {
return price;
}
}메서드 체인에 비해 함수의 중첩 방식이 도메인 객체 계층 구조에 그대로 반영된다는 것이 중첩된 함수 DSL 패턴의 장점이다.
결과 DSL에 더 많은 괄호를 사용해야 한다는 단점이 있다.
더군다나 인수 목록을 정적 메서드에 넘겨줘야 한다는 제약도 있다.
도메인 객체에 선택 사항 필드가 있으면 인수를 생략할 수 있으므로 이 가능성 처리를 위해 여러 메서드 오버라이드를 구현해야 한다.
마지막으로 인수의 의미가 이름이 아니라 위치에 의해 정의되었다.
10.3.3 람다 표현식을 이용한 함수 시퀀싱
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(3755.00);
t.stock( s -> {
s.symbol("GOOGLE");
s.market("NASDAQ");
});
});
});- 함수 시퀀싱 DSL을 제공하는 주문 빌더
public class LamdaOrderBuilder {
private Order order = new Order();
public static Order order(Consumer<LamdaOrderBuilder> consumer){
LamdaOrderBuilder builder = new LamdaOrderBuilder();
consumer.accept(builder);
return builder.order;
}
public void forCustomer(String bigBank) {
order.setCustomer(bigBank);
}
public void buy(Consumer<TradeBuilder> consumer) {
trade(consumer, Trade.Type.BUY);
}
public void sell(Consumer<TradeBuilder> consumer) {
trade(consumer, Trade.Type.SELL);
}
private void trade(Consumer<TradeBuilder> consumer, Trade.Type type) {
TradeBuilder builder = new TradeBuilder();
builder.trade.setType(type);
consumer.accept(builder);
order.addTrade(builder.trade);
}
}public class TradeBuilder {
public 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(trade);
consumer.accept(builder);
}
}public class StockBuilder {
private final Trade trade;
private final Stock stock = new Stock();
public StockBuilder(Trade trade) {
this.trade = trade;
}
public void symbol(String symbol) {
stock.setSymbol(symbol);
}
public void market(String market) {
stock.setMarket(market);
}
}이 패턴은 많은 설정 코드가 필요하다며 DSL 자체가 자바 8 람다 표현식 문법에 의한 잡음의 영향을 받는다는 단점이 있다.
10.3.4 조합하기
여러 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))
);- 위의 주식 거래 주문은 중첩된 함수 패턴을 람다 기법과 혼용했다.
- TradeBuilder의 Consumer가 만든 각 거래는 람다 표현식으로 구현된다.
- 여러 형식을 혼합한 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 type){
TradeBuilder builder = new TradeBuilder();
builder.trade.setType(type);
consumer.accept(builder);
return builder;
}
}- 헬퍼 클래스 TradeBuilder와 StockBuilder는 내부적으로 메서드 체인 패턴을 구현해 플루언트 API를 제공한다. 이제 람다 표현식 바디를 구현해 가장 간단하게 거래를 구현할 수 있다.
public class TradeBuilder {
public Trade trade = new Trade();
public TradeBuilder quantity(int quantity) {
trade.setQuantity(quantity);
return this;
}
public TradeBuilder price(double price) {
trade.setPrice(price);
return this;
}
public StockBuilder stock(String symbol) {
return new StockBuilder(this, trade, symbol);
}
public void at(double price) {
trade.setPrice(price);
}
}public class StockBuilder {
private final TradeBuilder builder;
private final Trade trade;
private final Stock stock = new Stock();
public StockBuilder(TradeBuilder builder, Trade trade,
String symbol) {
this.builder = builder;
this.trade = trade;
stock.setSymbol(symbol);
}
public void symbol(String symbol) {
stock.setSymbol(symbol);
}
public void market(String market) {
stock.setMarket(market);
}
public TradeBuilder on(String market) {
stock.setMarket(market);
trade.setStock(stock);
return builder;
}
}10.3.5 DSL에 메서드 참조 사용하기
- 주식 거래 도메인 모델에 다른 간단한 기능을 추가해본다.
- 주문의 총 거래 가격에 0개 이상의 세금 정책을 적용해 최종값을 계산하는 기능을 추가해본다.
public class Tax {
public static double regional(double value) {
return value * 1.1;
}
public static double general(doublie 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(useSurcharge) value = Tax.surcharge(value);
return value;
}double value = calculate(order, true, false, true)- 이 구현은 가독성이 좋지 못하다. 불리언 변수의 올바른 순서를 기억하기도 어렵고 어떤 세금이 적용되었는지도 파악하기 어렵다.
- 플루언트 API를 이용해 불리언 플래그를 설정하는 최소 DSL을 제공하는 TaxCalculator로 주문의 총 거래 가격을 구하도록 하는 것이 더 좋은 방법이다.
public class TaxCalculator {
private boolean useRegional;
private boolean useGeneral;
private boolean useSurcharge;
public TaxCalculator withRegional() {
useRegional = true;
return this;
}
public TaxCalculator withGeneral() {
useGeneral = true;
return this;
}
public TaxCalculator withSurcharge() {
useSurcharge = true;
return this;
}
public double calculate(Order order) {
return calculate(order, useRegional, useGeneral, useSurcharge)
}
}double value = new TaxCalculator().withRegional()
.withSurcharge()
.calculate(order);- 코드가 장황하다는 것이 이 기법의 문제다.
- 도메인의 각 세금에 해당하는 불리언 필드가 필요하므로 확장성도 제한적이다.
- 자바의 함수형 기능을 이용하면 더 간결하고 유연한 방식으로 같은 가독성을 달성할 수 있다.
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());
}
}double value = new TaxCalculator().with(Tax::regional)
.with(Tax::surcharge)
.calculate(order);- 메서드 참조는 읽기 쉽고 코드를 간결하게 만들어준다. 새로운 세금 함수를 Tax 클래스에 추가해 도 함수형 TaxCalculator는 변경할 필요가 없이 바로 사용할 수 있는 유연성도 제공한다.
DSL 패턴의 장점과 단점

'Language > Java' 카테고리의 다른 글
| [OOP] 얕은 복사와 깊은 복사 (1) | 2025.07.20 |
|---|---|
| [Basic] 자바 기본 개념 개요 (3) | 2025.07.18 |
| [API] 람다와 스트림: Mordern Java - 9. 리팩터링, 테스팅, 디버깅 (4) | 2025.07.10 |
| [OOP] 내부 클래스와 익명 객체 (3) | 2025.07.09 |
| [API] 람다와 스트림: Mordern Java - 8. 컬렉션 API 개선 (0) | 2025.07.06 |