데이터 타입과 추상화

데이터 타입과 추상화

토픽: Programming 레슨: 3 of 16 선수 지식: What Is Programming, Programming Paradigms 목표: 데이터 타입, 타입 시스템, 추상 데이터 타입, 추상화가 복잡성을 관리하는 방법을 이해합니다.


타입이란 무엇인가?

타입(type)은 다음을 결정하는 데이터의 분류입니다: - 데이터가 가질 수 있는 값 - 데이터에 수행할 수 있는 연산 - 차지하는 메모리 크기 - 컴퓨터가 해석하는 방법

비유: 타입은 컨테이너와 같습니다 — 유리병은 액체를 담고, 판지 상자는 고체를 담습니다. 판지 상자에 물을 부을 수 없습니다 (또는 부어서는 안 됩니다). 타입은 코드에서 유사한 제약을 강제합니다.

타입이 중요한 이유

# Without types (conceptually):
x = "42"
y = 10
z = x + y  # What should this mean? "4210" or 52? Error?

# With types:
x: str = "42"
y: int = 10
# z = x + y  # Type error: can't add string and int
z = int(x) + y  # Explicit conversion: 52

타입은 어떤 연산이 유효한지에 대한 제약을 강제하여 오류를 방지합니다.


기본 타입(Primitive Types)

기본 타입은 언어가 제공하는 구성 요소입니다. 일반적으로 하드웨어 표현에 직접 매핑됩니다.

정수(Integers)

소수 부분이 없는 정수.

Python:

age = 25
population = 7_800_000_000  # Python allows underscores for readability
negative = -42

Java:

byte smallNumber = 127;       // 8-bit: -128 to 127
short mediumNumber = 32000;   // 16-bit: -32,768 to 32,767
int standardNumber = 100000;  // 32-bit: ~-2B to 2B
long largeNumber = 10000000000L;  // 64-bit

C++:

int x = 42;
unsigned int y = 100;  // Only positive values
long long z = 9223372036854775807LL;  // 64-bit

부동소수점 수(Floating-Point Numbers)

소수 부분이 있는 숫자. 이진 인코딩으로 인한 근사 표현.

Python:

pi = 3.14159
scientific = 6.022e23  # 6.022 × 10^23 (Avogadro's number)

JavaScript:

let price = 19.99;
let tiny = 0.0000001;
let notExact = 0.1 + 0.2;  // 0.30000000000000004 (floating-point precision issue)

Java:

float f = 3.14f;     // 32-bit, single precision
double d = 3.14159;  // 64-bit, double precision (default)

불리언(Booleans)

논리 연산을 위한 참 또는 거짓 값.

Python:

is_active = True
has_permission = False

if is_active and has_permission:
    print("Access granted")

JavaScript:

let isLoggedIn = true;
let isAdmin = false;
console.log(isLoggedIn && !isAdmin);  // true

C++:

bool flag = true;
bool result = (5 > 3);  // true

문자(Characters)

단일 문자, 종종 정수(ASCII/Unicode 코드 포인트)로 표현됩니다.

Java:

char letter = 'A';  // Single quotes for char
char unicode = '\u0041';  // Unicode: also 'A'

C++:

char c = 'x';
char newline = '\n';

Python:

# Python has no separate char type; single-character strings
letter = 'A'

문자열(Strings)

문자의 시퀀스. 일부 언어는 문자열을 기본형으로, 다른 언어는 객체로 취급합니다.

Python:

name = "Alice"
message = 'Hello, World!'
multiline = """This is
a multi-line
string"""

JavaScript:

let greeting = "Hello";
let template = `Hello, ${name}!`;  // Template literals

Java:

String text = "Hello, World!";  // String is an object, not primitive

C++:

#include <string>
std::string message = "Hello, C++!";

복합 타입(Composite Types)

기본 타입으로부터 구축된 타입.

배열(Arrays)

같은 타입의 요소로 이루어진 고정 크기의 정렬된 컬렉션.

Python:

# Python lists are dynamic, not fixed-size, but conceptually similar
numbers = [1, 2, 3, 4, 5]

JavaScript:

let numbers = [1, 2, 3, 4, 5];  // Dynamic arrays

Java:

int[] numbers = {1, 2, 3, 4, 5};  // Fixed size
int[] array = new int[10];  // Allocate size 10, initialized to 0

C++:

#include <array>
std::array<int, 5> numbers = {1, 2, 3, 4, 5};  // Fixed size 5

레코드/구조체(Records/Structs)

서로 다른 타입의 관련 데이터를 그룹화합니다.

C:

struct Person {
    char name[50];
    int age;
    double salary;
};

struct Person alice = {"Alice", 30, 75000.0};
printf("%s is %d years old\n", alice.name, alice.age);

C++:

struct Point {
    int x;
    int y;
};

Point p = {10, 20};
std::cout << "x: " << p.x << ", y: " << p.y << std::endl;

튜플(Tuples)

가능하게는 다른 타입의 요소로 이루어진 정렬된 고정 크기 컬렉션.

Python:

person = ("Alice", 30, "Engineer")  # (name, age, job)
name, age, job = person  # Unpacking

JavaScript (배열 사용):

let person = ["Alice", 30, "Engineer"];
let [name, age, job] = person;  // Destructuring

타입 시스템(Type Systems)

정적 vs 동적 타이핑

정적 타이핑(Static Typing): 타입을 컴파일 시점에 확인합니다. 변수는 고정된 타입을 갖습니다.

언어: Java, C++, C, Rust, Go, TypeScript

Java 예시:

int x = 10;
// x = "hello";  // Compile error: incompatible types
x = 20;  // OK

C++ 예시:

int count = 5;
// count = "text";  // Compile error
count = 10;  // OK

이점: - 오류를 일찍 포착 (프로그램 실행 전) - 더 나은 도구 (자동완성, 리팩토링) - 성능 최적화 (컴파일러가 타입을 알고 있음)

절충안: - 더 장황함 (타입 주석) - 덜 유연함


동적 타이핑(Dynamic Typing): 타입을 런타임에 확인합니다. 변수는 어떤 타입이든 담을 수 있습니다.

언어: Python, JavaScript, Ruby, PHP

Python 예시:

x = 10       # x is an int
x = "hello"  # Now x is a string — no error
x = [1, 2]   # Now x is a list

JavaScript 예시:

let x = 10;
x = "hello";  // OK
x = {key: "value"};  // OK

이점: - 덜 상용구적 - 더 유연함 - 더 빠른 프로토타이핑

절충안: - 런타임에 오류 포착 (프로덕션에서 충돌 가능) - 대규모 프로젝트에서 코드에 대한 추론 어려움 - 느린 성능 (런타임 타입 검사)


강한 vs 약한 타이핑

강한 타이핑(Strong Typing): 타입 규칙의 엄격한 강제. 호환되지 않는 타입 간의 암시적 변환 없음.

Python (강함):

x = "5"
y = 10
# z = x + y  # TypeError: can't add string and int
z = int(x) + y  # Must explicitly convert: 15

약한 타이핑(Weak Typing): 암시적 타입 변환(타입 강제) 허용.

JavaScript (약함):

let x = "5";
let y = 10;
let z = x + y;  // "510" — string concatenation (implicit conversion)
let w = x - y;  // -5 — subtraction (implicit conversion to number)
console.log(z, w);  // "510", -5

강한 타이핑의 이점: 더 적은 놀라움, 더 명확한 의도 약한 타이핑의 이점: 더 허용적, 덜 장황함 (하지만 더 많은 버그)


타입 추론(Type Inference)

컴파일러/인터프리터가 자동으로 타입을 추론합니다.

Kotlin:

val x = 10  // Inferred as Int
val name = "Alice"  // Inferred as String
// x = "text"  // Error: type mismatch

Rust:

let x = 10;  // Inferred as i32 (32-bit integer)
let y = 3.14;  // Inferred as f64 (64-bit float)

TypeScript:

let count = 5;  // Inferred as number
// count = "text";  // Error: Type 'string' is not assignable to type 'number'

이점: 동적 타이핑의 간결함 + 정적 타이핑의 안전성.


추상 데이터 타입(Abstract Data Types, ADTs)

추상 데이터 타입인터페이스(어떤 연산이 가능한지)와 구현(그 연산이 어떻게 수행되는지)을 분리합니다.

핵심 아이디어: 사용자는 내부 세부사항을 알 필요 없이 잘 정의된 인터페이스를 통해 ADT와 상호작용합니다.

스택(Stack) ADT

인터페이스 (연산): - push(item): 맨 위에 항목 추가 - pop(): 맨 위 항목 제거 및 반환 - peek(): 제거하지 않고 맨 위 항목 보기 - is_empty(): 스택이 비어있는지 확인

구현 1: 배열 사용

Python:

class ArrayStack:
    def __init__(self):
        self._data = []

    def push(self, item):
        self._data.append(item)

    def pop(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self._data.pop()

    def peek(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self._data[-1]

    def is_empty(self):
        return len(self._data) == 0


# Usage (same interface regardless of implementation)
stack = ArrayStack()
stack.push(10)
stack.push(20)
print(stack.pop())  # 20

구현 2: 연결 리스트 사용

Python:

class Node:
    def __init__(self, value, next=None):
        self.value = value
        self.next = next


class LinkedStack:
    def __init__(self):
        self._top = None
        self._size = 0

    def push(self, item):
        self._top = Node(item, self._top)
        self._size += 1

    def pop(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        value = self._top.value
        self._top = self._top.next
        self._size -= 1
        return value

    def peek(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self._top.value

    def is_empty(self):
        return self._top is None


# Usage (same interface!)
stack = LinkedStack()
stack.push(10)
stack.push(20)
print(stack.pop())  # 20

핵심 포인트: 인터페이스는 동일합니다. 사용자는 배열 기반인지 연결 리스트 기반인지 알 필요가 없습니다. 이것이 추상화입니다.


큐(Queue) ADT

인터페이스: - enqueue(item): 뒤쪽에 추가 - dequeue(): 앞쪽에서 제거 및 반환 - is_empty(): 비어있는지 확인

Java 구현:

import java.util.LinkedList;

public interface Queue<T> {
    void enqueue(T item);
    T dequeue();
    boolean isEmpty();
}

public class LinkedQueue<T> implements Queue<T> {
    private LinkedList<T> data = new LinkedList<>();

    public void enqueue(T item) {
        data.addLast(item);
    }

    public T dequeue() {
        if (isEmpty()) {
            throw new RuntimeException("Queue is empty");
        }
        return data.removeFirst();
    }

    public boolean isEmpty() {
        return data.isEmpty();
    }
}

맵/딕셔너리(Map/Dictionary) ADT

인터페이스: - put(key, value): 키-값 쌍 저장 - get(key): 키로 값 검색 - remove(key): 키-값 쌍 삭제 - contains(key): 키 존재 여부 확인

Python (내장 dict 사용):

# Python's dict is an implementation of the Map ADT
phonebook = {}
phonebook["Alice"] = "555-1234"
phonebook["Bob"] = "555-5678"

print(phonebook["Alice"])  # "555-1234"
print("Alice" in phonebook)  # True

JavaScript (Map 사용):

let map = new Map();
map.set("Alice", "555-1234");
map.set("Bob", "555-5678");

console.log(map.get("Alice"));  // "555-1234"
console.log(map.has("Alice"));  // true

제네릭과 템플릿(Generics and Templates)

제네릭(Generics) (Java, C#, TypeScript)과 템플릿(Templates) (C++)은 모든 타입과 작동하는 코드를 작성할 수 있게 합니다.

Java 제네릭

제네릭 없이:

// Must use Object, lose type safety
public class Box {
    private Object item;

    public void set(Object item) {
        this.item = item;
    }

    public Object get() {
        return item;
    }
}

Box box = new Box();
box.set("Hello");
String s = (String) box.get();  // Explicit cast needed

제네릭 사용:

public class Box<T> {
    private T item;

    public void set(T item) {
        this.item = item;
    }

    public T get() {
        return item;
    }
}

Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String s = stringBox.get();  // No cast needed, type-safe

Box<Integer> intBox = new Box<>();
intBox.set(42);
// intBox.set("text");  // Compile error

C++ 템플릿

template <typename T>
class Box {
private:
    T item;

public:
    void set(T value) {
        item = value;
    }

    T get() const {
        return item;
    }
};

// Usage
Box<int> intBox;
intBox.set(42);
std::cout << intBox.get() << std::endl;  // 42

Box<std::string> stringBox;
stringBox.set("Hello");
std::cout << stringBox.get() << std::endl;  // Hello

TypeScript 제네릭

class Box<T> {
    private item: T;

    set(value: T): void {
        this.item = value;
    }

    get(): T {
        return this.item;
    }
}

let stringBox = new Box<string>();
stringBox.set("Hello");
console.log(stringBox.get());  // Hello

let numberBox = new Box<number>();
numberBox.set(42);
console.log(numberBox.get());  // 42

이점: 코드 재사용, 타입 안전성, 런타임 오버헤드 없음 (Java는 타입 소거, C++는 템플릿 인스턴스화).


대수적 데이터 타입(Algebraic Data Types)

합 타입(Sum Types) (열거형, 태그 유니온)

값은 여러 변형 중 하나가 될 수 있습니다.

Rust enum:

enum Status {
    Success,
    Error(String),
    Loading,
}

let result = Status::Error("Network timeout".to_string());

match result {
    Status::Success => println!("Success!"),
    Status::Error(msg) => println!("Error: {}", msg),
    Status::Loading => println!("Loading..."),
}

TypeScript 식별 유니온:

type Status =
    | { kind: "success"; data: string }
    | { kind: "error"; message: string }
    | { kind: "loading" };

function handleStatus(status: Status) {
    switch (status.kind) {
        case "success":
            console.log("Data:", status.data);
            break;
        case "error":
            console.log("Error:", status.message);
            break;
        case "loading":
            console.log("Loading...");
            break;
    }
}

곱 타입(Product Types) (튜플, 레코드)

값은 여러 필드를 함께 포함합니다.

Rust struct:

struct Point {
    x: i32,
    y: i32,
}

let p = Point { x: 10, y: 20 };
println!("({}, {})", p.x, p.y);

TypeScript:

type Point = {
    x: number;
    y: number;
};

let p: Point = { x: 10, y: 20 };
console.log(`(${p.x}, ${p.y})`);

Null과 그 문제들

10억 달러 실수 — Tony Hoare (null 참조 발명자):

"나는 그것을 나의 10억 달러 실수라고 부릅니다. 1965년에 null 참조를 발명한 것이었습니다... 이것은 셀 수 없이 많은 오류, 취약점, 시스템 충돌을 초래했습니다."

문제

String name = getUserName();
int length = name.length();  // NullPointerException if name is null

많은 언어에서 변수가 null일 수 있어 런타임 충돌로 이어집니다.

해결책: Option/Maybe 타입

Rust의 Option:

fn divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 {
        None
    } else {
        Some(a / b)
    }
}

let result = divide(10, 2);
match result {
    Some(value) => println!("Result: {}", value),
    None => println!("Cannot divide by zero"),
}

// Or use combinators
let result = divide(10, 2).unwrap_or(0);  // Default to 0 if None

Java의 Optional:

import java.util.Optional;

public Optional<String> findUserName(int id) {
    if (id == 1) {
        return Optional.of("Alice");
    } else {
        return Optional.empty();
    }
}

Optional<String> name = findUserName(1);
name.ifPresent(n -> System.out.println("Name: " + n));

String result = name.orElse("Unknown");  // Default value

TypeScript:

function divide(a: number, b: number): number | null {
    return b === 0 ? null : a / b;
}

let result = divide(10, 2);
if (result !== null) {
    console.log("Result:", result);
} else {
    console.log("Cannot divide by zero");
}

이점: 값의 부재를 명시적으로 처리하도록 강제합니다. 더 이상 NullPointerException 놀라움이 없습니다.


타입 주석과 문서화

동적 타입 언어에서도 타입을 문서화할 수 있고 (해야 합니다).

Python 타입 힌트

def greet(name: str) -> str:
    """
    Greet a person by name.

    Args:
        name: The person's name

    Returns:
        A greeting message
    """
    return f"Hello, {name}!"

# Type checker (mypy) can catch errors:
# greet(42)  # Error: Argument 1 has incompatible type "int"; expected "str"

JavaScript with JSDoc

/**
 * Calculate the area of a rectangle
 * @param {number} width - The width
 * @param {number} height - The height
 * @returns {number} The area
 */
function area(width, height) {
    return width * height;
}

TypeScript

function area(width: number, height: number): number {
    return width * height;
}

// area("5", 10);  // Error: Argument of type 'string' is not assignable to 'number'

연습 문제

연습 문제 1: 타입 시스템 분석

이 JavaScript 코드가 주어졌을 때:

let x = "10";
let y = 5;
console.log(x + y);  // "105"
console.log(x - y);  // 5
  1. +"105"를 생성하지만 -5를 생성하나요?
  2. Python에서 작동할까요? 왜 또는 왜 안 될까요?
  3. 이것은 강한 타이핑인가요 약한 타이핑인가요? 정적인가요 동적인가요?

연습 문제 2: 스택 ADT 구현

선택한 언어로 스택 ADT를 구현하세요: - 배열을 기본 자료구조로 사용 - push, pop, peek, is_empty 구현 - 정수로 테스트하고, 그 다음 문자열로 테스트 (같은 코드가 작동해야 함)

연습 문제 3: 제네릭

잠재적으로 다른 타입의 두 값을 담는 제네릭 Pair<T, U> 클래스 구현: - 생성자: Pair(T first, U second) - 메서드: getFirst(), getSecond(), setFirst(T), setSecond(U) - 테스트: ("Alice", 30)에 대한 Pair<String, Integer>

연습 문제 4: Option 타입

간단한 Option<T> 타입 구현 (Rust나 Java와 유사): - Some(value): 값 포함 - None: 비어있음 - 메서드: - isSome(): Some이면 true 반환 - isNone(): None이면 true 반환 - unwrap(): 값 반환 또는 None이면 오류 던짐 - unwrapOr(default): 값 반환 또는 None이면 기본값

연습 문제 5: ADT 설계

도서관 시스템을 위한 ADT 설계: - 어떤 연산을 지원해야 하나요? - 책 추가 - 책 제거 - 제목, 저자, ISBN으로 검색 - 책 대출 - 책 반납 - 인터페이스 정의 (아직 구현하지 않음) - 고려사항: 어떤 자료구조가 이것을 구현할 수 있나요?

연습 문제 6: Null 안전성

이 Java 코드를 Optional을 사용하도록 리팩토링하세요:

public String getUserEmail(int userId) {
    if (userId == 1) {
        return "alice@example.com";
    }
    return null;
}

String email = getUserEmail(1);
System.out.println(email.toUpperCase());  // Potential NullPointerException

Optional<String>을 사용해 안전하게 만드세요.


요약

  • 타입은 데이터를 분류: 기본형 (int, float, bool, char, string), 복합형 (배열, 구조체, 튜플)
  • 타입 시스템:
  • 정적 vs 동적: 컴파일 시점 vs 런타임 검사
  • 강한 vs 약한: 엄격한 vs 허용적 타입 규칙
  • 타입 추론: 자동 타입 추론
  • 추상 데이터 타입: 인터페이스 (무엇) vs 구현 (어떻게)
  • 스택, 큐, 맵/딕셔너리
  • 제네릭/템플릿: 재사용성과 타입 안전성을 위한 타입 매개변수화 코드
  • 대수적 데이터 타입: 합 타입 (열거형), 곱 타입 (튜플/레코드)
  • Null 안전성: Option/Maybe/Optional 타입으로 null 관련 충돌 방지
  • 문서화: 타입 주석으로 코드를 더 명확하게 하고 오류를 일찍 포착

핵심 통찰: 추상화는 복잡성 관리에 관한 것입니다. ADT는 구현 세부사항을 걱정하지 않고 더 높은 수준에서 생각할 수 있게 합니다. 타입은 오류를 포착하고 코드를 더 유지보수 가능하게 만드는 데 도움이 됩니다.


탐색

← 이전: Programming Paradigms | 다음: Control Flow Patterns →

to navigate between lessons