스터디/Five Lines of Code

7장 컴파일러와의 협업

마디니 2023. 11. 4. 09:57
반응형

컴파일러에 대해 알아보기

  • 컴파일러의 목표는 소스 프로그램과 동일한 다른 언어로 된 프로그램을 생성하는 것

 

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

  • 정지문제(halting problem)란 런타임 동안 어떤 일이 일어날지 정확히 말할 수 없는 이유를 말한다.
    • 즉, 일반적으로 프로그램은 근본적으로 예측이 불가능하다.
  • 컴파일러는 런타임 중 실패를 포함해서 예상대로 동작하지 않는 프로그램이라고 해도 허용한다.
  • 프로그램이 안전하다고 보장할 수 없는 경우 컴파일러는 프로그램을 허용하지 않는다.  (="보수적 분석")
  • 보수적 분석은 프로그램에 특정한 실패 가능성이 없다는 것을 보증하기 때문에 우리는 보수적인 분석에만 의존할 수 있다.

 

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

  • 보수적 분석 중 하나는 메서드가 모든 경우에서 반환(return)되는지 확인하는 것

 

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

  • 변수가 사용되기 전에 변수에 값이 확실히 할당되었는지 여부를 알아낸다.
  • 의미 있는 값을 할당받은 걸 의미하지는 않지만 무언가를 할당받았다 는 것을 의미하기는 한다.
  • 지역변수, 특히 if 문을 사용해서 지역변수를 초기화하려는 경우에 적용된다.
// return 문이 실행될 때 결과 변수가 초기화되었다는 보장이 없음 -> 컴파일 에러

let result;
for(let i=0; i<arr.length; i++) {
	if(arr[i].name === "John") 
    	result = arr[i];
return result;

 

위 코드에서 arr에 name이 John인 요소를 분명히 가지고 있다는 것을 안다면,

이때는 확정 할당 분석이 적용되는 읽기 전용(또는 final) 필드를 사용해서 컴파일러에게 알릴 수 있다.

생성자를 종료할 때 읽기전용 필드는 초기화 되어야 하기 때문에 생성자에서 할당하거나 선언 시 할당 해야 한다.

 

즉  name이 John인 객체를 읽기 전용 필드로 가지는 객체로 배열을 감쌀 수 있다. ?????

// return 문이 실행될 때 결과 변수가 초기화되었다는 보장이 없음 -> 컴파일 에러

let result;
for(let i=0; i<arr.length; i++) {
	if(arr[i].name === "John") 
    	result = arr[i];
return result;

 

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

  • 멤버를 비공개(private)로 하면 오용되지 않을 것이라 확실할 수 있다.
  • private은 객체가 아니라 클래스에 적용되므로 서로 다른 객체가 동일한 클래스인 경우 다른 객체의  private 멤버를 검사 할 수 있음을 의미한다.

 

객체에서 private 메소드 접근 시 컴파일 에러 발생

 

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

  • 타입검사 : 변수와 멤버가 존재하는지 확인하는 역할을 한다. 
  • 순서 강제화나 오류가 발생하도록 이름을 변경할 때마다 사용 했다.
  • 강도가 높은 순으로 타입 시스템을 정렬하면 다음과 같다.
    • 대여타입(Rust)
    • 다형성 타입 유추(OCaml과 F#)
    • 타입 클래스(Haskell)
    • 유니언 타입과 교차 타입(타입스크립트)
    • 종속 타입(Coq와 Agda)

 

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

  • null로 메서드를 호출하려고 하면 오류가 발생하기 때문에 위험하다.
  • average(null)과 같은 코드는 에러를 발생하지만 컴파일 에러로 잡히지 않는다!!!
  • nullable 변수에 대한 null 검사를 하지 않는다면 null로 보는 것이 좋다. -> 너무 적게 확인하는 것 보다는 많이 확인하는게 낫다.
// 잠재적인 null 역참조

function average(arr: number[]) {
	return sum(arr)/arr.length;
}

 

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

  • 산술 오류
    • 컴파일러는 일반적으로 0으로 나누기(또는 나머지) 연산을 확인하지 않는다.
    • 오버플로우 될수 있는지도 확인하지 않는다.
  • 산술 연산을 할 때 컴파일러는 큰 도움이 되지 않기 때문에 주의해야 한다.
// 잠재적인 0으로 나누기가 존재하지만 컴파일러 오류는 없음

function average(arr: number[]) {
	return sum(arr)/arr.length;
}

 

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

  • 우리가 직접 데이터 구조에 접근할 때, 만약 데이터 구조의 범위 내에 있지 않은 인덱스에 접근하려고 하면 아웃-오브-바운드 에러가 발생한다.
  • 컴파일러는 이를 모름
  • 해결: 기대하는 요소를 찾지 못할 위험이 있는 경우 전체 데이터 구조를 탐색하거나 요소가 확실하게 있음을 증명하기 위해 확정 할당 사용

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

  • 아무일도 일어나지 않고 프로그램이 조용히 반복되는 무한루프에 컴파일러는 도움되지 않음
  • while에서 for, foreach, 스트림 연산자 등을 통해 개선되고 있다.

 

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

  • 멀티 스레딩에서 경쟁 상태, 교착 상태, 기아 등 변경 가능한 데이터를 공유하는 여러 쓰레드가 있으면 문제가 발생할 수 있다.
  • (예) 두 쓰레드가 업데이트 하기 위해 동시에 동일한 값을 읽기
    • 이 문제를 해결하기 위해 잠금(lock)을 도입한다. -> 진행전 잠금 부여하고 사용 전 다른 쓰레드의 잠금 해제 됐는지 확인
  •  "교착 상태"
    • 두 쓰레드가 모두 잠겨 있고 계속 진행하기 전에 서로가 잠금 해제를 기다리고 있는 상태 
  •  "기아 상태"
    • 한개의 쓰레드가 무한 루프라면 다른 쓰레드가 실행 될 수 없음
    • 매우 드물지만 가능한 상태

 

컴파일러 사용

  • 컴파일러의 장점을 활용하고 약점을 피하도록 소프트웨어를 설계해야 한다.
  • 프로그래밍은 문학과 훨씬 더 많은 공통점 이 있음 (건축 x)
  • 도메인에 대한 지식을 습득하고 머릿속에서 모델을 형성한 다음 이 모델을 코드로 성문화 한다.

 

컴파일러 활용

  • 컴파일러를 TODO 리스트로 사용해 안전성을 확보
    • 변경 하고 싶을 때 원래 메서드의 이름을 바꾸고 컴파일러에 의존하면 다른 모든 곳에서 우리가 해야할 일을 알 수 있다.
  • 순서 강제화를 이용한 안전성 확보
    • 메서드 매개변수에 특정 타입을 받아서 처리하는 함수
  • 캡슐화 강제를 통한 안정성 확보
    • 컴파일러의 접근 제어를 사용해서 불변속성을 지역화하기
    • 데이터를 캡슐화 하면 데이터가 기대하는 형태로 유지되는 것을 보장할 수 있다.
    • "비공개 도우미 메서드"
  • 컴파일러로 사용하지 않는 코드 감지
    • 여러가지 메서드를 한번에 삭제하면 컴파일러가 전체 코드베이스를 빠르게 스캔해서 어떤 메소드가 사용되는지 알려준다.
    • 빈 값을  가질 수 없는 리스트 데이터 구조에서 생성자가 종료될 때 값이 할당되어 있어야 한다. -> 그래야 컴파일러가 확정 할당 분석에 의존해 이를 보장한다. 
    • 다양한 생성자를 지원하는 언어에서도 초기화되지 않은 읽기 전용 필드가 있는 객체는 생성할 수 없다.확정 값을 통한 안정성 확보

 

생성자에 필수 값을 할당하도록 하여 empty 방지

 

컴파일러와 싸우지 말 것

  • 타입
    • 타입 검사는 컴파일러의 가장 강력한 기능이므로 이것을 무력화 하는 코드를 작성해서는 안된다.
    • 사람들이 자주하는 세 가지 잘못
      • 형 변환
      • 동적 타입
      • 런타임 타입
  • 게으름
    • 코드를 만들 때 (편리함만을 추구하는) 게으름은 더 나쁜 프로그램을 만들게 한다.
  • 기본값
    • 기본값을 사용하는 곳은 결국 다른 누군가가 기본값으로 넣지 말아야 할 값을 추가해 문제
    • 기본값을 사용하는 대신 개발자가 무언가를 추가하거나 직접 처리하게 해야한다.
  • 상속
    • 인터페이스에서만 상속을 받아야 한다. 그렇지 않으면 부모클래스의 메소드를 자식클래스에서 메소드를 강제 해야할지 말아야 할지 수동으로 확인해야 한다.
  • 처리를 강제하지 않은 예외
    • 예외가 발생한다면 예외를 처리하거나 최소한 호출자에게 예외가 처리되지 않았음을 알려야 한다.
  • 아키텍처
    • 아키텍처에 대한 이해도 반드시 필요하다.
    • getter와 setter의 캡슐화를 깨서는 안된다.

 

반응형