10장: 변수 사용 시 고려할 사항
10장: 변수 사용 시 고려할 사항
- 구현 단계에서는 요구사항과 아키텍처가 완전히 지정하지 않은 작은 세부를 채움
- 이 장은 그중 변수 사용이라는 낮은 수준의 구현 문제를 다룸
- 여기서 변수는 정수나 배열 같은 내장 데이터형뿐 아니라 객체도 포함함
- 데이터형은 주로 내장 데이터형을, 데이터는 객체와 내장 데이터 모두를 가리킴
10.1 데이터 활용 능력
- 효과적인 데이터를 만들기 위한 첫 단계는 어떤 종류의 데이터를 만들 수 있는지 아는 것임
- 다양한 데이터형에 익숙한 것은 프로그래머의 기본 도구함에 가까움
- 추상 데이터형, 배열, 비트맵, 불린, B-tree, 컨테이너, 열거형, 힙, 인덱스, 링크드 리스트, 상수, 포인터, 참조 무결성, 스택, 문자열, 트리,
typedef, 공용체, variant 같은 개념을 알고 있는지 점검함 - 목록에는 실제 데이터형이 아닌 항목도 섞여 있음. 모르는 것을 안다고 넘기지 않는 지적 정직성도 중요함
10.2 변수 선언을 쉽게 만들기
- 변수 선언은 작은 일이지만 자주 하므로 좋은 습관이 장기적으로 시간을 절약함
암시적 선언
- 암시적 선언은 매우 위험한 기능임
- 암시적 선언이 있는 언어에서는 가능한 한 기능을 끔
10.3 변수 초기화 지침
- 잘못된 데이터 초기화는 오류의 큰 원천임
- 초기화 문제는 변수에 예상하지 못한 값이 들어 있을 때 발생함
- 변수에 값이 한 번도 할당되지 않았거나, 값이 오래되었거나, 일부만 초기화되었을 수 있음
- 객체 멤버 일부만 초기화하거나, 포인터가 가리키는 메모리를 잘못 다루는 경우도 포함됨
초기화 문제를 피하는 방법
- 변수를 선언할 때 바로 초기화함
- 선언 시 초기화할 수 없다면 처음 쓰는 곳 가까이에서 초기화함
- 가능한 언어에서는 변수 선언과 정의를 첫 사용 지점 가까이에 둠
- 바뀌면 안 되는 값에는
final이나const를 사용함 - 카운터와 누산기는 사용할 때마다 제대로 초기화·재초기화되는지 특히 확인함
- 클래스 멤버 데이터는 생성자에서 초기화하고, 생성자에서 할당한 메모리는 소멸자에서 해제함
- 루프나 반복 호출 때문에 재초기화가 필요한지 확인하고, 반복되는 코드 안에 초기화를 둠
- 이름 있는 상수는 한 번만 초기화하고, 진짜 변수는 실행 코드에서 사용 지점 가까이에 초기화함
- 컴파일러의 자동 초기화 옵션을 쓴다면 그 의존성을 문서화함
- 컴파일러 경고를 적극 활용함
- 입력 매개변수는 값을 사용하기 전에 유효성을 확인함
- 포인터가 있는 환경에서는 메모리 접근 검사 도구를 사용함
- 프로그램 시작 시 작업 메모리를 알려진 값으로 채우면 초기화 결함을 더 빨리 드러낼 수 있음
10.4 범위
- 범위는 변수가 프로그램 어디까지 알려져 있고 참조될 수 있는지를 뜻함
- 작은 범위의 변수는 작은 루프나 한 루틴 안에서만 보임
- 큰 범위의 변수는 많은 루틴이나 전체 프로그램에서 보임
- 언어에 따라 블록, 루틴, 클래스, 패키지, 네임스페이스, 전체 프로그램 등 다양한 범위를 제공함
변수 참조를 지역화하기
- 변수 참조 사이의 코드는 취약한 창임
- 그 사이에 새 코드가 들어와 변수를 의도치 않게 바꾸거나, 읽는 사람이 변수 값을 잊을 수 있음
- 변수 참조는 가능하면 가까이 모음
- span은 한 변수의 참조와 다음 참조 사이에 있는 코드 길이를 뜻함
- span이 짧을수록 읽는 사람이 한 번에 이해해야 할 코드가 줄어듦
변수 생존 시간을 짧게 유지하기
- live time은 변수가 처음 참조된 문장부터 마지막으로 참조된 문장까지의 길이임
- 참조 횟수가 많아도 첫 참조와 마지막 참조가 멀면 생존 시간이 김
- 생존 시간이 짧으면 의도치 않은 변경 가능성이 줄어듦
- 초기화 오류 가능성이 줄고, 읽어야 할 코드 범위도 줄어듦
- 관련 코드가 가까워지므로 큰 루틴을 작은 루틴으로 나누기도 쉬워짐
- 전역 변수는 span과 live time이 매우 커지므로 피해야 할 이유가 하나 더 생김
범위를 최소화하는 지침
- 루프에서 쓰는 변수는 루틴 시작이 아니라 루프 바로 앞에서 초기화함
- 값은 실제로 쓰기 직전에 할당함
- 같은 변수를 다루는 관련 문장을 함께 묶음
- 관련 문장 묶음은 별도 루틴으로 분리함
- 가장 제한적인 가시성에서 시작하고, 필요할 때만 범위를 넓힘
- 지역 루프, 개별 루틴, 클래스의 비공개 멤버, 보호 멤버, 패키지, 전역 순서로 가능한 작은 범위를 우선함
- 편의성 때문에 범위를 넓히면 작성은 쉬울 수 있어도 읽기, 디버깅, 수정은 어려워짐
- 공유가 필요하면 노출된 전역 데이터보다 접근 루틴을 고려함
10.5 지속성
- 지속성은 데이터가 살아 있는 기간임
- 블록이나 루틴 동안만 지속되는 변수, 명시적으로 해제되거나 GC될 때까지 지속되는 변수, 프로그램 전체 동안 지속되는 변수, 실행 사이에도 저장되는 데이터가 있음
- 문제는 변수가 실제보다 더 오래 지속된다고 가정할 때 생김
- 값이 남아 있는 것처럼 보여 오류가 숨어 있을 수도 있음
지속성 오류를 피하는 방법
- 중요한 변수에는 디버그 코드나 어설션으로 합리적인 값인지 확인함
- 사용이 끝난 변수에는 일부러 말이 안 되는 값을 넣어 잘못된 재사용을 드러냄
- 데이터가 지속되지 않는다고 가정하고 작성함
- 언어가 보장하는 경우가 아니라면, 루틴을 다시 들어올 때 이전 값이 남아 있다고 기대하지 않음
- 데이터를 사용하기 직전에 선언하고 초기화하는 습관을 둠
10.6 바인딩 시점
- 바인딩 시점은 변수와 값이 연결되는 시점임
- 코드 작성 시점, 컴파일 시점, 로드 시점, 객체 생성 시점, 실제 사용 직전 등 여러 시점이 있음
- 일반적으로 바인딩이 늦을수록 유연성은 커짐
- 하지만 늦은 바인딩을 지원하려면 코드가 복잡해지고 오류 가능성도 늘어남
- 매직 넘버처럼 코드 작성 시점에 값을 박는 방식은 유연성이 낮음
- 이름 있는 상수는 컴파일 시점 바인딩으로, 가독성과 변경 용이성을 높이면서 런타임 비용을 들이지 않음
- 설정 파일이나 레지스트리에서 읽는 값은 로드 시점이나 실행 시점 바인딩으로, 프로그램 변경 없이 값을 바꿀 수 있음
- 필요한 유연성만 제공하고 요구사항을 넘는 유연성과 복잡성은 추가하지 않음
10.7 데이터형과 제어 구조 사이의 관계
- 데이터형과 제어 구조는 서로 잘 맞물림
- 순차 데이터는 순차 명령문으로 이어짐
- 선택 데이터는
if나case같은 조건문으로 이어짐 - 반복 데이터는
for,while,repeat같은 반복문으로 이어짐 - 실제 데이터는 순차, 선택, 반복이 조합된 형태일 수 있음
- 데이터 구조를 이해하면 자연스러운 제어 흐름도 더 쉽게 잡힘
10.8 각 변수를 정확히 한 가지 목적으로 사용
- 변수 하나를 여러 목적으로 쓰는 미묘한 관행은 피함
- 임시 변수 하나를 서로 관련 없는 계산과 교환 작업에 함께 쓰면 두 사용이 관련된 것처럼 보임
- 각 목적에는 그 목적을 드러내는 별도 변수를 사용함
- 이름 없는
temp보다discriminant,oldRoot처럼 의미를 드러내는 이름이 낫음
숨은 의미가 있는 변수 피하기
- 특정 값이면 오류를 뜻하고, 다른 값이면 정상 값을 뜻하는 식의 이중 의미를 피함
- 예를 들어 페이지 수가
-1이면 오류라는 식은 정수 변수가 불린 역할까지 겸하는 것임 - 이런 남용은 hybrid coupling에 해당함
- 두 종류의 정보를 담아야 한다면 두 변수를 사용함
- 선언한 변수는 실제로 사용되는지 확인함
- 미사용 변수는 결함률 상승과 관련이 있으므로 컴파일러나 도구 경고를 무시하지 않음
참고 자료
- Cormen, Leiserson & Rivest, Introduction to Algorithms
- Sedgewick, Algorithms in C++, Parts 1-4
- Sedgewick, Algorithms in C++, Part 5
체크리스트: 데이터 사용 시 일반 고려 사항
변수 초기화
- 각 루틴이 입력 매개변수의 유효성을 확인하는가?
- 변수를 처음 사용하는 위치 가까이에 선언하는가?
- 가능하다면 변수를 선언과 동시에 초기화하는가?
- 선언과 동시에 초기화할 수 없다면 처음 사용하는 위치 가까이에 초기화하는가?
- 카운터와 누산기가 올바르게 초기화되고, 필요하면 매번 재초기화되는가?
- 반복 실행되는 코드에서 변수가 올바르게 재초기화되는가?
- 컴파일러 경고를 모두 켠 상태에서 경고 없이 컴파일되는가?
- 언어가 암시적 선언을 사용한다면 그로 인한 문제를 보완했는가?
그 밖의 데이터 사용 문제
- 모든 변수가 가능한 가장 작은 범위를 갖는가?
- 변수 참조가 다음 참조까지의 거리와 전체 생존 시간 양쪽에서 가능한 한 가까운가?
- 제어 구조가 데이터형과 대응되는가?
- 선언된 모든 변수가 사용되는가?
- 모든 변수가 적절한 시점에 바인딩되는가? 늦은 바인딩의 유연성과 복잡성 사이에서 의식적으로 균형을 잡았는가?
- 각 변수가 오직 한 가지 목적만 갖는가?
- 각 변수의 의미가 명시적이며 숨은 의미가 없는가?
요점 정리
- 데이터 초기화는 오류가 생기기 쉬우므로, 예상하지 못한 초기값 문제를 피하기 위한 초기화 기법을 사용해야 한다.
- 각 변수의 범위를 최소화해라. 변수 참조를 가까이 두고, 루틴이나 클래스 안에 지역화하며, 전역 데이터는 피한다.
- 같은 변수를 다루는 문장들은 가능한 한 가까이 둔다.
- 이른 바인딩은 유연성을 제한하지만 복잡성을 줄인다. 늦은 바인딩은 유연성을 높이지만 복잡성을 증가시킨다.
- 각 변수는 오직 한 가지 목적에만 사용한다.