의존성?
INtro...
네이버 부스트캠프 그룹분들이랑 이야기를 하는 과정에서
의존성 주입과 관련된 이야기가 나왔다.
사실 개발을 깊이 공부해본 적도 없고, 컴퓨터공학과를 다니면서 많은 수업을 들은 것도 아니었기 때문에, 무슨 말이지? 라는 생각만 하게 된 것 같다.
의존성이란 말은 객체지향에 대해 입문을 하게 되면서 반드시 듣는 용어인 것 같다.
네부캠 수업에서도 그렇고, 그룹원들의 지식에 따라가보기 위해 이에 대한 내용을 학습해보고자 한다..!
시작하기 앞서..
- 공부하는 과정에서 작성하는 글이라 두서 없이, 진행될 수도 있습니다.
- 추가로 예시는 맨시티에 관련된 것이 많을 수 있습니다…🌐
의존성 주입은 객체에 인스턴스 변수를 주는 것을 의미합니다. 정말입니다. 그게 전부입니다. - 제임스 쇼어
의존성?
우선 의존성이란 용어가 뭘까?
뭔가 일상생활에서 쓰이는 용어라면…
“맨시티는 필 포든
에 의존하는 팀이다” 와 같이 사용되지 않을까?
→ 즉, 없어서는 안되는 그런 느낌으로 사용되는 용어다.
소프트웨어 엔지니어링에서도 비슷하게 사용되지 않을까?
우선 이 말은 공감할 수 있을 거다.
객체지향 프로그래밍 세계, 즉 객체의 세계에서 협력은 필수적이다.
- 이때, 협력한다는 것은
객체 간의 의존성이 존재한다는 것
이다. 의존성
이란, 파라미터나 리턴값 또는 지역변수 등으로 다른 객체를 참조하는 것을 의미한다.
예시로 아래와 같은 코드를 보자.
- Player 클래스는 자신의 팀을 나타내는 Team 클래스를 지역변수로 가지고 참조하고 있으므로 의존한다고 표현한다.
class Team {
let name: String
init(name: String) {
self.name = name
}
}
class Player {
let name: String
let team: Team
init(name: String, team: Team) {
self.name = name
self.team = team
}
func introducePlayer() {
print("\(name) plays for \(team.name).")
}
}
let manCity = Team(name: "Manchester City")
let philFoden = Player(name: "Phil Foden", team: manCity)
philFoden.introducePlayer()
현재 Player, Team의 관계는 다음과 같다.
지금까지 봤을 때…
- 이전까지 내가 짰던 방식 중 해당 되는 부분들도 정말 많았다.
- 예로, UIKit에서 ViewController가 특정 모델들에 대해 의존을 하거나,
- 특정 모델간에도 의존하는 것이 발생하는 경우가 많았다.
- 나의 궁금증은 하나로 모아졌다.
- 그래서 의존하는 관계가 만들어지는 것이 나쁜 걸까?
질문에 대한 답변
- 의존성은 최소화되어야 한다.
(사실 이런 답변이 제일 싫다. 🥲 → 어쩔 수 없을 땐 하지만, 최소화하자)
왜 최소화해야 할까?
- 의존성은 객체 간의 협력을 위해 필수적이다.
- 그치만…!
- 의존성이 존재한다는 것은
- 한 객체가 다른 객체에 의존한다는 것이고…
- 이는 하나의 객체가 변할 때 변경이 전파될 수 있기 때문이다.
- 예시로
- 맨시티가 필포든에 의존을 하고 있다면..
- 필포든이 부상으로 경기에 못나온다면 경기력에 영향을 미치는 것으로 생각해볼 수 있다.
하나의 객체가 변할 때 변경이 전파된다
이를 의존성 전이
라고 한다.
이전의 예시로 한번 보자.
- 예를 들어 Player가 Team이 아닌, 소속된 리그를 말을 해야되도록 변경하는 상황을 생각해보자.
- Team을 League로 수정을 하면서 Player 클래스도 많은 수정이 일어났다.
- 이러한 경우를
의존성이 전이
되었다고 한다.
- 이러한 경우를
class League {
let name: String
let teamName: String
init(name: String, teamName: String) {
self.name = name
self.teamName = teamName
}
}
class Player {
let name: String
let league: League
init(name: String, league: League) {
self.name = name
self.league = league
}
func introducePlayer() {
print("\(name) plays for \(league.teamName). league is \(league.name)")
}
}
let manCity = League(name: "Premier League", teamName: "ManCity")
let philFoden = Player(name: "Phil Foden", league: manCity)
philFoden.introducePlayer()
사실 위 상황은 간단해 보일 수도 있다.
그치만....
의존하는 다른 클래스들이 있었다면..?
그 수가 너무 많다면..?
이를 모두 변경해주어야 했을 거다.
이러한 것들은 불필요한 변경으로 간주하자.
개방 폐쇠 원칙
을 준수하도록 의존성 전이를 최소화해야 한다.
의존성 전이를 최소화하기 위해서는 어떻게 할 수 있을까?
컴파일 타임 의존성이 아닌 런타임 의존성을 가져야 한다.
벌써부터 무슨 말인지 모르겠다. 하나씩 봐보도록 하자.
컴파일타임 의존성?
- 말 그대로 코드를 컴파일하는 시점에 결정되는 의존성이며, 클래스 사이의 의존성에 해당한다.
- Swift의 경우 클래스와 객체 사이의 의존성이 컴파일 시점에 결정된다.
- 일반적으로 추상화된 프로토콜이나 인터페이스 대신 구체적인 클래스에 의존할 경우, 그 의존성은 컴파일타임에 고정된다.
이전 코드를 참고해보자.
class Player {
let name: String
let league: League
init(name: String, league: League) {
self.name = name
self.league = league
}
func introducePlayer() {
print("\(name) plays for \(league.teamName). league is \(league.name)")
}
}
위의 코드에서 Player는 컴파일될 때 League 클래스를 참조한다.
- 이는 Player가 League 정보를 가지고 있다는 것을 알고 있음을 의미한다.
- 그러므로 컴파일타임 의존성은 결합도가 높다.
소프트웨어 세계에서 결합도는 낮을수록 좋은데, 결합도를 낮추고 바람직한 의존성을 갖기 위해서는 결국 런타임 의존성을 가져야 한다.
런타임 의존성
- 이 또한 말 그대로 코드를 실행하는 시점에 결정되는 의존성이며, 객체 사이의 의존성에 해당한다.
- 추상화된 프로토콜이나 인터페이스에 의존할 때, 런타임에 구체적인 객체가 할당되므로 런타임 의존성이 발생할 수 있다.
이전에 예시로 들었던 코드에서 LeagueProtocol이라는 인터페이스를 만들었다고 하자.
protocol PlayableProtocol {
var description: String { get }
var teamName: String { get }
}
class League: PlayableProtocol {
let description: String
let teamName: String
init(description String, teamName: String) {
self.description = description
self.teamName = teamName
}
}
Player는 구체 클래스가 아닌 LeagueProtocol 인터페이스에 의존할 수 있다.
class Player {
let name: String
let league: PlayableProtocol
init(name: String, league: PlayableProtocol) {
self.name = name
self.league = league
}
func introducePlayer() {
print("\(name) plays for \(league.teamName). league is \(league.name)")
}
}
let manCity = League(description: "Premier League", teamName: "ManCity")
let philFoden = Player(name: "Phil Foden", league: manCity)
philFoden.introducePlayer()
위의 예시에서 Player는 컴파일 될 때 LeagueProtocol 인터페이스를 참조한다.
이는 컴파일 시점에는 Player가 어떠한 정보를 구체적으로 가지고 있는 지 알 수 없음을 의미하고, 코드가 실행이 될 때에만 league 구현체를 참조하는지 알 수 있다는 것이다.
즉, 추상화에 의존하면 컴파일 의존성과 런타임 의존성은 다를 수 있다.
- 런타임 의존성은 추상클래스 또는 인터페이스에 의존하므로 컴파일 시점에 어느 객체에 의존하는지 알지 못한다.
- 컴파일 시점에는 “오 이런 정보를 가지는 군~” 만 알 수 있고, 실행될 때 어떤 객체를 주입받아서 league와 결합되는지 알 수 있다.
이와 같은 이유로 런타임 의존성은 결합도가 낮으며
다른 객체들과 협력할 가능성을 열어두므로 변경에 유연
한 설계를 갖는다.
갑자기 상부의 지시로… league가 아닌 다른 것이 들어오게 된다면?
- Player에는 의존성이 전이되지 않는다.
- 런타임 의존성을 갖기 때문이다.
class Player {
let name: String
// League대신 다른 PlayableProtocol을 채택하는
// 클래스를 받을 수도 있음.
let league: PlayableProtocol
init(name: String, league: PlayableProtocol) {
self.name = name
self.league = league
}
func introducePlayer() {
print("\(name) plays for \(league.teamName). league is \(league.name)")
}
}
let manCity = League(description: "Premier League", teamName: "ManCity")
let philFoden = Player(name: "Phil Foden", league: manCity)
philFoden.introducePlayer()
위와 같은 런타임 의존 관계를 표현한 관계도는 아래와 같다.
코드를 통해 눈치챌 수도 있지만
결국 런타임 의존성을 갖기 위해서는 추상화에 의존해야 한다.
여기까지 오면서 나도 다음과 같은 생각이 났다.
그래서 "의존성 주입”은 언제 이야기가 나올까?
- (사실 위에서 주입이라는 단어가 1번 나왔었다)
컴파일 시점에는 “오 이런 정보를 가지는 군~” 만 알 수 있고, 실행될 때 어떤 객체를 주입받아서 league와 결합되는지 알 수 있다.
사실 이는 이는 의존성 주입(Dependency Injection)의 핵심 개념 중 하나다.
그렇다면 의존성 주입의 정의부터 보자.
의존성 주입
위키피디아의 정의는 아래와 같다.
“프로그램 디자인이 결합도를 느슨하게 되도록하고 의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것”
끊어서 봐보면 아래 4가지로 볼 수 있을 것 같다.
- 결합도를 느슨하게 되도록
- 의존관계 역전 원칙
- 단일 책임 원칙
- 클라이언트의 생성에 대한 의존성을
- 클라이언트의 행위로부터 분리하는 것.
오 1번은 아까 위에서 본 것 같은데?
→ 기억이 안난다면 컴파일, 런타임 의존성
부분을 봐보도록 하자..
2번 부터는 이후에 알아보도록 하자 (현재 시각 새벽 2시야!!!!!)
참고링크
'iOS' 카테고리의 다른 글
MVVM, Binding (0) | 2024.10.30 |
---|---|
Clean Architecture 적용해보기 (2) | 2024.09.29 |
추상화, 그리고 타입 캐스팅? (0) | 2024.09.10 |
의존성, 의존성 주입?(2) (2) | 2024.09.09 |
테스트 코드? (3) | 2024.09.01 |