Airflow Dag를 작성하다 보면 객체 지향 프로그래밍에 대한 중요성을 느낀다.
오랜만에 문법도 익힐 겸 ATM도 구현해보고 이전에 공부했던 전공 책을 꺼내어 OOP 개념과 python class 문법을 복습해보았다.
파이썬에서 클래스를 구현하는 기본 방법을 간단한 계산기 구현 예제로 살펴보겠습니다.
class Calculator:
def __init__(self):
self.result = 0
def add(self, num):
self.result += num
return self.result
cal1 = Calculator()
cal2 = Calculator()
class FourCal:
def __init__(self, first, second):
self.first = first
self.second = second
def setdata(self, first, second):
self.first = first
self.second = second
def add(self):
result = self.first + self.second
return result
class MoreFourCal(FourCal):
pass
a = MoreFourCal(4,2)
print(a.add())
이 예제에서는 기본적인 클래스 구현과 상속에 대해 보여줍니다. Calculator 클래스는 기본적인 덧셈 기능을 제공하며, FourCal 클래스는 두 숫자를 더하는 메서드를 포함합니다. MoreFourCal 클래스는 FourCal 클래스를 상속받아 추가 기능 없이 그대로 사용합니다. 이제 이를 기반으로 ATM기를 구현하여 객체 지향 프로그래밍(OOP)의 원칙을 다시 익혀보겠습니다.
OOP를 처음 접하는 분들에게는 어떠한 것도 참고하지 마시고 최소 기능만 구상하시고 ATM 구현해보시는 것을 추천합니다.
# Import Bank class from the bank module
from bank import Bank
# Define the class ATM which represents the ATM machine
class ATM:
# Constructor for ATM class. It takes a Bank object as argument
def __init__(self, bank: Bank):
self.bank = bank
self.card_number = None
self.account_number = None
# Method for inserting card. It validates the card_number with the bank
def insert_card(self, card_number: str) -> None:
if self.bank.validate_card(card_number):
self.card_number = card_number
else:
raise ValueError("Invalid card")
# Method for entering PIN. It validates the PIN with the bank
def enter_pin(self, pin: str) -> None:
if not self.bank.validate_pin(self.card_number, pin):
raise ValueError("Invalid PIN")
# Method for getting a list of accounts linked to the card
def get_accounts_list(self) -> list:
return self.bank.get_accounts(self.card_number)
# Method for selecting an account. It validates if the account_name exists in the bank
def select_account(self, account_name: str) -> None:
if account_name not in self.bank.get_accounts(self.card_number):
raise ValueError("Account does not exist")
self.account_number = account_name
# Method for checking the balance of the selected account
def check_balance(self) -> int:
return self.bank.get_balance(self.card_number, self.account_number)
# Method for depositing money into the selected account
def deposit_money(self, amount: int) -> None:
self.bank.deposit(self.card_number, self.account_number, amount)
# Method for withdrawing money from the selected account
def withdraw_money(self, amount: int) -> None:
self.bank.withdraw(self.card_number, self.account_number, amount)
# Define the class ATMController which controls the ATM machine
class ATMController:
def __init__(self, atm: ATM):
self.atm = atm
def insert_card(self, card_number: str) -> str:
try:
self.atm.insert_card(card_number)
return "Your card has been successfully inserted"
except ValueError as e:
return str(e)
def enter_pin(self, pin: str) -> str:
try:
self.atm.enter_pin(pin)
return "PIN number entered successfully"
except ValueError as e:
return str(e)
def get_accounts_list(self) -> list:
try:
return self.atm.get_accounts_list()
except ValueError as e:
return str(e)
def select_account(self, account_name: str) -> str:
try:
self.atm.select_account(account_name)
return "Account has been successfully selected"
except ValueError as e:
return str(e)
def check_balance(self) -> int:
try:
return self.atm.check_balance()
except ValueError as e:
return str(e)
def deposit_money(self, amount: int) -> str:
try:
self.atm.deposit_money(amount)
return "Deposit has been processed"
except ValueError as e:
return str(e)
def withdraw_money(self, amount: int) -> str:
try:
self.atm.withdraw_money(amount)
return "Withdrawal has been processed"
except ValueError as e:
return str(e)
- ATM 클래스는 Bank 클래스의 객체를 받아 초기화됩니다.
- insert_card, enter_pin, get_accounts_list, select_account, check_balance, deposit_money, withdraw_money 메서드를 통해 ATM의 기본 기능을 수행합니다.
- 각 메서드는 Bank 클래스의 메서드를 호출하여 필요한 작업을 수행합니다.
- ATMController 클래스는 ATM 객체를 받아 초기화됩니다.
- 각 메서드는 ATM 클래스의 메서드를 호출하여 결과를 반환하거나 예외를 처리합니다.
- insert_card, enter_pin, get_accounts_list, select_account, check_balance, deposit_money, withdraw_money 메서드를 통해 ATM의 기능을 제어합니다.
from abc import ABC, abstractmethod
# This class represents the blueprint for any bank
class Bank(ABC):
# Method to check if a card is valid
@abstractmethod
def validate_card(self, card_number: str) -> bool:
pass
# Method to verify the entered PIN
@abstractmethod
def validate_pin(self, card_number: str, pin: str) -> bool:
pass
# Method to retrieve all accounts associated with a card
@abstractmethod
def get_accounts(self, card_number: str) -> list:
pass
# Method to check balance of an account
@abstractmethod
def get_balance(self, card_number: str, account_number: str) -> int:
pass
# Method to deposit money into an account
@abstractmethod
def deposit(self, card_number, account_number: str, amount: int) -> None:
pass
# Method to withdraw money from an account
@abstractmethod
def withdraw(self, card_number, account_number: str, amount: int) -> None:
pass
# MockBank is a basic bank for testing purposes
class MockBank(Bank):
# MockBank has some predefined accounts for testing
def __init__(self):
self.accounts = {
"123456": {"pin": 1234, "accounts": {"account1": 5000, "account2": 10000}},
"654321": {"pin": 4321, "accounts": {"account1": 3000, "account2": 20000}}
}
def validate_card(self, card_number: str) -> bool:
return card_number in self.accounts
def validate_pin(self, card_number: str, pin: int) -> bool:
return self.accounts[card_number]["pin"] == pin
def get_accounts(self, card_number: str) -> list:
return list(self.accounts[card_number]["accounts"].keys())
def get_balance(self, card_number: str, account_name: str) -> int:
return self.accounts[card_number]["accounts"][account_name]
def deposit(self, card_number: str, account_name: str, amount: int) -> None:
self.accounts[card_number]["accounts"][account_name] += amount
def withdraw(self, card_number: str, account_name: str, amount: int) -> None:
if self.accounts[card_number]["accounts"][account_name] < amount:
raise ValueError("balance is insufficient")
self.accounts[card_number]["accounts"][account_name] -= amount
- Bank 클래스는 추상 클래스(ABC)로 정의되어 있으며, 여러 추상 메서드를 포함합니다.
- MockBank 클래스는 Bank 클래스를 상속받아 실제 구현을 제공합니다.
- validate_card, validate_pin, get_accounts, get_balance, deposit, withdraw 메서드를 통해 카드 유효성 검사, PIN 검사, 계좌 조회, 잔액 조회, 입금 및 출금 기능을 제공합니다.
from bank import MockBank
from atm import ATM, ATMController
import unittest
class ATMControllerTest(unittest.TestCase):
def setUp(self):
self.bank = MockBank()
self.atm = ATM(self.bank)
self.atm_controller = ATMController(self.atm)
def test_insert_card(self):
result = self.atm_controller.insert_card("123456")
self.assertEqual(result, "Your card has been successfully inserted")
def test_enter_pin(self):
self.atm_controller.insert_card("123456")
result = self.atm_controller.enter_pin(1234)
self.assertEqual(result, "PIN number entered successfully")
def test_get_accounts_list(self):
self.atm_controller.insert_card("123456")
result = self.atm_controller.get_accounts_list()
self.assertEqual(result, ["account1", "account2"])
def test_select_account(self):
self.atm_controller.insert_card("123456")
self.atm_controller.get_accounts_list()
result = self.atm_controller.select_account("account1")
self.assertEqual(result, "Account has been successfully selected")
def test_check_balance(self):
self.atm_controller.insert_card("123456")
self.atm_controller.get_accounts_list()
self.atm_controller.select_account("account1")
result = self.atm_controller.check_balance()
self.assertEqual(result, 5000)
def test_deposit_money(self):
self.atm_controller.insert_card("123456")
self.atm_controller.get_accounts_list()
self.atm_controller.select_account("account1")
result = self.atm_controller.deposit_money(1000)
self.assertEqual(result, "Deposit has been processed")
def test_withdraw_money(self):
self.atm_controller.insert_card("123456")
self.atm_controller.get_accounts_list()
self.atm_controller.select_account("account1")
result = self.atm_controller.withdraw_money(3000)
self.assertEqual(result, "Withdrawal has been processed")
if __name__ == '__main__':
unittest.main()
# if hard to understand unittest, use this
# def test_ATMController():
# bank = MockBank()
# atm = ATM(bank)
# atm_controller = ATMController(atm)
# result = atm_controller.insert_card("654321")
# assert result == "Your card has been successfully inserted"
# result = atm_controller.enter_pin(4321)
# assert result == "PIN number entered successfully"
# result = atm_controller.get_accounts_list()
# assert result == ["account1", "account2"]
# result = atm_controller.select_account("account1")
# assert result == "Account has been successfully selected"
# result = atm_controller.check_balance()
# assert result == 3000
# result = atm_controller.deposit_money(1000)
# assert result == "Deposit has been processed"
# result = atm_controller.withdraw_money(3000)
# assert result == "Withdrawal has been processed"
# test_ATMController()
- ATMControllerTest 클래스는 unittest.TestCase를 상속받아 ATM의 다양한 기능을 테스트합니다.
- setUp 메서드를 통해 테스트 환경을 초기화합니다.
- 각 테스트 메서드는 ATMController 클래스의 메서드를 호출하여 기능을 검증합니다.
해당 예제를 통해 기본적인 클래스 구현, 상속, 추상 클래스 사용 및 유닛 테스트 작성 방법을 알아보았습니다.
개인적인 생각으로, Airflow DAG(DAG: Directed Acyclic Graph)을 작성할 때 객체 지향 프로그래밍(OOP)이 중요한 이유는 특정 작업(task)을 클래스로 정의하고, 이를 여러 DAG에서 재사용하는 것이 Airflow 아키텍처의 지향점에 부합하다고 생각해서이다.
from airflow.models import BaseOperator
from airflow.utils.decorators import apply_defaults
class MyTaskOperator(BaseOperator):
@apply_defaults
def __init__(self, param1, param2, *args, **kwargs):
super(MyTaskOperator, self).__init__(*args, **kwargs)
self.param1 = param1
self.param2 = param2
def execute(self, context):
# 작업 실행 로직
print(f"Executing task with {self.param1} and {self.param2}")
from airflow import DAG
from airflow.operators.dummy_operator import DummyOperator
from datetime import datetime
# DAG 기본 설정
default_args = {
'owner': 'airflow',
'start_date': datetime(2023, 1, 1),
'retries': 1,
}
# DAG 정의
with DAG(dag_id='my_dag', default_args=default_args, schedule_interval='@daily') as dag:
start = DummyOperator(task_id='start')
# MyTaskOperator를 사용하여 작업 생성
task1 = MyTaskOperator(task_id='task1', param1='value1', param2='value2')
task2 = MyTaskOperator(task_id='task2', param1='value3', param2='value4')
end = DummyOperator(task_id='end')
# 작업 의존성 설정
start >> task1 >> task2 >> end
MyTaskOperator 클래스 정의:
MyTaskOperator
는 BaseOperator
를 상속받아 구현한 클래스입니다.__init__
메서드에서 필요한 매개변수를 초기화합니다.execute
메서드에서 실제 작업을 수행하는 로직을 작성합니다.DAG 정의:
MyTaskOperator
를 사용하여 작업을 생성합니다.start
, task1
, task2
, end
작업을 정의하고, 작업 간의 의존성을 설정합니다.Airflow DAG를 작성할 때 객체 지향 프로그래밍(OOP)을 활용하면 코드의 재사용성, 유지보수성, 확장성, 가독성을 크게 향상시킬 수 있기에, 예시처럼 작업을 클래스로 정의하고 이를 여러 DAG에서 재사용하면, 복잡한 워크플로우를 보다 효율적으로 관리할 수 있습니다 :)
ATM 코드에 대한 실행방법과 추가로 현금함을 구현하기 위한 가이드라인은 아래 github 링크의 README.md에 포함되어 있습니다 :)
소스코드: https://github.com/idle-danie/OOP_atm