이론만 외우고 있던 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/
오늘의 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시간째 깨어있어서 하루종일 제정신이아니었다
'iOS' 카테고리의 다른 글
[iOS] MVVM 패턴에서 데이터 로딩 책임 분리 (0) | 2025.06.18 |
---|---|
[아키텍처] Clean Architecture + MVVM (0) | 2025.06.13 |
[iOS] 리팩토링이라 쓰고 파일분리노가다라고 읽기 (0) | 2025.05.22 |
override init vs required init 초기화 차이 (0) | 2025.05.21 |
[iOS] SnapKit과 AutoLayout (0) | 2025.05.20 |