본문출처:
https://medium.com/@yjw113080/javascript%EC%9D%98-%EB%91%90-%EC%B6%95-310fc1091079
JavaScript의 두 축
Part 1: 프로토타입형 상속, 클래스 상속의 지옥불 탈출하기
medium.com
Javascript는 가장 중요한 프로그래밍 언어 중 하나입니다. 단순히 인기가 많아서가 아니라, 프로그래밍의 발전 역사에 굉장히 중요한 두 개의 패러다임을 차용하고 있기 때문이죠.
- 프로토타입형 상속(클래스 없는 객체, 프로토타입 위임(OLOO — Objects Linking to Other Objects)),
- 함수형 프로그래밍(클로저를 동반하는 람다형 함수로 구현)
이 둘을 합쳐서 JavaScript의 두 축(the two pillars of JavaScript)이라고 합니다. JS는 가장 영향력 있는 프로그래밍 언어 중 하나인 이유는 바로 이 때문입니다. 다른 많은 언어들이 이 두 가지중 하나, 혹은 모두를 가져가서 자기 만의 버전을 만들었습니다. 우리가 어플리케이션을 개발하는 방법, 내지 프로그래밍 언어라는 것 자체를 변화시켰다고 해도 무방합니다.
JS 창시자인 Brendan Eich가 이 두 축을 직접 개발한 것은 아닙니다. 그렇지만 대중에게 이 개념들을 전방위적으로 접하도록 한 것은 JS였죠. 이 둘 모두 똑같이 중요하지만, 많은 JS 개발자들이 이 둘 중 하나 혹은 모두를 완전히 놓치고 있습니다. JS는 개발자 스스로 갈고 닦지 않아도, 코드를 이상하게 짜도 돌아가기는 하기 때문이죠.
이건 사실 기능입니다. 사람들이 쉽게 JS를 선택하여 익히고 사용하게 만드니까요. 그렇지만 JS 개발자로서 여러분이 그런 식으로 개발하는 것은 최대 1년 미만이어야 합니다. 이 글을 읽는 여러분이 아직 그 단계에 있다면, 이제는 레벨업을 해야 할 때입니다.
여러분이 constructor를 정의하고 여기에서 상속 받는 객체를 생성한다면, JS를 제대로 배웠다고 할 수 없습니다. JS의 가장 강력한 장점을 이용하지 못하고 있으니까요.
거대한 쓰레기를 만들고 있지는 않나요?
“자신이 어둠 속을 걷고 있는지 모르는 자들은 절대 빛을 찾고자 하지 않을 것이다.”
~ Bruce
Constructor는 JS의 개방 폐쇄 원칙을 위반합니다. Constructor를 호출하는 모든 코드들이 오브젝트를 생성할 때 이 constructor에 묶여 버려서 높은 의존성(highly coupled)을 갖게 되기 때문입니다. 새 객체 인스턴스를 사용하는 것 대신에 객체 툴을 사용하고 싶으신가요? 객체를 재사용하고 Garbage collector가 프레임레이트를 지연시키지 않게 하시려구요? 안타깝지만 JS에서 이렇게 하시면 모든 호출자들이 망가지거나, 돌아가더라도 어디 좀 모자란 팩토리 함수로 겨우겨우 돌아갈 겁니다.
constructor 함수로 추상 개체를 리턴하게 되면, 프로토타입 링크가 끊어지고 ‘this’ 키워드와 new 객체 인스턴스 사이의 바인드가 해제됩니다. 게다가 이렇게 만들면 this를 전혀 쓸 수 없기 때문에, 원래의 팩토리 함수보다 훨씬 제한적이죠.
더욱이, strict 모드에서 돌아가지 않는 constructor는 엄청나게 위험합니다. 호출자가 ‘new’를 놓치거나 개발자가 strict mode나 ES6 클래스(휴..)를 사용하지 않으면 this에 할당되는 모든 게 글로벌 네임스페이스에 지정됩니다. 오염 수준이죠. 그나마 Strict mode가 있기 전까지는 이런 랭귀지 차원의 결함 때문에, 찾을 수도 없는 버그가 막 발생했죠.
JS에서는 팩토리 함수가 아주 간단한 constructor 함수입니다. ‘new’ 요건, 글로벌 스페이스 문제, 희한한 제약사항들(아주 짜증나는 첫 글자 대문자화 등등을 포함하죠)이, 위에서 설명한 constructor에서 빠진다는 점이 다르죠.
다시 한 번 명확히 말하면, JS에서는 constructor 함수가 필요 없습니다. 어떤 함수든 새로운 객체를 리턴할 수 있기 때문입니다. 역동적인 객체 확장, 오브젝트 리터럴, ‘object.create()’ 만으로 귀찮은 것들 없이, 필요한 모든 것을 할 수 있습니다. 게다가 this는 다른 함수에서처럼 모두 정상 동작하고요. 엄청나죠! :)
클래스 상속의 지옥불
“좀 그러는 게 현명할 텐데도, 저는 잘 비참해지지 않네요.” ~T.H. White
모든 분들이 끓는 물 속 개구리 비유를 들어보셨을 겁니다. 개구리를 끓는 물 안에 넣으면 뛰어올라 도망치겠지만, 차가운 물에 넣었다가 서서히 끓이면 위험을 감지하지 못하기 때문에 그 안에서 죽게 됩니다. 이 소주제에서는 우리가 그 개구리입니다.
constructor가 프라이팬이라고 하면, 클래스 상속은 그냥 불이 아니라 단테의 <신곡>에 등장하는 불입니다.
고릴라/바나나 문제:
“객체 지향 언어의 문제점은 이와 동반되는 암묵적 환경에 있습니다. 바나나를 달라고 했더니, 바나나를 들고 있는 고릴라와 정글이 통째로 오는 거죠.”
~ Joe Armstrong
클래스 상속은 보통, 하나의 부모에서 자식에게 상속되는데 이 때문에 이상한 분류체계를 사용하게 됩니다. 이상하다고 말하는 이유는, 모든 객체 지향 디자인 분류체계는 처음에는 맞게 시작했다가 종국에는 틀린 체계가 되기 때문입니다. Tool과 Weapon이라는 두 가지 클래스를 만들고우리가 개발을 시작했다고 해보죠. 결과는 이미 망했습니다.
높은 의존성 문제
부모-자식 간의 의존성은 객체 지향 디자인 중에 단연 최고로 강력합니다. 즉, 재사용이 되지 않고 현대적이지 못합니다. 이런 상태에서 클래스에 아주 작은 변화만 줘도, 관련되지 조차 않아야 하는 것들이 몽땅 망가집니다.
불가피한 코드 반복 문제(the Duplication by Necessity problem)
분류 체계의 문제를 해결하기 위한 방법은 시간을 되돌아 가서, 어떤 게 어떤 것으로부터 상속 받는지를 약간 바꾸어 새 클래스를 짜는 것입니다. 그렇지만 이미 너무 심하게 의존성이 높아져버려서 제대로 추출하거나 리팩터링할 수조차 없죠. 결국은 코드를 재사용하는 것이 아니라 아예 복제를 하게 될 것입니다. 여기서 DRY(Don’t Repeat Yourself) 원칙을 위배하게 되죠
결과적으로 여러분은 아주 미묘하게 다른 클래스로 가득찬 정글을 스스로 만들게 될 겁니다. 상속 레벨을 더하면서는 클래스가 점점 더 절뚝거리게 되는 기현상을 보게 됩니다. 버그를 하나 찾으면 그 장소에서만 고치는 게 아니라 모든 코드를 다 고쳐야 하는 상황이 발생하기도 하죠.
“앗, 하나 놓쳤다!” — 이 세상의 모~든 객체 지향 프로그래머.
이건 객체 지향 디자인 내에서는 불가피한 코드 반복 문제(the duplication by necessity problem)이라고 부릅니다. ES6 클래스는 이 문제들을 전혀 해결하지 못합니다. 심지어 이 문제는 더 악화됩니다. 쓸데없이 나와서, 수 없이 많은 책과 블로그 포스트에서 애초에 갱생 불가한 이 나쁜 아이디어를 재생산하기 때문이죠.
클래스는 현재 Javascript에서 가장 해로운 기능입니다. 기술 표준화에 최선을 다한 똑똑하고 성실한 모든 사람들을 존경하지만, 그들이 늘 옳은 일만 하는 것은 아닙니다. 여전히 Brendan Eich가 웹과 프로그래밍 언어와 컴퓨터 공학의 발전 전반에 엄청난 기여를 했다고 생각하기는 합니다. :)
The Fallout
이러한 문제는 어플리케이션이 확대되면서 같이 커집니다. 그리고 결국에는 어플리케이션을 처음부터 다시 써야 하는 지경에까지 이르죠. 비즈니스 차원에서 손실까지 감당해야 하는 때가 오기도 합니다.
개발 과정에서 이러한 문제는 끊임 없이, 회사를 옮겨도, 프로젝트를 시작해도 반드시 나타납니다. 우리는 실패에서 배우기는 하는 걸까요??
제가 일했던 한 회사에서는, 이 문제 때문에 코드를 다시 처음부터 쓰기 위해 릴리즈 날짜를 일년을 통째로 옮겨야 했었습니다. 저는 업데이트를 하자는 주의이지, 다시 쓰자는 입장을 좋아하지는 않습니다. 제가 컨설팅을 했던 또 다른 회사는 그 문제가 회사 전체를 크래시 내서 망하게 할 지경이었습니다.
클래스와 상속 문제는 입맛에 맞는 스타일의 문제가 아닙니다. 이런 선택이 프로덕트 자체를 망가뜨릴 수도 있습니다.
큰 회사들은 아무것도 잘못된 게 없는 양 지나가지만, 스타트업들은 그들의 프로덕트와 마켓 핏에 매달리는 동시에 이런 IT문제 위에서 비즈니스를 굴릴 비용이 없습니다.
클래스 상속을 모두 뺀 현대적인 코드에서는 이런 문제가 전혀 발생하지 않습니다.
JavaScript의 광명
“완벽은 더할 것이 없을 때 완성되는 게 아니라, 더 이상 뺄 것이 없을 때 완성된다.” ~ Antoine de Saint-Exupéry
오래 전에 프로토타입식 상속을 사용하기 위한 방법을 정리하기 위해서 라이브러리 관련 조사를 한 적이 있습니다. 그러다 아주 흥미로운 생각이 떠올랐죠. 팩토리 함수를 상속을 받을 부모로 삼고 같이 함수를 쓰면 어떨까? 이걸 저는 composable 팩토리 “스탬프”라고 이름 지었습니다. 관련 라이브러리는 “Stampit”이고요. 이 라이브러리는 아주 작고 간단합니다. 2013년에 O’Reilly Fluent Conferenc에서 Stampit에 대해 이야기할 기회가 있었습니다. 그리고 이에 관한 블로그 포스트도 적었죠.
스탬프를 사용해서 코딩 스타일을 바꾼 개발자들의 커뮤니티가 생겨났고, 아직 작지만 서서히 성장하고 있습니다. Stampit은 매월 수백만명의 사용자가 있는 다양한 상용 앱에서 이용되고 있습니다.
“ Stampit을 많이 사용해왔고, 프로토타입의 종류와 스탬프의 작성 방법을 분리함으로써 오는 간편함에 반했습니다. 클래스 기반의 상속 트리는 언제나 골치가 아픈데요, 특히 비즈니스 니즈가 자주 바뀌는 경우에 특히 그렇습니다. Stampit은 오버헤드 없이 인터페이스처럼 작업하고, 개별 데이터를 다양한 소스에서 상속 받을 수 있게 해줍니다. 이제 프로토타입식 상속에 익숙해서 잘 쓰고 있고, 제가 만든 객체들은 이제 형태도, 폼도 없이 물 같아요. 클래스 같은 MSG가 없죠.” ~ Justin Schroeder, Senior Developer, Wrecking Ball Media Group
Stampit 은 물론 유일한 대안은 아닙니다. Douglas Crockford는 new나 this를 전혀 쓰지 않는 대신 코드 재사용을 위한 함수적 접근을 택합니다.
그의 객체는 상태값을 갖지 않는 함수들 뭉치입니다. 혹은 메서드 없이 데이터만 들어가 있는 오브젝트입니다. 객체를 수백, 수천개 만들어서 사용하지 않는 이상, 어플리케이션이 아주 부드럽고 실시간으로 돌아갈 수 있게 해주죠. 이런 상태에서 메서드만 호출하도록 위임하면, 수동으로 관리하는 메모리를 많이 줄여줄 수 있습니다.
다른 좋은 대안에는, 상속 대신에 JavaScript 모듈을 활용하는 방법도 있고 그냥 단순하게 객체를 클론 뜨는 방법도 있습니다. (e.g. `Object.assign()`, `$.extend()`, `_.extend()`, etc…).
복사 메커니즘은 사실 프로토타입형 상속의 또 다른 형태입니다. 클론 속성의 소스는 exemplar prototype이라는 프로토타입의 한 종류입니다. concatenative inheritance라고도 불립니다.
Dogulas Crockford 처럼 this를 더 이상 사용하지 않더라도 프로토타입 방식으로 코드 처리를 할 수도 있습니다. 동적 객체 확장이라는 JavasScript의 속성 때문에 Concatenative 상속이 가능하기 때문이죠. 이 동적 객체 확장이란 객체가 최초에 생성된 후에 객체 자체에 내용을 추가할 수 있는 기능을 뜻합니다.요는 이런 기능을 대신 활용해야 하면 되기 때문에, JavaScript에서는 클래스가 필수불가결한 경우는 전혀 없다는 것입니다.
개발자들은 종종 프로그래밍 스타일이 자신을 표현하는 방법인 것처럼, 이렇게 클래스나 상속에 대해서 비판하면 방어적이어지는 경우가 있습니다. 그렇지만 그럴 필요가 전혀 없습니다. 개발자가 코딩하여 무엇을 만드느냐가 여러분을 표현하는 방식이기 때문이죠. 어떻게 구현하느냐는 전혀 중요하지 않습니다. 엄청 이상하게 구현하지 않는 이상 말이죠.
소프트웨어 개발에 있어 가장 중요한 것은 사용자들이 당신의 소프트웨어를 좋아하느냐 입니다.
“Design Patterns” book by the Gang of Four 는 이런 관점에서 아주 명작인데요, 이 책은 다음과 같은 두가지 중요한 원칙을 기반으로 쓰여졌습니다:
“구현이 아닌 인터페이스를 위한 프로그램”, “클래스 상속 보다는 객체 작성.”
자식 클래스가 부모 클래스를 수행하도록 되어 있기 때문에, 두번째 원칙은 첫번째에 기반한다고 볼 수 있으나 보다 자세히 짚어볼 필요가 있습니다.
클래스 객체 중심 디자인의 중대한 작품은 안티-클래스 상속이다.
이 책에는 constructor와 클래스 상속의 한계를 극복하기 위한 대안으로서의 객체 생성 패턴이아예 한 챕터로 담겨 있습니다. 구글에 “new가 해롭다”, “상속이 위험하다”, “super는 정말 별로다”를 한 번 쳐보세요. 수도 없이 많은 블로그 포스트와, JS가 개발되던 시절에 나온 Dr.Dobb’s Journal 같은 좋은 책들이 결과에 나올 것입니다. 이 많은 글들이 시사하는 것은 거의 똑같습니다. new라는 클래스 상속 체계와 부모-자식 의존성(e.g. super)은 재앙과 같다는 내용이죠. 심지어 JAVA의 창시자 James Gosling도 객체가 잘못되었다는 것을 인정했습니다.
Javascript 문헌이나 계속 보고 싶다고요? Douglas Crockford 가 new를 쓰지 않아도 되도록 object.create()를 새로 만들었으니 한 번 확인해보세요. Kyle Simpson (“You Don’t Know JS”저자) 는“JS Objects: Inherited a Mess.”라는 제목의 아주 흥미로운 세 편의 블로그 시리즈를 연재했는데, 이것도 참고할 만 합니다. Kyle은 프로토타입형 상속이 클래스 개념에 반대되며, 더 간단하고 잘 기능한다고 주장합니다. 심지어는 프로토타입 위임과 클래스 상속의 구분에 대해서 명확히 하기 위해 OLOO (Objects Linked to Other Objects)라는 새로운 용어도 만들었습니다.
좋은 코드는 심플하다
“단순함은 말할 필요 없이 명백한 것은 빼고, 의미 있는 것만 더하는 것을 의미한다. ” ~ John Maeda
JavaScript에서 constructor와 클래스 상속을 걷어내면:
- 더 간단해집니다: 더 읽고 쓰기 쉽습니다. 잘못된 분류 체계에 대해서도 고민할 필요가 없죠.
- 더 유연해집니다: 새 인스턴스에서 객체 풀이나 프록시로 전환하는 것도 전혀 문제되지 않습니다.
- 훨씬 더 강력하고 표현이 풍부해집니다: 여러 소스에서 상속을 받을 수도 있고 프라이빗 상태도 받아올 수 있죠.
더 나은 선택
“어떤 기능이 종종 위험을 초래하는데 더 나은 옵션도 존재한다면, 당연히 언제나 더 나은 옵션 쪽을 선택해야 한다.” ~ Douglas Crockford
I’m not trying to take a useful tool away from you. I’m warning you that what you think is a tool is actually a foot-gun. In the case of constructors and classes, there are several better options.
유용한 도구를 여러분에게서 앗아가려는 것이 아닙니다. 단지 여러분이 도구라고 생각하는 것이 사실은 제 발 찍을 도끼라는 것을 알려드리기 위함입니다. constructor나 클래스를 사용해야 한다는 생각이 들 때, 사실은 다른 여러 좋은 옵션이 많습니다.
코드 스타일이 예술이나 패션의 경지에 오른 양, 개발자들이 어떻게 스스로를 표현할 지를 자기가 결정할 수 있어야 한다고들 합니다. 그렇지만 아주 감정적이고 비이성적인 주장이라고 생각합니다:
화가의 붓이 자아 표현의 산물이 아니듯, 당신의 코드도 그렇지 않습니다. 코드는 도구입니다. 프로그램이 그 산물이죠.
어떤 코드는 그 자체로 예술 같을 때가 있죠. 그렇지만 이게 정말 하나의 책으로 발간되지 않는 이상, 여러분의 코드는 그렇게 되지 않습니다. 반면에 사용자들이 있는 한, 코드는 블랙 박스이고 그들이 즐기는 것은 프로그램입니다.
좋은 프로그래밍 스타일이란, 우아하고 심플하고 유연한 방법과 복잡하고 이상하고 제약이 많은 방법 중에 선택해야 할 때 전자를 선택하게 합니다. 언어의 기능을 열린 마음으로 받아들이는 게 요즘 트렌드라는 것은 알고 있지만, 여전히 옳은 방법과 그렇지 않은 방법은 있습니다. 여러분은 그 중 옳은 방법을 선택하셔야 합니다.
~ Eric Elliott
'Java > OOP' 카테고리의 다른 글
| [Java] 다형성 (Polymorphism): 하나의 부모 인스턴스로 자식 객체를 관리 + 강제 형변환 (1) | 2025.01.08 |
|---|---|
| [Java] 상속 + Override (1) | 2025.01.08 |
| [Java] 오버로드 (Overload), 가변인수(...) (1) | 2025.01.06 |
| [Java] 객체 지향 프로그래밍 vs 절차 지향 프로그래밍 (1) | 2025.01.03 |
| [Java] 클래스 (Class) 정의, 접근 제어자, 상속, 인터페이스, 패키지 (1) | 2025.01.03 |