Delphi [DelphiCon 요약] Spring4D 소개 - 델파이 개발을 한수준 높이기 (Introduction to Spring4D - Taking Delphi Development to the Next Level)
2021.01.08 13:52
- 원본 비디오 시청: https://delphicon.embarcadero.com/talks/introduction-to-spring4d-taking-delphi-development-to-the-next-level/
- DelphiCon 전체 보기 (현재 무료, 향후 유료 전환 예상): https://delphicon.embarcadero.com/replays/
- 데브기어의 DelphiCon 소개 페이지로 가기: https://devgear.co.kr/archives/3692
Spring4D 소개 - 델파이 개발을 한수준 높이기 (Introduction to Spring4D - Taking Delphi Development to the Next Level) 를 요약했습니다. (이 요약 번역은 원본 비디오와 내용이 일부 다르거나, Q&A등 일부 생략되었을 수 있습니다.)
이 스프링4D는 델파이 개발을 더 쉽고 더 견고하게 할 수 있도록 미리 구성된 라이브러리입니다. 많은 개발자들에게 필요했던 타입과 확장 기능이 구현되어 있습니다. 예를 들어, 스마트 포인터를 사용하면 메모리 누수 걱정없이 오브젝트를 만들어 쓸 수 있습니다. Spring4D의 컬렉션은 컬렉션이나 리스트 관리가 매우 간편합니다. (이런 라이브러리 또는 프레임워크를 사용하면) 코드는 더 짧아지고, 일관성과 품질은 더 높아집니다.
- Spring4D란?
- Spring4D를 받는 방법
- Spring4D의 기본 구성
- Spring4D 구성 요소: Spring.Base
- Nullable<T>
- Event<T>
- 스마트 포인터 (Shared<T> 와 Weak<T>)
- Collections
- Collections 인터페이스
- 읽기전용 vs 불변
- IEnumberable<T>
- 스트리밍과 지연 실행
- ICollection<T>
- IList<T>
- IMap, IDictionary, IMultiMap
-
ISet, IMultiSet
- 더 많은 내용
발표자 (Stefan Glienke)는 델파이 경력 20년이 넘은 개발자이며, Spring4D 오픈소소의 개발 책임자입니다. (웹페이지: https://delphisorcery.blogspot.com )
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>
- 특정 타입을 지키는 데이터 타입(Type safe date type) 이면서도 Null값을 가질 수 있다.
- Null (값이 없음)을 넣기 위해 아래와 같이 억지로 만든 값을 사용할 필요가 없다.
- 날짜 타입에 “날짜가 주어지지 않음”을 넣기 위해 -40000 이라는 가상의 값 넣기
- Boolean에 “참인지 거짓인지 모름”을 넣기 위해 True/False/FileNotFound로 Inum을 직접 만들기
- Variant 타입과는 다름
- Variant는 호환 가능한 타입 변환을 암시적으로 수행하므로 언제든 다른 타입으로 변환될 여지가 있지만 Nullable<T>는 타입을 지키므로 정해진 타입 이외에는 담을 수 없다.(type safe)
- 예를 들어 Variant에는 숫자 3과 문자열 ‘3’을 모두 넣을 수 있고, 상황에 따라 사용되지만, Nullable<Integer>에는 정수 3만 넣을 수 있고 문자열 3을 넣을 수는 없다.
//// Nullable<T> 예문
uses
Spring;
procedure …
var
n, n2: Nullable<TDateTime>; // uses Spring (Spring 유닛 사용) 필요
d: TDateTime;
begin
d := now;
Log.Lines.Add(n.HasValue.ToString);
Log.Lines.Add(n.ToString); // DataTime을 문자열로 출력할 때는 지역 설정 등 RTL에서 적용된 형식으로 출력
Log.Lines.AddParagraph;
n := d;
Log.Lines.Add(n.HasValue.ToString); // 로컬 변수로 사용된 Nullable<T>은 할당을 하지 않고 바로 사용 가능
Log.Lines.Add(n.ToString);
Log.Lines.AddParagraph;
n := nil; //n에 들어있는 값을 없앤다
Log.Lines.Add(n.HasValue.ToString);
Log.Lines.Add(n.GetValueOrDefault().ToString); // GetValueOrDefault() 값이 있으면 그 값을, 없으면 기본값을 출력
Log.Lines.AddParagraph;
Log.Lines.Add((n=n2).ToString);
n := d;
Log.Lines.Add((n=n2).ToString);
n2 := d;
Log.Lines.Add((n=n2).ToString);
end;
////결과 (및 해설)
False // 기본값이 적용되면, Nullable.HasValue의 결과는 False
Null // 기본값이 적용되면, Nullable.ToString의 결과는 ‘Null’(값없음)
True // 특정 시간을 넣으면, Nullable.HasValue의 결과는 True
19.11.2020 18:11:51 //독일 기준 날짜 형식 (발표자의 시간대가 독일로 되어 있음)
False // nil을 넣으면, Nullable.HasValue의 결과는 False
30.12.1899 // Nullable.GetValueOrDefault().ToString의 결과는 값없는 경우에 출력할 날짜 기본값
True // n과 n2는 둘다 값이 없으므로, n과 n2를 비교하면 ‘같음’이 된다
False // n에만 d를 넣고, n과 n2를 비교하면 ‘다름’이 된다
True // n과 n2는 둘다 d와 같은 값을 가지므로, n과 n2를 비교하면 ‘같음’이 된다.
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; // operator overloading
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; //Share<T>가 아닌 일반 TStringList
weakRef: Weak<TStrings>; //문자열을 Weak 참조하기로 한다.
begin
sl := TStringList.Create;
weakRef := sl; // operator overloading
sl.Add(‘델파이콘’);
Log.Lines.Add(weakRef.IsAlive.ToString);
Log.Lines.AddStrings(weakRef); // operator overloading
Log.Lines.AddParagraph;
sl.Free; //Share<T> 가 아니므로 직접 Free 했다. 그러면, Weak 참조하고 있는 곳에 모두 공지된다.
// Weak<T>는 모든 플랫폼에서 작동된다.
Log.Lines.Add(weakRef.IsAlive.ToString);
Log.Lines.Add(Assigned(weakRef.Target).ToString); //Weak<T>.Target은 Shared<T>.Value와 같은 역할
Log.Lines.Add(weakRef <> nil).ToString;
end;
////결과 (및 해설)
True // 참조 타겟이 살아있다
델파이콘 // 참조 타겟의 값
False // ‘참조 타겟이 제거되었음’을 알고 있음
False // ‘참조 타겟이 Assigned 되지 않았음’을 알고 있음
False // ‘참조 타겟이 nil 임’을 알고 있음
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개를 만들기 위해 필요한 만큼만 작동한다.
- 즉, 작동이 되고 나서야 컬렉션이 만들어진다.
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은 딕셔너리와 멀티맵의 기반 타입이다
- 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는 비교 기준에 맞추어 알맞은 순서로 저장한다.
- 내부적으로 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
- 원본 비디오 시청: https://delphicon.embarcadero.com/talks/introduction-to-spring4d-taking-delphi-development-to-the-next-level/
- DelphiCon 전체 보기 (현재 무료, 향후 유료 전환 예상): https://delphicon.embarcadero.com/replays/
- 데브기어의 DelphiCon 소개 페이지로 가기: https://devgear.co.kr/archives/3692