도서 내용 정리 - 스태프 엔지니어

개요


네이버 도서 정보: https://bit.ly/3JzLvQr

개발자는 관리자로만 커리어를 쌓아야 하는가?
관리자가 아닌 기술 리더로 성장하는 길은 없을까?

IT 업계가 계속 성장하면서 전에 없던 팀과 조직의 경계를 넘어서는 큰 문제를 다루게 되었다. 그러면서 많은 기업에서 기술적 전략을 주도하고, 경계를 넘어 프로젝트를 리드할 수 있는 관리자가 아닌 ‘경험 있는’ 엔지니어가 필요하다는 점을 자각하고 있다. 이러한 엔지니어는 어떻게 탄생하며, 이들의 역할과 업무는 무엇일까? 아직 여러 기업이 공유하는 보편적인 경력 개발 방법이 없거나, 그 직함이 일관적이지 않고 매우 다양하게 존재한다.
이 책은 이런 엔지니어를 ‘스태프 엔지니어’라는 직함으로 설명하고 있다. 모호한 스태프 엔지니어의 역할과 유형을 정리했으며 엔지니어링 전략 작성하기, 기술 품질 관리하기 등 스태프 엔지니어로서 해야 할 일들도 소개한다. 또한, 스태프 엔지니어가 될 수 있는 현실적인 주제인 스태프 프로젝트 수행하기나 조직에서 자신을 드러내는 법, 이직하기 등도 설명한다. 마지막으로 우버, 매일침프 등의 현업 스태프 엔지니어 14명에게 듣는 생생한 경험과 노하우까지 살펴볼 수 있다.



스태프 엔지니어 유형


기술리드

특정 팀의 방법과 실행을 가이드 한다. 단일 매니저와 밀접하게 협업하지만, 필요할 경우 둘 또는 세 명의 매니저와 협업 하기도 한다. 일부 기업에서는 기술 리드와 유사한 기술 리드 매니저라는 역할도 있다. 이 역할은 엔지니어링 관리자 진로에 해당하며 사람 관리에 대한 책임도 겸한다.

아키텍트

중요한 영역에서 엔지니어링의 방향과 품질, 접근법의 정의를 책임진다. 이 과정에서 기술 제한, 사용자 요구, 조직의 리더십에 대한 상세한 지식을 활용한다.

해결사

여러 복잡한 문제를 파고들어 적절한 해결책을 찾아낸다. 일부는 오랫동안 특정 분야에만 집중하지만 조직 리더십의 지시에 따라 여러 분야를 넘나 들기도 한다.

오른팔

임원의 주의를 넓히고 임원의 역할과 권한을 위임받아 특히 더 복잡한 조직을 운영한다. 대규모 조직에서는 리더의 리더십이 미치는 범위를 넓혀준다.



스태프 엔지니어의 실제 업무


  • 기술적 방향의 설정과 수정
  • 멘토십과 스폰서십
  • 의사 결정에 엔지니어링 관점의 제공
  • 새로운 해결책 모색
  • 접착제 역할 하기 → 눈에 띄지 않더라도 팀이 계속해서 발전하고 완료한 작업을 내보내는데 필요한 작업을 하는 것

직책이 중요한가?

  • 비공식적으로 실력을 검증하는 절차를 우회할 수 있다. → 신뢰를 위한 에너지를 소비할 필요가 없다.
  • 스태프 엔지니어링 회의에 들어갈 자격을 얻는다.
  • 현재의 직장 생활과 경력에 대한 보상이 증가한다.



스태프 엔지니어로 활동하기


중요한 일에 집중하자

  • 쉬운 일을 주워 먹지 말자 
  • 자기 과시하지 말자
  • 과거 경험에 너무 의존하지 말자
  • 실제로 문제가 되는 부분에 참여 하자
  • 자리가 있으면서 관심도 받는 업무를 추진하자
  • 주변 팀이 성장하도록 도와주자
  • 프로젝트 방향이 엇나가지 않도록 수정하자
  • 프로젝트 마무리할 수 있는 수준으로 조정해주고 올바른 방향으로 전환해주자 
  • 나만 할 수 있는 일을 하자

엔지니어링 전략을 기록하자

<설계문서를 작성하자>

  • 언제 작성하나?
    • 나중에 진행할 여러 프로젝트가 공통적으로 사용할 기능인 경우
    • 사용자에게 의미 있는 영향을 주게될 프로젝트 인 경우
    • 기간이 할 이상이 걸리는 일인 경우
  • 어떻게 작성하나?
    • 문제를 확실하게 제시
    • 간결한 템플릿 사용
    • 작성은 혼자 리뷰는 함께
    • 완벽주의를 버리자

<전략 문서 작성에 대해..>

  • 현재 가진 것 부터 시작
  • 구체적인 사항을 기술
  • 주장을 분명히 해야 함
  • 작업물을 보여줘야 함.

<비전 수립>

  • 향후 2-3년을 위한 내용을 작성
  • 비즈니스와 사용자를 토대로
  • 거만한 태도보다는 낙관적인 태도를 갖자
  • 구체적이고 명확하게 작성하자
  • 한두 페이지로 정리하자

기술 품질의 관리

<문제점>

  • 즉각적인 문제를 유발하는 바로 그 지점을 수정한다.
  • 품질을 개선하는 것으로 알려진 권장 사례를 도입한다.
  • 소프트웨어의 변화에 따라 품질을 유지할 수 있는 지렛점에 우선순위를 둔다.
  • 조직에서 소프트웨어를 변경하는 방법에 대한 기술적 요소를 조정한다.
  • 기술 품질을 측정해서 품질 향상에 대한 더욱 많은 투자를 종용한다.
  • 품질 개선을 위한 시스템과 도구를 구현한 기술 품질 팀을 꾸린다.
  • 품질 프로그램을 실행해서 품질 수준을 측정 및 추적하고 낮은 품질 수준이 발견되면 이를 개선한다.

<계단 오르기>

  • 빠르게 적용할 수 있는 것부터 먼저 시작하자! (어려운 것을 도입했다 실패하는 것보다 쉬운 것을 도입했다 실패하는 것에서 더 빨리 배울 수 있다.)

<작게 시작하고 천천히 추가하자>

  • 작은 것 부터 시작하고 정상 궤도에 올라설 때까지 반복
  • 그런 다음 다른 기법을 추가하고 또 다시 반복

지휘권 가진 사람과 긴밀하게

리드하려면 따라야 한다

  • 우선순위가 높은 것이 무엇인지 스스로 명확히 이해하고 주변에서 일어나는 일 때문에 흔들리지 말자
  • 뭔가를 개선하기 위해 일하는 다른 리더를 신속하게 지원하자
  • 일을 방해하지 않도록 피드백하자. (강요가 아닌 제안 .. ‘원한다면 받아들일 수 있는’ 것으로 주석 표시)

절대 틀리지 않는 방법을 배우자

  • 경청하고 명확히 하며 주변의 분위기를 읽자
  • ‘머저리들’ - 그룹 내에서 동의를 거부한 사람, 타협할 의사가 없는 사람, 다른 사람 말을 듣지 않는 사람 등등..
    • 머저리들이 힘을 쓰지 못할 사람을 회의에 참여 시킨다
    • 회의를 시작하기 전에 머저리들이 자신의 의견도 받아들여지고 있다고 느끼면서 토론에 집중할 수 있도록 교감하는 방법을 찾아본다.

타인을 위한 공간의 창출

내가 아닌 다른 사람을 발전시킬 수 있는 스폰서십을 할 수 있어야 한다

동료와 네트워크 구축

내부 뿐 아니라 외부 네트워크도 가지고 있어야 한다.

임원을 대상으로 하는 프레젠테이션

<효울적인 의사소통 방법>

첫 문단에서 SCQA 형식을 따르자

  • 현 상황: 관련된 문잭이 무엇인가
  • 문제점: 현 상황이 왜 문제가 되는가?
  • 의문점: 문제를 해결하기 위한 핵심 의문 사항은 무엇인가?
  • 해결법: 이 의문점에 대한 최선의 해결책은 무엇인가?

<피해야할 실수>

  • 피드백에 발끈하지 말자
  • 문제나 책임을 회피하지 말자
  • 해결책이 없이 문제를 제시하지 말자
  • 학술적인 프로젠테이션은 하지 말자
  • 본인이 선호하는 결과를 고집하지 말자



개인 의견


회사에서 스태프 트랙을 가져가려고 하고 있고, 모든 개발자가 팀장(매니저) 직책을 갖을 수는 없기 때문에..  개발자로서 앞으로 Next 레벨을 생각해볼 필요가 있음.

스태프엔지니어는 종류가 어떻게 되는지 그리고 어떤 역할을 하는지에 대해서 알고 앞으로 미래를 그림을 그려보면 좋을 것 같습니다.

또, 정리는 하지 않았지만 앞으로 미래를 그려볼 때 4장 5장 책은 많은 도움이 되는 부분 중 하나가 될 수 있습니다. 

 - 4장 이직 결정하기 

 - 5장 인터뷰 → 실제로 해외 기업에서 스태프 엔지니어로 일하시는 분들에 인터뷰 내용이 담겨 있습니다. 실제 스태프 엔지니어가 기술리드/아키텍트/해결사/오른팔 의 역할을 어떻게 수행하고 있는지를 알 수 있습니다.

정리 - 코드리뷰 가이드

 

원문 사이트

https://github.com/thoughtbot/guides/tree/main/code-review


공통사항

  • 많은 프로그래밍 결정이 의견임을 인정하세요. 선호하는 장단점을 논의하고 신속하게 해결책을 도출하세요.
  • 좋은 질문을 하되 요구하지 마세요. ("이 이름을 :user_id로 짓는 것은 어떨까요?")
  • 좋은 질문은 판단을 피하고 작성자의 관점에 대한 가정을 피합니다.
  • 설명을 요청하세요. ("이해가 안 되는데 설명해 주시겠어요?")
  • 코드의 선택적 소유권을 피하세요. ("내 것", "내 것이 아니다", "당신 것")
  • 개인적 특성을 지칭하는 것으로 보일 수 있는 용어는 사용하지 마세요. ("dumb", "stupid"). 모든 사람이 지적이고 선의가 있다고 가정합니다.
  • 명확하게 표현하세요. 온라인에서 사람들이 항상 내 의도를 이해하는 것은 아니라는 점을 기억하세요.
  • 겸손하게 행동하세요. ("잘 모르겠습니다 - 찾아보겠습니다.")
  • 과장된 표현을 사용하지 마세요. ("항상", "절대", "끝없이", "아무것도")
  • 비꼬는 표현을 사용하지 마세요.
  • 진실을 유지하세요. 이모티콘, 움직이는 GIF 또는 유머가 본인에게 맞지 않는다면 억지로 사용하지 마세요. 이모티콘을 사용해야 한다면, 유머러스하게 사용하세요.
  • "이해하지 못했습니다" 또는 "다른 해결책:"이라는 댓글이 너무 많으면 채팅, 화면 공유, 직접 대면 등의 방식으로 동시에 대화하세요. 토론 내용을 요약하는 후속 댓글을 게시합니다.
  • 새로운 것을 배웠다면 감사의 마음을 전하세요. ("몰랐던 사실인데 공유해 주셔서 감사합니다.")


코드 리뷰 요청자

  • 검토자의 제안에 감사하는 마음을 표현하세요. ("좋은 지적입니다. 변경해 보겠습니다.")
  • 온라인에서 감정과 의도를 전달하기는 어려울 수 있다는 점에 유의하세요(https://thoughtbot.com/blog/empathy-online).
  • 코드가 존재하는 이유를 설명합니다. ("이런 이유 때문에 그런 것입니다. 이 클래스/파일/메서드/변수의 이름을 바꾸면 더 명확해질까요?")
  • 일부 변경 사항과 리팩터링을 추출하여 향후 티켓/스토리에 추가합니다.
  • 티켓/스토리에서 코드 리뷰로 연결합니다. ("검토 준비 완료: https://github.com/organization/project/pull/1")
  • 이전 피드백을 기반으로 한 커밋을 브랜치에 격리된 커밋으로 푸시합니다. 브랜치를 병합할 준비가 될 때까지 스쿼시하지 마세요. 리뷰어는 이전 피드백을 기반으로 개별 업데이트를 읽을 수 있어야 합니다.
  • 리뷰어의 관점을 이해하려고 노력하세요.
  • 모든 댓글에 응답하려고 노력하세요.
  • 지속적 통합(TDDium, Travis CI, CircleCI 등)에서 브랜치에서 테스트 스위트가 초록색으로 표시될 때까지 브랜치 병합을 기다린다.
  • 코드와 코드가 프로젝트에 미치는 영향에 대해 확신이 들면 병합하세요.
  • 최종 편집 권한은 풀리퀘스트 작성자에게 있습니다.


코드 리뷰어

변경이 필요한 이유를 이해합니다(버그 수정, 사용자 경험 개선, 기존 코드 리팩터링). 그런 다음

  • 어떤 아이디어가 마음에 드는지, 어떤 아이디어가 마음에 들지 않는지 소통하세요.
  • 문제를 해결하면서 코드를 간소화할 수 있는 방법을 찾아보세요.
  • 토론이 너무 철학적이거나 학문적으로 변하면 금요일 오후의 정기적인 기술 토론으로 오프라인 토론을 전환하세요. 그 동안에는 작성자가 대체 구현에 대한 최종 결정을 내리도록 하세요.
  • 대체 구현 방법을 제시하되, 작성자가 이미 고려하고 있다고 가정합니다. ("여기에 사용자 지정 유효성 검사기를 사용하는 것에 대해 어떻게 생각하세요?")
  • 작성자의 관점을 이해하려고 노력하세요.
  • 풀 리퀘스트에 👍 또는 "병합 준비 완료" 댓글로 서명하세요.
  • 게이트키퍼가 아니라 피드백을 제공하기 위해 여기에 있다는 것을 기억하세요.
  • '제안 추가' 기능을 사용하여 변경 사항을 제안할 때:
    • 어떤 줄을 추가/제거할 것을 제안하는지 명확하게 전달하세요.
    • 가능한 경우 제안한 변경 사항을 테스트하여 제대로 작동하는지 확인합니다.
    • 테스트할 수 없는 경우에는 풀 리퀘스트 작성자에게 제안을 테스트하지 않았다는 사실을 알립니다.
    • 작성자에게 변경을 제안하는 이유를 알릴 수 있는 컨텍스트를 제공하세요.


스타일 코멘트

검토자는 누락된 스타일 가이드라인에 대해 코멘트를 달아야 합니다. 코멘트 예시:

→ 유용한 경로를 이름별로 알파벳순으로 정렬하세요.

스타일 댓글에 대한 응답 예시:

→ 이런. 잘 발견했습니다, 감사합니다. a4994ec에서 수정되었습니다.

가이드라인에 동의하지 않는 경우 코드 리뷰 내에서 토론하지 말고 가이드 리포지토리에 이슈를 개설하세요. 그 동안 가이드라인을 적용하세요. 표준과 같은 lint를 설정하여 코드 형식을 자동으로 지정하는 것이 도움이 되는 경우가 많습니다. 이렇게 하면 개인적인 스타일 선호도에 대한 논쟁보다는 PR에 대해 더 의미 있는 대화를 나눌 수 있습니다.

도움이 되는 다른 글

구글 코드 리뷰 가이드 :

https://google.github.io/eng-practices/

https://soojin.ro/review/

클린코드 13장 동시성

 “객체는 처리의 추상화다. 스레드는 일정의 추상화다.” - 제임스 O. 코플리

동시성과 깔끔한 코드는 양립하기 어렵다. 스레드를 하나만 실행 하는 코드는 짜기가 쉽다.

겉으로 보기에는 멀쩡해 보이는 다중 스레드코드도 짜기 쉽다. 이런코드는 시스템이 부하를 받기 전까지 멀쩡 하게 돌아간다.

이 장에서는 여러 스레드를 동시에 돌리는 이유를 논하고 여러 스레드를 동시에 돌리는 어려움도 논한다. 이런 어려움에 대처하고 깨끗한 코드를 작성하는 방법도 몇 가지 제안한다. 마지막으로, 동시성을 테스트하는 방법과 문제점을 논한다.

동시성이 필요한 이유?

동시성은 결합(coupling)을 없애는 전략이다. 즉, 무엇(what)과 언제(when)를 분리하는 전략이다.

스레드가 하나인 프로그램은 무엇과 언제가 서로 밀접하다. 그래서 호출 스택을 살펴보면 프로그램 상태가 곧바로 드러난다.

무엇(what)과 언제(when)를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다. 구조적인 관점에서 프로그램은 거대한 루프 하나가 아니라 작은 협력 프로그램 여럿으로 보인다. 따라서 시스템을 이해하기가 쉽고 문제를 분리하기도 쉽다.

구조적 개선만을 위해 동시성을 채택하는 건 아니다. 어떤 시스템은 응답 시간과 작업 처리량 개선이라는 요구사항으로 인해 직접적인 동시성 구현이 불가피하다. (예를 들면 웹 사이트 정보 수집기, 사용자 처리 시스템, 대량 정보 분석하는 시스템 등등)

미신과 오해

  • 동시성은 항상 성능을 높여준다.

    대기 시간이 아주 길어 여러 스레드가 프로세서를 공유할 수 있거나 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아진다.

  • 동시성을 구현해도 설계는 변하지 않는다.

    단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다. 일반적으로 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.

  • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다.

    실제로는 컨테이너가 어떻게 동작하는지, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지를 알아야만 한다.

동시성과 관련된 타당한 생각 몇 가지

  • 동시성은 다소 부하를 유발한다. 성능 측면에서 부하가 걸리며 코드도 더 짜야한다.
  • 동시성은 복잡하다. 간단한 문제라도 동시성은 복잡하다.
  • 일반적으로 동시성 버그는 재현하기 어렵다. 그래서 진짜 결함으로 간주되지 않고 일회성 문제로 여겨 무시하기 쉽다.
  • 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

난관

동시에 다른 스레드가 같은 객체를 사용하게 될 때 원하는 값을 못받을 수 있다. 대다수는 올바른 결과를 내지만, 문제는 잘못된 결과를 내놓는 일부가 존재한다는 것이다.

동시성 방어 원칙

단일 책임 원칙(Single Responsibility Principle, SRP)

SRP는 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다는 원칙이다.

동시성은 복잡성 하나만으로도 따로 분리할 이유가 충분하다. 즉, 동시성 관련 코드는 다른 코드와 분리해야 한다는 뜻이다.

동시성 구현시 고려 사항

  • 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.
  • 동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다.
  • 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다. 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로도 충분히 어렵다.

권장사항: 동시성 코드는 다른 코드와 분리하라.

따름 정리(corollary): 자료 범위를 제한하라

객체 하나를 공유한 후 동일 필드를 수정하던 두 스레드가 서로 간섭하므로 예상치 못한 결과를 내놓는다.

공유 객체를 시용하는 코드 내 임계영역(critical section)을 synchronized 키워드로 보호하라고 권장한다. 이런 임계영역의 수를 줄이는 기술이 중요하다.

공유자료를 수정하는 위치가 많을수록 커지는 문제

  • 보호할 임계영역을 빼먹는다. 그래서 공유 자료를 수정하는 모든 코드를 망가뜨린다.
  • 임계영역을 올바르게 보호했는지 확인하느라 똑같은 노력과 수고를 반복한다.
  • 찾기 어려운 버그가 더 찾기 어렵게 된다.

권장사항: 자료를 캡슐화하라. 공유 자료를 최대한 줄여라.

따름 정리: 자료 사본을 사용하라

공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다. 어떤 경우에는 객체를 복사해 읽기 전용으로 사용하는 방법이 가능하다. 어떤 경우에는 각 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본에서 결과를 가져오는 방법도 가능하다.

공유자료를 피하는 방법이 있다면 코드가 문제를 일으킬 가능성도 아주 낮아진다. 물론 객체를 복사하는 시간과 부하가 걱정스러울지 모르겠으나 복사 비용으 진짜 문제인지 실측해 볼 필요가 있다. (하지만 사본으로 동기화를 피할 수 있다면 내부 잠금을 없애 절약한 수행시간이 사본 생성과 가비지 컬렉션에 드는 부하를 상쇄할 가능성이 크다)

따름정리: 스레드는 가능한 독립적으로 구현하라

자신만의 세상에 존재하는 스레드를 구현한다. 즉, 다른 스레드와 자료를 공유 하지 않는다. 각 스레드는 클라이언트 요청 하나를 처리한다.모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저정한다. 그러면 각 스레드는 세상에서 자신만 있듯이 돌아간다.

권장사항: 독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라.

라이브러리를 이해하라

최신 자바 라이브러리를 검색해서 이해할 필요성이 있음. (자바5 기준으로 정리 되어 있어서 혼란을 줄 수 있기 때문에 따로 정리 하지 않음)

실행 모델을 이해하라

  • 한정된 자원 (Bound Resource)
    • 다중 스레드 환경에서 사용하는 자원으로, 크기나 숫자가 제한적이다. 대이터베이스 연결, 길이가 일정한 읽기/쓰기 버퍼 등이 예다.
  • 상호 배제 (Mutual Exclusion)
    • 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우를 가리킨다.
  • 기아 (Starvation)
    • 특정 스레드가 굉장히 오랫동안 또는 영원히 자원을 기다리는 경우오랫동안 혹은 영원히 자원을 기다린다. 예를 들어, 항상 짧은 스레드에게 우선순위를 준다면, 짧은 스레드가 지속적으로 이어질 경우, 긴 스레드가 기아 상태에 빠진다.
  • 데드락 (Deadlock)
    • 여러 스레드가 서로가 끝나기를 기다린다. 모든 스레드가 각기 필요한 지원을 다른 스레드가 점유하는 바람에 어느 쪽도 더 이상 진행하지 못한다.
  • 라이브락 (Livelock)
    • 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만, 공명(resonance)으로 인해, 굉장히 오랫동안 혹은 영원히 진행하지 못한다.

생산자-소비자

하나 이상 생산자 스레드가 정보를 생성해 빈 공간이 있으면 (없으면 대기) 버퍼나 대기열에 넣는다. 하나 이상 소비자 스레드가 대기열에서 정보가 있으면 (없으면 대기) 정보를 가져와 사용한다. 생산자 - 소비자 스레드가 사용하는 대기열은 한정된 자원이다

생산자 스레드는 대기열에 정보를 채운 다음 소비자 스레드에게 시그널을 보낸다. 소비자 스레드는 대기열에서 정보를 읽어들인 후 생산자에게 시그널을 보낸다. 잘못하면 생성자 스레드와 소비자 스레드가 둘 다 진행 가능함에도 불구하고 동시에 서로에게서 시그널을 기다릴 가능성이 존재한다.

읽기-쓰기

읽기 스레드를 위한 주된 정보원으로 공유 자원을 사용하지만, 쓰기 스레드가 이 공유 자원을 이따금 갱신한다고 하자. 이런 경우 처리률이 문제의 핵심이다. 처리율을 강조하면 기아 현상이 생기거나 오래된 정보가 쌓인다. 갱신을 허용하면 처리율에 영향을 미친다.

따라서 읽기 스레드의 요구와 쓰릭 스레드의 요구를 적절히 만족시켜 처리율도 적당히 높이고 기아도 방지하는 해법이 필요하다.

식사하는 철학자들

두근 식타에 철학자 한 무리가 둘러앉았다. 각 철학자 왼쪽에는 포크가 놓였다. 식탁 가운데는 커다란 스파게티 한 접시가 놓였다. 철학자들은 배가 고프지 않으면 생각하며 시간을 보낸다. 배가 고프면 양손에 포크를 집어들고 스파게티를 먹는다. 양손에 포크를 쥐지 않으면 먹지 못한다. 왼쪽 철학자나 오른쪽 첡하자가 포크를 사용한 중이라면 그쪽 철학자가 먹고 나서 포크를 내려놓고 배가 고플 때까지 다시 생각에 잠긴다.

여기에서 철학자를 스레드로, 포크를 자원으로 바꿔 생각하면 많은 기업 애플리케이션이 겪는 문제다. 기업 애플리케이션은 여러 프로세스가 자원을 얻으려 경쟁한다. 주의해서 설계하지 않으면 데드락, 라이브락, 처리율 저하, 효율성 저하등의 상황을 겪는다.

권장사항: 위에서 설명한 기본 알고리즘과 각 해법을 이해하라

동기화하는 메서드 사이에 존재하는 의존성을 이해하라

동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다. 자바 언어는 개별 메서드를 보호하는 synchronized라는 개념을 지원한다. (하지만 공유 클래스 하나에 동기화된 메서드가 여럿이라면 구현이 올바른지를 다시 한 번 확인하기 바란다.)

권장사항: 공유 객체 하나에는 메서드 하나만 사용하라

공유객체 하나에 여러 메서드가 필요한 경우

  • 클라이언트에서 잠금 : 클라이언트에서 첫 번째 메서드를 호출하기 전에 서버를 잠근다.
  • 서버에서 잠금 : 서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는 메서드를 구현한다.
  • 연결 서버 : 잠금을 수행하는중간단계 를 생성한다.

동기화하는 부분을 작게 만들어라

자바에서 synchronized 키워드를 사용하면 락을 설정한다. 같은 락으로 감싼 모든 코드 영역은 한 번에 한 스레드만 실행이 가능하다. 락은 스레드를 지연시키고 부하를 가증시킨다. 그러므로 synchronized 문을 남발하는 코드는 바람직하지 않다.

하지만 임계영역은 반드시 보호해야 한다. 따라서 코드를 짤때는 임계영역수를 최대한 줄여야 한다. 그렇다고 임계영역 크기를 키우면 스레드 간에 경쟁이 늘어나고 프로그램 성능이 떨어진다.

권장사항: 동기화하는 부분을 최대한 작게 만들어라

올바른 종료 코드는 구현하기 어렵다

영구적으로 돌아가는 시스템을 구현하는 방법과 잠시 돌다 깔끔하게 종료하는 시스템을 구현하는 방법은 다르다. 깔끔하게 종료하는 코드는 올바로 구현하기 어렵다. 가장 흔히 발생하는 문제가 데드락이다. 즉, 스레드가 절대 오지 않을 시그널을 기다린다. 그러므로 깔끔하게 종료하는 다중 스레드 코드를 짜야 한다면 시간을 투자해 올바로 구현하기 바란다.

권장사항: 종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라. 생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라.

스레드 코드 테스트하기

코드가 올바르다고 증명하기는 현실적으로 불가능하다, 테스트가 정확성을 보장하지는 않는다. 그럼에도 충분한 테스트는 위험을 낮춘다. 스레드가 하나일때는 이 말이 옳지만 스레드가 둘 이상으로 늘어나면 상황은 급격하게 복잡해진다.

권장사항: 문제를 노출하는 테스트 케이스를 작성하라. 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라. 테스트가 실패하면 원인을 추적하라. 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안 된다.

구체적인 여러 지침

  • 말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라
  • 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
  • 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드를 구현하라
  • 다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라
  • 프로세서 수보다 많은 스레드를 돌려보라
  • 다른 플랫폼에서 돌려보라
  • 코드에 보조 코드(instrument)를 넣어 돌려라. 강제로 실패를 일으키게 해보라

말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라

다중 스레드 코드는 때때로 말이 안 되는 오류를 일으킨다. 대다수의 개발자는 스레드가 다른 코드와 교류하는 방식을 직관적으로 이해하지 못한다. 그래서 많은 개발자가 하드웨어 문제, 단순한 ‘일회성’ 문제로 치부하고 무시한다.

일회성 문제란 존재하지 않는다고 가정하는 편이 안전하다. 일회성 문제를 계속 무시한다면 잘못된 코드 위에 코드가 계속 쌓인다.

권장사항: 시스템 실패를 ‘일회성’이라 치부하지 마라.

다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자

당연한 소리지만 다시 한 번 강조한다. 스레드 환경 밖에서 코드가 제대로 도는지 반드시 확인한다. 일반적인 방법으로, 스레드가 호출하는 POJO를 만든다. POJO는 스레드를 모른다. 따라서 스레드 환경 밖에서 테스트가 가능하다. POJO에 넣는 코드는 많을수록 더 좋다.

권장사항: 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 마라. 먼저 스레드 환경 밖에서 코드를 올바로 돌려라.

다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드를 구현하라

다중 스레드를 쓰는 코드를 다양한 설정으로 실행하기 쉽게 구현하라.

  • 한 스레드로 실행하거나, 여러 스레드로 실행하거나, 실행 중 스레드 수를 바꿔본다.
  • 스레드 코드를 실제 환경이나 테스트 환경에서 돌려본다.
  • 테스트 코드를 빨리, 천천히, 다양한 속도로 돌려본다.
  • 반복 테스트가 가능하도록 테스트 케이스를 작성한다.

권장사항: 다양한 설정에서 실행할 목적으로 다른 환경에 쉽게 끼워 넣을 수 있게 코드를 구현하라.

다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라

적절한 스레드 개수를 파악하려면 상당한 시행착오가 필요하다. 처음부터 다양한 설정으로 프로그램의 성능 측정 방법을 강구한다.

스레드 개수를 조율하기 쉽게 코드를 구현한다. 프로그램이 돌아가는 도중에 스레드 개수를 변경하는 방법도 고려한다. 프로그램 처리율과 효율에 따라 스스로 스레드개수를 조율하는 코드도 고민한다.

프로세서 수보다 많은 스레드를 돌려보라

시스템이 스레드를 스와핑(swapping)할 때도 문제가 발생한다. 스와핑을 일으키려면 프로세서 수보다 많은 스레드를 돌린다. 스와핑이 잦을수록 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다.

다른 플랫폼에서 돌려보라

다중 스레드 코드는 플랫폼에 따라 다르게 돌아간다. 따라서 코드가 돌아갈 가능성이 있는 플랫폼 전부에서 테스트를 수행해야 마땅하다.

권장사항: 처음부터 그리고 자주 모든 목표 플랫폼에서 코드를 돌려라.

코드에 보조 코드(instrument)를 넣어 돌려라. 강제로 실패를 일으키게 해보라

스레드 버그가 산발적이고 우발적이고 재현이 어려운 이유는 코드가 실행되는 수천 가지 경로 중에 아주 소수만 실패하기 때문이다. 보조코드를 추가해 코드가 실행되는 순서를 바꿔주어 오류를 좀 더 자주 일으킬수 있도록 할 수 있다.

결론

다중 스레드 코드는 올바로 구현하기 어렵다. 다중 스레드 코드를 작성한다면 각별히 깨끗하게 코드를 짜야 한다. 주의하지 않으면 희귀하고 오묘한 오류에 직면하게 된다.

무엇보다 먼저 SRP를 준수한다. POJO를 사용하 스레드를 아는 코드와 스레드를 모르는 코드를 분리한다. 스레드 코드를 테스트할 때는 전적으로 스레드만 테스트한다. 즉, 스레드 코드는 최대한 집약되고 작아야 한다는 의미이다.

동시성 오류를 일으키는 잠정적인 원인을 철저히 이해한다.

사용하는 라이브러리와 기본 알고리즘을 이해한다.

보호할 코드 영역을 찾아내는 방법과 특정 코드 영역을 잠그는 방법을 이해한다.

어떻게든 문제는 생긴다. 초반에 드러나지 않는 문제는 일회성으로 치부해 무시하기 십상이다. 소위 일회성 문제는 대개 시스템에 부하가 걸릴 때나 아니면 뜬금없이 발생한다. 그러므로 스레드 코드는 많은 플랫폼에서 많은 설정으로 반복해서계속 테스트해야 한다.

테스트 용이성은 TDD 3대 규칙을 따르면 자연히 얻어진다.

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

시간을 들여 보조 코드를 추가하면 오류가 드러날 가능성이 크게 높아진다.

깔끔한 접근 방식을 취한다면 코드가 올바로 돌아갈 가능성이 극적으로 높아 진다.

클린코드 12장 창발성

 

창발적 설계로 깔끔한 코드를 구현하자

착실하게 따르기만 하면 우수한 설계가 나오는 간단한 규칙 네가지가 있다면? 네 가지 규칙을 따르면 코드 구조와 설계를 파악하기 쉬워진다면? 그래서 SRP나 DIP와 같은 원칙을 적용하기 숴우진다면? 네 가지 규칙이 우수한 설계의 찰발성을 촉진한다면?

우리들 대다수는 켄트 벡이 제시한 단순한 설계 규칙 네 가지가 소프트웨어 품질을 크게 높여준다고 믿는다. (중요도 순)

  • 모든 테스트를 실행한다.
  • 중복을 없앤다.
  • 프로그래머 의도를 표현한다.
  • 클래스와 메서드 수를 최소로 줄인다.

단순한 설계 규칙 1: 모든 테스트를 실행하라

설계는의도한 대로 돌아가는 시스템을 내놓아야 한다. 문서로는 시스템을 완벽하게 설계했지만, 시스템이 의도한 대로 돌아가는지 검증할 간단한 방법이 없다면, 문서 작ㄱ성을 위해 투자한 노력에 대한 가치는 인정받기 힘들다.

테스트를 철저히 거쳐 모든 테스트 케이스를 항상 통과하는 시스템은 ‘테스트 가능한 시스템’이다. 당연하지만 중요한 말이다. 테스트가 불가능한 시스템은 검증도 불가능하다. 논란의 여지가 있지만, 검증이 불가능한 시스템은 절대 출시하면 안 된다.

테스트가 가능한 시스템을 만들려고 애쓰면 설계 품질이 더불어 높아진다. 크기가 작고 목적 하나만 수행하는 클래스가 나온다. (SRP를 준수하는 클래스는 테스트가 훨씬 더 쉽다)

테스트 케이스가 많을 수록 개발자는 테스트가 쉽게 코드를 작성한다. 따라서 철저한 테스트 가능한 시스템을 만들면 더 나은 설계가 얻어진다.

결합도가 높으면 테스트 케이스를 작성하기 어렵다. 테스트 케이스를 많이 작성할수록 개발자는 DIP와 같은 원칙을 적용하고 의존성 주입(Dependency Injection), 인터페이스, 추상화 등과 같은 도구를 사용해 결합도를 낮춘다. 따라서 설계 품질은 더 높아진다.

단순한 설계 규칙 2~4: 리팩터링

테스트 케이스를 모ㅓ두 작성했다면 이제 코드와 클래스를 정리해도 괜찮다. 구체적으로는 코드를 점진적으로 리팩토링 해나간다. 코드 몇 줄을 추가할 때마다 잠시 멎추고 설계를 조감한다. 새로 추가하는 코드가 설계 품질을 낮추는가? 그러다면 깔끔히 정리한 후 테스트 케이스를 돌려 기존 기능을 깨뜨리지 않았다는 사실을 확인한다. 코드를 정리하면서 시스템이 깨질까 걱정할 필요가 없다. 테스트 케이스가 있으니까!

응집도를 높이고, 결합도를 낮추고, 관심사를 분리하고, 시스템 관심사를 모듈로 나누고, 함수와 클래스 크기를 줄이고, 더 나은 이름을 선택하는 다양한 기법들이 동원한다.

또한 이 단계는 단순한 설계 규칙 중 나머지 3개를 적용해 중복을 제거하고, 프로그래머 의도를 표현하고, 클래스와 메서드 수를 최소로 줄이는 단게이기도 하다.

중복을 없애라

우수한 설계에서 중복은 커다란 적이다. 중복은 추가 작업, 추가 위험, 불필요한 복잡도를 뜻하기 때문이다. 같은 코드는 당연히 중복이다. 비슷한 코드는 더 비슷하게 고쳐주면 리팩터링이 쉬워진다. 깔끔한 시스템을 만들려면 단 몇 줄이라도 중복을 제거하겠다는 의지가 필요하다.

표현하라

자신이 이해하는 코드를 짜기는 쉽다. 코드를 짜는 동안에는 문제에 푹 빠져 코드를 구석구석 이해하니까. 하지만 나중에 코드를 유지보수할 사람이 코드를 짜는 사람만큼이나 문제를 깊이 이해할 가능성은 희박하다.

소프트웨어 프로젝트 비용 중 대다수는 장기적인 유지보수에 들어간다. 시스템이 점차 복잡해지면서 유지보수 개발자가 시스템을 이해하느라 보내는 시간은 점점 늘어나고 동시에 코드를 오해할 가능성도 점점 커진다. 그러므로 코드는 개발자의 의도를 분명히 표현해야 한다.

개발자가 코드를 명백하게 짤수록 다른 사람이 그 코드를 이해하기 쉬워진다. 그래야 결함이 줄어들고 유지보수 비용이 적게 든다.

  1. 좋은 이름을 선택한다. 이름과 기능이 완전히 딴판인 클래스나 함수로 개발자를 놀라게 해서는 안 된다.
  2. 함수와 클래스 크기를 가능한 한 줄인다. 작은 클래스와 작은 함수는 이름 짓기도 쉽고, 구현하기도 쉽고, 이해하기도 쉽다.
  3. 표준 명칭을 사용한다. 예를 들어, 디자인 패턴은 의사소통과 표현력 강화가 주요 목적이다.
  4. 단위 테스트 케이스를 꼼꼼히 작성한다.

하지만 표현력을 높이는 가장 중요한 방법은 노력이다. 나중에 읽을 사람을 고려해 조금이라도 읽기 쉽게 만드려는 충분한 고민은 거의 찾기 어렵다. 하지만 나중에 코드를 읽을 사람은 바로 자신일 가능성이 높다는 사실을 명심하자.

그러므로 자신의 작품을 조금 더 자랑하자. 함수와 클래스에 조금 더 시간을 투자하자. 더 나은 이름을 선택하고, 큰 함수를 작은 함수 여럿으로 나누고, 자신의 작품에 조금만 더 주의를 기울이자. 주의는 대단한 재능이다.

클래스와 메서드 수를 최소로 줄여라

중복을 제거하고, 의도를 표현하고, SRP를 준수한다는 기본적인 개념도 극단으로 치달으면 득보다 실이 많아진다.

클래스와 메서드 크기를 줄이자고 조그만 클래스와 메서드를 수없이 만드는 사례도 없지 않다. 그래서 이 규칙은 함수와 클래스 수를 가능한 줄이라고 제안한다.

때로는 무의미하고 독단적인 정책 탓에 클래스 수와 메서드 수가 늘어나기도 한다.

  • 클래스마다 무조건 인터페이스를 생성하라고 요구하는 구현 표준
  • 자료 클래스(자료구조)와 동작 클래스(객체)는 무조건 분리해야 한다고 주장하는 개발자
  • 가능한 독단적인 견해는 멀리하고 실용적인 방식을 택해야 한다.

목표는 함수와 클래스 크기를 작게 유지하면서 동시에 시스템 크기도 작게 유지하는 데 있다. 하지만 이 규칙은 간단한 설계 규칙 네 개 중 우선순위가 가장 낮다. 다시 말해, 클래스와 함수 수를 줄이는 작업도 중요하지만 그보다 테스트 케이스를 만들고 중복을 제거하고 의도를 표현하는 작업이 더 중요하다는 뜻이다.

결론

경험을 대신할 단순한 개발 기법이 있을까? 당연히 없다. 하지만 이 장, 아니 이 책에서 소개하는 기법은 저자들이 수십 년 동안 쌓은 경험의 정수다. 단순한 설계 규칙을 따른다면 (오랜 경험 후에야 익힐) 우수한 기법과 원칙을 단번에 활용할 수 있다.

클린코드 11장 시스템

 “복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다.” -레이 오지, 마이크로소프트 최고 기술 책임자

도시를 세운다면?

새로운 도시를 세우거나 이미 세워진 도시일 때 한 사람의 힘으로 관리하는 것은 무리다.

그럼에도 불구하고 도시는 잘 돌아간다. → 수도관리팀, 전력관리팀, 교통관리팀 등 각 분야를 관리하는 팀이 있기 때문이다. 도시에는 큰 그림을 그리는 사람들도 있으며 작은 사항에 집중하는 사람도 있다.

도시가 돌아가는 또 다른 이유는 적절한 추상화 모듈화 때문이다. 큰 그림을 이해하지 못해도 개인과 개인이 관리하는 구성요소는 효율적으로 돌아간다.

소프트웨어도 도시처럼 구성한다. 그런데 막상 팀이 제작하는 시스템은 비슷한 수준으로 관리를 분리하거나 추상화를 이뤄내지 못한다. 깨끗한 코드를 구현하면 낮은 추상화 수준에서 관심사를 분리하기 쉬워진다.

시스템 제작과 시스템 사용을 분리하라

“제작” 은 “사용” 과 아주 다르다.

소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 연결하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.

“관심사 분리” 는 우리 분야에서 가장 오래되고 가장 중요한 설계 기법 중 하나다. 하지만 불행히도 대다수 애플리케이션은 시작 단계라는 관심사를 분리하지 않는다. 준비 과정 코드를 주먹구구식으로 구현할 뿐만 아니라 런타임 로직과 마구 두섞는다.

아래 코드가 전형적인 예다

public Service getService() {
	if (service == null) {
		service = new MyServiceImpl(); // 모든 상황에 적합한 기본값일까?
	}
	return service;
}

위 코드의 장점은, 객체가 실제로 필요할 때까지 객체를 생성하지 않으므로 불필요한 부하가 걸리지 않는다. 어떤 경우에도 null 을 반환하지 않는다.

하지만 getService 메서드가 MyServiceImpl 과 생성자 인수에 명시적으로 의존한다. 런타임 로직에서 MyServiceImpl 런타임 로직에서 MyServiceImpl 객체를 전혀 사용하지 않더라도 의존성을 해결하지 않으면 컴파일이 안 된다.

테스트도 문제다. MyServiceImpl 이 무거운 객체라면 단위 테스트에서 getService 메서드를 호출하기 전에 적절한 전용 객체를 service 필드에 할당해야 한다. 또한 일반 런타임 로직에 객체 생성 로직을 섞어놓은 탓에 모든 실행 경로도 테스트해야 한다.

무엇보다 MyServiceImpl 이 모든 상황에 적합한 객체인지는 모른다는 사실이다. (주석도 그렇게 달려 있다)

초기화 지연 기법을 한 번 정도 사용한다면 별로 심각한 문제는 아니다. 하지만 많은 애플리케이션이 이처럼 좀스러운 설정 기겁을 수시로 사용한다. 그래서 전반적인 설정 방식이 애플리케이션 곳곳에 흩어져 있다. 모듈성은 저조하며 대개 중복이 심하다.

Main 분리

시스템 생성과 시스템 사용을 분리하는 한 가지 방법으로, 생성과 관련한 코드는 모두 main 이나 main 이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정한다.

제어 흐름은 따라가기 쉽다. main 함수에서 시스템에 필요한 객체를 생성한 후 이를 애플리케이션에 넘긴다. 애플리케이션은 그저 객체를 사용할 뿐이다.

애플리케이션은 main 이나 객체가 생성되는 과정을 전혀 모른다. 단지 모든 객체가 적절히 생성되었다고 가정한다.

팩토리

때로는 객체가 생성되는 시점을 애플리케이션이 결정할 필요도 생긴다. 예를 들어 , 주문 처리 시스템에서 애플리케이션은 LineItem 인스턴스를 생성해 Order 에 추가한다. 이 때 ABSTRACT FACTORY 패턴 을 사용한다. 그러면 LineItem 을 생성하는 시점은 애플리케이션이 결정하지만 LineItem 을 생성하는 코드는 애플리케이션이 모른다.

의존성 주입

사용과 제작을 분리하는 강력한 메커니즘 하나가 의존성 주입이다. 의존성 주입은 제어 역전 기법을 의존성 관리에 적용한 메커니즘이다. 제어 역전에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다. 새로운 객체는 넘겨 받은 책임만 맡으므로 단일 책임 원칙을 지키게 된다. 의존성 관리 맥락에서 객체는 의존성 자체를 인스턴스로 만드는 책임은 지지지 않는다. 대신 이런 책임을 다른 전담 메커니즘에 넘겨야 한다. 그렇게 함으로써 제어를 역전한다. 초기 설정은 시스템 전체에서 필요하므로 대개 책임질 메커니즘으로 main 루틴이나 특수 컨테이너를 사용한다.

클래스가 의존성을 해결하려 시도하지 않는다. 클래스는 완전히 수동적이다. 대신 의존성을 주입하는 방법으로 설정자 메서드나 생성자 인수를 제공한다. DI 컨테이너는 필요한 객체의 인스턴스를 만든 후 생성자 인수나 설정자 메서드를 사용해 의존성을 설정한다. 실제로 생성되는 객체 유형은 설정 파일에 지정하거나 특수 생성 모듈에서 코드를 명시한다.

스프링 프레임워크는 가장 널리 알려진 자바 DI 컨테이너를 제공한다.

초기화 지연으로 얻는 장점을 포기 해야하는 걸까? 이 기법은 DI를 사용하더라도 때론 여전히 유용하다. 먼저 대다수 DI 컨테이너는 필요할 때까지 객체를 생성하지 않고, 대부분은 계산 지연이나 비슷한 최적화에 쓸 수 있도록 팩토리를 호출하거나 프록시를 생성하는 방법을 제공한다. 즉, 계산 기법이나 이와 유사한 최적화 기법에서 이런 메커니즘을 사용할 수 있다.

확장

군락은 마을로, 마을은 도시로 성장한다. 처음에는 좁거나 사실상 없던 길이 포장되며 넓어지고 작은 건물과 공터는 큰 건물로 채워진다. 처음에는 없던 서비스도 나중에는 생겨난다.

그렇지만 성장에는 고통이 따른다. 확장 공사로 꽉 막힌 도로에서 왜 처음부터 넓게 만들지 않았는지라는 의문을 갖는다. 하지만 처음부터 넓은 도로로 만들면 비용을 정당화 할 수 없다.

처음부터 올바른 시스템을 만들 수 있다는 믿음은 미신이다. 대신 우리는 오늘 주어진 사용자 스토리에 맞춰 시스템을 구현해야 한다. 내일은 새로운 스토리에 맞춰 시스템을 조정하고 확장하면 된다. 이것이 반복적이고 점진적인 애자일 방식의 핵심이다. 테스트 주도 개발, 리팩터링, 깨끗한 코드는 코드 수준에서 시스템을 조정하고 확장히 쉽게 만든다.

하지만 시스템 수준에서는 어떨까? 시스템 아키텍처는 사전 계획이 필요하지 않을까? 단순한 아키텍처를 복잡한 아키텍처로 조금씩 키울 수 없다는 현실은 정확하다. 맞는 말 아닌가?

소프트웨어 시스템은 물리적인 시스템과 다르다. 관심사를 적절히 분리해 관리한다면 소프트웨어 아키텍처는 점진적으로 발전할 수 있다.

소프트웨어 시스템은 “수명이 짧다” 는 본질로 인해 아키테[ㄱ처의 점진적인 발전이 가능하다.

횡단(cross-cutting) 관심사

영속성 같은 관심사는 애플리케이션의 자연스러운 객체 경계를 넘나드는 경향이 있다. 모든 객체가 전반적으로 동일한 방식을 이용하게 만들어야 한다. 예를 들어 특정 DBMS나 독자적인 파일을 사용하고, 테이블과 열은 같은 명명 관계를 따르며, 트랜잭션 의미가 일관적이면 더욱 바람ㅈ기하다.

원론적으로는 모듈화되고 캡슐화된 방식으로 영속성 방식을 구성할 수 있다. 하지만 현실적으로는 영속성 방식을 구현한 코드가 온갖 객체로 흩어진다. 여기서 횡단 관심사라는 용어가 나온다. 영속성 프레임워크 또한 모듈화할 수 있다. 도메인 논리도 모듈화 할 수 있다. 문제는 이 두 영역이 세밀한 단위로 겹친다는 점이다. AOP 는 횡단 관심사에 대처해 모듈성을 확보하는 일반적인 방법론이다.

AOP 에서 관점이라는 모듈 구성 개념은 특정 관심스를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성있게 바꿔야한다 라고 명시한다.

영속성을 예로 들면 프로그래머는 영속적으로 저장할 객체와 속성을 선언한 후 영속성 책임을 영속성 프레임워크에 위임한다. 그러면 AOP 프레임워크는 대상 코드에 영향을 미치지 않는 상태로 동작 방식을 변경한다.

자바 프록시

단순한 상황에 적합하다. 개별 객체나 클래스에서 메서드 호출을 감싸는 경우가 좋은 예다.

하지만 JDK 에서 제공하는 동적 프록시는 인터페이스만 지원한다. 클래스 프록시를 사용하려면 CCLIB, ASM, Javassist 등과 같은 바이트 코드 처리 라이브러리가 필요하다.

순수 자바 AOP 프레임워크

대부분의 프록시 코드는 판박이라 도구로 자동화 할 수 있다. Spring, JBoss AOP와 등과 같은 여러 자바 프레임워크는 내부적으로 프록시를 사용한다.

스프링은 비지니스 논리를 POJO로 구현한다. POJO는 순수하게 도메인에 초점을 맞춘다. POJO는 엔터프라이즈 프레임워크에 의존하지 않는다. 따라서 테스트가 개념적으로 더 쉽고 간단하다.

프로그래머는 설정 파일이나 API를 활용해 필수적인 구조를 구현한다. 여기에는 영속성, 트랜잭션, 보안, 캐시, 장애조치 등과 같은 횡단 관심사 포함된다.

AspectJ 관점

관심사를 관점으로 분리하는 가장 강력한 도구는 AspectJ 언어다. AspectJ는 언어 차원에서 관점을 모듈화 구성으로 지원하는 자바 언어 확장이다. 새 도구를 사용하고 새 언어 문법과 사용법을 익혀야 한다는 단점이 있다.

스프링 프레임워크는 AspectJ에 미숙한 팀들이 어노테이션 기반 관점을 쉽게 사용하도록 다양한 기능을 제공한다.

테스트 주도 시스템 아키텍처 구축

관점으로 (혹은 유사한 개념으로) 관심사를 분리하는 방식으로 그 위력이 막강하다. 애플리케이션 도메인 논리를 POJO로 작성할 수 있다면, 즉 코드수준에서 아키텍처 관심사를 분리할 수 있다면, 진정한 테스트 주도 아키텍처 구축이 가능해진다.

아주 단순하면서도 멋지게 분리된 아키텍처로 소프트웨어 프로젝트를 진행해 결과물을 재빨리 출시한 후, 기반 구조를 추가하며 조금씩 확장해나가도 괜찮다는 말이다. 그렇다고 아무 방향 없이 프로젝트에 뛰어들어도 좋다는 뜻은 아니다. 프로젝트를 시작할때는 일반적인 범위, 목표, 일정은 물론이고 결과로 내놓을 시스템의 일반적인 구조도 생각해야 한다.

최선의 시스템 구조는 각기 POJO(또는 다른) 객체로 구현되는 모듈화되는 모듈화된 관심사 영역(도메인)으로 구성된다. 이렇게 서로 다른 영역은 해당 영역 코드에 최소한의 영향을 미치는 관점이나 유사한 도구를 사용해 통합한다. 이런 구조 역시 코드와 마찬가지로 테스트 주도 기법을 적용할 수 있다.

의사 결정을 최적화하라

모듈을 나누고 관심사를 분리하면 지엽적인 관리와 결정이 가능해진다. 도시든 소프트웨어 프로젝트든, 아주 큰 시스템에서는 한 사람이 모든 결정을 내리기 어렵다.

가장 적합한 사람에게 책임을 맡기면 가장 좋다. 우리는 때때로 가능한 마지막 순간까지 결정을 미루는 방법이 최선이라는 사실을 까먹곤한다.

게으르거나 무책임해서가 아니라, 최대한 정보를 모아 최선의 결정을 내리기 위해서이다. 성급한 결정은 불충분한 지식으로 내린 결정이다. 너무 일찍 결정하면 고객 피드백을 모으고 프로젝트를 더 고민하고, 구현 방안을 더 탐험할 기회가 사라진다.

관심사를 모듈로 분리한 POJO 시스템은 기민함을 제공한다. 이런 기민함 덕택에 최신 정보에 기반해

최선의 시점에 최적의 결정을 내리기가 쉬워진다. 또한 결정의 복잡성도 줄어든다.

명백한 가치가 있을 때 표준을 현명하게 사용하라

EJB2는 단지 표준이라는 이유만으로 많은 팀이 사용했다. 가볍고 간단한 설계로 충분했을 프로젝트에서도 EJB2를 채택했다. 아주 과장되게 포장된 표준에 집착하는 바람에 고객 가치가 뒷전으로 밀려난 사례도 많이 있다.

표준을 사용하면 아이디어와 컴포넌트를 재사용하기 쉽고, 적절한 경험을 가진 사람을 구하기 쉬우며, 좋은 아이디어를 캡슐화하기 쉽고, 컴포넌트를 엮기 쉽다. 하지만 때로는 표준을 만드는 시간이 너무 오래 걸려 업계가 기다리지 못한다. 어떤 표준은 원래 표준을 제정한 목적을 잊어버리기도 한다.

시스템은 도메인 특화 언어가 필요하다

DSL은 간단한 스크립트 언어나 표준 언어로 구현한 API를 가리킨다. DSL로 짠 코드는 도메인 전문가가 작성한 구조적인 산문처럼 읽힌다. 좋은 DSL은 도메인 개념과 그 개념을 구현한 코드 사이에 존재하는 "의사소통 간극"을 줄여준다.

도메인 특화 언어(Domail-Specific Language)를 사용하면 고차원 정책에서 저차원 세부사항에

이르기까지 모든 추상화 수준과 모든 도메인을 POJO로 표현할 수 있다.

결론

시스템 역시 꺠끗해야 한다. 깨끗하지 못한 아킽텍처는 도메인 논리를 흐리며 기민성을 떨어뜨린다. 도메인 논리가 흐려지면 제품 품질이 떨어진다. 버그가 숨어들기 쉬워지고, 스토리를 구현하기 어려워진다. 기민성이 떨어지면 생상성이 낮아져 TDD가 제공하는 장점이 사라진다.

모든 추숭화 단계에서 의도는 명확히 표현해야 한다. 그러려면 POJO를 작성하고 관점 혹은 관점과 유샇나 메커니즘을 사용해 각 구현 관심사를 분리해야 한다.

시스템을 설계하든 개별 모듈을 설계하든, 실제로 돌오가는 가장 단순한 수단을 사용해야 한다는 사실을 명심하자.

클린코드 10장 클래스

 

클래스 체계

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

정적, 공개 상수가 있다면 맨 처음에 나온다. 그 다음으로 비공개 변수가 나온다

이어서 비공개 인스턴스 변수가 나온다. 공개 변수가 필요한 경우는 거의 없다.

변수 목록 다음에는 공개 함수가 나온다. 비공개 함수는 자신을 호출하는 공개 함수 직후에 넣는다.

즉, 추상화 단계가 순차적으로 내려간다. 그래서 프로그램은 기사 처럼 읽는다.

public class Samlpe {
	private static final String PREFIX = "SAMPLE";
	public static final String TABLE_NAME = "SAMPLE";
	
	private static boolean locked = false;
	
	private String data;
	public String getData() {
		return this.data;
	}
	public String setData(String data) {
		this.data = data;
	}
	
	public String save() {
		lock();
		// save
		unlock();
	}
	
	private void lock() {
		this.locked = true;
	}
	
	private void unlock() {
		this.locked = false;
	}
}

캡슐화

변수와 유틸리티 함수는 가능한 공개하지 않는 편이 낫지만 반드시 숨겨야한다는 법칙도 없다. 떄로는 변수나 유틸리티 함수를 protected 로 선언해 테스트 코드에 접근을 허용하기도 한다. 같은 패키지 안에서 테스트 코드가 함수를 호출하거나 변수를 사용해야하면 그 함수나 변수 protected 로 선언하거나 패키지 전체로 공개한다.

하지만 캡슐화를 풀어주는 결정은 언제나 최후의 수단이다. 그 전에 다른 방법이 있는지 고민해봐야 한다.

클래스는 작아야 한다!

클래스를 만들 때

첫 번째 규칙은 크기다. 클래스는 작아야 한다. 두 번째 규칙도 크기다. 더 작어여 한다.

클래스를 설계할 때도, 함수와 마찬가지로 작게가 기본 규칙이다.

그렇다면 얼마나 작아야할까?

함수는 물리적인 행수로 크기를 측정했지만, 클래스는 다른 척도를 사용해야 한다. → 클래스가 맡은 책임으로 측정한다.

메서드가 적다고 책임이 적은 것이 아니다.

클래스 이름은 해당 클래스 책임을 기술해야 한다. 실제로 작명은 클래스 크기를 줄이는 첫 번째 관문이다.

간결한 이름이 떠오르지 않는다면 클래스 크기가 너무 커서 그런 것이다.

클래스 이름이 모호하다면 클래스 책임이 너무 많아서다. 예를 들어 클래스 이름에 Processor, Manager, Super 등과 같이 모호한 단어가 있따면 클래스에다 여러 책임을 떠안겼다는 증거다.

또한 클래스 설명은 만일(if), 그리고(and), 하며(or), 하지만(but) 을 사용하지 않고 25단어 내외에로 가능해야 한다.

단일 책임 원칙 (Single Responsibility Principle, SRP)

클래스나 모듈을 변경할 이유가 하나, 단 하나뿐이어야 한다는 원칙이다.

SRP 는 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시한다. 클래스는 책임, 즉 변경할 이유가 하나여아 한다는 의미다.

publuc class SuperDashboard extend JFrame implements MetaDataUser {
	public Component getLastFocusedComponent()
	public void setLastFoucsed(Component lastFocused)
	public int getMajorVersionNumber()
	public int getMinorVersionNumber()
	public int getBuildNumber()
}

위 클래스를 봤을 때 변경할 이유는 두 가지이다.

첫째, SuperDashboard 는 소프트웨어 정보를 추적한다. 그런데 버전 정보는 소프트웨어를 출시할 때마다 달라진다.

둘째, SuperDashboard 는 자바 스윙 컴포넌트를 관리한다. 즉, 스윙 코드를 변경할 때마다 버전 번호가 달라진다.

책임, 즉 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화기 쉬워진다. SuperDashboard 에서 버전 정보를다루는 메서드 세개를 따로 빼내 Version 이라는 독자적인 클래스를 만든다. Version 클래스는 다른 어플리케이션에서 재사용하기 쉬운 구조이다

public class Version {
	public int getMajorVersionNumber()
	public int getMinorVersionNumber()
	public int getBuildNumber()
}

SRP 는 객체 지향 설계에서 더욱 중요한 개념이다. 또한 이해하고 지키기 수월한 개념이기도 하다. 하지만 SRP는 클래스 설계자가 가장 무시하는 규칙 중 하나이다. 우리는 수많은 책임을 떠안은 클래스를 꾸준하게 접한다. 왜일까?

소프트웨어를 동라가게 만드는 활동과 소프트웨어를 깨끗하게 만드는 활동은 완전 별개다. 우리들 대다수는 두뇌 용량에 한계가 있어 깨끗하고 체계적인 소프트웨어 보다는 돌아가는 소프트웨어에 초점을 맞춘다.

문제는 우리들 대다수가 프로그램이 돌아가면 일이 끝났다고 여기는 데 있다. 깨끗하고 체계적인 소프트웨어라는 다음 관심사로 전환하지 않는다.

또, 많은 개발자는 자잘한 단일 책임 클래스가 많아지면 큰 그림을 이해하기 어려워진다고 우려한다. 큰 그림을 이해하려면 이 클래스 저 클래슬르 수없이 넘나들어야 한다 걱정한다.

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

그러므로 고민할 질문은 다음과 같다. “도구 상자를 어떻게 관리하고 싶은가?”

  • 작은 서랍을 많이 두고 기능과 이름이 명확한 컴포넌트로 나눠 넣고 싶은가?
  • 아니면 큰 서랍 몇 개를 두고 모두 던져 넣고 싶은가?

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

응집도

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

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

일반적으로 이처럼 응집도가 가장 높은 클래스는 가능하지도 바람직하지도 않다. 그렇지만 우리는 응집도가 높은 클래스를 선호한다. 응집도가 높다는 말은 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미기 때문이다.

public class Stack {
	private int topOfStack = 0;
	List<Integer> elements = new LinkedList<Integer>();
	
	public int size() {
		return topOfStack;
	}
	
	public void push(int element) {
		topOfStack++;
		elements.add(element);
	}
	
	public int pop() throws PoppedWhenEmpty {
		if (topOfStack == 0) {
			throw new PoppedWhenEmpty();
		}
		
		int element = elements.get(--topOfStack);
		elements.remove(topOfStack);
		
		return element;
	}
}

위 클래스는 응집도가 아주 높다. size()를 제외한 다른 두 메서드는 두 변수를 모두 사용한다.

“함수를 작게 매게 변수 목록을 짧게” 라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다. 이는 십줄팔구 새로운 클래스로 쪼개야 한다는 신호다. 응집도가 높아지도록 변수와 메서드를 적절히 분리해 새로운 클래스 두 세개로 쪼개준다.

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

큰 함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다. 예를 들어, 변수가 아주 많은 큰 함수 하나가 있다. 큰 함수 일부를 작은 함수 하나로 빼내고 싶은데, 빼내려는 코드가 큰 함수에 정의된 변수 넷을 사용한다. 그렇다면 변수 네 개를 새 함수에 인수로 넘겨야 옳을까? → 전혀 아니다. 만약 인스턴스 변수로 넘기면 새 함수는 인수가 전혀 필요 없다. 그 만큼 함수를 쪼개기 쉬워진다. 불행히도 이렇게 하면 클래스가 응집력을 잃는다. 몇몇 함수만 사용하는 인스턴스 변수가 점점 더 늘어나기 때문이다. → 몇몇 함수가 몇몇 변수만 사용한다면 독자적인 클래스로 분리하면 된다.

그래서 큰 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다.

리팩토링 코드는 책에서 직접 확인해보면 좋을 것..

  • 리팩토링 하고 나면 일단 프로그램이 길어진다.
    • 점 더 길고 서술적인 변수 이름을 사용한다.
    • 코드에 주석을 추가하는 수단으로 함수 선언과 클래스 선언을 활용한다.
    • 가독성을 높이고자 공백을 추가하고 형식을 맞추었다.
  • 원래 프로그램은 세 가지 책임으로 나눠져 있었다.
    • PrimePrinter 클래스는 main 함수 하나만 포함하며 실행 환경을 책임진다. → 호출 환경이 바뀌면 고쳐준다
    • RowColumnPagePrinter 클래스는 숫자 목록을 주어진 행과 열에 맞춰 페이지에 출력하는 방법을 안다. → 출력하는 모양새를 바꾸려면 고쳐준다.
    • PrimeGenerator 클래는 소수 목록을 생성하는 방법을 안다. → 소수를 계산하는 알고리즘이 바뀌면 클래스를 고쳐준다.

변경하기 쉬운 클래스

대다수 시스템은 지속적인 변경이 가해진다 .그리고 뭔가 변경할 때마다 시스템이 의도대로 동작하지 않을 위험이 따른다.

깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반하는 위험을 낮춘다.

변경으로부터 격리

요구사항은 변하기 마련이다 .따라서 코드도 변하기 마련이다.

상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다. 그래서 우리는 인터페이스와 추상 클래스를 사용해 구현이 미치는 영향을 격리한다.

상세한 구현에 의존하는 코드는 테스트가 어렵다.

클린코드 9장 단위테스트

 

TDD 법칙 세 가지

  • 첫째 법칙: 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  • 둘째 법칙: 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  • 셋째 법칙: 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

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

지저분한 테스트 코드라도 있는 것이 좋을 것이라고 판단하기 쉬우나 사실은 테스틑를 안하는 것 보다 더 나쁘다.

실제코드가 진화하면 테스트 코드도 변해야 한다. 그런데 테스트 코드가 복잡하면 실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더걸리기 십상이다.

새 버전 출시할 때 마다 팀이 테스트 케이스를 유지하고 보수하는 비용도 늘어 테스트 코드가 점차 개발자 사이에서 가장 큰 불만으로 자리 잡는다.

하지만 테스트 슈트가 없으면 개발자는 자신이 수정한 코드가 제대로 도는지 확인할 방법이 없다. 테스트 슈트가 없으면 시스템 이쪽을 수정해도 저쪽이 안전하다는 사실을 검증하지 못한다. 그래서 결합율이 높아지기 시작한다.

변경하면 듣곱다 해가 크다 생각해 더 이상 코드를 정리하지 않고, 그러면 코드가 망가지기 시작한다. 결국 테스트 슈트도 없고 얼기설기 뒤섞인 코드에 좌절한 고객과 테스트에 쏟아 부은 노력이 허라였다는 살망감만 남는다.

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

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

테스트 코드를 깨끗하게 유지하지 않으면 결국 잃어버린다. 그리고 테스트 케이스가 없으면 실제 코드를 유연하게 만드는 버팀목도 사라진다.

코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트다. 이유는 단순하다. 테스트 케이스가 있으면 변경이 두렵지 않다.

테스트 코드가 없으면 설계를 아무리 잘 해도 모든 변경이 잠정적인 버그다.

실제 코드를 점검하는 자동화된 단위 테스트 슈트는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠다. 테스트는 유연성, 유지보수성, 재사용성을 제공한다.

깨끗한 테스트 코드

깨끗한 테스트 코드를 만들려면 가독성이 필요하다.

가독성을 높이려면 명료성, 단순성, 풍부한 표현력이 필요하다.

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

도메인에 특화된 테스트 언어

public void testGetPageHierarchyAsXml() throws Exception {
	makePages("pageOne", "pageOne.ChiildOne", "PageTwo");
	
	submitRequest("root", "type:pages");
	
	assertResponseIsXml();
	assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");)
}

public void testSymbolicLinkAreNotInXmlPageHierachy() throws Exception {
	WikiPage page = makePage("PageOne");
	makePages("PageOne.ChioldOne", "PageTow");
	
	addLinkTo(page, "PageTwo", "SymPage");
	
	submitRequest("root", "type:pages");
	
	assertResponseIsXml();
	assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");)
	assertResponseDoesNotContain("SymPage");
}

public void testGetDataAXml() throws Exception {
	makePageWithContent("TestPageOne", "test page");
	
	submitRequest("TestPageOne", "type:data");
	
	assertREsponseIsXML();
	assertResponseContains("test page", "<test");
}

이 코드는 도메인에 특화된 언어로 테스트 코드를 구현하는 기법을 보여준다.

흔히 쓰는 시스템 조작 API 를 사용하는 대신 API 위에 함수와 유틸리티를 규현한 후 그 함수와 유틸리티를 사용하므로 테스트 코드를 짜기도 읽기도 쉬워진다.

이렇게 구현한 함수와 유틸리티를 사용하므로 테스트 코드를 짜기도 읽기도 쉬워진다.

이렇게 구현한 함수와 유틸리티는 테스트 코드에서 사용하는 특수 API가 된다. 즉, 테스트를 구현하는 당사자와 나중에 테스트를 일겅볼 독자를 도와주는 테스트 언어가 된다.

이중 표준

테스트 API 코드에 적용하는 표준은 실제 코드에 적용하는 표준과 확실히 다르다.

단순하고 간결하고 표현력이 풍부해야 하지만 실제 코드만큼 효율적일 필요는 없다.

실제 환경이 아니라 테스트 환경에서 돌아가는 코드이기 때문이다. 실제 환경과 테스트 환경은 요구사항이 판이하게 다르다.

아래 코드는 온도가 급격하게 떨어지면 경보 온풍기 송푸기 가 모두 가동되는지 확인하는 코드이다.

@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
	hw.setTemp(WAY_TOO_COLD);
	
	controller.tic();
	
	assertTrue(hw.heaterState());
	assertTrue(hw.blowerState());
	assertFalse(hw.collerState());
	assertFalse(hw.hiTempAlarm());
	assertTrue(hw.loTempAlarm());
}

위 코드는 가독성이 좋지 않다, 아래와 같이 변경하면 가독성이 높아진다.

@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
	wayTooColde();
	
	assertEquals("HBchL", hw.getState());
}

wayTooColde 함수를 만들어서 tic 함수 를 숨겼다.

“HBchL” 라는 문자열을 만들어 새로운 상태값을 만들었다. (물론 그릇된 정보를 피하라 규칙 위반에 가깝지만 여기에서는 적절해보인다.)

문자는 {heater, blower, cooler, hi-temp-alarm, lo-temp-alarm} 순서이고 대문자는 켜짐 소문자는 꺼짐이다.

이렇게 만들면 아래와 같이 테스트 코드를 확장할 수도 있다

@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
	tooHot();
	
	assertEquals("hBChl", hw.getState());
}

@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
	tooHot();
	
	assertEquals("HBchl", hw.getState());
}

getState 함수를 살펴보면 아래와 같이 작성했다.

public String getState() {
	String state = "";
	state += heater ? "H" : "h";
	state += blower ? "B" : "b";
	state += cooler ? "C" : "c";		
	state += hiTempAlarm ? "H" : "h";
	state += loTempAlarm ? "L" : "l";
	retyrb state;
}

StriugBufer는 보기에 흉하다. StriugBufer 를 안 써서 치르는 대가가 미미하다. 하지만 이 프로그램은 임베디드 시스템이기 때문에 자원과 메모리가 제한적일 가능성이 높다. 하지만 테스트 환경의 자원은 제한적이지 않다

이것이 이중 표준의 본질이다. 실제 환경에서는 절대로 안 되지만 테스트 환경에서는 전혀 문제가 없는 방식이 있다. 대게 메모리나 CPU 효율과 관련 있는 경우다. 코드의 깨끗함과는 무관하다.

테스트 당 assert 는 하나

JUnit 으로 테스트 코드를 짤 때는 함수마다 assert 문을 단 하나만 사용해야 한다고 주장하는 학파가 있따. 가혹한 규칙이라 여길지도 모르지만 확실히 장점은 있따. assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠르다.

하지만 출력이 xml 인 경우 assert 문과 특정 문자열 포함한다는 assert 문을 하나로 병합하는 방식이 불합리해 보인다.

public void testGetPageHierarchyAsXml() throws Exception {
	makePages("pageOne", "pageOne.ChiildOne", "PageTwo");
	
	submitRequest("root", "type:pages");
	
	assertResponseIsXml();
	assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");)
}
public void testGetPageHierachAsXml() throws Exception {
	givenPages("pageOne", "pageOne.ChiildOne", "PageTwo");
	
	whenRequestIsIssued("root", "type:pages");
	
	assertResponseIsXml();
}

public void testGetPageHierachyHasRightTags() throws Exception {
	givenPages("pageOne", "pageOne.ChiildOne", "PageTwo");
	
	whenRequestIsIssued("root", "type:pages");
	
	assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");)
}

바꾸면서 given-when-then 이라는 관례를 사용했다 그러면 읽기가 쉬워진다.

불행하게도 위에서 보듯이 테스트를 분리하면 중복되는 코드가 많아진다.

TEMPLATE MTEHOD 패턴을 사용하면 중복을 제거할 수 있다.

given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 두면 된다. 아니면 완전히 독자적인 테스트 클래스를 만들어 @before 함수에 given/when 부분을 넣고 @Test 부분에 then 을 넣어도 된다.

이것 저것 감안해보면 assert 문을 여러개 사용하는 편이 좋다고 생각한다.

단일 assert 문 이라는 규칙은 훌륭한 지침이라 생각한다. 하지만 때로는 주저 없이 함수 하나에 여러 assert 문을 넣기도 한다. 단지 assert 문 개수를 최대한 줄여야 좋다고 생각한다.

테스트 당 개념은 하나

어쩌면 테스트 함수마다 한 개념만 테스트하라는 규칙이 더 낫다. 이것저것 잡다한 개념을 연속으로 테스트 하는 긴 함수를 피한다.

아래 코드는 여러 개념을 하나의 테스트로 진행하기 때문에 좋지 않다.

public void testAddMonth() {
	SerialDate d1 = SeialDate.createInstance(31, 5, 2004);
	
	SerialDate d2 = SerialDate.addMonths(1, d1);
	assertEquals(30, d2.getDayOfMonth());
	assertEquals(6, d2.getMonth());
	assertEquals(2004, d2.getYYYY());
	
	SerialDate d3 = SerialDate.addMonths(2, d1);
	assertEquals(31, d3.getDayOfMonth());
	assertEquals(7, d3.getMonth());
	assertEquals(2004, d3.getYYYY());
	
	SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
	assertEquals(30, d4.getDayOfMonth());
	assertEquals(7, d4.getMonth());
	assertEquals(2004, d4.getYYYY());
}

F.I.R.S.T

깨끗한 테스트는 다음 다섯 가지 규칙을 따르게 된다.

빠르게 (First): 테스트는 빨라야 하낟.

독립적으로 (Independent): 각 테스트는 서로 의존하면 안 된다.

반복가능하게 (Repeateable): 테스트는 어떤 환경에서도 반복 가능해야 한다.

자가검증하는 (Self-Validating): 테스트는 부울 값으로 결과를 내야 한다.

적시에 (Timely): 테스트는 적시에 작성해야 한다.

결론

이 장은 주제를 수박 겉핡기 정로도만 훝었따. 사실상 꺠긋한 테스트 코드 라는 주제는 책 한권을 할애해도 모자랄 주제다. 테스트 코드는 실제 코드만큼이나 프로젝트 건강에 중요하다. 어쩌면 실제 코드보다 더 중요할지도 모르겠다. 테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화하기 떄문이다. 그러므로 테스트 코드는 지속저긍로 깨끗하게 관리하자. 표현력을 높이고 간결하게 정리하자. 테스트 API 구현해 도메인 특화 언어를 만들자 그러면 그만큼 테스트 코드를 짜기가 쉬워진다. 테스트 코드가 방치되어 망가지면 실제 코드도 망가진다. 테스트 코드를 깨끗하게 유지하자.

클린코드 8장 경계

 시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다 (오픈소스를 쓰거나, 다른 팀의 컴포넌트를 쓰거나..)

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

외부 코드 사용하기

인터페이스 제공자의 경우 더 많은 환경에서 돌아가기 위해서 적용성을 최대한 넓히려고 애를 쓴다 → 그로 인하여 시스템 경계에서 문제가 생길 소지가 많다.

예를 들어 java.util.Map 을 살펴보면 Map 은 다양한 인터페이스로 수많은 기능을 제공한다. 제공하는 기능성과 유연성은 확실히 유용하지만 그만큼 위험하다 → 프로그램에서 Map 을 만들어서 넘긴다고 가정하면, 넘기는 쪽에서 아무도 Map 을 삭제하지 않으리라 믿을지도 모르지만, Map 내용을 지울 권한은 누구에게나 있다.

Map<String, Sensor> sensors = new HashMap<>();
...
Senser s = sensors.get(sensorId);

위 코드 보다 아래 코드와 같으 사용하는 것이 바람직하다.

public class Sensors {
	private Map<String, Sensor> sensors = new HashMap<>();
	
	public Sensor getById(String id) {
		return sensors.get(id);
	}
}

경계 인터페이스인 Map을 Sensors 안으로 숨긴다. 따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. 또한 Sensors 클래스는 필요한 인터페이스만 제공할 수 있다.

Map 을 사용할 때마다 캡슐화를 해서 사용하라는 것이 아니라 Map 을 사용하여 여기저기 넘기지 말라는 말이다. (파라미터나 반환값으로 쓰지 말라는 것)

경계 살피고 익히기

외부 코드를 사용하면 적은 시간에 더 많은 기능을 출시하기가 쉬워진다.

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

타사 라이브러리를 가져왔고 사용법이 분명하지 않다고 가정하면 대개는 하루나 이틀 문서를 읽으며 사용법을 결정한다. 그런 다음 우리 쪽 코드를 작성하여 예상대로 동작하는지 확인한다. 때로는 우리 버그인지 라이브러리 버그인지 찾아내느라 오랜 디버깅으로 골치를 앟는다.

외부 코드는 익히기 어렵다. 외부 코드를 통합하기도 어렵다. 두가지를 동시에 하는건 더 어렵다.

그렇기 때문에 다르게 접근할 필요가 있다. 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히면 어떨까.. (이를 학습테스트 라고 부른다.)

학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다 .통제된 환경에서 API를 제대로 이해하는지를 확인하는 셈이고, 학습테스트는 API를 사용하려는 목적에 초점을 맞춘다.

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

학습 테스트에는 비용이 들지 않는다. (어째든 익혀야 하니까.. 오히려 필요한 지식만 확보하는 손쉬운 방법이다)

학습 테스트는 이해도를 높여주는 정확한 실험이다.

학습 테스트는 공짜 이상이다. 투자하는 노력보다 성과가 크다.

패키지가 새로운 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인할 수 있다.

학습 테스트는 패키지가 예상대로 도는지 검증한다. 일단 통합한 이후라고 하더라도 패키지가 우리 코드와 호환이되리라는 보장은 없다.

학습 테스트를 이용한 학습이 필요하든 그렇지 않든, 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다. 이런 경계 테스트가 있다면 패키지의 새 버전으로 이전하기 쉬워진다. (그렇지 않다면 낡은 패키지를 오랫동안 사용하게 된다.)

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

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

때로 우리 지식이 경계를 너머 미치지 못하는 코드 영역도 있다. 떄로는 알려고 해도 알 수 가 없다. 때로는 더이상 내다보지 않기로 결정한다.

다른 팀에 API를 설계하지 않았으므로 구체적인 방법은 모를 수 있기에 구현을 너중으로 미루고, 자체적인 인터페이스를 정의했다.

우리가 바라는 인터페이스를 구현하면 우리가 인터페이스를 전적으로 통제한다는 장점이 생긴다. 또한 코드 가독성도 높아지고 코드 의도도 분명해진다.

그런 다음 나중에 다른 팀이 구현한 API를 해당 인터페이스를 통하여 제공을 하면 된다.

깨끗한 경계

경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예다.

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

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

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

경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.

외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. Map 에서 봤듯이 새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.

클린코드 7장 오류 처리

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

여기 저기 흩어진 오류 처리 코드 때문에 실제 코드가 하는 일을 파악하기가 거의 불가능하다.

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

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

함수를 호출한 즉시 오류를 확인해야 하는 것은 잊어버리기 쉽다. 그래서 오류가 발생하면 예외를 던지는 편이 더 낫다. 그러면 호출자 코드가 더 깔끔해진다. (논리가 오류 처리 코드와 뒤섞이지 않으니까)

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

try catch finally 는 트랙잭션과 비슷하다. try 에서 무슨일이 생기든지 catch 블록은 일관성 있게 유지해야 한다.

미확인 예외를 사용하라

처음에는 확인된 예외가 멋진 아이디어라고 생각했다. 하지만 대부분의 경우 불필요하다.

확인된 예외는 OPC 를 위반한다. 메서드에서 확인된 예외를 던졌는데 catch 블록이 세 단계 위에 있다면 그 사이 메서드는 모두가 메서드 선언부를 고쳐줘야 한다. 모듈과 관련된 코드가 전혀 바뀌지 않았는데 메서드 선언분를 전부 고쳐야한다는 말이다.

예외에 의미를 제공하라

예외를 던질 때는 전후 상황을 충분히 덧 붙인다. 그러면 오류가 발생한 원인과 위치를 찾기 쉬워진다.

자바는 모든 예외 호출 스택을 제공한다. 하지만 실패한 코드의의도를 파악하려면 호출 스택만으로는 부족하다. 오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급한다. 애플리케이션이 로깅 기능이을 사용하려면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

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

같은 처리를 필요로하는 예외가 있으면 하나 감사서 같은 예외를 throw 하는 것이 바람직하다. 그래야 호출자가 예외에 대해서 별도로 catch 하지 않아도 된다.

정상 흐름을 정의하라

try {
	MealExpense expenses = expenseReportDAO.getMeals(employee.getId());
	m_total += expenes.getTotal();
} catch(MealExpenseNotFound(e) {
	m_total += getMealPerDiem();
}

과 같이 작성하지 않고..

DAO 클래스를 고쳐서 언제나 MealExpense 객체를 반환하도록 처리하도록 하는 것이 좋다

MealExpense expenses = expenseReportDAO.getMeals(employee.getId());
m_total += expenes.getTotal();
public class PerDiemMealExpenses implements MealExpenases {
	public int getTotal() {
		// 기본 값으로 일일 기본 식비를 반환한다.
	}
}

null 을 반환하지 마라

null 을 환환하는 습관은 좋지 않다. 한 줄 건너 하나씩 null 을 확인해야하는 코드로 가득한 애플리케이션이 된다.

null 을 반환하면 호출자는 매번 null 을 체크하고, 하지 않으면 오류가 발생할 가능성이커진다.

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

List<Employee> employees = getEmployees(); 
if (employees != null) {
	for (employee e: employees) {
		totalPay += e.getPay();
	}
}

getEmployees 가 null 을 반환하기 때문에 호출자에서 체크해줘야 한다.

하지만 null 이 아닌 빈 객체를 반환하면.. 호출자가 null 체크할 필요가 없어진다

public List<Employees> getEmlpoyees() {
	if (..직원이 없으면..) {
		return Collections.emptyList();
	}
	...
}

null 을 전달하지 마라

null 을 반환하는 것도 나쁘지만 null 을 인수로 받는 방식은 더 나쁘다.

public class MetricsCalculator {
	public double xProjection(Point p1, Point p2) {
		return (p2.x - p1.x) * 1.5;
	}
	...
}

위 코드에서 null 을 전달하면 NullPointException 이 발생한다.

public class MetricsCalculator {
	public double xProjection(Point p1, Point p2) {
		if (p1 == null || p2 == null) { 
			throw InvaildArgumentException("${Message}");
		}
		return (p2.x - p1.x) * 1.5;
	}
	...
}

위 코드 처럼 처리할 수 있지만, InvalidArgumentException 을 잡아내는 처리기가 필요하다.

assert 문을 사용하는 방법도 있다. (개인적으로 별로 좋아보이진 않는다..)

public class MetricsCalculator {
	public double xProjection(Point p1, Point p2) {
		assert p1 != null : "p1 should not be null";
		assert p2 != null : "p2 should not be null";
		return (p2.x - p1.x) * 1.5;
	}
	...
}

문서화가 잘 되어 코드 읽기 편하지만 문제를 해결하지는 못한다. 누군가 null 을 전달하면 여전히 실행 오류가 발생한다. 대다수 프로그래밍 언어는 호출자가 실수로넘기는 null 을 적절히 처리하는 방법이 없다. 그러면 애초에 null 을 넘기지 못하도록 금지하는 정책이 합리적이다. 즉, 인수로 null 이 넘어오면 코드에 문제가 있다는 말이다. 이런 정책을 따르면 그 만큼 부주의 한 실수를 저지를 확률도 작아진다.

→ NonNull annotaiton 같은것을 활용.. 하면 IDE에서 알려줄 수 있음.

결론

깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다. 이 둘은 상충하는 목표가 아니다. 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드를 작성할 수 있다. 오류 처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.

클린코드 6장 객체와 자료 구조

 변수를 private로 정하는 이유가 있음. 남들이 변수에 의존하지 않게 만들고 싶어서 임.

그렇다면 왜 모든 변수에 대해서 get / set 함수를 만들까

자료 추상화

변수를 private 로 하더라도 get / set 함수를 제공하면 구현을 외부로 노출하는 셈이 됨.

구현을 감추려면 추상화가 필요함.

아래 코드를 보면 연료 상태를 구체적인 숫자값으로 알려주는 인터페이스와 백분율이라는 추상적인 개념으로 알려주는 인터페이스가 있다. 그 중에 더 좋은 것은 추상적인 개념으로 알려주는 인터페이스다. → 자료를 세세하게 공개하는 것보다 추상적인 개념으로 표현하는 좋다.

인터페이스와 조회/설정 함수만으로는 추상화가 이루어지지 않기 때문에 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야한다.

구체적인 Vehicle 클래스

public interface Vehicle {
	double getFuelTankCapacityInGallons();
	double getGallonsOfGasoline();
}

추상적인 Vehicle 클래스

public interface Vehicle {
	double getPercentFuelRemaining();
}

자료/객체 비대칭

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

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

둘은 상반대 되는 개념이다.. 객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉬우며, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.

복잡한 시스템을 짜다 보면 새로운 함수가 아니라 새로운 자료 타입이 필요한 경우가 생긴다 → 클래스와 객체 지향 기법이 가장 적합하다.

반면, 새로운 자료 타입이 아니라 새로운 함수가 필요한 경우도 생긴다. → 절차적인 코드와 자료 구조가 좀 더 적합하다.

  • 분별 있는 프로그래머는 모든 것이 객체라는 생간이 미신임을 안다. 때로는 자료 구조와 절차적인 코드가 가장 적합한 상황도 있다.

디미터(데메테르) 법칙

데메테르의 법칙은 노스이스턴 대학교에서 개발되었습니다. 이 법칙은 그리스의 수확의 여신 데메테르의 이름을 딴 데메테르 프로젝트의 이름을 따서 명명되었습니다. 발음에 대해서는 의견이 분분하지만, 정확한 발음은 두 번째 음절을 강조하는 것이니 저희를 믿으셔도 됩니다.

디미터 법칙(Law of Demeter)은 데메테르 법칙이라고도 불리며 줄여서 LoD라고도 불립니다. 이 법칙은 "최소한의 지식 원칙(The Principle of Least Knowledge)으로 알려져 있으며, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 것을 의미합니다.

실제로 Demeter라는 프로젝트를 진행하던 개발자들은 어떤 객체가 다른 객체에 대해 지나치게 많은 정보를 알고 있다 보니, 서로에 대한 결합도가 높아지고 이로 인해 좋지 못한 설계를 야기한다는 사실을 발견하게 됩니다. 그래서 이를 개선하고자 다른 객체에게 어떠한 자료(내부 구조)를 가지고 있는지 숨기고 함수를 통해 공개하게 만들었는데 이것이 바로 디미터 법칙입니다.

즉, 간단하게 말하면 여러개의 .(dot)을 최대한 사용하지 말라는 법칙으로 말할 수 있음

기차 충돌

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutionPath();

여러 객체가 한 줄로 이어진 기차처럼 보이기 때문에 기차 충돌이라 불린다.

조잡한 구조기 때문에 피하는 것이 좋다.

아래와 같이 나누는 것이 좋다.

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutionPath();

디미터 법칙을 위반하는지 여부는 ctxt, Options, ScratchDir 이 객체인지 자료구지인지에 따라 달렸다. 객체라면 내부 구조를 숨겨야 하므로 확실히 디터 법칙을 위반한다. 자료구조라면 위반한 것이 아니다.

** ctxt가 객체인 경우는 밑에서 설명

잡종 구조

때때로는 절반은 객체, 절반은 자료 구조인 잡종 구조가 나온다.

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

공개 조회/설정 함수는 비공개 변수를 그대로 노출한다. 덕택에 다른 함수가 절차적인 프로그래밍의 자료 구조 접근 방식처럼 비공개 변수를 사용하고 싶은 유혹에 빠지기 쉽상이다.

이런 잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵다. 양쪽 단점만 모아놓은 구조다.

프로그래머가 함수나 타입을 보호할지 공개할지 확신하지 못해(무지해서..) 어중간하게 내놓은 설계에 불과하다.

구조체 감추기

만약 ctxt, options, scratchDir 이 진짜 객체라면 줄줄이 사탕으로 엮어서는 안된다. (객체 내부 구조를 감춰야 하니까)

ctxt.getAbsolutePathOfScratchDirectoryOption();
ctxt.getScratchDirectoryOption().getAbsolutePath();

위 코드를 봤을 때 첫번째 방법은 ctxt 에서 공개해야하는 메서드가 너무 많아진다. 두번째 방식은 getScrachDirctoryOption() 이 객체가 아니라 자료구조를 반환한다고 가정한다. 어느 방법도 썩 내키지 않는다.

ctxt가 객체라면 뭔가를 하라고 말해야지 속을 드러내면 안된다. 임시 디렉터리의 정대 경로가 왜 필요한지. 어디에 쓸려고 하는지 알아야 한다.

목적이 “임시 파일을 생성하기 위해서” 라면 아래 와 같이 ctxt 임시 파일을 생성 시키면 어떨까?

BufferendOutputStream bos = ctxt.createScratchFileStream(classFileName);

객체 내부 구조를 드러내지 않으며, 해당 함수는 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없다.

자료 전달 객체

자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다. 이런 자료 구조체를 때로는 자료 전달 객체 (Data TRansfer Object, DTO) 라고 한다.

활성 레코드

활성화 레코드는 DTO 의 특수한 형태다. 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료 구조지만, 대개 save / find 와 같은 탐색 함수도 제공한다. 활성 레코드는 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과다.

불행히도 활성 레코드에 비즈니스 규칙 메서드를 추가해 이런 자료 자료구조를 객체로 취곱하는 개발자가 흔하다. 하지만 이는 바람직하지 않다. 이러면 자료구조도 아니고 객체도 아닌 잡종 구조가 나오기 때문이다.

해결책은 활성 레코드는 자료 구조로 취급하고, 비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성한다. (여기에서 내부 자료는 활성 레코드의 인스턴스일 가능성이 높다.)

결론

객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기 쉬운 반면, 기존 객체에 새 동작을 추가하기는 조금 어렵다. 자료 구조는 별다른 동작 없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료 구조를 추가하기 어렵다.

시스템을 구현할 때, 새로운 자료 타입을 춧가하는 유연성이 필요하면 객체가 더 적합하고, 다른 경우로 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 더 적합하다. 우수한 소프트웨어 개발자는 편견 없이 이 사실을 이해해 직면한 문제에 최적인 해결책을 선택한다.

클린코드 5장 형식 맞추기

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

코드 형식을 맞추기 위한 간단한 규칙을 정하고 그 규칙을 착실히 따라야 한다. 팀으로 일한다면 팀이 합의해 규칙을 정하고 모두가 그 규칙을 따라야 한다.

형식을 맞추는 목적

코드의 형식은 중요하다. 너무 중요해서 무시하기 어렵다.

하지만 융통성 없이 맹목적으로 따르면 안된다.

‘돌아가는 코드’가 전문 개발자의 일차적인 의무가 아니다. 오늘 구현한 기능이 다음 버전에서 바뀔 확률은 매우 높다. 오늘 구현한 코드 코드의 가독성은 앞으로 바뀔 코드 품질에 매우 지대한 영향을 미친다. 코드는 사라질지라도 개발자의 스타일과 규율은 사라지지 않는다.

적절한 행길이를 유지하라

소스 코드는 얼마나 길어야 적당할까?

대다수 클래스의 경우 500 줄을 넘지 않고 대부분 200줄 정도인 파일로도 커다란 시스템을 구축 할 수 있다.

반드시 지킬 엄격한 규칙은 아니지만 바람직한 규칙으로 삼으면 좋다.

일반적으로 큰 파일보다 작은 파일이 이해하기 쉽다.

신문 기사 처럼 작성하라

독자는 위에서 아래로 기사를 읽는다. 최상단에 기사를 몇 마디로 요약하는 표제가 나온다. 독자는 표제를 보고서 기사를 읽을지 말지 결정한다. 첫 문단은 전체 기사 내용을 요약한다.

소스 파일도 신문 기사와 비슷하게 작성한다. 이름은 간단하면서도 설명이 가능하게 짓는다.

이름만 보고도 올바른 모듈을 살펴보고 있는지 아닌지를 판단할 정도로 신경써서 짓는다. 소스 파일 첫 부분은 고차원 개념과 알고리즘을 설명한다. 아래로 내려갈수록 의도를 세세하게 묘사한다.

개념은 빈 행으로 분리하라

거의 모든 코드는 왼쪽에서 오른쪽으로 그리고 위에서 아래로 읽힌다.

각 행은 수식이나 절을 나타내고, 일련의 행 묶음은 완결된 생각 하나를 표현한다. 생각 사이에는 빈 행을 넣어 분리해야 마땅하다.

package com.jobkorea.service

import java.util.regex.*;
import com.jobkorea.model.Board;

public class BoardService {
	public void save(Board board) {
		...
	}

	public void delete(long boardId) {
		...
	}
}

세로 밀집도

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

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

public class ReportConfig {
	/**
	 * 리포터 리스너의 클래스 이름
	*/
	private String m_className;

	/**
	 * 리포터 리스너의 속성
	*/
	private List<Property> m_properties = new ArrayList<Property>();
	public void addProperty(Property property) {
		m_properties.add(property);
	}
}

위의 코드 보다는 아래 코드가 더 잘 읽힌다.

public class ReportConfig {
	private String m_className;
	private List<Property> m_properties = new ArrayList<Property>();

	public void addProperty(Property property) {
		m_properties.add(property);
	}
}

수직 거리

함수 연관 관계와 동작 방식을 이해하려고 이 함수에서 저 함수로 오가며 소스 파일을 위 아래로 뒤지는 등 뺑뺑이를 돌았으나 결국은 미로 같은 코드 때문에 혼란만 가중된 경험이 있을 것이다.

함수나 변수나 정의된 코드를 찾으려 상속 관계를 줄줄이 거슬러 올라간 형험도 있을 것이다.

서로 밀접한 개념은 세로로 가까이 둬야 한다. (물론 두 개념이 서로 다른 파일에 속한다면 규칙이 통하지 않는다. 하지만 타당한 근거가 없다면 서로 밀접한 개념은 한 파일에 속해야 마땅하다. 이게 바로 protected 변수르 피해야하는 이유 중 하나다.)

연관성이 깊은 두 개념이 멀리 떨어져 있으면 코드를 읽는 사람이 소스파일과 클래스를 여기저기 뒤지게 된다.

지역 변수:

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

인스턴스 변수:

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

잘 설계한 클래스는 클래스의많은 메서드가 인스턴스 변수를 사용하기 때문이다.

종속 함수:

한 삼수가 다른 함수를 호출한다면 두 함수는 세로로 가까이 배치한다. 또한 가능하다면 호출하는 함수를 호출되는 함수보다 먼저 배치한다. 그러면 프로그램이 자연스럽게 읽힌다. 모듈 전체의 가독성도 높아지고 호출되는 함수를 찾기도 좋아 진다.

개념적 유사성:

어떤 코드는 서로 끌어당긴다. 개념적인 친화도가 높기 때문이다. 친화도가 높을수록 코드를 가까이 배치한다.

종속 함수일 수도 있고, 개념적으로 비슷한 함수일 수 도 있고, 비슷한 동작을 하는 함수일 수도 있다.

세로 순서

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

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

가로 형식 맞추기

한 행은 가로로 얼마나 길어야할까?

대 다수가 20-60 자 사이고 80자가 넘어가는 것은 급격하게 감소한다.

옛날에는 80자 제한을 권고했지만 이것은 인위적이고 모니터가 커짐에 다라 100자나 120자에 달해도 나쁘지 않다. 그 이상은 솔직히 주의부족이다.

요즘은 모니터가 넓어지면서 200자 까지도 한 화면에 들어오지만 저자의 경우 120자가 기준이 되는 것이 바람직하다고 한다.

가로 공백과 밀집도

  • 변수에 할당 연산자에는 앞 뒤로 공백을 준다.
    • User user = userService.findByName(”xxxx”)
  • 함수 이름과 이어지는 괄호 사이에는 공백을 주지 않는다. → 함수와 인수는 서로 밀접하기 떄문…
    • public User findByName(String name) {
  • 함수 호출 시 인수 사이에는 공백을 준다.
    • boardRepository.save(”title”, “content”, “author”);
  • 연산자 우선순위에 따라 공백을 추가하는 경우도 있다. → 하지만 툴이 대부분 지원하지 않는다.
    • return bb - 4a*c;
    • 사견) 개인적으로는 괄호로 표현하는게 더 직관적일 것이라 생각함. return (b * b) - (4 * a * c);

가로 정렬

아래 코드 처럼 억지로 맞추는 것은 좋지 않다. 코드가 어뚱한 부분을 강조해 진짜 의도가 가려지기 때문이다.

Strign firstName = "수열";
String lastName  = "정"; 
String address   = "서울시 강서구";
String zipcode   = "15243";
Date birth       = new Date(1986, 12, 18);

그냥 아래 처럼 적어도 가독성에 문제가 생기지 않는다.

정렬이 필요할 정도로 목록이 길다면 문제는 목록 일이지 정렬 부족이 아니다. 코드 선언 부분이 길다면 클래스를 쪼개야 한다는 의미다.

Strign firstName = "수열";
String lastName = "정"; 
String address = "서울시 강서구";
String zipcode = "15243";
Date birth = new Date(1986, 12, 18);

들여쓰기

scope 로 이뤄진 계층을 표현하기 위해 들여쓰기를 한다.

들여쓰기한 파일은 구조가 한눈에 들어온다. 변수, 생성자 함수, 메서드가 금방 보인다.

들여쓰기 무시하기

때로는 1줄 짜리 if , while , for 같은 문일 때 들여쓰기를 하지 않는 유혹에 빠질 때가 있지만 원점으러 돌아가 들여쓰기로 표현한다.

if (count < 5) { return false; }
for (int i = 0; i < count; i++) sum(value, i);

보다는..

if (count < 5) {
	return false;
}
for (int i = 0; i < count; i++) {
	sum(value, i);
}

가짜 범위

때로는 빈 while 문이나 for 문 을 접한다. 이런 경우를 피하지 못할 때는 빈 블록을 오바로 들여쓰고 괄호로 감싼다. 아래와 같이 while 문 끝에 세미콜론(;) 하나를 살짝 덧붙인 코드로 수없이 골탕을 먹는다. 세미콜론(;_ 은 새 행에다 제대로 들여써서 넣어준다.

while (dis.read(buf, 0, readBufferSize) != -1);

보다는

while (dis.read(buf, 0, readBufferSize) != -1)
;

팀 규칙

팀 규칙이라는 제목은 말 장난이다.

프로그래머라면 각자 선호하는 규칙이 있따. 하지만 팀에 속한다면 자신이 선호해야 할 규칙은 바로 팀 규칙이다.

팀은 한가지 규ㅣㄱ에 합의해야 한다. 그리고 모든 팀원은 그 규칙을 따라야 한다. 그래야 소프트웨어가 일관적인 스타일을 보인다 .개개인이 따로국밥처럼 맘대로 짜대는 코드는 피해야 한다.

어디에 괄호를 넣을지, 들여쓰기는 몇 자로 할지, 클래스와 변수와 메서드 이름은 어떻게 지을지 등등을 결정해야 한다.

좋은 소프트웨어 시슽셈은 읽기 쉬운 ㅁ누서로 이뤄진다는 사실을 기억하기 바란다. 스타일은 일관적이고 매끄러워야 한다. 한 소스 파일에서 봤떤 형식이 다른 소스 파일에도 쓰이리라는 신뢰감을 독자에게 줘야 한다. 온갖 스타일을 뒤섞어 소스 코드르 필요 이상으로 복잡하게 만드는 실수는 반드시 피한다.

도서 내용 정리 - 스태프 엔지니어

개요 네이버 도서 정보:  https://bit.ly/3JzLvQr 개발자는 관리자로만 커리어를 쌓아야 하는가? 관리자가 아닌 기술 리더로 성장하는 길은 없을까? IT 업계가 계속 성장하면서 전에 없던 팀과 조직의 경계를 넘어서는 큰 문제를 다루게 되...