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을 넣을 수는 없다.
-
Variant는 호환 가능한 타입 변환을 암시적으로 수행하므로 언제든 다른 타입으로 변환될 여지가 있다(type unsafe).
// [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는 비교 기준에 맞추어 알맞은 순서로 저장한다.
-
내부적으로 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
Recommended Comments
There are no comments to display.