개요
이번 글은 의존성 관리에 대한 이야기다.
사이드 프로젝트에서 약관 도메인의 CRUD API를 구현하면서,
공통 모듈의 기능을 그대로 사용하는 것이 의도치 않은 위험성을 유발할 수 있음을 깨달았다.
"필요하지 않은 기능까지 의존하게 되는 구조가 안전한 설계라고 볼 수 있을까?"
이 질문을 던지며,
어떻게 하면 공통 모듈의 장점은 살리면서도 안전하게 분리된 설계를 만들 수 있을지 고민하게 됐다.
이 글에서는 그 고민의 과정을 통해 정립된,
필자의 의존성 관리 철학과 구조 설계 원칙을 정리해보고자 한다.
목적
필자는 앞선 “레이어 및 패키지 구조 설계” 글에서
과거에 범했던 의존성 설계의 실수에 대해 돌아봤다.
WHY?
코드 재사용성을 높이겠다는 명분 아래,
여러 레이어 간 상호 의존을 허용한 결과,
서비스 객체는 점점 비대해지고 유지보수와 테스트가 어려워졌다.
그때 필자가 내린 결론은 이거다:
“설계의 본질은 책임과 방향을 명확히 부여하는 것이다.”
이번 글은 그 사고를 더 확장하여,
"의존성도 마찬가지로, 필요한 만큼만 부여되어야 한다"는 설계 철학을 정립하는 것이 목표다.
문제 인식
Spring Data JPA를 사용하다 보면, 공통 모듈의 Repository는 금세 수많은 기능을 갖게 된다.
처음엔 유용하다.
하지만 시간이 흐르고, 각 도메인의 책임이 분화되기 시작하면 문제가 발생한다.
예를 들어:
- "고객은 단지 약관을 조회만 하면 된다."
하지만 공통 Repository는 수정/삭제 기능까지 함께 노출되고 있다면?
- "고객이 알아서는 안 되는 정보"가,
공통 Repository를 통해 그대로 응답되고 있다면?
이 문제는 서비스 레이어에서 조절할 수 있다.
하지만 시간이 흐르고 기능이 많아진다면?
공통 Repository가 100개의 메서드를 갖고 있고,
그중 70%가 특정 모듈에선 절대 쓰이지 않는 기능이라면?
"그 기능은 불필요 하지만, 의존하고 있다."
이건 보안, 안정성, 유지보수성 모든 면에서 잠재적인 위험이다.
필자는 이 지점에서 질문을 던지게 됐다:
> "정말 필요한 기능만 의존하게 만들 수는 없을까?"
그리고 그 질문이 새로운 설계의 전환점이 되었다.
🛠 어떻게 이 문제를 해결했는가
사실 이 문제를 해결할 전략은 이미 알고 있었다.
“레이어 및 패키지 구조 설계” 글을 작성할 때,
관심사를 분리하기 위한 강력한 무기들을 정리해 두었기 때문이다.
그중에서도 이번 문제를 해결한 핵심 무기는 바로 '인터페이스'다.
하지만, 막상 적용하려 하니 예상보다 훨씬 많은 시행착오가 있었다.
과연 이 선택이 진짜 옳은 방향일까?
수없이 고민했던 그 과정을 아래에 기록해두려 한다.
1. 특정 모듈 전용 인터페이스를 설계한다
가장 먼저 시도한 것은
admin 모듈에서만 사용하는 Repository 기능을 인터페이스로 분리하는 것이었다.
public interface AdminTermsRepository {
Terms save(Terms terms); // 약관 저장
}
이렇게 인터페이스를 분리하면,
admin 모듈은 필요한 기능만 접근할 수 있게 된다.
2. 해당 인터페이스의 구현체를 설계한다
처음엔 Spring Data JPA의 확장 방식으로 쉽게 구현하려 했다.
public interface JpaTermsRepository extends JpaRepository<Terms, Long>, AdminTermsRepository {
}
JpaTermsRepository가 AdminTermsRepository까지 상속받으면
두 인터페이스의 구현체를 하나로 생성해 줄 거라 생각했다.
하지만 문제는 생각보다 복잡했다.
JPA는 내부적으로 프록시 객체를 생성하여 JpaRepository의 구현체를 만들어준다.
이 구조에서는 추가로 상속한 AdminTermsRepository의 구현 메서드가
Spring의 프록시 생성 과정과 충돌할 수 있다.
(※ 이 구현체 생성 방식과 기술적 한계는 다른 글에서 정리할 예정이다.)
그래서 최종적으로는 아래처럼 구성했다.
@Repository
public class TermsRepository implements AdminTermsRepository {
private final JpaTermsRepository jpaTermsRepository;
@Override
public Terms save(Terms terms) {
return jpaTermsRepository.save(terms);
}
}
TermsRepository는 AdminTermsRepository을 구현하고,
내부적으로는 JpaTermsRepository를 위임(Composition) 받는다.
이렇게 구성하면,
TermsRepository를 빈으로 등록하면서도
admin 모듈은 오직 필요한 기능만 AdminTermsRepository 타입으로 주입받을 수 있다.
3. 공통 모듈을 분리하자
여기서부터 본격적으로 구조 설계에 대한 고민이 깊어지기 시작했다.
“그럼 AdminTermsRepository 인터페이스의 위치는 어디가 적절할까?”
처음에는 admin 모듈 안에 두는 것이 자연스럽다고 생각했다.
왜냐하면 실제로 이 인터페이스를 사용하는 쪽은 admin 모듈이기 때문이다.
하지만 문제가 생겼다.
common 모듈에서 구현체(TermsRepository)를 만들기 위해
admin 모듈의 인터페이스를 참조해야 하는 구조가 된 것이다.
→ 💥 모듈 간의 순환 의존이 생겨버린다.
4. 그 사이에 하나 더, 조정자 역할의 common-api 도입
이때 떠오른 개념이 있었다.
바로 “조정자 객체”.
(이 또한 “레이어 및 패키지 구조 설계” 글을 작성하며 정립한 것이다)
과거에 도메인 간 상호 의존을 줄이기 위해 사용했던 이 패턴처럼,
common과 admin 사이를 분리할 중간 지점이 필요하다는 생각이 들었다.
그래서 새로운 모듈인 common-api를 만들게 되었다.
이 모듈은 특정 기능만 의도적으로 분리해서 제공하는 역할을 맡는다.
📌 이제 의존 구조는 이렇게 된다:
admin → common-api ← common
- AdminTermsRepository: common-api에 위치
- admin: 이 인터페이스만 참조
- common: 이 인터페이스를 구현만 할 뿐, admin을 직접 의존하지 않음
→ 모듈 간 순환 의존을 깨끗하게 분리할 수 있었다.
처음에는 모듈이 하나 더 늘어나는 게 맞는 선택일까? 걱정도 컸다.
설정할 것도, 관리할 것도, 추적할 책임도 늘어나니까.
하지만 끝까지 설계를 고민하면서 내린 결론은 하나였다.
설계는 내 편의를 위한 게 아니라,
의도를 명확히 표현하고, 의미를 보호하기 위한 것이다.
모듈 하나 늘어난 불편함보다
“명확한 책임 분리와 안전한 의존성 구조”가 훨씬 큰 가치라고 믿었기에,
기꺼이 common-api를 도입하기로 했다.
✅ 왜 이런 구조가 좋은가?
이번 설계는 단순히 "인터페이스를 하나 더 만들었다"는 차원의 얘기가 아니다.
이 선택을 통해 의도하지 않은 의존을 차단하고,
모듈 간의 경계를 명확히 나누고,
필요한 기능만 안전하게 전달하는 구조를 만든 것이 핵심이다.
이 설계가 가진 장점을 아래와 같이 정리할 수 있다.
1. 불필요한 의존을 사전에 차단할 수 있다 (안전성)
공통 모듈의 Repository가 모든 기능을 그대로 노출한다면,
사용자가 의도치 않게 민감한 기능까지 사용할 위험이 있다.
하지만 인터페이스를 분리하면,
필요한 기능만 제공하고 나머지는 애초에 접근 자체를 막을 수 있다.
❌ "조심해서 쓰자" → ✅ "애초에 못 쓰게 설계하자"
2. 의존 방향을 통제할 수 있다 (유지보수성)
admin → common 방향으로의 의존은 괜찮지만,
common → admin 방향으로 의존하면 모듈 설계가 깨진다.
common-api라는 중간 계층을 도입하면서
서로 의존하지 않으면서도 기능을 주고받을 수 있는 구조를 만들었다.
이건 단순한 분리 이상의 효과다.
모듈의 독립성과 확장성을 지키는 설계적 장치가 되는 것이다.
3. SOLID 원칙을 자연스럽게 지킬 수 있다 (설계 철학)
- DIP (Dependency Inversion Principle)
→ 고수준 모듈(admin)은 저수준 모듈(common)의 구현이 아니라
공통 인터페이스(common-api)에 의존한다. - ISP (Interface Segregation Principle)
→ 한 모듈이 필요한 기능만 사용하도록
작고 명확한 인터페이스를 제공했다.
→ 이 두 가지 원칙은 이번 구조에서 의도하지 않아도 저절로 지켜졌다.
"의미 있는 추상화"를 고민했다면 자연스러운 결과다.
4. 모듈 간 관심사가 명확히 구분된다 (가독성과 의도 표현)
기존에는 common 모듈에 너무 많은 기능이 들어가
“이 기능이 누구를 위한 건지”가 모호해졌다.
하지만 지금은
- admin 모듈: AdminTermsRepository 의존
- customer 모듈: CustomerTermsRepository 의존
- 구현체는 모두 common 모듈
→ 사용자의 입장에서 명확한 진입점이 생기고,
구현자의 입장에서도 책임이 나뉘어 유지보수가 쉬워진다.
작은 구조의 변화가 가져온 건
기능 분리 그 이상의 철학적 설계의 전환이었다.
결론
멀티 모듈 프로젝트에서 공통 모듈의 장점을 살리면서도,
각 모듈이 자신에게 필요한 기능만 의존하도록 만드는 건
단순한 구조적 분리가 아니라 안전한 아키텍처를 위한 선택이었다.
필자는 특정 모듈이 불필요한 공통 기능까지 모두 접근하게 되는 구조는
위험한 부작용을 낳을 수 있다고 판단했다.
이를 해결하기 위해,
각 모듈 전용 인터페이스를 설계하고
common-api라는 중간 모듈을 도입함으로써,
모듈 간의 책임과 의존성을 명확하게 분리했다.
그 결과,
이번 설계는 안전성과 유지보수성, 확장성 모두를 고려한
균형 잡힌 구조가 되었다.
생각 정리
이번 글은 common-api 모듈이라는 작은 구조를 도입하기까지
그 선택이 과연 옳은가 수없이 고민했던 나를 돌아보며 작성한 글이다.
이상하게 느껴질지도 모르지만,
작은 문제 하나를 깊이 고민하다 보면,
그 해결 과정에서 자연스럽게 SOLID 원칙을 지키게 되는 경험을 종종 하게 된다.
이번 경험도 그랬다.
"설계 원칙을 지켜야지!" 하고 시작한 게 아니었다.
단지 더 안전하고 명확한 설계를 하고 싶었을 뿐이다.
그런데 뒤를 돌아보니,
DIP와 ISP 같은 원칙을 자연스럽게 실천하고 있었다.
솔직히 말하면, 예전의 나는 이런 설계를 하지 못했다.
혹은 설계의 중요성을 알고도 외면했다.
“코드 재사용만 잘 되면 되는 거 아냐?”
“@Lazy로 어떻게든 순환 참조 피하면 되는 거지”
그렇게 스스로를 합리화하며,
지저분한 설계를 방치하거나 때론 만들어내기도 했다.
그리고 그 대가는 혹독했다.
더 복잡해지고, 예측하기 어려운 시스템을 마주해야 했다.
이제는 알게 됐다.
과거의 나는 “원칙”이 부족했던 게 아니다.
“태도”가 부족했다.
내가 작성하는 코드는
단지 기능을 수행하는 수단이 아니라,
수많은 사람들과 협업하고, 비즈니스를 지탱하는 기반이다.
그런데 나는 그 위에
편의성이라는 이름의 설계 부채를 올려두고 있었던 거다.
이제라도 나는 그렇게 하지 않으려 한다.
작은 기능 하나를 만들 때도,
“이건 정말 이 모듈이 알아야 할 책임인가?”
“이 구조는 의도를 명확히 드러내는가?”를 스스로 묻는다.
물론 여전히 부족한 점은 많다.
지금도 "내가 무의식적으로 위험한 설계를 하고 있는 건 아닐까?"
스스로 점검하고 또 점검한다.
그리고 때로는
모든 걸 안전하게 만들겠다는 과도한 완벽주의에 빠질 뻔한 적도 있었다.
하지만 결국 중요한 건 이거다.
설계는 내 편의를 위해 존재하는 게 아니라,
팀을 위해, 사용자와 협업을 위해 존재하는 것.
그렇기 때문에,
조금 불편해도, 시간이 좀 더 걸려도,
나는 의미 있는 설계를 선택할 것이다.
그리고 그 중심에는 언제나
“필요한 것만 의존하고, 의미 없는 연결은 제거하자”는
이 철학이 놓여 있을 것이다.
'개발 일기' 카테고리의 다른 글
[고찰하기] Lock 전략을 결정하는 건 기술이 아니라 도메인이다 (0) | 2025.04.03 |
---|---|
[사이드 프로젝트] 일관된 응답 구조 설계 (0) | 2025.04.02 |
[고찰하기] 엔티티가 아니라 "도메인"을 설계하기 (0) | 2025.03.31 |
[사이드 프로젝트] 기능 분석: 약관 도메인 (0) | 2025.03.27 |
[고찰하기] 레이어 및 패키지 구조 설계 (0) | 2025.03.23 |