본문 바로가기
프로그래밍언어론

[프로그래밍언어론] 30. 데이터 추상(Data Abstraction)

by 파스텔코랄 2023. 11. 6.

데이터 추상화(Data Abstraction)

  • 물리적 시스템은 한 가지 타입만 처리 : 비트 문자열(bit strings)
  • 고급 언어는 다양한 값에 대한 "래핑(wrapping)"을 제공
    • 타입 : 값 + 작업
  • 데이터 추상화 : 값은 하나로 결합될 수 있으며 적절한 작업을 통해 새롭고 더욱 추상적인 유형을 형성 가능
  • 이 새로운 유형을 사용하여 현실 세계에서 더욱 복잡한 객체를 표현 가능
  • 예) 레고 작품을 만들 때 아래 구조부터 쌓아서 올려서 합친다.

 

장점

  • 복잡한 데이터 구조를 쉽게 처리한다.
  • 세부 사항에 신경 쓸 필요가 없다.
    • 인터페이스(Interface)와 구현(implementation)의 분리
    • 구현(implementation)을 몰라도 인터페이스와 함께 사용 가능
  • 정보 숨기기(Information Hiding): 내부 데이터는 정의된 인터페이스를 통해서만 액세스

 



추상 데이터 타입(Abstract Data Type)

  • 추상 데이터 타입(ADT) 고려
    • 타입 부분(type part), 작업 부분(operation part)
  • 사양(Specification) :  ADT 작업의 의미에 대한 설명
    • 예) push() : 스택의 맨 위에 요소 삽입
abstract Stack {
    type Stack = // 타입 부분
        struct {
            int info;
            Stack next;
        }
    operations: // 작업 부분
        Stack create(){..}
        Stack push(Stack s, int x){..}
        Stack pop(Stack s){..}
        int top(Stack s){..}
}
  • 사양을 올바르게 구현하고 인터페이스를 유지하는 한 ADT 사용에는 차이 없다.
  • 사용으로부터 정의(definition)를 분리
  • 정보(데이터 및 구현)가 숨겨지고 보호

 



표현 독립성(Representation Independence)

  • 단일 사양 ADT의 두 가지 구현 : 이런 타입의 클라이언트에서 구별할 수 없다.
  • type-safe 언어
    • 하나의 ADT를 동일한 서명(signature 또는 인터페이스)이지만 구현이 다른 다른 ADT로 교체
    • 타입 오류가 발생하지 않는다.

 



ADT 한계

  • Counter 예제
  • 타입 부분(type part) : Counter가 정수임 나타낸다.
  • 작업 부분(operation part) : Counter의 일부 주요 기능을 구현
abstract Counter {
    type Counter = int;
    operations:
        void reset(Counter c) {
            c = 0;
        }
        int get(Counter c) {
            return c;
        }
        void increment(Counter c) {
            c = c + 1;
        }
}
  • 진보된 타입의 Counter가 필요하다고 가정
  • 재설정 횟수도 계산
  • ADT에 필드를 추가
  • reset()은 Counter와 다르다.
  • 캡슐화는 잘 유지된다.
abstract Counter2 {
    type Counter2 =
        struct {
            int x;
            int num_reset;
        }
    operations:
        void reset(Counter2 c) {
            c.x = 0;
            c.num_reset += 1;
        }
        int get(Counter2 c) {
            return c;
        }
        void increment(Counter2 c) {
            c = c + 1;
        }
        int get_resets(Counter2 c){
            return c.num_reset;
        }
}
  • 문제는 Counter와 Counter2 사이에 아무런 관계가 없다는 것
  • 어느 쪽이든 수정하면 다른 쪽에서는 인식할 수 없다.
  • Counter 사양이 변경된 경우
    • 두 ADT 모두에서 동일한 작업이 있더라도 모두 변경해야 한다.
    • 예) get() 및 increment().
abstract Counter2 {
    type Counter2 =
        struct {
            int x;
            int num_reset;
        }
    operations:
        void reset(Counter2 c) {
            c.x = 0;
            c.num_reset += 1;
        }
        int get(Counter2 c) {
            return c;
        }
        void increment(Counter2 c) {
            c = c + 1;
        }
        int get_resets(Counter2 c){
            return c.num_reset;
        }
}

 

Counter 상속(Inherit)

  • Counter의 이전 작업을 활용
  • Counter와 Counter3 모두에 변경 사항을 적용하려면 Counter만 수정
  • Counter에서 가능한 모든 작업은 Counter3에서도 가능
    • Counter3은 Counter와 호환
abstract Counter3 {
type Counter3 =
struct {
Counter x;
int num_reset;
}
operations:
void reset(Counter3 c) {
reset(c.x);
c.num_reset += 1;
}
int get(Counter3 c) {
return get(c.x);
}
void increment(Counter3 c) {
increment(c.x);
}
int get_resets(Counter3 c){
return c.num_reset;
}
}

 

문제가 해결되었는가?

  • 100개의 카운터(Counter 또는 Counter3)가 있다고 가정
  • 모두 재설정하고 싶다.
  • reset(arr[i])을 호출하면
  • 작업 고려 - Counter 또는 Counter3을 매개변수로 사용
    • reset(Counter)만 실행
    • 적절한 방법을 동적으로 선택할 수 없다.
Counter arr[100];
...
arr[9] = new Counter();
arr[10] = new Counter3();
...
for(int i=0; i<100; i++){
reset(arr[i]);
}
void reset(Counter c) {
c = 0;
}
void reset(Counter3 c) {
reset(c.x);
c.num_reset += 1;
}

 

ADT 한계

  • 캡슐화 및 정보 숨기기 적합하다.
  • 그러나 복잡한 설계 사용에 유연성 부족하다.
  • 객체 지향 패러다임이 충족할 수 있는 다음 네 가지 요구 사항
    • 캡슐화(Encapsulation), 정보 숨기기(Information hiding)
    • 상속(Inheritance.)
    • 하위 타입 호환성(Subtype compatibility)
    • 동적 방법 선택(Dynamic method selection)

댓글