ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프로그래밍과 추상화에 대하여.
    프로그래밍/설계 2017. 6. 30. 15:02

    HtDP를 읽는 중인데 오늘도 뻘 생각이 나서 그냥 필이 꽃힌 김에 적어봤다.

    (내 성격의 최대 장점이자 단점. 잡생각이 너무 많음. 글 중간에도 의식의 흐름대로 빠지는 것이 보인다.)


    내 맘대로 하는 프로그램 설계 시리즈.

    Chapter1 - 기초(4섹션)

    2017/12/27 - [프로그래밍/설계] - 내 맘대로 프로그램 설계 1. - 이유와 준비.

    2018/01/11 - [프로그래밍/설계] - 내 맘대로 프로그램 설계 2. - 데이터 타입.

    2018/01/16 - [프로그래밍/설계] - 내 맘대로 프로그램 설계 3. - 함수와 변수.

    2018/05/29 - [프로그래밍/설계] - 내 맘대로 프로그램 설계 4. - 고정 크기 데이터.

    2017/06/30 - [프로그래밍/설계] - 프로그래밍과 추상화에 대하여.[부록](현재)


    Chapter2 - 임의의 데이터 처리

    2018/06/10 - [프로그래밍/설계] - 내 맘대로 프로그램 설계 5. - 리스트와 재귀.


    각 챕터의 부록에 간단한 글들을 넣기로 하였습니다.

    그래서 예전에 써두었던 글을 Chapter 1의 부록으로 넣었습니다.

    1. 추상화의 정의.

    프로그래밍을 하다보면 으레 접하는 단어는'추상화(Abstraction)'이고,
    중요하다는 말을 많이 들었을 것이다.

    그렇다면 '추상화가 무엇인가?' 라고 묻는다면 답하기는 쉽지 않다.
    그리고 도무지 어떻게 해야 추상화를 시킬 수 있는가도 감이 잡히지 않을 것이다.

    일단 추상(抽象)의 뜻을 알아보기로 하자.
    국립국어원에 의하면,

    여러 가지 사물이나 개념에서 공통되는 특성이나 속성 따위를 추출하여 파악하는 작용.

    이고 추상화는 '추상적인 것으로 됨'이라 나와있다.


    굳이 더 깊게 들어가 한자에 딸린 의미를 해석해보자면,

    抽: 뺄 추
    象: 코끼리 상

    으로 '형상을 뽑아내는 것'이라 할 수 있다.
    (코끼리 상에 모양을 뜻하는 의미가 있기도 함.)

    또는 '법칙을 뽑아내는 것, 본떠서 뽑아내는 것.' 이라고도 해석이 가능하겠다.

    마지막으로 추상화에서 화(化)는 될 화, 화할 화 이므로,
    추상화란 '형상을 뽑아내어 변화시키는 것'이라 정의할 수 있다.

    사실 지금까지는 약간 어렵게 설명이 되어 있을수도 있는데
    쉽게 말하자면 '일반화'를 시킨 것이라 생각하면 편하다.

    그럼 추상화가 왜 중요하고, 어떻게 적용을 시키는지 궁금할 것이다.


    2. 중요성

    단순하게 컴퓨터/SW 공학 학생을 기준으로 추상화가 왜 중요한지 생각을 해보겠다.
    코딩은 진입장벽이 낮아져서 여러 학과가 코딩을 하기도 하는데
    코딩에 어느 정도 관심있는 학생이 시간을 들여서 코드를 짠다고 가정할 때
    정보통신/임베디드학과 계열이 회로를 잘 알고 있기 때문에 로우레벨에서 유리하며,
    수학계열의 학생이 수학에 대한 조예가 더 깊기 때문에 알고리즘 면에서 유리하다.

    그럼 컴퓨터/SW 공학을 배우는 학생은 어떠한 가치를 중시하여야 할까?
    내 개인적인 생각으로는 추상화가 아닐까 싶다.

    추상화를 적절히 시키면 코드의 재사용성, 가독성을 높이고,
    결국 생산성, 에러의 감소와 같은 요소에 영향을 미치게 된다.
    프로그래머가 기능 개발에 투자하는 시간보다
    유지 보수, 버그 해결에 많은 시간을 쓴다는 것을 감안하면, 추상화의 중요성을 쉽게 인식할 수 있다.
    (프로그래머가 되려면 '돌아가기만 하면되지'라는 마인드를 가지면 안된다는 것.)

    이외의 일반적인 일에도 추상화는 상당히 중요한 개념이다.
    우리가 많은 것을 기억해야 할 때 모든 것을 기억하기보다는 패턴이나 맥락을 외우면 기억해야 할 양을 줄여 효율이 올라가기도 한다.


    3. 적용법

    사실 추상화의 정의에 대해서 잘 기억하고 있다면 어려울 것은 없다.
    하지만 몇 가지 예를 들어 추상화의 개념을 쉽게 이해해보도록 하자.

    아래에 나오는 예시는 C, C++과 약간 생소할 만한 언어인 racket(scheme)으로 표시되어 있다.(일부 Java)
    (racket은 lisp 계열의 함수형 언어. 전위 표기법으로 표현되어 있다.)


    예 0.
    코딩을 하면서 빈번하게 쓰던 반복문을 살펴보자.

    가와 나가 별을 100개 찍는다.
    가:

    printf("******....100개가 될 때까지 반복");
    나:
    for( i = 0; i < 100; i++ )
    { printf("*");}

    나에서는 *을 찍는 행동을 추상화 시켰다고 볼 수 있다.
    나에서는 *을 한 번 찍는 다는 것 밖에 행동이 없었지만, 반복문을 사용함으로서 추상화 시켰고 반복문이 실행되며 행동이 실체화 되었다.

    우리가 자주 쓰던 반복문도 일종의 추상화였다!!
    이러하듯, 추상화가 마냥 어려운 개념만은 아니다.

    예 1.
    원의 넓이를 구하려고 한다.

    원의 넓이를 구함에 있어서 필요한 요소는 반지름과 원주율이다.($\pi \times r^2$)
    반지름은 항상 가변적으로 변할 수 있는 요소(변수)이지만 원주율은 변하지 않는 요소이다.(상수)

    각각을 정의하면


    반지름.

    $$r$$


    원주율.

    $$\pi = 3.14$$


    C/C++
    반지름.

    double r

    원주율.

    #define PI 3.14

    racket
    반지름(racket에서는 자동으로 수를 인식해준다.)

    r

    원주율.

    (define PI 3.14)
    이라 할 수 있다.


    이 반지름과 상수의 정의는 원 넓이를 구하는 방법에서 각각 변수와 상수라는 '특징'을 뽑아서 '추상화' 한 것이다.

    +
    자주 사용되는 상수 값에 대해서는 필요없어 보이지만 유지보수를 위해 선언을 해두는 것이 정신건강에 좋다.
    3.14라는 값이 원주율을 나타내기 위해 자주 사용되는데 정확도를 위해서 갑자기 3.141592...으로 모든 값들을 바꾸어야 한다고 생각해보자.
    (끔직하다.)



    예 2.
    계속 이어서 원의 넓이를 구해보자.


    식.

    $$\operatorname{area\_circle}(r)=\pi \times r^2$$


    C/C++

    double area_circle(double r)
    {
        return PI * (r * r);
    }


    racket (읽는 팁을 주자면, (함수 인자 인자)로 구성되어 있다. +, * 값은 요소들이 모두 다 일종의 함수 취급.)

    (define (area_circle r)
      (* PI (* r r)))

    로 표현 할 수 있다.

    이 함수는 원의 넓이를 구하는 방법을 '추상화'  하여 함수의 형태로 표현한 것이다.
    (사실 '$\pi \times r^2$ '이라는 공식 자체가 이미 문제를 추상화 시켰다고 볼 수 있다.)


    예 3.
    이젠 원에서 가운데가 뚫린 링 형태의 넓이를 구해보자.

    링의 넓이를 구하는 방식을 추상화 시키자면 '$큰 원의 넓이 -  작은 원의 넓이$' 이다..

    지금까지 해왔던 것처럼 수식과 소스 코드로 표현해보자.


    $$\operatorname{area\_ring}(outer, inner)=(\pi \times outer^2)-(\pi \times inner^2)$$


    C/C++

    double area_ring(double outer, double inner)
    {
       return ((PI * (outer * outer) - (PI * (inner * inner)));
    }

    racket

    (define (area_ring outer inner)
      (- (* PI (* outer outer)) (* PI (* inner inner))))

    무언가 복잡하지 않은가?

    이래서 필요한 것이 '함수의 모듈화'라는 개념이다.
    비대한 함수를 잘게 쪼갠다는 것.

    그럼 함수를 쪼갠 다음 표현해 보겠다.

    $$\operatorname{area\_ring}(outer, inner)=\operatorname{area\_circle}(outer)-\operatorname{area\_circle}(inner)$$


    C/C++

    double area_ring(double outer, double inner)
    {
        return (area_circle(outer) - area_circle(inner));
    }

    racket

    (define (area_ring outer inner)
      (- (area_circle outer) (area_circle inner)))

    원 넓이를 구한다는 '특성'을 뽑아 '추상화' 하였다는 것을 알 수 있다.
    그리고 함수를 '각 원들의 넓이 구하기'와 '원 넓이 빼기'로 나누었기 때문에 훨씬 직관적이고 간결하다는 것 또한 느낄 수 있다.

    문제해결 과정을 이렇게 나누어 구성하는 것도 일종의 추상화 과정이며 프로그램에 깔린 의도를 전달하기 쉽게 해준다.

    +
    제발 제발 프로젝트를 할 때  main 함수에 자신이 맡은 모든 기능을 박아놓고 보내주지 말자.
    받는 사람 입장에서는 어떻게 이루어져 있는지 알기가 쉽지 않아 욕이 입 천장을 뚫고 나올 것 같다.

    예 4.
    더더욱 문제를 확장시켜 이번엔 파이프 모양의 겉넓이와 부피를 동시에 구해보자.

    일단 예 3에서 얻은 교훈처럼 문제를 잘게 나누어본다
    - 파이프의 겉넓이 = 파이프 링 넓이 + 파이프 옆면 넓이
    - 파이프의 부피 = 링 넓이 * 높이
    - 파이프 링 넓이 = 2 * (링 넓이)
    - 링 넓이 = 큰 원 넓이 - 작은 원 넓이
    - 원 넓이 = 원주율 * 반지름 * 반지름
    - 파이프 옆면 넓이 = 겉면의 옆면 넓이 + 안면의 옆면 넓이
    - 옆면의 넓이(사각형) = 원주 * 높이
    - 사격형 넓이 = 가로 * 세로
    - 원주 = 2 * 원주율  * 반지름


    이제 식과 코드로 표현해보자.(이번에는 Full로 구현해보았다.)


    $$s.t. \; PI=3.14$$
    $$\operatorname{area\_circle}(r)=PI \times r^2\\
    \operatorname{area\_ring}(outer, inner)=\operatorname{area\_circle}(outer)-\operatorname{area\_circle}(inner)\\
    \operatorname{area\_pipe_ring}(outer, inner)=2 \times (\operatorname{area\_ring}(outer)-\operatorname{area\_ring}(inner))$$
    $$\operatorname{length\_circle}(r)=PI \times 2r \operatorname{i.e.}\mathsf{circumference}\\
    \operatorname{area\_ractangle}(width,height)=width \times height\\
    \operatorname{side\_area\_cylinder}(r, height)=\operatorname{area\_ractangle}(\operatorname{length\_circle}(r), height)\\
    \operatorname{side\_area\_pipe}(outer, inner, height)=\operatorname{side\_area\_cylinder}(outer, height)+\operatorname{side\_area\_cylinder}(inner, height)$$
    $$\operatorname{area\_pipe}(outer, inner, height)=\operatorname{area\_pipe\_ring}(outer, inner)+\operatorname{side\_area\_pipe}(outer, inner, height)\\
    \operatorname{volume\_pipe}(outer, inner, height)=\operatorname{area\_ring}(outer, inner) \times height$$


    C/C++

    #define PI 3.14
    
    double area_circle(double r)
    {
        return PI * (r * r);
    }
    double area_ring(double outer, double inner)
    {
        return (area_circle(outer) - area_circle(inner));
    }
    double area_pipe_ring(double outer, double inner)
    {
        return (2 * (area_ring(outer) - area_ring(inner)))
    }
    
    double length_circle(double r) //circumference
    {
        return (2 * PI * r);
    }
    double area_ractangle(double width, double height)
    {
        return (width * height);
    }
    double side_area_cylinder(double r, double height)
    {
        double width = length_circle(r);
        return area_ractangle(width, height);
    }
    double side_area_pipe(double outer, double inner, double height)
    {
        return (side_area_cylinder(outer, height) + side_area_cylinder(inner, height));
    }
    
    double area_pipe(double outer, double inner, double height)
    {
        return (area_pipe_ring(outer, inner) + side_area_pipe(outer, inner, height));
    }
    double volume_pipe(double outer, double inner, double height)
    {
        return (area_ring(outer, inner) * height);
    }


    racket

    (define PI 3.14)
    
    (define (area_circle r)
      (* PI (* r r)))
    (define (area_ring outer inner)
      (- (area_circle outer) (area_circle inner)))
    (define (area_pipe_ring outer inner)
      (* 2 (area_ring outer inner)))
    
    (define (length_circle r) ;;circumference
      (* PI (* 2 r)))
    (define (area_ractangle width height)
      (* width height))
    (define (side_area_cylinder r height)
      (area_ractangle (length_circle r) height))
    (define (side_area_pipe outer inner height)
      (+ (side_area_cylinder outer height)
        (side_area_cylinder inner height)))
    
    (define (area_pipe outer inner height)
      (+ (area_pipe_ring outer inner)
        (side_area_pipe outer inner height)))
    (define (volume_pipe outer inner height)
      (* (area_ring outer inner) height))

    이 정도 양의 코드를 그냥 area_pipe와 volume_pipe 함수에서 모두 다 작성한다면 상당히 복잡하겠죠?
    다시 한번 말하지만 함수를 길게 쓰지 맙시다.
    (예3 에서 못느낀 사람들을 위해..)

    예4 에서 가장 핵심적인 요소는 함수의 '재사용' 입니다.
    area_ring이 area_pipe_ring을 구하는데에도 사용이 되었고, volume_pipe를 구하는데에도 사용이 되었습니다.

    만약 함수를 나누어 놓지 않았다면 쓸데없이 코드가 중복이 되어 코드의 길이가 길어지게 됩니다.
    그리고 예1에서 다루었던 상수의 예처럼 혹시라도 링 넓이의 구현법을 바꾸어야 하는 일이 생긴다면 일일히 바꾸어야겠죠.

    예3, 4번으로 나누어 설명할 정도로 함수의 모듈화는 중요합니다.
    제 기준으로는 한 함수는 1~2개의 기능을 하도록 나누는 것이 좋다고 생각합니다.

    +
    내가 생각하는 적절한 함수의 길이.

    https://black7375.tumblr.com/post/162403828110/내가-생각하는-적절한-함수의-길이

    https://black7375.tumblr.com/post/162403828110/내가-생각하는-적절한-함수의-길이



    예 5.
    OOP(객체지향)에서 쓰이는 상속, 인터페이스 등도 추상화의 예이다.

    객체지향에 대해서 제대로 배우기 위해서는 시간을 조금 투자해야 하니,
    클래스로 예를 들도록 하겠다.

    클래스는 구조체에서
    함수(메소드)와 변수를 모아서 선언해두고 사용한다고 생각하면 된다.

    예4의 코드를 C++ 스타일로 바꾸어 보았다.(일부러 생성자, 소멸자는 제외시켰습니다.)
    C++

    class Circle
    {
    protected:
        const double PI = 3.14;
        double r = 0;
    public:
        double area_circle(double r)
        {
            return PI * (r * r);
        }
        double length_circle(double r); //circumference
        {
            return (2 * PI * r);
        }
    };
    
    class Ring: public Circle
    {
    public:
        double area_ring(double outer, double inner)
        {
            return (area_circle(outer) - area_circle(inner));
        }
    };
    
    class Cylinder: public Circle
    {
    public:
        double area_ractangle(double width, double height)
        {
            return (width * height);
        }
        double side_area_cylinder(double r, double height)
        {
            double width = length_circle(r);
            return area_ractangle(width, height);
        }
    };
    
    class Pipe: public Ring, public Cylinder
    {
    public:
        double area_pipe_ring(double outer. double inner)
        {
            return (2 * (area_ring(outer) - area_ring(inner)))
        }
        double side_area_pipe(double outer, double inner, double height)
        {
            return (side_area_cylinder(outer, height) + side_area_cylinder(inner, height));
        }
    
        double area_pipe(double outer, double inner, double height)
        {
            return (area_pipe_ring(outer, inner) + side_area_pipe(outer, inner, height));
        }
    
        double volume_pipe(double outer, double inner, double height)
        {
            return (area_ring(outer, inner) * height);
        }
    };


    상속 관계도.(위에 있을 수록 추상화가 되어있다.)



    그냥 절차형으로 나열이 되어있던 코드들을 도형이란  '특성'을 뽑아 추상화 시켜 Circle, Ring, Cylinder, Pipe라는 클래스로 묶었다.
    그리고 파이프는 원기둥과 링, 원기둥은 원, 링도 원의 특성을 가지고 있다는 것을 떠올려 상속을 시켰다.
    상속도 일종의 추상화인셈.

    '클래스 단위로 추상화를 하면 어떤 이득이 있는가?'라고 묻는다면

     함수보다 큰 분류인 클래스 단위로 프로그램을 바라보게 되면서 대단위의 코드를 읽고 분석하고 이해하기가 훨씬 쉬워진다.
    함수 뿐만 아니라 변수나 상수와 같은 것도 클래스에 따라 잘 분리만 되어 있다면 감에 따라서 클래스의 이름만 보고도 특정 코드를 찾는 것도 가능해진다.
    (캡슐화, 은닉화되어 인텔리전스에 나타나는 정보들이 줄어드는 것은 덤)

    개인적으로 가독성을 극대화 시키기 위해 다음과 같이 클래스 안에 정의를 해놓고 구현은 따로 하는 편이다.
    (따로 구현이 안되는 점은 자바를 별로 안 좋아하는 이유 중 하나)
    C++

    class Circle
    {
    protected:
        const double PI = 3.14;
        double r = 0;
    public:
        double area_circle(double r);
        double length_circle(double r); //circumference
    };
    
    double Circle::area_circle(double r)
    {
        return PI * (r * r);
    }
    double Circle::length_circle(double r) //circumference
    {
        return (2 * PI * r);
    }


    자바의 인터페이스는 추상화를 극대화 시켜서 오직 정의만 되어 있는 추상 클래스이다.(변수, 상수는 못오고 오직 함수(메소드)만.)
    마치 목차만 있는 책이나 생물에서 뼈와 같은 느낌이라 보면 된다.
    상속을 받으면 구현을 따로 해야 된다는 뜻.

    대충 인터페이스를 살펴보자면
    Java

    interface Circle
    {
        public double area_circle(double r);
        public double length_circle(double r);
    }
    
    class Ring implements Circle
    {
        final double PI = 3.14;
        int r = 0;
    
        //implements
        public double area_circle(double r)
        {
             return PI * (r * r);
        }
        public double length_circle(double r)
        {
            return (2 * PI * r);
        }
    
        //not implements
        public double area_ring(double outer, double inner)
        {
            return (area_circle(outer) - area_circle(inner));
        }
    }

    사실 인터페이스란 개념은 자바에서 상속은 한번밖에 못하기에 극복하고자 나온 개념에 가깝다.

    C++에서는 따로 인터페이스라는 것이 없지만 충분히 인터페이스처럼 사용할 수 있는 방법이 있다.
    인터페이스처럼 사용하는 방법은 내가 올려놓은 글을 참고하도록 하자.

    https://black7375.tumblr.com/post/162326257225/c-에서-자바-인터페이스처럼-사용하기
    https://black7375.tumblr.com/post/162326257225/c-에서-자바-인터페이스처럼-사용하기



    예 6.
    C++의 템플릿(Template)을 사용해도 추상화가 가능하다.
    템플릿은 Template라는 말 그대로 형틀이고, 적용 대상의 기능은 정해졌지만 자료형(double, int 같은 것.)은 정해지지 않고 가변적인 특성을 가지게 만듭니다.
    '자료형'을 추상화 한다는 것.

    그래서 의미하는 것은 같지만 여러 자료형이 쓰여야 될 변수, 쓸데없이 오버로딩(Overloading)된 함수의 코드를 줄일 수 있습니다.

    int와 double을 사용한 예시를 보자.(코드를 최대한 짧게 하기 위해 Cirlce 클래스의 정의부분만 사용했다.)

    class Circle
    {
    protected:
        const double PI = 3.14;
        int int_r = 0;
        double double_r = 0;
    public:
        double area_circle(int r);
        double area_circle(double r);
        double length_circle(int r); //circumference
        double length_circle(double r);
    };

    에서 템플릿을 사용하면

    template <typename T> // Hungarian notation: t 
    class Circle
    {
    protected:
        const double PI = 3.14;
        T tR = 0;
    public:
        double area_circle(T tR);
        double length_circle(T tR) //circumference
    };

    이렇게 됩니다.

    이때 템플릿이 적용되는 자료형은 'T'로 표시했으며 네이밍은 바꿀 수도 있습니다.

    헝가리안 표기법으로 t를 사용한 것은 인텔리전스를 사용할 때 헷갈리지 않기 위해 사용했습니다.(맨날 까먹어서...ㅜㅜ)
    (제가 헝가리안을 사용하는 경우는 딱 2경우. 전역변수, 템플릿)

    이젠 템플릿이 적용된 템플릿 클래스 코드를 어떻게 사용하는지 알아보자.

    int main(void)
    {
        Circle<int> int_Circle;
        Circle<double> double_Circle;
    }

    이렇게 사용하고 싶은 함수 내부에서  '<>'안에 자료형을 정의하여 사용할 수 있다.
    이후는 일반 구조체나 클래스를 사용하는 것처럼 사용하면 됨.

    솔직히 템플릿에 관한 것은 쓸 것이 너무 너무 많기 때문에 귀찮아서 여기까지만 쓰겠다.

    +
    템플릿 클래스의 정의와 구현을 파일단위로 분리하여 사용할 때 주의 사항.

    https://black7375.tumblr.com/post/162425322640/템플릿-클래스-정의와-구현-분리
    https://black7375.tumblr.com/post/162425322640/템플릿-클래스-정의와-구현-분리

    ++
    템플릿은 C++의 흑마법이라 불리는 TMP(Template MetaProgramming)의 첫번째 관문 이기도  하다.
    (실행이 아니라 컴파일시에 연산을 해버린다니.. 어마무시!!)
    필자는 TMP를 아직 잘 모르기 때문에 따로 적진 않겠다.(앞으로 공부할 예정)




    요약하면
    데이터를 적절히 변수와 상수로 나눈다.
    변수, 함수, 알고리즘을 잘 사용해서 함수를 구성한다.
    문제를 해결하는 과정에서 값들의 각 연관성에 대해 잘게 달라서 함수를 구성한다. (함수의 모듈화)
    OOP를 지원하는 언어라면 클래스와 상속도 적절히 사용한다.
    자료형이 가변적일 경우 템플릿도 사용할 수 있다.


    그리고...

    또 쓰지만 프로젝트할 때 main에 다 박지 말자.

    요즘 공부할 수록 C++이 마음에 든다 정도?


    어쨌든, 이것 하나만 기억하면 된다.


    프로그래밍은 추상화하는 과정의 연속이다.

    단순한 공식, 반복문부터 OSI7 Layer같은 아키텍쳐까지 모두 추상화라는 관점으로 바라볼 수 있다.


    후기
    하이라이팅을 위해 Visual Studio Code에서 DrRacket Syntax Colorizer를 깔아서 사용했는데 하이라이팅이 제대로 안되어 수작업을 했는데 너무 힘들었다.

    (define (volume_pipe outer inner height)
    (* (area_ring outer inner) height))

    이딴식으로 일관성을 삶아 드셨다.(이쁘지도 않구)

    그냥 highlight.js 로 포팅했다.

    끝~~


    댓글

Designed by black7375.