[Android] 앱 개발시 의존성 주입(Dependency Injection) 적용하기

2023. 3. 17. 10:41개발을 파헤치다/Android

반응형

 

의존성 주입이라는 개념을 알기 위해서는 먼저 의존성을 알아야 합니다.
하나의 클래스 안에서 다른 클래스를 초기화하고 필요한 메서드를 호출하는 일은 생각보다 많습니다.
별생각 없이 이렇게 코딩했다면 서비스 규모가 커지고 추후에 새로운 기능 추가나 코드 변경 시 정말이지 난감한 상황을 겪을 수 있는데요. 이 난감한 상황이 바로 클래스 간의 의존성 때문에 발생합니다. 그럼 먼저 의존성이 뭘 의미하는지, 그리고 어떤 문제가 발생하는지 한번 알아봐야겠죠?

아래 간단한 코드를 보면서 설명을 해보겠습니다.

class Car{
    private val engine = Engine()
    
    fun start(){
        engine.start()
    }
}

fun main(args: Array){
    val car = Car()
    car.start()
}

언뜻 보기에는 논리적, 직관적이고 딱히 문제가 없어 보이는 코드입니다. 그럼 대체 뭐가 문제일까요?
여기에서 Car와 Engine은 서로 의존성이 있다고 얘기하는데요. 그 이유를 한번 살펴보겠습니다.

  1. 만약 엔진의 종류가 다양해진다면 어떻게 될까요? Engine 클래스를 상속하는 서브 클래스들을 만들게 되면 Car 클래스에 어떻게 적용이 가능한가요? 그렇습니다. 매번 클래스 내의 코드를 수정해줘야 합니다. 혹은 Car 클래스도 Engine의 서브 클래스에 맞게끔 새롭게 클래스를 만들어야 하죠. 예를 들어 GasCar, ElectricCar처럼 말이죠. 이것은 굉장히 비효율적입니다.
  2. 만약 위의 상황에서 Engine에 부속품이 필요해서 생성자에 파라미터를 추가했다고 가정해 봅시다. 그럼 당연히 Car 내부의 Engine 클래스 초기화 시 수정이 들어가겠죠? 만약 이 Engine 클래스를 Car 클래스 이외에도 수많은 클래스들이 사용한다면?
    그럼 일일이 다 수정해줘야 하는 불상사가 발생합니다.
  3. 이번에는 협업을 하는 관점으로 한번 살펴봅시다. 개발자 1과 개발자 2는 각각 Car와 Engine 클래스를 맡아서 개발하고 있습니다. Car 개발자는 start 메서드를 구현하고 테스트하려면 Engine 클래스가 필요합니다. 근데 아직 개발 중이라는 답변만 돌아옵니다. Engine이 구현이 되지 않았으니 Car의 start 메서드는 구현할 수도, 테스트를 진행할 수도 없습니다. 


이것이 바로 클래스 간 의존성이 있을 때 발생할 수 있는 문제입니다. 위의 예시 코드는 굉장히 간단해서 직관적으로 해결이 가능할지도 모르겠습니다. 하지만 상용 서비스를 개발할 때는 어떨까요? 수많은 코드가 얽혀있고 많은 사람들이 같이 개발하는 상황이라면 이 의존성 문제는 굉장히 심각해집니다. 

 

 


이것을 해결하귀 위한 설계원칙이 바로 의존성 주입입니다. 의존성을 주입한다. 즉, 다른 클래스 내에 사용되는 클래스들을 관리하는 주체가 있고, 필요할 때 이 주체를 통해 필요한 클래스를 받아서 사용한다는 것입니다. 쉽게 말해, 관리자가 Car에 Engine이라는 클래스가 필요하다는 것을 알고 필요할 때 전달해 준다는 얘기입니다. 객체 생성 역시 관리자가 알아서 하기 때문에 Car 입장에서는 그냥 주는 것을 받아서 쓰기만 하면 됩니다. 이로써 의존성 문제가 해결되는 것이죠. 이것을 코드로 한번 살펴보겠습니다.

class Car(private val engine: Engine){
    fun start(){
        engine.start()
    }
}

fun main(args: Array){
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

이렇게 코드를 바꿔보면 어떤가요? Car 클래스 내부에서 Engine을 생성한 것이 아니라 main이라는 주체에서 Engine을 생성하고 Car의 생성자에 매개변수 형태로 전달해 줬습니다. 
Android에서는 두 가지 방식으로 의존성 주입을 할 수 있는데요. 위와 같이 생성자 삽입을 하거나 필드변수에 삽입하는 방법이 있습니다. 아래 코드처럼요.

class Car(){
    late init var engine: Engine
    fun start(){
        engine.start()
    }
}

fun main(args:Array){
    val car = Car()
    car.engine = Engine()
    car.start()
}

필드 변수에 삽입하는 경우는 생성자를 변경할 수 없을 때 주로 사용됩니다. 
위의 두 가지 코드 모두 main에서 Engine이 생성되고 Car로 전달됩니다. 이것을 의존성 주입이라고 하는데요. 이러면 어떤 이점이 있을까요?

  • 유지 보수가 훨씬 편해집니다.

    의존성 주입 방식으로 코드를 구현하게 되면 Engine 클래스를 상속하는 여러 서브클래스들을 만들어도 Car 클래스에 수정을 할 필요가 없습니다. 더불어 Car 이외의 다른 많은 클래스에서 Engine을 사용한다고 하더라도 역시 수정할 필요가 없어집니다.
    유지보수가 훨씬 편해지는 것이죠.
  • 테스트가 편리해집니다.

    위의 예시에서 얘기했던 협업 과정에서의 갈등이 사라집니다. Car 클래스를 개발하는 개발자 1 입장에서는 생성자나 필드변수를 통해 전달되는 Engine을 그저 사용하기만 하면 됩니다. Engine을 개발하는 개발자 2 역시 자기 업무에 몰두하면 되고요.
    의존성 주입을 해주는 main 클래스에서 Engine이 완성됐을 때의 모습을 대략적으로 꾸며낸 Mock-up 클래스를 넣어준다던지, Engine의 모양을 가지고 있지만 메서드 호출 시 정해진 값을 리턴하는 방식의 테스트 클래스를 넣어줄 수도 있습니다.
    이렇게 하면 개발자 1과 개발자 2 모두 동시에 일 진행이 가능하고 테스트도 간단해집니다.


의존성 주입은 더 많은 개발자가 함께 일할수록, 서비스의 규모가 클수록 굉장히 중요하고 체계적으로 꼭 적용이 되어야 하는 부분입니다. 더 나은 사용자 경험을 제공하고 빠르게 발전하는 서비스를 개발하기 위해 이런 설계 원칙을 알고 적용한다면 큰 도움이 되겠죠.

반응형