Kori 11월 16일, 2020에 포스트됨 공유하기 11월 16일, 2020에 포스트됨 << DelphiCon 2020 목록으로 이동 DelphiCon 의 2020 시리즈 중, Introduction to Spring4D - Taking Delphi Development to the Next Level - Stefan Glienke 의 한글 요약본입니다. 오픈 소스인 스프링4D를 사용하면, 코드는 더 짧아지고, 일관성과 품질은 더 높아집니다. 예를 들어, 스마트 포인터를 사용하면 오브젝트를 만들어 쓸 때 메모리 누수를 걱정할 필요가 없습니다. Spring4D의 컬렉션은 컬렉션이나 리스트 관리가 매우 간편합니다. 발표자 (Stefan Glienke)는 델파이 경력 20년이 넘은 개발자이며, Spring4D 오픈소소의 개발 책임자입니다. 원본 비디오에는 아래 요약보다 자세한 데모와 설명이 있습니다. 목차 Spring4D 란 Spring4D를 받는 방법 Spring4D의 기본 구성 Spring4D 구성 요소: Spring.Base Nullable Event 스마트 포인터 (Shared 와 Weak) Collections: 가장 많이 활용되는 라이브러리 Collections 인터페이스 읽기전용 vs 불변 IEnumberable ICollection IList IMap, IDictionary, IMultiMap ISet, IMultiSet 더 많은 내용 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 엠바카데로 기술자료 인용하기 이 댓글 링크 다른 사이트에 공유하기 더 많은 공유 선택 사항
Recommended Posts
이 토의에 참여하세요
지금 바로 의견을 남길 수 있습니다. 그리고 나서 가입해도 됩니다. 이미 회원이라면, 지금 로그인하고 본인 계정으로 의견을 남기세요.