CS

[CS] OOP란 무엇인가?

kimsangjunzzang 2025. 7. 21. 19:10

안녕하세요, 루피입니다.

 

오늘은 객체지향 프로그래밍 (OOP)에 대해 정리해보는 시간을 가져보겠습니다. 바로 시작합니다.


1. 객체 지향 프로그래밍의 등장 배경

객체 지향 프로그래밍의 등장 배경에 대해 간략하게 알아보겠습니다.

1) 절차 지향 프로그래밍의 한계

초기 프로그래밍은 절차지향 방식으로, 코드를 위에서부터 순차적으로 실행하는 방식이었습니다. 작은 규모의 프로그램에서는 효과적이었지만, 프로그램 규모가 커지면서 다음과 같은 문제들이 발생했습니다.

  • 스파게티 코드: 복잡한 알고리즘으로 인해 코드 흐름을 파악하기 어려움
  • 재사용성 부족: 동일한 로직이라도 매번 새로 작성해야 함
  • 유지보수의 어려움: 코드 수정 시 여러 곳을 동시에 변경해야 함

2) 구조적 프로그래밍의 등장과 한계

이를 해결하기 위해 구조적 프로그래밍이 등장했습니다. 함수 단위로 프로그램을 나누어 큰 문제를 작은 단위로 쪼개서 해결하는 Top-Down 방식이었습니다. 하지만 여전히 한계가 있었습니다.

  • 데이터 구조화 부족: 함수는 처리 부분만 구조화했을 뿐, 데이터 자체는 구조화하지 못함
  • 전역 변수 의존성: 독립적이지 못하고 의존적이며 전역 변수들을 사용하여 프로그램을 제어
  • 재사용성 문제: 의존성 때문에 코드를 일부만 가져다 쓸 수 없었음

3) 객체 지향 프로그래밍의 탄생

이러한 한계를 극복하기 위해 객체지향 프로그래밍이 등장했습니다. OOP는 Bottom-up 접근법을 채택하여, 작은 문제를 해결할 수 있는 객체들을 먼저 만들고 이를 조합해서 큰 문제를 해결하는 방식입니다. 장점으로는 다음과 같은 특징이 있습니다.

  • 독립성과 신뢰성: 객체 간 독립성과 신뢰성을 보장하여 재사용성이 높아짐
  • 현실 세계 모델링: 실제 세계의 개념을 프로그래밍으로 자연스럽게 표현
  • 유지보수 용이성: 레고 블럭 조립하듯이 컴포넌트를 유연하고 변경이 용이하게 만듦

2. 객체지향 프로그래밍 (OOP)이란?

OOP는 프로그램을 명령어와 함수의 순서가 아닌, 여러 개의 독립적인 단위, 즉 서로 통신하는 객체(Object) 들의 집합으로 보는 관점입니다. OOP에서 객체는 데이터와 그 데이터에 대한 작업을 수행하는 함수를 포함하는 단위로 각 객체는 자신만의 PropertiesMethods 을 가집니다. 이는 복잡한 문제를 작게 나누어 생각하게 도와주고, 코드의 재사용성과 유지보수를 쉽게 만들어 줍니다.


3. 클래스와 인스턴스 (Classes and Instances)

만약 사람이라는 객체를 여러 개 만들어야 한다면 어떻게 할까요? 이때 사람의 공통적인 특징을 정의한 설계도가 바로 클래스(Class) 입니다. 클래스는 객체가 가질 PropertiesMethods 를 정의합니다. 이 클래스를 바탕으로 메모리에 실체화된 객체를 Instance 라고 부릅니다. 예를 들어 Person이라는 클래스를 정의하고, kim과 Luffy이라는 두 개의 인스턴스를 만드는 코드 작성해보겠습니다.

class Person {
    // Properties
    var name: String
    var age : Int

    // Initializer
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    // Methods
    func eat() {
        print("\(name)가 밥을 맛있게 먹는다.")
    }
    func introduce() {
        print("\(name)가 자신의 \(age)년 인생을 말한다.")
    }
}

// instance 생성
let kim = Person(name: "김상준",age: 11)
let Luffy = Person(name: "루피",age: 12)

4. 상속 (Inheritance)

상속은 자식 클래스가 부모 클래스의 속성과 메서드를 물려받는 기능입니다. 이를 통해 코드를 재사용하고 클래스 간의 계층 구조를 만들 수 있습니다. ProfessorPerson의 특징을 모두 가지면서 자신만의 고유한 특징인 과목을 추가로 가집니다. Person 클래스를 상속받는 Professor 클래스입니다.

// '교수' 클래스 정의, Person을 상속
class Professor: Person {
    var subject: String

    init(name: String, age: Int, subject: String) {
        self.subject = subject
        // 부모 클래스의 생성자 호출
        super.init(name: name, age: age)
    }

    // 새로운 메서드 추가
    func teach() {
        print("\(name) 교수가 \(subject) 과목을 가르칩니다.")
    }

    // 부모 메서드 재정의 (Overriding)
    override func introduce() {
        print("안녕하세요, \(subject)를 가르치는 \(name) 교수입니다.")
    }
}

// Professor 인스턴스 생성
let realKim = Professor(name: "김교수", age: 99, subject: "iOS")
  • class Professor: Person : Person을 부모 클래스로 지정합니다.
  • super.init(name: name) : 부모 클래스의 생성자를 호출하여 name 속성을 초기화합니다.
  • override func introduce() : 부모의 introduce 메서드를 Professor에 맞게 재정의(Override) 합니다.

5. 캡슐화 (Encapsulation)

캡슐화는 객체의 속성과 메서드를 하나로 묶고, 외부에서 직접 접근하는 것을 제한하는 개념입니다. 이를 통해 객체 내부의 상태를 보호하고, 의도치 않은 변경을 막을 수 있습니다.

Swift에서는 접근 제어자(Access Control) 를 사용하여 캡슐화를 구현합니다.

  • private: 코드가 정의된 클래스 내부에서만 접근 가능합니다.
  • fileprivate: 같은 파일 내에서만 접근 가능합니다.
  • internal: 같은 모듈(앱 또는 프레임워크) 내에서 접근 가능합니다. (기본값)
  • public: 다른 모듈에서도 접근 가능합니다.

Student의 grade(성적)는 외부에서 마음대로 수정할 수 없도록 private으로 설정하고, 정해진 메서드를 통해서만 변경하도록 만듭니다.

class Student: Person {
    // 성적은 외부에서 직접 수정 불가
    private var grade: String = "F"

    // 성적을 확인하는 메서드 (읽기만 가능)
    func getGrade() -> String {
        return self.grade
    }

    // 시험을 통해 성적을 변경하는 메서드
    func takeExam(score: Int) {
        if score >= 90 {
            self.grade = "A"
        } else if score >= 80 {
            self.grade = "B"
        } else {
            self.grade = "C"
        }
        print("\(name)의 시험 결과: \(self.grade)")
    }
}

let choi = Student(name: "최지우", age: 99)
// choi.grade = "A+" // 컴파일 에러! private 속성에 직접 접근 불가

choi.takeExam(score: 95) // 출력: 최지우의 시험 결과: A
print("\(choi.name)의 최종 성적: \(choi.getGrade())") // 출력: 최지우의 최종 성적: A

 


6. 다형성 (Polymorphism)

다형성은 여러 가지 형태를 가질 수 있는 능력을 의미합니다. 즉, 같은 이름의 메서드라도 객체의 종류에 따라 다르게 동작하는 것을 말합니다. 상속과 메서드 재정의(Overriding)가 다형성을 구현하는 대표적인 방법입니다. Person 타입의 배열에 Person 인스턴스와 Professor, Student 인스턴스를 함께 담을 수 있습니다. introduce()라는 똑같은 메서드를 호출해도 각 인스턴스의 실제 클래스에 따라 재정의된 내용이 실행됩니다.

// Person 타입 배열에 여러 종류의 인스턴스를 담음
let people: [Person] = [
    Person(name: "루피", age: 19),
    Professor(name: "조로", age:21, subject: "자료구조"),
    Student(name: "상디", age: 21)
]

// 배열을 순회하며 각 객체의 introduce 메서드 호출
for person in people {
    person.introduce()
}

실행 결과

루피가 자신의 99년 인생을 말한다.
안녕하세요, 자료구조를 가르치는 조로 교수입니다.
상디가 자신의 99년 인생을 말한다.

이처럼 people 배열은 모든 요소를 Person으로 보고 있지만, introduce()를 실행하는 시점에는 각 객체의 실제 형태(Professor, Student)에 맞는 메서드가 호출됩니다. 이것이 바로 다형성의 핵심입니다.


7. 추상화(Abstraction)

추상화는 객체의 복잡한 내부 구현은 숨기고, 실제로 필요한 핵심 기능만 외부에 노출하는 것을 말합니다.

Swift에서는 주로 프로토콜(Protocol) 을 통해 추상화를 구현합니다. 프로토콜은 특정 작업을 수행하기 위해 필요한 메서드나 속성의 목록을 정의한 설계도(청사진) 입니다. "무엇을 해야 하는지(what)"만 정의하고, "어떻게 할지(how)"는 알려주지 않습니다.

// 프로토콜 정의 (추상화)
protocol Teachable {
    func teach()
    func prepareLesson()
}

// 프로토콜 채택 및 구현 (구체화)
class Professor: Person, Teachable {
    var subject: String

    init(name: String, age: Int, subject: String) {
        self.subject = subject
        super.init(name: name, age: age)
    }

    // 프로토콜 요구사항 구현
    func teach() {
        print("\(name) 교수가 \(subject)를 가르칩니다.")
    }

    func prepareLesson() {
        print("\(name) 교수가 \(subject) 수업을 준비합니다.")
    }
}

// 사용자는 구체적 구현을 몰라도 프로토콜 인터페이스만 알면 됨
let instructor: Teachable = Professor(name: "김교수", age: 45, subject: "Swift")
instructor.teach() // 추상화된 인터페이스 사용

CarPerson이 어떻게 움직이는지에 대한 내부 구현은 다르지만, 외부에서는 두 객체 모두 Movable이라는 공통된 기능을 수행할 수 있다는 사실만 알면 됩니다.


8. 객체지향 설계의 5원칙 (SOLID)

객체지향적으로 설계하기 위해서는 SOLID라고 불리는 다섯 가지 원칙을 따라야 합니다.이 원칙들을 준수하면 유지보수가 쉽고, 확장 가능하며, 재사용성이 높은 소프트웨어를 만들 수 있습니다.

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

하나의 클래스는 하나의 책임만 가져야 한다는 원칙입니다. 클래스를 변경하는 이유가 단 하나여야 합니다.

// ❌ 잘못된 예: 여러 책임을 가진 클래스
class User {
    var name: String
    var email: String

    func save() { }
    func sendEmail() { }
    func validateEmail() { }
}

// ✅ 올바른 예: 책임을 분리한 클래스들
class User {
    var name: String
    var email: String
}

class UserRepository {
    func save(user: User) { }
}

class EmailService {
    func sendEmail(to: String) { }
}

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

확장에는 열려있고, 변경에는 닫혀있어야 한다는 원칙입니다. 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있어야 합니다.

protocol Shape {
    func calculateArea() -> Double
}

class Rectangle: Shape {
    let width: Double
    let height: Double

    func calculateArea() -> Double {
        return width * height
    }
}

class Circle: Shape {
    let radius: Double

    func calculateArea() -> Double {
        return 3.14 * radius * radius
    }
}

// 새로운 도형을 추가해도 기존 코드를 수정할 필요 없음
class Triangle: Shape {
    let base: Double
    let height: Double

    func calculateArea() -> Double {
        return (base * height) / 2
    }
}

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

자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 원칙입니다. 상속 관계에서 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용해도 프로그램이 정상적으로 동작해야 합니다.

class Bird {
    func fly() {
        print("새가 날아갑니다")
    }
}

class Eagle: Bird {
    override func fly() {
        print("독수리가 높이 날아갑니다")
    }
}

// 부모 타입으로 선언했지만 자식 인스턴스로 대체 가능
let bird: Bird = Eagle()
bird.fly() // "독수리가 높이 날아갑니다"

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

클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다는 원칙입니다. 큰 인터페이스를 작은 단위로 분리해야 합니다.

// ❌ 잘못된 예: 너무 큰 인터페이스
protocol WorkerInterface {
    func work()
    func eat()
    func sleep()
}

// ✅ 올바른 예: 분리된 인터페이스들
protocol Workable {
    func work()
}

protocol Eatable {
    func eat()
}

protocol Sleepable {
    func sleep()
}

class Human: Workable, Eatable, Sleepable {
    func work() { print("일합니다") }
    func eat() { print("먹습니다") }
    func sleep() { print("잠을 잡니다") }
}

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

고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다는 원칙입니다.

// ❌ 잘못된 예: 고수준이 저수준에 의존
class EmailService {
    func sendEmail() { }
}

class NotificationManager {
    private let emailService = EmailService() // 구체 클래스에 직접 의존

    func sendNotification() {
        emailService.sendEmail()
    }
}

// ✅ 올바른 예: 추상화에 의존
protocol NotificationService {
    func send()
}

class EmailNotification: NotificationService {
    func send() { print("이메일로 알림 전송") }
}

class SMSNotification: NotificationService {
    func send() { print("SMS로 알림 전송") }
}

class NotificationManager {
    private let service: NotificationService

    init(service: NotificationService) {
        self.service = service // 추상화에 의존
    }

    func sendNotification() {
        service.send()
    }
}

Ref

https://developer.mozilla.org/ko/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object-oriented_programming

 

객체 지향 프로그래밍 - Web 개발 학습하기 | MDN

객체 지향 프로그래밍(OOP)은 Java 및 C++를 비롯한 많은 프로그래밍 언어의 기본이 되는 프로그래밍 패러다임입니다. 이 기사에서는 OOP의 기본 개념에 대한 개요를 제공합니다. 클래스와 인스턴스

developer.mozilla.org

https://doc.nette.org/en/introduction-to-object-oriented-programming

 

Introduction to Object-Oriented Programming

The term “OOP” stands for Object-Oriented Programming, which is a way to organize and structure code. OOP allows us to view a program as a collection of objects that communicate with each other, rather than a sequence of commands and functions.

doc.nette.org


오늘도 화이팅입니다!