Jump to content
과거의 기술자료(읽기 전용): https://tech.devgear.co.kr ×
과거의 기술자료(읽기 전용): https://tech.devgear.co.kr

[아티클][DelphiCon 요약] Spring4D 소개 - 델파이 개발을 한수준 높이기


Recommended Posts

<< DelphiCon 2020 목록으로 이동

DelphiCon 의 2020 시리즈 중, Introduction to Spring4D - Taking Delphi Development to the Next Level - Stefan Glienke 의 한글 요약본입니다.  

  • 오픈 소스인 스프링4D를 사용하면, 코드는 더 짧아지고, 일관성과 품질은 더 높아집니다.
  • 예를 들어,
    • 스마트 포인터를 사용하면 오브젝트를 만들어 쓸 때 메모리 누수를 걱정할 필요가 없습니다.
    • Spring4D의 컬렉션은 컬렉션이나 리스트 관리가 매우 간편합니다.
  • 발표자 (Stefan Glienke)는 델파이 경력 20년이 넘은 개발자이며, Spring4D 오픈소소의 개발 책임자입니다.
  • 원본 비디오에는 아래 요약보다 자세한 데모와 설명이 있습니다. 

목차


Spring4D 란

  • 델파이 개발을 더 쉽고 더 견고하게 할 수 있도록 미리 구성된 오픈소스 라이브러리
    • 2010년에 출시된 XE와 그 이후 버전에서 사용 가능)
    • 상업용 무료 사용 (아파치 2.0 오픈 소스 라이선스)
    • 델파이 RTL 확장
    • 제네릭스(Generics)와 RTTI를 적극 활용
    • 계속 발전하는 중이고 상업용 소프트웨어 개발에 활용되고 있음
  • 원칙: “골라 쓰기” - 원하는 것만 골라서 사용할 수 있다. 강제하지 않는다.
  • 버전: 2020년 12월 현재 1.2.4 - 곧 2.0 발표 예정

Spring4D를 받는 방법

  • Git에서 내려받기 (버전콘트롤을 사용하지 않는다면 전체 파일 다운로드): https://bitbucket.org/sglienke/spring4d.git 
  • Build.exe 를 실행하여 간편하게 설치/설정 가능 (또는 전체 파일을 직접 컴파일 하여 사용하기)
  • 코드를 모두 받아서 컴파일하여 사용할 수 있지만, 미리 컴파일된 .dcu를 사용하는 것이 편함 (단 몇초지만 매번 컴파일하는 시간도 아끼자)

Spring4D의 기본 구성

  • Base: RTL 확장, 컬렉션
  • Core: 의존성 주입(DI, Dependency Injection) 컨테이너, 인터셉션/목킹(Mocking)
  • Data: ObjectDataSet을 이용해 (TDBGrid 등) 데이터를 인식하는 UI 콘트롤에 연결
  • Persistence: ORM
  • Extensions: 암호화 및 기타 유틸리티 (암호화는 다른 암호화 전문 라이브러리보다 약함)

Spring4D 구성 요소: Spring.Base

  • Nullable<T>
  • Event<T> 멀티 캐스트 이벤트
  • 스마트 포인터 (Shared<T> 와 Weak<T>)
  • Collections: 가장 많이 활용되는 라이브러리
  • IEnumerable<T>
  • 리스트, 딕셔너리, 멀티맵, 세트, 큐

Nullable<T>

  • 특정 데이터 타입을 유지하면서도, Null값을 가질 수 있도록 한다.
  • 이제는 억지값 즉, Null (값이 없음)을 넣기 위해 아래와 같은 값이나 코드가 필요가 없다.
    • 날짜 타입에 “날짜가 주어지지 않음”을 넣기 위해 -40000 이라는 가상의 값 넣기
    • Boolean에 “참인지 거짓인지 모름”을 넣기 위해 True/False/FileNotFound로 Inum을 직접 만들어 사용하기
  • Variant 타입과는 다름
    • Variant는 호환 가능한 타입 변환을 암시적으로 수행하므로 언제든 다른 타입으로 변환될 여지가 있다(type unsafe).
      • 예를 들어 Variant에는 숫자 3과 문자열 ‘3’을 모두 넣을 수 있고, 상황에 맞게 반영된다.
    • Nullable<T>는 타입을 유지하기 때문에, 정해진 타입만 담을 수 있다(type safe).
      • Nullable<Integer>에는 정수 3만 넣을 수 있고 문자열 3을 넣을 수는 없다.
// [Nullable<T> 예문]

uses
  Spring;
procedure 
var
  n, n2: Nullable<TDateTime>; // Spring 유닛 필요
  d: TDateTime;
begin
  d := now;
  Log.Lines.Add(n.HasValue.ToString); //[결과: False]: 디펄트에서 Nullable.HasValue의 결과는 False
  Log.Lines.Add(n.ToString); //[결과: Null]:디펄트에서 Nullable.ToString의 결과는 ‘Null’(값없음)

  n := d;
  Log.Lines.Add(n.HasValue.ToString); //[결과: True]: 특정 시간을 넣으면, Nullable.HasValue의 결과는 True
  Log.Lines.Add(n.ToString); //[결과: 19.11.2020 18:11:51]: 독일 기준 날짜 형식 (발표자의 시간대가 독일로 되어 있음)

  n := nil; //n에 들어있는 값을 없앤다
  Log.Lines.Add(n.HasValue.ToString); //[결과: False]: nil을 넣으면, Nullable.HasValue의 결과는 False
  Log.Lines.Add(n.GetValueOrDefault().ToString); //[결과: 30.12.1899] // Nullable.GetValueOrDefault().ToString의 결과는 값이 있으면 그 값을, 없으면 기본값인 30.12.1899을 출력

  Log.Lines.Add((n=n2).ToString); //[결과: True]: n과 n2는 둘다 값이 없으므로, n과 n2를 비교하면 ‘같음’이 된다
  n := d;
  Log.Lines.Add((n=n2).ToString); //[결과: False]: n에만 d를 넣으면, n과 n2를 비교하면 ‘다름’이 된다
  n2 := d;
  Log.Lines.Add((n=n2).ToString); //[결과: True]: n과 n2는 둘다 d와 같은 값이므로, n과 n2를 비교하면 ‘같음’이 된다.
end;

 

Event<T>

  • (필요한 타입을 따로 만들 필요가 없어서 쉽게 사용되는) Observer 패턴과 유사하다.
  • publish와 Subscribe를 구현하기가 간편하하다.
  • 델파이에서 일반 이벤트 사용하는 것과 방식이 같다.
  • 제네릭 타입을 사용하므로 파라미터와 리턴 타입이 유연하다
    • <T>에 (TNotifyEvent 등) 원하는 타입을 지정하면 된다.
  • 빠르고 쓰레드에 안전하다
// [Event<T> 멀티 캐스트 이벤트 예문]
uses
  Spring;

private fOnMouseMove: Event<TMouseMoveEvent>; // 델파이 기본 이벤트에서 마우스 상태값을 받아온다

// Event<TMouseMoveEvent>를 구독(subscribe)하는 함수
procedure 
var
  subscriber: TEventSubscriber; //구독자 변수
begin
  subscriber := TEventSubscriber.Create(Self); //구독자 생성
  fOnMouseMove.Add(subscriber.HandleMouseMove); //구독자가 잡은 마우스 이동 이벤트를 넣는다 (fOnMouseMove는 실제로 이벤트핸들러들의 목록이므로 Add 메소드를 가지도록 되어있다. 그 결과 한번 작동하면 목록에 있는 모든 이벤트핸들러가 작동한다.)
  subscriber.OnUpdate.Add(ChangeCaption); //구독자의 OnUpdate이벤트 발생시 실행할 이벤트핸들러 지정 (좌표값을 폼의 캡션에 찍도록 함)
  subscriber.OnUpdate.Add(ChangeColor); // (마우스 Y좌표가 300이상이면 빨강으로 바꾸고 아니면, 일반 윈도우 색을 유지하도록 함)
  Log.OnMouseMove := fOnMouseMove; //TMomo인 Log의 OnMouseMove 이벤트에 Event<TMouseMoveEvent>를 연결하여 포함된 이벤트핸들러가 모두 실행되도록 한다.
  fOnMouseMove.OnChange := NotifyEventChange; //Event<T>에 이벤트핸들러가 추가/삭제 되면 공지한다. (예를 들어 이 예문에서 폼을 종료하면, 이 이벤트가 작동된다. 그 이유는 TEventSubscriber.Create(Self)로 생성되었고, Self는 MainForm였다. 따라서 TEventSubscriber의 owner 역시 메인폼이 된다. 메인 폼이 제거되면, 이 구독자 역시 제거되고 등록된 이벤트핸들러 역시 제거된다.)
end;

 

스마트 포인터 (Shared<T> 와 Weak<T>)

  • 오브젝트와 기타 리소스의 생명주기 관리 능력 추가
  • try finally 를 사용하여 명시적으로 오브젝트를 제거하지 않아도 된다.
    • 이점은 여러 리소스 간에 오브젝트를 주고 받는 것 역시 간편하게 해준다.(어느 곳에서 제거해야 하는지를 걱정할 필요가 없다)
  • Weak<T>에서는 순환 참조 문제가 없다 (심지어 델파이에 [Weak] 속성이 추가되기 전부터 있었다)
  • 보너스: Weak<T>로 만든 오브젝트는 모든 플랫폼에서 작동한다. 또한 오브젝트가 제거될 때 알림을 받을 수 있다
// [스마트 포인터 (Shared<T> 와 Weak<T>) 예문]

// Shared<T>: 레코드 타입으로 사용할 때
procedure 
var
  sl: Shared<TStringList>;
begin
  sl := TStringList.Create; // 연산자 오버로딩
  sl.Value.Add(‘델파이콘’); // Value 프로퍼티를 사용하여 접근한다 (멤버 변수에 직접 접근 불가) 
  Log.Lines.AddStrings(sl); // <T>에서 지정한 타입을 그냥 전달하는 것과 같은 방식으로 전달될 수 있다.
end;

// IShared<T>: 인터페이스 타입으로 사용할 때
procedure 
var
  sl: IShared<TStringList>; // 인터페이스 타입으로 사용할 때
begin
  sl := Shared.Make<TStringList>(TStringList.Create); // 직접 생성하지 않고 인터페이스에 맞추어야 한다.
  sl.Add(‘델파이콘’); // (이 인터페이스는 익명메소드처럼 지정된 타입을 반환하기 때문에) 멤버에 직접 액세스할 수 있다.
  Log.Lines.AddStrings(sl); //<T>에서 지정한 타입을 그냥 전달하는 것과 같은 방식으로 전달될 수 있다.
end;

// IShared<T>로 생성된 오브젝트를 직접 제거하고 싶으면, 파라미터로 익명메소드를 넣는다.
procedure 
var
  sl: IShared<TStrings>;
begin
  sl := Shared.Make<TStrings>(TStringList.Create,
    procedure(const s: TStrings)
    begin
      Log.Lines.AddStrings(sl); //TMemo인 Log에 ‘델파이콘’을 출력하고
      s.Free; //생성된 TStrings를 제거
    end);
  sl.Add(‘델파이콘’);
end;

// 스마트 포인터로 만든 오브젝트는 오브젝트 제거 코드를 쓰지 않아도. 자동 제거되므로 메모리 누수를 염려하지 않아도 된다.
// ReportMemoryLeakOnShutdown := True; //델파이 프로젝트에서 메모리 누수를 확인하는 코드

// Weak<T>
procedure 
var
  sl: TStringList; //Shared<T>가 아닌 일반 TStringList
  weakRef: Weak<TStrings>; //문자열을 Weak 참조하기로 한다.
begin
  sl := TStringList.Create;
  weakRef := sl; // 연산자 오버로딩
  sl.Add(‘델파이콘’);

  Log.Lines.Add(weakRef.IsAlive.ToString); //[결과: True]: 참조 타겟이 살아있다
  Log.Lines.AddStrings(weakRef); //[결과: 델파이콘]: 참조 대상의 값, 연산자 오버로딩
  Log.Lines.AddParagraph;

  sl.Free; //Share<T> 가 아니므로 직접 Free 했다. 그러면, Weak 참조하고 있는 곳에 모두 공지된다.

  // Weak<T>는 모든 플랫폼에서 작동된다.
  Log.Lines.Add(weakRef.IsAlive.ToString); //[결과: False]: 참조 대상이 제거되었음’을 알고 있음
  Log.Lines.Add(Assigned(weakRef.Target).ToString); //[결과: False]: 참조 대상이 Assigned 되지 않았음’을 알고 있음, Weak<T>.Target와 Shared<T>.Value는 같은 역할
  Log.Lines.Add(weakRef <> nil).ToString;  //[결과: False]: 참조 대상이 nil 임’을 알고 있음
end;

 

Collections: 가장 많이 활용되는 라이브러리

  • 인터페이스 기반이다.
    • 메모리 관리가 매우 쉽다
    • Collection를 함수에서 반환하고, 어느 곳에서든 사용하기만 하면 된다. 사용이 끝나면 자동 제거 된다.
  • IEnumberable<T>를 사용하여 API가 확장되었다.
    • 명확하게 표현되는 코드를 사용하도록 메소드가 제공된다.
    • 성가시게 루프를 사용할 필요가 적어진다.
  • Systems.Generics.Collections 대체한다.
    • 100%는 아니지만 왠만한 건 다 있고 델파이 컬렛션을 Spring.Collections로 마이그래이션도 쉽다.
  • TCollections와 IEnumberable에서 항상 팩토리 메소드를 사용한다.
    • (델파이 컬렉션과 달리) 클래스를 직접 사용하면 안된다. 제공되는 팩토리 메소드를 사용해야 한다.
    • Spring.Collection.* 유닛을 uses에 추가하면 해당 컬렉션을 사용할 수 있다.

Collections 인터페이스

  • IEnumberable<T>
    • (정렬되지 않은) 항목들의 나열을 제공하는 기반 타입
  • ICollection<T>, IReadOnlyCollection<T>
    • (인덱스 번호가 지정되지 않은) 항목들의 컬렉션, List의 기반 타입
  • IList<T>, IReadOnlyList<T>
    • (인덱스 기반의 메소드가 제공되는) 리스트
  • IMap<TKey, TValue>, IReadOnlyMap<TKey, TValue>
    • Dictionary와 MultiMap의 기반 타입
  • IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>
  • IMultiMap<TKey, TValue>, IReadOnlyMultiMap<TKey, TValue>
  • ISet<T>
    • MultiSet의 기반 타입
  • IMultiSet<T>, IReadOnlyMultiSet<T>

 

읽기전용 vs 불변

  • Spring4D에는 불변 컬렉션이 없다. (추가/변경/삭제가 해당 컬렉션 안에서 발생한다)
  • 하지만, 모든 타입은 ReadOnly 버전이 제공된다. (AsReadOnly 메소드 사용), 멀티쓰레드에 안전하지 않지만, 컬렉션을 사용하는 곳에서 추가/변경/삭제를 할 수 없다.
  • 컬렉션을 읽기 전용 목적으로 전달할 때에는 IEnumberable<T> 또는 알맞은 ReadOnly 컬렉션을 사용하자.

IEnumberable<T>

  • 정렬되지 않고, 실현되지 않은(not materialized) 항목들을 열거할 수 있는 기반 타입
  • 모든 컬렉션 타입의 기반
  • 사용할 수 있는 메소드가 매우 많음
  • ‘실현되지 않은 (not materialized)’ 이란?
    • 쿼리문 이라고 생각하면 됨 (Where절에 명시한 조건에 따라 가져올 항목이 결정되는 SQL구문과 유사)
    • SQL문이 실행되어야 해당 데이터 집합이 생기는 것과 같은 방식 (실행이 되고 난 후에야 해당 조건에 해당하는 컬렉션이 생겨난 다)
    • 물론 어딘가에는 전체 데이터가 있겠지만, 내가 원하는 컬렉션 세트는 조건이 실행되고 난 후에야 생긴다.
// [IEnumberable<T>예문 (주문 총액이 9,000 이상인 고객 목록)]
customers.Where (
  function (const c : TCustomer): Boolean
  begin
    Result := c.OrderTotal > 9000;
  end);

스트리밍과 지연 실행

  • IEnumberable<T>의 작동은 최대한 늦게 실행된다
    • 예를 들어,
    • customer.Where(조건).Take(10);
    • 에서 조건에 해당하는 대상을 모두 구하고 나서 거기에서 10개를 뽑는 것이 아니라, 대상 10개를 얻을 때까지만 조건이 작동한다. 
  • 즉, 작동이 되고 나서야 컬렉션이 만들어진다.

ICollection<T>

  • 정렬되지 않은 항목들의 (변경 가능한) 컬렉션
    • Add / Remove / Extract 메소드 제공
  • IEnumberable<T>와 달리 언제나 항목을 가지고 있는 컬렉션
  • OnChange 이벤트가 제공되므로, 어떠한 변경도 공지할 수 있음

IList<T>

  • 정렬된 항목들의 (변경 가능한) 리스트
  • 인덱스를 통해 각 항목에 접근 가능
  • 정렬 기반 기능 제공 (Insert, Delete, IndexOf, Sort,…)
// [IEnumberable<T> 예문]

procedure 
var
  numbers: IEnumberable<Integer>;
  i: Integer; 
  oddNumbers: IEnumberable<Integer>;
begin
  numbers := TEnumberable.Range(1,10);
  for i in numbers do
    Log.Lines.Add(i.ToString); // 1˜10까지 숫자 출력
    Log.Lines.AddParagraph;

  oddNumbers := numbers.Where(
    function(const n: Integer): Boolean
    begin
      Result := Odd(n);
    end);
  for i in oddNumbers do
    Log.Lines.Add(i.ToString); // 1~10 중 홀수만 출력
    Log.Lines.AddParagraph;
    Log.Lines.Add(oddNumbers.Last.ToString); // 9 출력
end;

// [IList<T> 예문]

// IList<T>에는 인덱스 위치에 삽입이 가능하다.
procedure 
var
  list: IList<Integer>;
  i: Integer;
  oddNumbers: IEnumberable<Integer>;
begin
  list := TCollection.CreateList<Integer>([4,5,6]);
  list.AddRange([7,8,9]);
  list.InsertRange(0, [1,2,3]);

  for i in list do
    Log.Lines.Add(i.ToString); // 1˜9까지 순서대로 숫자가 출력
    Log.Lines.AddParagraph;

  oddNumbers := list.Where(
    function(const n: Integer): Boolean
    begin
      Result := Odd(n);
    end);

  Log.Lines.Add(’숫자 11까지’);
  list.Add(11); // 뒤늦게 숫자 11을 List에 추가
  for i in oddNumbers do // oddNumbers는 정의된 조건에 맞는 컬렉션을 이 시점에서 다시 제공
    Log.Lines.Add(i.ToString); // 1~11까지 중 홀수만 출력 (11도 포함된다)
    Log.Lines.AddParagraph;
end;

 

IMap, IDictionary, IMultiMap

  • IMap은 IDictionary와 IMultiMap의 기반 타입이다
    • Add, Remove, ContainKey 등등의 메소드가 제공된다.
  • IDictionary는 ‘키-값’으로 구성된 쌍들이 모여있는 컬렉션 (키는 고유해야 함)
  • IMultiMap는 일종의 딕셔너리이며 각 값에 리스트가 들어간다 (즉, 키 하나에 여러 항목이 들어갈 수 있다)
    • 값에 들어 있는 컬렉션을 다룰 수 있는 여러 가지 기능이 제공된다.
// [IMultiMap<TKey, TValue> 예문]

// IMultiMap<TKey, TValue> 에는 키 하나에 여러 값이 들어갈 수 있다.
procedure 
var
  words: IMultiMap<Integer, string>;
  s: string;
  i: Integer;
  oddNumbers: IEnumberable<Integer>;
begin
  words := TCollection.CreateMap<Integer, string>;

  for s in LorenIpsum do // LorenIpsum은 저자가 만든 무작위 글자 생성기
    words.Add(s.Length, s); //키에는 단어 수를, 값에는 해당 단어를 넣어 추가한다.

  for i in words.Keys.Ordered do
  begin
    Log.Lines.Add(‘글자 길이가 %d자인 단어:’, [i]);
    for s in words[i] do
      Log.Lines.Add(s);
  end;
end;

{
[결과 (및 해설)]

// 키(글자 수)는 정렬되었지만
// 값(해당 글자)는 정렬되지 않고, 중복되기도 한다.
글자 길이가 2자인 단어:
ut
et
At
et
et

글자 길이가 3자인 단어:
sit
sed
sed
eos

글자 길이가  

// words := TCollection.CreateMap<Integer, string>; 대신
// words := TCollection.CreateHashMap<Integer, string>;을 사용하면
// 값(해당 글자)의 중복이 제거된다.
// 중복된 값은 받지 않기 때문이다.
글자 길이가 2자인 단어:
ut
et
At

글자 길이가 3자인 단어:
sit
sed
eos

글자 길이가 


// 이 때, 값을 출력하는 루프에서 값도 정렬하고 싶다면,
// for s in words[i] do를
// for s in words[i].Ordered do로 변경하면 된다 
// 다른 방법으로는 (더 간단하고 성능이 좋은 방법으로는),
// words := TCollection.CreateTreeMap<Integer, string>;을 사용하면
// Ordered 없이도 값(해당 글자)의 중복이 제거될 뿐만 아니라 값도 정렬된다.
글자 길이가 2자인 단어:
At
ea
et
no
ut

글자 길이가 3자인 단어:
duo
eos
est
sea
sed
sit

글자 길이가 

 

ISet, IMultiSet

  • Enum 세트와 유사하다. 하지만 enum 이외의 어떤 타입도 가능하다
  • 고유한 항목들로만 구성된 컬렉션이다 (마치 딕셔너리에서 값이 없는 키들의 집합과 같다.)
    • 내부적으로 hashtable 또는 tree를 통해 구현된다.
      • HashTable은 델파이 해시테이블과 달리 순서에 맞게 삽입한다.
      • Tree는 비교 기준에 맞추어 알맞은 순서로 저장한다.
  • IMultiSet은 Dictionary<T, Integer>이며 숫자 즉 갯수는 추가/삭제에 따라 증가/감소한다.
    • 갯수 파악이 매우 쉽다.
// [IMultiSet 예문]

procedure 
var
  wordCounts: IMultiSet<string>;
begin
  wordCounts := TCollection.CreateMultiSet<string>(LorenIpsum); //LorenIpsum은 문자열 배열을 반환

  for var entry s in wordCounts.Entries do
    Log.Lines.Add(‘%s %d, [entry.Item, entry.Count]);
  end;
end;

{
[결과 (및 해설)]

// 각 항목 별 갯수가 표시된다
Lorem 4
ipsum 4

elitr 2
sed 4

labore 2
et 8

// 만약 글자수 순으로 정렬되도록 하려면
// for var entry s in wordCounts.Entries do를
// for var entry s in wordCounts.OrderedByCount.Entries do 로 바꾸면 된다.
et 8
Lorem 4
ipsum 4
dolor 4
sit 4
amet 4

consetetur 2
sadipscing 2 

 

더 많은 내용

  • 오픈 소스 코드 살펴보기 (유닛 테스트 코드 역시 많이 있다)
  • 코드에는 xml 문서도 많다.
  • 도움말과 시작하기 문서
  • 질문 또는 의견: https://groups.google.com/g/spring4d
  • 버그 리포트: https://bitbucket.org/sglienke/spring4d/issues

 

 

<< DelphiCon 2020 목록으로 이동


View full 엠바카데로 기술자료

이 댓글 링크
다른 사이트에 공유하기

이 토의에 참여하세요

지금 바로 의견을 남길 수 있습니다. 그리고 나서 가입해도 됩니다. 이미 회원이라면, 지금 로그인하고 본인 계정으로 의견을 남기세요.

Guest
이 토픽(기고/질문)에 답하기

×   서식있는 텍스트로 붙여넣기.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   이전에 작성한 콘텐츠가 복원되었습니다..   편집창 비우기

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

중요한 정보

이용약관 개인정보보호정책 이용규칙 이 사이트가 더 잘 작동하기 위해 방문자의 컴퓨터에 쿠키가 배치됩니다. 쿠키 설정 변경에서 원하는 설정을 할 수 있습니다. 변경하지 않으면 쿠키를 허용하는 것으로 이해합니다.