저번 시간에는 객체와 객체 지향 프로그래밍의 정의에 대해 알아봤습니다. 속성과 행동을 지닌 각 객체들 간의 상호작용을 구현하는 것이 객체 지향 프로그래밍인데요.

이번 시간에는 본격적으로 객체를 만드는 법에 대해 알아보겠습니다.

🟥 클래스와 인스턴스

멜론과 같은 음원 제공 프로그램을 만드려고 합니다. 이때, 사용자를 나타내는 객체를 만들어야 하는데요. 어떻게 만들 수 있을까요?

객체에는 속성과 행동이 있다고 했죠? 음원 프로그램을 사용하는 사용자는 어떤 속성과 행동을 가지고 있을까요?

사용자 속성에는 사용자의 아이디와 비밀번호, 재생 목록과 팔로우 하는 가수 목록 등이 있습니다. 사용자 행동에는 재생 목록에 음원 추가하기나 마음에 드는 음원 다운로드 받기, 좋아하는 가수 팔로우 하기 등이 있습니다.

모든 사용자 객체는 이러한 속성과 행동을 동일하게 가지고 있습니다. 따라서, 우리는 사용자 객체의 속성과 행동을 정의함으로써 사용자 객체의 틀을 정했다고 할 수 있습니다. 앞으로 사용자 객체를 만들 때, 이 틀을 가지고 만들면 됩니다.

붕어빵 틀로 여러 개의 붕어빵을 만들 수 있듯이 사용자 객체도 이와 마찬가지입니다. 일단 틀을 만들면 그 다음에는 찍어내기만 하면 되는 것이죠.

Python에서는 이러한 객체의 틀클래스라고 하고 틀로 만든 결과물객체라고 합니다. 이는 곧, 클래스가 있으면 객체를 여러 개 만들 수 있음을 뜻하죠.

여기서 객체는 흔히 인스턴스라는 말로 대체되곤 합니다. 두 개념은 엄밀히 따져보면 다른 의미를 가지고 있지만 거의 유사하기 때문에 같은 의미처럼 쓰입니다.

이제 Python으로 클래스를 만들어 봅시다. 예시와 같이 사용자 클래스를 만들어 볼까요?

class User:
    pass

클래스를 만들 때는 class를 쓰고 그 옆에 클래스 명을 적어주면 됩니다. 이때, 클래스 명의 첫 글자는 항상 대문자로 써야 한다는 것을 잊지 마세요! 그 다음 콜론을 쓰면 됩니다.

그 다음 줄부터 들여쓰기를 하고 클래스의 내용을 적으면 됩니다. 지금은 따로 쓸 내용이 없기 때문에 pass라는 걸 적어줄 건데요. 클래스의 내용을 아예 비우게 되면 오류가 나기 때문에 아무 내용이 없다는 것을 의미하는 pass를 쓴 것입니다.

아직 속성과 행동을 정의하지는 않았지만 이 상태에서도 인스턴스를 만들 수 있습니다. 먼저, 변수 하나를 선언합니다.

user1 = User()

등호의 뒷 부분, User()을 통해서 User 인스턴스가 생성되고 변수 user1이 이 인스턴스를 가리킵니다. 위에서 정의한 User 클래스를 사용해서 User 인스턴스를 만들어낸 것이죠. 몇 개 더 만들어 볼까요?

user1 = User()
user2 = User()
user3 = User()

같은 클래스로 만들었지만 세 개의 인스턴스는 각각 다르다는 것을 알아두세요.

객체와 인스턴스의 차이를 궁금해하실 분들이 있을 것 같아 참고로 설명하자면 객체는 우리가 구현해야할 것을 말하고 인스턴스객체를 실체화한 것을 말합니다. 클래스설계도라고 보면 되구요.

쉽게 말해, 객체는 마우스를 구현하기 전 속성과 행동을 정의한 계획이라고 볼 수 있고 인스턴스는 객체(마우스)를 실체화하는 과정 혹은 실체화된 것을 의미하죠.

🟥 인스턴스 변수

위에서 정의한 User 클래스에는 속성과 행동에 대한 정의가 없습니다. 따라서, 인스턴스에 대한 속성과 행동이 없는 상태이죠.

지금부터 인스턴스의 속성을 정의하는 방법에 대해 알아보겠습니다. 이는 변수를 선언하는 것과 매우 유사한데요. 인스턴스의 변수를 정의하려면 인스턴스 이름.속성 이름(인스턴스 변수) = 속성에 넣을 값으로 코드를 적어주면 됩니다. 속성 이름은 간단하게 name으로 해봅시다. 그럼 다음과 같이 쓸 수 있습니다.

user1.name = "타키탸키"

이제 user1 인스턴스에 이름을 나타내는 name 속성을 추가했고 속성값으로는 문자열 '타키탸키'를 넣었습니다.

다른 속성도 추가해봅시다. 이번에는 아이디와 비밀번호를 넣어볼까요?

user1.id = "tataki26"
user1.pwd = "123456"

마찬가지로 user2, user3 인스턴스에도 속성을 추가해봅시다.

user1.name = "파이리"
user1.id = "firedragon12"
user1.pwd = "654321"

user1.name = "차정원"
user1.id = "carthegarden"
user1.pwd = "741852"

각 User 인스턴스는 같은 이름의 속성을 가지고 있어도 서로 다른 값을 가지고 있습니다. 다시 말해, 세 인스턴스들은 속성을 공유하지 않고 각각의 속성을 따로 가지고 있습니다.

이와 같이 개인적으로 가지고 있는 인스턴스의 속성인스턴스 변수라고 합니다. name, id, pwd 모두 인스턴스 변수입니다. 이제 이 변수들을 사용해봅시다.

인스턴스 변수를 사용하려면 인스턴스 이름.인스턴스 변수 이름과 같이 작성하면 됩니다. user1.id 같이 말이죠. 변수로 따지면 변수명을 적어준 것과 같습니다.

인스턴스 변수를 출력해볼까요?

print(user1.id)
tataki26

그런데 만약 추가하지 않은 인스턴스 변수를 사용하면 어떻게 될까요? 예를 들어, user1.gender와 같이 말이죠. 그럼 다음과 같은 에러 메세지가 뜹니다.

AttributeError: 'User' object has no attribute 'gender'

위 문구를 해석하면 'User'라는 객체에 'gender'라는 속성이 없다는 것을 뜻합니다. 따라서, 정의하지 않은 인스턴스 변수는 사용하면 안됩니다. 반드시 변수 선언을 먼저 하고 사용해야 하죠.

🟥 인스턴스 메소드

다음으로 인스턴스의 행동도 정의해봅시다. 인스턴스의 속성은 변수로 나타내는 반면, 행동은 함수로 나타낼 수 있습니다. 클래스 안에 함수를 정의하면 객체의 행동을 정의한 것과 같습니다. 이때, 함수를 메소드라고 부릅니다.

메소드에는 크게 세 가지 종류가 있습니다.

  1. 인스턴스 메소드
  2. 클래스 메소드
  3. 정적 메소드

먼저, 인스턴스 메소드인스턴스 변수사용하거나 인스턴스 변수에 값을 설정하는 메소드입니다.

앞서 비워둔 User class에 인스턴스 메소드를 추가해봅시다.

class User:
    def say_hi(users):
        print(f"안녕! 나는 {users.name}야.")

say_hi라는 함수를 정의했습니다. 이 함수의 역할인사 메시지를 출력하는 것입니다. 클래스 안에서 정의됐으니 이 함수는 메소드가 됩니다.

동작 원리를 살펴봅시다. 이 메소드는 users라는 파라미터를 받습니다. users에는 User 인스턴스를 넣어주면 됩니다. user1, user2, user3가 들어가겠네요. 만약 user1이 users 자리에 들어가게 되면 name 속성을 출력해야 하므로,

안녕! 나는 타키탸키야.

와 같은 문구가 출력됩니다.

정리하자면, say_hi라는 메소드는 name이라는 인스턴스 변수를 사용합니다. 따라서, say_hi는 인스턴스 메소드라고 할 수 있습니다.

이제 say_hi 메소드를 사용해봅시다.

User.say_hi(user2)
User.say_hi(user3)
안녕! 나는 파이리야.
안녕! 나는 차정원야.

❗ 인스턴스 메소드의 특별한 규칙

앞서 인스턴스 메소드를 사용할 때, 다음과 같이 작성했습니다. 클래스 이름.메소드 이름(인스턴스) 그런데 인스턴스 메소드를 사용하는 또 다른 방법이 있습니다.

user1.say_hi()

인스턴스 이름을 먼저 쓰고 그 다음 메소드 이름을 적은 건데요. 이상한 점이 있습니다. say_hi는 users라는 파라미터를 넘겨 받아야 하는데 위 코드에는 파라미터를 넘겨주지 않았습니다. 그럼에도 에러가 나지 않았죠. 이는 인스턴스 메소드의 특별한 규칙 덕분입니다.

기존의 방법은 클래스에서 메소드를 호출했고 새로운 방법은 인스턴스의 메소드를 호출했습니다.

User.say_hi(user1)
user1.say_hi()

후자와 같이 인스턴스의 메소드를 호출하면 user1 인스턴스가 자동으로 say_hi의 첫 번째 파라미터에 전달됩니다. 따라서, 파라미터를 따로 지정해줄 필요가 없습니다. 두 코드는 모양은 다르지만 완전히 같은 코드라고 볼 수 있습니다.

이때, 아래 코드에 파라미터를 넣어주면 에러 메세지가 뜹니다.

TypeError: say_hi() takes 1 positional argument but 2 were given

위 문구를 해석하면 say_hi는 파라미터를 한 개만 받는데 두 개가 들어왔다고 합니다. 앞서 user1 인스턴스로 메소드를 호출해서 자동으로 user1이 파라미터로 들어갔는데 user1 인스턴스를 또 넣어준 셈이죠.

user1.say_hi(user1)
User.say_hi(user1, user1)

또 다른 예시를 보죠. User 클래스에 새로운 인스턴스 메소드를 추가할 건데요.

def login(users, ids, pwds):
    if (users.id == ids and users.pwd == pwds):
        print("로그인 성공")
    else:
        print("로그인 실패, 없는 아이디거나 잘못된 비밀번호")

login 메소드는 파라미터users, ids, pwds를 받고 파라미터로 받은 값들이 User 인스턴스의 아이디, 비밀번호와 같은지 확인합니다.

login 메소드를 호출해봅시다.

user1.login(user1, "tataki26", "123456")
user1.login("tataki26", "123456")

두 코드 중 어떤 코드가 알맞게 쓰였을까요? 네, 맞습니다. 두 번째 코드가 알맞게 쓰였습니다. 다시 한 번 설명하자면, 인스턴스 메소드로 호출했을 때, 자동으로 user1 인스턴스 변수가 파라미터로 넘어가기 때문에 파라미터에는 따로 적어주지 않아도 됩니다.

🟥 self

앞서 인스턴스의 메소드를 호출할 때 인스턴스가 자동으로 메소드의 첫 번째 파라미터로 들어가는 것을 봤습니다. 따라서, 인스턴스 메소드를 정의할 때에는 항상 첫 번째 파라미터인스턴스를 받기 위한 파라미터를 써야 합니다. say_hi의 users와 login의 users가 바로 그 예입니다.

이 외에도 한 가지 규칙이 더 존재합니다. Python에서는 인스턴스 메소드의 첫 번째 파라미터 이름을 self로 쓰라고 권장합니다. 한 번 바꿔볼까요?

def say_hi(self):
    print(f"안녕! 나는 {self.name}야.")

def login(self, ids, pwds):
    if (self.id == ids and self.pwd == pwds):
        print("로그인 성공")
    else:
        print("로그인 실패, 없는 아이디거나 잘못된 비밀번호")

인스턴스 메소드를 호출하는 인스턴스 자신이 첫 번째 파라미터로 들어가는 걸 보면 self라는 이름이 잘 어울린다고 생각합니다. 인스턴스 메소드에는 첫 번째 파라미터가 중요한데 self라는 표기를 사용함으로써 그 중요성이 부각되는 것 같습니다. 그럼 훨씬 읽기 좋은 코드가 되겠죠.

사실 self가 아닌 다른 단어를 쓰더라도 프로그램은 문제 없이 돌아갑니다만 이는 Python 세계의 약속과 다름없습니다. 모두가 이렇게 쓰기로 약속한 것이니만큼 잘 지켜야겠죠?

🟥 인스턴스 변수와 같은 이름을 가진 파라미터

이번에는 User 클래스에 check_name이라는 메소드를 추가해봅시다.

def check_name(self, name):
    return self.name == name

이 메소드는 첫번째 파라미터로 User 인스턴스를 받습니다. 규칙 상 self라고 적었습니다. 두번째 파라미터에는 문자열로 이름을 받습니다. 이 메소드는 파라미터로 받은 name사용자의 이름과 같은지 불린형으로 반환합니다.

그런데 자세히 보면 인스턴스 변수파라미터의 이름이 둘 다 name으로 동일합니다. name 하나는 이 메소드 내에서 사용하는 값이고 다른 하나self의 name입니다. 이는 어떤 인스턴스가 가진 인스턴스 변수 name인 거죠.

nameself.name서로 다른 것으로 구분됩니다. 따라서, 인스턴스 변수와 파라미터의 이름이 같더라도 문제 없습니다. 심지어 이런 식의 표기가 꽤 일반적입니다.

이제 check_name 메소드를 사용해봅시다.

print(user1.check_name("타키탸키"))

인스턴스가 메소드를 직접 호출하여 첫 번째 파라미터로 넘어갑니다. 그럼 인스턴스 user1이 self에 들어가게 되죠. 두 번째 파라미터에는 입력한 문자열 "타키탸키"가 들어갑니다.

앞서 인스턴스 user1의 인스턴스 변수 name에 문자열 "타키탸키"를 넣었었죠? 그럼 self.name도 "타키탸키"이고 파라미터 name도 "타키탸키"이므로 True가 리턴됩니다.

하나만 더 해보죠.

print(user1.check_name("파이리"))

이 경우, self.name은 "타키탸키"인데 반해 파라미터 name은 "파이리"이므로 False를 리턴합니다.

🟥 __init__메소드

def initialize(self, name, id, pwd):
    self.name = name
    self.id = email
    self.pwd = pwd

initialize 메소드를 사용하면 새 인스턴스를 생성할 때마다 코드 단 두 줄인스턴스 변수들의 초깃값을 설정할 수 있습니다.

user1 = User()
user1.initialize("타키탸키", "tatakin26", "123456")

이런 식으로 User를 생성하는 줄 하나와 인스턴스 변수의 초깃값을 설정하는 줄 하나만 있으면 됩니다.

그런데 이 코드마저 한 줄로 줄일 수 있는 방법이 또 있습니다. 바로 initialize 자리에 __init__를 넣어 대체하는 방법이죠.

이렇게 이름 앞 뒤로 언더바가 두 개씩 있는 메소드매직 메소드(magic method) 혹은 스페셜 메소드(special method)라고 합니다. 우리말로는 특수 메소드라고 하는데요. 이 메소드는 특정 상황에서 자동으로 호출되는 메소드입니다.

앞으로는 __init__메소드를 '이닛 메소드'라고 부르겠습니다. 이닛 메소드는 어떤 상황에서 자동으로 호출될까요? 바로 인스턴스가 생성될 때입니다. 예시를 한 번 볼까요?

user1 = User()

이때까지는 인스턴스를 생성할 때 괄호 안을 비워두었습니다. 이제 이 괄호 안을 initialize의 파라미터로 채우겠습니다. 그리고 initialize 호출 부분의 코드는 삭제할 겁니다.

user1 = User("타키탸키", "tataki26", "123456")

바꾼 코드로 실행해도 문제 없이 돌아갑니다. 어떤 원리일까요? 이 코드가 실행되면 먼저 User 인스턴스 하나가 생성됩니다. 그리고 뒤이어 이닛 메소드가 자동으로 호출됩니다.

첫번째 파라미터 self에는 방금 막 생성된 User 인스턴스가 들어갑니다. 그 다음 괄호 안에 나머지 값들이 순서대로 들어가죠. 그럼 이닛 메소드가 정의된 코드에 따라 인스턴스 변수들의 초깃값을 설정해줍니다.

이 말은 즉, 이닛 메소드를 사용하면 인스턴스 생성과 인스턴스 변수 초깃값 설정을 한 줄에 할 수 있다는 뜻입니다. 이러한 간편성 때문에 클래스에는 이닛 메소드를 꼭 사용합니다.

🟥 __str__메소드

user1 = User("타키탸키", "tataki26", "123456")
user2 = User("파이리", "firedragon12", "654321")

User 클래스로 user1과 user2 두 개의 인스턴스를 생성했습니다. 이제 각 인스턴스를 출력해보겠습니다.

print(user1)
print(user2)

이들을 출력하면 조금 이상한 값들이 나옵니다.

<__main__.User object at 0x....>
<__main__.User object at 0x....>

먼저 해당 인스턴스가 어떤 클래스인지를 나타내고(User) 그 인스턴스가 저장되어 있는 메모리 주소(0x)가 나옵니다. 하지만 이 내용들은 우리가 출력하고 싶은 내용이 아닙니다. 그럼 우리가 원하는 내용을 출력하려면 어떻게 해야 할까요?

def __str__(self):
    return f"사용자: {self.name}, 아이디: {self.id}, 비밀번호: *****"

위 코드는 str 메소드를 정의한 것입니다. str 메소드 또한 특수 메소드 중 하나입니다.

특수 메소드는 '매직 메소드' 혹은 '스페셜 메소드' 외에도 다른 이름을 가지고 있는데요. 두 개의 언더바 때문에 'double underbar'를 줄인 'dunder' 메소드라고 부르기도 합니다. 따라서, 이제부터는 str 메소드를 던더 str 메소드라고 부르겠습니다.

그렇다면 던더 str 메소드는 어떤 상황에 자동으로 호출될까요? 바로 print 함수를 호출할 때입니다. 쉽게 말해, 어떤 인스턴스를 출력하면 던더 str 메소드의 리턴값이 출력됩니다.

그럼 위 코드를 추가했을 때에는 출력값에 어떤 변화가 생길까요? print함수를 통해 User 인스턴스를 출력했기 때문에 이 인스턴스의 던더 str 메소드가 호출됩니다.

self.name은 이 인스턴스의 이름, self.id는 이 인스턴스의 아이디죠? 그 값들이 문자열에 들어가서 사용자는 "타키탸키", 아이디에는 "tataki26"가 들어갑니다. 이제 이 코드를 실행하면,

사용자: 타키탸키, 아이디: tataki26, 비밀번호: ******
사용자: 파이리, 아이디: firedragon12, 비밀번호: ******

와 같이 우리가 원하는 결과를 출력합니다. 이처럼 던더 str 메소드를 클래스에 정의하면 우리가 원하는 형태로 인스턴스를 출력해줍니다.


지금까지 클래스와 인스턴스의 개념, 인스턴스의 속성인 변수와 행동인 메소드, 메소드 활용 규칙과 특수 메소드 두 개를 배웠습니다. 많은 개념이 한 번에 들어오니 복잡하시죠?

이 많은 개념들에 익숙해져야 앞으로 프로그래밍을 좀 더 효율적인 방법으로 구현해볼 수 있습니다. 차근히 이해하고 따라오시길 바랍니다.

다음 시간에는 나머지 메소드인 클래스 메소드와 정적 메소드에 대해 알아보겠습니다.

* 이 자료는 CODEIT의 '객체 지향 프로그래밍' 강의를 기반으로 작성되었습니다.
profile
There's Only One Thing To Do: Learn All We Can

0개의 댓글