======
선도적인 실천자들로부터 수집된 프로그래머를 위한 지혜의 보석들.
Pearls of wisdom for programmers collected from leading practitioners.
이 문서는 '프로그래머가 알아야 할 97가지 사항' 프로젝트의 GitBook 버전입니다.
모든 콘텐츠는 Creative Commons Attribution-NonCommercial-ShareAlike 3.0 라이센스에 따라 라이센스가 부여됩니다. 이 책의 인쇄본은 Amazon.com에서 구입할 수 있습니다.
오류를 발견하거나 제안 사항이 있다면, 이슈를 생성하거나 풀 요청을 제출하여 저장소에 기여할 수 있습니다.
- 신중하게 행동하라
- 함수형 프로그래밍 원칙을 적용하라
- "사용자는 무엇을 할까?"라고 물어보라 (당신은 사용자가 아니다)
- 코딩 표준을 자동화하라
- 아름다움은 단순함에 있다
- 리팩터링하기 전에
- 공유에 유의하라
- 보이스카우트 규칙
- 다른 사람을 탓하기 전에 먼저 당신의 코드를 확인하라
- 신중하게 도구를 선택하라
- 도메인의 언어로 코딩하라
- 코드는 설계다
- 코드 레이아웃이 중요하다
- 코드 리뷰
- 추론하며 코딩하기1
- 주석에 대한 의견
- 코드가 말할 수 없는 것만 주석으로 달아라
- 지속적인 학습
- 편의성은 -ility가 아니다
- 일찍 그리고 자주 배포하라
- 비즈니스 예외를 기술 예외와 구분하라
- 의도적인 연습을 많이 하라
- 도메인 특화 언어
- 무언가를 망가뜨리는 것을 두려워하지 마라
- 테스트 데이터로 재치를 부리지 마라
- 그 에러를 무시하지 마라!
- 언어만 배울 뿐 아니라, 그 문화도 이해하라
- 프로그램을 직립 자세로 못 박지 마라
- "여기서 마법이 일어난다"에 의존하지 마라
- 자신을 반복하지 마라
- 그 코드를 건드리지 마라!
- 상태만이 아닌 행동을 캡슐화하라
- 부동소수점 숫자는 실수가 아니다
- 오픈 소스로 당신의 야망을 실현하라
- API 설계의 황금률
- 구루 신화
- 노력은 결실을 맺지 않는다1
- 버그 트래커 사용법
- 코드를 제거해서 개선하라
- 나를 설치하라
- 프로세스 간 통신이 애플리케이션 응답 시간에 미치는 영향
- 빌드를 깨끗하게 유지하라
- 명령줄 도구 사용법을 알아라
- 두 개 이상의 프로그래밍 언어를 잘 알아라
- 당신의 IDE를 알아라
- 당신의 한계를 알아라
- 다음 커밋을 알아라
- 크고 상호연결된 데이터는 데이터베이스에 속한다
- 외국어를 배워라
- 추정하는 법을 배워라
- "Hello, World"라고 말하는 법을 배워라
- 프로젝트가 스스로 말하게 하라
- 링커는 마법의 프로그램이 아니다
- 임시 솔루션의 수명
- 인터페이스를 올바르게 사용하기 쉽고 잘못 사용하기 어렵게 만들어라
- 보이지 않는 것을 더 보이게 만들어라
- 메시지 전달은 병렬 시스템에서 더 나은 확장성으로 이어진다
- 미래에 보내는 메시지
- 다형성을 위한 놓친 기회들
- 이상한 소식: 테스터들은 당신의 친구다
- 하나의 바이너리
- 오직 코드만이 진실을 말한다
- 빌드를 소유하라 (그리고 리팩터링하라)
- 프로그래밍을 짝으로 하고 몰입을 느껴라
- 원시 타입보다 도메인별 타입을 선호하라
- 에러를 방지하라
- 전문 프로그래머
- 모든 것을 버전 제어 하에 두어라
- 마우스를 내려놓고 키보드에서 떨어져라
- 코드를 읽어라
- 인문학을 읽어라
- 바퀴를 자주 재발명하라
- 싱글톤 패턴의 유혹에 저항하라
- 성능으로 가는 길은 더러운 코드 폭탄으로 뒤덮여 있다
- 단순함은 축소로부터 나온다
- 단일 책임 원칙
- 예스에서 시작하라
- 한 걸음 물러서서 자동화하라, 자동화하라, 자동화하라
- 코드 분석 도구를 활용하라
- 부수적 동작이 아닌 필요한 동작을 테스트하라
- 정확하고 구체적으로 테스트하라
- 잠들어 있는 동안 테스트하라 (그리고 주말에도)
- 테스트는 소프트웨어 개발의 엔지니어링 엄격함이다
- 상태로 생각하기
- 하나보다는 두 개의 머리가 좋다
- 두 개의 잘못이 옳은 것을 만들 수 있다 (그리고 고치기 어렵다)
- 친구들을 위한 우분투 코딩
- Unix 도구들은 당신의 친구다
- 올바른 알고리즘과 데이터 구조를 사용하라
- 장황한 로깅은 당신의 잠을 방해할 것이다
- WET는 성능 병목 지점을 희석시킨다
- 프로그래머와 테스터가 협력할 때
- 평생 유지보수해야 한다고 생각하며 코드를 작성하세요1
- 예시를 사용하여 작은 함수를 작성하라
- 사람들을 위한 테스트를 작성하라
- 코드에 대해 신경써야 한다
- 고객들은 자신들이 말하는 것을 의미하지 않는다
"당신이 무엇을 하든, 신중하게 행동하고 결과(consequences)를 고려하라" - 익명
이터레이션(iteration)의 초기에는 일정계획(schedule)이 얼마나 편안해 보이든, 언젠가는 어느 정도의 압박(pressure)에 처하는 것을 피할 수 없습니다. "제대로 하는 것"과 "빨리 하는 것" 사이에서 선택해야 할 때가 오면, 종종 나중에 돌아와서 수정하겠다는 생각을 하고 "빨리 하겠다"고 하는 것이 매력적일 수 있습니다. 자신과 팀, 고객에게 이런 약속을 할 때, 당신은 진심이었을겁니다. 그러나 다음 이터레이션에 새로운 문제가 생기면, 당신은 그 문제에 집중하게 됩니다. 이러한 연기된 작업은 기술적 부채(technical debt)로 알려져 있으며, 이는 당신의 친구가 아닙니다. 특히, Martin Fowler는 그의 기술 부채 사분면에서 이를 고의적인 기술적 부채(deliberate technical debt)라고 부르며, 이는 우발적인 기술적 부채(inadvertent technical debt)와 혼동되어서는 안 됩니다.
기술 부채는 빚과 같습니다: 단기적으로는 혜택을 보지만, 완전히 상환될 때까지 이자를 지불해야 합니다. 코드에서의 지름길(shortcuts)은 기능을 추가하거나 코드를 리팩터링(refactor)하기 어렵게 만듭니다. 이들은 결함(defects)과 깨지기 쉬운 테스트 케이스의 온상이 됩니다. 시간을 두고 방치하면 할수록 상황은 악화됩니다. 원래의 수정을 수행할 때쯤이면 원래 문제 위에 여러 가지 잘못된 설계 선택이 쌓여 코드의 리팩터링과 수정이 훨씬 더 어려워질 수 있습니다. 실제로 상황이 매우 심각해져서 수정을 해야만 할 때까지는 되돌아가서 수정하지 않는 경우가 많습니다. 그리고 그때는 그렇게 수정하기가 너무 어려워서 시간이나 위험을 감당할 수 없는 경우가 많습니다.
마감 기한을 맞추거나 기능의 얇은 슬라이스(thin slice)를 구현하기 위해 기술적 부채를 감수해야 하는 경우가 있습니다. 이러한 상황에 처하지 않도록 하세요. 그러나 상황이 절대로 그렇게 요구한다면, 그렇게 하세요. 하지만 (그리고 이것은 큰 '그러나'입니다) 기술적 부채를 추적하고 빠르게 갚아야 하며, 그렇지 않으면 상황이 급속도로 악화됩니다. 타협하기로 결정하는 즉시, 작업 카드(task card)를 작성하거나 문제 추적 시스템에 기록하여 잊히지 않도록 하세요.
다음 이터레이션에서 부채 상환을 일정에 포함하면 비용은 최소화될 것입니다. 빚을 갚지 않으면 이자가 쌓이게 되고 그 이자는 비용을 가시화하기 위해 추적되어야 합니다. 이는 프로젝트의 기술적 부채가 비즈니스 가치에 미치는 영향을 강조하고 상환의 적절한 우선순위를 가능하게 합니다. 이자를 계산하고 추적하는 방법의 선택은 특정 프로젝트에 따라 다르겠지만, 반드시 추적해야 합니다.
가능한 한 빨리 기술적 부채를 갚으세요. 그렇지 않으면 신중하지 못한 행동이 될 것입니다.
Seb Rose가 작성함.
함수형 프로그래밍은 최근 주류 프로그래밍 커뮤니티에서 재조명을 받았습니다. 그 이유 중 하나는 함수형 패러다임의 *창발하는 속성(emergent properties)*이 우리 산업의 다중 코어(multi-core)로의 전환이 가져오는 도전 과제를 해결하는 데 잘 어울리기 때문입니다. 그러나 그것이 중요한 응용은 맞지만, 이 글에서 당신에게 함수형 프로그래밍을 알아야 한다고 경고하는 이유는 아닙니다.
함수형 프로그래밍 패러다임에 대한 숙달은 다른 맥락에서 작성하는 코드의 품질을 크게 향상시킬 수 있습니다. 함수형 패러다임을 깊이 이해하고 적용한다면, 당신의 설계는 훨씬 더 높은 정도의 *참조 투명성(referential transparency)*을 나타낼 것입니다.
참조 투명성은 매우 바람직한 속성입니다: 이는 함수가 호출되는 위치와 시간에 관계없이 같은 입력이 주어질 때 일관되게 같은 결과를 산출함을 의미합니다. 즉, 함수 평가가 가변 상태(mutable state)의 부작용에 덜 의존하게 되며, 이상적으로는 전혀 의존하지 않아야 합니다.
명령형 코드에서 결함의 주요 원인은 가변 변수(mutable variables) 때문입니다. 이 글을 읽는 모든 사람은 특정 상황에서 어떤 값이 예상과 다르게 나타나는 이유를 조사해본 적이 있을 것입니다. 가시성 의미론(visibility semantics)은 이러한 악성 결함을 완화하는 데 도움을 줄 수 있지만, 그들의 진정한 원인은 지나친 가변성을 사용하는 설계에서 오는 것일 수 있습니다.
그리고 이와 관련하여 산업계에서 많은 도움을 받지는 못합니다. 객체 지향(object orientation) 소개는 그러한 설계를 암묵적으로 촉진합니다. 왜냐하면 상대적으로 장기 생존 객체들이 서로에 대해 변조자(mutable methods)를 호출하는 예시를 자주 보여주기 때문입니다. 하지만, 정교한 테스트 주도 설계(test-driven design)를 통해, 특히 "Mock Roles, not Objects"라는 원칙을 준수할 때, 불필요한 가변성을 제거하는 설계를 할 수 있습니다.
결과적으로 이러한 설계는 일반적으로 더 나은 책임 배분(responsibility allocation)을 갖고 있으며, 인수(arguments)를 전달받아 작동하는 더 많고 작은 함수로 구성됩니다. 이는 가변적인 멤버 변수를 참조하는 것보다 결함을 줄이고, 이러한 설계에서 불량 값이 도입된 위치를 찾기가 더 쉽기 때문에 디버깅이 더 간단합니다. 이는 훨씬 높은 정도의 참조 투명성으로 이어지며, 이러한 계산 모델이 일반적인 함수형 프로그래밍 언어를 배우는 것만큼 이 아이디어를 깊이 내재화하는 방법은 없습니다.
물론 이 접근 방식이 모든 상황에서 최적의 결과를 내는 것은 아닙니다. 예를 들어, 객체 지향 시스템에서는 이 스타일이 사용자 인터페이스 개발(user-interface development)보다 도메인 모델 개발(domain model development)에서 더 나은 결과를 가져오는 경우가 많습니다.
함수형 프로그래밍 패러다임을 마스터하여 다른 도메인에 배운 교훈을 신중하게 적용할 수 있도록 하세요. 당신의 객체 시스템은 (하나의 예로) 참조 투명성의 장점을 가지고 있으며, 많은 사람들이 믿게 하려는 것보다 기능적 대응체와 훨씬 더 가까워질 것입니다. 사실, 어떤 사람들은 함수형 프로그래밍과 객체 지향의 정점이 서로의 반영(reflection)1일 뿐이며, 컴퓨테이셔널의 음과 양(yin and yang)의 형태라고 주장하기도 합니다.
에드워드 가슨이 씀.
우리는 모두 다른 사람들이 우리와 비슷하게 생각한다고 가정하는 경향이 있습니다. 하지만 그렇지 않습니다. 심리학자들은 이를 거짓 합의 편향(false consensus bias)이라고 부릅니다. 사람들이 우리와 다르게 생각하거나 행동할 때, 우리는 (무의식적으로) 그들을 어떤 면에서 결함이 있는 것으로 간주하는 경향이 있습니다.
We all tend to assume that other people think like us. But they don't. Psychologists call this the false consensus bias. When people think or act differently to us, we're quite likely to label them (subconsciously) as defective in some way.
이 편향은 프로그래머들이 사용자 입장에서 바라보는 것이 왜 그렇게 어려운지를 설명합니다. 사용자들은 프로그래머처럼 생각하지 않습니다. 우선, 그들은 컴퓨터를 사용하는 데 훨씬 적은 시간을 할애합니다. 그들은 컴퓨터가 어떻게 작동하는지 알지도, 신경 쓰지도 않습니다. 이는 그들이 프로그래머에게 익숙한 문제 해결 기법을 활용할 수 없음을 의미합니다. 사용자들은 프로그래머들이 인터페이스를 다루고, 통과하고, 주의하는 데 사용하는 패턴과 신호를 인식하지 못합니다.
사용자가 어떻게 생각하는지를 알아보는 가장 좋은 방법은 그들을 관찰하는 것입니다. 사용자에게 당신이 개발 중인 소프트웨어와 유사한 소프트웨어를 사용하여 작업을 완료하도록 요청하세요. 작업이 실제로 중요한 것이어야 합니다: "숫자 열을 더하세요"도 괜찮지만, "지난달의 지출을 계산하세요"가 더 좋습니다. "이 스프레드시트 셀을 선택하고 아래에 SUM 공식을 입력할 수 있나요?"처럼 너무 구체적인 작업은 피하세요 — 그 질문에는 큰 단서가 있습니다. 사용자가 자신의 진행 과정을 이야기할 수 있도록 하세요. 중단하지 말고 도와주려 하지 마세요. "왜 그가 그렇게 하고 있을까?"와 "왜 그녀는 그렇게 하지 않을까?"를 계속 자문해 보세요.
가장 먼저 주목할 점은 사용자가 핵심 작업을 유사하게 수행한다는 것입니다. 그들은 작업을 동일한 순서로 완료하려고 하며, 같은 위치에서 같은 실수를 저지릅니다. 당신은 이 핵심 행동을 중심으로 설계를 해야 합니다. 사용자를 관찰하는 것은 디자인 회의와는 다릅니다. 사람들이 디자인 회의에서는 "사용자가 ...를 원할 경우 어떻게 하지?"라고 말하는 것을 듣는 경향이 있습니다. 이러한 질문은 정교한 기능과 사용자가 원하는 것에 대한 혼란을 초래합니다. 사용자를 관찰하면 이러한 혼란이 해소됩니다.
사용자가 막히는 모습(get stuck)을 보게 될겁니다. 당신은 무언가 막히게 되면, 주변을 살펴봅니다. 그러나 사용자가 막힐 때는, 그들의 집중이 좁아집니다. 그로 인해 화면의 다른 곳에 있는 해결책을 보기 어려워집니다. 이것은 도움말 텍스트가 열악한 사용자 인터페이스 디자인에 대한 미흡한 해결책인 이유 중 하나입니다. 지침이나 도움말 텍스트가 꼭 필요하다면, 반드시 문제 영역 바로 옆에 배치해야 합니다. 사용자의 좁은 집중력은 툴팁이 도움말 메뉴보다 더 유용한 이유입니다.
사용자는 애매하게 해결하려고(muddle through) 합니다. 그들은 복잡하더라도 작동하는 방법을 찾아 그것을 고수할 것입니다. 두세 개의 지름길보다는 하나의 분명한 방법을 제공하는 것이 더 좋습니다.
사용자가 말하는 것과 실제로 하는 것 사이에는 간극이 있다는 것을 발견할 것입니다. 이런 점에서, 사용자의 요구 사항을 수집하는 일반적인 방법이 그들에게 묻는 방법이라는 점은 걱정스럽습니다. 사용자 요구 사항을 포착하는 가장 좋은 방법은 사용자를 관찰하는 것이기 때문에, 사용자 관찰에 한 시간을 보내는 것은 그들이 원하는 것을 추측하는 데 하루를 보내는 것보다 더 많은 정보를 제공합니다.
아마도 당신도 이런 경험이 있을 것입니다. 프로젝트 초기에는 모든 사람이 좋은 의도를 가지고 있습니다. 이를 "새 프로젝트 결심"이라고 부르죠. 이런 결심들은 종종 문서로 작성되곤 합니다. 코드에 관한 것들은 프로젝트의 코딩 표준에 포함됩니다. 킥오프 미팅에서 리드 개발자가 문서를 살펴보고, 최선의 경우 모든 사람이 이를 따르려고 노력하겠다고 동의합니다. 하지만 프로젝트가 본격적으로 시작되면, 이런 좋은 의도들이 하나씩 포기되기 시작합니다. 프로젝트가 마침내 완료되면 코드는 엉망이 되어 있고, 아무도 어떻게 이렇게 되었는지 알지 못하는 것 같습니다.
언제 잘못된 걸까요? 아마도 킥오프 미팅 때부터였을 겁니다. 일부 프로젝트 구성원들은 주의를 기울이지 않았습니다. 다른 사람들은 요점을 이해하지 못했죠. 더 나쁜 경우, 일부는 동의하지 않았고 이미 자신만의 코딩 표준 반란을 계획하고 있었습니다. 마지막으로, 일부는 요점을 이해하고 동의했지만, 프로젝트의 압박이 너무 심해지자 뭔가를 포기해야 했습니다. 잘 포맷된 코드는 더 많은 기능을 원하는 고객에게 점수를 주지 않으니까요. 게다가 코딩 표준을 따르는 것은 자동화되지 않는다면 상당히 지루한 작업이 될 수 있습니다. 손으로 지저분한 클래스를 들여쓰기 해보면 직접 알 수 있을 것입니다.
하지만 이렇게 문제가 된다면, 애초에 왜 코딩 표준을 두려고 하는 걸까요? 코드를 일관된 방식으로 포맷하는 한 가지 이유는 누구도 자신만의 개인적인 방식으로 포맷함으로써 코드 조각을 "소유"할 수 없도록 하기 위해서입니다. 우리는 개발자들이 특정 안티패턴을 사용하는 것을 방지하여 일반적인 버그를 피하고 싶을 수도 있습니다. 전체적으로, 코딩 표준은 프로젝트에서 작업하기 쉽게 만들고 처음부터 끝까지 개발 속도를 유지하는 데 도움이 되어야 합니다. 따라서 모든 사람이 코딩 표준에 동의해야 합니다. 한 개발자는 코드를 들여쓰기 할 때 공백 3개를 사용하고 다른 개발자는 4개를 사용한다면 도움이 되지 않습니다.
코드 품질 보고서를 생성하고 코딩 표준을 문서화하고 유지하는 데 사용할 수 있는 풍부한 도구들이 존재하지만, 그것만으로는 완전한 해결책이 되지 않습니다. 가능한 한 자동화되고 강제되어야 합니다. 다음은 몇 가지 예시입니다:
- 코드 포맷팅이 빌드 프로세스의 일부가 되도록 하여, 모든 사람이 코드를 컴파일할 때마다 자동으로 실행되도록 하세요.
- 정적 코드 분석 도구를 사용하여 원하지 않는 안티패턴을 찾아 코드를 스캔하세요. 발견되면 빌드를 중단시키세요.
- 이런 도구들을 구성하는 방법을 배워서 프로젝트별 안티패턴을 스캔할 수 있도록 하세요.
- 테스트 커버리지를 측정하는 것뿐만 아니라 결과를 자동으로 확인하세요. 다시 말하지만, 테스트 커버리지가 너무 낮으면 빌드를 중단시키세요.
중요하다고 생각하는 모든 것에 대해 이렇게 해보세요. 정말로 신경 쓰는 모든 것을 자동화할 수는 없을 것입니다. 자동으로 플래그를 지정하거나 수정할 수 없는 것들의 경우, 자동화된 코딩 표준을 보완하는 가이드라인 세트로 간주하되, 당신과 동료들이 그것들을 성실하게 따르지 않을 수도 있다는 것을 받아들이세요.
마지막으로, 코딩 표준은 정적이기보다는 동적이어야 합니다. 프로젝트가 발전함에 따라 프로젝트의 요구사항이 변하고, 처음에는 현명해 보였던 것이 몇 달 후에는 반드시 현명하지 않을 수 있습니다.
Filip van Laenen가 작성함.
모든 소프트웨어 개발자가 알아두고 마음에 간직해야 할 특별히 훌륭한 인용구가 하나 있습니다:
스타일과 조화, 우아함과 좋은 리듬의 아름다움은 단순함에 의존한다. — 플라톤 1
저는 이것이 우리가 소프트웨어 개발자로서 추구해야 할 가치들을 한 문장으로 요약해준다고 생각합니다.
우리가 코드에서 추구하는 여러 가지가 있습니다:
- 가독성 (Readability)
- 유지 관리성 (Maintainability)
- 개발 속도 (Speed of development)
- 찾기 힘든 아름다움의 질 (The elusive quality of beauty)
플라톤은 이 모든 품질을 가능하게 하는 요소가 단순함이라고 말하고 있습니다.
아름다운 코드는 무엇일까요? 이것은 잠재적으로 매우 주관적인 질문입니다. 아름다움에 대한 인식은 개인의 배경에 크게 의존하며, 우리 모든 것에 대한 인식 또한 배경에 의존합니다. 예술 교육을 받은 사람들은 자연 과학 교육을 받은 사람들과는 다른 아름다움에 대한 인식을 가지고 있습니다. 예술 전공자들은 소프트웨어를 예술 작품과 비교하는 방식으로 아름다움에 접근하는 반면, 과학 전공자들은 대칭성과 황금 비율에 대해 이야기하며 사물을 공식으로 줄이려는 경향이 있습니다. 제 경험상, 단순함은 양쪽 모두의 주장 대부분의 기초가 됩니다.
여러분이 공부한 소스 코드를 생각해 보세요. 다른 사람의 코드를 공부할 시간을 낸 적이 없다면, 지금 바로 이 글을 멈추고 공부할 오픈 소스 코드를 찾아보세요. 진지하게! 정말로요! 당신이 선택한 언어로 잘 알려진 전문가가 작성한 코드를 위해 웹을 검색해 보세요.
다시 돌아오셨나요? 좋습니다. 우리가 어디까지 이야기했죠? 아, 맞다... 저에게 아름답다고 생각되는 코드에는 공통적인 여러 특성이 있다는 것을 알았습니다. 이 중에서 가장 중요한 것은 단순함입니다. 전체 애플리케이션이나 시스템이 아무리 복잡해도 개별 구성 요소는 단순하게 유지되어야 한다고 생각합니다. 단일 책임을 가진 간단한 객체와, 설명적인 이름을 가진 비슷하게 단순하고 집중된 메서드들로 구성되어야 합니다. 일부 사람들은 5~10줄의 짧은 메서드를 가지는 아이디어가 극단적이라고 생각하고, 일부 프로그래밍 언어는 이를 매우 어렵게 만들지만, 저는 그러한 간결함이 여전히 바람직한 목표라고 생각합니다.
결론적으로, 아름다운 코드는 단순한 코드입니다. 각 개별 부분은 단순한 책임과 시스템의 다른부분과의 단순한 관계로 유지됩니다. 이것이 우리가 시간이 지나도 유지 가능한 시스템을 유지할 수 있는 방법이고, 깨끗하고 단순하며 테스트 가능한 코드로 시스템 수명 동안 높은 개발 속도를 유지할 수 있는 방법입니다. 아름다움은 단순함에서 태어나고 단순함에서 발견됩니다.
Jørn Ølmheim 씀.
언젠가 모든 프로그래머는 기존 코드를 리팩터링해야 할 때가 옵니다. 하지만 그렇게 하기 전에 다음 사항들을 고려해 보세요. 이는 당신과 다른 사람들의 시간(그리고 고통)을 크게 절약해 줄 수 있습니다:
-
재구조화를 위한 최선의 접근법은 기존 코드베이스와 그 코드에 대해 작성된 테스트를 파악하는 것부터 시작됩니다. 이는 현재 코드의 강점과 약점을 이해하는 데 도움이 되어, 강점은 유지하면서 실수는 피할 수 있게 해줍니다. 우리 모두는 기존 시스템보다 더 잘할 수 있다고 생각합니다... 하지만 기존 시스템의 실수로부터 배우지 못해서 이전 버전보다 나을 것 없거나 심지어 더 나쁜 결과물을 만들어내게 될 때까지 말이죠.
-
모든 것을 다시 작성하고 싶은 유혹을 피하세요. 가능한 한 많은 코드를 재사용하는 것이 최선입니다. 아무리 추한 코드라도, 이미 테스트되고 검토되었습니다. 기존 코드를 버리는 것 — 특히 그것이 운영 환경에 있었다면 — 은 당신이 알지 못하는 특정 우회 방법과 버그 수정이 포함된 수개월(또는 수년)간의 테스트되고 실전에서 검증된 코드를 버리는 것을 의미합니다. 이를 고려하지 않으면, 당신이 작성하는 새 코드가 기존 코드에서 수정되었던 동일한 신비로운 버그들을 다시 보여줄 수 있습니다. 이는 수년간 얻어온 많은 시간, 노력, 그리고 지식을 낭비하게 될 것입니다.
-
하나의 거대한 변경보다는 많은 점진적 변경이 더 좋습니다. 점진적 변경은 테스트와 같은 피드백을 통해 시스템에 미치는 영향을 더 쉽게 측정할 수 있게 해줍니다. 변경을 한 후 백 개의 테스트 실패를 보는 것은 재미없는 일입니다. 이는 좌절감과 압박으로 이어져 결국 나쁜 결정을 초래할 수 있습니다. 몇 개의 테스트 실패는 다루기 쉽고 더 관리하기 쉬운 접근법을 제공합니다.
-
각 이터레이션 후에는 기존 테스트가 통과하는지 확인하는 것이 중요합니다. 기존 테스트가 당신이 만든 변경사항을 충분히 커버하지 못한다면 새로운 테스트를 추가하세요. 신중한 고려 없이 기존 코드의 테스트를 버리지 마세요. 표면적으로는 이런 테스트들 중 일부가 당신의 새로운 설계에 적용되지 않는 것처럼 보일 수 있지만, 이 특정 테스트가 왜 추가되었는지에 대한 이유를 깊이 파고드는 노력을 기울일 가치가 있습니다.
-
개인적 선호와 자아가 방해가 되어서는 안 됩니다. 뭔가 망가지지 않았다면, 왜 고치려고 할까요? 코드의 스타일이나 구조가 당신의 개인적 선호에 맞지 않는다는 것은 재구조화의 타당한 이유가 되지 않습니다. 이전 프로그래머보다 더 잘할 수 있다고 생각하는 것도 타당한 이유가 아닙니다.
-
새로운 기술은 리팩터링의 충분한 이유가 되지 않습니다. 리팩터링의 최악의 이유 중 하나는 현재 코드가 오늘날 우리가 가진 모든 멋진 기술보다 너무 뒤처져 있고, 새로운 언어나 프레임워크가 훨씬 더 우아하게 일을 처리할 수 있다고 믿는 것입니다. 비용-편익 분석이 새로운 언어나 프레임워크가 기능성, 유지보수성, 또는 생산성에서 상당한 개선을 가져올 것임을 보여주지 않는다면, 그대로 두는 것이 최선입니다.
-
인간은 실수를 한다는 것을 기억하세요. 재구조화가 항상 새로운 코드가 더 나을 것 — 또는 심지어 이전 시도만큼 좋을 것 — 을 보장하지는 않습니다. 나는 몇 번의 실패한 재구조화 시도를 보았고 참여했습니다. 아름답지는 않았지만, 그것이 인간적인 것이었습니다.
Rajith Attapattu가 작성함.
이것은 회사에서의 첫 번째 프로젝트였습니다. 저는 방금 학위를 마친 상태였고, 스스로를 증명하고 싶어 매일 늦게까지 남아 기존 코드를 살펴보았습니다. 첫 번째 기능을 작업하면서 제가 배운 모든 것을 적용하기 위해 더욱 신경썼습니다 — 주석 달기, 로깅, 가능하면 공통 코드를 라이브러리로 빼내기 등등. 제가 그렇게 준비가 되었던 코드 리뷰는 저를 무섭게 깨우치는 경험이었습니다 — 재사용은 반대당했습니다!
이게 어떻게 가능할까요? 대학 시절 내내 재사용은 양질의 소프트웨어 공학의 정수로 여겨졌습니다. 제가 읽었던 모든 기사, 교과서, 저를 가르쳤던 경험 많은 소프트웨어 전문가들. 이 모든 것이 잘못된 것일까요?
결국 제가 놓치고 있던 중요한 것이 있었습니다.
맥락(Context).
시스템의 매우 다른 두 부분이 동일한 방식으로 몇 가지 로직을 수행한다는 사실은 제가 생각했던 것보다 덜 중요했습니다. 제가 그 공통 코드의 라이브러리를 뽑아내기 전까지, 이 부분들은 서로 의존성이 없었습니다. 각 부분은 독립적으로 발전할 수 있었습니다. 각자는 시스템의 변화하는 비즈니스 환경의 필요에 맞게 자신의 로직을 변경할 수 있었습니다. 그 유사한 코드의 네 줄은 우연한 일이었습니다 — 일시적인 이상 현상, 우연의 일치였습니다. 즉, 제가 등장하기 전까지는 말입니다.
제가 만든 공통 코드의 라이브러리는 각 발의 신발 끈을 서로 묶는 역할을 했습니다. 한 비즈니스 도메인에서의 단계는 다른 도메인과 먼저 동기화하지 않으면 진행할 수 없었습니다. 그 독립적인 기능의 유지보수 비용은 미미했지만, 공통 라이브러리는 훨씬 더 많은 테스트를 요구했습니다.
저는 시스템의 코드 라인 수는 줄였지만, 의존성의 수는 늘렸습니다. 이러한 의존성의 맥락은 매우 중요합니다 — 만약 그 의존성이 국한되었다면, 그것은 정당화될 수 있었고 긍정적인 가치를 가질 수 있었을 것입니다. 이러한 의존성이 제대로 통제되지 않으면, 그 덩굴손이 시스템의 더 큰 문제들(concerns)을 얽어지게 만들지만 코드 자체는 괜찮아 보입니다.
이러한 실수는 근본적으로 좋은 아이디어처럼 들리기 때문에 교묘합니다. 올바른 맥락에서 적용되었을 때, 이러한 기술은 가치가 있습니다. 잘못된 맥락에서는 가치를 증가시키기보다는 비용만 증가시킵니다. 다양한 부분이 사용될 맥락에 대한 전혀 지식 없이 기존 코드베이스에 들어갈 때, 요즘 저는 공유되는 내용에 대해 훨씬 더 주의 깊습니다.
공유에 주의하라. 당신의 맥락을 점검하라. 그때 비로소 진행하라.
Udi Dahan이 씀
보이스카우트에는 다음과 같은 규칙이 있습니다: "항상 캠프장을 발견했을 때보다 더 깨끗하게 두고 떠나라." 만약 땅에서 쓰레기를 발견하면, 누가 그 쓰레기를 만들었는지에 관계없이 치워야 합니다. 다음에 올 캠핑객들을 위해 의도적으로 환경을 개선하는 것입니다. 실제로 스카우팅의 아버지인 로버트 스티븐슨 스미스 베이든-파웰이 쓴 이 규칙의 원래 형태는 "이 세상을 당신이 발견했을 때보다 조금이라도 더 나은 곳으로 만들고 떠나도록 노력하라"였습니다.
만약 우리가 코드에서도 비슷한 규칙을 따른다면 어떨까요: "모듈을 체크아웃했을 때보다 항상 더 깨끗하게 체크인하라." 원래 작성자가 누구든 상관없이, 아무리 작은 것이라도 모듈을 개선하기 위해 항상 어떤 노력을 기울인다면 어떨까요? 그 결과는 무엇일까요?
만약 우리 모두가 이 간단한 규칙을 따른다면, 소프트웨어 시스템의 끊임없는 악화를 끝낼 수 있을 것이라고 생각합니다. 대신, 우리의 시스템은 진화하면서 점진적으로 더 나아질 것입니다. 또한 개인이 자신의 작은 부분만 돌보는 것이 아니라, 팀이 시스템 전체를 돌보는 것을 보게 될 것입니다.
이 규칙이 너무 많은 것을 요구한다고는 생각하지 않습니다. 체크인하기 전에 모든 모듈을 완벽하게 만들 필요는 없습니다. 단순히 체크아웃했을 때보다 조금이라도 더 나아지게 만들면 됩니다. 물론 이는 모듈에 추가하는 모든 코드가 깨끗해야 한다는 의미입니다. 또한 모듈을 다시 체크인하기 전에 적어도 한 가지 다른 것을 정리해야 한다는 의미이기도 합니다. 변수 이름 하나를 개선하거나, 긴 함수 하나를 두 개의 작은 함수로 나눌 수도 있습니다. 순환 종속성을 깨거나, 정책을 세부사항으로부터 분리하기 위한 인터페이스를 추가할 수도 있습니다.
솔직히, 이는 저에게는 일반적인 예의처럼 들립니다 — 화장실을 사용한 후 손을 씻거나, 쓰레기를 바닥에 버리는 대신 쓰레기통에 넣는 것과 같은 말이죠. 실제로 코드에 지저분함을 남기는 행위는 쓰레기 투기만큼 사회적으로 용납할 수 없는 것이어야 합니다. 그냥 하지 않는 것이어야 합니다.
하지만 그것보다 더한 의미가 있습니다. 자신의 코드를 돌보는 것과 팀의 코드를 돌보는 것은 전혀 다른 일입니다. 팀은 서로 도우며, 서로의 뒤를 치워줍니다. 그들은 자신에게만 좋은 것이 아니라 모든 사람에게 좋기 때문에 보이스카우트 규칙을 따릅니다.
Uncle Bob이 작성함.
개발자들 — 우리 모두가! — 은 종종 자신의 코드가 망가졌다는 사실을 믿기 어려워합니다. 그럴 리가 없으니, 한 번만은 컴파일러가 망가진 게 틀림없다고 생각하죠.
하지만 실제로는 컴파일러, 인터프리터, 운영체제, 애플리케이션 서버, 데이터베이스, 메모리 관리자, 또는 다른 시스템 소프트웨어의 버그로 인해 코드가 망가지는 것은 매우(매우) 드문 일입니다. 물론 이런 버그들은 존재하지만, 우리가 믿고 싶어하는 것보다 훨씬 드뭅니다.
한 번은 컴파일러 버그로 인해 루프 변수가 최적화로 인해 사라지는 진짜 문제를 겪은 적이 있었지만, 컴파일러나 운영체제에 버그가 있다고 상상한 횟수는 훨씬 더 많았습니다. 그 과정에서 많은 시간과 지원 시간, 그리고 관리 시간을 낭비했으며, 결국 제 실수였음이 밝혀질 때마다 조금씩 바보같다고 느꼈습니다.
도구가 널리 사용되고, 성숙하며, 다양한 기술 스택에서 사용된다고 가정한다면, 품질을 의심할 이유는 거의 없습니다. 물론 도구가 초기 릴리스이거나, 전 세계적으로 소수의 사람들만 사용하거나, 거의 다운로드되지 않는 버전 0.1의 오픈 소스 소프트웨어라면, 소프트웨어를 의심할 만한 충분한 이유가 있을 수 있습니다. (마찬가지로, 상용 소프트웨어의 알파 버전도 의심스러울 수 있습니다.)
컴파일러 버그가 얼마나 드문지를 고려할 때, 컴파일러가 틀렸다는 것을 증명하는 것보다는 코드에서 오류를 찾는 데 시간과 에너지를 투자하는 것이 훨씬 낫습니다. 모든 일반적인 디버깅 조언이 적용되므로, 문제를 격리하고, 호출을 스텁으로 만들고, 테스트로 둘러싸세요. 호출 규약, 공유 라이브러리, 버전 번호를 확인하고; 다른 사람에게 설명해보고; 스택 손상과 변수 타입 불일치를 주의깊게 살펴보세요. 다른 머신과 디버그 및 릴리스 같은 다른 빌드 구성에서 코드를 시도해보세요.
자신의 가정과 다른 사람들의 가정을 의심해보세요. 다른 벤더의 도구들은 서로 다른 가정을 내장하고 있을 수 있습니다 — 같은 벤더의 다른 도구들도 마찬가지일 수 있습니다.
다른 사람이 당신이 재현할 수 없는 문제를 보고할 때는, 가서 그들이 무엇을 하고 있는지 보세요. 그들이 당신이 생각해보지 못한 일을 하고 있거나 다른 순서로 뭔가를 하고 있을 수 있습니다.
개인적인 규칙으로, 찾아낼 수 없는 버그가 있고 컴파일러 문제라고 생각하기 시작한다면, 스택 손상을 찾아볼 때입니다. 특히 추적 코드를 추가했을 때 문제가 이리저리 움직인다면 더욱 그렇습니다.
멀티스레드 문제는 머리를 희끗희끗하게 만들고 기계에 소리를 지르게 만드는 또 다른 버그의 원천입니다. 단순한 코드를 선호하라는 모든 권장사항은 시스템이 멀티스레드일 때 배가됩니다. 디버깅과 단위 테스트는 이런 버그를 일관성 있게 찾는 데 의존할 수 없으므로, 설계의 단순함이 가장 중요합니다.
따라서 컴파일러를 탓하기 전에, 셜록 홈즈의 조언을 기억하세요: "불가능한 것을 제거하면, 아무리 개연성이 없더라도 남는 것이 진실이어야 한다." 그리고 이를 더크 젠틀리의 말보다 선호하세요: "개연성이 없는 것을 제거하면, 아무리 불가능하더라도 남는 것이 진실이어야 한다."
Allan Kelly가 작성함.
현대의 애플리케이션은 처음부터 구축되는 경우가 매우 드뭅니다. 여러 가지 좋은 이유로 기존 도구들 — 컴포넌트, 라이브러리, 프레임워크 — 을 사용하여 조립됩니다:
-
애플리케이션은 크기, 복잡성, 정교함에서 증가하는 반면, 개발할 수 있는 시간은 점점 짧아집니다. 개발자들이 인프라 코드보다는 비즈니스 도메인 코드 작성에 집중할 수 있다면 개발자의 시간과 지능을 더 잘 활용할 수 있습니다.
-
널리 사용되는 컴포넌트와 프레임워크는 사내에서 개발된 것들보다 버그가 적을 가능성이 높습니다.
-
웹에서 무료로 이용할 수 있는 고품질 소프트웨어가 많이 있어, 개발 비용이 낮아지고 필요한 관심과 전문성을 가진 개발자를 찾을 가능성이 높아집니다.
-
소프트웨어 생산과 유지보수는 인력 집약적인 작업이므로, 구매하는 것이 구축하는 것보다 저렴할 수 있습니다.
하지만 애플리케이션에 적합한 도구의 조합을 선택하는 것은 어떤 생각이 필요한 까다로운 일이 될 수 있습니다. 실제로 선택을 할 때 염두에 두어야 할 몇 가지가 있습니다:
-
서로 다른 도구들은 그들의 맥락에 대해 서로 다른 가정에 의존할 수 있습니다 — 예를 들어, 주변 인프라, 제어 모델, 데이터 모델, 통신 프로토콜 등 — 이는 애플리케이션과 도구 사이의 아키텍처 불일치로 이어질 수 있습니다. 이런 불일치는 코드를 필요 이상으로 복잡하게 만드는 해킹과 우회 방법들로 이어집니다.
-
서로 다른 도구들은 서로 다른 생명주기를 가지며, 그 중 하나를 업그레이드하는 것이 극도로 어렵고 시간이 많이 걸리는 작업이 될 수 있습니다. 새로운 기능, 설계 변경, 또는 심지어 버그 수정이 다른 도구들과의 비호환성을 야기할 수 있기 때문입니다. 도구의 수가 많을수록 문제는 더 심각해질 수 있습니다.
-
일부 도구들은 상당한 구성이 필요하며, 종종 하나 이상의 XML 파일을 통해 이루어지는데, 이는 매우 빠르게 통제 불능 상태가 될 수 있습니다. 애플리케이션이 모든 것이 XML로 작성되고 어떤 프로그래밍 언어로 된 몇 줄의 이상한 코드가 있는 것처럼 보일 수 있습니다. 구성의 복잡성은 애플리케이션을 유지보수하고 확장하기 어렵게 만들 것입니다.
-
벤더 종속은 특정 벤더 제품에 크게 의존하는 코드가 여러 측면에서 제약을 받게 될 때 발생합니다: 유지보수성, 성능, 진화 능력, 가격 등.
-
무료 소프트웨어를 사용할 계획이라면, 결국 그렇게 무료가 아님을 발견할 수 있습니다. 상용 지원을 구매해야 할 수도 있으며, 이것이 반드시 저렴할 것은 아닙니다.
-
무료 소프트웨어라 하더라도 라이선스 조건은 중요합니다. 예를 들어, 일부 회사에서는 바이럴 특성 때문에 GNU 라이선스 조건으로 라이선스된 소프트웨어를 사용하는 것이 받아들여지지 않습니다 — 즉, 그것으로 개발된 소프트웨어는 소스 코드와 함께 배포되어야 합니다.
이러한 문제들을 완화하기 위한 저의 개인적인 전략은 절대적으로 필요한 도구들만 사용하여 작게 시작하는 것입니다. 보통 초기 초점은 저수준 인프라 프로그래밍(과 문제들)에 참여할 필요를 제거하는 것입니다. 예를 들어, 분산 애플리케이션에서 원시 소켓을 사용하는 대신 미들웨어를 사용하는 것입니다. 그리고 필요하면 더 추가합니다. 또한 인터페이스와 계층화를 통해 외부 도구들을 비즈니스 도메인 객체들로부터 격리시키는 경향이 있어서, 필요할 때 적은 고통으로 도구를 변경할 수 있습니다. 이 접근법의 긍정적인 부작용은 일반적으로 원래 예상했던 것보다 더 적은 외부 도구를 사용하는 더 작은 애플리케이션을 얻게 된다는 것입니다.
Giovanni Asproni가 작성함.
두 개의 코드베이스를 상상해보세요. 하나에서 당신은 다음과 같은 코드를 발견합니다:
if (portfolioIdsByTraderId.get(trader.getId())
.containsKey(portfolio.getId())) {...}
머리를 긁적이며, 이 코드가 무엇을 위한 것인지 궁금해합니다. trader 객체에서 ID를 가져와서, 그것을 사용해 맵-of-맵으로 보이는 것에서 맵을 가져오고, 그 다음 portfolio 객체의 다른 ID가 내부 맵에 존재하는지 보는 것 같습니다. 더 머리를 긁적입니다. portfolioIdsByTraderId의 선언을 찾아보고 다음을 발견합니다:
Map<int, Map<int, int>> portfolioIdsByTraderId;
점차 트레이더가 특정 포트폴리오에 접근할 수 있는지와 관련된 것일 수도 있다는 것을 깨닫게 됩니다. 그리고 물론 동일한 검색 조각 — 또는 더 가능성 높게는 비슷하지만-미묘하게-다른 코드 조각 — 을 트레이더가 특정 포트폴리오에 접근할 수 있는지 신경 쓰는 곳마다 발견하게 될 것입니다.
다른 코드베이스에서는 다음과 같은 것을 발견합니다:
if (trader.canView(portfolio)) {...}
머리를 긁적일 필요가 없습니다. 트레이더가 어떻게 아는지 알 필요가 없습니다. 아마도 어딘가 내부에 이런 맵-of-맵 중 하나가 숨어있을 것입니다. 하지만 그것은 트레이더의 일이지, 당신의 일이 아닙니다.
이제 이 두 코드베이스 중 어디서 작업하고 싶으시겠습니까?
옛날에는 매우 기본적인 데이터 구조만 있었습니다: 비트와 바이트 그리고 문자들(실제로는 바이트이지만 우리는 그것들이 글자와 구두점이라고 가정했습니다). 십진수는 조금 까다로웠는데 우리의 십진법 숫자가 이진법에서는 잘 작동하지 않았기 때문에, 여러 크기의 부동소수점 타입이 있었습니다. 그 다음에는 배열과 문자열(실제로는 서로 다른 배열들)이 나왔습니다. 그리고 스택과 큐와 해시와 연결 리스트와 스킵 리스트 그리고 현실 세계에는 존재하지 않는 많은 다른 흥미로운 데이터 구조들이 있었습니다. "컴퓨터 과학"은 현실 세계를 우리의 제한적인 데이터 구조에 매핑하는 데 많은 노력을 들이는 것이었습니다. 진짜 구루들은 심지어 그들이 어떻게 그것을 했는지 기억할 수도 있었습니다.
그런 다음 사용자 정의 타입을 얻었습니다! 좋아요, 이것은 새로운 소식은 아니지만, 게임을 어느 정도 바꿔놓습니다. 도메인에 트레이더와 포트폴리오 같은 개념이 포함되어 있다면, Trader와 Portfolio라고 불리는 타입으로 그것들을 모델링할 수 있습니다. 하지만 이보다 더 중요한 것은, 도메인 용어를 사용해서 그들 사이의 관계도 모델링할 수 있다는 것입니다.
도메인 용어를 사용해서 코딩하지 않는다면, 여기 있는 이 int는 트레이더를 식별하는 방법을 의미하는 반면, 저기 있는 저 int는 포트폴리오를 식별하는 방법을 의미한다는 암묵적인(읽기: 비밀스러운) 이해를 만들어내고 있는 것입니다. (그것들을 섞지 않는 것이 최선입니다!) 그리고 비즈니스 개념("일부 트레이더는 일부 포트폴리오를 볼 수 없습니다 — 불법입니다")을 알고리즘 조각, 예를 들어 키의 맵에서의 존재 관계로 표현한다면, 감사와 컴플라이언스 담당자들에게 호의를 베푸는 것이 아닙니다.
다음에 올 프로그래머는 비밀을 알지 못할 수도 있으니, 왜 명시적으로 만들지 않을까요? 키를 다른 키에 대한 검색으로 사용해서 존재 검사를 수행하는 것은 그리 명백하지 않습니다. 누군가가 그곳이 이해충돌을 방지하는 비즈니스 규칙이 구현된 곳이라고 어떻게 직감할 수 있겠습니까?
코드에서 도메인 개념을 명시적으로 만드는 것은 다른 프로그래머들이 알고리즘을 도메인에 대해 이해하는 것에 끼워맞추려고 시도하는 것보다 훨씬 쉽게 코드의 의도를 파악할 수 있다는 것을 의미합니다. 또한 도메인 모델이 진화할 때 — 도메인에 대한 이해가 깊어지면서 진화할 것입니다 — 코드를 진화시킬 수 있는 좋은 위치에 있다는 것을 의미합니다. 좋은 캡슐화와 결합되면, 규칙이 한 곳에만 존재할 가능성이 높고, 의존적인 코드가 전혀 알지 못하는 상태에서 그것을 변경할 수 있습니다.
몇 달 후 코드를 작업하러 올 프로그래머가 당신에게 감사할 것입니다. 몇 달 후 올 프로그래머는 당신일 수도 있습니다.
Dan North가 작성함.
내일 아침 일어났을 때 건설 산업이 세기의 혁신을 이루었다고 상상해 보십시오. 수백만 대의 저렴하고 놀랍도록 빠른 로봇이 공기를 통해 자재를 제조할 수 있으며, 전력 비용은 거의 제로에 가깝고, 스스로 수리할 수 있습니다. 그리고 더 나아집니다: 건설 프로젝트에 대한 명확한 청사진이 주어지면, 로봇은 인간의 개입 없이 모든 것을 구축할 수 있으며, 비용은 거의 들지 않습니다.
건설 산업에 미칠 영향을 상상할 수 있지만, 상류(upstream)에서는 어떤 일이 발생할까요? 건설 비용이 거의 없어진다면 건축가와 디자이너의 행동은 어떻게 변화할까요? 오늘날 건설 전에는 물리적 모델과 컴퓨터 모델이 만들어지고 철저히 테스트됩니다. 건설 비용이 본질적으로 무료라면 우리는 신경 쓰지 않을까요? 설계가 붕괴해도 큰일이 아닙니다. 단지 무엇이 잘못되었는지 찾아내고 우리의 마법 로봇에게 또 다른 것을 만들도록 하면 됩니다. 더 나아가면 미완성 디자인은 완전한 목표의 근사치를 반복적으로 구축하고 개선함으로써 발전합니다. 평범한(casual) 관찰자는 미완성된 설계와 완성된 제품을 거의 구별할 수 없습니다.
우리의 일정 예측 능력은 사라질 것입니다. 건설 비용은 설계 비용보다 계산하기 더 쉽습니다 - 우리는 기둥을 설치하는 대략적인 비용과 필요한 기둥 수를 알고 있습니다. 예측 가능한 작업이 거의 없어진다면 덜 예측 가능한 설계 시간이 지배하게 됩니다. 결과는 더 빠르게 생산되지만, 신뢰할 수 있는 일정은 점점 불명확해집니다(results are produced more quickly, but reliable time lines slip away).
물론 경쟁 경제의 압박은 여전히 적용됩니다. 건설 비용이 제거되면, 신속하게 설계를 완성할 수 있는 회사가 시장에서 우위를 점하게 됩니다. 설계를 빠르게 완료하는 것이 엔지니어링 회사의 중심적인 추진력이 됩니다. 불가피하게, 설계에 대해 깊이 알지 못하는 누군가가 검증되지 않은 버전을 보고, 조기에 출시하는 시장적 이점을 보게 되면 "이것은 충분히 좋아 보인다"고 말할 것입니다.
생사가 걸린 프로젝트는 더 신중할 수 있지만, 많은 경우 소비자들은 불완전한 설계를 감수하는 법을 배웁니다. 회사들은 항상 우리의 마법 로봇을 보내 판매한 건물과 차량의 결함을 '수리'할 수 있습니다. 이러한 모든 것은 놀랍게도 역설적인 결론으로 이어집니다: 우리의 전제는 건설 비용의 극적인 감소였지만, 그 결과 품질이 나빠졌다는 것입니다.
위의 이야기가 소프트웨어에서도 반복되었다는 사실에 놀라지 않아야 합니다. 코드가 설계라는 것을 인정한다면 — 기계적인 과정이 아니라 창의적인 과정 — 소프트웨어 위기가 설명됩니다. 이제 우리는 설계 위기를 겪고 있습니다: 품질 있고 검증된 설계에 대한 수요가 그것을 창출할 수 있는 우리의 능력을 초과하고 있습니다. 불완전한 설계를 사용해야 한다는 압박이 강합니다.
다행히 이 모델은 우리가 더 나아질 수 있는 방법에 대한 단서도 제공합니다. 물리적 시뮬레이션은 자동화된 테스트에 해당합니다. 소프트웨어 설계는 철저한 테스트 배터리로 검증될 때까지 완료되지 않습니다. 이러한 테스트를 더욱 효과적으로 만들기 위해 우리는 대규모 시스템의 방대한 상태 공간(state space of large systems)을 억제하는 방법을 찾고 있습니다. 향상된 언어와 설계 프랙티스는 희망을 줍니다. 마지막으로, 피할 수 없는 사실이 하나 있습니다: 훌륭한 설계는 자신의 기술에 헌신하고 그 기법을 완벽히 터득한 훌륭한 설계자들에 의해 만들어집니다. 코드도 다르지 않습니다.
Ryan Brush 씀.
오래 전, 저는 COBOL 시스템에서 일했었습니다. 그곳에서는 직원들이 코드를 변경할 이유가 없다면 들여쓰기(indentation) 변경이 금지되어 있었습니다. 누군가 한 번 어떤 행의 첫 부분의 특수한 열들에 한 줄이 밀려 들어가서(a line slip into) 무언가가 깨진 적이 있었기 때문입니다. 이것은 레이아웃이 오해의 소지가 있을 때도 적용되었고, 실제로 그런 경우도 있었으므로, 우리는 코드를 정말 조심스럽게 읽어야 했습니다. 그 코드를 신뢰할 수 없었기 때문입니다. 이 정책으로 프로그래머들은 큰 대가를 치뤘습니다.
연구에 따르면 우리는 프로그래밍 시간을 코드 탐색과 읽기에 훨씬 더 많이 소모한다고 합니다. 즉, 변경해야 할 위치를 찾는 데 더 많은 시간을 쓰고, 실제로 타이핑하는 데는 그다지 시간을 사용하지 않는다는 것입니다. 그래서 이것이 우리가 최적화하고자 것은 바로 이 부분입니다.
-
스캔하기 쉬움. 사람들은 시각적 패턴 인식에 매우 능숙합니다(사바나에서 사자를 발견해야 했던 시절의 잔재이죠). 그래서 저는 도메인과 직접 관련이 없는 모든 것, 대부분의 상용 언어(commercial languages)가 가져오는 "우발적 복잡성(accidental complexity)"을 표준화하여 배경으로 희미하게 만드는 방법(fade into the background)으로 스스로를 도울 수 있습니다. 동일하게 동작하는 코드가 동일하게 보인다면, 저의 지각 시스템은 차이점을 쉽게 구별할 수 있도록 도와줄 것입니다. 그래서 저는 또한 클래스의 부분을 어떻게 레이아웃할지에 대한 규칙을 준수합니다: 상수, 필드, 퍼블릭 메서드, 프라이빗 메서드.
-
표현력 있는 레이아웃. 우리는 모두 코드를 가능한 한 명확하게 표현하기 위해 올바른 이름을 찾는 데 시간을 들여야 한다고 배웠습니다. 단순히 스텝을 나열하는 것이 아니라요 — 맞죠? 코드의 레이아웃도 이러한 표현력의 일부입니다. 첫 번째 단계는 팀이 기본을 위한 자동 포맷터에 합의하는 것이고, 그 후 코딩할 때 수동으로 조정할 수 있습니다. 의견 차이가 없으면 팀은 빠르게 공통의 "손으로 다듬은" 스타일에 수렴합니다. 포맷터는 제 의도를 이해할 수 없고(제가 한 번 포맷터를 만들어봐서 알죠), 저에게 더 중요한 것은 줄바꿈과 그룹핑 방식이 코드의 의도를 반영하는 것이지, 언어의 문법만 반영하는 것이 아닙니다. (Kevin McGuire 덕분에 저는 자동 코드 포맷터에 대한 의무에서 벗어났습니다.)
-
컴팩트한 형식. 화면에 더 많은 것을 표시할 수 있을수록, 스크롤하거나 파일을 전환하지 않고도 더 많은 것을 볼 수 있으며, 이는 제 머리 속에 저장해야 할 상태를 줄여 줍니다. 긴 절차 주석과 많은 공백은 8자 이름과 라인 프린터에 적합했지만, 지금 저는 구문 색상화 및 크로스 링크가 가능한 IDE에서 생활하고 있습니다. 픽셀이 제 제한 요소이므로 모든 픽셀이 코드 이해에 기여하게 하고 싶습니다. 레이아웃이 코드 이해에 도움이 되어야 하지만 그 이상은 원하지 않습니다.
비 프로그래머인 친구는 한 번 코드가 시처럼 보인다고 말한 적이 있습니다. 저는 정말 좋은 코드에서 그 느낌을 받습니다. 즉, 텍스트 속의 모든 것이 목적이 있고 아이디어를 이해하는 데 도움이 되도록 그곳에 존재한다는 것입니다. 불행히도 코드를 작성하는 것은 시를 쓰는 것과 같은 낭만적인 이미지는 없습니다.
코드 리뷰를 해야 합니다. 왜일까요? 코드 품질을 높이고 결함률을 줄이기 때문입니다. 하지만 당신이 생각하는 이유와는 반드시 같지 않을 수 있습니다.
이전에 리뷰와 관련해 좋지 않은 경험을 했을 수 있기 때문에, 많은 프로그래머들은 코드 리뷰를 싫어하는 경향이 있습니다. 모든 코드가 운영 환경에 배포되기 전에 공식적인 리뷰를 통과해야 한다고 요구하는 조직들을 본 적이 있습니다. 종종 이런 리뷰를 하는 것은 아키텍트나 리드 개발자인데, 이는 아키텍트가 모든 것을 리뷰한다고 설명할 수 있는 관행입니다. 이것이 그들의 소프트웨어 개발 프로세스 매뉴얼에 명시되어 있으므로, 프로그래머들은 준수해야 합니다. 그런 엄격하고 공식적인 프로세스가 필요한 조직이 있을 수도 있지만, 대부분은 그렇지 않습니다. 대부분의 조직에서 그런 접근법은 역효과를 낳습니다. 리뷰를 받는 사람들은 가석방 위원회에 의해 판단받는 것처럼 느낄 수 있습니다. 리뷰어는 코드를 읽을 시간과 시스템의 모든 세부사항을 최신 상태로 유지할 시간이 모두 필요합니다. 리뷰어는 이 프로세스에서 빠르게 병목이 될 수 있고, 프로세스는 곧 퇴화됩니다.
단순히 코드의 실수를 수정하는 대신, 코드 리뷰의 목적은 지식을 공유하고 공통 코딩 가이드라인을 확립하는 것이어야 합니다. 다른 프로그래머들과 코드를 공유하는 것은 집단적 코드 소유권을 가능하게 합니다. 무작위 팀 멤버가 나머지 팀과 함께 코드를 살펴보도록 하세요. 에러를 찾는 대신 코드를 배우고 이해하려고 시도함으로써 코드를 리뷰해야 합니다.
코드 리뷰 중에는 부드럽게 하세요. 코멘트가 건설적이지 신랄하지 않도록 확인하세요. 리뷰 미팅에서 조직적 선임자가 팀 멤버들 간의 코드 리뷰에 영향을 미치는 것을 피하기 위해 다른 리뷰 역할을 도입하세요. 역할의 예로는 한 리뷰어는 문서화에 집중하고, 다른 리뷰어는 예외에, 세 번째는 기능성을 보는 것이 있을 수 있습니다. 이 접근법은 팀 멤버들 간에 리뷰 부담을 분산시키는 데 도움이 됩니다.
매주 정기적인 코드 리뷰 날을 가지세요. 리뷰 미팅에 몇 시간을 보내세요. 간단한 라운드로빈 패턴으로 매 미팅마다 리뷰를 받을 사람을 순환시키세요. 매 리뷰 미팅마다 팀 멤버들 간의 역할도 바꾸는 것을 기억하세요. 코드 리뷰에 신입자들을 참여시키세요. 그들이 경험이 부족할 수 있지만, 그들의 신선한 대학 지식은 다른 관점을 제공할 수 있습니다. 경험과 지식을 위해 전문가들을 참여시키세요. 그들은 오류가 발생하기 쉬운 코드를 더 빠르고 정확하게 식별할 것입니다. 팀이 도구로 확인되는 코딩 규약을 가지고 있다면 코드 리뷰가 더 쉽게 진행될 것입니다. 그렇게 하면 코드 포맷팅이 코드 리뷰 미팅 중에 논의되지 않을 것입니다.
코드 리뷰를 재미있게 만드는 것이 아마도 성공에 가장 중요한 기여자일 것입니다. 리뷰는 리뷰하는 사람들에 관한 것입니다. 리뷰 미팅이 고통스럽거나 지루하다면 누구든 동기를 부여하기 어려울 것입니다. 주된 목적이 팀 멤버들 간의 지식 공유인 비공식적인 코드 리뷰로 만드세요. 비꼬는 코멘트는 밖에 두고 대신 케이크나 점심을 가져오세요.
Mattias Karlsson이 작성함.
추론하며 코딩하기1
소프트웨어의 정확성(correctness)에 대해 수작업으로 추론하려고 하면, 그 코드보다 긴 공식 증명(formal proof)이 나오게 되며, 이는 그 코드보다 오류를 가지고 있을 가능성이 더 큽니다. 자동화 도구가 바람직하지만, 항상 가능한 것은 아닙니다. 다음에서는 중간적 접근 방식(middle path)을 설명합니다: 정확성에 대해 반정형적(semi-formally)으로 추론하기(reasoning).
기본 접근(underlying approach)은 고려 대상인 모든 코드를 더 짧은 섹션들로 나누는 것입니다. 각 섹션은 단일 행, 예를 들어 함수 호출부터, 10행 미만의 블록들에 이르기까지 - 그리고 그 정확성에 대해 논의(arguing))합니다. 주장(arguments)은 동료 프로그래머인 악마의 대변인(devil's advocate)을 설득할 수 있을 만큼 강력해야 합니다.
각 섹션은 프로그램의 각 끝점(endpoint)에서 프로그램의 상태(state of the program) (즉, 프로그램 카운터와 모든 "살아 있는" 객체의 값)가 쉽게 설명할 수 있는 속성을 만족하도록 선택해야 하며, 이 섹션의 기능(상태 변환)은 단일 작업으로 설명하기 쉬워야 합니다. 이러한 조건들은 논리를 단순화하는 데 도움이 됩니다. 이러한 끝점 속성은 함수의 사전 조건(precondition), 사후 조건(postcondition), 반복문 및 클래스(그들의 인스턴스에 대한)의 *불변식(invariant)*와 같은 개념을 일반화합니다. 섹션들이 최대한 서로 독립적이도록 만들기 위해 노력하는(striving) 것은 추론을 단순화시켜 주며, 이러한 섹션이 변경될 때 필수적(indispensable)입니다.
잘 알려진, 그러나 아마도 덜 지켜지는 '좋은' 코딩 관행들은 논리를 더 쉽게 만들어줍니다. 따라서 코드를 논리적으로 다룰 생각만 해도 이미 더 나은 스타일과 구조를 생각하게 됩니다. 놀랍지 않게도, 이러한 대부분의 관행은 정적 코드 분석기를 통해 확인할 수 있습니다:
- 원거리 섹션의 상호 의존성을 극대화하는
goto문 사용을 피하세요. - 수정 가능한 전역 변수를 피하세요. 이는 이러한 변수를 사용하는 모든 섹션이 서로 의존하게 만듭니다.
- 각 변수의 범위는 가능한 한 작아야 합니다. 예를 들어, 지역 객체는 처음 사용하기 직전에 선언할 수 있습니다.
- 관련이 있을 때는 객체를 *불변(immutable)*으로 만드세요.
- 수평 및 수직으로 여백을 사용하여 코드를 읽기 쉽게 만드세요. 예를 들어, 관련 구조를 정렬하고 두 섹션을 구분하기 위해 빈 줄을 사용하는 것이 좋습니다.
- 객체, 타입, 함수 등에 대해 설명적이면서도 상대적으로 짧은 이름을 선택하여 코드를 자체 문서화하십시오.
- 중첩된 섹션이 필요하다면, 함수를 만드세요.
- 함수는 짧고 단일 작업에 집중해야 합니다. 옛날의 24행 제한은 여전히 유효합니다. 화면 크기와 해상도는 변화했지만, 1960년대 이후 인간 인지에 변화는 없었습니다.
- 함수는 매개변수를 적게 가져야 합니다(4개가 좋은 상한선입니다). 이것은 함수에 전달되는 데이터의 제한을 두지 않습니다: 관련 매개변수를 단일 객체로 그룹화하는 것은 *객체 불변성(Object Invariants)*에 도움이 되며, 그 일관성과 응집력과 같은 논리를 절약할 수 있습니다.
- 보다 일반적으로, 블록에서 라이브러리에 이르기까지 코드의 각 단위는 *좁은 인터페이스(narrow interface)*를 가져야 합니다. 통신이 감소하면 요구되는 논리가 줄어듭니다. 이는 내부 상태를 반환하는 getter가 불리하다는 것을 의미합니다 — 작업을 수행하기 위해 객체에 정보를 요청하지 마세요. 대신, 이미 가진 정보를 가지고 작업을 수행하도록 객체에 요청하세요. 즉, *캡슐화(encapsulation)*는 좁은 인터페이스에 관한 모든 것입니다.
- 클래스의 *불변성(invariants)*을 유지하기 위해서는 setter의 사용을 권장하지 않아야 합니다. setter는 객체 상태를 규제하는 불변성을 깨뜨릴 수있습니다.
정확성에 대한 논리적 접근 외에도, 코드를 논의하는 것은 코드에 대한 이해를 제공합니다. 여러분이 얻은 통찰을 모든 사람에게 알리세요.
대학교 첫 프로그래밍 수업에서, 선생님이 두 장의 BASIC 코딩 시트를 나누어 주었습니다. 칠판에는 "10개의 볼링 점수를 입력받아 평균을 구하는 프로그램을 작성하시오"라는 과제가 적혀 있었습니다. 그리고 선생님은 교실을 나가셨습니다. 이게 얼마나 어려울 수 있을까요? 최종 해답은 기억나지 않지만 FOR/NEXT 루프가 들어있었고 총 15줄을 넘지 않았을 것이 확실합니다. 코딩 시트는 — 이 글을 읽는 젊은 여러분들을 위해 설명하자면, 우리는 실제로 컴퓨터에 입력하기 전에 손으로 코드를 써서 작성했습니다 — 각각 약 70줄의 코드를 쓸 수 있었습니다. 선생님이 왜 두 장을 주었는지 매우 혼란스러웠습니다. 제 글씨는 항상 형편없었기 때문에, 스타일 점수를 몇 점이라도 더 받기를 바라며 두 번째 시트에 코드를 매우 깔끔하게 다시 써서 베꼈습니다.
놀랍게도, 다음 수업 시작에서 과제를 돌려받았을 때 겨우 통과하는 점수를 받았습니다. (이는 대학교 나머지 기간 동안 제게 있을 전조였습니다.) 깔끔하게 복사한 코드 맨 위에 흘린 글씨로 "주석이 없네요?"라고 적혀 있었습니다.
선생님과 제가 둘 다 프로그램이 무엇을 해야 하는지 안다는 것만으로는 충분하지 않았습니다. 과제의 목적 중 일부는 제 코드가 뒤에 올 다음 프로그래머에게 스스로를 설명해야 한다는 것을 가르치는 것이었습니다. 이는 제가 잊지 않은 교훈입니다.
주석은 악한 것이 아닙니다. 주석은 기본적인 분기나 루프 구조만큼 프로그래밍에 필요합니다. 대부분의 현대 언어들은 적절히 포맷된 주석을 파싱하여 자동으로 API 문서를 생성하는 javadoc과 유사한 도구를 가지고 있습니다. 이는 매우 좋은 시작이지만, 거의 충분하지 않습니다. 코드 내부에는 코드가 무엇을 해야 하는지에 대한 설명이 있어야 합니다. "쓰기 어려웠다면, 읽기도 어려워야 한다"는 오래된 격언으로 코딩하는 것은 고객, 고용주, 동료, 그리고 미래의 자신에게 해가 됩니다.
반면에, 주석을 달 때 너무 지나칠 수도 있습니다. 주석이 코드를 명확하게 하지만 모호하게 만들지는 않도록 확인하세요. 코드가 무엇을 성취해야 하는지 설명하는 관련 주석을 코드에 뿌려놓으세요. 헤더 주석은 어떤 프로그래머든 코드를 읽지 않고도 사용할 수 있을 만큼 충분한 정보를 제공해야 하며, 인라인 주석은 다음 개발자가 수정하거나 확장하는 데 도움이 되어야 합니다.
한 직장에서, 윗사람들이 내린 설계 결정에 동의하지 않았습니다. 젊은 프로그래머들이 종종 그렇듯 다소 건방지게 느껴져서, 그들의 설계를 사용하라고 지시한 이메일 텍스트를 파일의 헤더 주석 블록에 붙여넣었습니다. 알고 보니 이 특정 회사의 관리자들은 코드가 커밋될 때 실제로 검토했습니다. 그것이 경력 제한적 행동이라는 용어에 대한 첫 번째 소개였습니다.
Cal Evans가 작성함.
이론과 실무의 차이는 이론에서보다 실무에서 더 크다 — 이는 확실히 주석에도 적용되는 관찰입니다. 이론적으로, 코드에 주석을 다는 일반적인 아이디어는 가치있는 일처럼 들립니다: 독자에게 세부사항, 무슨 일이 일어나는지에 대한 설명을 제공하는 것입니다. 도움이 되는 것보다 더 도움이 될 수 있는 것이 무엇이겠습니까? 하지만 실무에서는 주석이 종종 골칫거리가 됩니다. 다른 형태의 글쓰기와 마찬가지로, 좋은 주석을 쓰는 것에는 기술이 필요합니다. 기술의 많은 부분은 언제 쓰지 말아야 하는지를 아는 것입니다.
코드가 잘못 형성되었을 때, 컴파일러, 인터프리터, 그리고 다른 도구들이 확실히 반대할 것입니다. 코드가 어떤 면에서 기능적으로 올바르지 않다면, 리뷰, 정적 분석, 테스트, 그리고 운영 환경에서의 일상적인 사용이 대부분의 버그를 찾아낼 것입니다. 하지만 주석은 어떨까요? The Elements of Programming Style에서 커니핸과 플라우거는 "주석이 틀렸다면 가치가 제로(또는 음수)다"라고 언급했습니다. 그런데도 그런 주석들은 코딩 에러가 결코 할 수 없는 방식으로 코드베이스에서 종종 어수선하게 남아 살아남습니다. 그들은 지속적인 산만함과 잘못된 정보의 원천을 제공하며, 프로그래머의 사고에 미묘하지만 지속적인 끌어내림을 줍니다.
기술적으로는 틀리지 않았지만 코드에 아무 가치를 더하지 않는 주석은 어떨까요? 그런 주석은 노이즈입니다. 코드를 앵무새처럼 따라하는 주석은 독자에게 아무것도 추가로 제공하지 않습니다 — 코드에서 한 번, 자연어로 다시 한 번 말하는 것이 그것을 더 진실하거나 더 실제적으로 만들지는 않습니다. 주석 처리된 코드는 실행 가능한 코드가 아니므로, 독자나 런타임 어느 쪽에도 유용한 효과를 주지 않습니다. 또한 매우 빠르게 오래됩니다. 버전 관련 주석과 주석 처리된 코드는 버전 관리와 히스토리의 문제를 다루려고 시도합니다. 이런 문제들은 이미 버전 관리 도구에 의해 (훨씬 더 효과적으로) 답이 주어졌습니다.
코드베이스에서 시끄러운 주석과 올바르지 않은 주석이 널리 퍼져 있으면 프로그래머들이 모든 주석을 무시하도록 격려합니다. 주석을 건너뛰거나 숨기기 위해 적극적인 조치를 취함으로써 말입니다. 프로그래머들은 수완이 풍부하며 손상으로 인식되는 모든 것을 우회할 것입니다: 주석을 접기; 주석과 배경이 같은 색이 되도록 색상 구성을 바꾸기; 주석을 필터링하는 스크립팅. 그런 프로그래머 독창성의 잘못된 적용으로부터 코드베이스를 구하고, 진정한 가치의 주석을 놓칠 위험을 줄이기 위해, 주석은 마치 코드인 것처럼 취급되어야 합니다. 각 주석은 독자에게 어떤 가치를 더해야 하며, 그렇지 않으면 제거되거나 다시 작성되어야 할 낭비입니다.
그렇다면 무엇이 가치로 자격을 갖출까요? 주석은 코드가 하지 않고 할 수 없는 무언가를 말해야 합니다. 코드 조각이 이미 말해야 할 것을 설명하는 주석은 코드가 스스로 말하도록 코드 구조나 코딩 규약을 바꾸라는 초대입니다. 불량한 메서드나 클래스 이름을 보상하는 대신, 그것들의 이름을 바꾸세요. 긴 함수에서 섹션들에 주석을 다는 대신, 이전 섹션들의 의도를 포착하는 이름을 가진 더 작은 함수들을 추출하세요. 코드를 통해 가능한 한 많이 표현하려고 노력하세요. 코드에서 표현할 수 있는 것과 전체적으로 표현하고 싶은 것 사이의 어떤 부족함이든 유용한 주석의 그럴듯한 후보가 됩니다. 코드가 말할 수 없는 것에 주석을 달되, 단순히 말하지 않는 것에는 달지 마세요.
Kevlin Henney가 작성함.
우리는 흥미로운 시대에 살고 있습니다. 개발이 전 세계에 분산되면서, 당신의 일을 할 수 있는 사람들이 많이 있다는 것을 알게 됩니다. 시장성을 유지하려면 계속 학습해야 합니다. 그렇지 않으면 공룡이 되어 같은 일에 갇혀 있다가, 언젠가는 더 이상 필요하지 않게 되거나 당신의 일이 더 저렴한 자원으로 아웃소싱될 것입니다.
그럼 어떻게 해야 할까요? 일부 고용주들은 기술 세트를 넓히기 위한 교육을 제공할 만큼 관대합니다. 다른 고용주들은 교육을 위한 시간이나 돈을 전혀 아끼지 못할 수도 있습니다. 안전하게 하려면, 자신의 교육에 대한 책임을 져야 합니다.
다음은 학습을 계속할 수 있는 방법들의 목록입니다. 이 중 많은 것들을 인터넷에서 무료로 찾을 수 있습니다:
- 책, 잡지, 블로그, 트위터 피드, 웹사이트를 읽으세요. 주제를 더 깊이 파고들고 싶다면, 메일링 리스트나 뉴스그룹에 가입하는 것을 고려해보세요.
- 정말로 기술에 몰입하고 싶다면, 직접 해보세요 — 코드를 작성해보세요.
- 항상 멘토와 함께 일하려고 노력하세요. 최고가 되는 것이 교육을 방해할 수 있기 때문입니다. 누구에게서든 뭔가를 배울 수 있지만, 당신보다 더 똑똑하거나 경험이 많은 사람에게서 훨씬 더 많이 배울 수 있습니다. 멘토를 찾을 수 없다면, 이직을 고려해보세요.
- 가상 멘토를 활용하세요. 웹에서 정말 좋아하는 저자와 개발자를 찾아서 그들이 쓰는 모든 것을 읽으세요. 그들의 블로그를 구독하세요.
- 사용하는 프레임워크와 라이브러리를 알아가세요. 뭔가가 어떻게 작동하는지 아는 것은 그것을 더 잘 사용하는 방법을 알게 해줍니다. 오픈 소스라면, 정말 운이 좋은 것입니다. 디버거를 사용해서 코드를 단계별로 실행하여 후드 아래에서 무슨 일이 일어나는지 보세요. 정말 똑똑한 사람들이 작성하고 검토한 코드를 볼 수 있을 것입니다.
- 실수를 하거나, 버그를 수정하거나, 문제에 부딪힐 때마다, 무슨 일이 일어났는지 정말로 이해하려고 노력하세요. 다른 누군가가 같은 문제에 부딪쳐서 웹 어딘가에 게시했을 가능성이 높습니다. 구글이 여기서 정말 유용합니다.
- 뭔가를 배우는 정말 좋은 방법은 그것을 가르치거나 발표하는 것입니다. 사람들이 당신의 말을 들을 것이고 질문을 할 것이라는 사실이 당신이 배우도록 강하게 동기를 부여할 것입니다. 직장에서 점심 시간 학습 모임이나, 사용자 그룹, 또는 지역 컨퍼런스를 시도해보세요.
- 관심 있는 언어, 기술, 또는 분야에 대한 스터디 그룹(패턴 커뮤니티 방식)이나 지역 사용자 그룹에 가입하거나 시작하세요.
- 컨퍼런스에 가세요. 갈 수 없다면, 많은 컨퍼런스가 그들의 발표를 온라인에 무료로 올립니다.
- 통근 시간이 긴가요? 팟캐스트를 들으세요.
- 코드베이스에 정적 분석 도구를 실행하거나 IDE의 경고를 본 적이 있나요? 그들이 무엇을 보고하는지, 왜 그런지 이해하세요.
- The Pragmatic Programmers의 조언을 따라 매년 새로운 언어를 배우세요. 적어도 새로운 기술이나 도구를 배우세요. 가지를 뻗어나가는 것은 현재 기술 스택에서 사용할 수 있는 새로운 아이디어를 줍니다.
- 배우는 모든 것이 기술에 관한 것일 필요는 없습니다. 요구사항을 더 잘 이해하고 비즈니스 문제를 해결하는 데 도움이 되도록 작업하고 있는 도메인을 배우세요. 더 생산적이 되는 방법 — 더 잘 일하는 방법 — 을 배우는 것도 좋은 선택입니다.
- 학교로 돌아가세요.
매트릭스의 네오가 가졌던 능력을 가져서 필요한 정보를 단순히 뇌에 다운로드할 수 있다면 좋겠지만, 우리는 그럴 수 없으므로 시간 투자가 필요할 것입니다. 깨어있는 모든 시간을 학습에 보낼 필요는 없습니다. 예를 들어 매주 조금씩이라도 아무것도 하지 않는 것보다는 낫습니다. 일 밖에도 인생이 있습니다(또는 있어야 합니다).
기술은 빠르게 변합니다. 뒤처지지 마세요.
Clint Shank가 작성함.
좋은 API를 설계하는 것의 중요성과 도전에 대해 많은 이야기가 있었습니다. 처음에 제대로 하기가 어렵고 나중에 바꾸기는 더욱 어렵습니다. 아이를 키우는 것과 비슷합니다. 대부분의 경험 있는 프로그래머들은 좋은 API가 일관된 추상화 수준을 따르고, 일관성과 대칭성을 보이며, 표현적인 언어를 위한 어휘를 형성한다는 것을 배웠습니다. 아쉽게도, 안내 원칙을 인식하는 것이 자동으로 적절한 행동으로 번역되지는 않습니다. 단 것을 먹는 것이 몸에 나쁘다는 것처럼 말입니다.
높은 곳에서 설교하는 대신, 저는 특정한 API 설계 '전략'을 꼬집고 싶습니다. 제가 계속해서 마주치는 것: 편의성이라는 논리입니다. 그것은 보통 다음과 같은 '통찰' 중 하나로 시작됩니다:
- 다른 클래스들이 이 한 가지 일을 하기 위해 두 번의 분리된 호출을 해야 하는 것을 원하지 않습니다.
- 이 메서드와 거의 같은 메서드라면 왜 다른 메서드를 만들어야 할까요? 간단한 스위치만 추가하겠습니다.
- 보세요, 매우 쉽습니다: 두 번째 문자열 매개변수가 ".txt"로 끝나면, 메서드가 자동으로 첫 번째 매개변수가 파일 이름이라고 가정하므로, 정말로 두 개의 메서드가 필요하지 않습니다.
좋은 의도이지만, 그런 논리는 API를 사용하는 코드의 가독성을 떨어뜨리기 쉽습니다. 다음과 같은 메서드 호출은
parser.processNodes(text, false);
구현을 알거나 적어도 문서를 참조하지 않고는 사실상 의미가 없습니다. 이 메서드는 호출자의 편의가 아니라 구현자의 편의를 위해 설계되었을 가능성이 높습니다 — "호출자가 두 번의 분리된 호출을 해야 하는 것을 원하지 않는다"가 "두 개의 분리된 메서드를 코딩하고 싶지 않다"로 번역된 것입니다. 편의성이 지루함, 어색함, 또는 꼴사나움의 해독제가 되려는 의도라면 근본적으로 잘못된 것은 없습니다. 하지만 좀 더 신중하게 생각해보면, 그런 증상들의 해독제는 반드시 편의성이 아니라 효율성, 일관성, 그리고 우아함입니다. API는 기본적인 복잡성을 숨기기로 되어 있으므로, 좋은 API 설계가 어느 정도 노력을 요구할 것이라고 현실적으로 기대할 수 있습니다. 하나의 큰 메서드가 확실히 잘 생각된 작업들의 집합보다 작성하기 더 편리할 수 있지만, 사용하기 더 쉬울까요?
API를 언어로 보는 은유는 이런 상황에서 더 나은 설계 결정으로 우리를 안내할 수 있습니다. API는 표현적인 언어를 제공해야 하며, 이는 위의 다음 계층에 유용한 질문을 묻고 답할 충분한 어휘를 줍니다. 이것이 묻는 가치가 있을 수 있는 각 질문에 대해 정확히 하나의 메서드, 또는 동사를 제공해야 한다는 것을 의미하지는 않습니다. 다양한 어휘는 의미의 미묘함을 표현할 수 있게 해줍니다. 예를 들어, 본질적으로 같은 작업이지만 다른 속도로 실행되는 것으로 볼 수 있음에도 불구하고 walk(true) 대신 run이라고 말하는 것을 선호합니다. 일관되고 잘 생각된 API 어휘는 위의 다음 계층에서 표현적이고 이해하기 쉬운 코드를 만듭니다. 더 중요하게는, 조합 가능한 어휘는 다른 프로그래머들이 당신이 예상하지 못했던 방식으로 API를 사용할 수 있게 해줍니다 — API 사용자들에게는 정말로 큰 편의입니다! 다음 번에 몇 가지를 하나의 API 메서드로 묶고 싶은 유혹이 든다면, 영어에는 그렇게 자주 요청되는 작업임에도 불구하고 MakeUpYourRoomBeQuietAndDoYourHomeWork에 대한 한 단어가 없다는 것을 기억하세요.
Gregor Hohpe가 작성함.
배포와 설치 프로세스를 디버깅하는 것은 종종 프로젝트 말미까지 미뤄집니다. 일부 프로젝트에서는 설치 도구를 작성하는 것이 릴리스 엔지니어에게 위임되어 "필요악"으로 그 작업을 맡게 됩니다. 리뷰와 데모는 모든 것이 작동하는 것을 보장하기 위해 수작업으로 만든 환경에서 이루어집니다. 결과적으로 팀은 변경하기에는 너무 늦을 수도 있는 시점까지 배포 프로세스나 배포된 환경에 대한 경험을 얻지 못합니다.
설치/배포 프로세스는 고객이 보는 첫 번째 것이며, 간단한 설치/배포 프로세스는 안정적인(또는 적어도 디버그하기 쉬운) 운영 환경을 갖기 위한 첫 번째 단계입니다. 배포된 소프트웨어가 고객이 사용할 것입니다. 배포가 애플리케이션을 올바르게 설정하는 것을 보장하지 않음으로써, 고객이 당신의 소프트웨어를 철저히 사용하기 전에 의문을 제기하게 만들 것입니다.
설치 프로세스로 프로젝트를 시작하는 것은 제품 개발 사이클을 거치면서 프로세스를 발전시킬 시간을 주고, 설치를 더 쉽게 만들기 위해 애플리케이션 코드를 변경할 기회를 줍니다. 깨끗한 환경에서 주기적으로 설치 프로세스를 실행하고 테스트하는 것은 또한 개발이나 테스트 환경에 의존하는 가정을 코드에서 만들지 않았다는 확인도 제공합니다.
배포를 마지막에 두는 것은 코드의 가정들을 우회하기 위해 배포 프로세스가 더 복잡해져야 할 수도 있다는 것을 의미합니다. 환경을 완전히 제어할 수 있는 IDE에서 좋은 아이디어로 보였던 것이 훨씬 더 복잡한 배포 프로세스를 만들 수도 있습니다. 나중보다는 빨리 모든 트레이드오프를 아는 것이 더 좋습니다.
"배포할 수 있다는 것"이 개발자의 노트북에서 실행되는 애플리케이션을 보는 것과 비교했을 때 초기에는 많은 비즈니스 가치가 있는 것처럼 보이지 않지만, 간단한 진실은 대상 환경에서 애플리케이션을 시연할 수 있을 때까지는 비즈니스 가치를 전달하기 전에 해야 할 일이 많다는 것입니다. 배포 프로세스를 미루는 이유가 그것이 사소하다는 것이라면, 비용이 낮으니까 어쨌든 하세요. 너무 복잡하거나 불확실성이 너무 많다면, 애플리케이션 코드에서 하는 것과 같이 하세요: 실험하고, 평가하고, 진행하면서 배포 프로세스를 리팩터링하세요.
설치/배포 프로세스는 고객이나 전문 서비스 팀의 생산성에 필수적이므로, 진행하면서 이 프로세스를 테스트하고 리팩터링해야 합니다. 우리는 프로젝트 전반에 걸쳐 소스 코드를 테스트하고 리팩터링합니다. 배포도 그만큼 대우받을 가치가 있습니다.
Steve Berczuk이 작성함.
런타임에 문제가 발생하는 이유에는 기본적으로 두 가지가 있습니다: 애플리케이션 사용을 방해하는 기술적 문제와 애플리케이션 오용을 방지하는 비즈니스 논리입니다. LISP, Java, Smalltalk, C#과 같은 대부분의 현대 언어는 이러한 상황 모두를 신호하는 데 예외를 사용합니다. 그러나 두 상황은 매우 다르기 때문에 신중하게 구분해야 합니다. 같은 예외 계층을 사용하여 두 가지를 모두 나타내는 것은 혼란의 잠재적 원인이 될 수 있으며, 같은 예외 클래스를 사용하는 것은 더욱 문제를 야기합니다.
프로그램 오류로 인해 해결할 수 없는 기술적 문제가 발생할 수 있습니다. 예를 들어, 크기가 17인 배열에서 83번째 요소에 접근하려고 한다면, 프로그램은 분명히 잘못된 방향으로 진행되고 있으며, 어떤 예외가 발생해야 합니다. 더 미묘한 경우는 부적절한 인수로 라이브러리 코드를 호출하여 라이브러리 내부에서 동일한 상황이 발생하는 경우입니다.
스스로 야기한 이러한 상황을 해결하려고 시도하는 것은 실수입니다. 대신, 예외를 가장 높은 아키텍처 수준으로 전파하여 일반 예외 처리 메커니즘이 시스템을 안전한 상태로 유지하기 위해 할 수 있는 일을 하도록 해야 합니다. 예를 들어, 트랜잭션을 롤백하거나, 로그를 남기고 관리자에게 경고하거나, 사용자에게 정중하게 보고하는 등의 행동이 필요합니다.
이 상황의 변형은 "라이브러리 상황"에서 호출자가 메서드의 계약을 깨트렸을 때 발생합니다. 예를 들어, 전혀 이상한 인수를 전달하거나 종속 객체가 제대로 설정되어 있지 않는 경우입니다. 이는 크기가 17인 배열에서 83번째 요소에 접근하는 것과 동등합니다: 호출자(caller)는 확인했어야 하며, 그렇게 하지 않는 것은 클라이언트 측의 프로그래머 오류입니다. 적절한 대응은 기술 예외를 발생시키는 것입니다.
또 다른 기술적 상황은 실행 환경의 문제로 인해 프로그램이 계속 진행할 수 없는 경우입니다. 예를 들어, 반응하지 않는 데이터베이스가 있을 때입니다. 이 경우 인프라가 상황을 해결하기 위해 가능한 모든 노력을 했다고 가정해야 하며 - 연결을 수리하고 합리적인 횟수만큼 재시도를 했고 - 그리곤 실패한 것입니다. 원인이 다르더라도 호출 코드의 상황은 유사합니다: 이에 대해 할 수 있는 것은 거의 없습니다. 따라서, 우리는 이 상황을 예외를 통해 신호로 보내며, 이를 일반 예외 처리 메커니즘으로 전파합니다.
이와 대조적인 상황은 도메인 논리적인 이유로 호출을 완료할 수 없는 경우입니다. 이 경우 우리는 예외적인 상황에 직면했으며, 즉 비정상적이고 바람직하지 않지만 이상하거나 프로그래밍적으로 오류가 발생한 것은 아닙니다. 예를 들어, 잔액이 부족한 계좌에서 돈을 인출하려고 할 때입니다. 즉, 이러한 종류의 상황은 계약의 일부이며, 예외를 발생시키는 것은 모델의 일부인 *대체 반환 경로(alternative return path)*라는 것이며 클라이언트가 인식하고 처리할 준비가 되어 있어야 합니다. 이러한 상황에 대해서는 클라이언트가 상황을 스스로 처리할 수 있도록 특정 예외 또는 별도의 예외 계층을 만드는 것이 적절합니다.
기술 예외와 비즈니스 예외를 동일한 계층에서 혼합하면 구별이 흐릿해지고 호출자가 메서드 계약이 무엇인지, 호출하기 전에 보장해야 하는 조건이 무엇인지, 어떤 상황을 처리해야 하는지 혼란을 겪게 됩니다. 각 경우를 분리하면 명확성을 제공하고, 기술 예외가 일부 애플리케이션 프레임워크에 의해 처리될 확률이 높아지며, 비즈니스 도메인 예외는 실제로 클라이언트 코드에 의해 고려되고 처리됩니다.
의도적인 연습은 단순히 작업을 수행하는 것이 아닙니다. "왜 이 작업을 수행하는가?"라고 자신에게 묻고 답이 "작업을 완성하기 위해서"라면, 의도적인 연습을 하고 있지 않은 것입니다.
작업을 수행하는 능력을 향상시키기 위해 의도적인 연습을 합니다. 그것은 기술과 테크닉에 관한 것입니다. 의도적인 연습은 반복을 의미합니다. 작업의 하나 이상의 측면에 대한 숙련도를 높이는 것을 목표로 작업을 수행하는 것을 의미합니다. 반복을 반복하는 것을 의미합니다. 천천히, 계속해서. 원하는 숙련도 수준을 달성할 때까지. 작업을 완성하기 위해서가 아니라 작업을 마스터하기 위해 의도적인 연습을 합니다.
유료 개발의 주된 목적은 제품을 완성하는 것인 반면, 의도적인 연습의 주된 목적은 성능을 향상시키는 것입니다. 이것들은 같지 않습니다. 자신에게 물어보세요, 다른 사람의 제품을 개발하는 데 얼마나 많은 시간을 보냅니까? 자신을 개발하는 데는 얼마나 보냅니까?
전문성을 습득하는 데 얼마나 많은 의도적인 연습이 필요할까요?
- Peter Norvig는 "10,000시간 [...] 이 마법의 숫자일 수 있다"고 씁니다.
- Leading Lean Software Development에서 Mary Poppendieck은 "엘리트 수행자들이 전문가가 되기 위해서는 최소 10,000시간의 의도적이고 집중된 연습이 필요하다"고 언급합니다.
전문성은 10,000번째 시간에 한 번에 오는 것이 아니라 시간이 지나면서 점진적으로 도착합니다! 그럼에도 불구하고 10,000시간은 많습니다: 10년 동안 주당 약 20시간입니다. 이 정도의 헌신을 고려하면 당신이 단지 전문가 재료가 아니라고 걱정할 수도 있습니다. 당신은 전문가 재료입니다. 위대함은 대부분 의식적인 선택의 문제입니다. 당신의 선택입니다. 지난 20년간의 연구는 전문성을 습득하는 주요 요인이 의도적인 연습을 하는 데 보낸 시간이라는 것을 보여주었습니다. 타고난 능력은 주요 요인이 아닙니다.
- Mary: "전문가 성능 연구자들 사이에는 타고난 재능이 임계값 이상으로는 많이 기여하지 않는다는 광범위한 합의가 있습니다; 스포츠나 직업을 시작하기 위해서는 최소한의 자연적 능력이 있어야 합니다. 그 후에는, 뛰어난 사람들이 가장 열심히 일하는 사람들입니다."
이미 전문가인 것을 의도적으로 연습하는 것은 의미가 거의 없습니다. 의도적인 연습은 당신이 잘하지 못하는 것을 연습하는 것을 의미합니다.
- Peter: "[전문성을 개발하는] 열쇠는 의도적인 연습입니다: 단지 반복해서 하는 것이 아니라, 현재 능력을 약간 넘어선 작업으로 자신에게 도전하고, 시도하고, 하는 동안과 한 후에 성능을 분석하고, 실수를 수정하는 것입니다."
- Mary: "의도적인 연습은 당신이 잘하는 것을 하는 것을 의미하지 않습니다; 자신에게 도전하고, 잘하지 못하는 것을 하는 것을 의미합니다. 따라서 반드시 재미있는 것은 아닙니다."
의도적인 연습은 학습에 관한 것입니다. 당신을 변화시키는 학습; 당신의 행동을 변화시키는 학습에 관한 것입니다. 행운을 빕니다.
Jon Jagger가 작성함.
체스 플레이어, 유치원 교사, 또는 보험 대리인 등 어떤 도메인의 전문가들의 토론을 들어보면, 그들의 어휘가 일상 언어와 상당히 다르다는 것을 알게 될 것입니다. 그것이 도메인 특화 언어(DSL)가 무엇인지에 대한 일부입니다: 특정 도메인은 그 도메인에 특별한 것들을 설명하기 위한 특화된 어휘를 가지고 있습니다.
소프트웨어 세계에서, DSL은 제한된 어휘와 문법을 가진 도메인에 특화된 언어로 된 실행 가능한 표현에 관한 것으로, 도메인 전문가가 읽을 수 있고, 이해할 수 있으며, — 바라건대 — 작성할 수 있습니다. 소프트웨어 개발자나 과학자를 대상으로 한 DSL은 오랫동안 존재해 왔습니다. 예를 들어, 구성 파일에서 발견되는 Unix의 '작은 언어들'과 LISP 매크로의 힘으로 만들어진 언어들이 더 오래된 예들 중 일부입니다.
DSL은 일반적으로 내부 또는 외부로 분류됩니다:
-
내부 DSL은 범용 프로그래밍 언어로 작성되지만 그 구문이 자연어처럼 보이도록 구부러집니다. 이는 더 많은 구문적 설탕과 포맷팅 가능성을 제공하는 언어들(예: Ruby와 Scala)에서는 더 쉽지만, 그렇지 않은 언어들(예: Java)에서는 더 어렵습니다. 대부분의 내부 DSL은 기존 API, 라이브러리, 또는 비즈니스 코드를 감싸고 기능에 대한 덜 정신적으로 구부러진 접근을 위한 래퍼를 제공합니다. 그들은 단순히 실행함으로써 직접 실행 가능합니다. 구현과 도메인에 따라, 데이터 구조를 구축하고, 의존성을 정의하고, 프로세스나 작업을 실행하고, 다른 시스템과 통신하거나, 사용자 입력을 검증하는 데 사용됩니다. 내부 DSL의 구문은 호스트 언어에 의해 제약됩니다. 호스트 언어를 당신의 DSL로 구부리는 데 도움이 될 수 있는 많은 패턴들 — 예를 들어, 표현 빌더, 메서드 체이닝, 주석 — 이 있습니다. 호스트 언어가 재컴파일을 요구하지 않는다면, 내부 DSL은 도메인 전문가와 나란히 작업하면서 꽤 빠르게 개발될 수 있습니다.
-
외부 DSL은 언어의 텍스트적 또는 그래픽적 표현입니다 — 비록 텍스트적 DSL이 그래픽적인 것보다 더 일반적인 경향이 있지만. 텍스트적 표현은 렉서, 파서, 모델 변환기, 생성기, 그리고 다른 유형의 후처리를 포함하는 도구 체인에 의해 처리될 수 있습니다. 외부 DSL은 대부분 추가 처리의 기초를 형성하는 내부 모델로 읽어집니다. 문법을 정의하는 것이 도움이 됩니다(예: EBNF로). 문법은 도구 체인의 일부를 생성하기 위한 시작점을 제공합니다(예: 편집기, 시각화기, 파서 생성기). 간단한 DSL의 경우, 예를 들어 정규 표현식을 사용한 수제 파서로 충분할 수 있습니다. 커스텀 파서는 너무 많은 것을 요구받으면 다루기 어려워질 수 있으므로, 언어 문법과 DSL로 작업하기 위해 특별히 설계된 도구들을 살펴보는 것이 의미가 있습니다 — 예: openArchitectureWare, ANTlr, SableCC, AndroMDA. 외부 DSL을 XML 방언으로 정의하는 것도 꽤 일반적입니다. 비록 가독성이 종종 문제가 되지만 — 특히 비기술적 독자들에게는.
DSL의 대상 독자를 항상 고려해야 합니다. 그들이 개발자, 관리자, 비즈니스 고객, 또는 최종 사용자입니까? 언어의 기술적 수준, 사용 가능한 도구, 구문 도움(예: 인텔리센스), 조기 검증, 시각화, 그리고 표현을 의도된 독자에 맞게 조정해야 합니다. 기술적 세부사항을 숨김으로써, DSL은 개발자의 도움을 요구하지 않고 시스템을 그들의 필요에 맞게 적응시킬 수 있는 능력을 줌으로써 사용자들에게 권한을 부여할 수 있습니다. 또한 초기 언어 프레임워크가 자리잡은 후 작업의 잠재적 분배 때문에 개발을 가속화할 수도 있습니다. 언어는 점진적으로 진화할 수 있습니다. 기존 표현과 문법에 대한 다른 마이그레이션 경로들도 사용 가능합니다.
Michael Hunger가 작성함.
업계 경험이 있는 모든 사람은 의심할 여지없이 코드베이스가 기껏해야 불안정한 프로젝트에서 일해본 적이 있을 것입니다. 시스템은 잘못 팩터링되어 있고, 한 가지를 바꾸면 항상 관련 없는 다른 기능을 망가뜨리게 됩니다. 모듈이 추가될 때마다, 코더의 목표는 가능한 한 적게 바꾸고, 모든 릴리스 동안 숨을 참는 것입니다. 이는 마천루에서 I형 보를 가지고 젠가를 하는 것과 같은 소프트웨어 버전이며, 재앙으로 향할 수밖에 없습니다.
변경을 만드는 것이 그렇게 신경을 곤두세우는 이유는 시스템이 아프기 때문입니다. 의사가 필요하며, 그렇지 않으면 상태가 더 악화될 뿐입니다. 당신은 이미 시스템에 무엇이 잘못되었는지 알고 있지만, 오믈렛을 만들기 위해 달걀을 깨는 것을 두려워하고 있습니다. 숙련된 외과의사는 수술하기 위해서는 절개를 해야 한다는 것을 알지만, 숙련된 외과의사는 또한 그 절개가 일시적이고 치유될 것이라는 것도 압니다. 수술의 최종 결과는 초기의 고통보다 가치가 있으며, 환자는 수술 전보다 더 나은 상태로 치유되어야 합니다.
당신의 코드를 두려워하지 마세요. 물건들을 이리저리 옮기는 동안 뭔가가 일시적으로 망가진다고 해서 누가 신경 쓰겠습니까? 변화에 대한 마비적인 두려움이 애초에 당신의 프로젝트를 이런 상태로 만든 것입니다. 리팩터링에 시간을 투자하는 것은 프로젝트의 생명주기 동안 몇 배로 보상을 받을 것입니다. 추가적인 혜택은 아픈 시스템을 다루는 팀의 경험이 그것이 어떻게 작동해야 하는지 아는 전문가로 여러분 모두를 만든다는 것입니다. 이 지식을 원망하기보다는 적용하세요. 당신이 싫어하는 시스템에서 일하는 것은 누구든 시간을 보내야 하는 방식이 아닙니다.
내부 인터페이스를 재정의하고, 모듈을 재구조화하고, 복사-붙여넣기된 코드를 리팩터링하고, 의존성을 줄임으로써 설계를 단순화하세요. 종종 부적절하게 결합된 기능으로부터 생기는 코너 케이스를 제거함으로써 코드 복잡성을 상당히 줄일 수 있습니다. 진행하면서 테스트하며 오래된 구조를 새로운 것으로 천천히 전환하세요. "한 번에 대대적으로" 대규모 리팩터링을 달성하려고 시도하는 것은 중간에 전체 노력을 포기하는 것을 고려하게 만들 만큼 충분한 문제를 일으킬 것입니다.
치유를 위한 공간을 만들기 위해 아픈 부분을 잘라내는 것을 두려워하지 않는 외과의사가 되세요. 이런 태도는 전염적이며 다른 사람들이 미뤄왔던 정리 프로젝트들을 시작하도록 영감을 줄 것입니다. 팀이 프로젝트의 일반적인 이익을 위해 가치있다고 느끼는 작업들의 "위생" 목록을 유지하세요. 이런 작업들이 눈에 보이는 결과를 생산하지 않을 수도 있지만, 비용을 줄이고 향후 릴리스를 가속화할 것이라고 관리진을 설득하세요. 코드의 일반적인 "건강"에 대해 신경 쓰는 것을 결코 멈추지 마세요.
Mike Lewis가 작성함.
늦어지고 있었다. 작업하고 있던 페이지 레이아웃을 테스트하기 위해 플레이스홀더 데이터를 넣고 있었다.
사용자 이름으로는 The Clash의 멤버들을 차용했다. 회사 이름은? Sex Pistols의 노래 제목이면 될 것이다. 이제 주식 티커 심볼이 필요했다 — 대문자로 된 네 글자 단어들.
그런 네 글자 단어들을 사용했다.
무해해 보였다. 그냥 나 자신을 즐겁게 하고, 실제 데이터 소스를 연결하기 전 다음 날 다른 개발자들을 즐겁게 할 뭔가였다.
다음 날 아침, 프로젝트 매니저가 프레젠테이션을 위해 스크린샷을 찍었다.
프로그래밍 역사는 이런 종류의 전쟁 이야기들로 어수선합니다. 개발자와 디자이너들이 "다른 누구도 볼 수 없을 것"이라고 생각하고 한 일들이 예기치 않게 보이게 된 것들 말입니다.
누출 유형은 다양할 수 있지만, 그것이 일어나면 책임이 있는 개인, 팀, 또는 회사에게 치명적일 수 있습니다. 예시들은 다음과 같습니다:
- 상태 미팅 중에, 클라이언트가 아직 구현되지 않은 버튼을 클릭합니다. 그들에게 "다시 클릭하지 마세요, 바보야"라고 말해집니다.
- 레거시 시스템을 유지보수하는 프로그래머가 에러 다이얼로그를 추가하라고 들었고, 그것을 동작시키기 위해 기존의 백그라운드 로깅 출력을 사용하기로 결정합니다. 뭔가 망가지면 사용자들이 갑자기 "이런 데이터베이스 커밋 실패, 배트맨!"같은 메시지를 마주하게 됩니다.
- 누군가가 테스트와 실제 관리 인터페이스를 혼동하고, "재미있는" 데이터 입력을 합니다. 고객들이 온라인 스토어에서 판매되는 $1m "빌 게이츠 모양 개인 마사지기"를 발견합니다.
"거짓말은 진실이 신발을 신는 동안 세계 반대편까지 갈 수 있다"는 오래된 속담을 차용하자면, 이 시대에는 실수가 개발자의 시간대에 있는 누구든 깨어나서 그것에 대해 뭔가를 하기 전에 Dugg되고, 트위터되고, Flibflarb될 수 있습니다.
심지어 당신의 소스 코드도 정밀 검사로부터 반드시 자유롭지는 않습니다. 2004년, Windows 2000 소스 코드의 타르볼이 파일 공유 네트워크에 나타났을 때, 일부 사람들이 즐겁게 욕설, 모욕, 그리고 다른 재미있는 내용을 찾기 위해 grep했습니다. (// TERRIBLE HORRIBLE NO GOOD VERY BAD HACK이라는 코멘트는, 저도 인정하건대, 그 이후로 때때로 제가 차용하게 되었습니다!)
요약하자면, 코드에서 어떤 텍스트를 작성할 때 — 코멘트든, 로깅이든, 다이얼로그든, 테스트 데이터든 — 그것이 공개되면 어떻게 보일지 항상 자신에게 물어보세요. 모든 사람의 얼굴이 빨개지는 것을 막아줄 것입니다.
Rod Begbie가 작성함.
어느 날 저녁 바에서 친구들을 만나기 위해 길을 걷고 있었다. 한동안 맥주를 함께 마시지 못했고 다시 보게 되기를 기대하고 있었다. 서둘러서 어디로 가는지 보지 않고 있었다. 연석 끝에 걸려 넘어져서 얼굴부터 바닥에 떨어졌다. 글쎄, 주의를 기울이지 않은 대가인 것 같다.
다리가 아팠지만, 친구들을 만나기 위해 서두르고 있었다. 그래서 몸을 일으켜 계속 갔다. 더 걸을수록 고통이 심해졌다. 처음에는 충격이라고 생각했지만, 뭔가 잘못되었다는 것을 빠르게 깨달았다.
그럼에도 불구하고 바로 서둘러 갔다. 도착했을 때는 고통이 극심했다. 너무 산만해서 좋은 밤을 보내지 못했다. 아침에 의사에게 가서 정강이뼈가 골절되었다는 것을 알았다. 고통을 느꼈을 때 멈췄다면, 그 위를 걸음으로써 야기한 많은 추가 손상을 예방할 수 있었을 것이다. 아마도 내 인생 최악의 다음 날 아침이었다.
너무 많은 프로그래머들이 제 재앙적인 밤처럼 코드를 작성합니다.
에러, 무슨 에러? 심각하지 않을 거야. 정말이야. 무시할 수 있어. 이것은 견고한 코드를 위한 성공적인 전략이 아닙니다. 사실, 그냥 순전히 게으름입니다. (잘못된 종류의.) 코드에서 에러가 얼마나 가능성이 낮다고 생각하든, 항상 확인해야 하고, 항상 처리해야 합니다. 매번. 그렇게 하지 않으면 시간을 절약하는 것이 아닙니다: 미래를 위한 잠재적 문제들을 저장하고 있는 것입니다.
우리는 다음을 포함한 여러 방법으로 코드에서 에러를 보고합니다:
-
리턴 코드는 "작동하지 않았다"는 의미로 함수의 결과값으로 사용될 수 있습니다. 에러 리턴 코드는 무시하기가 너무 쉽습니다. 문제를 강조하는 코드에서 아무것도 볼 수 없을 것입니다. 실제로, 일부 표준 C 함수의 리턴 값을 무시하는 것이 표준 관행이 되었습니다. printf의 리턴 값을 얼마나 자주 확인하십니까?
-
errno는 이상한 C의 일탈로, 에러를 신호하기 위해 설정되는 별도의 전역 변수입니다. 무시하기 쉽고, 사용하기 어렵고, 모든 종류의 불쾌한 문제들로 이어집니다 — 예를 들어, 여러 스레드가 같은 함수를 호출할 때 무슨 일이 일어날까요? 일부 플랫폼은 여기서 고통으로부터 당신을 보호해줍니다; 다른 플랫폼들은 그렇지 않습니다.
-
예외는 에러를 신호하고 처리하는 더 구조화된 언어 지원 방법입니다. 그리고 그것들을 무시할 수는 없습니다. 아니면 무시할 수 있을까요? 저는 이런 코드를 많이 봤습니다:
try {
// ...do something...
}
catch (...) {} // ignore errors
이 끔찍한 구조의 구원의 은총은 당신이 도덕적으로 의심스러운 일을 하고 있다는 사실을 강조한다는 것입니다.
에러를 무시하고, 눈을 감고, 아무것도 잘못되지 않았다고 가정한다면, 큰 위험을 감수하게 됩니다. 제 다리가 즉시 걷기를 멈췄을 때보다 더 나쁜 상태가 된 것처럼, 관계없이 계속 밀고 나가는 것은 매우 복잡한 실패로 이어질 수 있습니다. 가장 빠른 기회에 문제를 처리하세요. 짧은 계정을 유지하세요.
에러를 처리하지 않으면 다음과 같은 결과를 낳습니다:
- 부서지기 쉬운 코드. 흥미롭고 찾기 어려운 버그들로 가득한 코드.
- 불안전한 코드. 크래커들은 종종 소프트웨어 시스템에 침입하기 위해 잘못된 에러 처리를 이용합니다.
- 불량한 구조. 지속적으로 처리하기 지루한 에러들이 코드에서 나온다면, 아마도 불량한 인터페이스를 가지고 있을 것입니다. 에러들이 덜 침입적이고 그것들의 처리가 덜 부담스럽도록 표현하세요.
코드의 모든 잠재적 에러를 확인해야 하는 것처럼, 인터페이스에서 모든 잠재적으로 에러가 있는 조건들을 노출해야 합니다. 서비스가 항상 작동할 것이라고 가정하며 그것들을 숨기지 마세요.
왜 에러를 확인하지 않을까요? 여러 일반적인 변명이 있습니다. 이 중 어느 것에 동의하십니까? 각각을 어떻게 반박하겠습니까?
- 에러 처리는 코드의 흐름을 어수선하게 만들어, 읽기 어렵게 하고, "정상적인" 실행 흐름을 발견하기 어렵게 합니다.
- 추가 작업이고 마감일이 다가오고 있습니다.
- 이 함수 호출이 절대 에러를 리턴하지 않을 것을 압니다 (printf는 항상 작동하고, malloc은 항상 새 메모리를 리턴합니다 — 실패한다면 더 큰 문제가 있는 것입니다...).
- 단지 장난감 프로그램이고, 운영 수준으로 작성될 필요가 없습니다.
Pete Goodliffe가 작성함.
고등학교 때, 외국어를 배워야 했습니다. 그 당시 저는 영어에 능숙하면 잘 지낼 수 있을 거라고 생각해서, 프랑스어 수업에서 세 해 동안 졸기만 했습니다. 몇 년 후, 저는 튀니지로 휴가를 갔습니다. 아랍어가 공식 언어인 이곳에서, 예전 프랑스 식민지인 만큼 프랑스어도 흔히 사용됩니다. 영어는 관광지에서만 사용됩니다. 언어에 대한 무지 덕분에 저는 수영장 옆에 갇혀 제임스 조이스의 피네간의 경야(經夜)(Finnegan's Wake)를 읽고 있는 상황이었습니다. 조이스의 언어와 형식에 대한 탁월한 작품에서 40개 이상의 언어가 혼합된 저자의 유희적인 조합은 놀랍고도 힘든 경험이었습니다. 외국어와 구문이 얽히는 것이 저자에게 새로운 표현 방식을 제공한다는 것을 깨달은 것은 제 프로그래밍 경력에서 잊지 못할 경험입니다.
앤디 헌트(Andy Hunt)와 데이브 토마스(Dave Thomas)의 기념비적인 저서 *실용주의 프로그래머(The Pragmatic Programmer)*에서는 매년 새로운 프로그래밍 언어를 배우도록 권장합니다. 저는 그들의 조언을 따르려고 애썼고, 여러 해에 걸쳐 많은 언어로 프로그래밍하는 경험을 쌓았습니다. 다양한 언어에서 얻은 가장 중요한 교훈은 언어를 배우는 데 문법만 배우는 것이 아니라 그 문화를 이해해야 한다는 것입니다. 포트란(Fortran)을 어떤 언어로든 쓸 수 있지만, 진정한 언어를 배우려면 그 언어를 포용해야 합니다. 만약 당신의 C# 코드가 긴 Main 메서드인데다 대부분 정적 도우미 메소드들(helper methods)로 구성되어 있다면 변명하지 말고, 클래스가 왜 의미가 있는지 배워야 합니다. 함수형 언어에서 사용되는 람다 표현식을 이해하는 데 어려움이 있다면, 그것을 한번 사용해보도록 자신을 강제해보세요.
새로운 언어의 기초를 익히면, 이미 알고 있는 언어를 새로운 방식으로 사용하기 시작하는 것에 놀랄 것입니다. 저는 루비(Ruby) 프로그래밍을 통해 C#에서 델리게이트(delegate)를 효과적으로 사용하는 법을 배웠고, .NET의 제너릭을 최대한 활용함으로써 자바 제너릭을 더 유용하게 만드는 아이디어를 얻었습니다. 또한, LINQ는 제가 스칼라(Scala)를 독학하는 데 큰 도움이 되었습니다.
다양한 언어 사이를 옮겨 다니며 디자인 패턴에 대한 이해도 더 넓어질 것입니다. C 프로그래머들은 C#과 자바가 이터레이터 패턴(iterato pattern)을 상품화했다고 느끼게 됩니다. 루비 및 다른 동적 언어에서는 여전히 방문자 패턴(visitor pattern)을 사용할 수 있지만, 그 구현은 Gang of Four 책의 예제와는 다를 것입니다.
어떤 사람들은 *피네간의 경야(Finnegan's Wake)*가 읽을 수 없을 지경이라고 주장할 수도 있지만, 다른 사람들은 그 스타일적 아름다움 때문에 찬사를 보냅니다. 이 책을 덜 두렵게 만들기 위해 단일 언어 번역본이 제공되기도 합니다. 아이러니하게도, 이러한 번역의 첫 번째는 프랑스어로 되어 있었습니다. 코드도 여러 면에서 유사합니다. 만약 당신의 Wakese 코드가 약간의 파이썬(Python), 자바(Java), 그리고 약간의 얼랑(Erlang)으로 이루어져 있다면, 당신의 프로젝트는 엉망이 될 것입니다. 하지만 새로운 언어를 탐구하며 사고를 확장하고 새로운 방식으로 문제를 해결할 수 있는 신선한 아이디어를 얻는다면, 당신이 오래도록 아끼던 언어로 작성한 코드가 배운 새로운 언어마다 더 아름다워진다는 것을 발견하게 될 것입니다.
안데르스 노라스(Anders Norås) 씀.
한때 저는 패러디 C++ 퀴즈를 작성했는데, 그 안에서 예외 처리에 대한 다음 전략을 풍자적으로 제안했습니다:
코드베이스 전반에 걸친 풍부한
try...catch구조들의 힘으로, 우리는 때때로 애플리케이션이 중단되는 것을 방지할 수 있습니다. 우리는 그 결과 상태를 "시체를 직립 자세로 못 박는 것"이라고 생각합니다.
농담에도 불구하고, 저는 실제로 쓰라린 경험 부인 슬하에서 받은 교훈을 요약하고 있었습니다.
그것은 우리만의 수제 C++ 라이브러리에 있는 기본 애플리케이션 클래스였습니다. 수년간 많은 프로그래머들의 손가락 찌르기를 당했습니다: 누구의 손도 깨끗하지 않았습니다. 그것은 다른 모든 것에서 탈출한 모든 예외를 처리하는 코드를 포함했습니다. Catch-22의 요사리안을 본받아, 우리는 결정했거나, 오히려 느꼈습니다(결정했다는 것은 이 괴물의 구성에 들어간 것보다 더 많은 생각을 암시합니다) 이 클래스의 인스턴스는 영원히 살거나 시도하다 죽어야 한다고.
이를 위해, 우리는 여러 예외 핸들러를 얽혔습니다. Windows의 구조화된 예외 처리를 네이티브 종류와 섞었습니다 (C++에서 __try...__except를 기억하시나요? 저도 기억 안 납니다). 예기치 않게 던져질 때, 매개변수를 더 세게 눌러서 다시 호출하려고 시도했습니다. 돌이켜보면, 다른 catch 절 안에서 내부 try...catch 핸들러를 작성할 때, 좋은 관행의 고속도로에서 미친 상태의 향기롭지만 건강하지 않은 골목으로 실수로 빠져나가는 지름길을 택했을지도 모른다는 어떤 종류의 인식이 저를 스쳐갔을 것이라고 생각하고 싶습니다. 하지만, 이는 아마도 후한 지혜일 것입니다.
말할 것도 없이, 이 클래스에 기반한 애플리케이션에서 뭔가 잘못될 때마다, 그들은 부두의 마피아 희생자들처럼 사라져서, 도대체 무슨 일이 일어났는지 나타내는 유용한 거품의 흔적을 남기지 않았습니다. 재앙을 기록하기로 되어 있던 덤프 루틴들에도 불구하고 말입니다. 결국 — 긴 결국 — 우리는 우리가 한 일을 점검하고, 부끄러움을 경험했습니다. 우리는 전체 엉망을 최소한이고 견고한 보고 메커니즘으로 교체했습니다. 하지만 이는 많은 크래시들이 지나간 후였습니다.
저는 이것으로 당신을 귀찮게 하지 않았을 것입니다 — 확실히 다른 누구도 우리만큼 바보일 수는 없을 테니까요 — 하지만 최근에 학술적 직함이 더 잘 알아야 한다고 선언하는 녀석과 온라인 논쟁을 했기 때문입니다. 우리는 원격 트랜잭션에서 Java 코드를 논의하고 있었습니다. 코드가 실패하면, 그는 현장에서 예외를 잡고 차단해야 한다고 주장했습니다. ("그럼 그것으로 무엇을 하겠다는 거야?" 제가 물었습니다. "저녁으로 요리하겠다고?")
그는 UI 디자이너의 규칙을 인용했습니다: 사용자에게 예외 보고서를 절대 보여주지 마라, 마치 이것이 대문자로 되어 있고 모든 것으로 문제를 해결하는 것처럼. 그가 사진이 허술한 블로그들을 장식하는 그런 블루스크린된 ATM들 중 하나의 코드에 책임이 있었는지, 그리고 영구적으로 트라우마를 입었는지 궁금합니다.
어쨌든, 만약 당신이 그를 만난다면, 고개를 끄덕이고 미소 짓고 주의를 기울이지 말고, 문으로 살금살금 다가가세요.
Verity Stob이 작성함.
어떤 활동, 과정, 또는 분야든 충분히 멀리서 보면 단순해 보입니다. 개발 경험이 없는 관리자들은 프로그래머들이 하는 일이 단순하다고 생각하고, 관리 경험이 없는 프로그래머들은 관리자들이 하는 일에 대해서도 같은 생각을 합니다.
프로그래밍은 어떤 사람들이 하는 일입니다 — 때때로. 그리고 어려운 부분 — 사고하기 — 은 가장 보이지 않고 초보자들에게 가장 인정받지 못합니다. 수십 년에 걸쳐 이런 숙련된 사고의 필요성을 제거하려는 많은 시도들이 있었습니다. 가장 초기이고 가장 기억에 남는 것 중 하나는 Grace Hopper가 프로그래밍 언어를 덜 암호적으로 만들려는 노력으로 — 일부 설명에서는 전문 프로그래머의 필요성을 제거할 것이라고 예측했습니다. 그 결과(COBOL)는 이후 수십 년에 걸쳐 많은 전문 프로그래머들의 수입에 기여했습니다.
프로그래밍을 제거함으로써 소프트웨어 개발을 단순화할 수 있다는 지속적인 비전은, 무엇이 관련되어 있는지 이해하는 프로그래머에게는 명백히 순진합니다. 하지만 이런 실수로 이어지는 정신적 과정은 인간 본성의 일부이고 프로그래머들도 다른 모든 사람들만큼 그것을 범하기 쉽습니다.
어떤 프로젝트에서든 개별 프로그래머가 적극적으로 관여하지 않는 많은 것들이 있을 가능성이 높습니다: 사용자로부터 요구사항 도출하기, 예산 승인받기, 빌드 서버 설정하기, QA와 운영 환경에 애플리케이션 배포하기, 오래된 프로세스나 프로그램에서 비즈니스 마이그레이션하기 등.
적극적으로 관여하지 않는 일들에 대해서는 그것들이 단순하고 "마법에 의해" 일어난다고 가정하는 무의식적인 경향이 있습니다. 마법이 계속 일어나는 동안은 모든 것이 괜찮습니다. 하지만 — 보통 "만약"이 아니라 "언제" — 마법이 멈출 때 프로젝트는 곤경에 처합니다.
DLL의 "올바른" 버전이 로드되는 것에 어떻게 의존하는지 아무도 이해하지 못해서 몇 주의 개발자 시간을 잃은 프로젝트들을 알고 있습니다. 일들이 간헐적으로 실패하기 시작했을 때 팀 멤버들은 누군가가 DLL의 "잘못된" 버전이 로드되고 있다는 것을 알아차리기 전까지 다른 모든 곳을 찾아봤습니다.
다른 부서는 순조롭게 운영되고 있었습니다 — 프로젝트들이 제때 전달되고, 밤늦은 디버깅 세션도 없고, 긴급 수정도 없었습니다. 너무 순조로워서, 사실, 고위 경영진은 일들이 "저절로 돌아간다"고 결정하고 프로젝트 매니저 없이도 할 수 있다고 결정했습니다. 6개월 안에 그 부서의 프로젝트들은 조직의 나머지와 똑같아 보였습니다 — 늦고, 버그가 많고, 계속 패치되고 있었습니다.
당신의 프로젝트를 작동하게 하는 모든 마법을 이해할 필요는 없지만, 그 중 일부를 이해하거나 — 당신이 모르는 부분을 이해하는 누군가를 인정하는 것 — 은 해가 되지 않습니다.
가장 중요하게는, 마법이 멈췄을 때 다시 시작할 수 있도록 확인하세요.
AlanGriffiths가 작성함.
모든 프로그래밍 원칙 중에서, Don't Repeat Yourself (DRY)는 아마도 가장 근본적인 것 중 하나일 것입니다. 이 원칙은 The Pragmatic Programmer에서 Andy Hunt와 Dave Thomas에 의해 공식화되었으며, 잘 알려진 다른 많은 소프트웨어 개발 모범 사례와 디자인 패턴의 기반이 됩니다. 중복을 인식하는 법을 배우고, 적절한 관행과 올바른 추상화를 통해 그것을 제거하는 방법을 이해하는 개발자는 애플리케이션을 불필요한 반복으로 지속적으로 감염시키는 개발자보다 훨씬 더 깨끗한 코드를 생산할 수 있습니다.
애플리케이션에 들어가는 모든 코드 라인은 유지보수되어야 하며, 미래 버그의 잠재적 소스입니다. 중복은 불필요하게 코드베이스를 부풀려서, 더 많은 버그 기회를 만들고 시스템에 우발적 복잡성을 추가합니다. 중복이 시스템에 추가하는 부풀림은 또한 시스템으로 작업하는 개발자들이 전체 시스템을 완전히 이해하거나, 한 위치에서 만든 변경사항이 그들이 작업하고 있는 로직을 중복하는 다른 장소에서도 만들어져야 하는지 확신하기 어렵게 만듭니다. DRY는 "모든 지식 조각이 시스템 내에서 단일하고, 명확하고, 권위있는 표현을 가져야 한다"고 요구합니다.
소프트웨어 개발의 많은 프로세스는 반복적이고 쉽게 자동화됩니다. DRY 원칙은 애플리케이션의 소스 코드뿐만 아니라 이런 맥락에서도 적용됩니다. 수동 테스팅은 느리고, 오류가 발생하기 쉽고, 반복하기 어려우므로, 가능하다면 자동화된 테스트 스위트를 사용해야 합니다. 소프트웨어를 통합하는 것은 수동으로 한다면 시간이 많이 걸리고 오류가 발생하기 쉬우므로, 빌드 프로세스는 가능한 한 자주, 이상적으로는 모든 체크인마다 실행되어야 합니다. 자동화될 수 있는 고통스러운 수동 프로세스가 존재하는 곳마다, 그것들은 자동화되고 표준화되어야 합니다. 목표는 작업을 수행하는 한 가지 방법만 있도록 보장하고, 그것이 가능한 한 고통스럽지 않도록 하는 것입니다.
로직에서의 반복은 많은 형태를 취할 수 있습니다. 복사-붙여넣기 if-then 또는 switch-case 로직은 탐지하고 수정하기 가장 쉬운 것 중 하나입니다. 많은 디자인 패턴들은 애플리케이션 내에서 로직의 중복을 줄이거나 제거하는 명시적인 목표를 가지고 있습니다. 객체가 사용되기 전에 일반적으로 여러 가지 일이 일어나야 한다면, 이는 Abstract Factory나 Factory Method로 달성될 수 있습니다. 객체가 그 행동에서 많은 가능한 변화를 가진다면, 이런 행동들은 큰 if-then 구조보다는 Strategy 패턴을 사용해서 주입될 수 있습니다. 사실, 디자인 패턴 자체의 공식화는 공통 문제를 해결하고 그런 해결책을 논의하기 위해 필요한 노력의 중복을 줄이려는 시도입니다. 또한, DRY는 데이터베이스 스키마 같은 구조에 적용되어 정규화를 가져올 수 있습니다.
다른 소프트웨어 원칙들도 DRY와 관련이 있습니다. 코드의 기능적 행동에만 적용되는 Once and Only Once 원칙은 DRY의 부분집합으로 생각할 수 있습니다. "소프트웨어 엔티티는 확장에는 열려있고 수정에는 닫혀있어야 한다"고 명시하는 Open/Closed 원칙은 DRY가 따라질 때만 실제로 작동합니다. 마찬가지로, 잘 알려진 Single Responsibility 원칙은 클래스가 "변경할 이유를 하나만" 가져야 한다고 요구하며, DRY에 의존합니다.
구조, 로직, 프로세스, 그리고 기능에 관해 따라질 때, DRY 원칙은 소프트웨어 개발자들에게 근본적인 안내를 제공하고 더 간단하고, 더 유지보수하기 쉽고, 더 높은 품질의 애플리케이션 생성을 도와줍니다. 성능이나 다른 요구사항을 만족하기 위해 반복이 필요할 수 있는 시나리오들이 있지만(예: 데이터베이스에서 데이터 비정규화), 상상의 문제가 아닌 실제 문제를 직접 다루는 곳에서만 사용되어야 합니다.
Steve Smith가 작성함.
이것은 우리 모두에게 언젠가는 일어난 일입니다. 당신의 코드가 시스템 테스트를 위해 스테이징 서버에 배포되었는데, 테스트 매니저가 문제가 발생했다고 알려왔습니다. 당신의 첫 번째 반응은 "빨리, 내가 고쳐줄게 — 뭐가 잘못됐는지 알아"입니다.
하지만 더 큰 관점에서 보면, 잘못된 것은 개발자인 당신이 스테이징 서버에 접근할 수 있어야 한다고 생각하는 것입니다.
대부분의 웹 기반 개발 환경에서 아키텍처는 다음과 같이 나눌 수 있습니다:
- 개발자 머신에서의 로컬 개발 및 단위 테스트
- 수동 또는 자동 통합 테스트가 수행되는 개발 서버
- QA 팀과 사용자가 수락 테스트를 수행하는 스테이징 서버
- 프로덕션 서버
물론 소스 코드 제어나 티켓팅 같은 다른 서버와 서비스들이 여기저기 섞여 있지만, 요점은 알 수 있을 것입니다. 이 모델을 사용할 때, 개발자 — 심지어 시니어 개발자라 할지라도 — 개발 서버 너머로는 접근할 수 없어야 합니다. 대부분의 개발은 개발자의 로컬 머신에서 자신이 선호하는 IDE, 가상 머신, 그리고 행운을 위해 뿌려진 적절한 양의 블랙 매직을 조합해서 수행됩니다.
자동으로든 수동으로든 SCC에 체크인되면, 모든 것이 함께 작동하는지 확인하기 위해 테스트하고 필요한 경우 조정할 수 있는 개발 서버로 롤오버되어야 합니다. 하지만 이 시점부터 개발자는 프로세스의 관찰자가 됩니다.
스테이징 매니저가 QA 팀을 위해 코드를 패키징하고 스테이징 서버로 롤링해야 합니다. 개발자가 개발 서버 너머에 접근할 필요가 없는 것처럼, QA 팀과 사용자들도 개발 서버의 어떤 것도 건드릴 필요가 없습니다. 수락 테스트를 할 준비가 되었다면, 릴리스를 만들어서 롤링하세요. 사용자에게 개발 서버에서 "뭔가 빨리 좀 봐달라"고 요청하지 마세요. 기억하세요, 혼자서 프로젝트를 코딩하는 것이 아니라면, 다른 사람들도 거기에 코드를 가지고 있고 그들은 사용자가 그것을 보도록 준비되지 않았을 수도 있습니다. 릴리스 매니저만이 양쪽 모두에 접근할 수 있어야 하는 유일한 사람입니다.
어떤 상황에서도 — 절대로, 전혀 — 개발자가 프로덕션 서버에 접근해서는 안 됩니다. 문제가 있다면, 당신의 지원 스태프가 그것을 고치거나 당신에게 고쳐달라고 요청해야 합니다. SCC에 체크인된 후에 그들이 거기서 패치를 롤링할 것입니다. 내가 관련된 가장 큰 프로그래밍 재앙들 중 일부는 누군가가 *기침*나*기침* 이 마지막 규칙을 위반했기 때문에 일어났습니다. 망가졌다면, 프로덕션은 그것을 고칠 곳이 아닙니다.
Cal Evans가 작성함.
시스템 이론에서 격리(containment)는 크고 복잡한 시스템 구조를 다룰 때 가장 유용한 구조체 중 하나입니다. 소프트웨어 업계에서 격리 또는 캡슐화의 가치는 잘 이해되고 있습니다. 격리는 서브루틴과 함수, 모듈과 패키지, 클래스 등과 같은 프로그래밍 언어 구조체에 의해 지원됩니다.
모듈과 패키지는 캡슐화의 더 큰 규모의 요구사항을 해결하는 반면, 클래스, 서브루틴, 함수는 이 문제의 더 세밀한 측면을 다룹니다. 수년간 저는 클래스가 개발자들이 올바르게 이해하기 가장 어려운 캡슐화 구조체 중 하나인 것 같다는 것을 발견했습니다. 단일 3000줄 메인 메서드를 가진 클래스나, 기본 속성들에 대한 set과 get 메서드만 가진 클래스를 찾는 것은 흔한 일입니다. 이런 예시들은 관련된 개발자들이 객체지향적 사고를 완전히 이해하지 못했음을 보여주며, 모델링 구조체로서 객체의 힘을 활용하는 데 실패했습니다. POJO(Plain Old Java Object)와 POCO(Plain Old C# Object 또는 Plain Old CLR Object) 용어에 익숙한 개발자들에게, 이것이 모델링 패러다임으로서 OO의 기본으로 돌아가려는 의도였습니다 — 객체는 평범하고 단순하지만, 멍청하지는 않습니다.
객체는 상태와 행동을 모두 캡슐화하며, 행동은 실제 상태에 의해 정의됩니다. 문(door) 객체를 생각해보세요. 이것은 네 가지 상태를 가집니다: 닫힘, 열림, 닫히는 중, 열리는 중. 두 가지 동작을 제공합니다: 열기와 닫기. 상태에 따라 열기와 닫기 동작은 다르게 작동할 것입니다. 객체의 이런 고유한 속성은 설계 과정을 개념적으로 간단하게 만듭니다. 이는 두 가지 간단한 작업으로 귀결됩니다: 객체 간 상호작용 프로토콜을 포함하여 서로 다른 객체들에 대한 책임의 할당과 위임.
이것이 실제로 어떻게 작동하는지는 예시로 가장 잘 설명됩니다. Customer, Order, Item이라는 세 개의 클래스가 있다고 해봅시다. Customer 객체는 신용 한도와 신용 검증 규칙의 자연스러운 자리표시자입니다. Order 객체는 연관된 Customer를 알고 있으며, 그것의 addItem 동작은 customer.validateCredit(item.price())를 호출하여 실제 신용 확인을 위임합니다. 메서드의 후조건이 실패하면, 예외가 던져지고 구매가 중단될 수 있습니다.
경험이 적은 객체지향 개발자들은 모든 비즈니스 규칙을 OrderManager나 OrderService라고 자주 불리는 객체에 감쌀 수도 있습니다. 이런 설계에서 Order, Customer, Item은 레코드 타입에 조금 더한 것 정도로 취급됩니다. 모든 로직이 클래스에서 빼내져서 많은 내부 if-then-else 구조체를 가진 하나의 크고 절차적인 메서드에 묶입니다. 이런 메서드들은 쉽게 망가지고 유지보수가 거의 불가능합니다. 이유는? 캡슐화가 깨졌기 때문입니다.
그러므로 결국, 캡슐화를 깨뜨리지 말고, 그것을 유지하기 위해 당신의 프로그래밍 언어의 힘을 사용하세요.
Einar Landre가 작성함.
부동소수점 숫자는 수학적 의미에서 "실수(real numbers)"가 아닙니다. 비록 Pascal이나 Fortran 같은 일부 프로그래밍 언어에서 real이라고 불리지만 말입니다. 실수는 무한한 정밀도를 가지므로 연속적이고 손실이 없습니다. 부동소수점 숫자는 제한된 정밀도를 가지므로 유한하며, 범위 전체에 걸쳐 균등하게 분포되어 있지 않기 때문에 "잘못 동작하는" 정수와 비슷합니다.
예를 들어, 2147483647(가장 큰 부호 있는 32비트 정수)을 32비트 float 변수(x라고 하자)에 할당하고 출력해보세요. 2147483648이 보일 것입니다. 이제 x - 64를 출력해보세요. 여전히 2147483648입니다. 이제 x - 65를 출력하면 2147483520을 얻을 것입니다! 왜일까요? 그 범위에서 인접한 부동소수점 사이의 간격이 128이고, 부동소수점 연산은 가장 가까운 부동소수점 숫자로 반올림하기 때문입니다.
IEEE 부동소수점 숫자는 2진 과학적 표기법에 기반한 고정 정밀도 숫자입니다: 1.d1d2...dp-1 × 2e, 여기서 p는 정밀도입니다(float는 24, double은 53). 연속된 두 숫자 사이의 간격은 21-p+e이며, 이는 ε|x|로 안전하게 근사할 수 있습니다. 여기서 ε는 머신 입실론(machine epsilon)(21-p)입니다.
부동소수점 숫자 근방의 간격을 아는 것은 고전적인 수치 실수를 피하는 데 도움이 될 수 있습니다. 예를 들어, 방정식의 근을 찾는 것과 같은 반복적 계산을 수행하고 있다면, 답 근방에서 숫자 시스템이 줄 수 있는 것보다 더 큰 정밀도를 요구하는 것은 의미가 없습니다. 요청하는 허용 오차가 그곳의 간격보다 작지 않도록 확인하세요. 그렇지 않으면 영원히 루프를 돌 것입니다.
부동소수점 숫자는 실수의 근사치이므로, 필연적으로 작은 오차가 존재합니다. *반올림 오차(roundoff)*라고 불리는 이 오차는 놀라운 결과를 가져올 수 있습니다. 예를 들어, 거의 같은 숫자들을 뺄 때, 가장 중요한 자릿수들이 서로 상쇄되므로 가장 덜 중요한 자릿수(반올림 오차가 있는 곳)가 부동소수점 결과에서 가장 중요한 위치로 승격되어, 본질적으로 관련된 추가 계산들을 오염시킵니다(*스미어링(smearing)*이라고 알려진 현상). 이런 *치명적 소거(catastrophic cancellation)*를 방지하기 위해 알고리즘을 자세히 살펴봐야 합니다. 예를 들어, 이차 공식으로 방정식 x2 - 100000x + 1 = 0을 풀어보세요. 표현식 *-b + sqrt(b2 - 4)*에서 피연산자들이 크기가 거의 같으므로, 대신 근 *r1 = -b + sqrt(b2 - 4)*를 계산하고, 그다음 *r2 = 1/r1*을 얻을 수 있습니다. 어떤 이차 방정식 ax2 + bx + c = 0에서도, 근들은 r1r2 = c/a를 만족하기 때문입니다.
스미어링은 훨씬 더 미묘한 방식으로도 발생할 수 있습니다. 라이브러리가 공식 *1 + x + x2/2 + x3/3! + ...*로 *ex*를 순진하게 계산한다고 가정해보세요. 이것은 양수 x에서는 잘 작동하지만, x가 큰 음수일 때 무슨 일이 일어나는지 생각해보세요. 짝수 거듭제곱 항들은 큰 양수를 만들고, 홀수 거듭제곱 크기를 빼는 것은 결과에 영향을 주지도 않을 것입니다. 여기서 문제는 크고 양수인 항들의 반올림 오차가 진짜 답보다 훨씬 더 중요한 자릿수 위치에 있다는 것입니다. 답은 양의 무한대로 발산합니다! 여기서 해결책도 간단합니다: 음수 x에 대해서는 *ex = 1/e|x|*를 계산하세요.
금융 애플리케이션에 부동소수점 숫자를 사용하면 안 된다는 것은 말할 필요도 없습니다 — 그것은 Python이나 C# 같은 언어의 decimal 클래스가 하는 일입니다. 부동소수점 숫자는 효율적인 과학적 계산을 위한 것입니다. 하지만 정확성 없는 효율성은 가치가 없으므로, 반올림 오차의 원인을 기억하고 그에 따라 코딩하세요!
Chuck Allison이 작성함.
당신이 직장에서 개발하고 있는 소프트웨어가 당신의 가장 야심찬 소프트웨어 개발 꿈을 실현해주지 못할 가능성이 꽤 높습니다. 아마도 당신은 거대한 보험 회사에서 소프트웨어를 개발하고 있지만, 정작 Google, Apple, Microsoft, 또는 다음 대박을 개발하는 자신만의 스타트업에서 일하고 싶어할지도 모릅니다. 당신이 신경 쓰지 않는 시스템용 소프트웨어를 개발해서는 원하는 곳에 절대 도달할 수 없을 것입니다.
다행히 당신 문제에 대한 답이 있습니다: 오픈 소스입니다. 수천 개의 오픈 소스 프로젝트가 있으며, 그 중 많은 것들이 꽤 활발하고, 당신이 원할 수 있는 어떤 종류의 소프트웨어 개발 경험이라도 제공합니다. 운영 체제 개발 아이디어를 좋아한다면, 수십 개의 운영 체제 프로젝트 중 하나를 도와주세요. 음악 소프트웨어, 애니메이션 소프트웨어, 암호화, 로보틱스, PC 게임, 대규모 온라인 플레이어 게임, 모바일 폰, 또는 그 밖의 무엇이든 원한다면, 그 관심사에 전념하는 적어도 하나의 오픈 소스 프로젝트를 거의 확실히 찾을 수 있을 것입니다.
물론 공짜 점심은 없습니다. 아마도 낮 직장에서 오픈 소스 비디오 게임을 작업할 수는 없을 것이므로 — 여전히 고용주에 대한 책임이 있으니까요 — 자유 시간을 포기할 의지가 있어야 합니다. 게다가 오픈 소스 프로젝트에 기여해서 돈을 버는 사람은 매우 적습니다 — 일부는 그렇지만 대부분은 아닙니다. 자유 시간의 일부를 포기할 의지가 있어야 합니다(비디오 게임을 하고 TV를 보는 시간을 줄이는 것이 당신을 죽이지는 않을 것입니다). 오픈 소스 프로젝트에서 더 열심히 일할수록 프로그래머로서의 진정한 야망을 더 빨리 실현할 것입니다. 또한 직원 계약을 고려하는 것도 중요합니다 — 일부 고용주들은 당신 자신의 시간에도 기여할 수 있는 것을 제한할 수 있습니다. 게다가 저작권, 특허, 상표, 영업 비밀과 관련된 지적 재산권 법을 위반하지 않도록 주의해야 합니다.
오픈 소스는 동기 부여된 프로그래머에게 엄청난 기회를 제공합니다. 첫째, 다른 사람이 당신에게 흥미로운 솔루션을 어떻게 구현할지 볼 수 있습니다 — 다른 사람의 소스 코드를 읽으면서 많은 것을 배울 수 있습니다. 둘째, 프로젝트에 당신 자신의 코드와 아이디어를 기여할 수 있습니다 — 당신이 가진 모든 훌륭한 아이디어가 받아들여지지는 않겠지만 일부는 그럴 수도 있고, 솔루션을 작업하고 코드를 기여하는 것만으로도 새로운 것을 배울 것입니다. 셋째, 당신과 같은 종류의 소프트웨어에 대한 열정을 가진 훌륭한 사람들을 만날 것입니다 — 이런 오픈 소스 우정은 평생 지속될 수 있습니다. 넷째, 당신이 유능한 기여자라고 가정하면, 실제로 당신에게 흥미로운 기술에서 실세계 경험을 추가할 수 있을 것입니다.
오픈 소스 시작하기는 꽤 쉽습니다. 당신이 필요로 할 도구들(예: 소스 코드 관리, 편집기, 프로그래밍 언어, 빌드 시스템 등)에 대한 풍부한 문서가 있습니다. 먼저 작업하고 싶은 프로젝트를 찾고 그 프로젝트가 사용하는 도구들에 대해 배우세요. 프로젝트 자체에 대한 문서는 대부분의 경우 부족하겠지만, 코드를 직접 조사하는 것이 배우는 최고의 방법이므로 이것은 아마도 덜 중요할 것입니다. 참여하고 싶다면, 문서 작업을 도와주겠다고 제안할 수 있습니다. 또는 테스트 코드 작성 자원봉사부터 시작할 수 있습니다. 그것이 흥미롭게 들리지 않을 수도 있지만, 진실은 다른 사람의 소프트웨어용 테스트 코드를 작성하는 것이 소프트웨어에서 거의 모든 다른 활동보다 훨씬 빠르게 배우게 해준다는 것입니다. 테스트 코드를, 정말 좋은 테스트 코드를 작성하세요. 버그를 찾고, 수정을 제안하고, 친구를 만들고, 당신이 좋아하는 소프트웨어에서 작업하고, 당신의 소프트웨어 개발 야망을 실현하세요.
Richard Monson-Haefel이 작성함.
API 설계는 특히 대규모에서 어렵습니다. 수백 또는 수천 명의 사용자를 가질 API를 설계하고 있다면, 미래에 어떻게 변경할 수 있을지와 그 변경이 클라이언트 코드를 깨뜨릴 수 있는지에 대해 생각해야 합니다. 그 이상으로, API 사용자들이 당신에게 어떤 영향을 미치는지에 대해서도 생각해야 합니다. API 클래스 중 하나가 내부적으로 자체 메서드 중 하나를 사용한다면, 사용자가 당신의 클래스를 서브클래싱하고 그것을 오버라이드할 수 있다는 것을 기억해야 하며, 그것은 재앙적일 수 있습니다. 일부 사용자들이 그 메서드에 다른 의미를 부여했기 때문에 그 메서드를 변경할 수 없을 것입니다. 미래의 내부 구현 선택은 사용자들의 자비에 달려있습니다.
API 개발자들은 다양한 방법으로 이 문제를 해결하지만, 가장 쉬운 방법은 API를 잠그는 것입니다. Java로 작업하고 있다면 대부분의 클래스와 메서드를 final로 만들고 싶을 수도 있습니다. C#에서는 클래스와 메서드를 sealed로 만들 수도 있습니다. 사용하는 언어에 관계없이, 싱글톤을 통해 API를 제공하거나 정적 팩터리 메서드를 사용해서 행동을 오버라이드하고 나중에 당신의 선택을 제약할 수 있는 방식으로 코드를 사용할 수 있는 사람들로부터 보호하고 싶을 수도 있습니다. 이 모든 것이 합리적으로 보이지만, 정말 그럴까요?
지난 10년 동안, 우리는 단위 테스트가 실무의 극히 중요한 부분이라는 것을 점차 깨달았지만, 그 교훈이 업계에 완전히 스며들지는 않았습니다. 증거는 우리 주변 모든 곳에 있습니다. 서드파티 API를 사용하는 임의의 테스트되지 않은 클래스를 가져와서 그것을 위한 단위 테스트를 작성해보세요. 대부분의 경우, 문제에 부딪힐 것입니다. API를 사용하는 코드가 접착제처럼 달라붙어 있다는 것을 알게 될 것입니다. API 클래스들을 흉내내어 코드가 그들과 상호작용하는 것을 감지하거나, 테스트를 위한 반환값을 제공할 방법이 없습니다.
시간이 지나면서 이것은 나아질 것이지만, API를 설계할 때 테스트를 실제 사용 사례로 보기 시작할 때만 그럴 것입니다. 불행히도, 이것은 단순히 우리 코드를 테스트하는 것보다 조금 더 복잡합니다. 여기서 API 설계의 황금률이 들어맞습니다: 당신이 개발하는 API를 위한 테스트를 작성하는 것만으로는 충분하지 않습니다. 당신의 API를 사용하는 코드를 위한 단위 테스트를 작성해야 합니다. 그렇게 할 때, 사용자들이 독립적으로 코드를 테스트하려고 할 때 극복해야 할 장애물들을 직접 경험하게 됩니다.
개발자들이 당신의 API를 사용하는 코드를 테스트하기 쉽게 만드는 단 하나의 방법은 없습니다. static, final, sealed는 본질적으로 나쁜 구조체가 아닙니다. 때때로 유용할 수 있습니다. 하지만 테스트 문제를 인식하는 것이 중요하며, 그렇게 하려면 스스로 경험해야 합니다. 일단 경험하고 나면, 다른 설계 도전과제에 접근하는 것처럼 그것에 접근할 수 있습니다.
Michael Feathers가 작성함.
소프트웨어에서 충분히 오래 일해본 사람이라면 누구나 이런 질문들을 들어봤을 것입니다:
XYZ 예외가 발생하고 있어요. 문제가 뭔지 아세요?
질문하는 사람들은 스택 트레이스, 에러 로그, 또는 문제로 이어지는 어떤 맥락이라도 포함시키는 것을 거의 신경 쓰지 않습니다. 그들은 당신이 다른 차원에서 작동하며, 증거에 기반한 분석 없이도 해결책이 당신에게 나타난다고 생각하는 것 같습니다. 그들은 당신을 구루라고 생각합니다.
우리는 소프트웨어에 익숙하지 않은 사람들로부터 그런 질문을 기대합니다: 그들에게 시스템은 거의 마법 같아 보일 수 있으니까요. 저를 걱정시키는 것은 소프트웨어 커뮤니티에서 이것을 보는 것입니다. 프로그램 설계에서도 "재고 관리를 구축하고 있는데, 낙관적 잠금을 사용해야 할까요?"와 같은 비슷한 질문들이 나타납니다. 아이러니하게도, 질문하는 사람들은 종종 질문을 받는 사람보다 그것을 답할 더 나은 준비가 되어 있습니다. 질문자들은 아마도 맥락을 알고, 요구사항을 알고, 다른 전략들의 장단점에 대해 읽을 수 있습니다. 그런데도 그들은 맥락 없이 지능적인 답을 주기를 기대합니다. 그들은 마법을 기대합니다.
소프트웨어 업계가 이 구루 신화를 불식시킬 때입니다. "구루들"은 인간입니다. 그들은 우리 나머지와 마찬가지로 논리를 적용하고 체계적으로 문제를 분석합니다. 그들은 정신적 지름길과 직관을 활용합니다. 당신이 만났던 최고의 프로그래머를 생각해보세요: 어느 시점에서 그 사람은 지금의 당신보다 소프트웨어에 대해 덜 알았습니다. 누군가가 구루처럼 보인다면, 그것은 학습과 사고 과정 개선에 바친 수년간의 결과입니다. "구루"는 단순히 끊임없는 호기심을 가진 똑똑한 사람입니다.
물론, 타고난 적성에는 여전히 큰 차이가 있습니다. 저보다 더 똑똑하고, 더 지식이 많고, 더 생산적인 많은 해커들이 있고, 제가 그런 수준에 도달하지 못할 수도 있습니다. 그럼에도 불구하고, 구루 신화를 논파하는 것은 긍정적인 영향을 미칩니다. 예를 들어, 저보다 똑똑한 사람과 함께 일할 때 저는 반드시 기초 작업을 하고, 그 사람이 효율적으로 자신의 기술을 적용할 수 있도록 충분한 맥락을 제공합니다. 구루 신화를 제거한다는 것은 또한 개선에 대한 인지된 장벽을 제거한다는 의미입니다. 마법의 장벽 대신, 저는 제가 발전할 수 있는 연속체를 봅니다.
마지막으로, 소프트웨어의 가장 큰 장애물 중 하나는 의도적으로 구루 신화를 퍼뜨리는 똑똑한 사람들입니다. 이것은 자아 때문이거나, 클라이언트나 고용주에게 인식되는 자신의 가치를 높이기 위한 전략으로 행해질 수 있습니다. 아이러니하게도, 이런 태도는 똑똑한 사람들을 덜 가치 있게 만들 수 있는데, 그들이 동료들의 성장에 기여하지 않기 때문입니다. 우리에게는 구루가 필요하지 않습니다. 우리에게는 자신의 분야에서 다른 전문가들을 개발하려는 의지가 있는 전문가들이 필요합니다. 우리 모두를 위한 자리가 있습니다.
Ryan Brush가 작성함.
노력은 결실을 맺지 않는다1
프로그래머로서, 열심히 일한다고 항상 보상을 받는 것은 아닙니다. 긴 시간 사무실에 남아 있는 것만으로 프로젝트에 많은 기여를 하고 있다고 스스로와 몇몇 동료들을 속일 수 있을지 모르지만, 사실은 덜 일함으로써 더 많은 성과를 이룰 수 있습니다 — 때때로 훨씬 더 많이요. 한 주에 30시간 이상 집중하고 ‘생산적’이 되려 한다면, 아마도 너무 열심히 일하고 있는 것일 수 있습니다. 효율성을 높이고 더 많은 일을 완료하기 위해서는 업무량을 줄이는 것을 고려해 보아야 합니다.
이 주장은 직관에 반하고 심지어 논란이 될 수 있지만, 프로그래밍과 소프트웨어 개발은 기본적으로 지속적인 학습 과정이라는 사실의 직접적인 결과입니다. 프로젝트에 참여하면서 문제 영역을 더 잘 이해하게 되고, 목표를 달성하는 더 효과적인 방법들을 찾게 될 것입니다. 낭비된 작업을 피하기 위해서는 자신이 하는 일의 효과를 관찰할 시간을 허용하고, 이를 통해 보이는 것들에 대해 반성하며, 행동을 그에 맞게 조정해야 합니다.
전문 프로그래밍은 일반적으로 포장된 도로 끝에서 목표를 볼 수 있는 몇 킬로미터를 전력으로 질주하는 것과는 다릅니다. 대부분의 소프트웨어 프로젝트는 어두운 곳에서 스케치한 지도를 참고하며 진행하는 긴 오리엔티어링 마라톤과 더 유사합니다. 단순히 한 방향으로 달려 나가기만 한다면, 다른 사람을 감동시킬 수는 있겠지만 성공하기는 어려울 것입니다. 지속 가능한 속도를 유지해야 하며, 자신이 어디에 있는지, 어디로 향하고 있는지 더 많이 알게 될수록 방향을 조정해야 합니다.
또한, 일반적인 소프트웨어 개발과 특히 프로그래밍 기술에 대해 더 많이 배울 필요가 있습니다. 아마도 책을 읽고, 컨퍼런스에 참석하고, 다른 전문가와 소통하고, 새로운 구현 기술을 실험하며, 당신의 일을 간소화하는 강력한 도구들에 대해 배워야 할 것입니다. 전문 프로그래머로서 자신의 전문 분야에서 최신 정보를 유지해야 합니다 — 마치 뇌 외과의사와 조종사들이 자신의 전문 분야에서 최신 정보를 유지하는 것으로 기대되는 것처럼요. 그러므로 저녁, 주말 및 휴일 동안 현재 프로젝트에 추가 근무를 하는 것 대신 스스로 교육하는 데 시간을 할애해야 합니다. 뇌 외과의사가 주 60시간 수술을 하기를 기대하나요? 혹은 조종사가 주 60시간 비행하기를 기대하나요? 물론 아닙니다. 준비와 교육은 그들의 직업에서 필수적인 부분입니다.
프로젝트에 집중하고, 똑똑한 해결책을 찾아 가능한 한 많이 기여하며, 자신의 기술을 향상시키고, 자신이 하는 일에 대해 반성하고, 행동을 조정하세요. 스스로와 우리 직업을 수조 안의 햄스터처럼 허우적대게 하지 마세요. 전문 프로그래머로서, 주 60시간 동안 집중하고 '생산적'이 되려는 것은 이치에 맞지 않다는 것을 알아야 합니다. 전문적으로 행동하세요: 준비하고, 실천하고, 관찰하고, 반성하고, 변화를 주십시오.
Olve Maudal 씀.
그것들을 버그, 결함, 또는 심지어 설계 부작용이라고 부르든, 그것들로부터 벗어날 방법은 없습니다. 좋은 버그 리포트를 제출하는 방법과 버그 리포트에서 무엇을 찾아야 하는지 아는 것은 프로젝트를 순조롭게 진행시키기 위한 핵심 기술입니다.
좋은 버그 리포트에는 세 가지가 필요합니다:
- 가능한 한 정확하게 버그를 재현하는 방법, 그리고 이것이 얼마나 자주 버그를 나타나게 할지
- 최소한 당신의 의견으로는 무엇이 일어났어야 하는지
- 실제로 무엇이 일어났는지, 또는 최소한 당신이 기록한 만큼의 정보
버그에서 보고된 정보의 양과 품질은 버그 자체만큼이나 보고자에 대해 많은 것을 말해줍니다. 화나고 간결한 버그들("이 함수는 형편없어!")은 개발자들에게 당신이 힘든 시간을 보내고 있다는 것을 알려주지만, 그 외에는 별로 알려주지 않습니다. 재현하기 쉽게 만들기 위한 충분한 맥락을 가진 버그는 릴리스를 중단시키더라도 모든 사람의 존경을 얻습니다.
버그는 모든 기록이 모든 사람 앞에 있는 대화와 같습니다. 다른 사람들을 비난하거나 버그의 존재 자체를 부인하지 마세요. 대신 더 많은 정보를 요청하거나 당신이 놓쳤을 수 있는 것을 고려해보세요.
Open에서 Closed로와 같은 버그의 상태를 변경하는 것은 버그에 대해 당신이 어떻게 생각하는지의 공개적 선언입니다. 버그가 왜 닫혀야 한다고 생각하는지 설명하는 데 시간을 들이는 것은 나중에 좌절한 매니저들과 고객들에게 그것을 정당화하는 지루한 시간들을 절약해줄 것입니다. 버그의 우선순위를 변경하는 것은 비슷한 공개적 선언이며, 당신에게 사소한 것이라고 해서 그것이 다른 누군가가 제품을 사용하는 것을 막고 있지 않다는 의미는 아닙니다.
당신 자신의 목적을 위해 버그의 필드들을 과부하시키지 마세요. 버그의 제목 필드에 "VITAL:"을 추가하는 것은 어떤 리포트의 결과를 정렬하기 쉽게 만들어줄 수 있지만, 결국 다른 사람들에 의해 복사되고 필연적으로 잘못 타이핑되거나, 다른 리포트에서 사용하기 위해 제거되어야 할 것입니다. 대신 새로운 값이나 새로운 필드를 사용하고, 다른 사람들이 스스로를 반복할 필요가 없도록 그 필드가 어떻게 사용되어야 하는지 문서화하세요.
팀이 작업해야 하는 버그들을 모든 사람이 어떻게 찾을 수 있는지 확인하세요. 이것은 보통 명백한 이름을 가진 공개 쿼리를 사용해서 할 수 있습니다. 모든 사람이 같은 쿼리를 사용하고 있는지 확인하고, 먼저 모든 사람이 작업하고 있는 것을 변경한다고 팀에 알리지 않고는 이 쿼리를 업데이트하지 마세요.
마지막으로, 버그는 코드 줄이 노력의 정확한 측정이 아닌 것처럼 표준 작업 단위가 아니라는 것을 기억하세요.
Matt Doar가 작성함.
덜한 것이 더 많은 것입니다. 꽤 진부한 작은 격언이지만, 때때로 정말로 사실입니다.
지난 몇 주 동안 제가 우리 코드베이스에 한 개선 중 하나는 그것의 덩어리들을 제거하는 것이었습니다.
우리는 YAGNI(즉, You Aren't Gonna Need It)를 포함한 XP 원칙을 따라 소프트웨어를 작성했습니다. 인간의 본성이 그런 것이듯, 우리는 필연적으로 몇 곳에서 부족했습니다.
저는 제품이 특정 작업들 — 거의 즉시 되어야 하는 간단한 작업들 — 을 실행하는 데 너무 오래 걸린다는 것을 관찰했습니다. 이것은 그것들이 과도하게 구현되었기 때문이었습니다. 필요하지 않았지만 당시에는 좋은 아이디어처럼 보였던 추가적인 장식들로 치장되어 있었습니다.
그래서 저는 코드를 단순화하고, 제품 성능을 개선하고, 단순히 코드베이스에서 문제가 되는 기능들을 제거함으로써 전역 코드 엔트로피 수준을 줄였습니다. 다행히도, 제 단위 테스트들이 작업 중에 다른 것을 망가뜨리지 않았다고 알려줍니다.
간단하고 완전히 만족스러운 경험이었습니다.
그렇다면 왜 불필요한 코드가 애초에 거기 있게 되었을까요? 왜 한 프로그래머가 추가 코드를 작성할 필요를 느꼈고, 그것이 어떻게 리뷰나 페어링 과정을 통과했을까요? 거의 확실히 다음과 같은 것들 때문입니다:
- 그것은 재미있는 추가 기능이었고, 프로그래머가 그것을 작성하고 싶어했습니다. (힌트: 가치를 더하기 때문에 코드를 작성하세요, 그것이 당신을 즐겁게 하기 때문이 아니라.)
- 누군가가 미래에 필요할 수도 있다고 생각해서, 지금 코딩하는 것이 최선이라고 느꼈습니다. (힌트: 그것은 YAGNI가 아닙니다. 지금 당장 필요하지 않다면, 지금 당장 작성하지 마세요.)
- 그것이 그렇게 큰 "추가"로 보이지 않아서, 그것이 정말 필요한지 보기 위해 고객에게 돌아가기보다는 구현하는 것이 더 쉬웠습니다. (힌트: 추가 코드를 작성하고 유지보수하는 것은 항상 더 오래 걸립니다. 그리고 고객은 실제로 꽤 접근하기 쉽습니다. 작은 추가 코드 조각은 시간이 지나면서 유지보수가 필요한 큰 작업으로 눈덩이처럼 불어납니다.)
- 프로그래머가 문서화되지도 논의되지도 않은 추가 요구사항을 발명해서 추가 기능을 정당화했습니다. 요구사항은 실제로 가짜였습니다. (힌트: 프로그래머는 시스템 요구사항을 설정하지 않습니다. 고객이 합니다.)
지금 당신이 작업하고 있는 것은 무엇입니까? 그것이 모두 필요한가요?
Pete Goodliffe가 작성함.
나는 당신의 프로그램에 전혀 관심이 없습니다.
나는 문제에 둘러싸여 있으며, 할일 목록은 내 팔만큼 깁니다. 내가 지금 당신의 웹사이트에 있는 이유는 단 하나, 당신의 소프트웨어가 내 모든 문제를 해결해 줄 것이라는 불확실한 소문을 들었기 때문입니다. 제가 회의적이었다면 미안합니다.
눈 추적(eyeball tracking) 연구가 맞다면, 나는 이미 제목을 읽었고 이제는 지금 다운로드라는 파란색 밑줄이 있는 텍스트를 스캔하고 있습니다. 참고로, 만약 내가 UK IP로 Linux 브라우저로 이 페이지에 도착했다면, 유럽 미러에서 Linux 버전을 원할 가능성이 높습니다. 그러니 이 점은 묻지 말아 주세요. 파일 대화 상자가 즉시 열리면, 그 파일을 다운로드 폴더에 넣고 계속해서 읽습니다.
우리 모두는 우리가 하는 모든 일에 대해 비용-편익(cost-benefit) 분석을 지속적으로 수행합니다. 만약 당신의 프로젝트가 내 기준 이하로 떨어지면, 단 1초도 기다리지 않고 그것을 던져버리고 다른 것으로 넘어갈 것입니다. 즉각적인 만족이 최선입니다.
첫 번째 장애물은 설치입니다. 그게 별거 아닌 문제라고 생각하나요? 지금 다운로드 폴더로 가서 주변을 살펴보세요. tar와 zip 파일로 가득 차있죠? 그 중 몇 퍼센트나 unpack(압축 해제) 해 보았나요? 얼마나 설치했나요? 당신이 나와 같다면, 1/3 이상이 하드 드라이브의 용량을 채우기만 할 뿐입니다.
나는 문 앞의 편리함(doorstep convenience)을 원할지도 모르지만, 당신이 초대 없이 내 집에 들어오길 원하지는 않습니다. 설치를 입력하기 전에 당신이 정확히 무엇을 어디에 두는지 알고 싶습니다. 내 컴퓨터이며, 가능한 한 깔끔하게 유지하고 싶습니다. 또한 내가 당신의 프로그램에 실망하게 되면 즉시 제거할 수 있기를 원합니다. 만약 그게 불가능할 것 같다면, 처음부터 설치하지 않을 것입니다. 내 기계는 지금 안정적이며 그렇게 유지하고 싶습니다.
당신의 프로그램이 GUI 기반이라면, 나는 간단한 일을 하고 결과를 보고 싶습니다. 마법사(wizard)는 도움이 되지 않습니다. 왜냐하면 그들은 내가 이해하지 못하는 일을 하기 때문입니다. 나는 파일을 읽거나 하나를 작성하고 싶습니다. 프로젝트를 만들거나 디렉토리를 가져오거나 내 이메일 주소를 알려주고 싶지 않습니다. 모든 것이 잘 작동한다면, 튜토리얼로 넘어갑니다.
당신의 소프트웨어가 라이브러리라면, 나는 *빠른 시작 가이드(quick start guide)*를 찾기 위해 당신의 웹 페이지를 계속 읽습니다. 나는 아무 생각 없이도 다섯 줄짜리를 입력해서 당신의 웹사이트에 설명된 출력과 정확히 일치하는 "Hello world" 같은 것을 출력할 수 있기를 원합니다. 거대한 XML 파일이나 템플릿을 채우는 것도 원치 않으며, 단일 스크립트면 충분합니다. 기억하세요, 나는 또한 당신의 경쟁사의 프레임워크도 다운로드했습니다. 당신도 알겠지만, 포럼들에서 항상 당신의 것보다 훨씬 더 좋다고 주장하는 그거 말이에요. 모든 것이 잘 작동한다면, 튜토리얼로 넘어갑니다.
튜토리얼이 있겠죠? 내가 이해할 수 있는 언어로 나에게 이야기하는 튜토리얼요?
그리고 만약 튜토리얼이 내 문제를 언급한다면, 나는 기분이 좋아질 것입니다. 이제 내가 할 수 있는 것들에 대해 읽고 있으니 점점 흥미로워지고, 심지어 재미도 생깁니다. 나는 뒤로 기대고 차를 한 모금 마십니다— 내가 UK 출신이라고 말했나요? — 그리고 당신의 예제를 가지고 놀며 당신의 창작물을 사용할 수 있도록 배웁니다. 만약 그것이 내 문제를 해결한다면, 나는 감사 이메일을 보낼 것입니다. 충돌했을 때는 버그 보고를 하고, 기능에 대한 제안도 보낼 것입니다. 나는 당신의 소프트웨어가 최고라고 모든 친구들에게 말할 것입니다, 비록 나는 경쟁사의 것을 시도해보지 않았더라도요. 그리고 이 모든 것이 당신이 나의 첫 걸음에 그렇게 많은 주의를 기울였기 때문입니다. 어떻게 내가 당신을 의심할 수 있었겠습니까?
마커스 베이커 씀.
응답 시간은 소프트웨어 사용성에 중요합니다. 어떤 소프트웨어 시스템이 응답하기를 기다리는 것만큼 좌절스러운 것은 거의 없으며, 특히 소프트웨어와의 상호작용이 자극과 응답의 반복적인 사이클을 포함할 때 그렇습니다. 우리는 소프트웨어가 우리의 시간을 낭비하고 생산성에 영향을 미치고 있다고 느낍니다. 하지만 느린 응답 시간의 원인은 특히 현대 애플리케이션에서 잘 알려져 있지 않습니다. 많은 성능 관리 문헌들이 여전히 데이터 구조와 알고리즘에 초점을 맞추고 있는데, 이는 어떤 경우에는 차이를 만들 수 있지만 현대 다중 계층 엔터프라이즈 애플리케이션에서 성능을 지배할 가능성은 훨씬 낮습니다.
그런 애플리케이션에서 성능이 문제일 때, 제 경험으로는 데이터 구조와 알고리즘을 검토하는 것이 개선을 찾기 위한 올바른 장소가 아닙니다. 응답 시간은 자극에 대한 응답으로 수행되는 원격 프로세스 간 통신(IPC)의 수에 가장 강하게 의존합니다. 다른 로컬 병목 현상이 있을 수 있지만, 원격 프로세스 간 통신의 수가 보통 지배적입니다. 각 원격 프로세스 간 통신은 전체 응답 시간에 무시할 수 없는 지연을 기여하며, 이런 개별 기여들이 누적되고, 특히 순차적으로 발생할 때 그렇습니다.
대표적인 예는 객체-관계 매핑을 사용하는 애플리케이션에서의 리플 로딩입니다. 리플 로딩은 객체 그래프를 구축하는 데 필요한 데이터를 선택하기 위한 많은 데이터베이스 호출의 순차적 실행을 설명합니다(Martin Fowler의 Patterns of Enterprise Application Architecture에서 Lazy Load 참조). 데이터베이스 클라이언트가 웹 페이지를 렌더링하는 중간 계층 애플리케이션 서버일 때, 이런 데이터베이스 호출들은 보통 단일 스레드에서 순차적으로 실행됩니다. 그들의 개별 지연들이 누적되어 전체 응답 시간에 기여합니다. 각 데이터베이스 호출이 단지 10ms만 걸리더라도, 1000번의 호출이 필요한 페이지(흔하지 않은 일이 아님)는 최소 10초의 응답 시간을 보일 것입니다. 다른 예로는 웹 서비스 호출, 웹 브라우저로부터의 HTTP 요청, 분산 객체 호출, 요청-응답 메시징, 그리고 커스텀 네트워크 프로토콜을 통한 데이터 그리드 상호작용이 있습니다. 자극에 응답하는 데 필요한 원격 IPC가 많을수록, 응답 시간은 더 길어질 것입니다.
자극당 원격 프로세스 간 통신의 수를 줄이기 위한 몇 가지 상대적으로 명백하고 잘 알려진 전략들이 있습니다. 한 전략은 절약의 원칙을 적용하여, 프로세스 간 인터페이스를 최적화해서 당면한 목적에 정확히 맞는 데이터가 최소한의 상호작용으로 교환되도록 하는 것입니다. 다른 전략은 가능한 곳에서 프로세스 간 통신을 병렬화하여, 전체 응답 시간이 주로 가장 긴 지연을 가진 IPC에 의해 결정되도록 하는 것입니다. 세 번째 전략은 이전 IPC의 결과를 캐시하여, 로컬 캐시를 히트함으로써 미래의 IPC를 피할 수 있도록 하는 것입니다.
애플리케이션을 설계할 때, 각 자극에 대한 응답으로 일어나는 프로세스 간 통신의 수를 염두에 두세요. 성능이 좋지 않은 애플리케이션을 분석할 때, 저는 종종 수천 대 일의 IPC 대 자극 비율을 발견했습니다. 캐싱이나 병렬화 또는 다른 기법으로 이 비율을 줄이는 것은 데이터 구조 선택을 바꾸거나 정렬 알고리즘을 조정하는 것보다 훨씬 더 효과적일 것입니다.
Randy Stafford이 작성함.
나쁜 코딩에 대한 에세이 길이의 컴파일러 경고 목록을 보고 스스로 이렇게 생각해본 적이 있나요: "아, 정말 그것에 대해 뭔가 해야 하는데... 하지만 지금은 시간이 없어?" 반면에, 컴파일에서 방금 나타난 하나의 경고를 보고 그냥 고쳐본 적이 있나요?
처음부터 새로운 프로젝트를 시작할 때는 경고도 없고, 잡동사니도 없고, 문제도 없습니다. 하지만 코드베이스가 성장함에 따라, 주의를 기울이지 않으면 잡동사니, 찌꺼기, 경고, 그리고 문제들이 쌓이기 시작할 수 있습니다. 많은 소음이 있을 때, 신경 쓰지 않는 수백 개의 경고들 사이에서 정말로 읽고 싶은 경고를 찾는 것은 훨씬 더 어렵습니다.
경고를 다시 유용하게 만들기 위해, 저는 빌드로부터의 경고에 대해 무관용 정책을 사용하려고 합니다. 경고가 중요하지 않더라도, 저는 그것을 처리합니다. 중요하지는 않지만 여전히 관련이 있다면, 저는 그것을 고칩니다. 컴파일러가 잠재적인 null 포인터 예외에 대해 경고한다면, 저는 그 문제가 프로덕션에서 절대 나타나지 않을 것을 "안다"고 하더라도 원인을 고칩니다. 임베디드 문서(Javadoc 또는 유사한 것)가 제거되거나 이름이 바뀐 매개변수를 참조한다면, 저는 문서를 정리합니다.
정말로 신경 쓰지 않고 정말로 중요하지 않은 것이라면, 팀에게 우리의 경고 정책을 바꿀 수 있는지 물어봅니다. 예를 들어, 많은 경우에 메서드의 매개변수와 반환값을 문서화하는 것이 어떤 가치도 더하지 않는다고 생각하므로, 그것들이 누락되어도 경고가 되어서는 안 됩니다. 또는, 프로그래밍 언어의 새 버전으로 업그레이드하면 이전에는 괜찮았던 코드가 이제 경고를 내보낼 수 있습니다. 예를 들어, Java 5가 제네릭을 도입했을 때, 제네릭 타입 매개변수를 지정하지 않은 모든 오래된 코드가 경고를 줄 것입니다. 이것은 (적어도 아직은) 잔소리를 듣고 싶지 않은 종류의 경고입니다. 현실과 맞지 않는 경고 세트를 갖는 것은 아무에게도 도움이 되지 않습니다.
빌드가 항상 깨끗하도록 함으로써, 저는 경고를 만날 때마다 그것이 무관하다고 결정할 필요가 없을 것입니다. 것들을 무시하는 것은 정신적 작업이고, 저는 할 수 있는 모든 불필요한 정신적 작업을 제거해야 합니다. 깨끗한 빌드를 갖는 것은 또한 다른 누군가가 제 작업을 인수받기 쉽게 만듭니다. 제가 경고들을 남긴다면, 다른 누군가가 무엇이 관련 있고 무엇이 그렇지 않은지 헤쳐나가야 할 것입니다. 또는 더 가능성이 높게는, 중요한 것들을 포함하여 모든 경고들을 그냥 무시할 것입니다.
빌드로부터의 경고는 유용합니다. 그것들을 알아차리기 시작하려면 소음을 제거하기만 하면 됩니다. 큰 정리를 기다리지 마세요. 보고 싶지 않은 것이 나타나면, 즉시 처리하세요. 경고의 소스를 고치거나, 이 경고를 억제하거나, 도구의 경고 정책을 고치세요. 빌드를 깨끗하게 유지하는 것은 단순히 컴파일 에러나 테스트 실패를 없애는 것만이 아닙니다: 경고들도 코드 위생의 중요하고 중대한 부분입니다.
Johannes Brodwall이 작성함.
오늘날, 많은 소프트웨어 개발 도구들이 통합 개발 환경(IDE)의 형태로 패키지화되어 있습니다. Microsoft의 Visual Studio와 오픈소스 Eclipse가 두 가지 인기 있는 예이지만, 다른 많은 것들도 있습니다. IDE에 대해 좋아할 것이 많습니다. 사용하기 쉬울 뿐만 아니라, 빌드 프로세스와 관련된 많은 작은 세부사항들에 대해 생각하는 것에서 프로그래머를 해방시켜줍니다.
하지만 사용 편의성에는 단점이 있습니다. 일반적으로, 도구가 사용하기 쉬울 때, 그것은 도구가 당신을 위해 결정을 내리고 뒤에서 자동으로 많은 일들을 하고 있기 때문입니다. 따라서 IDE가 당신이 사용하는 유일한 프로그래밍 환경이라면, 당신은 도구들이 실제로 무엇을 하고 있는지 완전히 이해하지 못할 수도 있습니다. 당신은 버튼을 클릭하고, 어떤 마법이 일어나고, 실행 파일이 프로젝트 폴더에 나타납니다.
명령줄 빌드 도구로 작업함으로써, 당신은 프로젝트가 빌드될 때 도구들이 무엇을 하고 있는지에 대해 훨씬 더 많이 배울 것입니다. 자신만의 make 파일을 작성하는 것은 실행 파일을 빌드하는 데 들어가는 모든 단계들(컴파일, 어셈블, 링크 등)을 이해하는 데 도움이 될 것입니다. 이런 도구들의 많은 명령줄 옵션들을 실험해보는 것도 마찬가지로 가치 있는 교육적 경험입니다. 명령줄 빌드 도구 사용을 시작하려면, GCC 같은 오픈소스 명령줄 도구들을 사용하거나 당신의 상용 IDE와 함께 제공되는 것들을 사용할 수 있습니다. 결국, 잘 설계된 IDE는 단지 명령줄 도구 세트에 대한 그래픽 프런트엔드일 뿐입니다.
빌드 프로세스에 대한 이해를 향상시키는 것 외에도, IDE보다 명령줄 도구로 더 쉽게 또는 더 효율적으로 수행할 수 있는 몇 가지 작업들이 있습니다. 예를 들어, grep과 sed 유틸리티에 의해 제공되는 검색 및 교체 기능들은 종종 IDE에서 발견되는 것들보다 더 강력합니다. 명령줄 도구들은 본질적으로 스크립팅을 지원하므로, 예정된 일일 빌드 생성, 프로젝트의 여러 버전 생성, 테스트 스위트 실행과 같은 작업의 자동화를 허용합니다. IDE에서, 이런 종류의 자동화는 빌드 옵션들이 보통 GUI 대화상자를 사용해서 지정되고 빌드 프로세스가 마우스 클릭으로 호출되기 때문에 더 어렵거나 (불가능하지는 않더라도) 할 수 없을 수도 있습니다. IDE 밖으로 나가지 않는다면, 이런 종류의 자동화된 작업들이 가능하다는 것을 깨닫지도 못할 수도 있습니다.
하지만 잠깐. IDE가 개발을 더 쉽게 만들고, 프로그래머의 생산성을 향상시키기 위해 존재하는 것 아닌가요? 맞습니다. 여기서 제시하는 제안은 IDE 사용을 중단해야 한다는 것이 아닙니다. 제안은 "후드를 열어보고" IDE가 당신을 위해 무엇을 하고 있는지 이해해야 한다는 것입니다. 그렇게 하는 최고의 방법은 명령줄 도구 사용법을 배우는 것입니다. 그런 다음, IDE 사용으로 돌아갈 때, 그것이 당신을 위해 무엇을 하고 있는지와 빌드 프로세스를 어떻게 제어할 수 있는지에 대해 훨씬 더 나은 이해를 갖게 될 것입니다. 반면에, 일단 명령줄 도구의 사용을 마스터하고 그들이 제공하는 힘과 유연성을 경험하면, IDE보다 명령줄을 선호한다는 것을 알 수도 있습니다.
Carroll Robinson이 작성함.
프로그래밍 심리학 연구자들은 오랫동안 프로그래밍 전문성이 프로그래머가 편안하게 다룰 수 있는 서로 다른 프로그래밍 패러다임의 수와 직접적으로 관련이 있다는 것을 알고 있었습니다. 이것은 단순히 알고 있거나 조금 아는 것이 아니라, 진짜로 프로그래밍할 수 있다는 의미입니다.
모든 프로그래머는 하나의 프로그래밍 언어로 시작합니다. 그 언어는 프로그래머가 소프트웨어에 대해 생각하는 방식에 지배적인 영향을 미칩니다. 프로그래머가 그 언어를 사용해서 아무리 많은 년의 경험을 얻더라도, 그 언어에만 머물러 있다면, 그들은 오직 그 언어만 알게 될 것입니다. 한 언어 프로그래머는 그 언어에 의해 사고가 제약됩니다.
두 번째 언어를 배우는 프로그래머는 도전을 받게 될 것이며, 특히 그 언어가 첫 번째 언어와 다른 계산 모델을 가지고 있다면 더욱 그렇습니다. C, Pascal, Fortran은 모두 같은 기본적인 계산 모델을 가지고 있습니다. Fortran에서 C로 전환하는 것은 몇 가지 도전을 가져오지만, 많지는 않습니다. C나 Fortran에서 C++나 Ada로 이동하는 것은 프로그램이 작동하는 방식에서 근본적인 도전을 가져옵니다. C++에서 Haskell로 이동하는 것은 중대한 변화이고 따라서 중대한 도전입니다. C에서 Prolog로 이동하는 것은 매우 확실한 도전입니다.
우리는 여러 계산 패러다임들을 열거할 수 있습니다: 절차적, 객체지향, 함수형, 논리형, 데이터플로우 등. 이런 패러다임들 사이를 이동하는 것이 가장 큰 도전을 만듭니다.
왜 이런 도전들이 좋은 것일까요? 그것은 우리가 알고리즘의 구현과 적용되는 구현의 관용구 및 패턴에 대해 생각하는 방식과 관련이 있습니다. 특히, 교차 수정(cross-fertilization)이 전문성의 핵심입니다. 한 언어에서 적용되는 문제 해결을 위한 관용구들이 다른 언어에서는 불가능할 수도 있습니다. 한 언어의 관용구를 다른 언어로 포팅하려고 시도하는 것은 우리에게 두 언어와 해결되고 있는 문제에 대해 가르쳐줍니다.
프로그래밍 언어 사용에서의 교차 수정은 거대한 효과를 가집니다. 아마도 가장 명백한 것은 명령형 언어로 구현된 시스템에서 선언적 표현 방식의 증가하고 있는 사용입니다. 함수형 프로그래밍에 정통한 사람은 누구나 C 같은 언어를 사용할 때도 쉽게 선언적 접근법을 적용할 수 있습니다. 선언적 접근법을 사용하는 것은 일반적으로 더 짧고 더 이해하기 쉬운 프로그램으로 이어집니다. 예를 들어, C++는 거의 선언적 표현 방식을 필요로 하는 제네릭 프로그래밍에 대한 전심 전력의 지원으로 이것을 확실히 받아들입니다.
이 모든 것의 결과는 모든 프로그래머가 최소한 두 개의 서로 다른 패러다임에서 프로그래밍에 잘 숙련되어야 하고, 이상적으로는 최소한 위에서 언급한 다섯 개에서 그래야 한다는 것입니다. 프로그래머들은 항상 새로운 언어 배우기에 관심을 가져야 하며, 가급적이면 익숙하지 않은 패러다임의 언어를 배워야 합니다. 일상 업무가 항상 같은 프로그래밍 언어를 사용한다고 하더라도, 사람이 다른 패러다임들로부터 교차 수정할 수 있을 때 그 언어 사용의 증가된 정교함은 과소평가되어서는 안 됩니다. 고용주들은 이것을 받아들이고 현재 사용되고 있는 언어들의 사용 정교함을 증가시키는 방법으로 현재 사용되지 않는 언어들을 직원들이 배울 수 있도록 훈련 예산에 허용해야 합니다.
시작이긴 하지만, 일주일 훈련 과정은 새로운 언어를 배우기에 충분하지 않습니다: 언어에 대한 적절한 실무 지식을 얻으려면 일반적으로 파트타임이라고 하더라도 몇 달의 사용이 필요합니다. 중요한 요소는 구문과 계산 모델만이 아니라 사용의 관용구입니다.
Russel Winder가 작성함.
1980년대에 우리의 프로그래밍 환경은 일반적으로 미화된 텍스트 에디터 정도였습니다... 운이 좋다면 말입니다. 요즘 당연하게 여기는 구문 강조 표시는 확실히 모든 사람에게 제공되지 않는 럭셔리였습니다. 코드를 예쁘게 포맷해주는 프리티 프린터는 보통 우리의 간격을 고치기 위해 실행해야 하는 외부 도구였습니다. 디버거도 코드를 단계별로 실행하기 위해 실행하는 별도의 프로그램이었지만, 많은 암호 같은 키 조합이 필요했습니다.
1990년대 동안 회사들은 프로그래머들에게 더 좋고 더 유용한 도구를 제공함으로써 얻을 수 있는 잠재적 수입을 인식하기 시작했습니다. 통합 개발 환경(IDE)은 이전의 편집 기능을 컴파일러, 디버거, 프리티 프린터, 그리고 다른 도구들과 결합했습니다. 그 시기에 메뉴와 마우스도 인기를 얻게 되었는데, 이는 개발자들이 더 이상 편집기를 사용하기 위해 암호 같은 키 조합을 배울 필요가 없다는 것을 의미했습니다. 그들은 단순히 메뉴에서 명령을 선택할 수 있었습니다.
21세기에 IDE는 너무나 흔해져서 다른 영역에서 시장 점유율을 얻고자 하는 회사들에 의해 무료로 제공됩니다. 현대 IDE는 놀라운 기능들로 무장되어 있습니다. 제가 가장 좋아하는 것은 자동화된 리팩터링, 특히 Extract Method인데, 여기서 코드 덩어리를 선택해서 메서드로 변환할 수 있습니다. 리팩터링 도구는 메서드에 전달되어야 하는 모든 매개변수를 찾아내서, 코드를 수정하는 것을 극히 쉽게 만들어줍니다. 제 IDE는 심지어 이 메서드로 교체될 수 있는 다른 코드 덩어리들을 감지하고 그것들도 교체하고 싶은지 물어보기까지 합니다.
현대 IDE의 또 다른 놀라운 기능은 회사 내에서 스타일 규칙을 강제할 수 있는 능력입니다. 예를 들어, Java에서 일부 프로그래머들은 모든 매개변수를 final로 만들기 시작했습니다(제 의견으로는 시간 낭비입니다). 하지만 그들이 그런 스타일 규칙을 가지고 있으므로, 제가 그것을 따르기 위해 해야 할 일은 IDE에서 설정하는 것뿐입니다: final이 아닌 매개변수에 대해 경고를 받게 될 것입니다. 스타일 규칙은 또한 가능한 버그를 찾는 데 사용될 수 있는데, 예를 들어 참조 객체로 자동박싱된 기본값에 ==를 사용하는 것처럼 자동박싱된 객체들을 참조 동등성으로 비교하는 것 같은 경우입니다.
불행히도 현대 IDE는 우리가 그것들을 사용하는 방법을 배우기 위해 노력을 투자하도록 요구하지 않습니다. 제가 처음 Unix에서 C를 프로그래밍했을 때, 가파른 학습 곡선 때문에 vi 편집기가 어떻게 작동하는지 배우는 데 상당한 시간을 보내야 했습니다. 앞서 보낸 이 시간은 수년에 걸쳐 풍성하게 보상받았습니다. 저는 심지어 이 글의 초안도 vi로 타이핑하고 있습니다. 현대 IDE는 매우 점진적인 학습 곡선을 가지고 있는데, 이는 우리가 도구의 가장 기본적인 사용법을 넘어서 발전하지 않는 결과를 가져올 수 있습니다.
IDE를 배우는 제 첫 번째 단계는 키보드 단축키를 암기하는 것입니다. 코드를 타이핑할 때 제 손가락들이 키보드에 있으므로, 변수를 인라인하기 위해 Ctrl+Shift+I를 누르는 것은 흐름을 깨뜨리지 않고 절약해주는 반면, 마우스로 메뉴를 탐색하는 것으로 전환하는 것은 흐름을 방해합니다. 이런 방해들은 불필요한 컨텍스트 스위치로 이어져서, 모든 것을 게으른 방식으로 하려고 한다면 저를 훨씬 덜 생산적으로 만듭니다. 같은 규칙이 키보드 기술에도 적용됩니다: 터치 타이핑을 배우세요, 앞서 투자한 시간을 후회하지 않을 것입니다.
마지막으로, 프로그래머로서 우리에게는 코드를 조작하는 데 도움이 될 수 있는 시간이 증명된 Unix 스트리밍 도구들이 있습니다. 예를 들어, 코드 리뷰 중에 프로그래머들이 많은 클래스에 같은 이름을 지었다는 것을 알아차렸다면, find, sed, sort, uniq, grep 같은 도구들을 사용해서 이것들을 매우 쉽게 찾을 수 있습니다:
find . -name "*.java" | sed 's/.*\///' | sort | uniq -c | grep -v "^ *1 " | sort -r
우리 집에 오는 배관공이 토치를 사용할 수 있기를 기대합니다. IDE로 더 효과적이 되는 방법을 공부하는 데 약간의 시간을 보냅시다.
Heinz Kabutz가 작성함.
"사람은 자신의 한계를 알아야 한다." — 더티 해리
당신의 자원은 제한되어 있습니다. 당신은 지식, 기술, 도구를 최신 상태로 유지하는 데 필요한 시간과 돈을 포함하여 작업을 수행하는 데 제한된 시간과 돈만을 가지고 있습니다. 당신은 제한적으로만 열심히, 빠르게, 똑똑하게, 그리고 오래 일할 수 있습니다. 당신의 도구는 제한적으로만 강력합니다. 당신의 대상 머신들은 제한적으로만 강력합니다. 그러므로 당신은 자원의 한계를 존중해야 합니다.
그런 한계를 어떻게 존중할까요? 자신을 알고, 사람들을 알고, 예산을 알고, 자료를 알아야 합니다. 특히, 소프트웨어 엔지니어로서 데이터 구조와 알고리즘의 공간 및 시간 복잡도와, 시스템의 아키텍처 및 성능 특성을 알아야 합니다. 당신의 일은 소프트웨어와 시스템의 최적 결합을 만드는 것입니다.
공간과 시간 복잡도는 함수 *O(f(n))*로 주어지는데, 여기서 n이 입력의 크기와 같을 때 n이 무한대로 증가할 때 필요한 점근적 공간 또는 시간입니다. *f(n)*에 대한 중요한 복잡도 클래스들은 ln(n), n, n ln(n), ne, *en*을 포함합니다. 이런 함수들을 그래프로 그려보면 명확히 보이듯이, n이 커질수록 *O(ln(n))*은 *O(n)*이나 *O(n ln(n))*보다 훨씬 작고, 이들은 *O(ne)*나 *O(en)*보다 훨씬 작습니다. Sean Parent가 말하듯이, 달성 가능한 n에 대해 모든 복잡도 클래스들은 거의 상수, 거의 선형, 또는 거의 무한대에 해당합니다.
| 접근 시간 | 용량 | |
|---|---|---|
| 레지스터 | < 1 ns | 64b |
| 캐시 라인 | 64B | |
| L1 캐시 | 1 ns | 64 KB |
| L2 캐시 | 4 ns | 8 MB |
| RAM | 20 ns | 32 GB |
| 디스크 | 10 ms | 10 TB |
| LAN | 20 ms | > 1 PB |
| 인터넷 | 100 ms | > 1 ZB |
복잡도 분석은 추상 머신 관점에서 이루어지지만, 소프트웨어는 실제 머신에서 실행됩니다. 현대 컴퓨터 시스템은 언어 런타임, 운영 체제, CPU, 캐시 메모리, 임의 접근 메모리, 디스크 드라이브, 네트워크를 포함한 물리적 및 가상 머신의 계층으로 구성됩니다. 첫 번째 표는 전형적인 네트워크 서버의 임의 접근 시간과 저장 용량의 한계를 보여줍니다.
용량과 속도가 여러 자릿수만큼 변한다는 것을 주목하세요. 캐싱과 선행 읽기가 이런 변화를 숨기기 위해 시스템의 모든 수준에서 많이 사용되지만, 접근이 예측 가능할 때만 작동합니다. 캐시 미스가 빈번하면 시스템이 스래싱할 것입니다. 예를 들어, 하드 드라이브의 모든 바이트를 무작위로 검사하는 것은 32년이 걸릴 수 있습니다. 심지어 RAM의 모든 바이트를 무작위로 검사하는 것도 11분이 걸릴 수 있습니다. 무작위 접근은 예측 가능하지 않습니다. 무엇이 예측 가능할까요? 그것은 시스템에 따라 다르지만, 최근에 사용된 항목들을 다시 접근하고 항목들을 순차적으로 접근하는 것은 보통 승리입니다.
알고리즘과 데이터 구조는 캐시를 얼마나 효과적으로 사용하는지에서 차이가 납니다. 예를 들어:
- 선형 검색은 선행 읽기를 잘 활용하지만, O(n) 비교가 필요합니다.
- 정렬된 배열의 이진 검색은 단지 O(log(n)) 비교만 필요합니다.
- van Emde Boas 트리의 검색은 *O(log(n))*이고 캐시 무관(cache-oblivious)입니다.
| 요소들 | 검색 시간 (ns) | ||
|---|---|---|---|
| 선형 | 이진 | vEB | |
| 8 | 50 | 90 | 40 |
| 64 | 180 | 150 | 70 |
| 512 | 1200 | 230 | 100 |
| 4096 | 17000 | 320 | 160 |
어떻게 선택할까요? 최종 분석에서는 측정으로. 두 번째 표는 이 세 가지 방법을 통해 64비트 정수 배열을 검색하는 데 필요한 시간을 보여줍니다. 제 컴퓨터에서:
- 선형 검색은 작은 배열에서는 경쟁력이 있지만, 더 큰 배열에서는 지수적으로 집니다.
- van Emde Boas는 예측 가능한 접근 패턴 덕분에 압도적으로 승리합니다.
"돈을 내고 선택하는 거야." — Punch
Greg Colvin이 작성함.
저는 세 명의 프로그래머 어깨를 두드리며 무엇을 하고 있는지 물어봤습니다. "이 메서드들을 리팩터링하고 있어요"라고 첫 번째가 답했습니다. "이 웹 액션에 몇 가지 매개변수를 추가하고 있어요"라고 두 번째가 답했습니다. 세 번째는 "이 사용자 스토리를 작업하고 있어요"라고 답했습니다.
처음 두 명은 작업의 세부사항에 몰두하고 있는 반면 세 번째만이 큰 그림을 볼 수 있고, 후자가 더 나은 집중력을 가지고 있는 것처럼 보일 수 있습니다. 하지만 언제 그리고 무엇을 커밋할 것인지 물어봤을 때, 상황이 극적으로 바뀌었습니다. 처음 두 명은 어떤 파일들이 관련될 것인지 꽤 명확했고 한 시간 정도 내에 끝날 것이라고 했습니다. 세 번째 프로그래머는 "아, 아마 며칠 내에 준비될 것 같아요. 아마 몇 개의 클래스를 추가하고 그 서비스들을 어떤 방식으로든 변경할 것 같아요"라고 답했습니다.
처음 두 명은 전체적인 목표에 대한 비전이 부족하지 않았습니다. 그들은 생산적인 방향으로 이끈다고 생각하는 작업을 선택했고, 몇 시간 내에 끝낼 수 있는 것들이었습니다. 그 작업들을 끝내고 나면, 그들은 작업할 새로운 기능이나 리팩터링을 선택할 것입니다. 작성된 모든 코드는 따라서 명확한 목적과 제한적이고 달성 가능한 목표를 염두에 두고 이루어졌습니다.
세 번째 프로그래머는 문제를 분해할 수 없었고 모든 측면을 한 번에 작업하고 있었습니다. 그는 무엇이 필요할지 아이디어가 없었고, 기본적으로 추측 프로그래밍을 하며 커밋할 수 있는 어떤 지점에 도착하기를 희망하고 있었습니다. 이 긴 세션 시작에 작성된 코드는 아마도 끝에 나온 해결책과 잘 맞지 않을 것입니다.
처음 두 프로그래머가 작업이 두 시간 이상 걸린다면 어떻게 할까요? 너무 많이 맡았다는 것을 깨달은 후, 그들은 아마도 변경사항을 버리고, 더 작은 작업들을 정의하고, 다시 시작할 것입니다. 계속 작업하는 것은 집중력이 부족하고 추측 코드가 저장소에 들어가게 할 것입니다. 대신, 변경사항은 버려지지만 통찰력은 유지됩니다.
세 번째 프로그래머는 계속 추측하고 필사적으로 자신의 변경사항을 커밋할 수 있는 무언가로 짜맞추려고 할 수도 있습니다. 결국, 해온 코드 변경사항을 버릴 수는 없습니다 — 그것은 낭비된 작업이 될 테니까요, 안 그렇나요? 불행히도, 코드를 버리지 않는 것은 명확한 목적이 부족한 약간 이상한 코드가 저장소에 들어가게 합니다.
어느 시점에서 커밋 중심 프로그래머들조차 두 시간 내에 끝낼 수 있다고 생각하는 유용한 무언가를 찾는 데 실패할 수도 있습니다. 그러면, 그들은 직접 추측 모드로 들어가서 코드를 가지고 놀고, 물론 어떤 통찰력이 그들을 다시 궤도에 올려놓을 때마다 변경사항을 버릴 것입니다. 이런 겉보기에 구조화되지 않은 해킹 세션들조차 목적이 있습니다: 생산적인 단계를 구성할 작업을 정의할 수 있도록 코드에 대해 배우는 것입니다.
다음 커밋을 알아두세요. 끝낼 수 없다면, 변경사항을 버리고, 얻은 통찰력으로 믿을 수 있는 새로운 작업을 정의하세요. 필요할 때마다 추측적 실험을 하되, 알아차리지 못한 채 추측 모드로 빠지지 마세요. 추측 작업을 저장소에 커밋하지 마세요.
Dan Bergh Johnsson이 작성함.
애플리케이션이 크고, 지속적이고, 상호연결된 데이터 요소 집합을 처리하려고 한다면, 관계형 데이터베이스에 저장하는 것을 주저하지 마세요. 과거에 RDBMS는 비싸고, 희소하고, 복잡하고, 다루기 힘든 짐승이었습니다. 더 이상 그렇지 않습니다. 요즘 RDBMS 시스템은 찾기 쉽습니다 — 당신이 사용하고 있는 시스템에 이미 하나 또는 두 개가 설치되어 있을 가능성이 높습니다. MySQL과 PostgreSQL 같은 매우 유능한 RDBMS들이 오픈소스 소프트웨어로 제공되므로, 구매 비용은 더 이상 문제가 되지 않습니다. 더 좋은 것은, 소위 임베디드 데이터베이스 시스템들이 라이브러리로 직접 애플리케이션에 링크될 수 있어서, 거의 설정이나 관리가 필요 없다는 것입니다 — 두 개의 주목할 만한 오픈소스가 SQLite와 HSQLDB입니다. 이런 시스템들은 극히 효율적입니다.
애플리케이션의 데이터가 시스템의 RAM보다 크다면, 인덱스된 RDBMS 테이블은 가상 메모리 페이지를 스래싱할 라이브러리의 맵 컬렉션 타입보다 몇 자릿수 더 빠르게 수행될 것입니다. 현대 데이터베이스 제품들은 쉽게 당신의 요구와 함께 성장할 수 있습니다. 적절한 주의로, 필요할 때 임베디드 데이터베이스를 더 큰 데이터베이스 시스템으로 스케일업할 수 있습니다. 나중에 무료 오픈소스 제품에서 더 잘 지원되거나 더 강력한 상용 시스템으로 전환할 수 있습니다.
SQL의 요령을 터득하고 나면, 데이터베이스 중심 애플리케이션을 작성하는 것은 즐거움입니다. 적절히 정규화된 데이터를 데이터베이스에 저장한 후, 읽기 쉬운 SQL 쿼리로 효율적으로 사실들을 추출하기 쉽습니다. 복잡한 코드를 작성할 필요가 없습니다. 마찬가지로, 단일 SQL 명령이 복잡한 데이터 변경을 수행할 수 있습니다. 일회성 수정, 예를 들어 지속적인 데이터를 구성하는 방식의 변경에 대해서는 코드를 작성할 필요조차 없습니다: 그냥 데이터베이스의 직접 SQL 인터페이스를 시작하세요. 이 같은 인터페이스는 또한 일반적인 프로그래밍 언어의 컴파일-편집 사이클을 우회하여 쿼리를 실험할 수 있게 해줍니다.
RDBMS를 중심으로 코드를 기반으로 하는 것의 또 다른 장점은 데이터 요소들 간의 관계 처리를 포함합니다. 선언적 방식으로 데이터에 대한 일관성 제약조건을 기술할 수 있어서, 엣지 케이스에서 데이터를 업데이트하는 것을 잊으면 얻게 되는 댕글링 포인터의 위험을 피할 수 있습니다. 예를 들어, 사용자가 삭제되면 그 사용자가 보낸 메시지들도 제거되어야 한다고 지정할 수 있습니다.
또한 단순히 인덱스를 생성함으로써 언제든 원하는 때에 데이터베이스에 저장된 개체들 간의 효율적인 링크를 생성할 수 있습니다. 클래스 필드의 비싸고 광범위한 리팩터링을 수행할 필요가 없습니다. 게다가, 데이터베이스를 중심으로 코딩하는 것은 여러 애플리케이션이 안전한 방식으로 데이터에 접근할 수 있게 해줍니다. 이는 동시 사용을 위해 애플리케이션을 업그레이드하기 쉽게 만들고 또한 가장 적절한 언어와 플랫폼을 사용해서 애플리케이션의 각 부분을 코딩하기 쉽게 만들어줍니다. 예를 들어, 웹 기반 애플리케이션의 XML 백엔드를 Java로, 몇 가지 감사 스크립트를 Ruby로, 시각화 인터페이스를 Processing으로 작성할 수 있습니다.
마지막으로, RDBMS가 SQL 명령을 최적화하기 위해 열심히 일할 것이라는 점을 염두에 두세요. 이는 당신이 알고리즘 튜닝보다는 애플리케이션의 기능에 집중할 수 있게 해줍니다. 고급 데이터베이스 시스템들은 심지어 당신이 모르는 사이에 멀티코어 프로세서를 활용할 것입니다. 그리고, 기술이 개선되면서 애플리케이션의 성능도 개선될 것입니다.
Diomidis Spinellis가 작성함.
프로그래머는 소통해야 합니다. 많이.
프로그래머의 삶에는 대부분의 소통이 컴퓨터와 이루어지는 것처럼 보이는 시기들이 있습니다. 더 정확히 말하면, 그 컴퓨터에서 실행되는 프로그램들과의 소통입니다. 이 소통은 아이디어를 기계가 읽을 수 있는 방식으로 표현하는 것에 관한 것입니다. 이것은 여전히 신나는 전망으로 남아있습니다: 프로그램은 사실상 물리적 실체가 개입되지 않은 채로 현실로 바뀐 아이디어들입니다.
프로그래머는 실제든 가상이든 기계의 언어와, 개발 도구를 통해 그 언어와 관련될 수 있는 추상화들에 유창해야 합니다. 많은 서로 다른 추상화를 배우는 것이 중요한데, 그렇지 않으면 일부 아이디어들이 표현하기 엄청나게 어려워지기 때문입니다. 좋은 프로그래머들은 일상적인 루틴 밖에 서서, 다른 목적으로 표현력이 있는 다른 언어들을 인식할 수 있어야 합니다. 이것이 보상받는 때가 항상 옵니다.
기계와의 소통을 넘어서, 프로그래머는 동료들과 소통해야 합니다. 오늘날의 대규모 프로젝트들은 단순히 프로그래밍 기술의 적용이라기보다는 더 많은 사회적 노력입니다. 기계가 읽을 수 있는 추상화가 할 수 있는 것 이상을 이해하고 표현하는 것이 중요합니다. 제가 아는 최고의 프로그래머들 대부분은 또한 그들의 모국어에 매우 유창하며, 일반적으로 다른 언어들에도 그렇습니다. 이것은 단순히 다른 사람들과의 소통에 관한 것만이 아닙니다: 언어를 잘 구사하는 것은 또한 문제를 추상화할 때 없어서는 안 될 사고의 명확성으로 이어집니다. 그리고 이것이 프로그래밍이 또한 다루는 바입니다.
기계, 자신, 동료들과의 소통을 넘어서, 프로젝트에는 대부분 다르거나 기술적 배경이 없는 많은 이해관계자들이 있습니다. 그들은 테스팅, 품질 및 배포에 살고, 마케팅과 영업에 있으며, 어떤 사무실(또는 상점이나 집)의 최종 사용자들입니다. 당신은 그들과 그들의 관심사를 이해해야 합니다. 그들의 언어 — 그들의 세계, 그들의 도메인의 언어를 말할 수 없다면 이것은 거의 불가능합니다. 당신은 그들과의 대화가 잘 되었다고 생각할 수도 있지만, 그들은 아마도 그렇게 생각하지 않을 것입니다.
회계사들과 이야기한다면, 당신은 비용 센터 회계, 묶인 자본, 사용된 자본 등에 대한 기본 지식이 필요합니다. 마케팅이나 변호사들과 이야기한다면, 그들의 전문 용어와 언어(그리고 따라서 그들의 마음)의 일부가 당신에게 친숙해야 합니다. 이런 모든 도메인별 언어들은 프로젝트의 누군가에 의해 마스터되어야 합니다 — 이상적으로는 프로그래머들에 의해. 프로그래머들은 궁극적으로 컴퓨터를 통해 아이디어들을 생명으로 가져오는 책임이 있습니다.
그리고, 물론, 삶은 소프트웨어 프로젝트보다 더 많은 것입니다. 샤를마뉴가 언급했듯이, 다른 언어를 아는 것은 다른 영혼을 갖는 것입니다. 소프트웨어 업계를 넘어선 당신의 접촉에 대해, 당신은 외국어를 아는 것을 감사하게 여길 것입니다. 언제 말하기보다는 듣는지를 아는 것. 대부분의 언어가 말 없는 것임을 아는 것.
말할 수 없는 것에 대해서는 침묵해야 한다. - 루트비히 비트겐슈타인
Klaus Marquardt가 작성함.
프로그래머로서 당신은 수행해야 하는 작업들에 대해 매니저, 동료, 사용자에게 추정치를 제공할 수 있어야 하므로, 그들이 목표를 달성하는 데 필요한 시간, 비용, 기술, 그리고 다른 자원들에 대해 합리적으로 정확한 아이디어를 가질 수 있습니다.
잘 추정할 수 있으려면 분명히 몇 가지 추정 기법을 배우는 것이 중요합니다. 하지만 무엇보다도, 추정이 무엇인지, 그리고 무엇을 위해 사용되어야 하는지를 배우는 것이 근본적입니다 — 이상하게 들릴 수도 있지만, 많은 개발자와 매니저들이 이것을 정말로 모릅니다.
프로젝트 매니저와 프로그래머 사이의 다음 대화는 드물지 않습니다:
프로젝트 매니저: 기능 xyz를 개발하는 데 필요한 시간의 추정치를 줄 수 있나요?
프로그래머: 한 달입니다.
프로젝트 매니저: 그건 너무 오래요! 우리는 일주일밖에 없어요.
프로그래머: 최소한 3주는 필요해요.
프로젝트 매니저: 최대 2주를 줄 수 있어요.
프로그래머: 거래 성사!
프로그래머는 결국 매니저에게 수용 가능한 것과 맞는 "추정치"를 내놓습니다. 하지만 그것이 프로그래머의 추정치로 여겨지므로, 매니저는 프로그래머에게 그것에 대한 책임을 물을 것입니다. 이 대화에서 무엇이 잘못되었는지 이해하려면 세 가지 정의 — 추정, 목표, 약속이 필요합니다:
- 추정은 무언가의 가치, 수, 양, 또는 범위에 대한 대략적인 계산이나 판단입니다. 이 정의는 추정이 확실한 데이터와 이전 경험에 기반한 사실적 측정이라는 것을 함의합니다 — 희망과 소망은 그것을 계산할 때 무시되어야 합니다. 정의는 또한 대략적이므로 추정이 정확할 수 없다는 것을 함의합니다. 예를 들어, 개발 작업은 234.14일 지속될 것으로 추정될 수 없습니다.
- 목표는 바람직한 비즈니스 목적의 선언입니다. 예: "시스템은 최소한 400명의 동시 사용자를 지원해야 합니다."
- 약속은 특정 날짜나 이벤트까지 특정 수준의 품질로 지정된 기능을 전달하겠다는 약속입니다. 한 예는 "검색 기능은 제품의 다음 릴리스에서 사용 가능할 것입니다"일 수 있습니다.
추정, 목표, 약속은 서로 독립적이지만, 목표와 약속은 건전한 추정에 기반해야 합니다. Steve McConnell이 언급하듯이, "소프트웨어 추정의 주요 목적은 프로젝트의 결과를 예측하는 것이 아닙니다. 프로젝트의 목표가 프로젝트를 그것들을 달성하도록 제어할 수 있을 만큼 충분히 현실적인지 결정하는 것입니다." 따라서, 추정의 목적은 적절한 프로젝트 관리와 계획을 가능하게 만드는 것으로, 프로젝트 이해관계자들이 현실적인 목표에 기반하여 약속을 할 수 있게 해줍니다.
위 대화에서 매니저가 프로그래머에게 정말로 요청한 것은 추정을 제공하는 것이 아니라, 매니저가 마음에 두고 있던 명시되지 않은 목표에 기반한 약속을 하는 것이었습니다. 다음에 추정을 제공하라고 요청받을 때는 관련된 모든 사람이 그들이 무엇에 대해 이야기하고 있는지 알도록 확인하세요. 그러면 당신의 프로젝트가 성공할 더 나은 기회를 갖게 될 것입니다. 이제 몇 가지 기법들을 배울 때입니다....
Giovanni Asproni가 작성함.
Paul Lee, 사용자명 leep, 더 일반적으로 Hoppy라고 알려진 그는 프로그래밍 문제의 현지 전문가로 명성이 있었습니다. 저는 도움이 필요했습니다. 저는 Hoppy의 책상으로 걸어가서 물었습니다. 제 코드 좀 봐주실 수 있나요?
물론이죠, Hoppy가 말했습니다. 의자를 가져와 앉으세요. 저는 그의 뒤에 피라미드 모양으로 쌓여 있는 빈 콜라 캔들을 넘어뜨리지 않도록 조심했습니다.
무슨 코드인가요?
파일 안의 함수입니다, 제가 말했습니다.
그럼 이 함수를 한번 봅시다. Hoppy는 K&R 책 한 권을 옆으로 밀어내고 그의 키보드를 제 앞으로 밀었습니다.
IDE는 어디 있나요? 보아하니 Hoppy는 IDE를 실행하지 않고 있었고, 그냥 제가 조작할 수 없는 에디터만 사용하고 있었습니다. 그가 키보드를 다시 가져갔습니다. 몇 번의 키 입력 후에 우리는 파일을 열었고 — 꽤 큰 파일이었습니다 — 그 함수를 보고 있었습니다 — 꽤 큰 함수였습니다. 그는 제가 질문하고 싶었던 조건부 블록으로 페이지를 내렸습니다.
x가 음수라면 이 절이 실제로 무엇을 할까요? 제가 물었습니다. 분명히 잘못된 것 같은데요.
저는 온 아침 동안 x를 음수로 만들 방법을 찾으려고 노력했지만, 큰 파일 안의 큰 함수는 큰 프로젝트의 일부였고, 재컴파일한 다음 실험을 다시 실행하는 사이클이 저를 지치게 했습니다. Hoppy 같은 전문가가 그냥 답을 알려줄 수는 없을까요?
Hoppy는 확실하지 않다고 인정했습니다. 놀랍게도, 그는 K&R에 손을 뻗지 않았습니다. 대신, 그는 코드 블록을 새로운 에디터 버퍼에 복사하고, 들여쓰기를 다시 하고, 함수로 감쌌습니다. 잠시 후 그는 영원히 반복하는 main 함수를 코딩했는데, 사용자에게 입력값을 요청하고, 그것을 함수에 전달하고, 결과를 출력했습니다. 그는 버퍼를 tryit.c라는 새 파일로 저장했습니다. 이 모든 것은 제가 스스로 할 수 있었던 일이었지만, 아마도 그렇게 빠르지는 못했을 것입니다. 하지만 그의 다음 단계는 놀랍도록 간단했고, 그 당시 제 작업 방식과는 상당히 다른 것이었습니다:
$ cc tryit.c && ./a.out
보세요! 불과 몇 분 전에 생각해낸 그의 실제 프로그램이 이제 실행되고 있었습니다. 우리는 몇 가지 값을 시도해보았고 제 의심을 확인했습니다(그래서 제가 뭔가에 대해서는 옳았던 것입니다!) 그리고 나서 그는 K&R의 관련 섹션을 교차 확인했습니다. 저는 Hoppy에게 감사를 표하고 떠났습니다. 다시 한번 그의 콜라 캔 피라미드를 건드리지 않도록 조심하면서 말입니다.
제 책상으로 돌아와서, 저는 IDE를 닫았습니다. 저는 큰 제품 안의 큰 프로젝트에서 작업하는 것에 너무 익숙해져서 그것이 제가 해야 하는 일이라고 생각하기 시작했습니다. 범용 컴퓨터는 작은 작업들도 할 수 있습니다. 저는 텍스트 에디터를 열고 타이핑을 시작했습니다.
#include <stdio.h>
int main()
{
printf("Hello, World\n");
return 0;
}
Thomas Guest가 작성함.
당신의 프로젝트에는 아마도 버전 제어 시스템이 있을 것입니다. 아마도 자동화된 테스트로 정확성을 검증하는 지속적 통합 서버에 연결되어 있을 수도 있습니다. 그것은 훌륭합니다.
정적 코드 분석을 위한 도구들을 지속적 통합 서버에 포함시켜 코드 메트릭을 수집할 수 있습니다. 이러한 메트릭들은 코드의 특정 측면과 시간에 따른 진화에 대한 피드백을 제공합니다. 코드 메트릭을 설치할 때, 넘어서면 안 되는 빨간 선이 항상 있을 것입니다. 20% 테스트 커버리지로 시작해서 절대 15% 아래로 떨어지고 싶지 않다고 가정해봅시다. 지속적 통합은 이 모든 수치들을 추적하는 데 도움이 되지만, 여전히 정기적으로 확인해야 합니다. 이 작업을 프로젝트 자체에 위임하고 상황이 악화될 때 보고하도록 의존할 수 있다고 상상해보세요.
프로젝트에 목소리를 주어야 합니다. 이는 이메일이나 인스턴트 메시징을 통해 개발자들에게 수치의 최근 하락이나 개선에 대해 알려주는 방식으로 할 수 있습니다. 하지만 극단적 피드백 장치(XFD)를 사용해서 프로젝트를 사무실에 물리적으로 구현하는 것이 더욱 효과적입니다.
XFD의 아이디어는 자동 분석 결과에 기반해서 램프, 휴대용 분수, 장난감 로봇, 심지어 USB 로켓 발사기 같은 물리적 장치를 구동하는 것입니다. 한계를 위반할 때마다 장치가 상태를 바꿉니다. 램프의 경우, 밝고 명백하게 불이 켜집니다. 급하게 집에 가려고 문을 나서더라도 메시지를 놓칠 수 없습니다.
극단적 피드백 장치의 유형에 따라, 빌드가 깨지는 소리를 들을 수도 있고, 코드의 빨간 경고 신호를 볼 수도 있고, 심지어 코드 스멜의 냄새를 맡을 수도 있습니다. 분산 팀에서 작업한다면 장치들을 다른 위치에 복제할 수 있습니다. 프로젝트 매니저의 사무실에 전체 프로젝트 건강 상태를 나타내는 신호등을 설치할 수 있습니다. 프로젝트 매니저가 그것을 고마워할 것입니다.
적절한 장치를 선택하는 데 창의성을 발휘하세요. 문화가 상당히 기술적이라면, 팀 마스코트에 무선 조종 장난감을 장착할 방법을 찾아볼 수 있습니다. 더 전문적인 모습을 원한다면, 세련된 디자이너 램프에 투자하세요. 더 많은 영감을 얻기 위해 인터넷을 검색해보세요. 전원 플러그나 리모컨이 있는 모든 것은 극단적 피드백 장치로 사용될 가능성이 있습니다.
극단적 피드백 장치는 프로젝트의 음성 상자 역할을 합니다. 이제 프로젝트는 개발자들과 물리적으로 함께 거주하며, 팀이 선택한 규칙에 따라 그들에게 불평하거나 칭찬합니다. 음성 합성 소프트웨어와 한 쌍의 확성기를 적용해서 이 의인화를 더 진전시킬 수 있습니다. 이제 프로젝트가 정말로 스스로 말합니다.
Daniel Lindner가 작성함.
우울할 정도로 자주 (이 글을 쓰기 직전에도 제게 일어났습니다), 많은 프로그래머들이 컴파일된 언어에서 소스 코드로부터 정적으로 링크된 실행 파일을 만드는 과정에 대해 갖고 있는 관점은 다음과 같습니다:
- 소스 코드 편집
- 소스 코드를 오브젝트 파일로 컴파일
- 마법 같은 일이 일어남
- 실행 파일 실행
3단계는 물론 링킹 단계입니다. 제가 왜 이런 터무니없는 말을 할까요? 저는 수십 년 동안 기술 지원을 해왔고, 다음과 같은 질문들을 계속해서 받습니다:
- 링커가 def가 두 번 이상 정의되었다고 합니다.
- 링커가 abc가 미해결 심볼이라고 합니다.
- 제 실행 파일이 왜 이렇게 큰가요?
그 다음에는 "이제 어떻게 해야 하나요?"가 이어지는데, 보통 "~인 것 같다"와 "어떻게든"이라는 구문이 섞여 있고, 완전히 당황한 분위기가 감돕니다. 링킹 과정이 마법적인 과정으로 여겨지고 있다는 것을 나타내는 것이 바로 "~인 것 같다"와 "어떻게든"이며, 아마도 마법사와 흑마법사들만 이해할 수 있다고 추정됩니다. 컴파일 과정은 이런 종류의 구문을 이끌어내지 않는데, 이는 프로그래머들이 일반적으로 컴파일러가 어떻게 작동하는지, 또는 적어도 무엇을 하는지 이해한다는 것을 함의합니다.
링커는 매우 바보스럽고, 평범하고, 직관적인 프로그램입니다. 링커가 하는 일은 오브젝트 파일들의 코드와 데이터 섹션을 연결하고, 심볼 참조를 그들의 정의와 연결하고, 라이브러리에서 미해결 심볼을 끌어내고, 실행 파일을 작성하는 것뿐입니다. 그게 전부입니다. 주문도 없고! 마법도 없습니다! 링커를 작성할 때의 지루함은 보통 터무니없이 과도하게 복잡한 파일 형식을 디코딩하고 생성하는 것에 관한 것이지만, 그것이 링커의 본질적인 성격을 바꾸지는 않습니다.
그럼 링커가 def가 두 번 이상 정의되었다고 말한다고 합시다. C, C++, D 같은 많은 프로그래밍 언어들은 선언과 정의를 모두 갖고 있습니다. 선언은 보통 헤더 파일로 들어가며, 다음과 같습니다:
extern int iii;
이것은 심볼 iii에 대한 외부 참조를 생성합니다. 반면에 정의는 실제로 심볼을 위한 저장소를 마련하고, 보통 구현 파일에 나타나며, 다음과 같이 생겼습니다:
int iii = 3;
각 심볼에 대해 몇 개의 정의가 있을 수 있을까요? 영화 하이랜더에서처럼, 오직 하나만 있을 수 있습니다. 그럼 iii의 정의가 두 개 이상의 구현 파일에 나타난다면 어떻게 될까요?
// File a.c
int iii = 3;
// File b.c
double iii(int x) { return 3.7; }
링커는 iii가 중복 정의되었다고 불평할 것입니다.
오직 하나만 있을 수 있을 뿐만 아니라, 하나는 있어야 합니다. iii가 선언으로만 나타나고 정의는 전혀 없다면, 링커는 iii가 미해결 심볼이라고 불평할 것입니다.
실행 파일이 왜 그 크기인지 알아보려면, 링커가 선택적으로 생성하는 맵 파일을 살펴보세요. 맵 파일은 실행 파일의 모든 심볼들과 그들의 주소 목록에 불과합니다. 이것은 라이브러리에서 어떤 모듈들이 링크되었는지와 각 모듈의 크기를 알려줍니다. 이제 비대함이 어디서 오는지 볼 수 있습니다. 종종 왜 링크되었는지 전혀 모르겠는 라이브러리 모듈들이 있을 것입니다. 그것을 알아내려면, 의심스러운 모듈을 라이브러리에서 일시적으로 제거하고 다시 링크하세요. 그러면 생성되는 미정의 심볼 에러가 누가 그 모듈을 참조하고 있는지 나타낼 것입니다.
특정 링커 메시지를 받는 이유가 항상 즉시 명백한 것은 아니지만, 링커에 대해 마법적인 것은 없습니다. 메커니즘은 직관적입니다; 각각의 경우에 해결해야 하는 것은 세부사항입니다.
Walter Bright가 작성함.
우리는 왜 임시 솔루션을 만들까요?
보통 해결해야 할 즉각적인 문제가 있습니다. 그것은 개발 팀 내부의 문제일 수도 있고, 도구 체인의 공백을 메우는 도구일 수도 있습니다. 최종 사용자에게 보이는 외부 문제일 수도 있는데, 누락된 기능을 해결하는 우회책 같은 것입니다.
대부분의 시스템과 팀에서 시스템과 어느 정도 분리되어 있고, 언젠가 바뀔 초안으로 여겨지며, 나머지 코드를 형성한 표준과 가이드라인을 따르지 않는 소프트웨어를 찾을 수 있을 것입니다. 필연적으로 개발자들이 이런 것들에 대해 불평하는 소리를 들을 것입니다. 그것들이 만들어진 이유는 많고 다양하지만, 임시 솔루션의 성공 열쇠는 간단합니다: 그것이 유용하다는 것입니다.
하지만 임시 솔루션들은 관성(또는 관점에 따라 추진력)을 얻습니다. 그들이 거기 있고, 궁극적으로 유용하고 널리 받아들여지기 때문에, 다른 것을 할 즉각적인 필요가 없습니다. 이해관계자가 어떤 행동이 가장 많은 가치를 더하는지 결정해야 할 때마다, 임시 솔루션의 적절한 통합보다 높게 순위가 매겨지는 많은 것들이 있을 것입니다. 왜일까요? 그것이 거기 있고, 작동하고, 받아들여지기 때문입니다. 유일하게 인식되는 단점은 그것이 선택된 표준과 가이드라인을 따르지 않는다는 것인데 — 몇몇 틈새 시장을 제외하고는, 이것이 중요한 힘으로 여겨지지 않습니다.
그래서 임시 솔루션은 그 자리에 남아 있습니다. 영원히.
그리고 그 임시 솔루션과 관련해서 문제가 발생한다면, 그것을 받아들여진 생산 품질 수준에 맞게 업데이트하기 위한 준비가 있을 가능성은 낮습니다. 어떻게 해야 할까요? 그 임시 솔루션에 대한 빠른 임시 업데이트가 종종 일을 해결합니다. 그리고 아마도 잘 받아들여질 것입니다. 그것은 초기 임시 솔루션과 같은 강점을 보여줍니다... 단지 더 최신일 뿐입니다.
이것이 문제일까요?
답은 당신의 프로젝트와 생산 코드 표준에 대한 당신의 개인적인 이해관계에 달려 있습니다. 시스템에 너무 많은 임시 솔루션이 포함되어 있을 때, 엔트로피나 내부 복잡성이 증가하고 유지보수성이 감소합니다. 하지만 이것은 아마도 먼저 물어봐야 할 잘못된 질문일 것입니다. 우리가 솔루션에 대해 이야기하고 있다는 것을 기억하세요. 그것이 당신이 선호하는 솔루션이 아닐 수도 있습니다 — 그것이 누구의 선호 솔루션일 가능성도 낮습니다 — 하지만 이 솔루션을 다시 작업하려는 동기는 약합니다.
그럼 문제를 본다면 무엇을 할 수 있을까요?
- 애초에 임시 솔루션을 만드는 것을 피한다.
- 프로젝트 매니저의 결정에 영향을 미치는 힘을 바꾼다.
- 그대로 둔다.
이 옵션들을 더 자세히 살펴봅시다:
- 회피는 대부분의 곳에서 작동하지 않습니다. 해결해야 할 실제 문제가 있고, 표준들이 너무 제한적인 것으로 판명났습니다. 표준을 바꾸려고 어느 정도 에너지를 쓸 수도 있습니다. 명예롭긴 하지만 지루한 노력이고... 그 변화가 당면한 문제에 시간 안에 효과적이지 않을 것입니다.
- 이 힘들은 의지적 변화에 저항하는 프로젝트 문화에 뿌리를 두고 있습니다. 매우 작은 프로젝트에서 — 특히 그것이 당신뿐이라면 — 미리 묻지 않고 그냥 혼란을 정리하는 경우에 성공할 수 있습니다. 프로젝트가 눈에 띄게 정체된 그런 혼란이어서 정리를 위한 어느 정도의 시간이 일반적으로 받아들여지는 경우에도 성공할 수 있습니다.
- 이전 옵션이 적용되지 않는다면 현상 유지가 자동으로 적용됩니다.
당신은 많은 솔루션을 만들 것이고, 그 중 일부는 임시적일 것이며, 대부분은 유용할 것입니다. 임시 솔루션을 극복하는 최선의 방법은 그것들을 불필요하게 만드는 것, 더 우아하고 유용한 솔루션을 제공하는 것입니다. 당신이 바꿀 수 없는 것들을 받아들이는 평정심과, 바꿀 수 있는 것들을 바꿀 용기, 그리고 그 차이를 아는 지혜를 부여받기를 바랍니다.
Klaus Marquardt가 작성함.
소프트웨어 개발에서 가장 일반적인 작업 중 하나는 인터페이스 명세입니다. 인터페이스는 가장 높은 추상화 수준(사용자 인터페이스)에서, 가장 낮은 수준(함수 인터페이스)에서, 그리고 그 사이의 수준들(클래스 인터페이스, 라이브러리 인터페이스 등)에서 발생합니다. 최종 사용자와 함께 시스템과 상호작용하는 방법을 명세하든, 개발자들과 협력해서 API를 명세하든, 클래스에 비공개 함수를 선언하든, 인터페이스 설계는 당신 업무의 중요한 부분입니다. 잘 한다면, 당신의 인터페이스는 사용하기 즐거울 것이고 다른 사람들의 생산성을 향상시킬 것입니다. 잘못 한다면, 당신의 인터페이스는 좌절과 에러의 원천이 될 것입니다.
좋은 인터페이스는:
- 올바르게 사용하기 쉽습니다. 잘 설계된 인터페이스를 사용하는 사람들은 거의 항상 인터페이스를 올바르게 사용하는데, 그것이 가장 저항이 적은 경로이기 때문입니다. GUI에서, 그들은 거의 항상 올바른 아이콘, 버튼, 또는 메뉴 항목을 클릭하는데, 그것이 명백하고 쉬운 일이기 때문입니다. API에서, 그들은 거의 항상 올바른 매개변수와 올바른 값을 전달하는데, 그것이 가장 자연스럽기 때문입니다. 올바르게 사용하기 쉬운 인터페이스에서는, 그냥 작동합니다.
- 잘못 사용하기 어렵습니다. 좋은 인터페이스는 사람들이 범할 수 있는 실수를 예상하고 그것들을 어렵게 — 이상적으로는 불가능하게 — 만듭니다. 예를 들어, GUI는 현재 맥락에서 의미가 없는 명령들을 비활성화하거나 제거할 수 있고, API는 매개변수를 임의의 순서로 전달할 수 있게 해서 인수 순서 문제를 제거할 수 있습니다.
올바르게 사용하기 쉬운 인터페이스를 설계하는 좋은 방법은 그것들이 존재하기 전에 연습해보는 것입니다. GUI를 모의로 만들어보세요 — 아마도 화이트보드에나 테이블 위의 인덱스 카드를 사용해서 — 그리고 기반 코드가 만들어지기 전에 그것을 가지고 놀아보세요. 함수들이 선언되기 전에 API 호출을 작성해보세요. 일반적인 사용 사례들을 살펴보고 인터페이스가 어떻게 행동하기를 원하는지 명세하세요. 무엇을 클릭할 수 있기를 원하나요? 무엇을 전달할 수 있기를 원하나요? 사용하기 쉬운 인터페이스는 자연스럽게 보이는데, 그것들이 당신이 하고 싶어하는 것을 할 수 있게 해주기 때문입니다. 사용자의 관점에서 그것들을 개발한다면 그런 인터페이스를 생각해낼 가능성이 높습니다. (이 관점은 테스트 우선 프로그래밍의 강점 중 하나입니다.)
인터페이스를 잘못 사용하기 어렵게 만드는 것은 두 가지를 요구합니다. 첫째, 사용자들이 범할 수 있는 에러를 예상하고 그것들을 방지할 방법을 찾아야 합니다. 둘째, 초기 릴리스 동안 인터페이스가 어떻게 잘못 사용되는지 관찰하고 인터페이스를 수정해야 합니다 — 그렇습니다, 인터페이스를 수정하는 것입니다! — 그런 에러들을 방지하기 위해서 말입니다. 잘못된 사용을 방지하는 최선의 방법은 그런 사용을 불가능하게 만드는 것입니다. 사용자들이 계속해서 되돌릴 수 없는 행동을 취소하려고 한다면, 그 행동을 되돌릴 수 있게 만들려고 해보세요. 그들이 계속해서 API에 잘못된 값을 전달한다면, 사용자들이 전달하고 싶어하는 값을 받도록 API를 수정하기 위해 최선을 다하세요.
무엇보다도, 인터페이스는 구현하는 사람이 아니라 사용하는 사람의 편의를 위해 존재한다는 것을 기억하세요.
Scott Meyers가 작성함.
비가시성의 많은 측면들이 소프트웨어 원칙으로서 당연히 칭찬받습니다. 우리의 용어는 비가시성 은유가 풍부합니다 — 메커니즘 투명성과 정보 은닉을 예로 들 수 있습니다. 소프트웨어와 그것을 개발하는 과정은 Douglas Adams의 말을 빌리면, 대부분 보이지 않을 수 있습니다:
- 소스 코드는 타고난 존재감도, 타고난 행동도 없고, 물리 법칙에 순종하지도 않습니다. 에디터에 로드할 때는 보이지만, 에디터를 닫으면 사라집니다. 너무 오래 생각하면, 아무도 듣지 않는 상황에서 쓰러지는 나무처럼, 그것이 정말 존재하는지 궁금해지기 시작합니다.
- 실행 중인 애플리케이션은 존재감과 행동을 갖지만, 그것이 만들어진 소스 코드에 대해서는 아무것도 드러내지 않습니다. Google의 홈페이지는 기분 좋게 최소한입니다; 그 뒤에서 벌어지는 일들은 분명히 상당할 것입니다.
- 90% 완료했고 마지막 10%를 디버깅하려고 끝없이 발버둥치고 있다면 90% 완료한 것이 아닙니다, 그렇죠? 버그 수정은 진전이 아닙니다. 디버깅하라고 돈을 받는 것이 아닙니다. 디버깅은 낭비입니다. 낭비를 더 보이게 만들어서 그것이 무엇인지 볼 수 있고 애초에 그것을 만들지 않으려고 노력하는 것에 대해 생각하기 시작할 수 있게 하는 것이 좋습니다.
- 프로젝트가 겉보기에 순조롭게 진행되고 있다가 일주일 후에 6개월 늦어진다면 문제가 있는 것인데, 가장 큰 문제는 아마도 6개월 늦어진 것이 아니라 6개월의 지연을 숨길 만큼 강력한 비가시성 보호막일 것입니다! 보이는 진전의 부족은 진전의 부족과 같은 의미입니다.
비가시성은 위험할 수 있습니다. 생각을 묶을 수 있는 구체적인 것이 있을 때 더 명확하게 생각할 수 있습니다. 볼 수 있고 지속적으로 변화하는 것을 볼 수 있을 때 더 잘 관리할 수 있습니다:
- 단위 테스트 작성은 코드 단위가 단위 테스트하기 얼마나 쉬운지에 대한 증거를 제공합니다. 코드가 보여주기를 바라는 개발적 품질들의 존재(또는 부재)를 드러내는 데 도움이 됩니다; 낮은 결합도와 높은 응집도 같은 품질들 말입니다.
- 단위 테스트 실행은 코드의 행동에 대한 증거를 제공합니다. 애플리케이션이 보여주기를 바라는 런타임 품질들의 존재(또는 부재)를 드러내는 데 도움이 됩니다; 견고함과 정확성 같은 품질들 말입니다.
- 게시판과 카드 사용은 진전을 보이고 구체적으로 만듭니다. 작업들을 숨겨진 프로젝트 관리 도구에 대한 참조 없이, 그리고 허구적인 상태 보고서를 위해 프로그래머들을 쫓아다닐 필요 없이 시작 안 함, 진행 중, 또는 완료로 볼 수 있습니다.
- 점진적 개발 수행은 개발 증거의 빈도를 증가시켜 개발 진전(또는 그것의 부족)의 가시성을 증가시킵니다. 릴리스 가능한 소프트웨어의 완성은 현실을 드러냅니다; 추정치는 그렇지 않습니다.
정기적으로 보이는 충분한 증거와 함께 소프트웨어를 개발하는 것이 최선입니다. 가시성은 진전이 진짜이고 환상이 아니며, 의도적이고 의도하지 않은 것이 아니며, 반복 가능하고 우발적이지 않다는 확신을 줍니다.
Jon Jagger가 작성함.
프로그래머들은 컴퓨팅 공부의 시작부터 동시성 — 그리고 특히 동시성의 특별한 부분집합인 병렬성 — 이 어렵다고, 가장 뛰어난 사람들만이 그것을 올바르게 할 수 있기를 바랄 수 있고, 심지어 그들도 틀린다고 배웁니다. 스레드, 세마포어, 모니터, 그리고 변수에 대한 동시 접근을 스레드 안전하게 만드는 것이 얼마나 어려운지에 대해 항상 큰 초점이 있습니다.
사실, 많은 어려운 문제들이 있고, 그것들은 해결하기 매우 어려울 수 있습니다. 하지만 문제의 근본은 무엇일까요? 공유 메모리입니다. 사람들이 계속해서 이야기하는 동시성의 문제들은 거의 모두 공유 가변 메모리의 사용과 관련이 있습니다: 경쟁 조건, 데드락, 라이브락 등. 답은 명백해 보입니다: 동시성을 포기하거나 공유 메모리를 피하는 것입니다!
동시성을 포기하는 것은 거의 확실히 선택사항이 아닙니다. 컴퓨터는 거의 분기마다 점점 더 많은 코어를 갖고 있으므로, 진정한 병렬성을 활용하는 것이 점점 더 중요해집니다. 애플리케이션 성능을 향상시키기 위해 계속 증가하는 프로세서 클록 속도에 더 이상 의존할 수 없습니다. 병렬성을 활용해야만 애플리케이션의 성능이 향상될 것입니다. 분명히, 성능을 향상시키지 않는 것도 선택사항이지만, 사용자들에게 받아들여질 가능성은 낮습니다.
그럼 공유 메모리를 피할 수 있을까요? 확실히 가능합니다.
프로그래밍 모델로 스레드와 공유 메모리를 사용하는 대신, 프로세스와 메시지 전달을 사용할 수 있습니다. 여기서 프로세스는 단순히 실행 코드가 있는 보호된 독립적 상태를 의미하며, 반드시 운영체제 프로세스일 필요는 없습니다. Erlang(그리고 그 이전의 occam) 같은 언어들은 프로세스가 동시 및 병렬 시스템을 프로그래밍하는 매우 성공적인 메커니즘임을 보여주었습니다. 그런 시스템들은 공유 메모리, 멀티스레드 시스템들이 갖는 모든 동기화 스트레스가 없습니다. 게다가 그런 시스템들의 엔지니어링의 일부로 적용될 수 있는 형식적 모델 — Communicating Sequential Processes (CSP) — 이 있습니다.
더 나아가서 컴퓨팅 방법으로 데이터플로우 시스템을 도입할 수 있습니다. 데이터플로우 시스템에서는 명시적으로 프로그래밍된 제어 흐름이 없습니다. 대신 데이터 경로로 연결된 연산자들의 방향 그래프가 설정되고 나서 데이터가 시스템에 공급됩니다. 평가는 시스템 내 데이터의 준비성에 의해 제어됩니다. 확실히 동기화 문제가 없습니다.
이 모든 것을 말했지만, C, C++, Java, Python, Groovy 같은 언어들이 시스템 개발의 주요 언어들이고 이 모든 것들이 프로그래머들에게 공유 메모리, 멀티스레드 시스템을 개발하기 위한 언어로 제시됩니다. 그럼 무엇을 할 수 있을까요? 답은 공유 가변 메모리의 모든 사용을 피하면서 프로세스 모델과 메시지 전달을 제공하는 라이브러리와 프레임워크를 사용하거나 — 존재하지 않는다면 — 만드는 것입니다.
전체적으로, 공유 메모리로 프로그래밍하지 않고 대신 메시지 전달을 사용하는 것이 이제 컴퓨터 하드웨어에 만연한 병렬성을 활용하는 시스템을 구현하는 가장 성공적인 방법일 가능성이 높습니다. 이상하게도, 프로세스가 동시성의 단위로서 스레드보다 앞서지만, 미래는 프로세스를 구현하기 위해 스레드를 사용하는 것에 있는 것 같습니다.
Russel Winder가 작성함.
아마도 그들 대부분이 똑똑한 사람들이기 때문일 텐데, 제가 프로그래머들을 가르치고 함께 일한 모든 해를 통틀어, 그들 대부분이 자신들이 씨름하고 있던 문제들이 어려웠기 때문에 솔루션도 모든 사람(심지어 코드가 작성된 지 몇 달 후의 자신에게도)이 이해하고 유지보수하기에 똑같이 어려워야 한다고 생각하는 것 같았습니다.
제 자료구조 수업의 학생이었던 Joe와의 한 사건이 기억납니다. 그는 자신이 작성한 것을 보여주러 와야 했습니다. "이게 뭘 하는지 맞혀보세요!" 그가 큰소리쳤습니다.
"맞습니다," 저는 그의 예제에 너무 많은 시간을 쓰지 않고 동의했고, 어떻게 중요한 메시지를 전달할지 고민했습니다. "분명히 이것에 열심히 작업하셨을 것입니다. 하지만 중요한 것을 잊으신 것은 아닌지 궁금합니다. Joe, 동생이 있지 않나요?"
"네. 있어요! Phil이요! 선생님 입문 수업을 듣고 있어요. 그 애도 프로그래밍을 배우고 있어요!" Joe가 자랑스럽게 말했습니다.
"훌륭하네요," 제가 답했습니다. "그 애가 이 코드를 읽을 수 있을까요?"
"절대 안 돼요!" Joe가 말했습니다. "이건 어려운 거예요!"
"가정해봅시다," 제가 제안했습니다, "이것이 실제 작동하는 코드이고 몇 년 후에 Phil이 유지보수 업데이트를 하기 위해 고용되었다고 말입니다. 당신이 그를 위해 무엇을 했나요?" Joe는 그냥 눈을 깜빡이며 저를 쳐다봤습니다. "Phil이 정말 똑똑하다는 것을 아시죠?" Joe가 고개를 끄덕였습니다. "그리고 말하기 싫지만, 저도 꽤 똑똑해요!" Joe가 웃었습니다. "그럼 제가 여기서 당신이 한 일을 쉽게 이해할 수 없고 당신의 매우 똑똑한 동생도 이것을 두고 고민할 가능성이 높다면, 당신이 작성한 것에 대해 무엇을 의미할까요?" Joe가 자신의 코드를 조금 다르게 보는 것 같았습니다. "이건 어떨까요," 제가 최선의 '저는 당신의 친근한 멘토입니다' 목소리로 제안했습니다, "당신이 작성하는 모든 코드 줄을 미래의 누군가 — 당신의 동생일 수도 있는 — 에게 보내는 메시지라고 생각해보세요. 이 똑똑한 사람에게 이 어려운 문제를 어떻게 해결하는지 설명한다고 가정해보세요.
"이것이 당신이 상상하고 싶은 것인가요? 미래의 똑똑한 프로그래머가 당신의 코드를 보고 말하는 것이, '와! 이거 훌륭해! 여기서 무엇이 이루어졌는지 완벽하게 이해할 수 있고 얼마나 우아한 — 아니, 잠깐 — 얼마나 아름다운 코드인지 놀랍네. 팀의 다른 사람들에게 보여줄 거야. 이건 걸작이야!'
"Joe, 이 어려운 문제를 해결하지만 너무 아름다워서 노래할 코드를 작성할 수 있다고 생각하나요? 네, 마치 잊혀지지 않는 멜로디처럼 말입니다. 여기 있는 매우 어려운 솔루션을 생각해낼 수 있는 사람이라면 아름다운 것도 작성할 수 있다고 생각합니다. 음... 아름다움으로 점수를 매기기 시작해야 할까요? 어떻게 생각하세요, Joe?"
Joe는 자신의 작업을 집어 들고 저를 보았는데, 작은 미소가 얼굴에 스며들었습니다. "알겠어요, 교수님, Phil을 위해 세상을 더 좋게 만들러 가겠습니다. 고맙습니다."
Linda Rising이 작성함.
다형성은 객체지향의 근본이 되는 위대한 아이디어 중 하나입니다. 그리스어에서 가져온 이 단어는 많은(poly) 형태(morph)를 의미합니다. 프로그래밍의 맥락에서 다형성은 특정 클래스의 객체나 메서드의 많은 형태를 가리킵니다. 하지만 다형성은 단순히 대안적 구현에 관한 것이 아닙니다. 신중하게 사용된다면, 다형성은 장황한 if-then-else 블록의 필요 없이 작업할 수 있게 해주는 작고 지역화된 실행 컨텍스트를 만듭니다. 컨텍스트 안에 있다는 것은 우리가 올바른 일을 직접 할 수 있게 해주는 반면, 그 컨텍스트 밖에 있다는 것은 올바른 일을 할 수 있도록 그것을 재구성하도록 강요합니다. 대안적 구현의 신중한 사용으로, 우리는 더 읽기 쉬운 적은 코드를 생산하는 데 도움이 될 수 있는 컨텍스트를 포착할 수 있습니다. 이것은 다음과 같은 (비현실적으로) 간단한 장바구니 코드로 가장 잘 설명됩니다:
public class ShoppingCart {
private ArrayList<Item> cart = new ArrayList<Item>();
public void add(Item item) { cart.add(item); }
public Item takeNext() { return cart.remove(0); }
public boolean isEmpty() { return cart.isEmpty(); }
}
우리 웹샵이 다운로드할 수 있는 아이템과 배송이 필요한 아이템을 제공한다고 해봅시다. 이러한 작업을 지원하는 다른 객체를 만들어봅시다:
public class Shipping {
public boolean ship(Item item, SurfaceAddress address) { ... }
public boolean ship(Item item, EMailAddress address) { ... }
}
고객이 체크아웃을 완료했을 때 상품을 배송해야 합니다:
while (!cart.isEmpty()) {
shipping.ship(cart.takeNext(), ???);
}
??? 매개변수는 새로운 멋진 엘비스 연산자가 아니라, 아이템을 이메일로 보낼지 일반 우편으로 보낼지 물어보는 것입니다. 이 질문에 답하는 데 필요한 컨텍스트가 더 이상 존재하지 않습니다. 배송 방법을 boolean이나 enum으로 포착한 다음 if-then-else를 사용해서 누락된 매개변수를 채울 수 있습니다. 다른 솔루션은 둘 다 Item을 확장하는 두 개의 클래스를 만드는 것입니다. 이것들을 DownloadableItem과 SurfaceItem이라고 부릅시다. 이제 코드를 작성해봅시다. Item을 단일 메서드인 ship을 지원하는 인터페이스로 승격시키겠습니다. 장바구니의 내용을 배송하기 위해, item.ship(shipper)를 호출할 것입니다. DownloadableItem과 SurfaceItem 클래스 둘 다 ship을 구현할 것입니다.
public class DownloadableItem implements Item {
public boolean ship(Shipping shipper) {
shipper.ship(this, customer.getEmailAddress());
}
}
public class SurfaceItem implements Item {
public boolean ship(Shipping shipper) {
shipper.ship(this, customer.getSurfaceAddress());
}
}
이 예에서 우리는 Shipping과 작업하는 책임을 각 Item에 위임했습니다. 각 아이템은 어떻게 가장 잘 배송되는지 알고 있으므로, 이 배치는 if-then-else의 필요 없이 일을 진행할 수 있게 해줍니다. 코드는 또한 종종 잘 어울리는 두 패턴의 사용을 보여줍니다: Command와 Double Dispatch. 이러한 패턴의 효과적인 사용은 다형성의 신중한 사용에 의존합니다. 그렇게 될 때 우리 코드의 if-then-else 블록 수가 감소할 것입니다.
다형성 대신 if-then-else를 사용하는 것이 훨씬 더 실용적인 경우들이 있지만, 더 다형적인 코딩 스타일이 더 작고, 더 읽기 쉽고, 덜 취약한 코드 베이스를 산출하는 경우가 더 많습니다. 놓친 기회의 수는 우리 코드의 if-then-else 문의 간단한 개수입니다.
Kirk Pepperdine이 작성함.
그들이 스스로를 품질 보증이라고 부르든 품질 제어라고 부르든, 많은 프로그래머들은 그들을 문제라고 부릅니다. 제 경험상, 프로그래머들은 종종 자신들의 소프트웨어를 테스트하는 사람들과 적대적인 관계를 갖습니다. "그들은 너무 까다롭다"와 "그들은 모든 것이 완벽하기를 원한다"는 일반적인 불평입니다. 친숙하게 들리나요?
왜인지는 확실하지 않지만, 저는 항상 테스터들에 대해 다른 관점을 가져왔습니다. 아마도 제 첫 직장의 "테스터"가 회사 비서였기 때문일 것입니다. Margaret는 사무실을 운영하고, 몇 명의 젊은 프로그래머들에게 고객 앞에서 전문적으로 행동하는 법을 가르치려고 노력하는 매우 좋은 여성이었습니다. 그녀는 또한 아무리 모호한 버그라도 순간 만에 찾아내는 재능이 있었습니다.
그때 저는 자신을 프로그래머라고 생각하는 회계사가 작성한 프로그램에서 작업하고 있었습니다. 말할 필요도 없이, 그것에는 심각한 문제들이 있었습니다. 제가 한 부분을 정리했다고 생각할 때, Margaret가 그것을 사용해보려고 하면, 대개는 단지 몇 번의 키 입력 후에 새로운 방식으로 실패하곤 했습니다. 때로는 좌절스럽고 당황스러웠지만, 그녀는 너무 유쾌한 사람이어서 저를 형편없어 보이게 만드는 것에 대해 그녀를 탓한다고 생각해본 적이 없었습니다. 결국 Margaret가 프로그램을 깔끔하게 시작하고, 송장을 입력하고, 인쇄하고, 종료할 수 있는 날이 왔습니다. 저는 흥분했습니다. 더 좋았던 것은, 고객의 머신에 설치했을 때 모든 것이 작동했다는 것입니다. 그들은 어떤 문제도 보지 못했는데, Margaret가 먼저 그것들을 찾고 고치도록 도와주었기 때문입니다.
그래서 제가 테스터들이 당신의 친구라고 말하는 이유입니다. 사소한 문제들을 보고함으로써 테스터들이 당신을 형편없어 보이게 만든다고 생각할 수도 있습니다. 하지만 QC가 당신에게 고치도록 만든 모든 그 "작은 것들"에 방해받지 않아서 고객들이 기뻐할 때, 당신은 훌륭해 보입니다. 제 말이 무엇인지 아시겠나요?
이것을 상상해보세요: 동시성 문제를 찾고 고치기 위해 "혁신적인 인공지능 알고리즘"을 사용하는 유틸리티를 시험 사용하고 있습니다. 실행시키자마자 스플래시 화면에서 "intelligence"의 철자를 틀렸다는 것을 알아챕니다. 조금 불길하지만, 그냥 오타일 뿐이겠죠? 그러고 나서 설정 화면이 라디오 버튼이 있어야 할 곳에 체크박스를 사용하고, 일부 키보드 단축키가 작동하지 않는다는 것을 알아챕니다. 이제, 이것들 중 어느 것도 큰 문제는 아니지만, 에러들이 누적되면서 프로그래머들에 대해 궁금해지기 시작합니다. 그들이 간단한 것들을 올바르게 할 수 없다면, 그들의 AI가 동시성 문제 같은 까다로운 것을 정말로 찾고 고칠 수 있을 확률이 얼마나 될까요?
그들은 AI를 미친 듯이 훌륭하게 만드는 데 너무 집중해서 그런 사소한 것들을 알아채지 못한 천재들일 수도 있습니다. 그리고 문제들을 지적하는 "까다로운 테스터들" 없이는, 당신이 결국 그것들을 찾게 됩니다. 그리고 이제 당신은 프로그래머들의 능력을 의심하고 있습니다.
그래서 이상하게 들릴 수도 있지만, 당신의 코드에서 모든 작은 버그를 드러내기로 결심한 것 같은 그 테스터들은 정말로 당신의 친구입니다.
Burk Hufnagel이 작성함.
빌드가 코드의 일부분을 다시 작성해서 각 타겟 환경을 위한 커스텀 바이너리를 생성하는 여러 프로젝트를 보았습니다. 이것은 항상 일을 필요 이상으로 복잡하게 만들고, 팀이 각 설치에서 일관된 버전을 갖지 못할 위험을 도입합니다. 최소한 그것은 소프트웨어의 여러 개의 거의 동일한 복사본을 빌드하는 것을 포함하며, 각각은 올바른 장소에 배포되어야 합니다. 그것은 필요 이상으로 많은 움직이는 부품을 의미하며, 이는 실수할 기회가 더 많다는 것을 의미합니다.
한때 모든 속성 변경이 전체 빌드 사이클을 위해 체크인되어야 하는 팀에서 일한 적이 있어서, 테스터들이 사소한 조정이 필요할 때마다 기다려야 했습니다(빌드 시간도 너무 오래 걸렸다고 언급했나요?). 또한 시스템 관리자들이 프로덕션을 위해 처음부터 다시 빌드하기를 고집하는(우리가 사용한 것과 같은 스크립트를 사용해서) 팀에서 일한 적도 있는데, 이는 프로덕션의 버전이 테스트를 거친 것이라는 증거가 없다는 것을 의미했습니다. 등등.
규칙은 간단합니다: 릴리스 파이프라인의 모든 단계를 통해 식별하고 승진시킬 수 있는 단일 바이너리를 빌드하세요. 환경별 세부사항은 환경에 보관하세요. 이것은 예를 들어, 컴포넌트 컨테이너에, 알려진 파일에, 또는 경로에 보관하는 것을 의미할 수 있습니다.
팀이 코드를 변경하는 빌드를 갖고 있거나 모든 타겟 설정을 코드와 함께 저장한다면, 그것은 아무도 애플리케이션의 핵심인 기능과 플랫폼별 기능을 분리할 만큼 충분히 신중하게 설계를 생각해보지 않았다는 것을 시사합니다. 또는 더 나쁠 수도 있습니다: 팀이 무엇을 해야 할지 알지만 변화를 만들기 위한 노력의 우선순위를 매길 수 없습니다.
물론, 예외가 있습니다: 현저히 다른 자원 제약을 갖는 타겟들을 위해 빌드하고 있을 수도 있지만, 그것은 "데이터베이스에서 화면으로 그리고 다시 돌아가는" 애플리케이션을 작성하는 우리 대부분에게는 적용되지 않습니다. 대안적으로, 지금 당장 고치기에는 너무 어려운 레거시 혼란과 함께 살고 있을 수도 있습니다. 그런 경우에는, 점진적으로 움직여야 합니다 — 하지만 가능한 한 빨리 시작하세요.
그리고 한 가지 더: 환경 정보도 버전 관리하세요. 환경 설정을 깨뜨리고 무엇이 변경되었는지 알아낼 수 없는 것보다 더 나쁜 것은 없습니다. 환경 정보는 코드와 별도로 버전 관리되어야 하는데, 그것들이 다른 속도로 그리고 다른 이유로 변경될 것이기 때문입니다. 일부 팀들은 이것을 위해 분산 버전 제어 시스템을(bazaar와 git 같은) 사용하는데, 그것들이 프로덕션 환경에서 만들어진 변경사항들을 — 필연적으로 일어나는 것처럼 — 리포지토리로 다시 푸시하는 것을 더 쉽게 만들기 때문입니다.
Steve Freeman이 작성함.
프로그램의 궁극적인 의미는 실행 중인 코드에 의해 주어집니다. 이것이 바이너리 형태로만 있다면, 읽기 어려울 것입니다! 하지만 소스 코드는 당신의 프로그램이든, 어떤 일반적인 상업적 소프트웨어 개발이든, 오픈 소스 프로젝트든, 또는 동적으로 해석되는 언어의 코드든 사용 가능해야 합니다. 소스 코드를 보면, 프로그램의 의미가 명백해야 합니다. 프로그램이 무엇을 하는지 알기 위해서는, 소스가 궁극적으로 확신을 갖고 볼 수 있는 유일한 것입니다. 가장 정확한 요구사항 문서라도 전체 진실을 말하지 않습니다: 그것은 프로그램이 실제로 무엇을 하고 있는지에 대한 상세한 이야기를 담고 있지 않고, 요구사항 분석가의 고수준 의도만을 담고 있습니다. 설계 문서는 계획된 설계를 포착할 수도 있지만, 구현의 필요한 세부사항이 부족할 것입니다. 이러한 문서들은 현재 구현과 동기화되지 않을 수도 있고... 또는 단순히 분실되었을 수도 있습니다. 또는 애초에 작성되지 않았을 수도 있습니다. 소스 코드가 남은 유일한 것일 수도 있습니다.
이것을 염두에 두고, 당신의 코드가 당신이나 다른 프로그래머에게 그것이 무엇을 하고 있는지 얼마나 명확하게 말하고 있는지 스스로에게 물어보세요.
"오, 제 주석이 당신이 알아야 할 모든 것을 말해줄 것입니다"라고 말할 수도 있습니다. 하지만 주석은 실행되는 코드가 아니라는 것을 명심하세요. 그것들은 다른 형태의 문서와 마찬가지로 틀릴 수 있습니다. 주석이 무조건적으로 좋은 것이라고 하는 전통이 있어서, 의심 없이 일부 프로그래머들은 점점 더 많은 주석을 작성하며, 심지어 코드에서 이미 명백한 사소한 것들을 다시 설명하고 재설명합니다. 이것은 코드를 명확하게 하는 잘못된 방법입니다. 코드에 주석이 필요하다면, 필요하지 않도록 리팩터링하는 것을 고려해보세요. 긴 주석은 화면 공간을 어지럽힐 수 있고 IDE에 의해 자동으로 숨겨질 수도 있습니다. 변경을 설명해야 한다면, 코드가 아니라 버전 제어 시스템 체크인 메시지에서 하세요.
실제로 코드가 가능한 한 명확하게 진실을 말하도록 하기 위해 무엇을 할 수 있을까요? 좋은 이름을 위해 노력하세요. 응집력 있는 기능에 대해 코드를 구조화하세요. 이것은 또한 이름 짓기를 쉽게 만듭니다. 직교성을 달성하기 위해 코드를 분리하세요. 의도된 행동을 설명하고 인터페이스를 확인하는 자동화된 테스트를 작성하세요. 더 간단하고 더 나은 솔루션을 코딩하는 법을 배울 때 무자비하게 리팩터링하세요. 코드를 읽고 이해하기 가능한 한 간단하게 만드세요.
시, 에세이, 공개 블로그, 또는 중요한 이메일 같은 다른 작문처럼 코드를 다루세요. 표현하는 것을 신중하게 만드세요. 그래서 그것이 해야 할 일을 하고 무엇을 하고 있는지 가능한 한 직접적으로 소통하며, 당신이 더 이상 주변에 없을 때도 당신의 의도를 소통할 수 있도록 하세요. 유용한 코드는 의도했던 것보다 훨씬 오래 사용된다는 것을 기억하세요. 유지보수 프로그래머들이 당신에게 감사할 것입니다. 그리고, 당신이 유지보수 프로그래머이고 작업하고 있는 코드가 쉽게 진실을 말하지 않는다면, 위의 가이드라인을 능동적인 방식으로 적용하세요. 코드에서 어느 정도 정상성을 확립하고 당신 자신의 정상성을 유지하세요.
Peter Sommerlad가 작성함.
코딩 관행에 대해서는 매우 규율이 잡힌 팀들이 빌드 스크립트는 소홀히 하는 것이 드물지 않은데, 그것들이 단지 중요하지 않은 세부사항이라는 믿음이나 그것들이 복잡하고 릴리스 엔지니어링의 교단에서 돌봐야 한다는 두려움 때문입니다. 중복과 에러가 있는 유지보수할 수 없는 빌드 스크립트는 잘못 팩터링된 코드에서와 같은 규모의 문제를 야기합니다.
규율이 잡히고 숙련된 개발자들이 빌드를 자신들의 작업에 부차적인 것으로 취급하는 한 가지 이유는 빌드 스크립트가 종종 소스 코드와 다른 언어로 작성되기 때문입니다. 다른 이유는 빌드가 실제로 "코드"가 아니라는 것입니다. 이러한 정당화는 대부분의 소프트웨어 개발자들이 새로운 언어를 배우는 것을 즐기고, 빌드가 개발자와 최종 사용자가 테스트하고 실행할 실행 가능한 아티팩트를 만드는 것이라는 현실과 맞지 않습니다. 코드는 빌드되지 않으면 쓸모없고, 빌드는 애플리케이션의 컴포넌트 아키텍처를 정의하는 것입니다. 빌드는 개발 과정의 필수적인 부분이고, 빌드 과정에 대한 결정은 코드와 코딩을 더 간단하게 만들 수 있습니다.
잘못된 관용구를 사용해서 작성된 빌드 스크립트는 유지보수하기 어렵고, 더 중요하게는, 개선하기 어렵습니다. 변경을 만드는 올바른 방법을 이해하는 데 시간을 쓸 가치가 있습니다. 애플리케이션이 잘못된 버전의 의존성으로 빌드되거나 빌드 시간 설정이 잘못되었을 때 버그가 나타날 수 있습니다.
전통적으로 테스팅은 항상 "품질 보증" 팀에 맡겨진 것이었습니다. 우리는 이제 코딩하면서 테스트하는 것이 예측 가능하게 가치를 전달할 수 있기 위해 필요하다는 것을 깨달았습니다. 비슷한 방식으로, 빌드 과정은 개발 팀이 소유해야 합니다.
빌드를 이해하는 것은 전체 개발 생명주기를 단순화하고 비용을 줄일 수 있습니다. 실행하기 쉬운 빌드는 새로운 개발자가 빠르고 쉽게 시작할 수 있게 해줍니다. 빌드에서 설정을 자동화하는 것은 여러 사람이 프로젝트에서 작업할 때 일관된 결과를 얻을 수 있게 해주며, "제게는 작동합니다" 대화를 피할 수 있습니다. 많은 빌드 도구들이 코드 품질에 대한 보고서를 실행할 수 있게 해서, 잠재적인 문제를 일찍 감지할 수 있게 해줍니다. 빌드를 당신의 것으로 만드는 방법을 이해하는 데 시간을 쓰는 것으로, 당신 자신과 팀의 다른 모든 사람을 도울 수 있습니다. 기능 코딩에 집중할 수 있고, 이해관계자들에게 이익을 주고 작업을 더 즐겁게 만들 수 있습니다.
언제 그리고 어떻게 변경을 만들지 알 수 있을 만큼 빌드 과정을 충분히 배우세요. 빌드 스크립트는 코드입니다. 그것들은 다른 누군가에게 맡기기에는 너무 중요한데, 다른 이유가 없다면 애플리케이션이 빌드될 때까지는 완성되지 않기 때문입니다. 프로그래밍의 작업은 작동하는 소프트웨어를 전달할 때까지 완료되지 않습니다.
Steve Berczuk이 작성함.
당신이 하는 일에 완전히 몰입해 있다고 상상해 보세요 — 집중하고(focused), 헌신적이며(dedicated), 참여(involved)하고 있습니다. 시간을 잊어버린 것 같고, 아마도 행복한 기분이 드는 순간입니다. 당신은 몰입(flow)을 경험하고 있습니다. 전체 개발 팀이 몰입을 이루기에 필요한 것은 달성하기도 유지하기도 매우 어렵습니다. 왜냐하면 그것을 쉽게 깨트릴 수 있는 많은 방해 요소와 상호작용, 기타 주의 산만이 있기 때문입니다.
이미 페어 프로그래밍을 실천해본 경험이 있다면, 페어링이 어떻게 몰입에 기여하는지 익숙할 것입니다. 그렇지 않다면, 우리는 여러분의 경험을 바탕으로 지금 당장 시작하도록 동기 부여하고자 합니다! 페어 프로그래밍이 성공하기 위해서는 개별 팀원뿐만 아니라 전체 팀이 어느 정도 노력을 기울여야 합니다.
팀원으로서, 자신보다 경험이 적은 개발자들에게 인내심을 가지세요. 더 숙련된 개발자에게 위축될 수 있다는 두려움과 맞서보세요. 사람들은 다르며, 그 차이를 소중히 여기는 것을 인식하세요. 당신 자신의 강점과 약점, 그리고 다른 팀원들의 강점과 약점을 알아두세요. 동료들에게서 배울 수 있는 것이 얼마나 많은지 놀랄 수도 있습니다.
팀으로서, 기술과 지식을 프로젝트 전반에 걸쳐 분배하기 위해 페어 프로그래밍을 도입하세요. 과제를 두 사람이 함께 해결하고, 짝과 작업을 자주 교체해야합니다. 순환의 법칙(rule of rotation)에 동의하세요. 필요할 경우 이 규칙을 제쳐두거나 조정하세요. 우리의 경험은 다른 짝에게 넘기기 전에 반드시 작업을 완료할 필요는 없다는 것입니다. 작업을 중단한 채 다른 짝에게 전달하는 것이 직관적이지 않을 수 있지만, 실제로는 효과적이라는 것을 알게 되었습니다.
몰입을 깨트릴 수 있는 다양한 상황이 있지만, 페어 프로그래밍은 이를 유지하는 데 도움이 됩니다:
-
"트럭 요인" 줄이기: 이는 조금 음울한 사고 실험이지만, 팀원 중 몇 명이 트럭에 치여야 팀이 최종 결과물을 완료할 수 없게 될까요? 즉, 특정 팀원에게 의존하는 정도는 얼마인가요? 지식이 특권인가요, 아니면 공유되나요? 짝들 사이에 작업을 순환하고 있다면, 항상 그 지식을 가진 누군가가 있으며 일을 완료할 수 있습니다. 팀의 몰입은 "트럭 요인"에 덜 영향을 받습니다.
-
문제를 효과적으로 해결하기: 페어 프로그래밍을 하면서 도전적인 문제에 부딪히면 항상 함께 논의할 사람이 있습니다. 그런 대화는 혼자서 막힐 때보다 더 많은 가능성을 열어줄 가능성이 큽니다. 작업이 순환됨에 따라 당신의 해결책은 다음 짝에 의해 재검토되고 다시 고려되므로, 처음에 최적의 해결책을 선택하지 않더라도 그다지 중요하지 않습니다.
-
원활하게 통합하기: 현재의 작업이 다른 코드 조각을 호출하는 것이라면, 메서드 이름, 문서 및 테스트가 충분히 설명적이어야 합니다. 그렇지 않다면, 그 코드를 작성하는 데 참여했던 개발자와 페어링하면 더 나은 개요를 얻고 자신의 코드에 더 빨리 통합할 수 있습니다. 또한, 이 논의를 통해 명명, 문서화 및 테스트 개선의 기회로 삼을 수 있습니다.
-
중단 완화하기: 누군가가 질문을 하러 오거나 전화가 걸려오거나 긴급 이메일에 답변해야 하거나 회의에 참석해야 할 경우, 당신의 페어 프로그래밍 짝은 계속 코딩할 수 있습니다. 당신이 돌아오면 짝은 여전히 몰입 속에 있고, 당신은 빠르게 따라잡고 다시 합류할 수 있습니다.
-
새로운 팀원 신속하게 적응시키기: 페어 프로그래밍과 적절한 페어 및 과제의 순환을 통해 신입 팀원들은 코드와 다른 팀원들을 빠르게 익힐 수 있습니다.
몰입은 당신을 믿을 수 없을 만큼 생산적으로 만듭니다. 그러나 몰입은 또한 매우 취약합니다. 그것을 얻을 수 있도록 노력하고, 얻은 후에는 그것을 붙잡아 두세요!
Gudny Hauknes, Ann Katrin Gagnat, 그리고 Kari Røssland 씀.
1999년 9월 23일, 3억 2천 7백 6십만 달러의 화성 기후 탐사선이 지구의 소프트웨어 에러로 인해 화성 궤도 진입 중에 분실되었습니다. 이 에러는 나중에 미터법 혼동이라고 불렸습니다. 지상국 소프트웨어는 파운드 단위로 작업하고 있었는데 우주선은 뉴턴을 예상했고, 이로 인해 지상국이 우주선 추진기의 힘을 4.45배 과소평가하게 되었습니다.
이것은 더 강하고 더 도메인별 타이핑이 적용되었다면 방지될 수 있었을 소프트웨어 실패의 많은 예 중 하나입니다. 이것은 또한 Ada 언어의 많은 기능들 뒤에 있는 근거의 예이기도 한데, Ada의 주요 설계 목표 중 하나는 임베디드 안전 중요 소프트웨어를 구현하는 것이었습니다. Ada는 원시 타입과 사용자 정의 타입 모두에 대해 정적 검사가 있는 강한 타이핑을 갖고 있습니다:
type Velocity_In_Knots is new Float range 0.0 .. 500.00;
type Distance_In_Nautical_Miles is new Float range 0.0 .. 3000.00;
Velocity: Velocity_In_Knots;
Distance: Distance_In_Nautical_Miles;
Some_Number: Float;
Some_Number:= Distance + Velocity; -- 컴파일러에 의해 타입 에러로 잡힐 것입니다.
덜 까다로운 도메인의 개발자들도 더 도메인별 타이핑을 적용하는 것으로부터 이익을 얻을 수 있는데, 그렇지 않으면 언어와 라이브러리가 제공하는 원시 데이터 타입인 문자열과 부동소수점을 계속 사용할 것입니다. Java, C++, Python, 그리고 다른 현대 언어들에서 추상 데이터 타입은 클래스로 알려져 있습니다. Velocity_In_Knots와 Distance_In_Nautical_Miles 같은 클래스를 사용하는 것은 코드 품질에 대해 많은 가치를 더합니다:
- 코드가 Float나 String이 아닌 도메인의 개념을 표현하므로 더 읽기 쉬워집니다.
- 코드가 쉽게 테스트할 수 있는 행동을 캡슐화하므로 더 테스트하기 쉬워집니다.
- 코드가 애플리케이션과 시스템 간 재사용을 촉진합니다.
이 접근법은 정적 타입 언어와 동적 타입 언어 사용자 모두에게 똑같이 유효합니다. 유일한 차이점은 정적 타입 언어를 사용하는 개발자들은 컴파일러로부터 도움을 받는 반면 동적 타입 언어를 받아들이는 개발자들은 단위 테스트에 더 의존할 가능성이 높다는 것입니다. 검사 스타일은 다를 수 있지만, 동기와 표현 스타일은 그렇지 않습니다.
교훈은 품질 소프트웨어 개발 목적으로 도메인별 타입을 탐구하기 시작하는 것입니다.
Einar Landre가 작성함.
에러 메시지는 사용자와 시스템의 나머지 부분 간의 가장 중요한 상호작용입니다. 그것들은 사용자와 시스템 간의 소통이 파괴 지점 근처에 있을 때 일어납니다.
에러가 사용자의 잘못된 입력에 의해 야기된다고 생각하기 쉽습니다. 하지만 사람들은 예측 가능하고 체계적인 방식으로 실수를 합니다. 그래서 다른 시스템 컴포넌트들 간에 하는 것처럼 사용자와 시스템의 나머지 부분 간의 소통을 '디버깅'하는 것이 가능합니다.
예를 들어, 사용자가 허용된 범위 내의 날짜를 입력하기를 원한다고 합시다. 사용자가 임의의 날짜를 입력하게 하는 것보다는, 허용된 날짜들만 보여주는 목록이나 달력 같은 장치를 제공하는 것이 더 좋습니다. 이것은 사용자가 범위를 벗어난 날짜를 입력할 어떤 가능성도 제거합니다.
형식 에러는 또 다른 일반적인 문제입니다. 예를 들어, 사용자에게 날짜 텍스트 필드가 제시되고 "2012년 7월 29일" 같은 명확한 날짜를 입력한다면, 단순히 선호하는 형식("DD/MM/YYYY" 같은)이 아니라는 이유로 그것을 거부하는 것은 비합리적입니다. "29 / 07 / 2012"를 추가 공백이 포함되어 있다는 이유로 거부하는 것은 더욱 나쁜데 — 이런 종류의 문제는 날짜가 원하는 형식에 있는 것처럼 보이므로 사용자들이 이해하기 특히 어렵습니다.
이 에러는 가장 일반적인 서너 개의 날짜 형식을 파싱하는 것보다 날짜를 거부하는 것이 더 쉽기 때문에 발생합니다. 이런 종류의 사소한 에러들은 사용자 좌절로 이어지고, 이것은 사용자가 집중력을 잃으면서 추가적인 에러들로 이어집니다. 대신, 데이터가 아닌 정보를 입력하려는 사용자의 선호를 존중하세요.
형식 에러를 피하는 또 다른 방법은 단서를 제공하는 것입니다 — 예를 들어, 원하는 형식을 보여주는 필드 안의 레이블("DD/MM/YYYY")로 말입니다. 또 다른 단서는 필드를 2, 2, 4 문자의 세 개 텍스트 박스로 나누는 것일 수 있습니다.
단서는 지시사항과 다릅니다: 단서는 힌트인 경향이 있고; 지시사항은 장황합니다. 단서는 상호작용 지점에서 발생하고; 지시사항은 상호작용 지점 이전에 나타납니다. 단서는 맥락을 제공하고; 지시사항은 사용을 명령합니다.
일반적으로, 지시사항은 에러 방지에 효과적이지 않습니다. 사용자들은 인터페이스가 자신들의 과거 경험에 맞춰 작동할 것이라고 가정하는 경향이 있습니다("분명히 모든 사람이 '2012년 7월 29일'이 무엇을 의미하는지 알고 있을 것이다"). 그래서 지시사항을 읽지 않습니다. 단서는 사용자들을 에러로부터 밀어냅니다.
에러를 피하는 또 다른 방법은 기본값을 제공하는 것입니다. 예를 들어, 사용자들은 보통 오늘, 내일, 내 생일, 내 마감일, 또는 지난번에 이 양식을 사용할 때 입력한 날짜에 해당하는 값을 입력합니다. 맥락에 따라, 이 중 하나가 스마트 기본값으로 좋은 선택일 가능성이 높습니다.
원인이 무엇이든, 시스템은 에러에 관대해야 합니다. 모든 행동에 — 그리고 특히 사용자의 데이터를 파괴하거나 수정할 가능성이 있는 행동에 — 여러 수준의 실행 취소를 제공함으로써 이것을 할 수 있습니다.
실행 취소 행동을 로깅하고 분석하는 것은 또한 인터페이스가 사용자들을 무의식적인 에러로 이끌고 있는 곳을 강조할 수 있는데, 지속적으로 '잘못된' 버튼을 클릭하는 것 같은 것들입니다. 이러한 에러들은 종종 추가적인 에러를 방지하기 위해 재설계할 수 있는 오해를 불러일으키는 단서나 상호작용 순서에 의해 야기됩니다.
어떤 접근법을 택하든, 대부분의 에러는 체계적입니다 — 사용자와 소프트웨어 간의 오해의 결과입니다. 사용자들이 어떻게 생각하고, 정보를 해석하고, 결정을 내리고, 데이터를 입력하는지 이해하는 것은 소프트웨어와 사용자들 간의 상호작용을 디버깅하는 데 도움이 될 것입니다.
Giles Colborne이 작성함.
전문 프로그래머란 무엇일까요?
전문 프로그래머의 가장 중요한 특성은 개인적 책임감입니다. 전문 프로그래머는 자신의 경력, 추정, 일정 약속, 실수, 그리고 기술에 대한 책임을 집니다. 전문 프로그래머는 그 책임을 다른 사람들에게 넘기지 않습니다.
-
당신이 전문가라면, 당신이 자신의 경력에 대한 책임이 있습니다. 당신이 읽고 배우는 것에 대한 책임이 있습니다. 업계와 기술에 대해 최신 상태를 유지하는 것은 당신의 책임입니다. 너무 많은 프로그래머들이 자신들을 훈련시키는 것이 고용주의 일이라고 느낍니다. 죄송하지만, 이것은 완전히 틀렸습니다. 의사들이 그렇게 행동한다고 생각하나요? 변호사들이 그렇게 행동한다고 생각하나요? 아니요, 그들은 자신의 시간과 자신의 돈으로 스스로를 훈련시킵니다. 그들은 업무 시간 외의 많은 시간을 저널과 판결을 읽는 데 씁니다. 그들은 스스로를 최신 상태로 유지합니다. 그리고 우리도 그래야 합니다. 당신과 고용주 간의 관계는 고용 계약서에 잘 명시되어 있습니다. 간단히 말하면: 그들은 당신에게 돈을 지불할 것을 약속하고, 당신은 좋은 일을 할 것을 약속합니다.
-
전문가들은 자신이 작성하는 코드에 대한 책임을 집니다. 그들은 코드가 작동한다는 것을 알지 못하면 코드를 릴리스하지 않습니다. 이것을 잠깐 생각해보세요. 확신하지 못하는 코드를 기꺼이 릴리스한다면 어떻게 자신을 전문가라고 여길 수 있을까요? 전문 프로그래머들은 QA가 아무것도 찾지 못하기를 기대하는데 그들이 철저히 테스트하기 전까지는 코드를 릴리스하지 않기 때문입니다. 물론 QA가 일부 문제를 찾을 것인데, 아무도 완벽하지 않기 때문입니다. 하지만 전문가로서 우리의 자세는 QA가 찾을 것을 아무것도 남기지 않을 것이어야 합니다.
-
전문가들은 팀 플레이어입니다. 그들은 자신의 작업뿐만 아니라 전체 팀의 산출물에 대한 책임을 집니다. 그들은 서로 도우고, 서로 가르치고, 서로로부터 배우고, 필요할 때는 서로를 보완하기까지 합니다. 한 팀원이 쓰러질 때, 다른 사람들이 개입하는데, 언젠가 자신들이 보완이 필요한 사람이 될 것이라는 것을 알기 때문입니다.
-
전문가들은 큰 버그 목록을 용납하지 않습니다. 거대한 버그 목록은 조잡합니다. 이슈 추적 데이터베이스에 수천 개의 이슈가 있는 시스템들은 부주의함의 비극입니다. 실제로, 대부분의 프로젝트에서 이슈 추적 시스템의 필요성 자체가 부주의함의 증상입니다. 가장 큰 시스템들만이 자동화가 관리에 필요할 만큼 긴 버그 목록을 가져야 합니다.
-
전문가들은 혼란을 만들지 않습니다. 그들은 자신의 기술에 자부심을 갖습니다. 그들은 코드를 깨끗하고, 잘 구조화되고, 읽기 쉽게 유지합니다. 그들은 합의된 표준과 모범 사례를 따릅니다. 그들은 절대로, 절대로 서두르지 않습니다. 당신이 체외 경험을 하면서 의사가 당신에게 심장 수술을 하는 것을 보고 있다고 상상해보세요. 이 의사에게는 마감시한(문자 그대로의 의미로)이 있습니다. 심폐 우회 기계가 당신의 혈구를 너무 많이 손상시키기 전에 끝내야 합니다. 그가 어떻게 행동하기를 원하나요? 일반적인 소프트웨어 개발자처럼 서두르고 혼란을 만들기를 원하나요? "나중에 돌아가서 이것을 고치겠습니다"라고 말하기를 원하나요? 아니면 자신의 규율을 신중하게 지키고, 시간을 갖고, 자신의 접근법이 합리적으로 택할 수 있는 최선의 접근법이라고 확신하기를 원하나요? 혼란을 원하나요, 아니면 전문성을 원하나요?
전문가들은 책임감이 있습니다. 그들은 자신의 경력에 대한 책임을 집니다. 그들은 자신의 코드가 제대로 작동하도록 하는 것에 대한 책임을 집니다. 그들은 자신의 기술의 품질에 대한 책임을 집니다. 그들은 마감시한이 다가올 때 자신의 원칙을 포기하지 않습니다. 실제로, 압박이 가중될 때, 전문가들은 자신들이 옳다고 아는 규율을 더욱 단단히 붙잡습니다.
Uncle Bob이 작성함.
모든 프로젝트의 모든 것을 버전 제어 하에 두세요. 필요한 자원들이 있습니다: Subversion, Git, Mercurial, CVS 같은 무료 도구들; 풍부한 디스크 공간; 저렴하고 강력한 서버들; 편재하는 네트워킹; 그리고 심지어 프로젝트 호스팅 서비스들까지. 버전 제어 소프트웨어를 설치한 후에 작업을 리포지토리에 넣기 위해 필요한 것은 코드가 들어있는 깨끗한 디렉토리에서 적절한 명령을 실행하는 것뿐입니다. 그리고 배워야 할 기본 작업은 단 두 개뿐입니다: 코드 변경사항을 리포지토리에 커밋하고 프로젝트의 작업 버전을 리포지토리 버전으로 업데이트하는 것입니다.
프로젝트가 버전 제어 하에 있으면 분명히 그 이력을 추적하고, 누가 어떤 코드를 작성했는지 보고, 고유 식별자를 통해 파일이나 프로젝트 버전을 참조할 수 있습니다. 더 중요한 것은 두려움 없이 대담한 코드 변경을 할 수 있다는 것입니다 — 미래에 필요할 경우를 대비해 주석 처리된 코드는 더 이상 필요 없는데, 이전 버전이 리포지토리에 안전하게 살고 있기 때문입니다. 소프트웨어 릴리스를 상징적 이름으로 태그할 수 있고(그리고 해야 하고) 그래서 미래에 고객이 실행하는 소프트웨어의 정확한 버전을 쉽게 다시 방문할 수 있습니다. 병렬 개발의 브랜치를 만들 수 있습니다: 대부분의 프로젝트는 활발한 개발 브랜치와 적극적으로 지원되는 릴리스된 버전들을 위한 하나 이상의 유지보수 브랜치를 갖습니다.
버전 제어 시스템은 개발자들 간의 마찰을 최소화합니다. 프로그래머들이 독립적인 소프트웨어 부분들에서 작업할 때 이것들이 거의 마법처럼 통합됩니다. 그들이 서로의 발가락을 밟을 때 시스템이 알아차리고 충돌을 정리할 수 있게 해줍니다. 약간의 추가 설정으로 시스템은 각 커밋된 변경에 대해 모든 개발자에게 알릴 수 있어서, 프로젝트 진행에 대한 공통 이해를 확립합니다.
프로젝트를 설정할 때, 인색하지 마세요: 프로젝트의 모든 자산을 버전 제어 하에 두세요. 소스 코드 외에도, 문서, 도구, 빌드 스크립트, 테스트 케이스, 아트워크, 심지어 라이브러리도 포함하세요. 완전한 프로젝트가 (정기적으로 백업되는) 리포지토리에 안전하게 보관되면 디스크나 데이터를 잃는 손상이 최소화됩니다. 새로운 머신에서 개발을 위해 설정하는 것은 단순히 리포지토리에서 프로젝트를 체크아웃하는 것을 포함합니다. 이것은 다른 플랫폼에서 코드를 배포하고, 빌드하고, 테스트하는 것을 단순화합니다: 각 머신에서 단일 업데이트 명령이 소프트웨어가 현재 버전임을 보장할 것입니다.
버전 제어 시스템과 작업하는 아름다움을 보았다면, 몇 가지 규칙을 따르는 것이 당신과 팀을 더욱 효과적으로 만들 것입니다:
- 각 논리적 변경을 별도의 작업으로 커밋하세요. 많은 변경사항을 하나의 커밋에 함께 묶는 것은 미래에 그것들을 분리하기 어렵게 만들 것입니다. 이것은 프로젝트 전체 리팩터링이나 스타일 변경을 할 때 특히 중요한데, 이것들이 다른 수정사항들을 쉽게 가릴 수 있기 때문입니다.
- 각 커밋에 설명 메시지를 첨부하세요. 최소한 무엇을 변경했는지 간결하게 기술하지만, 변경의 근거도 기록하고 싶다면 이것이 저장할 최선의 장소입니다.
- 마지막으로, 프로젝트의 빌드를 깨뜨릴 코드를 커밋하는 것을 피하세요. 그렇지 않으면 프로젝트의 다른 개발자들에게 인기가 없어질 것입니다.
버전 제어 시스템 하에서의 삶은 쉽게 피할 수 있는 실수로 망치기에는 너무 좋습니다.
Diomidis Spinellis가 작성함.
몇 시간 동안 어떤 까다로운 문제에 집중했는데 해결책이 보이지 않습니다. 그래서 다리를 뻗으러 일어나거나, 자판기에 가려고 하는데, 돌아오는 길에 갑자기 답이 명백해집니다.
이 시나리오가 익숙하게 들리나요? 왜 그런 일이 일어나는지 궁금해한 적이 있나요? 트릭은 코딩하고 있을 때, 뇌의 논리적 부분이 활성화되고 창의적인 부분이 차단된다는 것입니다. 논리적 부분이 휴식을 취할 때까지는 아무것도 당신에게 제시할 수 없습니다.
실제 사례가 하나 있습니다: 저는 레거시 코드를 정리하고 있었는데 '흥미로운' 메서드를 발견했습니다. 그것은 문자열이 hh:mm:ss xx 형식을 사용해서 유효한 시간을 포함하는지 검증하도록 설계되어 있었는데, 여기서 hh는 시간, mm은 분, ss는 초를 나타내고, xx는 AM 또는 PM입니다.
메서드는 다음 코드를 사용해서 (시간을 나타내는) 두 문자를 숫자로 변환하고, 그것이 적절한 범위 내에 있는지 검증했습니다:
try {
Integer.parseInt(time.substring(0, 2));
} catch (Exception x) {
return false;
}
if (Integer.parseInt(time.substring(0, 2)) > 12) {
return false;
}
같은 코드가 분과 초를 테스트하기 위해 문자 오프셋과 상한을 적절히 변경해서 두 번 더 나타났습니다. 메서드는 AM과 PM을 확인하는 다음 줄들로 끝났습니다:
if (!time.substring(9, 11).equals("AM") &
!time.substring(9, 11).equals("PM")) {
return false;
}
이 일련의 비교들 중 어느 것도 실패하지 않아서 false를 반환하지 않으면, 메서드는 true를 반환했습니다.
앞의 코드가 장황하고 따라가기 어려워 보인다면, 걱정하지 마세요. 저도 그렇게 생각했습니다 — 이는 정리할 가치가 있는 것을 발견했다는 의미였습니다. 저는 그것을 리팩터링하고 여전히 작동하는지 확인하기 위해 몇 개의 단위 테스트를 작성했습니다.
끝냈을 때, 저는 결과에 만족했습니다. 새 버전은 읽기 쉬웠고, 크기는 절반이었으며, 원래 코드가 시간, 분, 초의 상한만 테스트했기 때문에 더 정확했습니다.
다음날 출근 준비를 하면서, 아이디어가 머리에 떠올랐습니다: 정규 표현식을 사용해서 문자열을 검증하는 건 어떨까? 몇 분 타이핑한 후에, 단 한 줄의 코드로 작동하는 구현을 갖게 되었습니다. 여기 있습니다:
public static boolean validateTime(String time) {
return time.matches("(0[1-9]|1[0-2]):[0-5][0-9]:[0-5][0-9] ([AP]M)");
}
이 이야기의 요점은 제가 결국 30줄 이상의 코드를 단 한 줄로 바꿨다는 것이 아닙니다. 요점은 컴퓨터에서 떨어질 때까지는, 제 첫 번째 시도가 문제에 대한 최선의 해결책이라고 생각했다는 것입니다.
그래서 다음에 까다로운 문제에 부딪히면, 스스로에게 호의를 베푸세요. 문제를 정말로 이해했다면 뇌의 창의적인 부분을 포함하는 일을 하러 가세요 — 문제를 스케치하거나, 음악을 듣거나, 아니면 그냥 밖에서 산책하세요. 때로는 문제를 해결하기 위해 할 수 있는 최선의 일은 마우스를 내려놓고 키보드에서 떨어지는 것입니다.
BurkHufnagel이 작성함.
우리 프로그래머들은 이상한 생물입니다. 우리는 코드를 작성하는 것을 좋아합니다. 하지만 그것을 읽는 것에 관해서는 보통 피하려고 합니다. 결국, 코드를 작성하는 것이 훨씬 더 재미있고, 코드를 읽는 것은 어렵습니다 — 때로는 거의 불가능합니다. 다른 사람의 코드를 읽는 것은 특히 어렵습니다. 반드시 다른 사람의 코드가 나쁘기 때문이 아니라, 그들이 아마도 당신과 다른 방식으로 생각하고 문제를 해결하기 때문입니다. 하지만 다른 누군가의 코드를 읽는 것이 당신 자신의 코드를 개선할 수 있다고 생각해본 적이 있나요?
다음에 코드를 읽을 때, 잠깐 멈춰서 생각해보세요. 코드가 읽기 쉬운가요 아니면 어려운가요? 읽기 어렵다면, 왜 그럴까요? 포맷팅이 나쁜가요? 이름 짓기가 일관성이 없거나 비논리적인가요? 여러 관심사가 같은 코드 조각에 섞여 있나요? 아마도 언어의 선택이 코드를 읽기 어렵게 만드는 건 아닐까요? 다른 사람의 실수로부터 배우려고 노력하세요. 그래서 당신의 코드에는 같은 실수가 포함되지 않도록 하세요. 몇 가지 놀라움을 받을 수도 있습니다. 예를 들어, 의존성 차단 기법은 낮은 결합도에는 좋을 수 있지만, 때로는 코드를 읽기 더 어렵게 만들 수도 있습니다. 그리고 일부 사람들이 우아한 코드라고 부르는 것을, 다른 사람들은 읽을 수 없다고 부릅니다.
코드가 읽기 쉽다면, 거기서 배울 수 있는 유용한 것이 있는지 보기 위해 멈춰보세요. 아마도 당신이 모르거나 이전에 구현하려고 애썼던 디자인 패턴이 사용되고 있을 수도 있습니다. 아마도 메서드들이 더 짧고 그들의 이름이 당신의 것보다 더 표현적일 수도 있습니다. 일부 오픈 소스 프로젝트들은 훌륭하고 읽기 쉬운 코드를 작성하는 방법의 좋은 예들로 가득한 반면 — 다른 것들은 정확히 반대의 예로 작용합니다! 그들의 코드 중 일부를 확인해보고 살펴보세요.
현재 작업하지 않는 프로젝트에서 당신 자신의 오래된 코드를 읽는 것도 깨달음을 주는 경험이 될 수 있습니다. 가장 오래된 코드부터 시작해서 현재까지 앞으로 나아가보세요. 아마도 작성할 때만큼 읽기 쉽지 않다는 것을 발견할 것입니다. 당신의 초기 코드는 또한 어떤 당황스러운 오락적 가치를 가질 수도 있는데, 어젯밤 술집에서 술 마시면서 말한 모든 것들을 상기시키는 것과 같은 방식으로 말입니다. 수년에 걸쳐 당신이 어떻게 기술을 발전시켰는지 보세요 — 그것은 정말로 동기를 부여할 수 있습니다. 코드의 어떤 영역들이 읽기 어려운지 관찰하고, 오늘날에도 여전히 같은 방식으로 코드를 작성하고 있는지 고려해보세요.
그래서 다음에 프로그래밍 기술을 향상시키고 싶은 충동을 느낄 때, 다른 책을 읽지 마세요. 코드를 읽으세요.
Karianne Berg가 작성함.
가장 작은 개발 프로젝트를 제외하고는 모든 프로젝트에서 사람들이 사람들과 함께 일합니다. 가장 추상적인 연구 분야를 제외하고는 모든 곳에서 사람들이 다른 사람들을 위해 그들의 목표를 지원하는 소프트웨어를 작성합니다. 사람들이 사람들과 함께 사람들을 위해 소프트웨어를 작성하는 것입니다. 이것은 사람의 사업입니다. 불행히도 프로그래머들에게 가르쳐지는 것들은 너무 자주 그들이 함께 일하고 일하는 대상인 사람들을 다루는 데 매우 부족한 준비를 시킵니다. 다행히 도움이 될 수 있는 전체 연구 분야가 있습니다.
예를 들어, 루트비히 비트겐슈타인은 철학적 탐구(그리고 다른 곳에서)에서 우리가 서로 대화하는 데 사용하는 어떤 언어도 한 사람의 머릿속에서 생각이나 아이디어나 그림을 꺼내서 다른 사람의 머릿속에 넣기 위한 직렬화 형식이 아니며, 그럴 수도 없다는 것을 매우 잘 논증합니다. 우리가 "요구사항을 수집할" 때 이미 오해에 대해 경계해야 합니다. 비트겐슈타인은 또한 우리가 서로를 이해할 수 있는 능력이 공유된 정의에서 생기는 것이 아니라 공유된 경험, 삶의 형태에서 생긴다는 것을 보여줍니다. 이것이 문제 영역에 깊이 빠져 있는 프로그래머들이 그것과 떨어져 있는 프로그래머들보다 더 잘하는 경향이 있는 한 가지 이유일 수 있습니다.
라코프와 존슨은 우리가 사는 은유들의 목록을 제시하며, 언어가 대부분 은유적이고 이러한 은유들이 우리가 세상을 이해하는 방식에 대한 통찰을 제공한다고 제안합니다. 금융 시스템에 대해 이야기할 때 접할 수 있는 현금 흐름(cash flow)같은 겉보기에 구체적인 용어들조차 은유적으로 볼 수 있습니다: "돈은 유체다." 그 은유가 돈을 다루는 시스템에 대해 우리가 생각하는 방식에 어떤 영향을 미칠까요? 또는 우리는 프로토콜 스택의 계층들에 대해 이야기할 수 있는데, 일부는 높은 수준이고 일부는 낮은 수준입니다. 이것은 강력하게 은유적입니다: 사용자는 "위"에 있고 기술은 "아래"에 있습니다. 이것은 우리가 구축하는 시스템의 구조에 대한 우리의 사고를 드러냅니다. 또한 때때로 벗어나는 것이 도움이 될 수 있는 게으른 사고 습관을 나타낼 수도 있습니다.
마르틴 하이데거는 사람들이 도구를 경험하는 방식을 면밀히 연구했습니다. 프로그래머들은 도구를 만들고 사용하며, 우리는 도구에 대해 생각하고 만들고 수정하고 재창조합니다. 도구는 우리에게 관심의 대상입니다. 하지만 사용자에게는 하이데거가 존재와 시간에서 보여주듯이, 도구는 사용에서만 이해되는 보이지 않는 것이 됩니다. 사용자에게 도구는 작동하지 않을 때만 관심의 대상이 됩니다. 이러한 강조점의 차이는 사용성이 논의될 때마다 염두에 둘 가치가 있습니다.
엘레노어 로시는 우리가 세상에 대한 이해를 조직하는 범주의 아리스토텔레스적 모델을 뒤집었습니다. 프로그래머들이 사용자에게 시스템에 대한 그들의 욕구에 대해 물을 때 우리는 술어로 구성된 정의를 요구하는 경향이 있습니다. 이것은 우리에게 매우 편리합니다. 술어의 용어들은 매우 쉽게 클래스의 속성이나 테이블의 열이 될 수 있습니다. 이런 종류의 범주들은 명확하고, 분리되어 있고, 깔끔합니다. 불행히도 로시가 "자연 범주"와 이후 작업들에서 보여준 바와 같이, 그것은 사람들이 일반적으로 세상을 이해하는 방식이 아닙니다. 그들은 예시에 기반한 방식으로 이해합니다. 일부 예시들, 소위 프로토타입들은 다른 것들보다 더 좋고, 그래서 결과적인 범주들은 퍼지하고, 겹치며, 풍부한 내부 구조를 가질 수 있습니다. 우리가 아리스토텔레스적 답변을 고집하는 한, 우리는 사용자에게 사용자의 세계에 대한 올바른 질문을 할 수 없고, 우리가 필요로 하는 공통된 이해에 도달하는 데 어려움을 겪을 것입니다.
Keith Braithwaite가 작성함.
"이미 존재하는 것을 사용해 — 바퀴를 재발명하는 것은 바보 같은 일이야..."
이런 말이나 이와 비슷한 변형을 들어본 적이 있나요? 물론 있을 것입니다! 모든 개발자와 학생들이 아마도 이런 댓글을 자주 들을 것입니다. 하지만 왜 그럴까요? 왜 바퀴를 재발명하는 것이 그렇게 눈살을 찌푸리게 할까요? 왜냐하면 대부분의 경우 기존 코드가 작동하는 코드이기 때문입니다. 그것은 이미 어떤 종류의 품질 관리, 엄격한 테스트를 거쳤고, 성공적으로 사용되고 있습니다. 게다가 재발명에 투자된 시간과 노력은 기존 제품이나 코드베이스를 사용하는 것만큼 보상을 받기 어려울 것입니다. 그렇다면 바퀴를 재발명하는 데 시간을 들여야 할까요? 왜? 언제?
아마도 소프트웨어 개발의 패턴에 관한 출판물이나 소프트웨어 설계에 관한 책들을 본적이 있을 것입니다. 이런 책들은 그 안에 들어 있는 정보가 아무리 훌륭해도 지루할 수 있습니다. 항해에 관한 영화를 보는 것과 실제로 항해를 하는 것이 매우 다른 것과 같은 방식으로, 기존 코드를 사용하는 것과 처음부터 자신만의 소프트웨어를 설계하고, 테스트하고, 망가뜨리고, 수리하고, 그 과정에서 개선하는 것도 다릅니다.
바퀴를 재발명하는 것은 단순히 코드 구조를 어디에 배치할지에 대한 연습이 아닙니다: 그것은 이미 존재하는 다양한 컴포넌트들의 내부 작동에 대한 친밀한 지식을 얻는 방법입니다. 메모리 매니저가 어떻게 작동하는지 아세요? 가상 페이징은? 이것들을 직접 구현할 수 있나요? 이중 연결 리스트는 어떤가요? 동적 배열 클래스는? ODBC 클라이언트는? 당신이 알고 좋아하는 인기 있는 것처럼 작동하는 그래픽 사용자 인터페이스를 작성할 수 있나요? 자신만의 웹 브라우저 위젯을 만들 수 있나요? 언제 멀티플렉스 시스템을 작성할지 멀티스레드 시스템을 작성할지 아세요? 파일 기반 데이터베이스와 메모리 기반 데이터베이스 중에서 어떻게 결정하나요? 대부분의 개발자들은 단순히 이런 종류의 핵심 소프트웨어 구현을 직접 만들어본 적이 없고, 따라서 그것들이 어떻게 작동하는지에 대한 친밀한 지식을 갖고 있지 않습니다. 결과적으로 이 모든 종류의 소프트웨어는 그냥 작동하는 신비로운 블랙박스로 여겨집니다. 물의 표면만 이해하는 것으로는 그 아래 숨겨진 위험을 드러내기에 충분하지 않습니다. 소프트웨어 개발의 더 깊은 것들을 모르는 것은 뛰어난 작업을 만들어내는 당신의 능력을 제한할 것입니다.
바퀴를 재발명하고 그것을 잘못하는 것이 첫 번째에 완벽하게 하는 것보다 더 가치 있습니다. 시행착오로부터 배우는 교훈들에는 기술 서적을 읽는 것만으로는 전달할 수 없는 감정적 구성요소가 있습니다!
학습된 사실과 책으로 얻은 지식은 중요하지만, 훌륭한 프로그래머가 되는 것은 사실을 수집하는 것만큼이나 경험을 습득하는 것에 관한 것입니다. 바퀴를 재발명하는 것은 웨이트 리프팅이 보디빌더에게 하는 것만큼 개발자의 교육과 기술에 중요합니다.
Jason P Sage가 작성함.
싱글톤 패턴은 당신의 많은 문제를 해결해줍니다. 하나의 인스턴스만 필요하다는 것을 알고 있습니다. 이 인스턴스가 사용되기 전에 초기화된다는 보장이 있습니다. 전역 접근점을 가짐으로써 설계를 단순하게 유지합니다. 모든 것이 좋습니다. 이 고전적인 디자인 패턴이 마음에 들지 않을 이유가 뭘까요?
사실 꽤 많습니다. 유혹적일 수는 있지만, 경험에 따르면 대부분의 싱글톤은 실제로 도움보다는 해를 더 많이 끼칩니다. 그들은 테스트 가능성을 저해하고 유지보수성을 해칩니다. 불행히도 이런 추가적인 지혜는 그래야 할 만큼 널리 퍼져 있지 않고, 싱글톤은 많은 프로그래머들에게 계속해서 거부할 수 없는 것으로 남아 있습니다. 하지만 저항할 가치가 있습니다:
-
단일 인스턴스 요구사항은 종종 상상입니다. 많은 경우 미래에 추가 인스턴스가 필요하지 않을 것이라는 순전한 추측입니다. 이런 추측적 속성을 애플리케이션의 설계 전반에 방송하는 것은 언젠가 고통을 야기할 수밖에 없습니다. 요구사항은 변할 것입니다. 좋은 설계는 이것을 받아들입니다. 싱글톤은 그렇지 않습니다.
-
싱글톤은 개념적으로 독립적인 코드 단위들 사이에 암시적 의존성을 야기합니다. 이것은 그들이 숨겨져 있고 단위들 사이에 불필요한 결합을 도입하기 때문에 문제가 됩니다. 이 코드 냄새는 느슨한 결합과 실제 구현을 위해 모의 구현을 선택적으로 대체할 수 있는 능력에 의존하는 단위 테스트를 작성하려고 할 때 코가 찡할 정도로 심해집니다. 싱글톤은 그런 간단한 모킹을 방해합니다.
-
싱글톤은 또한 암시적 영속 상태를 갖고 있는데, 이것이 다시 단위 테스트를 저해합니다. 단위 테스트는 테스트들이 서로 독립적이어야 하므로, 테스트들을 어떤 순서로든 실행할 수 있고 모든 단위 테스트 실행 전에 프로그램을 알려진 상태로 설정할 수 있어야 합니다. 가변 상태를 가진 싱글톤을 도입하면, 이것을 달성하기 어려울 수 있습니다. 게다가 그런 전역적으로 접근 가능한 영속 상태는 특히 멀티스레드 환경에서 코드에 대해 추론하기 더 어렵게 만듭니다.
-
멀티스레딩은 싱글톤 패턴에 추가적인 함정들을 도입합니다. 접근에 대한 직접적인 잠금은 그리 효율적이지 않기 때문에, 소위 이중 확인 잠금 패턴(DCLP)이 인기를 얻었습니다. 불행히도 이것은 치명적 매력의 또 다른 형태일 수 있습니다. 많은 언어에서 DCLP가 스레드 안전하지 않고, 그것이 스레드 안전한 곳에서도 여전히 미묘하게 잘못할 기회들이 있다는 것이 밝혀졌습니다.
싱글톤의 정리는 마지막 도전을 제시할 수 있습니다:
-
싱글톤을 명시적으로 죽이는 지원이 없는데, 이것은 일부 상황에서 심각한 문제가 될 수 있습니다. 예를 들어, 플러그인이 모든 객체가 정리된 후에만 안전하게 언로드될 수 있는 플러그인 아키텍처에서 말입니다.
-
프로그램 종료 시 싱글톤의 암시적 정리에는 순서가 없습니다. 이것은 상호 의존성을 가진 싱글톤들을 포함하는 애플리케이션에 문제가 될 수 있습니다. 그런 애플리케이션을 종료할 때, 한 싱글톤이 이미 파괴된 다른 싱글톤에 접근할 수 있습니다.
-
이런 단점들 중 일부는 추가적인 메커니즘을 도입함으로써 극복될 수 있습니다. 하지만 이것은 대안적 설계를 선택함으로써 피할 수 있었던 코드의 추가적인 복잡성의 대가로 옵니다.
따라서 정말로 한 번 이상 인스턴스화되어서는 안 되는 클래스들로 싱글톤 패턴의 사용을 제한하세요. 임의의 코드에서 싱글톤의 전역 접근점을 사용하지 마세요. 대신, 싱글톤에 대한 직접 접근은 잘 정의된 몇 곳에서만 이루어져야 하고, 거기서부터 인터페이스를 통해 다른 코드로 전달될 수 있습니다. 이 다른 코드는 알지 못하므로, 싱글톤이나 다른 어떤 종류의 클래스가 인터페이스를 구현하는지에 의존하지 않습니다. 이것은 단위 테스트를 방해했던 의존성을 깨뜨리고 유지보수성을 향상시킵니다. 그래서 다음에 싱글톤을 구현하거나 접근하는 것을 생각하고 있을 때, 바라건대 당신이 멈춰서 다시 생각하게 될 것입니다.
Sam Saariste가 작성함.
종종 시스템의 성능 튜닝을 하려면 코드를 변경해야 합니다. 코드를 변경해야 할 때, 지나치게 복잡하거나 높은 결합도를 가진 모든 청크는 노력을 탈선시키기 위해 대기하고 있는 더러운 코드 폭탄입니다. 더러운 코드의 첫 번째 희생자는 당신의 일정이 될 것입니다. 앞으로 나아갈 길이 매끄럽다면 언제쯤 끝낼지 예측하기 쉬울 것입니다. 더러운 코드와의 예상치 못한 만남은 정상적인 예측을 하는 것을 매우 어렵게 만들 것입니다.
실행 핫스팟을 발견하는 경우를 생각해보세요. 일반적인 행동 과정은 기본 알고리즘의 강도를 줄이는 것입니다. 당신이 매니저의 추정 요청에 3-4시간이라는 답변으로 응답한다고 해봅시다. 수정을 적용하면서 당신은 종속된 부분을 망가뜨렸다는 것을 빠르게 깨닫습니다. 밀접하게 관련된 것들은 종종 필연적으로 결합되어 있기 때문에, 이런 고장은 대부분 예상되고 고려됩니다. 하지만 그 의존성을 고치는 것이 다른 종속된 부분들을 망가뜨리는 결과를 가져온다면 어떻게 될까요? 게다가 의존성이 원점에서 멀어질수록, 당신이 그것을 그런 것으로 인식하고 추정에서 고려할 가능성이 낮아집니다. 갑자기 당신의 3-4시간 추정이 쉽게 3-4주로 부풀어질 수 있습니다. 종종 이런 예상치 못한 일정 팽창은 한 번에 1-2일씩 일어납니다. "빠른" 리팩토링이 결국 완료되는 데 몇 달이 걸리는 것을 보는 것은 드문 일이 아닙니다. 이런 경우들에서, 책임 있는 팀의 신뢰성과 정치적 자본에 대한 손상은 심각한 것에서 치명적인 것까지 범위에 있을 것입니다. 우리에게 이 위험을 식별하고 측정하는 데 도움이 되는 도구가 있다면 좋을 텐데요.
사실, 우리는 코드의 결합도와 복잡성의 정도와 깊이를 측정하고 제어하는 많은 방법을 가지고 있습니다. 소프트웨어 메트릭은 우리 코드에서 특정 기능의 발생을 세는 데 사용될 수 있습니다. 이런 개수의 값들은 코드 품질과 상관관계가 있습니다. 결합도를 측정하는 여러 메트릭 중 두 가지는 팬인(fan-in)과 팬아웃(fan-out)입니다. 클래스에 대한 팬아웃을 생각해보세요: 이것은 관심 있는 클래스에서 직접 또는 간접적으로 참조되는 클래스의 수로 정의됩니다. 이것을 당신의 클래스가 컴파일되기 전에 컴파일되어야 하는 모든 클래스의 개수로 생각할 수 있습니다. 반면에 팬인은 관심 클래스에 의존하는 모든 클래스의 개수입니다. 팬아웃과 팬인을 알면 *I = fo / (fi + fo)*를 사용하여 불안정성 팩터를 계산할 수 있습니다. I가 0에 가까워질수록 패키지는 더 안정해집니다. I가 1에 가까워질수록 패키지는 불안정해집니다. 안정한 패키지들은 리코딩을 위한 저위험 대상인 반면 불안정한 패키지들은 더러운 코드 폭탄으로 가득 찰 가능성이 더 높습니다. 리팩토링의 목표는 I를 0에 더 가깝게 이동시키는 것입니다.
메트릭을 사용할 때는 그것들이 단지 경험 법칙이라는 것을 기억해야 합니다. 순전히 수학적으로 우리는 fo를 변경하지 않고 *fi*를 증가시키는 것이 I를 0에 더 가깝게 이동시킬 것이라는 것을 볼 수 있습니다. 하지만 매우 큰 팬인 값에는 단점이 있는데, 이런 클래스들은 종속된 것들을 망가뜨리지 않고 변경하기가 더 어려울 것입니다. 또한 팬아웃을 다루지 않으면 실제로 위험을 줄이지 않는 것이므로 어떤 균형이 적용되어야 합니다.
소프트웨어 메트릭의 한 가지 단점은 메트릭 도구들이 생산하는 엄청난 숫자 배열이 초보자에게 위협적일 수 있다는 것입니다. 그렇긴 하지만, 소프트웨어 메트릭은 깨끗한 코드를 위한 우리의 싸움에서 강력한 도구가 될 수 있습니다. 그것들은 더러운 코드 폭탄들이 성능 튜닝 연습에 심각한 위험이 되기 전에 식별하고 제거하는 데 도움이 될 수 있습니다.
Kirk Pepperdine이 작성함.
"다시 해..."라고 상사가 말하며 삭제 키를 세게 눌렀습니다. 너무나도 익숙한 가라앉는 기분으로 컴퓨터 화면을 바라보며, 내 코드가 줄마다 — 하나씩 하나씩 — 망각으로 사라지는 것을 지켜봤습니다.
내 상사 스테판은 항상 가장 목소리 큰 사람은 아니었지만, 나쁜 코드를 보면 알아봤습니다. 그리고 그것을 어떻게 처리해야 하는지 정확히 알고 있었습니다.
나는 에너지가 넘치고 열정이 많지만 코딩하는 방법을 전혀 모르는 학생 프로그래머로서 현재 위치에 도착했습니다. 모든 문제의 해결책이 어딘가에 다른 변수를 추가하는 것이라고 생각하는 끔찍한 경향이 있었습니다. 또는 다른 줄을 던져 넣는 것 말입니다. 나쁜 날에는 각 수정으로 로직이 더 좋아지는 대신에, 내 코드가 점점 더 커지고, 더 복잡해지고, 일관되게 작동하는 것에서 더 멀어졌습니다.
특히 급할 때는 설령 끔찍하더라도 기존 코드 블록에 가장 최소한의 변경만 하려고 하는 것이 자연스럽습니다. 대부분의 프로그래머들은 새로 시작하는 것이 처음으로 돌아가는 것보다 훨씬 더 많은 노력을 필요로 할 것을 두려워하여 나쁜 코드를 보존할 것입니다. 그것은 작동에 가까운 코드에 대해서는 사실일 수 있지만, 모든 도움의 범위를 벗어난 코드가 있습니다.
나쁜 작업을 구제하려고 시도하는 데 필요한 것보다 더 많은 시간이 낭비됩니다. 무언가가 자원 싱크가 되면, 그것은 버려져야 합니다. 빠르게 말입니다.
그렇다고 해서 그 모든 타이핑, 네이밍, 포맷팅을 쉽게 버려야 한다는 것은 아닙니다. 내 상사의 반응은 극단적이었지만, 그것은 나로 하여금 두 번째(또는 때로는 세 번째) 시도에서 코드를 다시 생각하게 만들었습니다. 여전히 나쁜 코드를 고치는 최선의 접근법은 코드가 무자비하게 리팩터되고, 이동되고, 또는 삭제되는 모드로 전환하는 것입니다.
코드는 단순해야 합니다. 최소한의 수의 변수, 함수, 선언, 그리고 다른 구문적 언어 필수사항들이 있어야 합니다. 추가 줄들, 추가 변수들... 추가 무엇이든, 정말로, 제거되어야 합니다. 즉시 제거되어야 합니다. 거기에 있는 것, 남아 있는 것은 일을 완료하고, 알고리즘을 완성하거나 계산을 수행하기에 충분한 것만 있어야 합니다. 그 밖의 모든 것은 그냥 추가적인 원치 않는 노이즈로, 우연히 도입되어 흐름을 가리고 있습니다. 중요한 것들을 숨기고 있습니다.
물론, 그것으로도 안 된다면 그냥 모든 것을 삭제하고 다시 타이핑하세요. 그런 방식으로 자신의 기억에서 그려내는 것은 종종 많은 불필요한 혼란을 제거하는 데 도움이 될 수 있습니다.
Paul W. Homer가 작성함.
좋은 설계의 가장 기초적인 원칙 중 하나는:
같은 이유로 변경되는 것들은 함께 모으고, 다른 이유로 변경되는 것들은 분리하라.
이 원칙은 종종 단일 책임 원칙(Single Responsibility Principle) 또는 SRP로 알려져 있습니다. 간단히 말해서, 그것은 서브시스템, 모듈, 클래스, 또는 심지어 함수가 변경해야 할 이유를 하나 이상 가져서는 안 된다고 말합니다. 고전적인 예는 비즈니스 규칙, 보고서, 그리고 데이터베이스를 다루는 메서드들을 가진 클래스입니다:
public class Employee {
public Money calculatePay() ...
public String reportHours() ...
public void save() ...
}
일부 프로그래머들은 이 세 함수를 같은 클래스에 함께 두는 것이 완전히 적절하다고 생각할 수 있습니다. 결국, 클래스들은 공통 변수들을 조작하는 함수들의 컬렉션이어야 합니다. 하지만 문제는 세 함수가 완전히 다른 이유로 변경된다는 것입니다. calculatePay 함수는 급여 계산을 위한 비즈니스 규칙이 변경될 때마다 변경될 것입니다. reportHours 함수는 누군가가 보고서에 대해 다른 형식을 원할 때마다 변경될 것입니다. save 함수는 DBA들이 데이터베이스 스키마를 변경할 때마다 변경될 것입니다. 이 세 가지 변경 이유들이 결합되어 Employee를 매우 불안정하게 만듭니다. 그것은 그 이유들 중 어떤 것 때문에든 변경될 것입니다. 더 중요하게는, Employee에 의존하는 어떤 클래스들도 그런 변경들에 의해 영향을 받을 것입니다.
좋은 시스템 설계는 시스템을 독립적으로 배포될 수 있는 컴포넌트들로 분리하는 것을 의미합니다. 독립적 배포는 한 컴포넌트를 변경해도 다른 것들을 다시 배포할 필요가 없다는 것을 의미합니다. 하지만 Employee가 다른 컴포넌트들의 많은 다른 클래스들에 의해 많이 사용된다면, Employee에 대한 모든 변경은 다른 컴포넌트들이 다시 배포되게 할 가능성이 높습니다; 따라서 컴포넌트 설계의 주요 이점을 무효화시킵니다(또는 더 유행하는 이름을 선호한다면 SOA).
public class Employee {
public Money calculatePay() ...
}
public class EmployeeReporter {
public String reportHours(Employee e) ...
}
public class EmployeeRepository {
public void save(Employee e) ...
}
위에 보여진 간단한 분할이 문제들을 해결합니다. 이 클래스들 각각은 자신만의 컴포넌트에 배치될 수 있습니다. 또는 오히려, 모든 보고 클래스들은 보고 컴포넌트로 갈 수 있습니다. 모든 데이터베이스 관련 클래스들은 저장소 컴포넌트로 갈 수 있습니다. 그리고 모든 비즈니스 규칙은 비즈니스 규칙 컴포넌트로 갈 수 있습니다.
기민한 독자는 위 해결책에 여전히 의존성들이 있다는 것을 볼 것입니다. Employee가 여전히 다른 클래스들에 의해 의존된다는 것입니다. 그래서 Employee가 수정되면, 다른 클래스들은 아마도 다시 컴파일되고 다시 배포되어야 할 것입니다. 따라서 Employee는 수정되고 나서 독립적으로 배포될 수 없습니다. 하지만 다른 클래스들은 수정되고 독립적으로 배포될 수 있습니다. 그것들 중 하나의 수정도 다른 것들 중 어떤 것도 다시 컴파일되거나 다시 배포되도록 강요할 수 없습니다. Employee조차도 의존성 역전 원칙(Dependency Inversion Principle, DIP)의 신중한 사용을 통해 독립적으로 배포될 수 있지만, 그것은 다른 책의 주제입니다.
다른 이유로 변경되는 것들을 분리하는 SRP의 신중한 적용은 독립적으로 배포 가능한 컴포넌트 구조를 가진 설계를 만드는 열쇠 중 하나입니다.
Uncle Bob이 작성함.
최근 나는 "에다마메(edamame)"를 찾기 위해 고군분투하고 있는 한 식료품점에 있었습니다. 나는 그것이 어떤 종류의 채소라는 것만 대충 알고 있었을 뿐이었죠. 이걸 채소 섹션에서 찾을지, 냉동 섹션에서 찾을지, 아니면 통조림에서 찾을지 전혀 확신이 없었습니다. 결국 포기하고 나를 도와줄 직원을 찾았습니다. 그녀 또한 잘 모르겠다고 했습니다!
그 직원은 여러 가지 방법으로 반응할 수 있었습니다. 내가 어디서 찾아야 하는지를 모르고 있다는 이유로 무지하다는 기분이 들게 하거나, 모호한 가능성을 제시하거나, 아니면 아예 그 물건이 없다고 말할 수도 있었습니다. 하지만 그녀는 그 요청을 문제를 해결하고 고객을 도와줄 기회로 생각했습니다. 그녀는 다른 직원에게 전화를 걸어 몇 분 만에 냉동 섹션에서 내가 찾고 있는 정확한 물건으로 나를 안내해 주었습니다.
이 경우 직원은 요청을 바라보며(looked at) 우리가 문제를 해결하고 요청을 충족시킬 것이라는 전제(premise)에서 시작했습니다. 그녀는 '아니오'에서 시작하는 대신 '예'에서 시작했습니다.
내가 처음 기술 리더십 역할을 맡았을 때, 내 일은 제품 관리자와 비즈니스 분석가가 던지는 황당한 요구들로부터 나의 아름다운 소프트웨어를 보호하는 것이라고 느꼈습니다. 나는 대부분의 대화를 요청을 물리쳐야 할 무엇으로 여기고 시작했습니다.
어느 순간, '아니오'에서 시작하는 대신 '예'에서 시작하는 다른 작업 방식이 있을지도 모른다는 깨달음이 있었습니다. 사실, '예'에서 시작하는 것이 기술 리더가 되기 위한 필수적인 부분이라고 믿게 되었습니다.
이 간단한 변화는 내가 내 일에 접근하는 방식을 급진적으로(radically) 바꿨습니다. 알고 보니, '예'라고 말하는 방법은 정말 많이 있습니다. 누군가가 "이 앱은 모든 창이 둥글고 반투명해지면 정말 멋질 거예요!"라고 말했을 때, 그걸 불합리하다고 거절할 수도 있습니다. 하지만 대신 "왜 그렇게 생각하나요?"로 시작하는 것이 더 낫습니다. 종종 그 사람이 둥글고 반투명한 창을 요청하는 이유는 실제로 중요한 사유가 있을 수 있습니다. 예를 들어, 당신이 둥글고 반투명한 창을 필수로 요구하는 표준 위원회를 가진 대형 고객과 계약을 맺으려고 하는 상황일 수 있습니다.
요청의 맥락을 알면 새로운 가능성이 열린다는 것을 보게 될 것입니다. 요청은 이미 있는 기존 제품을 사용하면서도 다른 방법으로 달성할 수 있는 경우가 흔하여, '예'라고 말하면서 아무런 작업 없이 해결할 수 있는 경우가 많습니다: "사실, 사용자 설정에서 둥글고 반투명한 창 스킨을 다운로드하고 활성화할 수 있습니다."
때때로 다른 사람이 제시한 아이디어가 당신의 제품 관점과 상충할 수 있습니다. 이럴 때는 그 "왜?"를 스스로에게 돌리는 것이 유용합니다. 이유를 언급하는 행위가 당신의 첫 반응이 말이 안 된다는 것을 명확하게 해 줄 수 있습니다. 그렇지 않다면, 다른 주요 의사 결정자를 불러 올려서 문제를 확대할 필요가 있을 수도 있습니다. 기억하세요, 이 모든 것의 목표는 상대방에게 '예'라고 말하고 그것이 자신과 팀을 위해도 작동하는지를 노력하는 것입니다.
기능 요청이 기존 제품과 부합하지 않는 이유에 대해 설득력 있는 설명을 할 수 있다면, 당신은 옳은 제품을 만들고 있는지에 대해 생산적인 대화를 나누게 될 것입니다. 그 대화가 어떻게 결론이 나든, 모든 사람은 제품이 무엇이며 무엇이 아닌지에 대해 더 분명하게 초점을 맞추게 될 것입니다.
'예'에서 시작하는 것은 동료들과 함께 일하는 것을 의미하며, 그들에게 반대하지 않는 것입니다.
Alex Miller 씀.
나는 모듈의 코드 라인 수를 세라는 요청을 받았을 때, 파일들을 워드 프로세서에 붙여넣고 "줄 세기" 기능을 사용하는 프로그래머들과 함께 일했습니다. 그리고 그들은 다음 주에도 똑같이 했습니다. 그 다음 주에도. 정말 형편없었습니다.
나는 코드 서명과 서버로 결과를 이동하는 것을 포함하여 많은 마우스 클릭을 필요로 하는 번거로운 배포 프로세스가 있는 프로젝트에서 일했습니다. 누군가가 그것을 자동화했고 그 스크립트는 최종 테스트 동안 예상했던 것보다 훨씬 더 자주, 수백 번 실행되었습니다. 정말 좋았습니다.
그러면 왜 사람들은 한 걸음 물러서서 시간을 들여 자동화하는 대신 똑같은 작업을 반복하고 또 반복할까요?
물론 테스트 자동화는 훌륭하지만, 왜 거기서 멈춰야 할까요? 반복적인 작업들은 어떤 프로젝트에서든 풍부합니다: 버전 관리, 컴파일링, JAR 파일 빌드, 문서 생성, 배포, 그리고 보고. 이런 작업들 중 많은 것에서 스크립트가 마우스보다 강력합니다. 지루한 작업들을 실행하는 것이 더 빠르고 더 신뢰할 수 있게 됩니다.
팀원들과 "하지만 내 머신에서는 (체크아웃|빌드|테스트 통과)가 된다고?" 하는 논쟁을 해본 적이 있나요? 최신 IDE들은 수천 가지의 잠재적 설정을 가지고 있고, 모든 팀 구성원들이 동일한 구성을 갖도록 보장하는 것은 본질적으로 불가능합니다. Ant나 Autotools 같은 빌드 자동화 시스템은 제어와 반복성을 제공합니다.
적절한 셸 언어(bash나 PowerShell 같은)와 빌드 자동화 시스템만으로도 꽤 멀리 갈 수 있습니다. 웹사이트와 상호작용해야 한다면, iMacros나 Selenium 같은 도구를 사용하세요.
프로세스의 일부가 Word 문서, 스프레드시트, 또는 이미지를 필요로 한다면, 실제로 자동화하기 어려울 수 있습니다. 하지만 그것이 정말로 필요한가요? 평문을 사용할 수 있나요? 쉼표로 구분된 값들은? XML은? 텍스트 파일에서 그림을 생성하는 도구는? 종종 프로세스의 약간의 조정으로 지루함을 극적으로 줄이면서 좋은 결과를 얻을 수 있습니다.
시작하기 위해 bash나 Ant의 모든 것을 배울 필요는 없습니다. 해나가면서 배우세요. 자동화할 수 있고 자동화해야 한다고 생각하는 작업이 있을 때, 그것을 하기 위해 도구에 대해 충분히만 배우세요. 그리고 시간을 찾기가 보통 더 쉬운 프로젝트 초기에 하세요. 성공하고 나면, 당신(과 당신의 상사)은 자동화에 투자하는 것이 이치에 맞다는 것을 보게 될 것입니다.
Cay Horstmann이 작성함.
테스트의 가치는 소프트웨어 개발자들의 프로그래밍 여정 초기 단계부터 머릿속에 드럼처럼 박혀 있는 것입니다. 최근 몇 년 동안 단위 테스트, 테스트 주도 개발, 그리고 애자일 방법론의 부상으로 개발 사이클의 모든 단계에서 테스트를 최대한 활용하는 것에 대한 관심이 급증했습니다. 하지만 테스트는 코드 품질을 향상시키기 위해 사용할 수 있는 많은 도구 중 하나일 뿐입니다.
먼 옛날, C가 아직 새로운 현상이었을 때, CPU 시간과 모든 종류의 저장 공간은 매우 귀했습니다. 첫 번째 C 컴파일러들은 이것을 염두에 두고 일부 의미 분석을 제거함으로써 코드를 거치는 패스의 수를 줄였습니다. 이것은 컴파일러가 컴파일 시간에 감지될 수 있는 버그의 작은 부분집합만을 확인한다는 것을 의미했습니다. 이를 보상하기 위해 스티븐 존슨은 lint라는 도구를 작성했는데 — 이는 코드에서 보풀을 제거하는 것입니다 — 이것은 자매 C 컴파일러에서 제거되었던 정적 분석 중 일부를 구현했습니다. 하지만 정적 분석 도구들은 많은 수의 거짓 양성 경고들과 항상 따를 필요가 없는 문체적 관례에 대한 경고들을 준다는 평판을 얻었습니다.
언어, 컴파일러, 그리고 정적 분석 도구들의 현재 환경은 매우 다릅니다. 메모리와 CPU 시간은 이제 상대적으로 저렴하므로, 컴파일러들은 더 많은 오류를 확인할 여유가 있습니다. 거의 모든 언어가 스타일 가이드 위반, 일반적인 함정, 그리고 때로는 잠재적 null 포인터 역참조같이 잡기 어려울 수 있는 교묘한 오류들을 확인하는 도구를 적어도 하나는 자랑합니다. C를 위한 Splint나 Python을 위한 Pylint 같은 더 정교한 도구들은 구성 가능합니다. 즉, 구성 파일, 명령줄 스위치, 또는 IDE를 통해 도구가 내보내는 오류와 경고를 선택할 수 있습니다. Splint는 심지어 프로그램이 어떻게 작동하는지에 대한 더 나은 힌트를 주기 위해 주석으로 코드에 주석을 달게 해줍니다.
다른 모든 것이 실패하고, 컴파일러, IDE, 또는 lint 도구들에 의해 잡히지 않는 간단한 버그나 표준 위반을 찾고 있다면, 언제든지 자신만의 정적 검사기를 만들 수 있습니다. 이것은 들리는 것만큼 어렵지 않습니다. 대부분의 언어들, 특히 동적이라고 브랜드화된 것들은 추상 구문 트리와 컴파일러 도구들을 표준 라이브러리의 일부로 노출합니다. 당신이 사용하고 있는 언어의 개발팀이 사용하는 표준 라이브러리의 먼지 낀 구석들을 알아가는 것은 충분히 가치가 있는데, 이런 곳들에는 종종 정적 분석과 동적 테스트에 유용한 숨겨진 보석들이 포함되어 있기 때문입니다. 예를 들어, Python 표준 라이브러리에는 일부 컴파일된 코드나 코드 객체를 생성하는 데 사용된 바이트코드를 알려주는 디스어셈블러가 포함되어 있습니다. 이것은 python-dev 팀의 컴파일러 작성자들을 위한 모호한 도구처럼 들리지만, 실제로는 일상적인 상황에서 놀랍도록 유용합니다. 이 라이브러리가 디스어셈블할 수 있는 한 가지는 마지막 스택 추적으로, 마지막 잡히지 않은 예외를 던진 정확한 바이트코드 명령에 대한 피드백을 제공합니다.
그러므로 테스트가 품질 보증의 끝이 되도록 하지 마세요 — 분석 도구들을 활용하고 자신만의 것을 만드는 것을 두려워하지 마세요.
Sarah Mount가 작성함.
테스트에서 흔한 함정은 구현이 수행하는 것이 정확히 테스트하고자 하는 것이라고 가정하는 것입니다. 첫눈에 이것은 함정보다는 미덕처럼 들립니다. 하지만 다른 방식으로 표현하면 문제가 더 명백해집니다: 테스트에서 흔한 함정은 구현의 세부사항에 테스트를 고정시키는 것인데, 그 세부사항들이 부수적이고 원하는 기능과 관련이 없는 경우입니다.
테스트가 구현의 부수적 사항들에 고정되면, 실제로는 필요한 동작과 호환되는 구현 변경이 테스트 실패를 야기하여 거짓 양성을 초래할 수 있습니다. 프로그래머들은 일반적으로 테스트를 다시 작성하거나 코드를 다시 작성함으로써 응답합니다. 거짓 양성이 실제로 참 양성이라고 가정하는 것은 종종 두려움, 불확실성, 또는 의심의 결과입니다. 이것은 부수적 동작의 지위를 필요한 동작으로 올리는 효과가 있습니다. 테스트를 다시 작성할 때, 프로그래머들은 테스트를 필요한 동작에 다시 집중시키거나(좋음) 아니면 단순히 새로운 구현에 고정시킵니다(좋지 않음). 테스트들은 충분히 정확해야 하지만, 정확하기도 해야 합니다.
예를 들어, C의 strcmp나 Java의 String.compareTo 같은 3방향 비교에서, 결과에 대한 요구사항은 왼쪽이 오른쪽보다 작으면 음수, 왼쪽이 오른쪽보다 크면 양수, 그리고 같다고 간주되면 0이라는 것입니다. 이런 스타일의 비교는 C의 qsort 함수용 비교자와 Java의 Comparable 인터페이스의 compareTo를 포함한 많은 API에서 사용됩니다. 특정 값 -1과 +1이 구현에서 각각 작음과 큼을 나타내기 위해 일반적으로 사용되지만, 프로그래머들은 종종 이 값들이 실제 요구사항을 나타낸다고 잘못 가정하고 결과적으로 이 가정을 공개적으로 못박는 테스트를 작성합니다.
비슷한 문제가 간격, 정확한 표현, 그리고 부수적인 텍스트 포맷팅과 표현의 다른 측면들을 주장하는 테스트들에서 발생합니다. 예를 들어, 구성 가능한 포맷팅을 제공하는 XML 생성기를 작성하는 것이 아니라면, 간격은 결과에 중요하지 않아야 합니다. 마찬가지로, UI 컨트롤에서 버튼과 레이블의 배치를 고정시키는 것은 미래에 이런 부수적 사항들을 변경하고 정제할 옵션을 줄입니다. 구현의 사소한 변경과 포맷팅의 중요하지 않은 변경이 갑자기 빌드 중단기가 됩니다.
과도하게 명시된 테스트들은 종종 단위 테스트에 대한 화이트박스 접근법의 문제입니다. 화이트박스 테스트는 필요한 테스트 케이스를 결정하기 위해 코드의 구조를 사용합니다. 화이트박스 테스트의 전형적인 실패 모드는 테스트들이 결국 코드가 코드가 하는 일을 한다고 주장하게 되는 것입니다. 코드에서 이미 명백한 것을 단순히 다시 말하는 것은 아무런 가치를 추가하지 않고 잘못된 진전감과 안전감을 초래합니다.
효과적이 되려면, 테스트들은 구현을 앵무새처럼 따라하기보다는 계약 의무를 명시해야 합니다. 그들은 테스트 대상 단위들에 대한 블랙박스 관점을 취하여 인터페이스 계약들을 실행 가능한 형태로 스케치해야 합니다. 따라서 테스트된 동작을 필요한 동작과 일치시키세요.
Kevlin Henney가 작성함.
코드 단위의 특정 구현의 부수적 동작을 테스트하기보다는 원하는 필수 동작을 테스트하는 것이 중요합니다. 하지만 이것을 모호한 테스트에 대한 변명으로 받아들이거나 오해해서는 안 됩니다. 테스트는 정확하고 정확해야 합니다.
시도되고, 테스트되고, 테스트의 고전인 정렬 루틴은 설명적인 예시를 제공합니다. 정렬 알고리즘을 구현하는 것이 프로그래머에게 매일의 작업은 아니지만, 정렬은 대부분의 사람들이 그것에서 무엇을 기대하는지 안다고 믿을 정도로 친숙한 아이디어입니다. 하지만 이런 무심한 친숙함은 특정 가정들을 넘어서 보는 것을 더 어렵게 만들 수 있습니다.
프로그래머들에게 "무엇을 테스트하겠습니까?"라고 물었을 때, 단연코 가장 일반적인 응답은 "정렬의 결과는 정렬된 요소 시퀀스입니다"입니다. 이것이 사실이긴 하지만, 전체 진실은 아닙니다. 더 정확한 조건을 위해 촉구받았을 때, 많은 프로그래머들은 결과 시퀀스가 원본과 같은 길이여야 한다고 추가합니다. 정확하긴 하지만, 이것도 여전히 충분하지 않습니다. 예를 들어, 다음 시퀀스가 주어졌을 때:
3 1 4 1 5 9
다음 시퀀스는 비내림차순으로 정렬되고 원본 시퀀스와 같은 길이를 갖는다는 사후조건을 만족합니다:
3 3 3 3 3 3
사양을 만족하긴 하지만, 이것은 또한 확실히 의도된 것이 아닙니다! 이 예시는 실제 프로덕션 코드에서 가져온 오류(다행히 릴리스되기 전에 잡힌)를 기반으로 한 것으로, 키 입력의 간단한 실수나 순간적인 이성의 실수가 주어진 배열의 첫 번째 요소로 전체 결과를 채우는 정교한 메커니즘으로 이어졌습니다.
완전한 사후조건은 결과가 정렬되고 원본 값들의 순열을 보유한다는 것입니다. 이것은 필요한 동작을 적절히 제약합니다. 결과 길이가 입력 길이와 같다는 것은 자연스럽게 따라오므로 다시 말할 필요가 없습니다.
설명된 방식으로 사후조건을 명시하는 것조차 좋은 테스트를 제공하기에는 충분하지 않습니다. 좋은 테스트는 읽기 가능해야 합니다. 그것은 이해할 수 있고 당신이 그것이 올바른지(또는 아닌지) 쉽게 볼 수 있을 정도로 단순해야 합니다. 시퀀스가 정렬되었는지 확인하고 한 시퀀스가 다른 시퀀스의 값들의 순열을 포함하는지 확인하는 코드가 이미 있지 않다면, 테스트 코드가 테스트 대상 코드보다 더 복잡할 가능성이 꽤 높습니다. 토니 호어가 관찰한 바와 같이:
소프트웨어 설계를 구성하는 두 가지 방법이 있습니다: 한 가지 방법은 명백히 결함이 없을 정도로 단순하게 만드는 것이고 다른 방법은 명백한 결함이 없을 정도로 복잡하게 만드는 것입니다.
구체적인 예시를 사용하는 것은 이런 우발적 복잡성과 사고의 기회를 제거합니다. 예를 들어, 다음 시퀀스가 주어졌을 때:
3 1 4 1 5 9
정렬의 결과는 다음과 같습니다:
1 1 3 4 5 9
다른 답은 안 됩니다. 대체품을 받아들이지 마세요.
구체적인 예시는 접근 가능하고 모호하지 않은 방식으로 일반적인 동작을 설명하는 데 도움이 됩니다. 빈 컬렉션에 항목을 추가한 결과는 단순히 그것이 비어있지 않다는 것이 아닙니다: 컬렉션이 이제 단일 항목을 가진다는 것입니다. 그리고 보유된 단일 항목이 추가된 항목이라는 것입니다. 두 개 이상의 항목들도 비어있지 않다는 자격을 갖춥니다. 그리고 그것도 틀릴 것입니다. 다른 값의 단일 항목도 틀릴 것입니다. 테이블에 행을 추가한 결과는 단순히 테이블이 한 행 더 크다는 것이 아닙니다. 그것은 또한 행의 키가 추가된 행을 복구하는 데 사용될 수 있다는 것을 수반합니다. 기타 등등.
동작을 명시할 때, 테스트들은 단순히 정확하기만 해서는 안 됩니다: 그들은 또한 정확해야 합니다.
Kevlin Henney가 작성함.
안심하세요. 나는 해외 개발 센터, 주말 야근, 또는 야간 교대에 대해 말하는 것이 아닙니다. 오히려, 우리가 마음대로 쓸 수 있는 컴퓨팅 파워가 얼마나 많은지에 대해 당신의 주의를 끌고 싶습니다. 구체적으로, 프로그래머로서 우리의 삶을 조금 더 쉽게 만들기 위해 우리가 활용하지 않고 있는 것이 얼마나 많은지 말입니다. 업무 시간 동안 충분한 컴퓨팅 파워를 얻는 것이 지속적으로 어렵다는 것을 발견하고 있나요? 그렇다면, 정상 근무 시간 밖에 당신의 테스트 서버들은 무엇을 하고 있습니까? 대부분의 경우, 테스트 서버들은 밤사이와 주말에 놀고 있습니다. 이것을 당신의 장점으로 사용할 수 있습니다.
-
모든 테스트를 실행하지 않고 변경을 커밋한 적이 있나요? 프로그래머들이 코드를 커밋하기 전에 테스트 스위트를 실행하지 않는 주된 이유 중 하나는 그것들이 걸릴 수 있는 시간의 길이 때문입니다. 마감일이 다가오고 절박해지면, 인간은 자연스럽게 모서리를 자르기 시작합니다. 이것을 해결하는 한 가지 방법은 큰 테스트 스위트를 두 개 이상의 프로필로 나누는 것입니다. 빠르게 실행되는 더 작고 필수적인 테스트 프로필은 각 커밋 전에 테스트가 실행되도록 보장하는 데 도움이 될 것입니다. 모든 테스트 프로필들(필수 프로필을 포함해서 — 확실히 하기 위해)은 밤사이에 실행되도록 자동화되어, 아침에 결과를 보고할 준비가 될 수 있습니다.
-
제품의 안정성을 테스트할 충분한 기회가 있었나요? 더 오래 실행되는 테스트들은 메모리 누수와 다른 안정성 문제들을 식별하는 데 필수적입니다. 그것들은 시간과 자원을 묶어둘 것이기 때문에 낮에는 거의 실행되지 않습니다. 밤에 실행될 흡수 테스트를 자동화하고, 주말에는 조금 더 길게 실행할 수 있습니다. 금요일 오후 6시부터 다음 월요일 오전 6시까지 60시간의 잠재적 테스트 시간이 있습니다.
-
성능 테스트 환경에서 품질 시간을 얻고 있나요? 나는 팀들이 성능 테스트 환경에서 시간을 얻기 위해 서로 다투는 것을 본 적이 있습니다. 대부분의 경우 어느 팀도 낮에 충분한 품질 시간을 얻지 못하는 반면, 환경은 시간 외에는 사실상 놀고 있습니다. 서버들과 네트워크는 밤이나 주말에는 그렇게 바쁘지 않습니다. 품질 좋은 성능 테스트를 실행하기에 이상적인 시간입니다.
-
수동으로 테스트하기에는 너무 많은 순열이 있나요? 많은 경우 당신의 제품은 다양한 플랫폼에서 실행되도록 목표로 합니다. 예를 들어, 32비트와 64비트 모두에서, Linux, Solaris, 그리고 Windows에서, 또는 단순히 같은 운영 체제의 다른 버전들에서 말입니다. 상황을 더 악화시키기 위해, 많은 최신 애플리케이션들은 다수의 전송 메커니즘과 프로토콜들(HTTP, AMQP, SOAP, CORBA 등)에 자신들을 노출시킵니다. 이 모든 순열들을 수동으로 테스트하는 것은 매우 시간 소모적이고 자원 압박으로 인해 릴리스에 가까워서야 행해질 가능성이 높습니다. 슬프게도, 특정 나쁜 버그들을 잡기에는 사이클에서 너무 늦을 수 있습니다.
밤이나 주말에 실행되는 자동화된 테스트들은 이 모든 순열들이 더 자주 테스트되도록 보장할 것입니다. 약간의 생각과 일부 스크립팅 지식으로, 밤과 주말에 일부 테스트를 시작하기 위해 몇 개의 cron 작업을 스케줄할 수 있습니다. 도움이 될 수 있는 많은 테스트 도구들도 있습니다. 일부 조직들은 심지어 자원이 효율적으로 사용되도록 보장하기 위해 다른 부서와 팀들에 걸쳐 서버들을 풀링하는 서버 그리드를 가지고 있습니다. 이것이 당신의 조직에서 사용 가능하다면, 밤이나 주말에 실행될 테스트들을 제출할 수 있습니다.
Rajith Attapattu가 작성함.
개발자들은 가족 구성원, 배우자, 그리고 다른 비기술자들에게 자신들이 하는 일을 설명하려고 할 때 억지스러운 비유를 사용하는 것을 좋아합니다. 우리는 자주 교량 건설과 다른 "딱딱한" 엔지니어링 분야에 의존합니다. 하지만 이 모든 비유들은 너무 강하게 밀어붙이기 시작할 때 빠르게 무너집니다. 소프트웨어 개발은 많은 중요한 방면에서 많은 "딱딱한" 엔지니어링 분야와 같지 않다는 것이 밝혀집니다.
"딱딱한" 엔지니어링과 비교했을 때, 소프트웨어 개발 세계는 일반적인 전략이 교량을 건설하고 나서 무거운 것을 그 위로 굴리는 것이었던 때의 교량 건설자들과 대략 같은 위치에 있습니다. 그것이 서 있으면 좋은 교량이었습니다. 그렇지 않으면, 글쎄, 다시 제도판으로 돌아갈 시간이었습니다. 지난 몇천 년 동안, 엔지니어들은 그것을 실제로 지어보지 않고도 구조적 해결책을 위해 사용할 수 있는 수학과 물리학을 개발했습니다. 우리는 소프트웨어에서 그런 것을 갖고 있지 않고, 소프트웨어가 실제로 매우 다르기 때문에 아마도 결코 갖지 못할 것입니다. 소프트웨어 "엔지니어링"과 일반 엔지니어링 사이의 비교에 대한 심층 탐구를 위해서는, 1992년 Jack Reeves가 C++ Journal에 쓴 "What is Software Design?"이 고전입니다. 거의 20년 전에 쓰여졌음에도 불구하고, 여전히 놀랍도록 정확합니다. 그는 이 비교에서 암울한 그림을 그렸지만, 1992년에 누락되었던 것은 소프트웨어에 대한 강한 테스트 정신이었습니다.
"딱딱한" 것들을 테스트하는 것은 어려운데, 테스트하기 위해 그것들을 만들어야 하기 때문이고, 이것은 무슨 일이 일어날지 보기 위한 투기적 건설을 억제합니다. 하지만 소프트웨어에서 건설 과정은 터무니없이 저렴합니다. 우리는 정확히 그것을 하기 쉽게 만드는 도구들의 전체 생태계를 개발했습니다: 단위 테스트, 모의 객체, 테스트 하네스, 그리고 많은 다른 것들. 다른 엔지니어들은 무언가를 만들고 현실적인 조건에서 테스트할 수 있기를 바랄 것입니다. 소프트웨어 개발자로서, 우리는 소프트웨어의 주요한(하지만 유일하지 않은) 검증 메커니즘으로서 테스트를 받아들여야 합니다. 소프트웨어를 위한 어떤 종류의 미적분학을 기다리기보다는, 우리는 이미 좋은 엔지니어링 관행을 보장하기 위한 도구들을 우리 마음대로 쓸 수 있습니다. 이런 관점에서 보면, 우리는 이제 "우리는 테스트할 시간이 없다"고 말하는 매니저들에 대한 탄약을 갖고 있습니다. 교량 건설자는 결코 상사로부터 "그 건물에 구조 분석을 하는 것을 신경쓰지 마 — 우리는 빠듯한 마감일이 있어"라는 말을 듣지 않을 것입니다. 테스트가 실제로 소프트웨어에서 재현성과 품질로 가는 길이라는 인식은 개발자로서 우리가 그것에 반대하는 논증들을 전문적으로 무책임한 것으로 반박할 수 있게 해줍니다.
테스트는 시간이 걸리는데, 구조 분석이 시간을 걸리는 것과 같습니다. 두 활동 모두 최종 제품의 품질을 보장합니다. 소프트웨어 개발자들이 그들이 생산하는 것에 대한 책임의 외투를 입을 시간입니다. 테스트만으로는 충분하지 않지만, 필요합니다. 테스트는 소프트웨어 개발의 엔지니어링 엄격함입니다.
Neal Ford가 작성함.
현실 세계의 사람들은 상태와 이상한 관계를 가지고 있습니다. 오늘 아침 나는 카페인을 코드로 변환하는 또 다른 하루를 준비하기 위해 지역 상점에 들렀습니다. 그것을 하는 내가 가장 좋아하는 방법은 라떼를 마시는 것인데, 우유를 찾을 수 없어서 점원에게 물어봤습니다.
"죄송하지만, 우리는 우유가 완전히, 엄청나게, 매우 떨어졌습니다."
프로그래머에게 그것은 이상한 진술입니다. 우유가 떨어졌거나 아니거나입니다. 우유가 떨어진 것에 대한 척도는 없습니다. 아마도 그녀는 일주일 동안 우유가 떨어져 있을 것이라고 말하려고 했을 것이지만, 결과는 같았습니다 — 나에게는 에스프레소 날이었습니다.
대부분의 현실 세계 상황에서, 사람들의 상태에 대한 느슨한 태도는 문제가 되지 않습니다. 불행히도 많은 프로그래머들도 상태에 대해 꽤 모호합니다 — 그리고 그것이 문제입니다.
신용카드만 받고 고객에게 송장을 보내지 않는 간단한 웹샵을 생각해보세요. 이 메서드를 포함하는 Order 클래스가 있습니다:
public boolean isComplete() {
return isPaid() && hasShipped();
}
합리적이죠? 글쎄요, 표현식이 모든 곳에 복사해서 붙여넣기되는 대신 메서드로 잘 추출되었더라도, 그 표현식은 전혀 존재해서는 안 됩니다. 그것이 존재한다는 사실은 문제를 강조합니다. 왜일까요? 주문이 결제되기 전에는 배송될 수 없기 때문입니다. 따라서 isPaid가 true가 아니라면 hasShipped는 true가 될 수 없고, 이것은 표현식의 일부를 중복으로 만듭니다. 코드의 명확성을 위해 여전히 isComplete를 원할 수 있지만, 그러면 다음과 같이 보여야 합니다:
public boolean isComplete() {
return hasShipped();
}
내 작업에서, 나는 누락된 검사와 중복된 검사를 항상 봅니다. 이 예시는 작지만, 취소와 환불을 추가하면 더 복잡해질 것이고 좋은 상태 처리의 필요성이 증가합니다. 이 경우, 주문은 세 가지 구별되는 상태 중 하나에만 있을 수 있습니다:
- 진행 중: 항목을 추가하거나 제거할 수 있습니다. 배송할 수 없습니다.
- 결제됨: 항목을 추가하거나 제거할 수 없습니다. 배송될 수 있습니다.
- 배송됨: 완료. 더 이상의 변경은 받아들여지지 않습니다.
이런 상태들은 중요하고 당신은 작업을 하기 전에 예상되는 상태에 있는지 확인해야 하고, 당신이 있는 곳에서 합법적인 상태로만 이동해야 합니다. 간단히 말해서, 당신은 올바른 장소에서 객체들을 신중하게 보호해야 합니다.
하지만 어떻게 상태로 생각하기를 시작할까요? 표현식을 의미 있는 메서드로 추출하는 것은 매우 좋은 시작이지만, 그것은 단지 시작일 뿐입니다. 기초는 상태 머신을 이해하는 것입니다. CS 수업에서 나쁜 기억을 가지고 있을지도 모르지만, 그것들을 뒤로 남겨두세요. 상태 머신은 특별히 어렵지 않습니다. 그것들을 이해하기 쉽고 이야기하기 쉽게 만들기 위해 시각화하세요. 유효하고 무효한 상태와 전환을 밝혀내고 그것들을 올바르게 유지하기 위해 코드를 테스트 주도하세요. 상태 패턴을 공부하세요. 편안해지면, 계약에 의한 설계를 읽어보세요. 그것은 들어오는 데이터와 각 공개 메서드의 진입과 종료에서 객체 자체를 검증함으로써 유효한 상태를 보장하는 데 도움이 됩니다.
상태가 올바르지 않다면, 버그가 있고 중단하지 않으면 데이터를 망칠 위험이 있습니다. 상태 검사들이 소음이라고 발견하면, 그것들을 숨기기 위해 도구, 코드 생성, 위빙, 또는 애스펙트를 사용하는 방법을 배우세요. 어떤 접근법을 선택하든, 상태로 생각하는 것은 당신의 코드를 더 단순하고 더 견고하게 만들 것입니다.
Niclas Nilsson이 작성함.
프로그래밍은 깊은 사고를 요구하며, 깊은 사고는 고독을 필요로 합니다. 프로그래머의 고정 관념은 그렇습니다.
이 "외로운 늑대" 방식의 프로그래밍은 점점 더 협업적인 접근 방법으로 대체되고 있습니다. 이는 제가 주장하듯이 프로그래머의 품질, 생산성 및 직무 만족도를 향상시킵니다. 이 접근 방식은 개발자들이 서로 더 긴밀하게 협력하는 것은 물론, 비개발자들인 비즈니스 및 시스템 분석가, 품질 보증 전문가 및 사용자들과 함께 작업하는 것을 포함합니다.
이것이 개발자에게는 무슨 의미일까요? 전문가 기술자로 있는 것만으로는 더 이상 충분하지 않습니다. 다른 사람들과 협력하는 데 효과적이어야 합니다.
협력은 질문을 하고 답하는 것, 회의에 앉아있는 것만을 의미하지 않습니다. 그것은 누군가와 함께 소매를 걷어붙이고 함께 작업을 처리하는 것입니다.
저는 페어 프로그래밍(pair programming)의 열렬한 팬입니다. 이를 "극도의 협력"이라 부를 수 있습니다. 개발자로서, 저는 페어링할 때 제 기술이 성장합니다. 만약 제가 도메인이나 기술에서 제 페어링 파트너보다 부족하다면, 저는 그 사람의 경험에서 분명히 배우게 됩니다. 제가 어떤 측면에서 더 강할 경우, 저를 설명해야 하므로 제가 아는 것과 모르는 것에 대해 더 많이 배우게 됩니다. 불가피하게, 우리는 모두 테이블에 무언가를 가져오고 서로에게서 배웁니다.
페어링할 때 우리는 각자의 집합적인 프로그래밍 경험 — 기술 뿐만 아니라 도메인도 — 을 현재의 문제에 가져와 소프트웨어를 효과적이고 효율적으로 작성하는 데 고유한 통찰력과 경험을 제공합니다. 도메인이나 기술 지식에서 극도의 불균형이 있더라도, 더 경험이 많은 참가자는 불가피하게 다른 사람에게서 무언가를 배우게 됩니다 — 어쩌면 새로운 키보드 단축키, 또는 새로운 도구나 라이브러리에 대한 노출일 수 있습니다. 페어의 경험이 적은 구성원에게는 이것이 빠르게 적응할 수 있는 훌륭한 방법입니다.
페어 프로그래밍은 애자일 소프트웨어 개발의 지지자들 사이에서 인기가 있지만 독점적이지는 않습니다. 페어링에 반대하는 사람들은 "왜 제가 두 명의 프로그래머에게 한 명의 일을 시키기 위해 돈을 줘야 합니까?"라고 질문합니다. 제 대답은, 사실, 그렇게 하지 말아야 한다는 것입니다. 저는 페어링이 품질, 도메인 및 기술에 대한 이해, 기술(예: IDE 팁)을 높이고 복권 리스크(전문 개발자 중 한 명이 복권에 당첨되어 다음 날 퇴사하는 상황)의 영향을 완화한다고 주장합니다.
새로운 키보드 단축키를 배우는 것의 장기적인 가치는 무엇인가요? 페어링으로 인해 제품에 대해 전체 품질 개선이 얼마나 이루어졌는지 어떻게 측정할 수 있을까요? 문제가 어려운 경우, 당신의 파트너가 당신이 잘못된 접근 방식을 추구하지 못하게 하는 것의 영향을 어떻게 측정할까요? 한 연구에서는 효과성과 속도가 40% 증가했다고 합니다 (J T Nosek, "The Case for Collaborative Programming," Communications of the ACM, 1998년 3월). 당신의 "복권 리스크"를 완화하는 것의 가치는 무엇일까요? 이러한 이득의 대부분은 측정하기 어렵습니다.
누가 누구와 페어링해야 할까요? 팀에 새로 합류했다면, 지식이 있는 팀원을 찾는 것이 중요합니다. 마찬가지로, 좋은 대인 관계 및 코칭 기술을 가진 사람을 찾는 것도 중요합니다. 도메인 경험이 많지 않다면, 해당 도메인의 전문가인 팀원과 페어링하세요.
설득이 되지 않았다면, 실험해보세요: 동료들과 협력해보세요. 흥미롭고 복잡한 문제를 페어링하여 해결해보세요. 그 느낌이 어떤지 보세요. 몇 번 시도해보세요.
Adrian Wible 씀.
코드는 결코 거짓말을 하지 않지만, 자기 자신과 모순될 수는 있습니다. 일부 모순들은 "그것이 어떻게 가능하게 작동할 수 있을까?" 순간들로 이어집니다.
한 인터뷰에서, 아폴로 11호 달 착륙선 소프트웨어의 주 설계자인 Allan Klumpp는 엔진을 제어하는 소프트웨어가 착륙선을 불안정하게 만들었어야 할 버그를 포함하고 있었다고 밝혔습니다. 하지만 다른 버그가 첫 번째를 보상했고, 그 소프트웨어는 두 버그 중 어느 것도 발견되거나 고쳐지기 전에 아폴로 11호와 12호 달 착륙 모두에 사용되었습니다.
완료 상태를 반환하는 함수를 생각해보세요. 그것이 true를 반환해야 할 때 false를 반환한다고 상상해보세요. 이제 호출하는 함수가 반환 값을 확인하는 것을 소홀히 한다고 상상해보세요. 어느 날 누군가가 누락된 검사를 발견하고 그것을 삽입할 때까지 모든 것이 잘 작동합니다.
또는 상태를 XML 문서로 저장하는 애플리케이션을 생각해보세요. 노드 중 하나가 문서에서 말하는 대로 TimeToDie 대신 TimeToLive로 잘못 작성되었다고 상상해보세요. 작성자 코드와 읽기 코드 모두가 같은 오류를 포함하고 있는 동안에는 모든 것이 괜찮게 보입니다. 하지만 하나를 고치거나 같은 문서를 읽는 새로운 애플리케이션을 추가하면, 대칭성이 깨지고 코드도 깨집니다.
코드의 두 결함이 하나의 가시적 장애를 만들 때, 장애를 고치는 체계적 접근법 자체가 무너질 수 있습니다. 개발자는 버그 리포트를 받고, 결함을 찾고, 고치고, 다시 테스트합니다. 하지만 두 번째 결함이 작동하고 있기 때문에 보고된 장애가 여전히 발생합니다. 그래서 첫 번째 수정이 제거되고, 두 번째 기본 결함이 발견될 때까지 코드가 검사되고, 그것에 대한 수정이 적용됩니다. 하지만 첫 번째 결함이 돌아왔고, 보고된 장애가 여전히 보이므로, 두 번째 수정이 롤백됩니다. 과정이 반복되지만 이제 개발자는 두 가지 가능한 수정을 기각했고 결코 작동하지 않을 세 번째를 만들려고 합니다.
하나의 가시적 장애로 나타나는 두 코드 결함 사이의 상호작용은 문제를 고치기 어렵게 만들 뿐만 아니라 개발자들을 막다른 골목으로 이끌어, 그들이 초기에 올바른 답을 시도했다는 것을 발견하게 합니다.
이것은 코드에서만 일어나지 않습니다: 문제는 작성된 요구사항 문서에도 존재합니다. 그리고 그것은 바이러스처럼 한 곳에서 다른 곳으로 퍼질 수 있습니다. 코드의 오류가 작성된 설명의 오류를 보상합니다.
그것은 사람들에게도 퍼질 수 있습니다: 사용자들은 애플리케이션이 왼쪽이라고 말할 때 오른쪽을 의미한다는 것을 배우므로, 그에 따라 그들의 행동을 조정합니다. 그들은 심지어 그것을 새로운 사용자들에게도 전달합니다: "그 애플리케이션이 왼쪽 버튼을 클릭하라고 말할 때 실제로는 오른쪽 버튼을 의미한다는 것을 기억하세요." 버그를 고치면 갑자기 사용자들이 재교육을 받아야 합니다.
단일 잘못들은 발견하기 쉽고 고치기 쉬울 수 있습니다. 여러 원인을 가지고, 여러 변경을 필요로 하는 문제들이 해결하기 더 어렵습니다. 부분적으로는 쉬운 문제들이 너무 쉽게 고쳐져서 사람들이 상대적으로 빠르게 그것들을 고치고 더 어려운 문제들을 나중으로 미루는 경향이 있기 때문입니다.
공감적 결함들에서 발생하는 장애들을 어떻게 다룰지에 대한 간단한 조언을 주기는 어렵습니다. 가능성에 대한 인식, 명확한 머리, 그리고 모든 가능성을 고려하려는 의지가 필요합니다.
Allan Kelly가 작성함.
너무 자주 우리는 고립된 상태에서 코드를 작성하고, 그 코드는 문제에 대한 우리의 개인적 해석과 매우 개인화된 해결책을 반영합니다. 우리는 팀의 일부일 수 있지만, 우리는 고립되어 있고, 팀도 마찬가지입니다. 우리는 고립 상태에서 만들어진 이 코드가 다른 사람들에 의해 실행되고, 사용되고, 확장되고, 의존될 것이라는 것을 너무 쉽게 잊습니다. 소프트웨어 생성의 사회적 측면을 간과하기 쉽습니다. 소프트웨어 만들기는 사회적 연습에 섞여 있는 기술적 연습입니다. 우리는 고립되어 일하지 않는다는 것을 깨닫기 위해 그저 더 자주 고개를 들어야 하고, 개발팀만이 아니라 모든 사람의 성공 확률을 높이는 것에 대한 공유된 책임을 갖고 있습니다.
한 관점에서, 그것은 자기중심적 접근법입니다(ego를 거만함이 아니라 개인적인 것으로서). 그것은 또한 선적 관점이고 그것은 코드를 만드는 그 순간에 당신에 관한 것입니다. 나는 항상 그 순간에 살려고 노력하는데 그것이 좋은 품질에 더 가까이 가는 데 도움이 되기 때문이지만, 그러면 나는 내 순간에 삽니다. 내 팀의 순간은 어떨까요? 내 순간이 팀의 순간과 같을까요?
줄루어에서, 우분투의 철학은 "Umuntu ngumuntu ngabantu"로 요약되는데, 이것은 대략 "사람은 (다른) 사람들을 통해 사람이다"로 번역됩니다. 나는 당신의 좋은 행동들을 통해 당신이 나를 더 좋게 만들기 때문에 더 좋아집니다. 뒤집어서는 내가 내가 하는 일에서 나쁠 때 당신이 당신이 하는 일에서 더 나빠진다는 것입니다. 개발자들 사이에서, 우리는 그것을 "개발자는 (다른) 개발자들을 통해 개발자다"로 줄일 수 있습니다. 그것을 메탈까지 내려가면, "코드는 (다른) 코드를 통해 코드다"입니다.
내가 작성하는 코드의 품질이 당신이 작성하는 코드의 품질에 영향을 미칩니다. 내 코드가 품질이 낮다면 어떨까요? 당신이 매우 깨끗한 코드를 작성하더라도, 당신이 내 코드를 사용하는 지점들에서 당신의 코드 품질이 내 코드의 품질에 가깝게 저하될 것입니다. 당신은 손상을 제한하기 위해 많은 패턴과 기법들을 적용할 수 있지만, 손상은 이미 발생했습니다. 나는 내 순간에 살면서 당신에 대해 생각하지 않았기 때문에 당신이 필요한 것보다 더 많은 일을 하게 만들었습니다.
나는 내 코드가 깨끗하다고 생각할 수 있지만, 우분투 코딩을 함으로써 여전히 그것을 더 좋게 만들 수 있습니다. 우분투 코드는 어떻게 보일까요? 그것은 좋은 깨끗한 코드처럼 보입니다. 그것은 코드, 산출물에 관한 것이 아닙니다. 그것은 그 산출물을 만드는 행위에 관한 것입니다. 우분투로 친구들을 위해 코딩하는 것은 당신의 팀이 당신의 가치를 살고 당신의 원칙을 강화하는 데 도움이 될 것입니다. 어떤 방식으로든 당신의 코드를 건드리는 다음 사람은 더 좋은 사람이고 더 좋은 개발자가 될 것입니다.
선은 개인에 관한 것입니다. 우분투는 사람들의 그룹을 위한 선에 관한 것입니다. 매우, 매우 드물게 우리는 우리 자신만을 위한 코드를 만듭니다.
Aslam Khan이 작성함.
만약 무인도로 유배를 가는 길에 IDE와 Unix 도구상자 중 하나를 선택해야 한다면, 나는 한 번의 망설임도 없이 Unix 도구들을 택할 것입니다. 당신이 Unix 도구들에 능숙해져야 하는 이유들은 다음과 같습니다.
첫째, IDE들은 특정 언어들을 목표로 하는 반면, Unix 도구들은 텍스트 형태로 나타나는 모든 것과 함께 작동할 수 있습니다. 새로운 언어들과 표기법들이 매년 생겨나는 오늘날의 개발 환경에서, Unix 방식으로 일하는 법을 배우는 것은 계속해서 보상을 받을 투자입니다.
게다가 IDE들이 개발자들이 생각해낸 명령들만 제공하는 반면, Unix 도구들로는 상상할 수 있는 어떤 작업이든 수행할 수 있습니다. 그것들을 (바이오니클 이전의 클래식) 레고 블록으로 생각하세요: 작지만 다재다능한 Unix 도구들을 결합함으로써 단순히 자신만의 명령들을 만들어냅니다. 예를 들어, 다음 시퀀스는 각 파일의 세미콜론, 중괄호, 그리고 따옴표의 시퀀스인 커닝햄의 시그니처 분석의 텍스트 기반 구현으로, 파일의 내용에 대해 많은 것을 드러낼 수 있습니다.
for i in *.java; do
echo -n "$i: "
sed 's/[^"{};]//g' $i | tr -d '\n'
echo
done
게다가 당신이 배우는 각 IDE 작업은 주어진 그 작업에 특정적입니다; 예를 들어, 프로젝트의 디버그 빌드 구성에 새로운 단계를 추가하는 것. 대조적으로, Unix 도구 기술을 연마하는 것은 어떤 작업에서든 당신을 더 효과적으로 만듭니다. 예시로, 나는 앞의 명령 시퀀스에서 사용된 sed 도구를 여러 프로세서 아키텍처에서 크로스 컴파일링을 위해 프로젝트의 빌드를 변형하는 데 사용했습니다.
Unix 도구들은 멀티유저 컴퓨터가 128kB의 RAM을 가지고 있던 시대에 개발되었습니다. 그들의 설계에 들어간 독창성은 오늘날 그들이 거대한 데이터 세트들을 극도로 효율적으로 처리할 수 있다는 것을 의미합니다. 대부분의 도구들은 필터처럼 작동하여, 한 번에 단일 줄만 처리하므로, 그들이 처리할 수 있는 데이터 양에는 상한이 없습니다. 0.5테라바이트의 영어 위키피디아 덤프에 저장된 편집 수를 검색하고 싶나요? 간단한 호출
grep '<revision>' | wc –l
이 땀 한 방울 흘리지 않고 답을 줄 것입니다. 명령 시퀀스가 일반적으로 유용하다고 발견하면, 데이터를 루프와 조건문으로 파이핑하는 것 같은 독특하게 강력한 프로그래밍 구조들을 사용하여 그것을 셸 스크립트로 쉽게 패키징할 수 있습니다. 더욱 인상적으로, 앞의 것처럼 파이프라인으로 실행되는 Unix 명령들은 자연스럽게 최신 멀티코어 CPU의 많은 처리 단위들 사이에 그들의 부하를 분산시킬 것입니다.
Unix 도구들의 작은-것이-아름답다 유래와 오픈 소스 구현들은 그것들을 내 셋톱 미디어 플레이어나 DSL 라우터 같은 자원 제약 플랫폼에서도 어디서나 사용 가능하게 만듭니다. 그런 장치들은 강력한 그래픽 사용자 인터페이스를 제공할 가능성은 낮지만, 가장 일반적으로 사용되는 도구들을 제공하는 BusyBox 애플리케이션을 종종 포함합니다. 그리고 Windows에서 개발하고 있다면, Cygwin 환경이 실행 파일과 소스 코드 형태 모두로 상상할 수 있는 모든 Unix 도구를 제공합니다.
마지막으로, 사용 가능한 도구들 중 어느 것도 당신의 필요에 맞지 않는다면, Unix 도구들의 세계를 확장하는 것은 매우 쉽습니다. 몇 가지 간단한 규칙을 따르는 프로그램을 (당신이 좋아하는 어떤 언어로든) 작성하기만 하면 됩니다: 당신의 프로그램은 단일 작업만 수행해야 합니다; 표준 입력에서 텍스트 줄로 데이터를 읽어야 합니다; 그리고 표준 출력에 헤더와 다른 노이즈로 장식되지 않은 결과를 표시해야 합니다. 도구의 작동에 영향을 미치는 매개변수들은 명령줄에서 주어집니다. 이 규칙들을 따르면 "지구와 그 안의 모든 것이 당신의 것입니다."
Diomidis Spinellis가 작성함.
많은 지점을 가진 큰 은행이 창구 직원들을 위해 산 새 컴퓨터들이 너무 느리다고 불평했습니다. 이것은 모든 사람이 전자 뱅킹을 사용하기 전이고 ATM이 지금처럼 널리 퍼지지 않았던 시절이었습니다. 사람들은 은행을 훨씬 더 자주 방문했고, 느린 컴퓨터들이 사람들을 줄 서게 만들고 있었습니다. 결과적으로, 은행은 공급업체와의 계약을 파기하겠다고 위협했습니다.
공급업체는 지연의 원인을 알아내기 위해 성능 분석 및 튜닝 전문가를 보냈습니다. 그는 곧 터미널에서 실행되는 특정 프로그램 하나가 거의 모든 CPU 용량을 소모하고 있다는 것을 발견했습니다. 프로파일링 도구를 사용하여, 그는 프로그램을 확대해서 보았고 원인이 되는 함수를 볼 수 있었습니다. 소스 코드는 다음과 같았습니다:
for (i=0; i<strlen(s); ++i) { if (... s[i] ...) ... }
그리고 문자열 s는 평균적으로 수천 글자 길이였습니다. (은행이 작성한) 코드는 빠르게 변경되었고, 은행 창구 직원들은 그 후로 행복하게 살았습니다....
프로그래머가 불필요하게 제곱으로 확장되는 코드를 사용하는 것보다 더 나아야 하지 않았을까요? strlen에 대한 각 호출은 종료하는 null 문자를 찾기 위해 문자열의 수천 글자 각각을 순회했습니다. 하지만 문자열은 결코 변경되지 않았습니다. 미리 길이를 결정함으로써, 프로그래머는 수천 번의 strlen 호출(그리고 수백만 번의 루프 실행)을 절약할 수 있었을 것입니다:
n=strlen(s);
for (i=0; i<n; ++i) {
if (... s[i] ...) ...
}
모든 사람이 마이크로 최적화의 함정을 피하기 위해 "먼저 작동하게 만들고, 그다음 빠르게 작동하게 만들라"는 격언을 알고 있습니다. 하지만 위의 예시는 프로그래머가 마키아벨리식 아다지오 "먼저 느리게 작동하게 만들라"를 따랐다고 거의 믿게 만들 것입니다.
이런 무사려함은 당신이 한 번 이상 마주칠 수 있는 것입니다. 그리고 이것은 단순히 "바퀴를 재발명하지 마라"는 것이 아닙니다. 때로는 초보 프로그래머들이 실제로 생각하지 않고 그냥 타이핑을 시작해서 갑자기 버블 정렬을 '발명'했습니다. 그들은 심지어 그것에 대해 자랑할 수도 있습니다.
올바른 알고리즘을 선택하는 것의 다른 면은 데이터 구조의 선택입니다. 큰 차이를 만들 수 있습니다: 당신이 검색하고 싶어하는 백만 개 항목의 컬렉션에 대해 연결 리스트를 사용하는 것 — 해시된 데이터 구조나 이진 트리와 비교해서 — 은 당신의 프로그래밍에 대한 사용자의 평가에 큰 영향을 미칠 것입니다.
프로그래머들은 바퀴를 재발명해서는 안 되고, 가능한 곳에서 기존 라이브러리를 사용해야 합니다. 하지만 은행과 같은 문제를 피할 수 있으려면, 그들은 또한 알고리즘과 그것들이 어떻게 확장되는지에 대해 교육받아야 합니다. 최신 텍스트 에디터들이 1980년대 워드스타 같은 구식 프로그램들만큼 느리게 만드는 것이 단지 화려한 장식일까요? 많은 사람들이 프로그래밍에서 재사용이 가장 중요하다고 말합니다. 하지만 무엇보다도, 프로그래머들은 언제, 무엇을, 그리고 어떻게 재사용할지 알아야 합니다. 그것을 할 수 있으려면 그들은 문제 영역과 알고리즘 및 데이터 구조에 대한 지식을 가져야 합니다.
좋은 프로그래머는 또한 언제 혐오스러운 알고리즘을 사용해야 하는지 알아야 합니다. 예를 들어, 문제 영역이 결코 5개 이상의 항목이 있을 수 없다고 지시한다면(야치 게임의 주사위 수처럼), 당신은 항상 최대 5개의 항목을 정렬해야 한다는 것을 알고 있습니다. 그 경우, 버블 정렬이 실제로 항목들을 정렬하는 가장 효율적인 방법일 수 있습니다. 모든 개가 자신의 날이 있습니다.
그러므로, 좋은 책들을 읽으세요 — 그리고 그것들을 이해하도록 하세요. 그리고 정말로 도널드 크누스의 컴퓨터 프로그래밍의 예술을 잘 읽는다면, 운이 좋을지도 모릅니다: 저자의 실수를 찾고 돈 크누스의 16진법 달러($2.56) 수표 중 하나를 받으세요.
JC van Winkel이 작성함.
이미 한동안 개발 중이거나 프로덕션 중인 시스템을 마주칠 때, 실제 문제의 첫 번째 징후는 항상 더러운 로그입니다. 내가 무엇을 말하는지 알 것입니다. 웹 페이지의 정상적인 흐름에서 단일 링크를 클릭하는 것이 시스템이 제공하는 유일한 로그에서 메시지의 홍수로 이어질 때. 너무 많은 로깅은 전혀 없는 것만큼 쓸모없을 수 있습니다.
당신의 시스템들이 내 것과 같다면, 당신의 일이 끝날 때 다른 누군가의 일이 막 시작됩니다. 시스템이 개발된 후, 그것은 바라건대 고객들을 서비스하며 길고 번영하는 삶을 살 것입니다. 운이 좋다면 말입니다. 시스템이 프로덕션에 있을 때 무언가 잘못되면 어떻게 알 것이고, 그것을 어떻게 다룰 것입니까?
아마도 누군가가 당신의 시스템을 모니터링하거나, 아니면 당신이 직접 모니터링할 것입니다. 어느 쪽이든, 로그들은 아마도 모니터링의 일부가 될 것입니다. 무언가가 나타나서 당신이 그것을 처리하기 위해 깨워져야 한다면, 그것에 대한 좋은 이유가 있는지 확인하고 싶을 것입니다. 내 시스템이 죽고 있다면, 알고 싶습니다. 하지만 단지 딸꾹질이라면, 차라리 미용 수면을 즐기고 싶습니다.
많은 시스템에서, 무언가 잘못되었다는 첫 번째 징후는 어떤 로그에 로그 메시지가 기록되는 것입니다. 대부분, 이것은 오류 로그가 될 것입니다. 그러므로 자신에게 호의를 베푸세요: 첫날부터 오류 로그에 무언가가 기록되면, 한밤중에 누군가가 그것에 대해 전화해서 당신을 깨우는 것을 기꺼이 받아들일지 확인하세요. 시스템 테스트 동안 시스템에 부하를 시뮬레이션할 수 있다면, 노이즈 없는 오류 로그를 보는 것도 당신의 시스템이 합리적으로 견고하다는 좋은 첫 번째 징후입니다. 또는 그렇지 않다면 조기 경고입니다.
분산 시스템들은 또 다른 수준의 복잡성을 추가합니다. 외부 의존성이 실패할 때 어떻게 처리할지 결정해야 합니다. 시스템이 매우 분산되어 있다면, 이것은 일반적인 발생일 수 있습니다. 로깅 정책이 이것을 고려하도록 하세요.
일반적으로, 모든 것이 괜찮다는 최고의 징후는 더 낮은 우선순위의 메시지들이 행복하게 돌아가고 있다는 것입니다. 모든 중요한 애플리케이션 이벤트에 대해 대략 하나의 INFO 수준 로그 메시지를 원합니다.
어수선한 로그는 시스템이 프로덕션에 도달하면 제어하기 어려울 것이라는 징후입니다. 오류 로그에 아무것도 나타나지 않을 것으로 예상하지 않는다면, 무언가가 나타날 때 무엇을 해야 할지 아는 것이 훨씬 쉬울 것입니다.
Johannes Brodwall이 작성함.
DRY 원칙(Don't Repeat Yourself)의 중요성은 시스템의 모든 지식 조각이 단일 표현을 가져야 한다는 아이디어를 체계화한다는 것입니다. 다시 말해서, 지식은 단일 구현에 포함되어야 합니다. DRY의 정반대는 WET(Write Every Time)입니다. 지식이 여러 다른 구현에서 체계화될 때 우리의 코드는 WET입니다. DRY 대 WET의 성능 함의는 성능 프로필에 대한 그들의 수많은 효과를 고려할 때 매우 명확해집니다.
우리 시스템의 기능 X가 CPU 병목 지점이라고 가정하는 것부터 시작해봅시다. 기능 X가 CPU의 30%를 소모한다고 합시다. 이제 기능 X가 열 가지 다른 구현을 가지고 있다고 합시다. 평균적으로, 각 구현은 CPU의 3%를 소모할 것입니다. 빠른 성과를 찾고 있다면 이 수준의 CPU 사용률은 걱정할 가치가 없기 때문에, 우리는 이 기능이 우리의 병목 지점이라는 것을 놓칠 가능성이 높습니다. 하지만 우리가 어떻게든 기능 X를 병목 지점으로 인식했다고 합시다. 우리는 이제 모든 단일 구현을 찾고 고치는 문제에 직면했습니다. WET로는 우리가 찾고 고쳐야 할 열 가지 다른 구현이 있습니다. DRY로는 30% CPU 사용률을 명확히 보고 고칠 코드가 십분의 일이 있을 것입니다. 그리고 각 구현을 찾아 헤매는 시간을 보낼 필요가 없다는 것을 언급했나요?
우리가 종종 DRY 위반의 죄를 짓는 한 가지 사용 사례가 있습니다: 컬렉션의 사용. 쿼리를 구현하는 일반적인 기법은 컬렉션을 반복하고 나서 각 요소에 차례로 쿼리를 적용하는 것입니다:
public class UsageExample {
private ArrayList<Customer> allCustomers = new ArrayList<Customer>();
// ...
public ArrayList<Customer> findCustomersThatSpendAtLeast(Money amount) {
ArrayList<Customer> customersOfInterest = new ArrayList<Customer>();
for (Customer customer: allCustomers) {
if (customer.spendsAtLeast(amount))
customersOfInterest.add(customer);
}
return customersOfInterest;
}
}
이 원시 컬렉션을 클라이언트들에게 노출함으로써, 우리는 캡슐화를 위반했습니다. 이것은 리팩터링할 우리의 능력을 제한할 뿐만 아니라, 우리 코드의 사용자들로 하여금 각자가 잠재적으로 같은 쿼리를 다시 구현하게 함으로써 DRY를 위반하도록 강요합니다. 이 상황은 API에서 노출된 원시 컬렉션들을 제거함으로써 쉽게 피할 수 있습니다. 이 예시에서 우리는 CustomerList라고 불리는 새로운, 도메인별 집합 타입을 도입할 수 있습니다. 이 새로운 클래스는 우리 도메인과 의미적으로 더 일치합니다. 그것은 우리의 모든 쿼리들을 위한 자연스러운 홈 역할을 할 것입니다.
이 새로운 컬렉션 타입을 갖는 것은 또한 이런 쿼리들이 성능 병목 지점인지 쉽게 볼 수 있게 해줄 것입니다. 쿼리들을 클래스에 통합함으로써 우리는 ArrayList와 같은 표현 선택들을 클라이언트들에게 노출할 필요를 제거합니다. 이것은 클라이언트 계약을 위반할 두려움 없이 이런 구현들을 변경할 자유를 줍니다:
public class CustomerList {
private ArrayList<Customer> customers = new ArrayList<Customer>();
private SortedList<Customer> customersSortedBySpendingLevel = new SortedList<Customer>();
// ...
public CustomerList findCustomersThatSpendAtLeast(Money amount) {
return new CustomerList(customersSortedBySpendingLevel.elementsLargerThan(amount));
}
}
public class UsageExample {
public static void main(String[] args) {
CustomerList customers = new CustomerList();
// ...
CustomerList customersOfInterest = customers.findCustomersThatSpendAtLeast(someMinimalAmount);
// ...
}
}
이 예시에서, DRY 준수는 우리가 고객들의 지출 수준을 키로 하는 SortedList로 대안적인 인덱싱 스키마를 도입할 수 있게 해줬습니다. 이 특별한 예시의 구체적인 세부사항보다 더 중요한 것은, DRY를 따르는 것이 코드가 WET였다면 찾기 더 어려웠을 성능 병목 지점을 찾고 수리하는 데 도움이 되었다는 것입니다.
Kirk Pepperdine이 작성함.
테스터와 프로그래머가 협력하기 시작할 때 마법 같은 일이 일어납니다. 결함 추적 시스템을 통해 버그를 주고받는 데 소비되는 시간이 줄어듭니다. 무언가가 정말로 버그인지 새로운 기능인지 알아내려고 노력하는 데 낭비되는 시간이 줄어들고, 고객 기대를 충족하는 좋은 소프트웨어를 개발하는 데 더 많은 시간이 소비됩니다. 코딩이 시작되기 전에도 협력을 시작할 많은 기회들이 있습니다.
테스터들은 Fit(Framework for Integrated Test) 같은 도구를 사용하여 고객들이 그들의 도메인 언어로 수락 테스트를 작성하고 자동화하는 것을 도울 수 있습니다. 이런 테스트들이 코딩이 시작되기 전에 프로그래머들에게 주어지면, 팀은 수락 테스트 주도 개발(ATDD)을 실천하고 있는 것입니다. 프로그래머들은 테스트를 실행하기 위한 픽스처를 작성하고, 그다음 테스트를 통과시키기 위한 코드를 작성합니다. 이런 테스트들은 그 후 회귀 스위트의 일부가 됩니다. 이런 협력이 발생할 때, 기능 테스트들이 일찍 완료되어 더 큰 그림의 엣지 조건이나 워크플로우에 대한 탐색적 테스트를 위한 시간을 허용합니다.
한 걸음 더 나아갈 수 있습니다. 테스터로서, 나는 프로그래머들이 새로운 기능을 코딩하기 시작하기 전에 내 테스트 아이디어들의 대부분을 제공할 수 있습니다. 프로그래머들에게 제안이 있는지 물어볼 때, 그들은 거의 항상 더 나은 테스트 커버리지를 도와주거나 불필요한 테스트에 많은 시간을 소비하는 것을 피하는 데 도움이 되는 정보를 제공합니다. 종종 테스트들이 초기 아이디어들의 많은 것을 명확히 하기 때문에 우리는 결함들을 방지했습니다. 예를 들어, 내가 참여한 한 프로젝트에서, 내가 프로그래머들에게 준 Fit 테스트들은 와일드카드 검색에 응답하기 위한 쿼리의 예상 결과를 보여줬습니다. 프로그래머는 완전한 단어 검색만을 코딩할 완전한 의도를 가지고 있었습니다. 우리는 고객과 이야기하여 코딩이 시작되기 전에 올바른 해석을 결정할 수 있었습니다. 협력함으로써, 우리는 결함을 방지했고, 이것은 우리 둘 모두에게 많은 낭비된 시간을 절약해 주었습니다.
프로그래머들은 또한 성공적인 자동화를 만들기 위해 테스터들과 협력할 수 있습니다. 그들은 좋은 코딩 관행을 이해하고 전체 팀을 위해 작동하는 견고한 테스트 자동화 스위트를 설정하는 데 테스터들을 도울 수 있습니다. 나는 테스트들이 잘못 설계되어서 테스트 자동화 프로젝트들이 실패하는 것을 종종 봤습니다. 테스트들이 너무 많은 것을 테스트하려고 하거나 테스터들이 테스트들을 독립적으로 유지할 수 있을 만큼 기술에 대해 충분히 이해하지 못했습니다. 테스터들은 종종 병목 지점이므로, 프로그래머들이 자동화 같은 작업에서 그들과 함께 일하는 것이 이치에 맞습니다. 아마도 간단한 도구를 제공함으로써 무엇이 일찍 테스트될 수 있는지 이해하기 위해 테스터들과 함께 일하는 것은 프로그래머들에게 또 다른 피드백 사이클을 줄 것이고, 이것은 그들이 장기적으로 더 나은 코드를 전달하는 데 도움이 될 것입니다.
테스터들이 자신들의 유일한 일이 소프트웨어를 망가뜨리고 프로그래머들의 코드에서 버그를 찾는 것이라고 생각하는 것을 멈출 때, 프로그래머들은 테스터들이 '자신들을 잡으려고 한다'고 생각하는 것을 멈추고, 협력에 더 열려 있게 됩니다. 프로그래머들이 자신들이 코드에 품질을 구축하는 것에 책임이 있다는 것을 깨닫기 시작할 때, 코드의 테스트 가능성은 자연스러운 부산물이고, 팀은 더 많은 회귀 테스트들을 함께 자동화할 수 있습니다. 성공적인 팀워크의 마법이 시작됩니다.
Janet Gregory가 작성함.
평생 유지보수해야 한다고 생각하며 코드를 작성하세요1
프로그래머가 뭘 알아야 하고 뭘 해야 하는지 97명에게 물어보면, 97가지 서로 다른 답변을 들을 것입니다. 이는 압도적이고 위압적으로 느껴질 수 있습니다. 모든 조언은 좋고, 모든 원칙은 타당하며, 모든 이야기는 설득력이 있지만, 어디서 시작해야 할까요? 더 중요하게는, 시작한 후에는, 어떻게 배운 모든 베스트 프랙티스들을 계속 유지하고, 당신의 프로그래밍 프랙티스의 중요한 부분으로 만들 수 있을까요?
그 해결책은 당신의 사고 방식, 또는 간단히 말해, 당신의 태도에 있다고 생각합니다. 동료 개발자, 테스터, 관리자, 영업 및 마케팅 담당자, 최종 사용자에 대해 신경 쓰지 않는다면, 예를 들어, 테스트 주도 개발(Test-Driven Development)을 구현하거나 코드에 명확한 주석을 달려는 동기가 생기지 않을 것입니다. 제가 생각하는 간단한 방법이 있습니다:
평생 유지보수해야 한다고 생각하며 코드를 작성하세요.
그것으로 충분합니다. 이 개념을 받아들이면 많은 놀라운 일이 발생할 것입니다. 만약 당신의 이전 또는 현재 고용주가 한밤중에 당신에게 전화를 걸어 fooBar 메서드를 작성할 때의 선택에 대해 설명해 달라고 요청할 권리가 있다고 믿는다면, 당신은 점차 전문가 프로그래머가 되는 방향으로 발전할 것입니다. 자연스럽게 더 나은 변수명과 메서드명을 고민하게 될 것입니다. 수백 줄의 코드 블록은 피하게 될 것이고, 디자인 패턴을 찾고 배우며 사용할 것입니다. 주석을 작성하고, 코드를 테스트하고, 지속적으로 리팩터링할 것입니다. 평생 동안 작성한 모든 코드를 유지보수하는 것은 확장 가능한 작업이어야 하므로, 더 나은, 더 똑똑하며, 더 효율적으로 진행할 수밖에 없습니다.
가만히 돌아보면, 당신이 몇 년 전에 작성한 코드는 좋든 싫든 당신의 경력에 여전히 영향을 미치고 있습니다. 당신이 설계하고 작성하는 각 메서드, 클래스, 모듈에는 당신의 지식, 태도, 끈기, 전문성, 헌신 수준, 그리고 즐거움의 정도가 남아 있습니다. 사람들은 그들이 보는 코드를 바탕으로 당신에 대한 의견을 형성할 것입니다. 만약 그 의견이 부정적이라면, 당신은 경력에서 기대했던 것보다 더 적은 것을 얻게 될 것입니다. 모든 코드 줄마다 자신의 경력, 고객, 사용자에게 신경 쓰세요 — 평생 유지보수해야 한다고 생각하며 코딩하세요.
우리는 올바른 코드를 작성하고 그것이 올바르다는 증거를 손에 가지고 있고 싶어합니다. 함수의 "크기"에 대해 생각하는 것이 두 문제 모두에 도움이 될 수 있습니다. 함수를 구현하는 코드의 양이라는 의미에서가 아니라 — 비록 그것도 흥미롭긴 하지만 — 오히려 우리 코드가 나타내는 수학적 함수의 크기 말입니다.
예를 들어, 바둑 게임에서 플레이어의 돌이 상대방에 의해 잡힐 수 있는 아타리라고 불리는 조건이 있습니다: 인접한 두 개 이상의 자유 공간(활로라고 불림)을 가진 돌은 아타리에 있지 않습니다. 돌이 얼마나 많은 활로를 가지고 있는지 세는 것은 까다로울 수 있지만, 그것이 알려져 있다면 아타리를 결정하는 것은 쉽습니다. 우리는 다음과 같은 함수를 작성하는 것으로 시작할 수 있습니다:
boolean atari(int libertyCount)
libertyCount < 2
이것은 보이는 것보다 더 큽니다. 수학적 함수는 집합으로 이해될 수 있는데, 그것의 정의역(여기서는 int)과 치역(여기서는 boolean)인 집합들의 데카르트 곱의 어떤 부분집합입니다. 그 값들의 집합들이 Java에서와 같은 크기라면 집합 int×boolean에 2L*(Integer.MAX_VALUE+(-1L*Integer.MIN_VALUE)+1L) 또는 8,589,934,592개의 구성원이 있을 것입니다. 이것들의 절반이 우리 함수인 부분집합의 구성원이므로, 우리 함수가 올바르다는 완전한 증거를 제공하려면 대략 4.3×109개의 예시를 확인해야 할 것입니다.
이것이 테스트가 버그의 부재를 증명할 수 없다는 주장의 본질입니다. 하지만 테스트는 기능의 존재를 보여줄 수 있습니다. 그러나 여전히 크기의 문제가 있습니다.
문제 영역이 우리를 도와줍니다. 바둑의 특성상 돌의 활로 수는 어떤 int가 아니라 정확히 {1,2,3,4} 중 하나입니다. 그래서 우리는 대안적으로 다음과 같이 작성할 수 있습니다:
LibertyCount = {1,2,3,4}
boolean atari(LibertyCount libertyCount)
libertyCount == 1
이것은 훨씬 더 다루기 쉽습니다: 계산된 함수는 이제 최대 8개의 구성원을 가진 집합입니다. 사실, 4개의 확인된 예시가 함수가 올바르다는 완전한 확실성의 증거를 구성할 것입니다. 이것이 네이티브 타입보다는 문제 영역과 밀접하게 관련된 타입을 사용하여 프로그램을 작성하는 것이 좋은 아이디어인 한 가지 이유입니다. 도메인에서 영감을 받은 타입을 사용하는 것은 종종 우리의 함수들을 훨씬 더 작게 만들 수 있습니다. 그런 타입들이 무엇이어야 하는지 알아내는 한 가지 방법은 함수를 작성하기 전에 문제 영역 용어로 확인할 예시들을 찾는 것입니다.
Keith Braithwaite가 작성함.
당신은 프로덕션 코드의 일부 또는 전부를 위한 자동화된 테스트를 작성하고 있습니다. 축하합니다! 코드를 작성하기 전에 테스트를 작성하고 있나요? 더욱 좋습니다!! 이것만 해도 당신을 소프트웨어 엔지니어링 실천의 최첨단에 있는 얼리 어답터 중 하나로 만듭니다. 하지만 좋은 테스트를 작성하고 있나요? 어떻게 알 수 있을까요? 한 가지 방법은 "내가 누구를 위해 테스트를 작성하고 있는가?"라고 묻는 것입니다. 대답이 "나를 위해, 버그를 고치는 노력을 절약하기 위해" 또는 "컴파일러를 위해, 그래서 실행될 수 있도록"이라면 최선의 테스트를 작성하지 않을 가능성이 높습니다. 그러면 누구를 위해 테스트를 작성해야 할까요? 당신의 코드를 이해하려고 하는 사람을 위해서입니다.
좋은 테스트는 그들이 테스트하는 코드에 대한 문서 역할을 합니다. 그들은 코드가 어떻게 작동하는지 설명합니다. 각 사용 시나리오에 대해 테스트는:
- 만족되어야 하는 컨텍스트, 시작점, 또는 사전조건을 설명합니다
- 소프트웨어가 어떻게 호출되는지 보여줍니다
- 검증될 예상 결과나 사후조건을 설명합니다
다른 사용 시나리오들은 이것들 각각에 대해 약간 다른 버전을 가질 것입니다. 당신의 코드를 이해하려고 하는 사람은 몇 가지 테스트를 보고 문제의 테스트들의 이 세 부분을 비교함으로써, 무엇이 소프트웨어를 다르게 행동하게 만드는지 볼 수 있어야 합니다. 각 테스트는 이 세 부분 사이의 원인과 결과 관계를 명확하게 보여줘야 합니다. 이것은 테스트에서 보이지 않는 것이 보이는 것만큼 중요하다는 것을 의미합니다. 테스트의 너무 많은 코드는 중요하지 않은 사소한 것들로 읽는 사람의 주의를 산만하게 합니다. 가능할 때마다 그런 사소한 것들을 의미 있는 메서드 호출 뒤에 숨기세요 — Extract Method 리팩토링이 당신의 가장 친한 친구입니다. 그리고 각 테스트에 특정 사용 시나리오를 설명하는 의미 있는 이름을 주어서 테스트 읽는 사람이 다양한 시나리오들이 무엇인지 이해하기 위해 각 테스트를 역공학할 필요가 없도록 하세요. 테스트 클래스와 클래스 메서드의 이름들은 함께 적어도 시작점과 소프트웨어가 어떻게 호출되는지를 포함해야 합니다. 이것은 메서드 이름들의 빠른 스캔을 통해 테스트 커버리지가 검증될 수 있게 해줍니다. 이름들이 보거나 읽기에 너무 길어지지 않는 한, 테스트 메서드 이름에 예상 결과를 포함하는 것도 유용할 수 있습니다.
당신의 테스트를 테스트하는 것도 좋은 아이디어입니다. 당신이 생각하는 오류들을 프로덕션 코드에 삽입함으로써(물론 당신이 버릴 자신만의 개인 복사본에) 그들이 당신이 생각하는 오류들을 감지하는지 확인할 수 있습니다. 그들이 도움이 되고 의미 있는 방식으로 오류를 보고하는지 확인하세요. 또한 당신의 테스트들이 당신의 코드를 이해하려고 하는 사람에게 명확하게 말하는지 확인해야 합니다. 이것을 하는 유일한 방법은 당신의 코드에 익숙하지 않은 누군가가 당신의 테스트를 읽고 무엇을 배웠는지 말하게 하는 것입니다. 그들이 말하는 것을 주의 깊게 들으세요. 그들이 무언가를 명확하게 이해하지 못했다면 아마도 그들이 그리 밝지 않아서가 아닐 것입니다. 당신이 그리 명확하지 않았을 가능성이 더 높습니다. (계속해서 그들의 테스트를 읽음으로써 역할을 바꿔보세요!)
Gerard Meszaros가 작성함.
좋은 프로그래머가 좋은 코드를 작성한다는 것을 알아내는 데 셜록 홈즈가 필요하지 않습니다. 나쁜 프로그래머들은... 그렇지 않습니다. 그들은 나머지 우리가 치워야 하는 괴물들을 만들어냅니다. 좋은 것을 작성하고 싶으시죠? 좋은 프로그래머가 되고 싶으시죠?
좋은 코드는 허공에서 튀어나오지 않습니다. 행성들이 정렬될 때 운으로 일어나는 일이 아닙니다. 좋은 코드를 얻으려면 그것을 위해 노력해야 합니다. 열심히. 그리고 실제로 좋은 코드에 대해 신경쓸 때만 좋은 코드를 얻을 수 있습니다.
좋은 프로그래밍은 단순한 기술적 역량에서 태어나지 않습니다. 나는 강렬하고 인상적인 알고리즘을 만들어낼 수 있고, 언어 표준을 속속들이 알고 있지만, 가장 끔찍한 코드를 작성하는 고도로 지적인 프로그래머들을 봤습니다. 읽기 고통스럽고, 사용하기 고통스럽고, 수정하기 고통스럽습니다. 나는 매우 간단한 코드를 고수하지만, 함께 일하기 즐거운 우아하고 표현력 있는 프로그램을 작성하는 더 겸손한 프로그래머들을 봤습니다.
소프트웨어 공장에서의 수년간의 경험을 바탕으로, 나는 적절한 프로그래머와 훌륭한 프로그래머 사이의 진짜 차이는 이것이라고 결론지었습니다: 태도. 좋은 프로그래밍은 전문적인 접근법을 취하고, 소프트웨어 공장의 현실 세계 제약과 압력 안에서 당신이 할 수 있는 최고의 소프트웨어를 작성하고 싶어하는 데 있습니다.
지옥으로 가는 코드는 좋은 의도로 포장되어 있습니다. 훌륭한 프로그래머가 되려면 좋은 의도를 넘어서서, 실제로 코드에 대해 신경써야 합니다 — 긍정적인 관점을 기르고 건강한 태도를 개발하세요. 훌륭한 코드는 엉성한 프로그래머들에 의해 무심하게 해킹되거나 자칭 코딩 구루들에 의해 신비롭게 세워지는 것이 아니라 숙련된 장인들에 의해 신중하게 만들어집니다.
좋은 코드를 작성하고 싶습니다. 좋은 프로그래머가 되고 싶습니다. 그러므로, 코드에 대해 신경씁니다:
- 어떤 코딩 상황에서든, 작동하는 것처럼 보이기만 하는 무언가를 해킹하는 것을 거부합니다. 명확히 올바른(그리고 그것이 올바르다는 것을 보여주는 좋은 테스트를 가진) 우아한 코드를 만들어내려고 노력합니다.
- 발견 가능한(다른 프로그래머들이 쉽게 집어들고 이해할 수 있는), 유지보수 가능한(당신이나 다른 프로그래머들이 미래에 쉽게 수정할 수 있을), 그리고 올바른(문제를 해결했지 그냥 프로그램이 작동하는 것처럼 보이게 만든 것이 아니라는 것을 결정하기 위해 가능한 모든 단계를 취하는) 코드를 작성합니다.
- 다른 프로그래머들과 잘 일합니다. 어떤 프로그래머도 섬이 아닙니다. 혼자 일하는 프로그래머는 거의 없습니다; 대부분은 회사 환경이나 오픈 소스 프로젝트에서 프로그래머들의 팀에서 일합니다. 다른 프로그래머들을 고려하고, 다른 사람들이 읽을 수 있는 코드를 구성합니다. 자신을 영리해 보이게 만들기보다는 팀이 가능한 최고의 소프트웨어를 작성하기를 원합니다.
- 코드 조각을 건드릴 때마다 당신이 발견한 것보다 더 낫게(더 잘 구조화되거나, 더 잘 테스트되거나, 더 이해하기 쉽게...) 남겨두려고 노력합니다.
- 코드와 프로그래밍에 대해 신경쓰므로, 지속적으로 새로운 언어, 관용구, 그리고 기법들을 배웁니다. 하지만 적절할 때만 그것들을 적용합니다.
다행히, 당신은 코드에 대해 신경쓰기 때문에 이 조언 모음을 읽고 있습니다. 그것이 당신의 흥미를 끕니다. 그것이 당신의 열정입니다. 프로그래밍을 즐기세요. 까다로운 문제들을 해결하기 위해 코드를 자르는 것을 즐기세요. 당신이 자랑스러워하는 소프트웨어를 만들어내세요.
Pete Goodliffe가 작성함.
그들이 원하는 것을 말하는 것을 너무나 기뻐하지 않는 고객을 아직 만난 적이 없습니다 — 보통 아주 자세하게. 문제는 고객들이 항상 전체 진실을 말하지 않는다는 것입니다. 그들은 일반적으로 거짓말을 하지 않지만, 개발자 말이 아닌 고객 말로 이야기합니다. 그들은 자신들의 용어와 자신들의 맥락을 사용합니다. 중요한 세부사항들을 빠뜨립니다. 당신이 그들처럼 그들의 회사에 20년 동안 있었다고 가정합니다. 이것은 많은 고객들이 실제로 처음부터 자신들이 무엇을 원하는지 모른다는 사실로 인해 더욱 복잡해집니다! 일부는 "큰 그림"을 파악할 수 있지만, 그들의 비전의 세부사항을 효과적으로 전달할 수 있는 경우는 드뭅니다. 다른 사람들은 완전한 비전에 대해서는 조금 부족할 수 있지만, 자신들이 원하지 않는 것은 압니다. 그러면, 자신들이 원하는 것에 대한 전체 진실을 말하지 않는 누군가에게 어떻게 소프트웨어 프로젝트를 전달할 수 있을까요? 꽤 간단합니다. 그냥 그들과 더 많이 상호작용하세요.
고객들에게 일찍 도전하고 자주 도전하세요. 단순히 그들의 말로 그들이 원한다고 말한 것을 되풀이하지 마세요. 기억하세요: 그들은 자신들이 말한 것을 의미하지 않았습니다. 나는 종종 그들과의 대화에서 단어들을 바꿔가며 그들의 반응을 판단함으로써 이것을 합니다. 고객이라는 용어가 클라이언트라는 용어와 완전히 다른 의미를 갖는 경우가 얼마나 많은지 놀랄 것입니다. 하지만 소프트웨어 프로젝트에서 자신이 원하는 것을 말하는 사람은 그 용어들을 서로 바꿔 사용하며 당신이 자신이 어떤 것에 대해 이야기하고 있는지 추적하기를 기대할 것입니다. 혼란스러워질 것이고 당신이 작성하는 소프트웨어가 고통받을 것입니다.
당신이 그들이 필요로 하는 것을 이해한다고 결정하기 전에 고객들과 주제들을 여러 번 논의하세요. 그들과 문제를 두세 번 다시 말해보려고 노력하세요. 더 나은 맥락을 얻기 위해 당신이 이야기하고 있는 주제 바로 전이나 바로 후에 일어나는 일들에 대해 그들과 이야기하세요. 가능하다면, 여러 사람이 별개의 대화에서 같은 주제에 대해 말하게 하세요. 그들은 거의 항상 다른 이야기를 할 것이고, 이것은 별개이지만 관련된 사실들을 밝혀낼 것입니다. 같은 주제에 대해 말하는 두 사람은 종종 서로 모순될 것입니다. 성공할 최고의 기회는 당신의 극도로 복잡한 소프트웨어 제작을 시작하기 전에 차이점들을 해결하는 것입니다.
대화에서 시각적 보조도구를 사용하세요. 이것은 회의에서 화이트보드를 사용하는 것처럼 간단할 수도 있고, 설계 단계 초기에 시각적 모형을 만드는 것처럼 쉬울 수도 있고, 기능적 프로토타입을 만드는 것처럼 복잡할 수도 있습니다. 대화 중에 시각적 보조도구를 사용하는 것이 우리의 주의 지속 시간을 늘리고 정보의 보존율을 증가시키는 데 도움이 된다는 것이 일반적으로 알려져 있습니다. 이 사실을 활용하고 당신의 프로젝트를 성공으로 설정하세요.
과거 생활에서, 나는 화려한 프로젝트들을 제작하는 팀의 "멀티미디어 프로그래머"였습니다. 우리의 한 클라이언트가 프로젝트의 모양과 느낌에 대한 그들의 생각을 아주 자세하게 설명했습니다. 설계 회의에서 논의된 일반적인 색 구성표는 프레젠테이션을 위한 검은색 배경을 나타냈습니다. 우리는 완벽하게 이해했다고 생각했습니다. 그래픽 디자이너 팀들이 수백 개의 계층화된 그래픽 파일들을 만들어내기 시작했습니다. 최종 제품을 만드는 데 많은 시간이 소비되었습니다. 우리가 클라이언트에게 우리 노동의 열매를 보여준 날에 놀라운 계시가 이루어졌습니다. 제품을 봤을 때, 배경색에 대한 그녀의 정확한 말은 "내가 검은색이라고 말했을 때, 흰색을 의미한 것이었습니다"였습니다. 그러니까, 보시다시피, 그것은 결코 흑백처럼 명확하지 않습니다.
Nate Jackson이 작성함.
