개요
이전 글에서 IoC, DI, DIP를 알아보았다. 이제 여기서 확장해서 DI Container와 생명 주기 관리, Web Framework의 원리를 알아보자.
1. Framework
프레임워크 본질 중 하나가 IoC다. 일반적으로는 개발자가 직접 코드 흐름을 제어하지만, 프레임워크는 이 흐름을 뒤집는다. (제어의 역전)
프레임워크가 애플리케이션 코드를 호출 및 흐름을 제어하고, 개발자는 의존 관계만 정의하는 방식이다.
framework라고 하면 DI 패턴을 통해서 여러 자원을 관리할 수 있는데, 이게 바로 IoC를 실현하는 구체적인 방법이다.
- FastAPI: 함수 인자에 Depends()를 명시한다. FastAPI가 호출 시점에 자동으로 의존성을 주입한다.
- Spring: @Component, @Service, @Repository Spring IoC 컨테이너가 의존를 생성하고 주입한다.
2. DI Container로 의존성 관리하기
2.1 DI Container가 없는 상황 (문제 코드)
# DI Container가 없는 상황
from abc import ABC, abstractmethod
class Teacher:
def __init__(self, name: str):
self.name = name
def teach(self):
return f"{self.name} 선생님이 수업합니다."
class Student:
def __init__(self, name: str):
self.name = name
def study(self):
return f"{self.name} 학생이 공부 중이에요."
class ClassRoom:
def __init__(self, teacher: Teacher, student: Student):
self.teacher = teacher
self.student = student
def conduct_session(self):
print("== 수업 시작 ==")
print(self.teacher.teach())
print(self.student.study())
print("== 수업 끝 ==")
if __name__ == "__main__":
# file: classroom.py
teacher1 = Teacher("김선생")
student1 = Student("cobinding")
classroom1 = ClassRoom(teacher1, student1)
classroom1.conduct_session()
# file: exam.py
teacher2 = Teacher("이선생")
student2 = Student("cobinding") # 다른 객체를 또 생성하게 됨
classroom2 = ClassRoom(teacher2, student2)
classroom2.conduct_session()
2.2 실제 웹 프레임 워크 상황에 빗대면...
# 실제 웹 프레임 워크 상황이 되면...
@app.get(f"/class/{student_id}")
def start_class(student_id: int):
# 각 api 경로에서 아래와 같은 객체들을 매번 생성해야 함.
db = Database("localhost:5432")
cache = Redis("localhost:6379")
logger = Logger("app.log")
teacher = Teacher("김선생")
student = db.get_student(student_id)
classroom = ClassRoom(teacher, student)
result = classroom.conduct_session()
db.close()
return result
- 모든 API에서 db, redis, logger 등 필요한 코드를 반복하게 되어, 어떤 비즈니스 로직이 담겨있는지 보다 무엇을 만들어야 하는지에 대한 코드가 대부분이 된다.
- 객체의 생성/소멸 라이프사이클을 개발자가 직접 제어하게 된다.
- 테스트 시에도 진짜 DB/Redis 인스턴스가 계속 만들어진다.
- ClassRoom은 구체 클래스(Teacher, Student)에 직접 의존하게 된다. (DIP 실패)
- 의존성이 늘수록 생성 코드가 폭발..
웹 프레임워크가 없다면 이러한 어려움을 겪게 된다.
위 문제를 한번 DI Container를 생성하고 웹 프레임워크의 핵심 메커니즘을 이해해보자!
3. DIContainer로 여러 의존성 관리
3.1 DIContainer 구현
의존성 주입 컨테이너를 만들어서 위 문제를 해결해보자.
앞선 코드에는 다음과 같은 한계가 있다.
- Student("cobinding")을 여러 번 생성하는 문제
- 여러 파일/모듈 간에 동일 객체를 공유하기 어려운 구조
- 전역 변수를 사용하지 않고는 의존성을 중앙에서 관리하기 어려움
이번에는 DI(Dependency Injection) Container를 직접 구현해보며, FastAPI의 Depends()와 같은 프레임워크가 내부적으로 어떻게 의존성을 주입하는지 이해해보자. 이를 통해 웹 프레임워크가 의존성을 생성/관리/주입하는 원리를 간단한 예제로 살펴본다.
"""
DI Container: 의존성 주입 컨테이너
main.py의 문제 해결:
1. Student("cobinding")을 여러 번 생성하는 문제
2. 여러 파일/모듈에서 같은 객체 공유 어려움
3. 전역 변수 없이 중앙에서 관리
"""
class CoreDIContainer:
""" 의존성 주입 컨테이너 """
def __init__(self):
self._services = {} # 싱글톤 객체 저장소
def register(self, name: str, instance):
self._services[name] = instance
print(f" `{name} 등록됨. ID: {id(instance)}")
def get(self, name: str):
""" 의존성 가져오기 """
if name not in self._services:
raise ValueError(f"{name}이 등록되어 있지 않습니다.")
return self._services[name]
def has(self, name: str) -> bool:
""" 의존성 존재 여부 확인 """
return name in self._services
def clear(self):
""" 모든 의존성 제거 """
self._services.clear()
print("모든 의존성 제거 완료")
3.2 DIContainer 사용 예제
이를 실제로 사용하는 구현은 다음과 같다. 이전 코드에서 발생했던 문제를 DI Container를 통해 어떻게 해결할 수 있는지가 담겨있다.
if __name__ == "__main__":
from main import Teacher, Student, ClassRoom
print(" == DI Container 사용 예시 == ")
# 1. 컨테이너 생성
container = CoreDIContainer()
# 2. 의존성 등록 (한번만)
container.register("teacher_kim", Teacher("김선생"))
container.register("teacher_lee", Teacher("이선생"))
container.register("student_co", Student("cobinding"))
# 3. file: classroom.py
print("[classroom.py] 첫 번째 수업")
teacher = container.get("teacher_kim")
student = container.get("student_co")
print(f"student ID: {id(student)}")
classroom1 = ClassRoom(teacher, student)
classroom1.conduct_session()
# 4. file: exam.py => 같은 객체 재사용
print("[exam.py] 두 번째 시험")
teacher = container.get("teacher_lee")
student = container.get("student_co")
print(f"student ID: {id(student)}")
classroom2 = ClassRoom(teacher, student)
classroom2.conduct_session()
print()
1. 컨테이너 생성
CoreDIContainer()를 한 번만 생성한다. 이 컨테이너는 모든 의존성을 중앙에서 관리하는 저장소 역할을 한다.
2. 의존성 등록
Teacher, Student 같은 객체를 register() 메서드로 한 번만 등록한다.
이 단계에서 객체는 “싱글톤처럼” 관리되며, 이후 어디서든 동일한 인스턴스를 꺼내 쓸 수 있다.
3. classroom.py 실행
container.get("teacher_kim")과 container.get("student_co")를 호출해 필요한 의존성을 주입받는다.
직접 Teacher()나 Student()를 생성하지 않아도 되고, 객체의 생성 위치가 코드 전역에 흩어지지 않는다.
4. exam.py 실행
같은 방식으로 container.get()을 호출하면 동일한 객체 인스턴스가 재사용된다.
출력된 student ID가 동일한 값으로 찍히는 것을 보면,
“Student("cobinding")이 여러 번 생성되는 문제”가 해결된 것을 확인할 수 있다.
알게된 점
그동안은 웹 프레임워크를 사용할 때 내부 동작보다는 제공되는 편의성에만 의존해왔다. 하지만 이번에 DI Container와 IoC 원리를 직접 구현해보며, 프레임워크가 어떻게 의존성을 관리하고 주입하는지를 구체적으로 이해할 수 있었다. 회사에서 자주 보는 프로젝트에서도 KRPCore라는 중앙 의존성 컨테이너가 있는데, 이제는 왜 그렇게 설계했는지, 어떤 장점을 가지는지 명확히 이해할 수 있게 되었다! FastAPI Depends()의 원리나 내부 구조, 프레임워크의 장단점 같은 걸 더 공부해봐야겠다고 생각들었다.
'Dev > Backend' 카테고리의 다른 글
| [BackEnd] 비동기 테스크 큐와 DI Container (feat. Celery) (0) | 2025.11.16 |
|---|---|
| [BackEnd] 싱글톤 패턴 & DI Container 직접 만들어서 사용하기 (0) | 2025.11.09 |
| [BackEnd] IoC & DIP & DI (0) | 2025.10.20 |
| [BackEnd] 커뮤니티 게시물 목록 조회 API 쿼리를 QueryDsl로 구현해보기 (0) | 2024.10.23 |
| [BackEnd] API의 멱등성을 고려하여 개발하기 (0) | 2024.04.26 |