사전 D
구현체

D

gabury1고친 사람 github-actions[bot]

D 는 C 나 C++ 처럼 빠르게 도는 프로그램을 짜는 프로그래밍 언어입니다. 코드를 실행 전에 기계어로 컴파일합니다. C++ 보다 적은 코드로 같은 일을 하도록 언어가 수고를 덜어 줍니다. 메모리 정리는 기본으로 가비지 컬렉션이 맡습니다.

쉽고 빠른 이해

D 는 프로그램을 미리 기계어로 바꿔 실행 파일로 만드는 언어입니다. 명령줄 도구나 서버 프로그램을 C++ 만큼 빠르게 돌리려고 씁니다.

C++ 는 빠르지만 함수 선언을 두 파일에 나눠 적어야 합니다. 다 쓴 메모리도 개발자가 직접 돌려줘야 해서 실수가 잦습니다. D 는 속도를 지킵니다. 그리고 이런 수고를 언어 쪽으로 옮깁니다.

  1. 파일 하나가 묶음 하나라서 함수 선언을 두 번 적지 않습니다
  2. 다 쓴 메모리는 기본으로 언어가 알아서 치웁니다
  3. 테스트와 들어오고 나가는 값의 조건(계약)을 함수 옆에 적어 두면 실행할 때 검사됩니다

대가는 두 가지입니다. 메모리를 치우는 동안 프로그램이 잠깐 멈출 수 있습니다. 쓰는 사람이 적어서 필요한 라이브러리를 직접 짜야 할 때가 많습니다.

상세

D 는 C 와 C++ 의 문법을 이어받은 프로그래밍 언어입니다. 이 절은 D 가 C++ 의 어떤 수고를 덜었는지를 짧은 코드와 함께 차례로 봅니다.

미리 컴파일하는 언어

D 코드는 컴파일러가 기계어로 미리 바꿔 실행 파일로 만듭니다. 실행할 때 코드를 한 줄씩 읽어 가며 도는 인터프리터가 끼지 않습니다. JVM(Java Virtual Machine, 자바 가상 머신) 같은 가상 머신도 끼지 않습니다. 그래서 C 와 C++ 처럼 운영체제 위에서 바로 돕니다.

변수와 함수의 타입은 컴파일할 때 정해집니다. 이런 방식을 정적 타입이라고 부릅니다. 타입이 어긋난 코드는 실행 전에 걸립니다. 문법은 C 계열이라 중괄호로 블록을 묶고 문장 끝에 세미콜론을 붙입니다. Java 나 C 를 읽을 줄 알면 D 코드도 낯설지 않습니다.

헤더 대신 모듈

C 와 C++ 에서는 함수를 부르기 전에 그 함수의 선언이 먼저 나와 있어야 합니다. 선언은 함수 이름과 인자 타입만 적은 한 줄입니다. 함수가 실제로 하는 일까지 적은 것은 정의라고 부릅니다.

그래서 함수 하나를 두 곳에 적습니다. 선언은 헤더 파일에, 정의는 본문 파일에 둡니다. 다른 파일이 헤더 파일을 끌어와야 그 함수를 부를 수 있습니다.

끌어오는 일은 컴파일 전에 도는 전처리기가 맡습니다. 전처리기는 헤더 파일의 내용을 글자 그대로 복사해 붙입니다. 복사만 할 뿐 헤더의 선언과 본문의 정의가 같은지는 대조하지 않습니다. 그래서 두 곳에 적은 선언이 어긋나면 알아채기 어려운 오류가 납니다.

D 에서는 소스 파일 하나가 모듈 하나입니다. 함수와 클래스는 한 번만 정의합니다. 다른 모듈은 이것을 import 로 가져다 씁니다. 선언을 따로 적지 않으니 둘이 어긋날 일이 없습니다.

같은 파일 안에서도 선언을 먼저 적을 필요가 없습니다. 함수를 정의보다 앞에서 불러도 D 컴파일러가 알아서 찾습니다.

메모리 정리는 기본으로 가비지 컬렉션

프로그램은 힙에서 메모리를 얻어 씁니다. 다 쓰면 돌려줘야 합니다. C 와 C++ 에서는 이 반납을 개발자가 합니다. 반납을 잊으면 메모리 누수가 납니다. 두 번 반납하면 프로그램이 망가집니다.

D 는 이 반납을 기본으로 가비지 컬렉션에 맡깁니다. 가비지 컬렉션은 더 이상 아무도 가리키지 않는 메모리를 실행 중에 찾아서 대신 돌려주는 방식입니다. new 로 얻은 메모리는 개발자가 반납하지 않아도 됩니다.

가비지 컬렉션에도 약점이 있습니다. 버릴 메모리를 찾는 동안 프로그램이 잠깐 멈출 수 있습니다. 게임이나 음향 처리처럼 짧은 멈춤도 곤란한 프로그램에서는 이것이 문제가 됩니다.

그래서 D 는 가비지 컬렉션 없이 짜는 길도 둡니다. 함수에 @nogc 를 붙이면 그 안에서 가비지 컬렉션을 쓰는 코드를 컴파일러가 거절합니다. 이런 함수에서는 C 의 malloc 과 free 로 메모리를 직접 얻고 돌려줍니다. 대신 표준 라이브러리의 많은 부분이 가비지 컬렉션을 쓰므로 이 길에서는 쓸 수 있는 기능이 줄어듭니다.

배열과 슬라이스

D 의 배열은 자기 길이를 압니다. C 의 배열은 첫 칸의 주소일 뿐이라 길이를 따로 들고 다녀야 합니다. D 는 기본 설정에서 배열 밖을 읽으려 하면 실행 중에 오류를 내고 멈춥니다.

슬라이스는 배열의 한 구간을 가리키는 값입니다. 원소를 복사하지 않고 원래 배열의 메모리를 같이 봅니다. 그래서 큰 배열을 잘라 함수에 넘겨도 복사 비용이 들지 않습니다.

d
import std.stdio;

void main()
{
    int[] a = [1, 2, 3, 4, 5];
    int[] b = a[1 .. 3];
    writeln(b);      // [2, 3]
    b[0] = 20;
    writeln(a[1]);   // 20
}

a[1 .. 3] 은 1번 칸부터 3번 칸 바로 앞까지를 가리킵니다. 그래서 b 는 원소 둘짜리입니다. b[0] 을 고치자 a[1] 도 20 으로 바뀌었습니다. 두 이름이 같은 메모리를 보기 때문입니다.

아래 그림은 b 가 새 배열이 아니라는 것을 보여 줍니다. b 는 a 의 1번과 2번 칸을 그대로 가리킵니다.

flowchart TD
    B["슬라이스 b · 길이 2"] --> A1
    B --> A2
    subgraph A["배열 a"]
        A0["0번 · 1"] --- A1["1번 · 20"] --- A2["2번 · 3"] --- A3["3번 · 4"] --- A4["4번 · 5"]
    end

함수 옆에 적는 테스트와 계약

단위 테스트는 함수 하나를 떼어 놓고 입력과 기대한 결과를 맞춰 보는 테스트입니다. 다른 언어에서는 대개 테스트 라이브러리를 따로 들여야 합니다. D 는 unittest 블록을 언어가 직접 압니다.

d
int twice(int x) { return x * 2; }

unittest
{
    assert(twice(3) == 6);
}

unittest 블록은 검사할 함수 바로 옆에 둡니다. 테스트를 켜는 옵션을 주고 컴파일하면 프로그램이 본래 일을 시작하기 전에 이 블록들이 먼저 돕니다. 옵션 없이 컴파일하면 블록은 실행 파일에 들어가지 않습니다.

계약은 함수가 받는 값과 내놓는 값이 지켜야 할 조건을 함수 머리에 적어 두는 방식입니다. 이 생각은 계약에 의한 설계라는 이름으로 알려져 있습니다. D 에서는 in 에 들어오는 값의 조건을, out 에 나가는 값의 조건을 적습니다. 조건이 깨지면 실행 중에 바로 멈춰서 잘못된 값이 멀리 번지기 전에 잡힙니다.

컴파일하면서 함수를 돌리기

D 컴파일러는 평범한 함수를 컴파일 도중에 실행할 수 있습니다. 이 기능을 CTFE(Compile-Time Function Execution, 컴파일 시점 함수 실행)라고 부릅니다. 실행할 때마다 계산하던 값을 컴파일할 때 한 번만 계산해 실행 파일에 넣어 둡니다.

d
int square(int x) { return x * x; }

enum n = square(4);   // 16

D 의 enum 은 열거형을 만들 때만 쓰는 키워드가 아닙니다. 이름 붙은 상수 하나를 선언할 때도 씁니다. 이 상수의 값은 컴파일할 때 정해져야 합니다.

그래서 컴파일러가 square(4) 를 직접 돌려 16 을 얻습니다. 실행 파일에는 결과 16 만 들어갑니다. square 는 이 용도로 따로 짠 함수가 아닙니다. 실행 중에도 부를 수 있는 보통 함수입니다.

C 함수를 바로 부르기

함수를 부를 때 인자를 어디에 놓고 결과를 어디서 받는지 정한 약속을 호출 규약이라고 부릅니다. 두 언어의 호출 규약이 같으면 한쪽 언어로 짠 함수를 다른 쪽이 감싸는 코드 없이 부를 수 있습니다. D 는 C 의 호출 규약을 따를 수 있습니다.

d
import std.stdio;

extern (C) int abs(int x);

void main()
{
    writeln(abs(-7));   // 7
}

extern (C) 는 이 함수를 C 의 호출 규약으로 부르라고 컴파일러에 알립니다. 함수 본문은 C 라이브러리에 있습니다. 링커는 컴파일된 조각들을 실행 파일 하나로 묶는 도구입니다. 링커가 묶을 때 D 쪽 호출과 C 쪽 본문을 잇습니다.

위 예제의 abs 도 D 로 다시 짠 함수가 아닙니다. C 표준 라이브러리에 든 함수를 그대로 부른 것입니다. 이처럼 C 로 쓰인 라이브러리를 새로 짜지 않고 D 에서 씁니다.

D 가 맞는 곳과 안 맞는 곳

D 는 C++ 만큼 빨라야 하는데 코드는 짧게 두고 싶은 프로그램에 맞습니다. 명령줄 도구, 서버 프로그램, 데이터 처리 프로그램이 그런 예입니다. 기존 C 라이브러리를 바로 부를 수 있어서 C 로 된 코드가 많은 곳에서도 옮겨 가기 쉽습니다.

가비지 컬렉션의 멈춤을 견딜 수 없는 프로그램에는 덜 맞습니다. @nogc 를 쓰면 가비지 컬렉션 없이 짤 수 있습니다. 그 대신 표준 라이브러리의 많은 부분을 못 쓰게 됩니다. 이런 프로그램은 처음부터 가비지 컬렉션이 없는 C++ 나 Rust 를 고르는 경우가 많습니다.

쓰는 사람의 수도 따져야 합니다. D 는 C++ · Go · Rust 보다 쓰는 사람이 적습니다. 가져다 쓸 라이브러리도 적습니다. 필요한 라이브러리가 없으면 직접 짜거나 C 라이브러리를 불러 메워야 합니다.

관련 항목

D 를 이루는 언어 기능

모듈 · 슬라이스 · 단위 테스트 · 계약에 의한 설계 · CTFE · 템플릿 · 믹스인 · UFCS

D 코드를 실행 파일로 만드는 도구

컴파일러 · DMD · GDC · LDC · 링커 · DUB

D 가 바탕에 두는 개념

기계어 · 정적 타입 · 힙 · 가비지 컬렉션 · 호출 규약 · 헤더 파일 · 전처리기 · 메모리 누수

D 가 속하는 상위 분류

프로그래밍 언어 · 시스템 프로그래밍 · 컴파일 언어

같은 일을 두고 겨루는 시스템 프로그래밍 언어

C · C++ · Rust · Go · Zig · Nim

다른 이름: D 언어 · D 프로그래밍 언어 · Dlang · dlang