인생은 고통의 연속

OOP 까는글을 까는글 본문

잡담/개발다반사

OOP 까는글을 까는글

gnidoc 2018. 12. 31. 07:19
    반응형

    최근에 유통기간이 지난 순대를 먹어서 고통받았는데

    회복을 위한 와식생활 중에 페북을 하면서

    우연히 김창준 선배님의 글을 봤고

    재밌는 내용이 있어서 그에 대한 생각을 정리하고자 한다.


    글의 제목을 그대로 번역하자면 "OOP를 빨리 잊을 수록 좋다"는 뜻이고
    김창준 선배님은 이 글에 대해서 "OOP 초심자들의 이해수준을 보여주는 좋은 예"라고 하셨다.
    왜인지 깊이 탐색해보자.




    글 초반에 주로 함수형 프로그래밍(FP)하는 분들이 OOP를 깔때 자주 사용하는 문구를 넣었는데,

    Object-oriented programming is an exceptionally bad idea which could only have originated in California.

    이는 좋지도 않은 OOP는 촌동네(캘리포니아)에서나 쓴다는 말이고 데이크스트라(Dijkstra)가 남긴 말 중 하나이다.

    데이크스트라는 세마포어, 병렬/분산 컴퓨팅 등 최근 전산학에 엄청나게 많은 공헌을 한 분인데 가장 유명한 걸로는 최단 경로 알고리즘(Dijkstra 알고리즘)이 있다. GOTO문을 매우 싫어해서 논문까지 내셨는데 여기까지만 봐도 컴퓨터 엔지니어보다는 사이언티스트에 가깝다.(물론 나도 GOTO문에 대해서는 반대다.) 즉, 아키텍처보다 알고리즘이 더 중요한 분이다.

    대단하신 분이지만 아마 이렇게 빨리 다른 사람들과 코딩할 세상이 올거라고 예상하지 못했을거라 생각한다. 마치 80년대에 메모리가 640KB면 누구에게나 충분하다고 말한 빌게이츠처럼... 그 당시에 누가 하나의 SW프로젝트에 몇 십에서 몇 백명까지 개발자가 투입될거라 생각했겠는가? ㅋㅋㅋㅋ


    그리고 글 가장 처음 이 말을 사용한다.

    Data is more important than code

    난 처음에 번역이 잘못된 줄 알았다. Data와 Code가 다르게 동작한다는 것인가...?
    곰곰히 생각해보니 맞는 말이다. 예를 들어서 Mysql에서 Table 구조가 바뀌면 API 서버의 code를 바꿔야 하는거처럼 Code보다 Data가 더 중요하다. 왜냐? Data가 바뀌면 Code를 바꿔야하지만 Code가 바뀐다고 Data가 바뀌지는 않기 때문이다. 하지만 Data가 Code보다 중요하다는 말에 대해서 완전히 동의할 수는 없었다.

    왜냐하면 예전에 Lisp을 공부하면서 느낀건데 Lisp Code 100줄이 있으면 그걸 이해하는데 적어도 1시간은 걸린다.(Java라면 물론 같은 기능이 100줄로 안되어있겠지만...) 이해하고 보면 엄청 효율적으로 작성된 Code지만 이해하는데 너무 오랜 시간이 걸리고 사실 어떤 부작용을 낳을지 모르기에 고치기 두렵다. 즉, 나는 Code도 중요하다는 것이다. 왜냐? 1시간 뒤에 내가 이 코드를 봤을때나 다른 사람이 봤을때 명확하게 이해가 가야되기 때문이다.

    그리고 어떤 SW를 디자인할때 [목표-Data-아키텍처-Code]의 순서를 따른다고 한다. 근데 OOP에서는 Data Model 설계를 무시하고 모든걸 객체로 넣는다는 것이다. 예를 들어 Customer가 필요하면 class Customer, 렌더링 컨텍스트가 필요하다면 class RenderingContext를 생성한다는 것이다. 그 부분을 보고 사실 뜨끔하긴 했는데 나도 클래스를 마구 생성하긴 했다. ㅎㅎ;; 물론 리팩토리을 거쳐서 필요없는 클래스는 최대한 지양하긴 했지만 어떻게 보면 OOP로 개발하는 사람들의 습관을 잘 꼬집은 것 같다. 하지만 Data를 먼저 설계하라고 하는데 그게 Class를 설계하는거와 다른 의미인가...?라는 생각이 들었다. 어떤점이 다른거지..? 예를 들어서 Java에서는 DTO(Data Transfer Object)로 데이터를 오브젝트로 변환해서 사용한다. 이러면 결국 Data == Class(Object) 아닌가?(물론 Data === Class는 아니겠지만) 이래서 첫 주장부터 삐걱거리기 시작했다.

    • 컨텍스트(Context)가 직역하면 맥락, 문맥으로 쓰이지만 SW에서는 현재 값/상태를 뜻한다.
      예를 들면 OS에서 Context Switching과 같은 의미이다.(프로세스의 상태나 레지스터 값을 교체하는 작업)


    그 다음 더욱 모호한 영역에 들어간다.

    Encouraging complexity

    복잡한걸 권장한다고 FizzBuzz 알고리즘을 OOP로 풀어낸 예시를 보여준다. 간단하게 테스트 코드만 보면

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    class TestConstants {
        static final String _1_2_FIZZ = "1\n2\nFizz\n";
        static final String _1_2_ = "1\n2\n";
        static final String _1_ = "1\n";
        ...
     
        static final int INT_1 = 1;
        static final int INT_2 = 2;
        static final int INT_3 = 3;
        ...
    }
     
    class FizzBuzzTest { 
        
        ...
     
        @Test
        public void testFizzBuzz() throws IOException {
            this.doFizzBuzz(TestConstants.INT_1, TestConstants._1_);
            this.doFizzBuzz(TestConstants.INT_2, TestConstants._1_2_);
            this.doFizzBuzz(TestConstants.INT_3, TestConstants._1_2_FIZZ);
            this.doFizzBuzz(TestConstants.INT_4, TestConstants._1_2_FIZZ_4);
            this.doFizzBuzz(TestConstants.INT_5, TestConstants._1_2_FIZZ_4_BUZZ);
            this.doFizzBuzz(TestConstants.INT_6, TestConstants._1_2_FIZZ_4_BUZZ_FIZZ);
            this.doFizzBuzz(TestConstants.INT_7, TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7);
            this.doFizzBuzz(TestConstants.INT_8, TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7_8);
            this.doFizzBuzz(TestConstants.INT_9, TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7_8_FIZZ);
            this.doFizzBuzz(TestConstants.INT_10, TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7_8_FIZZ_BUZZ);
            this.doFizzBuzz(TestConstants.INT_11, TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7_8_FIZZ_BUZZ_11);
            this.doFizzBuzz(TestConstants.INT_12, TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7_8_FIZZ_BUZZ_11_FIZZ);
            this.doFizzBuzz(TestConstants.INT_13, TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7_8_FIZZ_BUZZ_11_FIZZ_13);
            this.doFizzBuzz(TestConstants.INT_14, TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7_8_FIZZ_BUZZ_11_FIZZ_13_14);
            this.doFizzBuzz(TestConstants.INT_15,
                    TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7_8_FIZZ_BUZZ_11_FIZZ_13_14_FIZZ_BUZZ);
            this.doFizzBuzz(TestConstants.INT_16,
                    TestConstants._1_2_FIZZ_4_BUZZ_FIZZ_7_8_FIZZ_BUZZ_11_FIZZ_13_14_FIZZ_BUZZ_16);
        }
    }
    cs

    비슷한 사례 : 연차별 Hello World 코드

    OOP는 for문과 if문 4개면 끝날 간단한 문제도 이렇게 복잡하게 Coding한다고 한다. 왜냐? 모든걸 Class화 해야하기 때문이다. 출력될 String이 좀 더 복잡하고 최적화까지 해야된다면 StringBuilder를 쓰게되면서 더 복잡해질 것이다. 솔직히 억지긴 하지만 DTO말고 VO(Value Object)도 구현해서 사용하는 Java에서는 외면할 수 없는 문제이다. 안드로이드만해도 Display Class로 화면이 켜져있나 꺼져있나에 대한 상태값을 상수로 두고 있기 때문이다. 그런데 만약에 VO를 안쓴다면 어떻게 쓰지...? FP에서는 공유된 상태와 변경 가능한 데이터를 피해야되기 때문에 애초에 이런 걱정이 없다.

    하지만 FP는 간단할까...? FP에서 Currying으로 3개의 숫자의 합을 구하는 코드이다.

    1
    2
    3
    4
    5
    6
    7
    /* curried function */
    const sumOfThreeThings = x =>
      y =>
        z =>
          x + y + z;
     
    sumOfThreeThings(1)(2)(3); // 6
    cs
    자바스크립트에서 람다&클로저를 사용하여 구현한 것인데,
    보통 다들 구현한다면 아래와 같은 코드가 익숙할 것이다.
    1
    2
    3
    4
    5
    /* not curried function */
    function sumOfThreeThings(x, y, z) {
      return x + y + z;
    }
    sumOfThreeThings(123); // 6
    cs


    난 FP에서 저렇게 람다로 굳이 구현하는 이유가 공감되지 않는다;;
    (비슷하게 JavaScript 공부할때도 bind가 잘 이해가 안됐다)
    Pure function을 쓰고 데이터가 변경되지 않도록 하기 위해서(부작용을 막기 위해서) 저렇게 구현한건데
    아직도 별로 와닿지 않는 방식이다(학교에서 OOP만 배워서 그런가)
    정리하자면 OOP나 FP나 둘 다 관점에 따라서 복잡성이 다른 것 같다.
    OOP는 관계가 FP는 로직이...? 그래서 OOP는 UML로 Class를 볼 수 있는데 FP는 뭐로 보는지 모르겠다 ㅎ;;

    그래서 이 글에서는 UML을 왜 쓰는지에 대한 내용을 바로 이어서 깐다.
    Graphs everywhere
    바나나를 얻으려면 고릴라와 정글 전체를 얻어야한다고 비꼬았는데 대충 코드로 나타내면
    1
    Banana bnn = new Jungle.getBanana(new Gorilla())
    cs

    이렇지 않을까 싶다.
    이건 매우매우 공감되긴했다. 예를 들어서 Java에서 가장 쓰기 귀찮다는 JSONObject. 생성/parsing도 귀찮고 try catch로 예외처리도 해줘야한다. 물론 예외처리는 Javascript, python 들도 마찬가지긴 하지만 Java에서는 굳이 JSONObject, JSONArray를 따로 생성해줘야한다. 물론 Gson 라이브러리를 쓰면 고생은 덜하지만 결국 VO가 필요하기 때문에 Class 간의 참조가 많아져서 거대한 Class Graph를 이루게 된다. 대표적으로 안드로이드에서 Context Class를 UML로 뽑는다면 어마무시할 것이다. Activity, Service 등 안쓰는 곳이 없기 때문이다;; FP도 로직이 복잡할텐데 이걸 어떻게 개발자끼리 공유할지가 궁금하다. 설마 하나하나 읽으려나...

    그리고 주로 학교 과제할때 고민했던 문제가 있는데
    Cross-cutting concerns

    번역하면 횡단관심사인데 찾아본건 좀 다른거 같다. 보통 입출금처리할때 로깅/보안/트랜젝션을 어떻게 처리할까라는 문제가 대표적인 예시이다. 즉 시스템간의 중복, 의존성 문제를 말하는것이다. 하지만 이미  AOP라는 개념으로 위의 문제는 해결이 되었고 글에서 든 예시는 설계를 잘못한 예시 같았다.

    예를 들면 Player와 Monster가 있을때 OOP에서는 Player.hits(Monster m)을 호출해야할지, Monster.isHitBy(Player p)을 호출해야할지, 만약에 Weapon도 고려해야된다면 isHitBy(Player p, Weapon w)로 호출할지 등 캡슐화와 상속으로 인해서 HP와 attackPower와 같은 Data를 사용하는데 매우 꼬인 메서드끼리 호출하게 된다는 것이다. 이건 별도의 AttackManager(마치 자바의 Comparable class처럼!)로 관리하면 될텐데 마치 게임에서 렌더링과 물리엔진을 하나의 메서드에서 구현하는거랑 다를바가 없다;;


    뒤로 갈수록 점차 이상한 얘길하는데

    Object encapsulation is schizophrenic

    There are multiple ways to look at the same data

    Bad performance

    캡슐화는 제약만 생기고 OOP는 경직된 데이터 구조를 요구하고 간접 참조와 포인터 남발로 인해 나쁜 성능이 나온다는 것이다. 설계를 잘했을때 이러면 모를까 이건 Worse Case(=잘못된 설계/개발)인데 그럼 다른 방법론은 다를까...? 결국 설계나 구현이 잘못되면 똑같이 발생하는 문제이다.

    결국 Data를 깊게 고려하라는데 MSA나 DDD에서 DB를 어떻게 나누지라는 생각을 했을때 고려한 내용이 많이 나왔다. 예전에 서비스를 나눌때 하나의 유저에게 다른 타입의 여러개의 데이터가 발생해서 select를 위한 indexing이나 insert 속도를 고민을 많이 했었는데 Nosql도 답이 없고(트랜젝션X) Mysql도 답이 없어서(insert 속도 느림) 참어려웠던 문제가 있었다. 결국 잘해결되긴 했는데 지금은 퇴사해서 진짜 잘해결됐는지는 잘모르겠다 ㅋㅋㅋㅋ


    결과적으로 전체적으로 봤을땐 그냥 퇴사한 전회사를 비판하는 듯한 느낌이다. 
    (나는 이런걸 경험했는데 답이 없었다! 같은 느낌?)
    물론 일부 공감할 수 있는 내용도 있는데 결국 silver bullet을 제시할 수 없는 문제들이다. 예를 들면 Bad Performance와 같이 성능이 그렇게 중요하면 뭘로 하든 잘못 개발/설계한다면 모두 문제일 것이다.(설마 cpu 명령어 하나하나 신경쓰면서 코딩할 일은 이제 없겠지) 아마도 제대로 OOP로 Coding을 하지 않는 조직에 있었을거 같다. 그렇다고 FP나 다른 방법론을 예시를 들면서 구체적으로 얘기하지 않아서 의문이 가는 점도 많았고 특히 Data != Class 라는게 제일 이해되지 않았다. 물론 OOP도 silver bullet이 아니지만 작성자도 답을 찾지 못한거 같은데 좀 OOP를 비판하기에는 성급한거 같았다. 참 의문점도 많았고 배울점도 많았던 글이었다.

    마지막으로

    나는 저런 OOP의 안좋은 사례가 되지 말아야지...

    반응형
    Comments