Tech/FastAPI

FastAPI Depends 클래스 - Dependency Injection 의존성 주입

@~@ 2024. 1. 12. 19:39

<점프 투 FastAPI>을 읽고 공부한 내용을 정리한 글입니다.

 

의존성이란?

의존성이란 두 클래스나 모듈의 연결, 관계를 의미한다. 만약 A와 B라는 두 모듈이 있을 때 A가 B에 의존한다면, B의 일부가 변경될 때 이것이 A에 영향을 끼친다는 의미이다. 자바 코드로는 아래와 같이 작성할 수 있다.

// 예제 1
public class A{
    private B b;
    public A(B b){
    	this.b = b;
    }
}

// 예제 2
public class A{
    private B b;
    public A(){
    	this.b = new B();
    }
}

클래스 B의 객체가 클래스 A의 멤버 변수로, A는 B에 의존한다.

 

// 예제 3
public class A{
	public void doSomething(){
    	B b = new B();
        b.performanceAction();
    }
}

public class B{
    public void performanceAction() {}
}

 

클래스 B의 performanceAction 메소드 안의 내용이 수정되면 클래스 A는 클래스 B에서 수정된 내용의 영향을 받는다. 이처럼 의존성을 띄는 것은 코드의 변경이나 확장에 영향을 미친다.

 

 

의존성 역전 원칙 Dependency Inversion Principle (DIP) 란?

의존성 역전 원칙이란 고수준 모듈이 저수준 모듈에 의존하면 안 되고 추상화에 의존해야하며, 추상화가 세부 사항에 의존하는 것이 아니라, 세부 사항이 추상화에 의존해야 하는 것이다. 말이 너무 어렵다..

먼저 고수준 모듈과 저수준 모듈부터 정리하자.

  • 고수준 모듈: 호출자, 상위 수준의 정책
  • 저수준 모듈: 수신자, 하위 수준의 정책, 고수준 모듈이 기능을 수행할 수 있도록 도와주는 작은 단위, 구체적

한 마디로, 의존성 역전 원칙은 나보다 변하기 쉬운 것에 의존하면 안 된다. 라는 의미이다. 여기에서 '나'는 고수준 모듈, 변하기 쉬운 것은 저수준 모듈이다.

 

예를 들어, 고수준 모듈이 요리사라면 저수준 모듈은 햄버거 레시피, 피자 레시피 등이 있을 것이다. 고수준 모듈이 Child라면 저수준 모듈은 갖고 노는 장난감들(퍼즐, 블록 등)이다. 고수준 모듈이 Order라면 저수준 모듈은 카드 결제, 현금 결제, 포인트 결제 등이 있을 것이다. 예제 코드를 보면서 더 자세히 살펴보자.

// 의존 관계 Order -> cardPayment
public class Order{
    cardPayment card = new cardPayment();	// 카드로 결제하는 방법
    
    card.processPayment();		// 카드 결제 진행
}

public class cardPayment{
    public void processPayment() {...}
}

 

먼저, 주문을 하고 결제하는 클래스로 Order 클래스를 만들었다. 현재 이 가게는 카드 결제만 가능하다. processPayment 메소드를 호출하여 결제를 진행한다.

 

계속 카드 결제만 진행하다가 현금 결제로 결제 방법을 변경하게 되었다. 그래서 cardPayment 객체 생성을 주석 처리하고 cashPayment 객체를 생성했다.

// 의존 관계 Order -> cashPayment
public class Order{
//    cardPayment card = new cardPayment();	// 카드로 결제하는 방법
    cashPayment cash = new cashPayment();	// 현금으로 결제하는 방법
    
//    card.processPayment();		// 카드 결제 진행
    cash.processPayment();		// 현금 결제 진행
}

public class cardPayment{
    public void processPayment() {...}
}

public class cashPayment{
    public void processPayment() {...}
}

결제 방법이 바뀔 때마다 Order 클래스를 수정해야 한다. 이것이 Order 클래스가 Payment 클래스에 의존하는 것이다. 이는 위에서 언급한 '나보다 변하기 쉬운 것에 의존하면 안 된다'라는 내용에 위배된다. 

 

이렇듯 고수준(호출자)이 저수준(수신) 모듈에 의존하면 구조 변경이나 코드 수정이 어렵고, 효율성과 유연성이 굉장히 낮다. 따라서 이러한 현상을 완화하기 위해 구체적인 구현체가 아닌 추상화에 의존함으로써 의존 관계를 역전하고 의존도를 낮추는 의존성 역전 원칙이 필요하다.

 

의존성 역전 원칙을 적용한 코드는 다음과 같다.

먼저 Payment라는 추상화 클래스를 만든다.

public interface Payment{
    public void processPayment();
}

 

그리고 이 인터페이스를 이용하여 카드 결제, 현금 결제 방법을 구현한 구현체를 만든다.

public interface Payment(){
    public void processPayment();
}

// 구체적인 결제 방법: 카드 결제
public class CardPayment implements Payment {
    @Override
    public void processPayment() {
        // 카드 결제 처리 로직
    }
}

// 구체적인 결제 방법: 현금 결제
public class CashPayment implements Payment {
    @Override
    public void processPayment() {
        // 현금 결제 처리 로직
    }
}

 

그리고 Order 클래스가 Payment 인터페이스에 의존하도록 바꿔준다.

// 의존 관계 Order -> Payment(인터페이스) <- card, cash
public class Order {
    private Payment payment;

    // 의존성 주입을 통한 초기화
    public Order(Payment payment) {
        this.payment = payment;
    }

    // 결제 처리
    payment.processPayment(); // 결제에 대한 구체적인 구현에 의존하지 않음
}

이렇게 변경함으로써 Order 클래스는 더 이상 특정 결제 방법에 강하게 결합지 않게 된다. 

 

호출하여 사용할 때는 아래와 같이 쓰면 된다.

Payment cardPayment = new CardPayment();
Order cardOrder = new Order(cardPayment);
cardOrder.processOrder();  // CardPayment의 processPayment() 호출

Payment cashPayment = new CashPayment();
Order cashOrder = new Order(cashPayment);
cashOrder.processOrder();  // CashPayment의 processPayment() 호출

 

 

 

의존성 역전 원칙의 핵심은 구현이 아닌 추상화에 의존하는 것이다. 고수준 모듈은 저수준 모듈에 의존하지 않아야 하며, 추상화에 의존해야 한다. 전통적인 의존성 방식의 코드는 고수준 모듈이 저수준 모듈에 의존하는 코드이다. 하지만 구현체에 의존할 경우, 유연성이 낮아지고 변경에 취약해진다. 따라서 결합도를 낮추는 의존성 역전 원칙이 필요하다. 고수준 모듈은 구현하는 것에 신경 쓸 필요가 없어야 한다. 호출만으로도 모든 일이 이루어져야 하며, 기능 구현에 대한 상세한 내용은 몰라도 된다.

 

 

의존성 주입 Dependency Injection (DI) 란?

DIP를 적용할 때 사용한 방법이 의존성 주입다. 의존성 주입은 말 그대로 의존성을 주입하는 것이다. 여기에서 주입이란

  • 주입: 외부에서 객체를 생성하여 내부로 넣어주는 것, 외부에서 의존성을 넣어주는 것.

을 의미한다. 위의 코드에서는 

public Order(Payment payment) {
        this.payment = payment;
    }

이렇게 매개변수로 payment 객체를 Order 클래스에 넣어주는 것이 DI이다.

 

 

Depends 클래스

FastAPI에서는 의존성을 주입할 때 Depends 클래스를 사용한다.

 

먼저, 의존성을 주입하지 않은 예제 코드를 살펴보자. 아래는 질문 글 리스트를 가져오는 함수이다.

@router.get("/list")
def question_list():
    db = SessionLocal()
    _question_list = db.query(Question).order_by(Question.create_date.desc()).all()
    db.close()
    return _question_list

 

 

의존성을 주입하지 않았기 때문에 데이터베이스 세션을 직접 생성하여 사용한다. 만약 데이터베이스를 다른 종류로 변경해야 하거나 SQLAlchemy가 아닌 다른 ORM을 사용해야 할 경우, 코드의 많은 부분을 수정해야 할 수 있다. 또한 세션 관리, 예외 처리, 리소스 해제 등을 수동으로 처리해야 하므로 실수가 발생할 가능성이 높아진다. 특히 db.close()를 하지 않으면 사용한 세션이 컨넥션 풀에 반환되지 않는다. 

 

하지만 우리가 만들 대부분의 API는 데이터베이스를 사용하기 때문에 세션 객체를 생성하고 데이터 처리 작업을 진행하고 이를 close하여 컨넥션 풀에 반환하는 작업이 계속 반복될 것이다. 이 때 의존성 주입을 사용하면 일련의 과정을 자동화할 수 있다.

 

먼저 get_db라는 함수를 만든다.

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

 

그리고 질문 글 리스트를 가져오는 함수에 매개변수 get_db 의존성을 주입 받으면

@router.get("/list", response_model=list[question_schema.Question])
def question_list(db: Session = Depends(get_db)):
    _question_list = question_crud.get_question_list(db)
    return _question_list

Depends를 통해 생성된 세션 객체가 주입된다.

'Tech > FastAPI' 카테고리의 다른 글

alembic 에러  (0) 2024.01.16
FastAPI - app.get과 router.get 차이 (FastAPI와 APIRouter)  (0) 2024.01.09
FastAPI 시작하기 - FastAPI 설치 및 실행  (1) 2024.01.09