코드 한 줄의 기록

Java 패키지와 모듈 시스템: 체계적인 코드 구조화 가이드 본문

JAVA

Java 패키지와 모듈 시스템: 체계적인 코드 구조화 가이드

CodeByJin 2025. 10. 12. 20:06
반응형

개발을 하다 보면 클래스가 점점 많아지고 프로젝트가 복잡해집니다. 이때 코드를 어떻게 정리하고 관리할지가 중요한 문제가 되는데요. 저도 처음 Java를 배울 때 이 부분이 많이 헷갈렸습니다. 오늘은 Java의 패키지와 모듈 시스템, 그리고 접근성에 대해 함께 알아보면서 효율적인 코드 구조화 방법을 정리해보겠습니다.

패키지(Package): Java의 기본 구조화 도구

패키지란 무엇인가?

패키지는 비슷한 성격의 클래스들을 모아 놓은 자바의 디렉터리입니다. 마치 컴퓨터에서 파일을 폴더별로 정리하듯이, Java에서는 관련된 클래스들을 패키지로 분류해 관리합니다.

package com.example.userservice;

public class User {
    private String name;
    private int age;
    
    // 생성자와 메서드들
}

 

패키지의 주요 특징

패키지를 사용할 때 지켜야 할 중요한 규칙들이 있습니다.

  • 하나의 소스파일에는 첫 번째 문장으로 단 한번의 패키지 선언만을 허용합니다.
  • 모든 클래스는 반드시 하나의 패키지에 속해야 합니다.
  • 패키지는 점(.)을 구분자로 하여 계층구조로 구성할 수 있습니다.
  • 패키지는 물리적으로 클래스 파일(.class)을 포함하는 하나의 디렉토리입니다.

패키지 명명 규칙

패키지 이름을 정할 때는 다음과 같은 관례를 따릅니다.

// 일반적인 패키지 명명 규칙
com.회사명.프로젝트명.기능명

// 예시
com.example.bookstore.model
com.example.bookstore.service  
com.example.bookstore.controller

접근 제어자: 정보 은닉의 핵심

Java에서 접근 제어자는 클래스, 메서드, 필드의 접근 수준을 결정하는 중요한 요소입니다. 객체지향 프로그래밍에서 정보 은닉(data hiding) 개념을 구현하는 핵심 도구이기도 합니다.
 

네 가지 접근 제어자의 특징

1. public - 완전 개방형

public class MyClass {
    public String name;
    public void printName() {
        System.out.println(name);
    }
}

 
public은 가장 개방적인 접근 제어자로, 어떤 클래스나 패키지에서든 접근할 수 있습니다. 하지만 너무 많은 public 멤버는 객체 간 결합도를 높일 수 있으므로 신중하게 사용해야 합니다.
 
2. protected - 상속 관계 허용

public class Parent {
    protected String familyName = "Smith";
    
    protected void printFamilyName() {
        System.out.println(familyName);
    }
}

public class Child extends Parent {
    public void showFamily() {
        printFamilyName(); // 자식 클래스에서 부모의 protected 메서드 접근 가능
    }
}

 
protected같은 패키지 내의 다른 클래스 또는 서브클래스에서 접근할 수 있도록 허용합니다. 상속 관계에서 부모 클래스의 멤버를 하위 클래스에서 사용하도록 허용하려는 경우에 주로 사용됩니다.
 
3. default (package-private) - 패키지 내 공유

class MyClass {
    int id;  // default 접근 제어자
    
    void printId() {  // default 접근 제어자
        System.out.println(id);
    }
}

 
default 접근 제어자는 명시적으로 설정하지 않은 경우 자동으로 적용됩니다. 같은 패키지 내에서는 접근이 가능하지만, 다른 패키지에서는 접근할 수 없습니다.
 
4. private - 완전 은닉

public class MyClass {
    private int age;
    
    public void setAge(int age) {
        if (age > 0) {
            this.age = age;
        }
    }
    
    public int getAge() {
        return age;
    }
}

 
private은 가장 제한적인 접근 제어자로, 해당 클래스 내에서만 접근 가능합니다. 클래스의 내부 구현을 외부로부터 숨기고 캡슐화를 강화할 수 있습니다.
 
접근 제어자 범위 비교표

접근 제어자같은 클래스같은 패키지자식 클래스그 외의 영역
public
protectedX
defaultXX
privateXXX

Java 9 모듈 시스템 (JPMS): 차세대 구조화 도구

모듈 시스템 도입 배경

Java 8 이전의 클래스패스 기반 시스템은 여러 한계점을 가지고 있었습니다.

  • JAR Hell 문제: 동일한 클래스가 여러 JAR 파일에 존재할 때 어떤 것이 로드될지 예측하기 어려웠습니다.
  • 캡슐화 부족: public 클래스는 애플리케이션 전체에서 접근 가능했고, 내부 API와 외부 API의 구분이 모호했습니다.
  • 런타임 의존성 검증 부재: 클래스패스에 필요한 JAR이 없어도 컴파일은 성공하지만 런타임에서 ClassNotFoundException이 발생하는 경우가 빈번했습니다.

JPMS의 핵심 개념

Java Platform Module System (JPMS)Java 9에서 도입된 모듈 시스템으로, Java 애플리케이션과 라이브러리를 모듈 단위로 구성하고 관리합니다. 모듈은 관련된 패키지들의 집합으로, 명시적으로 다른 모듈에 대한 의존성을 선언하고 외부에 공개할 API를 정의합니다.
 

module-info.java 파일 구조

모든 모듈의 핵심은 module-info.java 파일입니다. 이 파일은 모듈의 루트 디렉토리에 위치하며, 모듈의 메타데이터를 정의합니다.

module com.example.bookstore {
    // 의존하는 모듈 선언
    requires java.base;        // 기본적으로 암시적 의존
    requires java.logging;
    requires transitive java.sql;  // 전이적 의존성
    
    // 외부에 노출할 패키지 선언
    exports com.example.bookstore.api;
    exports com.example.bookstore.model to com.example.bookstore.web;
    
    // 서비스 제공
    provides com.example.bookstore.spi.PaymentProcessor 
        with com.example.bookstore.impl.CreditCardProcessor;
    
    // 서비스 사용
    uses com.example.bookstore.spi.NotificationService;
}

주요 모듈 지시문

requires 지시문

현재 모듈이 다른 모듈에 의존함을 선언합니다.

module com.example.orderservice {
    requires java.base;                    // 명시적 의존성
    requires transitive java.logging;      // 전이적 의존성
    requires static java.compiler;         // 컴파일 타임 전용 의존성
    requires com.example.userservice;      // 다른 모듈 의존성
}

 
transitive 키워드는 특히 중요합니다. 현재 모듈을 사용하는 다른 모듈도 자동으로 해당 의존성에 접근할 수 있게 합니다.
 

exports 지시문

모듈 외부에서 접근 가능한 패키지를 정의합니다.

module com.example.paymentservice {
    exports com.example.payment.api;                    // 모든 모듈에 공개
    exports com.example.payment.internal to            // 특정 모듈에만 공개
        com.example.order, 
        com.example.billing;
}

 

JPMS의 장점

JPMS를 사용하면 다음과 같은 이점을 얻을 수 있습니다.

  • 유지보수성: 모듈 단위로 코드를 구성하여 코드의 복잡성을 줄이고, 이해하기 쉬워집니다
  • 재사용성: 모듈을 독립적으로 개발하고 테스트할 수 있어 코드의 재사용성을 높입니다
  • 충돌 방지: 명시적인 모듈 의존성 정의로 인해 클래스패스 충돌을 방지합니다
  • 성능: 필요 없는 모듈을 로드하지 않아 런타임 성능을 향상시킵니다

패키지 vs 모듈: 언제 무엇을 사용할까?

패키지 사용이 적합한 경우

  • 소규모 프로젝트에서 클래스들을 논리적으로 그룹화할 때
  • 기존 Java 8 이하 버전을 사용하는 환경
  • 단순한 구조의 애플리케이션 개발

모듈 사용이 적합한 경우

  • 대규모 프로젝트나 라이브러리 개발 시
  • Java 9 이상을 사용하는 환경
  • 명확한 API 경계가 필요한 프로젝트
  • 마이크로서비스 아키텍처 구현

실무에서의 활용 팁

1. 계층형 vs 도메인형 패키지 구조

계층형 구조는 각 계층을 대표하는 디렉터리를 기준으로 코드들이 구성됩니다.

src/main/java/com/example/demo/
├── controller/
├── service/
├── repository/
├── domain/
└── dto/

 
도메인형 구조는 비즈니스 도메인을 중심으로 패키지를 구성합니다.

src/main/java/com/example/demo/
├── user/
│   ├── UserController.java
│   ├── UserService.java
│   └── UserRepository.java
└── order/
    ├── OrderController.java
    ├── OrderService.java
    └── OrderRepository.java

 

2. 접근 제어자 사용 가이드라인

  • public: API로 외부에 노출해야 하는 클래스와 메서드에만 사용
  • protected: 상속을 고려한 설계에서 부모-자식 클래스 간 공유가 필요한 경우
  • default: 같은 패키지 내에서만 사용되는 헬퍼 클래스나 유틸리티 메서드
  • private: 클래스 내부 구현 세부사항, 대부분의 필드는 private으로 선언

3. 모듈화 전략

모듈을 설계할 때는 다음 원칙을 따르는 것이 좋습니다.

  • 응집도는 높이고 결합도는 낮추기
  • 명확한 API 경계 정의
  • 의존성 방향은 단방향으로 유지
  • 필요한 것만 exports하여 캡슐화 강화

Java의 패키지와 모듈 시스템은 단순해 보이지만 실제로는 매우 강력한 도구입니다. 특히 프로젝트가 커질수록 이러한 구조화의 중요성은 더욱 부각됩니다. 처음에는 복잡해 보일 수 있지만, 체계적으로 접근하면 더 유지보수하기 쉽고 안정적인 코드를 작성할 수 있게 됩니다.
 
앞으로 Java 개발을 할 때 이러한 개념들을 적극 활용해서 더 나은 코드 구조를 만들어 나가시길 바랍니다. 특히 Java 9 이상을 사용하신다면 모듈 시스템도 한 번 시도해보시는 것을 추천드립니다!

자바 equals/hashCode/toString 설계 원칙 총정리 + Lombok 사용 시 주의점까지 한 번에 정리

가장 먼저 결론부터 말하면, “도메인 모델의 동등성은 일관되고 예측 가능해야 하며, 컬렉션에 넣는 순간부터 계약을 끝까지 지켜야 한다.” 이 문장을 머리에 박아두면 equals/hashCode/toString을

byteandbit.tistory.com

반응형