Visual Basic, C & C++
분류 Delphi

[펌] 델파이 객체지향프로그래밍을 위한 20가지 규칙

페이지 정보

본문

델파이 객체지향프로그래밍을 위한 20가지 규칙









 

한의원 아저씨로부터 부탁이 있어서 번역을 하게 되었습니다. 번역이 자연스럽게 되면 좋겠는데 어떨지 모르겠네요.

저도 공부하는 거라 생각하고 시작하게 되었습니다.

읽어주셔서 감사합니다.


 


부분의 델파이 프로그래머들은 그들의 직접 작업을 하지 않고도 Visual Basic[Editor는 간단히 두려움을 떨쳐버려주지요...]처럼 개발환경을 사용하지요. 델파이는 강력한 VCL 구조와 제각기 델파이 어플리케이션에 객체지향 아키텍쳐의 근본을 두고 있습니다.

 


  이 글에서 필자는 OOP 이론을 주장하려는 것이 아니라 여러분이 프로그램의 구조를 개선하도록 도움될 만한 몇가지 간단한 제안을 할까 합니다. 나열되는 규칙들은 여러분이 작성하는 어플리케이션의 실제 형태에 적용될 수도 안될 수도 있습니다. 그냥 필자가 하는 제안하는 규칙들을 기억해주시면 고맙지요. ^^

 


  제가 제일 강조하는 원칙은 캡슐화인데요. 우리는 프로그램의 다른 부분에 영향을 주지 않고 차후에 코드를 변경할 수 있는 유연성과 내구성이 있는 클래스를 생성하길 바라고 있습니다. 단지 좋은 OOP의 유일한 척도는 아니라도, OOP의 기초라고 할 수 있습니다. 그래서 필자가 이 글에서 정말 OOP를 지나치게 강조하는데에는 몇가지 그럴싸한 이유가 있습니다.

 


  마지막으로, 이러한 원칙들이 델파이 프로그래머에 의해 일상 생활에서 사용된다는 사실을 강조하기 위해서, 필자는 여러가지 규칙들이 컴포넌트의 개발에 똑같이 적용되고 있더라도 주로 폼 개발에 초점을 맞출려고 합니다. 컴포넌트를 작성하는 프로그래머는 특히 OOP와 클래스를 생각해야합니다. 때때로 컴포넌트를 사용하는 프로그래머는 OOP에 관해 잊고 있는데 이 기사가 회상시켜줄 수도 있겠죠.

 


 Part 1: 폼은 곧 클래스다

 



  프로그래머는 오브젝트로서 폼을 취급하죠. 사실, 폼은 클래스인데 말이죠. 여기서 차이점은 동일한 폼 클래스를 바탕으로 다중 폼 오브젝트를 가질 수 있다는 것이죠. 햇갈리는 것은 델파이는 여러분이 정의한 모든 폼 클래스를 기본적으로 글로벌 오브젝트를 생성한다는 것입니다. 이는 프로그래머를 아주 초보적인 것이고 나쁜 습관으로 빠뜨릴 수 있습니다.

 


 Rule 1: 유닛 하나에 클래스 하나!

 



  항상 기억하세요, 클래스의 private과 protected 부분은 단지, 다른 유닛에서 클래스와 프로시저에서는 숨겨진다는 것을 말입니다. 따라서 효과적인 캡슐화를 하려 한다면, 모든 클래스에 대해 각자 다른 유닛을 사용해야 한다는 것입니다. 다른 클래스에서 클래스를 상속하는 간단한 클래스에 대해서는, 실제로 공유 유닛을 사용할 수 있지만, 단지 클래스의 수를 제한한다면: 하나의 유닛에서 20개의 클래스의 복합적인 계층을 위치시키지 말았으면 하는 것이지요. 볼랜드에서 VCL소스 코드에 대량으로 공유 유닛을 작성했을지라도...
  폼에 대해 생각하자면, 델파이는 단순히 '한 클래스에 한 유닛' 원칙을 고수하고 프로젝트에 폼이 아닌 클래스를 추가할 때는 새 유닛을 생성하고 있습니다.

 


 Rule 2: 컴포넌트 명

 



  폼이나 유닛에 의미있는 명명을 하는 것은 아주 중요한데요. 안타깝게도, 이 두 명칭은 AboutForm과 About.pas와 같이 이 두가지에 대해 유사한 이름을 주려해도 반드시 달라야 합니다.

 


  또한 컴포넌트에 대한 적당한 명칭 사용도 중요한데요. 가장 일반적인 표기는 btnAdd 나 editName 과 같은 컴포넌트의 역할에 따라, 클래스 타입에 대해 소문자로 머리글자를 사용하는 것입니다. 이 스타일에 따라 실제로 많은 유사한 표기방법이 있는데요. 이 방법을 가장 좋은 방법이라고 할 수는 없겠지만 개인적인 취향이 있는거 아니겠습니까!

 


 Rule 3: 이벤트 명

 



  이벤트 핸들링 메소드에 적당한 명칭을 부여하는 것 또한 쉽게 생각할 일은 아니죠. 예를들어, Button1Click 이라는 컴포넌트명이 있다고 하구요. 메소드명이 버튼명에서 생성되는 것이라고 추측하겠지만, 컴포넌트명에 추가된 메소드명 보다는 메소드의 기능을 잘 설명하는 명칭이 더 나는 방법이라는 생각을 하게 됩니다. 예를들자면, btnAdd 버튼의 OnClick 이벤트는 AddToList라고 할 수도 있겠지요. 이 방법은 특히 클래스의 다른 메소드로부터 이벤트 핸들러를 호출할 때 판독성이 좋아지구요. 필자가 Actions를 사용하는 것이 더 좋다고 말하고 싶지만 같은 메소드를 다양한 이벤트나 다른 컴포넌트에 추가하는데 있어 개발자에게 도움이 됩니다.

 


 Rule 4: 폼 메소드를 사용하자

 



  폼이 클래스라면 그 코드는 메소드로 이루어집니다. 게다가 특별한 역할도 하고 다른 메소드로서도 호출가능한 이벤트 핸들러는 사용자 메소드를 폼 클래스에 추가하는게 종종 유용할게 먹힐 때가 있습니다. 여러분은 폼의 액션을 수행하고 상태를 액세스하는 메소드를 추가할 수도 있습니다. 말하자면, 폼의 컴포넌트를 다른 폼에서 직접 조작하는 것보다는 폼의 public 메소드를 사용하는 것이 더 낫다는 것이지요.

 


 Rule 5: 폼 생성자를 추가하자

 



  실시간에 생성된 두번째 폼은 디폴트로 있는 거 외에 특정 생성자를 작성할 수 있습니다( 상속된 폼 TComponent 클래스). 델파이 이전 버전과 호환이 필요없다면, 필자의 제안은 초기화 파라메터와 함께 Create 메소드를 오버로드(overload)하는 것입니다. 예제 1을 봅니다.

 






public
constructor Create (Text: string); reintroduce; overload;
constructor TFormDialog.Create(Text: string);
begin
inherited Create (Application);
Edit1.Text := Text;
end;


예제 1


 


 Rule 6: 전역 변수를 피하자

 



  (유닛의 Interface 부분에 선언된) 전역 변수는 되도록 피하도록 합니다. 여기에 괜찮은 제안을 보여주겠습니다.

 


  여러분들 폼에 여분의 데이터 저장공간이 필요하다면, private 필드들을 사용해 보세요. 이 경우에 각 폼 인스턴스는 데이터의 복사본을 가지게 되고, 폼 클래스의 여러 인스턴스들 중에서 공유된 데이터에 대한 유닛 변수들(유닛의 implementation 부분에 선언된)을 여러분이 사용하게 되는 겁니다. 

 


  여러분이 다른 타입의 폼들 사이에서 공유된 데이터가 필요하다면, 메인 폼이나 전역 개체에서 데이터를 놓고 데이터를 공유할수도 있고 데이터를 액세스하기 위해 메소드나 프로퍼티를 사용할 수도 있습니다.

 


 Rule 7: TFrom1 에서는 Form1를 사용하지 말자

 



  특정 오브젝트의 클래스의 메소드에서 특정 오브젝트를 참조하지 말아야 합니다. 바꾸어 말하면, TForm1 클래스의 메소드에서 Form1 을 참조하지 마라는 겁니다. 현재 오브젝트에 대한 참조가 필요하다면, Self 키워드를 사용하시면 됩니다. 직접 현재 오브젝트의 메소드와 데이터를 참조할 수 있기 때문에, 대부분의 경우 필요치 않다는 것을 십분 인지 하시기 바랍니다.

 


  여러분이 이 규칙에 따르지 않는다고 봤을 때에는 폼의 여러 인스턴스를 생성할 때 문제 발생의 여지가 많습니다.

 


 Rule 8: 다른 폼에서 Form1을 사용하지 말자

 



  심지어는 다른 폼의 코드에서도 Form1 과 같은 전역 오브젝트에 대한 직접적인 참조를 피하는 것이 좋습니다. 다른 폼에 대한 참조를 로컬 변수나 private 필드로 선언하는게 바람직합니다.

 


  예를들어, 프로그램의 메인폼이 다이얼로그 박스를 참조하는 private 필드를 가질 수 있습니다. 확실히, 이 규칙은 두번째 폼의 여러 이스턴스를 생성하는 것으로 계획한다면 필수적인 것입니다. 이것은 메인 폼의 한 필드에서 리스트를 갖고 있을 수 있고, 그렇지 않으면 전역적인 Screen 오브젝트의 Forms 배열를 사용할 수도 있는 것입니다.

 


 Rule 9: Form1을 제거하자

 



  실제로, 필자의 생각은 델파이에 의해 프로그램에 자동으로 추가된 전역 폼 오브젝트를 제거하는 것입니다. 이것은 단지(델파이에 의해 다시 추가된) 폼의 자동 생성을 불허한다면 가능합니다.

 


  필자의 생각에 전역 폼 오브젝트를 제거한다는 것은 델파이 초보에게도 매우 유용합니다. 클래스와 전역 오브젝트 사이를 혼동하지 않는 초보에게 말이죠. 사실, 전역 오브젝트가 제거된 후, 어떤 참조가 있다면 에러를 초래하게 되겠죠.

 


 Rule 10: 폼 프로퍼티를 추가하자

 



  필자가 이미 언급한 것처럼 폼에 데이터가 필요하면 private 필드를 추가하면 됩니다. 다른 클래스에 대한 액세스가 필요하다면, 그 때는 프로퍼티를 폼에 추가하면 됩니다. 이러한 접근에 따라 여러분은 다른 폼이나 클래스의 코드를 변경하지 않고 그 폼과 데이터(사용자 인터페이스를 포함해서)의 코드를 변경하게 될 것입니다.

 


  두번째 폼이나 다이알로그 박스 초기화를 위해서나 마지막 값을 얻기 위해 프로퍼티나 메소드를 사용할 수 있습니다. 초기화라는 것은 위에서 언급했듯이 생성자를 사용해서 수행할 수 있습니다.
 

 


 Rule 11: 컴포넌트 프로퍼티를 보이자

 



  여러분이 다른 폼의 상태를 액세스하기를 원할 때, 여러분은 그 컴포넌트를 직접 참조하지 않도록 해야합니다. 이 방법은 대부분의 변경에 대한 어플리케이션의 주요 부분중에 하나인 사용자 인터페이스에 다른 폼이나 클래스의 코드를 설정하는 것입니다. 더블어, 컴포넌트 프로퍼티로 맵된 프로퍼티를 선언합니다.

 


  만약 사용자 인터페이스를 변경하고자 한다면, 다른 컴포넌트로 해당 컴포넌트를 대체하세요. 여러분들이 해야할 것은 프로퍼티와 관련된 Get, Set 메소드를 수정하구요. 컴포넌트를 참조할 모든 폼과 클래스의 소스코드를 체크나 수정하지 않아야 합니다. 예제 2를 볼까요.

 






private
function GetText: String;
procedure SetText(const Value: String);
public
property Text: String
read GetText write SetText;
function TFormDialog.GetText: String;
begin
Result := Edit1.Text;
end;
procedure TFormDialog.SetText(const Value: String);
begin
Edit1.Text := Value;
end;

예제 2: 컴포넌트의 프로퍼티가 공개되도록 폼에 프로퍼티를 추가.


 


 Rule 12: 프로퍼티 배열

 



  폼안에 연속된 값을 처리할 필요가 있다면, 프로퍼티 배열을 선언하면 됩니다. 이런 경우에는 SpecialForm[3]을 작성해서 직접 값을 액세스 할 수 있도록 폼의 기본 프로퍼티 배열을 만들수 있다는 것은 폼에는 대단히 중요한 정보입니다. 

 


  예제 3에서는 프로퍼티 배열을 다루는 폼의 기본 배열 프로퍼티로서 listbox의 아이템을 어떻게 나타내는지 보여주고 있습니다.

 






type
TFormDialog = class(TForm)
private
ListItems: TListBox;
function GetItems(Index: Integer): string;
procedure SetItems(Index: Integer; const Value: string);
public
property Items[Index: Integer]: string read GetItems write SetItems; default;
end;
function TFormDialog.GetItems(Index: Integer): string;
begin
if Index >= ListItems.Items.Count then
raise Exception.Create('TFormDialog: Out of Range');
Result := ListItems.Items [Index];
end;
procedure TFormDialog.SetItems(Index: Integer; const Value: string);
begin
if Index >= ListItems.Items.Count then
raise Exception.Create('TFormDialog: Out of Range');
ListItems.Items [Index] := Value;
end;

예제 3: 폼에 기본 프로퍼티 배열의 정의



 


 Rule 13: 프로퍼티의 부과효과를 사용하자

 



  전역 변수를 사용하는 대신, 프로퍼티의 장점중에 하나가 프로퍼티의 값을 읽거나 쓸 때 부과효과가 나타날 수 있다는 것을 알아두세요.

 


  예를들면, 여러분이 폼에 직접 그리고, 다양한 프로퍼티의 값을 설정하고, 특별한 메소드를 호출하며, 한번에 여러 컴포넌트의 상태를 변경할 수 있다는 것인데 가능하다면 이벤트를 발생하는 것이죠.

 


 Rule 14: 컴포넌트를 숨기자

 



  필자는 캡슐화의 원칙을 따르지 않는 방식인 published 섹션에서 컴포넌트의 리스트를 포함하는 델파이 폼 때문에 OOP 추종자들이 불평하는 소리를 너무나 자주 들어왔습니다. 그들은 실제로 중요한 문제를 지적하고 있지만 그들의 대부분은 델파이를 재작성하지 않거나 언어를 변경하지 않고 해결책이 가까이에 있다는 것을 인지하지 못하는 것 같습니다.

 


  델파이는 폼에 추가할 수 있는 컴포넌트 레퍼런스가 private 부분으로 이동할 수 있다는 것 입니다. 다른폼에서 액세스하지 못하도록 말이죠. 컴포넌트의 상태를 액세스 하도록 (규칙 11)컴포넌트 맵된 프로퍼티를 의무적으로 사용을 하게 하는 방법입니다.

 


  델파이가 published 섹션에 컴포넌트를 위치하는건 이러한 필드가 *.DFM 파일로부터 생성된 컴포넌트를 설정하는 방법 때문이죠. 컴포넌트 명을 설정할 때 VCL은 자동으로 폼에 컴포넌트의 오브젝트를 그 레퍼런스에 추가합니다. 델파이가 이를 수행하기 위해 RTTI와 TObject 메소드를 사용하기 때문에 레퍼런스가 published 되는 이유이죠.

 


  더욱 자세하게 살펴보고 싶다면 InsertComponent, RemoveComponent, 그리고 SetName에 의해 호출된 TComponent 클래스의 SetReference 메소드의 코드를 가진 예제 4를 참조하세요.

 


  여러분이 이해를 했다면, published에서 private 섹션으로 컴포넌트 레퍼런스를 이동해서 이런 자동적인 작업을 벗어나게 됩니다. 이런한 문제를 해결하기 위해서 단순히 폼 OnCreate 이벤트 핸들러에 각 컴포넌트에 대해 아래와 같은 코드를 추가를 합니다.

 


Edit1 := FindComponent(.Edit1.)As TEdit;

 



  다음으로 해야할 일은 시스템에서 컴포넌트 클래스를 등록하는 것 입니다. RTTI 정보가 컴파일된 프로그램에 포함되고 시스템에서 이용될 수 있도록 말이죠. 모든 컴포넌트 클래스에 대해서 단지 한번이면 됩니다. private 섹션에 컴포넌트 레퍼런스 타입을 이동한다면 말이죠. 여러분은 이 작업이 필요하지 않더라도 RegisterClasses 메소드에 대한 추가적인 호출이 무해하기 때문에 이러한 호출을 추가할 수 있습니다. RegisterCalsses 메소드는 흔히 폼을 다루는 유닛의 initialization 섹션에 추가되고 있습니다.

 


RegisterClasses([TEdit]);

 






procedure TComponent.SetReference(Enable: Boolean);
var
Field: ^TComponent;
begin
if FOwner <> nil then
begin
Field := FOwner.FieldAddress(FName);
if Field <> nil then
if Enable then
Field^ := Self
else
Field^ := nil;
end;
end;

예제 4: owner 폼에서 컴포넌트가 그 참조를 가로채는 VCL 코드



 


 Rule 15: OOP Form Wiard

 



  모든 폼의 모든 컴포넌트에 대해 위의 두가지 방법을 반복한다면 아주 지루하고 시간낭비가 아닐 수 없습니다. 이런 과중한(?) 부담을 피하기 위해서 필자는 작은 윈도우에서 프로그램에 추가할 수 있는 코드의 라인을 생성하는 간단한 wizard를 작성했습니다. 각 폼에 대해 두번의 Copy & Paste 작업을 할 필요가 있습니다.

 


  Wizard는 적당한 위치에 자동으로 소스코드를 배치하지는 않습니다: 필자는 update 버전을 만들면서 필자의 웹사이트 (http://www.marcocantu.com)에 올려놓고 있습니다.


 




 


 Part 2: 상속 

 



  위의 규칙들이 클래스, 특별히 폼 클래스에 집중되어 있지만 이제부터는 상속과 비주얼 폼 상속에 관련된 간단한 제안과 팁들을 볼 수 있습니다.

 


 Rule 16: 비주얼 폼 상속

 



  상속성이란 적당히 사용하면, 강력한 메카니즘입니다. 필자의 경험으로 비추어 볼 때, 이 가치는 프로젝트의 단위가 커질수록 상승합니다. 복잡한 프로그램에서 다형성과 함께 폼 그룹상에 작용하는 폼들 사이에 계층관계를 사용할 수 있습니다.

 


  비주얼 폼 상속은 다양한 폼의 일반적인 작업의 공유가 가능하게 합니다: 메소드, 프로퍼티, 이벤트 핸들러, 컴포넌트 이벤트 핸들러 등.

 


 Rule 17: Protected 데이터의 제한

 



  클래스 계층을 작성할 때, 어떤 프로그래머들은 private 필드들이 서브클래스에서 액세스 되지 않기 때문에 주로 protected 필드를 사용하는 경향이 있습니다. 필자는 이것이 나쁘다고 말하고 싶지는 않지만 캡슐화의 개념과는 상반되는 것입니다. protected 데이터의 구현은 모든 상속된 폼들에서 공유됩니다. 데이터의 오리지날 정의에서의 변경 같은 경우에도 모든 것을 업데이트 해야할지도 모르죠.

 


  규칙 14의 컴포넌트 숨기기를 따른다면, 상속된 폼은 상위 클래스의 private 컴포넌트를 액세스 할 수 없다는 것을 명심하세요. 상속된 폼에서는 "Edit1.Text := ..;"와 같은 코드가 더이상 컴파일 되지 않습니다. 필자는 이러한 것이 매울 불편하지만, 적어도 이론상으로는 나쁘지 않다고 봅니다. 캡슐화에 대한 특권이 과하다면, 상위 폼에 protected 섹션에서 컴포넌트 레퍼런스를 선언하세요.

 


 Rule 18: Protected 액세스 메소드

 



  private섹션에서 컴포넌트 레퍼런스를 배치하는게 더 좋고, 상위 클래스에 컴포넌트 프로퍼티를 액세스 함수를 추가하는 것도 좋지요. 이러한 액세스 함수는 단지 내부적으로 사용되고 클래스 인터페이스 부분이 아니면, protected로서 이들을 선언해야 하지요. 예를 들자면, 규칙 11에 서술된 GetText, SetText 폼 메소드는 protected되고 호출에 의해 edit text를 액세스할 수 있게 됩니다:

 


SetText(..);

 



  실제로, 메소드가 프로퍼티로 맵되기 때문에, 우리는 단순히 아래와 같이 작성합니다:

 


Text := ..;

 


 Rule 19: Protected 가상 메소드

 



  유연한 계층을 위한 또 다른 키 포인트는 다형성을 갖는 외부 클래스로부터 호출할 수 있는 가상 메소드를 선언하는 것입니다. 이를 일반적인 접근법이라고 한다면, 다른 public 메소드로 호출된 protected 가상 메소드를 자주 볼 수는 없습니다. 이는 오브젝트의 작업을 수정하면서 상속된 클래스에서 가상 메소드를 조작할 수 있기에 중요한 테크닉이 됩니다.

 


 Rule 20: 프로퍼티를 위한 가상 메소드

 



  프로퍼티 액세스 메소드는 상속된 클래스가 프로퍼티를 재정의하지 않고 프로퍼티의 작업을 변경할 수 있도록 가상으로 선언될 수 있습니다. 이러한 접근법은 VCL에서는 거의 사용되지 않지만 매우 유연하고 강력합니다. 이를 구현하기 위해, 단순히 규칙 11의 Get, Set 메소드를 가상으로 선언하면 됩니다. 상위 폼은 예제 5 코드를 갖게 될 겁니다.

 


  상속된 폼에서 여러분은 몇가지 기능추가로 가상 메소드 SetText를 override할 수 있습니다:

 



procedure TFromInherit.SetTetx(Const Value: String);
begin
inherited SetText (Value);
if Value = .. then
Button1.Enabled := False;
end;

 






type
TFormDialog = class(TForm)
procedure FormCreate(Sender: TObject);
private
Edit1: TEdit;
protected
function GetText: String; virtual;
procedure SetText(const Value: String); virtual;
public
constructor Create (Text: string); reintroduce; overload;
property Text: String read GetText write SetText;
end;

예제 5: 가상메소드로 구현된 프로퍼티의 폼 클래스.


 


 The Code 

 



  이 강좌에서 모든 코드는 이달의 디스크에 포함된 OOPDemo 예제 프로젝트에 있습니다. 두번째 폼(frm2 유닛)을 체크하고 상속된 폼(inherited unit)도 체크해보세요. initialization 코드와 private 컴포넌트 레퍼런스로 동시에 조작된 생성자를 사용하기 위해 폼의 OldCreateOrder 프로퍼티를 설정해야 한다는 것을 명심하세요. 그렇지 않으면 폼 생성자(컴포넌트를 사용하는)에서 initialization 코드는 실제 컴포넌트에 레퍼런스로 연결하는 폼의 OnCreate 메소드 이전에 실행될 것입니다.

 


  디스크상에서 OOP Form Wizard라는 첫 작품의 컴파일된 팩키지를 볼 수 있습니다만 필자의 홈페이지에서 업데이트된 버전을 찾는게 훨씬 나을 것입니다.

 


 Conclusion 

 



  훌륭한 OOP 원칙에 따라 델파이로 프로그래밍은 필지가 강조한 규칙들의 명확함과는 동떨어져 있습니다. 규칙은 적당한 내용으로 적용되어야 하고 규칙에 따라 작업하는 다수의 프로그래머 사이에 어플리케이션의 크기가 성장하는 만큼 더 중요합니다. 작은 프로그램을 개발하는데 있어도 필자가 주장하는 규칙의 OOP의 원칙(무엇보다 캡슐화)을 기억한다는 것은 실제로 도움이 될 것 입니다.

 


  특정 기사가 될만큼 아주 복잡한 RTTI 이슈와 메모리 핸들링에 익숙하지 않았기 때문에 여러분이 배울 수 있는 다양한 규칙들이 있겠지요.

 


  필자의 결론은 필자가 강조했던 규칙들이 추가적 코드에 의하여 대가를 필요로 하는 것이죠: 더 유연하고 내구성있는 프로그램을 얻고자 지불해야하는 값어치 말입니다. 미래의 델파이 버전은 우리가 고생을 덜 하도록 도움을 주리라 믿습니다.

 




Marco Cantu는 Mastering Delphi Series, Delphi Developer's Handbook, 그리고 무료 온라인 북 Essential Pascal의 작가입니다. 그는 델파이 기초와 고급과정을 가르치고 있습니다. 더 자세한 사항은 그의 홈페이지 http://www.marcocantu.com 에서 확인하시길 바랍니다. 여러분은 그의 공개 뉴스그룹에서 그를 볼 수 있습니다: 상세한 정보는 그의 홈페이지를 보세요.


 









번역: 소백촌닭, 원문: Marco Cantu, Issue 47, Delphi Magazine

관련자료

등록된 댓글이 없습니다.
프로그래밍
Today's proverb
식욕없는 식사는 건강에 해롭듯이, 의욕이 동반되지 않은 공부는 기억을 해친다. (레오나르도 다빈치)