본문 바로가기
카테고리 없음

Custom DSL 읽기 쉬운 코드 설계

by sujupark54 2026. 2. 14.

코딩하는 모습

한 번 작성된 코드는 생각보다 오래 살아남는다. 코드는 쓰는 시간보다 읽히는 시간이 훨씬 길다. 한 명의 개발자가 작성한 코드는 여러 명의 개발자가 여러 번 읽고, 수정하고, 확장한다.

그래서 좋은 코드는 “동작하는 코드”를 넘어서 의도를 쉽게 파악할 수 있는 코드여야 한다. 이때 도움이 되는 개념이 바로 Custom DSL이다.

DSL은 개발 언어를 모르는 사람이라도 비즈니스 용어만 알고 있다면 대략적인 의미를 이해할 수 있도록 돕는다. DDD에서 말하는 유비쿼터스 언어를 코드로 옮기는 가장 현실적인 방법이기도 하다.


Custom DSL 고수준 코드란 무엇인가

다음은 주어진 횟수만큼 메시지를 출력하는 가장 전통적인 코드다.

for (int i = 0; i < count; i++) {
    System.out.println(message);
}

이 코드는 정확히 동작하지만, 자바를 처음 보는 사람에게는 의미가 잘 전달되지 않는다.

같은 기능을 아래와 같이 표현하면 어떨까.

Example1
    .say("Hello, DSL!")
    .times(3)
    .print();

자바 문법을 몰라도 “무엇을 하는 코드인지”는 충분히 유추할 수 있다. 이처럼 자연어에 가깝게 읽히는 코드를 고수준 코드라고 부른다.

Custom DSL은 저수준의 구현 세부 사항을 숨기고 “무엇을 한다”에만 집중하게 만들어준다. 그 결과 코드의 잡음은 줄고, 의도는 또렷해진다.


Custom DSL 동작 파라미터화 활용

Custom DSL을 만들기 위해 가장 중요한 개념 중 하나가 동작 파라미터화다.

동작 파라미터화란 “아직 어떻게 실행될지 결정되지 않은 행위를 외부에서 전달하는 것”이다. 자바에서는 람다 표현식과 함수형 인터페이스로 이를 구현할 수 있다.

예제로 아주 작은 토마토 애플리케이션을 생각해보자. 토마토는 색상, 크기, 용도를 가지고 있고 조건에 따라 필터링되어야 한다.

가장 단순한 구현은 메서드 오버로딩이다.

public List<Tomato> findAll(Color color, Size size) { ... }
public List<Tomato> findAll(Color color, Size size, Usage usage) { ... }

하지만 조건이 늘어날수록 메서드는 폭발적으로 증가한다. 유지보수는 점점 어려워진다.

이를 동작 파라미터화로 바꾸면 구조가 달라진다.

public List<Tomato> findAll(Predicate<Tomato> condition) {
    return repository.findAll()
        .stream()
        .filter(condition)
        .toList();
}

이제 필터 조건은 외부에서 자유롭게 조합할 수 있다. AND, OR 조건도 메서드 추가 없이 해결된다.

이 방식은 DSL의 기초가 된다. “무엇을 필터링할지”를 코드가 아니라 행위로 전달하는 것이다.


Custom DSL 검증 로직 설계

단순한 CRUD 애플리케이션이라도 검증 로직은 반드시 필요하다.

예를 들어 토마토를 저장할 때 다음 조건이 있다고 가정해보자.

  • 익명 사용자는 생성 불가
  • 이름 길이 제한
  • 특정 이름 사용 금지
  • 토마토 용도는 반드시 DANCER

이를 단순 if 문으로 작성하면 금방 길고 읽기 어려운 코드가 된다.

if (users.type() == ANONYMOUS) ...
if (users.name().length() > 5) ...
if (tomato.usage() != DANCER) ...

이 방식은 동작하지만 요구사항이 늘어날수록 수정 비용이 급격히 증가한다.

Custom DSL을 적용하면 검증을 다음과 같이 표현할 수 있다.

TomatoCustomDsl.action(tomato, users)
    .validUsers(checkType())
    .validUsers(checkNameLength())
    .validTomato(checkUsage())
    .save(repository::save);

이 코드는 요구사항 문서를 그대로 읽는 느낌을 준다. 누락된 조건도 한눈에 보인다.

검증 로직은 Predicate, Consumer 같은 표준 함수형 인터페이스로 분리된다. 그 결과 재사용성과 테스트 용이성도 크게 향상된다.


Custom DSL 유지보수와 확장성

Custom DSL의 가장 큰 장점은 변경과 확장에 강하다는 점이다.

검증 순서를 바꾸거나, 특정 조건을 제거하거나, 새로운 검증을 추가해도 기존 DSL 구조를 거의 수정하지 않는다.

.validTomato(checkUsage())
.validUsers(checkNameLength())
.validUsers(checkType())

또한 람다 표현식을 사용하면 DSL 자체를 수정하지 않고도 외부 의존성을 자연스럽게 끼워 넣을 수 있다.

이 구조는 헥사고날 아키텍처의 포트와 어댑터 개념과도 잘 어울린다. DSL은 어댑터 역할을 하고, 검증 로직은 포트처럼 결합된다.

물론 단점도 있다. 초기 설계 비용이 들고, 추상화에 익숙하지 않다면 읽기 어려울 수 있다.

하지만 한 번 구조가 잡히면 코드는 짧아지고, 의미는 또렷해지며, 변경에 대한 두려움은 크게 줄어든다.


마무리 정리

Custom DSL은 문법을 꾸미는 기술이 아니다. 의도를 코드로 드러내는 설계 방식이다.

동작 파라미터화, 함수형 인터페이스, 메서드 참조를 적절히 사용하면 CRUD 중심의 애플리케이션에서도 읽기 쉬운 코드와 유지보수 가능한 구조를 만들 수 있다.

모든 프로젝트에 DSL이 필요한 것은 아니다. 하지만 코드가 자주 읽히고, 요구사항이 자주 바뀌는 영역이라면 Custom DSL은 충분히 고려해볼 만한 선택지다.