상속과 다형성 - virtual 과 override
이 장에서 배우는 것
앞 장에서는 클래스로 객체를 만들고, 다 쓴 객체를 직접 해제하는 방법을 익혔다. 이 장에서는 이미 만든 클래스를 바탕으로 더 구체적인 클래스를 만드는 방법을 다룬다. 이를 상속(inheritance)이라 한다. 상속을 쓰면 서로 다른 종류의 객체를 같은 방식으로 다루면서도 호출했을 때 각자 맞는 동작을 하게 만들 수 있다. 이 성질을 다형성(polymorphism)이라 한다.
카페 계산대 예제에서는 메뉴 항목을 음료와 베이커리로 나누고, 가격 계산 방식이 서로 다른 항목을 하나의 배열에 담아 처리한다. 마지막에는 클래스 계층과 별개로 "할 수 있는 일"만 약속하는 인터페이스를 짧게 맛본다.
- 부모 클래스를 지정해 자식 클래스를 만들고, 부모의 기능을 그대로 물려받는다.
virtual,override,inherited가 각각 무엇을 하는지 설명한다.- 본문이 없는
abstract메서드로 "자식이 반드시 채울 자리"를 만든다. is와as로 객체의 실제 클래스를 확인하고 안전하게 바꿔 다룬다.TObject가 모든 클래스의 뿌리라는 점과 인터페이스의 기본 사용법을 안다.
문제 상황
카페 계산대 프로그램에 메뉴가 늘어난다. 처음에는 이름과 가격만 있으면 충분했다. 그런데 아이스 음료는 500원이 더 붙고, 마감 시간의 베이커리는 10%를 깎아 판다. 앞 장까지의 방식으로 하나의 클래스에 Kind(종류) 필드를 두면 가격 계산 함수가 이렇게 된다.
function CalcPrice(Item: TMenuItem): Integer;
begin
if Item.Kind = '음료' then
Result := Item.BasePrice + ...
else if Item.Kind = '베이커리' then
Result := Item.BasePrice * 90 div 100
else
...
end;
새 종류가 생길 때마다 이런 if 사슬을 가진 함수를 전부 찾아 고쳐야 한다. 하나라도 빠뜨리면 영수증과 정산의 금액이 서로 달라진다. 이 장의 목표는 "종류마다 자기 가격을 스스로 계산한다"는 구조로 바꾸는 것이다. 그러면 계산대 코드는 Item.Price만 부르면 되고, 새 종류는 클래스 하나를 추가하는 것으로 끝난다.
상속과 가상 메서드
상속: 물려받고 덧붙이기
클래스 이름 뒤 괄호에 부모 클래스를 적으면 상속이 된다. 자식 클래스는 부모의 필드, 메서드, 프로퍼티를 모두 가지며 거기에 자기 것을 더한다. 부모를 적지 않은 클래스는 자동으로 TObject의 자식이다. 따라서 모든 클래스는 거슬러 올라가면 TObject에 닿는다. TObject는 Create, Free, ClassName 같은 모든 객체의 공통 기능을 갖고 있다.
상속 관계는 "~는 ~의 한 종류다"로 읽는다. 아이스 라떼는 음료이고, 음료는 메뉴 항목이다. 이 문장이 어색하면 상속이 아니라 필드로 포함하는 쪽이 맞는 설계다.
virtual 과 override: 호출할 때 고르기
부모 타입의 변수에 자식 객체를 담을 수 있다. Item: TMenuItem 변수에 TDrink 객체를 넣는 것이 그 예다. 이때 Item.Price를 호출하면 어느 구현이 실행될지가 문제다. 메서드를 virtual로 선언하지 않으면 변수에 적힌 타입, 즉 TMenuItem의 구현이 컴파일 시점에 고정된다. virtual로 선언하고 자식에서 override로 다시 정의하면, 실행 시점에 객체의 실제 클래스를 보고 구현을 고른다.
그림처럼 가상 메서드를 가진 객체는 자기 클래스의 메서드 표(VMT, virtual method table)를 가리키는 값을 품고 있다. 호출 시점에 그 표를 찾아가므로, 변수 타입이 같아도 결과가 달라진다.
| 지시어 | 어디에 쓰나 | 의미 | 호출 구현을 정하는 때 |
|---|---|---|---|
| (없음) | 일반 메서드 | 변수에 적힌 타입의 구현을 쓴다 | 컴파일 시점 |
virtual | 부모 | 자식이 바꿔 쓸 수 있게 열어 둔다 | 실행 시점 |
override | 자식 | 부모의 가상 메서드를 새로 정의한다 | 실행 시점 |
abstract | 부모(virtual과 함께) | 본문 없이 자리만 만든다 | 실행 시점 |
inherited: 부모 구현을 이어 쓰기
자식이 메서드를 재정의하더라도 부모의 계산을 버릴 필요는 없다. 본문 안에서 inherited Price라고 쓰면 부모 클래스의 Price를 호출한다. 아이스 음료의 가격은 "부모가 계산한 기본 가격 + 500"으로 쓰는 것이 부모의 규칙이 바뀌어도 따라가므로 안전하다. 생성자와 소멸자에서도 inherited Create(...), inherited Destroy로 부모의 초기화와 정리를 이어 부른다. 앞 장에서 inherited Create를 한 줄 적었던 이유가 이것이다.
abstract: 자식이 채울 자리
메뉴 항목의 종류 이름(Kind)은 부모가 정할 수 없다. 이럴 때 function Kind: string; virtual; abstract;처럼 선언하면 본문 없이 자리만 만든다. 모든 자식은 이 메서드를 override로 구현해야 한다. 구현하지 않은 클래스로 만든 객체는 그 메서드를 부르는 순간 실행 오류를 낸다. 그래서 abstract 메서드가 남아 있는 클래스는 직접 Create하지 않고 부모 역할로만 쓴다. fpc는 이런 객체를 만들려는 코드에 경고를 낸다.
is, as 와 인터페이스
is 와 as: 실제 클래스 확인하기
Item is TDrink는 Item이 가리키는 객체가 TDrink이거나 그 자식일 때 참이다. 변수 타입이 아니라 객체의 실제 클래스를 본다. Item as TDrink는 같은 조건에서 TDrink 타입으로 바꾼 값을 돌려주고, 조건이 맞지 않으면 실행 오류를 낸다. 그래서 보통 is로 먼저 확인한 뒤 as를 쓴다. 오류를 받아 처리하는 방법은 다음 장에서 다룬다.
자식에만 있는 기능(Iced 프로퍼티 등)은 부모 타입 변수로는 접근할 수 없으므로 이때 as가 필요하다. 다만 is 검사가 여기저기 늘어나면 가상 메서드로 풀 수 있는 문제를 거꾸로 풀고 있다는 신호일 수 있다. 공통 동작은 가상 메서드에, 정말 특정 클래스에만 있는 기능은 is와 as에 맡긴다.
인터페이스 맛보기
클래스 상속은 한 줄기 계통만 만든다. 서로 관계없는 클래스에게 같은 역할을 시키고 싶을 때는 인터페이스(interface)를 쓴다. 인터페이스는 메서드의 이름과 모양만 적은 약속이고, 구현은 클래스가 맡는다. 이 장의 예제에서는 쿠폰의 "이름을 말하고, 금액에 적용한다"는 약속을 IDiscount로 만든다.
인터페이스를 구현하는 클래스는 TInterfacedObject를 부모로 삼는다. 이 부모는 참조 횟수를 세어 마지막 참조가 사라질 때 객체를 스스로 해제한다. 그래서 인터페이스 변수로만 들고 있는 객체에는 Free를 부르지 않는다. 클래스 변수로 들고 Free하는 방식과 섞으면 이중 해제가 일어날 수 있다.
| 구분 | 클래스 상속 | 인터페이스 | 해제 |
|---|---|---|---|
| 정하는 것 | 필드와 구현을 물려준다 | 메서드 약속만 정한다 | - |
| 부모 수 | 하나 | 여러 개 구현 가능 | - |
| 클래스 변수로 든 객체 | - | - | 직접 Free |
| 인터페이스 변수로 든 객체 | - | - | 참조가 사라지면 자동 |
Delphi 와 다른 점: 이 장의 문법은 대부분 같다. 다만 Delphi는
uses System.SysUtils처럼 네임스페이스를 붙이고,as로 인터페이스를 얻거나Supports를 쓰려면 인터페이스에 GUID를 붙여야 한다. 이 장의 예제는 인터페이스 변수에 대입만 하므로 GUID가 필요 없다.
완성 코드
아래를 main.pas 한 파일로 저장한다. TMenuItem은 abstract 메서드가 있어 직접 만들지 않고, TDrink와 TBakery만 만든다.
{$mode objfpc}{$H+}
program Main;
uses
SysUtils;
type
{ 할인 규칙: 어떻게 구현하는지는 묻지 않고, 할 수 있는 일만 약속한다 }
IDiscount = interface
function Title: string;
function Apply(Amount: Integer): Integer;
end;
TMenuItem = class
private
FName: string;
FBasePrice: Integer;
public
constructor Create(const AName: string; ABasePrice: Integer);
destructor Destroy; override;
function Kind: string; virtual; abstract;
function Price: Integer; virtual;
function Describe: string; virtual;
property Name: string read FName;
end;
TDrink = class(TMenuItem)
private
FIced: Boolean;
public
constructor Create(const AName: string; ABasePrice: Integer; AIced: Boolean);
destructor Destroy; override;
function Kind: string; override;
function Price: Integer; override;
function Describe: string; override;
property Iced: Boolean read FIced;
end;
TBakery = class(TMenuItem)
private
FClosingSale: Boolean;
public
constructor Create(const AName: string; ABasePrice: Integer; AClosingSale: Boolean);
function Kind: string; override;
function Price: Integer; override;
function Describe: string; override;
function WarmNote: string;
end;
TPercentOff = class(TInterfacedObject, IDiscount)
private
FPercent: Integer;
public
constructor Create(APercent: Integer);
function Title: string;
function Apply(Amount: Integer): Integer;
end;
TWonOff = class(TInterfacedObject, IDiscount)
private
FWon: Integer;
public
constructor Create(AWon: Integer);
function Title: string;
function Apply(Amount: Integer): Integer;
end;
{ TMenuItem }
constructor TMenuItem.Create(const AName: string; ABasePrice: Integer);
begin
inherited Create;
FName := AName;
FBasePrice := ABasePrice;
end;
destructor TMenuItem.Destroy;
begin
Writeln(' 해제: ', FName);
inherited Destroy;
end;
function TMenuItem.Price: Integer;
begin
Result := FBasePrice;
end;
function TMenuItem.Describe: string;
begin
Result := Format('[%s] %s %d원', [Kind, FName, Price]);
end;
{ TDrink }
constructor TDrink.Create(const AName: string; ABasePrice: Integer; AIced: Boolean);
begin
inherited Create(AName, ABasePrice);
FIced := AIced;
end;
destructor TDrink.Destroy;
begin
Writeln(' 음료 정리: ', Name);
inherited Destroy;
end;
function TDrink.Kind: string;
begin
Result := '음료';
end;
function TDrink.Price: Integer;
begin
Result := inherited Price;
if FIced then
Result := Result + 500;
end;
function TDrink.Describe: string;
begin
Result := inherited Describe;
if FIced then
Result := Result + ' (아이스)';
end;
{ TBakery }
constructor TBakery.Create(const AName: string; ABasePrice: Integer; AClosingSale: Boolean);
begin
inherited Create(AName, ABasePrice);
FClosingSale := AClosingSale;
end;
function TBakery.Kind: string;
begin
Result := '베이커리';
end;
function TBakery.Price: Integer;
begin
Result := inherited Price;
if FClosingSale then
Result := Result * 90 div 100;
end;
function TBakery.Describe: string;
begin
Result := inherited Describe;
if FClosingSale then
Result := Result + ' (마감 할인)';
end;
function TBakery.WarmNote: string;
begin
Result := '오븐에 데워 제공';
end;
{ 쿠폰 }
constructor TPercentOff.Create(APercent: Integer);
begin
inherited Create;
FPercent := APercent;
end;
function TPercentOff.Title: string;
begin
Result := Format('%d%% 할인', [FPercent]);
end;
function TPercentOff.Apply(Amount: Integer): Integer;
begin
Result := Amount - Amount * FPercent div 100;
end;
constructor TWonOff.Create(AWon: Integer);
begin
inherited Create;
FWon := AWon;
end;
function TWonOff.Title: string;
begin
Result := Format('%d원 쿠폰', [FWon]);
end;
function TWonOff.Apply(Amount: Integer): Integer;
begin
Result := Amount - FWon;
if Result < 0 then
Result := 0;
end;
var
Menu: array[0..3] of TMenuItem;
Item: TMenuItem;
C: TClass;
Coupon: IDiscount;
Line: string;
I, Total: Integer;
begin
Menu[0] := TDrink.Create('아메리카노', 3000, False);
Menu[1] := TDrink.Create('카페라떼', 3500, True);
Menu[2] := TBakery.Create('크루아상', 3200, False);
Menu[3] := TBakery.Create('스콘', 2800, True);
Writeln('== 메뉴 ==');
Total := 0;
for I := 0 to High(Menu) do
begin
Writeln(Menu[I].Describe);
Total := Total + Menu[I].Price;
end;
Writeln('합계: ', Total, '원');
Writeln('== 클래스 확인 ==');
for I := 0 to High(Menu) do
begin
Item := Menu[I];
Line := Item.ClassName + ' <- ' + Item.ClassParent.ClassName;
if Item is TDrink then
begin
if (Item as TDrink).Iced then
Line := Line + ' : 아이스 음료'
else
Line := Line + ' : 따뜻한 음료';
end
else if Item is TBakery then
Line := Line + ' : ' + (Item as TBakery).WarmNote;
Writeln(Line);
end;
Line := '';
C := TBakery;
while C <> nil do
begin
Line := Line + C.ClassName;
C := C.ClassParent;
if C <> nil then
Line := Line + ' -> ';
end;
Writeln('계층: ', Line);
Writeln('== 쿠폰 ==');
Coupon := TPercentOff.Create(10);
Writeln(Coupon.Title, ': ', Total, '원 -> ', Coupon.Apply(Total), '원');
Coupon := TWonOff.Create(1000);
Writeln(Coupon.Title, ': ', Total, '원 -> ', Coupon.Apply(Total), '원');
Writeln('== 해제 ==');
for I := 0 to High(Menu) do
Menu[I].Free;
end.
줄별 해설
IDiscount: interface 안에는 메서드의 머리만 있다. 필드와 본문이 없다. 이 약속을 지키는 클래스는 Title과 Apply를 같은 모양으로 구현해야 한다.
TMenuItem: Kind는 virtual; abstract;라서 본문이 없다. Price와 Describe는 virtual이므로 자식이 바꿔 쓸 수 있다. 소멸자 Destroy는 TObject에서 이미 가상이어서 override로 다시 정의한다. 이 덕분에 TMenuItem 타입 변수로 Free해도 실제 클래스의 소멸자가 불린다.
TMenuItem.Describe: 본문에서 Kind와 Price를 부른다. 이 코드는 부모에 있지만, 객체가 TDrink이면 TDrink.Kind와 TDrink.Price가 실행된다. 부모의 메서드 안에서도 가상 호출은 실제 클래스를 따른다.
TDrink: 생성자는 부모와 매개변수가 다르다. 먼저 inherited Create(AName, ABasePrice)로 부모 몫의 초기화를 끝내고 자기 필드를 채운다. Price는 inherited Price로 기본 가격을 받아 아이스일 때 500을 더한다. Describe는 부모가 만든 문장 뒤에 표시를 덧붙인다. 소멸자는 자기 몫을 먼저 출력한 뒤 inherited Destroy로 부모에게 넘긴다. 생성은 부모가 먼저, 해제는 자식이 먼저인 순서다.
TBakery: 마감 할인은 Result * 90 div 100으로 계산한다. 정수 연산만 쓰므로 출력이 항상 같다. WarmNote는 이 클래스에만 있는 일반 메서드다. 소멸자를 따로 정의하지 않았으므로 부모의 소멸자가 그대로 쓰인다.
TPercentOff, TWonOff: class(TInterfacedObject, IDiscount)에서 첫째 이름은 부모 클래스, 둘째 이름은 구현할 인터페이스다. 퍼센트 문자를 출력하려면 Format 안에서 %%로 적는다.
메인 블록의 메뉴 루프: 배열 원소 타입은 TMenuItem이지만 담긴 객체는 TDrink와 TBakery다. Menu[I].Describe와 Menu[I].Price를 같은 코드로 부르는데 결과는 객체마다 다르다. 합계는 12720원이다.
클래스 확인 루프: ClassName은 실제 클래스 이름이고, ClassParent는 부모 클래스 정보(TClass)다. if Item is TDrink로 확인한 뒤에 Item as TDrink로 바꿔 Iced를 읽는다. 뒤따르는 while 루프는 TBakery에서 부모를 따라 올라가며 nil(더 이상 부모가 없음)이 될 때까지 이름을 이어 붙인다. 마지막에 TObject가 나오는 것을 확인할 수 있다.
쿠폰 구간: Coupon은 인터페이스 변수다. 두 번째 대입에서 앞의 객체의 참조가 사라져 자동으로 해제된다. 그래서 쿠폰 객체에는 Free가 없다.
해제 구간: 앞 장에서 배운 대로 만든 객체는 직접 Free한다. 출력의 순서가 "음료 정리"가 먼저이고 "해제"가 그 다음인 것은 TDrink.Destroy가 inherited Destroy를 마지막에 부르기 때문이다.
실행 결과
$ fpc main.pas
$ ./main
== 메뉴 ==
[음료] 아메리카노 3000원
[음료] 카페라떼 4000원 (아이스)
[베이커리] 크루아상 3200원
[베이커리] 스콘 2520원 (마감 할인)
합계: 12720원
== 클래스 확인 ==
TDrink <- TMenuItem : 따뜻한 음료
TDrink <- TMenuItem : 아이스 음료
TBakery <- TMenuItem : 오븐에 데워 제공
TBakery <- TMenuItem : 오븐에 데워 제공
계층: TBakery -> TMenuItem -> TObject
== 쿠폰 ==
10% 할인: 12720원 -> 11448원
1000원 쿠폰: 12720원 -> 11720원
== 해제 ==
음료 정리: 아메리카노
해제: 아메리카노
음료 정리: 카페라떼
해제: 카페라떼
해제: 크루아상
해제: 스콘
실무에서 자주 틀리는 것
부모에 virtual 이 없는데 같은 이름으로 다시 쓴다
부모 메서드에 virtual이 없으면 자식에서 같은 이름을 선언해도 재정의가 아니라 가리기(hiding)가 된다. 자식 타입 변수로 부르면 자식 것이, 부모 타입 변수로 부르면 부모 것이 실행되어 결과가 달라진다. 컴파일러가 가려졌다는 경고를 내므로 경고를 무시하지 않는다.
type
TMenuItem = class
function Price: Integer; // virtual 이 없다
end;
TDrink = class(TMenuItem)
function Price: Integer; // 부모 것을 가릴 뿐이다
end;
var Item: TMenuItem;
...
Item := TDrink.Create('라떼', 3500, True);
Writeln(Item.Price); // 부모 구현이 실행된다
고친 코드는 부모에 virtual, 자식에 override를 적는 것이다.
TMenuItem = class
function Price: Integer; virtual;
end;
TDrink = class(TMenuItem)
function Price: Integer; override;
end;
inherited 를 빼고 부모의 계산을 복사한다
자식이 부모 로직을 직접 다시 쓰면 부모 규칙이 바뀔 때 자식이 따라가지 못한다. 필드가 private이면 아예 접근도 안 된다.
function TDrink.Price: Integer;
begin
Result := 3000 + 500; // 기본 가격을 직접 적었다
end;
function TDrink.Price: Integer;
begin
Result := inherited Price;
Result := Result + 500;
end;
abstract 메서드가 남은 클래스로 객체를 만든다
컴파일은 되더라도 fpc가 경고를 내고, Kind를 부르는 순간 프로그램이 오류로 멈춘다.
Item := TMenuItem.Create('이름만 있는 항목', 1000);
Writeln(Item.Describe); // Describe 안의 Kind 호출에서 실행 오류
Item := TDrink.Create('아메리카노', 3000, False);
Writeln(Item.Describe);
as 를 확인 없이 쓴다
객체가 그 클래스가 아니면 as는 실행 오류를 낸다. 메뉴에 베이커리가 섞여 있는데 모두 음료라고 가정하면 프로그램이 도중에 멈춘다.
for I := 0 to High(Menu) do
Writeln((Menu[I] as TDrink).Iced); // 베이커리 차례에서 오류
for I := 0 to High(Menu) do
if Menu[I] is TDrink then
Writeln((Menu[I] as TDrink).Iced);
한눈에 보기
| 문법 | 예 | 하는 일 | 주의 |
|---|---|---|---|
| 상속 | TDrink = class(TMenuItem) | 부모의 멤버를 물려받는다 | 부모를 쓰지 않으면 TObject |
| 가상 메서드 | function Price: Integer; virtual; | 자식이 바꿀 수 있게 연다 | 선언은 부모에서 |
| 재정의 | function Price: Integer; override; | 실행 시점에 자기 구현이 선택된다 | 모양이 부모와 같아야 한다 |
| 부모 호출 | inherited Price | 부모 구현을 이어 쓴다 | 소멸자는 보통 마지막에 |
| 추상 메서드 | virtual; abstract; | 자식이 채울 자리 | 이 클래스는 직접 만들지 않는다 |
| 클래스 검사 | Item is TDrink | 실제 클래스를 확인한다 | 자식 클래스도 참이다 |
| 클래스 변환 | Item as TDrink | 자식 타입으로 다룬다 | 아니면 실행 오류 |
| 인터페이스 | class(TInterfacedObject, IDiscount) | 메서드 약속을 구현한다 | 인터페이스 변수의 객체는 Free하지 않는다 |
연습 문제
- 메뉴에 원두 봉지를 파는
TBean클래스를 추가한다.TMenuItem을 상속하고Kind는 '원두'로 한다.Price는 기본 가격에 10% 부가세를 더한다(정수 연산, 소수점 아래는 버린다). 기본 가격 12000원이면 가격이 얼마인가. - 다음 코드는 무엇을 출력하는가.
TA와TB의 메서드 본문은 각각Writeln('A.Who'),Writeln('A.Hello'),Writeln('B.Who'),Writeln('B.Hello')이다.type TA = class procedure Who; procedure Hello; virtual; end; TB = class(TA) procedure Who; procedure Hello; override; end; var X: TA; begin X := TB.Create; X.Who; X.Hello; X.Free; end. IDiscount를 구현하는TBulkOff를 만든다. 금액이 10000원 이상이면 500원을 깎고, 그보다 적으면 그대로 돌려준다.Title은 '대량 구매 할인'이다. 메서드 본문만 쓴다.TMenuItem.Create(...)를 직접 호출하면 어떤 일이 생기는지, 그리고 이를 피하는 가장 간단한 규칙이 무엇인지 설명한다.
정답과 해설
1. 12000 + 12000 div 10 = 13200원이다. 클래스는 다음과 같다.
type
TBean = class(TMenuItem)
public
function Kind: string; override;
function Price: Integer; override;
end;
function TBean.Kind: string;
begin
Result := '원두';
end;
function TBean.Price: Integer;
begin
Result := inherited Price;
Result := Result + Result div 10;
end;
생성자는 부모 것(Create(AName, ABasePrice))을 그대로 쓰면 된다. Kind를 빠뜨리면 abstract 메서드가 남아 객체를 만들 때 경고가 난다.
2. 출력은 두 줄이다.
A.Who
B.Hello
Who는 virtual이 아니므로 변수 타입인 TA의 구현이 쓰인다. Hello는 가상이므로 실제 객체인 TB의 구현이 쓰인다. TB.Who는 부모 것을 가리기만 하므로 컴파일러가 경고를 낼 수 있다.
3. 다음과 같이 쓴다.
function TBulkOff.Title: string;
begin
Result := '대량 구매 할인';
end;
function TBulkOff.Apply(Amount: Integer): Integer;
begin
if Amount >= 10000 then
Result := Amount - 500
else
Result := Amount;
end;
클래스 머리는 TBulkOff = class(TInterfacedObject, IDiscount)이다. 사용할 때는 Coupon := TBulkOff.Create;처럼 인터페이스 변수에 넣고 Free는 쓰지 않는다.
4. TMenuItem에는 Kind가 abstract로 남아 있다. fpc가 컴파일할 때 abstract 메서드를 가진 클래스를 만든다는 경고를 낸다. 실행하면 Describe가 Kind를 부르는 순간 "추상 메서드가 호출되었다"는 취지의 실행 오류로 멈춘다. 피하는 규칙은 abstract 메서드가 하나라도 남아 있는 클래스는 부모 타입으로만 쓰고, 객체는 모든 abstract 메서드를 override한 자식 클래스로만 만드는 것이다.
이 장에서 객체를 만들고 해제하는 코드는 모두 오류가 나지 않는 경우만 다뤘다. 도중에 오류가 나면 Free까지 가지 못하고 객체가 남는다. 다음 장의 예외 처리가 이 문제를 해결한다.