Files
cognitive-load/README.ko.md
2025-06-17 18:19:09 +09:00

40 KiB
Raw Permalink Blame History

인지 부하가 중요합니다

소개 (Introduction)

세상에는 수많은 유행어와 모범 사례가 있지만, 대부분은 실패했습니다. 우리에게는 더 근본적인 것, 틀릴 수 없는 무언가가 필요합니다.

때때로 우리는 코드를 살펴보면서 혼란을 느낍니다. 혼란은 시간과 비용을 초래합니다. 혼란은 높은 인지 부하로 인해 발생합니다. 높은 인지 부하는 화려하고 추상적인 개념이 아니라 근본적인 인간의 제약입니다. 상상 속의 존재가 아니라 실제로 존재하며 우리가 느낄 수 있는 것입니다.

우리는 코드를 작성하는 시간보다 읽고 이해하는 데 훨씬 더 많은 시간을 소비하므로, 코드에 과도한 인지 부하를 심고 있지는 않은지 끊임없이 자문해야 합니다.

인지 부하 (Cognitive load)

인지 부하란 개발자가 작업을 완료하기 위해 생각해야 하는 양입니다.

우리는 코드를 읽을 때 변수 값, 제어 흐름 논리, 호출 순서와 같은 것들을 머릿속에 넣습니다. 평균적인 사람은 작업 기억에 대략 4개의 덩어리를 담을 수 있습니다. 인지 부하가 작업 기억의 임계값에 도달하면 내용을 이해하기가 훨씬 더 어려워집니다.

완전히 낯선 프로젝트의 수정 요청을 받았다고 가정해 봅시다. 정말 똑똑한 개발자가 이 프로젝트에 기여했다고 들었습니다. 수많은 멋진 아키텍처, 화려한 라이브러리, 유행하는 기술이 사용되었습니다. 다시 말해, 작성자가 우리에게 높은 인지 부하를 안겨준 것입니다.

인지 부하

우리는 프로젝트에서 인지 부하를 최대한 줄여야 합니다.

인지 부하와 방해 요소

인지 부하의 유형 (Types of cognitive load)

내재적 인지 부하 - 작업 자체의 고유한 어려움으로 인해 발생합니다. 줄일 수 없으며, 소프트웨어 개발의 핵심에 있습니다.

외재적 인지 부하 - 정보가 제시되는 방식으로 인해 발생합니다. 똑똑한 작성자의 기행과 같이 작업과 직접 관련 없는 요인으로 인해 발생합니다. 크게 줄일 수 있습니다. 이 글에서는 이 외재적 인지 부하에 초점을 맞출 것입니다.

내재적 대 외재적

외재적 인지 부하의 구체적인 실제 사례로 바로 넘어가 보겠습니다.


인지 부하 수준은 다음과 같이 표현합니다:
🧠: 새로운 작업 기억, 인지 부하 없음
🧠++: 작업 기억에 두 가지 사실, 인지 부하 증가
🤯: 인지 과부하, 4가지 이상의 사실

인간의 뇌는 훨씬 더 복잡하고 미지의 영역이지만, 이 글에서는 이 단순화된 모델을 사용합니다.

복잡한 조건문 (Complex conditionals)

if val > someConstant // 🧠+
    && (condition2 || condition3) // 🧠+++, 이전 조건은 참이어야 하고, c2 또는 c3 중 하나는 참이어야 함
    && (condition4 && !condition5) { // 🤯, 이 시점에서 우리는 혼란에 빠짐
    ...
}

의미 있는 이름을 가진 중간 변수를 도입합니다.

isValid = val > someConstant
isAllowed = condition2 || condition3
isSecure = condition4 && !condition5
// 🧠, 조건을 기억할 필요가 없음, 설명적인 변수가 있음
if isValid && isAllowed && isSecure {
    ...
}

중첩된 if 문 (Nested ifs)

if isValid { // 🧠+, 유효한 입력에만 중첩된 코드 적용
    if isSecure { // 🧠++, 유효하고 안전한 입력에 대해서만 작업 수행
        stuff // 🧠+++
    }
}

조기 반환과 비교해 보세요.

if !isValid
    return

if !isSecure
    return

// 🧠, 이전 반환은 신경 쓰지 않음, 여기까지 왔다면 모든 것이 좋음

stuff // 🧠+

우리는 행복한 경로에만 집중할 수 있으므로, 온갖 종류의 전제 조건으로부터 작업 기억을 해방시킬 수 있습니다.

상속의 악몽 (Inheritance nightmare)

관리자 사용자를 위해 몇 가지 사항을 변경해 달라는 요청을 받았습니다: 🧠

AdminController extends UserController extends GuestController extends BaseController

아, 기능의 일부가 BaseController에 있군요, 한번 봅시다: 🧠+
기본적인 역할 메커니즘은 GuestController에 도입되었습니다: 🧠++
UserController에서 일부 내용이 변경되었습니다: 🧠+++
드디어 AdminController입니다, 코딩합시다! 🧠++++

아, 잠깐, AdminController를 확장하는 SuperuserController가 있습니다. AdminController를 수정하면 상속된 클래스에서 문제가 발생할 수 있으므로, 먼저 우리는 SuperuserController를 살펴봐야 합니다: 🤯

상속보다는 합성을 선호하세요. 자세한 내용은 다루지 않겠습니다. 이미 많은 자료가 있습니다.

너무 많은 작은 메서드, 클래스 또는 모듈 (Too many small methods, classes or modules)

이 맥락에서 메서드, 클래스, 모듈은 서로 바꿔 사용할 수 있습니다.

"메서드는 15줄 미만이어야 한다" 또는 "클래스는 작아야 한다"와 같은 만트라는 다소 잘못된 것으로 밝혀졌습니다.

깊은 모듈 - 간단한 인터페이스, 복잡한 기능
얕은 모듈 - 인터페이스가 제공하는 작은 기능에 비해 상대적으로 복잡함

깊은 모듈

얕은 모듈이 너무 많으면 프로젝트를 이해하기 어려울 수 있습니다. 각 모듈의 책임뿐만 아니라 모든 상호 작용까지 염두에 두어야 합니다. 얕은 모듈의 목적을 이해하려면 먼저 관련된 모든 모듈의 기능을 살펴봐야 합니다. 🤯

정보 은닉은 가장 중요하며, 얕은 모듈에서는 복잡성을 그다지 많이 숨기지 않습니다.

필자는 두 개의 개인 프로젝트를 가지고 있는데, 둘 다 약 5,000줄의 코드입니다. 첫 번째 프로젝트에는 80개의 얕은 클래스가 있고, 두 번째 프로젝트에는 7개의 깊은 클래스만 있습니다. 1년 반 동안 이 두 프로젝트를 유지보수하지 않았습니다.

돌아와서 보니 첫 번째 프로젝트에서 80개 클래스 간의 모든 상호 작용을 푸는 것이 극도로 어렵다는 것을 깨달았습니다. 코딩을 시작하기 전에 엄청난 양의 인지 부하를 다시 구축해야 했습니다. 반면에 두 번째 프로젝트는 간단한 인터페이스를 가진 몇 개의 깊은 클래스만 있었기 때문에 빠르게 파악할 수 있었습니다.

최고의 구성 요소는 강력한 기능을 제공하면서도 간단한 인터페이스를 가진 것입니다. 존 K. 아우스터하우트

유닉스 I/O의 인터페이스는 매우 간단합니다. 다섯 가지 기본 호출만 있습니다.

open(path, flags, permissions)
read(fd, buffer, count)
write(fd, buffer, count)
lseek(fd, offset, referencePosition)
close(fd)

이 인터페이스의 최신 구현에는 수십만 줄의 코드가 있습니다. 많은 복잡성이 내부적으로 숨겨져 있습니다. 그러나 간단한 인터페이스 덕분에 사용하기 쉽습니다.

이 깊은 모듈 예제는 존 K. 아우스터하우트의 저서 소프트웨어 설계 철학에서 가져왔습니다. 이 책은 소프트웨어 개발의 복잡성의 본질을 다룰 뿐만 아니라, 파나스의 영향력 있는 논문 모듈로 시스템을 분해하는 데 사용될 기준에 관하여에 대한 가장 훌륭한 해석을 담고 있습니다. 둘 다 필독서입니다. 기타 관련 자료: 소프트웨어 설계 철학 대 클린 코드, 클린 코드를 추천하는 것을 멈춰야 할 때일지도 모릅니다, 작은 함수는 해롭다고 여겨집니다.

P.S. 만약 이 글이 너무 많은 책임을 가진 비대한 갓 오브젝트를 지지한다고 생각한다면, 잘못된 생각입니다.

한 가지 책임 (Responsible for one thing)

너무나 자주, 우리는 "모듈은 한 가지, 오직 한 가지 일에만 책임져야 한다"는 모호한 원칙에 따라 수많은 얕은 모듈을 만들게 됩니다. 이 흐릿한 한 가지란 무엇일까요? 객체를 인스턴스화하는 것은 한 가지 일이죠, 그렇죠? 그러니 MetricsProviderFactoryFactory는 괜찮아 보입니다. 그런 클래스의 이름과 인터페이스는 전체 구현보다 정신적으로 더 부담스러운데, 그게 무슨 추상화일까요? 뭔가 잘못되었습니다.

그런 얕은 구성 요소 사이를 오가는 것은 정신적으로 지치며, 선형적 사고가 우리 인간에게 더 자연스럽습니다.

우리는 사용자와 이해 관계자를 만족시키기 위해 시스템을 변경합니다. 우리는 그들에게 책임이 있습니다.

모듈은 한 명, 오직 한 명의 사용자 또는 이해 관계자에게만 책임져야 합니다.

이것이 바로 단일 책임 원칙의 전부입니다. 간단히 말해, 한 곳에서 버그를 발생시켰는데 두 명의 다른 비즈니스 담당자가 불평하러 온다면, 우리는 원칙을 위반한 것입니다. 이는 모듈에서 수행하는 작업의 수와는 아무런 관련이 없습니다.

하지만 지금도 이 규칙은 득보다 실이 많을 수 있습니다. 이 원칙은 개인의 수만큼이나 다양한 방식으로 이해될 수 있습니다. 더 나은 접근 방식은 이러한 원칙 적용이 얼마나 많은 인지 부하를 발생시키는지 살펴보는 것입니다. 한 곳에서의 변경이 여러 다른 비즈니스 흐름에 걸쳐 연쇄 반응을 일으킬 수 있다는 것을 기억하는 것은 정신적으로 부담스럽습니다. 그리고 그것이 핵심입니다. 배울 만한 화려한 용어는 없습니다.

너무 많은 얕은 마이크로서비스 (Too many shallow microservices)

이 얕고 깊은 모듈 원칙은 규모에 구애받지 않으며, 마이크로서비스 아키텍처에 적용할 수 있습니다. 너무 많은 얕은 마이크로서비스는 아무런 도움이 되지 않습니다. 업계는 다소 "매크로서비스", 즉 그렇게 얕지 않은(=깊은) 서비스로 향하고 있습니다. 가장 최악이고 수정하기 어려운 현상 중 하나는 소위 분산 모놀리스인데, 이는 종종 이러한 지나치게 세분화된 얕은 분리의 결과입니다.

필자는 한때 5명의 개발자로 구성된 팀이 17(!)개의 마이크로서비스를 도입한 스타트업에 컨설팅을 한 적이 있습니다. 이 팀은 예정보다 10개월 늦었고 공개 시점에 가까워 보이지 않았습니다. 새로운 요구 사항이 있을때마다 4개 이상의 마이크로서비스의 변경으로 이어졌습니다. 통합된 영역에서의 진단 난이도는 급증했습니다. 공개 시점과 인지 부하 모두 용납할 수 없을 정도로 높았습니다. 🤯

이러한 접근 방식이 새로운 시스템의 불확실성에 접근하는 올바른 방법일까요? 처음부터 올바른 논리적 경계를 도출하는 것은 엄청나게 어렵습니다. 핵심은 책임감 있게 기다릴 수 있는 한 결정을 최대한 늦추는 것입니다. 왜냐하면 그때가 결정을 내리는 데 필요한 가장 많은 정보를 가지고 있기 때문입니다. 처음부터 네트워크 계층을 도입함으로써 우리는 설계 결정을 처음부터 되돌리기 어렵게 만듭니다. 팀의 유일한 정당화는 "FAANG 기업들이 마이크로서비스 아키텍처가 효과적이라는 것을 증명했다"는 것이었습니다. 이봐요, 큰 꿈은 그만 꾸세요.

타넨바움-토발즈 논쟁은 리눅스의 모놀리식 설계가 결함이 있고 구식이며, 마이크로커널 아키텍처를 사용해야 한다고 주장했습니다. 실제로 마이크로커널 설계는 "이론적이고 미적인" 관점에서 우월해 보였습니다. 실용적인 측면에서는 30년이 지난 지금도 마이크로커널 기반 GNU Hurd는 여전히 개발 중이며, 모놀리식 리눅스는 어디에나 있습니다. 이 페이지는 리눅스로 구동되며, 여러분의 스마트 주전자도 리눅스로 구동됩니다. 모놀리식 리눅스로 말이죠.

진정으로 격리된 모듈을 갖춘 잘 만들어진 모놀리스는 종종 여러 마이크로서비스보다 훨씬 유연합니다. 또한 유지 관리에 훨씬 적은 인지적 노력이 필요합니다. 개발팀 확장과 같이 별도의 배포 필요성이 중요해질 때만 모듈, 즉 미래의 마이크로서비스 사이에 네트워크 계층을 추가하는 것을 고려해야 합니다.

기능이 풍부한 언어 (Feature-rich languages)

우리는 좋아하는 언어에 새로운 기능이 출시될 때 흥분을 느낍니다. 우리는 이러한 기능을 배우는 데 시간을 보내고, 새로운 기능을 기반으로 코드를 작성합니다.

기능이 많으면 한두 가지 기능을 사용하기 위해 몇 줄의 코드를 가지고 30분을 보낼 수도 있습니다. 그리고 그러한 시간 소모는 낭비입니다. 하지만 더 나쁜 것은 나중에 돌아왔을 때 그 사고 과정을 다시 만들어야 한다는 것입니다!

이 복잡한 프로그램을 이해해야 할 뿐만 아니라, 프로그래머가 사용 가능한 기능 중에서 왜 이런 방식으로 문제에 접근하기로 결정했는지 이해해야 합니다. 🤯

이런 주장은 다름 아닌 롭 파이크가 한 것입니다.

선택의 수를 제한하여 인지 부하를 줄이십시오.

언어 기능은 서로 직교하는 한 괜찮습니다.

C++ 경력 20년 엔지니어의 생각
며칠 전 RSS 리더를 보다가 "C++" 태그 아래에 읽지 않은 기사가 300개 정도 있다는 것을 알았습니다. 지난 여름 이후로 언어에 대한 기사를 단 한 편도 읽지 않았는데, 기분이 아주 좋습니다!

저는 지금까지 20년 동안 C++를 사용해 왔는데, 이는 제 인생의 거의 3분의 2에 해당합니다. 제 경험의 대부분은 언어의 가장 어두운 구석(예: 온갖 종류의 정의되지 않은 동작)을 다루는 데 있습니다. 재사용 가능한 경험이 아니며, 지금 모든 것을 버리는 것은 좀 소름 끼치는 일입니다.

상상해 보세요. || 토큰은 requires ((!P<T> || !Q<T>))requires (!(P<T> || Q<T>))에서 다른 의미를 갖습니다. 첫 번째는 제약 조건 논리합이고, 두 번째는 예전의 논리 OR 연산자이며, 다르게 동작합니다.

단순한 유형에 대한 공간을 할당하고 거기에 바이트 집합을 memcpy하는 것만으로는 추가적인 노력 없이 객체의 수명을 시작할 수 없습니다. 이것은 C++20 이전의 경우였습니다. C++20에서 수정되었지만 언어의 인지 부하는 증가했을 뿐입니다.

문제가 해결되었음에도 불구하고 인지 부하는 지속적으로 증가하고 있습니다. 무엇이 수정되었는지, 언제 수정되었는지, 이전에는 어땠는지 알아야 합니다. 저는 결국 전문가입니다. 물론 C++는 레거시 지원이 훌륭하며, 이는 또한 여러분이 그 레거시에 직면하게 될 것을 의미합니다. 예를 들어, 지난달 동료가 C++03의 일부 동작에 대해 저에게 물었습니다. 🤯

초기화 방법에는 20가지가 있었습니다. 균일 초기화 구문이 추가되었습니다. 이제 21가지 초기화 방법이 있습니다. 그런데 초기화 목록에서 생성자를 선택하는 규칙을 기억하는 사람이 있습니까? 정보 손실이 가장 적은 암시적 변환에 관한 것이지만, 만약 값이 정적으로 알려져 있다면... 🤯

이렇게 증가된 인지 부하는 당면한 비즈니스 작업으로 인해 발생하는 것이 아닙니다. 도메인의 본질적인 복잡성이 아닙니다. 단지 역사적인 이유로 존재하는 것입니다(외재적 인지 부하).

몇 가지 규칙을 만들어야 했습니다. 예를 들어, 코드 줄이 명확하지 않고 표준을 기억해야 한다면 그렇게 작성하지 않는 것이 좋습니다. 참고로 표준은 약 1500페이지입니다.

결코 C++를 비난하려는 것이 아닙니다. 저는 이 언어를 사랑합니다. 단지 지금은 지쳤을 뿐입니다.

\n

0xd34df00d님 작성 감사합니다.

\n

비즈니스 로직과 HTTP 상태 코드 (Business logic and HTTP status codes)

백엔드에서는 다음을 반환합니다. 401 만료된 JWT 토큰 403 접근 권한 부족 418 차단된 사용자

프론트엔드 엔지니어는 백엔드 API를 사용하여 로그인 기능을 구현합니다. 그들은 일시적으로 다음과 같은 인지 부하를 뇌에 만들어야 합니다:
401은 만료된 JWT 토큰입니다 // 🧠+, 그냥 일시적으로 기억하세요
403은 접근 권한 부족입니다 // 🧠++
418은 차단된 사용자입니다 // 🧠+++

프론트엔드 개발자는 (바라건대) 숫자 상태 -> 의미 사전을 만들어 후속 기여자들이 이 매핑을 뇌에서 다시 만들 필요가 없도록 할 것입니다.

그런 다음 QA 엔지니어가 등장합니다. "이봐요, 403 상태를 받았는데, 토큰이 만료된 건가요, 아니면 접근 권한이 부족한 건가요?"\nQA 엔지니어는 백엔드 엔지니어가 한때 만들었던 인지 부하를 먼저 다시 만들어야 하기 때문에 바로 테스트를 시작할 수 없습니다.

왜 이러한 사용자 지정 매핑을 작업 기억에 담아두어야 할까요? 비즈니스 세부 정보를 HTTP 전송 프로토콜에서 추상화하고 응답 본문에 직접 자체 설명 코드를 반환하는 것이 좋습니다.

{
    "code": "jwt_has_expired"
}

프론트엔드 측의 인지 부하: 🧠 (새로움, 마음에 담아둔 사실 없음)
QA 측의 인지 부하: 🧠

데이터베이스나 어디에서든 모든 종류의 숫자 상태에 동일한 규칙이 적용됩니다. 자체 설명 문자열을 선호하세요. 현대 개발 환경은 메모리를 최적화하기 위해 640K 컴퓨터 시대에 살고 있지 않습니다.

사람들은 401403 사이에서 논쟁하며 자신의 멘탈 모델을 기반으로 결정을 내립니다. 새로운 개발자가 들어오고 새로운 개발자는 그 사고 과정을 다시 만들어야 합니다. 코드에 대한 "이유"(ADR)를 문서화하여 새로운 사람이 내린 결정을 이해하도록 도울 수 있습니다. 하지만 결국에는 아무 의미가 없습니다. 오류를 사용자 관련 또는 서버 관련으로 분리할 수 있지만, 그 외에는 상황이 다소 모호합니다.

추신: "인증"과 "권한 부여"를 구별하는 것은 종종 정신적으로 부담스럽습니다. 인지 부하를 줄이기 위해 "로그인" 및 "권한"과 같은 더 간단한 용어를 사용할 수 있습니다.

DRY 원칙 남용 (Abusing DRY principle)

반복하지 마십시오(Do not repeat yourself) - DRY 원칙은 소프트웨어 엔지니어로서 배우는 첫 번째 원칙 중 하나입니다. 우리 자신에게 너무 깊이 내재되어 있어서 몇 줄의 추가 코드를 참을 수 없습니다. 일반적으로 좋고 근본적인 규칙이지만, 과도하게 사용하면 감당할 수 없는 인지 부하로 이어집니다.

요즘 모든 사람은 논리적으로 분리된 구성 요소를 기반으로 소프트웨어를 구축합니다. 종종 이러한 구성 요소는 별도의 서비스를 나타내는 여러 코드베이스에 분산되어 있습니다. 모든 반복을 제거하려고 노력하면 관련 없는 구성 요소 간에 긴밀한 결합이 발생할 수 있습니다. 결과적으로 한 부분의 변경이 다른 관련 없어 보이는 영역에 의도하지 않은 결과를 초래할 수 있습니다. 또한 전체 시스템에 영향을 주지 않고 개별 구성 요소를 교체하거나 수정하는 기능을 방해할 수 있습니다. 🤯

사실, 동일한 문제는 단일 모듈 내에서도 발생합니다. 장기적으로 실제로 존재하지 않을 수 있는 인식된 유사성을 기반으로 공통 기능을 너무 일찍 추출할 수 있습니다. 이로 인해 수정하거나 확장하기 어려운 불필요한 추상화가 발생할 수 있습니다.

롭 파이크는 이렇게 말했습니다.

약간의 복사본이 약간의 종속성보다 낫습니다.

우리는 바퀴를 재발명하지 않으려는 유혹이 너무 강해서, 스스로 쉽게 작성할 수 있는 작은 함수를 사용하기 위해 크고 무거운 라이브러리를 가져올 준비가 되어 있습니다.

모든 종속성은 여러분의 코드입니다. 가져온 라이브러리의 10개 이상의 스택 추적 수준을 살펴보고 무엇이 잘못되었는지 알아내는 것(왜냐하면 문제가 발생하기 때문입니다)은 고통스럽습니다.

프레임워크와의 긴밀한 결합 (Tight coupling with frameworks)

프레임워크에는 많은 "마법"이 있습니다. 프레임워크에 너무 많이 의존함으로써 프로젝트는 다가올 모든 개발자에게 그 "마법"을 먼저 배우도록 강요합니다. 몇 달이 걸릴 수 있습니다. 프레임워크를 사용하면 며칠 만에 MVP를 출시할 수 있지만, 장기적으로는 불필요한 복잡성과 인지 부하를 추가하는 경향이 있습니다.

더 나쁜 것은, 어느 시점에서 프레임워크가 아키텍처에 맞지 않는 새로운 요구 사항에 직면했을 때 심각한 제약이 될 수 있다는 것입니다. 이 시점부터 사람들은 프레임워크를 포크하고 자체 사용자 지정 버전을 유지 관리하게 됩니다. 새로운 사람이 가치를 제공하기 위해 구축해야 하는 인지 부하의 양(즉, 이 사용자 지정 프레임워크를 배우는 것)을 상상해 보십시오. 🤯

결코 모든 것을 처음부터 발명하라고 주장하는 것이 아닙니다!

우리는 다소 프레임워크에 구애받지 않는 방식으로 코드를 작성할 수 있습니다. 비즈니스 로직은 프레임워크 내에 있어서는 안 되며, 오히려 프레임워크의 구성 요소를 사용해야 합니다. 프레임워크를 핵심 로직 외부에 두십시오. 프레임워크를 라이브러리처럼 사용하십시오. 이러한 접근 방식은 새로운 기여자가 프레임워크 관련 복잡성의 잔해를 거칠 필요 없이 첫날부터 가치를 추가할 수 있습니다.

내가 프레임워크를 싫어하는 이유

계층형 아키텍처 (Layered architecture)

이런 기술적인 주제엔 엔지니어로서의 확실한 설렘이 있다 (certain engineering excitement).

필자도 수년 동안 Hexagonal/Onion 아키텍처의 열렬한 지지자였습니다. 여기저기서 사용했고 다른 팀에도 그렇게 하도록 권장했습니다. 필자가 참여한 프로젝트의 복잡성은 증가했고, 파일 수만 해도 두 배가 되었습니다. 마치 많은 접착 코드를 작성하는 것처럼 느껴졌습니다. 끊임없이 변화하는 요구 사항에 따라 여러 추상화 계층에 걸쳐 변경해야 했고, 모든 것이 지루해졌습니다. 🤯

추상화는 복잡성을 숨기기 위한 것이지만, 여기서는 단지 간접성을 추가할 뿐입니다. 호출에서 호출로 이동하여 읽고 무엇이 잘못되었고 무엇이 누락되었는지 파악하는 것은 문제를 신속하게 해결하기 위한 필수 요구 사항입니다. 이 아키텍처의 계층 분리는 실패가 발생하는 지점에 도달하기 위해 기하급수적으로 많은, 종종 분리된 추적을 필요로 합니다. 이러한 각 추적은 제한된 작업 기억 공간을 차지합니다. 🤯

이 아키텍처는 처음에는 직관적으로 이해가 되었지만, 프로젝트에 적용하려고 할 때마다 득보다 실이 훨씬 많았습니다. 결국 우리는 오래된 의존성 역전 원칙을 위해 모든 것을 포기했습니다. 배울 포트/어댑터 용어도 없고, 불필요한 수평적 추상화 계층도 없고, 외재적 인지 부하도 없습니다.

코딩 원칙과 경험
@flaviocopes

이러한 계층화가 데이터베이스나 다른 종속성을 신속하게 교체할 수 있게 해줄 것이라고 생각한다면 착각입니다. 스토리지를 변경하면 많은 문제가 발생하며, 데이터 액세스 계층에 대한 일부 추상화가 있다는 것은 걱정거리 중 가장 작은 부분이라는 것이 분명합니다. 기껏해야 추상화는 마이그레이션 시간의 약 10%를 절약할 수 있지만(있다면), 진짜 고통은 데이터 모델 비호환성, 통신 프로토콜, 분산 시스템 문제 및 암시적 인터페이스에 있습니다.

API 사용자가 충분히 많으면,\n> 계약서에 무엇을 약속하든 상관없습니다.\n> 시스템의 모든 관찰 가능한 동작은\n> 누군가에게 의존하게 될 것입니다.

한 프로젝트에서는 스토리지 마이그레이션을 했고, 약 10개월이 걸렸습니다. 이전 시스템은 단일 스레드였기 때문에 노출된 이벤트는 순차적이었습니다. 모든 시스템이 관찰된 해당 동작에 의존했습니다. 이 동작은 API 계약의 일부가 아니었고 코드에 반영되지 않았습니다. 새로운 분산 스토리지는 그러한 보장이 없었습니다. 이벤트가 순서 없이 발생했습니다. 추상화 덕분에 새로운 스토리지 어댑터를 코딩하는 데 몇 시간밖에 걸리지 않았습니다. 다음 10개월은 순서 없는 이벤트 및 기타 문제를 처리하는 데 보냈습니다. 이제 추상화가 구성 요소를 신속하게 교체하는 데 도움이 된다고 말하는 것은 우스꽝스럽습니다.

그렇다면 미래에 성과를 거두지 못한다면 왜 그러한 계층형 아키텍처에 대해 높은 인지 부하라는 대가를 치러야 할까요? 게다가 대부분의 경우 일부 핵심 구성 요소를 교체하는 미래는 결코 오지 않습니다.

이러한 아키텍처는 근본적인 것이 아니라 더 근본적인 원칙의 주관적이고 편향된 결과일 뿐입니다. 왜 그러한 주관적인 해석에 의존해야 할까요? 대신 근본적인 규칙을 따르십시오: 의존성 역전 원칙, 단일 진실 공급원, 인지 부하 및 정보 은닉. 비즈니스 로직은 데이터베이스, UI 또는 프레임워크와 같은 하위 수준 모듈에 의존해서는 안 됩니다. 인프라에 대해 걱정하지 않고 핵심 로직에 대한 테스트를 작성할 수 있어야 하며, 그것이 핵심입니다. 토론.

아키텍처를 위해 추상화 계층을 추가하지 마십시오. 실용적인 이유로 정당화되는 확장 지점이 필요할 때마다 추가하십시오.

추상화 계층은 공짜가 아닙니다. 제한된 작업 기억에 담아두어야 합니다.

계층

도메인 주도 설계 (Domain-Driven Design)

도메인 주도 설계(Domain-driven design)는 분명 훌륭한 개념들이 많지만, 종종 오해를 받기도 한다. 사람들은 '우리는 DDD 방식으로 코드를 짠다'고 말하곤 하는데, 이는 다소 어색한 표현이다. 왜냐하면 DDD는 '해결 방법(solution space)'이 아니라 '문제 영역(problem space)'에 대한 접근 방식이기 때문이다.

유비쿼터스 언어, 도메인, 경계 컨텍스트, 집계, 이벤트 스토밍은 모두 '문제 영역(problem space)'에 관한 것입니다. 도메인에 대한 통찰력을 배우고 경계를 추출하는 데 도움이 되도록 만들어졌습니다. DDD를 통해 개발자, 도메인 전문가 및 비즈니스 담당자는 단일하고 통일된 언어를 사용하여 효과적으로 의사소통할 수 있습니다. DDD의 이러한 '문제 영역(problem space)' 측면에 초점을 맞추는 대신 특정 폴더 구조, 서비스, 리포지토리 및 기타 '해결 방법(solution space)' 기술을 강조하는 경향이 있습니다.

각자가 DDD를 해석하는 방식은 독특하고 주관적일 가능성이 높습니다. 그리고 이러한 이해를 바탕으로 코드를 작성한다면, 즉 많은 외재적 인지 부하를 만든다면 미래의 개발자는 파멸할 것입니다. 🤯

팀 토폴로지는 팀 전체에 걸쳐 인지 부하를 분산하는 데 도움이 되는 훨씬 더 좋고 이해하기 쉬운 프레임워크를 제공합니다. 엔지니어는 팀 토폴로지에 대해 배운 후 다소 유사한 멘탈 모델을 개발하는 경향이 있습니다. 반면에 DDD는 10명의 다른 독자에게 10개의 다른 멘탈 모델을 만드는 것처럼 보입니다. 공통 기반이 되는 대신 불필요한 논쟁의 장이 됩니다.

예시 (Examples)

이러한 아키텍처는 매우 지루하고 이해하기 쉽습니다. 누구나 큰 정신적 노력 없이 파악할 수 있습니다.

아키텍처 검토에 주니어 개발자를 참여시키십시오. 그들은 정신적으로 부담스러운 영역을 식별하는 데 도움을 줄 것입니다.

익숙한 프로젝트의 인지 부하 (Cognitive load of familiar projects)

문제는 익숙함이 단순함과 같지 않다는 것입니다. 익숙함과 단순함은 같은 느낌, 즉 별다른 정신적 노력 없이 공간을 쉽게 이동하는 느낌을 주지만, 매우 다른 이유 때문입니다. 사용하는 모든 "영리한"(즉, "자기 만족적인") 비관용적 트릭은 다른 모든 사람에게 학습 페널티를 부과합니다. 일단 그 학습을 마치면 코드로 작업하는 것이 덜 어렵다는 것을 알게 될 것입니다. 그래서 이미 익숙한 코드를 단순화하는 방법을 인식하기가 어렵습니다. 이것이 제가 "신입"이 너무 제도화되기 전에 코드를 비판하도록 하려는 이유입니다!

이전 작성자가 이 거대한 혼란을 한 번에 만든 것이 아니라 한 번에 조금씩 점진적으로 만들었을 가능성이 높습니다. 그래서 당신은 이 모든 것을 한 번에 이해하려고 노력한 첫 번째 사람입니다.

제 수업에서 참여자들은 어느 날 거대한 WHERE 절에 수백 줄의 조건문이 있는 광범위한 SQL 저장 프로시저를 보고 있었습니다. 누군가 어떻게 아무도 이렇게 나빠지도록 내버려 둘 수 있었는지 물었습니다. 저는 이렇게 말했습니다. "조건문이 23개밖에 없을 때는 하나를 더 추가해도 아무런 차이가 없습니다. 조건문이 2030개가 되면 하나를 더 추가해도 아무런 차이가 없습니다!"

코드베이스에 작용하는 "단순화하는 힘"은 개발자가 내리는 의도적인 선택 외에는 없습니다. 단순화에는 노력이 필요하며, 사람들은 너무 자주 서두릅니다.

댄 노스님의 의견 감사합니다.

프로젝트의 멘탈 모델을 장기 기억에 내재화했다면 높은 인지 부하를 경험하지 않을 것입니다.

멘탈 모델

배워야 할 멘탈 모델이 많을수록 새로운 개발자가 가치를 제공하는 데 더 오랜 시간이 걸립니다.

프로젝트에 새로운 사람을 온보딩할 때 그들이 겪는 혼란의 양을 측정해 보십시오(페어 프로그래밍이 도움이 될 수 있습니다). 만약 그들이 연속으로 40분 이상 혼란스러워한다면 코드에서 개선해야 할 부분이 있는 것입니다.

인지 부하를 낮게 유지하면 사람들이 회사에 합류한 지 몇 시간 만에 코드베이스에 기여할 수 있습니다.

결론 (Conclusion)

두 번째 장에서 우리가 추론한 내용이 실제로는 사실이 아니라고 잠시 상상해 보십시오. 만약 그렇다면, 우리가 방금 부정한 결론과 이전 장에서 유효하다고 받아들였던 결론도 정확하지 않을 수 있습니다. 🤯

느껴지시나요? 의미를 파악하기 위해 기사 전체를 뛰어다녀야 할 뿐만 아니라(얕은 모듈!), 단락 자체가 이해하기 어렵습니다. 우리는 방금 여러분의 머릿속에 불필요한 인지 부하를 만들었습니다. 동료들에게 이런 짓을 하지 마십시오.

똑똑한 저자

우리는 개발 작업에 내재된 것 이상의 모든 인지 부하를 줄여야 합니다.


링크드인, X, 깃허브

읽기 쉬운 버전

댓글

롭 파이크
좋은 기사입니다.

안드레이 카르파티 (ChatGPT, 테슬라)
소프트웨어 공학에 대한 좋은 글입니다. 아마도 가장 사실에 가깝지만 가장 실천되지 않는 관점일 것입니다.

일론 머스크
사실입니다.

애디 오스마니 (크롬, 세계에서 가장 복잡한 소프트웨어 시스템)
똑똑한 개발자들이 최신 디자인 패턴과 마이크로서비스를 사용하여 인상적인 아키텍처를 만든 수많은 프로젝트를 보았습니다. 하지만 새로운 팀원이 변경을 시도했을 때, 모든 것이 어떻게 맞춰지는지 이해하는 데만 몇 주를 보냈습니다. 인지 부하가 너무 높아 생산성이 급락하고 버그가 증식했습니다.

아이러니한 점은? 이러한 복잡성을 유발하는 패턴 중 다수가 "클린 코드"라는 이름으로 구현되었다는 것입니다.

정말로 중요한 것은 불필요한 인지 부담을 줄이는 것입니다. 때로는 이것이 많은 얕은 모듈 대신 더 적고 깊은 모듈을 의미하기도 합니다. 때로는 관련된 로직을 작은 함수로 나누는 대신 함께 유지하는 것을 의미하기도 합니다.

그리고 때로는 영리한 해결책보다 지루하고 간단한 해결책을 선택하는 것을 의미하기도 합니다. 최고의 코드는 가장 우아하거나 정교한 코드가 아니라 미래의 개발자(자신 포함)가 빠르게 이해할 수 있는 코드입니다.

당신의 기사는 우리가 브라우저 개발에서 직면하는 문제들과 정말로 공감됩니다. 현대 브라우저가 가장 복잡한 소프트웨어 시스템 중 하나라는 당신의 말은 절대적으로 옳습니다. 크로미움에서 그 복잡성을 관리하는 것은 인지 부하에 대해 당신이 지적한 많은 점들과 완벽하게 일치하는 끊임없는 도전입니다.

크로미움에서 이를 처리하는 한 가지 방법은 신중한 구성 요소 격리와 하위 시스템(렌더링, 네트워킹, 자바스크립트 실행 등) 간의 잘 정의된 인터페이스를 통하는 것입니다. 유닉스 I/O를 사용한 깊은 모듈 예제와 유사하게, 우리는 상대적으로 간단한 인터페이스 뒤에 강력한 기능을 목표로 합니다. 예를 들어, 우리의 렌더링 파이프라인은 엄청난 복잡성(레이아웃, 합성, GPU 가속)을 처리하지만 개발자는 명확한 추상화 계층을 통해 상호 작용할 수 있습니다.

불필요한 추상화를 피하는 것에 대한 당신의 지적도 정말 마음에 와 닿았습니다. 브라우저 개발에서 우리는 웹 표준 및 호환성의 고유한 복잡성을 처리하면서 새로운 기여자가 코드베이스에 접근하기 쉽게 만드는 것 사이에서 끊임없이 균형을 맞춥니다.

때로는 복잡한 시스템에서도 가장 간단한 해결책이 최선일 때가 있습니다.

antirez (레디스)
완전히 동의합니다 :) 또한, 언급된 "소프트웨어 설계 철학"에서 빠졌다고 생각하는 것은 "설계 희생"이라는 개념입니다. 즉, 때로는 무언가를 희생하고 단순성이나 성능, 또는 둘 다를 얻을 수 있습니다. 저는 이 아이디어를 지속적으로 적용하지만 종종 이해받지 못합니다.

좋은 예는 제가 항상 해시 항목 만료를 거부했다는 사실입니다. 이것은 설계 희생입니다. 왜냐하면 최상위 항목(키 자체)에만 특정 속성이 있는 경우 설계가 더 간단해지고 값은 그냥 객체가 되기 때문입니다. 레디스에 해시 만료 기능이 추가되었을 때 좋은 기능이었지만 (실제로) 많은 부분에 많은 변경이 필요하여 복잡성이 증가했습니다.

또 다른 예는 제가 지금 하고 있는 벡터 세트, 새로운 레디스 데이터 유형입니다. 저는 레디스가 벡터에 대한 진실의 원천이 아니라 근사치 버전만 가져갈 수 있도록 결정했습니다. 그래서 디스크에 큰 부동 소수점 벡터를 유지하려고 하지 않고 삽입 시 정규화, 양자화를 할 수 있었습니다. 많은 벡터 DB는 사용자가 입력한 내용(전체 정밀도 벡터)을 기억한다는 사실을 희생하지 않습니다.

이것들은 단지 두 가지 임의의 예일 뿐이지만, 저는 이 아이디어를 모든 곳에 적용합니다. 이제 문제는 물론 올바른 것을 희생해야 한다는 것입니다. 종종 매우 큰 복잡성을 차지하는 5%의 기능이 있는데, 그것이 바로 없애야 할 좋은 것입니다 :D