DIP - 의존성 역전
DIP - 의존성 역전
의존성 역전 원칙 (Dependency Inversion Principle)
핵심 개념
구현체에 의존하지 않고, 프로토콜에 의존하기
- 상위 계층은 하위 계층의 구현이 아닌 프로토콜에 의존
- 추상화에 의존하여 결합도를 낮추고 유연성을 높임
의존성 역전의 두 가지 원칙
- 상위 모듈은 하위 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다
- 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항이 추상화에 의존해야 한다
잘못된 예시 (DIP 위반)
1
2
3
4
5
6
7
8
// 구체 클래스에 직접 의존하는 잘못된 방식
class LoginUseCase {
let service = KakaoAuthService() // 구체 클래스에 강하게 결합
func execute() {
service.login()
}
}
문제점:
- 다른 인증 서비스로 변경하려면 코드 수정 필요
- 테스트 시 실제 카카오 API 호출
- 확장성과 유연성 부족
올바른 예시 (DIP 적용)
1. 프로토콜 정의 (추상화)
1
2
3
protocol AuthService {
func login()
}
2. 구현체 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class KakaoAuthService: AuthService {
func login() {
print("카카오 로그인")
}
}
class GoogleAuthService: AuthService {
func login() {
print("구글 로그인")
}
}
class AppleAuthService: AuthService {
func login() {
print("애플 로그인")
}
}
3. 상위 계층 정의 (UseCase)
1
2
3
4
5
6
7
8
9
10
11
class LoginUseCase {
let service: AuthService // 프로토콜에 의존
init(service: AuthService) {
self.service = service
}
func execute() {
service.login()
}
}
4. 사용 예제
1
2
3
4
5
6
7
8
9
10
11
12
// 다양한 구현체로 같은 UseCase 사용 가능
let kakaoAuth = KakaoAuthService()
let googleAuth = GoogleAuthService()
let appleAuth = AppleAuthService()
let kakaoLogin = LoginUseCase(service: kakaoAuth)
let googleLogin = LoginUseCase(service: googleAuth)
let appleLogin = LoginUseCase(service: appleAuth)
kakaoLogin.execute() // "카카오 로그인"
googleLogin.execute() // "구글 로그인"
appleLogin.execute() // "애플 로그인"
장점
1. 유연성
- 런타임에 구현체 교체 가능
- 새로운 구현체 추가 시 기존 코드 수정 불필요
2. 테스트 용이성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 테스트용 Mock 구현
class MockAuthService: AuthService {
var loginCalled = false
func login() {
loginCalled = true
print("Mock 로그인")
}
}
// 테스트에서 Mock 사용
let mockAuth = MockAuthService()
let useCase = LoginUseCase(service: mockAuth)
useCase.execute()
// mockAuth.loginCalled == true
3. 확장성
1
2
3
4
5
6
7
8
9
10
// 새로운 인증 방식 추가 시 기존 코드 변경 없음
class FacebookAuthService: AuthService {
func login() {
print("페이스북 로그인")
}
}
let facebookAuth = FacebookAuthService()
let facebookLogin = LoginUseCase(service: facebookAuth)
facebookLogin.execute() // "페이스북 로그인"
의존성 주입 방법
1. 생성자 주입 (Constructor Injection)
1
2
3
4
5
6
7
class LoginUseCase {
private let authService: AuthService
init(authService: AuthService) {
self.authService = authService
}
}
2. 프로퍼티 주입 (Property Injection)
1
2
3
4
5
6
7
class LoginUseCase {
var authService: AuthService?
func execute() {
authService?.login()
}
}
3. 메서드 주입 (Method Injection)
1
2
3
4
5
class LoginUseCase {
func execute(with authService: AuthService) {
authService.login()
}
}
실제 적용 시나리오
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 설정에 따라 다른 구현체 사용
class AuthServiceFactory {
static func create(type: AuthType) -> AuthService {
switch type {
case .kakao:
return KakaoAuthService()
case .google:
return GoogleAuthService()
case .apple:
return AppleAuthService()
}
}
}
enum AuthType {
case kakao, google, apple
}
// 사용
let authType: AuthType = .kakao // 설정값
let authService = AuthServiceFactory.create(type: authType)
let loginUseCase = LoginUseCase(service: authService)
loginUseCase.execute()
결론
의존성 역전 원칙을 적용하면:
- 낮은 결합도: 구체 클래스 대신 추상화에 의존
- 높은 응집도: 각 클래스는 단일 책임을 가짐
- 확장 가능: 새로운 구현체 추가 용이
- 테스트 가능: Mock 객체로 단위 테스트 수행 가능
유지보수 용이: 변경 사항이 다른 모듈에 미치는 영향 최소화
- 사용 예제
1
2
3
let kakaoAuth = KakaoAuthService()
let useCase = LoginUseCase(service: kakaoAuth)
useCase.execute()
This post is licensed under CC BY 4.0 by the author.