객체 지향 이론의 이해
객체 지향이란?
“객체를 기반으로 문제를 분석하고 해결하는 프로그래밍 패러다임”
프로그래밍 패러다임의 변화
- 1940’s : 기계중심 프로그래밍, 컴퓨터 = 기계(하드웨어) > 하드웨어에 명령하는 Assembly, 사람에 대한 이해 없어 생산성이 떨어졌음
- 1950’s : 사람 중심의 프로그래밍, 프로그램이란 명령어의 순차적 실행, 고급 언어 등장으로 기계 명령의 추상화, goto문의 남용으로 흐름이 복잡해져 유지보수 어려움 (Fortran, Cobol)
- 1960’s : 구조적 프로그래밍, 절차 유지 + 흐름의 구조화, 기능 중심, goto제거, 블럭구조 & 함수분해 & 제어흐름제한 도입, 데이터와 기능이 분리되어 책임이 불분명하고 변경시 연쇄적 전파의 문제 (C, Pascal)
- 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 구성요소
- 데이터(속성) : 객체의 상태, 변수 ex. 이름, 나이, 성별 등
- 행위(메서드) : 객체의 기능, 함수 ex. 공부하기(), 공격하기()
- 생성자(Constructor) : 처음 생성될 때 하는 일, 초기화(initialization),
__init__메서드
class를 만든다는 것의 진짜 의미
“단순히 코드를 짜는게 아님”
- 책임의 단위(Responsibility) : 흩어진 데이터와 행위를 하나로 묶어 책임 독립적 부품을 만듬
- 현실의 모델링(Modeling) : 복잡한 현실 세게의 문제를 해결하기 위해, 필요한 특징만 뽑아 소프트웨어로 재구성
- 새로운 타입(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)와 결합도에 따라 나뉨
- 합성 (Composition)
- 사람 & 심장 : 사람이 죽으면 심장도 멈춘다. (생명주기 동일)
- 집합 (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대 요소
“코드를 구조화하고, 변경에 유연한 구조를 만들기 위한 객체지향 사고의 토대”
- 추상화 (Abstraction) : 핵심만 남긴다
- 캡슐화 (Encapsulation) : 내부를 숨긴다
- 상속 (Inheritance) : 종류와 계층을 표현한다
- 다형성 (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)에 따른 추상화의 차이
“같은 사람이라도, 어떤 프로그램이냐에 따라 모습이 달라짐”
- Context : 병원 시스템
- class Patient : 혈액형, 키, 몸무게
- Context : 은행 시스템
- class Customer : 통장 잔고, 신용 등급
캡슐화(Encapsulation) : 보호와 책임
“관련된 속성과 기능을 하나의 캡슐(class)로 묶고, 외부로부터 데이터를 보호하며 접근을 제어”
- 데이터 노출 : obj.meney = 5000
- 캡슐화된 객체 : obj.deposit(1000)
왜 캡슐화를 해야 할까?
- 데이터 보호 : 정보 은닉 (Private)
- 무결성 보장 : 유효하지 않은 값 차단
#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 요약 정리
- SRP
- 핵심 메시지 : 하나는 하나만 해라
- 비유 : 맥가이버 칼 vs 전문가 도구들
- OCP
- 핵심 메시지 : 확장엔 열고 변경엔 닫아라
- 비유 : 벽면과 플러그
- LSP
- 핵심 메시지 : 자식은 부모를 대체할 수 있어야 한다
- 비유 : 자동차 엑셀
- ISP
- 핵심 메시지 : 안 쓰는 것을 강요하지 말라
- 비유 : 프린터 vs 복합기
- DIP
- 핵심 메시지 : 추상화에 의존하라
- 비유 : 건전지 규격
댓글남기기