객체 지향 설계 및 구현

💡 가장 자주 접했던 객체 지향 설계에 대하여 학습하였습니다.

 

객체 지향 프로그래밍(OOP)

  • 프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고, 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 기법입니다.
  • 명령형 프로그래밍인 절차지향 프로그래밍의 단점을 보완하기 위하여 등장하였습니다.

절차지향 프로그래밍

  • 무엇을 어떤 절차로 할 것인가를 중점으로 두고 있으며, 순차적인 처리를 중요시 하는 프로그래밍 기법입니다.
  • 프로그램 전체가 유기적으로 연결되도록 하며, 대표적으로 C언어가 있습니다.
  • 실행속도가 빠르지만, 코드의 순서가 바뀌면 동일한 결과를 보장하기 어렵다는 단점이 있습니다.

절차지향 프로그래밍에서 개선점

  • 절차지향 프로그래밍은 모듈을 재활용하기 어렵기 때문에 대규모 프로젝트에서 반복된 코드로 비효율 적인 코드가 될 수 있습니다.
  • 따라서 아래와 같은 해결 방안을 고민할 수 있으며, 이는 객체 지향 프로그래밍의 특징이 되었습니다.
    • 데이터와 이를 처리하는 함수를 어떻게 묶을 것인지
    • 프로그램을 어떻게 구조적으로 나눌 것인지
    • 코드의 재사용 성을 높이는 방법은 무엇인지

객체 지향 패러다임

  • 객체 지향 패러다임 관점에서 핵심은 역할(role), 책임(responsibility), 협력(collaboration) 입니다.
  • 협력을 구성하기 위해 적절한 객체를 찾고, 적절한 책임을 할당하는 과정이 중요합니다.

협력

  • 어떤 기능을 구현하기 위해 다양한 객체들이 메시지를 주고 받으면서 상호작용하는 것입니다.
  • 두 객체 사이의 협력은 하나의 객체가 다른 객체에게 도움을 요청할 때 시작됩니다.
  • 이를 메시지 전송이라고 하며, 객체 사이의 협력을 위한 유일한 커뮤니케이션 수단입니다.
  • 다른 객체의 상세한 내부 구현에 직접 접근할 수 없기 때문에 메시지 전송을 통해서만 요청을 전달합니다.

책임

  • 객체가 협력에 참여하기 위해 수행하는 로직입니다.
  • 협력에 필요한 행동을 수행할 수 있는 적절한 객체를 찾고, 그 객체가 수행하는 행동을 결정합니다.
  • 이는 하는 것과 아는 것으로 구분됩니다.
    • 하는것
      • 객체를 생성하거나 계산을 수행
      • 다른 객체의 행동을 시작시킴
      • 다른 객체의 활동을 제어하고 조절
    • 아는것
      • 사적인 정보에 관해 아는 것
      • 관련된 객체에 관해 아는 것
      • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
class Book {
    // 책 정보를 알고 있다. => PricingPolicy
    // 대여 가격을 계산한다.
    val title: String
    val author: String

    fun calculateRentalPrice(): Double {
        return pricingPolicy.calculateDiscountedPrice(this)
    }

    private val pricingPolicy: PricingPolicy = DefaultPricingPolicy()
}

class PricingPolicy {
    // 가격 정책을 알고 있다. => RentalCondition
    // 할인된 대여 가격을 계산한다.
    fun calculateDiscountedPrice(book: Book): Double {
        return getBasePrice(book)
    }

    private fun getBasePrice(book: Book): Double {
        // 기본 대여 가격을 계산
        return 5.0 // 예시로 기본 가격을 5.0으로 설정
    }
}
  • 적절한 협력이 적절한 책임을 제공하고, 적절한 책임을 객체에 할당해야만 단순하고 유연한 설계를 창조합니다.
  • 객체에게 얼마나 적절한 책임을 할당하느냐가 설계의 전체적인 품질을 결정합니다.

역할

  • 객체들이 협력 안에서 수행하는 책임들이 모여 객체가 수행하는 역할을 구성합니다.
  • 객체는 협력이라는 문맥 안에서 특정한 목적을 갖게 되는데, 객체가 맡게 되는 책임의 집합을 역할이라고 합니다.
  • 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있습니다.
  • 역할은 다른 것으로 교체할 수 있는 책임의 집합입니다.
  • 일반적으로 객체가 협력에 참여하는 잠시 동안에만 존재하는 일시적인 개념으로 볼 수 있습니다.

객체지향 설계 및 구현 방식

  • 객체지향 설계 방식으로 아래 두가지 방법이 있습니다.
    • Bottom Up 설계
    • Top Down 설계

Bottom Up

  • 일단 구현 후 지속적인 리팩터링을 진행합니다.
  • 최상위 함수 구현으로 시작한 후 지속적인 리팩터링을 진행하며, 객체지향 생활 체조 원칙과 클린 코드 원칙을 참고합니다.
  • 객체지향 생활 체조 총정리

Top Down

  • 책임 주도 설계라고도 하며, 프로그래밍의 책임을 찾고 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계합니다.

책임 주도 설계 과정

  • 사용자에게 제공해야 하는 책임을 파악하고 더 작은 책임으로 분할합니다.
  • 분할 된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당합니다.
  • 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾아 책임을 할당합니다.
  • 이를 통해 두 객체가 협력하게 할 수 있으며, 구현이 아닌 책임에 집중해야 합니다.

객체의 협력

  • 객체들이 협력하는 과정에서 의존성을 어떻게 부여할 것인지를 결정해야 합니다.
  • 의존성을 어떻게 관리하느냐에 따라 유연한 설계 여부가 달라질 수 있습니다.

컴파일 타임 의존성

class Dog {
    fun bow() {
        val bowRepository = BowRepository()
        if (bowRepository.canBow()) {
            this.level = level.levelUp()
        }
    }
}

런타임 의존성

class Dog {
    fun bow(bowRepository: BowRepository) {
        if (bowRepository.canBow()) {
            this.level = level.levelUp()
        }
    }
}
  • 유연한 설계는 컴파일 타임 의존성을 런타임 의존성으로 대체해야 합니다.
  • 런타임 의존성을 통해 테스트하기 쉬운 설계를 할 수 있으며, 유연한 설계가 가능해 집니다.

객체지향의 한계점

함수의 비일관성

  • 함수형 프로그래밍과 연관지었을 때, 객체지향 프로그래밍은 함수의 비일관성 문제가 발생합니다.
  • 객체는 현재 상태 값에 의존하여 동작하기 때문에 함수를 매번 호출할 때 다른 결과값이 반환됩니다.
  • 이는 테스트 케이스를 작성하기 어렵게 되며, 같은 input에 매번 다른 output이 나와 예측이 힘들고 디버깅 시 출력에 문제가 있는지 여부를 확인하기 어렵습니다.

객체 간 의존성 증가

  • 객체 간 상호 작용을 위해 함수가 다른 객체의 함수를 호출합니다.
  • 이 과정에서 오버헤드를 발생시킬 수 있고, Side-Effect 문제가 발생하여 재사용성이 떨어지고 프로그램의 복잡도가 증가할 수 있습니다.

객체 상태 변화 통제와 추적의 어려움

  • 대부분의 함수는 객체 내 변수들을 외부에서 변경 가능하게 하는데, 다수의 외부 경로를 통해 상태가 변경되는 경우 디버깅이 어려울 수 있습니다.

참고

https://arc.net/l/quote/lrdroskq

https://velog.io/@dbworud/프로그래밍-패러다임-객체지향-프로그래밍OOP