모듈

모듈 (Modules)

D 프로그램을 구성하는 기본 단위인 모듈과, 그 모듈을 가져오는 import의 다양한 형태를 다루는 페이지입니다. 모듈 선언, 심볼 이름 찾기, public/static/renamed/selective/scoped import, 모듈 스코프 연산자, 정적 생성·소멸 순서, mixin, 패키지 모듈까지 다룹니다.

출처: Modules — D Language Specification

본문

문법 (Grammar)

Module:
 ModuleDeclaration
 ModuleDeclarationopt DeclDefs

DeclDefs:
 DeclDef
 DeclDef DeclDefs

DeclDef:
 AttributeSpecifier
 AliasAssign
 Declaration
 AggregateMember
 UnitTest
 StaticConstructor
 StaticDestructor
 SharedStaticConstructor
 SharedStaticDestructor
 DebugSpecification
 VersionSpecification
 MixinDeclaration
 EmptyDeclaration

AggregateMember:
 Constructor
 NewDeclaration
 Destructor
 Postblit
 Invariant
 AliasThis

EmptyDeclaration:
 ;

모듈은 보통 하나 이상의 DeclDef 선언을 가집니다.

Declaration은 함수 본문 안의 Statement로 허용되는 유일한 DeclDef입니다. DeclDef는 또한 집계 타입 선언과 템플릿의 멤버이기도 합니다. AggregateMember는 (구문상 집계 타입 밖에서도 유효하지만) 의미상으로는 집계 타입 멤버로만 유효합니다.

개요 (Overview)

모듈은 소스 파일과 일대일 대응합니다. ModuleDeclaration으로 명시적으로 설정하지 않으면, 모듈 이름은 기본적으로 파일 이름에서 경로와 확장자를 뗀 이름이 됩니다.

모듈의 이름은 자동으로 그 내용물의 네임스페이스 스코프 역할을 합니다. 모듈은 표면적으로 struct와 유사하지만 다음 점에서 다릅니다:

  • 모듈의 인스턴스는 하나만 존재하며 정적으로 할당된다.
  • 소스 파일은 하나의 모듈만 포함할 수 있다.
  • 모듈의 심볼은 import 될 수 있다.
  • 모듈은 항상 전역 스코프에서 컴파일되며 저장 클래스나 다른 수식어의 영향을 받지 않는다.
  • 모듈은 패키지(package)라고 부르는 계층으로 그룹화될 수 있다.

모듈은 여러 보장(guarantee)을 제공합니다:

  • 모듈이 import 되는 순서는 그 의미론에 영향을 주지 않는다.
  • 모듈의 의미론은 그것이 import 되는 스코프에 영향을 받지 않는다.
  • 모듈 C가 모듈 A와 B를 import 하면, B에 대한 어떤 수정도 A에 의존하는 C의 코드를 조용히 바꾸지 않는다.

모듈 선언 (Module Declaration)

ModuleDeclaration은 모듈의 이름과 그것이 속한 패키지를 설정합니다. 없으면 모듈 이름은 소스 파일 이름(경로와 확장자를 뗀)과 같은 이름으로 취해집니다.

ModuleDeclaration:
 ModuleAttributesopt module ModuleFullyQualifiedName Editionopt ;

ModuleAttributes:
 ModuleAttribute
 ModuleAttribute ModuleAttributes

ModuleAttribute:
 DeprecatedAttribute
 UserDefinedAttribute

ModuleFullyQualifiedName:
 ModuleName
 Packages . ModuleName

ModuleName:
 Identifier
Packages:
 PackageName
 Packages . PackageName

PackageName:
 Identifier

가장 오른쪽 Identifier 앞에 오는 Identifier들은 모듈이 들어 있는 Package입니다. 패키지는 소스 파일 경로의 디렉토리 이름에 대응합니다. 패키지와 모듈 이름은 Keyword일 수 없습니다.

ModuleDeclaration이 있으면 소스 파일의 첫 번째이자 유일한 그러한 선언이어야 하며, 주석과 #line 지시문만이 앞에 올 수 있습니다.

예:

module c.stdio; // module stdio in the c package

관례상 패키지와 모듈 이름은 모두 소문자입니다. 이는 이 이름들이 운영체제의 디렉토리·파일 이름과 일대일 대응하고, 많은 파일 시스템이 대소문자를 구분하지 않기 때문입니다. 모두 소문자 패키지·모듈 이름을 사용하면 서로 다른 파일 시스템 사이로 프로젝트를 옮길 때 문제를 피하거나 최소화할 수 있습니다.

모듈의 파일 이름이 유효한 모듈 이름이 아니면(예: foo-bar.d), 모듈 선언으로 유효한 모듈 이름을 설정하세요:

module foo_bar;

구현 정의 (Implementation Defined): 패키지와 모듈 식별자를 디렉토리·파일 이름에 매핑하는 방법.

모범 사례 (Best Practices): PackageNameModuleName은 최대의 이식성과 다양한 파일 시스템 호환성을 위해 ASCII 소문자, 숫자, _로 구성되어야 합니다. 패키지·모듈의 파일 이름도 ASCII 소문자, 숫자, _만으로 구성되어야 하며 Keyword가 아니어야 합니다.

폐기된 모듈 (Deprecated modules)

ModuleDeclaration은 선택적 DeprecatedAttribute를 가질 수 있습니다. 컴파일러는 폐기된 모듈이 import 될 때 메시지를 생성합니다.

deprecated module foo;
module bar;
import foo; // Deprecated: module foo is deprecated

DeprecatedAttribute은 더 유익한 메시지를 제공하기 위해 선택적 AssignExpression 인자를 가질 수 있습니다. 그 AssignExpression은 컴파일 타임에 문자열로 평가되어야 합니다.

deprecated("Please use foo2 instead.")
module foo;
module bar;
import foo; // Deprecated: module foo is deprecated - Please use foo2 instead.

구현 정의 (Implementation Defined): 폐기 메시지가 사용자에게 어떻게 제시되는지.

import 선언 (Import Declaration)

한 모듈의 심볼은 ImportDeclaration을 사용해 다른 모듈에서 사용 가능해집니다:

ImportDeclaration:
 import ImportList ;
 static import ImportList ;

ImportList:
 Import
 ImportBindings
 Import , ImportList

Import:
 ModuleFullyQualifiedName
 ModuleAliasIdentifier = ModuleFullyQualifiedName

ImportBindings:
 Import : ImportBindList

ImportBindList:
 ImportBind
 ImportBind , ImportBindList

ImportBind:
 Identifier
 Identifier = Identifier

ModuleAliasIdentifier:
 Identifier

ImportDeclaration에는 여러 형태가 있으며, 일반적인 것에서 세밀한 것까지입니다.

ImportDeclaration이 나타나는 순서는 중요하지 않습니다. ImportDeclaration 안의 ModuleFullyQualifiedName은 그것이 들어 있는 패키지로 완전히 한정되어야 합니다. 그것을 import 하는 모듈에 상대적이라고 간주되지 않습니다.

구현 정의 (Implementation Defined): 컴파일러가 import 선언의 패키지·모듈 식별자를 해당 소스 파일로 해석하는 방법.

심볼 이름 찾기 (Symbol Name Lookup)

import 하는 가장 단순한 형태는 import 하는 모듈들을 나열하는 것입니다:

module myapp.main;

import std.stdio; // import module stdio from package std

class Foo : BaseClass
{
 import myapp.foo; // import module myapp.foo in this class' scope
 void bar ()
 {
 import myapp.bar; // import module myapp.bar in this function' scope
 writeln("hello!"); // calls std.stdio.writeln
 }
}

심볼 이름이 한정 없이(qualified 없이) 사용되면 두 단계 조회(two-phase lookup)가 사용됩니다. 먼저 모듈 스코프가 가장 안쪽 스코프부터 검색됩니다. 예를 들어, 이전 예에서 writeln을 찾을 때 순서는 다음과 같습니다:

  1. bar 안의 선언들.
  2. Foo 안의 선언들.
  3. BaseClass 안의 선언들.
  4. 모듈 스코프의 선언들.

첫 조회가 성공하지 않으면, imports에 대해 두 번째 조회가 수행됩니다. 두 번째 조회 단계에서는 상속된 스코프가 무시됩니다. 여기에는 기반 클래스와 인터페이스의 스코프(이 예에서 BaseClass의 imports는 무시됨)뿐 아니라 혼합된(mixed-in) 템플릿의 imports도 포함됩니다.

심볼 조회는 일치하는 심볼을 찾는 즉시 멈춥니다. 같은 조회 단계에서 같은 이름의 두 심볼이 발견되면, 이 모호성은 컴파일 오류를 초래합니다.

module A;
void foo();
void bar();
module B;
void foo();
void bar();
module C;
import A;
void foo();
void test()
{
 foo(); // C.foo() is called, it is found before imports are searched
 bar(); // A.bar() is called, since imports are searched
}
module D;
import A;
import B;
void test()
{
 foo(); // error, A.foo() or B.foo() ?
 A.foo(); // ok, call A.foo()
 B.foo(); // ok, call B.foo()
}
module E;
import A;
import B;
alias foo = B.foo;
void test()
{
 foo(); // call B.foo()
 A.foo(); // call A.foo()
 B.foo(); // call B.foo()
}

public import (Public Imports)

기본적으로 imports는 private입니다. 즉 모듈 A가 모듈 B를 import 하고, 모듈 B가 모듈 C를 import 하면, C 안의 이름은 A 안에서는 보이지 않고 B 안에서만 보입니다.

import는 명시적으로 public으로 선언될 수 있으며, 그러면 import 된 모듈의 이름이 더 나아간 imports에 보이게 됩니다. 그래서 위 예에서 모듈 A가 모듈 B를 import 하는데, 모듈 B가 모듈 C를 public으로 import 하면 C의 이름이 A에서도 보입니다.

public으로 import 된 모듈의 모든 심볼은 import 하는 모듈에서도 별칭이 됩니다. 즉 위 예에서 C가 이름 foo를 포함하면, A에서 foo, B.foo, C.foo로 접근할 수 있습니다.

또 다른 예:

module W;
void foo() { }
module X;
void bar() { }
module Y;
import W;
public import X;
...
foo(); // calls W.foo()
bar(); // calls X.bar()
module Z;
import Y;
...
foo(); // error, foo() is undefined
bar(); // ok, calls X.bar()
X.bar(); // ditto
Y.bar(); // ok, Y.bar() is an alias to X.bar()

static import (Static Imports)

static import는 모듈의 이름을 참조할 때 완전히 한정된 이름을 요구합니다:

static import std.stdio;

void main()
{
 writeln("hello!"); // error, writeln is undefined
 std.stdio.writeln("hello!"); // ok, writeln is fully qualified
}

이름 바꾼 import (Renamed Imports)

import에 로컬 이름을 줄 수 있으며, 모듈의 심볼에 대한 모든 참조는 그 이름으로 한정되어야 합니다:

import io = std.stdio;

void main()
{
 io.writeln("hello!"); // ok, calls std.stdio.writeln
 std.stdio.writeln("hello!"); // error, std is undefined
 writeln("hello!"); // error, writeln is undefined
}

모범 사례 (Best Practices): 이름 바꾼 import는 매우 긴 import 이름을 다룰 때 유용합니다.

선택적 import (Selective Imports)

모듈에서 특정 심볼만을 독점적으로 import 해 현재 네임스페이스에 바인딩할 수 있습니다:

import std.stdio : writeln, foo = write;

void main()
{
 std.stdio.writeln("hello!"); // error, std is undefined
 writeln("hello!"); // ok, writeln bound into current namespace
 write("world"); // error, write is undefined
 foo("world"); // ok, calls std.stdio.write()
 fwritefln(stdout, "abc"); // error, fwritefln undefined
}

static은 선택적 import와 함께 사용할 수 없습니다.

이름 바꾸기와 선택적 import (Renamed and Selective Imports)

이름 바꾸기와 선택적 import가 결합될 때:

import io = std.stdio : foo = writeln;

void main()
{
 writeln("bar"); // error, writeln is undefined
 std.stdio.foo("bar"); // error, foo is bound into current namespace
 std.stdio.writeln("bar"); // error, std is undefined
 foo("bar"); // ok, foo is bound into current namespace,
 // FQN not required
 io.writeln("bar"); // ok, io=std.stdio bound the name io in
 // the current namespace to refer to the entire
 // module
 io.foo("bar"); // error, foo is bound into current namespace,
 // foo is not a member of io
}

스코프 import (Scoped Imports)

import 선언은 어떤 스코프에서도 사용될 수 있습니다. 예를 들어:

void main()
{
 import std.stdio;
 writeln("bar");
}

import는 그 스코프의 해결되지 않은 심볼을 충족시키기 위해 조회됩니다. 로컬로 import 된 심볼은 바깥 스코프에서 import 된 심볼을 숨길 수 있습니다.

함수 스코프에서 import 된 심볼은 import 선언이 함수 본문에 어휘적으로 나타난 뒤에만 보입니다. 다시 말해, 함수 스코프의 import 된 심볼은 앞으로 참조(forward reference)될 수 없습니다.

void main()
{
 void writeln(string) {}
 void foo()
 {
 //std.stdio.writeln("stdio"); // error, `std` is undefined
 writeln("main"); // calls `main.writeln`
 import std.stdio;
 writeln("main"); // still calls `main.writeln`
 std.stdio.writeln("stdio"); // `std` is now in scope
 void writeln(string) {}
 writeln("foo"); // calls `main.foo.writeln`
 }
 foo();
 writeln("main"); // calls `main.writeln`
 //std.stdio.writeln("stdio"); // error, `std` is undefined
}

모듈 스코프 연산자 (Module Scope Operator)

선행 점(dot, .)은 식별자를 모듈 스코프에서 조회하게 합니다.

const int x = 1;

void main()
{
 int x = 5;
 assert(x == 5); // main.x, not global x
 assert(.x == 1); // global x
}

정적 생성과 소멸 (Static Construction and Destruction)

정적 생성자(static constructor)는 모듈의 상태를 초기화하기 위해 실행됩니다. 정적 소멸자(static destructor)는 모듈의 상태를 종료합니다.

모듈은 여러 정적 생성자와 정적 소멸자를 가질 수 있습니다. 정적 생성자는 어휘 순서로 실행되고, 정적 소멸자는 역 어휘 순서로 실행됩니다.

비공유(non-shared) 정적 생성자와 소멸자는 스레드가 생성되거나 파괴될 때마다 실행됩니다. 여기에는 메인 스레드도 포함됩니다.

공유(shared) 정적 생성자는 main()이 호출되기 전에 한 번 실행됩니다. 공유 정적 소멸자는 main() 함수가 반환된 후 실행됩니다.

import resource;

Resource x;
shared Resource y;
__gshared Resource z;

static this() // non-shared static constructor
{
 x = acquireResource();
}

shared static this() // shared static constructor
{
 y = acquireSharedResource();
 z = acquireSharedResource();
}

static ~this() // non-shared static destructor
{
 releaseResource(x);
}

shared static ~this() // shared static destructor
{
 releaseSharedResource(y);
 releaseSharedResource(z);
}

모범 사례 (Best Practices): 공유 정적 생성자와 소멸자는 공유 전역 데이터를 초기화·종료하는 데 사용합니다. 비공유 정적 생성자와 소멸자는 스레드 로컬 데이터를 초기화·종료하는 데 사용합니다.

정적 생성 순서 (Order of Static Construction)

모든 모듈의 공유 정적 생성자는 어떤 비공유 정적 생성자보다 먼저 실행됩니다.

정적 초기화의 순서는 각 모듈의 import 선언에 의해 암시적으로 결정됩니다. 각 모듈은 import 된 모듈이 먼저 정적으로 생성된다고 가정됩니다. 모듈 정적 생성자의 실행에 부과되는 다른 순서는 없습니다.

import 선언의 순환(순환 의존)은 두 모듈 중 어느 쪽도 정적 생성자 또는 정적 소멸자를 포함하지 않거나(둘 중 하나만 포함) 한쪽만 포함하면 허용됩니다. 이 규칙을 위반하면 런타임 예외가 발생합니다.

구현 정의 (Implementation Defined): 구현은 순환 감지 중단을 재정의하는 수단을 제공할 수 있습니다. 전형적인 방법은 D Runtime 스위치 --DRT-oncycle=...을 사용하는 것으로, 다음 동작이 지원됩니다:

  • abort — 기본 동작. 이전 섹션에서 설명한 정상 동작.
  • print — 감지된 모든 순환을 출력하되 실행은 중단하지 않는다. 순환이 있으면 정적 생성 순서는 구현 정의이며 유효함이 보장되지 않는다.
  • ignore — 실행을 중단하지도 않고 순환을 출력하지도 않는다. 순환이 있으면 정적 생성 순서는 구현 정의이며 유효함이 보장되지 않는다.

모범 사례 (Best Practices): 실용적인 곳에서는 순환 import를 피하세요. 순환 import는 프로그램 구조를 독립 모듈로 제대로 분해하지 못했음을 나타낼 수 있습니다. 서로를 import 하는 두 모듈은 종종 순환 없는 세 모듈로 재구성될 수 있는데, 세 번째가 다른 둘에 필요한 선언을 포함하면 됩니다.

모듈 안의 정적 생성 순서 (Order of Static Construction within a Module)

모듈 안에서 정적 생성은 그것이 나타나는 어휘 순서로 발생합니다.

정적 소멸 순서 (Order of Static Destruction)

이것은 정적 생성의 정확히 역순으로 정의됩니다. 개별 모듈의 정적 소멸자는 해당 정적 생성자가 성공적으로 완료된 경우에만 실행됩니다.

공유 정적 소멸자는 정적 소멸자 이후에 실행됩니다.

단위 테스트 순서 (Order of Unit tests)

단위 테스트는 모듈 안에서 나타나는 어휘 순서로 실행됩니다.

mixin 선언 (Mixin Declaration)

MixinDeclaration:
 mixin ( ArgumentList ) ;

ArgumentList의 각 AssignExpression은 컴파일 타임에 평가되며, 결과는 문자열로 표현 가능해야 합니다. 결과 문자열들이 연결되어 하나의 문자열을 이룹니다. 그 문자열의 텍스트 내용은 유효한 DeclDef로 컴파일될 수 있어야 하며, 그렇게 컴파일됩니다.

mixin의 내용은 아직 AST로 끌어들여지지 않았기 때문에, 같은 스코프의 다른 DeclDef에 의해 앞으로 참조될 수 없습니다.

class B : A {} // Error: undefined identifier `A`
mixin ("class A {}");

앞으로 참조는 선언들 이후에 처리되기 때문에 함수 본문에서만 동작할 수 있습니다:

void v()
{
 class B : A {}
}
mixin ("class A {}");

패키지 모듈 (Package Module)

패키지 모듈은 다른 모듈을 public으로 import 하면서 더 단순한 import 문법을 제공하는 데 사용됩니다. 이는 기존 코드를 깨뜨리지 않고 모듈을 패키지의 모듈로 변환할 수 있게 해 줍니다. 일련의 라이브러리 모듈의 예:

libweb/client.d:

module libweb.client;

void runClient() { }

libweb/server.d:

module libweb.server;

void runServer() { }

libweb/package.d:

module libweb;

public import libweb.client;
public import libweb.server;

패키지 모듈의 파일 이름은 package.d여야 합니다. 모듈 이름은 패키지의 완전히 한정된 이름으로 선언됩니다. 패키지 모듈은 다른 모듈처럼 import 될 수 있습니다:

test.d:

module test;

// import the package module
import libweb;

void main()
{
 runClient();
 runServer();
}

패키지 모듈은 하위 패키지 안에 중첩될 수 있습니다:

libweb/utils/package.d:

// must be declared as the fully qualified name of the package, not just 'utils'
module libweb.utils;

// publicly import modules from within the 'libweb.utils' package.
public import libweb.utils.conv;
public import libweb.utils.text;

그런 다음 패키지 모듈을 표준 모듈 import 선언으로 import 할 수 있습니다:

test.d:

module test;

// import the package module
import libweb.utils;

void main() { }

더 알아보기