코드 안에서 필요한 객체가 있으면 그 자리에서 바로 생성해 쓰는 경우가 많아요. 그런데 테스트 코드를 짜려는 순간 벽에 부딪힙니다. 클래스 안에 콕 박힌 생성 코드 때문에 도무지 갈아끼울 수가 없거든요.
의존성 주입(DI)이 뭔지 검색해봐도 “제어의 역전이니 IoC 컨테이너니” 하는 말만 나와서 더 헷갈리기 쉽습니다.
그래서 오늘은 최대한 쉬운 방식으로 풀어보려고 합니다. 결론부터 말씀드릴게요.
의존성 주입(DI)이란 결국 클래스 안에서 직접 만들던 객체를 밖에서 만들어 넣어주는 것입니다. 딱 그 한 가지 이동이 핵심이에요. 객체 생성의 위치를 클래스 밖으로 꺼내는 것. 그게 DI의 거의 전부입니다.
이 글을 끝까지 읽으시면 DI가 왜 필요한지, 코드로는 어떻게 바뀌는지, 그리고 Swinject 같은 DI 라이브러리가 왜 이걸 대신 해주는지까지 감이 잡히실 거예요.
객체 생성이 클래스 안에 있으면 뭐가 문제일까요?
예를 들어 주문을 처리하는 클래스가 있다고 해볼게요. 이 클래스는 결제를 위해 결제 모듈이 필요합니다.
가장 흔한 초보 코드는 이렇게 생겼어요.
class OrderService {
// 클래스 안에서 직접 결제 객체를 생성
private let pay = KakaoPay()
func order() { pay.pay() }
}
문제는 이 OrderService가 KakaoPay에 딱 붙어버렸다는 겁니다.
카카오페이 대신 네이버페이로 바꾸고 싶으면요? 클래스 안으로 들어가서 생성 부분을 고쳐야 해요.
테스트할 때 가짜 결제 객체를 넣고 싶어도 방법이 없습니다. 이미 안에서 진짜 KakaoPay를 만들어버렸으니까요.
즉 객체 생성을 클래스 안에 두면 그 클래스는 특정 구현에 손발이 묶여버립니다.
그래서 객체 생성을 밖으로 꺼내봤습니다
해결은 생각보다 싱겁습니다. 직접 만들지 말고, 밖에서 만들어서 받기만 하면 돼요.
class OrderService {
private let pay: Pay
// 밖에서 만들어 넣어준 걸 받기만 함
init(pay: Pay) { self.pay = pay }
func order() { pay.pay() }
}
달라진 건 딱 하나예요. 직접 생성이 사라지고 생성자(이니셜라이저)로 받는 형태가 됐습니다.
이제 OrderService는 결제가 카카오페이인지 네이버페이인지 몰라도 됩니다. 그냥 “Pay라는 걸 받아서 쓴다”까지만 알아요.
DI의 본질은 거창한 게 아닙니다. 객체 생성을 클래스 밖으로 옮겨, 무엇을 넣을지 결정할 권한을 바깥으로 넘기는 것뿐입니다.
이렇게 밖에서 객체를 넣어주는 걸 “주입한다(injection)“고 부릅니다. 의존하는 객체를 주입한다, 그래서 의존성 주입이에요.
이름이 어려웠을 뿐이지, 하는 일은 방금 보신 그게 전부입니다.
DI를 하면 뭐가 좋아지나요?
말로만 좋다고 하면 와닿지 않으니, 실제로 뭐가 달라지는지 정리해봤어요.
- 교체가 쉬워집니다: 카카오페이를 네이버페이로 바꿔도 OrderService는 한 줄도 안 고칩니다. 넣어주는 객체만 바꾸면 돼요.
- 테스트가 편해집니다: 진짜 결제 대신 가짜(Mock) 결제 객체를 넣어서 돈 안 나가고 로직만 검증할 수 있어요.
- 역할이 분리됩니다: OrderService는 “주문 처리”에만 집중하고, “무슨 결제를 쓸지”는 바깥이 결정합니다.
저는 세 번째가 제일 크게 와닿았어요. 클래스가 자기 일에만 집중하니까 코드가 훨씬 읽기 편해지더라고요.
정리하면 이렇습니다.
| 구분 | 생성이 안에 있을 때 | 생성을 밖으로 뺐을 때(DI) |
|---|---|---|
| 결제 교체 | 클래스 수정 필요 | 넣는 객체만 교체 |
| 테스트 | 가짜 객체 못 넣음 | Mock 주입 가능 |
| 관심사 | 생성+로직 뒤섞임 | 로직에만 집중 |
그럼 Swinject 같은 DI 라이브러리는 왜 필요한가요?
여기까지 오면 자연스럽게 드는 의문이 있어요. “밖에서 넣어준다는 건 알겠는데, 그 ’밖’은 누가 관리하죠?”
객체가 몇 개 안 되면 손으로 만들어서 넣어주면 됩니다. 그런데 실무 프로젝트는 객체가 수백 개고 서로 얽혀 있어요.
이걸 사람이 일일이 순서 맞춰 만들어 넣는 건 지옥입니다.
그래서 이 “밖에서 만들어 넣어주는 일”을 대신 해주는 도구가 등장해요. Swift 진영의 Swinject나 Factory 같은 DI 컨테이너가 바로 그 역할입니다.
내가 필요한 객체를 등록해두면, 컨테이너가 알아서 만들고 필요한 곳에 넣어줍니다. 제가 직접 생성할 일이 거의 사라지는 거죠.
흔히 말하는 “제어의 역전(IoC)“도 여기서 나옵니다. 객체를 만들고 연결하는 제어권이 내 코드에서 컨테이너로 넘어갔다는 뜻이에요.
말하자면 DI는 개념이고, DI 컨테이너는 그 개념을 대신 실행해주는 일꾼입니다.
자주 묻는 질문
Q. DI랑 IoC는 같은 말인가요? 아니요. IoC(제어의 역전)는 “제어권을 넘긴다”는 넓은 원칙이고, DI는 그걸 실현하는 구체적 방법 중 하나예요. DI는 IoC의 한 종류라고 보시면 됩니다.
Q. 생성자 말고 다른 주입 방법도 있나요? 네. 생성자 주입, 프로퍼티 주입, 메서드 주입이 있어요. 다만 Swift에서는 생성자 주입을 권장합니다. 의존성이 let으로 고정돼 값이 바뀌지 않고 테스트하기도 편하거든요.
Q. 무조건 라이브러리가 있어야 DI인가요? 아닙니다. 방금 손으로 생성자에 넣어준 것도 엄연한 DI예요. 라이브러리는 그걸 자동화해줄 뿐입니다.
오늘은 의존성 주입을 객체 생성 위치라는 아주 작은 이동으로 풀어봤습니다.
DI가 어렵게 느껴졌다면, 딱 하나만 기억하세요. “클래스 안에서 만들지 말고, 밖에서 만들어 넣자.” 이 문장 하나면 나머지는 자연스럽게 따라옵니다.
테스트 코드 앞에서 막막함을 느껴보셨다면, 오늘 본 예제를 직접 손으로 한 번 바꿔보시길 권해요. 눈으로 볼 때보다 훨씬 확실하게 감이 옵니다.
