스파르타코딩 클럽/기초

객체지향 프로그래밍이 뭔데? + SOLID 원칙

UDDT 2025. 3. 23. 23:40

객체지향 프로그래밍(OOP)

 객체가 뭔데?

    1. 객체

       객체지향에 대해 공부하기 전 객체에 대해서 알아보자.

   객체의 사전적 의미는 다음과 같다. 

객체 : '메소드, 변수'를 가지며 특정 역할을 수행하도록 인간이 정의한 추상적인 개념

    사전적 정의는 그냥 '그런가보다' 하고,

 객체(Object)란 우리가 실제로 존재하는 사물이나 개념을 프로그래밍에서 표현한 것이라 생각하면 된다.
 결국 객체는 "속성(특징)과 행동(기능)을 가진 것"이다.

 

   자동차로 생각해보자

 자동차에는 다음과 같이 속성과 행동이 있다

이를 코드로 표현해보면 다음과 같다

class Car {
    var color: String
    var speed: Int

    init(color: String, speed: Int) {
        self.color = color
        self.speed = speed
    }

    func run() {
        print("\(color) 자동차가 \(speed)km/h 속도로 달립니다")
    }
}

let myFirstCar = Car(color: "빨강", speed: 100)
myFirstCar.run()
// 빨강 자동차가 100km/h 속도로 달립니다

 

 

 그리고 그 자동차에서도 각각의 객체를 나눌 수 있다

큰 객체인 자동차는 여러 개의 작은 객체(타이어, 엔진, 운전자 등등)로 구성되어 있다

이를 객체지향 프로그래밍(OOP)의 중요 요소 중 하나인 "객체의 조합"이라 한다.

 

class Car {
    var color : String
    var speed : Int
    var tires : [Tire]
    var engine: Engine
    var driver: Driver?

    init(color: String, speed: Int, tires: [Tire], engine: Engine, driver: Driver? = nil) {
        self.color = color
        self.speed = speed
        self.tires = tires
        self.engine = engine
        self.driver = driver
    }

    func drive() {
        guard let driver = driver else {
            print("운전자가 없어 운전할 수 없습니다")
            return
        }
        print("\(driver.name)이 \(speed)km/h로 운전합니다.")
    }
}

class Tire {
    var size: Int
    var material : String
    var company: String

    init(size: Int, material: String, company: String) {
        self.size = size
        self.material = material
        self.company = company
    }

    func leftSpin() {
        print("바퀴가 왼쪽으로 돌아갑니다")
    }

    func stop() {
        print("바퀴가 멈춥니다")
    }
}

class Engine {
    var outPut: Int
    var type: String

    init(outPut: Int, type: String) {
        self.outPut = outPut
        self.type = type
    }

    func startTheCar() {
        print("시동을 겁니다")
    }

    func increaseOutPut() {
        print("출력을 증가시킵니다")
    }
}

class Driver {
    var name: String
    var licenseNumber: Int

    init(name: String, licenseNumber: Int) {
        self.name = name
        self.licenseNumber = licenseNumber
    }

    func driveCar() {
        print("운전을 시작합니다")
    }

    func parkingCar() {
        print("주차를 합니다")
    }
}

 

 객체지향 프로그래밍(Object Oriented Progamming)

    1. 객체지향 프로그래밍(OOP)

      객체지향 프로그래밍이란? 객체를 만들고 그것들을 조합하여 프로그램을 구성하는 방식

     즉, 실제 세상을 프로그래밍으로 표현하는 방법!

 

      위의 자동차 예시처럼 객체(Object)를 생성하면 자동차(Car)라는 객체가 만들어지고
속성(색상, 속도)과 기능(달리기, 멈추기) 등을 정의할 수 있다.

    또 바퀴나 엔진도 각각 객체(속성과 기능을 포함하는 것)로 만들 수 있다.

 

      자동차는 바퀴 객체를 사용해서, 엔진 객체로부터 힘을 받고 움직이고, 운전자 객체가 자동차를 운전한다.

 

      각 객체가 서로 협력하면서 프로그램이 동작하는 것이 OPP의 핵심 원리!

 

    2. 객체지향 프로그래밍의 4가지 핵심 개념

        1. 캡슐화(Encapsulation)

            객체의 속성과 기능을 하나로 묶고, 외부에서 함부로 접근하지 못하게 보호하는 것

            예) 자동차 내부(엔진이나 배선 등)을 운전자는 건드리지 못하게 하고, 운전자는 운전대와 페달만 사용하는 것

class Car {
    private var speed: Int = 0  // 외부에서 직접 수정할 수 없음 (캡슐화)

    func accelerate() {
        speed += 10
        print("현재 속도: \(speed)km/h")
    }
}

let myFirstCar = Car()
myFirstCar.accelerate()  // 정상 실행
myFirstCar.speed = 100   // 오류 (private로 선언한 변수는 접근 불가)

 

        2. 상속(Inheritance)

           부모(상위) 클래스의 속성과 기능을 자식(하위) 클래스가 물려받는 것

           자동차 클래스가 있고, 이를 상속받아 전기차를 만들 수 있음

class Car {  // 부모 클래스
    var speed: Int = 0
    func run() {
        print("자동차가 달립니다!")
    }
}

class ElectricCar: Car {  // 자식 클래스 (Car를 상속)
    var batteryLevel: Int = 100
}

let tesla = ElectricCar()
tesla.run()  // ElectricCar 안에 메서드 선언이 없더라도 부모 클래스의 메서드 사용 가능
print(tesla.batteryLevel)  // 100

 

        3. 다형성(Polymorphism)

           같은 기능을 각 객체가 다르게 구현할 수 있는 것

           달리기(run) 기능을 일반 자동차, 스포츠카, 트럭이 다르게 구현할 수 있는 것(속도가 다를 수 있음)

class Car {
    func run() {
        print("자동차가 달립니다!")
    }
}

class SportsCar: Car {
    override func run() {  // 기능을 다르게 구현 (재정의)
        print("스포츠카가 빠르게 달립니다!")
    }
}

let myCar = Car()
myCar.run()  // 자동차가 달립니다!

let mySportsCar = SportsCar()
mySportsCar.run()  // 스포츠카가 빠르게 달립니다!

 

        4. 추상화(Abstraction)

            불필요한 세부 사항은 숨기고, 필요한 기능만 제공하는 것

            자동차를 운전할 때 "엑셀을 밟으면 움직이는구나" 정도만 알면 되고, 내부에서 어떻게 연료가 연소되서 동작하는지는 몰라도 됨

class Car {
    func drive() {
        startEngine()
        print("자동차가 출발합니다!")
    }
    
    private func startEngine() {  // 내부 동작 숨김
        print("엔진이 켜졌습니다.")
    }
}

let myCar = Car()
myCar.drive()  // "엔진이 켜졌습니다." → "자동차가 출발합니다!"
myCar.startEngine()  // 오류 (private 함수 접근 불가)

 

 

 

 

 

 

 SOLID 원칙

    1. SOLID 원칙이 뭔데?

 

이 솔리드 아닙니다..

 

      SOLID 원칙은 객체지향 프로그래밍에서 좋은 설계를 위한 5가지 원칙

    이 원칙을 따르면 코드의 유지보수가 용이해지고, 확장성 향상에 도움을 주고, 버그 발생 가능성이 줄어든다

 

    2. SOLID 

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

           하나의 클래스는 하나의 책임(기능)만 가져야 한다

// 잘못된 예시
class Car {
    func drive() { print("자동차가 달립니다.") }
    func wash() { print("자동차를 세차합니다.") }  // 자동차가 세차 기능까지 갖는건 이상하다
}

// 올바른 예시
class Car {
    func drive() { print("자동차가 달립니다.") }
}

class CarWash {
    func wash(car: Car) { print("자동차를 세차합니다.") }
}

 

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

           기능의 확장은 가능하지만, 기존 코드를 수정해서는 안된다

// 잘못된 예
class Car {
    func run() {
        print("일반 자동차가 달립니다.")
    }
    
    func runFast() {  // 새로운 기능 추가하면서 기존 코드 수정
        print("스포츠카가 빠르게 달립니다!")
    }
}

// 올바른 예
class Car {
    func run() {
        print("자동차가 달립니다.")
    }
}

class SportsCar: Car {
    override func run() {      // 클래스를 채택한 후 기존 코드의 변경 없이 override해서 사용
        print("스포츠카가 빠르게 달립니다!")
    }
}

let myCar: Car = SportsCar()
myCar.run()  // "스포츠카가 빠르게 달립니다!"

 

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

          부모 클래스를 자식 클래스로 바꿔도 프로그램이 정상적으로 동작해야 한다

// 잘못된 예
class Car {
    func refuel() {
        print("주유소에서 기름을 넣습니다.")
    }
}

class ElectricCar: Car {
    override func refuel() {  // 전기차는 기름을 넣지 않음
        print("전기차는 충전이 필요합니다!")
    }
}

// 올바른 예
class Car {
    func move() {
        print("자동차가 이동합니다.")
    }
}

class ElectricCar: Car {
    override func move() {
        print("전기차가 전기로 이동합니다.")
    }
}

func drive(car: Car) {
    car.move()  // 부모 타입(Car)으로 ElectricCar를 대체해도 문제 없음!
}

let myTesla = ElectricCar()
drive(car: myTesla)  // "전기차가 전기로 이동합니다."

 

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

          클라이언트는 자신이 사용하지 않는 기능에 의존하면 안된다

// 잘못된 예
protocol Vehicle {
    func drive()
    func fly()  // 자동차가 날아야 한다고 강제됨
}

class Car: Vehicle {
    func drive() {
        print("자동차가 달립니다.")
    }
    
    func fly() {  // 자동차는 날지 못함
        fatalError("자동차는 날 수 없습니다.")
    }
}

// 올바른 예
protocol Drivable {
    func drive()
}

protocol Flyable {
    func fly()
}

class Car: Drivable {
    func drive() {
        print("자동차가 달립니다.")
    }
}

class Airplane: Flyable {
    func fly() {
        print("비행기가 하늘을 납니다.")
    }
}

 

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

         상위 모듈(큰 기능)은 하위 모듈(세부 기능)에 의존해서는 안 된다

         "구체적인 구현이 아닌, 추상적인 개념(인터페이스)에 의존해야 한다."

// 잘못된 예
class GasolineEngine {
    func start() {
        print("휘발유 엔진이 작동합니다.")
    }
}

class Car {
    let engine = GasolineEngine()  // 특정 엔진에 의존
    
    func startCar() {
        engine.start()
    }
}


// 올바른 예 : 추상화된 인터페이스에 의존
protocol Engine {
    func start()
}

class GasolineEngine: Engine {
    func start() {
        print("휘발유 엔진이 작동합니다.")
    }
}

class ElectricEngine: Engine {
    func start() {
        print("전기 엔진이 작동합니다.")
    }
}

class Car {
    let engine: Engine  // 특정 엔진이 아닌 "추상적인 엔진"에 의존

    init(engine: Engine) {
        self.engine = engine
    }

    func startCar() {
        engine.start()
    }
}

let myCar = Car(engine: ElectricEngine())  // 전기차로도 쉽게 교체 가능!
myCar.startCar()  // "전기 엔진이 작동합니다."