Feature Flags ile Kontrollü Release
Giriş — Işık Anahtarı
Evindeki ışık anahtarını düşün. Bir elektrik tesisatçısı gelip odana yeni bir avize taktı. Ama hemen açmak istemiyorsun — belki gece lambayla nasıl göründüğünü görmek istiyorsun, belki sadece misafir geldiğinde açacaksın. Anahtar orada, avize tavanda, ama sen ne zaman açacağına kendin karar veriyorsun. Beğenmezsen geri kapatırsın, kimse fark etmez.
Feature flags (özellik bayrakları) tam olarak bu ışık anahtarı. Yeni bir özelliği koda deploy edersin ama kullanıcıya göstermezsin. İstediğin zaman, istediğin kullanıcı grubuna, istediğin oranda açarsın. Sorun çıkarsa geri kapatırsın — yeni deploy gerekmez, kod değişikliği gerekmez.
Bu neden önemli? Çünkü geleneksel yaklaşımda "deploy = release" demek. Kodu production'a attığın an herkes yeni özelliği görüyor. Bir bug varsa geri almak (rollback) gerekiyor — ki bu stresli, riskli ve zaman alıcı bir süreç. Feature flags ile deploy ve release'i birbirinden ayırırsın. Kod production'da ama özellik kapalı. Her şey yolundaysa açarsın. Bu paradigma değişikliği modern yazılım geliştirmenin temel taşlarından biridir.
Proje Kurulumu
Feature flags için Togglz kütüphanesini kullanacağız. Java/Spring ekosisteminin en olgun feature flag kütüphanesidir.
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Togglz — Spring Boot Starter -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
<!-- Togglz — Web Console (yönetim arayüzü) -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-console</artifactId>
<version>4.4.0</version>
</dependency>
<!-- Togglz — Spring Security entegrasyonu (opsiyonel) -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-spring-security</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies># application.yml
togglz:
enabled: true
feature-enums: com.example.flags.AppFeatures # Feature enum class
console:
enabled: true
path: /togglz-console # Yönetim paneli URL'i
secured: false # Geliştirme ortamında güvenlik kapalı
# Production'da secured: true yapıp sadece admin'lere aç!Feature Tanımlama — Enum Yaklaşımı
Togglz'da feature'lar bir enum ile tanımlanır. Her enum sabiti bir feature flag'dir.
public enum AppFeatures implements Feature {
@Label("Yeni Dashboard")
@EnabledByDefault // Varsayılan olarak açık — dikkatli kullan
NEW_DASHBOARD,
@Label("Gelişmiş Arama")
ADVANCED_SEARCH,
@Label("Dark Mode")
DARK_MODE,
@Label("Yeni Ödeme Sistemi")
NEW_PAYMENT_SYSTEM,
@Label("AI Tavsiyeleri")
@DefaultActivationStrategy(id = GradualRolloutActivationStrategy.ID,
parameters = {
@ActivationParameter(name = GradualRolloutActivationStrategy.PARAM_PERCENTAGE,
value = "20")
})
AI_RECOMMENDATIONS,
@Label("Beta Özellikler")
BETA_FEATURES;
// Togglz'ın gerektirdiği yardımcı metod
public boolean isActive() {
return FeatureContext.getFeatureManager().isActive(this);
}
}@Label yönetim panelinde görünen açıklamadır. @EnabledByDefault uygulama ilk başladığında flag'in açık olmasını sağlar. @DefaultActivationStrategy ile varsayılan aktivasyon stratejisini belirleyebilirsin.
Activation Stratejileri — Kim Görecek?
Feature flag'in açık olması yetmez — kime ve ne zaman açık olacağını da belirlemen gerekir. Togglz bunun için çeşitli activation stratejileri sunar.
1. Herkese Açık / Kapalı
En basit strateji. Flag ya herkese açık ya da herkese kapalı.
// Togglz konsolu üzerinden veya programatik olarak
featureManager.enable(AppFeatures.NEW_DASHBOARD); // Herkese aç
featureManager.disable(AppFeatures.NEW_DASHBOARD); // Herkese kapat2. Kademeli Yayılım (Gradual Rollout)
Kullanıcıların belirli bir yüzdesine açmak için. Önce %5'e aç, sorun yoksa %20, %50, %100 şeklinde kademeli olarak yayılırsın.
@Label("AI Tavsiyeleri")
@DefaultActivationStrategy(id = GradualRolloutActivationStrategy.ID,
parameters = {
@ActivationParameter(
name = GradualRolloutActivationStrategy.PARAM_PERCENTAGE,
value = "25") // Kullanıcıların %25'ine açık
})
AI_RECOMMENDATIONS;Gradual rollout kullanıcı ID'sinin hash'ine göre çalışır. Aynı kullanıcı her zaman aynı sonucu alır (consistent hashing). Yani bir kullanıcı özelliği gördüyse, yüzde değişmediği sürece görmeye devam eder.
3. Kullanıcı Listesi
Belirli kullanıcılara açmak için. Beta tester grubu, şirket içi çalışanlar gibi.
@DefaultActivationStrategy(id = UsernameActivationStrategy.ID,
parameters = {
@ActivationParameter(
name = UsernameActivationStrategy.PARAM_USERS,
value = "admin, tester1, tester2, product-manager")
})
BETA_FEATURES;4. Server IP / Hostname
Canary deployment senaryolarında belirli sunucularda aktif etmek için.
5. Tarih Bazlı
Belirli bir tarihten sonra otomatik aktif etmek için. "Black Friday özelliği 29 Kasım'da açılsın" gibi.
6. Custom Strateji
Kendi iş kurallarına göre özel strateji yazabilirsin:
@Component
public class PremiumUserStrategy implements ActivationStrategy {
@Override
public String getId() {
return "premium-users";
}
@Override
public String getName() {
return "Premium Kullanıcılar";
}
@Override
public boolean isActive(FeatureState featureState, FeatureUser user) {
if (user == null) return false;
// Kullanıcının attribute'larına bak
String plan = user.getAttribute("plan");
return "premium".equals(plan) || "enterprise".equals(plan);
}
@Override
public Parameter[] getParameters() {
return new Parameter[0];
}
}Custom strateji ile istediğin kadar karmaşık koşullar tanımlayabilirsin: coğrafi bölge, kullanıcı planı, cihaz tipi, A/B test grubu...
Spring Boot Entegrasyonu
Service Katmanında Kullanım
@Service
@RequiredArgsConstructor
public class SearchService {
private final BasicSearchEngine basicSearch;
private final AdvancedSearchEngine advancedSearch;
public SearchResult search(String query) {
if (AppFeatures.ADVANCED_SEARCH.isActive()) {
// Yeni gelişmiş arama motoru
return advancedSearch.search(query);
}
// Eski basit arama
return basicSearch.search(query);
}
}Bu kadar basit. isActive() çağrısı o anki kullanıcı ve aktivasyon stratejisine göre true veya false döner.
Controller Katmanında Kullanım
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class DashboardController {
private final DashboardService dashboardService;
private final LegacyDashboardService legacyDashboardService;
@GetMapping("/dashboard")
public ResponseEntity<?> getDashboard() {
if (AppFeatures.NEW_DASHBOARD.isActive()) {
return ResponseEntity.ok(dashboardService.getNewDashboard());
}
return ResponseEntity.ok(legacyDashboardService.getClassicDashboard());
}
@GetMapping("/features")
public Map<String, Boolean> getFeatureStates() {
// Frontend'in hangi özelliklerin aktif olduğunu bilmesi için
return Map.of(
"newDashboard", AppFeatures.NEW_DASHBOARD.isActive(),
"advancedSearch", AppFeatures.ADVANCED_SEARCH.isActive(),
"darkMode", AppFeatures.DARK_MODE.isActive(),
"aiRecommendations", AppFeatures.AI_RECOMMENDATIONS.isActive()
);
}
}/features endpoint'i frontend uygulamaların hangi özelliklerin aktif olduğunu sorgulamasını sağlar. Böylece backend ve frontend aynı flag'leri kullanır.
FeatureUser Yapılandırması
Togglz'ın kullanıcı bazlı stratejiler (gradual rollout, username list) çalıştırabilmesi için mevcut kullanıcıyı tanıması gerekir:
@Component
public class CustomUserProvider implements FeatureUserProvider {
@Override
public FeatureUser getCurrentFeatureUser() {
// Spring Security'den mevcut kullanıcıyı al
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()
|| auth instanceof AnonymousAuthenticationToken) {
return new SimpleFeatureUser("anonymous", false);
}
String username = auth.getName();
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
SimpleFeatureUser user = new SimpleFeatureUser(username, isAdmin);
// Ek attribute'lar — custom stratejilerde kullanılır
user.setAttribute("plan", getUserPlan(username));
user.setAttribute("region", getUserRegion(username));
return user;
}
private String getUserPlan(String username) {
// Veritabanından kullanıcının planını getir
return "premium"; // Basitlik için sabit değer
}
private String getUserRegion(String username) {
return "TR";
}
}Feature State Repository — Durumu Nerede Saklayacağız?
Varsayılan olarak Togglz feature state'leri bellekte tutar — uygulama restart olunca sıfırlanır. Production'da kalıcı bir storage gerekir:
JDBC — Veritabanında Sakla:
@Configuration
public class TogglzConfig {
@Bean
public StateRepository stateRepository(DataSource dataSource) {
return new JDBCStateRepository(dataSource);
// TOGGLZ_FEATURES tablosunu otomatik oluşturur
}
}File — Dosyada Sakla:
@Bean
public StateRepository stateRepository() {
return new FileBasedStateRepository(new File("/config/features.properties"));
}features.properties dosyası şöyle görünür:
NEW_DASHBOARD=true
ADVANCED_SEARCH=false
DARK_MODE=true
NEW_PAYMENT_SYSTEM=false
AI_RECOMMENDATIONS=true
AI_RECOMMENDATIONS.strategy=gradual
AI_RECOMMENDATIONS.param.percentage=50JDBC repository production için en iyi seçimdir. Birden fazla uygulama instance'ı aynı veritabanını kullanarak feature state'leri paylaşabilir. Dosya tabanlı depolama ise tek instance'lı uygulamalarda veya geliştirme ortamında pratiktir.
Togglz Web Console — Yönetim Paneli
Togglz'ın en güçlü yanlarından biri built-in yönetim konsolu. /togglz-console adresinden feature'ları görsel olarak yönetebilirsin:
# application.yml
togglz:
console:
enabled: true
path: /togglz-console
secured: true # Production'da true olmalı!Konsoldan yapabileceklerin:
Feature'ları açıp kapatmak
Activation stratejisini değiştirmek
Gradual rollout yüzdesini ayarlamak
Kullanıcı listesini güncellemek
⚠️ Dikkat: Production ortamında togglz.console.secured: true olmalı ve sadece admin rolüne sahip kullanıcılar erişebilmeli. Aksi halde herkes feature'ları açıp kapatabilir — bu ciddi bir güvenlik açığıdır.
Güvenlik yapılandırması:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/togglz-console/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.build();
}
}Feature Flag Kullanım Kalıpları
Kalıp 1: Kill Switch
Bir servise veya özelliğe acil olarak "off" çekebilmek. Örneğin dış bir API sağlayıcısı çöktüğünde o entegrasyonu devre dışı bırakmak:
@Service
public class NotificationService {
public void sendNotification(User user, String message) {
// Email her zaman gider
emailService.send(user.getEmail(), message);
// SMS sadece flag açıksa gider — sorun olursa anında kapat
if (AppFeatures.SMS_NOTIFICATIONS.isActive()) {
try {
smsService.send(user.getPhone(), message);
} catch (SmsProviderException e) {
log.error("SMS gönderilemedi, kill switch devreye alınabilir", e);
}
}
// Push notification — yeni özellik, kademeli açılıyor
if (AppFeatures.PUSH_NOTIFICATIONS.isActive()) {
pushService.send(user.getDeviceToken(), message);
}
}
}Kalıp 2: A/B Testing
İki farklı implementasyonu karşılaştırmak. Kullanıcıların yarısı eski checkout akışını, yarısı yeni checkout akışını görür:
@Service
public class CheckoutService {
public CheckoutResult processCheckout(Cart cart) {
if (AppFeatures.NEW_CHECKOUT_FLOW.isActive()) {
// Yeni akış — basitleştirilmiş, tek sayfa
CheckoutResult result = newCheckoutEngine.process(cart);
metricsService.record("checkout_v2", result.getDuration());
return result;
}
// Eski akış — çok adımlı
CheckoutResult result = legacyCheckoutEngine.process(cart);
metricsService.record("checkout_v1", result.getDuration());
return result;
}
}Metrik toplama ile hangi versiyonun daha iyi performans gösterdiğini ölçebilirsin.
Kalıp 3: Trunk-Based Development
Feature branch'ler yerine tüm geliştiriciler main branch'e commit eder. Tamamlanmamış özellikler flag arkasına gizlenir:
@Service
public class ReportService {
public Report generateReport(ReportRequest request) {
Report report = createBaseReport(request);
// Bu özellik henüz geliştirme aşamasında — production'da kapalı
if (AppFeatures.REPORT_EXPORT_PDF.isActive()) {
report.setPdfExportEnabled(true);
report.setPdfUrl(pdfGenerator.generate(report));
}
// Bu da henüz bitmedi — sadece geliştirici ortamında açık
if (AppFeatures.REPORT_AI_SUMMARY.isActive()) {
String summary = aiService.summarize(report.getData());
report.setAiSummary(summary);
}
return report;
}
}Feature Flag Lifecycle — Yaşam Döngüsü
Her feature flag geçici bir yapıdır — sonsuza dek kodda kalmamalı. Bir flag'in yaşam döngüsü:
1. OLUŞTUR → Yeni özellik geliştiriliyor, flag ile gizle
2. TEST ET → QA ortamında flag açarak test et
3. YAYGINLAŞTIR → Kademeli rollout (%5 → %20 → %50 → %100)
4. KARARLASTA → %100'e ulaştı, sorun yok
5. TEMİZLE → Flag'i kaldır, kodu sadeleştir ← BU ADIM KRİTİKTemizleme — Teknik Borç Önleme
Flag %100'e ulaşıp özellik kararlı hale geldikten sonra:
// ÖNCE — flag ile koşullu kod
public SearchResult search(String query) {
if (AppFeatures.ADVANCED_SEARCH.isActive()) {
return advancedSearch.search(query);
}
return basicSearch.search(query);
}
// SONRA — flag kaldırıldı, eski kod silindi
public SearchResult search(String query) {
return advancedSearch.search(query);
}Flag kaldırılınca:
Enum'dan sabiti sil
Tüm
if (flag.isActive())koşullarını kaldırEski implementasyonu sil
State repository'den flag kaydını temizle
💡 İpucu: Her flag'e bir "expiry date" (son kullanma tarihi) tanımla. Sprint planlamasında "flag cleanup" ticket'ları oluştur. 3 aydan eski flag = teknik borç.
Anti-Pattern'ler — Yapılmaması Gerekenler
1. Flag Cehennemine Düşmek
// ❌ YANLIŞ — İç içe flag kontrolü, okunmaz ve test edilmez
public void processOrder(Order order) {
if (AppFeatures.NEW_PRICING.isActive()) {
if (AppFeatures.DISCOUNT_ENGINE_V2.isActive()) {
if (AppFeatures.TAX_CALCULATION_V3.isActive()) {
// Bu kod kaç farklı kombinasyonda test edilecek?
// 2 × 2 × 2 = 8 farklı senaryo!
}
}
}
}// ✅ DOĞRU — Her flag bağımsız, tek sorumluluk
public void processOrder(Order order) {
double price = pricingService.calculate(order); // İçinde kendi flag'i var
double discount = discountService.apply(order); // İçinde kendi flag'i var
double tax = taxService.calculate(price - discount); // İçinde kendi flag'i var
}2. Flag'leri Temizlememek
Temizlenmeyen flag'ler birikir. 6 ay sonra kodda 50 tane flag var, hangisi aktif hangisi ölü kimse bilmiyor. Kod okunmaz hale gelir, her yeni geliştirici "bu flag ne işe yarıyor?" diye sorar.
Kural: Bir özellik %100 rollout'a ulaşıp 2 hafta sorunsuz çalıştıktan sonra flag kaldırılmalı.
3. Flag'i İş Mantığı Olarak Kullanmak
// ❌ YANLIŞ — Bu bir feature flag değil, iş kuralı
if (AppFeatures.PREMIUM_USER.isActive()) {
showPremiumContent();
}Feature flag geçici bir mekanizmadır. Kalıcı iş kuralları (kullanıcı rolü, abonelik planı) için authorization/configuration sistemi kullan. Flag, "yeni bir özelliği güvenle yayınlamak" içindir, "kullanıcı X'in Y'ye erişip erişemeyeceği" için değildir.
4. Flag State'ini Sık Sorgulamak
// ❌ YANLIŞ — Döngüde her iterasyonda flag kontrolü
for (Product product : products) {
if (AppFeatures.NEW_PRICING.isActive()) { // 10.000 kez sorgulanıyor
product.setPrice(newPricing.calculate(product));
}
}
// ✅ DOĞRU — Bir kere kontrol et, sonucu kullan
boolean useNewPricing = AppFeatures.NEW_PRICING.isActive();
for (Product product : products) {
if (useNewPricing) {
product.setPrice(newPricing.calculate(product));
}
}Togglz kendi içinde cache kullanır, dolayısıyla performans etkisi minimal. Ama yine de gereksiz çağrıdan kaçınmak iyi pratiktir.
5. Test Yazmamak
Her flag kombinasyonu için test yazılmalı:
@Test
void search_with_advanced_search_enabled() {
// Flag'i açık olarak test et
TestFeatureManager.enable(AppFeatures.ADVANCED_SEARCH);
SearchResult result = searchService.search("spring batch");
assertThat(result.getResults()).isNotEmpty();
assertThat(result.isAdvanced()).isTrue();
}
@Test
void search_with_advanced_search_disabled() {
// Flag'i kapalı olarak test et
TestFeatureManager.disable(AppFeatures.ADVANCED_SEARCH);
SearchResult result = searchService.search("spring batch");
assertThat(result.getResults()).isNotEmpty();
assertThat(result.isAdvanced()).isFalse();
}Test için Togglz'ın TestFeatureManager'ını kullan:
@ExtendWith(TogglzExtension.class) // JUnit 5 extension
class SearchServiceTest {
// TestFeatureManager otomatik devreye girer
// Her test bağımsız flag durumu ile çalışır
}Alternatiflere Kısa Bakış
Togglz dışında başka seçenekler de var:
| Kütüphane | Tür | Avantaj | Dezavantaj |
|---|---|---|---|
| Togglz | Open-source, Java | Spring Boot entegrasyonu, olgun, ücretsiz | Sadece Java |
| Unleash | Open-source, polyglot | Çoklu dil desteği, dashboard | Kendi sunucun gerekli |
| LaunchDarkly | SaaS | Güçlü dashboard, analytics, SDK'lar | Ücretli, vendor lock-in |
| FF4J | Open-source, Java | Feature flag + audit, monitoring | Togglz kadar olgun değil |
| Spring Cloud Config | Spring | Config server ile entegre | Feature flag'e özel tasarlanmamış |
Küçük-orta projeler için Togglz yeterli. Büyük organizasyonlarda (çoklu dil, çoklu takım) LaunchDarkly veya Unleash daha uygun olabilir.
Bütünleşik Örnek — E-Ticaret Özellik Yönetimi
Tüm kavramları birleştiren bir senaryo. Bir e-ticaret uygulamasında birden fazla feature flag'i koordineli kullanıyoruz:
public enum ShopFeatures implements Feature {
@Label("Yeni Ürün Kartı Tasarımı")
NEW_PRODUCT_CARD,
@Label("AI Ürün Tavsiyeleri")
@DefaultActivationStrategy(id = GradualRolloutActivationStrategy.ID,
parameters = {
@ActivationParameter(
name = GradualRolloutActivationStrategy.PARAM_PERCENTAGE,
value = "10")
})
AI_RECOMMENDATIONS,
@Label("Hızlı Checkout")
@DefaultActivationStrategy(id = UsernameActivationStrategy.ID,
parameters = {
@ActivationParameter(
name = UsernameActivationStrategy.PARAM_USERS,
value = "beta-tester-1, beta-tester-2, product-manager")
})
QUICK_CHECKOUT,
@Label("SMS Bildirimleri — Kill Switch")
@EnabledByDefault
SMS_NOTIFICATIONS;
public boolean isActive() {
return FeatureContext.getFeatureManager().isActive(this);
}
}@RestController
@RequestMapping("/api/shop")
@RequiredArgsConstructor
public class ShopController {
private final ProductService productService;
private final RecommendationService recommendationService;
@GetMapping("/products/{id}")
public Map<String, Object> getProductPage(@PathVariable Long id) {
Product product = productService.getProduct(id);
Map<String, Object> response = new HashMap<>();
response.put("product", product);
// Yeni kart tasarımı flag'e bağlı
response.put("useNewCard", ShopFeatures.NEW_PRODUCT_CARD.isActive());
// AI tavsiyeleri sadece flag açıksa hesaplanır — kaynak tasarrufu
if (ShopFeatures.AI_RECOMMENDATIONS.isActive()) {
List<Product> recommendations = recommendationService.getAiRecommendations(product);
response.put("recommendations", recommendations);
response.put("recommendationType", "ai");
} else {
List<Product> recommendations = recommendationService.getPopularProducts();
response.put("recommendations", recommendations);
response.put("recommendationType", "popular");
}
// Quick checkout flag'ini frontend'e bildir
response.put("quickCheckoutEnabled", ShopFeatures.QUICK_CHECKOUT.isActive());
return response;
}
// Feature durumlarını topluca döndür — frontend için
@GetMapping("/feature-states")
public Map<String, Boolean> getFeatureStates() {
return Arrays.stream(ShopFeatures.values())
.collect(Collectors.toMap(
Enum::name,
ShopFeatures::isActive
));
}
}Bu örnekte:
NEW_PRODUCT_CARD: Frontend'e bilgi olarak gidiyor, UI'ın hangi template'i kullanacağını belirliyor
AI_RECOMMENDATIONS: Pahalı bir hesaplama, flag kapalıysa hiç çalışmıyor (kaynak tasarrufu)
QUICK_CHECKOUT: Sadece beta tester'lara açık
SMS_NOTIFICATIONS: Varsayılan açık, sorun olursa kapatılacak kill switch
Özet
Feature flags, deploy ve release'i birbirinden ayırır. Kodu production'a deploy edip özelliği kapalı tutabilir, istediğin zaman istediğin kitleye açabilirsin. Risk azaltır, hız artırır.
Togglz, Java/Spring ekosisteminin en olgun feature flag kütüphanesidir. Enum tabanlı feature tanımlama, çeşitli activation stratejileri ve built-in web konsolu sunar.
Activation stratejileri ile "herkese aç", "yüzdelik rollout", "belirli kullanıcılara aç", "custom koşul" gibi senaryoları destekleyebilirsin. Kademeli rollout production'da en yaygın kullanılan stratejidir.
Feature flag lifecycle kritiktir: oluştur → test et → yayginlaştır → kararlılaştır → temizle. Temizlemeyi atlayan ekipler flag cehennemine düşer — okunmaz kod, test edilemez kombinasyonlar.
Anti-pattern'lerden kaçın: iç içe flag kontrolü, flag'leri kalıcı iş mantığı olarak kullanma, temizlemeyi erteleme. Her flag geçicidir — doğduğu gün ölüm tarihi de belirlenmelidir.
Kill switch olarak feature flag'ler operasyonel güvenlik sağlar. Dış servis çöktüğünde veya yeni özellik sorun çıkardığında anında kapatabilirsin — deploy gerekmez.
AI Asistan
Sorularını yanıtlamaya hazır