Scala로 DSL 흉내내보기

이용환
7 min readOct 13, 2018

--

Gradle을 사용하면서 DSL(Domain Specific Language, 도메인 특화 언어)을 접하게 되었다.

빌드 스크립트를 만드는 것과 같이 ‘한정된 주제’ 내에서 ‘반복적인 일’을 하는 상황에서는 일반적인 프로그래밍 언어보다 DSL을 만들어 제공하는 것이 매우 합리적이라는 생각이 든다.

데이터 분석에서 비개발자인 사용자분들은 SQL을 통해 데이터를 추출할 수 있는 하이브(Apache Hive)를 많이 사용하는데, 아무래도 SQL 자체가 정적이다 보니 한계가 있을 수 있다.

그러던 도중 ‘Scala를 통해 DSL을 만들고, 이를 비개발자인 사용자분들에게 제공하면 동적으로 데이터 분석을 편하게 할 수 있지 않을까?’ 라는 생각이 들었다.

그래서 간단하게 Scala를 통해 DSL 형식으로 SQL문을 만들어보는 예제를 작성해보았다.

예제 수준이므로 모든 SQL문 포팅하기는 힘들고, 간단히 SELECT, WHERE, GROUPBY Operation만을 지원하는 DSL을 만들어볼 예정이다.

케이스 클래스(Case Class)와 패턴 매칭(Pattern Matching)

Scala에서는 케이스 클래스(Case Class)와 패턴 매칭(Pattern Matching)이라는 기능을 제공한다.

Java의 case문의 경우 값에 대한 분기 밖에 제공하지 않지만, Scala의 경우 객체의 형태에 따른 분기 또한 제공한다.

이 글에서 스칼라의 문법 설명을 하기는 어렵기 때문에, 해당 개념이 궁금하다면 케이스 클래스패턴 매칭 페이지를 확인해보기를 바란다.

아래 코드는 패턴 매칭을 통해 AbstractFile이라는 클래스를 상속받은 디렉토리(Directory)과 파일(ConcreteFile)를 구분하는 예시이다.

코드를 실행하면 아래와 같은 결과를 얻을 수 있다.

This is Directory. Directory Name: /home/leeyh0216
This is Concrete File. Parent Directory: /home/leeyh0216, File Name: test.out

이 기능을 활용하여 SQL에서 지원하는 Operation 들의 결합 가능성에 대해 체크할 예정이다.

연산자(Operation) 정의하기

위에서 소개한 케이스 클래스와 패턴 매칭 기능을 활용하여, SQL의 구문 3개(SELECT, WHERE, GROUPBY)를 Operation으로 정의해보았다.

일단 모든 Operation들을 추상화하는 Operation이라는 Trait을 정의한다.

그 후 우리가 원하는 3가지 Operation(SELECT, WHERE, GROUPBY)를 케이스 클래스로 정의한다.

Select

SELECT은 테이블을 조회하는데에 사용되는 가장 기본적인 구문이다.

Select 클래스의 생성자는 테이블에서 선택할 컬럼 목록(columns: String*)을 인자로 받을 수 있게 해 놓았다.

Select 클래스의 from 메서드는 테이블명(tableName: String)을 인자로 전달받아 테이블(Table) 객체를 초기화하여 반환한다. 테이블 클래스는 아래에서 설명할 예정이다.

Select Object의 경우 Select 객체를 생성할 수 있는 함수인 select를 제공한다.

new를 통해 새로운 Select 객체를 생성하는 대신, select 함수를 이용하여 좀 더 DSL 처럼 보이게 할 예정이다.

WhereOperation, GroupByOperation

WhereOperation의 경우 WHERE 구문을 추상화한 클래스이며, GroupByOperation의 경우 GROUPBY 구문을 추상화한 클래스이다.

WhereOperation은 WHERE절(whereClause: String)을 생성자 인자로 전달받고, GroupByOperation은 그룹핑을 수행할 컬럼 목록(keyCols: String*)을 생성자 인자로 전달받는다.

또한 3개 클래스(Select, WehreOperation, GroupByOperation) 모두 toString() 메서드를 Override하여 문자열 형태로 구문을 생성할 수 있다.

테이블(Table) 정의하기

테이블 클래스는 우리가 만드는 SQL DSL의 핵심 클래스로써, DSL 구문의 Context라고 볼 수 있다.

필드 구성

테이블 클래스의 필드는 OperationStack 객체만 존재한다.

OperationStack은 테이블에 적용되는 SQL 구문들을 저장하는 스택(Stack) 객체이다.

메소드 구성

메소드는 테이블을 생성하는데 사용되는 select을 제외한 where과 groupby, 그리고 and가 있다.

테이블 클래스는 빌더 패턴으로 만들어져 있기 때문에 toString() 메소드를 제외한 모든 메소드는 테이블 객체 자신을 반환하도록 만들어져 있다.

  1. where 메소드

테이블에 WhereOperation을 적용한다. 인자로 전달받은 whereCluase(String)을 이용해 WhereOperation 객체를 생성하여 OperationStack에 집어넣는다.

2. groupby 메소드

테이블에 GroupByOperation을 적용한다. 인자로 전달받은 keyCols(String*)을 이용해 GroupByOperation을 생성하여 Operation 스택에 집어넣는다.

3. and 메소드

WHERE 절을 구성하는 조건이 여러 개 있을 경우 and 메소드를 사용할 수 있다.

만일 OperationStack이 비어 있을 경우 AND 구문을 사용할 수 없으므로 IllegalArgumentException을 발생시키도록 require구문을 넣어놓았다.

OperationStack이 비어있지 않을 경우 맨 위의 Operation을 선택하여 패턴매칭을 수행한다.

만일 선택된 Operation이 WhereOperation일 경우 기존 WhereOperation의 WhereCluase에 새로운 AND 구문을 추가한 WhereClause 객체를 생성하여 반환하여 OperationStack의 맨 위에 넣는다.

선택된 Operation이 WhereOperation이 아닐 경우 IllegalArgumentException을 발생시킨다.

4. toString 메소드

다른 빌더 클래스들이 .build()를 제공하는 것과 다르게 Table 클래스에서는 toString()을 Overriding하여 생성된 구문을 반환하도록 하였다.

DSL 구문 작성해보기

위에서 만든 클래스들을 이용하여 DSL 구문을 작성/테스트 해보았다.

스칼라에서는 함수로 전달되는 인자가 1개일 경우, 함수를 감싸는 ()을 사용하지 않아도 되기 때문에 위와 같이 DSL 처럼 표현할 수 있다.

위 테스트의 결과는 아래와 같다.

SELECT
a,b
FROM
tbl
GROUPBY (a,b)
WHERE a is not null and b is null

requirement failed: 빈 구문에 and를 적용할 수 없습니다.
and 구문을 사용할 수 없는 조건입니다.

간단하게 Scala의 케이스 클래스, 패턴매칭과 빌더 패턴을 이용하여 DSL을 만들어보았다.

이렇게 Scala 를 통해 DSL을 만들고, Spark SQL과 Core에 결합하면 ‘비 개발자들도 동적으로 데이터 분석이 가능하지 않을까?’ 라는 생각이 든다.

좀 더 발전시켜서 프로젝트화 해보고 싶다는 생각 또한 들었다.

--

--