파이썬 – OOP Part 1. 객체 지향 프로그래밍(OOP)은 무엇인가? 왜 사용하는가?

13 분 소요

이번 강좌에서는 객체 지향 프로그래밍(Object-Oriented Programming, OOP)에 대해서 알아 보겠습니다.

OOP는 내용이 조금 많기 때문에 다음과 같이 본 강좌를 포함하여 7개의 강좌로 나눠서 진행하도록 하겠습니다.

  1. 객체 지향 프로그래밍(OOP)은 무엇인가? 왜 사용하는가?
  2. 클래스와 인스턴스(Class and Instance)
  3. 클래스 변수(Class Variable)
  4. 클래스 메소드와 스태틱 메소드(Class Method and Static Method)
  5. 상속과 서브 클래스(Inheritance and Subclass)
  6. 매직 메소드(Magic Method)
  7. 속성 데코레이터(Property Decorator) – Gettes, Setters, Deleters

객체 지향 프로그램이란?

위키백과에 OOP에 대한 정의는 다음과 같습니다.

객체 지향 프로그래밍(영어: Object-Oriented Programming, OOP)은 컴퓨터 프로그래밍의 패러다임의 하나이다. 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 “객체”들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.

객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다. 또한 프로그래밍을 더 배우기 쉽게 하고 소프트웨어 개발과 보수를 간편하게 하며, 보다 직관적인 코드 분석을 가능하게 하는 장점을 갖고 있습니다. 그러나 지나친 프로그램의 객체화 경향은 실제 세계의 모습을 그대로 반영하지 못한다는 비판을 받기도 한다.

프로그램은 특수한 목적을 가지고 데이터를 처리하기 위하여 만들어집니다. 그런데 복잡한 데이터를 수많은 함수로만 처리하다 보면 여러가지 에러와 버그가 발생하는 문제점이 나타납니다. OOP는 이러한 문제를 해결하고 복잡한 데이터를 조금 더 쉽게 처리하게 도와줍니다. 그렇다면 OOP는 무엇일까요? OOP란 클래스란 이름의 블루프린트를 이용하여 새로운 데이터 타입을 만들어 데이터와 함수(클래스 안에서는 메소드라고 부름)의 논리적 그룹을 만들어 사용하는 것이라고 생각하시면 됩니다.

주의점!

일반적으로 프로그램을 만들때 항상 염두에 둬야할 아주 중요한 포인트 2가지가 있습니다.

  • 같은 코드를 반복하지 않는다. DRY (Don’t Repeat Yourself)
  • 코드는 항상 바뀔 수 있다는 것을 기억한다.

만약에 코딩을 할때 복사와 붙이기를 많이 하신다면 그 코드에는 중복되는 코드가 많다는 뜻이고, 이런 중복되는 부분은 많은 문제를 발생시키기 때문에 필히 최소화할 필요가 있습니다. 중복되는 코드는 코딩의 시간을 늘릴뿐 아니라, 골치아픈 버그를 만들어내고, 코드 변경시 수많은 곳을 수정해야 하는 문제를 발생시킵니다. 그래서 일반적으로 이런 중복된 코드를 줄이기 위해서 함수를 사용합니다. 함수를 사용하면 한번 정의한 함수를 필요한 곳에서 호출만 하면되고, 코드를 수정해야 할때는 한 곳만 수정하면 됩니다.

OOP 역시 함수와 비슷하게 반복되는 코드를 없애서 코딩 시간을 줄여 주며, 코드의 관리를 더 간단하게 해줍니다. 하지만 한번 사용하고 버리는 불필요한 클래스를 만드는 것 또한 피해야 한다는 점을 잊어서는 안됩니다.

객체 지향 프로그램은 왜 사용하는가?

이번 강좌에서는 먼저 어떤 경우에 클래스를 사용해야 하는지에 대해서 알아보고, 다음 강좌부터는 클래스의 기능과 사용법을 알아보도록 하겠습니다.

우리가 좋아하는 게임의 캐릭터를 만드는 예제를 보도록 하죠. 이름, 에너지, 데미지, 인벤토리를 가진 간단한 케릭터를 만들어 보려고 합니다. 만약에 클래스를 사용하지 않으면 다음과 같이 케릭터를 정의할 수 있을겁니다.

원하는 디렉터리에 “oop.py”라는 이름의 파이썬 파일을 만든후, 다음의 코드를 저장하여 주십시오.

oop.py
hero_name = '아이언맨'
hero_health = 100
hero_damage = 200
hero_inventory = [
{'gold': 500},
{'weapon': '레이저'}
]

“아이언맨”이라는 이름을 가진 케릭터를 만들어 봤습니다. 그런데 게임에 케릭터가 하나만 있으면 안되겠죠? 히어로와 몬스터 케릭터를 더 추가하겠습니다.

oop.py
# 히어로 1
hero_1_name = '아이언맨'
hero_1_health = 100
hero_1_damage = 200
hero_1_inventory = [
{'gold': 500},
{'weapon': '레이저'}
]
 
# 히어로 2
hero_2_name = '데드풀'
hero_2_health = 300
hero_2_damage = 30
hero_2_inventory = [
{'gold': 300},
{'weapon': '장검'}
]
 
# 히어로 3
hero_3_name = '울버린'
hero_3_health = 200
hero_3_damage = 50
hero_3_inventory = [
{'gold': 350},
{'weapon': '클로'}
]
 
# 몬스터 1
monster_1_name = '고블린'
monster_1_health = 90
monster_1_damage = 30
monster_1_inventory = [
{'gold': 50},
{'weapon': '창'}
]
 
# 몬스터 2
monster_2_name = '드래곤'
monster_2_health = 200
monster_2_damage = 80
monster_2_inventory = [
{'gold': 200},
{'weapon': '화염'}
]
 
# 몬스터 3
monster_3_name = '뱀파이어'
monster_3_health = 80
monster_3_damage = 120
monster_3_inventory = [
{'gold': 1000},
{'weapon': '최면술'}
]

누군가 프로그래밍의 기본은 복사와 붙여넣기라고 했는데… 열심히 복사해서 붙여넣었습니다. 🥵 그런데 위의 코드는 누가봐도 잘 만들어진 코드는 아닌 것 같네요. 리스트를 사용하여 조금 더 세련된 코드를 만들어 보겠습니다.

oop.py
hero_name = ['아이언맨', '데드풀', '울버린']
hero_health = [100, 300, 200]
hero_damage = [200, 30, 50]
hero_inventory = [
{'gold': 500,'weapon': '레이저'},
{'gold': 300, 'weapon': '장검'},
{'gold': 350, 'weapon': '클로'}
]
 
monster_name = ['고블린', '드래곤', '뱀파이어']
monster_health = [90, 200, 80]
monster_damage = [30, 80, 120]
monster_inventory = [
{'gold': 50,'weapon': '창'},
{'gold': 200, 'weapon': '화염'},
{'gold': 1000, 'weapon': '최면술'}
]

이제 “아이언맨”은 인덱스 0, “데드풀”은 인덱스 1, “울버린”은 인덱스 2를 사용하여 케릭터의 데이터에 엑세스할 수가 있습니다. 그런데 위와 같은 코드는 쉽게 버그를 만들어 냅니다. 그 예를 한번 볼까요?

oop.py
hero_name = ['아이언맨', '데드풀', '울버린']
hero_health = [100, 300, 200]
hero_damage = [200, 30, 50]
hero_inventory = [
{'gold': 500, 'weapon': '레이저'},
{'gold': 300, 'weapon': '장검'},
{'gold': 350, 'weapon': '클로'}
]
 
monster_name = ['고블린', '드래곤', '뱀파이어']
monster_health = [90, 200, 80]
monster_damage = [30, 80, 120]
monster_inventory = [
{'gold': 50, 'weapon': '창'},
{'gold': 200, 'weapon': '화염'},
{'gold': 1000, 'weapon': '최면술'}
]
 
 
# 히어로가 죽으면 호출되는 함수
def hero_dies(hero_index):
del hero_name[hero_index]
del hero_health[hero_index]
del hero_damage[hero_index]
# <--- 개발자가 실수로 del hero_inventory[hero_index]를 빠뜨렸다고 가정하시죠^^;;
 
hero_dies(0)
 
template = '히어로 이름: {}\n히어로 체력: {}\n히어로 데미지: {}\n히어로 인벤토리: {}'
 
print(template.format(hero_name[0],
hero_health[0],
hero_damage[0],
hero_inventory[0]))

파일을 저장한 후, 실행하여 보겠습니다.

실행 결과
히어로 이름: 데드풀
히어로 체력: 300
히어로 데미지: 30
히어로 인벤토리: {'gold': 500, 'weapon': '레이저'}

위의 코드와 같이 히어로의 에너지가 0이 되어 죽었을때 히어로를 리스트에서 지우는 함수를 추가했습니다. 그런데 개발자가 실수로 코드 한줄을 넣지 않았다면 “데드풀”이 죽은 “아이언맨”의 레이저를 사용하게 되는 문제가 발생합니다.

이러한 문제는 밑의 코드와 같이 각 히어로의 데이터를 파이썬 사전에 넣어 리스트로 묶어서 해결할 수 있습니다.

oop.py
heroes = [
{'name': '아이언맨', 'health': 100, 'damage': 200, 'inventory': {'gold': 500, 'weapon': '레이저'}},
{'name': '데드풀', 'health': 300, 'damage': 30, 'inventory': {'gold': 300, 'weapon': '장검'}},
{'name': '울버린', 'health': 200, 'damage': 50, 'inventory': {'gold': 350, 'weapon': '클로'}}
]
 
monsters = [
{'name': '고블린', 'health': 90, 'damage': 30, 'inventory': {'gold': 50, 'weapon': '창'}},
{'name': '드래곤', 'health': 200, 'damage': 80, 'inventory': {'gold': 200, 'weapon': '화염'}},
{'name': '뱀파이어', 'health': 80, 'damage': 120, 'inventory': {'gold': 1000, 'weapon': '최면술'}}
]
 
print('# 아이언맨 삭제 전')
print(heroes)
del heroes[0]
print('\n# 아이언맨 삭제 후')
print(heroes)
실행 결과
# 아이언맨 삭제 전
[{'name': '아이언맨', 'health': 100, 'damage': 200, 'inventory': {'gold': 500, 'weapon': '레이저'}}, {'name': '데드풀', 'health': 300, 'damage': 30, 'inventory': {'gold': 300, 'weapon': '장검'}}, {'name': '울버린', 'health': 200, 'd
amage': 50, 'inventory': {'gold': 350, 'weapon': '클로'}}]
 
# 아이언맨 삭제 후
[{'name': '데드풀', 'health': 300, 'damage': 30, 'inventory': {'gold': 300, 'weapon': '장검'}}, {'name': '울버린', 'health': 200, 'damage': 50, 'inventory': {'gold': 350, 'weapon': '클로'}}]

이 방법을 통해 데이터 핸들링은 쉬워졌지만, 만약 히어로의 인벤토리에 여러가지 아이템을 가진 가방이 있다고 했을 때 사전과 리스트는 중첩이 되고 코드는 더욱 더 복잡해질 것 입니다. 또 다른 문제점으로는 똑같은 코드가 많이 반복되는 것을 볼 수가 있습니다. 이런 경우가 OOP를 사용해야 하는 좋은 예입니다. OOP를 사용하여 반복되는 코드를 없애고 사전이나 리스트가 지원하지 않는 상속과 같은 클래스의 기능을 사용할 수가 있습니다.

위에서 클래스는 새로운 데이터 타입을 만드는 블루프린트라고 했습니다. 블루프린트란 한국말로 “청사진”이라고 부르며 건축물이나 자동차를 설계한 도면입니다. 자동차의 모델을 디자인하고 설계한 블루프린트를 이용하여 같은 모델의 자동차를 원하는 만큼 찍어 낼 수 있는거죠. 프로그래밍의 클래스도 같은 개념입니다.

위의 히어로와 몬스터 또한 같은 종류의 데이터를 가지고 있기 때문에 다음의 코드와 같이 클래스를 이용하여 논리적인 집합으로 묶을 수가 있습니다.

oop.py
# class 정의
class Character:
def __init__(self, name, health, damage, inventory):
self.name = name
self.health = health
self.damage = damage
self.inventory = inventory
 
    def __repr__(self):
        return self.name
 
 
# Character 클래스의 오브젝트 생성
heroes = []
heroes.append(Character('아이언맨', 100, 200, {'gold': 500, 'weapon': '레이저'}))
heroes.append(Character('데드풀', 300, 30, {'gold': 300, 'weapon': '장검'}))
heroes.append(Character('울버린', 200, 50, {'gold': 350, 'weapon': '클로'}))
 
monsters = []
monsters.append(Character('고블린', 90, 30, {'gold': 50, 'weapon': '창'}))
monsters.append(Character('드래곤', 200, 80, {'gold': 200, 'weapon': '화염'}))
monsters.append(Character('뱀파이어', 80, 120, {'gold': 1000, 'weapon': '최면술'}))
 
template = '{}'
print('# 히어로 리스트 확인')
print(heroes)
 
print('\n# 히어로 데이터 확인')
for hero in heroes:
print(hero.__dict__)
 
print('\n# 몬스터 리스트 확인')
print(monsters)
 
print('\n# 몬스터 데이터 확인')
for monster in monsters:
print(monster.__dict__)
 
del heroes[0]  # 히어로 리스트에서 아이언맨 삭제
 
print('\n# 히어로 리스트 재확인')
print(heroes)
 
print('# 히어로 데이터 재확인')
for hero in heroes:
print(hero.__dict__)
실행 결과
# 히어로 리스트 확인
[아이언맨, 데드풀, 울버린]
# 히어로 데이터 확인
{'name': '아이언맨', 'health': 100, 'damage': 200, 'inventory': {'gold': 500, 'weapon': '레이저'}}
{'name': '데드풀', 'health': 300, 'damage': 30, 'inventory': {'gold': 300, 'weapon': '장검'}}
{'name': '울버린', 'health': 200, 'damage': 50, 'inventory': {'gold': 350, 'weapon': '클로'}}
# 몬스터 리스트 확인
[고블린, 드래곤, 뱀파이어]
# 몬스터 데이터 확인
{'name': '고블린', 'health': 90, 'damage': 30, 'inventory': {'gold': 50, 'weapon': '창'}}
{'name': '드래곤', 'health': 200, 'damage': 80, 'inventory': {'gold': 200, 'weapon': '화염'}}
{'name': '뱀파이어', 'health': 80, 'damage': 120, 'inventory': {'gold': 1000, 'weapon': '최면술'}}
# 히어로 리스트 재확인
[데드풀, 울버린]
# 히어로 데이터 재확인
{'name': '데드풀', 'health': 300, 'damage': 30, 'inventory': {'gold': 300, 'weapon': '장검'}}
{'name': '울버린', 'health': 200, 'damage': 50, 'inventory': {'gold': 350, 'weapon': '클로'}}

여기까지 Character라는 클래스를 사용하여 OOP를 구현해 보았습니다. 아직 OOP에 대해서 감이 오지 않는 분들은 걱정하지 마세요. 이 OOP 시리즈를 모두 읽으시면 완벽히 이해하시게 되실 겁니다. 😄

객체 지향 프로그램에 대한 첫 소개는 여기까지하고 다음 강좌에서는 구체적인 클래스 사용 방법에 대해서 알아보도록 하겠습니다.