부트캠프와 다른 AI학교,
AI는 아이펠에서 배우세요
#소프트웨어 

객체 지향 쉽지 않다면? OOP 입문자를 위한 친절한 가이드

프로그래밍 페러다임으로 주로 언급되는 절차 지향 프로그래밍(Procedural Programming)과 객체 지향 프로그래밍(Object-Oriented Programming, OOP)에 대해, 예제 코드로 비교해봅니다.

2024-03-15 | 나융

프로그래밍에는 각 상황에 적합한 정말 다양한 방식이 있습니다. 그중에서도 주로 언급되는 두 가지가 바로 ‘절차 지향 프로그래밍(Procedural Programming)’과 ‘객체 지향 프로그래밍(Object-Oriented Programming, OOP)’입니다. 예제 코드로 비교해보는 친절한 가이드 시작하겠습니다.

 

절차 지향 프로그래밍

절차 지향 프로그래밍은 마치 레시피를 따르는 것과 비슷합니다. 요리를 할 때는 재료를 준비하고, 재료를 다듬고, 조리하는 단계를 차례대로 따라갑니다. 절차 지향 프로그래밍에서는 프로그램을 여러 개의 순서대로 진행되는 절차(함수나 명령)들로 구성합니다. 이런 방식은 프로그램이 어떻게 실행되어야 하는지, 단계별로 무엇을 해야 하는지를 중시합니다.

예를 들어, “학교에서 집까지 가는 길을 찾는 프로그램”을 만든다고 생각해 보세요. 절차 지향 방식으로 접근하면, 먼저 학교에서 출발해야 한다는 절차를 정의하면서 시작합니다. 그 다음 길을 따라 가는 방법, 교통 수단을 선택하는 방법 등을 차례로 명령으로 만들어 나가면 됩니다.

객체 지향 프로그래밍

객체 지향 프로그래밍은 좀 더 현실 세계에 가까운 방식으로 프로그래밍하는 것입니다. 현실 세계에는 많은 ‘객체’들이 있듯이, OOP 에서는 모든 것을 객체로 바라봅니다. 여기서 객체란 데이터(속성)와 그 데이터와 관련된 기능(메서드)을 하나로 묶은 것을 말합니다.

객체 지향 방식을 적용할 때는 프로그램이 해결해야 할 문제를 작은 부분들로 나누고, 각 부분을 ‘객체’로 만듭니다. 이 객체들은 서로 ‘상호작용’하면서 전체 프로그램이 작동하게 됩니다. 객체들은 각자의 데이터를 가지고 있으며, 다른 객체들과 메시지를 주고받으며 상호작용할 수 있습니다.

“학교에서 집까지 가는 길을 찾는 프로그램”을 객체 지향 방식으로 접근하면, ‘학교’, ‘집’, ‘길’, ‘교통 수단’ 등을 각각의 객체로 정의합니다. 이 객체들이 서로 상호작용하면서 최종 목적지인 집에 도착하는 방법을 찾도록 만듭니다.

 


두 방식을 서로 비교 해봅시다

  • 절차 지향 프로그래밍은 프로그램을 명령의 순서와 절차로 보는 방식입니다. 이 방식은 간단한 프로그램을 빠르게 만들 때 유용합니다.
  • 객체 지향 프로그래밍은 현실 세계의 객체를 모델링해서 프로그램을 구성하는 방식입니다. 이 방식은 복잡하거나 유지보수와 확장성이 중요한 프로그램을 개발할 때 강점을 보입니다.

결국, 이 두 방식은 프로그램을 어떻게 바라보고 구성하느냐의 차이입니다. 절차 지향은 단계를 따라가며 문제를 해결합니다. 객체 지향은 현실 세계의 객체들이 상호작용하며 문제를 해결하는 방식이죠. 프로그래밍을 배울 때는 이 두 가지 방식 모두를 이해하고, 상황에 맞게 적절히 선택해서 사용하는 것이 중요합니다!

간단한 예제로 “은행 계좌 관리 시스템”을 만들어 보겠습니다. 이 시스템에서는 계좌의 잔액을 확인하고, 입금과 출금 기능을 수행할 수 있어야 합니다. 먼저 절차 지향 방식으로 구현한 후, 같은 기능을 객체 지향 방식으로 구현해 비교해 보겠습니다.

 

예제로 보는 절차 지향

절차 지향적 코딩에서는 함수를 중심으로 코드를 작성합니다. 여기서는 계좌의 잔액을 전역 변수로 관리하며, 입금과 출금을 위한 함수를 정의합니다.

 

# 절차 지향 프로그래밍 예제

# 계좌 잔액
account_balance = 0

# 잔액 확인 함수
def check_balance():
    global account_balance
    print(f"현재 잔액은 {account_balance}원 입니다.")

# 입금 함수
def deposit(amount):
    global account_balance
    account_balance += amount
    print(f"{amount}원을 입금했습니다.")

# 출금 함수
def withdraw(amount):
    global account_balance
    if account_balance >= amount:
        account_balance -= amount
        print(f"{amount}원을 출금했습니다.")
    else:
        print("잔액이 부족합니다.")

# 함수 사용 예시
check_balance()   # 현재 잔액은 0 원 입니다.
deposit(50000)   # 50000 원을 입금했습니다.
check_balance()   # 현재 잔액은 50000 원 입니다.
withdraw(20000)   # 20000 원을 출금했습니다.
check_balance()   # 현재 잔액은 30000 원입니다.

절차 지향적 스크립트에서는 주로 전역 변수를 활용하며, 프로그램의 어느 곳에서나 접근하고 수정할 수 있습니다. 또한 코드가 특정 작업을 수행하는 함수들 단위로 분리되어 있습니다. 이러한 패턴에서는 코드가 위에서 아래로 순차적으로 실행되는 구조를 따릅니다. 예를 들어, 먼저 잔액을 확인한 다음(check_balance), 입금(deposit)을 합니다. 다시 잔액을 확인하며, 마지막으로 출금(withdraw)합니다. 또 OOP 에서는 데이터(여기서는 계좌 잔액)와 이를 조작하는 행동(입금, 출금)이 분리되어 있습니다. 반면, 객체 지향 프로그래밍에서는 데이터와 이를 조작하는 메서드(행동)가 하나의 ‘객체’ 내에 함께 묶여 있습니다. 만약 스크립트가 더욱 길어진다면, 데이터와 행동을 비교하면서 확인하기 매우 번거로워 질 것을 예상할 수 있죠.

위 예제는 매우 간단하기 때문에 모듈화하기 어렵다는 것이 큰 문제가 되지 않습니다. 하지만, 더 복잡한 프로그램에서는 각 함수들이 특정한 데이터(여기서는 account_balance)에 대해 서로 의존하는경우가 있습니다. 만약 이런 연결속에서 작은 변경 사항이 생긴다면 많은 부분에 영향을 줄 수 있겠죠. 이는 코드의 유지보수와 확장성을 어렵게 만듭니다.

 

예제로 보는 객체 지향

객체지향적 코딩에서는 계좌를 하나의 ‘객체’로 취급합니다. 이 객체는 자신의 잔액을 속성으로 가지고 입금과 출금 같은 기능을 메서드(함수)로 가집니다.

# 객체 지향 프로그래밍 예제

class BankAccount:
    def __init__(self):
        self.balance = 0  # 계좌의 초기 잔액을 0으로 설정
    
    def check_balance(self):
        print(f"현재 잔액은 {self.balance}원 입니다.")
    
    def deposit(self, amount):
        self.balance += amount
        print(f"{amount}원을 입금했습니다.")
    
    def withdraw(self, amount):
        if self.balance >= amount:
            self.balance -= amount
            print(f"{amount}원을 출금했습니다.")
        else:
            print("잔액이 부족합니다.")

# 객체 사용 예시
account = BankAccount()   # 객체 생성
account.check_balance()   # 현재 잔액은 0 원 입니다.
account.deposit(50000)   # 50000 원을 입금했습니다.
account.check_balance()   # 현재 잔액은 50000 원 입니다.
account.withdraw(20000)   # 20000 원을 출금했습니다.
account.check_balance()   # 현재 잔액은 30000 원입니다.

절차 지향적 코드는 기능 중심으로 구성되어 있고, 전역 변수를 통해 상태를 관리합니다. 반면, 객체 지향적 코드는 상태와 행위를 하나의 클래스 라는 단위로 묶어 관리합니다. 클래스 내부를 수정하면 해당 클래스를 이어받는 여러 객체들과 변경사항을 공유할 수 있습니다. 이러한 면을 통해 코드의 재사용성과 유지보수성이 더 상승합니다.

객체 지향적 코딩이 특히 유용한 상황 중 하나는 여러 개체가 서로 다른 행동을 해야 할 때입니다. 예를 들어, 은행 시스템을 확장하여 다양한 종류의 계좌(예: 저축 계좌, 체크 계좌)를 관리해야 하는 경우를 생각해 볼 수 있습니다. OOP는 이런 상황에서 코드의 재사용성과 확장성을 크게 향상시킵니다.

각 계좌 종류에 따라 다른 규칙(예: 이자율, 수수료 등)을 적용해야 한다고 가정해 보겠습니다. 객체 지향 접근 방식을 사용하면, ‘계좌’라는 기본 클래스를 정의하고, 이를 상속받아 각 계좌 종류에 맞는 특성을 갖는 서브클래스를 만들 수 있습니다.

 

 


 

# 좀 더 상세한 개념을 이해하고 싶은 분들을 위한 심화 예제

# 기본 계좌 클래스
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        print(f"{self.owner}님, {amount}원을 입금하셨습니다. 현재 잔액은 {self.balance}원입니다.")

    def withdraw(self, amount):
        if self.balance >= amount:
            self.balance -= amount
            print(f"{self.owner}님, {amount}원을 출금하셨습니다. 현재 잔액은 {self.balance}원입니다.")
        else:
            print("잔액이 부족합니다.")

# 저축 계좌 서브클래스
class SavingsAccount(BankAccount):
    def __init__(self, owner, balance=0, interest_rate=0.02):
        super().__init__(owner, balance)
        self.interest_rate = interest_rate

    def apply_interest(self):
        interest = self.balance * self.interest_rate
        self.balance += interest
        print(f"{self.owner}님의 계좌에 이자 {interest}원이 지급되었습니다. 현재 잔액은 {self.balance}원입니다.")

# 체크 계좌 서브클래스
class CheckingAccount(BankAccount):
    def __init__(self, owner, balance=0, transaction_fee=100):
        super().__init__(owner, balance)
        self.transaction_fee = transaction_fee

    def withdraw(self, amount):
        if self.balance >= amount + self.transaction_fee:
            self.balance -= (amount + self.transaction_fee)
            print(f"{self.owner}님, {amount}원을 출금하셨습니다. 수수료 {self.transaction_fee}원이 부과되었습니다. 현재 잔액은 {self.balance}원입니다.")
        else:
            print("잔액이 부족합니다.")

# 계좌 생성 및 사용 예시
# 각 서브 클래스는 기본 BankAccount 클래스의 속성과 기능을 상속받았습니다.
savings = SavingsAccount("홍길동", 10000)   # 어떤 출력이 나타날지 예상해보고, 실제 출력과 비교해봅시다
savings.deposit(5000)
savings.apply_interest()

checking = CheckingAccount("이순신", 20000)
checking.withdraw(5000)

 

이 예제에서는 BankAccount라는 기본 클래스를 만들고, 이를 상속받는 SavingsAccountCheckingAccount 클래스를 정의했습니다. 각 클래스는 상속을 통해 BankAccount의 기능을 재사용하면서, 각 계좌 유형에 특화된 기능(예: 이자 적용, 거래 수수료 부과)을 추가로 구현할 수 있습니다. 각 객체는 서로 구분되어 같은 메소드를 활용하더라도 서로 다른 상태를 유지할 수 있습니다.

마무리하며

객체 지향 프로그래밍은 코드의 재사용성과 유지보수성을 높이고, 시스템의 확장성을 개선하는데 유리합니다. 특히, 여러 유형의 객체가 서로 다른 행동을 해야 하는 복잡한 시스템을 개발할 때 그 장점이 두드러집니다.

첫 술에 배부를 순 없겠죠! 중요한 프로그래밍 페러다임 중 하나인 객체 지향 프로그래밍에 대해서 간략하게 알아보았습니다. 더 상세한 내용이 궁금하시다면 OOP 의 S.O.L.I.D 원칙이나, 중요 개념인 상속, 캡슐화, 다형성 등에 대해 한 발자국 더 공부해 볼 수 있겠습니다.

 

 

fullstack bootcamp