상속과 인터페이스
이 장에서 배우는 것
앞 장에서 비품의 이름과 상태를 클래스의 속성으로 묶었다. 비품을 하나의 객체로 다루면 이름을 따로 보관하고 대여 상태를 다른 변수에 보관할 때보다 관련된 정보를 찾기 쉽다. 그러나 비품의 종류가 늘어나면 새로운 고민이 생긴다. 노트북과 프로젝터는 모두 대여할 수 있지만, 안내 문구와 대여 기간은 서로 다르다. 공통 코드를 공유하면서도 종류별 차이를 표현할 방법이 필요하다.
이 장에서는 상속(inheritance)으로 공통 기능을 물려주고, 인터페이스(interface)로 필요한 기능의 약속을 정의한다. 이어서 서로 다른 종류의 비품을 같은 형식의 변수나 매개변수로 받아 처리한다. 예제는 작은 사무실의 비품 대여·반납 관리 콘솔 앱이다. 입력을 받지 않고 정해진 순서로 실행하므로 실행할 때마다 같은 결과를 확인할 수 있다.
Inherits로 공통 상태와 기능을 물려받는 클래스를 만든다.Overridable과Overrides로 종류에 따라 달라지는 동작을 정의한다.MustInherit와MustOverride로 직접 만들 수 없는 공통 틀을 작성한다.Interface와Implements로 대여 기능의 약속을 정의한다.- 다형성(polymorphism)을 이용해 실제 객체의 종류에 맞는 동작을 실행한다.
문제 상황
사무실에는 회의용 노트북과 이동식 프로젝터가 있다. 담당자는 비품을 대여하기 전에 안내 문구와 대여 가능 일수를 확인한다. 노트북은 충전기를 함께 받아야 하고 최대 7일 동안 빌릴 수 있다. 프로젝터는 연결 케이블을 확인해야 하며 기본 대여 기간인 3일을 적용한다.
두 비품은 모두 이름과 대여 상태를 가진다. 대여 중인 비품을 다시 대여하려 하면 실패해야 하고, 보관 중인 비품을 반납하려 해도 상태가 바뀌지 않아야 한다. 이 규칙을 노트북 클래스와 프로젝터 클래스에 각각 작성하면 같은 코드를 두 군데에서 관리하게 된다. 나중에 상태 문구를 바꿀 때 한쪽만 고치는 실수도 생길 수 있다.
반대로 모든 비품을 하나의 클래스에 넣고 종류를 나타내는 문자열로 동작을 나누면, 새로운 종류가 생길 때마다 기존 클래스의 조건을 늘려야 한다. 종류 이름을 잘못 입력했을 때 어떤 동작을 해야 하는지도 별도로 정해야 한다. 여기서는 공통 상태와 대여 규칙을 한 클래스에 두고, 종류별 안내와 기간만 필요한 곳에서 작성한다.
대여 처리 절차도 비품 종류에 의존하지 않게 만들고 싶다. 이 절차에는 대여 요청, 반납 요청, 상태 조회만 있으면 된다. 충전기 안내나 프로젝터 안내를 알아야 할 이유는 없다. 따라서 비품의 공통 틀과 대여 절차가 요구하는 기능의 약속을 구분한다.
상속으로 공통 기능과 차이를 표현한다
기본 클래스와 파생 클래스
상속에서 기능을 물려주는 클래스를 기본 클래스(base class), 물려받는 클래스를 파생 클래스(derived class)라고 한다. 이 예제의 기본 클래스는 Equipment이고 파생 클래스는 Notebook과 Projector다. 파생 클래스 선언 안에 Inherits Equipment를 쓰면 두 클래스의 관계가 정해진다.
상속은 단순히 코드를 짧게 만드는 문법이 아니다. “노트북은 비품이다”라는 관계를 프로그램에 표현한다. 그래서 Notebook 객체를 Equipment 형식의 변수에 넣을 수 있다. 하지만 “대여 담당자는 비품이다”라는 관계는 성립하지 않는다. 담당자 클래스에 대여 관련 코드가 필요하다는 이유만으로 비품 클래스를 상속하면 역할이 섞인다.
기본 클래스에는 이름, 대여 상태, 대여와 반납 처리처럼 모든 비품에 공통인 내용을 둔다. 파생 클래스에는 노트북의 충전기 안내처럼 그 종류에서 달라지는 내용만 둔다. 대여 상태를 나타내는 필드는 Private으로 숨기고, 상태를 변경하는 공통 메서드를 통해서만 값을 바꾸게 한다.
파생 객체를 만들 때 기본 클래스의 필드가 별개의 다른 객체에 생기는 것은 아니다. 노트북 객체 하나가 기본 클래스에서 정의한 이름과 대여 상태도 가진다. 다만 Private 필드는 파생 클래스의 코드에서 직접 접근할 수 없다. 이 예제에서는 파생 클래스가 대여 상태를 직접 고칠 필요가 없으므로 그 제한이 공통 규칙을 지켜 준다.
생성자는 객체를 처음 준비하는 절차다. 파생 클래스의 생성자에서 MyBase.New(name)을 호출하면 기본 클래스의 생성자가 이름을 저장한다. 이 호출은 파생 생성자의 첫 번째 문장에 둔다. 예제의 기본 생성자는 이름을 반드시 받으므로, 파생 생성자도 전달할 이름을 받아야 한다. 생성자 자체를 물려받아 그대로 사용하는 것으로 이해하면 안 된다.
기본 구현을 바꾸거나 구현을 요구한다
Overridable은 파생 클래스가 해당 메서드의 동작을 바꿀 수 있다는 뜻이다. 기본 클래스의 LoanDays는 3을 반환하지만, 노트북은 Overrides를 붙인 같은 메서드에서 7을 반환한다. 이처럼 물려받은 메서드의 동작을 다시 정의하는 것을 재정의(overriding)라고 한다. 프로젝터는 재정의하지 않으므로 기본 구현의 3을 사용한다.
재정의할 때는 메서드 이름만 같게 쓰면 되는 것이 아니다. 매개변수의 개수와 형식, 반환 형식도 기본 메서드에 맞아야 한다. 이 예제에서는 두 메서드 모두 매개변수가 없고 Integer를 반환한다. 기본 메서드에 Overridable이 없으면 파생 클래스에서 마음대로 Overrides를 붙일 수 없다.
한편 비품의 안내 문구에는 유용한 공통 기본값이 없다. 노트북과 프로젝터가 각자 문구를 정해야 한다. 이때 기본 클래스에 MustOverride 메서드를 선언한다. 이 선언에는 메서드 본문과 End Function을 쓰지 않는다. 실제 객체를 만들 수 있는 파생 클래스는 그 메서드를 구현해야 한다.
MustInherit을 붙인 클래스를 추상 클래스(abstract class)라고 한다. 이 클래스는 직접 객체를 만들 수 없는 공통 틀이다. 예제에서는 종류가 정해지지 않은 일반 비품을 만들지 않고 노트북이나 프로젝터를 만든다. MustOverride 멤버를 가진 클래스에는 MustInherit이 필요하다. 다만 MustInherit 클래스가 반드시 MustOverride 멤버를 가져야 하는 것은 아니다.
| 키워드 | 쓰는 위치 | 예제에서의 의미 |
|---|---|---|
Inherits | 파생 클래스 내부 | 노트북과 프로젝터가 비품의 기능을 물려받는다 |
Overridable | 기본 클래스의 메서드 | 기본 대여 기간 3일을 필요하면 바꾼다 |
Overrides | 파생 클래스의 메서드 | 노트북의 대여 기간과 각 비품의 안내를 정한다 |
MustInherit | 클래스 선언 | 종류가 정해지지 않은 비품 객체 생성을 막는다 |
MustOverride | 추상 클래스의 메서드 | 실제 비품 종류에서 안내 구현을 요구한다 |
인터페이스로 필요한 기능을 약속한다
상속이 “어떤 종류인가”를 표현한다면, 인터페이스는 “어떤 기능을 제공하는가”를 표현한다. 여기서는 ILoanable에 대여, 반납, 상태 조회 기능을 선언한다. 앞의 I는 인터페이스 이름에 흔히 사용하는 관례이며, 문법이 요구하는 글자는 아니다.
이 장에서 작성하는 인터페이스에는 메서드 이름과 매개변수, 반환 형식만 있고 실행할 본문은 없다. Borrow와 ReturnItem은 성공 여부를 Boolean으로 반환한다. Status는 현재 상태를 String으로 반환한다. 이 약속만으로 대여 처리 절차가 어떤 호출을 할 수 있는지 정할 수 있다.
클래스가 약속을 지킨다고 선언할 때는 클래스 내부에 Implements ILoanable을 쓴다. 각 구현 메서드에도 Implements ILoanable.Borrow처럼 어떤 인터페이스 멤버를 구현하는지 표시한다. 클래스에 이름이 같은 메서드가 있다는 것만으로 인터페이스 구현이 연결되지는 않는다.
예제에서는 Equipment가 인터페이스를 구현한다. 모든 비품에 같은 대여 규칙을 적용하기 때문이다. Notebook과 Projector는 이 구현을 물려받으므로 각각 다시 Implements ILoanable을 쓰지 않는다. 두 객체 모두 ILoanable 형식의 매개변수에 전달할 수 있다.
하나의 클래스는 기본 클래스를 하나만 지정할 수 있지만, 여러 인터페이스를 구현할 수 있다. 이 장의 프로그램에는 인터페이스 하나만 사용한다. 기능의 약속을 많이 나누는 것보다, 대여 절차가 실제로 요구하는 세 가지 동작을 먼저 분명히 하는 편이 이해하기 쉽다.
| 비교 기준 | 추상 클래스 | 이 예제의 인터페이스 |
|---|---|---|
| 주요 역할 | 비품이라는 공통 종류를 표현한다 | 대여할 수 있다는 기능을 표현한다 |
| 상태 보관 | 이름과 대여 상태를 필드에 보관한다 | 객체별 상태를 보관할 필드를 두지 않는다 |
| 동작 구현 | 공통 구현과 구현을 요구하는 선언을 함께 둔다 | 호출할 메서드의 약속만 선언한다 |
| 연결 문법 | Inherits Equipment | Implements ILoanable |
인터페이스는 이름만 같으면 무엇이든 제대로 동작한다고 보장하지 않는다. Borrow가 성공했을 때 상태를 대여 중으로 바꾼다는 규칙은 구현 코드가 지켜야 한다. 컴파일러는 요구한 메서드와 형식이 연결되었는지 확인하지만, 사무실의 업무 규칙까지 판단하지는 않는다.
다형성으로 실제 객체에 맞는 동작을 실행한다
다형성은 같은 형식으로 다루는 여러 객체가 각자의 구현에 따라 동작하는 성질이다. 예를 들어 Dim notebook As Equipment = New Notebook("회의용 노트북")에서 변수의 선언 형식은 Equipment이고 실제 객체의 형식은 Notebook이다. 변수에 담았다고 해서 노트북 객체가 일반 비품 객체로 바뀌는 것은 아니다.
변수의 선언 형식은 코드에서 어떤 멤버를 호출할 수 있는지 정한다. Equipment에 선언된 Describe와 LoanDays를 호출할 수 있다. 실행할 때 재정의된 메서드의 구현은 실제 객체에 따라 선택된다. 노트북에서는 노트북의 안내와 7일이 나오고, 프로젝터에서는 프로젝터의 안내와 기본값 3일이 나온다.
이 차이는 Overridable, Overrides, MustOverride로 연결된 메서드에 적용된다. 메서드 이름이 같다는 사실만으로 같은 동작 선택 규칙이 생기지는 않는다. 초기에 이 연결을 분명히 작성하면 호출하는 코드가 객체 종류를 하나씩 검사할 필요가 줄어든다.
인터페이스도 공통 형식으로 사용할 수 있다. ProcessLoan의 매개변수를 ILoanable로 선언하면 이 프로시저 안에서는 대여, 반납, 상태 조회를 호출할 수 있다. 반면 Describe는 인터페이스에 없는 멤버이므로 호출할 수 없다. 실제로 전달된 객체가 비품이라는 것을 알고 있더라도, 컴파일러는 매개변수의 선언 형식을 기준으로 확인한다.
여기서 사용한 대입과 전달은 Option Strict On에서도 허용된다. 노트북은 비품이고, 비품은 대여 인터페이스를 구현하므로 필요한 약속을 이미 제공한다. 반대로 모든 비품이 노트북인 것은 아니다. Equipment 형식의 값을 아무 확인 없이 Notebook 변수에 넣는 방향은 같은 이유로 허용되지 않는다.
완성 코드에서는 비품 안내를 출력하는 프로시저와 대여를 진행하는 프로시저를 구분한다. 안내 출력은 Equipment의 기능을 사용하고, 대여 진행은 ILoanable의 기능만 사용한다. 각 프로시저의 매개변수 형식은 그 작업에 필요한 기능을 드러낸다.
완성 코드
.NET 10 SDK가 설치된 macOS 또는 Linux에서 Visual Basic 콘솔 프로젝트를 만든다. 생성된 Program.vb의 내용을 다음 코드 전체로 바꾼다. 다른 소스 파일이나 추가 패키지는 필요하지 않다. 기본 프로젝트 설정에서 경고와 오류 없이 실행되도록 작성했으며, 사용자 입력이나 현재 시각을 사용하지 않는다.
Option Strict On
Option Explicit On
Imports System
Public Interface ILoanable
Function Borrow() As Boolean
Function ReturnItem() As Boolean
Function Status() As String
End Interface
Public MustInherit Class Equipment
Implements ILoanable
Private ReadOnly _name As String
Private _isBorrowed As Boolean = False
Protected Sub New(name As String)
_name = name
End Sub
Public ReadOnly Property Name As String
Get
Return _name
End Get
End Property
Public Overridable Function LoanDays() As Integer
Return 3
End Function
Public MustOverride Function Describe() As String
Public Function Borrow() As Boolean Implements ILoanable.Borrow
If _isBorrowed Then
Return False
End If
_isBorrowed = True
Return True
End Function
Public Function ReturnItem() As Boolean Implements ILoanable.ReturnItem
If Not _isBorrowed Then
Return False
End If
_isBorrowed = False
Return True
End Function
Public Function Status() As String Implements ILoanable.Status
If _isBorrowed Then
Return Name & ": 대여 중"
End If
Return Name & ": 보관 중"
End Function
End Class
Public Class Notebook
Inherits Equipment
Public Sub New(name As String)
MyBase.New(name)
End Sub
Public Overrides Function LoanDays() As Integer
Return 7
End Function
Public Overrides Function Describe() As String
Return Name & ": 충전기를 함께 대여한다."
End Function
End Class
Public Class Projector
Inherits Equipment
Public Sub New(name As String)
MyBase.New(name)
End Sub
Public Overrides Function Describe() As String
Return Name & ": 연결 케이블을 확인한다."
End Function
End Class
Module Program
Sub Main()
Dim notebook As Equipment = New Notebook("회의용 노트북")
Dim projector As Equipment = New Projector("이동식 프로젝터")
ShowEquipment(notebook)
ProcessLoan(notebook)
Console.WriteLine()
ShowEquipment(projector)
ProcessLoan(projector)
End Sub
Private Sub ShowEquipment(item As Equipment)
Console.WriteLine(item.Describe())
Console.WriteLine("대여 가능 일수: " & item.LoanDays().ToString())
End Sub
Private Sub ProcessLoan(item As ILoanable)
Console.WriteLine("첫 대여 성공: " & item.Borrow().ToString())
Console.WriteLine("재대여 성공: " & item.Borrow().ToString())
Console.WriteLine(item.Status())
Console.WriteLine("반납 성공: " & item.ReturnItem().ToString())
Console.WriteLine(item.Status())
End Sub
End Module
줄별 해설
파일 시작과 기능의 약속
Option Strict On은 형식이 맞지 않는 값을 암묵적으로 좁혀 변환하는 일 등을 제한한다. Option Explicit On은 변수를 선언한 뒤 사용하게 한다. Imports System은 콘솔 출력에 사용하는 Console을 짧은 이름으로 쓸 수 있게 한다.
Public Interface ILoanable부터 End Interface까지는 대여 기능의 약속이다. 세 줄의 Function 선언에는 실행문이 없다. 대여와 반납의 결과를 성공 또는 실패로 표현하고, 상태 조회의 결과를 문장으로 표현한다. 인터페이스를 호출하는 쪽은 이 반환 형식에 맞춰 결과를 사용한다.
공통 상태와 대여 규칙
Public MustInherit Class Equipment는 비품의 공통 틀을 선언한다. 그 아래의 Implements ILoanable은 이 틀이 대여 기능을 제공한다고 표시한다. 추상 클래스여도 인터페이스 메서드의 공통 구현을 가질 수 있으므로, 같은 대여 규칙을 파생 클래스마다 반복하지 않는다.
_name은 생성할 때 받은 이름을 보관한다. ReadOnly 필드이므로 이 코드에서는 생성자에서 값을 정한 뒤 바꾸지 않는다. _isBorrowed는 객체마다 따로 존재하는 대여 상태다. 처음 값이 False이므로 두 비품 모두 보관 중인 상태에서 출발한다.
Protected Sub New의 Protected는 해당 클래스와 파생 클래스에서 접근할 수 있다는 뜻이다. 외부 호출자는 Equipment를 직접 만들지 않고 파생 클래스의 공개 생성자를 사용한다. 파생 생성자에서 전달한 이름을 _name = name이 저장한다.
Name 속성은 Get 안에서 이름을 반환한다. 공개 읽기 전용 속성이므로 파생 클래스는 안내 문구에 이름을 사용할 수 있지만, 외부에서 새 이름을 대입할 수는 없다. 숨긴 필드와 읽기 전용 속성을 함께 두어 이름을 읽는 방법을 하나로 정했다.
LoanDays는 기본값 3을 반환하는 실제 구현이다. Overridable이 있으므로 파생 클래스가 필요할 때 바꿀 수 있다. 바로 다음의 Describe는 구현이 없는 선언이다. MustOverride로 종류별 안내를 요구하므로 노트북과 프로젝터가 각각 문구를 작성한다.
Borrow의 첫 조건은 이미 대여 중인지 확인한다. 그렇다면 False를 즉시 반환하고 필드를 바꾸지 않는다. 보관 중이라면 상태를 True로 바꾸고 성공을 반환한다. 같은 메서드를 연속으로 두 번 호출하면 첫 호출은 성공하고 두 번째 호출은 실패한다.
ReturnItem은 반대 방향의 상태 변경이다. Not _isBorrowed가 참이면 이미 보관 중이므로 실패한다. 대여 중일 때만 필드를 False로 바꾸고 성공한다. 실패한 대여나 반납 요청은 현재 상태를 유지한다는 규칙이 두 메서드에 들어 있다.
Status는 필드를 읽어 이름과 상태 문구를 반환한다. 이 메서드는 상태를 바꾸지 않는다. 세 메서드 끝의 Implements ILoanable.…은 각각 대여, 반납, 상태 조회 약속에 구현을 연결한다. 출력은 이 메서드들 안에서 하지 않고 호출하는 프로시저에 맡겼다.
종류별 구현과 실행 순서
Notebook의 Inherits Equipment는 공통 비품 기능을 가져온다. 생성자의 MyBase.New(name)은 이름 저장을 기본 생성자에 맡긴다. LoanDays는 7을 반환하도록 재정의하고, Describe는 충전기를 함께 대여한다는 안내를 반환한다.
Projector도 같은 방식으로 이름을 전달하고 안내를 재정의한다. 하지만 LoanDays는 작성하지 않는다. 따라서 기본 클래스가 제공하는 3일을 사용한다. 공통 기본값이 이미 알맞다면 파생 클래스에서 같은 메서드를 다시 작성할 필요가 없다.
Main의 두 변수는 모두 Equipment 형식이다. 오른쪽의 New는 서로 다른 파생 객체를 만든다. 이 대입은 선언 형식을 통일할 뿐, 실제 객체의 종류를 지우지 않는다. 두 객체의 상태도 공유되지 않으므로 노트북을 대여해도 프로젝터의 상태는 바뀌지 않는다.
ShowEquipment는 같은 두 줄로 모든 비품의 안내와 기간을 출력한다. 노트북을 받으면 두 메서드 모두 노트북 구현이 실행된다. 프로젝터를 받으면 안내는 프로젝터 구현, 기간은 기본 구현이 실행된다. 출력 앞에 비품 종류를 검사하는 조건문은 없다.
ProcessLoan은 인터페이스 형식으로 객체를 받는다. 첫 대여, 재대여, 상태 조회, 반납, 상태 조회 순서로 호출한다. ToString()은 성공 여부와 일수를 문자열로 표현한다. 이 예제에서는 성공 여부가 True 또는 False로 출력된다. 중간의 매개변수 없는 Console.WriteLine()은 두 비품의 결과 사이에 빈 줄을 하나 만든다.
실행 결과
새 프로젝트를 만들 때 사용할 명령은 다음과 같다. 프로젝트 생성 뒤 Program.vb를 완성 코드로 교체하고 마지막 명령을 실행한다. 이미 Visual Basic 콘솔 프로젝트가 있다면 해당 폴더에서 파일을 교체한 뒤 dotnet run만 실행한다.
dotnet new console -lang VB -f net10.0 -o OfficeLoans
cd OfficeLoans
dotnet run
프로젝트 생성 과정의 안내 문구를 제외한 앱의 예상 출력은 다음과 같다. 두 비품 모두 첫 대여가 성공하고 같은 객체에 대한 재대여는 실패한다. 반납 뒤에는 보관 중으로 돌아오며 프로그램이 종료된다.
회의용 노트북: 충전기를 함께 대여한다.
대여 가능 일수: 7
첫 대여 성공: True
재대여 성공: False
회의용 노트북: 대여 중
반납 성공: True
회의용 노트북: 보관 중
이동식 프로젝터: 연결 케이블을 확인한다.
대여 가능 일수: 3
첫 대여 성공: True
재대여 성공: False
이동식 프로젝터: 대여 중
반납 성공: True
이동식 프로젝터: 보관 중
대여 가능 일수는 안내 값이다. 날짜를 기록하거나 기한이 지났는지 판단하지 않는다. 출력에 7일과 3일이 나오는 까닭은 시간 계산 때문이 아니라 실제 객체가 선택한 메서드 구현이 서로 다르기 때문이다.
실무에서 자주 틀리는 것
다음 코드는 오류를 설명하기 위한 부분 코드다. 완성 코드에 그대로 추가하는 예제가 아니다. 각 문제의 위치를 확인하고 대응하는 선언이나 호출을 수정한다.
기본 메서드를 재정의할 수 있게 선언하지 않는다
기본 메서드에 Overridable이 없는데 파생 메서드에 Overrides를 쓰면 컴파일 오류가 발생한다. 같은 이름의 메서드를 썼다는 것과 기본 메서드를 재정의했다는 것은 다르다. 종류별 동작을 바꾸려는 의도를 기본 선언에도 표시해야 한다.
틀린 코드다.
' 기본 클래스 내부
Public Function LoanDays() As Integer
Return 3
End Function
' 파생 클래스 내부
Public Overrides Function LoanDays() As Integer
Return 7
End Function
기본 선언을 다음처럼 고친다. 파생 메서드는 그대로 둔다.
' 기본 클래스 내부
Public Overridable Function LoanDays() As Integer
Return 3
End Function
Shadows로 같은 이름을 가리는 방식은 이 문제의 목적에 맞지 않는다. 이 예제는 기본 클래스 형식으로 호출해도 파생 구현이 선택되어야 하므로 재정의 관계를 유지한다.
추상 클래스를 직접 만들려고 한다
MustInherit 클래스는 객체를 직접 만들 수 없다. 비품 공통 틀에는 종류별 안내 구현이 빠져 있으므로 New Equipment는 허용되지 않는다. 변수의 형식으로 사용하는 것은 가능하지만, 실제 생성 대상은 구현을 갖춘 파생 클래스여야 한다.
틀린 코드다.
Dim item As Equipment = New Equipment("회의용 비품")
다음처럼 실제 종류를 정해 만든다.
Dim item As Equipment = New Notebook("회의용 노트북")
추상 클래스의 객체 생성 오류와 생성자의 접근 제한은 서로 다른 규칙이다. 생성자를 공개하더라도 MustInherit 클래스의 직접 생성은 허용되지 않는다. 파생 클래스에서는 기본 생성자를 호출해 공통 상태를 준비할 수 있다.
인터페이스 구현 연결을 빠뜨린다
클래스 수준에 Implements ILoanable을 적었어도 메서드 수준의 연결을 빠뜨리면 해당 약속을 구현했다고 인정되지 않는다. 특히 이름이 같은 메서드가 이미 있으면 이 부분을 놓치기 쉽다.
Equipment 내부에서 Borrow 선언을 다음과 같이 바꾸면 틀린 코드가 된다. 다른 인터페이스 구현은 그대로 있어도 대여 약속의 연결이 빠져 있다.
Public Function Borrow() As Boolean
If _isBorrowed Then
Return False
End If
_isBorrowed = True
Return True
End Function
메서드 선언에 구현 대상을 명시한다.
Public Function Borrow() As Boolean Implements ILoanable.Borrow
If _isBorrowed Then
Return False
End If
_isBorrowed = True
Return True
End Function
구현 메서드 이름은 인터페이스 멤버와 다르게 정할 수도 있지만, 이 예제에서는 읽기 쉽게 같은 이름을 사용했다. 연결을 결정하는 것은 이름의 우연한 일치가 아니라 Implements 뒤에 적은 대상이다.
인터페이스에 없는 멤버를 호출한다
인터페이스 형식의 변수에서는 그 인터페이스에 선언된 멤버를 사용한다. 실제 객체가 노트북이라는 이유만으로 모든 노트북 기능을 호출할 수는 없다. 필요한 멤버를 찾을 수 없을 때 Option Strict를 끄기보다 프로시저가 어떤 기능을 요구하는지 먼저 확인한다.
틀린 코드다.
Private Sub ShowGuide(item As ILoanable)
Console.WriteLine(item.Describe())
End Sub
비품 안내가 목적이라면 비품 형식으로 받도록 고친다.
Private Sub ShowGuide(item As Equipment)
Console.WriteLine(item.Describe())
End Sub
대여 상태 조회가 목적이라면 매개변수 형식은 유지하고 호출을 고친다.
Private Sub ShowLoanState(item As ILoanable)
Console.WriteLine(item.Status())
End Sub
매개변수 형식은 호출 가능한 기능의 범위를 정한다. 안내가 필요한 작업과 대여 상태만 필요한 작업을 구분하면 인터페이스에 관련 없는 멤버를 계속 추가하지 않아도 된다.
한눈에 보기
공통 상태와 기본 동작은 Equipment에 모은다. 기본값이 있는 동작은 필요할 때만 재정의하고, 종류별 구현이 반드시 필요한 동작은 추상 메서드로 선언한다. 대여 절차는 ILoanable의 약속만 사용하며, 어떤 비품 종류인지 직접 판단하지 않는다.
| 사용 지점 | 선언 형식 | 실제 객체 또는 구현 | 결과 |
|---|---|---|---|
notebook.LoanDays() | Equipment | Notebook의 재정의 | 7을 반환한다 |
projector.LoanDays() | Equipment | Equipment의 기본 구현 | 3을 반환한다 |
item.Describe() | Equipment | 각 파생 클래스의 구현 | 종류별 안내를 반환한다 |
item.Borrow() | ILoanable | 물려받은 공통 대여 구현 | 보관 중일 때만 성공한다 |
새로운 비품을 추가할 때는 기존 대여 절차를 고치기 전에 필요한 구현부터 살펴본다. 기본 기간을 사용할 수 있으면 안내만 작성하면 된다. 별도의 기간이 필요하면 기간도 재정의한다. 대여 규칙이 모든 비품에서 같다면 그대로 물려받는다.
다음 장에서는 여러 비품을 한데 모아 관리하는 방법을 다룬다. 지금은 두 객체를 각각 호출했지만, 같은 기본 형식과 인터페이스 형식으로 다룰 수 있다는 점이 이후 여러 객체를 처리하는 바탕이 된다.
연습 문제
- 완성 코드에
Monitor클래스를 추가한다. 비품 이름은 “보조 모니터”, 안내 문구는 “전원 어댑터를 확인한다.”, 대여 기간은 2일로 한다.Main에서Equipment형식의 변수로 만들고 기존 두 프로시저로 처리한다. - 노트북 객체 하나를
ILoanable형식으로 만든다. 보관 중인 상태에서 반납, 대여, 반납, 재반납을 차례로 호출하고 각 성공 여부를 출력한다. 네 반환값과 마지막 상태를 예상하고 이유를 설명한다. Equipment.LoanDays의 기본 반환값을 3에서 5로 바꾼다. 노트북과 프로젝터의 출력이 각각 어떻게 달라지는지 설명한다. 두 파생 클래스의 코드는 수정하지 않는다.Equipment형식의 매개변수로 받은 객체의 안내를 출력하고,ILoanable형식의 매개변수로 받은 객체의 상태를 출력하는 프로시저를 각각 작성한다. 두 매개변수 형식의 차이를 설명한다.
정답과 해설
새로운 종류를 추가한다
다음 클래스를 Module Program 바깥에 추가한다. 생성자는 이름을 기본 클래스에 전달하고, 두 메서드는 모니터에 맞게 재정의한다. 대여와 반납 메서드는 이미 기본 클래스에 있으므로 다시 작성하지 않는다.
Public Class Monitor
Inherits Equipment
Public Sub New(name As String)
MyBase.New(name)
End Sub
Public Overrides Function LoanDays() As Integer
Return 2
End Function
Public Overrides Function Describe() As String
Return Name & ": 전원 어댑터를 확인한다."
End Function
End Class
Main의 마지막에 다음 호출을 추가한다. 기존 프로시저는 수정하지 않는다.
Console.WriteLine()
Dim monitor As Equipment = New Monitor("보조 모니터")
ShowEquipment(monitor)
ProcessLoan(monitor)
추가 출력의 안내는 “보조 모니터: 전원 어댑터를 확인한다.”이며 대여 가능 일수는 2다. 대여와 반납의 성공 여부는 기존 두 비품과 같다. 새 종류의 안내와 기간은 새 클래스에 모였고, 대여 처리 절차는 그대로 재사용했다.
실패한 요청은 상태를 유지한다
다음 코드를 Main의 내용으로 사용해 확인할 수 있다. 처음에는 보관 중이므로 반납이 실패한다. 이어서 대여가 성공하고, 그 대여를 반납하는 요청도 성공한다. 마지막에는 다시 보관 중이므로 재반납이 실패한다.
Dim item As ILoanable = New Notebook("회의용 노트북")
Console.WriteLine(item.ReturnItem().ToString())
Console.WriteLine(item.Borrow().ToString())
Console.WriteLine(item.ReturnItem().ToString())
Console.WriteLine(item.ReturnItem().ToString())
Console.WriteLine(item.Status())
예상 출력은 다음과 같다.
False
True
True
False
회의용 노트북: 보관 중
성공 여부를 출력하는 일과 상태를 바꾸는 일은 구분된다. 이 코드에서는 메서드를 호출할 때 상태가 바뀌고, 그 반환값을 Console.WriteLine이 보여 준다. 마지막 실패는 앞선 반납의 결과를 되돌리지 않는다.
기본값을 쓰는 종류만 영향을 받는다
기본 클래스의 메서드를 다음과 같이 바꾼다.
Public Overridable Function LoanDays() As Integer
Return 5
End Function
노트북의 대여 가능 일수는 계속 7이다. 노트북이 이미 그 메서드를 재정의했기 때문이다. 프로젝터는 기본 구현을 사용하므로 3에서 5로 바뀐다. 기본값을 수정하면 그 기본값을 물려받아 사용하는 종류에 함께 반영된다는 점을 확인할 수 있다.
작업에 필요한 형식을 선택한다
다음 두 프로시저를 Module Program 안에 작성한다.
Private Sub PrintGuide(item As Equipment)
Console.WriteLine(item.Describe())
End Sub
Private Sub PrintState(item As ILoanable)
Console.WriteLine(item.Status())
End Sub
PrintGuide는 비품에 선언된 안내 기능을 요구한다. PrintState는 대여 기능의 약속 중 상태 조회만 요구한다. 완성 코드의 노트북과 프로젝터는 두 프로시저에 모두 전달할 수 있다. 다른 클래스가 비품을 상속하지 않고 ILoanable만 구현했다면 상태 출력에는 전달할 수 있지만 비품 안내 출력에는 전달할 수 없다.
상속은 공통 종류와 구현을 묶고, 인터페이스는 사용하는 쪽이 요구하는 기능을 드러낸다. 두 방법을 함께 사용하면 비품의 상태 규칙은 한곳에서 관리하면서 종류별 안내를 각각 작성할 수 있다. 다형성은 그 차이를 같은 호출 코드로 실행하게 한다.