
제네릭 프로그래밍(Generic Programming)은 구체적인 효율적인 알고리즘에서 추상화를 수행하여 제네릭 알고리즘을 도출하고,
이를 다양한 데이터 표현과 결합하여 유용한 소프트웨어를 생성하는 아이디어를 중심으로 한다.
예를 들어, 유한한 시퀀스(finite sequences)에서 동작하는 **제네릭 정렬 알고리즘(generic sorting algorithms)**을 정의할 수 있으며,
이를 배열(arrays)이나 연결 리스트(linked lists)와 같은 서로 다른 데이터 구조에 적용하여 다양한 정렬 알고리즘을 생성할 수 있다.
논문에서는 데이터(data), 알고리즘적(algorithmic), 구조적(structural), 표현적(representational) 추상화의 네 가지 유형을 다루고 있으며,
이를 활용하여 Ada 라이브러리에서 소프트웨어 컴포넌트를 구축하는 예제를 소개한다.
주요 논점은 제네릭 알고리즘과 그 형식적 명세(formal specification) 및 검증(verification) 방법이며, 이를 퀵소트(quicksort) 알고리즘에서
사용되는 **분할 알고리즘(partitioning algorithm)**을 예로 들어 설명한다.
논문의 결론은, 제네릭 프로그래밍을 이용한 소프트웨어 컴포넌트 라이브러리가 소프트웨어 생산성 향상에 중요한 이점을 제공할 수 있다는 점을 강조한다.
Alexander A.Stepanov 1988
제네릭 프로그래밍이란 무엇인가?
제네릭 프로그래밍이란 하나의 데이터가 특정 데이터 타입에만 종속되지 않고, 여러 데이터 타입을 가질 수 있는 기술에 중점을 두어
재사용성을 높일 수 있는 프로그램 방식이라고 요약할 수 있다.
그럼 우선 우리가 시작해야할 지점은 무엇인가? 그 부분을 찾아보자.
우리는 왜 제네릭 프로그래밍이 나왔는가에 대해서 시작할 것이다.

제네릭 프로그래밍의 시작은 1980년대 OOP의 부상속에서 차별화에서 시작했다.
1980년대 초 OOP는 '객체', '클래스'를 중심으로 프로그래밍의 대세를 차지하기 시작했다.
하지만 이러한 접근 방식에는 한계가 엄연히 존재했다.
1. 타입에 종속적인 코드: 특정 데이터 타입에만 작동하는 클래스를 작성한다.
예를들어 정수 배열을 정렬하는 함수와, 문자열 배열을 정렬하는 함수는 같은 알고리즘을 공유함에도 불구하고 별개로 작성해야만 한다
2. 재사용성의 제한: OOP는 상속과 다형성을 통해 일부 문제를 해결할 수 있었지만 여전히 코드의 재사용성에는 제약이 있었다.
제네릭 프로그래밍은 이러한 문제를 극복하기 위해 등장했다.
위에 논문의 서문에도 나왔듯
다양한 데이터 표현 방식과 결합할 수 있는 일반적 알고리즘을 얻는데 집중할 수 있다.
제네릭 프로그래밍의 특징은 많지만, 현대의 제네릭 프로그래밍은 다음 3가지를 가진다.
1. 타입 독립성(Type Independence): 특정 데이터 타입에 종속되지 않음
2. 코드 재사용성(Code Reusability) : 동일 알고리즘이 다른 타입에도 적용될 수 있음
3. 효율성(Efficiency) : 런타임 오버헤드 없이 컴파일 타입에 최적화된 코드를 생성한다.

STL은, 적어도 나에게 있어서, 프로그래밍이 가능하게 하는 유일한 길을 의미합니다. 사실 STL은 대부분의 교과서에서 제시되었고
여전히 제시되고 있는 C++ 프로그래밍과는 꽤 다릅니다. 하지만 아시다시피, 저는 C++로 프로그래밍하려고 했던 것이 아니라,
소프트웨어를 다루는 올바른 방법을 찾으려고 노력했습니다. 저는 오랫동안 제가 말하고 싶은 것을 표현할 수 있는 언어를 찾아왔습니다.
다시 말해서, 저는 제가 무엇을 말하고 싶은지 알고 있습니다. 저는 C++로 말할 수 있고, Ada로 말할 수 있으며, Scheme으로도 말할 수 있습니다.
저는 언어에 저 자신을 맞춥니다. 하지만 제가 말하려는 것의 본질은 언어에 구애받지 않습니다.
지금까지, C++는 제가 말하고 싶은 것을 말하기에 제가 발견한 최고의 언어입니다. 이상적인 매개체는 아니지만,
저는 다른 어떤 언어보다 C++에서 더 많은 것을 할 수 있습니다.
사실 언젠가 제네릭 프로그래밍을 염두에 두고 특별히 설계된 언어가 나오기를 저는 진심으로 바랍니다.
-Alexander A.Stepanov
제네릭 프로그래밍은 1970년대 후반부터 연구하다가, Ada에서 시작해 Scheme을 거쳐 C++에 정착하여
알렉산더 스테파노프는 C++에서
알고리즘은 데이터 구조에 독립적이어야한다는 철학을 실현할 라이브러리 STL을 만든다.
사실 C++의 시작이 C에서 OOP를 적용하기 위해서 시작된 것이라는 걸 생각하면 조금 블랙코미디처럼 느껴지기도 한다.
OOP의 핵심 개념의 캡슐화는 데이터와 행위의 결합인데, STL은 그 행위와 데이터를 분리하는데 힘을 쓰기때문에,
마치 OOP를 위해 태어난 언어에서 OOP와는 다른 철학을 구현하는 핵심 라이브러리가 탄생했다는 것은 참 재밌는 것이다.
(물론 생각에 따라 정반대의 철학은 아니기때문에 충분히 나올 수 있는 개념이긴했다)
실제로 제네릭 프로그래밍이 OOP에 대항해서 나왔다는 사실은 이후에
C++ 표준 템플릿 라이브러리(STL)의 창시자인 알렉산더 스테파노프가 말한다

네, STL은 객체 지향적이지 않습니다. 저는 객체 지향성이라는 것이 거의 인공 지능만큼이나 사기라고 생각합니다.
저는 이 OOP를 하는 사람들로부터 흥미로운 코드 조각을 아직 보지 못했습니다. 어떤 면에서는, 제가 AI에게 불공평하긴 합니다.
저는 MIT AI 연구소 사람들로부터 많은 것을 배웠고, 그들은 정말 근본적인 작업을 해냈습니다.
빌 고스퍼의 Hakmem은 프로그래머가 읽어야 할 최고의 것들 중 하나입니다. AI는 진지한 기반이 없을 수도 있지만,
고스퍼와 스톨먼(Emacs), 모세스(Macsyma), 그리고 서스만(가이 스틸과 함께 Scheme)을 만들어냈습니다.
저는 OOP가 기술적으로 건전하지 않다고 생각합니다. OOP는 단일 유형에서만 달라지는 인터페이스의 관점에서 세상을 분해하려고 시도합니다.
실제 문제를 다루려면 여러 유형에 걸쳐있는 인터페이스 패밀리인 다중 정렬 대수가 필요합니다.
저는 OOP가 철학적으로 건전하지 않다고 생각합니다. OOP는 모든 것이 객체라고 주장합니다.
그것이 사실이라 할지라도 그다지 흥미롭지 않습니다. 모든 것이 객체라고 말하는 것은 아무것도 말하지 않는 것과 같습니다.
저는 OOP가 방법론적으로 잘못되었다고 생각합니다. OOP는 클래스로 시작합니다.
마치 수학자들이 공리부터 시작하는 것과 같습니다. 당신은 공리부터 시작하지 않습니다.
당신은 증명부터 시작합니다. 관련 증명들을 많이 발견했을 때만 공리를 떠올릴 수 있습니다. 당신은 공리로 끝을 맺습니다.
프로그래밍에서도 마찬가지입니다. 흥미로운 알고리즘부터 시작해야 합니다.
알고리즘을 잘 이해했을 때만 알고리즘이 작동하도록 하는 인터페이스를 떠올릴 수 있습니다.
-Alexander A.Stepanov
알렉산더는 OOP를 기술적으로나 철학적으로 극렬하게 비판하는데, 사실 현대 언어들의 OOP 개념을 보면 어느정도 이해가 가는 맥락이다.
왜냐하면 C#과 자바등 현대의 언어들은 모든것을 'object'를 상속받는 식으로 설계되어있기때문에, 생성하는 모든것을 Object(객체)라는 관점에서 보기때문이다.
이렇게 되면 객체의 경계가 모호해지기때문에, 이론상의 OOP와 현실적 구현의 간극이 엄연히 존재하게 되는 것이다.
사실 스테파노프의 OOP 비판은 상속Inheritance 에 국한되어있던 90년대 초반의 OOP에 가깝다. 실제로 OOP는 그 초기에 모습에 변화가 되기도했고
최근에는 명확한 is-a 관계가 아니면 상속을 가급적 쓰지 않기를 권장하는 부분도 있다.
이 글을 보고 그럼 OOP가 무조건 나쁜 것인가? 라고 볼 사람떄문에 부연설명하자면
스테파노프의 OOP의 '타입' 문제에 초점을 맞추지만, 실제로 OOP 프로그래밍은 일반적으로 '관계'에 초점을 두기때문에
뛰어난 프로그래머의 자신의 학문에 대한 자부심으로 인해서 조금 다른 철학을 깍아 내리는 것이 아닐까하는 것도 사실이다.
이제 역사를 알았으니, 이제 어떻게 사용하는지 한번 보자.
현대에서의 제네릭 프로그래밍

제네릭을 구분 짓는데 여러가지가 있다.
기본적으로 여러방식이 나뉘지만 일반적으로 자주 사용되는 분류법으로 어떻게 분리되어있는지만 이야기해보자.
정적 제네릭 vs 동적 제네릭

(C++로 작성된 제네릭)
C++ 템플릿, Rust, Swift가 가지고 적용하는 방식으로
컴파일 타임에 타입이 결정된다.
코드가 인스턴스화될때(사용 되어질때) 타입이 결정되어지므로, 오버헤드 없이 최적화된 코드가 생성된다.(인스턴스화 시점 명확한 타입이 결정된다)
타입을 직접 지정하거나, 컴파일러가 타입을 추론하는 형식이다.

(Java로 작성된 제네릭)
Java,C# 제네릭이 가지고 있는 방식.
Jjav,C#의 제네릭은 내부적으로 런타임에 타입이 결정되지 않고, 타입정보를 삭제하는 type erasure를 사용한다.(타입 체크 후, 런타임시 타입삭제)
즉, 컴파일 타입에만 타입을 체크, 런타임에는 제네릭이 object나, system.object로 변환된다.
덕분에 같은 바이트 코드를 사용하여 런타임 오버헤드를 없애는 방식이다.
타입 매개변수 vs 컨셉 기반 제네릭

C++ 템플릿, Java제네릭,C# 제네릭이 사용하는 방식으로
T,K,V 같은 타입 매개변수를 사용하여 코드 재사용성을 높이는 방식이다.

C++20 Concepts, Rust의 Trait Bounds, Swift의 Protocol Generics등이 사용하는 방식으로
제약조건을 추가하여 안정성을 높인 방식이다.
이후에 제네릭은 발전해서
템플릿 메타 프로그래밍이 된다.

기존에 타입을 일반화하여 재사용 가능한 코드를 작성하는 프로그래밍 방식이 '제네릭 프로그래밍'이라면
템플릿 메타 프로그래밍은 컴파일 타임에 코드를 실행하고, 최적회된 결과를 생성하는 방식이다.
즉 제네릭 프로그래밍의 코드 실행시점이 런타임 이면, 템플릿 메타 프로그래밍의 코드 실행시점은 컴파일 타임이라고 할 수 있다.
핵심은 템플릿을 프로그래밍 언어 처럼 사용한다는 것이고, 템플릿 안에서 다양한 프로그램을 만들 수 있다.
즉 제네릭 프로그래밍에서 성능을 얻기 위해 만들어진 방식인 것이다.
제네릭은 잘 보면 마치 은탄환처럼 알고리즘을 추출하여 모든것에 적용하기 쉬운 것처럼 보인다.
하지만 이는 이론의 관점이지 실적용에서는 많은 비판을 받는다


리누스가 앞장서서 STL을 비판했지만 너무 직설적이라서 번역은 하지 않겠다.
제네릭 프로그래밍은 장점만 보이는 것 같은데 단점은 없어?

예시 코드를 하나보자.
C++의 std::transform 함수는 입력범위[first,last]의 각 요소에 대해 단항 연산자(unary_op)를 적용하고, 결과를 출력한 후, 반복자(d_first)로 저장한다.
이 코드는 매우 간단한 코드지만, 문제가 생길때 오류 메시지를 길게 내뿜기 시작한다.
템플릿 코드에서 문제가 발생하면, 컴파일러는 추상화 계층까지 올라가 분석하고, 이 과정에서 복잡한 오류 메세지를 발생시켜 프로그래머로 하여금 실수를 찾기 힘들게 한다.
흔히 우리가 하는 프로그래밍 방식인 '레고 조립(래핑된 메서드 블록을 가져와 조립)'하는 방식은 실수가 눈에 금방 잡히지만, 템플릿 인스턴스화 실패는 정말 버그 잡기가 어렵다.
또한 자바 제네릭 방식인 Type Erasure 방식은
컴파일러가 체크하는 것처럼 보이지만, 런타임에는 타입 정보가 소거되어 예상치 못한 오류가 발생할 수도 있다.
(C#은 런타임에도 제네릭 타임 정보가 유지된다 Reification - IL에서 구체적인 제네릭 타입이 보존되어 런타임에도 타입정보를 확인할 수 있기에 자바 제네릭의 문제다.)
그리고 타입 매개변수 제네릭 프로그래밍은 가독성을 떨어뜨리기로 악명높고.
결국
이론상으로는 우월하고 멋있어보이지만, 실무에서는 프로그래머에게 지옥을 선사한다고 할 수 있다.
제네릭 프로그래밍은 정말로 아름다운 프로그래밍을 가능하게 만들 수 있고,
이를 통해서 엄청난 소프트웨어 공학의 '부'를 축적할 수 있다.
하지만 잘못 사용하면 제네릭 프로그래밍 속에 숨겨진 추상화의 '빚'을 판단하지 못해, 프로그래밍이 지옥이 되는 대공황도 올 수 있다는 점을 알아야한다.
결국 이 모든 것을 감안해서 조절하는 것이 프로그래머의 실력인 것이다.
댓글 영역
획득법
① NFT 발행
작성한 게시물을 NFT로 발행하면 일주일 동안 사용할 수 있습니다. (최초 1회)
② NFT 구매
다른 이용자의 NFT를 구매하면 한 달 동안 사용할 수 있습니다. (구매 시마다 갱신)
사용법
디시콘에서지갑연결시 바로 사용 가능합니다.