읽은 기록

Clean Code

개발자의 필독서..

왜 필독서인지 읽으면서 여러번 깨달은 책이다.

진행중인 아카데미 컨퍼런스의 영감을 받아 동아리 스터디로 만든 북클럽에서 읽게 된 첫 번째 책이다.

BRIDGE_BookClub

내용 자체도 정말 좋지만 두고두고 다시 읽을 수 있다는 점이 너무 좋았다

그리고 같이 책을 읽어준 동료분들에게 감사의 인사를..

1장 깨끗한 코드

이 책을 읽는 이유는 크게 두가지로 프로그래머이거나 성장하는 프로그래머이고 싶어서 정도로 정리될 것 같다.

앞으로 등장하는 코드를 다양한 각도에서 살펴보며 좋은코드와 나쁜코드를 구별하는 능력과 나쁜 코드를 좋은 코드로 바꾸는 능력까지..!

코드가 존재하리라

시작부터 ChatGPT를 까고 시작하는.. 마틴아저씨는 여기까지 생각했을까..?

최근 읽기 시작한 소프트웨어 아키텍처 101에서도 AI는 설계, 요구사항 분석 등 다양한 환경에 맞는 트레이드오프까지는 대체할 수 없는 영역으로 보고 있다.

다양한 프레임워크, 언어가 등장하면서 특정 분야에 맞는 특화된 프로그래밍 언어가 등장하고 있다.

무려 2008년에 이 책을 집필했다고 해도.. 15년을 예측하고 있는 듯 하다..

말 그대로 언어마다 새로운 기능들이 나오면서 추상화 수준도 높아지고 점점 high 레벨의 언어들이 등장하고 있다.

궁극적으로 코드는 요구사항을 표현하는 언어라는 사실을 명심한다.

나쁜 코드

나쁜 코드는 나쁜 코드를 만든다.

  • 켄트 백

“이 책은 좋은 코드가 중요하다는 다소 미약한 전제에 기반한다.”

우리는 좋은코드가 좋다는 사실을 왜 알고 있을까?

앞서 나쁜코드에 대한 경험이 있기 때문이다.

그렇다면 왜 다시 나쁜코드를 짜는 문제에 빠지는 걸까??

급해서, 서두르느라, 나중에 고쳐야지.. 등등 여러가지 이유가 있을 것이다.

하지만 이제는 알아야 한다, 나중은 결코 오지 않는다.

르블랑의 법칙(leblanc’s Law) WQRE 콤보

가장 느린길이 가장 빠른 길이다..

이제는 아는 것 같다 초반에 급하게 설계하지 않고 나름 탄탄하게 쌓아 올려야 미래의 내가 고생하지 않는 것 을..

하지만, 여타 솔루션을 해결하기 위한 애플리케이션이나 게임도 그렇듯 요구사항을 완벽하게 분석하여 설계할 수 없다.

그렇다면 계속 나쁜 코드를 생산해야하는가.. 에 대한 대답은 NO..!

계속 인지하면서 좋은 코드를 쓰기 위해 노력만 해도 크게 달라진다.

나쁜 코드로 치르는 대가

2~3년이 지난 프로젝트에 참여하게 되면 흔히 레거시코드, 쓰레기 코드등을 만날 수 있는데 대부분의 프로그래머가 그 때 해당 사람을 속으로 욕하거나 비판한다.

나도 과거에는 그랬던 것 같지만 사실 그 코드는 그 상황에서 최선이였을 것이다.

내가 짠 최선의 코드를 다른 사람의 비판적인 시각엔 쓰레기 코드가 될 수 있듯이 관점의 차이나 그 때에 급박한 상황 or 기술 부채등의 원인일 것이다.

나쁜코드가 쌓일수록 팀 자체의 생산성은 떨어진다.

인디게임팀은 더 심한 것 같다. 1년을 개발한 프로젝트를 인수인계 받는다고 한다면 정말 머리가 아프다..

다른 시스템을 개발하는 것과 별개로 해당 코드에서 이어 나가야 하는 그런 경험들은 거의 해독에 가깝다.

원대한 재설계처럼 선택과 집중을 해야한다.

레거시를 받아들이는 차이점인데 실제로 해당 시스템을 다시 만드는 것과 리팩터링의 차이는 그렇게 크지 않다..

처음 설계부터 잘 만들어진 소프트웨어는 최적화를 따로 하지 않기 때문에 주어진 자리에서 이걸 그냥 끝내야지라는 생각이 들면 그냥 끝내는 게 맞고..

리팩터링이나 리소스만 살려서 다시 제작한다고 한다면 그 길을 가는게 맞는 것 같다.

태도

수많은 외부 압박에도 프로그래머는 자신의 미래를 위해..? 프로페셔널한 모습을 보여야 한다.

현재 구조에서 고민이 많이 필요하고 시간이 많이 들어간다면 충분한 논의 끝에 구조를 잡아가는게 좋다.

만약 급하게 구현한다면.. 역 피라미드 구조를 만들어야 할 수도 있다.

그들이 일정과 요구사항을 강력하게 밀어붙이는 이유는 그것이 그들의 책임이기 때문이다.

개발자 정신을 가져야 하는 이유이다.

그들은 자신의 일을 하는 것이다.

불화나 마찰이 싫다고 해서 급하게 결과물을 만들어 내는 것 또한 전문가 답지 못하다..!

원초적 난제

나쁜 코드를 양산하면 기한을 맞추지 못하고 오히려 엉망진창인 상태로 인해 속도가 곧바로 늦어지고, 결국 기한을 놓친다.

기한을 맞추는 유일한 방법은, 그러니까 빨리 가는 유일한 방법은, 언제나 코드를 깨끗하게 유지하는 습관이다.

깨끗한 코드라는 예술?

깨끗한 코드란 무엇일까?

깨끗한 코드를 구현하는 행위는 마치 그림을 그리는 행위와 비슷하다..

비슷한 예로 100명이 같은 기능을 개발한다고 해도 내부 설계는 전부 다 다를 것이다.

여기서 1등으로 깨긋한 코드를 고를 수 있을까??

어느정도 트레이드오프를 생각해서 상위정도는 선택할 수 있지만 상당히 주관적인 부분이 있다.

다시 말해, 깨끗한 코드를 작성하는 프로그래머는 빈 캔퍼스를 우와한 작품으로 바꿔가는 화가와 같다.

깨끗한 코드란?

노련한 프로그래머의 의견들.

  • 비야네 스트롭스트룹

이름 웃기다.

나는 우아하고 효율적인 코드를 좋아한다.
논리가 간단해야 버그가 숨어들지 못한다.
의존성을 최대한 줄여야 유지보수가 쉬워진다.
오류는 명백한 전략에 의거해 철저히 처리해야 한다.
성능을 최적으로 유지해야 사람들이 워칙 없는 최적화로 코드를 망치려는 유혹에 빠지지 않는다.
깨긋한 코드는 한 가지를 제대로 한다.

우아한: 외양이나 태도가 기품 있고 단아하며 보기에 즐거운; 교모하고 단순해 보기에 즐거운

두 가지 측면을 강조한다. 보기에 즐거운 코드 그리고 효율적인 코드

또한 가장 중요한 부분이 나와있는데 나쁜코드는 나쁜코드를 유혹한다.

비슷한 심리효과로 깨진 유리창 이론이 있다..

프로그래머들이 대충 넘어가는 부분 중 하나가 오류 처리이다.

메모리누수, 경쟁 상태, 일관성 없는 명명법이 또 다른 예이다.

한마디로 요약하자면 깨끗한 코드는 세세한 사항까지 꼼꼼하게 처리하는 코드이다.

항상 느끼는 부분이지만 이런 감각, 세세한 부분이 정말 중요하다는 사실이다.

게임에서도 플레이어가 완성도를 느끼는 부분도 세세한 디테일에서 온다고 생각하고 좋은 코드 또한 감각적인 부분이 필요하다고 생각된다.

이런 부분이 애자일에서 다루는 가장 중요한 반복이 아닐까?

한가지 역할을 잘한다는 것은 그만큼 클래스, 메서드가 세분화되어 낮은 레벨로서 동작한다는 것이고 이는 즉, 객체지향 개념에도 부합한다.

  • 그래디 부치

깨끗한 코드는 단순하고 직접적이다.
깨끗한 코드는 잘 쓴 문장처럼 읽힌다.
깨끗한 코드는 결코 설계자의 의도를 숨기지 않는다.
오히려 명쾌한 추상화와 단순한 제어문으로 가득하다.

가장 공감이 되는 말 깨끗한 코드는 잘 쓴 문장처럼 읽힌다.라는 부분이다.

게임쪽 코드에서는 질 좋은 코드를 많이 본적 없지만 최근에 객체지향 아키텍처에 잘 맞는 코드를 본적이 있다.

정말 글을 읽듯이 내용이 한눈에 들어오고 잘 짰다는 생각이 가득했다가..

객체지향 관련 글을 읽어보고 쉽게 교조주의에 빠지면 안된다는 생각이 들었다.

만약 내가 디자인 패턴을 공부한다면 해당 디자인 패턴을 공부하기 위해 프로젝트에 적용시키는 것은 바보같은 짓이다.

디자인 패턴은 정말 필요할 때 적용해야한다.

이러한 가독성 좋은 코드들은 주석이 따로 필요 없으며 추상 계층도 정리가 잘 되어 있으며 기본적으로 복잡한 상속구조를 가지지 않는다.

코드는 추측이 아니라 사실에 기반해야 한다.

반드시 필요한 내용만을 담아야 한다.

코드를 읽는 사람에게 프로그래머가 단호하다는 인상을 줘야 한다.

  • 큰 데이브 토마스

깨끗한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다.
단위 테스트 케이스와 인수 테스트 케이스가 존재한다.
깨끗한 코드에는 의미 있는 이름이 붙는다.
특정 목적을 달성하는 방법은 하나만 제공한다.
의존성은 최소이며 각 의존성을 명확히 정의한다.
API는 명확하며 최소로 줄였다.
언어에 따라 필요한 모든 정보를 코드만으로 명확히 표현할 수 없기에 코드는 문학적으로 표현해야 마땅하다.

마찬가지로 가독성의 중요성과 깨끗한 코드는 작성자가 아닌 다른 사람도 읽고 고치기 쉽다고 강조한다.

여기서 다른 사람에 대한 나의 생각은 좋은 코드 나쁜 코드를 읽을 때 나왔던 말처럼 1년 뒤의 나도 다른 사람이다.

당장 2주전에 짠 코드를 봐도 제대로 못 읽는데.. 같은 사람이라고 할 수 있을까?

코드를 작성할 때는 나만 보는 코드라는 생각은 버리고 미래의 나, 다른 사람까지 읽기 쉽고 수정이 편한 코드를 작성해야 한다.

TDD.. 진짜로 프로젝트에 도입해보고 싶다는 생각은 있지만 먼저 단위테스트관련 책과 공부를 하고 진행하고 싶다.

유니티는 원활한 테스트를 위해 Test Runner라는게 있지만.. 정보도 적고 라이브러리 아니면 실제 프로젝트에 적용되는 경우는 많이 보지 못했다.

  • 마이클 페더스

깨끗한 코드의 특징은 많지만 그 중에서도 모두를 아우르는 특징이 있다.
깨끗한 코드는 언제나 누군가 주의 깊게 짰다는 느낌을 준다.
고치려고 살펴봐도 딱히 손 댈 곳이 없다.
작성자가 이미 모든 사항을 고려했으므로, 고칠 궁리를 하다보면 언제나 제자리로 돌아온다.
그리고는 누군가 남겨준 코드, 누군가 주의 깊게 짜놓은 작품에 감사를 느낀다.

한마디로 요약하자면 주의다.

이 책의 주제이며 우리가 앞으로 행해야 하는? 행해야 할 것이다.

인지하며 작성하자

  • 론 제프리스

최근 들어 나는 켄트 벡이 제안한 단순한 코드 규칙으로 구현을 시작한다.
중요한 순으로 나열하자면 간단한 코드는

  • 모든 테스트를 통과한다.
  • 중복이 없다.
  • 시스템 내 모든 설계 아이디어를 표현한다.
  • 클래스, 메서드, 함수등을 최대한 줄인다.

중복을 행하고 있다면 코드가 아이디어를 제대로 표현하지 못한다는 증거이다.

이러한 증거가 발견되면 문제의 아이디어를 명확하게 표현하려고 해야 한다.

표현력은 의미있는 이름을 뜻하기도 하지만 객체 자체의 보여주는 부분또한 표현력으로 볼 수 있다.

한가지 객체가 너무 많은 기능을 담당하면 표현력이 떨어지고, 한가지 기능만 담당하면 표현력이 높아진다.

따라서 중복과 표현력만 신경을 써도 깨끗한 코드에 한발짝 다가갈 수 있다.

요약: 중복을 피해라. 한 기능만 수행해라. 제대로 표현해라. 작게 추상화하라

  • 워드 커닝햄

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.
코드가 그 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드라 불러도 되겠다.

짐작 했던 기능.. 대부분 중복되는 내용처럼 한가지 언어이기 때문에 가독성이 정말 정말 중요하다는 걸 강조한다.

  • 밥 아조씨

장을 시작하기 앞서 자신의 의견을 이 책에서 진리처럼 설명을 한다는 말을 한다.

이 방법은 절대적인 방법이 아니며 단순한 접근법이다.

더 좋은 더 나은 코드를 만들기 위해 고민하여 작성된 책.

우리 생각이 절대적으로 ‘옳다’라는 단정은 금물이다.

쉽게 확증편향, 교조주의에 빠지지 않도록 주의하자.

성장을 하기 위해선 그냥 받아들이는 것이 아닌 부정하고 반대하라.(너무 과격한 감이 있지만 비판적인 시각을 가져야 더 큰 배움을 얻는 듯 하다..)

우리는 저자다

다른 개발책에서도 나오는 이야기지만(프로그래머의 뇌, 좋은 코드 나쁜 코드) 대부분의 프로그래머는 코드를 작성하는 시간보다 읽는 시간이 압도적으로 많다.

주변 코드를 읽지 않으면 새 코드를 짜지 못한다.

주변 코드를 읽으면 새 코드도 짜기 쉽다.

주변 코드가 읽기 어려우면 새 코드도 짜기 어렵다..!

생각난 일화로.. 회사에서도 천줄 씩 커밋하는 개발자보다 4~5줄 고치고 명확한 커밋을 날리는 개발자가 더 유능하다고 한다.

보이스카우트 규칙

하루 아침에 잘 짠 코드는 전부가 아니다.

시간이 지나도 언제나 깨끗하게 유지해야 한다.

우리는 코드의 퇴보를 막아야 한다.

캠프장을 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라

내가 무슨 작업을 한다고 세분화를 거쳐서 내용을 작성 했다.

이후 해당 내용으로 코딩을 진행하다가 수정 사항이 생겨 즉각적으로 코드에 반영을 했을 때, 이후에 해당 내용을 작업 내용에 반영해야 한다.

요즘에 이것에 대한 중요성을 많이 느끼고 있으며 틀렸다는 생각은 들지 않는다.

코드를 작성하여 동작할 때 기분이 좋을려면 계속 좋은 코드 위에 코드를 쌓아야 한다.

나쁜 코드는 쉽게 전염, 유혹되기 때문에..

프리퀄과 원칙

SOLD 원칙을 이야기 하고 있다.

결론

예술에 대한 책을 읽는다고 예술가가 되지 않는다.

책은 단지 다른 예술가가 사용하는 도구와 기법, 그리고 생각하는 방식을 소개할 뿐이다.

느낀점

등장하는 얼굴들이 확신의 개발자 상..

객체지향, TDD, 클린코드, 리팩토링, 디자인패턴, 소프트웨어공학, 애자일 등등..

다양한 이야기를 담고 있어서 좋았다.

무조건 읽어야지 읽어야지 했는데 읽기 시작하니까 재밌어서 읽기 잘했다는 생각..++

논의사항

자신만의 좋지 못한 구조를 가져간 경험이 있나요?

그 이유가 궁금합니다..

급해서, 프로젝트에 애정이 없어서, 나중에 할려고 등등..

2장 의미 있는 이름

소프트웨어의 분야에서 이름은 중요하게 어디든지 쓰인다.

변수, 함수, 인수, 클래스, 디렉토리, 소스파일 등등..

이 장에선 이름을 잘 짓는 간단한 규칙을 설명한다.

의도를 분명히 밝혀라

“의도를 분명하게 이름을 지으라”

의도가 분명하다는 것은 이름만 보고도 어떤 역할을 하고 있는지를 알 수 있다는 것이다.

좋은 이름을 지으려면 시간이 걸리지만 앞 장에서 말한대로 결국엔 더 시간을 절약하는 곡선을 타게된다.

앞 장에서 잘 작성된 코드는 주석이 필요없다(가독성이 매우 좋다)와 같은 맥락으로 변수의 이름에서 존재이유, 수행기능, 사용방법에 대한 주석이 따로 필요하다면 의도를 분명하게 하지 못했다는 것이다.

이러한 특성이 가장 많이 발견되는게 알고리즘 풀이인 것 같다.

좋은 코드 나쁜 코드에서도 등장하는 내용이지만.. 그런 네이밍으로 훈련되어 이름을 짓고 여러가지 역할을 몰아 넣는 습관이 생기기 때문에.. 위험할 수 있다.

혹자는 프로그래머의 가장 어려운 부분이 네이밍이라고 한다.

이름 자체가 해당 코드를 나타내는 1순위이기 때문에 그 만큼 중요하다.

++ 이름이 너무 긴 경우도 너무 많은 담당을 하고 있는게 아닐까?

또 반복되는 이야기 같지만 프로그래머는 읽는 데 시간을 많이 사용하는데 모호하거나 의도를 알아채지 못하는 이름은 그만큼 읽는 시간을 늘린다.

청킹이라는 개념이 있는데, 이는 사람이 읽는 데 한번에 읽을 수 있는 단어의 수를 말한다.(묶어서)

한번 자신이 짠 코드를 읽어보고, 명시적으로 드러나지 않는 부분이 있는지 확인해보자.

너무 많아서.. 반성 중이다..ㅜㅜ

그릇된 정보를 피하라

프로그래머는 그릇돈 단서를 남겨서는 안 된다.

이 말을 정리하자면 “프로그래머는 오용될 정보를 만들면 안된다.”정도일 것 같다.

그릇된 정보는 코드의 의미를 흐리게 하고, 유지보수를 어렵게 만든다.

의미 있게 구분하라

컴파일러나 인터프리터만 통과하려는 생각으로 코드를 구현하는 프로그래머는 스스로 문제를 일으킨다.

public static void copyChars(char a1[], char a2[]) {
    for (int i = 0; i < a1.length; i++) {
        a2[i] = a1[i];
    }
}

많이 이렇게 복사 한 것 같긴 한데.. 객체의 깊은 복사를 만들 때 이런 식으로 구현했던 기억이 난다.

함수 인수 이름을 source와 destination으로 바꾸면 의도가 분명해진다.

public static void copyChars(char source[], char destination[]) {
    for (int i = 0; i < source.length; i++) {
        destination[i] = source[i];
    }
}

불용어를 제거해라..!

info, data, a, an, the 등등.. 정말 정말 많이 보고 많이 사용하는 네이밍들..

variable, table, name 등등..

NameString이 Name보다 뭐가 나은가?

Name이 부동소수가 될 가능성이 있다면 앞서 말한 “그릇된 정보를 피하라” 규칙을 위반한다.

명확한 관례가 없다면 기준 자체가 모호해지면서 의미가 분명해지지 않는다.

발음하기 쉬운 이름을 사용하라

사람들은 단어에 능숙하다 우리 두뇌에서 상당 부분은 단어라는 개념만 전적으로 처리한다.

발음이 뭐가 중요하다고 생각할 수 있지만 예제와 같이 자신들의 규약으로 만들어진 단어들은 발음자체가 어려워 지적인 대화가 불가능하다.

검색하기 쉬운 이름을 사용하라

grep으로 cli상에서 찾는다는 부분을 보고.. 공감이 안되긴 했지만..?

그래도 검색하기 쉬운 이름을 사용하는게 유리할 것 같다..

게임에서는 각 시스템에 맞는 이름을 사용하여 분리하는게 좋지 않을까..?

이름의 길이는 범위 크기에 비례해야 한다.

약간의 논쟁거리 같긴 하지만 나는 클래스의 이름이 성격을 나타낸다면 굳이 앞에 또 다른 명칭을 붙일 필요가 없다고 생각했다.(명확하다면)

클래스 자체가 세분화 되어 있다면 굳이 value라는 값이 헷갈릴 필요가 없지 않나..

하지만 클래스 단위가 구성으로 커짐에 따라 중복되는 값을 생각해보면 구분하는게 맞는 것 같기도..

아직 경험 부족인 것 같다.

인코딩을 피하라

인코딩은 이름에 정보를 추가하는 방법이다.

  • 헝가리식 표기법

IDE와 컴파일러가 발전함에 따라 헝가리식 표기법은 더 이상 필요하지 않다.

오히려 헝가리식 표기법은 이름에 불필요한 단어를 추가하고, 이름의 길이를 늘리고, 이름을 읽는 데 드는 시간을 늘린다.

  • 멤버 변수 접두어

이제 멤버 변수에 m_이라는 접두어를 붙이는 것은 더 이상 필요하지 않다.

클래스와 함수는 접두어가 필요 없어질 정도로 작아야 마땅하다.

public class Part {
    String m_dsc; // 설명
    void setName(String name) {
        m_dsc = name;
    }
}

위 코드는 다음과 같이 수정할 수 있다.

public class Part {
    String description;
    void setDescription(String description) {
        this.description = description;
    }
}

컨벤션에 관련된 이야기지만.. 아직도 강제되거나 권장되는 컨벤션이 많은 것 같다..

팀 by 팀이라고 생각하면 괜찮을 것 같다.

  • 인터페이스 클래스와 구현 클래스

인터페이스 이름에 접두어를 붙이지 않는다…?

처음 글을 읽을 때 팩토리 인터페이스 클래스는 IShapeFactory로 구현 클래스는 ShapeFactory로 만들어야 한다고 생각했는데..

오히려 인터페이스를 드러내지 않고 싶어 한다는 부분에서 많이 신기했다..

자신의 기억력을 자랑하지 마라

독자가 코드를 읽으면서 변수 이름을 자신이 아는 이름으로 변환해야 한다면 그 변수 이름은 바람직하지 못하다.

이는 일반적으로 문제영역이나 해법 영역에서 사용하지 않는 이름을 선택했기 때문이다.

전문가 프로그래머는 명료함이 최고라는 사실을 이해한다.

클래스 이름

클래스 이름과 객체 이름은 명사나 명사구가 적합하다.

Ex) Customer, WikiPage, Account, AddressParser 등등

동사는 피하고 manager, processor, data, info 등과 같은 단어는 피하자.

좋은 것만 쏙쏙 피해서 했네요?

메서드 이름

메서드 이름은 동사나 동사구가 적합하다.

Ex) postPayment, deletePage, save 등등

접근자(Accessor), 변경자(Mutator), 조건자(Predicate)는 javabean 표준에 따라 값 앞에 get, set, is를 붙인다.

C#의 경우엔 거의 프로퍼티로 처리하는 것 같다.

string name = employee.getName();
customer.setName("mike");
if (paycheck.isPosted()) {
    //...
}
  • 생성자를 중복정의 할 때는 정적 팩토리 메서드를 사용한다.

메서드는 인수를 설명하는 이름을 사용한다.

// 좋은 예
Complex fulcrumPoint = Complex.FromRealNumber(23.0);

Complex fulcrumPoint = new Complex(23.0);

생성자 사용을 제한하려면 해당 생성자를 private로 선언한다.

음… 모든 생성자를 private으로 선언하고 정적 팩토리 메서드를 사용하라는 것 같다..?

이거에 대해서 조금 더 생각해봐야 할 것 같다..

물론 더 명시적이고 가독성이 좋지만 모든 생성자를 가리고 정적 팩토리 메서드를 사용하는 비용이 조금 생각이 든다..

선택과 집중을 해야하는 부분일까 아니면 좀 더 좋은 쪽의 결정일까.

기발한 이름은 피하라

개그 욕심을 줄이자.. 이 글을 적으면서도 생각나는 다양한 웃긴 내용들을 적지만..(확신의 N)

이런 글과 코드는 엄연하게 다르다.

기발한 이름보다 명료한 이름을 택하라..!

한 개념에 한 단어를 사용하라

추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다.

똑같은 메서드를 클래스마다 fetch, retrieve, get으로 부르면 혼란스럽다..

이 내용에 대해서 어느정도 나름 룰을 만들어서 지키고 있었다.

싱글톤 매니저 객체는 매니저, 한가지 대표격인 클래스는 컨트롤러 이런 식으로..

말장난을 하지 마라

add 엄청 많이 사용하는데..

맞는 말이라 맞는 중..

insert, append, attach, extend등이 적합하다

해법 영역에서 가져온 이름을 사용하라

문제영역과 해법영역을 잘 생각해서 이름을 지어야 한다.

문제 영역에서 가져온 이름을 사용하라

코드를 읽는 사람이 문제 영역에 익숙하다면 문제 영역에서 이름을 가져오는 것이 좋다.

의미 있는 맥락을 추가하라

의미 있는 맥락을 추가하기 위해 클래스, 함수, 이름공간에 넣어 맥락을 부여한다.

이러한 수단이 실패하면 대다수가 접두어를 붙인다.

firstName, lastName, street, houseNumber, city, state, zipcode 등등

add라는 접두어를 추가하면 addFirstName, addLastName, addStreet, addHouseNumber, addCity, addState, addZipcode 등등

맥락이 조금 더 분명해진다.

과연 변수에 좀 더 의미있는 맥락이 필요할까?

사실 앞에서 다룬 이야기이자 고민이였지만 이걸 보고 대강 이해가 된 것 같다.

해당 클래스의 멤버 변수에 대한 접두어를 붙이는 것은 좋은 방법이 아닌 것 같다.

차라리 클래스 명에서 성격을 밝히고 알고리즘으로 맥락을 제공하는 것이 더 바람직한 방법인 것 같다.

즉, 세 변수는 확실하게 GuessStaticsMessage 클래스에 속한다.

불필요한 맥락을 없애라

불필요한 맥락을 없애라..!

불필요한 맥락을 없애기 위해선 가장 먼저 클래스를 설계할 때 한 기능을 너무 무겁게 들고 있으면 안된다.

한 객체는 한가지 기능만 해야 하지만 한 가지 기능을 위해 엄청난 약 500줄이 넘어가는 클래스는 이름 자체가 해당 종속적인 기능에 대한 이름으로 될 수 밖에 없다.

반대로 아닐 경우(이름이 추상적인)도 모듈화가 전혀 안되기 때문에 쓸모없다..

마치며

좋은 이름을 선택하려면 설명 능력이 뛰어나야 하고 문화적인 배경이 같아야 한다.

이것이 제일 어렵다..

내 기준에선 좋은 이름을 쓰기 위해서 다양하게 읽고 써봐야 한다.

다른 사람의 코드를 읽거나 써보고 이러한 일련의 경험을 잘하기 위해선 책을 가까이 해야한다..

느낀점

https://github.com/orgs/BRIDGE-DEV/discussions/5

과거에 올린 적 있는 네이밍 사이트 추천 글..

읽으면서 느낀점은 영어를 잘해야겠다..?

영어의 필요성은 하는 사람과 안하는 사람의 정보 습득력에서 많이 벽을 느꼈지만 코딩 단위에서도 영어는 정말 중요한 것 같다..

  • 책도 중요.

논의사항

자신만의 네이밍 룰같은게 있다면 알려주시면 감사하겠습니다..!

3장 함수

예제에서 주어진 함수가 읽기 쉽고 이해하기 쉬운 이유는 무엇일까?

의도를 분명히 표현하는 함수를 어떻게 구혀할 수 있을까?

함수에 어떤 속성을 부여해야 사람이 프로그램 내부를 직관적으로 파악할 수 있을까?

작게 만들어라

함수를 만드는 첫째 규칙은 ‘작게!’다. 함수를 만드는 둘째 규칙은 ‘더 작게!’다.

이 규칙에 대해서는 매우 동의한다..

프로그래머의 뇌라는 코드를 잘 읽는 방법에도 나오지만 사람은 생각보다 더 멍청하기 때문에 메모리와 하드디스크에 비유하여 읽을 수 있는 한계량이 정해진다.

LTM, STM으로 읽을 때 함수의 길이가 5줄, 10줄 이상으로 길어지면 이해가 안되는 순간 다시 위로 올라가야 하기 때문에 함수를 작성할 때 사람이 읽는 순서, 맥락을 만지는 것이 중요하다.

너무 길다 싶으면 분활하여 작성하는 것이 현명..

앞서 말한 한가지 역할을 하는 클래스는 한가지 역할을 하는 함수와 같다.

하지만 작성된 여러가지 객체들과 협력하여 프로그램이 동작하는 것.

한 가지만 해라

“함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한가지만을 해야 한다.”

여기서 한가지의 개념을 명확하게 해야한다.

이는 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.

우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서다.

또 다른 방법으로 의미있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러가지 작업을 하는 셈이다.

이름이 진짜 핵심..

함수 당 추상화 수준은 하나로

함수가 확실히 한 가지작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.

특정 표현이 근본 개념인지 아니면 세부사항인지 구분하기 어려운 탓이다.

근본 개념과 세부사항을 뒤섞기 시작하면, 깨어진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.

깨진 유리창 이론..

함수 당 추상화 수준은 하나라는.. 생각을 안해봐서 예제를 보니 감동받았다.. 명심할 것

위에서 아래로 코드 읽기: 내려가기 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다.

함 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.

즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

이를 내려가기규칙이라고 부른다.

함수만 적용되는 규칙이 아니라 코드를 위에서 아래로 읽기 때문에 클래스 단위에서도 적용되어야 한다.

물론 클래스가 함수의 개념 자체가 유사할 수 있지만..

Switch 문

switch 문은 작게 만들기 어렵다.

case분기가 두 개 이상이라도 길고 한 가지 작업만 하는 switch문도 만들기 어렵다.

switch문을 완전히 피할 방법은 없지만 각 switch문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법이 있다.

이 예제에 관해서 공부한 적이 있다.

팩토리 메서드 패턴이 예제인 경우 같은데..

따로 글을 쓰기보다 전에 썼던 글을 첨부한다.

https://fkdl0048.github.io/patterns/Patterns_FactoryMethod/

DI와 인터페이스를 사용해 추상화 수준을 높여서 관리하는 방법이다.

다형적 객체를 생성하는 코드안에서 switch문을 꼭 사용하는 것 보다 위 링크의 방법을 사용하는 것이 더 좋을 수 있다..

서술적인 이름을 사용하라

서술적인 이름을 사용하기 위해선.. 영어를 잘해야 할 것 같다..

좋은 이름이 주는 가치는 아무리 강조해도 지나치지 않다..

“코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.”

확실히 짧은 이름을 가진 함수들은 해당 클래스가 성격을 나타낸다고 해도 쉽게 오용될 수 있다.

이름이 서술적일 경우엔 해당 기능을 예측하고 해당 기능대로 동작하기 때문에 읽기 쉽다.

이를 실현하려면 앞 서 말한 함수는 작아야 하고 한 기능만을 수행해야 한다.

서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.

이름을 붙일 땐 일관성이 있어야 한다.

모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.

includeSetupPage, includeSetupAndTeardownPages 처럼..

함수 인수

함수에서 이상적인 인수의 개수는 0개(무항)이다..

다음은 1개(단항)이고, 다음은 2개(이항)이다..

3개 이상은 가능한 피하는 편이 좋다.

4개 이상은 특별한 이유가 필요하고 있더라도 사용해서는 안된다.

인수는 어렵다. 인수는 개념을 이해하기 어렵게 만든다.

만약 인수로 함수를 넘긴다면 해당 코드를 읽는 사람은 해당 코드가 어떤 동작을 하는지 알아야 한다.

이는 함수 당 추상화 수준 자체를 바꿔버릴 수 있다.

이런 인수를 줄이는 방법에는 여러가지가 있지만 C#의 선택적 매개변수 기능을 추천한다.

많이 쓰는 단항 형식

함수에 인수 1개를 넘기는 가장 흔한 이유는 두가지다.

  1. 인수에 질문을 던지는 경우

    boolean fileExists("MyFile");
    
  2. 인수를 변형하는 경우

    InputStream fileOpen("MyFile");
    

이 두 가지 경우는 읽을 때 당연하게 받아들인다.

해당 함수의 동작 과정 자체가 이해가 되고 결과 값도 명확하기 때문

이벤트 함수에 관련한 내용이 나오는데..

이벤트 기반 프로그래밍 같은 패러다임도 있는 만큼 유용한 건 사실이다.

C#에서도 많이 사용하고 On과 같은 접두사를 붙여서 명확하게 드러낸다.

위에서 말한 경우가 아닌 단항함수는 피해야 한다.

플래그 인수

플래그 인수는 추하다.

함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이라 플래그가 참이면 이걸 하고 거짓이면 저걸한다는 말이니까..

이항 함수

인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다..

이항함수가 필요한 경우도 있지만 이항함수는 말 그대로 이해해야 하는 영역이 두가지가 추가된 것이다.

STM에 정보를 2개를 더 넣어야 하는 만큼 읽을 수 있는 영역이 줄어든다.

그렇다고 무조건 나쁘다는 것은 아니고 불가피한 경우도 생긴다.

하지만 그 만큼 위험이 따른다는 사실을 이해하고 가능하면 단항함수로 바꾸도록 애써야 한다.

삼항 함수

인수가 3개인 함수는 당연하게 2개보다 이해하기 어렵다.

여기서 부터는 고칠 것을 권장보다 권고..

인수 객체

인수가 2~3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다.

Circle makeCircle(double x, double y, double radius);

Circle makeCircle(Point center, double radius);

위 두 함수는 동일한 기능을 수행하지만 두번째 함수가 더 이해하기 쉽다.

인수 목록

때로는 인수 개수가 가변적인 함수도 필요하다.

가변 인수를 취하는 모든 함수에 같은 원리가 적용된다.

가변 인수를 취하는 함수는 단항, 이항, 삼항 함수로 취급할 수 있다.

가변인수.. c프로그래밍 할 때 이후로 처음 본다.

시스템적인 부분을 설계할 때는 필요할 것 같은데..

동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 이름이 필수다.

단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.

예를 들어, write(name)은 곧바로 이해한다.

이름을 쓴다라는 뜻이지만 좀 더 좋은 이름은 writeField(name)이다.

그러면 이름이 필드라는 사실이 분명하게 드러난다.

다른 예제로 함수 이름에 키워드를 넣는 방식이다.

assertEquals보다 assertExpectedEqualsActual이 더 좋다.

(expected, actual) 순서로 인수를 넘기는 것도 좋다.

그러면 인수 순서를 기억할 필요가 없이 함수 이름만 보고도 이해할 수 있다.

이렇게 사용하고 싶은데 왜 이렇게 어렵지,,

부수 효과를 일으키지 마라

부수 효과(Side Effect)는 거짓말이다.

함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른짓도 하니까..

때로는 예상치 못하게 클래스 변수를 수정한다.

때로는 함수로 넘어온 인수나 시스템 전역 변수를 수정한다.

어느 쪽이든 교활하고 해로운 거짓말이다.

이는 시간적인 결합이나 순서 종속성을 초래한다.

앞에서 다룬 플래그 인수나 전역 변수들이 이런 부수 효과를 일으키는 주범이다.

출력 인수

일반적으로 우리는 인수를 함수 입력으로 해석한다.

인수를 출력으로 사용하는 경우는 거의 없지만 C#은 이런 경우를 위해 out 키워드를 제공한다.

사실 객체지향적인 코딩을 지향한다면 이런 경우는 거의 없을 것이다.

appendFooter(s) 함수가 있다고 가정하자.

report.appendFooter(s)가 더 자연스럽다.

일반적으로 출력 인수는 피해야 한다.

함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.

명령과 조회를 분리하라

함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.

둘 다 하면 안된다.

명령과 조회를 명확하게 구분 한다면 함수 자체를 오용할 일이 없다.

오용은 side effect를 일으키는 주범이다.

오용을 예방하기 위해선 명령과 조회를 분리해야 한다.. ++ 위에서 말한 규칙들

오류 코드보다 예외를 사용하라

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.

오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.

try/catch 블록 뽑아내기

try/catch 블록은 원래 추하다..?!

코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다.

그러므로 try/catch 블록을 별도의 함수로 뽑아내는 편이 좋다.

오류 처리도 한 가지 작업이다

함수는 한 가지 작업만 해야 한다.

오류 처리도 한 가지 작업에 속한다.

그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.

함수에 try가 있다면 catch/finally도 함께 있어야 한다.

예제에서 처럼 오류를 enum값으로 들고 있게 되면 의존성이 생기게 되고 재컴파일/재배치가 이뤄져야 한다.

하지만 예외는 Exception 클래스를 상속받아서 만들 수 있기 때문에 이런 문제가 발생하지 않는다.

반복하지 마라

중복은 문제다.

코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 연관된 코드를 전부 고쳐야 한다.

상속도 이런 측면에서 나온 개념이겠지만.. 많이 이야기하는 상속의 문제점들도 남은 챕터들에서 해결되면 좋겠다..

AOP, OOP모두 이러한 중복 제거 전략이다.

구조적 프로그래밍

구조적 프로그래밍 원칙 중 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 하는 원칙이 있다.

즉, 함수는 return문이 하나여야 한다는 말이다.

해당 함수에서 발생할 수 있는 다른 return은 또 다른 함수로 분리..

루프 안에서 break나 continue를 사용해서는 안되거 goto는 절대로 안된다.

구조적 프로그래밍의 목표와 규율에는 공감하지만, 함수가 작다면 그렇게 큰 이익을 제공하지 못한다.

그러므로 함수를 작게 만다는다면 사용해도 괜찮다.. goto는 나가라!

함수를 어떻게 짜죠?

소프트웨어를 짜는 행위는 글짓기와 비슷하다.

논문이나 기사를 작성할 때는 먼저 생각을 기록한 후 읽기 좋게 다듬는다.

초안은 대게 서투르고 어수선하므로 원하는 대로 읽힐 때까지 맣을 다듬고 고치고 문단을 정리한다.

함수를 짤 때도 마찬가지다.

처음에는 길고 복잡하지만 서투른 코드가 많지만 그런 코드들을 단위 테스트 케이스로 만든 뒤 코드를 다듬고 함수를 만들고, 이름을 바꾸고, 메서드의 순서를 바꾸고, 클래스를 쪼갠다.

이와중에도 단위 테스트는 통과한다.

이게 TDD의 핵심아닐까.. 기능을 동작하게 만든 뒤 테스트를 통과시킨다.

이후에 리팩터링을 하면서 실시간으로 유닛테스트를 거치면 문제가 될 수 없다..

하지만 이런 방법은 처음부터 쉽게 할 수 있는 과정이 아니며, 궤도에 오르고 스스로 성장한다고 느끼며 계속 벽을 마주해야 한다.

피하지 말고 극복해야 한다.

결론

모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어DSL로 만들어진다.

함수는 그 언어에서 동사며, 클래스는 명사다.

프로그래밍의 기술은 언제나 언어 설계의 기술이다..

대가 프로그래머는 시스템 프로그램이 아니라 이야기로 여긴다.

프로그래밍 언어라는 수단으로 좀 더 풍부하고 좀 더 표현력이 강력한 언어를 만들어 이야기를 풀어간다.

“여러분이 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어가기가 쉬워진다는 사실을 기억하기 바란다.”

느낀점

가장 중요한 챕터가 아닐까 싶다..

객체지향적 설계뿐만 아니라 가장 기본이 되는 코드를 작성하는 방법들을 알려줘서 제일 읽을 때 영양가가 있었다.

물론 이미 나름 지키고 있는 룰도 있고 알고 있지만 지키고 있지 않은 방법, 처음 알게된 방법들도 있어서 좋았다..!

논의사항

사이드 이펙트를 경험해보셨다면 해당 이야기를 들어보고 싶습니다..!

저는 전역 상태와 각 객체마다 상태를 따로둬서 같은 상태값을 두개로 분리했었는데 이후에 오용되거나 헷갈려서 발생하는 사이드 이펙트를 경함한 적이 있습니다..

찾기 정말 힘들었던 기억이..

4장 주석

주석은 순수하게 선하지 못하다.

사실상 주석은 필요악이다.

주석을 사용하는 이유는 역설적으로 자신의 의도를 코드로 다 표현하지 못해서 사용하는 것이다.

만약 프로그래밍 언어를 치밀하게 표현할 능력이 있다면 주석은 필요하지 않다..

다른 말로 주석은 실패를 의미한다.

따라서 주석을 사용하는 상황에선 코드로 표현할 방법이 없는지 다시 한번 생각해봐야 한다.

저자는 주석을 이토록 싫어하는 이유로 거짓말을 하기 때문이라고 한다.

이는 주석 자체는 코드에 영향이 없기 때문에 업데이트되지 않고 유산으로 남기 때문이라고 한다.

코드가 커지며, 분리되며 부정확한 고아로 변해가는 사례가 너무나도 흔하기 때문에 주석은 거짓말을 한다고 할 수 있다.

그리고 애초에 모든 주석을 실시간으로 전부 업데이트, 추적을 할 수 없는 노릇이기에 없는 주석보다 부정확한 주석은 더욱 나쁘다.

부정확한 주석은 오히려 독자를 현혹하고, 오용하게 만든다..

진실은 언제나 하나..! (코난) 바로 코드이다.

프로그래머는 코드로만 대화해야 하며 언어 도중에 다른 언어를 혼용할 필요가 없다.

그러므로 우리는 주석을 줄이는 꾸준한 노력을 해야한다..

다른 프로그래밍 책에서도 많이 등장하는 파트로 역시 주석의 해악을 다룬다.

주석은 나쁜 코드를 보완하지 못한다

코드에 주석을 추가하는 일반적인 이유는 코드의 품질이 나쁘기 때문이다.

약간 변명의 성격도 가지는 것 같다..?

표현력이 풍부하고 깔끔하며 주석이 거의 없는 코드가, 복잡하고 어수선하며 주석이 많이 달린 코드보다 훨씬 좋다.

자신이 저지른 난장판을 주석으로 설명하려 애쓰는 대신에 그 난장판을 깨끗이 치우는 데 시간을 보내라.

코드로 의도를 표현하라

코드만으로 의도를 표현하기 어려운 경우가 있다.

불행히도 많은 개발자가 이를 코드는 훌륭한 수단이 아니라는 의미로 해석한다.

분명히 잘못된 생각이다..

// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

// 위의 코드를 아래와 같이 바꿔보자.
if (employee.isEligibleForFullBenefits())

명확하다.

좀 더 생각을 하면 답이 나오는데 왜..!

좋은 주석

어떤 주석은 필요하거나 유익하다.

법적인 주석

저작권 정보나 소유권 정보, 소스코드의 유래 등을 표현하는 주석이다.

오픈소스나 라이브러리 정의로 들어가보면 쉽게 볼 수 있다.

라이선스에 대한 내용이 많다..

정보를 제공하는 주석

때로는 기본적인 정보를 주석으로 제공하면 편리하다.

하지만 함수단위에서 해석이 가능하다.

의도를 설명하는 주석

코드만으로는 의도를 설명하기 어려운 경우가 있다.

그런 경우엔 주석을 사용하여 다른 개발자에게 의도를 설명해야 한다.

의미를 명료하게 밝히는 주석

때때로 모호한 인수나 반환값은 그 의미를 읽기 좋게 표현하면 이해하기 쉬워진다.

일반적으로 인수나 반환값 자체를 명확하게 만들면 더 좋겠지만, 인수나 반환값이 표준 라이브러리나 변경하지 못하는 코드에 속한다면 주석이 유용하다.

결과를 경고하는 주석

때로는 다른 프로그래머에게 결과를 경고할 목적으로 주석을 사용한다.

C#의 관점으로는 애트리뷰트로 쉽게 경고할 수 있는 것 같다. [Obsolete]

TODO 주석

TODO주석은 프로그래머가 당장 필요하다고 여기지만 당장 구현하기 어려운 업무를 기술한다.

이 주석은 나도 진짜 많이 사용한다.

TODO보다는 //temp로 많이 사용하는데 동작을 확인하기 위해서 해당 리터럴 값을 박아두거나 캐싱을 하지 않거나 구조를 잡기 위해서 임시 코딩같은 경우에 //temp로 잡아두고 커밋이 전에 temp를 검색해가며 구조를 다시 잡는다.

주석보다는 코딩 스타일에 가까운 것 같은데 미리 시각적인 완성을 먼저 보고 고치는걸 선호해서 그런 것 같기도 하다.

좀 더 나아가서 Github Action을 사용해서 Todo주석을 자동으로 Issue로 만들어 주는 기능도 있다..

중요성을 강조하는 주석

자칫 대수롭지 않다고 여겨질 뭔가의 중요성을 강조하기 위해서 주석을 사용한다.

이 부분도 PR단계에서 동료 개발자에게 공유가 되겠지만 매번 추적이 힘들기 때문에 어느정도 중요한 로직에는 주석도 괜찮을 것 같다.

나쁜 주석

대다수의 주석이 이 범주에 속한다..!

일반적으로 대다수 주석은 허술한 코드를 지탱하거나, 엉성한 코드를 변명하거나, 미숙한 결정을 합리화하는 등 프로그래머가 주절거리는 독백에서 크게 벗어나지 못한다.

주절거리는 주석

특별한 이유 없이 의무감으로 혹은 프로세스에서 하라고 하니까 마지못해 주석을 단다면 전적으로 시간낭비다.

2장에서 다룬 이름과 같이 주석또한 오용의 위험이 있기 때문에 그 당시 최고의 주석을 달아야 한다.

바이트 낭비라니..

같은 이야기를 중복하는 주석

이 부분은 더블체크에 관한 내용으로 같은 내용의 반복은 오히려 가독성을 떨어트린다..

비슷한 예제로 var키워드로 클래스명 그리고 변수명으로 두번씩 같은 내용을 반복하는 부분이 비슷한 예제라고 생각한다.

한마디로 쓸모없는 짓이다.

이해가 되지 않는다면 책을 읽는 상상을 해보면 쉽다.

책을 읽는데 앞에서 읽은 내용이 두번씩 자꾸 반복된다면 책이 이해가 잘 될까? 짜증나서 던져버릴 듯..

오해할 여지가 있는 주석

오해, 오용과 같이 다른 개발자 또는 내가 오해할 만한 주석을 달면 하늘에 쏘아올린 공이 나중에 크게 다가올 수 있다.

의무적으로 다는 주석

의무적으로 주석을 달게 되면 생산성 자체가 매우매우 떨어진다.

변경할 때 마다 2번씩 작업을 해야하며 오히려 거짓말을 퍼뜨려 혼동과 무질서를 초래한다..

이력을 기록하는 주석

과거에는 좋은 방법이였지만 지금은 킹왕짱 GIT을 사용하자.

있으나 마나 한 주석

더블체크와 같은 맥락이다 술취한 사람 처럼 주절거리거나 두번이상 말하거나 있으나 마나 한 이야기를 하지 말자..^^

무서운 잡음

위와 같은 내용

함수나 변수로 표현할 수 있다면 주석을 달지 마라

이 부분을 읽고 든 생각은 좋은 코드, 함수를 짜기 위해선 일단 주석과 같이 글로 적고 주석을 없애보는 훈련도 좋은 방법같다.

자신의 생각, 로직을 주석으로 적고 주석이 없어질 수 있도록 코드로 설명하는 방법이라면 납득할 수 있지 않을까?

위치를 표시하는 주석

특정 위치를 알려주려는 주석.. 요즘엔 IDE가 하도 좋아져서 이 부분이 의미가 있을까 싶다가 Unity에서 -------를 사용해 계층구조에서 구분을 사용했던 기억이 났다.

그런 부분도 명확하다면 구분이 필요가 없을까..?

닫는 괄호에 다는 주석

이건 처음 보는 주석 스타일.. 나쁜 주석이니까 그냥 넘어가자..

공로를 돌리거나 저자를 표시하는 주석

GIT이 있기 때문에 저자를 표시할 필요가 없다..!!

주석으로 처리한 코드

음.. 아까운 로직은 나는 아직까지 주석으로 남겨둔다.

따로 깃에 저장하는 경우도 있지만 프로젝트 진행도중 바뀐 로직에 의해 다시 사용할 수도 있기 때문에 주석으로 남겨둔다.

이후에 Tag버전이나 완전히 종료된 경우에 리팩터링 과정에서 삭제하는 것 같다.

HTML 주석

로버트 마틴: 혐오 그 자체다.

진짜 이 아저씨는 코드 그 자체인듯..

전역 정보

주석을 달아야 한다면 근처에 있는 코드만 기술하라

이 부분은 나는 지금까지 README또한 주석이라 생각했다.

하나의 소프트웨어가 있다면 사용방법에 대한 정도로 생각했는데 조금 다른 문제인 것 같다.

너무 많은 정보

불필요한 정보는 주석으로 달지 말자

모호한 관계

주석과 주석이 설명하는 코드는 둘 사이 관계가 명백해야 한다.

함수 헤더

짧은 함수는 긴 설명이 필요 없다.

짧고 한 가지만 수행하며 이름을 잘 붙인 함수가 주석으로 헤더를 추가한 함수보다 훨씬 좋다.

느낀점

음.. 읽고 생각난 Spine코드를 분석할 때 일인데..

실제 해당 라이브러리 코드에는 꽤 많은 함수 설명을 위한 주석이 있었다.

함수의 깊이또한 깊었기 때문에 그걸 다 타고 들어가서 동작 과정을 이해하기엔 무리가 있으니 라이브러리 같은 경우는 사용되는 API에 주석을 달아두는 경우가 많은 것 같다.

같지만 다른 맥락, 다른 상황이라고 생각한다.

논의사항

중간에 나온 나쁜 주석: 주석으로 처리한 코드부분에서 저는 리팩터링 이전에는 남겨두는 편인데 같은 작업자가 있다면 안좋은 습관 같습니다..

유산적인 코드들을 남겨놓는 좋은 방법이 있을까요?

5장 형식 맞추기

우리는 코드를 하나 열어볼 때 깔끔하고, 일관적이고, 꼼꼼하고, 질서정연하다면 나머지 코드들도 똑같이 깔끔할 것이라 기대한다.

반대로 술에 취한 사람들이 짜 놓은 것 같은 코드를 봤다면 프로젝트에 대한 다른 측면도 동일하게 생각하게 될 것이다.

프로그래머라면 형식을 깔끔하게 맞춰 코드를 짜야 한다.

코드 형식을 맞추기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다.

팀으로 일한다면 팀이 합의해 규칙을 정하고 그 규칙을 따라야 한다..!

코드 컨벤션은 시작 시에 맞추는 컨벤션과 모든 코드가 동일하게 적용되는 비인지적인 부분이 있는 것 같다.

생각나는게 여러개 있는데 등장하는 목차에 따라 연결지어서 나열해본다.

형식을 맞추는 목적

코드 형식은 중요하다.

너무나도 중요하므로 융통성 없이 맹목적으로 따르면 안 된다.

코드 형식은 의사소통의 일환이다.

의사소통같은 느낌으로 생각하니 같은 언어라도 지역마다 사투리 정도로 생각된다.

팀 by 팀, 프로젝트 by 프로젝트별 컨벤션이 조금씩 상이하니 읽을 순 있지만 사투리 정도로 들리는게 아닌가.. 생각이 든다.

오늘 구현한 기능은 다음 버전에서 바뀔 가능성이 높다.

하지만 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드의 품질에 지대한 영향을 미친다.

맨 처음 잡아놓은 구현 스타일과 가독성 수준은 유지보수성과 용이성에서 계속 마이너스를 불러온다.

그럼 원활한 소통을 장려하는 코드 형식은 어떤 것이 있을까?

적절한 행 길이를 유지하라

소스 파일의 크기, 코드의 줄 수는 몇줄이 적당할까?

대부분 개발 서적에서도 같은 맥락으로 100줄을 넘어가면 의심을 시작하라고 하는데 사실 주관적인 부분이라 다른 책에서도 절대적이진 않다고 한다.

하지만 그에 대한 반증으로 잘 짜여진 대형 애플리케이션의 소스코드를 본다면 좋은 방향인 것은 명백하다.

따라서 약 65~100줄 정도가 적당하다고 생각한다.

클래스 단위에서 이 줄을 넘어가면 한 가지 기능보다 더 많은 것을 수행하게 된다.

행 길이를 유지하는 것 또한 하나의 토템으로 작동할 수 있다.

신문 기사처럼 작성하라

아주 좋은 신문 기사를 떠올리면 우리는 표제를 보고 읽을지 말지를 결정한다.

첫 문단은 전체 기사를 요약한다.

세세한 사실은 숨기고 그림을 보여준다.

쭉 읽으며 내려가고 세세한 사실이 조금씩 드러난다.

소프 파일도 비슷하게 작성한다.

이름은 간단하면서도 설명이 가능하게 짓는다.

이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지를 판단할 수 있어야 한다..

소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다.

아래로 내려갈수록 의도를 세세하게 묘사한다.

마지막에는 가장 저차원 세부 사항이 나온다.

한 마디로 모든 정보를 정리하는게 아닌 개발자 측면에서 읽기 유리하게 코드를 작성해야 한다는 것이다.

개념은 빈 행으로 분리하라

코드는 왼쪽 위 정렬로 읽는다.

각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현한다.

빈 행은 새로운 개념을 시작한다는 시각적 단서다.

빈행 또한 코드의 일부이다.

이런 부분들이 조금 비인지적인 컨벤션이라고 생각된다.

괄호의 위치같은게 아닌 당연시 되는 부분이라고 생각한다.

세로 밀집도

줄바꿈이 개념을 분리한다면 세로 밀집도는 연관성을 의미한다.

즉, 서로 밀집한 코드 행은 세로로 가까이 놓여야 한다는 뜻이다.

바뀐 코드를 봐도 눈이나 머리를 거의 쓸 필요가 없다.

수직 거리

타고 타고 위 아래로 반복하게 하는 코드는 절대 좋은 코드가 아니다.

서로 밀접한 개념끼리는 세로 거리를 강한 밀집도를 지녀야 한다.

나름의 우선순위와 이해가 가능한 규칙으로 구성해야 한다.

이 이유가 상속과 protected 변수를 멀리해야 하는 이유와 같은 맥락이다.

변수 선언

변수는 사용하는 위치에 최대한 가까이 선언한다.

인스턴스 변수

인스턴스 변수는 클래스 맨 처음에 선언한다.

논쟁에 대한 가위 규칙에 대한 부분은 처음보는 부분이라..

저런 규칙이 있다는 정도만 알고 넘어간다.

종속 함수

한 함수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다.

또한, 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다.

개념적 유사성

어떤 코드는 서로 끌어당긴다.

개념적인 친화도가 높기 때문이다.

친화도가 높을수록 코드를 가까이 배치한다.

예로는 함수가 다른 함수의 종속성을 가지고 있을 때, 변수와 그 변수를 사용하는 함수 정도가 있다.

예제처럼 오버로딩과 관련된 부분도 모아두는 것이 바람직하다.

세로 순서

일반적으로 함수 호출 종속성은 아래 방향으로 유지한다.

따라서 호출되는 함수를 호출하는 함수보다 나중에 배치한다.

그러면 소스 코드 모듈이 고차원에서 저차원으로 자연스럽게 내려간다.

신문 기사와 마찬가지로 중요한 개념을 가장 먼저 표현한다.

세세한 사항은 최대한 배제하고 가장 마지막에 표현한다.

여기서 세세한 사항조차 래핑을 하면 그 함수를 볼려고 어차피 눈이 이동해야 하는게 아닌가?

라고 생각을 한다면 2장을 다시 읽어야 한다.

간단한 제곱을 하는 Math함수를 동작과정을 이해하기 위해 내부 정의로 들어가지 않는 것과 같은 맥락이다.

하나 더 조심해야하는 부분은 코드 계약으로 3장 함수의 내용처럼 오용될 수 있는 함수나 기능을 예상하여 작성하는 함수들이다..

다시 강조하지만 이름은 해당 함수의 기능을 명확하게 나타내고 해당 함수는 그 기능만을 수행한다면 코드는 글처럼 동작한다.

가로 형식 맞추기

세로 형식과 마찬가지로 가로또한 완성도 있는 프로젝트를 조사해보니 약 10자 미만, 20~60자 정도까지 나온다.

프로그래머는 명백하게 짧은 행을 선호한다.

가로 공백과 밀집도

가로로는 공백을 사용해 밀접한 개념과 느슨한 개념을 표현한다.

할당 부분은 공백으로 구분하고 함수와 이어지는 부분은 구분하지 않는다.

이 처럼 밀접한 개념 그리고 느슨한 개념을 공백으로 표현할 수 있다.

가로 정렬

가로 정렬은 오히려 코드를 읽기 어렵게 만든다.

따라서 패스.

들여쓰기

소스 파일은 윤곽도와 계층이 비슷하다.

파일 전체에 적용되는 정보가 있고, 파일 내 개별 클래스에 적용되는 정보가 있고, 클래스 내 메서드에 적용되는 정보가 있고, 블록 내 블록에 재귀적으로 적용되는 정보가 있다.

이렇듯 범위로 이루어진 계층을 표현하기 위해 들여쓴다.

들여쓰기 정도는 계층에서 코드가 자리잡은 수준에 비례한다.

들여쓰기 무시하기

선호의 차이일까? 람다정도는 들여쓰기를 무시하고 작성하는 편이다.

실제로 C#컨벤션에서 프로퍼티나 한줄 람다는 많이 사용하는 편인 것 같다

하지만 한줄에 가로 10줄 이상 길어지는 로직은 들여쓰기가 반드시 필요해보인다.

가짜 범위

이건 닌자코드같은 느낌이라..

패스으

팀 규칙

가장 우선시 되는 규칙이 바로 팀 규칙이다.

회사에 들어가 자신이 올바르다고 하며 해당 팀 규칙을 무시하는 신입사원은 없을 것이다.

좋은 소프트웨어는 읽기 쉬운 문서로 이뤄진다.

밥 아조씨의 형식 규칙

대괄호 위치를 제외하고 자바 자체가 C#과 매우 비슷해서 어색하지 않게 느껴진다.

느낀점

지식을 다시 한번 정리하는 느낌의 챕터였다.

논의사항

자신만의 컨벤션이 있을까요?

저는 유니티로 작업할 때 하이어라키에서 중요도에 따라 우선순위로 작업하고 코드도 해당 순위를 따라가는 편입니다.

코드레벨이 아니라 유니티에선 컴포넌트 단위에서도 컨벤션이 어느정도 (비인지적인)부분이 존재하는 것 같습니다.

순위가 없을 땐 솔류션 뷰에서 정렬된 순서로 작업하는 것 같습니다.

6장 객체와 자료 구조

객체지향 코딩에서 변수를 private로 정의하는 이유가 있다.(캡슐화)

남들이 변수에 의존하지 않게 만들고 싶어서이다.

여기서는 굉장히 많은 이유가 존재하지만 앞부분에서 다룬 내용과 중복되거나 뒤에서 다룰 내용이기 때문에 생략한다.

수 많은 프로그래머 get, set함수를 당연하게 get, set함수를 당연하게 공개 비공개 변수를 외부로 노출할까?

프로퍼티를 사용하는 이유와 같은 맥락

자료 추상화

추상적인 클래스를 만들어서 용도를 가리고 자료구조의 형태는 명백하게 한다.

반면 구체적인 클래스는 용도를 명백하게 하고 사용하는데 멤버를 private으로 가리고 get, set을 열어두더라도 구현을 외부로 노출하는 셈이다.

변수 사이에 함수라는 계층을 넣는다고 구현이 저절로 감춰지지는 않는다.

구현을 감추려면 추상화가 필요하다..!

그 보다는 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스이다.

여기 부분이 정말 좋은 부분..

인터페이스나 조회/설정 함수만으로는 추상화가 이뤄지지 않는다.

개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다.

아무 생각 없이 조회/설정 함수를 추가하는 방법이 가장 나쁘다.

이 부분을 아직도 알아가는 중이라 생각이 드는데 객체가 포함하는 자료를 표현할 가장 좋은 방법을 항상 고민하는 습관을 가져야겠다.

추천하는 책으로는 지금 읽고 있는 객체지향 사고 프로세스라는 책이 있는데 객체지향의 본질적인 내용을 주로 하기에 위에서 다룬 추상화 수준에 관한 내용이 많이 나온다

자료/객체 비대칭

객체는 추상화 수준을 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.

자료 구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.

문단을 읽어보면 두 정의는 본질적으로 상반된다.

절차지형적인 코드와 객체지향적인 코드의 차이점은 객체지향이 유리한 지점에 있지만 항상 수반되거나 트레이드 오프가 발생한다.

절차적인 코드는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기가 쉽다. 반면, 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기가 쉽다.

절차적인 코드는 새로운 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다. 객체 지향 코드는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.

다시 말해 객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉬우며, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.

복잡한 시스템을 짜다 보면 새로운 함수가 아니라 새로운 자료 타입이 필요한 경우가 생긴다.

이때는 클래스와 객체지향 기법이 가장 적합하다.

반면 새로운 함수가 필요한 경우에는 절차적인 코드와 자료 구조가 좀 더 적합하다.

많은 객제지향에 다룬 책, 그리고 앞서 이야기 한 객체 지향 사고 프로세스에서도 객체지향, 절차지향(구조적)인 코드를 적절히 사용하는 것이 중요하다고 말한다.

디미터 법칙

모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙

객체는 자료를 숨기고 함수를 공개한다.

즉, 객체는 조회 함수로 내부 구조를 공개하면 안 된다는 의미다.

기차 충돌

메서드 체인을 걸어서 반환하는 객체에 함수를 호출하고 해당 함수의 반환하는 객체에 함수를 호출하고… 반복

이를 기차 충돌이라고 한다.

여러 객체가 한줄로 이어진 기차처럼 보이기 때문이다.

따라서 구조를 할당받아 명확하게 하는게 좋다.

잡종 구조

절반은 객체, 절반은 자료구조인 잡종 구조가 니온다..(절비)

잡종 구조는 중요한 기능을 수행하는 함수도 있고, 공개 변수나 공개 조회/설정 함수도 있다.

공개 조회/설정 함수는 비공개 변수를 그대로 노출시킨다.

덕택에 다른 함수가 절차적인 프로그래밍의 자료 구조 접근 방식처럼 비공개 변수를 사용하고픈 요혹에 빠지기 십상이다..

이런 잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵다.

게임쪽 마이너 코드는 잡종 구조가 많이 보인다..

구조체 감추기

ctex가 객체라면 우리는 뭔가를 하라고 말해야지 속을 드러내라고 말하면 안된다.

즉, 자료구조를 가지고 있는 잡종 구조라도 해당 자료 자체를 그대로 반환하는게 아닌 깊은 복사 후 새로운 객체를 반환하는 팩토리단위의 이야기를 하는게 아닐까?

객체는 행위의 주체이기 때문에 그 속의 내부 사항은 구성하는 클래스에서는 몰라야 하며 사용하는 입장에선 메서드의 이름으로 판단해야 한다.

이런 측면에선 차라리 코딩할 때 클래스가 바뀔 때 마다 즉, 전환하며 작업할 때 마다 전 클래스에서 작업했던 내용을 지워버리면 더 커플링이 줄어들 것 같다..(극단적인 예시)

자료 전달 객체

자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다.

이런 자료 전달 객체를 DTO라고 부른다.

흔히 DTO는 데이터베이스에 저장된 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음 사용하는 구조체다.

자료 전달 객체이기 때문에 구조체라고 하는걸까?

좀 더 일반적인 형태는 빈(bean)구조이다.

빈은 비공개 변수를 조회/설정 함수로 조작한다.

활성 레코드

활성 레코드는 DTO의 특수한 형태다.

공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료 구조지만, 대개 save나 find 같은 탐색 함수도 제공한다.

하지만 이런 비즈니스 규칙 메서드를 추가해 이런 자료 구조를 객체로 취급하는 개발자가 흔하다..

해결책은 활성 레코드를 자료구조로 취급하면 된다..

결론

객체는 동작을 공개하고 자료를 숨긴다.

그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기 쉽다.

기존 객체에 새 동작을 추가하기는 어렵다.

자료 구조는 별다른 동작 없이 자료를 노출한다.

그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료 구조를 추가하기는 어렵다.

느낀점

자료구조와 객체에 대해서 깊이 생각해본적 없어서 객체지향에 대한 생각을 정리하기 좋았던 챕터이다.

논의사항

디미터의 법칙이 스스로 코딩할 때 점검하기 좋은 요소라고 생각되는데 이를 점검하는 프로세스를 만들면 좋겠다는 생각입니다.

디미터의 법칙과 같이 자신만의 객체지향 프로세스를 가지기 위해 노력하는 부분이 있나요?

저는 다른 코드에서 많이 배울려고 했었고 최근에는 코드 스타일 자체에 많이 녹여볼려고 시도중입니다.

7장 오류 처리

오류 처리는 프로그램에 반드시 필요한 요소 중 하나일 뿐이다.

프로그램은 뭔가 잘못될 가능성은 늘 존재한다.

슈퍼마리오 64의 버그문제, 만조에 일어나는 서버문제 등등 예상조차 할 수 없는 문제도 있다

뭔가 잘못되면 바로 잡을 책임은 바로 우리 프로그래머에게 있다.

깨끗한 코드와 오류 처리는 확실히 연관성이 있다.

상당수 코드 기반은 전적으로 오류 처리 코드에 좌우된다.

하지만 오류 처리 코드로 인해 프로그램의 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다.

오류 코드보다 예외를 사용하라

분기로 오류를 잡아서 처리하는 방법과 try/catch 블록을 사용하는 방법이 있다.

예제 1번같은 경우엔 호출자 코드가 복잡해지면서 역순으로 타고 가야하는 계층의 문제도 발생할 수 있다.

따라서 오류가 발생하면 예외를 던지는 방법이 더 좋다.

이런 예외 처리의 부분은 게임쪽에선 서버, I/O, 파일 입출력 부분에서 많이 사용되는 것 같다.

게임 루틴자체에선 사용하기 무거운 느낌이라 테스트 코드로 잡고 예외가 발생하는 지점만 체크하는 방식.

Try-Catch-Finally 문부터 작성하라

예외에서 프로그램 안에다 범위를 정의한다는 사실이 매우 흥미롭다.

try-catch-finally 문에서 try블록에 들어가는 코드를 실행하면 어느 시점에서든 실행이 중단된 후 catch 블록으로 이동한다.

트랜잭션과 비슷

try블록에서 무슨 일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.

그러므로 예외가 발생할 코드를 짤 때는 try-catch-finally 문으로 시작하는 편이 낫다..!

미확인 예외를 사용하라

확인된 예외는 OCP를 위반한다.

메서드에서 확인된 예외를 던졌는데 catch블록이 세 단계 위에 있다면 그 사이 메서드 모두가 선언부에 해당 예외를 정의해야 한다.

때로는 확인된 예외도 유용하지만 의존성이 필수적으로 발현되기 때문에 미확인 예외를 사용해라

실제로 C#은 미확인 예외만 지원하며 확인된 예외를 지원하지 않음

예외에 의미를 제공하라

예외를 던질 때는 전후 상황을 충분히 덧붙인다.

그러면 오류가 발생한 위치를 찾기 쉬워진다.

즉, 오류 메세지에 정보를 담아 예외와 함께 던진다.

호출자를 고려해 예외 클래스를 정의하라

오류를 분류하는 방법은 수없이 많다.

오류가 발생한 위치로 분류가 가능하다.

오류가 발생한 컴포넌트, 유형으로 분류가 가능하지만 가장 주된 관심사는 오류를 잡아내는 방법이 되어야 한다.

정상 흐름을 정의하라

오류처리에 대한 논의대로 작성하다 보면 코드가 깨긋해 보이고 간결한 알고리즘으로 분류되기 시작한다.

하지만 그러다 보면 오류 감지가 프로그램 언저리로 밀려난다.

외부 API를 감싸 독자적인 예외를 던지고, 코드 위에 처리기를 정의해 중단된 계산을 처리한다.

대개는 멋진 처리 방식이지만, 때로는 중단이 적합하지 않은 때도 있다..

null을 반환하지 마라

오류 처리를 논하는 장이라면 우리가 흔히 저지르는 바람에 오류를 유발하는 행위도 언급해야 한다고 생각한다.

그 중 첫째가 null을 반환하는 습관이다.

null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다.

확실히 null이 불러오는 문제는 덩치가 커질 수 있고, 그 당시에는 알고리즘이 간단해 보일 수 있지만 결국에는 일거리가 늘어나는 것 같다.

나도 지금 짠 코드 중에 null을 반환하는 메서드가 종종 있던 것 같다.

메서드에서 null을 반환하고 싶은 유혹이 든다면 그 대신 예외를 던지거나 특수 사례 객체를 반환하라.

예제 처럼 몬스터 리스트를 반환 받는 경우 해당 리스트의 개수만큼 크기, 개수에 종속되지 않고 있는 만큼만 실행하는 로직으로 만들어 두면 이후에 수정할 필요가 전혀 없어지고 null 반환할 필요도 없다.

null을 전달하지 마라

메서드에서 null을 반환하는 것도 나쁘지만, 메서드로 null을 전달하는 행위는 더 나쁘다.

이 부분은 예외 처리로 잡기보다 애초에 null을 전달하는 행위 자체가 이해가 되지 않는다..

따라서 null을 넘기지 못하도록 금지하는 것이 적당하다.

결론

깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다.

이 둘은 상충하는 목표가 아니다.

오류 처리 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드를 작성할 수 있다.

오류 처리 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성이 크게 높아진다.

논의사항

null값 체크 자체가 비용이 든다고 생각을 안했었는데 책을 읽고 다시 생각해보니 null을 반환하는 메서드를 만드는 것 자체가 무책임하다고 느끼게 된 것 같습니다..

null 체크에 대한 생각이 궁금합니다!

8장 경계

시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다.

어떤 식으로든 외부 코드를 우리 코드에 깔끔하게 통합해야 한다.

외부 코드 사용하기

인터페이스 제공자와 인터페이스 사용자 사이에는 특유의 긴장이 존재한다.

패키지 제공자나 프레임 제공자는 적용성을 최대한 넓히려 애쓰는 반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다.

예제와 같이 만약 유니티에서 몬스터 리스트를 싱글톤으로 구현하여 프로퍼티로 get만 열었다고 해도 다른 사용자는 해당 리스트를 clear할 수 있다.

접근이 가능하다는 것은 해당 API를 호출할 수 있다는 것이기 때문에 위험도는 매우매우 높아진다. (안티패턴)

따라서 이런 문제점을 해결하기 위해 private으로 두고 필요한 기능만 여는 방법이 있다.

경계 살피고 익히기

바퀴를 다시 만들지 마라

즉, 외부 코드를 사용하면 개발 시간이 많이 단축된다.

외부 패키지는 우리 책임은 아니지만 우리 자신을 위해 코드를 테스트하는 것이 바람직하다.

대개 타사 라이브러리를 사용한다고 하면 문서를 읽으면서 2일정도 사용법을 결정한다.

그 다음 실제로 우리가 원하는 대로 동작하는지 확인한다.

외부 코드는 익히고, 통합하기 어렵다.

두가지를 동시에 하기에도 많이 어렵기 때문에 반대로 생각해 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 간단한 테스트 케이스를 작성해 외부 코드를 익히는 벙법을 학습 테스트라 한다.

처음 라이브러리를 사용할 때 해당 example이나 클론코딩의 과정과 유사하다

과정 자체가 토이 프로젝트, 샌드박스에 적용해보면서 학습하는 것이 가장가장 좋은 방법이라고 말해준다.

실제로 나도 라이브러리를 사용할 때는 무조건 샌드박스에서 사용할 기능 위주로 다양하게 테스트를 해보고 적용한다.

학습 테스트는 공짜 이상이다

학습 테스트에 드는 비용은 없다.

어쨌든 API를 배워야 하므로 오히려 필요한 지식만 확보하는 좋은 방법이다.

학습 테스트는 공짜 이상이다.

투자하는 노력보다 얻는 성과가 더 크다.

아직 존재하지 않는 코드를 사용하기

경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.

때로는 우리 지식이 경계를 너머 미치지 못하는 코드 영역도 있다.

때로는 알려고 해도 알 수가 없다.

따라서 아직 존재하지 않는 코드를 사용하기 위해선 인터페이스로 분리하는 것이 좋다.

우리가 바라는 인터페이스를 구현하면 인터페이스를 통해 전적으로 통제한다는 장점이 생긴다.

깨끗한 경계

소프트웨어가 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.

엄청난 시간과 노력와 재작업을 요구하지 않는다.

통제하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 각별히 주의해야 한다.

경계에 위치하는 코드는 깔끔히 분리한다.

또한 기대치를 정의하는 테스트 케이스도 작성한다.

이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다.

통제 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 좋다.

느낀점

코드에는 어떤식으로든 경계는 존재하는 것 같다.

그런 경계와 깊이에 대해 모든 걸 다 알려고 하는 삽질은 너무 큰 비용을 불러온다.

과거에 삽질을 되게 좋아해서 많이 했던 기억이 있는데 물론 그런 삽질은 새로운 지식을 습득하는 능력을 길러줬다고 생각하지만 지금은 그런 삽질을 하지 않는 것이 좋다고 생각한다.

공부하는 것 이상으로 정보가 쏟아지기 때문에 잘 선별해서 공부해야 한다..!

논의사항

단순한 궁금증인데 사용하기 어려웠던 라이브러리, 패키지가 있나요?

왜 그랬는지 다시한번 생각해본서 이야기 해보면 재밌을 것 같습니다.

저는 스파인을 사용했을 때 되게 어려웠던 기억이 있습니다.

공식문서에 예제와 글이 존재하지만 구글 자체에 정보도 매우 적어서 고생을 했던 기억이 있는데 지금 생각해보면 미련하게 공부했던 것 같습니다.

9장 단위 테스트

TDD란, 테스트가 주도하는 개발이다.

서비스를 제작하기전 테스트를 먼저 만들고, 테스트를 통과하는 코드를 작성하는 것이다.

이런 플로우를 가지게 되면 이후에 구현부가 수정되어 세부동작이 달라져도 테스트만 통과한다면 시스템 자체의 문제가 없어지게 된다.

예제에선 테스트코드가 없는 개발이 불러올 수 있는 문제점을 보여준다.

현재의 개발추세는 애자일과 TDD덕택에 단위 테스트를 자동화하는 프로그래머가 많아졌다.

CI/CD, 데브옵스와 같이 게임쪽에서도 젠킨스 GameCI가 주목받고 있다.

TDD 법칙 세 가지

  1. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

위 세가지 규칙을 따르면 개발과 테스트가 대략 30초 주기로 묶인다.

아직 테스트코드를 주도적으로 짜본 경험이 없어서 이 세가지 법칙에 대해 감이 안집히는 것 같다.

웹 상에서도 게임 개발에 TDD를 사용하는 예제가 적다보니 더욱 멀게 느껴진다.

깨끗한 테스트 코드 유지하기

역설적이게도 실제 코드베이스의 코드가 변경되면서 테스트 코드 마찬가지로 도메인이 원하는 형태로 변경되어야 한다.

그렇다면 테스트 코드도 앞서 다룬 유지보수를 위한 가독성 좋은 코드를 짜야한다는 결론이 나온다.

이 이야기에서 주는 교훈은 더러운 테스트 코드는 안짜는 것보다 못하다는 결론이다.

깨끗한 테스트 코드를 유지하기 위해선 코드 자체를 작고, 한가지 기능만 할 수 있도록 짜야한다.

테스트 코드는 실제 코드 못지 않게 중요하다.

테스트는 유연성, 유지보수성, 재사용성을 제공한다.

이런 이점을 제공하는 버팀목이 바로 단위 테스트이다.

이유는 위에서 설명했듯이 테스트 케이스 코드가 있다면 변경이 두럽지 않다.

테스트 코드의 이점은 이 부분과 처음 설계를 유연하게 가져갈 수 있다는 점에서 매우 유용한 것 같다.

반대로 테스트 케이스가 없다면 모든 변경이 버그를 유발할 수 있는 가능성이 있고, 설계를 잘해도 변경을 두려워하게 된다.

깨끗한 테스트 코드

그렇다면 깨끗한 테스트 코드를 만드려면..?

가독성, 가독성, 가독성이 가장 중요하다 (*3)

테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.

책에서 나오는 테스트 코드의 리팩터링 과정을 보니,,? 리팩터링 책도 빨리 읽어보고 싶다는 생각이 든다..

테스트 당 assert 하나

함수마다 assert문 단 하나만 사용해야 한다고 주장하는 사람도 있다.

아마 예상하기론 함수당 기능을 하나, 클래스당 기능을 하나만 가지는 것과 같은 이유가 아닐까 싶다..

마찬가지로 비슷한 이점을 가지는데 assert문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽다는 장점이 있다.

하지만 하나의 테스트 함수에 assert가 하나만 들어가지 못하는 경우도 있는데 이런 상충되는 부분을 쉽게 생각할 수 있는 것이 테스트 당 개념 하나이다.

테스트 당 개념 하나

테스트당 한 개념만 테스트하라라는 규칙이 있다면 현재 작성한 코드에서 개념들을 분리할 수 있을 것이다.

이 과정에서 코드를 어떻게 설계할지, 재구성할지 조금 가닥이 잡히지 않을까?

F.I.R.S.T

깨끗한 테스트 코드를 만들기 위한 다섯 가지 규칙이다.

  • Fast: 테스트는 빨라야 한다.
  • Independent: 각 테스트는 서로 의존하면 안된다.
  • Repeatable: 테스트는 어떤 환경에서도 반복 가능해야 한다.
  • Self-Validating: 테스트는 bool 값으로 결과를 내야 한다.
  • Timely: 테스트는 적시에 작성해야 한다.

결론

사실상 TDD나 테스트 코드에 관한 내용은 책이 따로 있을 정도로 중요한 내용이라 이번 챕터에선 가볍게 다룬 것이 사실이다.

느낀점

테스트 코드, 테스트 코드 해서 조금 알아보고 내용 자체는 알고 있었지만 각 잡고 제대로 TDD개발을 해본적이 없어서 많이 아쉬운 것 같다.

웹상에서도 정보가 적어서.. 다음 북클럽에 TDD를 주제로 한 책을 읽어보면 좋을 것 같다.

게임 개발자끼리 이런 내용을 실제 프로젝트에 적용해보며 공유하는 시간을 가지고 싶다..!

논의사항

이런 분야의 새로운 패러다임을 공부할 때 어떻게 공부하는지 궁금합니다..!

좋은 접근방법이 있으면 같이 이야기 해보면 좋을 것 같습니다.

10장 클래스

코드의 표현력과 그 코드로 이루어진 함수에 아무리 신경을 쓸지라도 좀 더 높은 차원에 신경을 쓰지 않으면 깨끗한 코드를 얻기는 어렵다.

클래스 체계

클래스를 정의하는 표준 자바 관례에 따르면 가장 먼저 변수 목록이 나온다.

정적, 공개 상수가 있다면 처음으로 나오고 다음으로 정적 비공개 변수, 이어서 비공개 인스턴스 변수가 나온다.

앞서 배운 내용처럼 공개 변수가 필요한 경우는 거의 없고 없어야 한다.

변수 목록 다음으론 공개 함수가 나오고 비공개 함수는 자신을 호출하는 공개 함수 직후에 넣는다.(5장 형식 맞추기)

즉, 추상화 단계가 순차적으로 내려간다.

그래서 프로그램은 신문 기사처럼 읽힌다.

캡슐화

변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야 한다는 법칙도 없다.

때로는 protected로 선언해 테스트 코드의 접근을 허용하기도 한다.

이런 방법 이전에 우리는, 프로그래머는 비공개 상태를 유지할 방법을 계속 강구해야 한다.

클래스는 작아야 한다

함수파트와 동일한 이야기(해당 파트에서도 이야기 했지만) 클래스도 마찬가지로 작아야 한다.

여기서 이 작게라는 규칙은 객체지향의 기본 성격을 설명하는 정말 다양한 이유가 있다.

단일 책임 원칙

단일 책임 원칙(Single Responsibility Principle, SRP)은 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다는 원칙이다.

여기서 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시한다.

클래스는 책임, 즉 변경할 이유가 하나여야 한다는 의미이다.

책임, 즉 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화하기도 쉬워진다.

더 좋은 추상화가 더 쉽게 떠오른다.

하지만 마이너한 코드 대부분이 SRP를 준수하고 있지 않다.

이는 깨끗하고 체계적인 코드보다 돌아가는 소프트웨어에 집중하기 때문이다.

따라서 관심사를 분리하는 작업은 프로그램만이 아니라 프로그래밍 활동에서도 마찬가지로 중요하다.

많은 개발자가 자잘한 단일 클래스가 많아지면 큰 그림을 이해하기 어려워진다고 우려한다.

큰 그림을 이해하려면 이 클래스 저 클래스를 수없이 넘나들어야 한다고 걱정한다.

하지만 작은 클래스가 많은 시스템이든 큰 클래스가 몇 개뿐인 시스템이든 돌아가는 부품은 그 수가 비슷하다.

이를 다시 생각해서 개별적인 책임이 있다고 생각할 때 예를 들어 도구 상자에 많이 몰아 넣고 싶은지와 명확한 이름으로 구분하여 나눠 넣고 싶은지를 생각해보면 된다.

규모가 어느 수준에 이르는 시스템은 논리가 많고도 복잡하다.

이런 복잡성을 다루려면 체계적인 정리가 필수다.

그래야 개발자가 무엇이 어디에 있는지 쉽게 찾는다.

큰 클래스가 몇 개가 아니라 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다.
작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유도 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

응집도

클래스는 인스턴스 변수 수가 작아야 한다.

각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.

사용하지 않는다면 그건 아마 static 순수 함수와 같은 수준일 것..

일반적으로 메서드가 변수를 더 많이 사용 할수록 메서드와 클래스는 응집도가 더 높다.

일반적으로 이처럼 응집도가 높은 클래스는 가능하지도 바람직하지도 않다.

그렇지만 우리는 응집도가 높은 클래스를 선호한다.

함수를 작게, 매개변수 목록을 짧게라는 전략을 따르다 보면 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다.

이는 새로운 클래스로 쪼개여 한다는 신호이다.

응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두세 개로 쪼개준다.

응집도를 유지하면 작은 클래스 여럿이 나온다

큰 함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다.

예를 들어, 변수가 아주 많은 큰 함수 하나가 있다.

큰 함수 일부를 작은 함수로 빼내고 싶은데, 빼내려는 코드가 큰 함수에 정의된 변수 넷을 사용한다.

그렇다면 변수 네 개를 새 함수에 인수로 넘겨야 할까?

전혀 아니다..!

네 변수를 클래스 인스턴스 변수로 승격한다면 새 함수는 인수가 필요없다.

그만큼 함수를 쪼개기 쉬워진다.

불행히도..? 이렇게 되면 클래스가 응집력을 잃어버린다.

여기서 잠깐..! 몇몇 함수가 몇몇 변수만 사용한다면 독자적인 클래스로 분리해야 한다. 라는 규칙을 위해서 말했으니 클래스가 응집력을 잃는다면 쪼개라.

여기서 말하는 이 과정은 사실 가독성, 유지보수, 모듈화를 위한 작업이다.

이런 작업이 아니더라도 코드는 분명 동작하지만 이후 수정이나 변경에 있어서 폭탄해제와 같은 일을 경험해야 하기 때문이다.

즉, 클래스가 거대하고 응집력이 강하다면 큰 함수부터 작은 함수로 분리하고 그 함수에서 사용되는 인스턴스 변수가 독자적인 성격을 띈다면 작은 클래스로 다시 분활한다.

이 예제를 읽으면서 뭔가 다리부터 공략해서 천천히 정복하는 땅따먹기의 리팩터리 기법같다는 생각이 든다..

변경하기 쉬운 클래스

시스템은 개발과 이후에 과정까지 정말 많은 변경이 이루어진다.

개발자는 깨끗한 시스템을 짜서 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춰야 한다.

예제에서 보여주는 클래스의 형태와 같이 극도로 단순한 클래스는 변경하기 쉽다.(이해도 빠르고)

변경으로부터 격리

상세한 구현에 의존하는 클라이언트와 추상 클래스는 구현이 바뀌면 위험에 빠진다.

그래서 우리는 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리한다.

DI과정을 설명하는 것 같다.

인터페이스와 추상클래스를 활용하여 의존성을 주입하고 이를 테스트 코드로 미리 구현을 할 수 있게 흉내내는 즉, Fake객체를 만들어 테스트를 진행하는 것이다.

이후 의존성, 응집도를 고려한 실제 클래스를 설계하는 것

느낀점

클래스에 대한 내용을 설명하는 장이였는데 응집도를 낮추는 방법이 쉽게 정리된 것 같다.

이 장은 두고 다시 한번 씩 읽어보는게 좋을 것 같다.

논의사항

객체지향에 대한 이해도가 있어야 이해가 되는 부분들이 많았는데 특히 이해가 안되는 부분이 있었나요?

11장. 시스템

복잡성은 죽음이다.
개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다.

도시를 세운다면?

도시를 세운다면 온갖 세세한 사항을 혼자서 직접 관리할 수 있을까?

한 사람의 힘으로는 절대 불가능하며 그럼에도 도시는 문제없이 돌아간다.

이는 각 분야로 나눠진 전문기관이 존재하기 때문이다.

또 다른 이유로는 적절한 추상화와 모듈화 때문이다.

그래서 큰 그림을 이해하지 못할지라도 개인과 개인이 관리하는 구성요소는 효율적으로 동작한다.

소프트웨어도 마찬가지로 팀을 도시처럼 구성해야 한다.

하지만 팀이 제작하는 시스템은 비슷한 수준으로 관심사를 분리하거나 추상화를 이뤄내지 못한다.

깨끗한 코드를 구현하면 낮은 추상화 수준에서 관심사를 분리하기 쉬워진다.

시스템 제작과 시스템 사용을 분리하라

제작과 사용은 아주 다르다는 사실을 명심하라

소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 연결하는) 준비과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.

시작단계는 모든 애플리케이션이 풀어야 할 관심사이다.

불행히도 대다수 애플리케이션은 시작 단계라는 관심사를 분리하지 않는다.

  • 초기화 지연
    • 실제로 객체가 필요할 때 초기화하여 객체를 생성하는 기법

초기화 지연은 사실 테스트 코드에 적합하지 않으며 단일 책임 원칙을 위반한다.

초기화 지연이 사용되는 기능 자체에는 크게 문제없지만 이러한 작은 단위의 들이 큰 스노우 볼을 만드는 법이다.

프로젝트마다의 컨벤션이 있는 이유도 마찬가지로 쳬계적이고 탄탄한 시스템을 만들고 싶다면 흔히 쓰는 좀스럽고 손쉬운 기법으로 모둘성을 깨서는 절대로 안 된다.

간단하게 초기화 지연을 예로 들었지만 이 부분은 사실 앞서 다룬 오용될 수 있는 부분을 없애는 것과 같은 맥락이다.

관심사를 분리하는 것부터 시작해야 프로그램에서 원하는 도메인을 잘 설계할 수 있다.

Main 분리

시스템 생성과 시스템 사용을 분리하는 방법 중 하나는 main을 분리하는 것이다.

이러한 방법은 제어흐름을 따라가기 쉽고 모든 화살표가 한 방향을 가리키는 코드를 작성할 수 있게 해준다.

팩토리

객체의 생성이 런타임 도중에 결정되어야 하는 부분이 있다.

이 때 팩토리 메서드 패턴을 사용하면 생성하는 시점은 애플리케이션이 결정하지만 생성하는 코드는 전혀 모른다.

추상수준으로 연결되어 있기 때문에

의존성 주입(DI)

사용과 제작을 분리하는 강력한 메커니즘 중 하나는 의존성 주입이다.

의존성 주입은 제어 역전기법을 의존성 관리에 적용한 메커니즘으로 제어 역전에서는 한 개체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다.

새로운 객체는 넘겨받은 책임만 맡으므로 단일 책임 원칙을 지키게 된다.

의존성 관리 맥락에서 객체는 의존성 자체를 인스턴스로 만드는 책임은 지지 않는다.

대신에 이런 책임을 다른 전담 메커니즘에 넘겨야만 한다.

그렇게 함으로써 제어를 역전한다.

호출하는 객체는 실제로 반환되는 객체의 유형을 제어하지 않는다.

개신 호출하는 객체는 능동적으로 해결한다.

진정한 의존성 주입은 여기서 한 걸음 더 나간다.

클래스가 의존성을 해결하려 하지 않고 완전히 수동적인 형태를 가진다.

대신 의존성을 주입하는 방법으로 설정자 메서드를 통해 생성자 인수를 제공한다.

의존성 주입에 대한 심화내용이라 조금 어려울 수 있는 내용인 것 같다..

과거에 의존성 주입에 관해 정리한 내용을 첨부

의존성 주입이란..?

확장

군락은 마을로, 마을은 도시로 성장한다.

시스템도 마찬가지이다.

처음부터 모든 상황을 예측하고 완벽한 시스템을 만들 수는 없다..!

자동차를 만드는 그림이 있는데 처음에는 킥보드부터 시작해서 자전거, 오토바이, 자동차까지 확장되는 그림이다.

그렇다면 여기서 주는 교훈은 이런 변화를 유연하게 대체하기 위해선 생성과 사용을 분리하고, 컨벤션을 지키고, 수준을 낮고 작게 유지하는 것이 핵심이다.

이러한 개발 프로세스가 가장 돋보이는 것이 바로 TDD..

소프트웨어 시스템은 물리적인 시스템과 다르다.
관심사를 적절히 분리해 관리 한다면 소프트웨어 아키텍처는 점지적으로 발전할 수 있다.

소프트웨어 시스템은 수명이 짧다라는 본질로 인해 아키텍처의 점진적인 발전이 가능하다.

횡단 관심사

영속성과 같은 관심사는 애플리케이션의 자연스러운 객체 경계를 넘나드는 경향이 있다.

모든 객체가 전반적으로 동일한 방식을 이용하게 만들어야 한다.

원론적으로는 모듈화되고 캡슐화된 방식으로 영속성 방식을 구상할 수 있다.

하지만 현실적으로는 영속성 방식을 구현한 코드가 온갖 객체로 흩어진다.

여기에 횡단 관심사라는 키워드가 나온다.

도메인 논리도 모듈화할 수 있다.

문제는 이 두 영역이 세밀한 단위로 겹친다는 것이다.

영속성을 예로 들면, 프로그래머는 영속적으로 저장할 객체와 속성을 선언한 후 영속성 책임을 영속성 프레임 워크에 위임한다.

아래는 자바에서 사용하는 관점 호근 관점 과 유사한 메커니즘 세 개를 살펴본다.

  • 자바 프록시 : 객체를 감싸서 객체에 대한 접근을 제어한다.

내부 구현하는 방식을 보니.. C#에서는 리플렉션을 사용하여 좀 더 유연한 구조를 가져갈 수 있을 것 같다.

이후 등장하는 내용이 자바와 스프링에 관련된 세부사항 내용들이라 조금 아쉽다..

테스트 주도 시스템 아키텍처 구축

관점으로 관심사를 분리하는 방식은 그 위력이 막강하다.

즉, 코드 수준에서 아키텍처 관심사를 분리할 수 있다면 진정한 테스트 주도 아키텍처 구축이 가능해진다.

다시 말해, 아주 단순하면서 멋지게 우아하게 분리된 아키텍처로 소프트웨어 프로젝트를 진행해 결과물을 재빨리 출시한 후, 기반 구조를 추가하며 조금씩 확장해 나가는 괜찮다는 방식이다.

애자일 방식..

의사 결정을 최적화하라

모듈을 나누고 관심사를 분리하면 지엽적인 관리와 결정이 가능해진다.

도시든 소프트웨어 프로젝트든, 아주 큰 시스템에서 한 사람이 모든 결정을 내리기 어렵다.

가장 적합한 사람에게 책임을 맡기면 가장 좋다.

이 말이 몇몇 소프트웨어 개발자들이 공부하면서 인생의 많은 부분이 소프트웨어 개발론과 관련이 많다고 입을 모아 이야기 한다.

예술적, 기예적이라는 말도 많이 들어보고 문제를 해결하기 위한 최적의 방법을 계속 찾다보니 범용적으로 인간관계나 다른 문제를 해결할 때도 적용이 가능해진 듯 하다..

명백한 가치가 있을 때 표준을 현명하게 사용하라

표준을 사용하면 아이디어와 컴포넌트를 재사용하기 쉽고, 절절한 경험을 가진 사람을 구하기 쉬우며, 좋은 아이디어를 캡슐화하기 좋고, 컴포넌트를 엮기 쉽다.

하지만 이러한 표준을 만드는 시간이 너무 오래 걸려 기다리지 못한다.

결론

시스템은 역시 깨끗해야 한다.

깨끗하지 못한 아키텍처는 도메인 논리를 흐리며, 기민성을 떨어뜨린다.

도메인 논리가 흐려지면 제품 품질이 떨어진다.

버그가 숨어들기 쉬워지고, 스토리를 구현하기 어려워지는 탓이다.

모든 추상화 단계에서 의도는 명확히 표현해야 한다.

시스템을 설계하든 개별 모듈을 설계하든, 실제로 돌아가는 가장 단순한 수단을 사용해야 한다는 사실을 명심하자

느낀점

처음 구조를 잡는 방법에 있어서 사용과 생성을 분리하는 부분은 많이 도움이 된 것 같다.

중간에도 몇가지 생각을 적었지만 이 챕터에선 자바와 스프링에 대한 세부사항이 많아서 조금 아쉬웠다.

논의사항

저는 요즘 설계를 할 때 모든 걸 예측할 수 없음을 깨닫고 열려있는 작은 단위로 코드를 많이 짜려고 노력하는데 앞서 나온 테스트코드나 좀 더 유연하고 확실하게 가져가는 방법들을 보니 조금 더 고민해봐야겠다는 생각이 들었습니다..

이번 챕터에서 하나만 제대로 챙겨가는 개념이 있다면 뭐가 있을까요?

12. 창발성

창발또는 떠오름 현상은 하위 계층에는 없는 특성이나 행동이 상위 계층에서 자발적으로 돌연히 출현하는 현상이다.

착실하게 따르기만 하면 우수한 설계가 나오는 간단한 규칙이 있다..?!

  1. 모든 테스트를 실행한다.
  2. 중복을 없앤다.
  3. 프로그래머 의도를 표현한다.
  4. 클래스와 메서드 수를 최소로 줄인다.

중요도 순

단순한 설계 규칙 1: 모든 테스트를 실행하라

설계는 의도한 대로 돌아가는 시스템을 내놓아야 한다.

문서로는 시스템을 완벽하게 설계했지만, 시스템이 의도한 대로 돌아가는지 검증할 간단한 방법이 없다면, 문서 작성을 위해 투자한 노력에 대한 가치는 인정받기 어렵다.

여기서 말하는 시스템은 테스트를 철저히 거쳐 모든 테스트 케이스를 항상 통과하는 시스템이다.

이어 말해서 테스트가 불가능한 시스템은 검증 또한 불가능하다.

검증을 하지 못한다면 해당 시스템은 출시가 불가능한 제품이다.

따라서 개발자는 테스트가 가능한 시스템을 만드려고 하는데 이때 설계 품질또한 같이 올라간다.

단위 테스트에서 나온 관심사 분리

크기가 작고 목적 하나만 수행하는 클래스가 나온다.

SRP를 준수하는 클래스는 테스트가 휠씬 더 쉽다.

테스트 케이스가 많을수록 개발자는 테스트가 쉽게 코드를 작성한다.

따라서 철저한 테스트가 가능한 시스템을 만들면 더 나은 설계가 얻어진다.

테스트 케이스를 만들고 계속 돌려라라는 간단한 규칙은 시스템 자체의 낮은 결합도와 높은 응집력이라는 객체지향 방법론이 지향하는 목표를 달성한다.

즉, 테스트 케이스를 작성하면 설계 품질이 높아진다.

단순한 설계 규칙 2~4: 리팩터링

테스트 케이스를 모두 작성했다면, 이제 코드와 클래스를 정리할 차례이다.

구체적으로는 코드를 점진적으로 리팩터링 해나간다.

이때는 새로 추가하거나 변경하는 코드에 대해 설계에 대한 영향을 생각하며 가독성을 높이는 작업을 한다.

코드를 정리해가며 시스템이 깨질까 걱정할 필요가 전혀 없다. 테스트 케이스가 있으니까..!

응집도를 높이고, 결합도를 낮추고, 관심사를 분리하고, 시스템 관심사를 모듈로 나누고, 함수와 클래스 크기를 줄이고, 더 나은 이름을 선택하는 등 다양한 기법을 동원한다.

중복을 없애라

우수한 설계에서 중복은 커다란 적이다.

중복은 추가 작업, 추가 위험, 불필요한 복잡도를 뜻한다.

똑같은 코드는 물론이거니와 비슷한 코드도 중복이다.

중복을 없애는 과정에서도 마찬가지로 함수로 작성하게 되면서 하나의 목적 그리고 더 작게 만들게 된다.

그렇게 만든 클래스는 다시 보니 SRP를 위반하게 되고 더 작게 클래스로 분활되게 된다.

표현하라

자신이 이해하는 코드는 짜기 쉽다.

코드를 짜는 동안 문제에 몰입하여 코드를 작성하니..

하지만 나중에 코드를 유지보수할 사람이 코드를 짜는 사람만큼 문제를 이해할 가능성은 희박하다.

여기서 말하는 다른 사람은 자신도 포함

그러므로 코드는 개발자의 의도를 분명히 표현해야 한다.

개발자가 코드를 명백하게 짤수록 다른 사람이 그 코드를 이해하기 쉬워진다.

  • 좋은 이름을 선택하라
  • 함수와 클래스 크기를 가능한 줄인다.
  • 표준 명칭을 사용한다.
  • 단위 테스트 케이스를 꼼꼼히 작성한다.

프로그래머에게 있어서 코드를 몇 만줄짠건 사실 자랑이 아니다.

좋은 구조를 한번이라도 제대로 만들어본 경험이 더 비싸고 값진 것이다.

클래스와 메서드 수를 최소로 줄여라

중복을 제거하고, 의도를 표현하고, SRP를 준수한다는 기본적인 개념도 극단적으로 치달으면 득보다 실이 많아진다.

클래스와 메서드 크기를 줄이자고 조그만 클래스와 메서드를 수없이 만드는 경우도 많다.

그래서 이 규칙은 함수와 클래스 수를 가능한 줄이라고 말한다.

결론

경험을 대신할 단순한 개발 기법이 있을까?? 읍다..!

마찬가지로 책을 읽고 지식을 쌓는 것도 중요하지만 실제로 적용해보고 몸으로 느끼는 것이 제일 중요하다.

느낀점

앞서 나온 내용을 잘 정리해준 챕터인 것 같다.

클린코드에 대한 감이 슬슬 잡히는 것 같다.

논의사항

테스트 코드를 알고 있었지만 왜 짜지 않을까..? 이렇게 유용하고 중요한데..

라는 의문에서 생각을 해봤는데 저는 사실 그냥 돌아가는 프로그램만 생각하고 넘겼던 것 같습니다.(귀찮다고 생각한게 1순위)

여러분들은 왜 테스트코드를 짜지 않는지.. 아직 시도해보지 않았다면 왜 짜지 않는지 생각해보시면 좋을 것 같습니다.

13. 동시성

객체는 처리의 추상화다. 스레드는 일정의 추상화다

동시성과 깔끔한 코드는 양립하기 어렵다.(매우)

스레드를 하나만 실행하는 코드는 짜기 쉽다..

동시성이 필요한 이유

동시성은 커플링을 없애는 전략이다.

즉, 무엇과 언제를 분리하는 전략이다.

스레드가 하나인 프로그램은 무엇과 언제가 서로 밀접하다.

그래서 호출 스택을 살펴보면 프로그램의 상태가 곧바로 드러난다.

흔히 단일 스레드 프로그램을 디버깅하는 프로그래머는 브레이크 포인트를 정한 후 어느 지점이 문제인지 파악하며 시스템의 상태를 본다.

무엇언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다.

구조적인 관점에서 프로그램은 거대한 루프 하나가 아니라 작은 협력 프로그램 여럿으로 볼 수 있다.

따라서 시스템을 이해하기가 쉽고 문제를 분리하기도 쉽다.

미신과 오해

책에서 나오는 동시성이 무조건적으로 필요한 상황이 존재한다.

하지만 동시성은 어렵다.(따라서 몇가지 주의사항이 있다.)

미신 몇 가지

  • 동시성은 항상 성능을 높여준다. (아니다. 때로 성능을 높여준다.)
  • 동시성을 구현해도 설계는 변하지 않는다. (아니다. 단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다.)
  • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다. (아니다. 컨테이너가 동시성을 처리해주긴 하지만, 그렇다고 동시성을 이해할 필요가 없는 것은 아니다.)

반대로 타당한 생각 몇 가지

  • 동시성은 다소 부하를 유발한다. (성능 측면에서 부하가 걸리며, 코드도 더 짜야한다.)
  • 동시성은 복잡하다 (간단한 문제라도 동시성은 복잡하다)
  • 일반적으로 동시성 버그는 재현하기 어렵다. (그래서 진짜 결함으로 간주되지 않고 일회성 문제로 여겨 무시하기 쉽다.)
  • 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

난관

한개의 인스턴스를 두 스레드가 공유하여 값에 접근하면 문제가 되는 예제는 유명한 예제이다.

이런 조회가 아닌 값을 건드는 경우에은 C#lock 키워드를 사용하면 된다.

그렇다고 문제가 쉽게 해결되는 것은 아니다..

동시성 방어 원칙

동시성 코드가 일으키는 문제로부터 시스템을 방어하는 원칙과 기술들이 있다.

단일 책임 원칙

앞에서 다룬 SRP는 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 함을 말한다.

동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분하다.

즉, 동시성 관련 코드는 다른 코드와 분리해야 한다는 뜻이다.

결국 궁극적으로 모든 면에서 이득인 부분은 좋은 코드(클린 코드)를 작성하는 것이다.

동시성을 구현할 때 고려사항

  • 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.
  • 동시성 코드에는 독자적인 난관이 있다.
  • 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다.

즉, 동시성 코드는 다른 코드와 분리하라

따름 정리: 자료 범위를 제한하라

공유 객체를 사용하는 코드 내 임계영역 키워드로 보호하라고 권장한다.

앞서 언급한 SRP를 지키면 공유된, 흩어져 있는 임계영역 또한 줄어든다.

즉, 자료를 캡슐화하라, 공유 자료를 최대한 줄여라

따름 정리: 자료 사본을 사용하라

공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다.

어떤 경우에는 객체를 복사해 읽기 전용으로 사용하는 방법이 가능하다.

어떤 경우에는 각 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본에서 결과를 가져오는 방법도 가능하다.

객체는 불변이어야 한다.

따름 정리: 스레드는 가능한 독립적으로 구현하라

자신만의 세상에 존재하는 스레드를 구현한다.

즉, 다른 스레드와 자료를 공유하지 않는다.

각 스레드는 클라이언트 요청 하나를 처리한다.

모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장한다.

그러면 각 스레드는 세상에 자신만 있는 듯이 돌아갈 수 있다.

다른 스레드를 동기화할 필요가 없으므로

즉, 독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라

라이브러리를 이해하라

C#은 async/await 키워드를 5.0부터 지원한다.

게임에서 동시성은 클라이언트 단위에서는 서버에 데이터를 받아오는 작업, 작은 단위의 게임에선 씬 로드 정도로 처리된다고 보면 된다.

생산자-소비자

하나 이상 생산자 스레드가 정보를 생성해 버퍼나 대기열에 넣는다.

하나 이상 소비자 스레드가 대기열에서 정보를 가져와 사용한다.

생산자 스레드와 소비자 스레드가 사용하는 대기열은 한정된 자원이다..!

생산자 스레드는 대기열에 빈 공간이 있어야 정보를 채운다.

즉, 빈 공간이 생길 때 까지 기다리는 다는 말이다.

서로에게 시그널을 보내 소통하며 잘못하면 둘다 진행이 가능하지만 서로의 시그널을 기다릴 가능성이 존재한다.

읽기-쓰기

읽기 스레드를 위한 주된 정보원으로 공유 자원을 사용하지만, 쓰기 스레드가 이 공유 자원을 이따금 갱신한다고 하자.

이런 경우 처리율의 문제의 핵심이다.

처리율을 강조하면 기아 현상이 생기거나 오래된 정보가 쌓인다.

갱신을 허용하면 처리율에 영향을 미친다.

따라서 복잡한 균형을 잡기가 필요하다.

읽기 스레드의 요구, 쓰기 스레드의 요구를 적절히 만족시켜 처리율도 적당히 높이고 기아도 방지하는 해법이 필요하다.

간단한 전략은 읽기 스레드가 없을 때까지 갱신을 원하는 쓰기 스레드가 버퍼를 기다리는 방법이다.

따라서 정답은 없고 필요에 따라 적절히 선택해야 한다.

식사하는 철학자들

42서울에서 본 내용과 같은 문제로,, 스레드에 관한 자원할당에 예로 가장 적절한 것 같다.

철학자를 스레드로 포크를 자원으로

동기화하는 메서드 사이에 존재하는 의존성을 이해하라

동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다.

즉, 공유 객체 하나에는 메서드 하나만 사용하라

사실 이 모든 내용들이 앞서 다룬 이름, 클래스, 함수, 모듈에 대한 내용과 연결이 되는 내용이다.

동기화하는 부분을 작게 만들어라

락은 스레드를 지연시키고 부화를 가중한다.

따라서 여기저기서 lock을 남발하는 코드는 바람직하지 않다.

임계영역은 반드시 보호해야 하며 코드를 짤 때는 보호의 명목으로 임계영역을 줄여야 한다.

줄이라고 거대한 임계영역을 만들라는 것이 아니다. 입구와 출구를 하나로 모듈성을 의미

즉, 동기화하는 부분을 최대한 작게 만들어라

올바른 종료 코드는 구현하기 어렵다

영구적으로 돌아가는 시스템을 구현하는 방법과 잠시 둘다 깔끔하게 종료하는 시스템을 구현하는 방법은 다르다..!

깔끔하게 종료하는 코드는 올바르게 구현하기 어렵다.

가장 흔히 발생하는 문제가 데드락이다. 즉, 스레드가 절대 오지 않을 시그널을 기다린다.

따라서 종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라

생각보다 어렵고 오래 걸린다. 이미 나온 알고리즘을 검토하라

스레드 코드 테스트하기

코드가 올바르다고 증명하기는 현실적으로 불가능하다.

테스트가 정확성을 보장하지 않는다.

그럼에도 테스트는 위험을 낮춘다.

스레드가 하나인 프로그램은 지금까지 한 말이 모두 옳다.

그런데 같은 코드를 여러 스레드에서 동시에 실행하면 테스트가 더 어려워진다.

따라서 문제를 노출하는 테스트 케이스를 작성하라.

결론

다중 스레드 코드는 올바르게 구현하기 어렵다.

간단했던 코드가 여러 스레드와 공유 자료를 추가하면서 악몽으로 변한다.

다중 스레드 코드를 작성한다면 각별히 깨끗하게 코드를 짜야 한다.

주의하지 않으면 희귀하고 오묘한 오류에 직면하게 된다.

느낀점

비동기 코드를 작성해보면서 이 챕터를 읽으니 테스트 하기 얼마나 어려울까..? 라는 생각을 했다..

아직 제대로 된 테스트코드도 못 짜봤는데 과연 어떻게 테스트를 해야할지 감이 안온다.

중간 중간 말했지만 대부분의 내용이 사실 앞선 챕터의 명시적인 규칙들만 잘 지켜도 해결되는 문제들이다.

논의사항

동시성 코드를 작성하다 발생한 버그들의 사례를 듣고 싶습니다..!

14. 점진적인 개선

이 장은 개념보다 실제로 리팩터링 되는 과정과 그 과정에서 지켜야 하는 규칙들을 보여준다.

앞서 나온 챕터들의 총 집합

전부 코드적인 내용이라 나중에 다시 읽을 때 도움 될만한 내용만 간추려서 정리한다.

인터페이스로 구분

상속대신 인터페이스로 연결부만 정의하고 실제 구현은 상속받은 클래스가 담당한다.

DI를 통해 인터페이스를 구현한 클래스를 주입받아 사용한다.

처음부터 완벽한 클래스는 없다

가장 먼저 보여주는 Args클래스는 정말 잘 읽히고 이해가 되는 걸 봐서 매우 좋은, 클린 코드이다.

하지만 저자 또한, 처음부터 이렇게 완벽하게 짜지 않았다고 밝힌다.

이러한 숙련공 또한 도메인에 맞춰서 동작하게 만든 뒤 여러번의 리팩터링을 통해 좋은 코드를 생산한다는 점을 잊지말 것

“프로그래밍은 과학, 수학보다 공예에 가깝다. (매우 많은 능력을 요구하는 예술적인 분야라 생각된다.)”

자동차를 만드는 예제 참고

시스템 리팩터링

한 가지 시스템을 개발한다고 할 때 코딩을 하다보면 사실 알게될 때가 많다.

“여기서 이렇게 하게 되면 슬슬 문제가 생길 것 같은데..? 구조를 변경해야 하나.. 급하니까 일단 그냥 두자..”

여기서 가장 큰 문제가 발생하고 여기서 아낀 시간은 뒤에서 곱으로 돌아온다.

시스템의 덩치만 커지고 전혀 유지보수가 되지 않는다고 판단될 때 리팩터링을 해야하는 시기이다.

“지나치지 말것”

저자는 TDD 개발방법을 사용해서 리팩터링을 진행하였다.

TDD의 장점

그냥 리팩터링 과정을 보니까 왜 유리한지를 보여준다.

단계적인 수정이 가능해지면서 앞서 다룬 단위 테스트의 모든 장점들이 드러난다.

이 부분은 단위 테스트 책을 읽고 다시 볼 것

결론

그저 돌아가는 코드만으로는 부족하다.

돌아가는 코드가 심하게 망가지는 사례는 흔하다.

단순히 돌아가는 코드에 만족하는 프로그래머는 전문가 정신이 부족하다.

동의.. 프로그래머를 1~2년하고 그만둘게 아니라면,

설계와 구조를 개선할 시간이 없다고 변명할지 모르지만 동의하기 어렵다.

많은 사례와 예제를 통해 급하게 작성한 나쁜 코드가 이후에 프로젝트에 미치는 영향도 많은 사람들은 알고 있지만 항상 같은 실수를 반복한다.

느낀점

좋은 개발자가 되기 위해서 읽는 필독서라는 강한 인상이 있는 책인데 책의 마지막에 가까워지면서 저자가 말하는 내용이 더욱 공감이 갔다.

프로그래머의 길 멘토에게 묻다.라는 책을 읽고 정말 많은 생각을 했는데.. 그 뒤로 이런 책이나 개발에 대한 생각 자체가 많이 뒤바뀐 느낌이다.

논의사항

예졔중에서 가장 인상에 남는 부분은 어디인가요?

15. JUnit 들여다보기

JUnit은 자바 프레임워크 중에서 가장 유명하다.

개념은 단순하며 정의는 밀접 그리고 구현은 우아하다.

그 중에서 살펴볼 모듈은 문자열을 비교 오류를 파악할 때 유용한 코드이다.

ComparisonCompactor.java

유닛 테스트에서 생성자 그리고 메서드 체이닝을 통해 객체를 생성하고 해당 객체를 테스트한다.

실제 모듈또한 약 200줄 정도 되는 것 같다.

책에서는 보이스카우트 규칙에 따라서 리팩토링을 진행한다.

처음 왔던 것 보다 깨끗하게~

  • 멤버 변수 접두어 제거
    • fContextLength -> contextLength
  • 조건문 캡슐화
    • 조건문을 메서드로 추출한다.
  • 부정문을 긍정문으로 변경
    • if(!a.equals(b)) -> if(a.equals(b))
  • 메서드 추출
    • compact() 메서드를 추출한다.
  • 숨겨진 시간적인 결합을 제거
  • 연산자 정리
  • 이름 명확하게 변경

위 과정을 거쳐서 코드 모듈을 변경하는데 코드가 상당히 깔끔하다.

모듈은 일련의 분석 함수와 일련의 조합 함수로 나뉜다.

전체 함수는 위상적으로 정렬했으므로 각 함수가 사용된 직후에 정의된다.

분석 함수가 나오고 조합 함수가 그 뒤를 이어서 나온다.

자세하게 위 과정을 다시 되짚어 봐도 만들어진 모듈의 기능성을 수정하거나 추가한 것이 아니다.

클린코드를 만들기 위해 리팩터링만 했을 뿐..

시작부터 로직을 잘 짜거나 아키텍처를 만들고 싶다면 직접 경험하거나 알고리즘을 푸는게 더 도움될 것

이 책은 전제부터 클린코드이기 때문에

느낀점

설명 했지만 클린코드의 본질을 보여준다.

책에서 강조하는 클린 코드를 짰을 때의 이점이 미래의 나에게 도움이 되기에 왜 필요한지 이해하는 과정이 앞 과정이고 뒷 부분은 실습적인 성격이 강한 것 같다.

스스로 위 과정이나 책에서 강조한 부분을 생각해서 자신의 코드를 리팩터링 해보는 것도 좋을 것 같다.

매우매우 좋은 경험일 것

논의사항

사용해보신 프레임워크의 모듈을 열어서 보신적이 있나요?

유니티에서 패키지를 만들어보고 싶어서 분석하면서 여러 라이브러리를 열어봤는데

잘 만들어진 패키지는 내부에 꼭 테스트 케이스로 구분이 되어 있더라구요,,

16. SerialDate 리팩터링

앞서 모듈을 리팩터링한 것과 비슷하게 오픈소스 라이브러리를 리팩터링하는 과정이다.

내가 모든 프로젝트나 개인 활동을 Public으로 관리하는 이유와 같은 맥락으로 내가 짠 코드를 누군가 보고 피드백을 준다면 엄청난 행운이라고 생각한다.

물론 몇몇 사람들은 코드를 열어보고 그냥 지나치겠지만 만약 인사이트를 얻거나 질문하는 경우 또한 나에게 이점이 크다는 것이다.

더 나아질 수 있는 기회를 private으로 두지 말고 열어두는 것이 현명하다.

책에서 나온 모든 과정을 따라할 수 없기도 하고 실제로 이 리팩터링 과정을 전부 따라가기엔 무리가 있다고 생각이 든다.

따라서 책을 다 읽고 나면 앞 장에서 말한 것과 같이 다른 라이브러리를 리팩터링하면서 공부해볼 생각이다.

책에서 읽고 실천을 해야 뭔가 바뀔 수 있다고 생각하기에..

고민 중인 라이브러리는 다음과 같다.

고도 게임엔진인데 제대로된 오픈소스라 하루 하루 달라지는 것을 볼 수 있다.

나도 들어가서 코드를 분석해보고 풀리퀘를 올려서 기여해볼 생각..!

따라서 앞장의 순서와 이번 장의 리팩터링 순서에 집중하자

첫째, 둘러보자

먼저 테스트 케이스를 찾아서 리팩터링을 진행해야 한다.

앞서 단위테스트 챕터에서 말했지만 당연하게 테스크케이스를 벗어나는 리팩터링은 말이 안된다.

기능자체를 바꿔버리기에 테스트 케이스를 찾아 올바르게 클린코드로 변경할 수 있다면 실천해라

저자는 해당 라이브러리 자체를 보고 있기에 커버러지 분석 도구까지 사용하여 전략적으로 접근한다.

따라서 테스트 케이스를 분석하면 이 코드를 왜 만들었는지 알 수 있다. (중요!)

둘째, 고쳐보자

테스트 케이스를 통해 분석이 끝났다면 이제 고치는 단계이다.

이 단계에서는 리팩터링을 진행하면서 테스트 케이스를 통과시키는 것이 목표이다.

리팩터링 과정은 이 책에서 나온 모든 지식을 활용하여 진행한다.

저자의 과정을 보면 역시 이름에 집중하여 올바른 수준의 이름인지 생각하며 계속 수정해 나간다.

서술적인 표현, 추상화 수준을 위해 팩토리, 오용될 수 있는 변수 변경 등등..

느낀점

내가 과연 오픈소스 라이브러리의 코드를 분석하고 리팩터링한다고 도움이 될까?

라는 생각을 해봤는데 도움이 될 것 같다.

물론 일부분이지만 엔진에 대한 이해나 코드의 리팩터링 과정을 이해하는데 도움이 될 것 같다.

논의사항

논의 사항을 적을게 없어서.. 이 장을 읽고 정말 고민인게 단위 테스트 관련 책을 읽을 것인지 리팩터링 2판을 읽을 것인지… 고민이 됩니다

생각하시는 우선순위가 있을까요?

17. 냄새와 휴리스틱

이번 장은 마틴 파울러의 책 Refactoring에서 코드 냄새의 연장선이다.

코드를 짜면서 나오는 기교와 휴리스틱을 정리한 장이다.

왜라고 자문하는 것에 집중해야 하는데 우리는 스스로 회고, 생각하지 않는다.

요즘에 든 생각, 멘토링 받으면서 배운 내용이지만 진짜 깨달음은 회고하는 과정에서 온다고 한다.

회고를 하지 않으면서 더 나은 방법을 찾을 수 없다.

방향을 못 잡겠다면 일단 해보고 나중에 되돌아보자

여기서 말하는 경험을 해보고 다시 회고하고 나아가는 것을 휴리스틱이라고 한다.

물론 그냥 돌파하는 것 보다 좋은 길이 있을 수 있지만 우리는 살면서 이 휴리스틱능력을 키워야 한다.

애자일에서도 많이 나오는 내용이라고 하는데 또 읽어야 하는게 늘어난 기분..!

주석

C1: 부적절한 정보

다른 시스템에 저장할 정보는 주석으로 적절하지 못하다.

(소스코드 관리 프로그램, 버그 추적 시스템, 이슈 추적 시스템 등등)

C2: 쓸모없는 주석

오래된 주석, 엉뚱한 주석, 잘못된 주석은 더 이상 쓸모가 없다.

주석은 빨리 낡는다.

쓸모 없어질 주석은 아예 달지 않는 편이 가장 좋다.

C3: 중복된 주석

코드만으로 충분한데 구구절절 설명하는 주석은 중복이다.

C4: 성의 없는 주석

작성할 가치가 있는 주석은 잘 작성할 가치도 있다.

만약 주석을 달아야 하는 상황이라면 최대한 생각한 후 작성한다.

C5: 주석 처리된 코드

코드를 읽다가 주석으로 처리된 코드가 줄줄이 나오면 신경이 아주 거슬린다.

얼마나 오래된 코드인지, 중요한 코드인지 아닌지, 알 길이 없다..!

삭제하지 않는 이유는 누군가 필요로 하거나 사용할 코드라고 생각하여 매일 낡아간다.

주석으로 된 코드는 지워버려라 그 코드는 시스템이 기억한다.

환경

E1: 여러 단계로 빌드해야 한다

빌드는 간단히 한 단계로 끝나야 한다.

소스 코드 관리 시스템에서 이것저건 따로따로 체크아웃할 필요가 없어야 한다.

자동화가 필요,, 데브옵스

E2: 여러 단계로 테스트해야 한다

모든 단위 테스트는 한 명령으로 돌려야 한다.

IDE에서 버튼 하나로 모든 테스트를 돌린다면 가장 이상적이다.

함수

F1: 너무 많은 인수

함수에서 인수 개수는 적을수록 좋다.

아예 없으면 가장 좋다..!

함수 챕터 참고

F2: 출력 인수

출력 인수는 직관을 정면으로 위배한다.

출력이 아닌 입력으로 간주한다.

함수에서 뭔가 상태를 변경해야 한다면 함수가 속한 객체의 상태를 변경한다.

F3: 플래그 인수

boolean 인수는 추하다..! (기억이 난다.)

F4: 죽은 함수

아무도 호출하지 않는 함수는 삭제한다.

일반

G1: 한 소스 파일에 여러 언어를 사용한다

현실에서 여러 언어의 사용은 불가피 하지만 한 소스 파일에선 하나의 언어만 사용하는 것이 좋다.

G2: 당연한 동작을 구현하지 않는다

최소 놀람의 원칙에 의거해 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다.

G3: 경계를 올바로 처리하지 않는다

코드는 올바로 동작해야 한다.

너무나도 당연한 말이다.

흔히 개발자들은 머릿속에서 코드를 돌려보고 끝낸다.

자신의 직관에 의존할 뿐 모든 경계와 구석진 곳에서 코드를 증명하려 애쓰지 않는다.

부지런함을 대신할 지름길은 없다.

스스로 직관에 의존하지 말고 모든 경계 조건을 찾아내고 테스트하자.

G4: 안전 절차 무시

안전 절차를 수헹하기 번거롭다는 단순한 이유로 대 참사가 나는 경우가 있다.

비슷한 예제로 책에서 읽은 체크리스트를 활용한 예가 있다.

릴리즈전, 테스트전 활용하는 메뉴얼로 사용된다.

G5: 중복

이 책에서 나오는 가장 중요한 규칙 중 하나로 유명한 경지의 사람들이 입을 모아서 말하는 원칙이다.

코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라

추상화로 중복을 정리하면 설계언어의 어휘가 늘어난다.

다른 프로그래머들이 그만큼 어휘를 사용하기 쉬워진다.

G6: 추상화 수준이 올바르지 못하다

추상화는 저차원 상세 개념에서 고차원 일반 개념으로 분리한다.

때로 우리는 추상 클래스와 파생 클래스를 생성해 추상화를 수행한다.

추상화로 개념을 분리할 때는 철저해야 한다.

모든 저차원의 개념은 파생 클래스로 고차원은 기초 클래스에 둔다.

G7: 기초 클래스가 파생 클래스에 의존한다

개념을 기초 클래스와 파생 클래스로 나누는 가장 흔한 이유는 고차원 기초 클래스 개념을 저차원 파생 클래스 개념으로부터 분리해 독립성을 보장하기 위해서다.

그러므로 기초 클래스가 파생 클래스를 사용한다면 뭔가 문제가 있다는 말이다.

일반적으로 기초 클래스는 파생 클래스를 전혀 몰라야 마땅하다.

G8: 과도한 정보

잘 정의된 모듈은 인터페이스가 아주 작다.

하지만 작은 인터페이스로도 많은 동작이 가능하다.

부실하게 정의된 모듈은 인터페이스가 구질구질하다.

그래서 간단한 동작 하나에도 온갖 인터페이스가 필요하다.

잘 정의된 인터페이스는 많은 함수를 제공하지 않는다.

그래서 결합도가 낮다.

G9: 죽은 코드

죽은 코드란 실행되지 않는 코드를 가리킨다.

불가능한 조건을 확인하는 if문과 throw문이 없는 try문에서 catch블록이 좋은 예이다.

아무도 호출하지 않는 유틸리티 함수와 switch/case 문에서 불가능한 case 조건도 또 다른 좋은 예이다.

따라서 죽은 코드는 시스템에서 제거한다.

G10: 수직 분리

변수와 함수는 사용되는 위치에 가깝게 정의한다.

지역 변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 위치해야 한다.

선언한 위치로부터 몇백 줄 아래에서 사용하면 안된다.

비공개 함수는 처음으로 호출한 직후에 정의한다.

G11: 일관성 부족

어떤 개념을 특정 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현한다.

중복과는 다른 문제이다.

G12: 잡동사니

비어 있는 기본 생성자가 왜 필요한가?

사용하지 않는 변수, 호출하지 않는 함수, 정보를 제공하지 못하는 주석 등이 모든 예이다.

쓸데없는 코드는 복잡하기만하다.

G13: 인위적 결합

서로 무관한 개념을 인위적으로 결합하지 않는다.

일반적인 enum은 특정 클래스에 속할 이유가 없다.

enum이 클래스에 속한다면 enum을 사용하는 코드가 특정 클래스를 알아야만 한다.

범용 static 함수도 마찬가지로 특정 클래스에 속할 이유가 없다.

G14: 기능 욕심

클래스 메서드는 자기 클래스의 변수와 함수에 관심을 가져야지 다른 클래스의 변수와 함수에 관심을 가져서는 안된다.

메서드가 다른 객체의 참조자와 변경자를 사용해 그 객체 내용을 조작한다면 메서드가 그 객체 클래스의 범위를 욕심내는 탓이다.

G15: 선택자 인수

함수 호출 끝에 달리는 false 인수만큼이나 끔찍한 인수는 없다.

예제와 같이 함수 호출을 WeeklyPay(false)와 같은 코드는 뭘 의미하는지 직관적이지 못하다.

G16: 모호한 의도

코드를 짤 때는 의도를 최대한 분명히 밝힌다.

행을 바꾸지 않고 표현한 수식, 헝가리식 표기법, 매직 번호 등은 모두 저자의 의도를 흐린다.

G17: 잘못 지운 책임

소프트웨어 개발자가 내리는 가장 중요한 결정 중 하나가 코드를 배치하는 위치이다.

G18: 부적절한 static 함수

순수 static 함수는 좋은 static 함수다.

Mathf.Max()

오히려 a.Max()와 같은 형태는 이상하다.

결정적으로 Max 메서드를 재 정의할 가능성은 전혀 없다.

그런데 간혹 우리는 static으로 정의하면 안되는 함수를 static으로 정의한다.

G19: 서술적 변수

프로그램 가독성을 높이는 가장 효과적인 방법 중 하나가 계산을 여러 단계로 나누고 서술적인 변수 이름을 사용하는 것이다.

서술적인 이름이라 조금 길어지더라도 사용하는게 좋다.

G20: 이름과 기능이 일치하는 함수

Data newData = data.add(5);

위 코드는 5일을 더하는 함수인지, 5주인지 5시간인지 알 수 없다.

data 인스턴스를 변경하는 함수인지, date는 두고 새로 반환하는 함수인지 알 수 없다.

addDayTo()와 같은 함수는 이름과 기능이 일치한다. (increaseByDays() 가능)

G21: 알고리즘을 이해하라

대다수 괴상한 코드는 사람들이 알고리즘을 충분히 이해하지 않은 채 코드를 구현한 탓이다.

잠시 멈추고 실제 알고리즘을 고민하는 대신 여기저기 if문과 플래그를 넣어보며 돌리는 탓이다.

G22: 논리적 의존성은 물리적으로 드러내라

한 모듈이 다른 모듈에 의존한다면 물리적인 의존성도 있어야 한다.

논리적인 의존성만으로는 부족하다.

의존하는 모든 정보를 명시적으로 요청하는 편이 좋다.

G23: if/else 혹은 switch/case 문보다 다형성을 사용하라

대다수의 개발자가 switch/case 문을 사용하는 이유는 그 상황에서 가장 올바른 선택이기보다 당장 손쉬운 선택이기 때문이다.

그러므로 다형성을 고려하라는 것이다.

또한 유형보다 함수가 더 쉽게 변하는 경우는 극히 드물다.

그러므로 모든 switch/case 문을 의심해야 한다.

G24: 표준 표기법을 따르라

팀은 업계 표준에 기반한 구현 표준을 따라야 한다.

컨벤션이라고 하는데 인스턴스 변수 명명 규칙, 위치, 괄호 위치 등등 다양하다.

이는 협업의 가장 베이스가 되는 장치이다.

G25: 매직 숫자는 명명된 상수로 교체하라

리터럴값을 사용하지 말 것

G26: 정확하라

검색 결과 중 첫 번째 결과만 유일한 결과로 간주하는 행동은 순진하다.

부동 소수점으로 통화를 표시하는 것은 거의 범죄에 가깝다.

갱신할 가능성이 희박하다고 잠금과 트랜잭션 관리를 건너뛰는 행동은 아무리 잘 봐줘도 게으름이다.

모든 변수를 protected로 선언하는 것은 무절제하다.

결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다.

G27: 관례보다 구조를 사용하라

설계 결정을 강제할 때는 규칙보다 관례를 사용한다.

명명 관례도 좋지만 구조자체도 강제하면 더 좋다.

예를 들어, enum 변수가 멋진 switch/case 문보다 추상 메서드가 있는 기초 클래스가 더 좋다.

G28: 조건을 캡슐화하라

부울 논리는 이해하기 어렵다.

조건의 의도를 분명히 밝히는 함수로 표현하라

G29: 부정 조건은 피하라

부정 조건은 긍정 조건보다 이해하기 어렵다.

G30: 함수는 한 가지만 해야 한다

함수를 짜다보면 한 함수안에 여러 단락을 이어, 일련의 작업을 수행하고픈 유혹에 빠진다.

이런 함수는 한 가지만 수행하는 함수가 아니다.

한 가지만 수행하는 좀 더 작은 함수 여럿으로 나눠야 마땅하다.

G31: 숨겨진 시간적인 결합

때로는 시간적인 결합이 필요하다.

하지만 시간적인 결합은 숨겨서는 안 된다.

함수를 짤 때는 함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백하게 드러낸다.

G32: 일관성을 유지하라

코드 구조를 잡을 때는 이유를 고민하라.

그리고 그 이유를 코드 구조로 명백히 표현하라.

구조에 일관성이 없어 보인다면 남들이 마음대로 바꿔도 괜찮다고 생각한다.

시스템 전반에 걸쳐 구조가 일관성이 있다면 남들도 일관성을 따르고 보존한다.

G33: 경계 조건을 캡슐화하라

경계 조건은 빼먹거나 놓치기 십상이다.

경계 조건은 한 곳에서 별도로 처리한다.

코드 여기저기에서 처리하지 않는다.

다시 말해 코드 여기저기에 +1이나 -1을 흩어놓지 않는다.

G34: 함수는 추상화 수준을 한 단계만 내려가야 한다

함수 내 모든 문장은 추상하 수준이 동일해야 한다.

그리고 그 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다.

G35: 설정 정보는 최상위에 둬라

추상화 최상위 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안된다.

대신 고차원 함수에서 저차원 함수를 호출할 때 인수로 넘긴다.

G36: 추이적 탐색을 피하라

일반적으로 한 모듈은 모를수록 좋다.

좀 더 구체적으로, A가 B를 사용하고 B가 C를 사용한다면 A가 C를 알아야 할 필요는 없다는 뜻이다.

디미터의 법칙.. 좋은 코드 나쁜코드에서 봤던 내용이다.

간단한 이야기로 디미터의 법칙 *(기차 충돌)의 문제와 자바의 메서드 체이닝에 관한 논의도 있었다.

자바

자바는 내가 사용하는 언어는 아니지만 기술의 폭 그리고 객체지향언어의 표준이기 때문에 마저 정리한다.

그리고 C#과 매우매우 비슷

J1: 긴 import 목록을 피하고 와일드카드를 사용하라

패키지에서 클래스를 둘 이상 사용한다면 와일드카드를 사용해 패키지 전체를 가져오라

C#에서는 와일드 카드를 써야할 만큼의 세분화가 되어 있지 않아서 애초에 불러올 때 와일드 카드 범위인 것 같다.

자바에선 패키지 C#에선 네임스페이스의 개념이라고 생각하면 될 것 같다.

J2: 상수는 상속하지 않는다

프로그래머가 상수를 인터페이스에 넣은 다음 그 인터페이스를 상속해 해당 상수를 사용한다.

이 방식은 사용하는 사람이 그 의도와 위치를 확인하기 위해 다시 거슬러 올라가야하는 작업이 수행된다.

상속은 이렇게 사용하는 것이 아니다.

언어의 범위 규격을 속이는 행위이다.

J3: 상수 VS Enum

Enum 최고..!

이름

N1: 서술적인 이름을 사용하라

반복하는 이야기지만 서술적인 이름을 사용..!

모른다면 검색하면서 영어공부는 덤,,

대부분 알고리즘 문제풀이의 내용을 보면 거의 알파벳나열 수준의 코딩이다.

하지만 서술적인 이름을 사용하면 가독성은 올라간다.

정말 신문이라고 생각하면 조금 편하다..?

N2: 적절한 추상화 수준에서 이름을 선택하라

구현을 드러내는 이름은 피하라

작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하라

N3: 가능하다면 표준 명명법을 사용하라

기존 명명법을 사용하는 이름은 이해하기 더 쉽다.

예를 들어 Decorator 패턴을 사용한다면 Decorator를 붙여서 사용한다.

해당 언어의 표준 관례(오버라이딩하는 메서드 이름 등), 프로젝트 용어등을 적절하게 사용하라

N4: 명확한 이름

함수나 변수의 목적을 명확히 밝히는 이름을 선택한다.

N5: 긴 범위는 긴 이름을 사용하라

이름 길이는 범위 길이에 비례해야 한다.

범위가 아주 작으면 아주 짧은 이름을 사용해도 괜찮다.

하지만 범위가 길어지면 긴 이름을 사용한다.

범위가 5줄 안밖이라면 i나 j와 같은 이름을 사용해도 괜찮다.

(STM을 초과하지 않기 때문에)

N6: 인코딩을 피하라

이름에 유형 정보나 범위 정보를 넣어서는 안 된다.

접두어 X

N7: 이름으로 부수 효과를 설명하라

함수, 변수, 클래스가 하는 일을 모두 기술하는 이름을 사용한다.

이름에 부수효과를 숨기지 않는다.

오용될 여지가 있기 때문에 사용자에게 명확하게 전달한다.

테스트

T1: 불충분한 테스트

테스트 케이스는 잠재적으로 깨질 만한 부분을 모두 테스트해야 한다.

테스트 케이스가 확인하지 않는 조건이나 검증하지 않는 계산이 있다면 그 테스트는 불완전하다.

T2: 커버리지 도구를 사용하라

커버러지 도구는 테스트가 빠뜨리는 공백을 알려준다.

커버러지 도구를 사용하면 테스트가 불충분한 모듈, 클래스, 함수를 찾기가 쉬워진다.

T3: 사소한 테스트를 건너뛰지 마라

사소한 테스트는 짜기 쉽다.

사소한 테스트가 제공하는 문서적 가치는 구현에 드는 비용을 넘어선다.

T4: 무시한 테스트는 모호함을 뜻한다

때로는 요구사항이 불분명하기에 프로그램이 돌아가는 방식을 확신하기 어렵다.

불분명한 요구사항은 테스트 케이스를 주석으로 처리하거나 테스트 케이스에 Ignore를 붙이는 방식으로 처리한다.

T5: 경계 조건을 테스트하라

경계 조건은 각별히 신경 써서 테스트한다.

알고리즘 중앙 조건은 올바로 짜놓고 경계 조건에서 실수하는 경우가 흔하다.

T6: 버그 주변은 철저히 테스트하라

버그는 서로 모이는 경향이 있다.

한 함수에서 버그를 발견했다면 그 함수를 철저히 테스트하는 편이 좋다.

T7: 실패 패턴을 살펴라

때로는 테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다.

테스트 케이스를 최대한 꼼꼼하게 짜라는 이유도 여기에 있다.

합리적인 순서로 정렬된 꼼꼼한 테스트 케이스는 실패 패턴을 드러낸다.

T8: 테스트 커버리지 패턴을 살펴라

통과하는 테스트가 실행하거나 실행하지 않는 코드를 살펴보면 실패하는 테스트 케이스의 실패 원인이 드러난다.

T9: 테스트는 빨라야 한다

느린 테스트 케이스는 실행하지 않게 된다.

일정이 촉박하면 느린 테스트 케이스를 제일 먼저 건너뛴다.

그러므로 테스트 케이스가 빨리 돌아가게 최대한 노력한다.

결론

이 장에서 소개한 목록이 전부를 말한다고 보기는 어렵다.

하지만 완전한 목록이 목표가 아니다.

여기서 소개한 목록은 가치 쳬계를 피력할 뿐이다.

사실상 가치 체계는 이 책의 주제이자 목표이다.

느낀점

이 책은 한번 읽어서 절대 안된다는 생각이 있어서 두고두고 읽어야겠다는 생각이 있었는데

이 챕터가 목차의 맛보기 페이지 같은 느낌이라 유용할 듯하다.

논의사항

지금까지 읽으시면서 가장 기억에 남는 챕터가 몇 챕터인가요?

댓글남기기