카테고리 없음

SOLID와 객체지향의 사실과 오해 독서 후기

GH_iOS 2024. 8. 3. 19:52

 

SOLID 란?

SOLID는 객체지향 프로그래밍 및 설계에서 소프트웨어의 구조를 개선하고 유지 보수성을 높이기 위한 다섯 가지 원칙의 약자입니다.

각 원칙은 코드의 결합도를 낮추고 응집도를 높여 시스템의 유연성과 확장성을 향상시키는 것을 목표로 합니다.

 

S : SRP 단일 책임 원칙(Single Responsibility Principle)
O : OCP 개방- 폐쇄 원칙 (Open/Closed Principle)
L : LSP 리스코프 치환 원칙 (Liskov Substitution Principle)
I : ISP 인터페이스 분리 원칙 (Interface Segregation Principle)
D : DIP 의존 역전 원칙 (Dependency Inversion Principle)

이제 이 원칙들을 자세히 살펴보겠습니다.

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

각 객체가 단일 책임을 가져야 하는 원칙입니다.

하나의 객체는 하나의 연관 Task들을 처리하도록 설계해야 합니다.

 

왜?

 

하나의 객체가 하나의 연관 Task를 처리하지 않을 경우 객체는 비대해지고 유지 보수와 테스트를 어렵게 만듭니다.

// SRP 위반
class UserManager {
    func addUser() { /*...*/ }
    func deleteUser() { /*...*/ }
    func sendEmail() { /*...*/ }
}

// SRP 준수
class UserManagerSRP {
    func addUser() { /*...*/ }
    func deleteUser() { /*...*/ }
}

class EmailManagerSRP {
    func sendEmail() { /*...*/ }
}
 

 

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

객체의 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 합니다.

즉 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있어야 합니다.

왜?

 

기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있으면, 기준 기능의 리스크를 줄이고 유지 보수를 용이하게 합니다.

// OCP 위반
class GraphicEditorOCP {
    func drawShape(shape: Shape) {
        if shape is Rectangle {
            // Draw rectangle
        } else if shape is Circle {
            // Draw circle
        }
    }
}

// OCP 준수
protocol ShapeOCP {
    func draw()
}

class RectangleOCP: ShapeOCP {
    func draw() {
        print("Drawing a rectangle")
    }
}

class CircleOCP: ShapeOCP {
    func draw() {
        print("Drawing a circle")
    }
}

class GraphicEditorOCPCompliant {
    func drawShape(shape: ShapeOCP) {
        shape.draw()
    }
}
 

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

서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다는 원칙입니다.

상위 타입의 객체를 하위 타입으로 치환해도 프로그램이 정상적으로 동작해야 합니다.

왜?

 

만약 리스코프 치환 원칙을 준수하지 않을 경우, 예외가 발생할 수 있으며, 프로그램이 예상하지 못한 방식으로 동작할 수 있습니다.

// LSP 위반
protocol BirdLSP {
    func fly()
}

class SparrowLSP: BirdLSP {
    func fly() {
        print("Sparrow flying")
    }
}

class OstrichLSP: BirdLSP {
    func fly() {
        fatalError("Ostriches can't fly")
    }
}

class BirdWatcherLSP {
    func watchBirdFly(bird: BirdLSP) {
        bird.fly()
    }
}

// LSP 준수
protocol BirdLSPCompliant {
    func move()
}

class SparrowLSPCompliant: BirdLSPCompliant {
    func move() {
        print("Sparrow flying")
    }
}

class OstrichLSPCompliant: BirdLSPCompliant {
    func move() {
        print("Ostrich running")
    }
}

class BirdWatcherLSPCompliant {
    func watchBirdMove(bird: BirdLSPCompliant) {
        bird.move()
    }
}
 

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

자신이 사용하지 않는 메서드에 의존하지 않도록 하기 위해 인터페이스를 여러 개로 분리하는 원칙입니다.

왜?

 

사용하지 않는 메서드에 의존하지 않도록 하면, 인터페이스의 변경이 빈번해지는 것을 방지하고, 클라이언트 코드가 불필요한 변경에 영향을 받지 않도록 합니다. 이는 코드의 응집도를 높이고, 유지 보수를 쉽게 만듭니다.

// ISP 위반
protocol WorkerISP {
    func work()
    func eat()
    func sleep()
}

class RobotISP: WorkerISP {
    func work() {
        print("Robot working")
    }
    
    func eat() {
        // 로봇은 먹지 않지만 이 메서드를 구현해야 합니다.
    }
    
    func sleep() {
        // 로봇은 잠을 자지 않지만 이 메서드를 구현해야 합니다.
    }
}
// ISP 준수
protocol WorkableISP {
    func work()
}

protocol EatableISP {
    func eat()
}

protocol SleepableISP {
    func sleep()
}

class HumanISP: WorkableISP, EatableISP, SleepableISP {
    func work() {
        print("Human working")
    }
    
    func eat() {
        print("Human eating")
    }
    
    func sleep() {
        print("Human sleeping")
    }
}

class RobotISPCompliant: WorkableISP {
    func work() {
        print("Robot working")
    }
}
 

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

구현된 모듈이 추상화된 모듈에 의존하되, 반대는 성립이 안되도록 구현하는 원칙입니다.

왜?

 

추상화된 모듈에만 의존되도록 구현하여 결합도 낮춥니다.

코드를 좀 더 모듈화하고, 유지 보수를 용이하게 합니다.

// DIP 준수
protocol SwitchableDIP {
    func turnOn()
    func turnOff()
}

class LightDIPCompliant: SwitchableDIP {
    func turnOn() {
        print("Light is on")
    }
    
    func turnOff() {
        print("Light is off")
    }
}

class FanDIPCompliant: SwitchableDIP {
    func turnOn() {
        print("Fan is on")
    }
    
    func turnOff() {
        print("Fan is off")
    }
}

class SwitchDIPCompliant {
    var device: SwitchableDIP
    
    init(device: SwitchableDIP) {
        self.device = device
    }
    
    func operate() {
        device.turnOn()
    }
}
 

객체지향 사실과 오해 후기

처음 객체지향에 대해 생각하게 되는 책이었습니다.

객체지향을 어떤 식으로 접근하며 흔히 하는 오해들을 들어주며, 객체지향에 입문하는 저에게 정말 도움이 많이 된 책인 거 같습니다.

 

이 책에서 말하는 객체지향이란?

  • 시스템을 상호작용하는 자율적인 객체들의 공동체로 바라보고 객체를 이용해 시스템을 분할하는 방법
  • 자율적인 객체란 상태와 행위를 함께 지니며 스스로 책임지는 객체
  • 객체는 시스템의 행위를 구현하기 위해 다른 객체와 협력함, 각 객체는 협력 내에서 정해진 역할을 수행하며 역할은 관련된 책임의 집합
  • 객체는 다른 객체와 협력하기 위해 메시지를 전송, 이 메시지를 수신한 객체는 처리하는데 적합한 메소드를 자율적으로 선택

객체를 상태 중심으로 보는 것의 문제점

객체를 상태 중심으로 바로 보는 것은 올바르지 않다고 설명합니다. 상태가 무엇인지 결정하고 그 상태에 필요한 행동을 결정하는 것은 잘못된 것입니다.

 

만약 상태를 중심으로 바라볼 경우:

 

1. 캡슐화가 저해됩니다: 객체 내부로 깔끔하게 캡슐화되지 못하고 공용 인터페이스에 그대로 노출될 확률이 높아집니다.

2. 객체가 고립된 섬이 됩니다: 상태를 먼저 고려하는 방식은 협력이라는 문맥에서 멀리 벗어난 채 객체를 설계합니다.

3. 객체의 재사용성이 저하됩니다.

 

행동 중심으로 객체 설계

객체는 행동 중심으로 설계하며, 객체의 행동은 열려있고 상태는 닫혀 있어야 한다고 말합니다.

 

행동 중심의 설계는 객체의 책임을 명확하게 하고 시스템의 변경과 확장에 유리하게 작용합니다.

책임에 대한 행동으로 설계된 객체는 응집도가 높아져 객체 간의 결합도를 낮출 수 있습니다.

결합도가 낮은 코드는 유지 보수하기 쉽고 변경과 확장에 대한 사이드 이펙트의 위험도 줄어듭니다.

캡슐화를 통해 객체의 내부 상태를 보호하고, 객체 간의 결합도를 낮추며, 시스템의 유지 보수성을 높입니다.

 

 

이 책을 읽고 난 후 내가 생각하는 객체지향

제가 생각하는 객체지향은 각 객체가 협력을 통해 구현을 완성시키는 것입니다. 이때 각각의 객체는 메시지를 주고받으며, 객체의 행동은 열려있고 상태는 닫혀 있어야 합니다.

 

메시지가 객체를 선택해야 하며, 메시지를 선택하고 그 후 메시지를 수신하기에 적절한 객체를 선택해야 합니다.

각 객체는 자율성이 보장되어야 합니다. 자율성이란 외부의 영향 없이 메서드를 자유롭게 변경할 수 있고, 구현을 선택 후 인터페이스 뒤로 감추는 것을 의미합니다.

객체는 요청에 대해 대답하거나 적절한 행동을 할 의무가 있을 때 책임을 가집니다.

 

그럼 객체란 무엇인가?

 

객체는 상태와 행동을 포함하거나 포함하지 않을 수 있는 독립적인 단위입니다.

 

이 책을 읽으면서 확실히 객체 지향이 무엇인지, 어떤 식으로 접근하고 설계해야 하는지 알게 되는 그런 책이었습니다.

혹시 객체지향이 무엇인지 어떤 식으로 접근할지 어려우신 분들은 꼭 이 책을 읽어 보시는 것을 추천드립니다.