객체지향 #4부 - 객체지향 추상화와 분해

객체지향 #4부 - 객체지향 추상화와 분해

추상화와 분해에 대한 역사를 바탕으로 왜 객체지향에서의 추상화와 분해 기법이 채택되었는지 알아보자.

객체지향에서의 추상화란 무엇인가?
구체적인 구현에 의존하여 변경에 불안정한 설계를 쉽게 변하지 않는 불변성에 기대게 해주는 도구라고 생각한다.

저자는 이렇게 말했다.

불필요한 정보를 제거하고 현재의 문제 해결에 필요한 핵심만 남기는 작업이 추상화이다.

불필요한 정보는 무엇인가? 바로 What이 아닌, How이다. 세부적인 내용이 밖으로 드러나고 구체적인 구현에 의존하는 것이다.
문제 해결에 필요한 핵심은 무엇인가? 프로그램이 해결하고 싶은 도메인의 문제를 객체끼리 메세지를 통해 협력관계를 맺고 해결하는 것이다.
객체는 어떠한 책임을 부여받고, 그 책임을 수행하기 위해 필요한 상태를 가지고 자주적으로 행동한다.

객체지향의 창시자라 불리는 앨런 커티스 케이는 콜로라도 대학에서 분자 생물학에 대한 학사를 받았다.
왜 객체가 자주적이고 살아있고, 필요한 기능만을 제공하며 협력한다고 하는지 객체지향의 근간이 이해가 되기 시작한다.
객체와 협력은 세포의 동작과 비슷하다.

세포들의 협력관계에서 영감을 받은 그의 사상인 메시징, 캡슐화, 동적 바인딩은 객체지향의 핵심이라고 할 수 있다.

우리는 복잡한 현실세계의 문제 해결을 위해 아주 작은 단위까지 나누고 쪼개는 ‘분해’ 를 해야 한다.
세포들의 작은 일들이 모여서 생명체의 기관이 동작하듯, 프로그램의 문제도 아주 작은 단위로 분해하는 것이 추상화의 시작인 것 이다.
분해를 하면 복잡해보이는 문제도 비교적 단순해진다.

일하는세포들 알고보면 OOP의 정수가 담겨있는 만화 일하는 세포들


프로시저 추상화 vs 데이터 추상화

현대 프로그래밍 언어를 특징 짓는 두 가지 추상화 방법은 프로시저 추상화, 데이터 추상화 이다.

  • 프로시저 추상화 : 소프트웨어가 무엇을 해야 하는지를 추상화
    • 기능 분해의 길
  • 데이터 추상화 : 소프트웨어가 무엇을 알아야 하는지를 추상화
    • 데이터의 타입을 추상화 (추상 데이터 타입)
    • 데이터 중심의 프로시저 추상화 (우리가 배우고 있는 객체지향)

시스템 분해의 시작은 이 추상화 방법들 중 한 가지 선택에서 시작된다.
지금까지 우리가 배우고 있는 객체지향은 결국 데이터 중심의 프로시저 추상화다. 역할과 책임을 수행하는 객체추상화 하고, 기능을 협력하는 객체 공동체분해한다.

필요한 객체 식별, 협력관계 설정으로 기능 분해가 끝났다면
(데이터 추상화 + 프로시저 추상화 = 클래스) 클래스 단위로 시스템 분해를 진행한다.



Q: 데이터를 중심으로 프로시저를 추상화하는 객체지향이 기능 분해 방법들 중 가장 효과적이라고 말하는 이유는 무엇일까?

먼저 객체지향이 아닌 방법에 대해 알아보자.

  • 전통적인 기능 분해 방법 (Top-Down 접근)
- 직원의 급여를 계산한다.
1. 사용자로부터 소득세율을 입력받는다.
    1-1."세율을 입력하세요" 라는 문장을 화면에 출력한다.
    1-2.키보드를 통해 세율을 입력받는다.
2. 직원의 급여를 계산한다.
    2-1.전역 변수에 저장된 직원의 기본급 정보를 얻는다.
    2-2.급여를 계산한다.
3. 양식에 맞게 결과를 출력한다.
    3-1."이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다.

일종의 책의 목차매뉴얼과 흡사하다.
상위 기능을 생각하고 하위로 내려가면서 기능을 세분화하는 방법이다.
이 기능을 분해하고 구체화하는 과정에서 필요한 데이터의 종류와 저장 방식을 식별한다.




기능 위주 분해의 문제점 5가지

기본적으로 기능 분해는 결합도가 높아 변경에 취약하다.

  1. 시스템이 하나의 메인 함수(Top)으로 구성되어 있지 않다.
    • 기능의 최 상단 (Top)이란 개념은 없다.
    • 변경되면서 여러 개의 동등한 수준의 함수 집합으로 성장하게 된다.
  2. 기능 추가나 요구사항 변경으로 메인 함수를 빈번하게 수정해 코드를 추가할 때 리스크가 커진다.
    • 새로운 기능이 추가되면 정상인 메인부터 수정이 들어가야 한다.
    • 이미 정해진 탑다운 구조에 새로운 기능을 억지로 끼워넣어야 하는 상황이 된다.
  3. 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
    • 비즈니스 로직과 사용자 인터페이스는 변경되는 빈도가 다르다. 변경 빈도는 사용자 인터페이스 »> 비즈니스 로직
    • 사용자 인터페이스를 GUI로 변경 시, 강하게 결합되어 있기 때문에 전체 재설계가 필요하다.
  4. 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시켜 유연성과 재사용성이 떨어진다.
    • 설계에서 실행 순서부터 정하기 때문에 what 보다 how 에 집중하게 만든다.
    • 기능 분해에서는 실행 순서나 조건, 반복 제어구조를 결정하지 않으면 진행이 불가능하기 때문이다.
  5. 데이터 형식이 변경되면 파급효과가 크다.
    • 어떤 데이터를 어떤 분해된 기능이 사용하고 있는지 추적이 힘들다.
    • 한 데이터를 수정해도 다른 의도치 않은 기능에 영향을 미칠 수 있다.

변경에 대한 영향을 최소화하기 위해 영향을 받는부분 / 받지 않는 부분으로 분리하고
퍼블릭 인터페이스를 통해 변경되는 부분에 대한 접근을 통제해야 한다.
하향식 설계는 이미 완성된 결과나 시스템에 대한 서술로는 훌륭한 기법이다.
그러나 실제로 동작하는 커다란 소프트웨어를 설계하는데 적합한 방법이 아니다.




정보은닉 & 모듈

앞서 하향식 설계에 대한 문제점에서 좋은 설계의 요건을 도출해낸 결과는 아래와 같다.

변경에 대한 영향을 최소화하기 위해 영향을 받는부분 / 받지 않는 부분으로 분리하고
퍼블릭 인터페이스를 통해 변경되는 부분에 대한 접근을 통제해야 한다.

이는 외부에 감춰야 할 것에 따라 시스템을 모듈 단위로 분할 로 극복할 수 있다.
변경될 가능성이 있는 것들을 내부로 감추고 외부에는 잘 정의되어 변경되지 않는 인터페이스만 공개하는 것이다.
모듈 분할 단위에 대한 두 가지 고려사항은 아래와 같다.

  • 복잡성
    • 외부에서 쓰기 편하게 추상화된 구조의 인터페이스를 공개한다.
  • 변경 가능성
    • 변경될 가능성이 있는 것들을 내부로 잘 감춘다.

변경 가능성이 있어 모듈 내부로 감추고 싶은 것은 데이터, 복잡한 로직, 변경이 잦은 자료구조 등이다.




모듈단위 설계와 정보은닉의 효과

책에서 의미하는 모듈은 인스턴스화 되지 않은 클래스와 같은 개념이다.
EX) 회사에 속한 모든 직원을 가지고 있는 Employees 모듈

모듈은 기능이 아닌, 변경의 정도에 따라 시스템을 분해한다.
각각의 모듈들은 외부에 감춰야 하는 비밀과 관련도가 높은 데이터와 인터페이스의 집합이다.

  • 앞의 Top-down 설계와 다르게 변경의 영향이 모듈 내부에서만 영향을 미친다. 파급효과를 제어할 수 있게 된다.
  • 모듈 내부에 비즈니스 로직이 감춰져있기 때문에 사용자 인터페이스를 변경해도 영향을 받지 않는다.
  • 자바에서는 Package로 분리하게 되는데 동일한 이름에 대한 충돌을 방지해준다.

자연스럽게 모듈은 높은 응집도와 낮은 결합도를 가지게 된다.
모듈은 정보은닉을 통해 설계의 중심을 기능 -> 데이터로 가져왔다.

이를 통해 미지의 시스템 설계에서는 기능 중심적 분해보다 감춰야할 데이터를 중심으로 설계하는 것이 더 우수하다는 사실을 알 수 있었다.

추가적으로 더 나아가야 할 점은 아직 모듈이 개별적인 인스턴스를 다루는 개념까지 발전하지 못했다는 점이다.
개별적인 인스턴스를 다루기 위해서는 추상 데이터 타입도 고려하는 방식으로 나아가야 한다.




추상 데이터 타입

type : 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수

특정 프로그램이 직원의 급여를 계산한다는 목적을 가질 때, 사람들은 직원의 급여를 계산하는 기능을 통해 사고하지 않는다.
사장 , 직원의 개별 개념들을 떠올린 후 이들을 이용해 계산에 필요한 절차를 생각해내는데 익숙하다.




추상 데이터 타입이란?

  • 타입 정의를 선언할 수 있어야 한다.
  • 타입 인스턴스를 다루기 위해 오퍼레이션의 집합을 정의할 수 있어야 한다.(메서드)
  • 제공된 오퍼레이션을 통해서만 조작할 수 있게하여 데이터를 외부로부터 보호해야한다.(캡슐화)
  • 타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 한다.(new)




앞서 고려했던 모듈과 추상 데이터 타입의 차이점은 무엇일까?

일상에서 Employee를 말할 때, 우리는 개별적인 객체로 본다. 상태와 행위를 함께 가지고 있는 독립적인 객체다.
모듈에서의 Employees는 전체 직원을 캡슐화하는 개념이다. 개별적인 객체가 아니라 인스턴스를 찍을 수 없다.

추상 데이터만으로 프로그램을 설계하기에는 아직 부족하다. 추상 데이터 타입은 그저 상태를 저장할 데이터를 표현한다.
추상 데이터 타입으로 표현된 데이터를 이용해 기능을 구현하는 핵심 로직은 외부에 존재한다.(main)




그렇다면 자바 클래스와 추상 데이터 타입의 차이점은 무엇일까?

바로 상속과 다형성을 지원하지 않던 고전적인 방식이 추상 데이터 타입이다.
단순히 데이터를 저장하고, 데이터를 조작하는 오퍼레이션을 제공하는 것이 추상 데이터 타입의 전부다.

윌리엄 쿡은 이 차이점에 대해 클래스는 절차를 추상화한 것이고 추상 데이터 타입은 타입을 추상화한 것이라고 한다.

추상 데이터 타입의 Employee는 정규 직원, 아르바이트 직원 등을 한 타입으로 추상화해서 같은 오퍼레이션을 쓴다.



Employee 단순 추상 데이터 타입

오퍼레이션정규 직원아르바이트 직원
월급 계산calculatePay()직원 월급 계산아르바이트 월급 계산
기본급basePay()직원 기본급0

겉에서 Employee의 월급계산, 기본급 로직을 호출할 경우,
정규 직원이 있는지, 아르바이트 직원이 있는지 호출부에서 알 수 없다.
오퍼레이션 기준으로 타입을 통합하기 때문이다.


Employee 객체지향

클래스 타입오퍼레이션설명
EmployeecalculatePay()추상 메서드, 월급 계산
 basePay()추상 메서드, 기본급 반환
정규직원calculatePay()정규 직원의 월급 계산
 basePay()정규 직원의 기본급 반환
아르바이트calculatePay()아르바이트 직원의 월급 계산
 basePay()아르바이트 직원의 기본급은 0으로 설정

객체지향 설계로 넘어와서야 Employee라는 직원타입이 추상화된 부모가 생기고,
다형성으로 calculatePay()를 발동시키는 순간 적절한 실제 자식에 따라 절차가 실행된다.
객체지향은 실제 내부 절차는 다르지만 클래스를 이용한 다형성으로 절차를 감추는 절차 추상화다.

이렇게 동일한 메세지에 대해 서로 다르게 반응하는 것을 다형성이라고 한다.


결론적으로 추상 데이터 타입은 오퍼레이션 기준, 클래스는 타입을 기준으로 절차를 추상화한다.
이것이 분해에서 추상 데이터 타입과 클래스의 차이다.




최종적인 객체지향설계로써 핵심

단순히 클래스를 사용하는 것이 주요한것이 아니다. 객체지향 프로그래밍을 한다는 것은
타입을 기준으로 절차를 추상화해야 한다.
앞서 추상 데이터 타입에서처럼 내부에 직원의 유형을 저장한다면, 그 저장된 값으로 메서드 내에서 타입을 구분한다면 객체지향이 아니다.

객체지향은 타입 변수를 통해 조건문을 다형성으로 대체하기 때문이다.

  • 추상 데이터 타입의 Employee
//file: `추상화 데이터 타입인 경우`

public class Employee {

    // 알바인지 여부를 나타내는 변수 (예시로 추가)
    private boolean 알바인가;

    public double calculatePay(double 세금률) {
        if (알바인가) {
            return 알바월급계산(taxRate);
        }
        return 직원월급계산(taxRate);
    }
    
}



  • 객체지향 (다형성) 조건문이 제거된 케이스
//file: `객체지향설계로 다형적으로 조건문 제거`

// Employee 추상 클래스
public abstract class Employee {
 public abstract double calculatePay(double taxRate);
}

// RegularEmployee 클래스
public class RegularEmployee extends Employee {
 @Override
 public double calculatePay(double taxRate) {
  // 정규직 계산 로직 구현
  // 예: return 기본급 - 세금률 * 기본급;
  return 0; // 임시 반환값
 }
}

// PartTimeEmployee 클래스
public class PartTimeEmployee extends Employee {
 @Override
 public double calculatePay(double taxRate) {
  // 알바 계산 로직 구현
  // 예: return 기본시급 * 시간 - 세금률 * 기본시급 * 시간;
  return 0; // 임시 반환값
 }
}

객체지향 설계시 기존 코드에 아무런 영향을 미치지 않고 새로운 객체의 유형과 행위를 추가할 수 있게 된다. (OCP법칙)




결론

지금까지 객체지향의 기본인 추상화와 분해에 대한 기법의 역사 대해 알아보았다.
어떻게 Top-Down 기능 중심 분해에서 데이터를 추상화 하는 추상 데이터 타입,
데이터를 중심으로 기능을 추상화 하는 객체지향에 대한 결론에 이를 수 있었는지.

중요한 점은 특정 방법이 좋다 나쁘다의 영역이 아니라 늘 그렇듯 트레이드 오프의 영역이다.
하향식 기능 설계는 다 만들어진 프로그램의 매뉴얼, 책의 목차 등에 좋다.
추상 데이터 타입은 주된 변경점이 오퍼레이션의 추가가 이루어진다면 좋다.
타입이 자주 바뀐다면 간단하게 클래스를 상속 계층에 추가하기만 해도 되는 객체지향이 좋다.