[iOS] 의존성 주입(DI)

황석범·2025년 2월 10일
0

내일배움캠프_iOS_5기

목록 보기
74/76

의존성 주입(DI)

의존성 주입(DI)는 객체 간의 의존 관계를 외부에서 주입하는 설계 패턴이다.
객체 간의 결합도를 낮추고 테스트와 유지보수성을 향상시킬 수 있다.

iOS 개발에서 객체지향의 5원칙인 SOLID 원칙(특히 OCP, DIP)을 적용할 때 유용

OCP (Open-Closed Principle, 개방-폐쇄 원칙)

  • 확장에는 열려(Open) 있고, 변경에는 닫혀(Closed) 있어야 한다.
  • 즉, 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야 한다.

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

  • 고수준 모듈(예: AlarmManager)이 저수준 모듈(예: SoundPlayer)에 직접 의존하면 안 되고, 둘 다 추상(인터페이스, 프로토콜)에 의존해야 한다.
  • 즉, 구체적인 구현이 아니라 추상화된 인터페이스(프로토콜)에 의존해야 한다.

1. 의존성

어떤 객체가 다른 객체를 사용할 때 그 객체에 대한 의존성이 발생
예) AlarmManager가 SoundPlayer를 직접 생성하고 사용한다면 AlarmManager는 SoundPlayer에 강하게 의존

//의존성을 직접 생성하는 방식 (안 좋은 예)
class AlarmManager {
    private let soundPlayer = SoundPlayer()

    func triggerAlarm() {
        soundPlayer.playSound()
    }
}

위 방식에서는 AlarmManager가 SoundPlayer를 직접 생성(SoundPlayer() 호출)하고 있기 때문에

  1. SoundPlayer의 변경이 AlarmManager에도 영향을 줌 (결합도가 높아짐)
  2. SoundPlayer를 다른 클래스로 교체하기 어려움
  3. 유닛 테스트에서 SoundPlayer를 쉽게 Mocking 할 수 없음
//의존성 주입 방식 (좋은 예)
class AlarmManager {
    private let soundPlayer: SoundPlayer

    init(soundPlayer: SoundPlayer) {
        self.soundPlayer = soundPlayer
    }

    func triggerAlarm() {
        soundPlayer.playSound()
    }
}

// 외부에서 주입
let player = SoundPlayer()
let alarmManager = AlarmManager(soundPlayer: player)

AlarmManager가 SoundPlayer를 직접 생성하지 않고, 생성자로 주입받음
이를 통해 SoundPlayer를 다른 객체로 교체하기 쉬워지고, 테스트도 용이해짐


2. 의존성 주입을 프로토콜로 하기

위의 방식은 AlarmManager가 SoundPlayer의 구체적인 구현에 의존하고 있습니다.
하지만 의존성을 프로토콜을 통해 추상화하면 더 유연한 구조를 만들 수 있음

1. 프로토콜을 사용한 의존성 주입

protocol SoundPlaying {
    func playSound()
}

class SoundPlayer: SoundPlaying {
    func playSound() {
        print("🔊 알람 소리 재생")
    }
}

class AlarmManager {
    private let soundPlayer: SoundPlaying

    init(soundPlayer: SoundPlaying) {
        self.soundPlayer = soundPlayer
    }

    func triggerAlarm() {
        soundPlayer.playSound()
    }
}

SoundPlaying 프로토콜을 정의하고, SoundPlayer가 이를 준수하도록 구현
AlarmManager는 SoundPlaying 프로토콜만 알면 되므로, SoundPlayer의 구체적인 구현을 몰라도 됨

2. 다른 구현체와의 교체

SoundPlaying을 준수하는 새로운 클래스를 만들어서 쉽게 교체할 수 있음

class SilentSoundPlayer: SoundPlaying {
    func playSound() {
        print("🔇 소리를 내지 않음 (무음 모드)")
    }
}

// 다른 사운드 플레이어를 사용하여 AlarmManager 생성 가능
let silentPlayer = SilentSoundPlayer()
let alarmManager = AlarmManager(soundPlayer: silentPlayer)

alarmManager.triggerAlarm() // 🔇 소리를 내지 않음 (무음 모드)

이제 SoundPlaying을 따르는 어떤 구현체든 주입할 수 있음
즉, 코드를 수정하지 않고도 다양한 방식의 사운드 플레이어를 사용할 수 있음

3. 테스트 용도로 Mock 객체 사용

유닛 테스트에서는 Mock 객체를 만들어 주입할 수도 있음

class MockSoundPlayer: SoundPlaying {
    var isPlayed = false

    func playSound() {
        isPlayed = true
    }
}

// 테스트 코드
let mockPlayer = MockSoundPlayer()
let alarmManager = AlarmManager(soundPlayer: mockPlayer)

alarmManager.triggerAlarm()

assert(mockPlayer.isPlayed == true, "playSound가 호출되지 않음!")

MockSoundPlayer를 이용하면 실제 소리를 재생하지 않고, 호출 여부만 확인 가능
이를 통해 유닛 테스트가 쉽고 빠르게 실행될 수 있음


정리

의존성 주입(DI)

  • 객체가 다른 객체를 직접 생성하지 않고, 외부에서 주입받는 방식.
  • 결합도를 낮추고, 유지보수성과 테스트 용이성을 높임

의존성을 프로토콜로 하는 DI

  • 특정 구현체가 아닌 프로토콜을 주입함으로써 더 유연한 코드 작성 가능.
  • 다양한 구현체로 쉽게 교체 가능 (SoundPlayer ↔ SilentSoundPlayer)
  • 테스트에서 Mock 객체를 사용하여 유닛 테스트 용이

profile
iOS 공부중...

0개의 댓글

관련 채용 정보