LYnLab

블로그취미로그

[PL] 데이터 타입과 그 정보

데이터는 그저 컴퓨터 내부의 0과 1의 비트 값들에 불과하다. 컴퓨터의 자원은 유한하기 때문에, 이렇게 비트로 데이터를 표현하는 것에 한계가 존재한다. 예를 들어 우리가 현실에서 생각하는 '정수'라는 범위는 무수히 많은 숫자를 포함하고 있으나, 컴퓨터의 데이터로는 가장 큰 수와 가장 작은 수 사이 범위의 값들만 데이터로서 기억, 저장할 수 있다.

2015-12-06#프로그래밍

💡 이 글은 작성된지 1년 이상 지났습니다. 정보글의 경우 최신 내용이 아닐 수 있음에 유의해주세요.

Data Types and Type Information

개요

데이터는 그저 컴퓨터 내부의 0과 1의 비트 값들에 불과하다. 컴퓨터의 자원은 유한하기 때문에, 이렇게 비트로 데이터를 표현하는 것에 한계가 존재한다. 예를 들어 우리가 현실에서 생각하는 '정수'라는 범위는 무수히 많은 숫자를 포함하고 있으나, 컴퓨터의 데이터로는 가장 큰 수와 가장 작은 수 사이 범위의 값들만 데이터로서 기억, 저장할 수 있다. 이러한 범위는 언어마다, 기계마다 미세한 차이가 있어 언어가 기계 의존적이도록 만든다.

언어 차원에서 정적 타입 확인(static type checking)을 하는 것은 장단점을 가진다. 타입 확인이 느슨할 수록 언어의 유연성(flexibility)은 증가하지만, 신뢰성(reliability)는 떨어지기 때문이다. 그럼에도 최소한의 타입 확인이 필요한 이유는

  • 실행 효율성
  • 번역 효율성
  • 코드 작성의 편의성
  • 안정성과 보안
  • 코드의 가독성
  • 모호성의 제거
  • 개발 도구 활용의 용의성
  • 인터페이스의 일관성과 정확성

의 다양한 장점이 있기 때문이다.

Definition of Types

프로그램의 데이터는 타입(type)을 통해 분류된다. 일반적으로 타입은 다음과 같이 두 가지 방식으로 정의한다.

  • 값의 집합 : 타입을 이루는 모든 값들의 집합. (예: Java에서 int는 -2,147,483,648에서 2,147,483,647의 정수값을 가진다.) 모든 값들을 나열하거나, 기존의 알려진 값들의 부분으로 정의할 수 있다.
  • 값과 연산의 집합 : 타입을 이루는 모든 값들과, 이들 값들이 이룰 수 있는 연산들.

코드 전반적으로 연산자와 피연산자의 타입 일관성(type consistency)을 유지하기 위해서는 타입 확인(type checking)이 필요하다. 특정한 표현(expression)에 타입을 부여하는 것, 예를 들어 z = x / y 라는 표현을 수행하기 위해 x / y에 int 또는 double이라는 타입을 부여하는 것을 타입 추리(type inference)라고 한다. 타입 추리는 타입 확인의 일부라고 볼 수도 있고, 독립된 하나의 과정이라고도 볼 수 있다. (PPT에서는 서로 의존적인 과정으로 취급한다.)

Constructing Types

대부분의 언어는 int, double 등의 기초적인 타입을 확장하여 더욱 복잡한 타입을 만들 수 있도록 지원한다. 이러한 메커니즘을 타입 생성자(type constructor)라고 하며, 이렇게 만들어진 타입을 유저 정의 타입(user-defined types)이라고 한다. 우리가 흔히 접하는 타입 생성자 중 하나는 배열이다.

typedef int int10[10];

C에서는 이와 같은 방식으로 타입 선언(type declaration)을 수행한다.

유저 정의 타입이 등장하며, 서로 다르게 정의한 타입이 같은 값을 가지는 경우를 고려해야하는 경우가 발생한다. 언어들은 동일 타입 알고리즘(type equivalence algorithm)이라는 이름의 규칙을 가지고 있다. 아울러, 이러한 과정들을 통틀어 타입 시스템(type system)이라고 부른다.

Typing

프로그램 언어 차원에서 타입 중첩 오류를 가능한 사전에 발견하여 발생하지 않음을 보장하는 시스템을 갖추었다면, 이러한 언어는 strongly typed라고 한다. 이 경우 거의 모든 불안전한 프로그램(unsafe programs)은 번역 단계에서 기각된다. 하지만 이렇게 깐깐한 규칙으로 인해, 많은 경우에서 오류가 없는 프로그램임에도 불구하고 기각되기도 한다.

일부 언어에서는 타입 체킹의 예외나 취약점(loophole)이 존재하기도 한다. 특히 C언어는 타입 확인에 많은 loophole이 존재한다. 이러한 이유로 C를 weakly typed language라고 부르기도 한다. 이는 C의 특징을 물려받은 C++에도 마찬가지로 나타나는 증상이다.

반대로, 정적 타입 시스템이 없는 언어들은 untyped languages 혹은 dynamically typed langauges라고 불린다. 많은 스크립트 언어들이 이에 속하며, 대부분 실행 단계에서 타입 확인이 이루어진다.

Simple Types

모든 언어는 자체적으로 타입을 가지고 있으며 이를 predefined types이라고 한다. 이 타입은 대부분 심플 타입(simple types)이다. 하지만 심플 타입임에도 사전 정의되지 않은 경우도 있는데, enumerated types와 subrange types가 이에 해당한다.

enumerated type은 이름에 순서대로 값을 부여하는 것이고, subrange type은 기존의 타입의 일부를 새로운 타입으로 정의하는 것이다.

Type Constructors

Cartesian Product

많은 언어에서, 카르테시안 곱 타입 생성자는 record construction(또는 structure construction)으로 나뉜다. 예를 들어,

struct IntCharReal {
    int i;
    char c;
    double r;
}

은 int × char × double의 카르테시안 곱 타입이다.

하지만 카르테시안 곱과 레코드 구조에는 차이가 있다. 레코드 구조는 구성 요소가 이름을 가지는데 반해, 카르테시안 곱은 이 역할을 위치로 대체한다. 따라서 레코드 구조에서는 요소를 선택하기 위한 component selector operation이 존재한다. 위에서 정의한 IntCharReal의 경우 x.i 등의 접근자가 이에 해당한다.

반면 카르테시안 곱의 대표적인 예시인 ML의 튜플(tuple) 타입은 다음과 같이 이용한다.

type IntCharReal = int * char * real;

#3 (2, #"a", 3.14) = 3.14

Union

두 개의 값을 묶어 하나의 메모리상에 정의하는 타입을 유니온이라고 부른다. 유니온은 크게 discriminated union과 undiscriminated union으로 나누어진다. 유니온에 태그(구분자; discriminator)가 포함되어있다면 discriminated union이다.

// Undiscriminated union
union IntOrReal {
    int i;
    double r;
};

// Discriminated union
enum Disc {IsInt, IsReal};
struct IntOrReal {
    enum Disc which;
    union {
        int i;
        double r;
    } val;
};

유니온 구조는 메모리를 절약하는데 도움이 되나, OOP 언어에서는 유니온 대신 상속의 개념을 사용하는 것이 더 나은 설계 방법이다.

Subset

대부분의 언어는 서브타입(subtype)의 형태로 서브셋을 지원한다. Ada에서는 기존 타입의 하한과 상한을 통해 subtype을 정의하며, type 연산자를 통해 전혀 새로운 타입을 만드는 것과는 구분된다.

레코드의 많은 개념들이 서브타입을 통해 고쳐질 수 있다. 예를 들어, Ada에서의 IntOrReal 타입은

subtype IRInt is IntOrReal(IsInt);
subtype IRReal is IntOrReal(IsReal);

의 형태로 상속하여 나누어 정의할 수 있다. OOP 언어에서의 상속의 개념을 서브타입에 적용시킬 수도 있다.

Array

f: U → V 형태의 함수는 배열 타입이나 함수 타입의 두 가지 방식으로 생각할 수 있다. 만약 U가 순서를 나타내는 타입이라면, U를 인덱스(index) 타입으로, V로 요소(component) 타입으로 생각하여 f는 배열으로 생각할 수 있다. 배열 타입의 크기의 유무와 상관 없이 정의는 될 수 있으나, 배열 변수의 크기는 번역 단계에서 반드시 정의되어야한다.

각 언어별로 다음과 같은 미세한 차이를 보인다.

  • C/C++에서는 배열의 크기는 배열의 요소로 생각하지 않는다. 때문에 배열을 parameter로 넘겨주기 위해서는 반드시 그 배열의 크기를 함께 넘겨주어야 한다.
    • C에서 배열의 크기는 반드시 숫자이어야 하나, C++에서는 상수 표현식도 가능하다.
  • Java에서는 배열의 크기를 배열의 요소로 생각한다. 배열은 동적으로 할당되며, .length 속성에 배열의 크기가 저장된다.
  • Ada에서는 배열이 선언되는 시점에서, 즉 완전히 동적으로 크기가 정의된다.

다차원 배열의 경우 또한 다음과 같은 언어별 차이가 있다.

  • 정의 방법
    • C/C++, Java : 배열의 배열로 정의.
    • Ada, FORTRAN : 다차원 배열을 별도의 개념으로 정의. 이 경우, 배열의 배열과 다차원 배열은 서로 다른 의미를 가지게 된다.
  • 메모리 할당 방법
    • Row-Major 형태 : 대부분의 언어.
    • Column-Major 형태 : FORTRAN.

앞서 얘기했듯이 C/C++ 등의 언어에서는 배열의 크기를 배열의 요소로 포함하지 않으므로, 다차원 배열 또한 parameter로 이용하기 위해서는 크기 값이 같이 넘어가야 한다.

대부분의 함수형 언어는 배열의 개념을 지원하지 않고, list의 개념을 활용한다.

Function

험수는 대부분의 경우 후술할 포인터 타입으로 취급한다. 아래는 C에서 함수를 다루는 예시이다.

 typedef int (*IntFunction)(int);
 int square(int x) { return x*x; }
 IntFunction f = square;
 int evaluate(IntFunction g, int value) {
    return g(value);
 }

OOP 언어는 이를 객체형 변수로 충분히 대체할 수 있으므로, 함수형 변수를 지원하지 않는다.

Pointer(Reference)

포인터 생성자(pointer constructor) 또는 레퍼런스 생성자(reference -)는 타입들을 참조하는 모든 메모리의 모음이다.

typedef int* IntPtr;

위와 같은 C 선언문이 있을 때, 만약에 x가 IntPtr 타입의 변수라면 *x = 10과 같은 연산으로 x의 메모리 주소를 derefernce하여 10이라는 값을 부여할 수 있다. 또한 C/C++에서의 배열은 상수 포인터와 같은 형태이다. 즉 a[2]는 *(a + 2)와 같은 의미를 가진다.

Recursive

재귀적 타입은, 타입을 선언할 때 자기 자신을 이용하는 타입을 이야기한다. 재귀적 타입을 선언하기 위해서는 대부분 포인터를 사용하게 된다. 타입 그 자체를 재귀적으로 선언하게 되면 번역 단계에서 데이터가 가지는 최대 메모리 크기를 파악할 수 없기 때문이다.

struct CharListNode {
    char data;
    struct CharListNode* next;
}
typedef struct CharListNode* CharList;

Type Nomenclature in Languages

C

  • Basic Types
    • void
    • Numeric
      • Integral
        • signed, unsigned, char, int, short int, long int
        • enum
      • Floating: float, double, long double
  • Derived Types
    • Pointer, Array, Fuction, union, struct

Java

  • Primitive Types
    • boolean
    • Numeric
      • Integral: char, byte, short, int, long
      • Floating point: float, double
  • Reference Types
    • Array, class, interface

Ada

  • scalar
    • enumeration: Boolean, Character
    • numeric: Integer, Float
  • access (포인터)
  • composite
    • array, record

Type Equivalence

초반에 언급되었듯, 유저가 새로운 타입을 추가할 수 있게 되며 동일한 타입을 확인할 필요성이 생겼다. 이에 대한 방법으로는 다음과 같은 세 가지 경우가 있다.

Structural Equivalence (구조 동일성)

물리적으로 같은 구조를 가질 경우, 즉 동일한 기본 타입을 통해 새로운 타입을 만들었을 경우이다. A × B와 B × A는 서로 다른 구조를 가지는 것으로 여긴다. 번역기는 구조적 동일성을 확인하기 위해 트리 구조를 통한 재귀적인 방법을 이용한다.

단, 배열의 경우 크기가 배열 타입의 요소로 포함되지 않는다면 크기가 서로 다른 배열도 동일한 구조로 여긴다. 예시로, 다음 C 코드의 S와 T는 구조적으로 동일하다.

typedef int S[10];
typedef int T[5];

구조 동일성은 Alogl, FORTRAN, COBOL에서 사용하며, 일부 현대 언어에서도 선택적으로 사용하고 있다.

Name Equivalence (이름 동일성)

단순히 같은 이름의 타입이 같은 것으로 취급하는 방법이다. Ada는 완전한 이름 동일성을 채택한 언어이다. 단, 서브타입과 타입은 서로 다른 것으로 취급한다.

아래는 C언어의 예시이다.

struct A {
    char x;
    int y;
}
struct B {
    char x;
    int y;
}
typedef struct A C;

구조체와 유니온은 이름 동일성을 사용한다. 유의할 점은, typedef는 새로운 자료형을 만드는 것이 아니라 이름을 하나 더 붙혀주는 것 뿐이다. 때문에 A와 C는 동일하지만, B는 다르다.

typedef C* P;
typedef struct B* Q;
typedef struct A* R;

마찬가지로 P와 R은 동일하지만 Q는 다르다.

Pascal에서는 타입 생성자로 만들어지는 모든 타입이 새로운 것으로 간주한다. 단, 기존의 타입에 새로운 이름(alias)을 붙히는 것은 동일한 것으로 본다.

Java에는 OOP 언어의 특성에 맞게 typedef 생성자가 존재하지 않는다. 대신 클래스와 인터페이스 선언을 사용하며, 앞서 언급했던 서브타입 개념에 상속을 이용한다.

ML은 datatype이라는 생성자를 통해 새로운 타입을 만든다. type은 새로운 이름(alias)을 붙힐 때 사용한다.

관련된 글

Rails와 GitHub Actions에 커버리지 레포트를 달아보자

이 블로그의 CMS이기도 한 Shiori를 대폭 리팩토링하면서 테스트가 얼마나 잘 작성되어있는지 궁금해졌습니다.

Rails Global ID로 전역 객체 식별하기

Global ID는 Rails의 모든 객체를 식별할 수 있는 URI(Uniform Resource Identifier)입니다.

Ruby on WebAssembly: 살짝 맛보기

Ruby 3.2에 추가된 WebAssembly 지원을 간단하게 테스트해봅시다.

작성한 댓글은 giscus를 통해 GitHub Discussion에 저장됩니다.

크리에이티브 커먼즈 라이선스크리에이티브 커먼즈 저작자표시크리에이티브 커먼즈 동일조건변경허락

본 사이트의 저작물은 별도의 언급이 없는 한 크리에이티브 커먼즈 저작자표시-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.

© 2011 - 2024 Hoerin Doh, All rights reserved.

LYnLab 로고GitHubTwitterInstagram