멍청한 추상화 하지 마라.

C++이 일반화되면서 아주 바보같은 관행이 생겼는데 나는 이걸 ‘멍청한 추상화’라고 부른다.

소켓 프로그래밍을 처음 시작하면…열에 아홉 정도는 CSocket이란 클래스를 만든다. 18년전에 나도 그랬다.
18년전에 내가 짠 코드는 똥이었다. 그리고 그 시절이나 지금이나 소켓 프로그래밍 초짜들이 만드는 CSocket은 십중팔구 똥이다.

단순히 소켓 디스크립터를 랩핑하고 관련 함수를 몇개 넣은 이 클래스는 대개 block mode에 싱글스레드를 기준으로 만들어진다.
여기서 I/O모델이 바뀌면 멀티 스레드가 들어가고 작동 방식이 왕창 바뀐다.
IOCP / select poll / Event Select 등등 I/O모델이 바뀌면 대개 살릴 수 있는 코드가 거의 없다.

예전에도 몇번인가 말한적 있지만 여기선 다이어그램 이쁘게 그려가며 설계(라 쓰고 허세라 읽는다)를 해봐야 소용이 없다. 실제 동작원리를 빠싹하게 알기 전엔 쓰레기같은 추상화만 반복할 뿐이다.

DirectX프로그래밍을 하면 대개 CVertexBuffer와 CIndexBuffer 그리고 CTexture를 만든다. 음..이 경우는 DX 7->8->9->11까지도 약간의 유지보수를 거쳐 계속 사용하곤 한다.
근데 DX12가 되면 살릴 수 있는 코드가 없다. API이름이 바뀐게 문제가 아니고 DX12에서 멀티스레드를 필히 써야하고 리소스 리네이밍같이 드라이버에서 자동화해서 지원해주는 것들이 몽창 빠지다보니 단순히 캡슐화한 코드는 쓸수가 없다. 오히려 디버깅하다보면 빌어먹을 클래스들을 하나씩 벗겨내게 된다. 결국 시간 지나고 보면 다 분해해서 최대한 raw API호출을 한 상태로 전체적으로 재설계에 들어갈 수 밖에 없다.

나는 가능하면 추상화 하지 말자는 주의다.
성능 문제는 일단 접어두고 제일 큰 문제는 어설픈 추상화야말로 코드를 알아보기 어렵게 만드는 주범이기 때문이다.

대개는 무조건 추상화부터 하려고 한다. 해당 플랫폼과 API에 대해서 빠싹하게 아는게 아닌데 추상화부터 한다. 그래서 결함(버그가 아니다)투성이의 클래스가 만들어진다. 물론 그걸 되게 뿌듯해하지. 그리고 재앙이 시작된다. 후임자가 결함을 고치려면 그 빌어먹을 클래스를 뜯어내야 한다. 이것도 후임자가 실력있고 의욕있을때나 가능하다. 보통 후임자들은 전임자보다도 더 떨어지는 경우가 많으므로 그 이해할 수 없는 클래스를 뜯어내기 보단 그 위에 한겹 더 씌운다.
그리고 다음 후임자는 또 위에다 바보같은 클래스를 씌운다. 전임자의 코드는 똥으로 보이는 경향이 있는데 그래서 그 똥을 보지 않으려고 그 위에다 클래스를 또 씌운다. 야 전임자 코드가 똥같으면 차라리 그 클래스 뽀개고 새 클래스로 대체해. 위에다 또 씌우지 말고.

크로스 플랫폼 지원을 위해 추상화가 필요하다고도 주장하는데 틀린 소린 아니다. 그런데 그 방법이 또 문제다.
아까처럼 소켓 디스크립터를 감싸는 정도의 CSocket따위로 크로스플랫폼 지원이 될것 같아? 전혀 아니올시다.
여기서 가장 큰 문제는 최초로 추상화를 시도하는 시점에서, 예를 들면 CSocket을 처음 만들던 시점에서 여러 플랫폼에 대한 이해가 없는 경우가 많다는 것이다. Windows 기반에서 CSocket을 만들었을 당시에, 리눅스라든가 다른 플랫폼의 소켓 API에 대한 지식이 없을 가능성이 매우 높다.(대부분의 경우 다른 플랫폼은 고사하고 Windows에서 제공하는 I/O모델만 해도 엄청 많은데 그중에 하나도 제대로 아는게 없다.)
물론 대부분의 경우 처음짠 네트워크 코드가 정상작동할리 없으므로 정말로 크로스플랫폼 지원을 해야할 날은 오지 않지만. 하여간 크로스 플랫폼 지원할 날이 왔을때 이런 식으로 만든 클래스가 전혀 도움이 되지 않는다.

경험상 다른 플랫폼으로 갈때는 차라리 베이스로 하나의 OS, 하나의 API를 결정해두고 가는 것이 좋다.
win32가 주플랫폼이라면 그냥 win32로 짜고 다른 OS의 버전에서 win32 API와 모양이 똑같은(혹은 거의 같은) 함수 껍데기를 만들고 그 안에 해당 OS에서 비슷한 기능을 구현하면 된다.
이렇게 하면 적어도 멍청한 추상화 클래스를 모두 숙지할 필요가 없다. 하나의 OS에만 익숙한 프로그래머라도 다른 OS의 버전도 작업할 수 있다. 가장 좋은 점은 확실히 동작하는 신뢰할 수 있는 버전이 하나는 존재하게 된다는 것이다.

그리고 계속 언급했지만 멀티스레드라든가 비동기 I/O에 관련해서 함수단위로는 똑같은 기능을 보장하기 어려울 수 있다.
따라서 크로스플랫폼을 위한 추상화라면 개별 함수의 작은 단위의 기능보다는 좀더 큰 단위의 기능으로 추상화하는 것이 낫다.
그래픽스의 예를 들면 CVertexBuffer와 CTexture보다는 RenderBox()라든가 RenderCharacter()라는 식으로 큰 덩어리의 기능을 추상화시켜서 각각 구현하는게 훨씬 현실적이고 유연하다.

정리한다.
내가 전에 설계하지 말라고 한적 있지?
설계 뽕, C++ 뽕, 객체지향 뽕, 디자인패턴 뽕 맞은 이들에게 고하노니…
빠싹하게 내부를 알기 전엔 추상화도 하지 마라.
그 추상화 똥될거 뻔하거든.

잠이 안오는데 갑자기 지난 18년간 API돌아가는건 하나도 모르면서 미친듯이 쓰레기같은 추상화를 해왔던 놈들(물론 초창기의 나도 포함해서)이 생각나서 적어봤다.


멍청한 추상화 하지 마라.”에 대한 답글 9개

  1. 미분이랑 프로그래밍은 무슨 연관성이 있나요
    특히 삼각함수 미분이나 체인룰(연쇄법칙)같은 것을 보면 머리가 지끈 거립니다. 미분은 사회과학에도 쓰이고 자연과학에도 쓰이니 어찌되었든 쓰일 것 같긴 한데 말이죠. 뜬금없는 질문 불쾌하셨다면 사과드립니다.

    좋아요

    1. 생각나는건 물리처리할때 정도요? 쉐이더 프로그래밍할때 편미분 쓰는 경우가 있긴한데 직접 계산할 필요는 없고 명령어로 제공됩니다.
      뭘 만드냐에 따라 다를텐데 전자공학 관련 시뮬레이터나 물리 관련 뭘 만든다면 박터지게 미적분을 하겠죠.

      좋아요

  2. 저도 비슷한 경험이 있습니다. 기존에 작성해서 10년 넘게 사용하던 라이버러리를 2005년도 쯤 작성했던것을, 멀티쓰레딩을 지원하려고 하니, 상태값 의존이 많고 추상화 범위가 적절하지 않아서, 동시성 문제가 발생하더군요, 결국 상태값 의존 걷어내고, 추상화 범위가 큰 함수는 걷어내고, 함수들이 오버랩 되서 실행되도, 문제없는 단위로 잘게 나누고. 함수내부에서 사용하는 지역변수에만 의존해서 돌게 해야 하니,
    결국 최근에 함수형 프로그래밍이 인기인게, 객체지향 프로그래밍이 상태 의존적인 동기화 프로그래밍에 적합하다 보니. 여러개의 동작을 묶어서 하나의 동작으로 만드는 추상화는 결국 동시성 문제에 취약한 문제가 있어서 언어레벨에서 좋은 해법이라 그런것 같더군요. 그래서 함수형 언어인 F#도 기욷거리게 되었는데…
    어릴때 1999년도 에 사서 봤던, 군대 신검 하는데 까지 들고 가서 읽었던.. 코드 컴플리트에서 너무나 강렬하게 받은 추상화는 아름다운 것이고, 세상에 모든 문제를 해결할 수 있는 마법이라고 뇌에 주입이 되어 있었는데
    뭔가 종교적 도그마 때문에 찝찝했는데 이 글을 읽으니 좀 마음이 편하네요.

    좋아요

    1. 어설픈 추상화의 문제는 멀티스레드 프로그래밍이나 비동기 프로구래밍에서 크게 두드러지는것 같습니다. 코드 컴플릿 나오던 시절엔 지금처럼 비동기나 멀티스레드 프로그래밍을 많이 하진 않았을테니 책이 전적으로 틀렸다고까지 생각은 안하고요. 근데 저도 코드 컴플릿 보면서 이건 아니다 싶은 내용들이 있었네요.

      좋아요

  3. 안녕하세요 c++ 책을 한번 보고, 자료구조 책을 보면서(열혈강의 자료구조) c++을 사용해서 코딩하고 있습니다.
    저는 나름대로 뭔가 설계를 하고, 추상화?를 하고 있습니다.

    https://github.com/junog115/Data_Structure/tree/master/Circular_linked_list

    혹시 봐주실 수 있으실지 모르겠지만, 이런 식의 추상화?는 영천님이 말씀하시는 멍청한 추상화에 해당하나요? 아니면 추상화, 설계 축에도 못드는 걸까요..?

    좋아요

    1. 전 원래 코드 리뷰 요청엔 응하지 않습니다만…올려주신 링크 따라가보니 코드가 짧아서 조금 봤습니다. 코드 깔끔하게 짜셨네요. 코드는 알아보기 쉽습니다만 Linked List하나 구현하는데
      굳이 virtual까지 써서 클래스화할 필요가 있는지 저는 잘 모르겠습니다. 전 몇줄 안되는 C함수 세개와 구조체 한개로 구현해서 십수년째 쓰고 있거든요. 코드가 알아보기 어려운것도 아니고 돌아가는 코드를 몇겹 감싼것도 아니니 멍청한 최적화까진 아니라고 생각합니다. 다만 제 기준에선 다소 불필요한 추상화가 될지도 모르겠습니다. 하지만 작성하신 분 본인이 저렇게 짜는게 편하다면 그게 본인에게는 가장 맞는 방법이겠죠.

      좋아요

      1. 제가 공부한 책에선 CMyCirNode 라는 껍데기를 하나 만들어서 거기에 next 에 관한 맴버가 들어가서 굳이 이럴 필요는 있나? 했지만 이렇게 짜봤는데, 굳이 그렇게는 할 필요가 없었나보네요!

        짧은 코드지만 봐주셔서 감사합니다!

        좋아요

댓글 남기기