시작하기 전..
- 구글링 + 여러 포스팅을 통해 작성된 내용을 저만의 방식으로 해석한 것이라 틀린 부분이 있을 수도 있습니다.
- 언제든 댓글로 의견, 조언 환영입니다~!
지난 시간에..
의존성 주입의 정의를 이야기하며
프로그램 디자인이 결합도를 느슨하게 되도록하고 의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것
이를 4가지로 분리해서 보는 것으로 글을 마무리 했었다.
정리하자면
- 결합도를 느슨하게 되도록
- 의존관계 역전 원칙
- 단일 책임 원칙
- 클라이언트의 생성에 대한 의존성을
- 클라이언트의 행위로부터 분리하는 것.
이것이 의존성 주입의 메인 키워드였다.
Intro
오늘 알아볼 내용은 아래와 같다.
- 의존관계 역전 원칙?
- 단일 책임 원칙?
- 의존성 주입에 대한 방법?
위 3가지에 대해 하나씩 알아보자.
1. 의존관계 역전 원칙 - DIP (Dependency Inversion Principle)
- 객체지향 설계에서 중요시되는 SOLID 원칙에서 D를 맡고 있는 녀석이다.
- 이 원칙의 의미하는 바는 뭘까?
상위계층(정책결정)이 하위계층(세부사항)에 의존하는 전통적인 의존관계를 반전시킴으로써 상위계층이 하위계층의 구현으로부터 독립되게 할 수 있는 구조
- 를 의미한다고 한다...

차근차근! 하나하나씩 봐보자.
- 위의 글을 분리해보자.
- “상위계층이 하위계층에 의존하는 전통적인 의존 관계” 반전
- “상위계층이 하위계층의 구현으로부터 독립” 되는
- 구조이다.
1번은 어디선가 많이 본 말이다. 이전 글 참고
상위계층이 하위계층에 의존하는 관계를 방지하려면?!
→ 추상화를 적용하면 된다.
추상화에 의존하게 되면 자연스레 2번도 해당되도록 할 수 있다.
Swift에서는 추상화를 하기 위한 프로토콜을 생성 후 채택하게 끔 하는 방법으로 이를 적용해볼 수 있을 거 같다.
다시 한 번 정리를 하면 DIP를 만족하게 하기 위해선
- 상위모듈은 하위모듈에 의존해서는 안된다.
- 상위모듈과 하위모듈 모두 추상화에 의존해야한다.
- 추상화는 세부사항에 의존해서는 안된다.
- 세부사항이 추상화에 의존해야한다.
예시를 한번 보자.
protocol Playable: AnyObject {
var goals: Int { get set }
}
class CityPlayer: Playable {
var memberCount = 11
}
class ManCity {
var players: Playable
init(player: Playable) {
self.players = player
}
}
let manCity = ManCity(player: CityPlayer())
- 해당 구조는 아래와 같은 구조를 가진다.
- 상위모듈: ManCity
- 하위모듈: CityPlayer
- 상위, 하위 모듈 모두 Playable(추상화 프로토콜)에 의존한다.
- 상위-하위 모듈 간 의존 관계를 독립시킨 상태이다.
- ManCity → Playable ← ManCity 와 같은 구조를 가진다.
그럼 나쁜 예시는 뭔가?
아래의 예시를 보자.
class ManCity {
func play() -> String {
return "Phil Foden scores a goal!"
}
}
class Foden {
let team: ManCity = ManCity()
func perform() {
let result = team.play()
print(result)
}
}
- 이는 아래와 같은 특징을 가진 구조이다.
- 상위 모듈: Foden
- 하위 모듈: ManCity
- 상위 모듈인 Foden이 ManCity에 의존하고 있는 관계로 만약 ManCity가 수정, 변화가 생긴다면 Foden 상위 모듈을 수정해야 한다.
- 즉, DIP의 원칙을 어긴 프로그램의 설계이다.
이를 어떻게 수정할 수 있을까?
protocol Playable {
func play() -> String
}
class ManCity: Playable { // 하위 모듈
func play() -> String {
return "Phil Foden scores a goal!"
}
}
class Foden { // 상위 모듈
let team: Playable
init(team: Playable) {
self.team = team
}
func perform() {
let result = team.play()
print(result)
}
}
let manCity = ManCity()
let philFoden = Foden(team: manCity)
philFoden.perform()
뭐가 다른 거지?
- Foden은 기존 ManCity에 의존하지 않고 추상화 시킨 객체인 Playable에 의존하게 된다.
- 따라서 Playable 프로토콜 구현부는 외부에서 변화에 따라 지정해주면 된다.
- Foden은 구현부에 상관없이 변화에 민감하지 않게 되는
“DIP 원칙을 지켰다”
라고 할 수 있다.
뭔가 폭풍우 처럼 지나갔다…
기억하기 힘들다~! 하면
추상화를 통한 상위-하위 모듈의 분리
로 암기하면 될 거 같다.
(지극히 개인적인 의견)
이어서 단일 책임 원칙도 봐보도록 하자.
2. 단일 책임 원칙 (Single Responsibility Principle)
- 이 녀석은 SOLID 원칙 중 S를 담당하는 녀석이다.
해당 원칙의 정의를 보자.
모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 함을 일컫는다.
(내가 이전에 작성하던)
맨시티 구단을 관리하기 위한 ManCityService라는 클래스가 있다고 하자.
class ManCityService {
func playMatch() {
let matchData = requestMatch()
let performance = decodePerformance(data: matchData)
savePerformanceToHistory(performance: performance)
}
private func requestMatch() -> Data {
// Call for match data
return Data()
}
private func decodePerformance(data: Data) -> Performance {
// Decoding match performance from data
return Performance(goals: 2, assists: 1)
}
private func savePerformanceToHistory(performance: Performance) {
// Save performance data to history
}
}
- ManCityServie 클래스는…
- playMatch
- requestMatch
- decodePerformance
- savePerformanceToHistory…
너무 많은 역할을 가지고 있다....
클래스는 하나의 책임만 가지도록 해보자.
protocol MatchRequester {
func requestMatch() -> Data
}
protocol PerformanceDecodable {
func decodePerformance(data: Data) -> Performance
}
protocol PerformanceSavable {
func savePerformanceToHistory(performance: Performance)
}
class MatchDataRequester: MatchRequester {
func requestMatch() -> Data {
// Call for match data
return Data()
}
}
class PerformanceDecoder: PerformanceDecodable {
func decodePerformance(data: Data) -> Performance {
// Decoding match performance from data
return Performance(goals: 2, assists: 1)
}
}
class PerformanceSaver: PerformanceSavable {
func savePerformanceToHistory(performance: Performance) {
// Save performance data to history
}
}
class ManCityService {
let dataRequester: MatchRequester
let decoder: PerformanceDecodable
let saver: PerformanceSavable
init(dataRequester: MatchRequester, decoder: PerformanceDecodable, saver: PerformanceSavable) {
self.dataRequester = dataRequester
self.decoder = decoder
self.saver = saver
}
func playMatch() {
let matchData = dataRequester.requestMatch()
let performance = decoder.decodePerformance(data: matchData)
saver.savePerformanceToHistory(performance: performance)
}
}
// 의존성 주입
let manCityService = ManCityService(dataRequester: MatchDataRequester(), decoder: PerformanceDecoder(), saver: PerformanceSaver())
manCityService.playMatch()
- 뭔가 프로토콜, 클래스가 많아졌는데…
- 왜 이렇게..?
뭐가 다른데?
- 각 역할을 분리된 클래스로 분리하여 단일 책임을 만족하도록 만들었다.
- MatchDataRequester: 경기 데이터를 요청하는 책임.
- PerformanceDecoder: 경기 데이터를 해석하는 책임.
- PerformanceSaver: 데이터를 저장하는 책임.
- 이전과 비교하면
- ManCityService는 각각의 모듈들을 활용
- playMatch에 대한 책임만을 가지고 있다.
이와 같이
- 클래스는 하나의 책임만을
- 그 책임을 완전히 캡슐화해야 SRP를 만족하는 코드라고 할 수 있다.
해당 원칙을 만족하도록 하는 수정하는 과정에서 그냥 class로만 분리해야지~! 한다면 (protocol 사용 X)
- DIP를 만족하지 않을 수 있다.
즉, SOLID 원칙은 연결되어 있다는 것을 글을 작성하면서 인지하게 되었다.
그래서 드디어 본론으로 돌아갈 수 있게 되었다.
의존성 주입(Dependency Injection) 이란?
- 줄여서 DI라고 많이 불린다. 정의는 아래와 같다.
의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것
- DIP, SRP를 따르면서 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것.
- 이를 조금만 수정해보면
- 객체 간의 의존성을 외부에서 주입하는 방식으로, 객체가 직접 의존하는 객체를 생성하거나 초기화하지 않고 외부에서 주입받는 방식이라고 할 수 있다.
우선은… “아니 그래서 이걸 왜 사용하는데?”
- 객체 간의 의존성을 줄여서 코드의 재활용성 / 확장성이 높아진다.
- 객체 간의 결합도가 낮아져 유연한 코드/유연한 프로그램을 작성 가능
- 유지 보수가 쉬워진다.
- Unit Test가 가능해진다.
- 특정 객체에 대한 의존성을 없애고 Test 객체를 주입할 수 있기 때문!
그래서… 어떤 방법들이 있어요?
- 의존성 주입 방법으로는 3가지가 있다.
- Constructor Injection
- Setter Injection
- interface Injection
각각의 예시를 보자.
// 1. Constructor Injection
class Player {
let team: Team
init(team: Team) {
self.team = team
}
}
let team = Team(name: "Man City")
let player = Player(team: team)
// 2. Setter Injection
class Player {
var team: Team?
func setTeam(_ team: Team) {
self.team = team
}
}
let player = Player()
player.setTeam(Team(name: "Man City"))
// 3. Interface Injection
protocol TeamInjectable {
func injectTeam(team: Team)
}
class Player: TeamInjectable {
var team: Team?
func injectTeam(team: Team) {
self.team = team
}
}
let player = Player()
player.injectTeam(team: Team(name: "Man City"))
위 3가지 중 Constructor, Setter Injection이 쓰이는 편이고..
사실 생성자 주입(Constructor Injection)이 가장 많이 사용된다고 한다.
이유는 아래와 같다.
- 안정성: 객체가 생성될 때 의존성을 확실하게 주입받기 때문에, 의존성이 누락될 가능성이 적다.
- 불변성: 생성자로 주입된 의존성은 주로 변경되지 않는 경우가 많아, 클래스의 불변성을 유지할 수 있다.
- 테스트 용이성: 테스트 시, 의존성을 생성자에서 쉽게 주입할 수 있어 모의 객체(Mock)를 사용하는 단위 테스트에 적합하다.
그래서 뭐가 달라지는 지 확인해보고 싶다!
예시를 통해 비교해보는 것이 가장 좋을 것 같다.
우선 의존성 주입이 없는 코드이다.
class ManCity {
func play() -> String {
return "Phil Foden scores for Manchester City!"
}
}
class Foden {
let team = ManCity() // 직접 인스턴스 생성 (의존성 주입 없음)
func playMatch() {
let result = team.play()
print(result)
}
}
let philFoden = Foden()
philFoden.playMatch()
다음은 위의 코드를 의존성 주입(Constructor Injection 사용)이 있는 코드로 변경한 결과이다.
protocol Team {
func play() -> String
}
class ManCity: Team {
func play() -> String {
return "Phil Foden scores for Manchester City!"
}
}
class Foden {
let team: Team
init(team: Team) { // 의존성 주입
self.team = team
}
func playMatch() {
let result = team.play()
print(result)
}
}
let manCity = ManCity()
let philFoden = Foden(team: manCity) // 의존성 주입
philFoden.playMatch()
뭐가 달라졌는데?!
- Foden 클래스가 ManCity와 같은 구체 클래스에 의존하지 않고, Team 프로토콜에 의존함으로써 다른 팀 객체도 쉽게 주입할 수 있다.
- 모의 객체(Mock)를 주입하여 테스트할 수 있기 때문에 단위 테스트가 쉬워진다.
오… 첫번째는 알법한데 2번째는 체감이 안된다.
테스트 코드에 대한 차이도 살펴보자.
// 의존성 주입이 없는 경우
import XCTest
class FodenTests: XCTestCase {
func testPlayMatchWithoutDependencyInjection() {
let philFoden = Foden()
let result = philFoden.playMatch()
// ManCity 객체에 의존하여 테스트
XCTAssertEqual(result, "Phil Foden scores for Manchester City!")
}
}
// 의존성 주입이 있는 경우
import XCTest
// Mock 클래스
class MockTeam: Team {
func play() -> String {
return "Mock Foden scores for Mock Team!"
}
}
class FodenTests: XCTestCase {
func testPlayMatchWithMockTeam() {
// Mock 객체 주입
let mockTeam = MockTeam()
let philFoden = Foden(team: mockTeam)
let result = philFoden.playMatch()
XCTAssertEqual(result, "Mock Foden scores for Mock Team!")
}
}
둘의 차이가 뭘까?
- 의존성 주입 없는 코드
- 강한 결합도즉, 다른 팀이나 모의 객체를 사용할 수 없다.
- Foden 클래스는 ManCity 클래스에 직접적으로 의존한다.
- 테스트의 유연성 부족또한, 변경이 발생하면 여러 코드가 함께 수정되어야 합니다.
- Foden 클래스가 특정 객체에 고정되어 있어, 다양한 시나리오나 객체로 테스트할 수 없다.
- 의존성 주입이 있는 코드:
- 낮은 결합도이로 인해, 런타임에 다양한 팀 객체를 주입받을 수 있다.
- Foden 클래스는 Team 프로토콜에 의존한다.
- 테스트 용이성
- Mock 객체를 쉽게 주입하여 다양한 시나리오를 테스트할 수 있다.
오… 의존성 주입을 하면 내가 원하던 객체지향 설계에 조금 다가갈 수 있겠구나….
라고 느끼는 하루다.
다음 시간에는 UIKit에서 앱을 만드는 과정에서, SceneDelegate, AppDelegate 등에서 의존성 주입해주는 과정을 알아보려고 한다.
(사실 정확히 몰라서 명확한 주제 선정이 안되는 점 양해바랍니다)
오늘은 여기까지 알아보자…ㅎㅎ
이제 미션해야지!!!!!
'iOS' 카테고리의 다른 글
MVVM, Binding (0) | 2024.10.30 |
---|---|
Clean Architecture 적용해보기 (2) | 2024.09.29 |
추상화, 그리고 타입 캐스팅? (0) | 2024.09.10 |
의존성, 의존성 주입(1) ? (2) | 2024.09.09 |
테스트 코드? (3) | 2024.09.01 |
시작하기 전..
- 구글링 + 여러 포스팅을 통해 작성된 내용을 저만의 방식으로 해석한 것이라 틀린 부분이 있을 수도 있습니다.
- 언제든 댓글로 의견, 조언 환영입니다~!
지난 시간에..
의존성 주입의 정의를 이야기하며
프로그램 디자인이 결합도를 느슨하게 되도록하고 의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것
이를 4가지로 분리해서 보는 것으로 글을 마무리 했었다.
정리하자면
- 결합도를 느슨하게 되도록
- 의존관계 역전 원칙
- 단일 책임 원칙
- 클라이언트의 생성에 대한 의존성을
- 클라이언트의 행위로부터 분리하는 것.
이것이 의존성 주입의 메인 키워드였다.
Intro
오늘 알아볼 내용은 아래와 같다.
- 의존관계 역전 원칙?
- 단일 책임 원칙?
- 의존성 주입에 대한 방법?
위 3가지에 대해 하나씩 알아보자.
1. 의존관계 역전 원칙 - DIP (Dependency Inversion Principle)
- 객체지향 설계에서 중요시되는 SOLID 원칙에서 D를 맡고 있는 녀석이다.
- 이 원칙의 의미하는 바는 뭘까?
상위계층(정책결정)이 하위계층(세부사항)에 의존하는 전통적인 의존관계를 반전시킴으로써 상위계층이 하위계층의 구현으로부터 독립되게 할 수 있는 구조
- 를 의미한다고 한다...

차근차근! 하나하나씩 봐보자.
- 위의 글을 분리해보자.
- “상위계층이 하위계층에 의존하는 전통적인 의존 관계” 반전
- “상위계층이 하위계층의 구현으로부터 독립” 되는
- 구조이다.
1번은 어디선가 많이 본 말이다. 이전 글 참고
상위계층이 하위계층에 의존하는 관계를 방지하려면?!
→ 추상화를 적용하면 된다.
추상화에 의존하게 되면 자연스레 2번도 해당되도록 할 수 있다.
Swift에서는 추상화를 하기 위한 프로토콜을 생성 후 채택하게 끔 하는 방법으로 이를 적용해볼 수 있을 거 같다.
다시 한 번 정리를 하면 DIP를 만족하게 하기 위해선
- 상위모듈은 하위모듈에 의존해서는 안된다.
- 상위모듈과 하위모듈 모두 추상화에 의존해야한다.
- 추상화는 세부사항에 의존해서는 안된다.
- 세부사항이 추상화에 의존해야한다.
예시를 한번 보자.
protocol Playable: AnyObject {
var goals: Int { get set }
}
class CityPlayer: Playable {
var memberCount = 11
}
class ManCity {
var players: Playable
init(player: Playable) {
self.players = player
}
}
let manCity = ManCity(player: CityPlayer())
- 해당 구조는 아래와 같은 구조를 가진다.
- 상위모듈: ManCity
- 하위모듈: CityPlayer
- 상위, 하위 모듈 모두 Playable(추상화 프로토콜)에 의존한다.
- 상위-하위 모듈 간 의존 관계를 독립시킨 상태이다.
- ManCity → Playable ← ManCity 와 같은 구조를 가진다.
그럼 나쁜 예시는 뭔가?
아래의 예시를 보자.
class ManCity {
func play() -> String {
return "Phil Foden scores a goal!"
}
}
class Foden {
let team: ManCity = ManCity()
func perform() {
let result = team.play()
print(result)
}
}
- 이는 아래와 같은 특징을 가진 구조이다.
- 상위 모듈: Foden
- 하위 모듈: ManCity
- 상위 모듈인 Foden이 ManCity에 의존하고 있는 관계로 만약 ManCity가 수정, 변화가 생긴다면 Foden 상위 모듈을 수정해야 한다.
- 즉, DIP의 원칙을 어긴 프로그램의 설계이다.
이를 어떻게 수정할 수 있을까?
protocol Playable {
func play() -> String
}
class ManCity: Playable { // 하위 모듈
func play() -> String {
return "Phil Foden scores a goal!"
}
}
class Foden { // 상위 모듈
let team: Playable
init(team: Playable) {
self.team = team
}
func perform() {
let result = team.play()
print(result)
}
}
let manCity = ManCity()
let philFoden = Foden(team: manCity)
philFoden.perform()
뭐가 다른 거지?
- Foden은 기존 ManCity에 의존하지 않고 추상화 시킨 객체인 Playable에 의존하게 된다.
- 따라서 Playable 프로토콜 구현부는 외부에서 변화에 따라 지정해주면 된다.
- Foden은 구현부에 상관없이 변화에 민감하지 않게 되는
“DIP 원칙을 지켰다”
라고 할 수 있다.
뭔가 폭풍우 처럼 지나갔다…
기억하기 힘들다~! 하면
추상화를 통한 상위-하위 모듈의 분리
로 암기하면 될 거 같다.
(지극히 개인적인 의견)
이어서 단일 책임 원칙도 봐보도록 하자.
2. 단일 책임 원칙 (Single Responsibility Principle)
- 이 녀석은 SOLID 원칙 중 S를 담당하는 녀석이다.
해당 원칙의 정의를 보자.
모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 함을 일컫는다.
(내가 이전에 작성하던)
맨시티 구단을 관리하기 위한 ManCityService라는 클래스가 있다고 하자.
class ManCityService {
func playMatch() {
let matchData = requestMatch()
let performance = decodePerformance(data: matchData)
savePerformanceToHistory(performance: performance)
}
private func requestMatch() -> Data {
// Call for match data
return Data()
}
private func decodePerformance(data: Data) -> Performance {
// Decoding match performance from data
return Performance(goals: 2, assists: 1)
}
private func savePerformanceToHistory(performance: Performance) {
// Save performance data to history
}
}
- ManCityServie 클래스는…
- playMatch
- requestMatch
- decodePerformance
- savePerformanceToHistory…
너무 많은 역할을 가지고 있다....
클래스는 하나의 책임만 가지도록 해보자.
protocol MatchRequester {
func requestMatch() -> Data
}
protocol PerformanceDecodable {
func decodePerformance(data: Data) -> Performance
}
protocol PerformanceSavable {
func savePerformanceToHistory(performance: Performance)
}
class MatchDataRequester: MatchRequester {
func requestMatch() -> Data {
// Call for match data
return Data()
}
}
class PerformanceDecoder: PerformanceDecodable {
func decodePerformance(data: Data) -> Performance {
// Decoding match performance from data
return Performance(goals: 2, assists: 1)
}
}
class PerformanceSaver: PerformanceSavable {
func savePerformanceToHistory(performance: Performance) {
// Save performance data to history
}
}
class ManCityService {
let dataRequester: MatchRequester
let decoder: PerformanceDecodable
let saver: PerformanceSavable
init(dataRequester: MatchRequester, decoder: PerformanceDecodable, saver: PerformanceSavable) {
self.dataRequester = dataRequester
self.decoder = decoder
self.saver = saver
}
func playMatch() {
let matchData = dataRequester.requestMatch()
let performance = decoder.decodePerformance(data: matchData)
saver.savePerformanceToHistory(performance: performance)
}
}
// 의존성 주입
let manCityService = ManCityService(dataRequester: MatchDataRequester(), decoder: PerformanceDecoder(), saver: PerformanceSaver())
manCityService.playMatch()
- 뭔가 프로토콜, 클래스가 많아졌는데…
- 왜 이렇게..?
뭐가 다른데?
- 각 역할을 분리된 클래스로 분리하여 단일 책임을 만족하도록 만들었다.
- MatchDataRequester: 경기 데이터를 요청하는 책임.
- PerformanceDecoder: 경기 데이터를 해석하는 책임.
- PerformanceSaver: 데이터를 저장하는 책임.
- 이전과 비교하면
- ManCityService는 각각의 모듈들을 활용
- playMatch에 대한 책임만을 가지고 있다.
이와 같이
- 클래스는 하나의 책임만을
- 그 책임을 완전히 캡슐화해야 SRP를 만족하는 코드라고 할 수 있다.
해당 원칙을 만족하도록 하는 수정하는 과정에서 그냥 class로만 분리해야지~! 한다면 (protocol 사용 X)
- DIP를 만족하지 않을 수 있다.
즉, SOLID 원칙은 연결되어 있다는 것을 글을 작성하면서 인지하게 되었다.
그래서 드디어 본론으로 돌아갈 수 있게 되었다.
의존성 주입(Dependency Injection) 이란?
- 줄여서 DI라고 많이 불린다. 정의는 아래와 같다.
의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것
- DIP, SRP를 따르면서 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것.
- 이를 조금만 수정해보면
- 객체 간의 의존성을 외부에서 주입하는 방식으로, 객체가 직접 의존하는 객체를 생성하거나 초기화하지 않고 외부에서 주입받는 방식이라고 할 수 있다.
우선은… “아니 그래서 이걸 왜 사용하는데?”
- 객체 간의 의존성을 줄여서 코드의 재활용성 / 확장성이 높아진다.
- 객체 간의 결합도가 낮아져 유연한 코드/유연한 프로그램을 작성 가능
- 유지 보수가 쉬워진다.
- Unit Test가 가능해진다.
- 특정 객체에 대한 의존성을 없애고 Test 객체를 주입할 수 있기 때문!
그래서… 어떤 방법들이 있어요?
- 의존성 주입 방법으로는 3가지가 있다.
- Constructor Injection
- Setter Injection
- interface Injection
각각의 예시를 보자.
// 1. Constructor Injection
class Player {
let team: Team
init(team: Team) {
self.team = team
}
}
let team = Team(name: "Man City")
let player = Player(team: team)
// 2. Setter Injection
class Player {
var team: Team?
func setTeam(_ team: Team) {
self.team = team
}
}
let player = Player()
player.setTeam(Team(name: "Man City"))
// 3. Interface Injection
protocol TeamInjectable {
func injectTeam(team: Team)
}
class Player: TeamInjectable {
var team: Team?
func injectTeam(team: Team) {
self.team = team
}
}
let player = Player()
player.injectTeam(team: Team(name: "Man City"))
위 3가지 중 Constructor, Setter Injection이 쓰이는 편이고..
사실 생성자 주입(Constructor Injection)이 가장 많이 사용된다고 한다.
이유는 아래와 같다.
- 안정성: 객체가 생성될 때 의존성을 확실하게 주입받기 때문에, 의존성이 누락될 가능성이 적다.
- 불변성: 생성자로 주입된 의존성은 주로 변경되지 않는 경우가 많아, 클래스의 불변성을 유지할 수 있다.
- 테스트 용이성: 테스트 시, 의존성을 생성자에서 쉽게 주입할 수 있어 모의 객체(Mock)를 사용하는 단위 테스트에 적합하다.
그래서 뭐가 달라지는 지 확인해보고 싶다!
예시를 통해 비교해보는 것이 가장 좋을 것 같다.
우선 의존성 주입이 없는 코드이다.
class ManCity {
func play() -> String {
return "Phil Foden scores for Manchester City!"
}
}
class Foden {
let team = ManCity() // 직접 인스턴스 생성 (의존성 주입 없음)
func playMatch() {
let result = team.play()
print(result)
}
}
let philFoden = Foden()
philFoden.playMatch()
다음은 위의 코드를 의존성 주입(Constructor Injection 사용)이 있는 코드로 변경한 결과이다.
protocol Team {
func play() -> String
}
class ManCity: Team {
func play() -> String {
return "Phil Foden scores for Manchester City!"
}
}
class Foden {
let team: Team
init(team: Team) { // 의존성 주입
self.team = team
}
func playMatch() {
let result = team.play()
print(result)
}
}
let manCity = ManCity()
let philFoden = Foden(team: manCity) // 의존성 주입
philFoden.playMatch()
뭐가 달라졌는데?!
- Foden 클래스가 ManCity와 같은 구체 클래스에 의존하지 않고, Team 프로토콜에 의존함으로써 다른 팀 객체도 쉽게 주입할 수 있다.
- 모의 객체(Mock)를 주입하여 테스트할 수 있기 때문에 단위 테스트가 쉬워진다.
오… 첫번째는 알법한데 2번째는 체감이 안된다.
테스트 코드에 대한 차이도 살펴보자.
// 의존성 주입이 없는 경우
import XCTest
class FodenTests: XCTestCase {
func testPlayMatchWithoutDependencyInjection() {
let philFoden = Foden()
let result = philFoden.playMatch()
// ManCity 객체에 의존하여 테스트
XCTAssertEqual(result, "Phil Foden scores for Manchester City!")
}
}
// 의존성 주입이 있는 경우
import XCTest
// Mock 클래스
class MockTeam: Team {
func play() -> String {
return "Mock Foden scores for Mock Team!"
}
}
class FodenTests: XCTestCase {
func testPlayMatchWithMockTeam() {
// Mock 객체 주입
let mockTeam = MockTeam()
let philFoden = Foden(team: mockTeam)
let result = philFoden.playMatch()
XCTAssertEqual(result, "Mock Foden scores for Mock Team!")
}
}
둘의 차이가 뭘까?
- 의존성 주입 없는 코드
- 강한 결합도즉, 다른 팀이나 모의 객체를 사용할 수 없다.
- Foden 클래스는 ManCity 클래스에 직접적으로 의존한다.
- 테스트의 유연성 부족또한, 변경이 발생하면 여러 코드가 함께 수정되어야 합니다.
- Foden 클래스가 특정 객체에 고정되어 있어, 다양한 시나리오나 객체로 테스트할 수 없다.
- 의존성 주입이 있는 코드:
- 낮은 결합도이로 인해, 런타임에 다양한 팀 객체를 주입받을 수 있다.
- Foden 클래스는 Team 프로토콜에 의존한다.
- 테스트 용이성
- Mock 객체를 쉽게 주입하여 다양한 시나리오를 테스트할 수 있다.
오… 의존성 주입을 하면 내가 원하던 객체지향 설계에 조금 다가갈 수 있겠구나….
라고 느끼는 하루다.
다음 시간에는 UIKit에서 앱을 만드는 과정에서, SceneDelegate, AppDelegate 등에서 의존성 주입해주는 과정을 알아보려고 한다.
(사실 정확히 몰라서 명확한 주제 선정이 안되는 점 양해바랍니다)
오늘은 여기까지 알아보자…ㅎㅎ
이제 미션해야지!!!!!
'iOS' 카테고리의 다른 글
MVVM, Binding (0) | 2024.10.30 |
---|---|
Clean Architecture 적용해보기 (2) | 2024.09.29 |
추상화, 그리고 타입 캐스팅? (0) | 2024.09.10 |
의존성, 의존성 주입(1) ? (2) | 2024.09.09 |
테스트 코드? (3) | 2024.09.01 |