9 분 소요

객체 지향이란?

“객체를 기반으로 문제를 분석하고 해결하는 프로그래밍 패러다임”

프로그래밍 패러다임의 변화

  1. 1940’s : 기계중심 프로그래밍, 컴퓨터 = 기계(하드웨어) > 하드웨어에 명령하는 Assembly, 사람에 대한 이해 없어 생산성이 떨어졌음
  2. 1950’s : 사람 중심의 프로그래밍, 프로그램이란 명령어의 순차적 실행, 고급 언어 등장으로 기계 명령의 추상화, goto문의 남용으로 흐름이 복잡해져 유지보수 어려움 (Fortran, Cobol)
  3. 1960’s : 구조적 프로그래밍, 절차 유지 + 흐름의 구조화, 기능 중심, goto제거, 블럭구조 & 함수분해 & 제어흐름제한 도입, 데이터와 기능이 분리되어 책임이 불분명하고 변경시 연쇄적 전파의 문제 (C, Pascal)
  4. 1980’s : 객체지향 프로그래밍, 데이터와 행위를 하나의 단위로 모델링, 프로그램을 객체간 상호작용으로 정의,객체와 책임의 캡슐화 (Simula, Smaltalk, C++, C#, Java, Python)
  • 위 목록에서 2~3번을 절차지향 이라 부름
  • 소프트웨어 규모/복잡도 증가로 생산성과 유지보수성의 문제가 발생해옴

절차지향 vs 객체지향

절차지향

  • 개념 : 함수를 중심으로 프로그램을 절차적으로 구성
  • 핵심사고 : 알고리즘(How to do)을 순차적으로 표현
  • 모델링 : 프로세스/작업 흐름 중심의 모델링
  • 데이터와 기능 관계 : 데이터와 기능(함수)이 분리 (데이터는 함수 외부에 존재하며 여러 함수에서 접근 가능)
  • 재사용 단위 : 함수 단위 재사용
  • 변경/유지보수 : 전역 데이터 수정 -> 전체 영향 (대규모 프로젝트에 취약)
  • 장점 : 단순하고 직관적, 실행속도 빠름 (소규모 프로그램에 적합)
  • 단점 : 코드 규모가 커질 수록 관리가 어려움. 데이터 무결성 보장 어려우며 코드 중복 발생
  • 구현 언어 : C, Pascal, Fotran (Python 등 현대 언어에서도 절체 스타일 구현은 가능)

객체지향

  • 개념 : 데이터와 기능을 하나의 객체로 캡슐화 하여 현실 세계를 모델링
  • 핵심사고 : 책임 중심(What todo) (객체 간 메시지 전달과 책임 분배 중심)
  • 모델링 : 현실 세계의 개체(Entity)를 객체로 직접 매핑하여 모델링
  • 데이터와 기능 관계 : 데이터와 행위가 하나의 객체로 캡슐화되어 외부로부터 보호됨
  • 재사용 단위 : 클래스/객체 단위(상속, 합성, 다형성(Composition) 활용)
  • 변경/유지보수 : 캡슐화와 정보은닉으로 변경 영향 범위 최소화(대규모 시스템에 유리)
  • 장점 : 코드 재사용성/확장성/유지보수성 높음 (대규모, 복잡한 시스템에 적합)
  • 단점 : 초기 셀계 및 학습 비용이 높음. 런타임 오버헤드 약간 존재
  • 구현 언어 : Simula, Samltalk, C++, Java, C#, Python, Ruby 등

왜 객체지향일까?

  • 절차지향의 경우 데이터와 기능이 분리되어 있음 (데이터가 흩어져서 관리 불가)

    객체지향으로 데이터와 기능을 하나의 객체로 묶어 문제 해결

객체지향의 주요 요소

class와 object

  • class란? 객체를 만들어내기 위한 설계도이자 틀 역할
  • 객체(object)란? 클래스를 통해 생성된 생산물

    비유하자면 class는 빵틀, object는 빵틀로 만든 빵의 역할 빵틀 자체는 직접 먹거나 다양화 하기 어렵지만, 빵은 직접 먹고 가공할 수 있으며 넣는 재료에 따라 다양화도 가능함

class 구성요소

  1. 데이터(속성) : 객체의 상태, 변수 ex. 이름, 나이, 성별 등
  2. 행위(메서드) : 객체의 기능, 함수 ex. 공부하기(), 공격하기()
  3. 생성자(Constructor) : 처음 생성될 때 하는 일, 초기화(initialization), __init__ 메서드

class를 만든다는 것의 진짜 의미

“단순히 코드를 짜는게 아님”

  1. 책임의 단위(Responsibility) : 흩어진 데이터와 행위를 하나로 묶어 책임 독립적 부품을 만듬
  2. 현실의 모델링(Modeling) : 복잡한 현실 세게의 문제를 해결하기 위해, 필요한 특징만 뽑아 소프트웨어로 재구성
  3. 새로운 타입(Custom Type) : int, str 만으로는 부족해 세상에 없던 나만의 자료형을 참조

class와 object의 관계(Relation)

설계도와 제품, 1:N 관계

  • 클래스는 하나지만, 객체는 무수히 만들 수 있으며, 서로 독립적임

메모리 속의 진실

  • 같은 class 출신이라도, 실제 데이터 위치는 다름

객체간 대화(Message Passing)

  • 객체지향에서 함수 호출을 “메세지를 보낸다” 라고 부름
  • 직접 데이터를 꺼내는 것이 아니라, 정보를 달라고 메스드를 통해 요청(ex. a.get_info())

객체간 구조적 관계 : Is-a와 Has-a (상속이냐, 포함이냐)

객체는 구조적 관계를 맺음

“객체 간의 관계란 객체들이 어떤 구조로 연결되어 있는지를 정의하는 것

  • Is-a 관계 : “~은 ~의 일종이다”(상속/계층 관계)
  • Has-a 관계 : “~은 ~을 가진다”(포함/구성 관계)

Is-a 관계 : 상속(Inheritance)

“A is a B” (A는 B의 일종이다)

상속의 의미

  • 부모(A)는 일반적(General)인 개념
  • 자식(B)는 구체적(Specific)인 개념
  • 자식은 부모의 속성과 행동을 그대로 물려받음(재사용)
  • 부모타입을 기대하는 자리에 자식 객체를 넣어도 문제업싱 동작해야 함(LSP 원칙)
  • 상속의 목적 : 객체의 종류(분류) 표현 (부가적 효과 : 코드 재사용)

파이썬으로 보는 Is-a

#1. 부모 클래스
class Person:
  def eat(self):
    print("밥을 먹었습니다.")

#2. 자식 클래스
class Student(Person):
  def study(self):
    print("공부를 했습니다.")

#3. 사용
s = Student()
s.eat()      # 부모에게 물려받은 기능 사용 가능
s.study()    # 자신의 기능

잘못된 상속의 예 (Bad Inheritance)

말이 된다고 무조건 상속하면 괴물 객체가 탄생한다.

Case 1 : 경찰 is a 총

  • 경찰이 총을 쓰고 싶다고 상속 받으면?

    경찰.발사()가 가능해짐 경찰은 총이 아니다. 총을 가져야(Has-a) 함

Case 2 : 팽귄 is a 새

  • 생물학적으로는 맞지만 프로그래밍에선?

    부모는 fly() 기능이 있음 팽귄은 날지 못함. 상속받으면 문제 발생 행위(Behavior)가 일치해야 상속 가능

Has-a 관계 : 포함 (Composition)

“A has a B” (A는 B를 가지고 있다.)

포함의 의미

  • 전체(Whole)와 부품(Part)의 관계
  • 상속보다 유연한 결합 (부품 교체 가능)
  • 내 기능을 내가 직접하지 않고, 부품에게 시킴 (위임)
  • 포함의 목적 : 객체를 조립하고, 역할을 분리하여 변경에 유연한 구조를 만드는 것

파이썬 코드로 보는 Has-a

#1. 부품 클래스
class Engine:
  def start(self):
    print("엔진 on!")

#2. 전체 클래스
class Car:
  def __init__(self):
    self.engine = Engine()  #Car has a Engine

  def drive(self):
    self.engine.start()     # 위임. 직접 돌리는게 아니라 엔진에게 시킴
    print("출발!")

#3. 사용
my_car = Car()
my_car.drive()

Has-a의 두가지 얼굴

“생명주기(Lifecycle)와 결합도에 따라 나뉨

  1. 합성 (Composition)
    • 사람 & 심장 : 사람이 죽으면 심장도 멈춘다. (생명주기 동일)
  2. 집합 (Aggregation)
    • 학교 & 학생 : 학교가 사라져도 학생은 살아있다. (생명주기 독립)

한번에 비교하기 (Is-a vs Has-a)

Is-a (상속)

  • 의미 : A는 B의 일종이다 (kind of)
  • 구현 방식 : Class A(B) (상속 문법 사용)
  • 결합도 : 강함 (부모 변경 시 자식에게 연쇄적 영향
  • 관계 : 수직적 계층 구조

Has-a (포함)

  • 의미 : A는 B를 부품으로 가진다 (part of)
  • 구현 방식 : self.b = B() (멤버 변수로 객체 소유) or self.b = b (외부에서 전달받음)
  • 결합도 : 유연함 (부품 교체가 쉬움)
  • 관계 : 수평적 (조립 구조)

상속과 포함의 권장 설계 원칙

“상속(Is-a)보다는 포함(Has-a)을 우선하라”

  • 상속의 문제점
    • 부모 구현 의존 : 부모 구현에 강하게 의존. 부모 변경에 취약
    • 유연성 부족 : 실행 중에 부모를 바꿀 수 없음 (정적)
    • 클래스 폭발 : 기능 조합을 위해 수많은 클래스가 생기게 됨
  • 포함의 장점
    • 블랙박스 재사용 : 내부를 몰라도 인터페이스만 알면 됨
    • 런타임 교체 : 실행 중에 부품(객체)을 갈아끼울 수 있음
    • 단순성 : 클래스 수를 줄이고 관계를 단순화 함

**객체지향의 핵심은 “거대한 상속 계층 만들기”가 아니라, “적절한 책임을 나누고 조립하기”이다.

객체지향의 4대 요소

“코드를 구조화하고, 변경에 유연한 구조를 만들기 위한 객체지향 사고의 토대”

  1. 추상화 (Abstraction) : 핵심만 남긴다
  2. 캡슐화 (Encapsulation) : 내부를 숨긴다
  3. 상속 (Inheritance) : 종류와 계층을 표현한다
  4. 다형성 (Polymorphism) : 같은 메시지, 다른 동작

1. 추상화 (Abstraction)

“현실의 복잡함에서 공통적이고 본질적인 특징만 추출하여 불필요한 세부 사항은 제거하고 핵심에 집중”

  • ex. 복잡한 현실 지도에서 지하철 노선도만 추출해 내는 방식
  • 현실 세계의 개체를 그대로 옮기는 것이 아니라, 프로그램에 필요한 핵심 역할과 책임만 뽑아 객체로 표현하는 것

2. 캡슐화 (Encapsulation)

“데이터와 기능을 하나의 캡슐로 묶어 외부로부터 데이터를 보호하고 접근을 제어”

  • ex. 안전 금고와 같은 역할
  • 데이터는 금고에 숨기고 공개된 메서드를 통해서만 접근 가능. 외부에서 마이너스로 만들 수 없음

  • bank_account.py 예시
    class Account:
    def __init__(self):
      self.__moeny = 0    #__변수명 : 비공개 변수
    
    def deposit(self, amount):
      if amount>0:
        self.__money += amount
    

3. 상속 (Inheritance)

“객체의 종류와 분류를 표현하기 위한 설계 방법, 부모의 속성과 기능을 물려받아 재사용 가능”

  • 부모 특징이 움직인다와 먹는다 일 경우, 자식은 움직인다와 먹는다를 기본으로 가지고 추가 기능으로 확장됨
  • 주의! Is-a 관계가 확실한 때만 사용. 무분별한 상속은 코드를 망침

4. 다형성 (Polymorphism)

“동일한 메시지에 다른 동작 호출. 같은 메서드를 호출해도 객체마다 다르게 동작”

  • ex. 캐릭터가 attack() 할 경우, 전사는 칼을 휘두르고, 마법사는 화염을 던짐

좋은 객체의 조건 : 추상화와 캡슐화

“복잡함은 덜어내고, 중요한 것은 감춰라”

추상화(Abstraction) : 모델링의 미학

“복잡한 객체에서 공통적이고 본질적인 속성과 기능만 추출할 것” (불필요한 세부 사항은 제거하고 핵심 기능에 집중)

이 과정에서 필터링의 기준은 문맥

문맥(Context)에 따른 추상화의 차이

“같은 사람이라도, 어떤 프로그램이냐에 따라 모습이 달라짐”

  1. Context : 병원 시스템
    • class Patient : 혈액형, 키, 몸무게
  2. Context : 은행 시스템
    • class Customer : 통장 잔고, 신용 등급

캡슐화(Encapsulation) : 보호와 책임

“관련된 속성과 기능을 하나의 캡슐(class)로 묶고, 외부로부터 데이터를 보호하며 접근을 제어”

  • 데이터 노출 : obj.meney = 5000
  • 캡슐화된 객체 : obj.deposit(1000)

왜 캡슐화를 해야 할까?

  1. 데이터 보호 : 정보 은닉 (Private)
  2. 무결성 보장 : 유효하지 않은 값 차단
#1. 데이터 숨기기 (변수명 앞 __)
def __init__(self):
  self.__money =0

#2. 메서드를 통해서만 접근 허용 (규칙 적용)
def deposit(self, amount):
  if amount < 0:
    print("마이너스 금액 입금 불가")
    return
  self._money += amount

유연한 설계의 기술 : 다형성(Polymorphism)

다형성이란?

“하나의 이름(Interface)으로 여러 가지 형태의 동작을 수행하는 능력”

  • 같은 메서드를 호출해도 다른 동작과 다른 결과가 나오는 것

핵심 메커니즘 : 오버라이딩 (Overriding)

“부모가 물려준 기능을 내 방식대로 재정의 하는 것”

  • 일반 케릭터 부모의 경우 attack()print("주먹 치기")일 경우 상속 및 오버라이딩을 통해 마법사 자녀는 print("파이어볼!")을 출력하게 수정할 수 있음

절차지향 vs 객체지향 (코드의 변화)

“if-else 지옥에서 탈출하여 다형성의 천국으로!”

  • 절차지향의 경우 : 새 직업이 나오면 코드를 다 고쳐야 함
    def attack(character):
      if character.type == "Warrior":
        print("대검 배기")
      elif character.type = "Mage":
        print("파이어볼")
      elif character.type = "Archer":
        print("화살")
    # 직업잋 ㅜ가될 때마다 if문 계속 추가됨
    
  • 객체지향의 경우 : 코드를 수정할 필요 없음
    def attack(character):
      #다형성 : 캐릭터가 누구든 알아서 공격
      character.attack()
    # 새로운 직업이 나와도 이 함수는 건드릴 필요 없이 해당 직업의 클래스만 잘 만들면 됨
    

다형성의 진짜 효과는 확장성

“기존 코드를 건드리지 않고, 새로운 기능을 추가할 수 있음(OCP : 개방-폐쇄 원칙)”

  • Plug & Play : USB 포트 처럼 규격(메서드)만 맞으면 무엇이든 꽂아서 쓸 수 있음
  • 유연한 유지보수 : 기능을 추가할 때 메인 로직을 수정할 필요 없으므로 버그 감소

파이썬의 다형성 : Duck Typing

“파이썬은 상속 없이도 다형성이 가능”

#상속을 받지 않음
class Dog:
  def speak(self): print("멍멍!")

class Cat:
  def speak(self): print("야옹~")

class Robot:
  def speak(self): print("삐리삐리")

#객체들이 서로 달라도 'speak'만 할 줄 알면 OK
objs = [Dog(), Cat(), Robot()]

for o in objs:
  o.speak()
  • Java vs Python
    • Java : Animal이라는 부모 클래스를 상속받아야만 다형성 가능 (엄격)
    • Python : 상속 관계 없어도 메서드 이름만 같으면 실행 가능 (유연)
  • Duck Typing : 오리처럼 걷고 꽥꽥거리면, 그것은 다 오리다.

객체지향 설계 5원칙, SOLID

“유연하고 확장 가능한 SW개발을 위한 객체지향 설계 원칙”

왜 설계 원칙이 필요할까?

  • 문법을 안다곳 ㅗ설을 잘 쓰는것은 아님
  • 객체지향의 도구를 어떻게 써야 하는지 알려주는 기준이 설계 원칙

  • 나쁜 설계 (Bad Design)
    • 하나 고치면 다 고장남 (경직성)
    • 어디가 문제인지 모름 (취약성)
    • 재사용 불가능 (부동성)
  • 좋은 설계 (Clean Architecture)
    • 변경에 유연함 (유연성)
    • 수정해도 쉽게 깨지지 않음 (안전성)
    • 재사용 가능한 구조 (재사용성)

1. SRP : 단일 책임의 원칙

Single Resposibility Principle

  • 하나의 클래스는 하나의 책임(목적)만 가져야 한다.
  • 하나의 클래스는 하나의 이유로만 변경되어야 한다.

  • 좋은 구존는 맥가이버 칼이 아닌 전문가용 도구들이다.

    모든걸 집어 넣는게 아니라, 각자 자기 일만 잘하자

2. OCP : 개방-폐쇄 원칙

Open/Closed Principle

  • 확장에는 열려있고, 변경에는 닫혀있어야 한다.

  • 새로운 가전제품을 쓰고 싶을 땐, 벽을 뜯어내지 않고 플러그만 꽂으면 됨 (Duck Typing)

3. LSP : 리스코프 치환 원칙

Liskov Substitution Principle

  • 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다.

  • 부모가 할 수 있는 일은 자식도 할 수 있어야 함. 부모가 차를 운전하면 앞으로 가지만 자식이 차를 운전하면 뒤로 간다면? LSP 위반

4. ISP : 인터페이스 분리 원칙

Interface Segregation Principle

  • 내가 사용하지 않는 기능에 의존하게 만들지 마라.

  • 나는 인쇄만 하면 되는데, 팩스까지 가능한 복합기를 구현할 필요는 없음
  • 각각 필요한 기능(인터페이스)만 골라 쓰면 됨

5. DIP : 의존 역전 원칙

Dependency Inversion Principle

  • 구체적인 것이 아니라 추상적인 것(역할)에 의존하라.

  • 로봇 장난감의 건전지 규격이 AA라고 했을 때, 로봇은 ‘에너자이저’에 의존하지 않고 ‘AA 규격’에만 의존하므로 무엇이든 끼울 수 있음\

SOLID 요약 정리

  1. SRP
    • 핵심 메시지 : 하나는 하나만 해라
    • 비유 : 맥가이버 칼 vs 전문가 도구들
  2. OCP
    • 핵심 메시지 : 확장엔 열고 변경엔 닫아라
    • 비유 : 벽면과 플러그
  3. LSP
    • 핵심 메시지 : 자식은 부모를 대체할 수 있어야 한다
    • 비유 : 자동차 엑셀
  4. ISP
    • 핵심 메시지 : 안 쓰는 것을 강요하지 말라
    • 비유 : 프린터 vs 복합기
  5. DIP
    • 핵심 메시지 : 추상화에 의존하라
    • 비유 : 건전지 규격

댓글남기기