💻 프로그래밍

OOP

Object-Oriented Programming

객체지향 프로그래밍. 캡슐화, 상속, 다형성, 추상화가 핵심 원칙. SOLID 원칙을 통해 유지보수성 높은 소프트웨어 설계.

📖 상세 설명

OOP(Object-Oriented Programming)는 프로그램을 객체(Object)의 집합으로 모델링하는 프로그래밍 패러다임입니다. 네 가지 핵심 원칙으로 구성됩니다. 캡슐화(Encapsulation)는 데이터와 메서드를 하나로 묶고 외부 접근을 제한합니다. 상속(Inheritance)은 부모 클래스의 속성과 메서드를 자식 클래스가 물려받습니다. 다형성(Polymorphism)은 같은 인터페이스로 다양한 구현을 제공합니다. 추상화(Abstraction)는 복잡한 구현을 숨기고 필수 기능만 노출합니다.

OOP의 역사는 1960년대로 거슬러 올라갑니다. 노르웨이에서 개발된 Simula(1967)가 최초의 객체지향 언어로, 클래스와 객체 개념을 도입했습니다. 이후 Xerox PARC에서 Alan Kay가 개발한 Smalltalk(1972)는 "모든 것이 객체"라는 순수 OOP 철학을 확립했습니다. Smalltalk에서 메시지 패싱, 동적 타이핑 개념이 발전했으며, 이는 현대 Python, Ruby에도 영향을 주었습니다. C++(1983), Java(1995)의 등장으로 OOP는 산업 표준이 되었습니다.

SOLID 원칙은 Robert C. Martin이 정립한 OOP 설계 5원칙입니다. SRP(Single Responsibility): 클래스는 하나의 책임만 가져야 합니다. OCP(Open-Closed): 확장에는 열려있고 수정에는 닫혀야 합니다. LSP(Liskov Substitution): 자식 클래스는 부모 클래스를 대체할 수 있어야 합니다. ISP(Interface Segregation): 인터페이스는 작고 구체적으로 분리해야 합니다. DIP(Dependency Inversion): 추상화에 의존하고 구체 구현에 의존하지 말아야 합니다.

OOP vs FP(함수형 프로그래밍)는 상호 배타적이 아닌 보완적 관계입니다. OOP는 상태와 행위를 객체로 묶어 실세계 모델링에 강합니다. FP는 불변성과 순수 함수로 부작용을 최소화합니다. 현대 언어(Kotlin, Scala, TypeScript)는 두 패러다임을 혼합합니다. AI/ML 코드에서는 데이터 파이프라인에 FP를, 모델 아키텍처에 OOP를 사용하는 것이 일반적입니다. 결국 문제에 맞는 패러다임 선택이 중요합니다.

💻 코드 예제

"""
OOP 핵심 개념 예제 - Python
캡슐화, 상속, 다형성, 추상화 + SOLID 원칙 적용
"""

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Protocol


# ===== 추상화 (Abstraction) =====
class PaymentProcessor(ABC):
    """결제 처리기 추상 클래스 - ISP 원칙 적용"""

    @abstractmethod
    def process_payment(self, amount: float) -> bool:
        """결제 처리 (구현 필수)"""
        pass

    @abstractmethod
    def refund(self, transaction_id: str) -> bool:
        """환불 처리"""
        pass


# ===== 캡슐화 (Encapsulation) =====
@dataclass
class Money:
    """금액을 캡슐화한 값 객체"""
    _amount: float
    _currency: str = "KRW"

    @property
    def amount(self) -> float:
        return self._amount

    def add(self, other: 'Money') -> 'Money':
        if self._currency != other._currency:
            raise ValueError("통화가 다릅니다")
        return Money(self._amount + other._amount, self._currency)

    def __str__(self) -> str:
        return f"{self._amount:,.0f} {self._currency}"


# ===== 상속 (Inheritance) + 다형성 (Polymorphism) =====
class CreditCardProcessor(PaymentProcessor):
    """신용카드 결제 - LSP 원칙 준수"""

    def __init__(self, api_key: str):
        self._api_key = api_key
        self._transactions: dict = {}

    def process_payment(self, amount: float) -> bool:
        # 실제로는 PG사 API 호출
        transaction_id = f"CC_{id(self)}"
        self._transactions[transaction_id] = amount
        print(f"[신용카드] {amount:,.0f}원 결제 완료")
        return True

    def refund(self, transaction_id: str) -> bool:
        if transaction_id in self._transactions:
            print(f"[신용카드] {transaction_id} 환불 완료")
            return True
        return False


class KakaoPayProcessor(PaymentProcessor):
    """카카오페이 결제 - 다형성 활용"""

    def process_payment(self, amount: float) -> bool:
        print(f"[카카오페이] {amount:,.0f}원 결제 완료")
        return True

    def refund(self, transaction_id: str) -> bool:
        print(f"[카카오페이] {transaction_id} 환불 완료")
        return True


# ===== DIP 원칙 - 추상화에 의존 =====
class OrderService:
    """주문 서비스 - DIP 원칙 적용 (구체 클래스가 아닌 추상화에 의존)"""

    def __init__(self, payment_processor: PaymentProcessor):
        # 구체 클래스가 아닌 추상 클래스에 의존
        self._processor = payment_processor

    def checkout(self, items: List[dict]) -> bool:
        total = sum(item['price'] * item['quantity'] for item in items)
        print(f"\n총 금액: {total:,.0f}원")
        return self._processor.process_payment(total)


# ===== SRP 원칙 - 단일 책임 =====
class OrderValidator:
    """주문 검증만 담당 - SRP 원칙"""

    @staticmethod
    def validate(items: List[dict]) -> bool:
        if not items:
            raise ValueError("주문 항목이 비어있습니다")
        for item in items:
            if item.get('quantity', 0) <= 0:
                raise ValueError("수량은 1 이상이어야 합니다")
        return True


# ===== 실행 예시 =====
if __name__ == "__main__":
    # 다형성 활용: 동일 인터페이스로 다양한 결제 수단 처리
    items = [
        {"name": "AI 강의", "price": 50000, "quantity": 1},
        {"name": "교재", "price": 30000, "quantity": 2}
    ]

    # 신용카드로 결제
    cc_processor = CreditCardProcessor(api_key="test_key")
    order_service = OrderService(cc_processor)
    order_service.checkout(items)

    # 카카오페이로 결제 (동일 코드, 다른 결제 수단)
    kakao_processor = KakaoPayProcessor()
    order_service = OrderService(kakao_processor)
    order_service.checkout(items)

🗣️ 실무 대화 예시

기술 면접에서 SOLID 원칙 질문

"SOLID 중 가장 중요하게 생각하는 원칙은 DIP입니다. 구체 클래스가 아닌 추상화에 의존하면 테스트가 쉬워지고, 결제 모듈처럼 외부 의존성이 있는 코드도 Mock으로 대체할 수 있습니다. 저는 항상 생성자 주입으로 의존성을 받고, 인터페이스를 먼저 정의한 후 구현합니다."

코드 리뷰에서 설계 패턴 논의

"이 OrderService가 결제도 처리하고 알림도 보내고 로깅도 하네요. SRP 위반입니다. 결제는 PaymentProcessor, 알림은 NotificationService로 분리하고, OrderService는 이들을 조율하는 역할만 하면 됩니다. 각 클래스가 변경되는 이유는 하나여야 해요."

아키텍처 설계 미팅에서

"이번 결제 시스템은 Strategy 패턴으로 설계하죠. PaymentStrategy 인터페이스를 두고, CreditCard, KakaoPay, NaverPay 구현체를 만듭니다. 새 결제 수단 추가 시 기존 코드 수정 없이 새 클래스만 추가하면 되니까 OCP 원칙도 만족합니다."

⚠️ 주의사항

⚠️
과도한 상속 금지 (Fragile Base Class Problem)

상속 계층이 깊어지면 부모 클래스 변경 시 모든 자식에 영향을 줍니다. 3단계 이상의 상속은 피하고, "is-a" 관계가 확실할 때만 상속을 사용하세요. 대부분의 경우 조합(Composition)이 더 유연합니다.

⚠️
인터페이스 분리 (Fat Interface 회피)

하나의 거대한 인터페이스보다 작고 구체적인 여러 인터페이스가 낫습니다. 클라이언트가 사용하지 않는 메서드에 의존하게 만들지 마세요. "결제" 인터페이스와 "환불" 인터페이스를 분리하는 것이 ISP 원칙입니다.

⚠️
상속보다 조합 우선 (Favor Composition over Inheritance)

GoF 디자인 패턴의 핵심 원칙입니다. 상속은 컴파일 타임에 고정되지만, 조합은 런타임에 교체 가능합니다. Logger를 상속받는 대신 Logger를 필드로 갖고 위임(Delegation)하세요. 테스트와 유지보수가 훨씬 쉬워집니다.

🔗 관련 용어

📚 더 배우기