객체지향 프로그래밍
객체지향 프로그래밍 (Object-oriented programming)
객체지향 프로그래밍(OOP)은 프로그래밍 언어에서 크고 불분명하게 정의된 개념이에요. 각 언어마다 자기만의 구현 방식을 가져서 매우 다양한 의미를 포함하는 경향이 있어요. 언어마다 다른 언어의 구현과 유사점과 차이점을 가지면서 저마다의 시각을 구현하죠.
하지만 한 가지 모델이 "승리"했어요. 그저 압도적인 인기 덕분이지만, 객체지향이 무엇을 의미하는지에 대한 논쟁에서 이겼죠. 그것은 Java 프로그래밍 언어에서 사용되는 모델이에요. C++이 사용하는 것과 매우 유사하죠. 몇 가지 특징적인 속성은 다음과 같아요:
- 타입 파생 및 확장(Type derivation and extension): 대부분의 객체지향 언어는 사용자가 파생 타입에 필드를 추가할 수 있게 해요.
- 하위 타입(Subtyping): 기본 타입에서 파생된 타입의 객체는 어떤 경우에는 기본 타입의 객체로 대체될 수 있어요.
- 런타임 다형성(Runtime polymorphism): 객체 타입에 붙은 서브프로그램(보통 메서드(method) 라고 함) 호출은 객체의 정확한 타입에 따라 런타임에 디스패치(dispatch)될 수 있어요.
- 캡슐화(Encapsulation): 객체는 자신의 데이터 일부를 숨길 수 있어요.
- 확장성(Extensibility): 패키지 외부, 또는 전체 라이브러리 외부의 사람들도 여러분의 객체 타입에서 파생하고 자신만의 동작을 정의할 수 있어요.
Ada는 객체지향 프로그래밍이 오늘날만큼 인기를 얻기 전에 만들어졌어요. 위 목록의 일부 메커니즘과 개념들은 우리가 OOP라고 부르는 것이 추가되기 전의 Ada 초기 버전에도 이미 있었어요:
- 앞서 봤듯이 캡슐화는 Ada에서 타입 수준이 아니라 패키지 수준에서 구현돼요.
- 하위 타입은 말 그대로 하위 타입(subtype)으로 구현할 수 있는데, 완전하고 관대한 정적 대체(static substitutability) 모델을 가져요. 대체는 하위 타입의 동적 제약이 충족되지 않으면 런타임에 실패해요.
- 런타임 다형성은 가변 레코드(variant record)로 구현할 수 있어요.
하지만 이 목록에는 타입 확장(가변 레코드를 고려하지 않는다면)과 확장성이 빠져 있어요.
Ada의 1995년 개정판은 이 공백을 메우는 기능을 추가했어요. 이 기능 덕분에 사람들이 객체지향 패러다임을 더 쉽게 따라 프로그래밍할 수 있게 됐죠. 그 기능은 태그 타입(tagged type) 이에요.
참고 (Note) 태그 타입을 전혀 만들지 않고도 Ada에서 프로그래밍할 수 있어요. 그것이 선호하는 프로그래밍 스타일이거나 태그 타입에 특별히 쓸 곳이 없다면, Ada의 많은 기능이 그렇듯 사용하지 않는 것도 자유예요.
하지만 태그 타입은 특정 문제에 대한 해결책을 표현하는 가장 좋은 방법일 수 있고, 여러분의 문제를 푸는 가장 좋은 방법일 수도 있어요. 그렇다면 계속 읽어볼게요!
출처: 객체지향 프로그래밍 문서
본문
파생 타입 (Derived types)
태그 타입을 소개하기 전에, 지금까지 간단히 언급했지만 실제로는 다루지 않은 주제를 논의해야 해요: Ada의 모든 타입에서 하나 이상의 새 타입을 만들 수 있어요. 타입 파생은 언어에 내장돼 있어요.
package Newtypes is
type Point is record
X, Y : Integer;
end record;
type New_Point is new Point;
end Newtypes;
타입 파생은 strong typing을 강제하는 데 유용해요. 타입 시스템이 두 타입을 호환되지 않는 것으로 취급하기 때문이에요.
하지만 장점은 여기서 그치지 않아요: 원래 타입으로부터 것들을 상속할 수 있어요. 데이터의 표현뿐 아니라 동작(behavior)도 상속할 수 있어요.
타입을 상속하면 기본 연산(primitive operations) 이라고 부르는 것도 상속해요. 기본 연산(또는 간단히 프리미티브(primitive))은 타입에 붙은 서브프로그램이에요. Ada는 타입과 같은 스코프에서 정의된 서브프로그램을 프리미티브로 정의해요.
주의 (Attention) 서브프로그램은 다음 경우에만 타입의 프리미티브가 돼요:
- 서브프로그램이 타입과 같은 스코프에서 선언되고
- 타입과 서브프로그램이 패키지에 선언된 경우
with Ada.Text_IO; use Ada.Text_IO;
procedure Primitives is
package Week is
type Days is (Monday, Tuesday, Wednesday,
Thursday, Friday,
Saturday, Sunday);
-- Print_Day는 타입 Days의 프리미티브
procedure Print_Day (D : Days);
end Week;
package body Week is
procedure Print_Day (D : Days) is
begin
Put_Line (Days'Image (D));
end Print_Day;
end Week;
use Week;
type Weekend_Days is new
Days range Saturday .. Sunday;
-- 여기서 프로시저 Print_Day가 자동으로
-- 상속됨. 마치 프로시저
--
-- procedure Print_Day (D : Weekend_Days);
--
-- 가 같은 본문으로 선언된 것과 같음
Sat : Weekend_Days := Saturday;
begin
Print_Day (Sat);
end Primitives;
이런 종류의 상속은 매우 유용할 수 있고, 레코드 타입에만 국한되지 않아요(위 예시에서처럼 이산 타입에도 사용할 수 있어요). 하지만 객체지향 상속과 겉으로만 비슷해요:
- 이 메커니즘만으로는 레코드를 확장할 수 없어요. 새 타입에 대한 새 표현을 지정할 수도 없어요: 그것은 항상 기본 타입과 같은 표현을 가져요.
- 동적 디스패치나 다형성을 위한 기능이 없어요. 객체는 고정된 정적 타입이에요.
- 다른 차이점들도 있지만 여기서 전부 나열하는 것은 유용하지 않아요. 기억할 점은, 코드 중복이나 합성(composition) 없이 동작을 정적으로 상속하기만 하고 싶을 때 사용할 수 있지만, OOP와 보통 관련된 동적 기능이 필요하다면 사용할 수 없는 상속 종류라는 것이에요.
태그 타입 (Tagged types)
Ada 언어의 1995년 개정판은 이 장의 시작 부분에서 설명한 것과 유사한 객체지향 스타일로 프로그래밍을 가능하게 하는 통합된 해결책의 필요성을 충족시키기 위해 태그 타입을 도입했어요.
태그 타입은 일반 레코드와 매우 유사하지만 일부 기능이 추가돼요:
- 타입에는 각 객체 안에 저장되는 태그(tag) 가 있으며, 이 태그가 객체의 런타임 타입(runtime type)을 식별해요.
- 프리미티브가 디스패치할 수 있어요. 태그 타입의 프리미티브는 Java나 C++에서 메서드(method) 라고 부르는 것이에요. 기본 타입을 파생하고 그 프리미티브를 오버라이드하면, 객체에 대해 호출할 때 호출되는 프리미티브가 객체의 정확한 런타임 타입에 따라 달라지는 결과를 얻는 경우가 많아요.
- 기본 타입에서 파생된 태그 타입이 기본 타입과 정적으로 호환될 수 있게 하는 하위 타입 규칙이 도입돼요.
첫 태그 타입 선언을 볼게요:
package P is
type My_Class is tagged null record;
-- 일반 레코드와 같지만
-- tagged 한정자가 붙음
-- 메서드는 타입 정의 밖에 있음:
procedure Foo (Self : in out My_Class);
-- 같은 패키지에서 My_Class 인자를 받는
-- 프로시저를 정의하면 메서드가 됨
-- 태그 타입을 파생하는 방법:
type Derived is new My_Class with record
A : Integer;
-- 파생 타입에서 필드를 추가할 수 있음
end record;
overriding
procedure Foo (Self : in out Derived);
-- "overriding" 한정자는 선택 사항이지만,
-- 있으면 유효해야 함
end P;
with Ada.Text_IO; use Ada.Text_IO;
package body P is
procedure Foo (Self : in out My_Class) is
begin
Put_Line ("In My_Class.Foo");
end Foo;
procedure Foo (Self : in out Derived) is
begin
Put_Line ("In Derived.Foo, A = "
& Integer'Image (Self.A));
end Foo;
end P;
클래스와이드 타입 (Classwide types)
언어의 나머지 부분과 일관성을 유지하기 위해, "이 객체는 이 타입 또는 그 타입에서 파생된 모든 타입이다"라고 말하는 새 표기법을 도입할 필요가 있었어요.
Ada에서는 이를 클래스와이드 타입(classwide type) 이라고 불러요. 다형성이 필요할 때마다 OOP에서 사용돼요. 예를 들어 다음은 할 수 없어요:
with P; use P;
procedure Main is
O1 : My_Class;
-- My_Class 타입의 객체 선언
O2 : Derived := (A => 12);
-- Derived 타입의 객체 선언
O3 : My_Class := O2;
-- INVALID: Derived 타입의 값을
-- My_Class 타입의 변수에 대입하려 함
begin
null;
end Main;
그것은 타입 T의 객체는 T가 태그 타입이든 아니든 정확히 타입 T이기 때문이에요. 프로그래머로서 여러분이 말하고 싶은 것은 "O3가 My_Class 타입 또는 My_Class에서 파생된 어떤 타입의 객체를 담을 수 있게 하고 싶다"는 것이에요. 이렇게 하면 돼요:
with P; use P;
procedure Main is
O1 : My_Class;
-- My_Class 타입의 객체 선언
O2 : Derived := (A => 12);
-- Derived 타입의 객체 선언
O3 : My_Class'Class := O2;
-- 이제 유효: My_Class'Class는
-- My_Class의 클래스와이드 타입을 나타내며,
-- 이는 My_Class에서 파생된 모든 타입의
-- 집합 (My_Class 자신 포함)
begin
null;
end Main;
주의 (Attention) 클래스와이드 타입의 객체는 기본 타입의 어떤 자손의 크기가 될 수 있으므로 크기를 알 수 없어요. 따라서 indefinite 타입이며 예상되는 제약이 있어요:
- 레코드의 필드/컴포넌트로 저장될 수 없어요
- 클래스와이드 타입의 객체는 즉시 초기화되어야 해요(그런 타입의 제약을 초기화로 지정하는 것 외에는 방법이 없어요).
디스패칭 연산 (Dispatching operations)
다른 태그 타입에서 파생된 타입에서 연산을 오버라이드할 수 있다는 것을 봤어요. OOP의 궁극적인 목표는 객체의 정확한 타입에 의존하는 프리미티브(메서드)를 호출하는 디스패칭 호출(dispatching call)을 만드는 것이에요.
하지만 신중히 생각해 보면, My_Class 타입의 변수는 항상 정확히 그 타입의 객체를 담아요. My_Class 또는 어떤 파생 타입을 담을 수 있는 변수를 원한다면, 그것은 My_Class'Class 타입이어야 해요.
즉, 디스패칭 호출을 하려면 먼저 어떤 타입 또는 그 타입에서 파생된 어떤 타입이 될 수 있는 객체, 즉 클래스와이드 타입의 객체가 있어야 해요.
with P; use P;
procedure Main is
O1 : My_Class;
-- My_Class 타입의 객체 선언
O2 : Derived := (A => 12);
-- Derived 타입의 객체 선언
O3 : My_Class'Class := O2;
O4 : My_Class'Class := O1;
begin
Foo (O1);
-- 디스패치 안 함: My_Class.Foo 호출
Foo (O2);
-- 디스패치 안 함: Derived.Foo 호출
Foo (O3);
-- 디스패치: Derived.Foo 호출
Foo (O4);
-- 디스패치: My_Class.Foo 호출
end Main;
주의 (Attention)
Derived 타입의 객체를 My_Class 타입의 객체로 변환할 수 있어요. Ada 용어로 이를 뷰 변환(view conversion) 이라고 하며, 예를 들어 부모 메서드를 호출하고 싶을 때 유용해요.
그 경우 객체는 실제로 My_Class 객체로 변환되는데, 이는 태그가 바뀐다는 뜻이에요. 태그 객체는 항상 참조로 전달되므로 이 종류의 변환을 사용해 객체의 상태를 수정할 수 있어요: 변환된 객체에 대한 변경은 원본에 영향을 미쳐요.
with P; use P;
procedure Main is
O1 : Derived := (A => 12);
-- Derived 타입의 객체 선언
O2 : My_Class := My_Class (O1);
O3 : My_Class'Class := O2;
begin
Foo (O1);
-- 디스패치 안 함: Derived.Foo 호출
Foo (O2);
-- 디스패치 안 함: My_Class.Foo 호출
Foo (O3);
-- 디스패치: My_Class.Foo 호출
end Main;
점 표기법 (Dot notation)
객체지향 프로그래머에게 더 익숙한 표기법으로 태그 타입의 프리미티브를 호출할 수도 있어요. 위의 Foo 프리미티브가 주어지면 위 프로그램을 이렇게도 쓸 수 있어요:
with P; use P;
procedure Main is
O1 : My_Class;
-- My_Class 타입의 객체 선언
O2 : Derived := (A => 12);
-- Derived 타입의 객체 선언
O3 : My_Class'Class := O2;
O4 : My_Class'Class := O1;
begin
O1.Foo;
-- 디스패치 안 함: My_Class.Foo 호출
O2.Foo;
-- 디스패치 안 함: Derived.Foo 호출
O3.Foo;
-- 디스패치: Derived.Foo 호출
O4.Foo;
-- 디스패치: My_Class.Foo 호출
end Main;
프리미티브의 디스패칭 매개변수가 첫 번째 매개변수라면(우리 예시가 그런 경우), 점 표기법으로 프리미티브를 호출할 수 있어요. 나머지 매개변수는 평소처럼 전달돼요:
with P; use P;
procedure Main is
package Extend is
type D2 is new Derived with null record;
procedure Bar (Self : in out D2;
Val : Integer);
end Extend;
package body Extend is
procedure Bar (Self : in out D2;
Val : Integer) is
begin
Self.A := Self.A + Val;
end Bar;
end Extend;
use Extend;
Obj : D2 := (A => 15);
begin
Obj.Bar (2);
Obj.Foo;
end Main;
비공개 & 리미티드 (Private & Limited)
앞서(Privacy 챕터에서) 타입을 리미티드(limited)나 비공개(private)로 선언할 수 있다는 것을 봤어요. 이러한 캡슐화 기법은 태그 타입에도 적용될 수 있어요. 이 절에서 다룰게요.
태그 비공개 타입의 예시:
package P is
type T is tagged private;
private
type T is tagged record
E : Integer;
end record;
end P;
태그 리미티드 타입의 예시:
package P is
type T is tagged limited record
E : Integer;
end record;
end P;
당연히 limited 와 private 을 결합해 태그 리미티드 비공개 타입을 선언할 수 있어요:
package P is
type T is tagged limited private;
procedure Init (A : in out T);
private
type T is tagged limited record
E : Integer;
end record;
end P;
package body P is
procedure Init (A : in out T) is
begin
A.E := 0;
end Init;
end P;
with P; use P;
procedure Main is
T1, T2 : T;
begin
T1.Init;
T2.Init;
-- 다음 줄은 T 타입이 private이라
-- 동작하지 않음:
--
-- T1.E := 0;
-- 다음 줄은 T 타입이 limited라
-- 동작하지 않음:
--
-- T2 := T1;
end Main;
위 Main 프로시저의 코드는 타입 T가 리미티드 비공개 타입이기 때문에 컴파일 오류를 유발하는 두 대입을 보여줘요. 실제로 다음을 할 수 없어요:
- 타입
T가 private이므로T1.E에 직접 대입할 수 없어요 - 타입
T가 limited이므로T1을T2에 대입할 수 없어요
이 경우 태그 타입과 비-태그 타입 사이에 차이가 없어요: 이 컴파일 오류들은 비-태그 타입에서도 발생해요.
클래스와이드 접근 타입 (Classwide access types)
이 절에서는 Ada에서 객체지향 프로그래밍에 유용한 패턴인 클래스와이드 접근 타입(classwide access type)을 다룰게요. 태그 타입 T와 파생 타입 T_New를 선언하는 예시로 시작할게요:
package P is
type T is tagged null record;
procedure Show (Dummy : T);
type T_New is new T with null record;
procedure Show (Dummy : T_New);
end P;
with Ada.Text_IO; use Ada.Text_IO;
package body P is
procedure Show (Dummy : T) is
begin
Put_Line ("Using type "
& T'External_Tag);
end Show;
procedure Show (Dummy : T_New) is
begin
Put_Line ("Using type "
& T_New'External_Tag);
end Show;
end P;
두 타입 T와 T_New에 널 레코드를 사용한다는 점에 주목하세요. 이 타입들은 실제 컴포넌트가 없지만 디스패칭을 보여주는 데는 사용할 수 있어요. 또한 위 예시는 Show 프로시저 구현에서 'External_Tag 애트리뷰트를 사용해 해당 태그 타입에 대한 문자열을 얻는다는 점에 주목하세요.
앞서 봤듯이 디스패칭 호출을 할 수 있는 객체를 만들려면 클래스와이드 타입을 사용해야 해요. 즉, T'Class 타입의 객체는 디스패치돼요. 예를 들어:
with P; use P;
procedure Dispatching_Example is
T2 : T_New;
T_Dispatch : constant T'Class := T2;
begin
T_Dispatch.Show;
end Dispatching_Example;
더 유용한 응용은 디스패치할 수 있는 객체 배열을 선언하는 것이에요. 예를 들어 배열 T_Arr를 선언하고, 이 배열을 순회하면서 각 개별 요소의 실제 타입에 따라 디스패치하고 싶다고 해볼게요:
for I in T_Arr'Range loop
T_Arr (I).Show;
-- T_Arr (I)의 실제 타입에 따라
-- Show 프로시저 호출
end loop;
하지만 T'Class 타입의 배열을 직접 선언하는 것은 불가능해요:
with P; use P;
procedure Classwide_Compilation_Error is
T_Arr : array (1 .. 2) of T'Class;
-- ^
-- 컴파일 오류!
begin
for I in T_Arr'Range loop
T_Arr (I).Show;
end loop;
end Classwide_Compilation_Error;
실제로 컴파일러가 배열의 각 요소에 실제로 어떤 타입이 사용될지 알 수 없어요. 하지만 접근 타입을 통한 동적 할당을 사용하면 배열 T_Arr의 개별 요소를 위해 서로 다른 타입의 객체를 할당할 수 있어요. 이는 클래스와이드 접근 타입으로 하는데, 형식은 다음과 같아요:
type T_Class is access T'Class;
T_Class 타입을 사용해 이전 예시를 다시 작성할 수 있어요. 이 경우 이 타입의 동적으로 할당된 객체는 할당 중 사용된 실제 타입에 따라 디스패치돼요. 또한 파생된 T_New 타입에 대해 오버라이드되지 않을 Init 프로시저를 도입할게요. 다음이 적응된 코드예요:
package P is
type T is tagged record
E : Integer;
end record;
type T_Class is access T'Class;
procedure Init (A : in out T);
procedure Show (Dummy : T);
type T_New is new T with null record;
procedure Show (Dummy : T_New);
end P;
with Ada.Text_IO; use Ada.Text_IO;
package body P is
procedure Init (A : in out T) is
begin
Put_Line ("Initializing type T...");
A.E := 0;
end Init;
procedure Show (Dummy : T) is
begin
Put_Line ("Using type "
& T'External_Tag);
end Show;
procedure Show (Dummy : T_New) is
begin
Put_Line ("Using type "
& T_New'External_Tag);
end Show;
end P;
with Ada.Text_IO; use Ada.Text_IO;
with P; use P;
procedure Main is
T_Arr : array (1 .. 2) of T_Class;
begin
T_Arr (1) := new T;
T_Arr (2) := new T_New;
for I in T_Arr'Range loop
Put_Line ("Element # "
& Integer'Image (I));
T_Arr (I).Init;
T_Arr (I).Show;
Put_Line ("-----------");
end loop;
end Main;
이 예시에서 첫 번째 요소(T_Arr (1))는 T 타입이고, 두 번째 요소는 T_New 타입이에요. 예시를 실행하면 T_Arr 배열의 두 요소 모두에 대해 T 타입의 Init 프로시저가 호출되고, Show 프로시저 호출은 T_Arr의 각 요소 타입에 따라 해당 프로시저를 선택해요.