
객체지향 프로그래밍이란?
객체를 먼저 만들고 객체들을 조합해 프로그램을 완성하는 방법론
위키피디아에서는 객체지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉 '객체'들의 모임으로 파악하고자 하는것이라 나와있다.
객체지향 프로그래밍은 프로그램의 일부분에 해당하는 객체(object)를 먼저 만들고, 이렇게 만들어진 객체들을 조합해서 전체 프로그램을 완성하는 방법론을 의미한다.
객체(object)란 무엇인가?
객체는 자신 고유의 속성(attribute)를 가지고, 행위(behavior)를 가진 것을 말한다.
객체가 되기 위해서는 객체 내부에서 데이터(속성)을 갖고, 이 데이터를 활용해 행위를 할 수 있어야 한다.
우리가 살고 있는 현실 세계에 존재하는 모든 것들을 객체로 표현하는 것이 객체지향이다.
객체지향의 장점
객체지향의 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들어준다.
코드의 변경을 최소화 할 수 있고, 유지보수가 용이해진다.
객체 단위로 프로그래밍을 하기에 코드의 재사용성이 높아지고, 이에 따라 간결한 코드 작성이 가능해진다.
객체지향 프로그래밍의 4가지 특징
캡슐화 ( Encapsulation )
캡슐화는 변수(데이터)와 메서드(기능)을 클래스(캡슐)로 묶어 외부에서 마음대로 접근하지 못하도록 보호하는 것을 말한다. 외부에서 알면 안되는 것들, 외부에서 알 필요가 없는 것들을 숨기고, 꼭 필요한 부분만 공개하는 것을 의미한다.
캡슐화를 통해 외부에는 꼭 필요한 부분만 노출함으로서 클래스 내 변수와 메서드를 보호할 수 있기에 캡슐화를 한다.
추상화 ( Abstraction )
객체지향에서 추상화는 핵심적이거나 공통되는 속성과 기능을 추출하여 정의하는 것을 의미한다. 객체들은 클래스를 토대로 생성되는데, 이때 객체들이 어떤 공통된 속성이나 기능을 갖고 있어야 한다고 정의하는 것이다.
클래스를 상속하는 방법으로 추상화를 실현한다.
언어마다 다르지만 추상 클래스(abstract class, c++)나 인터페이스(interface, java)가 있는 경우가 있다.
이들을 상속하거나 구현하는 방법으로 추상화를 한다.
상속 ( Inheritance )
상속은 새로운 클래스가 기존 클래시의 자료와 연산을 이용 할 수 있게 하는 기능이다.
상속을 받는 클래스를 대게 자식 클래스라고 부르며, 상속하는 기존 클래스를 부모 클래스라고 한다.
상속을 통해 클래스를 만들면 공통되는 코드를 하나의 클래스로 관리할 수 있기에 코드의 재사용성이 높아지고 유지보수가 편리해진다.
자바는 하나의 클래스를 상속받을 수 있다. ( 단일상속 )
C++에서는 여러 클래스를 상속받을 수 있는데 이를 다중 상속이라 한다.
다중상속은 클래스의 상속관계에 혼란을 줄 수 있기에 가급적 피하는 것이 좋다.
다형성 ( Polymorphism )
다형성은 하나의 요소에 여러 개념을 넣어 놓는 것을 말한다.
프로그래밍에서 다형성은 하나의 객체에 여러 타입을 대입할 수 있는 성절을 뜻한다.
다형성은 오버라이딩(같은 이름의 메서드가 여러 클래스에서 다른 기능을 하는 것)이나
오버로딩(같은 이름의 메서드가 인자의 개수나 자료형에 따라 다른 기능을 하는 것)으로 구현한다.
객체지향 프로그래밍 SOLID 원칙
S. 단일 책임 원칙 ( SRP, Single Responsibility Principle )
SRP = 한 클래스는 하나의 책임만 가져야 한다.
객체지향 프로그래밍에서 단일 책임 원칙이란 모든 클래스는 하나의 책임만 갖고, 클래스는 그 책임을 완전히 캡슐화해야하는 것을 말한다.
클래스를 변경하는 이유는 오직 하나여야 한다.
class Animal:
def __init__(self, type: str):
self.type = type
def say(self):
if self.type == "dog":
print("멍멍")
elif self.type == "cat":
print("미야옹")
...
위 파이썬 코드는 SRP 원칙을 위반했다.
say라는 메서드는 type이 "dog"면 '멍멍'을, type이 "cat" 이면 "미야옹"을 출력한다.
기능이 분리가 되어 있지 않고 하나의 메서드가 2개의 기능을 갖고 있다.
그럼 이를 옳게 고치면 어떻게 작성할까?
from abc import ABC, abstractmethod # 추상 베이스 클래스
class Animal(ABC):
@abstractmethod
def say(self):
pass
class Dog(Animal):
def __init__(self):
pass
def say(self):
print("멍멍")
class Cat(Animal):
def __init__(self):
pass
def say(self):
print("미야옹")
dog = Dog()
dog.say()
cat = Cat()
cat.say()
Animal 클래스라는 추상 클래스를 만들고, Dog과 Cat 클래스가 Animal 클래스를 상속받게 된다.
Dog은 멍멍을 출력하고, Cat은 미야옹을 출력한다.
하나의 클래스가 하나의 기능을 갖도록 수정했다.
SRP 원칙을 지켜 분리를 해두면 나중에 변경이 필요할 때 수정 대상이 명확해진다.
정리하면, 클래스는 하나의 기능(관심사)를 갖고, 클래스는 하나의 기능을 수행하는데 집중되어야 한다.
O. 개방 - 폐쇄 원칙 ( OCP, Open-Closed Principle )
OCP = 소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다.
쉽게 말하면 기존의 코드를 변경하지 않으면서, 새로운 기능을 추가할 수 있어야 한다는 의미이다.
새로운 변경 사항이 발생했을 때 코드를 추가하면 손쉽게 소프트웨어의 기능을 확장할 수 있다.
그러나 객체를 직접적으로 수정하는건 제한해야 한다는 것이다. (= 변경에 닫혀 있다는 의미)
객체를 직접적으로 수정하게 되면 유지보수 비용이 증가한다.
객체지향 프로그래밍 4대 요소 중 하나인 추상화를 의미한다.
class Animal:
def __init__(self, type: str):
self.type = type
def say(self):
if self.type == "dog":
print("멍멍")
elif self.type == "cat":
print("미야옹")
...
SRP에서 사용한 예제이다.
여기에서 뱀이라는 동물을 추가하려면, say 메서드에 if문을 추가해줘야 할 것이다.
이는 객체에 직접적인 수정이 발생한다.
우리가 아까 SRP를 적용한 코드로 수정해보자.
from abc import ABC, abstractmethod # 추상 베이스 클래스
class Animal(ABC):
@abstractmethod
def say(self):
pass
class Dog(Animal):
def __init__(self):
pass
def say(self):
print("멍멍")
class Cat(Animal):
def __init__(self):
pass
def say(self):
print("미야옹")
# 뱀 추가
class Snake(Animal):
def __init__(self):
pass
def say(self):
print("스스슥")
snake = Snake()
snake.say()
이렇게 기존 객체를 수정하지 않고, 새로운 Snake 라는 클래스를 만들어 해결했다.
코드를 작성할 때는 최대한 기존 객체를 수정하지 않는 선에서 새로운 기능을 추가할 수 있도록 해야 한다.
L. 리스코프 치환 원칙 ( LSP, Liskov Substitution Principle )
LSP = 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서, 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
리스코프 치환 원칙은 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 것을 뜻한다.
즉 자식 객체는 언제든 부모 객체로 교체할 수 있어야 한다.
from abc import ABC, abstractmethod # 추상 베이스 클래스
class Animal(ABC):
@abstractmethod
def say(self):
pass
class Dog(Animal):
def __init__(self):
pass
def say(self):
print("멍멍")
class Cat(Animal):
def __init__(self):
pass
def say(self):
print("미야옹")
class Person:
def __init__(self, pet: Animal):
self.pet = pet
def set_pet(self,pet: Animal):
self.pet = pet
def breed(self):
self.pet.say()
hyunju = Person(Dog())
hyunju.breed()
hyunju.set_pet(Cat())
hyunju.breed()
위 코드에서 Person의 생성자는 Animal을 필요로 한다.
저 자리에 같은 부모를 갖는 자식 클래스인 Dog와 Cat을 넣어도 잘 작동해야 한다는 것을 말한다.
I. 인터페이스 분리 원칙 ( ISP, Interface Segregation Principle )
ISP = 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
SRP가 클래스의 단일책임이라면, ISP는 인터페이스의 단일 책임을 의미한다.
ISP는 인터페이스를 사용하는 클라이언트를 기준으로, 클라이언트의 목적과 관심에 맞는 인터페이스 만을 제공하는 것이 목표이다.
인터페이스를 클라이언트에 따라 분리해 변경에 의한 영향을 제어하는 것을 인터페이스 분리 원칙이라 부른다.
큰 덩어리의 인터페이스들을 객체의 필요에 따라 구체적이고 작은 단위로 분리시킴으로써 클라이언트들이 꼭 필요한 메서드만을 이용할 수 있게 한다.
P. 의존관계 역전 원칙 ( DIP, Dependency Inversion Principle )
DIP = 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.
의존관계 역전 원칙은 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다는 것을 의미하는 원칙이다.
from abc import ABC, abstractmethod # 추상 베이스 클래스
class Animal(ABC):
@abstractmethod
def say(self):
pass
class Dog(Animal):
def __init__(self):
pass
def say(self):
print("멍멍")
class Cat(Animal):
def __init__(self):
pass
def say(self):
print("미야옹")
class Person:
def __init__(self, pet: Animal):
self.pet = pet
def set_pet(self,pet: Animal):
self.pet = pet
def breed(self):
self.pet.say()
hyunju = Person(Dog())
hyunju.breed()
hyunju.set_pet(Cat())
hyunju.breed()
해당 코드에서, 만약 Person이 Animal에 의존하지 않고 저수준의 Dog나 Cat에 의존하고 있었다면, 그것이 DIP에 위배되는 경우가 된다.
이 코드에서는 Dog과 Cat을 추상화하기 위한 Animal 클래스(추상 클래스 혹은 인터페이스)를 만들어 Person이 Animal에 의존하게 만들었다.
References
- w3schools - Java OOP
- mdn web docs - Object-Oriented Programming
- spiceworks - What Is OOP?
- 짜이한 - 객체 지향 프로그래밍(OOP)의 개념과 4가지 특징
- 위키피디아 - 객체 지향 프로그래밍
- 망나니개발자 - 객체지향 프로그래밍의 5가지 설계 원칙
'Infra > Architecture' 카테고리의 다른 글
모놀리식과 마이크로서비스 아키텍쳐 중 무엇을 선택해야 할까 (0) | 2025.01.16 |
---|
소프트웨어학과 현주씌의 일상을 담는 블로그
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!