객체지향 프로그래밍
객체지향 프로그래밍(Object-Oriented Programming)은 좀 더 나은 프로그램을 만들기 위한 프로그래밍 패러다임으로 로직을 상태(state)와 행위(behave)로 이루어진 객체로 만드는 것이다. 이 객체들을 마치 레고블럭처럼 조립해서 하나의 프로그램을 만드는 것이 객체지향 프로그래밍이라고 할 수 있다. 다시말해서 객체지향 프로그래밍은 객체를 만드는 것이다. 따라서 객체지향 프로그래밍의 시작은 객체란 무엇인가를 이해하는 것이라고 할 수 있다.
객체가 없다면
하나의 프로그램은 변수와 함수로 이루어져있다. 변수에 값을 담고, 연산을 함수로 묶는다. 그리고 이것들을 하나씩 실행하면서 프로그램이 동작하게 된다. 아래의 코드는 우항과 좌항을 더한 결과를 출력하는 매우 간단한 프로그램이다. 두개의 변수와 하나의 함수가 있고, 함수 sum을 호출하고 있다. 결과는 3이다.
left = 1 right = 2 def sum(): global left, right return left + right print sum()
이 정도 규모의 코드에서는 나쁜일이 일어날 가능성이 거의 없다. 하지만 프로그램은 점점 커지고 그에 따라 더 많은 사람이 코드의 작성에 참여하게 된다. 프로그램의 규모가 커지기 시작하면서 위의 코드가 약 1만줄에 이르는 방대한 코드가 되었다고 간주해보자. 코드의 복잡도는 기하급수적으로 늘어날 것이고 다양한 어려움에 봉착하게 될 것이다.
예를들어, 새로 합류한 개발자가 사용자의 적립금을 출력하는 로직을 작성하기 위해서 로직을 아래와 같이 고쳤다고 해보자.
left = 1 right = 2 def sum(): global left, right return left + right right = 10000 def my(): global right return right print sum() #10001 print my() #10000
함수 my의 실행결과(10000)는 정상적으로 출력 되지만 함수 sum은 엉뚱한 값(10001)을 출력하고 있다. 이것은 나중에 합류한 개발자에 의해서 right의 의미가 계산식의 오른쪽항에서 사용자의 권리에 해당하는 금액으로 달라졌기 때문이다. 프로그램의 규모가 커지고 개발자가 많아질수록 이런 일이 빈번해질 것이다. 즉 서로 관련이 없는 로직인 sum과 my가 하나의 프로그램 안에서 동작하고 있기 때문에 서로에게 영향을 끼치고 있는 것이다.
객체화
그럼 어떻게 해야할까? 이해를 돕기 위해서 비유를 들어보자. 컴퓨터에서 데이터를 보관하는 기본 단위는 파일이다. 파일이 있기 때문에 저장장치(하드디스크, SSD) 전체를 하나로 사용하는 것이 아니라 내용에 따라서 파일 단위로 쪼개서 사용할 수 있다. 마찬가지로 파일도 많아지면 관리의 어려움이 생긴다. 이것을 위해서 고안된 방법이 디렉토리(폴더)다. 즉 디렉토리를 이용해서 연관된 파일들을 그룹핑 함으로써 관리의 편의를 도모하게 되는 것이다.
객체란 디렉토리와 비슷한 목적으로 고안된 것이라고 생각하자. 연관되어 있는 변수와 메소드를 그룹핑해서 관리의 편의를 도모하는 것이 객체를 사용하는 이유다. (물론 훨씬 더 많은 이유가 있다)
다음 예제는 이전 예제를 객체화 시킨 것이다. 이 예제를 통해서 객체가 무엇인가를 알아보자.
class Calculator: def __init__(self, left, right): self._left = left self._right = right def sum(self): return self._left + self._right class User: def __init__(self, right): self._right = right def my(self): return self._right c = Calculator(1, 2) print c.sum() u = User(10000) print u.my()
클래스와 인스턴스
클래스란 객체의 설계도이고 인스턴스는 그 설계도에 따라서 만들어진 구체적인 제품이라고 비유 할 수 있다. 이 말의 의미를 실제 코드를 통해서 알아보자.
아래의 코드는 Calculator, User라는 이름의 클래스를 만들기 위한 것이다. 이 클래스는 객체 Calculator과 User에 대한 일종의 설계도라고 할 수 있다.
class Calculator:
class User:
익숙한 함수로 비유를 해보자. 함수도 설계도가 있다. 바로 def라는 키워드다. 함수의 설계도는 아래와 같은 형식을 가지고 있다.
def 함수명:
위의 내용은 함수를 정의 하는 것일 뿐 실제로 함수의 로직을 실행하기 위해서는 아래와 같이 함수를 호출해야 한다.
함수명()
객체도 마찬가지다. 객체를 실제로 사용하기 위해서는 아래와 같이 객체의 생성을 컴퓨터에게 지시해야 한다.
s = Calculator(1,2)
위의 코드는 변수 s에 객체 Calculator을 담아서 저장한다는 의미다. 위의 구문이 실행되면 변수 s를 이용해서 Calculator 객체를 사용할 수 있게 된다. 이렇게 생성된 객체를 인스턴스(instance)라고 부른다. 즉 객체의 설계도를 클래스라고 부르고, 이 설계도를 바탕으로 실제로 사용할 수 있게 만들어진 객체를 인스턴스라고 한다. 이것들을 포괄해서 객체라고 부르기도 한다. 객체라는 말의 의미는 맥락적으로 파악해야 한다.
생성자
Calculator()은 객체를 생성할 때 사용하는 규칙이다. 클래스의 이름()이 호출되면 클래스 내의 def __init__ 함수가 실행된다. 이 메소드를 생성자(constructor)라고 부른다. 함수 __init__은 객체의 초기화를 위한 특수한 함수로 이 이름으로 클래스 내에 함수를 정의하면 클래스가 인스턴스화 될 때 실행된다. 이것은 최초로 한번 실행되기 때문에 뒤에서 배울 인스턴스 변수의 초기 값을 셋팅하거나, 데이터베이스 접속을 시도하는 것과 같이 기본적으로 필요한 작업을 할 때 사용된다.
생성자는 인자를 가질 수 있다. 위의 예제에서는 Calculator(1, 2)를 통해서 1과 2를 인자로 전달했다. 이것은 아래 Calculator 클래스 내의 아래 코드로 인해서 인자변수 left의 값으로 1, right의 값으로 2가 전달된다. 이 변수들의 값은 각각 변수 self._left, self._right에 담겨진다. 그럼 다음 절에서 'self'가 무엇인지 알아보자.
def __init__(self, left, right): self._left = left self._right = right
인스턴스 변수와 메소드
파이썬에서는 객체 안에서 정의된 함수를 메소드(method)라고 부른다. 그런데 파이썬의 메소드는 함수와는 조금 다른 특징이 있다. 위의 예제에서 정의한 메소드 __init__은 인자가 3개다. 아래 그림을 보자.
인스턴스가 생성될 때 첫번째 인자 1은 메소드 __init__의 두번째 인자의 값으로 전달 되었다. 좀 이상하다. 그럼 self는 무엇일까? 파이썬에서 메소드의 첫번째 인자는 그 메소드가 소속된 인스턴스를 가르키는 값이 들어가도록 약속되어 있다. 따라서 self._left는 인스턴스에 속해있는 변수라는 의미다.
메소드의 첫번째 인자(위의 코드에서는 self) 뒤에 .(점)을 붙여 따라오는 변수를 인스턴스 변수(instance variable)라고 부른다. 인스턴스에 소속된 변수라는 것이다. 위의 예제에서 left, right는 메소드 안에서만 유효한 지역변수고 self_.left, self._right는 인스턴스에서 소속된 인스턴스 변수이다. 다시말해서, 인스턴스 변수인 self._left, self._right는 인스턴스에 소속되어 있기 때문에 인스턴스를 통해서만 접근이 가능하다는 의미다. 덕분에 첫번째 예제 처럼 right의 의미를 다르게 사용해도 right가 속해 있는 객체가 다르기 때문에 문제가 발생할 확률이 현저하게 낮아진다.
상태와 행위
앞에서 객체를 '연관된 상태와 행위의 그룹'이라고 정의했다. 그리고 상태를 변수, 행위를 메소드라고 생각하자고 했다. 이 말의 의미를 곱씹어보면서 아래를 보자.
c1 = Calculator(1, 2) c2 = Calculator(3, 4)
두개의 인스턴스를 만들었다. 각각의 인스턴스는 생성자로 인자1과 인자2가 전달 됐다. 인스턴스 c1의 상태는 아래와 같다.
- self._left : 1
- self._right : 2
아래는 c2의 상태다.
- self._left : 3
- self._left : 4
인스턴스 c1과 c2는 서로 다른 상태를 가지고 있는 것이다. 즉 인스턴스 변수의 값이 그 객체의 '상태(state)'가 되고, 메소드는 그 상태를 참고해서 '행위(behave)'를 하게 되는 것이다. 그 결과 c1.sum의 결과는 3이 되고, c2.sum의 결과는 7이 된다. 객체에 대해서 다시 정리해보자.
객체화란
- 설계도인 클래스를 바탕으로
- 서로 다른 상태(변수)에 따라
- 동일한 행동(메소드)을 하는
- 구체적인 제품인 인스턴스를
- 사용한 것이다.
마치며
이번 토픽에서는 객체가 무엇인가를 설명하기 위해서 많은 노력을 들였다. 하지만 객체지향 프로그래밍은 책 한권이 별도로 존재할 수 있을 정도로 그 내용이 깊고 방대하다. 이것을 다 알면 좋겠지만 그건 차차하면 된다. 이번 토픽에서 소개된 객체지향의 개념만 알고 있어도 Python에서 프로그래밍을 하는데 큰 도움이 될 것이다. 객체지향에 대한 더 많은 내용은 후속 강의를 통해서 다루기로 하겠다.