들어가며: "객체는 자신의 일을 해야 할까, 아니면 필요한 객체까지 직접 만들어야 할까?"
앞 장에서는 객체지향 프로그래밍이 역할과 구현을 분리하고, 다형성을 통해 변화에 유연한 설계를 지향한다는 점을 다루었다. 하지만 실제 프로젝트 현장에서는 이러한 이상적인 구조를 지속해서 유지하기가 매우 어렵다. 그 근본적인 이유는 대부분의 객체가 자신의 본래 책임을 수행하는 것뿐만 아니라, 자신이 사용할 객체를 직접 생성하고 조립하는 책임까지 떠안고 있기 때문이다.
이번 장에서는 객체가 직접 객체를 생성하는 행위가 어떻게 객체지향의 유연성을 무너뜨리는지 철저히 파헤쳐 본다.
그리고 본인 스스로 다음 한 가지 명확한 결론을 깨닫도록 이끄는 것이 이번 장의 핵심 목적이다.
💡 이번 장의 핵심 결론: "객체 생성(Construction)과 객체 사용(Use)은 결코 같은 객체가 담당해서는 안 된다."
이 깨달음이 뇌리에 각인되어야만, 비로소 다음 장에서 배울 IoC(제어의 역전)와 DI(의존성 주입)가 필수 불가결한 솔루션으로 자연스럽게 다가오게 된다.
2.1 객체는 왜 다른 객체를 필요로 하는가?
현실 세계의 소프트웨어에서 단 하나의 객체가 모든 일을 독식하는 구조는 존재하지 않는다. 객체는 자신의 책임을 완수하기 위해 필연적으로 다른 객체의 도움이 필요하다. 온라인 쇼핑몰의 주문 서비스를 코드로 표현해 보자.
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void order(Order order) {
// 주문 관련 비즈니스 로직 수행...
// 결제 처리는 결제 전문 객체에게 위임
paymentService.pay(order);
}
}
OrderService의 본래 책임은 주문 요청을 검증하고 처리하는 것이다. 하지만 결제 처리라는 정교한 세부 작업은 자신의 전문 분야가 아니므로 PaymentService라는 타 객체에게 요청(메시지 전송)한다.
이처럼 하나의 객체가 자신의 책임을 수행하기 위해 다른 객체를 이용하는 관계를 의존성(Dependency)이라고 부른다. 의존성 자체는 나쁜 것이 아니다. 오히려 객체들이 협력하는 객체지향 생태계에서는 지극히 자연스럽고 건강한 현상이다. 진짜 문제는 "그 의존하는 대상(객체)을 누구의 손으로 직접 만들어 올리고 연결하느냐"에서 발생한다.
2.2 가장 흔한 방식이자 비극의 시작, new
자바를 배운 개발자들이 코드를 작성할 때 가장 습관적으로 적어 내리는 방식은 다음과 같다.
public class OrderService {
// 필요한 객체를 클래스 내부에서 직접 new로 생성한다.
private final PaymentService paymentService = new KakaoPaymentService();
public void order(Order order) {
paymentService.pay(order);
}
}
언뜻 보면 자바 프로그래밍의 정석처럼 보인다. 필드 선언과 동시에 객체를 인스턴스화하여 사용하는 지극히 평범한 코드다. 하지만 이 단 한 줄의 new KakaoPaymentService()는 OrderService에게 본래 맡은 범위를 초과하는 두 가지 중대한 책임을 동시에 부여한다.
- 주문 흐름을 처리하는 책임 (주업)
- 어떤 결제 구체 구현체를 사용할지 직접 결정하고 생성하는 책임 (부업)
원래 OrderService는 주문 로직만 신경 쓰면 되는 객체였다. 그러나 이 순간 객체의 생명주기까지 직접 관리하게 되면서 단일 책임 원칙(SRP, Single Responsibility Principle)이 무너지기 시작한다.
2.3 객체 생성까지 담당할 때 발생하는 3가지 문제
클라이언트 객체가 타 객체의 생성권까지 쥐고 있을 때, 코드는 급격하게 썩어 들어가기 시작한다.
첫 번째 문제: 구현체에 강하게 결합된다 (OCP 위반)
현재 코드에는 new KakaoPaymentService()라는 구체 클래스명이 명시적으로 타투처럼 박혀 있다.
만약 카카오페이의 수수료 인상으로 인해 토스페이(TossPaymentService)로 결제 모듈을 전환해야 한다면 어떻게 될까?
public class OrderService {
// 결제 수단을 바꿀 때마다 클라이언트 코드를 직접 열어서 수정해야 한다!
private final PaymentService paymentService = new TossPaymentService();
}
OrderService 내부 코드를 직접 수정해야만 한다. 요구사항 확장(결제 수단 변경)이 일어났을 때 기존 코드를 수정해야 하므로, 개방-폐쇄 원칙(OCP, Open-Closed Principle)을 정면으로 위반한다.
두 번째 문제: 단위 테스트가 불가능해진다
단위 테스트(Unit Test)의 핵심은 외부 요소(DB, 네트워크, PG사 통신 등)를 차단하고, OrderService의 주문 로직만 0.01초 만에 빠르게 검증하는 것이다. 이를 위해서는 실제 결제를 실행하지 않는 가짜 객체(Mock)를 주입해야 한다. 하지만 객체 내부에서 new KakaoPaymentService()로 단단히 묶여 있다면 외부에서 Mock 객체로 교체할 길이 완전히 차단된다. 결국 테스트를 작성하기 위해서 코드를 더럽히거나, 테스트 자체를 포기하는 상황에 이른다.
세 번째 문제: 파급력이 애플리케이션 전체로 전염된다
서비스가 성장함에 따라 결제 모듈을 필요로 하는 서비스가 늘어난다.

결제 구현체를 변경해야 할 때, new KakaoPaymentService()가 작성된 프로젝트 내의 수십, 수백 개 클래스를 일일이 찾아다니며 수정해야 한다. 누락된 곳이 있다면 런타임에 심각한 장애로 이어진다.
2.4 인터페이스만 도입하면 다 해결될까?
많은 개발자가 이 지점에서 착각에 빠진다. "변수 타입을 인터페이스로 선언해 두었으니 결합도가 낮아진 것 아닌가요?" 절반은 맞고, 절반은 완전히 틀렸다. 다시 문제의 코드를 보자.
// 왼쪽: 인터페이스 (추상화)
// 오른쪽: 구체 클래스 (구현체)
private final PaymentService paymentService = new KakaoPaymentService();
변수의 선언 타입(왼쪽)은 인터페이스가 맞다. 따라서 컴파일 시점 의존성은 인터페이스를 향한다.
하지만 실행을 위해 객체를 인스턴스화하는 시점(오른쪽)에는 여전히 구체 클래스인 KakaoPaymentService를 명확히 알아야만 한다.
결과적으로 런타임 의존성뿐만 아니라 코드 작성 시점의 의존성까지 구체 클래스에 여전히 강하게 매여 있는 상태다. 인터페이스라는 껍데기만 씌웠을 뿐, `new` 키워드가 존재하는 한 강한 결합의 고리는 결코 끊어지지 않는다.
2.5 객체 생성과 객체 사용은 서로 다른 책임이다
이제 본질적인 질문을 던져보자. "주문 로직을 수행하는 객체가, 왜 자신이 사용할 부품을 직접 조립까지 해야 하는가?"
현실 세계의 비유를 들면 이 상황이 얼마나 기이한지 바로 알 수 있다.
- 자동차와 운전자: 운전자는 핸들과 페달을 이용해 자동차를 "사용"한다. 하지만 운전하기 위해 엔진을 직접 가공하고 나사를 조여 자동차를 "제작"하지 않는다. 자동차의 제작은 '공장(Factory)'의 책임이다.
- 식당과 손님: 식당에 온 손님은 셰프가 만든 음식을 "소비"한다. 손님이 직접 주방에 들어가 양파를 까고 고기를 볶아 요리를 "생성"하지 않는다.
소프트웨어 세계의 객체도 완전히 동일하다.
- 객체의 사용(Use): 자신의 역할과 책임을 다하기 위해 비즈니스 로직을 수행하는 것
- 객체의 생성(Construction): 필요한 부품(의존성)을 인스턴스화하고 조립하여 완성품을 만드는 것
이 두 과정은 성격이 완전히 다른 별개의 책임이다.
객체에게 두 일을 동시에 시키는 순간, 그 객체는 순수한 비즈니스 객체가 아니라 "조립공 겸 작업자"라는 이상한 혼종이 되어버린다.
2.6 객체 그래프(Object Graph)와 조립의 책임
애플리케이션이 켜지고 런타임 상태가 되면, 메모리상에는 수많은 객체가 꼬리에 꼬리를 물고 연결된다.

이처럼 실행 시점에 객체들이 서로의 참조를 통해 거대한 네트워크처럼 얽혀 있는 구조를 객체 그래프(Object Graph)라고 부른다.
올바른 객체지향 설계가 되려면, 이 거대한 객체 그래프를 그리는(조립하는) 작업에서 비즈니스 로직 객체들을 완전히 배제해야 한다.
비즈니스 객체들은 이미 완성되어 전달된 타 객체의 메서드를 호출(사용)하기만 해야지, 객체 그래프에 어떤 가지를 새로 붙일지 결정해서는 안 된다.
2.7 누가 객체를 만들어야 할까?
자연스럽게 다음과 같은 근본적인 질문들이 쏟아져 나온다.
- 객체는 자신의 일을 해야 한다면, 대체 누구 손으로 생성해야 하는가?
- 복잡한 의존관계는 누가 연결해 주어야 하는가?
- 런타임의 객체 그래프는 누가 그려주어야 하는가?
Spring 프레임워크가 제시한 정답은 매우 단호하고 명쾌하다.
"애플리케이션의 비즈니스 객체가 아닌, 제3의 존재인 컨테이너(Container)가 전담한다."
객체는 그저 자신이 일할 때 필요한 인터페이스(역할)만 뚫어두고 가만히 기다린다. 그러면 외부의 컨테이너가 알아서 적절한 구현체 객체를 생성하여 뒤로 쓱 넣어준다. 비즈니스 객체는 객체 생성이라는 무거운 짐을 벗어던지고, 오직 자신의 본업에만 집중할 수 있게 된다. 이것이 바로 객체 생성을 애플리케이션 코드에서 프레임워크로 넘기는 제어의 역전(IoC, Inversion of Control)이 탄생하게 된 진짜 배경이다.
Chapter 2 핵심 정리
- 의존성의 자연스러움: 객체가 타 객체를 이용하는 의존성 자체는 객체지향 협력에서 지극히 당연한 현상이다.
- new 키워드의 위험성: 객체 내부에서 new로 의존성을 직접 만들면 SRP 위반, OCP 위반, 테스트 불능이라는 3대 악재를 맞이한다.
- 인터페이스의 한계: 변수 타입을 인터페이스로 지정해도, new 구체클래스()가 남아있다면 결합도는 전혀 낮아지지 않는다.
- 책임의 철저한 분리: "객체 생성"과 "객체 사용"은 완전히 별개의 책임이다.
- Spring의 해법: 비즈니스 객체에서 객체 생성 및 객체 그래프 구성 책임을 박탈하고, 이를 제3의 존재인 스프링 컨테이너에게 전적으로 위임한다.
'🍃SpringBoot' 카테고리의 다른 글
| Spring IoC & DI 완전 정복: Chapter 4. Spring Container는 무엇인가? (0) | 2026.07.22 |
|---|---|
| Spring IoC & DI 완전 정복: Chapter 3: "제어를 프레임워크에게 넘긴다는 것은 무슨 의미일까?" (0) | 2026.07.22 |
| Spring IoC & DI 완전 정복: Chapter 1: "객체지향은 왜 등장했을까?" (0) | 2026.07.22 |
| JPA [2/4] : 엔티티 매핑과 연관관계 (0) | 2026.06.23 |
| JPA [1/4] : JPA 시작과 영속성 컨텍스트 (0) | 2026.06.23 |
