본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로소프트웨어 공학 › 설계 › 객체지향

객체지향 프로그래밍

목차

객체지향 프로그래밍: 상태와 규칙을 함께 다루기

객체지향 프로그래밍(OOP)은 관련된 상태와 동작을 객체에 묶고, 객체 사이의 협력으로 기능을 구현하는 방식이다. 핵심은 현실 사물을 클래스 이름으로 옮기는 일이 아니라 누가 어떤 상태 변경을 책임지는지 결정하는 데 있다. 절차적 코드도 잘 설계하면 재사용 가능하고, 객체를 사용해도 책임이 뒤섞이면 변경하기 어렵다.

왜 데이터와 동작을 묶을까?

주문 항목의 quantity가 항상 1 이상이어야 한다고 하자. 필드를 외부에 공개하면 화면, 배치, API가 각자 값을 바꾸므로 모든 호출 경로에서 검증해야 한다. 객체가 변경 메서드를 제공하면 규칙을 한곳에서 강제할 수 있다.

final class OrderLine {
    private final String productId;
    private int quantity;

    OrderLine(String productId, int quantity) {
        if (productId == null || productId.isBlank()) {
            throw new IllegalArgumentException("상품 ID가 필요합니다");
        }
        this.productId = productId;
        changeQuantity(quantity);
    }

    void changeQuantity(int newQuantity) {
        if (newQuantity < 1) {
            throw new IllegalArgumentException("수량은 1 이상이어야 합니다");
        }
        this.quantity = newQuantity;
    }

    int quantity() { return quantity; }
}

생성자도 changeQuantity를 거치므로 생성 직후와 변경 후에 같은 규칙이 적용된다. 외부는 quantity를 직접 쓰지 못한다. 이것이 캡슐화와 정보 은닉의 실질적인 효과다. 단, 여러 객체를 함께 바꿔야 하는 규칙이라면 한 객체의 메서드만으로 충분하지 않다. 주문 총수량 상한처럼 주문 항목 전체를 봐야 하는 규칙은 주문 객체가 관리하는 편이 자연스럽다.

추상화와 다형성: 호출하는 쪽을 안정적으로 유지하기

배송비를 계산할 때 택배와 직접 수령의 규칙이 다르다고 하자. 호출자가 if (type == ...)를 여러 곳에 반복하는 대신, 필요한 동작을 인터페이스로 표현할 수 있다.

interface DeliveryPolicy {
    int feeWon(int subtotalWon);
}

final class CourierDelivery implements DeliveryPolicy {
    public int feeWon(int subtotalWon) {
        return subtotalWon >= 30_000 ? 0 : 3_000;
    }
}

final class StorePickup implements DeliveryPolicy {
    public int feeWon(int subtotalWon) {
        return 0;
    }
}

final class Checkout {
    private final DeliveryPolicy deliveryPolicy;

    Checkout(DeliveryPolicy deliveryPolicy) {
        this.deliveryPolicy = deliveryPolicy;
    }

    int totalWon(int subtotalWon) {
        return subtotalWon + deliveryPolicy.feeWon(subtotalWon);
    }
}

new Checkout(new CourierDelivery()).totalWon(20_000)은 23,000원, StorePickup을 넘기면 20,000원이다. Checkout은 배송 정책의 구체 클래스가 아닌 DeliveryPolicy의 동작만 안다. 이를 추상화라 하고, 같은 feeWon 호출이 객체에 따라 다르게 실행되는 성질을 다형성이라 한다. 여기서는 정책 객체를 생성자로 전달하는 합성을 사용했다.

상속은 언제 쓸까?

상속은 하위 클래스가 상위 타입의 계약을 지킬 수 있을 때 유용하다. 단순히 코드를 공유하려고 Order를 상속한 DiscountedOrder를 만들면, 상위 클래스의 상태 변경 규칙이나 저장 방식이 바뀔 때 하위 클래스까지 영향을 받는다. 할인 규칙만 달라진다면 DiscountPolicy를 별도 객체로 두고 주문에 전달하는 편이 책임을 분명히 한다. 상속은 기존 기능을 임의로 제거할 수 있다는 뜻도 아니다. 상위 타입을 기대하는 호출자가 하위 타입을 받아도 약속된 동작을 사용할 수 있어야 한다.

객체마다 인터페이스를 만들 필요는 없다. 구현이 하나뿐이고 변화할 이유도 없는 단순 계산에는 함수 하나가 더 명료할 수 있다. 객체지향 설계의 기준은 클래스 수가 아니라, 변경되는 규칙의 소유자가 분명하고 잘못된 상태가 만들어지기 어려운가에 있다.

같은 카테고리의 글