Kurumsal Bir Acentelik Uygulamasını Nasıl Modernize Ediyoruz ?

Gürkan Şafak
Bimar Teknoloji Blog
11 min readSep 15, 2023

--

Yaklaşık 20 yıldır çalışan bir uygulama hayal edin, farklı ülkelerde yüzlerce kullanıcısı olan, gün içinde üzerinden binlerce transaction akışı gerçekleşen ve yıllardır kritik işler için kullanılmış, kullanıcılarına zaman şirketine kar sağlamış, yüzlerce developer’ın nazını çekmiş, yeri gelmiş “crash” olmuş ama yıkılmamış tekrar ayağa kalkmış 🙂. Devreye alım saatleri dışında hep ayakta. Seneler geçtikçe metal yorgunluğu başlamış ve gelişen yazılım ekosistemi karşısında o da yerini almak üzere sabırla beklemiş; artık sahne’ye çıkma zamanı için antre sırası ona gelmiş!. Hazırsanız perdeler açılıyor ve bir uygulama modernize oluyor!

“Legacy” bir uygulama nasıl modernize edilir, uygulama modernizasyonu nedir, hangi aşamaları vardır, zorlukları nelerdir, geçişler nasıl planlanır ve yapılır; bu ve benzeri sorular ilginizi çekiyor ise doğru yerdesiniz, başlıyoruz👇

Bu yazıda denizcilik alanında faaliyet gösteren; liman, gümrük, depo ve armatör sistemleri ile sürekli dirsek temas halinde olan kurumsal bir acentelik yazılımının faz 1 modernizasyonu aşamasında şirket olarak neler yaşadığımızın bir özetini yapacağım. Uygulama kullanıcılara hizmet ettiği sırada; yani “motor çalışırken” parça değişimini nasıl yaptığımızdan bahsedeceğim. Benzer proje dönüşümü yapan veya yapacak olan sevgili yazılımcı arkadaşlarımızın kendilerinden bir parça bulmaları ve faydalanmaları dileğiyle.

1. Neden Modernize Ediyoruz?

Monolitik bir yapı deyince aklıma işlenmemiş blok halinde bir mermer kütlesi geliyor. Kaynağından yeni çıkmış bloklar lüks mutfak ve banyolara yer döşemesi olmadan önceki ilk halleri ile tır dorsesinde imalathanelerine gönderiliyor. İşlenecekler ve modern bir görünüme kavuşarak işlevsellik kazanacaklar. Bütün cevher bu devasa bloğun içerisinde ama birilerinin onu işleyerek daha faydalı olacak hale getirmesi gerekiyor.

Legacy uygulamaların modernize edilme süreci de yukarıdaki tasvire benzer bir duygu yaratıyor bende. Devasa boyutta binlerce kod satırı ve onunla bağlantılı onlarca servis ile kendi içinde adeta büyük bir şehir. Bu haliyle gereksinimi karşılıyorlar fakat zamanla büyüyen yapı günün getirdiği gereksinim ve hıza ayak uyduramıyor. Projeler içindeki farklı bölümler birlikte hareket etmek zorunda “deployment treni”ne aynı anda binmek durumundalar, aynı veritabanlarına bakıyorlar ve bağımlılıklar doğru yönetilmediğinde herkes birbirine ayak bağı olmakta.

Şunu ifade etmeliyim ki her şirketin ihtiyaç duyduğu uygulama büyüklüğü birbirinden farklı ve bu uygulamaların gereksinim duyduğu tasarım prensipleri ayrışıyor olabilir. Kimi butik bir monolitik uygulama ile işini yürütebilirken kimi birden fazla modülü içeren ve nerdeyse her biri farklı bir ürün olarak satılabilecek projeleri bir arada senfonik bir uyum içerisinde çalıştırabilme çabasında. Keza dönüşümüne başladığımız uygulamanın da zamanın “hype” teknolojileri ile harmanlanmış olduğunu bir gerçeklik olarak kenara not edebiliriz.

Bu bölümün başlığına cevap olarak; daha modern arayüze sahip bir yazılım, daha modüler ve birbirinden bağımsız parçalar ile yönetilebilen bir yapı ve gelişen yazılım ekosisteminin getirdiği yeniliklere adapte olabilmek adına modernize ediyoruz diyebilirim.

2. Mevcut Uygulama Mimarimiz ve Hedefimiz

Aşağıda uygulamanın dış bileşenlerinden arındırılmış basit bir şemasını görüyorsunuz. 1 numarada aspx sayfalarının olduğu frontend katmanı, 2 numaralı bölümde ise business projeleri yer alıyor. Her iki katman da geçiş öncesi .net framework 4.8 ile çalışıyordu.

Bu yazıya konu olan çalışmanın asıl hedefi frontend katmanını backend’ten bağımsız hale getirebilmek için öncelikle backend projelerinin farklı platformlarda çalışabilecek şekilde yeniden modernize edilmesiydi. Çünkü bu çalışmadan sonraki amacımız mevcuttaki frontend projelerini bölüm bölüm react altyapısına geçirmek. Dolayısıyla projenin backend kodlarını bir süreliğine (tüm dönüşüm süreci tamamlanana kadar) hem aspx’ten gelen isteklere hem de react sayfalarından gelen isteklere yanıt verecek şekilde hazır etmemiz gerekiyordu.

Aşağıdaki çizim gitmek istediğimiz noktayı biraz daha net belirtiyor. Şuana kadar yaptığımız geliştirim bir REST API projesinin yaratılması ve backend projelerinin net standard altyapısı ile yeniden oluşturulması kısmını kapsıyor. Çizimde gösterilen gumruk.js isimli proje henüz ayrı bir şekilde react ile yazılmadı. İlerleyen aşamalarda tüm modernizasyon süreci tamamlandığında buna benzer bir yapı ouşturmuş olacağız. Şuan için business projelerin net standard dönüşümü tamamlandı ve rest api üzerinden bu katmana erişim başarılı bir şekilde sağlanıyor.

2.1. Rest API

Backend ve frontend’i birbirinden izole etmek için ilk yaptığımız şey bir API oluşturarak iki yapının birbirleri ile haberleşmesini bu katman üzerine aktarmak oldu. .NET 6.0 ile ayarlanmış bir API yaratarak yola koyulduk. Amacımız bir bütün olarak duran backend’teki projelerin runtime’de farklı davranış sergileyerek .net core ve .net framework 4.8 ortamlarında sorunsuz çalışabilmesiydi.

Aynı backend kodlarının farklı ortamlara hizmet edebilmesini net standard ve multitargeting framework altyapısını kullanarak sağladık. Net standard, farklı platformlara hizmet etmesi gereken projelerin bu farklılığı karşılayacak şekilde yeniden dizayn edilmesine imkan veren bir framework. Microsoft bu altyapı ile bizim gibi geçiş projeleriyle uğraşan ekiplerin işini oldukça kolaylaştırmış. Net standard’la ilgili daha detaylı bilgi için şuradaki yazıyı inceleyebilirsiniz.

3. Karşılaşılan Zorluklar Ve Çözümleri

Photo by Richard Lee on Unsplash

3.1. ORM Katmanındaki Değişiklikler

Orm Erişimi İçin Kullanılan Projenin Dönüşümü:
API’yi oluşturduk ve ilk kontrol etmek istediğimiz nokta veritabanı erişimlerini sorunsuz yapıp yapamayacağımızdı. ORM aracı olarak kullandığımız LLBLGen kendi içinde uygulama katmanının ihtiyaç duyduğu tüm veritabanı işlemlerini soyutlayarak daha kolay ve düzenli kod yazmamızı sağlıyor (entity framework benzeri bir uygulama). Fakat bu yazılımın doğrudan .net core versiyonlu bir uygulama ile çalışabilmesi için bazı değişiklikler ihtiyaç vardı. LLBLGen üzerinden generate edilerek oluşturulan ve projede kullanılan dll’ler net standard altyapısı ile uyumlu olmalıydı. Bu yüzden orm katmanında kullandığımız projeleri net standard altyapısı ile yeniledik.

HttpContext Kullanımındaki Farklılaşma;
Hali hazırdaki uygulamamızda boş bir log nesnesi ORM katmanına HttpContext içerisinde gönderilerek o katmanda yapılan değişikliklerin tüm izleri bu nesne üzerinde saklanıyor. ORM katmanında yapılan işler tamamlandığında LOG nesnesinin tüm alanları doldurulmuş bir şekilde log atılabilecek düzeye gelmiş oluyor. Bu noktada oluşturduğumuz .NET 6.0 versiyonlu API’nin doğrudan HttpContext sınıf desteği yoktu. HttpContext sınıfını ORM katmanı içinde dependency injection pattern’i ile kullanılabilir hale getirmemiz gerekiyordu. Fakat önümüzde bir engel daha vardı; kullandığımız LLBLGen 5.5 sürümü DI özelliğini desteklemiyordu, bu desteğin versiyon 5.9 ile geldiğini öğrendikten sonra bütün enerjimiz ORM katmanının versiyon upgrade’ine yönelmişti. ORM’deki bir versiyon upgrade demek tüm uygulamanın veritabanı etkileşimi olan noktalarının testi ve versiyonla birlikte gelen breaking change’lerin yönetilmesi demekti. Bu süreç geliştirim ve testleriyle birlikte yaklaşık 1,5 ayımızı aldı. Süreç sonunda HttpContext sınıfını .net core ve .net framework 4.8 ile birlikte çalışabilir duruma getirmiştik.

3.2. Multitargeting Library Desteği

Photo by Remy Gieling on Unsplash

İlk aşamaları geçtikten sonra işin kabasını almıştık. Artık ince işçilik gereken yerlere girip tek tek değişmesi gereken noktalara müdahale etmemiz gerekiyordu. Business katmanımızdaki tüm .net 4.8 projelerini net standard olarak upgrade etmiştik fakat net standard olarak destek alamadığımız bazı yapılarımız mevcuttu. Mesela in-memory cache yapısını 4.8 projelerinde System.Web.Caching paketi üzerinden yönetiyorduk ve bu paketin net standard içerisinde 4.8 ve .net core’u birlikte destekleyen bir çözümü bulunmuyordu. Bununla birlikte resource’ları yönettiğimiz resource provider projesinde kullandığımız dll’ler, hata loglarını koordine etmek için ihtiyacımız olan elmah paketi doğrudan net standard framework’ü içinde her iki yapıya destek sağlayamıyordu ve bu noktalar için ek geliştirimlere ihtiyaç vardı.

Bu tür doğrudan net standard silahını kullanamadığımız yerlerde Microsoft’un multitargeting proje yapısı ile devam etmeye karar verdik. Bu yapı ile birlikte tek bir proje oluşturarak, bu proje içinde ön işlemci directive’leri ile beraber runtime’da çalışacak kodları düzenleyebiliyoruz. Uygulama bu yapı ile birlikte isteğin geldiği runtime’a göre .net framework 4.8'e ait dll’i mi kullanacak yoksa .net core’a ait dll’le mi devam edilecek karar verebiliyor. Bu bizim için büyük bir esneklikti. Bu şekilde .net core için bazı projeleri bu yapıyı kullanarak yeniden yazdık.

Aşağıda caching projesinde kulandığımız ön işlemci directive’leriyle ilgili bir kod parçasını görüyorsunuz. Using tanımlarını runtime’da kullanılacak namespacelere göre sınıfın en üstünde tanımlanıyoruz. Ortam 6.0 ise şu dll’i 4.8 ise bunu kullan diyoruz.

#if NET6_0
using Microsoft.Extensions.Caching.Memory;
using StaticMemoryCacheAccessor.Helpers;
using System;
#endif

#if NET48
using Rota.CachingCore.Base;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Web;
using System.Web.Caching;
using System.Web.UI;
#endif

Sonrasında ilgili paketler kod içinde if case’leri ile yönetilerek hangi ortamda hangi kodun çalışacağı belirleniyor.

        private void PrepareCache()
{
DateTime timeToExpire = DateTime.Now.AddSeconds(SecondsToExpire);
#if NET48
CacheMain.AddCacheValue(KeyName, FetchData, timeToExpire);
#elif NET6_0
MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions()
{
AbsoluteExpiration = timeToExpire,
Priority = CacheItemPriority.Normal
};
AppMemoryCache.Current.Set(KeyName, FetchData, cacheEntryOptions);
#endif
}

Error loglarını yönettiğimiz proje altındaki ilgili multitarget projesinin dependencies altındaki görünümü ise şu şekilde oluyor;

Multitarget Projenin Dependencies Görünümü

Gördüğünüz gibi net48 ve net60 olarak yapılandırılmış 2 ayrı bölüm var ve bunların kullandıkları elmah paketleri ayrı frameworkler ile projede kullanılabilir durumda. Bu sayede net standard ile proje yapısını değiştirerek çözemediğimiz yerlerde terzi işi bir işçilik ile gerekli noktaları yönetmiş ilgili projeleri değiştirmiş olduk.

3.3. Backend Tarafındaki Bağımlılıkların Temizliği

Bir takım nedenlerle bazı kodlar ve projeler onlara gerçekten ihtiyaç duyduğunuz alanın dışında bir yerde duruyorlar ise bu tür bir modernizasyon işi ile uğraştığınızda bunları da göz önünde bulundurmak ve refactoring işlemini yapmak zorunda kalacaksınız. Aksi durumda projeleriniz her iki ortamı da destekleyecek şekilde çalışmayacaktır.

Belli başlı büyük sorunları aştıktan sonra geriye bu tür temizlik işlerinin yapılması kalmıştı. Başlıca değişim gerçekleştirdiğimiz noktaları aşağıdaki gibi sıralamaya çalıştım.

Photo by Gary Chan on Unsplash

3.3.1. Herşeyi Olması Gerektiği Yere Koyuyoruz (Collection sınıf refactorü)
Sadece frontend’te kullanılan bazı sınıfların backend kodlarından da çağrıldığını fakettik. Böyle bir kullanım şu açıdan sıkıntı yaratıyordu; aynı backend koduna erişim sağlayan .net core api’si bu projedeki herhangi bir yere erişmeye çalıştığında ilgili paket .net core için kullanılamadığından api çağrımların fail olmasına neden oluyordu. Dolayısıyla webform yapısında aslında sadece aspx sayfalarında kullanılmasi beklenen kodlar backend’te de olduğundan bunların düzeltilmesi gerekiyordu. .Net core için bu yapılar kullanılmayacağından ve sadece frontend’i ilgilendirdiğinden bu sınıfları ayrı bir projede toplayarak sadece kullanıldığı yere hizmet eder duruma getirdik ve backend katmanından çıkararak sadece frontend’ten çağrılacak şekilde değiştirdik. Frontend’teki drowdown’lar için kullanılan ListItem nesnelerini barındıran collection sınıfları ayrı bir projeye alınarak sadece ön yüze referans olarak verildi ve backend katmanından çıkarıldı.

3.3.2. Servis Client’larının WCF Olarak Yeniden Oluşturulması
Kendi uygulamamızdan farklı uygulamaların SOAP servislerini çağırdığımız client sınıflarında System.Web.Services paketi kullanılıyordu. Fakat .NET 6.0 ile birlikte bu kullanımda sorun yaşadığımızı ve çağrım yapamadığımızı farkettik. Farklı uygulamaların service client sınıflarını ayrı bir NET standard projesi altında topladık. Hatta bu aşamada projede client sınıfı bulunan ama aslında atıl durumda kaldığını farkettiğimiz bazı servisleri uygulama kodlarından sildik. Backend bağımlılıklarının gözden geçirilmesinin bu açıdan da bize faydaları oldu. Kod içinde kalan ama kullanılmayan kısımlar temizlenerek kafa karışıklığı yaratma riskinin önüne geçmiş olduk. Daha sonra varolan ve aktif kullanılacağını tespit ettiğimiz tüm servisler WCF servis olarak yeniden generate ettik. Bu yöntem ile yeni oluşan servis sınıflarını hem 4.8 hem de 6.0 ortamında sorunsuz kullanabildik.

3.3.4. DataTable Dönüşümü
Veritabanından gelen nesnelerin datatable olarak dönüştürülmesi işini yapan ve frontend tarafına bu şekilde gönderilmesini sağlayan System.Data paketi için de proje genelinde bir değişikliğe gittik. Eskiden kullandığımız System.Data paketi üzerindeki asDataView metodu ile datatable dönüşümlerini yaparken bu desteğin artık sağlanmadığını farketmemizle birlikte farklı bir linq metodu olan copyToDataTable ile devam etmemiz gerekti. Tüm proje kapsamında bunun da dönüşümünü gerçekleştirdik.

Tüm bu adımların tamamlanmasının ardından birim testlerimiz ve UI testlerimizin sorunsuz çalıştığından emin olduk. Ayrıca 3 hafta sürecek olan ve tüm test ekibimizin katıldığı genel bir manuel uygulama test süreci geçirdik. Otomatize edilmiş testler ve manuel testlerin kontrolü sonrasında çıkan problemler de giderildikten sonra artık varolan yapıyı canlı sistemde devreye alma zamanı gelmişti.

Bir sonraki adımda yaklaşık 1 ay süren canlı geçiş sürecindeki tecrübelerimizi okuyacaksınız.

4. Hasat Zamanı: Release Sürecimiz

Photo by Maja Petric on Unsplash

Uzun bir development sürecinin ardından artık meyveleri toplama zamanımız gelmişti. Yaptığımız tüm değişklikleri canlı sistem üzerine çıkabilecek durumdaydık. Bu aşamada odaklandığımız şey hali hazırda işlerini yürütmekte olan kullanıcılarımızı olabildiğince az rahatsız etmek ve mümkünse kesintileri onlara hissettirmeden pürüzsüz bir geçiş yapmaktı. Ayrıca herhangi bir olumsuz durumda işler yolunda gitmediğinde geri dönüş planlarımızın kenarda hazır olması gerekliydi.

4.1. Devreye alım stratejimiz;

Üzerinde çalıştığımız uygulama Türkiye dahil yaklaşık 12 ülkede kullanılmakta ve farklı saat dilimlerinde kullanıcılar sisteme bağlanmakta. Dolayısıyla bu farklılıkları da gözeterek devreye alım planlarını bu doğrultuda yaptık ve devreye alımları tüm ülkelerde aynı anda yapmak yerine adım adım giderek yükün daha az olduğu ülkeler ile başlayarak canlı geçişlerini planladık. Buradaki yaklaşımımız aslında canary release olarak adlandırılan daha küçük kullanıcı kitlesini hedef alan bir stratejiydi. Ve bu şekilde ilerlememizin faydasını fazlasıyla gördük. Adım adım devreye alınan ülkelerde çıkan sorunları hemen çözerek sonraki ülkelerde bu açıkların kapatılmasını sağlıyor bir sonraki ülke devreye alımında daha stabil bir versiyonla kullanıcı karşısına çıkıyorduk.

Azure Devops Üzerinde CI/CD Pipeline’larımız

4.2. DevOps Tarafında Yaşadıklarımız;

Bu operasyonları yürütürken elimizin altında güçlü bir CI/CD birikimi vardı ve bunun avantajını geçiş boyunca yaşadık. DevOps tarafı ile güçlü bir işbirlikteliği bu süreçte hızlı aksiyon alarak ilerleyebilmemizi sağladı. Canlı geçiş süreci boyunca her an aşılamayacak bir sorunla karşı karşıya kalma riskinden dolayı eski kodları içeren branch yedeklenmişti. Ve belli rutinlerle devam eden geliştirimler ile gelen yeni kodları içeren branch’teki değişiklikler eskisine de yansıtılıyordu. Yeni branch için deployment paketlerini hazır eden ve release aşamasında kullanılan pipeline’lar Azure Devops üzerinde yeni ülke geçişleri ile birlikte sürekli güncellenerek eklemeler yapılıyordu.

Bir yandan ülke configleri her geçiş öncesi stabil versiyonları ile hazırlanarak akşam yapılacak devreye alımlar için hazır ediliyordu. Akşam kullanıcıların sistemde olmadığı saatlerdeki devreye alımlardan sonra kısıtlı bir developer grubu ile manuel “smoke test” olarak da adlandırabileceğimiz, ertesi gün sabahın ilk ışıklarında kullanıcıların beklenmedik bir durumla karşılaşmamaları için genel kontroller sağlanıyordu.

Bu aşamada proje altyapımızdaki birim ve UI testlerin continuous integration kapsamında sürekli çalışarak sistem ile ilgili bir alarm varsa bize iletmesi önümüze ışık tuttu. Bu testler sayesinde bir çok hatayı henüz canlıya çıkmadan yakalama fırsatına sahip olduk.

Validation Pipeline ile Çalışan Unit Tetlerimiz
Her Gün Belli Bir Saatte Çalışan UI Testlerimiz (Özet Rapor)

Bu süreç boyunca belli başlı kod kaynaklı sorunlar yaşadık ve bunları düzelterek tekrar yolumuza devam ettik. Fakat burada bahsedebileceğim önemli bir sorun var ki bu geçişin beklenenden daha uzun sürmesine sebep olarak burada ayrı bir başlık olarak “değer” görmeyi haketti 👇

4.3. Adım Adım Devreye Alımın Faydası: Erken Yakalanan Memory Leak Problemi;

Devreye aldığımız ülkelerden biri sonrasında öğlene doğru destek ekibinden arkadaşlarımız teams kanallarına kullanıcılardan yavaşlık problemleri geldiğine dair bilgilendirme geçiyordu. DevOps adminimiz Cem sunucuları kontrol etmiş ve yeni devreye aldığımız uygulamanın ortalama kullanması gereken memory miktarından 2 kat daha fazla tüketim yaptığını tespit etmişti. Bunun üzerine dynatrace izlemesi başlatıp bir yandan da bu sorunun kaynağının ne olduğuna dair sürekli fikir alışverişinde bulunuyorduk. Öncelikle garbage collector’ın bir sorunu olabileceğini düşünüp config üzerinde birkaç gc düzenlemesi yaptık fakat sonuç değişmedi. Cache projesinde yaptığımız değişikliklerin yan etkisi olabileceğini düşünerek ordaki kodları tekrar gözden geçirdik. Dynatrace üzerinde uzun sürdüğünü gördüğümüz senaryoları localde visual studio üzerinden tekrar ederek diagnostic tools üzerinden incelemeler yaptık fakat sunucudaki durumu tekrar edemedik.

Sunucu üzerinde sorunun yaşandığı zamana ait dump dosyasını alarak bunun incelenmesi için Microsoft ile iletişime geçtik, bir yandan da şirket içinde dump analiz yapabilen kişiler eş zamanlı olarak dump içinde işimize yarayabilecek bir ipucunun olup olmadığını incediler. Analiz sonucunda oklar oracle’ı gösteriyordu. Oracle nesnelerinin memory üzerinde ciddi bir etki yarattığını farkettik. Bu durumun nedeni ise net standard geçişimizle birlikte Oracle.ManagedDataAccess versiyonunda bir upgrade yapmamızdı. Bu bilgi doğrultusunda kısa bir araştırma ile aynı versiyonda başkalarının da memory sorunu yaşadığı ve oracle’ın bu durumu son versiyon ile çözümlediği belirtiliyordu.

Memory issues after upgrading to 3.21.90 · Issue #281 · oracle/dotnet-db-samples · GitHub

Çözüm;
Biz son versiyona geçerek denediğimizde de benzer bir memory sorunu yaşamaya devam ettik. Bu noktada problemi Oracle support’a bildirdik ve onlardan dönen cevapta connection string attribute’lerinde değişiklik yapmamız istendi. Connection string üzerinde yaptığımız 3 değişiklik ile memory sorunumuz çözüldü. Bu değişiklikler; min pool size ve connection lifetime bilgilerinin eklenmesi ve connection timeout değerinin artırılmasıydı.

Yoğun bir geçiş maratonu ile artık tüm ülkelerde isteğimiz backend versiyonu ile canlıda çalışır duruma gelmiştik ve tüm ekip bu durumdan çok mutluydu 🙂

5. Yeni Bir Başlangıç: Faz 2

Photo by Aamir Suhail on Unsplash

Yorucu ama çok keyif aldığımız faz 1 modernizasyon sürecimiz bu şekilde tamamladık. Artık backend’imiz eskisinden daha derli toplu ve en önemlisi hem eski hem de yeni yazacağımız frontend projesine hizmet edebilir durumdaydı. Bundan sonraki hedefimiz web form ile tasarlanan arayüz projelerimizin modernizasyonu olacak.

Umarım benzer dönüşüm süreçleri yaşayan ekipler için faydalı bir yazı olmuştur. Buraya kadar okuduğunuz için teşekkürler. 🙂

--

--