12장: 기본 데이터형

12장: 기본 데이터형

  • 기본 데이터형은 다른 모든 데이터형의 기초 구성 요소임
  • 이 장은 숫자 일반, 정수, 부동소수점, 문자와 문자열, 불린, 열거형, 이름 있는 상수, 배열, 그리고 자신만의 형 만들기를 다룸

12.1 숫자 일반

  • 매직 넘버를 피함. 매직 넘버는 100이나 47524처럼 설명 없이 코드 중간에 나타나는 리터럴 숫자임. 이름 있는 상수를 지원하는 언어라면 상수를 사용함
  • 매직 넘버를 피하면 세 가지 이점이 있음: 변경이 더 신뢰할 수 있고(다른 의미의 100을 잘못 바꾸지 않음), 변경이 더 쉽고(상수 정의 한 곳만 바꾸면 됨), 코드가 더 읽기 쉬움(for i = 0 to MAX_ENTRIES-1은 의심의 여지가 없음)
  • 0과 1은 하드코딩해도 됨. 증감과 루프 시작에 쓰이는 0과 1은 괜찮음. 경험칙으로 프로그램 본문에 나타나도 되는 리터럴은 0과 1뿐임
  • 0으로 나누기 오류를 예상함. 나눗셈 기호를 쓸 때마다 분모가 0이 될 수 있는지 생각함
  • 형 변환을 명시적으로 드러냄. y = x + (float) i처럼 변환이 일어남을 읽는 사람이 알게 하고, 컴파일러마다 다른 암시적 변환에 운을 맡기지 않음
  • 혼합형 비교를 피함. 부동소수점 x와 정수 i의 if ( i = x )는 거의 확실히 동작하지 않음. 수동으로 변환해 같은 형끼리 비교함
  • 컴파일러 경고에 주의를 기울임. 최고의 프로그래머는 모든 컴파일러 경고를 제거하도록 코드를 고침. 컴파일러가 일하게 하는 것이 직접 하는 것보다 쉬움
    컴파일러 경고 발생 시 => 빌드 fail 혹은 배포 fail 나는 방식 고려.
    만약 regacy 때문에 어려운 상황이라면, IDE 도움받아서 commit fail 만이라도 나도록.
    

12.2 정수

  • 정수 나눗셈을 확인함. 정수 세계에서 7/10은 0.7이 아니라 대개 0임. 중간 결과에도 똑같이 적용됨: 10 * (7/10)은 0이지만 (10*7) / 10은 7임. 나눗셈을 마지막에 하도록 식을 재배열함
  • 정수 오버플로를 확인함. 곱셈이나 덧셈의 결과가 최대 정수를 넘을 수 있는지, 각 항이 가질 수 있는 최댓값을 상상해 봄
  • 대략적인 범위: signed 16비트는 ±32,767, signed 32비트는 약 ±21억, unsigned 32비트는 약 42억, 64비트는 약 922경
  • 향후 확장도 고려함. 값이 몇 년간 꾸준히 커질 것으로 예상되면 그것도 감안함
  • 중간 결과의 오버플로도 확인함. 1000000 * 1000000 / 1000000은 중간 결과가 32비트 정수를 넘어 -727 같은 엉뚱한 값이 나옴. 긴 정수형이나 부동소수점형으로 전환해 해결함

12.3 부동소수점 숫자

  • 핵심 문제는 많은 소수를 이진수로 정확히 표현할 수 없다는 것임. 1/3이나 1/7 같은 순환소수는 보통 7자리(32비트)나 15자리(64비트) 정확도로만 표현됨
  • 크기가 크게 다른 숫자 간 덧셈과 뺄셈을 피함. 32비트에서 1,000,000.00 + 0.1은 아마 1,000,000.00이 됨. 크기 차이가 큰 수열을 더해야 하면 정렬 후 작은 값부터 더함
  • 등호 비교를 피함. 같아야 할 부동소수점 수가 항상 같지는 않음. 0.1을 10번 더해도 1.0이 되는 경우는 드묾
  • 허용 가능한 정확도 범위(예: ACCEPTABLE_DELTA = 0.00001)를 정하고, 두 값의 차가 그 안에 있으면 참을 반환하는 Equals() 같은 불린 함수를 사용함
  • 반올림 오류를 예상함. 해결책: 더 정밀한 형으로 변경(단정밀도 → 배정밀도), BCD(이진화 십진법) 변수로 변경(느리고 공간을 더 쓰지만 달러·센트처럼 정확히 맞아야 하는 값에 유용), 부동소수점을 정수로 변경(센트를 정수로 관리하고 달러는 100센트 단위로 관리하는 식. DollarsAndCents 클래스로 감싸면 다루기 쉬움)
  • 언어와 라이브러리의 특정 데이터형 지원을 확인함. Visual Basic의 Currency처럼 반올림에 민감한 데이터를 위한 내장 형이 있다면 사용함

12.4 문자와 문자열

  • 매직 문자와 매직 문자열을 피함. ‘A’ 같은 리터럴 문자나 “Gigamatic Accounting Program” 같은 리터럴 문자열도 이름 있는 상수로 대체함
  • 이유: 프로그램 이름·명령·보고서 제목 같은 문자열은 바뀔 수 있음, 국제화 시 리소스 파일로 모인 문자열이 번역하기 쉬움, 문자열 리터럴은 공간을 많이 차지함, 리터럴은 암호 같음(0x1B보다 ESCAPE가 명확함)
  • off-by-one 오류를 조심함. 부분 문자열도 배열처럼 인덱싱되므로 끝을 넘어 읽거나 쓰지 않는지 확인함
  • 언어와 환경의 유니코드 지원을 알아둠. Java는 모든 문자열이 유니코드지만 C/C++은 별도 함수가 필요함. 유니코드를 쓸지, 어디서 쓸지 일찍 결정함
  • 국제화/지역화 전략을 프로그램 수명 초기에 결정함. 문자열을 외부 리소스에 저장할지, 언어별 빌드를 만들지 런타임에 결정할지가 핵심 고려 사항임
  • 단일 알파벳 언어만 지원하면 ISO 8859 문자 집합을, 여러 언어를 지원해야 하면 유니코드를 고려함
  • 문자열 형 간 일관된 변환 전략을 정함. 프로그램 내부에서는 단일 형식을 유지하고 입출력에 최대한 가까운 곳에서 변환하는 방식이 흔함

C의 문자열

  • 문자열 포인터와 문자 배열의 차이를 인식함. C에서 등호가 들어간 문자열 식은 의심함. StringPtr = "Some Text String"은 내용을 복사하는 것이 아니라 포인터만 설정함. ps(포인터)와 ach(문자 배열) 같은 접두어 규칙으로 구분함
  • C 스타일 문자열은 길이를 CONSTANT+1로 선언함. 길이 n인 문자열은 널 종료 문자 때문에 n+1바이트가 필요함. 선언은 NAME_LENGTH + 1로 하고 나머지 코드에서는 항상 NAME_LENGTH를 사용함. 매번 같은 방식을 쓰면 문자열마다 어떻게 선언했는지 기억할 필요가 없어짐
  • 끝없는 문자열을 피하려고 널로 초기화함. 선언 시 = { 0 }으로 초기화하고, 동적 할당에는 malloc() 대신 0으로 초기화해 주는 calloc()을 사용함
  • 메모리가 제약이 아니라면 포인터 대신 문자 배열을 사용함. 포인터 문제를 피하고 컴파일러 경고를 더 받을 수 있음
  • strcpy() 대신 strncpy() (strscpy(), strlcpy(), snprintf() 등)를 사용함. strcpy(), strcmp()는 널 종료 문자를 만날 때까지 계속 진행하는 위험한 버전임. 책은 strncpy(), strncmp()를 안전한 버전으로 권하지만, 현대 기준으로 strncpy()는 아래 단점 때문에 더 이상 권장되지 않음

※ strncpy()의 단점 (2004년 책의 한계, 현대 기준 보충)

  • 단점 1: 버퍼가 꽉 차면 널 종료 문자를 붙이지 않음. 원본이 버퍼 크기 이상이면 잘린 채 널 없이 끝나서, 책이 경고하는 “끝없는 문자열”을 strncpy() 스스로 만들어 냄
char dest[ 8 ];
strncpy( dest, "Hello, World!", sizeof( dest ) );
/* dest = { 'H','e','l','l','o',',',' ','W' } — 널 종료 없음! */
printf( "%s\n", dest );   /* 버퍼 끝을 지나 계속 읽음 (undefined behavior) */
  • 그래서 strncpy()를 쓰려면 매번 널 종료를 수동으로 보장해야 하는데, 이걸 잊는 것이 전형적인 버그 패턴임
strncpy( dest, src, sizeof( dest ) - 1 );
dest[ sizeof( dest ) - 1 ] = '\0';   /* 이 한 줄을 항상 기억해야 함 */
  • 단점 2: 원본이 짧으면 남은 공간을 전부 0으로 채움. 4KB 버퍼에 5바이트 문자열을 복사하면 나머지 약 4천 바이트를 매번 0으로 채우는 불필요한 낭비가 발생함
char buffer[ 4096 ];
strncpy( buffer, "abc", sizeof( buffer ) );
/* "abc" 복사 후 buffer[3]~buffer[4095]까지 4093바이트를 전부 0으로 채움 */
  • 단점 3: 애초에 안전한 복사 함수로 설계된 것이 아님. 초기 UNIX의 고정 길이 디렉터리 엔트리(널 종료가 필요 없는 고정 크기 필드)를 다루기 위한 함수여서, “안전한 strcpy”로 쓰는 것 자체가 오용에 가까움
  • 현대적 대안:
/* 1) snprintf — 이식성 최고, 항상 널 종료 보장 */
snprintf( dest, sizeof( dest ), "%s", src );

/* 2) strlcpy — BSD/glibc 2.38+, POSIX 2024 표준. 항상 널 종료 보장 */
if ( strlcpy( dest, src, sizeof( dest ) ) >= sizeof( dest ) ) {
    /* 잘림(truncation) 발생 */
}

/* 3) strscpy — 리눅스 커널 전용. 널 종료 보장 + 잘리면 -E2BIG 반환 */
if ( strscpy( dest, src, sizeof( dest ) ) < 0 ) {
    /* 잘림 발생 */
}
  • 참고로 strlcpy()도 반환값이 strlen(src) 전체라서 원본을 끝까지 읽어야 하는 결함이 있고, 이를 고친 것이 커널의 strscpy()임 (커널에서는 2024년 초 strlcpy()가 완전히 제거됨). C++이라면 이 모든 문제 대신 std::string을 사용함

12.5 불린 변수

  • 불린 변수로 프로그램을 문서화함. 불린 식을 그냥 검사하지 말고, 검사의 의도를 드러내는 변수에 할당함
  • if ( ( elementIndex < 0 ) || ... )보다 finished = ...; repeatedEntry = ...; if ( finished || repeatedEntry )가 목적을 분명히 함
  • 불린 변수로 복잡한 검사를 단순화함. 조건이 여러 줄에 걸친 복잡한 if 문은 읽는 사람이 “필요하면 나중에 이해하지”라고 넘기게 만듦. allDataRead, legalLineCount 같은 불린 변수로 분해하면 읽기 쉽고, 오류가 적고, 수정하기 쉬움
  • 필요하면 자신만의 불린 형을 만듦. C처럼 불린 형이 없는 언어에서는 typedef int BOOLEAN;이나 enum으로 정의함. int보다 의도가 분명해지고 코드가 자기 문서화됨

12.6 열거형

  • 열거형은 어떤 부류의 각 값을 말로 표현할 수 있게 하는 데이터형임. 변수의 가능한 값을 모두 알고 있고 그것을 단어로 표현하고 싶을 때 사용함
  • “1은 빨강, 2는 초록, 3은 파랑” 같은 낡은 방식의 강력한 대안임
  • 가독성을 위해 사용함: if chosenColor = 1보다 if chosenColor = Color_Red. 숫자 리터럴이 보이면 열거형으로 바꿀 수 있는지 자문함
  • 루틴 매개변수 정의에 특히 유용함. RetrievePayrollData( data, true, false, false, true )는 알 수 없지만 EmploymentStatus_CurrentEmployee, PayrollType_Salaried, ...는 이해할 수 있음
  • 신뢰성을 위해 사용함: 일부 언어(특히 Ada)에서는 컴파일러가 열거형 변수에 유효한 값만 할당되는지 검사해 줌
  • 수정 용이성을 위해 사용함: 형 정의에 요소를 추가하고 재컴파일하면 됨
  • 불린 변수의 대안으로 사용함: true/false로 부족해지면(예: 실패에 두 종류가 있을 때) Status_Success, Status_Warning, Status_FatalError 같은 열거형이 더 유용하고 확장도 쉬움
  • 잘못된 값을 검사함: case 문의 else 절로 유효하지 않은 값을 잡음
  • 루프 한계용으로 첫 요소와 마지막 요소를 정의함: Country_First, Country_Last를 명시적 값으로 정의하면 열거형 요소를 도는 루프를 쓸 수 있음
  • 첫 항목은 잘못된 값으로 예약함: 많은 컴파일러가 첫 요소에 0을 할당하므로, 0을 invalid로 선언하면 초기화되지 않은 변수를 잡는 데 도움이 됨
  • InvalidFirst, First, Last 요소의 사용법을 프로젝트 코딩 표준에 정확히 정의하고 일관되게 사용함
  • 요소에 명시적 값을 할당할 때의 함정을 조심함: 1, 2, 4, 8처럼 값을 건너뛰면 루프가 3, 5, 6, 7 같은 무효한 값도 돌게 됨
  • 언어에 열거형이 없으면 전역 변수나 클래스로 흉내 냄. Java(당시)에서는 private 생성자와 public static final 멤버로 typesafe하게 구현할 수 있음

12.7 이름 상수

  • 이름 있는 상수는 값을 한 번 할당하면 바꿀 수 없는 변수와 같음. 최대 직원 수 같은 고정 값을 1000이 아니라 MAXIMUM_EMPLOYEES라는 이름으로 참조하게 함
  • 상수 사용은 프로그램을 “매개변수화”하는 것임. 바뀔 수 있는 측면을 한 곳에서 바꿀 수 있는 매개변수로 만듦. 이 “단일 지점 제어”가 소프트웨어를 정말로 부드럽게(바꾸기 쉽게) 만듦
  • 데이터 선언에 이름 있는 상수를 사용함. 전화번호 길이를 리터럴 7 대신 LOCAL_NUMBER_LENGTH로 선언하면, 회사가 해외로 확장돼 자릿수가 바뀌어도 상수 정의 한 곳만 바꾸면 됨
  • 상수 사용이 유지보수를 크게 돕는다는 것이 입증되어 있음. 바뀔 수 있는 것에 대한 제어를 중앙화하는 기법은 모두 유지보수 노력을 줄이는 좋은 기법임
  • “안전한” 리터럴도 피함. For i = 1 To 12의 12는 아마 1년의 12개월이겠지만 확신할 수 있는가? 1년이 12개월인 것이 바뀔 리 없어도, 목적에 조금이라도 의심의 여지가 있다면 NUM_MONTHS_IN_YEAR로 명확히 함. 더 나아가 인덱스도 month로, 최종적으로는 For month = Month_January To Month_December처럼 열거형까지 쓸 수 있음
  • 코드에서 리터럴을 뿌리 뽑는 데 광적으로 임함. 에디터로 2~9를 검색해 실수로 쓴 리터럴이 없는지 확인함
  • 언어가 상수를 지원하지 않으면 적절한 범위의 변수나 클래스로 흉내 냄. 지역 → 클래스 → 전역 순으로 좁은 범위를 선호함
  • 상수를 일관되게 사용함. 같은 것을 어떤 곳에서는 상수로, 다른 곳에서는 리터럴로 쓰는 것은 위험함. 상수 값을 바꾸면 다 바꿨다고 생각하지만 하드코딩된 리터럴을 놓쳐 미스터리한 결함이 생김

12.8 배열

  • 배열은 가장 단순하고 흔한 구조적 데이터형임. 같은 형의 항목들을 인덱스로 직접 접근함
  • 모든 배열 인덱스가 배열 경계 안에 있는지 확인함. 배열의 모든 문제는 요소를 임의로 접근할 수 있다는 사실에서 비롯됨. 가장 흔한 문제는 경계를 벗어난 접근임
  • 배열 대신 컨테이너 사용을 고려하거나 배열을 순차 구조로 생각함. 배열의 임의 접근은 임의의 goto와 비슷하게 무질서하고 오류가 잦고 정확성을 증명하기 어렵다는 주장이 있음. 집합, 스택, 큐처럼 순차 접근하는 컨테이너 클래스를 먼저 대안으로 고려함
  • 배열의 끝 지점을 확인함. 첫 요소, 마지막 요소, 중간 요소를 올바르게 접근하는지, off-by-one 오류가 없는지 자문함
  • 다차원 배열은 첨자 순서가 맞는지 확인함. Array[ i ][ j ]를 쓰려다 Array[ j ][ i ]를 쓰기 쉬움
  • 인덱스 혼선(index cross-talk)을 조심함. 중첩 루프에서 i를 쓸 자리에 j를 쓰기 쉬움. i, j보다 의미 있는 인덱스 이름을 쓰면 애초에 실수하기 어려워짐
  • C에서는 ARRAY_LENGTH() 매크로를 사용함: #define ARRAY_LENGTH( x ) (sizeof(x)/sizeof(x[0])). 항목을 넣고 빼도 크기 상수를 따로 갱신할 필요가 없어 특히 크기 없는 배열에 유용함

12.9 자신만의 형 만들기 (형 별칭)

  • 프로그래머 정의 데이터형은 언어가 주는 가장 강력한 능력 중 하나임. 예상 못 한 변경으로부터 프로그램을 보호하고, 새 클래스를 설계·구축·테스트하지 않고도 읽기 쉬운 코드를 만듦
  • 좌표 변환 프로그램에서 typedef float Coordinate;로 형을 만들면, 나중에 배정밀도가 필요해져도 typedef 문 한 곳만 double로 바꾸면 됨
  • 문자열이나 배열이 관련되면 길이를 나타내는 상수를 정의하고 형 정의에서 그 상수를 사용함 (NAME_LENGTH = 30; employeeName = array[ 1..NAME_LENGTH ] of char)
  • 형 만들기는 정보 은닉과 결합할 수 있음. Coordinate를 일관되게 쓰면 데이터의 형을 사실상 숨김. C++은 비유적 정보 은닉까지만 지원하고, Ada는 패키지의 private 선언으로 문자 그대로의 정보 은닉을 지원함
  • 자신만의 형을 만드는 이유: 수정이 쉬워짐, 과도한 정보 확산을 피함(형 세부 사항을 한 곳에 중앙화), 신뢰성이 높아짐(Ada의 range 0..99 같은 런타임 검사), 언어의 약점을 보완함(C에 불린이 없으면 직접 만듦)
  • Ada의 currentTemperature: INTEGER range 0..212; 같은 선언은 int temperature;에 없는 중요한 의미 정보를 담음. 클래스로 같은 의미를 강제할 수도 있지만, 한 줄짜리 형 정의에서 클래스로 가는 것은 큰 걸음이라 실제로는 생략되기 쉬움

형 생성 지침

  • 기능 지향적 이름으로 형을 만듦. BigInteger, LongString처럼 기저 컴퓨터 데이터를 가리키는 이름을 피하고, 좌표·통화·급여 코드·나이처럼 실세계 문제를 가리키는 이름을 사용함. 언어 형을 가리키는 이름은 형이 제공하는 절연층에 구멍을 냄
  • 사전 정의된 형을 피함. 형이 바뀔 가능성이 조금이라도 있으면 typedef나 형 정의 밖에서는 사전 정의된 형을 쓰지 않음. Coordinate xfloat x보다 x에 대해 훨씬 많은 것을 말해 줌
  • 사전 정의된 형을 재정의하지 않음. 언어에 Integer가 있다면 Integer라는 형을 새로 만들지 않음. 읽는 사람이 익숙한 Integer로 착각함
  • 이식성을 위해 대체 형을 정의함. INT32, LONG64 같은 형을 정의해 두면 하드웨어 플랫폼이 바뀔 때 재정의로 대응할 수 있음. 단 INT처럼 사전 정의된 형과 혼동되기 쉬운 이름은 피함
  • typedef 대신 클래스 생성을 고려함. 단순 typedef도 정보 은닉에 크게 기여하지만, 더 많은 유연성과 제어가 필요하면 클래스를 만듦

체크리스트: 기본 데이터

숫자 일반

  • 코드가 매직 넘버를 피하는가?
  • 0으로 나누기 오류를 예상하는가?
  • 형 변환이 명시적인가?
  • 서로 다른 두 형의 변수가 한 식에 쓰일 때 의도대로 평가되는가?
  • 혼합형 비교를 피하는가?
  • 경고 없이 컴파일되는가?

정수

  • 정수 나눗셈을 쓰는 식이 의도대로 동작하는가?
  • 정수 식이 오버플로 문제를 피하는가?

부동소수점 숫자

  • 크기가 크게 다른 숫자 간 덧셈과 뺄셈을 피하는가?
  • 반올림 오류를 체계적으로 방지하는가?
  • 부동소수점 수의 등호 비교를 피하는가?

문자와 문자열

  • 매직 문자와 문자열을 피하는가?
  • 문자열 참조에 off-by-one 오류가 없는가?
  • C 코드가 문자열 포인터와 문자 배열을 다르게 취급하는가?
  • C 코드가 문자열을 CONSTANT+1 길이로 선언하는 규칙을 따르는가?
  • C 코드가 적절한 경우 포인터 대신 문자 배열을 쓰는가?
  • C 코드가 끝없는 문자열을 피하려고 널로 초기화하는가?
  • C 코드가 strcpy() 대신 strncpy()를 쓰는가? strncat()과 strncmp()도? (※ 현대 기준: strncpy() 대신 snprintf()/strlcpy()/strscpy() 권장)

불린 변수

  • 조건 검사를 문서화하기 위해 불린 변수를 추가로 쓰는가?
  • 조건 검사를 단순화하기 위해 불린 변수를 추가로 쓰는가?

열거형

  • 가독성, 신뢰성, 수정 용이성을 위해 상수 대신 열거형을 쓰는가?
  • 변수의 용도가 true/false로 완전히 표현되지 않을 때 불린 대신 열거형을 쓰는가?
  • 열거형 검사에서 잘못된 값을 확인하는가?
  • 열거형의 첫 항목이 “invalid”로 예약되어 있는가?

이름 있는 상수

  • 데이터 선언과 루프 한계에 매직 넘버 대신 상수를 쓰는가?
  • 상수를 일관되게 쓰는가? 어떤 곳에서는 상수로, 다른 곳에서는 리터럴로 쓰지 않는가?

배열

  • 모든 배열 인덱스가 배열 경계 안에 있는가?
  • 배열 참조에 off-by-one 오류가 없는가?
  • 다차원 배열의 모든 첨자가 올바른 순서인가?
  • 중첩 루프에서 올바른 변수를 배열 첨자로 써서 인덱스 혼선을 피하는가?

형 생성

  • 바뀔 수 있는 데이터 종류마다 다른 형을 쓰는가?
  • 형 이름이 언어의 형이 아니라 실세계 개체를 향하는가?
  • 형 이름이 데이터 선언을 문서화할 만큼 서술적인가?
  • 사전 정의된 형의 재정의를 피했는가?
  • 형을 재정의하는 대신 새 클래스 생성을 고려했는가?

요점 정리

  • 특정 데이터형을 다루려면 형마다 많은 개별 규칙을 기억해야 한다. 이 장의 체크리스트로 흔한 문제를 고려했는지 확인해라.
  • 자신만의 형을 만들면 프로그램이 수정하기 쉬워지고 더 자기 문서화된다(언어가 지원한다면).
  • typedef나 그 등가물로 단순한 형을 만들 때는 새 클래스를 만들어야 하는 것은 아닌지 고려해라.

results matching ""

    No results matching ""