모듈과 모듈 시스템

1. 모듈이란 무엇인가?

소프트웨어 공학에서 모듈이란 독립성(Independence)과 은닉성(Hiding)을 만족하며 연관된 코드들의 묶음입니다.

  • 독립성:
    모듈은 독립적이어야 합니다.
    • 모듈을 사용하기 전에 필요한 의존성을 알 수 있어야 한다.
    • 모듈의 의존성이 모두 준비된다면 모듈을 사용하는 데 아무런 문제가 없어야 한다.
  • 은닉성:
    모듈의 사용자는 모듈의 내부 구현을 몰라도 됩니다.
    • 공개된 인터페이스를 이용해 모듈과 통신합니다.

2. 모듈 시스템이란?

모듈 시스템은 연관된 코드 묶음이 모듈성을 갖출 수 있게 도와주는 시스템적인 해결책입니다.
모듈성을 지원하기 위해 모듈 시스템은 다음과 같은 기능을 필수적으로 지원해야 합니다.

  1. 의존성 관리:
    모듈을 사용하기 위해 어떤 의존성이 필요한지 명시할 수 있어야 한다.

  2. 캡슐화 관리:
    모듈은 불필요한 구현을 외부로 드러내지 않아야 한다.

즉, 모듈 시스템을 통해 개발자는 어떤 모듈을 정의하면서 해당 모듈에 필요한 의존 모듈이 무엇인지 명시할 수 있으며, 모듈을 배포할 때 모든 내용을 배포하는 것이 아니라 일부만 배포할 수 있습니다.

이를 위해 모듈 시스템은 모듈이 가진 독립성과 은닉성을 보장하기 위한 의존성 관리와 캡슐화 관리 기능을 모듈 수준에서 지원해야 합니다.


3. NestJS에서의 모듈 관리 예시

다음은 NestJS에서 모듈 데코레이터(@Module)를 활용하여 모듈을 관리하는 예시 코드입니다.

import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  // 만약 다른 모듈을 import하거나 export할 필요가 있다면 아래와 같이 작성할 수 있습니다.
  // imports: [SomeOtherModule],
  // exports: [UsersService],
})
export class UsersModule {}
  • import 프로퍼티:
    이 모듈은 어떤 외부 모듈에 의존하고 있는지를 나타냅니다.

    즉, 모듈의 의존성을 관리하는데 사용되며, 해당 모듈을 사용하기 위해 어떤 부수적인 모듈이나 컴포넌트가 필요한지를 선언하는 역할을 합니다.

  • exports 프로퍼티:
    모듈 중 어떤 컴포넌트만 외부로 노출할 것인지를 나타냅니다.

    exports 프로퍼티는 모듈의 캡슐화를 위해 사용되며, 해당 모듈을 임포트하면 어떤 컴포넌트를 사용할 수 있는지 나열하는 역할을 합니다.

    중요: 외부로 공개할 컴포넌트를 제한함으로써 모듈 수준의 캡슐화가 이뤄집니다.


4. 자바의 패키지 시스템 vs 모듈 시스템

자바의 패키지 시스템을 모듈 시스템이라고 볼 수 있을까요?

정답은 ‘아니오’입니다.

왜냐하면 자바의 패키지 시스템은 패키지 수준의 의존성 관리와 캡슐화 관리 기능을 지원하지 않기 때문입니다.

대신, 자바에는 보다 확실한 모듈 시스템이 존재합니다. 바로 module-info.java라는 모듈 디스크립터(module descriptor)입니다.

module com.example.myapp {
    // 필요한 모듈을 명시 (java.base는 기본적으로 포함되어 있으므로 생략 가능)
    requires java.logging;
    requires java.sql;

    // 외부에 공개할 패키지를 지정
    exports com.example.myapp.api;
    exports com.example.myapp.utils;

    // 리플렉션 등을 위해 특정 패키지를 특정 모듈에만 열어줄 때 사용
    opens com.example.myapp.internal to com.example.myapp.tests;
}
  • 위 코드에서는 모듈 디스크립터에 개발자가 myproject.main이라는 이름으로 모듈을 정의하고,
    requires를 이용해 외부 의존성을 관리합니다.
  • 또한 exports를 통해 외부로 노출할 모듈 내 인터페이스 패키지를 관리합니다.
    이렇게 특정 패키지만 외부로 노출함으로써 모듈 수준의 캡슐화 관리라는 목표를 달성할 수 있습니다.

예를 들어, 두 개의 모듈로 구성된 간단한 프로젝트를 생각해 볼 수 있습니다.

project-root/
├── core/
   └── src/
       └── main/
           └── java/
               ├── module-info.java
               └── com/myorg/core/Utility.java
└── app/
    └── src/
        └── main/
            └── java/
                ├── module-info.java
                └── com/myorg/app/Main.java

core 모듈

  • module-info.java
    // com.myorg.core 패키지를 외부로 공개(export)하여 다른 모듈에서 사용할 수 있도록 합니다.
     module com.myorg.core {
      exports com.myorg.core;
    }
    
  • Utility.java ```java package com.myorg.core;

public class Utility { public static String getGreeting() { return “Hello from core module!”; } }


#### app 모듈
- `module-info.java`
```java
// com.myorg.app 모듈은 com.myorg.core 모듈에 의존하고 있으므로 requires 키워드를 사용합니다.
module com.myorg.app {
    requires com.myorg.core;
}
  • Main.java ```java package com.myorg.app;

import com.myorg.core.Utility;

public class Main { public static void main(String[] args) { System.out.println(Utility.getGreeting()); } }

이렇게 각 모듈은 module-info.java를 통해 자신이 제공하는 API와 필요한 의존성을 명확히 하여, 자바 컴파일러와 런타임이 모듈 경로(module path)를 통해 올바르게 모듈을 로드하도록 합니다.

---

## 5. 모듈을 이루는 독립성과 은닉성

### 5.1 독립성

- **정의**:  
  ‘모듈은 독립적이어야 한다’라는 의미는 모듈이 다른 모듈이나 컴포넌트에 강하게 의존하지 않고, 각 모듈을 개별적으로 수정하거나 교체할 수 있어야 한다는 뜻입니다.

- **이유**:  
  모듈이 독립적이어야 하는 이유는 유지보수를 용이하게 하고, 확장성을 높이며, 코드의 재사용성을 높이기 위함입니다.

- **의존성 관리와의 관계**:  
  모듈은 하위 의존성을 어딘가에 명시함으로써 사용법을 사용자에게 알려줄 수 있습니다.

  즉, "이 모듈을 사용하고 싶다면 모듈을 실행하는 데 필요한 하위 의존성을 모두 가져와라"라고 적어두는 것과 같습니다.

  의존성을 명확히 하는 것만으로도 모듈을 독립적으로 만들 수 있습니다.

### 5.2 은닉성

- **정의**:  
  모듈이 은닉성을 추구해야 한다는 말은, 클래스가 은닉성을 추구하는 것처럼 모듈 수준의 캡슐화가 가능해야 한다는 의미입니다.

- **목표**:  
  모듈을 외부에 공유하더라도 공개된 인터페이스 이외에 불필요한 정보를 숨길 수 있어야 합니다.

> **API 사용자가 충분히 많다면 계약을 어떻게 했는지는 크게 중요하지 않습니다. 시스템의 모든 관측 가능한 행동은 사용자에 의해 결정될 것입니다.**  
> — 하이럼 라이트 (Hyrum Wright)

- **하이럼 법칙 (Hyrum’s law)**:  
  'API를 사용하는 사용자가 충분히 많다면 개발자의 설계 의도는 더 이상 중요하지 않다'라고 해석할 수 있습니다.

  사용자의 행동은 예측할 수 없으며, 모듈의 사용자 역시 마찬가지입니다.

  (혹은 “모듈의 사용자는 개발자니까 합리적으로 사용하지 않을까?”라는 생각도 배제할 수 없습니다. 실제로 개발자들이 더 지독한 경우도 있습니다.)

- **예시 – 몽키 패치 (Monkey Patch)**:  
  파이썬에는 **몽키 패치**라는 개념이 있습니다.

  이는 프로그램 실행 시 런타임에 동적으로 코드를 수정하여 기존 클래스, 모듈, 함수 등의 동작을 변경하는 것입니다.

  몽키 패치가 단순히 버그를 임시로 수정하는 정도에서 그친다면 다행이겠지만, 일부 사용자는 이를 이용해 기존 라이브러리나 프레임워크의 동작을 임의로 수정하기도 합니다.

  이 사례는 사용자가 모듈 개발자의 의도를 벗어나 사용할 수 있는 한계를 보여줍니다.

  외부에 한 번 공개된 인터페이스는 쉽게 삭제할 수 없으며, 모든 사용자를 위한 하위 호환성을 보장해야 하기 때문입니다.

  따라서 어떤 기능을 외부로 공표하는 것은 모듈 개발자에게 굉장한 부담이 될 수 있습니다.

---

## 6. Gradle을 활용한 멀티모듈 프로젝트
최근 Gradle을 이용해 멀티모듈 구조의 프로젝트를 구성하는 경우가 많습니다.

Gradle 멀티모듈 프로젝트에서는 각 서브모듈이 독립적인 모듈(예, 위의 `core`와 `app`)로 관리되며, 상위 프로젝트에서 전체적인 빌드 설정과 공통 설정을 적용할 수 있습니다.
- 프로젝트 구조 예시
```css
MultiModuleProject/
├── settings.gradle
├── build.gradle          // 공통 설정 (루트 빌드 파일)
├── core/
│   ├── build.gradle
│   └── src/main/java/
│       ├── module-info.java
│       └── com/myorg/core/Utility.java
└── app/
    ├── build.gradle
    └── src/main/java/
        ├── module-info.java
        └── com/myorg/app/Main.java

settings.gradle

  • 루트 디렉토리의 settings.gradle 파일에서는 포함할 서브 프로젝트(모듈)을 명시합니다.
    rootProject.name = 'MultiModuleProject'
    include 'core', 'app'
    

    루트 build.gradle

  • 루트의 build.gradle 파일에는 모든 서브 프로젝트에 공통으로 적용할 설정을 정의합니다. ```groovy plugins { id ‘java’ }

allprojects { group = ‘com.myorg’ version = ‘1.0’ }

subprojects { apply plugin: ‘java’ sourceCompatibility = ‘11’ targetCompatibility = ‘11’ }

#### core/build.gradle
- core 모듈의 경우 별도의 의존성이 없다면 기본 설정만 사용하면 됩니다.
```groovy
// core/build.gradle
dependencies {
    // 필요한 의존성이 있다면 추가
}

app/build.gradle

  • app 모듈은 core 모듈에 의존하므로, project dependency를 추가합니다.
    // app/build.gradle
    dependencies {
      implementation project(':core')
    }
    

    Gradle과 자바 모듈 시스템

  • module-info.java 파일이 각 서브모듈의 src/main/java에 위치하면, Gradle은 자바 9 이상에서 제공하는 모듈 시스템을 인식하여 모듈 경로를 설정합니다.
  • Gradle의 최신 버전(예: Gradle 7 이상)은 자바 모듈 시스템을 지원하도록 설계되었으며, 별도의 설정 없이도 각 모듈의 의존성을 모듈 단위로 관리할 수 있습니다.
  • 만약 추가적인 모듈 경로 설정이나 특정 플러그인이 필요할 경우, 각 서브모듈의 build.gradle 파일이나 루트 build.gradle에서 추가 설정을 할 수 있습니다.

이처럼 Gradle 멀티모듈 프로젝트에서는 각 모듈이 독립적인 module-info.java 파일을 통해 자신의 역할과 의존성을 명시하며, 루트 프로젝트에서 전체 프로젝트의 공통 설정과 모듈 간 의존관계를 관리할 수 있습니다. 이를 통해 코드의 독립성과 캡슐화가 강화되며, 유지보수와 확장성이 용이한 프로젝트 구조를 만들 수 있습니다.


7. 결론

결론적으로, 모듈 수준의 인터페이스 관리는 매우 중요합니다.

모듈을 사용한다는 것은 모듈의 모든 기능에 접근할 수 있게 된다는 의미가 아니라, 모듈이 책임지는 공개된 일부 기능에만 접근할 수 있어야 한다는 뜻입니다.

이렇게 해야만 모듈의 내부 구현이 변경되더라도 모듈 사용자에게 주는 영향 범위를 최소화할 수 있습니다.

모듈 시스템은 의존성 관리캡슐화 관리를 모듈 수준에서 지원함으로써, 개발자가 모듈의 독립성과 은닉성을 보장하고, 유지보수성과 확장성을 높일 수 있도록 돕습니다.