Modern Object Pascal Introduction for Programmers | Castle Game Engine
프로그래머를 위한 현대 Object Pascal 입문
Castle Game Engine의 Object Pascal 입문서가 스트림 기반 입출력, 제네릭 컨테이너, 객체 복사를 설명합니다. FPC와 Delphi 사이의 호환성을 고려한 컨테이너 선택부터 TPersistent.Assign의 상속 규칙까지 코드 예제로 다룹니다.
- 주제
AI 요약
Castle Game Engine의 Object Pascal 입문서 일부는 파일과 URL에서 데이터를 읽고 쓰는 방법, 제네릭 컨테이너 사용법, 클래스 인스턴스의 상태를 복사하는 방법을 설명합니다. 예제는 Free Pascal Compiler(FPC)와 Delphi의 문법 차이를 조건부 컴파일로 처리합니다.
TStream으로 입출력하기
일반적인 입출력에는 TStream과 하위 클래스를 사용하라고 안내합니다. 파일에는 TFileStream, 메모리에는 TMemoryStream, 문자열에는 TStringStream을 예로 듭니다. 샘플은 TFileStream으로 정수 값을 파일에 기록한 뒤 다시 읽습니다. 스트림은 try-finally 블록에서 FreeAndNil로 해제합니다.
Castle Game Engine에서는 Download 함수가 URL을 스트림으로 엽니다. HTTP·HTTPS 주소뿐 아니라 일반 파일과 Android 에셋도 같은 방식으로 다룹니다. 게임 데이터 디렉터리 안의 파일은 castle-data:/ 접두사를 붙여 엽니다. 텍스트 파일에는 스트림을 감싸는 TCastleTextReader를 권하며, ReadLn과 Eof를 이용해 한 줄씩 읽는 예를 제시합니다.
제네릭 컨테이너
리스트나 사전을 만들 때는 타입 안전성과 유연성을 이유로 제네릭 컨테이너를 권합니다. FPC에는 여러 제네릭 컨테이너 라이브러리가 있지만, 글은 FPC와 Delphi 양쪽에서 호환되고 기능과 성능을 갖춘 Generics.Collections를 추천합니다. 주요 타입은 값 목록인 TList, 객체 목록인 TObjectList, 키와 값을 연결하는 TDictionary, 객체 소유 기능을 지원하는 TObjectDictionary입니다.
TObjectList는 생성자에 true를 전달하면 항목 객체를 목록이 소유합니다. 목록을 해제할 때 항목도 함께 해제됩니다. TDictionary 예제는 AddOrSetValue로 항목을 넣고 TryGetValue로 찾은 뒤 Keys, Values, 키-값 쌍을 순회합니다. 객체를 직접 관리한다면 제거와 해제를 구분해야 합니다. 반면 TObjectDictionary는 doOwnsValues 옵션을 지정해 값을 자동 해제하도록 만들 수 있습니다. 소유 옵션은 객체에만 써야 합니다. 정수 키처럼 객체가 아닌 값에 doOwnsKeys를 적용하면 실행 중 오류가 발생할 수 있다고 경고합니다.
정렬이나 검색처럼 두 항목을 비교하는 작업에는 comparer를 씁니다. 샘플은 IComparer를 구현하는 비교 함수를 TComparer<T>.Construct로 감싸고, 그 비교 기준에 따라 사과 객체 목록을 정렬합니다. FGL의 TFPGList와 TFPGMap도 대안으로 소개합니다. 다만 이 컨테이너는 타입에 따라 동등 연산자나 크기 비교 연산자가 필요합니다. Castle Game Engine의 별도 제네릭 컨테이너는 비교 연산자를 요구하지 않지만, 엔진 6.3부터 사용 중단 대상이므로 Generics.Collections로 옮기라고 안내합니다.
TPersistent.Assign으로 객체 복사하기
클래스 변수에 대입 연산자를 쓰면 객체 내용이 복사되는 게 아니라 참조가 복사됩니다. 따라서 두 변수가 같은 인스턴스를 가리킵니다. 내용 복사가 필요하면 TPersistent를 상속하고 Assign을 재정의해 복사할 필드를 직접 지정합니다. 예제에서는 기본 클래스가 정수를 복사하고, 하위 클래스가 문자열을 복사한 다음 inherited를 호출해 조상 클래스의 필드도 처리합니다.
Assign 구현에서는 Source의 타입과 상속 관계를 고려해야 합니다. TApple.Assign이 사과 필드를 복사한 뒤 조상인 TFruit.Assign을 호출하면, 다른 하위 타입인 TOrange에서 공통 과일 필드만 복사할 수 있습니다. 반면 관련 없는 TWerewolf를 넘기면 조상까지 올라가 TPersistent.Assign의 예외 처리에 도달합니다. 조상 클래스가 이미 Assign을 재정의했다면 inherited를 호출해야 합니다. 직접 TPersistent를 상속했고 조상에서 Assign을 재정의하지 않았다면, 처리할 수 없는 경우에만 inherited를 호출합니다. TPersistent가 필드를 자동 복사해 주는 것은 아니므로 복사 대상 필드는 직접 지정해야 합니다. TPersistent 하위 클래스의 기본 공개 범위는 published지만, 스트리밍이 필요 없다면 public으로 바꿀 수 있습니다.
Reddit 반응
- @u/Carbon_Gelatin — Delphi인가요?
- @u/FantaZmio — 솔직히 처음에는 “Pascal이라고요? 요즘에도요? 정말요?”라고 생각했습니다. 그래도 모든 것에는 제자리와 때가 있습니다. 어렸을 때 Blitz BASIC 언어를 쓰는 BlitzMax로 게임을 만들어 보려 했던 기억이 납니다. 언어는 아주 단순했지만 정말 재미있었습니다.
- @u/AnnoyedVelociraptor — 2026년입니다. 객체지향 프로그래밍이 좋은 게 아니라는 사실을 아직도 배우지 못했나요?
- @u/Donzulu — 대학 2학년이 Python을 발견했군요.
- @u/AnnoyedVelociraptor — C++, C#, Java, Python, TypeScript를 15년 넘게 써 왔고, 이제야 C와 Rust를 쓰게 됐습니다.
- @u/IvanDSM_ — 2026년에 C++를 버리고 C를 택하는 건 확실히 독특한 선택이네요.
- @u/AnnoyedVelociraptor — 펌웨어를 C로 많이 작성합니다. 그걸 다루고, Rust로 연결하는 일을 합니다.
- @u/IvanDSM_ — 그렇군요. 그냥 가볍게 농담한 거니 신경 쓰지 마세요.
- @u/LIGHTNINGBOLT23 — 그럼 어떤 패러다임을 추천하시나요?
- @u/evilgipsy — 클래스와 상속을 쓰지 않는 것은 무엇이든 괜찮습니다. OOP는 정의하기 어렵다고 생각합니다. OOP라고 부르는 언어만의 고유한 요소는 클래스와 상속뿐이라고 봅니다. 편집: OOP를 싫어한다고 반대 투표부터 하지 말아 주세요. 동의하지 않는다면 논거를 제시하면 어떨까요?
- @u/LIGHTNINGBOLT23 — 클래스와 상속에 본질적으로 잘못된 점이 있나요? OOP를 정의하는 진짜 특징은 동적 디스패치와 다형성이라고 봅니다. 그런 것 없이 상속만 쓰면 합성에 장식을 붙인 셈입니다.
- @u/thussy-obliterator — 동적 디스패치와 다형성은 OOP에만 있는 기능이 아닙니다. OOP만의 기능인 상속 기반 다형성이 큰 문제입니다. 취약한 추상화이기 때문입니다. 상속 관계는 복잡하게 꼬이고, 기반 클래스 메서드를 바꾸면 위험하며, super() 호출도 제대로 처리하기 까다롭습니다. 깊은 클래스 계층에 갇힌 적이 몇 번인지 셀 수도 없습니다. 다이아몬드 문제, 취약한 기반 클래스 문제, 요요 문제 같은 고전적인 문제도 있습니다. 상속은 매우 제한된 문제를 푸는데 OOP 언어에서는 지나치게 중요한 자리를 차지합니다. 요즘은 인터페이스 다형성, 매개변수 다형성, 합집합 타입처럼 더 단순한 추상화를 선호합니다. 코드와 데이터를 한데 묶는 방식도 불편할 때가 많습니다. 함수가 특정 클래스에 들어맞지 않을 수 있고, 데이터는 변경 불가능한 레코드로 표현하는 편이 나을 수 있습니다. 코드를 묶는 추상화로는 모듈이 적절합니다. OOP에서 가져갈 만한 요소는 메시지 전달이지만, 현대 OOP 언어에는 그런 기능이 없고 Erlang 같은 함수형 언어가 상당 부분 대신하고 있습니다.
- @u/LIGHTNINGBOLT23 — 동적 디스패치와 다형성이 OOP만의 기능은 아니지만, 그 점이 논점은 아닙니다. OOP를 이루는 개념 중 어느 것도 OOP만의 것은 아닙니다. 그래서 그 개념들을 더 많이 결합할수록 OOP라는 말이 의미를 갖습니다. 상속 기반 다형성도 JavaScript처럼 프로토타입 기반으로 구현할 수 있으니 OOP에만 있다고 할 수 없습니다. 상속도 다른 프로그래밍 개념처럼 지나치게 쓰면 문제가 됩니다.
- @u/evilgipsy — OOP를 정의하기 어렵다는 말이 바로 그 뜻입니다. 다형성과 동적 디스패치는 OOP가 아닌 함수형이나 절차형 패러다임에서도 쓸 수 있습니다. OOP를 정의하려고 하면 대개 그 패러다임만의 것이 아닌 유용한 기법들을 나열합니다. 클래스와 상속이 본질적으로 잘 푸는 문제를 아직 보지 못했습니다. 캡슐화, 네임스페이스, 추상화, 모듈화처럼 클래스가 유용한 이유로 드는 것들도 클래스만의 특징은 아닙니다. 제가 OOP에서 고유하다고 보는 변경 가능한 객체의 정체성, 지연 바인딩 디스패치, 상속 등은 복잡성을 더합니다. 제가 반대하는 것은 OOP가 특별히 유용한 패러다임이라서 거의 모든 초보자가 배워야 한다는 암묵적인 주장입니다. 그 주장은 근거를 제시해야 합니다.
- @u/LIGHTNINGBOLT23 — 동적 디스패치와 다형성을 다른 패러다임에서도 쓸 수 있다는 점에는 동의합니다. 하지만 클래스와 상속도 다른 패러다임에서 구현할 수 있습니다. OOP를 반대하는 쪽이 OOP를 절반으로 잘라 놓고 그 절반만 보고 전체를 평가한다고 생각해서 반대 투표를 하는 것 같습니다. OOP의 범위는 Smalltalk처럼 ‘객체’를 진지하게 다루는지에 따라 정의하기 어려워집니다. 초보자에게 OOP를 가르치는 이유는 데이터베이스 질의 언어를 배울 때 SQL을 가르치는 이유와 같습니다. 널리 쓰이기 때문입니다. 영어를 싫어하더라도 영어를 모르면 불리한 것과 비슷합니다.
- @u/Hubba_Bubba_Lova — 함수형 프로그래밍이 더 나은 대안인가요, 아니면 다른 것이 있나요?
- @u/AnnoyedVelociraptor — 함수형은 좋습니다. C#보다 언제든 함수형을 택하겠습니다. 다만 Rust와는 전혀 다릅니다.
- @u/FantaZmio — 더 낫다는 뜻은 아닙니다. 다만 언어와 생태계가 제대로 지원하고 실제 서비스에 쓸 수 있게 해 준다면 좋습니다. 예를 들어 Kotlin, Scala, Swift처럼요. 관리자가 “다 망가졌으니 지금 당장 고쳐야 해요”라고 할 때 빠르게 임시 해결책을 쓸 수 있어야 합니다.
- @u/thussy-obliterator — 저는 작업 수준에 따라 함수형이나 절차형을 선호합니다.
원문: Castle Game Engine / 번역·요약: Trawling