개요
- Inversion of Control, Dependency Inversion Principle, Dependency Injection에 대해 학습한다.
- DIP를 위해 → IoC 개념으로 → DI를 사용하는 큰 흐름을 가지고 각 내용을 살펴본다.
1. IoC (Inversion of Control) : 제어(관리)의 역전
프로그램의 제어 흐름을 개발자가 아닌 프레임워크/컨테이너가 담당하는 설계 원칙이다.
1.1 제어(관리)의 두 가지 의미
- 객체 생명 주기 제어(생성, 소멸)
- 프로그램 실행 흐름 제어(언제, 어떤 순서로)
1.2 전통적 방식 vs IoC
- IoC 적용 전
class Teacher:
def teach(self):
student = Student() # 내가 직접 생성
result = student.study() # 내가 직접 호출
return result
# main에서 내가 모든 걸 제어함
def main():
teacher = Teacher() # 내가 생성
result = teacher.teach() # 내가 호출
- IoC 적용 후
class Learner(ABC):
@abstractmethod
def study(self) -> str:
pass
class Student(Learner):
def study(self) -> str:
return "학생이 공부 중이에요."
class CollegeStudent(Learner):
def study(self) -> str:
return "대학생이 공부 중이에요."
class Teacher:
# DI로 구현하는 것이 가장 일반적이다.
def __init__(self, learner: Learner):
self.learner = learner # 외부에서 생성 받아옴 => 제어권 역전
def teach(self):
return self.learner.study()
# main, continer의 제어
def main():
# 외부에서 제어 (IoC, DI)
student = Student()
teacher1 = Teacher(student) # DI
college = CollegeStudent()
teacher2 = Teacher(college) # DI
print(teacher1.teach()) # 학생이 공부 중이에요.
print(teacher2.teach()) # 대학생이 공부 중이에요.
- Teacher는 교육 로직에만 집중한다. (SRP 준수)
- SRP(Single Responsibility Principle) : 단일 책임 원칙
- 한 클래스는 오직 하나의 책임(역할)만 가져야한다.
- 외부에서 다른 Student를 Teacher에 주입 가능하다.
2. DIP(Dependency Inversion Principle)
DIP는 IoC 철학을 “무엇에 의존할 것인가”로 구체화한 설계 원칙이다.
'상위 레벨의 모듈은 하위 레벨의 모듈에 의존해서는 안된다. 둘 모두 추상화에 의존해야한다. 추상화는 세부 사항에 의존해서는 안된다.’
구체적인 것이 아니라 추상적인 것에 의존한다. 의존 방향이 상위 → 추상 ← 하위 로 둘다 추상에 의존한다.
2.1 DIP 적용 전후 의존 관계
# DIP 적용 전
Teacher --> Student (고수준 모듈이 저수준 모듈(구체 class)에 바로 의존)
# DIP 적용 후
Teacher --> Learner <-- Student
(추상화) (하위 구체 클래스)
^ 의존 방향 역전
- Teacher : 고수준 모듈 (비즈니스 로직)
- Learner : 추상화 (인터페이스/추상 클래스)
- Student : 저수준 모듈 (구체적 구현) </aside>
from abc import ABC, abstractmethod
# 1. 추상화 정의
class Learner(ABC): # 추상 클래스 (인터페이스)
@abstractmethod
def study(self) -> str:
"""공부하는 행위"""
pass
# 2. 구체 클래스들이 추상화를 구현
class Student(Learner):
def study(self) -> str:
return "학생이 공부 중이에요."
class CollegeStudent(Learner):
def study(self) -> str:
return "대학생이 공부 중이에요."
## 새로운 학습자를 추가하더라도 ... Learner를 상속 받는 구체 클래스만 추가하면 됨
# 3. Teacher는 추상화에 의존 : Learner 인터페이스만 알고, 구체 구현은 몰라도 됨
class Teacher:
def __init__(self, learner: Learner): # 추상 클래스에 의존해야함! (DI)
self.learner = learner
def teach(self) -> str:
return self.learner.study()
# 4. 사용 (DI + DIP)
if __name__ == "__main__":
# 이제 모든 Learner 구현체 사용 가능!
student = Student()
college = CollegeStudent()
teacher1 = Teacher(student)
teacher2 = Teacher(college)
print(teacher1.teach()) # 학생이 공부 중이에요.
print(teacher2.teach()) # 대학생이 공부 중이에요.
2.2 왜 “역전(Inversion)”인가?
- 전통적 의존 방향
- Applicatiion Layer → Business Logic → DB Layer
- DIP 적용 후
- Application Layer → Interface ← DB Impl
3. Dependency Injection
DI는 런타임 시점의 의존 관계 결정을 코드 외부로 위임함으로써, 컴파일 타임 의존성과 런타임 의존성을 분리하는 설계 기법이다.
: 컴파일 단계에서는 소스 코드 자체(타입/인터페이스)만 확인하고, 런타임에 실제로 어떤 구체 객체를 사용할지 동적으로 결정할 수 있다.
핵심은 객체가 자신의 협력 객체를 직접 생성(new)하지 않고, 외부에서 주입 받음으로써 결합도는 낮추고 응집도는 높이는 것이다.
# Case 1 : DI 적용 X
class Teacher:
def teach(self):
student = Student() # 소스코드에 Student가 명시됨 -> 컴파일 타임 의존성
return student.study()
# Case 2 : DI 적용 O
class Teacher:
def __init__(self, student: Student): # 타입 힌트만 있음
self.student = student
student = Student()
student = CollegeStudent() # 다른 객체도 가능
teacher = Teacher(student) # 여기서 의존 관계 결정!
3.1 의존성
- 어떤 코드가 다른 코드에 기대고 있는 것. 즉,변경 전파 가능성을 의미한다.
- A가 B에게 의존한다 → B의 변경이 A에게 전파될 수 있다.
class Teacher: def teach(self): student = Studnet() # 구체 클래스에 직접 의존 return student.study()- Teacher는 Student라는 구체 클래스에 소스코드 레벨에서 결합됨
- Teacher가 협력 객체의 생성과 사용이라는 두 가지 책임을 가진다. (SRP 위반)
- 새로운 학습자 타입을 추가할 때 Teacher 코드는 수정이 필요하다. (OPC 위반)
- OCP(Open-Closed Principle)
- 새로운 기능 추가는 가능하되, 기존 코드 수정은 하지 않아야 한다.
class Student:
def study(self):
return "공부 중이에요."
class Teacher:
def teach(self):
student = Student()
return student.study()
- Teacher 클래스가 Student 클래스의 메소드를 직접 사용하면서 의존하고 있음
→ 나중에 Student 코드가 변경되면 Teacher 코드도 변경되어야함. 강한 의존
3.2 강한 의존을 피하기 위한 외부 의존성 주입
class Student:
def study(self):
return "공부 중이에요."
class Teacher:
def __init__(self, student: Student):
self.student = student # 외부에서 주입받음
def teach(self):
return self.student.study()
student = Student()
teacher = Teacher(student) # 외부에서 만들어서, 객체 의존성 주입
print(teacher.teach()) # "공부 중이에요."
- Teacher는 클래스 내부에서 직접적으로 Student 객체에 의존하지 않고, 외부를 통해 의존한다.
→ Student 내부 구현이 바뀌어도 상관없음 → 유지보수, 테스트, 확장이 쉬워진다.
4. DI 방식 3가지
- 생성자 주입
- 메서드 주입
- 필드 주입
4.1 생성자 주입 → 권장. KRP Core.
- 객체를 만들 때 필요한 것을 받아서, 변동 없이 그것만 사용한다.
- 객체를 __init__으로 받아서 붙여줌 → 의존성이 명확하다. __init__ 보면 바로 알 수 있음
- 한번 주입되면 변경 불가능 → 불변성 보장
- 클래스 전체에서 일관된 의존성 사용 ↔ 변경하려면 새 객체 생성
- 매번 파라미터 전달 안해도됨
class Student:
# 1. 생성자 정의
def __init__(self, name="보배"):
self.name = name
# 2. 출력 형식(문자열) 정의
def __str__(self):
return self.name
def study(self):
return "학생이 공부 중이에요."
class Teacher:
# 주입받을 의존성을 private 필드로 선언
_student: Student
# 외부에서 객체를 파라미터로 받아서 생성자 주입
def __init__(self, student: Student):
self._student = student
def teach(self):
return self._student.study()
# 주입받은 Student 객체를 Teacher 클래스의 모든 메서드에서 자유롭게 활용 가능하다.
def check_homework(self):
return f"{self._student.study()} 숙제를 확인합니다."
def give_feedback(self):
return f"{self._student} 에 대한 피드백"
if __name__ == "__main__":
student = Student()
teacher = Teacher(student) # 외부에서 student를 생성하고 주입
print(teacher.teach()) # 학생이 공부중이에요.
print(
4.2 Method Injection → 일반적으로 사용하지 않는다.
- 생성자가 아닌 일반 메서드의 파라미터로 의존성을 주입 받는다.
- 메서드마다 다른 의존성 주입 가능하다.
- 필요한 메서드만 의존받을 수 있다.
- 매번 파라미터로 전달해야 하는 번거로움과 명확하지 않은 의존성(함수마다 달라지기 때문)
class Student:
def __init__(self, name="보배"):
self.name = name
def __str__(self):
return self.name
def study(self):
return "학생이 공부 중이에요."
class Teacher:
# 생성자에서는 의존성을 받지 않음
def __init__(self):
pass
# 메서드 파라미터로 의존성을 주입 받음
def teach(self, student: Student):
return student.study()
def check_homework(self, student: Student):
return f"{student.study()} 숙제를 확인합니다."
def give_feedback(self, student: Student):
return f"{student}에 대한 피드백"
if __name__ == "__main__":
student = Student()
teacher = Teacher() # 의존성 없이 생성
# 메서드 호출 시마다 의존성을 주입
print(teacher.teach(student)) # 학생이 공부 중이에요.
print(teacher.check_homework(student)) # 학생이 공부 중이에요. 숙제를 확인합니다.
print(teacher.give_feedback(student)) # 보배 에 대한 피드백
4.3 Field Injection → @Autowired
- 객체 생성 후, 필드에 직접 할당한다. teacher.student = student
- 약간 구현부에서 setter 선언하는 것처럼 사용하는 느낌
- 객체 생성과 의존성 주입 분리가 가능하다.
- 불변성 보장 안됨(언제든 변경 가능)
- 테스트 하기 어려움
- @Autowired 이 방식인데 deprecated 추세
class Student:
def __init__(self, name="보배"):
self.name = name
def __str__(self):
return self.name
def study(self):
return "학생이 공부 중이에요."
class Teacher:
def __init__(self):
# 필드만 선언해두고 생성자에서 의존성 받지 않음
self.student = None
def teach(self):
return self.student.study()
def check_homework(self):
return f"{self.student.study()} 숙제를 확인합니다."
def give_feedback(self):
return f"{self.student}에 대한 피드백"
if __name__ == "__main__":
student = Student()
teacher = Teacher() # 의존성 없이 생성
# 필드에 직접 의존성을 주입
teacher.student = student
# 메서드 호출 시 파라미터 불필요
print(teacher.teach()) # 학생이 공부 중이에요.
print(teacher.check_homework()) # 학생이 공부 중이에요. 숙제를 확인합니다.
print(teacher.give_feedback()) # 보배에 대한 피드백