본문 바로가기
STUDY/CS

SOLID 5원칙을 아라보자!!!

by 23g 2025. 3. 27.

안녕하세요?

 

오늘은 객체지향의 5가지 원칙인 SOLID 5원칙에 대해 공부해보았답니다.

사실 며칠 걸림 (ㅋ)

 

(오랜만에 이런 공부해보니까 대학생 된 거 같고 좋았어요)

(그 땐 왜 하기 싫었을까요?)

 

제 애증의 단짝 Gemini가 그려준 엉망진창 이미지,,, 하

 

아무튼 여러 문서들을 보면서 아래와 같이 간단하게 정리해 보았어요

 

그럼 시작~

SOLID 원칙 정리

1. SRP (Single Responsibility Principle) - 단일 책임 원칙

  • 개념
    : 하나의 클래스(객체)는 단 하나의 책임(기능)만 가져야 함.
    → 여기서 책임(Responsibility)은 "변경해야 하는 이유"와 연결됨.
    → 즉, 하나의 클래스는 단 하나의 이유로만 변경되어야 함.
  • 오해하기 쉬운 점:
    • "하나의 클래스에는 하나의 함수만 있어야 한다"는 것이 아님! -> 처음에 이렇게 오해함 ㅎ
    • 여러 개의 메서드가 있어도 괜찮음. 단, 모두 같은 책임(기능)을 수행하는 데 필요한 것들이어야 함.
  • 예시
  • SRP 위반 코드 (여러 책임이 한 클래스에 섞여 있음)
class UserManager {
    func saveUser() { /* 데이터베이스 저장 로직 */ }
    func validateUser() { /* 유효성 검사 로직 */ }
    func sendEmail() { /* 이메일 전송 로직 */ }
}

 

UserManager에게 너무 과도한 임무가 몰렸어요

  •  SRP 준수 코드 (책임을 분리함)
class UserRepository {
    func saveUser() { /* 데이터베이스 저장 로직 */ }
}

class UserValidator {
    func validateUser() { /* 유효성 검사 로직 */ }
}

class EmailService {
    func sendEmail() { /* 이메일 전송 로직 */ }
}

 

각 클래스는 하나의 책임만 하도록 나눠줍니다!


2. OCP (Open Closed Principle) - 개방 폐쇄 원칙

  • 개념:
    • 확장에는 열려 있고, 수정에는 닫혀 있어야 함
    • 즉, 새로운 기능을 추가할 때 기존 코드를 직접 수정하지 않고, 확장을 통해 추가해야 함.
  • 왜 필요한가?
    • 기존 코드 수정 없이 새로운 기능을 추가할 수 있어야 유지보수가 쉬움.
  • 예시
  • OCP 위반 코드 (기능 추가 시 기존 코드 수정 필요)
class PaymentProcessor {
    func process(paymentType: String) {
        if paymentType == "CreditCard" {
            print("신용카드 결제 처리")
        } else if paymentType == "PayPal" {
            print("PayPal 결제 처리")
        }
    }
}

 

이렇게 구성하면 나중에 새로운 결제 수단 추가 할 때 아주 골치 아파지겠죠

  •  OCP 준수 코드 (새로운 결제 방식 추가 시 기존 코드 수정 불필요)
protocol Payment {
    func process()
}

class CreditCardPayment: Payment {
    func process() {
        print("신용카드 결제 처리")
    }
}

class PayPalPayment: Payment {
    func process() {
        print("PayPal 결제 처리")
    }
}

class PaymentProcessor {
    func process(payment: Payment) {
        payment.process()
    }
}

 

결제 수단을 프로토콜로 만들어서 각 클래스에서 프로토콜 채택하는 방식으로~


 

3. LSP (Liskov Substitution Principle) - 리스코프 치환 원칙

  • 개념:
    • 자식 클래스가 부모 클래스로 대체되어도 정상 작동해야 함.
    • 부모 클래스를 사용하는 코드가 자식 클래스로 변경되어도 오류가 발생하면 안 됨.
  • 위반 예시 (LSP를 지키지 않는 코드)
class Bird {
    func fly() {
        print("날아간다!")
    }
}

class Penguin: Bird {
    override func fly() {
        fatalError("펭귄은 날 수 없음!")
    }
}

func makeBirdFly(bird: Bird) {
    bird.fly()
}

let penguin = Penguin()
makeBirdFly(bird: penguin) // 💥 런타임 에러 발생!

 

아무래도 펭귄은 날지 못하니깐요...

  • 해결 방법 (LSP 준수 코드)
protocol Flyable {
    func fly()
}

class Bird {}

class Sparrow: Bird, Flyable {
    func fly() {
        print("참새가 난다!")
    }
}

class Penguin: Bird {
    // 날지 못하는 새이므로 fly() 미구현
}

func makeBirdFly(bird: Flyable) {
    bird.fly()
}

let sparrow = Sparrow()
makeBirdFly(bird: sparrow) // ✅ 정상 작동

 

4. ISP (Interface Segregation Principle) - 인터페이스 분리 원칙

  • 개념:
    • 인터페이스(프로토콜)를 세분화하여, 필요한 기능만 구현하도록 해야 함.
    • 하나의 커다란 인터페이스(프로토콜)보다는 여러 개의 작은 인터페이스로 나누는 것이 좋음.
  • 위반 예시 (ISP를 지키지 않는 코드 - 불필요한 메서드 구현 강제됨)
protocol Worker {
    func work()
    func eat()
}

class Robot: Worker {
    func work() {
        print("로봇이 일함")
    }

    func eat() {
        fatalError("로봇은 먹지 않음!")
    }
}

 

프로토콜에서도 분리 원칙을 지켜줘야해요

  • 해결 방법 (ISP 준수 코드 - 필요한 기능만 구현하도록 인터페이스 분리)
protocol Workable {
    func work()
}

protocol Eatable {
    func eat()
}

class Robot: Workable {
    func work() {
        print("로봇이 일함")
    }
}

class Human: Workable, Eatable {
    func work() {
        print("사람이 일함")
    }

    func eat() {
        print("사람이 밥을 먹음")
    }
}

 


5. DIP (Dependency Inversion Principle) - 의존 역전 원칙

  • 개념:
    • 구체적인 클래스에 의존하지 말고, 프로토콜과 같은 추상화된 인터페이스에 의존해야 함.
    • 상위 모듈(고수준)이 하위 모듈(저수준)에 의존하면 안 됨.
  • 위반 예시 (DIP를 지키지 않는 코드 - 구체적인 클래스에 의존)
class FileLogger {
    func log(message: String) {
        print("파일에 로그 저장: \(message)")
    }
}

class UserService {
    let logger = FileLogger() // ❌ 직접 FileLogger에 의존
    
    func registerUser(name: String) {
        logger.log(message: "\(name) 유저 등록됨")
    }
}

 

전 다 이렇게 했었는뎁,,,ㅎ

  • 해결 방법 (DIP 준수 코드 - 추상화된 프로토콜에 의존)
protocol Logger {
    func log(message: String)
}

class FileLogger: Logger {
    func log(message: String) {
        print("파일에 로그 저장: \(message)")
    }
}

class ConsoleLogger: Logger {
    func log(message: String) {
        print("콘솔에 로그 출력: \(message)")
    }
}

class UserService {
    let logger: Logger // ✅ 프로토콜(추상화)에 의존
    
    init(logger: Logger) {
        self.logger = logger
    }

    func registerUser(name: String) {
        logger.log(message: "\(name) 유저 등록됨")
    }
}

 

앞으로 이렇게 할게요


📌 한 줄 정리

SRP: 하나의 클래스(객체)는 하나의 책임(기능)만 가져야 함
OCP: 확장에는 열려 있고, 수정에는 닫혀 있어야 함 (기능 추가는 확장으로)
LSP: 자식 클래스가 부모 클래스로 대체되어도 정상 작동해야 함
ISP: 인터페이스(프로토콜)를 작은 단위로 분리해야 함
DIP: 구체적인 클래스가 아니라 프로토콜(추상화된 인터페이스)에 의존해야 함

 

그러니까~ 하나의 클래스는 하나의 책임만 가지고 있어야하고

구체적인 클래스에 의존하는게 아니라 프로토콜 같은 추상화 인터페이스에 의존해야함

그래야 자식 클래스가 부모 케이스로 변경되어도 정상 작동 원칙이 가능하고

곧 확장에는 열려있고, 수정에는 닫혀있는 코드가 됨~~~

 

 

 

최근댓글

최근글

skin by © 2024 ttutta