데이터 지향 프로그래밍 1~2

2024-11-23

Data-Oriented Programming

1장 객체지향 프로그래밍의 복잡성

1.1 복잡성에 대해서

OOP 의 복잡성은 언어의 구문(syntax)이나 의미(semantics)와 관련이 없다.

복잡성은 프로그램은 객체로 구성되어야 하고, 객체는 상태를 갖고, 상태에 접근하거나 조작하는 메서드와 함께 한다는 점에 있다.

수년에 걸쳐 OOP 생태계는 언어에 새로운 특징(익명 클래스, 무명 함수 등)을 추가하고 개발자에게

더 간단한 인터페이스를 제공해 일부 복잡도를 숨겨 이 복잡도를 경감시켰다.

프레임워크는 내부에서 리플렉션과 사용자 정의 어노테이션과 같은 고급 언어적 특징에 의존한다.

1.1.1 설계 단계

설계시 UML 클래스도를 그린다. 객체지향 프로그래밍에선 모든 비즈니스 개체는 객체로 표현되고, 모든 객체는 클래스로부터 만들어진다.

클래스 도출은 쉬운 작업이지만 클래스 간 관계를 정의하는 순간 어려워진다.

포함(composition), 연관(association), 상속(inheritance), 사용(usage)

포함과 연관은 객체가 서로가 없어도 살 수 있는지 여부에 따라 다르다.

포함 관계에서는 한 객체가 죽으면 다른 객체도 죽는다. 연관 관계는 각자의 삶을 산다. 즉, 포함 관계는 한 객체가 없어지면 다른 객체도 없어지고, 연관 관계는 각 개체의 생애주기가 독립적이다.

UML 도면을 봐도 뭔말인지 모르겠다!!!!!!!!!!!

1.2 복잡성의 근원

  • 시스템 구현에서 그가 선택한 프로그래밍 프로그래밍 패러다임에 관한 것
  • 객체지향 패러다임에 대한 것
  • OOP가 시스템의 복잡도를 증가시키는 경향을 보이는 점에 대한 것

책에서 언급되는 유형의 복잡도는 시스템을 이해하기 어렵게 만드는 정도를 말한다.

즉, 프로그램이 소비하는 자원의 양을 다루는 유형의 복잡도와는 관련이 없다.

마찬가지로, 단순성은 복잡하지 않다(즉, 이해하기 쉽다)는 의미로 사용된다.

복잡성과 단순성(어려움과 쉬움처럼)은 절대적인 개념이 아니고 상대적인 것임을 명심해야한다.

책에서 복잡도에 대해서 언급된 “타르 웅덩이 밖으로(Out of the Tar Pit)” 논문을 살짝 봤는데 재미있는 내용인듯 Out of the Tar Pit

OOP 에 내재된 복잡성의 주요 근원은 다음과 같다.

  • 코드와 데이터가 섞여있다.
  • 객체가 변경 가능하다.
  • 데이터가 멤버로 객체에 고정된다.
  • 코드는 메서드로 클래스에 고정된다.

이 분석 내용은 함수형 프로그래밍이 기존 OOP 에 대해 갖는 생각과 비슷하다.

그러나 이 책에선 DOP 가 시스템 복잡도를 줄이려고 취하는 데이터 접근법은 FP와 다르다.

특징 복잡도에 미치는 영향
코드와 데이터가 섞여 있다. 클래스가 많은 관계에 연루되는 편이다.
객체가 변경 가능하다. 코드를 읽을 때 더 많은 생각이 필요하다.
다중 스레드 환경에서 명시적 동기화가 필요하다  
데이터가 객체에 묶여 있다. 데이터 직렬화가 쉽지 않다.
코드는 클래스에 묶여 있다. 클래스 계층 구조가 복잡하다.

1.2.1 다량의 클래스 간 관계

클래스 복잡도를 산정하는 한 가지 방법은 멤버와 메서드를 생략하고

클래스와 클래스 간의 관계만 보는 것이다.

시스템 분석의 관점에서, 코드와 데이터가 함께 혼합되면

각 소프트웨어의 구성 요소가 많은 관계를 맺을 가능성이 높아져 시스템이 복잡해진다.

  • 데이터 관계 :
    • Library 는 여러 개의 Member 를 갖는다.
    • Member 는 여러 개의 BookLending 을 갖는다.
  • 코드 관계 :
    • Member 는 User 를 확장한다.
    • Librarian 은 Member 를 사용한다.
    • Member 는 BookItem 을 사용한다.

코드 요소는 MemberCode 로 분리

데이터 요소는 MemberData 로 분리

1.2.2 예상치 못한 코드 동작

// ex 1.1 super simple code
class Member {
    isBlocked;

    displayBlockedStatusTwice() {
        const isBlocked = this.isBlocked;
        console.log(isBlocked);
        console.log(isBlocked);
    }
}

member.displayBlockedStatusTwice();

위 코드를 동작시켰을때 첫 번째 로그에선 true를 출력했다면 두 번째는 true 를 출력한다고 말할것이다. (맞음)

// ex 1.2 simple code
class Member {
    isBlocked;

    displayBlockedStatusTwice() {
        console.log(isBlocked);
        console.log(isBlocked);
    }
}

member.displayBlockedStatusTwice();

하지만 약간 다르게 변수를 할당하지 않는다면 어떻게 될까?

위와 같이 첫 번째는 true를 반환한다면 두번째는??

단일 스레드인 경우에선 true 겠지만 다중 스레드 환경에서는 예측할 수 없다.

두 코드의 차이는 다음과 같다.

  • 예제 1.1(위) 는 원시 자료형 값인 불리언 값에 두번 접근했다.
  • 예제 1.2(아래) 는 객체 멤버에 두 번 접근 했다.

데이터가 변경될 수 있다면 코드는 불확실해진다.

두 번째 코드의 불확실한 동작은 OOP 의 불편한 결과중 하나다.

보통은 값이 바끼지 않는 원시 자료형과 달리 객체의 멤버는 값이 바뀔 수 있다.

이를 방지하기 위해 mutex 와 같은 동시성 안전 장치로 코드를 보호하지만,

이런 장치는 성능에 심각한 영향을 미치거나 데드락에 빠질 수 있다.

코드와 데이터 분리

DOP 의 첫 번째 통찰은 코드와 데이터를 분리함으로써 시스템의 복잡도를 낮출 수 있다는 것이다.

데이터에서 코드가 분리되면 시스템이 데이터 개체와 코드 모듈이라는 큰 두 부분으로 나뉘어

각각 독립적으로 생각할 수 있게 된다.

코드를 함수 안에 두는 방식으로 데이터에서 코드를 분리한다. 함수의 동작은 어떤 식으로든 함수 콘텍스트에 캡슐화된 데이터에 따라 달라져서는 안된다.

2.1분리하면 얻게되는 이점

  • 시스템이 단순하다. 즉 이해하기 쉽다.
  • 시스템이 유연하고 확장 가능하다. 변경된 요구 사항에 맞춰 설계를 바꿀 필요가 거의 없다.

2.2 데이터 개체체

데이터 개체는 시스템에서 정보를 보관하는 부분이다. 명사와 명사구로 구분한다.

요구 사항에서 데이터 개체에 대응하는 용어에 표시함

  • 사용자에는 도서관 회원사서, 두 유형이 있다.
  • 사용자는 이메일과 비밀번호로 시스템에 로그인한다.
  • 회원을 빌릴 수 있다.
  • 회원사서제목이나 저자도서 정보를 검색할 수 있다.
  • 사서회원에 대해 대출을 금지하거나 해제할 수 있다.
  • 사서는 회원에게 대출된 책 현황을 조회할 수 있다.
  • 도서 하나에 인쇄본이 여러 권 있을 수 있다.

위 개체를 자연스럽게 묶는다면 아래와 같다.

시스템 데이터 개체가 정리된 다단 목록

  • 장서 데이터
    • 도서 정보 데이터
    • 저자 데이터
    • 책 데이터
    • 대출 데이터
  • 사용자 관리 데이터
    • 사용자 데이터
    • 회원 데이터
    • 사서 데이터

2.3 코드 모듈

시스템의 기능을 식별하는 부분. 동사로 구분한다.

요구 사항에서 데이터 개체에 대응하는 용어에 표시함

  • 사용자에는 도서관 회원과 사서, 두 유형이 있다.
  • 사용자는 이메일과 비밀번호로 시스템에 로그인한다.
  • 회원은 책을 빌릴 수 있다.
  • 회원과 사서는 제목이나 저자로 도서 정보를 검색할 수 있다.
  • 사서는 회원에 대해 대출을 금지하거나 해제할 수 있다.
  • 사서는 회원에게 대출된 책 현황을 조회할 수 있다.
  • 도서 하나에 인쇄본이 여러 권 있을 수 있다.

도서관 시스템의 용도

  • 도서 정보 검색
  • 책 추가
  • 회원 대출 금지
  • 회원 대출 금지 해제
  • 사용자 시스템 로그인
  • 회원 대출 도서 조회
  • 도서 대출
  • 도서 반납
  • 사서 여부 확인

모듈은 함수를 모아놓은것이고 OOP 에서는 모듈이 클래스로 표현된다. 하지만 다른 언어에서는 패키지나 namespace가 될 수 있다.

중요한 점은 DOP 코드 모듈에는 무상태 함수만 포함된다.

// in OOP
class Library {
    catalog
    userManagement

    getBookLendings(userId, memberId) {
        // 도서관의 상태는 this.catalog, this.userManagement로 접근함
    }
}

기존 OOP 에서 객체의 메서드에게는 객체의 상태가 암시적 인자다.

DOP 에서는 명시적인 인자로 전달한다.

// in DOP
class Library {
    static getBookLendings(libraryData, userId, memberId) {
        // 도서관의 상태는 libraryData로 전달받음
    }
}

DOP 에서 코드 모듈의 함수는 상태가 없다.

2.4 이해하기 쉬운 DOP 시스템

시스템이 코드 모듈과 데이터 개체로 나뉘기 때문에 이해하기 쉽다.

코드가 데이터 개체를 어떻게 다루는지 자세히 고려할 필요가 없고,

관심사가 코드와 데이터로 명확히 분리되어 더 쉽게 이해할 수 있다.

시스템의 부분 구성 요소에 대한 제약 관계의 제약
데이터 개체 (코드 없는)순수 데이터 연관과 포함
코드 모듈 (데이터 개체 없는)무상태 함수 사용(상속 없음)

2.5 유연한 DOP 시스템

슈퍼 회원과 VIP 회원을 시스템에 추가해야한다면??

// ex2.3 회원의 대출 도서 획득
class Library {
    static getBookLendings(libraryData, userId, memberId) {
        if(UserManagement.isLibrarian(libraryData.userManagement, userId)) {
            return Catalog.getBookLendings(libraryData.catalog, memberId);
        } else {
            throw "대출된 도서를 조회할 권한이 없음";
        }
    }
}

class UserManagement {
    static isLibrarian(userManagementData, userId) {
        // todo
    }
}

class Catalog {
    static getBookLendings(catalogData, memberId) {
        // todo
    }
}
// ex2.4 슈퍼 회원이 다른 회원의 대출 도서를 얻을 수 있도록 함
class Library {
    static getBookLendings(libraryData, userId, memberId) {
        const isLibrarian = UserManagement.isLibrarian(libraryData.userManagement, userId);
        const isSuperMember = UserManagement.isSuperMember(libraryData.userManagement, userId);
        if(isLibrarian || isSuperMember) {
            return Catalog.getBookLendings(libraryData.catalog, memberId);
        } else {
            throw "대출된 도서를 조회할 권한이 없음";
        }
    }
}

class UserManagement {
    static isLibrarian(userManagementData, userId) {
        // todo
    }
    static isSuperMember(userManagementData, userId) {
        // todo
    }
}

class Catalog {
    static getBookLendings(catalogData, memberId) {
        // todo
    }
}
// ex2.5 Library.addBookItem signature
class Library {
    static addBookItem(libraryData, userId, bookItemInfo) {
        // 
    }
}

VIP 회원 로직 추가

// ex2.6 VIP 회원이 도서관에 책을 추가할 수 있도록 함
class Library {
    static addBookItem(libraryData, userId, bookItemInfo) {
        const isLibrarian = UserManagement.isLibrarian(libraryData.userManagement, userId);
        const isVIPMember = UserManagement.isVIPMember(libraryData.userManagement, userid)
        if (isLibrarian || isVIPMember) {
            return Catalog.addBookItem(libraryData.catalog, bookItemInfo);
        } else {
            thorw "대출된 도서를 조회할 권한이 없음";
        }
    }
}

class UserManagement {
    static isLibrarian(userManagementData, userId) {
        // todo
    }
    static isVIPMember(userManagementData, userId) {
        // todo
    }
}