All Articles

Class가 무엇인가요?

Why Is Class So Hard To Understand? And Even Harder To Explain?

아마 많은 개발자들 (경력자들 포함해서)이 클래스에 관해서 설명하는데 굉장히 어려워할 수 있을 것 같다. 클래스를 많이 사용하고 익숙한 개발자들도 클래스가 무엇인지에 대한 추상적인 이해도는 있지만, 클래스를 명확하게 정의한다는 것은 어려울 수 있는 일이다. 그래서 면접에서 클래스에 관해서 설명해보라는 질문이 나오면 많은 개발자들이 긴장한다. 실제로 우리 위코드의 수강생들도 코딩을 배울 때 특별히 어려워하는 개념들이 몇 가지가 있는데 그중 하나가 클래스이다.

그렇다면 클래스는 왜 이해하기가 어려울까? 내가 생각하기에 클래스가 이해하기 어려운 이유는 직관적이지 않기 때문이다. 우리가 일상적으로 생활하면서 접할 수 있는 개념이 아니다. 그러다 보니 한 번에 이해하고 습득해서 사용하기가 어렵다. 대부분 객체지향 프로그래밍(OOP, Object Oriented Programming) 언어를 어느 정도 사용해봐야 개념이 제대로 잡힌다.

클래스 개념을 이해하고 사용하는 것보다 더 어려운 건 바로 클래스를 설명하는 것이다. 클래스를 잘 이해하고 사용하고 있는 개발자도 다른 사람에게 클래스가 뭔지 설명하려고 하면 뜻밖에 어려움을 겪을 때가 많다. 워낙 추상적인 개념이다 보니, 반복 사용을 통해 체득하는 경우가 대부분이어서 애초에 논리적으로 완벽히 이해하고 정리한 게 아니다 보니 설명이 어렵지 않나 생각한다. 온라인에서 클래스의 정의해 대해 검색해보면 많은 내용이 나오는데, 그 내용을 대략 짧게 요약하면 다음과 비슷할 것이다:

Class implements a real-world entity.
Class is essentially user`s own data type with data and functions.

하지만 아마 잘 와 닿지 않을 것이다. 그래서 도대체 class 이놈이 무엇인지에 대해서 설명하는 이 어려운 task에 한번 도전에 보려고 한다!

The Life Before Class & OOP (Object Oriented Programming)

위코드에서 가르칠 때도 항상 기술의 발전 역사에 대해서 먼저 가르친다. 어떤 특정 기술이 나온 이유가 있고, 그 기술들이 어떻게 발전했는지를 이해해야 기술의 목적과 존재 의미를 알게 된다. 그리고 기술의 목적과 존재 의미를 알게 되면 그 기술을 배우는 데 훨씬 도움이 되기 때문이다. 탄생 목적과 이유 그리고 발전 역사를 모르면 정말 원래 그런가 보다 하고 아무 생각 없이 배우는 주입식 교육이 되기 때문이다. 자 그런 의미에서 클래스라는 개념이 프로그래밍에 나타나게 된 배경을 살펴보자.

클래스, 그리고 객체지향 프로그래밍(OOP)이라는 개념은 1966년 정도에 Alan Kay라는 개발자가 세상에 탄생시켰다. 그의 대학원 논문의 주제였다. 하지만 클래스 그리고 OOP라는 개념이 나오기 전에도 개발자들은 나름대로 개발을 잘하고 있었다.

(TMI: 솔직히 이때 개발자들이 정말 찐 개발자들 아닐까? 프레임워크/라이브러리도 없고 구글/스택오버플로우 도 없이 프로그래밍의 세계를 발전시킨 주역들! 리스펙트 👍)

그렇다면 객체지향 프로그래밍(OOP) 이전에 개발자들은 어떻게 개발을 했을까? 그리고 Alan Kay는 왜 클래스와 OOP라는 개념을 만들었을까? 이 질문에 대한 답을 얻기 위해 직접 클래스 없이 코딩을 해보도록 하자.

클래스를 설명할 때 자주 등장하는 예제 중 하나인 Car (자동차)를 시뮬레이션하는 코딩을 짜보자. Python을 사용해서 만들어 보도록 하겠다(파이썬에는 클래스가 당연히 있지만 그래도 일부로 사용하지 않도록 해보자).

먼저 자동차에 대한 데이터들을 저장할 수 있는 자료구조가 필요할 것이다. 딕셔너리를 사용해서 만들어보자.

car = {
    "maker"              : "BENZ",
    "year"               : 2020,
    "model"              : "E-Class",
    "AI Auto Drive Mode" : True
}

자 이제 car에 대한 데이터는 만들었으니 car가 실행할 수 있는 기능들을 함수로 표현해보도록 하자. 예를 들어, drive라는 함수를 구현해보자. 요즘은 세상이 좋아져서 AI 자동 주행 기능이 탑재 되어 있으면 사람이 운전할 필요가 없다(라고 가정해보자! ㅎㅎ). 그러므로 drive라는 함수는 AI 자동 주행 옵션이 들어가 있는지 체크 해야 한다. drive 함수는 car.py 라는 파일에 구현된다고 가정해보자.

# car.py

def drive(car):
    if car[`AI Auto Drive Mode`]:
        print("AI가 대신해서 운전하고 있습니다. 목적지에 도착할 때까지 편히 누워서 넷플릭스나 시청하시지요!")
    else:
        print("AI는 무슨! 얼른 운전대 안 잡니?")

이제 앞서 구현한 데이터와 함수를 실제로 사용해보자!

from car import drive

car = {
    "maker"              : "BENZ",
    "year"               : 2020,
    "model"              : "E-Class",
    "AI Auto Drive Mode" : True
}

drive(car) # => prints "AI가 대신해서 운전하고 있습니다. 목적지에 도착할 때까지 편히 누워서 넷플릭스나 시청하시지요!"

자 지금까지는 별문제 없어 보인다! “클래스 왜 필요해? 그딴 거 없어도 코딩만 잘되는데?” 라고 생각할 수도 있다. 하지만 속단하지 말기를. 다음과 같은 문제가 있을 수 있다.

from car import drive

car = {
    "maker" : "BENZ",
    "year"  : 2020,
    "model" : "E-Class"
}

drive(car) # => KeyError!

drive 함수에 매개변수로 넘겨지는 딕셔너리에 요구되는 필드인 AI Auto Drive Mode가 없는 경우에 KeyError가 난다. 실제로 이런 에러는 쉽게 일어날 수 있다. 특히 drive 함수를 호출하는 사용자가 drive 함수에 매개변수로 넘겨지는 car 매개변수에 정확히 어떤 데이터들이 있어야 하는지 알지 못할 때 자주 일어날 것이다. 심지어는 딕셔너리가 아니라 다른 자료 구조(예를 들어, array 혹은 단순 정숫값)을 매개변수로 넘기는 오류를 낼 수도 있다. 이 모두 다 drive 함수의 사용자가 drive의 작동 로직에 대한 이해도가 부족해서 나는 오류이다.

문제는, drive 함수를 이해하려면 drive 함수의 실제 implementation(구현된 코드)을 이해하거나 해당 함수의 API 문서를 (만일 제공 되었다면) 읽고 이해하는 수밖에 없다. 물론 그렇게 하는 것이 불가능 한 건 아니고 지금도 API 공식 문서는 당연히 보는 거지만, 요구되는 데이터의 구조와 정확한 필드명까지 이해해야 어떠한 함수를 사용할 수 있다는 건 사용자로서 너무 불편할 수밖에 없는 것이고 오류가 나기 너무 쉬운 구조일 수 밖에 없다..

Class == Data + Functions

앞서 본 예제들의 문제는 데이터와 함수가 따로 분리돼서 정의되어 있으므로 생기는 문제들이다. 하지만 생각해보면 굳이 데이터와 함수를 따로 정의할 필요가 없다. 해당 데이터와 함수 전부다 Car에 대한 내용이다. drive 함수가 다른 context(내용/문맥)에 사용될 일이 없고 사용될 수도 없다 (해당 함수의 로직이 car 데이터에만 작동하도록 구현되었기 때문에). 그러므로 굳이 데이터와 함수를 분리할 필요 없이 하나로 묶어서 사용할 수 있게 하려고 만든 개념이 바로 class이다!

자 그럼 클래스를 사용하면 얼마나 삶이 편해지는지 직접 구현해보자:

# car.py

class Car:
    def __init__(self, maker, year, model, ai_auto_drive_mode):
        self.maker              = maker
        self.year               = year
        self.model              = model
        self.ai_auto_drive_mode = ai_auto_drive_mode

    def drive(self):
        if self.ai_auto_drive_mode:
            print("AI가 대신해서 운전하고 있습니다. 목적지에 도착할 때까지 편히 누워서 넷플릭스나 시청하시지요!")
        else:
            print("AI는 무슨! 얼른 운전대 안 잡니?")
from car import Car

car = Car(
    maker              = "BENZ",
    year               = 2020,
    model              = "E-Class",
    ai_auto_drive_mode = True
)

car.drive() # => prints "AI가 대신해서 운전하고 있습니다. 목적지에 도착할 때까지 편히 누워서 넷플릭스나 시청하시지요!"

클래스를 사용하니 관계된 데이터와 함수를 하나의 클래스로 묶었기 때문에 일단 Car이라는 클래스의 객체(object)가 생성되면 그 후에는 함수의 코드가 어떻게 구현되었는지를 전혀 몰라도 함수 사용 하는 데 아무 문제가 없다! 그리고 어느 함수가 Car에 속하는 함수 인지 전혀 혼동되지도 않는다. 데이터 생성 측면에서도, 어떤 데이터를 Car가 필요로 하는지 전보다 훨씬 더 명확해진다. 그래서 여러모로 코딩하기가 클래스 이전의 삶보다 편해지는 것이다.

이뿐만이 아니다. 클래스를 정의함으로써 Car 라는 나만의 새로운 타입(type)이 생겼다. 클래스를 사용하지 않고 딕셔너리와 함수를 사용한 코드에서는 해당 데이터가 Car 에 대한 데이터라고 확인할 수 있는 뚜렷한 방법은 별로 없다. 사람은 변수 이름, 데이터값을 눈으로 보고 확인해야 할 것이고, 프로그래밍 적으로 확인하기 위해서는 딕셔너리에 해당 데이터 내용을 확인할 수 있는 데이터를 추가로 생성하거나 할 수밖에 없다. 왜냐면 딕셔너리를 사용했기 때문에 데이터 타입은 딕셔너리 이기 때문이다. 다만 해당 딕셔너리가 담고 있는 데이터가 Car에 관한 데이터일 뿐이다. 하지만 클래스를 사용하면 Car이라고 하는 나만의 새로운 데이터 타입을 만든 것이다. 데이터 타입이기 때문에 당연히 타입도 확인할 수 있다.

print(type(car))
# ==> prints <class `__main__.Car`>

정리를 하지면:

클래스는 (자동차, 사용자와 같은) 어떠한 개념/컨셉/실체를 나타내는 데이터와 해당 데이터를 기반으로 실행시킬 수 있는 함수들을 분리하지 않고 하나로 묶어서 해당 개념을 표현하는 나만의 새로운 데이터 타입을 만들 수 있게 하여 프로그래밍을 훨씬 효과적으로 할 수 있도록 해준다.

이 정리를 영어로 번역하면 본문의 시작 부분 단락에서 보았던 영문의 클래스 정의 내용과 비슷해진다! 🙂

Class implements a real-world entity.
Class is essentially user`s own data type with data and functions.

Encapsulation, Inheritance, And Polimorphism

클래스에 대한 정의는 여기서 마무리할 수 있었으면 좋으련만…. 아쉽게도 클래스를 더 깊게 이해하고 설명하기 위해서는 꼭 알아야 하는 3가지 개념이 있는데 바로 encapsulation, inheritance, 그리고 polimorphism 이다. Inheritance와 polimorphism은 클래스가 처음 생겼을때 부터 적용된 개념은 아닌데 객체 지향 프로그래밍 언어들이 발전하면서 나중에 도입된 개념들이다. 여하간 이 3개념은 클래스를 더 잘 사용하고 설명하기 위해서는 꼭 이해해야 하는 개념들이다. 그리고…. 생소한 사람들에게는 이해하기 쉽지는 않은 개념들이다. Encapsulation, inheritance, 그리고 polimorphism을 설명하려면 지금까지 쓴 글보다 더 긴 글을 써야 할 것 같아서 해당 개념들의 설명은 2부 포스팅으로 넘기도록 하겠다 😎