객체지향 프로그래밍과 SOLID원칙

SOLID는 객체 지향 프로그래밍과 설계에서 중요한 다섯가지 원칙을 나타내는 약어이다. 각 글자는 특정한 설계 원칙을 의미하고, 이 원칙들을 통해 더 유지보수가 쉽고, 확장 가능하며, 이해하기 쉬운 코드를 작성할 수 있도록 도와준다.

 

Single Responsibilty Principle

단일 책임 원칙으로써 클래스는 하나의 목적을 가져야하며, 클래스를 변경하는 이유는 단 하나의 이유여야 한다.

SRP를 위반하는 예와 준수하는 예를 한번 살펴보자

  • SRP를 위반하는 예
class UserManager {
    func createUser(name: String, age: Int) {
        // 유저 생성 로직
    }
    
    func sendWelcomeEmail(email: String) {
        // 환영 이메일 발송 로직
    }
}
  • SRP를 준수하는 예
class UserManager {
    func createUser(name: String, age: Int) {
        // 유저 생성 로직
    }
}

class EmailService {
    func sendWelcomeEmail(email: String) {
        // 환영 이메일 발송 로직
    }
}

위 예제에서 UserManger 클래스는 유저 생성에만 책임을 지고, 이메일 발송은 EmailService 클래스에서 담당하게 된다.

 

Open/Closed Principle

개방/폐쇄 원칙 소프트 웨어는 확장에는 열려 있어야 하지만 수정에는 닫혀있어야 한다. 즉 기존의 코드를 변경하지 않고도 객체의 기능을 확장할 수 있어야 한다.

  • OCP 위반 예
class Discount {
    func calculate(price: Double, discountType: String) -> Double {
        if discountType == "percentage" {
            return price * 0.9
        } else if discountType == "fixed" {
            return price - 10
        }
        return price
    }
}
  • OCP 준수 예
protocol DiscountStrategy {
    func calculate(price: Double) -> Double
}

class PercentageDiscount: DiscountStrategy {
    func calculate(price: Double) -> Double {
        return price * 0.9
    }
}

class FixedDiscount: DiscountStrategy {
    func calculate(price: Double) -> Double {
        return price - 10
    }
}

class Discount {
    private let strategy: DiscountStrategy

    init(strategy: DiscountStrategy) {
        self.strategy = strategy
    }

    func calculate(price: Double) -> Double {
        return strategy.calculate(price)
    }
}

Discount클래스가 DiscountStrategy 프로토콜을 통해 확장 가능하도록 설계 되어 있습니다.

 

Liskov Substitution Principle

상위 타입의 객체를 하위 타입으로 바꾸어도 프로그램은 일관되게 동작되어야 한다.

  • LSP 위반 예
class Bird {
    func fly() {
        // 나는 로직
    }
}

class Penguin: Bird {
    override func fly() {
        // 펭귄은 날 수 없기 때문에 예외를 던질 수 있다.
        fatalError("Penguins can't fly!")
    }
}
  • LSP 준수 예
class Bird {
    func move() {
        // 이동 로직
    }
}

class FlyingBird: Bird {
    override func move() {
        fly()
    }
    
    func fly() {
        // 나는 로직
    }
}

class Penguin: Bird {
    override func move() {
        // 수영 로직
    }
}

Penguin 이 Bird 의 서브 클래스이지만, fly 메소드를 오버라이드 하지 않고 move 메소드를 통해 적절한 동작을 수행합니다.

 

Interface Segregation Principle

자신이 사용하지 않는 인터페이스를 구현하도록 강제되어서는 안된다. 클라이언트가 필요로 하는 메서드만 포함하도록 여러 개의 구체적인 인터페이스가 더 낫다.

  • ISP 위반 예
protocol Worker {
    func work()
    func eat()
}

class HumanWorker: Worker {
    func work() {
        // 작업 로직
    }
    
    func eat() {
        // 식사 로직
    }
}

class RobotWorker: Worker {
    func work() {
        // 작업 로직
    }
    
    func eat() {
        // 로봇은 먹을 수 없기 때문에 예외를 던질 수 있다.
        fatalError("Robots don't eat!")
    }
}
  • ISP 준수 예
protocol Workable {
    func work()
}

protocol Eatable {
    func eat()
}

class HumanWorker: Workable, Eatable {
    func work() {
        // 작업 로직
    }
    
    func eat() {
        // 식사 로직
    }
}

class RobotWorker: Workable {
    func work() {
        // 작업 로직
    }
}

Worker 인터페이스를 Workable과 Eatable 인터페이스로 분리하여 로봇이 사용하지 않는 메서드에 의존하지 않도록 한다.

 

Dependency Inversion Principle

클라이언트는 추상화에 의존해야 하며, 구체화(구현된 클래스)에 의존해선 안된다.

  • DIP 위반 예
class Database {
    func save(data: String) {
        // 데이터 저장 로직
    }
}

class DataManager {
    private let database = Database()
    
    func saveData(data: String) {
        database.save(data: data)
    }
}
  • DIP 준수 예
protocol DataStore {
    func save(data: String)
}

class Database: DataStore {
    func save(data: String) {
        // 데이터 저장 로직
    }
}

class DataManager {
    private let dataStore: DataStore
    
    init(dataStore: DataStore) {
        self.dataStore = dataStore
    }
    
    func saveData(data: String) {
        dataStore.save(data: data)
    }
}

DataManager가 Database 클래스에 직접 의존하지 않고, DataStore 프로토콜에 의존하도록 하여 의존성을 역전시킨다.

 

위 내용과 같이 SOLID 원칙을 따르도록 노력하면 코드가 더 유연해지고 유지보수성도 높아지며 확장성을 챙길 수 있게된다.

각 원칙은 구체적인 문제를 해결하는데 중점을 두고 있어 이를 통해 코드의 좋은 품질을 바랄 수 있다.

 

앞으로 해당 내용을 파악 하고 이유가 있는 좋은 코드를 만들어 나가고 싶다.