소프트웨어 설계 관련해서 책 중 하나인 클린 아키텍처(로버트 C. 마틴 저)를 기반으로 한 책인 만들면서 배우는 클린 아키텍처라는 책을 최근 읽었다. 이 책은 단순히 설계 패턴을 소개하는 책이라기보다 어떤 방식으로 시스템을 설계해야 장기적으로 안정적이고 유지보수 가능한지를 이야기 하고 있다.
그중에서도 특히 핵심 개념은 의존성 역전 원칙(DIP, Dependency Inversion Principle) 이었다. 이 글에서는 DIP의 이론적 의미와 필요성, 클린 아키텍처에서 DIP가 어떻게 활용되는지 그리고 내가 고민하고 있는 코드 개선 방향까지 정리해보고자 한다.
의존성 역전 원칙(DIP)
DIP는 잘 알려진 SOLID 원칙 다섯 가지 중 마지막 항목으로 정의는 다음과 같다.
고수준 모듈(정책, 비즈니스 규칙)은 저수준 모듈(세부 구현)에 의존하지 않는다. 둘 다 추상화(인터페이스)에 의존해야 하며, 추상화는 세부 구현에 의존하지 않는다.
여기서 중요한 점은 의존성의 방향이다.
일반적인 코드에서는 서비스나 비즈니스 로직 같은 상위 계층이 데이터베이스, 프레임워크, 외부 API 같은 하위 계층에 직접 의존하는 경우가 많다. 하지만 DIP는 이 관계를 뒤집어, 고수준 모듈이 구체 구현에 묶이지 않도록 만든다.
예를 들어
- 서비스 계층은 AccountJPARepository 같은 구체 구현이 아니라 LoadAccountPort, SaveAccountPort 같은 추상적 인터페이스만 알아야 한다.
- JPA, MongoDB, Redis 같은 세부 기술은 이 인터페이스를 구현하는 어댑터로 동작한다.
이런 구조에서는 서비스 계층이 어떤 저장소 기술을 쓰는지 알 필요가 없다. 언제든 저장소 구현을 교체하거나 추가할 수 있으며 테스트 시에도 인터페이스만 모킹(mocking)해 빠르게 단위 테스트를 작성할 수 있다.
표면적으로 보면 인터페이스로 구현체를 분리하라는 단순한 얘기 같지만, DIP의 목적은 훨씬 근본적이다.
변경 가능성 관리
세부 기술은 반드시 바뀐다. 데이터베이스 교체, 프레임워크 업데이트, 외부 API 추가는 장기 프로젝트에서 흔하다. DIP를 지키면 핵심 비즈니스 로직은 그대로 두고 외부 모듈만 교체하거나 추가할 수 있다. DIP가 없으면 서비스 로직, 도메인 모델까지 연쇄적으로 수정해야 하며 유지보수 피로가 급격히 커진다.
테스트 용이성 확보
하위 모듈에 직접 의존하면 테스트할 때 실제 DB나 외부 시스템을 붙여야 한다. DIP 구조에서는 추상화된 인터페이스만 모킹해도 서비스, 도메인 로직을 독립적으로 검증할 수 있다. 이는 테스트 속도와 안정성을 크게 높인다.
장기적 유지보수성
작은 프로젝트나 초기 단계에서는 단순 설계로 충분할 수 있다. 하지만 프로젝트가 성장하면 서비스 간 얽힘, 기술 변경, 코드 복잡성이 급격히 늘어난다. DIP는 이런 복잡성을 제어할 수 있는 중요한 설계 전략이다.
클린 아키텍처에서 DIP의 역할
클린 아키텍처는 DIP를 시스템 전체에 적용하는 데 초점을 맞춘다. 핵심 아이디어는 의존성은 항상 안쪽(도메인, 비즈니스 규칙)으로 향해야 한다 는 것이다.
구체적으로
- 가장 안쪽: 도메인 엔티티, 비즈니스 규칙
- 중간: 유스케이스, 애플리케이션 서비스
- 바깥쪽: 어댑터(웹, DB, 외부 시스템), 프레임워크, UI
모든 의존성은 안쪽으로 향하고, 바깥 계층은 언제든 교체 가능한 플러그인처럼 설계한다. 이를 위해 중간 계층에 포트(인터페이스)를 두어 경계를 만든다.
예를 들어
- SendMoneyUseCase (포트): 유스케이스의 인터페이스.
- SendMoneyService (애플리케이션): 포트를 구현.
- AccountPersistenceAdapter (어댑터): 영속성 계층에서 포트를 구현.
이렇게 설계하면 DB, 프레임워크, UI를 바꿔도 도메인과 유스케이스는 영향받지 않는다. 변경 비용은 바깥쪽에서만 발생하고 핵심 로직은 안전하게 보호된다.
코드 개선 방향
내가 다루는 Spring Boot 기반 프로젝트들은 현재 JPA 중심 설계로 되어 있다.
@Repository
public interface PostRepository extends JpaRepository<Post, Long>
{ ... }
@RequiredArgsConstructor
@Service
@Slf4j
public class PostService {
private final PostRepository postRepository;
...
}
@Table(name = "posts")
@Getter
@Builder
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Post extends BaseEntity {
...
}
서비스 계층에서는 PostRepository라는 인터페이스를 주입받고 있지만, 이 인터페이스는 JpaRepository를 상속하고 있다. 즉, 형식적으로는 인터페이스에 의존하고 있는 것처럼 보이지만, 실제로는 JPA라는 특정 구현체에 종속된 인터페이스에 의존하고 있는 구조다.
결과적으로 PostService는 JPA를 사용하는 영속성 계층과 강하게 결합되어 있다. 이처럼 상위 계층이 특정 DB 기술에 직접 연결되어 있으면, 구조 변경이나 기술 교체가 매우 어렵다.
예를 들어 MongoDB 같은 NoSQL DB를 도입하려 할 경우 다음과 같은 문제가 발생한다
- JPA의 @Entity가 아닌 MongoDB의 @Document 어노테이션을 써야 하므로, 엔티티 구조 자체를 바꿔야 한다.
- JPA 기반의 PostRepository는 MongoDB에서는 동작하지 않으므로, 완전히 다른 리포지터리를 정의해야 한다.
- 그에 따라 서비스 계층의 코드도 함께 변경된다.
뿐만 아니라, 테스트 코드에서도 JPA를 떼어내지 못하면 빠른 단위 테스트 작성이 어렵고, 외부 시스템에 의존하는 느리고 불안정한 테스트가 될 수 있다.
이런 구조는 의존성 역전 원칙(DIP) 뿐만 아니라, 개방-폐쇄 원칙(OCP)도 위반하고 있는 셈이다. 처음에는 간단하고 생산성이 높을 수 있지만, 시간이 지날수록 유지보수성과 확장성 측면에서 제약이 커질 수밖에 없다
그래서 앞으로 내가 의존성 역전 원칙을 적용하여 시도해보고 싶은 개선 방향은 다음과 같다
- 서비스 계층은 LoadPostPort, SavePostPort 같은 추상적 포트(인터페이스)에만 의존하도록 설계를 수정한다.
- 현재는 JPA 어댑터가 이 포트를 구현하지만 나중에 MongoDB나 Redis용 어댑터를 추가할 수 있도록 구조를 열어둔다.
이 개선을 통해 DIP가 단순한 이론이 아니라, 실제 코드 안에서 장기적인 유지보수성과 확장성을 어떻게 높이는지 확인해보고 싶다.
DIP 적용의 한계
책에서도 강조하지만 DIP는 만능 해법은 아닐 수 있다. 다음과 같은 현실적 고민이 필요하다고 말하고 있다.
과도한 인터페이스 분리 문제
- 모든 작은 CRUD 서비스까지 포트와 어댑터로 쪼개면 오히려 보일러플레이트 코드만 늘고 생산성이 저하될 수 있다.
팀 합의 필요
- 어디까지 DIP를 적용할지 어느 수준에서 단순 설계를 유지할지는 팀 내 합의가 필요하다. 혼자만 DIP를 밀어붙이면 코드베이스에 혼란을 줄 수 있다.
설계 복잡도 증가
- DIP를 지키려면 설계가 한두 단계 더 복잡해지고 학습 비용도 올라간다. 팀원이 모두 설계 의도를 이해하지 못하면 오히려 관리 비용이 커질 수 있다.
마치며
클린 아키텍처는 설계 원칙이나 패턴을 단순히 암기하라고 강요하는 책이 아닌거 같다. 실제로 읽어보면 오히려 왜 그 설계를 하고 있는가?, 그 설계가 장기적으로 시스템에 어떤 영향을 주는가? 같은 질문을 끊임없이 던지고 있다.
특히 DIP는 이론적으로만 보면 간단해 보이지만, 실전에서 적용하려고 하면 많은 고민과 설계 판단이 필요하다는 점을 깨달았다.앞으로 내 코드 안에서 DIP를 점진적으로 적용하며, 내 코드를 확장성이 높은 코드 유지보수하기 좋은 코드로 바꾸고 싶다.
과도한 설계 복잡성을 피하면서도, 장기적으로 유지보수가 가능한 구조를 만들고 기술 교체나 확장에 강한 시스템을 만들어가는 연습을 해보고 싶다.
'개념' 카테고리의 다른 글
고가용성(High Availability)을 위한 로드 밸런싱과 스케일링 (0) | 2025.04.21 |
---|---|
상황 따라 골라 쓰는 데이터베이스 (1) | 2025.03.23 |
MySQL과 PostgreSQL의 차이 (0) | 2025.03.16 |
[Docker 입문] Registry & Repository (0) | 2025.03.09 |
[Docker 입문] 컨테이너 띄워보기 (0) | 2025.03.06 |