객체지향 프로그래밍 (OOP, Object-Oriented Programming)
정의
- 프로그램을
객체
들의 집합으로 구성하여 개발하는 방법 - 각 객체는
데이터(속성)
와기능(메소드)
를 함께 가진다. - 현실 세계를 코드로 모델링하는 방식이라고 이해하면 된다.
관련 용어
- 클래스 (Class)
- 객체를 만들기 위한 설계도 같은 것
- 객체 (Object)
- 클래스를 이용해 만든 실제 인스턴스(실체)
- 속성 (Attribute)
- 객체가 가지는 데이터/상태(변수)
- 메소드 (Method)
- 객체가 할 수 있는 동작/기능(함수)
- 상속 (Inheritance)
- 기존 클래스를 물려받아 새로운 클래스를 만들기
- 캡슐화 (Encapsulation)
- 데이터(속성)를 은닉하고 메서드로 제어하기
- 다형성 (Polymorphism)
- 같은 메서드 이름이 다른 동작을 하게 만들기
클래스 (Class)
클래스란?
클래스는 현실 세계의 사물이나 개념을
프로그래밍적으로 표현하기 위한 설계도다.
예를 들어 철수
라는 사람과 영희
라는 사람에 대한 데이터가 있다고 가정해보자.
철수와 영희에 대한 데이터를 딕셔너리를 통해서 나타낼 수도 있을 것이다.
철수 = {'name': '철수', 'age': 20, 'gender': "M"}
영희 = {'name': '영희', 'age': 21, 'gender': "F"}
하지만 이렇게 2명에 대한 데이터만 아니라 100명, 1000명에 대한 데이터라면
일일이 딕셔너리로 표현하기가 힘들 것이다.
그래서 이러한 데이터들의 공통적인 부분을 추출해서 만들 설계도가 클래스인 것이다.
위에서는 철수와 영희라는 사람
이라는 데이터에 대해 다루었다.
이 둘의 공통점은 사람
이라는 것이다.
그렇다면 해당 예시에서는 사람
을 클래스로 만들 수 있다.
클래스가 필요한 이유
- 코드의 구조화
- 관련 있는 데이터와 기능을 하나의 클래스 안에 묶는다.
- 코드를 논리적으로 구성하고 관리하기 쉽게 만들어준다.
- 재사용성
- 한번 정의해 놓은 클래스를 여러 번 재사용할 수 있다.
- 유지보수 용이성
- 공통되는 부분을 모은 것이 클래스다.
- 특정 부분을 수정해야 할 때 어느 부분을 수정해야 하는 지 찾기 쉬워진다.
- 상속 (Inheritance)
- 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 만들 수 있다.
- 코드의 중복을 줄이고 확장성을 높여준다.
- 캡슐화 (Encapsulation)
- 클래스 내부의 속성과 메서드를 외부로부터 숨길 수 있다.
- 필요한 인터페이스만 제공해서 데이터의 무결성을 보호할 수 있다.
- 다형성 (Polymorphism)
- 같은 이름의 메서드가 클래스에 따라 다르게 동작하게 한다.
- 코드를 더욱 유연하게 만들어준다.
기본 사용법
클래스는 기본적으로 아래와 같이 사용한다.
class Person:
def fun1(self):
print("함수 1 호출")
def fun2(self):
print("함수 2 호출")
def fun3(self):
print("함수 3 호출")
철수 = Person()
철수.fun1()
우선 class 클래스명:
을 통해 클래스임을 선언한다.
그 다음에는 클래스 내부에 메소드를 정의한다.
클래스를 사용할 때는 변수명 = 클래스명()
처럼 사용하면 된다.
클래스 내의 메소드를 호출할 때는 변수명.메소드명(매개변수)
처럼 사용하면 된다.
__init__ 메소드
클래스는 각 객체들의 공통점을 모아서 만든 설계도다.
그렇다면 해당 설계도를 통해서 각 객체마다의 데이터를 담게하려면 어떻게 해야 할까?
그럴 때 사용하는 것이 생성자(Constructor)
다.
생성자는 객체가 생성될 떄 자동으로 호출되는 특수한 목적의 메소드다.
해당 객체의 초기화를 위해 사용한다.
파이썬에서는 __init__
이라는 고정된 이름으로 메소드를 생성하면
해당 메소드를 생성자로 사용한다.
생성자에서는 해당 객체가 가지게 될 고유한 값을 저장하는 역할을 한다.
class Person:
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
철수 = Person("철수", 20, "M")
print(철수.name) # 출력 : 철수
print(철수.age) # 출력 : 20
print(철수.gender) # 출력 : M
self
앞선 코드에서 보면 self
라는 키워드가 많이 사용되는 것을 확인할 수 있다.
self
는 해당 단어의 뜻 그대로 자기 자신을 가리킨다.
그래서 self.name=name
의 경우에는
매개변수로 받은 name이라는 매개변수의 값을
객체 자신이 갖고 있는 name이라는 인스턴스 변수에 저장하라는 뜻이 된다.
사실 self라는 것은 관례상 쓰는 것이고,
본인이 원하는 이름으로 작성해도 된다.
또한 이건 파이썬의 특징인데
멤버 메소드에서는 매개변수 목록에 항상 self를 가지고 있어야 한다.
그래야지 멤버 메소드에서 객체 자신이 가지고 있는 값을 사용할 수 있다.
class Person:
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
def introduce(self):
print(f"제 이름은 {self.name}이고, 나이는 {self.age}살입니다.")
철수 = Person("철수", 20, "M")
철수.introduce()
클래스 변수, 인스턴스 변수, 멤버 변수
클래스 내에서 사용되는 변수에는 3가지 종류가 있다.
우선 아래의 예제를 살펴보자.
class Person:
count = 0
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
Person.count+=1
def introduce(self):
print(f"제 이름은 {self.name}이고, 나이는 {self.age}살입니다.")
철수 = Person("철수", 20, "M")
영희 = Person("영희", 21, "F")
print(f"총 인원 수 : {Person.count}")
우선 인스턴스 변수에 대해서 알아보자.
인스턴스 변수(Instance Variable)
는 이전까지 사용했던
self를 통해 접근할 수 있었던
이름 그대로 인스턴스 고유의 변수다.
클래스 변수(Class Variable)
는 클래스의 모든 인스턴스끼리 공유하는 변수다.
클래스 자체에 속해있으며, 접근하려면 클래스명.변수명
으로 접근해야 한다.
멤버 변수(Member Variable)
는 클래스 내부에 정의된 모든 변수를 의미한다.
그래서 클래스 변수와 인스턴스 변수 모두 멤버 변수에 포함된다.
클래스 자체에 대한 전체적인 설명을 할 때는 멤버 변수
를,
클래스 내부에 대한 자세한 설명을 할 때는 클래스 변수
와 인스턴스 변수
를 사용한다.
인스턴스 메소드 (Instance Method)
인스턴스 메소드는 클래스의 인스턴스가 사용하는 메소드를 의미한다.
해당 메소드는 self를 통해 인스턴스 자기 자신이 가지고 있는 값을 사용할 수 있다.
그래서 항상 매개변수에 무조건 self가 있어야 한다.
상속 (Inheritance)
상속은 기존에 정의된 클래스의 속성과 메소드를 물려받아
새로운 클래스를 정의하는 것을 의미한다.
부모의 유전자를 받아서 자식이 부모의 특성을 갖는 것에 비유할 수 있다.
관련 용어
- 부모 클래스
- 속성과 메소드를 물려주는 기존의 클래스
- 명칭
- Parent Class
- Base Class
- SuperClass
- 자식 클래스
- 부모 클래스의 속성과 메소드를 물려받아 새롭게 정의된 클래스
- 물려받은 속성과 메소드를 그대로 사용할 수 있다.
- 필요에 따라
오버라이딩
하여 물려받은 메소드를 재정의할 수 있다. - 물려받은 것 이외에도 새로운 속성과 메소드를 추가할 수 있다.
- 명칭
- Child Class
- Derived Class
- Subclass
상속을 사용하는 이유
- 코드 재사용성 증대
- 부모 클래스에 이미 정의된 코드를 자식 클래스에서 다시 작성할 필요 없이 그대로 사용할 수 있다.
- 코드의 중복을 줄이고 개발 시간을 단축시킬 수 있다.
- 코드의 확장성 용이
- 기존 클래스를 수정하지 않고 자식 클래스를 통해 새로운 기능이나 특성을 쉽게 추가할 수 있다.
- 프로그램의 확장성을 높여준다.
- 유지보수 용이성 향상
- 공통된 기능을 부모 클래스에 정의하고, 특수한 기능만 자식 클래스에 정의한다.
- 코드의 구조가 명확해지고 유지보수가 쉬워진다.
- 다형성 구현의 기반
- 오버라이딩을 통해 다형성을 구현할 수 있다.
기본 사용법
기본 사용법은 아래와 같다.
class 부모클래스:
# 부모 클래스의 속성 및 메소드
class 자식클래스(부모클래스):
# 자식 클래스에서 재정의하거나 추가할 속성 및 메소드
사람 클래스와 학생 클래스를 통해
상속에 대해서 알아보자.
# 부모 클래스
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce1(self):
print(f"안녕하세요, 제 이름은 {self.name}입니다.")
# 자식 클래스
class Student(Person):
def __init__(self, name, age, school, grade):
super().__init__(name, age) # 부모 클래스의 __init__ 호출
self.school = school
self.grade = grade
def introduce2(self):
print(f"저는 {self.school} {self.grade}학년입니다.")
student1 = Student("홍길동", 20, "대학교", 3)
student1.introduce1() # 출력 : 안녕하세요, 제 이름은 홍길동입니다.
student1.introduce2() # 출력 : 저는 대학교 3학년입니다.
print(student1.age) # 출력: 20 (부모 클래스 속성)
super()
super()
는 부모 클래스의 인스턴스를 가져온다.
super()
를 통해 부모 클래스의 변수나 메소드에 접근할 수 있다.
메소드 오버라이딩 (Method Overriding)
메소드 오버라이딩은 부모 클래스로부터 상속받은 메소드의 내용을
자식 클래스에서 재정의하는 것을 의미한다.
아까의 예제를 좀 고쳐보자.
# 부모 클래스
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
print("자기소개 - 부모 클래스")
# 자식 클래스
class Student(Person):
def __init__(self, name, age, school, grade):
super().__init__(name, age) # 부모 클래스의 __init__ 호출
self.school = school
self.grade = grade
def introduce(self):
print("자기소개 - 자식 클래스")
student1 = Student("홍길동", 20, "대학교", 3)
student1.introduce()
위 코드를 살펴보면 부모 클래스와 자식 클래스에
introduce
라는 같은 이름의 메소드가 있는 것을 확인할 수 있다.
이렇게 자식 클래스에서 부모 클래스에 있는 메소드와 이름과 매개변수 목록이 같은 메소드를
재정의하는 것을 메소드 오버라이딩이라고 부른다.
다중 상속 (Multiple Inheritance)
다중 상속은 말 그래도 하나의 자식 클래스가
여러 개의 부모 클래스를 상속받는 것을 뜻한다.
다중 상속은 상속받을 부모 클래스들의 이름을
쉼표로 구분해서 나열한다.
아래의 예제를 통해 간단하게 알아보자.
class Parent1:
def method1(self):
print("Parent1의 메서드")
class Parent2:
def method2(self):
print("Parent2의 메서드")
class Child(Parent1, Parent2):
def method3(self):
print("Child의 메서드")
child_obj = Child()
child_obj.method1() # 출력: Parent1의 메서드
child_obj.method2() # 출력: Parent2의 메서드
child_obj.method3() # 출력: Child의 메서드
위 코드처럼 다중 상속은 다양한 클래스들로부터
변수와 메소드를 가져와서 사용할 수 있게 해준다.
여러 클래스들로부터 변수와 메소드를 가져온다는 것은 매우 강력한 기능이다.
다만 강력한 만큼 주의해야할 점도 있다.
그것은 이름의 충돌이다.
아래와 같은 코드가 있다고 가정해보자.
class A:
def show(self):
print("A 클래스의 show 메서드")
class B(A):
def show(self):
print("B 클래스의 show 메서드")
class C(A):
def show(self):
print("C 클래스의 show 메서드")
class D(B, C):
pass
d = D()
d.show()
해당 코드에서 d.show()
를 실행했을 때
어떤 부모 클래스의 코드가 실행되는지 단번에 파악하기가 어렵다.
파이썬은 MRO(Method Resolution Order)
라고 해서
메소드 결정 순서라는 것이 있는데,
이는 클래스 상속이 발생했을 때 어떤 부모 클래스의 메소드를 실행할 지에 대한 순서다.
단순한 상속의 경우에는 해당 메소드가 정의된 마지막 자식 클래스의 메소드가 호출되겠지만
다중 상속의 경우에는 어떤 부모 클래스의 메소드가 호출될지에 대한 순서를 알기가 어렵다.
그러니 다중 상속을 할 때는 애초에 겹치는 메소드명이 없게 설계하는 것이 좋다.
접근 제어자 (Access Modifier)
접근 제어자란?
- 클래스 내의 속성에 접근을 제한하는 역할을 하는 문법을 뜻한다.
- 정보 은닉을 구현하기 위해 사용한다.
- 접근 제어자의 종류는 public, protected, private가 있다.
- 부를 때는
접근 제어자
+명칭
으로 부른다.public
+method
=public method
private
+member
=private member
public
public은 접근을 완전히 허용하는 방식이다.
평소처럼 count
나 sum()
과 같이
평범하게 이름을 지으면 그게 public이다.
public은 외부에 전부 공개된다.
그래서 같은 패키지인지 다른 패키지인지 상관없이
어디에서도 접근할 수 있다.
protected
protected는 접근을 부분 허용하는 방식이다.
앞에 _
를 붙이면 protected가 된다.
protected는 상속의 개념이 적용됬을 때 사용할 수 있다.
protected member는 부모 클래스에서는 public으로,
부모 클래스 외에는 private로 취급한다.
다른 언어에서는 protected를 적용했을 때
실제로 해당 클래스를 상속받은 클래스나
같은 패키지의 클래스에서만 접근할 수 있도록 한다.
다만 파이썬에서는 실제로 접근을 제어하지는 않고,
protected임을 명시하는 정도로만 사용한다.
private
private는 접근을 완전히 제어하는 방식이다.
해당 클래스 내부에서만 접근하게 한다.
그래서 클래스 내부의 public method를 통해서
private member의 값을 반환하거나 수정하는 방식을 사용한다.
이 점을 통해 주로 클래스 내부에서만 사용되어야 하는 속성이나 메소드를 정의할 때 사용한다.
다만 다른 언어와 다르게 파이썬에서는 완전히 막는 것은 아니다.
파이썬 인터프린터는 네임 맹글링(Name Mangling)
이라는 매커니즘을 통해서
실제 속성이나 메소드명을 _클래스명__멤버명
처럼 변경한다.
그래서 클래스명.__멤버명
처럼은 호출할 수는 없어도
인스턴스 멤버나 인스턴스 메소드라면
객체명._클래스명__멤버명
처럼 호출하면 private member에도 접근할 수 있다.
클래스 변수는 네임 맹글링으로도 접근할 수가 없다.
클래스 심화
__str__ 메소드
만약에 아래와 같은 클래스가 있다고 가정해보자.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
이 때 아래와 같이 객체를 직접 출력하면 어떻게 될까?
p = Person("홍길동", 30)
print(p) # 출력 : <__main__.Person object at 0x76a3dec07440>
그러면 <__main__.Person object at 0x76a3dec07440>
처럼
해당 객체가 어떤 클래스인지가 출력된다.
그런데 만약 객체를 print()로 출력하려고 했을 때
객체가 가진 데이터를 보여주고 싶다면 어떻게 해야할까?
그럴 때 사용하는 것이 __str__
함수다.
__str__
함수는 객체를 직접 출력했을 때 동작할 코드를 작성할 수 있다.
아까의 클래스를 아래와 같이 수정해보자.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"이름: {self.name}, 나이: {self.age}"
그런 다음에 다시 객체를 직접 출력해보면
해당 객체가 가지고 있던 데이터를 포맷팅해서 출력하는 것을 확인할 수 있다.
p = Person("홍길동", 30)
print(p) # 출력 : '이름 : 홍길동, 나이 : 30'
참고로 __str__
메소드는 print(p)
나 str(p)
처럼
“문자열을 출력”하려는 경우에 호출된다.
그리고 만약 __str__
이 정의되어 있지 않다면
다음에 소개할 __repr__
이 자동으로 대신 호출된다.
__repr__ 메소드
__repr__
메소드도 __str__
메소드처럼 객체의 문자열 표현을 정의하기 위한 메소드다.
다만 __str__
메소드는 데이터를 보기 쉽게 확인하기 위한 문자열을 반환했다면,
__repr__
메소드는 실제 코드처럼 보이게 하는 객체를 다시 생성할 수 있는 문자열을
반환하는 것이 목적이다.
보통 객체를 디버깅용으로 표현할 때 사용한다.
아까의 클래스를 아래와 같이 수정해보자.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"이름: {self.name}, 나이: {self.age}"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
이제 print 메소드와 repr 메소드를 통해 __repr__
메소드를 호출해보자.
p = Person("홍길동", 30)
print(repr(p)) # 출력 : Person('홍길동', 30)
참고로 __repr__
메소드는 repr(obj)
나 인터프린터에서 객체 입력 시 호출된다.
또한 __repr__
메소드를 별도로 정의하지 않아도 기본값으로 항상 존재한다.
클래스 메소드 (Class Method)
private member는 인스턴스 변수나 인스턴스 메소드였다면
권장되는 방법은 아니지만 네임 맹글러를 통해서 접근할 수 있긴 하다.
다만 클래스 변수가 private member였다면 네임 맹글러로도 접근할 수 없다.
그렇다면 정상적인 방법으로 private member에 접근하려면 어떻게 해야 할까?
그럴 때 사용하는 것이 바로 클래스 메소드다.
인스턴스 메소드는 관례에 의해서 self라는 매개변수를 받았었다.
이 때의 self는 객체 자신을 가리켰었다.
이와 다르게 클래스 메소드는 관례에 의해서 cls라는 매겨변수를 받는다.
이 때의 cls는 클래스를 가리킨다.
아까의 클래스를 아래와 같이 수정해보자.
class Person:
__count = 0 # private 클래스 변수
def __init__(self, name, age, birthdate):
self.name = name
self.age = age
self.__birthdate = birthdate # private 인스턴스 변수
__count += 1
def __str__(self):
return f"이름: {self.name}, 나이: {self.age}"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
만약 이와 같이 클래스가 변경되었을 때
__count
와 __birthdate
를 출력하려면 어떻게 해야할까?
우선은 print()를 통해 출력을 시도해보자.
p = Person("홍길동", 30, "1974-03-02")
print(p.__birthdate) # 오류 발생
print(p.__count) # 오류 발생
실제로 실행을 해보면
'Person' object has no attribute '속성명'
처럼 출력된다.
이는 private 접근 제어자때문에 접근을 하지 못 하다보니
없는 속성이라고 판단한 것이다.
이제 실제로 접근하기 위해서 클래스 메소드를 사용해보자.
클래스 메소드는 애노테이션(Annotation)
이라는 것과 함께 사용한다.
@classmethod
라는 애노테이션을 추가해서
해당 메소드가 클래스 메소드라는 것을 인지시키고,
매개변수로는 cls(클래스)를 받으면 된다.
이전의 클래스를 아래와 같이 수정해보자.
class Person:
__count = 0 # private 클래스 변수
def __init__(self, name, age, birthdate):
self.name = name
self.age = age
self.__birthdate = birthdate # private 인스턴스 변수
Person.__count += 1
def __str__(self):
return f"이름: {self.name}, 나이: {self.age}"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
def get_birthdate(self):
return self.__birthdate
@classmethod
def get_count(cls):
return cls.__count
get_birthdate는 private 접근 제어자의 이해를 위해 추가해봤다.
self.__속성명=값
으로 값을 저장한 경우에는 해당 인스턴스에 값을 저장한 것이다.
클래스 메소드는 이름 그대로 클래스를 위한 메소드라서
private 인스턴스 변수를 제어하려면 인스턴스 메소드를 사용해야 한다.
이제 클래스 메소드에 대해 알아보자.
원리 자체는 단순하다.
cls.속성명
을 통해 클래스 변수를 불러와서 제어하면 된다.
실제로 출력해보면 잘 불러오는 것을 알 수 있다.
p = Person("홍길동", 30, "0000-00-00")
print(p.get_birthdate())
print(Person.get_count()) # 접근 방법 1 : 클래스를 통한 접근
print(p.get_count()) # 접근 방법 2 : 객체를 통한 접근
출력하는 코드를 보면 클래스 메소드를 방법을 2가지로 불러오는 것을 알 수 있다.
우선 클래스 메소드는 기본적으로 클래스를 위한 메소드이기 때문에
클래스명.메소드명
으로 호출할 수 있다.
재밌는 부분은 객체명.메소드명
으로도 부를 수 있다는 것인데,
이것이 가능한 이유는 객체가 자신의 클래스에 대한 정보를 가지고 있기 때문이다.
왜냐하면 파이썬이 객체를 통해서 변수나 메소드를 호출하면
해당 클래스의 변수나 메소드를 찾아서 호출하기 위해 설계되어 있기 때문이다.
스태틱 메소드 (Static Method)
스태틱 메소드는 클래스나 인스턴스와는 관계없는 유틸리티 함수다.
그래서 self나 cls를 매개변수로 받지 않는다.
주로 일반 함수와 같지만 클래스 내부에 논리적으로 묶고 싶을 때 사용한다.
다만 매개변수를 안 받아도 되는 것이지 받으면 안 되는 것은 아니다.
필요 시 클래스의 메소드가 아닌 일반적인 함수처럼 매개변수를 넘겨줄 수 있다.
스태틱 메소드는 @staticmethod
애노테이션을 추가해서
함수를 정의하면 된다.
이전의 클래스를 아래와 같이 수정해보자.
class Person:
__count = 0 # private 클래스 변수
def __init__(self, name, age, birthdate):
self.name = name
self.age = age
self.__birthdate = birthdate # private 인스턴스 변수
Person.__count += 1
def __str__(self):
return f"이름: {self.name}, 나이: {self.age}"
def __repr__(self):
return f"Person('{self.name}', {self.age})"
def get_birthdate(self):
return self.__birthdate
@classmethod
def get_count(cls):
return cls.__count
@staticmethod
def sum(a, b):
print(a + b)
sum이라는 간단한 스태틱 메소드를 만들어보았다.
함수를 구현한 부분을 보면 알 수 있듯이
인스턴스나 클래스에 대한 매개변수가 존재하지 않는다.
이제 실제로 호출해보자.
p = Person("홍길동", 30, "0000-00-00")
Person.sum(1, 2) # 접근 방법 1 : 클래스를 통한 접근
p.sum(1,2) # 접근 방법 2 : 객체를 통한 접근
스태틱 메소드도 클래스 메소드처럼 호출할 수 있다.
그래서 클래스를 통해 호출할 수도 있고 객체를 통해 호출할 수도 있다.