좋은 객체 지향 설계를 위해 우리는 SOLID 원칙을 배웁니다. 하지만 이론만 알고 코드를 짜다 보면 "인터페이스를 도입했는데 왜 코드를 계속 고쳐야 하지?"라는 의문에 빠지게 됩니다. 오늘은 그 의문을 해결해준 '관심사의 분리'와 이를 자동화해주는 '스프링 컨테이너'에 대해 정리해 보겠습니다.
https://run-code-rich.tistory.com/entry/why-we-need-spring-di-with-solid
"왜 다형성만으로는 부족할까? SOLID 원칙과 스프링 DI의 본질"
1. 객체 지향 프로그래밍 (OOP)객체 지향 프로그래밍은 프로그램을 단순히 명령어의 나열로 보는 것이 아니라, 여러 개의 독립된 단위인 '객체'들의 모임으로 파악하고자 하는 것입니다.본질: 각
run-code-rich.tistory.com
1. 다형성만으로는 부족했던 이유 (DIP, OCP의 위기)
우리가 배송 시스템을 만든다고 가정해 봅시다. 처음에는 '기본 배송비 정책'을 사용하다가, 나중에 '지역별 차등 배송비 정책'으로 바꾸라는 요구사항이 들어옵니다.
public class OrderService {
// 1. 인터페이스(ShippingPolicy)에 의존함 (OK)
// 2. 하지만 구현 클래스(BasicShippingPolicy)도 직접 생성함 (DIP 위반!)
private ShippingPolicy shippingPolicy = new BasicShippingPolicy();
}
여기서 정책을 바꾸려면 결국 클라이언트인 OrderService의 코드를 열어서 직접 수정해야 합니다.
- OCP(개방-폐쇄 원칙) 위반: 기능을 확장하는데 기존 코드를 고쳐야 함.
- DIP(의존관계 역전 원칙) 위반: 추상화(인터페이스)뿐만 아니라 구체 클래스(구현체)에도 의존하고 있음.
결국, 인터페이스만 도입한다고 해서 모든 문제가 해결되는 것은 아니었습니다.
2. 해결책: 관심사의 분리와 AppConfig의 등장
이 문제를 해결하려면 누군가 외부에서 "너는 이 배송비 정책을 써!"라고 대신 결정해줘야 합니다. 여기서 모든 객체의 생성과 연결을 담당하는 AppConfig가 등장합니다. 이것이 바로 관심사의 분리입니다.
- 사용 영역(Service, Repository): 자신의 로직(주문 처리 등)을 실행하는 데만 집중합니다. 마치 연극의 배우와 같습니다.
- 구성 영역(AppConfig): 어떤 정책을 사용할지 결정하고 객체를 생성해 연결합니다. 연극의 공연 기획자 역할입니다.
3. 생성자 주입으로 DIP 완성하기
이제 OrderService는 더 이상 특정 클래스를 직접 만들지 않습니다. 그저 생성자를 통해 어떤 정책이든 들어오기만을 기다립니다.
public class OrderService {
private final ShippingPolicy shippingPolicy;
// 생성자를 통해 외부(AppConfig)에서 주입(Injection)받음
public OrderService(ShippingPolicy shippingPolicy) {
this.shippingPolicy = shippingPolicy;
}
}
이제 OrderService 코드는 고정됩니다. 배송비 정책이 수백 번 바뀌어도 이 코드는 단 한 줄도 수정할 필요가 없습니다. 제어권이 외부로 넘어간 제어의 역전(IoC)과 의존관계 주입(DI)이 일어난 것입니다.
4. 정적 의존관계 vs 동적 의존관계
이 개념은 설계의 유연성을 이해하는 핵심입니다.
- 정적인 클래스 의존관계: 클래스 다이어그램이나 import 문만 봐도 알 수 있는 관계입니다. OrderService가 ShippingPolicy를 사용한다는 사실은 변하지 않습니다.
- 동적인 객체 인스턴스 의존관계: 앱이 실행될 때(Runtime) AppConfig가 실제로 어떤 객체를 꽂아주느냐에 따라 결정되는 관계입니다.
핵심 포인트: "의존관계 주입(DI)을 사용하면, 정적인 클래스 의존관계를 변경하지 않고도 실행 시점의 객체 인스턴스를 자유롭게 바꿀 수 있다."
5. 더 나아가서: 스프링 컨테이너(Spring Container)
지금까지는 우리가 순수 자바 코드로 AppConfig를 직접 관리했습니다. 하지만 프로젝트가 커지면 관리해야 할 객체(Bean)가 너무 많아집니다. 이를 자동화해주는 것이 바로 스프링 컨테이너입니다.
@Configuration
public class AppConfig {
@Bean
public OrderService orderService() {
return new OrderServiceImpl(shippingPolicy());
}
@Bean
public ShippingPolicy shippingPolicy() {
return new BasicShippingPolicy();
}
}
이제 우리는 AppConfig에 @Configuration과 @Bean 설정을 더해 스프링에게 넘겨줍니다.
- 스프링 컨테이너의 역할: 설정 정보를 읽어 객체(Bean)를 미리 생성하고, 필요한 곳에 주입해 의존관계를 연결해줍니다.
- 싱글톤 보장: 똑같은 객체라면 딱 하나만 만들어 공유하므로 메모리 효율이 극대화됩니다.
6. 결론: 왜 이렇게까지 해야 할까?
- SRP(단일 책임 원칙): 서비스는 로직만, 설정은 AppConfig만 담당합니다.
- DIP/OCP 완성: 새로운 기능을 추가하거나 변경할 때 구성 영역(AppConfig)만 수정하면 되며, 사용 영역(Service)은 변경에 닫혀 있게 됩니다.
추상화라는 비용(코드를 한 번 더 열어봐야 하는 탐색 비용)이 발생하지만, 이를 통해 얻는 유지보수의 편리함과 확장성은 비교할 수 없을 만큼 큽니다. 비즈니스 코드를 외부의 변화로부터 지키는 '성역'으로 만드는 것, 그것이 우리가 스프링과 DI를 사용하는 이유입니다.
'TECH > 웹개발' 카테고리의 다른 글
| [Java] 메서드 체이닝(Method Chaining) 완벽 가이드: 원리부터 실무 활용까지 (0) | 2026.02.10 |
|---|---|
| [Java] Optional 완벽 가이드: 더 이상 NullPointerException은 없다 (0) | 2026.02.09 |
| [Java] 람다식(Lambda) 완벽 가이드: 개념부터 실무 활용 예시까지 (0) | 2026.02.07 |
| [Java] "for문은 이제 그만!" Java Stream API 실무 심화 가이드: 지연 연산부터 병렬 처리까지 (0) | 2026.02.06 |
| "왜 다형성만으로는 부족할까? SOLID 원칙과 스프링 DI의 본질" (0) | 2026.01.19 |