글 목록

Unibase: SQL과 언어를 통합하는 다음 세대의 언어

SQL을 1급 Statement로 다루고 여러 언어와 플랫폼으로 변환하는 Unibase를 소개합니다. 데이터 타입, 패키지, 클래스·상속, 제어문의 기본 문법과 ERWeb에서 집중하는 역할을 살펴봅니다.

이 글의 내용 7개 항목

첫 글에서는 사람과 AI가 함께 볼 개발 기준을 만들고 싶다는 생각과 ERWeb을 시작한 배경을 소개했습니다. 이번에는 ERWeb과 함께 개발하고 있는 언어, Unibase를 이야기하려 합니다.

Unibase는 객체 지향 설계 원칙을 바탕으로 명료한 코드를 작성하고, 이를 여러 언어와 플랫폼으로 이어 가기 위한 언어입니다. 자체 컴파일러를 갖춘 컴파일 언어이면서, 다른 언어의 소스로 변환하는 트랜스파일러를 함께 갖추고 있습니다. 여기에 SQL을 언어의 1급 Statement로 통합해 애플리케이션 로직과 데이터 처리를 함께 표현합니다.

명료한 코드를 위한 문법과 구조

개발자는 코드를 작성할 때 기능의 의도를 생각하면서도, 선택한 언어와 프레임워크의 규칙에 맞춰 표현해야 합니다. 같은 의도를 다른 플랫폼에 옮길 때에는 그곳의 문법과 구조에 맞춰 다시 작성하는 일이 생깁니다.

제가 Unibase에서 지향하는 것은 객체 지향 프로그래밍(OOP)의 설계 원칙을 준수하며 명료한 코드를 작성할 수 있는 문법과 구조입니다. 클래스와 타입, 객체 사이의 관계를 분명하게 표현하고, 읽는 사람이 코드의 의도와 책임을 쉽게 따라갈 수 있도록 만들고 있습니다.

코드를 관리하는 구조는 Java의 패키지 구조를 따릅니다. 제가 Java의 패키지 구조를 가장 검증된 코드 관리 방법론이라고 생각하기 때문입니다. 패키지로 소속을 정리하고, 클래스와 인터페이스의 역할과 접근 범위를 드러내는 방식을 바탕으로 삼았습니다.

모던한 문법을 채용하면서도 키워드가 불필요하게 늘어나지 않도록 신경을 썼습니다. 코드가 다른 언어보다 짧아질 수는 있지만, 문법을 축약하거나 괄호를 없애는 것을 목표로 삼지는 않습니다. 조건문과 반복문에서는 괄호와 중괄호로 조건과 실행 범위를 명시합니다.

라이브러리는 다른 언어로 변환할 수 있도록 공통화하고, 기능과 인터페이스를 명료하게 정리하는 방향으로 설계했습니다. 사람과 AI가 함께 코드를 읽고 다루는 시대에도 프로그램의 의미와 관계를 분명하게 이해할 수 있는 기반을 만들고자 합니다.

Unibase의 기본 Syntax를 살펴보면

Unibase는 익숙한 객체 지향 구조를 바탕으로 코드를 작성합니다. 아래에서는 데이터 타입부터 패키지, 클래스, 상속, 제어문까지 기본 문법을 짧은 예제로 살펴보겠습니다. 변수 선언과 제어문 예제는 메서드 안에서 사용하는 코드입니다.

소스 파일과 프로그램의 시작점

소스 파일은 UTF-8로 작성하며 .uni 확장자를 사용합니다. 클래스와 메서드의 본문은 중괄호로 구분하고, 문장 끝에는 세미콜론을 붙입니다. 프로그램의 시작점은 static void main()입니다.

class Hello {
    static void main() {
        print("Hello, Unibase!");
    }
}

한 줄 주석은 //, 여러 줄 주석은 /* ... */로 작성합니다.

데이터 타입과 변수 선언

변수는 타입을 먼저 쓰고 이름과 값을 이어서 작성합니다. var를 사용하면 초기값에서 타입을 추론합니다.

타입·표기 표현하는 값이나 역할
byte, short, int, long 정수
float, double 실수
boolean, bool 참·거짓. bool은 boolean의 별칭
char 한 문자
string 문자열
string?, int?, User? null을 허용하는 타입
List<T> 원소 타입을 지정한 순서 있는 목록
Set<T> 원소 타입을 지정한 집합
Map<K, V> 키와 값의 타입을 지정한 맵
void 메서드가 값을 반환하지 않음을 표시
int count = 3;
long id = 42L;
float ratio = 0.5f;
double amount = 12.5;
bool ready = true;
char grade = 'A';
string name = "Kim";
string? nickname = null;
var inferredCount = 1;
final int maxCount = 10;

L과 f는 각각 long과 float 리터럴을 표시합니다. ?는 값이 없을 수 있음을 타입에 표현하고, final은 변수에 다른 값을 다시 대입할 수 없도록 합니다. var도 추론된 타입을 갖는 변수입니다.

컬렉션과 제네릭

목록은 [ ... ], 집합과 맵은 { ... }으로 작성할 수 있습니다. <T>나 <K, V>에는 실제로 사용할 타입을 넣습니다.

List<int> ids = [1, 2, 3];
Set<string> tags = {"Unibase", "SQL"};
Map<string, int> counts = {"new": 1, "done": 2};

ids.add(4);
int firstId = ids.get(0);

위 예제의 List<int>는 정수 목록이고, Map<string, int>는 문자열 키와 정수 값을 연결합니다. 컬렉션의 원소와 메서드에서 사용하는 타입을 함께 표현하는 방식입니다.

패키지와 import, 라이브러리 묶음

package는 소스가 속한 이름 공간을 선언하고, import는 사용할 타입을 가져옵니다. 패키지 이름은 점으로 구분하며, 특정 타입이나 *로 패키지의 타입을 가져올 수 있습니다.

package app.greetings;

import unibase.collections.List;

public class Greeting {
    public List<string> messages() {
        return ["Hello", "Unibase"];
    }
}

import unibase.collections.*;처럼 작성할 수도 있습니다. 공개할 클래스에는 public을 명시합니다. 최상위 클래스와 인터페이스는 기본적으로 같은 패키지 안에서 사용하는 범위이며, 클래스 멤버의 기본 접근 범위는 private입니다.

라이브러리를 묶는 도구는 unic pack입니다. 예를 들어 위 소스를 Greeting.uni로 저장하면 다음과 같이 라이브러리 검사를 하고 이름·버전을 지정해 .ulib 파일로 묶을 수 있습니다.

unic check Greeting.uni --library
unic pack Greeting.uni -o greetings.ulib --name greetings --version 0.1.0

--library 검사는 실행 시작점 없이 라이브러리 소스의 타입과 선언을 확인합니다. 현재 .ulib에는 패키지 이름·버전과 공개 타입 메타데이터가 담깁니다. 소스에서 이름을 관리하는 package·import와, 라이브러리를 파일로 묶는 pack은 서로 다른 역할입니다.

클래스, 생성자와 메서드

클래스에는 필드, 생성자, 메서드를 작성합니다. 생성자는 클래스와 같은 이름을 사용하고, 메서드는 반환 타입과 이름을 먼저 선언합니다. this는 현재 객체를 가리킵니다.

class User {
    private string name;
    private int age;

    public User(string name, int age) {
        this.name = name;
        this.age = age;
    }

    public string getName() {
        return this.name;
    }

    public bool isAdult() {
        return this.age >= 18;
    }
}

객체를 만들 때에는 클래스 이름으로 생성자를 호출합니다. 인자를 순서대로 전달하거나 이름을 붙여 전달할 수 있습니다.

var user = User(name: "Kim", age: 20);
string userName = user.getName();
bool adult = user.isAdult();

접근 범위는 public, private, protected로 명시합니다. static은 객체 인스턴스가 아닌 클래스에 속한 메서드 등에 사용하며, 앞의 main이 그 예입니다.

상속과 인터페이스

extends로 부모 클래스를 지정하고, implements로 구현할 인터페이스를 지정합니다. 인터페이스는 구현 클래스가 제공할 메서드의 계약을 표현합니다.

class Entity {
    protected long id;
}

interface Named {
    public string getName();
}

class Member extends Entity implements Named {
    public string getName() {
        return "Member";
    }
}

Member는 Entity를 상속하고, Named가 요구하는 getName()을 구현합니다. 데이터와 동작의 공통 구조, 외부에 제공할 계약을 각각 표현할 수 있습니다.

조건문과 반복문

조건 분기는 if, else if, else로 작성합니다. 조건에는 괄호를 사용하고, 실행할 본문은 중괄호로 구분합니다.

int count = 3;

if (count > 0) {
    print("Ready");
} else if (count == 0) {
    print("Empty");
} else {
    print("Invalid");
}

아래 예제에서는 for, while, do ... while로 반복을 작성합니다. for는 초기값·조건·갱신식을 함께 표현하고, while은 조건을 먼저 확인합니다. do ... while은 본문을 한 번 실행한 뒤 조건을 확인합니다.

for (int i = 0; i < 5; i = i + 1) {
    if (i == 1) {
        continue;
    }
    if (i == 4) {
        break;
    }
    print(i);
}

int remaining = 2;
while (remaining > 0) {
    remaining = remaining - 1;
}

do {
    remaining = remaining + 1;
} while (remaining < 2);

continue는 현재 회차의 나머지 처리를 건너뛰고 다음 반복으로 이어 갑니다. break는 반복을 끝냅니다. return은 메서드의 실행을 마치며 필요한 경우 값을 반환합니다.

예외 처리

예외는 throw로 발생시키고, try·catch로 처리합니다.

try {
    throw RuntimeException("Invalid count");
} catch (RuntimeException error) {
    print("Check the count");
}

이처럼 데이터의 타입, 객체의 구조, 실행 흐름을 소스에서 명확하게 표현하는 것이 Unibase 문법의 기본 방향입니다.

자세한 문법과 소스 구성, Standard 라이브러리 설명은 Unibase 매뉴얼에서 확인해 주세요.

컴파일 언어이자, 여러 언어로 이어지는 출발점

Unibase에는 두 가지 경로가 있습니다. 컴파일러는 Unibase 소스를 실행 가능한 결과물로 만들고, 트랜스파일러는 Unibase 소스를 다른 언어의 소스로 변환합니다.

컴파일 경로에서는 Unibase 자체의 문법과 의미를 바탕으로 프로그램을 실행할 수 있도록 만듭니다. 현재 컴파일러는 LLVM을 사용하는 경로를 갖고 있습니다.

트랜스파일 경로에서는 작성한 프로그램을 대상 언어와 플랫폼의 구조에 맞춰 옮깁니다. ERWeb과 함께 사용하는 웹 개발 흐름에서는 Vue·React·Svelte 같은 프론트엔드와 Java·Spring Boot, Python·FastAPI 같은 백엔드의 소스 생성으로 이어집니다.

이때 중요한 것은 원래 작성한 프로그램의 의미와 관계를 결과물에 담는 일입니다. 언어와 플랫폼마다 구조와 실행 방식이 다르기 때문에, 각 대상의 특성에 맞는 변환이 필요합니다. 지원 대상을 넓히는 일도 그 대상에 맞는 코드 생성과 검증을 함께 진행하는 과정입니다.

SQL을 언어의 1급 Statement로 제공합니다

Unibase의 중요한 특징은 SQL이 언어 안에서 직접 다뤄지는 1급 Statement라는 점입니다. Statement는 프로그램에서 하나의 작업을 표현하는 문장을 뜻합니다. Unibase에서는 SQL 문장도 언어가 이해하는 프로그램의 일부가 됩니다.

많은 개발 환경에서 SQL은 문자열이나 별도 매핑 파일에 작성됩니다. 프로그램의 변수와 SQL의 매개변수, 조회 결과와 애플리케이션의 타입을 연결하는 과정도 필요합니다.

Unibase는 SQL을 직접 해석하고, 그 구조를 언어의 처리 과정에 연결합니다. 데이터를 조회하는 SQL과 그 결과를 사용하는 로직을 같은 소스 안에서 표현하고, 변수 바인딩과 결과 타입의 관계도 함께 다룰 수 있도록 설계하고 있습니다.

예를 들어 주문 조회 기능에는 조회 조건, 필요한 데이터, 조회 결과를 사용하는 로직이 함께 있습니다. Unibase는 이 관계를 SQL과 언어가 통합된 구조로 표현하려 합니다. 개발자는 SQL로 데이터 처리 의도를 드러내고, 언어로 그 결과를 사용하는 동작을 이어서 작성할 수 있습니다.

이 통합은 도구가 프로그램을 이해하는 범위에도 영향을 줍니다. SQL의 구조와 언어의 타입을 함께 다루면 매개변수와 결과의 연결을 확인하고, 오류가 발생한 소스 위치를 설명하는 기반을 만들 수 있습니다.

직접 SQL Statement를 작성하는 모습도 살펴보겠습니다. 다음 코드는 메서드 안에서 사용하는 문법 예제입니다.

int _id = 1;
ResultSet rows = SELECT name FROM users WHERE id = _id;

위 예제에서 SELECT는 언어가 해석하는 SQL Statement이며, 조회 결과는 ResultSet으로 연결됩니다. _id는 SQL에 바인딩하는 변수입니다. 실제 조회에는 대상 테이블과 데이터베이스 연결이 필요합니다.

AI 시대를 준비하는 언어

AI가 코드를 작성하고 수정하는 과정에서도 데이터 구조와 프로그램의 의미를 정확히 이해하는 일이 필요합니다. 조회 결과의 필드가 바뀌면 그 값을 사용하는 로직도 검토해야 하고, 기능의 조건이 바뀌면 관련 SQL도 확인해야 합니다.

저는 SQL과 언어를 통합한 구조가 사람과 AI가 이런 관계를 함께 이해하는 데 도움이 될 수 있다고 생각합니다. 같은 소스에서 데이터 처리와 애플리케이션 로직을 읽고, 언어가 해석한 구조를 바탕으로 변경과 검증을 이어 갈 수 있기 때문입니다.

Unibase가 AI 시대를 준비하는 다음 세대의 언어를 지향하는 이유도 여기에 있습니다. 명료한 코드로 표현한 의도를 사람과 AI가 함께 이해하고, 여러 실행 환경으로 이어 갈 수 있는 언어를 만들고자 합니다.

지금은 ERWeb과 결합해 트랜스파일러에 집중합니다

Unibase의 방향은 컴파일 언어와 트랜스파일러를 모두 포함합니다. 현재는 ERWeb과 융합해, 모델링과 소스를 실제 개발에 사용할 수 있는 코드로 변환하는 트랜스파일러 역할에 집중하고 있습니다.

ERWeb은 요구사항과 설계, 모델 사이의 관계를 정리하고 편집하는 환경을 제공합니다. Unibase는 작성한 소스의 문법과 의미를 해석하고, 선택한 언어와 플랫폼에 맞게 변환하는 역할을 맡습니다.

ERWeb의 모델링 소개 글에서 살펴본 요구사항과 설계의 연결은 이 흐름의 앞부분입니다. 그 뒤에서 Unibase가 프로그램의 의미와 데이터 처리를 표현하고 코드 변환을 담당하며, 설계와 구현을 이어 가는 기반이 됩니다.

이 방향은 명료하게 작성한 코드와 실제 프로젝트에서 사용하는 기술을 연결하기 위한 선택입니다. ERWeb에서 정리한 모델과 Unibase 소스를 바탕으로 대상 환경의 코드를 생성하고, 생성한 결과를 확인하며 변환 범위를 넓혀 가고 있습니다.

작성한 의도를 더 오래 이어 가기 위해

개발 환경과 기술은 계속 바뀝니다. 그 안에서 해결하려는 문제와 데이터의 의미, 기능 사이의 관계는 다음 구현으로 이어져야 합니다.

Unibase를 통해 제가 만들고 싶은 것은 OOP 설계 원칙과 Java의 패키지 구조를 바탕으로 명료한 코드를 작성하고, SQL과 로직을 함께 다루며, 그 표현을 여러 언어와 플랫폼으로 이어 갈 수 있는 기반입니다. 현재는 ERWeb과 함께 이 방향을 코드 변환에 집중해 구체화하고 있습니다.

앞으로 이 블로그에서는 실제 Unibase 문법과 SQL 작성 방식, ERWeb에서 작성한 내용을 다른 언어의 코드로 변환하는 사례를 차례로 소개하겠습니다.

댓글

GitHub 계정으로 의견과 질문을 남겨 주세요. 댓글은 공개됩니다.

GitHub에서 댓글 보기 (새 창)