모듈화 - 모던 자바 인 액션
소프트웨어를 만들다 보면 자연스럽게 이런 질문을 하게 됩니다.
“이 기능을 어디에 넣어야 하지?”
“이 코드가 다른 데에 영향을 줄까?”
“나중에 고치기 쉽게 하려면 어떻게 설계해야 할까?”
이런 고민들을 잘 해결하기 위한 중요한 개념 두 가지가 있습니다.
바로 관심사 분리(Separation of Concerns, SoC) 와 정보 은닉(Information Hiding)입니다.
관심사 분리 SoC, Separation of concerns
말 그대로 역할을 나누는 것입니다.
예를 들어, 회사 회계 앱을 만든다고 가정해보겠습니다.
- 사용자의 지출 데이터를 파싱하는 기능
- 그 데이터를 분석하는 기능
- 분석 결과를 보고서로 만드는 기능
이 세 가지는 서로 다른 역할(=관심사)이죠.
이 기능들을 한 군데에 다 몰아넣는다면 나중에 유지보수도 어렵고, 협업도 힘들어집니다.
그래서 기능별로 모듈을 나누는 것, 이게 바로 관심사 분리입니다.
이렇게 분리하면 좋은 점은 명확합니다:
- 팀이 각자 맡은 부분만 신경 쓰면 되니 협업이 쉬워지고
- 각 모듈을 재사용성
- 전체 시스템을 관리 용이
SoC 원칙은 모델,뷰,컨트롤러 같은 아키텍처 관점 그리고 복구 기법을 비지니스 로직과 분리하는 등의 하위 수준 접근 등의 상황에 유용합니다.
파싱, 분석, 레포트 기능을 모듈이라는 각각의 부분, 즉 서로 거의 겹치지 않는 코드 그룹으로 분리할 수 있습니다.
클래스를 그룹화한 모듈을 이용해 애플리케이션의 클래스 간의 관계를 시각적으로 보여줄 수 있습니다.
"자바 패키지가 클래스를 그룹으로 만든다"는 말도 맞지만,
자바9의 모듈은 클래스가 어떤 다른 클래스를 볼 수 있는지를 컴파일 시간에 정교하게 제어할 수 있습니다.
특히 자바 패키지는 모듈성을 제어하지 않습니다.
정보 은닉이란?
정보 은닉은 안 보이게 숨기는 것입니다.
좀 더 구체적으로 말하면, 내부 구현을 외부에 알리지 않는 것입니다.
예를 들어 어떤 클래스가 있다고 할 때, 그 내부 상태나 동작이 바깥에서 막 바뀌면 문제가 생깁니다.
그래서 자바에서는 private 같은 접근 제한자를 통해 클래스 내부의 정보를 보호합니다.
정보 은닉이 잘 되어 있으면:
- 내부 코드를 바꾸더라도 바깥에 영향이 가지 않고
- 실수로 잘못된 사용을 막을 수 있고
- 보안적인 측면에서도 코드가 더 안전해집니다
이걸 우리는 흔히 캡슐화(encapsulation)라고 부르기도 하죠.
자바에서는 클래스 내의 컴포넌트에 적절하게 private 키워드를 사용했는지를 기준으로 컴파일러를 이용해 캡슐화를 확인할 수 있습니다. 하지만 자바 9 이전까지는 클래스와 패키지가 의도된대로 공개되었는지를 컴파일러로 확인할 수 있는 기능이 없었습니다.
자바는 클래스, 패키지, JAR 세가지 수준의 코드 그룹화를 제공합니다. 클래스와 관련해 자바는 접근 제한자와 캡슐화를 지원했습니다.
하지만 패키지와 JAR 수준에서는 캡슐화를 거의 지원하지 않았습니다.
예를 들어, 패키지(package)는 클래스를 묶는 단위이긴 하지만, 패키지 내부 구현을 외부에 노출하지 않도록 제어하기는 어려웠죠.
impl 같은 내부 구현 패키지가 외부에서 자유롭게 사용되는 일도 많았습니다.
그러다 보니, 원래 내부용으로 만든 코드가 외부에서 의존하게 되는 문제가 생깁니다.
이걸 바꾸려고 해도 외부 사용자들이 이미 쓰고 있어서 함부로 손을 대기 어렵습니다. 결국 코드가 점점 엉키게 되는 거죠.
애플리케이션을 번들하고 실행하는 기능과 관련해 자바는 태생적으로 약점을 갖고 있습니다.
클래스를 모두 컴파일한 다음 보통 한 개의 평범한 JAR 파일에 넣고 클래스 경로에 이 JAR 파일을 추가해 사용할 수 있습니다.
그러면 JVM 이 동적으로 클래스 경로에 정의된 클래스를 필요할 때 읽습니다.
안타깝게도 클래스 경로와 JAR 조합에는 몇 가지 약점이 존재합니다.
1. 클래스 경로에는 같은 클래스를 구분하는 버전 개념이 없다.
클래스 경로에 두 가지 같은 버전의 같은 라이브러리가 존재할 때 어떤 일이 일어날지 예측할 수 없다.
다양한 컴포넌트가 같은 라이브러리의 다른 버전을 사용하는 상황이 발생할 수 있는 큰 애플리케이션에서 이런 문제가 두드러진다.
2. 클래스 경로는 명시적인 의존성을 지원하지 않는다. 각각의 JAR 안에 있는 모든 클래스는 classes라는 한 주머니로 합쳐진다.
즉 한 JAR 가 다른 JAR 에 포함된 클래스 집합을 사용하라고 명시적으로 의존성을 정의하는 기능을 제공하지 않는다.
이 상황에서는 클래스 경로때문에 어떤일이 일어나는지 파악하기 어렵고 빠지거나 충돌이 있는지 의심이 든다.
메이븐이나 그레이들 같은 빌드 도구가 이런 문제의 해결에 도움을 준다.
하지만 자바9이전에는 자바, JVM 누구도 명시적인 의존성 정의를 지원하지 않아 이를 JAR 지옥, 클래스 경로 지옥이라 부르는 사람도 있다.
JVM 이 ClassNotFoundException을 발생시키지 않을 때까지 클래스 경로에 파일을 더하거나 제거해보는 수밖에 없었다.
자바 9 모듈 시스템에는 컴파일 타임에 이런 종류의 에러를 모두 검출할 수 있다.
JDK 자바 개발 키트
JDK는 자바 개발에 필요한 모든 것을 담은 개발 키트입니다.
컴파일러, 기본 클래스 라이브러리, 실행 도구 등… 자바 개발에 필요한 건 대부분 들어 있어요.
그런데 요즘은 상황이 달라졌습니다.
- 모바일 앱: 메모리와 저장공간이 부족해요.
- 클라우드 환경: 작은 단위로 빠르게 실행되고 배포돼야 해요.
이런 환경에서는 “내가 사용하는 기능만 최소한으로 가지고 가고 싶다”는 욕구가 커졌습니다.
하지만 기존의 JDK는 그걸 잘 도와주지 못했습니다.
전체 JDK를 통째로 가져가야 했기 때문이죠.
자바 8의 임시처방: 컴팩트 프로파일
자바 8에서는 이 문제를 어느 정도 해결해 보려고 했습니다.
그게 바로 **컴팩트 프로파일(compact profiles)**입니다.
"JDK를 아예 3가지 정도로 나눠서, 필요한 만큼만 가져다 쓰자!"
아이디어는 좋았습니다.
실제로 compact1, compact2, compact3 같은 프로파일이 생겨서
어느 정도 메모리 풋프린트를 줄일 수 있었죠.
하지만 이건 임시방편에 가까웠습니다.
- 라이브러리가 논리적으로만 나뉘었고
- 내부 구조는 여전히 뒤엉켜 있었고
- 무엇보다도 내부 API가 외부에서 그대로 보이는 구조였어요
이 말인즉슨,
“원래 외부에서 쓰면 안 되는 내부 코드가 그냥 노출되어 있었고, 어떤 개발자들은 그걸 써버렸고… 나중에 바꾸기도 어렵게 되었다”는 얘기입니다.
자바 9부터는 이런 문제를 해결하기 위해 모듈 시스템이 도입되었습니다.
이제는 클래스뿐 아니라 모듈 단위로 기능을 나누고, 공개할지 말지를 명시적으로 정할 수 있게 된 겁니다.
모듈은 module이라는 키워드에 이름와 바디를 추가해 정의합니다.
모듈 디스크립터는 module-info.java 라는 특별한 파일에 저장됩니다.
이 안에서 어떤 패키지를 외부에 공개할지(exports), 어떤 다른 모듈에 의존할지(requires)를 선언할 수 있죠.
모듈 디스크립터는 보통 패키지와 같은 폴더에 위치하며, 한 개 이상의 패키지를 서술하고 캡슐화할 수 있지만, 단순한 상황에서는 이들 패키지 중 한개만 외부로 노출시킵니다.
모듈 시스템의 여러 부분이 두드러질 수 있도록 기능별로 잘게 분해해 작은 기능까지 캡슐화한다면 장점에 비해 초기화 비용이 높아지고 옳은 결정인지 논란이 생길 수 있습니다. 하지만 프로젝트가 커지면 캡슐화와 추론의 장점이 두드러집니다.
가장 좋은 방법은 실용적으로 분해하면서 진화하는 소프트웨어 프로젝트가 이해하기 쉽고 고치기 쉬운 수준으로 적절하게 모듈화되어 있는지 주기적으로 확인하는 프로세스를 갖는 것입니다.
예:
module accounting { -- 모듈명 accounting
exports com.myapp.report; -- 한 패키지를 노출 시킴
requires data.parser; -- 0 개 이상.. 다른 노출된 패키지 사용
}
이렇게 모듈을 정의하면:
- 내부 구현 패키지는 외부에서 접근할 수 없고
- 다른 모듈과의 의존 관계도 명확해지며
- 컴파일 타임부터 의존성 문제를 잡을 수 있습니다
게다가 보안적으로도 더 안전해지고, 불필요한 라이브러리까지 포함되지 않아서 JDK 자체도 더 작고 유연하게 구성할 수 있게 되었습니다.