Post

DIP - 의존성 역전

DIP - 의존성 역전

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

핵심 개념

구현체에 의존하지 않고, 프로토콜에 의존하기

  • 상위 계층은 하위 계층의 구현이 아닌 프로토콜에 의존
  • 추상화에 의존하여 결합도를 낮추고 유연성을 높임

의존성 역전의 두 가지 원칙

  1. 상위 모듈은 하위 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다
  2. 추상화는 세부 사항에 의존해서는 안 되며, 세부 사항이 추상화에 의존해야 한다

잘못된 예시 (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.