[리팩터링] 7장 컴파일러와의 협업

공효은·2023년 5월 16일
0

refactoring

목록 보기
7/12
post-thumbnail
post-custom-banner

이번 장에서 다룰 내용

  • 컴파일러의 장단점 이해
  • 컴파일러의 장점을 활용한 불변속성 제거
  • 컴파일러와 책임 분담

컴파일러는 코드를 높은 수준의 언어에서 낮은 수준의 언어로 변환할 뿐만 아니라 여러 속성의 유효성을 검사하고 프로그램을 실행할 때 특정 오류가 발생하지 않도록 보장한다.

컴파일러가 우리보다 프로그램의 동작을 더 잘 예측한다는 사실을 받아들이자!

7.1 컴파일러에 대해 알아보기

컴파일러는 프로그램이다. 따라서 일관성 같은 특정 작업에 능숙하다.
일반적으로 하나를 여러 번 컴파일한다고 다른 결과가 나오지는 않는다. 반대로 판단과 같은 특정 작업에는 서툴다. 컴파일러는 '의심 가면 물어보라' 라는 일반적인 관용구를 따른다.

기본적으로 컴파일러의 목표는 소스 프로그램과 동일한 다른 언어로 된 프로그램을 생성하는 것이다. 그러나 최신 컴파일러들은 런타임 중에 특정 오류가 발생할 수 있는지도 확인한다.

7.1.1 약점: 정지 문제는 컴파일 시 알 수 있는 것을 제한한다.

런타임 동안 어떤 일이 일어날지 정확히 말할 수 없는 이유를 정지문제(halting problem) 이라고 한다. (프로그램을 실행하지 않고는 프로그램이 어떻게 동작할지 알 수 없다.)

정지문제: 일반적으로 프로그램은 근본적으로 예측이 불가능하다.

어떤 프로그램은 분명히 실패하고 거부되지만, 어떤 프로그램은 실패하지 않고 허용될 수도 있다.
정지문제는 컴파일러가 이 둘 사이에서 프로그램을 어떻게 할 것인지를 결정해야 함을 의미한다.
경우에 따라 컴파일러는 런타임 중 실패를 포함해서 예상대로 동작하지 않는 프로그램이라고 해도 허용한다. 다른 경우로,

프로그램이 안전하다고 보장할 수 없는 경우 컴파일러는 프로그램을 허용하지 않는다. 이것을 보수적 분석(conservative analysis) 이라고한다.

우리는 보수적인 분석에만 의존할 수 있다.

7.1.2 장점: 도달성 검증은 메서드의 반환을 보장한다.

보수적인 분석 중 하나는 메서드가 모든 경우에서 반환(return) 되는지 확인하는 것이다. return 문 없이 메서드를 벗어나는 것은 허락되지 않는다.

다음은 런타임 오류처럼 보이지만 never 키워드 때문에 컴파일러는 assertExhausted가 실행되는 경우가 있는지 없는지를 분석한다.
예제 7.2의 예에서 컴파일러는 열거형의 모든 값을 확인하지 않았다는 것을 알아낸다.

enum Color {
  RED, GREEN, BLUE
}
function assertExhausted(x:never):never {
  throw new Error("Unexpected object: " + x);
}
function handle(t: Color){
  if (t === Color.RED) return '##ff0000';
  if (t === Color.GREEN) return '#00ff00';
  assertExhausted(t); // Color.BLUE를 처리하지 않기 때문에 컴파일러 오류 발생
}

7.1.3 장점: 확정 할당은 초기화되지 않은 변수에 대한 접근을 막는다.

변수가 사용되기 전에 변수에 값이 확실히 할당되었는지 여부를 알아내는 데 능하다.
이 검사는 지역변수 ,특히 if문을 사용해서 지역변수를 초기화하려는 경우에 적용된다.
이때 모든 경로에서 변수를 초기화할 것이라는 보장이 없다.

name이 John인 요소를 찾는 코드를 생각해보자. return 문이 실행될 때 결과 변수가 초기화 되었다는 보장이 없다. 따라서 컴파일러는 이 프로그램을 허용하지 않는다.

//초기화 되지 않은 변수
let result;
for (let i = 0; i < arr.length; i++)
  if(arr[i].name === "John")
    result = arr[i];
return result;

7.1.4 장점: 접근 제어로 데이터 캡슐화를 지원한다.

컴파일러는 데이터를 캡슐화할 때 사용하는 접근 제어에도 탁월하다. 멤버를 비공개(private)로 하면 오용되지 않을 것이라 확신할 수 있다.

불변속성에 민감한 메서드가 있을 경우 비공개로 만들어 보호 할 수 있다.

// 접근 제어로 인한 컴파일러 오류
class Class {
  private sensitiveMethod(){
    //...
  }
}

let c = new Class();
c.sensiviveMethod(); //컴파일러 오류 발생!

7.1.5 장점: 타입(형) 검사기는 속성을 보증한다.

가장 강력한 것은 타입검사다. 타입 검사는 변수와 멤버가 존재하는지 확인하는 역할을 한다.

하나의 요소나 요소 다음에 리스트로만 구성되게 해서 비어 있을 수 없는 리스트 데이터 구조를 코드로 만들었다.

// 타입으로 인한 컴파일러 오류
interface NonEmptyList<T>{
  head: T;
}
class Last<T> implements NonEmptyList<T> {
  constructor(public readonly head: T){ }
}
class Cons<T> implements NonEmptyList<T>{
  constructor(public readonly head: T
              public readonly tail:NonEmptyList<T>){ }
}
function first<T>(xs:NonEmptyList<T>){
  return xs.head;
}
first([]) // 타입오류

7.1.6 약점: null을 역참조하면 애플리케이션이 손상된다.

null로 메서드를 호출하려고 하면 오류가 발생하기 때문에 위험하다. 일부 도구는 이러한 경우 일부를 감지할 수 있지만, 모든 경우를 감지할 수 없으므로 무작정 도구에 의존할 수 없다. null 검사를 제거 하지 말자.

너무 적게 확인하는 것 보다 너무 많이 확인하는 것이 낫다.

average(null)과 같이 호출하면 프로그램을 중단시킬 수 있음에도 불구하고 예시와 같은 코드가 받아들여진다.

// 잠재적인 Null 역참조 이지만 컴파일러 오류는 없음
function average(arr:number[]){
  return sum(arr) / arr.length;
}

7.1.7 약점: 산술 오류는 오버플로나 손상을 일으킨다.

컴파일러가 일반적으로 확인하지 않는 것은 0으로 나누기(또는 나머지) 연산이다. 컴파일러는 무언가가 오버플로될 수 있는지 여부도 확인하지 않는다. 이를 산술 오류라고 한다.

// 빈 배열로 호출하면 거의 어떤 컴파일러도 잠재적인 0으로 나누기를 발견하지 못한다.
function average(arr:number[]){
  return sum(arr) / arr.length;
}

산술 연산을 할 때 컴파일러는 큰 도움이 되지 않기 떄문에 매우 주의해야한다. 나누는 수가 0이 될 수 없고 오버플로 또는 언더플로를 유발할 만큼 큰 숫자를 더하거나 뺴지 않아야 하며 그래야할 경우 BigInteger과 그 변형을 사용하라.

7.1.8 약점: 아웃-오브-바운드 오류는 애플리케이션을 손상시킨다.

데이터 구조의 범위 내에 있지 않은 인덱스에 접근하려고 하면 아웃-오브-바운드 오류가 발생한다.

배열에서 첫 번째 소수의 인덱스를 찾는 함수가 있다고 가정해보자. 함수를 사용해서 다음과 같이 첫 번째 소수를 찾을 수 있다.

// 잠재적으로 접근이 범위를 벗어날 수 있지만 컴파일러 오류는 없음
function firstPrime(arr: number[]){
  return arr[indexOfPrime(arr)];
}

배열에 소수가 없으면 이 함수는 -1을 반환하므로 아웃-오브-바운드 에러가 발생한다.

7.1.9 무한루프는 애플리케이션을 지연시킨다.

프로그램이 실패하는 방법은 아무 일도 일어나지 않고 프로그램이 조용히 반복되는 동안 빈 화면을 응시하게 되는것이다.

아래 예제에서는 현재 위치가 문자열 내인지를 감지하려고한다. 그러나 이전 quotePosition을 indexOf에 대한 두 번째 호출에 전달하는 것을 잊었다. s에 따옴표가 포함된 경우 이것은 무한 루프가 되지만, 컴파일러는 이를 못본다.

// 잠재적 무한 루프지만 컴파일러 오류는 없음
let insideQuote = false;
let quotePosition = s.indexOf("\"")
while(quotePosition >= 0) {
  insideQuote = !insideQuote;
  quotePosition = s.indexOf("\"");
}
let insideQuote = false;
let quotePosition = s.indexOf("\"")
while(quotePosition >= 0) {
  insideQuote = !insideQuote;
  quotePosition = s.indexOf("\"", quotePosition + 1); 
}

7.1.10 약점: 교착 상태 및 경쟁 상태로 인해 의도하지 않은 동작이 발생한다.

책에서 확인하자.ㅎㅎ p220

변경 가능한 데이터를 공유하는 멀티스레드를 사용하지 말자!

7.2 컴파일러 사용

컴파일러의 장점을 활용하고 약점을 피하도록 소프트웨어를 설계해야한다.

7.2.1 컴파일러 활용

컴파일러를 TODO 리스트로 사용해 안정성을 확보

  • 이 책에서 컴파일러를 활용하는 가장 일반적인 방법은 무언가를 수정할 때마다 TODO 리스트로 사용한다.
  • 변경하고 싶을 때 원래 메서드의 이름을 바꾸고 컴파일러에 의존하면 다른 모든 곳에서 우리가 해야 할 일을 알 수 있다.
  • 컴파일러가 어떤 참조도 놓치지 않는다는 것을 알고 안심할 수 있다.

switch에서 default를 사용하는지 확인하기 위해 열거형을 사용하는 모든 위치를 찾아야 한다고 가정하자. 열거형 이름에 _handled와 같은 것을 추가함으로써 컴파일러를 이용해서 default를 비롯한 열거형을 사용하는 모든 곳을 찾을 수 있다.

// 컴파일러 오류로 열거형을 사용하는 위치 찾기
enum Color_handled {
  RED, GREEN, BLUE
}
function toString(c:Color){
  switch (c) {
    case Color.RED: return "Red";
    default: return "No color";
  }
}

순서 강제화를 이용한 안전성 확보

  • 순서 강제화 패턴은 프로그램의 불변속성에 대해 컴파일러에게 알려주는 데 집중해서 불변속성을 일반 속성으로 만든다.
  • 이것은 컴파일러가 컴파일할 때마다 속성이 항상 유지되도록 보장하기 때문에 이후로 불변속성이 실수로 깨지는 일은 없을 것이다.

예제 클래스들은 모두 문자열이 출력 전에 대문자임을 보장한다.

// 내부
class CapitalizedString {
  private value:string;
  constructor(str:string){
    this.value = capitalize(str);
  }
  print() {
    console.log(this.value);
  }
}

// 외부
class CapitalizedString {
  public readonly value: string;
  constructor(str: string){
    this.value = capitalize(str);
  }
  function print(str: CapitalizedString){
    console.log(str.value)
  }
}

캡슐화 강제를 통한 안전성 확보

엄격한 캡슐화를 강제하기 위해 컴파일러의 접근 제어를 사용해서 불변속성을 지역화한다.
데이터를 캡슐화하면 데이터가 기대하는 형태로 유지되는 것을 보장할 수 있다.

// 비공개 도우미 메서드
class Transfer {
  constructor(from: string, private amount:number){
    this.depositHelper(from, -this.amount);
  }
  private depositHelper(to:string, amount:number){
    let accountId = database.find(to);
    database.updateOne(accountId, {$inc: {balance: amount} });
  }
   
  deposit(to:string){
    this.depositHelper(to, this.amount);
  
  }
}

컴파일러로 사용하지 않는 코드 감지

확정 값을 통한 안정성 확보

앞부분에서 비어 있을 수 없는 리스트 데이터 구조를 보았다. 읽기 전용 필드를 사용해서 이를 보장하는데, 이러한 필드는 컴파일러의 확정 할당 분석에 의존하며, 생성자가 종료될 때 값이 할당돼 있어야한다. 다양한 생성자를 지원하는 언어에서도 초기화되지 않은 읽기 전용 필드가 있는 객체는 생성할 수 없다.

// 읽기 전용 필드로 인해 비어 있을 수 없는 리스트
interface NonEmptyList<T>{
  head: T;
}
class Last<T> implements NonEmptyList<T>{
  constructor(public readonly head: T){}
}
class Cons<T> implements NonEmptyList<T>{
  constructor(
    public readonly head: T,
    public readonly tail: NonEmptyList<T>){}
}

7.2.2. 컴파일러와 싸우지 말 것

의도적으로 컴파일러와 싸우면서 컴파일러가 제 역할을 하지 못하도록 막는것
주로 타입을 이해하지 못하고, 게으르며, 아키텍처를 이해하지 못한 죄로 인해 발생한다.

타입

타입 검사기는 컴파일러의 가장 강력한 부분이다. 그러므로 그것을 속이거나 무력화하는 것은 최악의 선택이다. 사람들은 타입을 세 가지 다른 방법으로 잘못 사용해서 타입 검사기를 무력화한다.

형 변환

  • 형 변환은 컴파일러에게 컴파일러보다 자신이 더 잘 알고 있다고 말하는 것과 같다.
  • 형 변환은 컴파일러가 사용자를 돕지 못하게 하고 기본적으로 특정 변수나 표현식에 대해 비활성화 한다.
  • 형 변환이 필요하다는 것은 관련된 어디선가 타입에 대해 이해하지 못하고 있다는 말이다.

동적 타입

  • 타입 검사를 방해하는 것보다 더 나쁜 것은 타입 검사기를 비활성화 하는 것이다.
  • 무수한 잠재적 오류로 가는 길을 열어준다.

최근 일부 타입스크립트가 버전 ES6 에서 실행되고 있지만 컴파일러가 ES5로 구성되어 ES6의 모든 메서드에 대해 알지 못하는 문제가 있다. 특히 배열 findIndex를 모른다. 이 문제를 해결하기 위해 개발자는 변수를 any로 형 변환해서 컴파일러가 해당 변수에 대한 모든 메서드 호출을 허용하게 했다.

(<any> arr).findIndex(x => x === 2);

이 메서드가 런타임에 존재하지 않을 가능성은 거의 없어서 위험하지 않지만 설정을 업데이트 하는 것이 더 안전하고 영구적이다!

런타임 타입

컴파일러를 속이는 세 번째 방법은 컴파일 시간에서 런타임으로 판단에 필요한 정보를 옮기는 것이다.

10개의 매개 변수가 있는 메서드가 있다고 생각해보자. 이것은 하나의 매개변수를 추가하거나 제거할 때마다 메서드를 호출하는 대신 모든 곳에서 수정해야 하기 때문에 혼란스럽다. 따라서 10개의 매개변수를 사용하는 대신 문자열 키를 가진 값들을 Map에 담아 한 개만 사용하기로 했다. 그러면 코드를 변경하지 않고도 많은 값을 쉽게 추가할 수 있다. 하지만 이것은 판단할 수 있는 정보를 없애버리는 끔찍한 생각이다.
컴파일러는 Map에 어떤 키가 있는지 알 수 없기에 우리가 존재하지 않는 키에 접근 하는지 아닌지 확인 불가능하다.

세 개의 개별 인자를 전달하는 대신 하나의 Map을 전달한다. 그런 다음 get 을 사용해서 값을 가져 올 수 있다.

// 런타임 타입
function stringConstructor(
conf: Map<string, string>,
pars:string[]){
  return conf.get("prefix")
        + parts.join(conf.get("jointer"))
        + conf.get("postfix");
}

// 더 안전한 해결책은 이런 특정 필드로 객체를 만드는 것이다.
// 정적 타입
class Configuration {
  constructor(
    public readonly prefix: string,
    public readonly joiner:string,
    public readonly postfix:string
  ){}
}
function stringConstructor(
   conf:Configuration,
   parts: string[]){
  return conf.prefix
     + parts.join(conf.joiner)
     + conf.postfix;
}

기본값

  • 기본값을 사용하는 곳마다 결국 다른 누군가가 기본값으로 넣지 말아야할 값을 추가하고 수정하는 것을 잊어버릴 것이다.
  • 기본값을 사용하는 대신 개발자가 무언가를 추가하거나 변경할 때마다 직접 처리하게 하자.
  • 기본값을 제공하지 않으므로써 컴파일러는 개발자가 결정을 내리도록 강제할 것이다.

상속

인터페이스에서만 상속 받을 것

예제에서 Mammal 에 다른 메서드를 추가하면 이 메서드가 모든 자손 클래스에서 유효한지 수동으로 확인해야한다. 이 때 몇 가지를 놓치거나 확인하는 것을 놓치기 쉽다. 이 코드에서는 오리너구리 (Platypus)를 제외한 대부분의 자손에 대해 작동하는 laysEggs 메서드를 Mammal 슈퍼클래스에 추가했다.

// 상속으로 인한 문제
class Mammal {
  laysEggs() {return false
}
class Dolphin extends Mammal { }
class Platypus extends Mammal {
  // laysEggs를 오버라이드 해야함
  
}

처리를 강제하지 않은 예외

예외는 두 가지로 나타난다. 처리를 강제한 예외와 처리가 강제되지 않은 예외이다.
예외가 발생하면 예외를 처리하거나 최소한 호출자에게 예외가 처리되지 않았음을 알려야한다.

다음 예제는 있을 수 있는 일에 대해 예외 처리를 강제하지 않을 경우의 문제 이다,
산술 오류가 발생하기 때문에 합리적으로 입력 배열이 비어있는지 확인한다. 그러나 예외 처리를 강제하지 않기 때문에 호출자는 빈 배열로 메서드를 호출 할 수 있고 프로그램은 여전히 에러가 발생한다.

더 나은 해결책은 처리를 강제한 예외(try, catch를 사용해 명시적으로 처리)를 사용하느 ㄴ것이다.

// 확인되지 않은 예외를 사용
classs EmptyArray extends RuntimeException {}
function average(arr:number[]){
  if(arr.length === 0) throw new EmptyArray();
  return sum(arr) / arr.length;
}
///...
console.log(average([]));

// 확인된 예외 사용
class Impossible extends RuntimeException { }
class EmptyArray extends CheckedException { }
function average(arr: number[]) throws EmptyArray {
  if(arr.length === 0) throw new EmptyArray();
  return sum(arr) / arr.length;
}
/// ...
try {
  console.log(average(arr));
}catch (EmptyArray e){
  throw new Impossible();
}

아키텍처

마이크로 아키텍처는 팀에 영향을 미치지만 다른 팀에는 영향을 주지 않는 아키텍처이다.
getter와 setter로 캡슐화를 깨는 것이다. 그렇게 하면 수신 측과 필드 사이에 결합이 만들어지고 컴파일러가 접근을 제어하지 못하게 된다.
다음 예지의 스택 구현에서는 내부 배열을 노출하여 캡슐화를 무효로 만든다. 이것은 외부 코드가 그것에 의존할 수 있음을 의미한다. 외부 코드는 배열을 변경해서 스택을 변경할 수 있다.

// getter가 있는 열악한 마이크로 아키텍처
class Stack<T>{
  private data: T[];
  getArray() {return this.data;}
}
stack.getArray()[0] = newBottomElement;  // 이 줄은 스택을 변경

문제가 될 수 있는 또 다른 방법은 private 필드를 외부 함수에 인자로 전달하는 경우이다.

// 매개변수가 있는 열악한 마이크로 아키텍처
class Stack<T>{
  private data: T[];
  printLast() {printFirst(this.data);}
}
function printFirst<T>(arr: T[]){
  arr[0] = newBottomElement; // 이 줄은 스택을 변경
}

대신 불변속성을 지역적으로 유지할 수 있도록 this를 전달해야한다.

7.3 컴파일러 신뢰하기

컴파일러보다 우리가 더 잘 알고 있다는 비생산적인 생각에서 벗어나 컴파일러가 말하는 것에 세심한 주의를 기울일 수 있다.

7.3.1 컴파일러에게 불변속성 가르치기

지역 불변속성은 범위가 제한적이고 명확하기 때문에 관리하기 쉽다. 그러나 컴파일러에게는 동일한 충돌이 발생한다. 우리는 컴파일러가 프로그램에 대해 모르는 것을 알고 있는 것이다.

예제에서는 원소의 수를 세기 위한 데이터 구조를 만들고 있다. 따라서 멤버를 추가할 때 데이터 구조는 추가한 각 멤버의 수를 기록한다. 편의를 위해 추가된 총 멤버의 수도 이록한다.

// 집합의 개수
class CountingSet {
  private data: StringMap<number> = { };
  private total = 0;
  add(element: string){
    let c = this.data.get(element);
    if(c === undefined)
      c = 0;
      this.data.put(element, c + 1)
      this.total++;
  }
}

이 데이터 구조에서 임의의 멤버를 선택하는 메서드를 추가하려고 한다. total보다 작은 임의의 숫자를 선택하고 그것이 배열이었다면 그 위치에 있었을 원소를 반환하는 함수를 만든다. 배열에 저장하지 않았기 때문에 키를 반복하면서 index에서 멤버의 수만큼 빼간다.

// 임의의 멤버 선택(오류)
class CountingSet {
  // ...
  randomElement(): string { //도달성으로 인한 오류
    let index = randomInt(this.total); 
    for (let key in this.data.keys()){
      index -= this.data[key]; 
      if(index <= 0)
        return key;
    }
  }
}

이 메서드는 컴파일러가 도달성 분석에 실패하기 때문에 컴파일되지 않는다. 컴파일러는 total이 데이터 구조에서 총 멤버의 수라는 것을 알지 못하기 떄문에 항상 멤버가 선택되어 반환된다는 것을 알지 못한다. 이것은 이 클래스의 모든 메서드가 종료될 때 참이 유지되는 지역 불변속성이다.

이 경우 Imppassible 예외를 추가해서 오류를 해결 할 수 있다.

class Impossible { }
class CountingSet {
  // ...
  randomElement(): string {
    let index = randomInt(this.total);
    for (let key in this.data.keys()){
      index -= this.data[key];
      if(index <= 0)
        return key;
    } 
    throw new Impossible(); //에러를 방지하기 위한 예외 처리
  }
}

그러나 이것은 컴파일 에러를 막기 위한 일시적인 조치이다.
이 불변속성이 나중에도 깨지지 않도록 보완 조치를 하지 않았다. 멤버를 제거하는 remove 함수를 만들면서 total을 줄이는 것을 잊었다고 생각해보라. 컴파일러는 그런 위험성 때문에 randomElement 메서드를 허락하지 않는다.

프로그램에 불변속성이 있을 때마다 '이길 수 없다면 같은 편으로 만들어라' 라는 말을 생각해서 다음 내용을 떠올려라!

  1. 그것을 제거하라
  2. 할 수 없다면, 컴파일러에게 그것에 대해 가르치라.
  3. 그렇게 할 수 없다면, 자동화된 테스트로 런타임한테 그것에 대해 가르치라
  4. 그렇게도 할 수 없다면, 그것에 대해 광범위하게 문서화하여 팀에게 가르치라
  5. 그마저 할 수없다면, 테스터에게 해당 정보를 알려주고 수동으로 테스트하라.
  6. 만약 전부 할 수 없다면, 기도하세요! 지구상 누구도 도울 수 없다.

7.3.2 컴파일러의 경고에 주의를 기울일 것

정상적인 경고의 개수는 0이어야한다.
위험은 사소한 경고 뒤에 숨어있다.

7.4 컴파일러만 신뢰할 것

"만일 당신이 이 방에서 가장 똑똑한 사람이라면 당신은 잘못된 방에 있는 것이다."

요약

  • 최신 컴파일러의 일반적인 장점과 약점을 알아야한다. 약점을 피하고 장점을 활용하기 위해 코드를 수정할 수 있다.

    • 컴파일러의 도달성 분석을 이용해서 switch가 모든 case를 커버하고 있는지 확인하라.
    • 변수에 값이 있는지 확인하려면 확정 할당을 사용하라.
    • 접근 제어(private, public)를 사용해서 민감한 불변속성이 있는 메서드를 보호한다.
    • 변수를 역참조하기 전에 변수가 null이 아닌지 확인한다.
    • 나누기 전에 나누는 숫자가 0이 아닌지 확인한다.
    • 작업이 오버플로 또는 언더플로되지 않는지, 또 BigInteger를 사용하지 않는지 확인한다.
    • 아웃-오브-바운드 에러를 피하기 위해 데이터 구조의 전체 데이터를 검사하거나 확정 할당을 사용한다.
    • 더 높은 수준의 구조를 사용해서 무한 루프를 방지한다.
    • 여러 스레드가 변경 가능한 데이터를 공유하지 않도록 해서 스레드 문제를 방지한다.
  • 컴파일러와 싸우는 대신, 컴파일러를 사용해서 더 높은 수준의 안정성에 도달하는 방법을 배운다.

    • 리팩터링 시 컴파일 오류를 TODO 리스트로 사용한다.
    • 컴파일러를 이용해서 순서 불변속성을 강제한다.
    • 컴파일러를 이용해서 사용하지 않는 코드를 찾아낸다.
    • 형변환 또는 동적/런타임 타입을 사용하지 말라
    • 기본값과 클래스에서의 상속 또는 확인되지 않은 예외를 사용하지말라
  • 컴파일러를 신뢰하고, 컴파일러의 출력 결과에 주의를 기울이며, 오염되지 않은 코드베이스를 유지해서 경보 피로를 피하라

  • 코드가 작도할지를 예측할 때 컴파일러에 의존하라.

profile
잼나게 코딩하면서 살고 싶어요 ^O^/
post-custom-banner

0개의 댓글