iOS

[Swift] SOLID 원칙 iOS에 적용해보기

양밀루 2025. 6. 10. 20:52

이론만 외우고 있던 SOLID 원칙,, 자세히 보니 이미 자연스럽게 쓰고 있었던 것들이다
더 체계적으로 적용하고 활용하기 위해 정리해본다

 

✅ SOLID?

로버트 마틴(Uncle Bob)이 정리한 객체지향 설계 5원칙. 이 원칙들을 지키면:

  • 변경에 유연한 구조: 결합도는 낮고, 응집도는 높은 코드
  • 이해하기 쉬운 구조: 가독성이 좋고 디버깅하기 편한 코드

무작정 따르는 게 아니라 판단력을 가지고 적용해야 함. 과도하게 적용하면 오히려 복잡해질 수 있음!

1️⃣ SRP: 단일 책임 원칙

"클래스 변경 이유는 딱 하나여야 한다"

"하나의 책임만 가져야 한다" -> 사실 '책임'은 주관적인 평가

=> 정확한 정의: "변경의 이유가 하나, 오직 하나뿐이어야 한다"

// ❌ 나쁜 예: 4가지 변경 이유를 가짐
class UserViewController: UIViewController {
    func loadUserData() {
        // API 요청 (API 변경 시 수정)
        URLSession.shared.dataTask(...) { data, _, _ in
            // JSON 파싱 (응답 구조 변경 시 수정)
            let user = try! JSONDecoder().decode(User.self, from: data!)
            // DB 저장 (스키마 변경 시 수정)
            CoreDataStack.save(user)
            // UI 업데이트 (디자인 변경 시 수정)
            self.nameLabel.text = user.name
        }.resume()
    }
}
// ✅ 좋은 예: 각각 하나의 변경 이유만
class UserViewController: UIViewController {
    private let userService = UserService()
    
    private func loadUser() {
        userService.fetchUser { [weak self] user in
            self?.nameLabel.text = user.name  // UI 변경만 담당
        }
    }
}

class UserAPIClient {
    func fetchUser(completion: @escaping (User) -> Void) { /* API만 */ }
}

class UserRepository {
    func save(_ user: User) { /* DB만 */ }
}

class UserService {
    func fetchUser(completion: @escaping (User) -> Void) { /* 비즈니스 로직만 */ }
}

2️⃣ OCP: 개방-폐쇄 원칙

"확장에는 열려있고, 변경에는 닫혀있어야 한다"

// ❌ 나쁜 예: 새로운 로그인 방식 추가할 때마다 수정 필요
class LoginManager {
    func login(type: LoginType, completion: @escaping (Bool) -> Void) {
        switch type {
        case .email: // 이메일 로그인
        case .kakao: // 카카오 로그인  
        case .apple: // 애플 로그인
        // 구글 로그인 추가하려면 여기도 수정해야 함
        }
    }
}
// ✅ 좋은 예: 프로토콜로 확장 가능
protocol LoginService {
    func login(completion: @escaping (Bool) -> Void)
}

class EmailLoginService: LoginService { /* 이메일 로그인 */ }
class KakaoLoginService: LoginService { /* 카카오 로그인 */ }
class GoogleLoginService: LoginService { /* 새로 추가해도 기존 코드 수정 없음! */ }

class LoginManager {
    private let loginService: LoginService
    
    func login(completion: @escaping (Bool) -> Void) {
        loginService.login(completion: completion)  // 기존 코드 변경 없음
    }
}

3️⃣ LSP: 리스코프 치환 원칙

"부모 클래스 자리에 자식 클래스를 넣어도 똑같이 동작해야 한다"

// ❌ 나쁜 예: CustomButton이 예상과 다르게 동작
class CustomButton: UIButton {
    override func setTitle(_ title: String?, for state: UIControl.State) {
        super.setTitle(title?.uppercased(), for: state)  // 무조건 대문자로!
    }
}

func updateButton(button: UIButton) {
    button.setTitle("로그인", for: .normal)
    // UIButton: "로그인", CustomButton: "로그인" (예상과 다름!)
}
// ✅ 좋은 예: 예상 가능한 동작 보장
protocol Downloadable {
    func download() -> String
}

class ImageDownloader: Downloadable {
    func download() -> String { return "image.jpg" }
}

class VideoDownloader: Downloadable {
    func download() -> String { return "video.mp4" }
}

func processFile(downloader: Downloadable) {
    let fileName = downloader.download()
    print("파일 저장: \(fileName)")  // 어떤 구현체든 동일하게 동작
}

4️⃣ ISP: 인터페이스 분리 원칙

"사용하지 않는 것에 의존하지 않아야 한다"

"기능별로 인터페이스를 나누자" -> 핵심은 사용하지 않는 메서드에 의존하면 안 된다!

// ❌ 나쁜 예: 사용하지 않는 메서드들에 의존
protocol MediaPlayerProtocol {
    func play()
    func pause() 
    func nextTrack()
    func shuffle()
}

class SimplePlayer: MediaPlayerProtocol {
    func play() { /* 실제 사용 */ }
    func pause() { /* 실제 사용 */ }
    func nextTrack() { /* Do nothing - ISP 위반! */ }
    func shuffle() { /* Do nothing - ISP 위반! */ }
}
// ✅ 좋은 예: 사용하는 것만 의존
protocol BasicPlayable {
    func play()
    func pause()
}

protocol PlaylistControllable {
    func nextTrack()
    func shuffle()
}

class SimplePlayer: BasicPlayable {
    func play() { /* 기본 재생만 */ }
    func pause() { /* 일시정지만 */ }
}

class AdvancedPlayer: BasicPlayable, PlaylistControllable {
    func play() { /* 재생 */ }
    func pause() { /* 일시정지 */ }
    func nextTrack() { /* 다음 곡 */ }
    func shuffle() { /* 셔플 */ }
}

5️⃣ DIP: 의존관계 역전 원칙

"추상화에 의존해야 하며 구체화에 의존하면 안 된다"

'역전'이라는 건 제어 흐름과 코드 의존성이 반대 방향

// ❌ 나쁜 예: 구체적인 클래스에 의존
class PhotoViewController: UIViewController {
    let coreDataManager = CoreDataManager()  // 특정 구현체에 의존
    
    @IBAction func savePhoto(_ sender: UIButton) {
        coreDataManager.save(photo)  // Realm으로 바꾸려면? 전체 수정
    }
}
// ✅ 좋은 예: 프로토콜에 의존 + 의존성 주입
protocol PhotoStorage {
    func save(_ photo: Photo)
}

class PhotoViewController: UIViewController {
    private let storage: PhotoStorage  // 추상화에 의존
    
    init(storage: PhotoStorage) { self.storage = storage }  // 의존성 주입
    
    @IBAction func savePhoto(_ sender: UIButton) {
        storage.save(photo)  // 어떤 저장소든 OK!
    }
}

// 구현체들 - 언제든 교체 가능!
class CoreDataStorage: PhotoStorage { /* Core Data */ }
class RealmStorage: PhotoStorage { /* Realm */ }
class CloudStorage: PhotoStorage { /* Cloud */ }

// 사용: 어떤 구현체든 주입 가능
let photoVC = PhotoViewController(storage: CoreDataStorage())

🔥 Clean Architecture의 핵심도 DIP

// Use Case가 Presenter 기능을 사용하려면?
protocol PhotoPresenterOutput {
    func presentPhoto(_ photo: Photo)
}

class PhotoUseCase {
    private let presenter: PhotoPresenterOutput  // 인터페이스에 의존
    
    func displayPhoto() {
        presenter.presentPhoto(photo)  // 제어흐름: UseCase → Presenter
        // 하지만 코드 의존성: UseCase → 인터페이스 (역전!)
    }
}

🎯 마무리

SOLID 원칙을 지키면:

  • 변경에 유연한 구조: 작은 기능 추가에도 수십 개 테스트가 깨지는 일 없음
  • 이해하기 쉬운 구조: 본인만 이해할 수 있는 코드가 아닌 팀 모두가 읽기 쉬운 코드
  • 재사용 가능한 구조: 다른 프로젝트에서도 쉽게 가져다 쓸 수 있는 컴포넌트

⚠️ 하지만!

SOLID 원칙은 만능이 아님. 과도하게 적용하면:

  • 불필요한 복잡성 증가
  • 과도한 설계로 인한 개발 속도 저하
  • 간단한 기능도 여러 클래스로 나뉘어 오히려 이해하기 어려워짐

 

 

 

https://www.marcosantadev.com/solid-principles-applied-swift/

https://medium.com/@jgj455/%EC%98%A4%EB%8A%98%EC%9D%98-swift-%EC%83%81%EC%8B%9D-%EA%B0%9D%EC%B2%B4%EC%99%80-solid-%EC%9B%90%EC%B9%99-270415c64b64

 

오늘의 Swift 상식 (객체와 SOLID 원칙)

SOLID 원칙은 모든 객체 지향 프로그래밍에 사용되는 원칙이다.

medium.com

https://tech.kakaobank.com/posts/2411-solid-truth-or-myths-for-developers/

 

모든 개발자가 알아야 할 SOLID의 진실 혹은 거짓

기술 면접 자리에서 SOLID 5대 원칙에 대한 질문을 받아보신 분이라면 주목! 이 글에서는 SOLID 원칙의 역사와 장점, 그리고 각각의 원칙에서 중요한 점을 면접 상황 예시를 통해 가볍게 풀어보았습

tech.kakaobank.com

 

 

떱떱디씨행사때문에 대략 36시간째 깨어있어서 하루종일 제정신이아니었다