[Python TIL] 함수(Function)와 클래스(Class)

함수 → 클래스로 가는 사고 전환
함수는 "동작"만 담당했지만,
어떤 대상(데이터)과 관련된 동작이 많아지면
"이 데이터와 함수들을 한 덩어리로 묶는 것이 좋다!"
→ 이게 바로 클래스(class) 개념!
예:
class TextCleaner:
def __init__(self, text):
self.text = text
def clean(self):
return self.text.lower().strip()
이것이 **객체지향 프로그래밍(OOP)**의 시작점이다!
자주 묻는 질문 (FAQ)
질문 | 답변 |
함수 하나만 만들면 되나요? | NO! 작업 단위를 작게 쪼개서 여러 개의 함수를 만들어 조립하는 게 좋다. |
함수 이름은 어떻게 짓나요? | 무슨 일을 하는지 동사 중심으로: clean_text(), calculate_avg() 등 |
함수로 부족할 때, 클래스가 필요해지는 순간들
함수는 프로그래밍에서 굉장히 강력한 도구이다. 어떤 **동작(기능)**을 여러 번 반복해서 사용하고 싶을 때, 우리는 자연스럽게 함수를 만든다.
그런데 실무를 하다 보면 단순한 기능만으론 부족한 경우가 많다. 바로, "상태(state)"도 함께 관리해야 하는 상황이다.
✅ 함수는 ‘기능 도구 상자’다
예를 들어, 은행 계좌의 입금과 출금을 처리하는 함수를 만든다고 해보자.
def deposit(balance, amount):
return balance + amount
def withdraw(balance, amount):
return balance - amount
겉보기엔 멀쩡해 보인다. 하지만…
매번
balance
를 인자로 넘겨줘야 하고,계좌가 여러 개 생기면
balance1
,balance2
,balance3
… 복잡도가 올라간다.
이처럼 함수는 ‘동작’은 잘 처리하지만, 데이터를 기억하지 못한다는 한계가 있다.
🧠 비유: 함수 vs 클래스
구분 | 비유 | 설명 |
함수 | 드라이버 하나하나가 담긴 도구 상자 | 필요한 기능을 하나씩 꺼내 쓰는 구조 |
클래스 | 다양한 기능과 설정을 포함한 전동 공구 키트 | 데이터도 기억하고 여러 기능을 조합할 수 있음 |
함수는 필요한 도구를 꺼내 쓰는 데 유용하지만, 상황이 복잡해질수록 ‘설정’을 기억하고, 다양한 동작을 함께 다루는 클래스가 훨씬 편리하다.
✅ 실무에서 마주치는 클래스의 필요성
🔹 예시 1: 데이터 전처리기를 만들어야 할 때
def clean_text(text):
return text.lower().replace(",", "")
간단하고 좋아 보이지만, 문제가 있다.
전처리 옵션이 사용자마다 달라질 때는?
대량의 데이터를 한 번에 처리해야 할 때는?
→ 함수 하나만으로는 처리 옵션을 일일이 넘기기 불편하고, 유지보수가 어려워진다.
🔧 클래스 도입
class TextCleaner:
def __init__(self, lower=True, remove_punc=True):
self.lower = lower
self.remove_punc = remove_punc
def clean(self, text):
if self.lower:
text = text.lower()
if self.remove_punc:
text = text.replace(",", "")
return text
cleaner1 = TextCleaner(lower=True, remove_punc=True)
cleaner2 = TextCleaner(lower=False)
print(cleaner1.clean("Hello, World!")) # hello world!
print(cleaner2.clean("Hello, World!")) # Hello world!
➡️ 각 전처리기는 자신의 상태를 기억하며 동작한다.
➡️ 마치 ‘설정 가능한 기계’를 쓰는 느낌이다.
🔹 예시 2: 모델 평가기를 관리할 때
def evaluate(preds, labels):
return sum([p == l for p, l in zip(preds, labels)]) / len(labels)
정확도(accuracy)만 측정할 땐 좋지만, F1, AUC, Precision 등 다양한 평가 지표를 써야 할 경우 함수가 기하급수적으로 늘어난다.
→ 함수만으로는 지표와 평가 데이터를 구조적으로 관리하기 어려워진다.
🔧 클래스 도입
class Evaluator:
def __init__(self, metric="accuracy"):
self.metric = metric
def evaluate(self, preds, labels):
if self.metric == "accuracy":
return sum(p == l for p, l in zip(preds, labels)) / len(labels)
elif self.metric == "f1":
return self._f1_score(preds, labels)
# 더 많은 지표 추가 가능
else:
raise ValueError("Unsupported metric")
def _f1_score(self, preds, labels):
# F1 계산 로직 (생략)
return 0.85
acc_evaluator = Evaluator(metric="accuracy")
f1_evaluator = Evaluator(metric="f1")
➡️ Evaluator
는 평가 지표라는 상태를 기억하고, 거기에 맞는 방식으로 평가를 수행한다.
클래스 선언
✅ 정리: 함수에서 클래스가 필요한 이유
문제 상황 | 함수로 구현 | 클래스로 구현 |
상태 저장 | 매번 인자로 넘겨줘야 함 | 내부 속성으로 저장 가능 |
기능 조합 | 여러 함수를 나열 | 메서드로 구조화 가능 |
설정 관리 | 파라미터가 점점 많아짐 | __init__ 으로 정리 |
확장성 | 새 기능마다 새 함수 | 메서드 추가로 유연하게 확장 가능 |
✅ 클래스 개념 쉽게 이해하기
개념 | 정의 | 비유 |
클래스 | 상태 + 기능을 함께 정의한 설계도 | 게시판 양식, 붕어빵 틀 |
인스턴스 | 클래스로 만든 실제 객체 | 작성된 게시물 한 장, 붕어빵 한 개 |
메서드 | 인스턴스가 수행할 수 있는 행동 | 게시글 수정, 삭제 |
속성 | 인스턴스가 가진 정보 | 작성자, 내용, 좋아요 수 |
✅ 실무에서는 이렇게 쓰인다
분야 | 활용 예시 |
데이터 전처리 | TextCleaner , Normalizer 등 클래스로 여러 설정 관리 |
모델 학습 | Trainer 클래스에서 학습, 평가, 저장까지 한 번에 |
파이프라인 구성 | 각 단계를 PipelineStep 클래스로 구성해 유연한 흐름 설계 |
➡️ 클래스는 복잡한 시스템을 구조화하고, 유지보수와 협업을 쉽게 만든다.
❓ 자주 묻는 질문
질문 | 답변 |
함수만으로도 가능한데 왜 클래스를 써야 하나요? | 간단한 작업은 함수로 충분하지만, 상태와 동작을 함께 다뤄야 하는 순간이 오면 클래스가 필요하다. |
인스턴스를 여러 개 만들면 각각 따로 동작하나요? | ✅ 그렇다. 각 인스턴스는 독립적인 객체이므로 서로 간섭하지 않는다. |
self 는 왜 꼭 써야 하나요? | self 는 인스턴스 자신을 가리키는 키워드로, 속성이나 메서드에 접근할 때 꼭 필요하다. |
✅ 함수는 동작을 담은 훌륭한 도구
def deposit(balance, amount):
return balance + amount
def withdraw(balance, amount):
return balance - amount
이런 함수는 간단한 입출금 기능을 재사용할 수 있어서 좋다.
하지만…
매번
balance
를 넘겨줘야 하고,여러 계좌를 다룰 때는
balance1
,balance2
, … 복잡해진다 😵
➡️ “상태”를 기억할 수 없다는 한계가 있다.
🧠 비유: 함수 vs 클래스
구분 | 비유 | 설명 |
함수 | 기능만 담긴 공구 상자 | 설정은 매번 직접 해야 함 |
클래스 | 기능 + 상태가 담긴 공구 키트 | 각 도구가 설정 상태를 기억함 |
✅ 생성자와 self 이해하기
class User:
def __init__(self, author, content):
self.author= author
self.content = content
__init__
: 인스턴스를 만들 때 자동 실행 (생성자)모든 클래스의 가장 기본이 되는 메소드
인스턴스가 처음 만들어질 때 어떻게 세팅할 것인지 결정
참고: __” ”__로 되어 있는 함수들을 Python에서 중요한 함수
self.name
: 인스턴스가 기억하는 속성self
: 클래스 내부에서 속성/메소드에 접근할 때User.name
: 클래스 밖에서 접근할 때
비유:
self
는 현재 조립 중인 제품
사용 방법
my_user = User("elice", "I love coding!")
생성자의 매개변수 vs. 클래스의 속성
from user import User
class Post:
# 생성자는 매개변수로 작성자와 내용을 받아,
# 그 값을 속성에 저장합니다.
def __init__(self, author, content):
# 속성들을 초기화합니다.
self.author = author
self.content = content
self.comments = []
self.likes = 0
# 새로운 사용자 me를 생성합니다.
me = User("Kiwi")
my_post = Post(me, "프로그래밍!")
print(my_post)
속성 만들 때 주의할 점
class Post: # Post를 할 때 생성되는 인스턴스의 틀
def __init__(self, author, content):
self.likes = 0 # 좋아요 개수
self.liked_users = [] # 좋아요를 누른 사람 리스트
my_post = Post("elice", "I love coding!")
my_post.likes += 1
“좋아요” 개수는 올라가지만, “좋아요”를 누른 사람은 추가가 안 됨.
- 이런 모순이 생기면 발견하기 어렵다.
따라서, 클래스를 만들 때 하나의 개념을 뜻하는 속성은 하나여야 한다.
메소드 만들기(개념과 속성 수 일치)
class Post:
def __init__(self, author):
self.author = author
self.liked_users = []
def like(self, user):
self.liked_users.append(user)
def num_likes(self):
return len(self.liked_users)
# 사용 방법
post = Post("Alex")
post.like("user1")
post.like("user2")
post.like("user3")
print(post.num_likes()) # 출력: 3
위처럼 “좋아요“ 개수를 세는 메소드를 만들어서 관리하는 게 좋다.
이렇게 하면, 누가 눌렀는지 자동으로 기록되고, 좋아요 개수는 자동으로
len(self.liked_users)
로 셀 수 있다.즉, 메소드를 속성처럼 사용하는 것이며,
이렇게 하면 속성의 값과 num_likes의 결과물이 항상 같게 된다.
💡 참고: @property
를 쓰면?
@property
def num_likes(self):
return len(self.liked_users)
이렇게 바꾸면 print할 때, ()
없이 속성처럼 쓸 수 있다.
print(post.num_likes) # 출력: 3
원하지 않는 값 배제하기
class Post:
def __init__(self, author):
...
my_post = Post("elice", 1457) # content로 글이 들어와야 하는데 숫자가 들어옴
my_post.like(["Hello","World"]) # user를 하나 받아야 하는데 리스트가 들어옴
- 값이 잘못 들어오면, 배제하고 알려주는 기능이 있어야 한다.
class User:
def __init__(self, year_of_birth):
if type(year_of_birth) is not int:
return # 값이 잘못 들어오면 아무 것도 없이 return
class Post:
def __init__(self, author, content):
if not isinstance(author, User):
return
if type(content) is not str:
return
타입 체크를 해서 잘못 들어오면 아무것도 없이 리턴
타입 비교를 할 때는 “is“와 “is not“을 사용해야 한다.
class User:
def __init__(self, year_of_birth):
if year_of_birth > 2005:
raise Exception("Too young")
- return을 사용하면 코드가 그냥 끝나지만, raise 함수를 사용하면 코드를 끝내면서 사용자에게 그 이유를 알려줄 수 있다.
✅ isinstance() & raise: 견고한 클래스 만들기
1️⃣ isinstance()
def set_age(age):
if not isinstance(age, int):
raise TypeError("나이는 숫자여야 합니다.")
➡️ 입력값이 올바른 타입인지 검사하는 방어코드
2️⃣ raise
def divide(a, b):
if b == 0:
raise ValueError("0으로 나눌 수 없습니다!")
return a / b
➡️ 잘못된 입력에 대해 명확한 에러 메시지와 함께 실행 중단
✨ raise와 사용하는 에러의 종류
raise ValueError("값이 이상해요")
raise TypeError("타입이 틀렸어요")
raise Exception("그냥 일반적인 에러예요")
ValueError
: 값이 잘못된 경우TypeError
: 타입이 안 맞는 경우Exception
: 가장 기본적인 에러 (다른 에러의 부모 클래스)
이런 식으로 다양한 에러 종류 중 하나를 선택해서 raise할 수 있다.
🚧 꼭 기억해야 할 포인트
raise
는 에러를 직접 발생시킬 때 쓴다.Exception
은 어떤 종류의 에러인지 알려주는 클래스다.직접 에러 메시지를 붙여서 사용자나 개발자에게 원인을 설명할 수 있다.
✅ 실무에서 클래스 + raise + 타입 검사 쓰이는 예
상황 | 설명 |
데이터 전처리기 | 옵션이 문자열인지 검사 |
모델 트레이너 | 하이퍼파라미터 유효성 검사 |
API 호출 | 필수 파라미터 빠졌는지 확인 후 raise |
def load_file(path):
if not isinstance(path, str):
raise TypeError("파일 경로는 문자열이어야 합니다.")
if not os.path.exists(path):
raise FileNotFoundError(f"파일이 없습니다: {path}")
➡️ 에러는 숨기는 게 아니라, 빨리 드러내야 한다.
🎯 마무리 핵심 정리
개념 | 설명 |
클래스 | 상태 + 동작을 묶는 설계도 |
인스턴스 | 클래스로 만든 실제 객체 |
self | 현재 객체 자신을 가리키는 예약어 |
init | 인스턴스를 초기화하는 생성자 |
isinstance | 타입을 검사하는 함수 |
raise | 명시적으로 에러를 발생시키는 문법 |
💡 클래스 공부는 객체지향의 출발점
이제 클래스에 익숙해졌다면, 다음은 상속과 다형성을 배우면 된다.
클래스를 통해 프로그램을 더 유연하고 구조화된 방식으로 설계할 수 있게 된다.
Subscribe to my newsletter
Read articles from KiwiChip directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

KiwiChip
KiwiChip
I'm currently learning Python and studying RAG (Retrieval-Augmented Generation).