좋은 코드, 나쁜 코드 3장

 

 

3장 다른 개발자와 코드 계약

자신의 코드와 다른 개발자의 코드

  • 개발자들의 코드는 서로 의존하게 된다.
  • 여러 가지 다른 기능을 위해 작성한 코드가 재사용되고 있음을 알 수 있다.
  • 요구사항이 항상 변한다는 점을 인지해야 한다.
  • 고품질 코드를 작성할 때 가장 중요한 고려 사항 중 하나는 다른 개발자가 변경하거나 코드와 상호작용할 때 발생할 수 있는 문제를 파악하는 것이다.
  • 아래 3가지를 고려하는 것이 유용하다
    • 자신에게 명백하다고 해서 다른 사람에게도 명백한 것은 아니다.
    • 다른 개발자는 무의식중에 여러분의 코드를 망가뜨릴 수 있다.
    • 시간이 지남에 따라 자신의 코드를 기억하지 못한다.
  • 자신에게 분명하다고 해서 다른 사람에게도 분명한 것은 아니다
    • 코드가 어떻게 사용되어야 하는지, 무엇을 하는지, 왜 그 일을 하고 잇는지를 설명하는 것이 유용하다.
    • 주석문을 많이 작성하는 것을 의미하는 바가 아니며, 코드를 이해하기 쉽고 코드 자체로 설명이 되게 하는 것이 좋은 방법이다.
  • 다른 개발자는 무의식중에 여러분의 코드를 망가뜨릴 수 있다.
    • 작성한 코드는 독립적으로 존재하지 않는다.
    • 다른 개발자가 리팩터링하고 수정함에 따라 코드는 계속 변한다.
    • 코드를 기반으로 계속해서 변호하는 코드 역시 끊임없이 작성된다.
    • 코드가 작동하지 않거나 오용하는 결과를 가져온다면, 그 코드는 코드 베이스에 병합할 수 없다.
      • 이를 위한 신뢰방안은 컴파일 중지나 테스트에 실패하도록 하는 것이다.
      • 이 두가지 중 하나를 하는 것이 고품질 코드의 작성과 관련된 많은 고려 사항들이 궁극적으로 이루고자 하는 것이다.
  • 시간이 지나면 자신의 코드를 기억하지 못한다.
    • 당장은 코드가 신선하고 중요한 것으로 여겨지기 때문에, 결코 잊을 수 없을 것이라는 생각이 든다.
    • 하지만 1년 후 새로운 기능을 추가하거나, 자신의 코드에서 발견 된 버그를 해결해야 한다면 자신이 작성한 코드임에도 불구하고 그 코드의 자세한 내용을 더 이상 기억하지 못할 수도 있다.
    • 1~2년 전에 작성한 코드는 다른 사람이 작성한 코드를 보는 것과 다르지 않다.
    • 코드에 버그가 발생하는 것이 어려워야 하며, 이는 미래의 자신에게도 유익하다.

여러분이 작성한 코드의 사용법을 다른 사람들은 어떻게 아는가?

  • 다른 개발자가 본인의 코드를 사용하거나 코드에 의존하는 코드를 수정하는 상황이다.
  • 코드를 어떻게 사용해야 하는지, 그 코드가 무슨 일을 하는지 파악해야 한다.
    • 여러 가지 상황에서 어떤 함수를 호출해야 하는지
    • 클래스가 무엇을 나타내는지 그리고 언제 사용되야 하는지
    • 어떤 값을 인수로 사용해야 하는지
    • 코드가 수행하는 동작이 무엇인지
    • 어떤 값을 반환하는지
  • 이름 확인
    • 자신의 코드를 다른 개발자가 어떻게 사용해야 하는지에 대해 가장 잘 전달할 수 있는 방법 중 하나는 이름을 잘 짓는 것이다.
  • 데이터 유형 확인
    • 컴파일이 필요한 정적 유형의 언어에서는 데이터 유형을 인식하고 올바르게 사용해야 한다.
  • 문서 읽기
    • 코드를 사용하는 방법에 관한 문서는 두 가지 이상의 형태로 존재할 수 있으며, 다음을 포함한다.
      • 함수 및 클래스 수준의 비공식적인 주석문
      • 자바독과 같은 좀 더 공식적인 코드 내 문서
      • 외부 문서(README.md, 웹 페이지. 지침 문서 등)
    • 이는 매우 유용하지만, 어느 정도만의 신뢰를 제공한다.
      • 잘못 해석할 수 있으며, 실제로 읽지 않는 경우가 많다.
      • 문서의 업데이트가 제대로 되지 않을 수도 있다.
  • 직접 물어보기
    • 가장 빠르고 효과적이지만, 어느정도의 신뢰도가 필요하다.
      • 코드를 많이 작성할 수록 질문에 대한 많은 시간을 필요할 것이다.
      • 코드 작성자의 부재, 1년 이상 지난 코드인 경우 접근법이 어렵다.
      • 코드를 작성한 사람이 회사를 떠난 경우 보존이 어렵다.
  • 코드를 살펴보는 것
    • 가장 확실하지만, 오래 걸리는 방법이다.
    • 일부 또는 모든 하위 의존 코드의 구현 세부 사항을 읽어야 할 것이고, 수천 개의 라이브러리를 확인해야 한다.
    • 코드를 사용하는 방법을 알기 위해 개발자가 구현 세부 사항을 읽어야 한다면, 이는 추상화 계층의 많은 이점을 부정하는 것이 된다.

코드 계약

  • 계약에 의한 프로그래밍
  • 계약에 의한 디자인
  • 위 원칙은 다른 사람들이 어떻게 코드를 사용할지, 그 코드가 무엇을 할 것으로 기대할 수 있는지에 대한 것이다.
  • 서로 다른 코드 간의 상호작용을 마치 계약처럼 생각한다.
  • 아래 세 가지 범주로 나눌 수 있다.
    • 선결 조건 : 코드를 호출하기 전에 사실이어야 하는 것, 시스템이 어떤 상태에 있는지를 명시한다.
    • 사후 조건 : 코드가 호출된 후에 사실이어야 하는 것, 시스템이 새로운 상태에 놓인다든지 반환되는 값과 같은 사항이다.
    • 불변 사항 : 코드가 호출되기 전과 후에 시스템 상태를 비고해서 변경되지 않아야 하는 사항
  • 계약에 의한 프로그래밍을 의도하지 않더라도, 작성하는 코드는 어떤 종류의 계약을 맺는 것이라고 봐도 무방하다.
  • 아래와 같이 상태를 수정할 경우, 이것은 계약을 생성한 것이 된다.
    • 입력 매개변수가 있는 함수를 작성
    • 값을 반환
    • 어떤 상태를 수정
  • 이는 코드를 호출하는 사람에게 무언가를 설정하거나 입력을 제공해야 할 요건을 부여하고, 호출 결과 일어날 일 혹은 반환될 값에 대한 기대를 갖게 하기 때문이다.
  • 계약의 세부 조항
    • 실제 계약은 아주 명백한 항목과 작은 글씨로 되어있어, 꼼꼼하게 읽지 않는 한 모든 항목을 읽기 힘들다.
    • 코드에 계약을 정의할 때 명확한 부분과 세부 조항으로 나뉜다.
    • 명확한 부분
      • 함수와 클래스 이름 : 호출하는 쪽에서 모르면 사용할 수 없다.
      • 인자 유형 : 호출하는 쪽에서 인자의 유형을 잘못 사용하면 코드는 컴파일조차 되지 않는다.
      • 반환 유형 : 호출하는 쪽에서 함수의 반환 유형을 알아야 한다.
      • 검사 예외 : 호출하는 코드가 이것을 처리하지 않으면 코드는 컴파일되지 않는다.
    • **세부 조항**
      • 주석문과 문서 : 실제로 잘 읽지 않으며, 이 사실을 실용적인 관점에서 봐야한다.
      • 비검사 예외 : 몇 계층 아래에서 함수가 비검사 예외를 생성하지만, 이 함수의 작서앚가 문서에서 그것을 언급하는 것을 잊어버리는 경우가 생긴다.
    • 세부 조항을 읽지 않는 경우가 많으며, 문서화 업데이트 이슈로 계약 조건을 명확하게 하는 것이 낫다.
  • 세부 조항에 너무 의존하지 않기
    • 세부 조항에 너무 많이 의존하면 오용하기 쉬운 취약한 코드가 될 가능성이 크고, 예상과 다르게 동작하기 쉽다.
    • 세부 조항에 의존하는 것을 피할 수 없는 경우도 있다.
      • 어떤 문제들이 항상 주의 사항이 있고, 이것을 설명해야 할 때
      • 어쩔 수 없이 다른 사람이 작성한 저품질의 코드에 의존해야 할 때
    • 코드를 오용할 수 있는 방법이 많을수록 실제로 오용되고 있는 소프트웨어에 버그가 있을 가능성이 크다.
  • 세부 조항을 제거하는 방법
    • 가장 좋은 방법은 세부 조항에 의존하지 못하게 하는 것이다.
    • 코드가 오용되거나 잘못 설정되면 컴파일조차 되지 않도록 하는 것을 목표로 해야한다.
    • 아래의 코드에서 UserSettings 클래스는 정적 팩토리 함수를 사용해 초기화가 완전히 이루어진 인스턴스를 얻는 것만 가능하도록 수정할 수 있다.
    class UserSettings {
    	private UserSettings() {...}
    	
    	static UserSettings? create(File location) {
    		UserSettings settings = new UserSettings();
    		if (!settings.loadSettings(location){
    				return null;
    		}
    	}
    }
    
    • 위와 같은 변경 사항으로, 잘못 된 상태에서 클래스 인스턴스를 만드는 것을 불가능하게 할 수 있다.
    • 이는 상태나 가변성이 클래스 외부로 노출되는 것을 없애는 방법이다.
    • 상태 : 객체가 담고 있는 어떤 값이나 데이터
    • 가변성 : 객체의 상태를 수정할 수 있는 것을 뜻한다.
    • UserSetting 클래스는 그럼에도 세부 조항을 완벽하게 제거하지 못했다.
      • 실패한 오류에 대한 오류 정보를 포함하고 있지 않는다.
      • 널 값을 반환하는 대신, 사용할 수 있는 대안을 살펴봐야 한다.

체크 및 어서션

  • 컴파일러를 사용하여 코드 계약을 확인하는 것에 대한 대안으로 런타임 검사를 진행할 수 있다.
  • 이는 코드를 실행하는 동안 발생하는 문제에 대한 테스트에 의존하기 때문에 컴파일 타임 확인만큼 강력하지 않다.
  • Check()
    • 코드 계약이 준수되었는지 확인하기 위한 추가 로직이다.
    • 준수되지 않은 경우 체크는 실패를 유발하는 오류를 생성하며, 이는 신속한 실패로 이어진다.
    • ex> 전기 스쿠터를 예로 설명하면, 유저가 30마일로 달리면 안전장치로 스쿠터를 정지시키는 방법이다. 실제로는 벌금을 부과하는 방법을 사용하지만, 신속한 실패 를 적용하여 안전한 코드를 작성하는 것이다. 이는 현실 세계에서는 위험하지만 최상의 방법이 된다.
    • 전제 조건 검사 : 입력 인수가 올바르거나, 초기화가 수행되었거나, 일부 코드를 실행하기 전에 시스템이 유효한 상태인지 확인하는 경우
    • 사후 상태 검사 : 반환값이 올바르거나 일부 코드를 실행한 후 시스템이 유효한 상태인지 확인하는 경우
    • 체크를 사용할 때 기대하는 것은 코드가 오용되면 고객에게 배포되거나 실제 프로덕션에서 서비스가 되기 전에 개발 단계나 테스트 단계에서 발견되고 수정하는 것이다.
      • 체크가 잘 동작해서 실패가 명백함에도 아무도 알아차리지 못할 위험이 생길 수 있다.
      • 예외가 일어나더라도 시스템이 작동을 완전히 멈추지 않도록 하기 위해 프로그램의 상위 수준에서 예외가 처리되고 오류의 자세한 사항이 로그에 기록될 수 있다.
      • 개발자가 이 오류를 신경쓰지 않는다면 오류를 아무도 알아차리지 못한다.
      • 이는 필요 이상으로 자주 발생하며, 심각한 문제가 발생한다.
    • 코드에 체크가 많다면 세부 조항을 없애는 것에 대하여 고려해봐야하는 신호이다.
  • 퍼즈 테스트(fuzz test)
    • 코드나 소프트웨어 버그, 잘못 된 설정을 드러낼 수 있는 입력값을 생성해 테스트를 수행하는 테스트의 한 종류이다.
    • 퍼즈 테스트에 체크를 사용하면, 잘못 된 설정이나 버그를 발견할 가능성을 높이는 데 도움이 된다.
  • 어서션(assertion)
    • 많은 언어에서 지원하며, 코드 계약을 준수하기 위한 방법이라는 점에서 체크와 유사하다.
    • 조건이 위되면 오류가 명백하게 보이거나 예외가 발생한다.
    • 어서션과 체크 사이의 주요 차이점은 배포를 위한 빌드할 때 어서션은 보통 컴파일에서 제외된다.
    • 이는 코드가 실제 서비스 환경에서 사용될 때 실패를 명백하게 보여주지 않는 것을 의미한다.
    • 코드를 배포할 때 컴파일하지 않는 이유는 아래와 같다.
      • 성능 향상을 위해
      • 코드 오류 발생률을 낮추기 위해

요약

  • 코드베이스는 계속 변하고 일반적으로 여러 개발자에 의해 변경된다.
  • 다른 개발자가 어떻게 코드를 오용할 수 있을지 생각해보고, 이러한 가능성을 최소화하거나 오용이 불가능하게 만드는 방식으로 코드를 작성하는 것이 유용하다.
  • 코드를 작성할 때는 일조으이 코드 계약이 만들어지며, 명백한 항목이나 세부 조항이 포함될 수 있다.
  • 코드 계약의 세부 조항은 다른 개발자가 계약을 준수하도록 만들어지지만, 신뢰할 방법은 아니며 명백한 항목으로 계약의 내용을 전달하는 것이 좋다.
  • 컴파일러를 사용하여 계약을 확인하는 것이 가장 신뢰하는 방법이며, 불가능하다면 체크나 어서션을 사용하여 실행 시간에 계약을 확인할 수 있다.