← Kursa Dön
📄 Text · 15 min

Spring Profiles

Giriş

Bir uygulamayı geliştirirken bilgisayarınızda H2 in-memory veritabanı kullanırsınız. Test sunucusunda MySQL'e bağlanırsınız. Production'da ise PostgreSQL cluster'ınıza bağlanmanız gerekir. Aynı kod, üç farklı ortamda üç farklı konfigürasyonla çalışmalıdır. Peki bunu nasıl yönetirsiniz? Her deploy'dan önce application.properties'i elle mi değiştirirsiniz? Hayır! Spring Profiles tam olarak bu sorunu çözer.

Gerçek Dünya Analojisi

Bir arabanın sürüş modlarını düşünün: Eco modda düşük güç tüketimi, Sport modda yüksek performans, Comfort modda dengeli sürüş. Araba aynı araba, motor aynı motor — ama "profil" değiştirdiğinizde davranış değişir. Spring Profiles de uygulamanızın sürüş modudur: dev profilinde debug loglar açık ve H2 veritabanı, prod profilinde loglar minimum ve PostgreSQL.

Neden Profiles Kullanmalıyız?

  • Kod değişikliği yapmadan ortam ayarlarını değiştirmek

  • Güvenlik: Production şifrelerini development ortamına sızdırmamak

  • Farklı implementasyonlar: dev'de yerel dosya sistemi, prod'da AWS S3

  • 12-Factor App: "Store config in the environment" prensibi

  • CI/CD uyumu: Pipeline'da profil belirterek otomatik deployment


@Profile Annotation — Bean Bazında Kontrol

@Profile annotation'ı, bir bean'in sadece belirli profil aktifken oluşturulmasını sağlar. Aktif olmayan profildeki bean'ler Spring container'a hiç eklenmez.

Temel Kullanım

// StorageService interface'i — ortak sözleşme
public interface StorageService {
    String store(String filename, byte[] data);
    byte[] retrieve(String filename);
    void delete(String filename);
}

// === Development ortamı: Yerel dosya sistemi ===
@Service
@Profile("dev")
public class LocalStorageService implements StorageService {

    private final Path uploadDir = Paths.get("/tmp/uploads");

    @PostConstruct
    public void init() throws IOException {
        Files.createDirectories(uploadDir);
        System.out.println("📁 Local storage initialized: " + uploadDir);
    }

    @Override
    public String store(String filename, byte[] data) {
        try {
            Path filePath = uploadDir.resolve(filename);
            Files.write(filePath, data);
            return filePath.toString();
        } catch (IOException e) {
            throw new StorageException("Failed to store file locally", e);
        }
    }

    @Override
    public byte[] retrieve(String filename) {
        try {
            return Files.readAllBytes(uploadDir.resolve(filename));
        } catch (IOException e) {
            throw new StorageException("File not found: " + filename, e);
        }
    }

    @Override
    public void delete(String filename) {
        try {
            Files.deleteIfExists(uploadDir.resolve(filename));
        } catch (IOException e) {
            throw new StorageException("Failed to delete: " + filename, e);
        }
    }
}

// === Production ortamı: AWS S3 ===
@Service
@Profile("prod")
public class S3StorageService implements StorageService {

    private final AmazonS3 s3Client;
    private final String bucketName;

    public S3StorageService(AmazonS3 s3Client,
                            @Value("${aws.s3.bucket}") String bucketName) {
        this.s3Client = s3Client;
        this.bucketName = bucketName;
    }

    @Override
    public String store(String filename, byte[] data) {
        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentLength(data.length);
        s3Client.putObject(bucketName, filename,
                new ByteArrayInputStream(data), metadata);
        return "s3://" + bucketName + "/" + filename;
    }

    @Override
    public byte[] retrieve(String filename) {
        S3Object object = s3Client.getObject(bucketName, filename);
        try {
            return object.getObjectContent().readAllBytes();
        } catch (IOException e) {
            throw new StorageException("Failed to read from S3: " + filename, e);
        }
    }

    @Override
    public void delete(String filename) {
        s3Client.deleteObject(bucketName, filename);
    }
}

Bu örnekte:

  • dev profili aktifken → LocalStorageService bean oluşturulur, S3 oluşturulmaz

  • prod profili aktifken → S3StorageService bean oluşturulur, local oluşturulmaz

  • Diğer service'ler sadece StorageService interface'ine bağımlı — hangi implementasyon geldiğini bilmez

@Profile ile @Configuration

@Configuration
@Profile("dev")
public class DevConfig {

    @Bean
    public DataSource dataSource() {
        // H2 in-memory veritabanı
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("schema.sql")
                .addScript("test-data.sql")
                .build();
    }

    @Bean
    public JavaMailSender mailSender() {
        // Fake mail sender — gerçek mail göndermez
        return new JavaMailSenderImpl(); // localhost:25 (MailHog)
    }
}

@Configuration
@Profile("prod")
public class ProdConfig {

    @Bean
    public DataSource dataSource(
            @Value("${spring.datasource.url}") String url,
            @Value("${spring.datasource.username}") String username,
            @Value("${spring.datasource.password}") String password) {

        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        return new HikariDataSource(config);
    }
}

Profile-Specific Properties — Dosya Bazında Konfigürasyon

Her profil için ayrı properties/yml dosyası oluşturabilirsiniz. Spring Boot aktif profile göre doğru dosyayı otomatik yükler:

src/main/resources/
├── application.properties          # Ortak ayarlar (tüm profillerde geçerli)
├── application-dev.properties      # dev profili ayarları
├── application-test.properties     # test profili ayarları
├── application-staging.properties  # staging profili ayarları
└── application-prod.properties     # prod profili ayarları

Ortak Ayarlar — application.properties

# Tüm ortamlarda geçerli
app.name=MyApplication
app.version=1.0.0
server.port=8080

# Varsayılan profil
spring.profiles.active=dev

Development — application-dev.properties

# Veritabanı: H2 in-memory
spring.datasource.url=jdbc:h2:mem:devdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# H2 Console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# Loglama: Detaylı
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.level.org.hibernate.SQL=DEBUG

# Özel ayarlar
app.feature.email-verification=false
app.storage.type=local
app.cors.allowed-origins=http://localhost:3000

Production — application-prod.properties

# Veritabanı: PostgreSQL
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}

# JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false

# Loglama: Minimum
logging.level.root=WARN
logging.level.com.example=INFO

# Güvenlik
app.feature.email-verification=true
app.storage.type=s3
app.cors.allowed-origins=https://myapp.com,https://www.myapp.com

# Performans
server.tomcat.max-threads=200
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5

YAML Alternatifi

Tek dosyada çoklu profil tanımı (Spring Boot 2.4+):

# application.yml
app:
  name: MyApplication

---
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:h2:mem:devdb
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop

---
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: ${DATABASE_URL}
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate

⚠️ Dikkat: Tek dosyada çoklu profil tanımı (--- ayracı ile) okunabilirliği düşürür. Büyük projelerde ayrı dosyalar tercih edin.


Profil Aktifleştirme Yolları

1. application.properties ile (Geliştirme için)

spring.profiles.active=dev

2. JVM Argument ile (IDE ve jar çalıştırma)

# IDE'de VM Options'a ekleyin
-Dspring.profiles.active=prod

# JAR çalıştırırken
java -jar myapp.jar --spring.profiles.active=prod

# Alternatif
java -Dspring.profiles.active=prod -jar myapp.jar

3. Ortam Değişkeni ile (Docker ve CI/CD için ideal)

# Linux/Mac
export SPRING_PROFILES_ACTIVE=prod
java -jar myapp.jar

# Docker
docker run -e SPRING_PROFILES_ACTIVE=prod myapp:latest

# Docker Compose
services:
  app:
    image: myapp:latest
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DATABASE_URL=jdbc:postgresql://db:5432/proddb

4. Maven ile (Build sırasında)

mvn spring-boot:run -Dspring-boot.run.profiles=dev

5. Programmatik Olarak

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MyApp.class);
    app.setAdditionalProfiles("dev", "swagger");
    app.run(args);
}

Öncelik Sırası (Yüksekten düşüğe)

  1. --spring.profiles.active=prod (command line argument)

  2. SPRING_PROFILES_ACTIVE=prod (environment variable)

  3. -Dspring.profiles.active=prod (JVM system property)

  4. spring.profiles.active=prod (application.properties)

  5. @ActiveProfiles("test") (test annotation)

💡 İpucu: Production'da asla application.properties içinde profil belirtmeyin. Ortam değişkeni veya command line argument kullanın. Böylece aynı JAR dosyası her ortamda çalışır.


Birden Fazla Profil ve Profile Groups

Çoklu Profil Aktifleştirme

# Virgülle ayırarak birden fazla profil aktifleştirin
spring.profiles.active=dev,metrics,swagger
// Bu bean, "metrics" profili aktifken oluşturulur
@Configuration
@Profile("metrics")
public class MetricsConfig {
    @Bean
    public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
        return registry -> registry.config()
                .commonTags("application", "myapp");
    }
}

Profile Groups (Spring Boot 2.4+)

Birden fazla profili gruplayarak tek bir profil adıyla aktifleştirebilirsiniz:

# application.properties
spring.profiles.group.development=dev,swagger,debug,h2
spring.profiles.group.production=prod,metrics,security
spring.profiles.group.staging=staging,metrics

# Kullanım:
# spring.profiles.active=development → dev + swagger + debug + h2 hepsi aktif olur
# spring.profiles.active=production → prod + metrics + security hepsi aktif olur

Profile Negation ve Çoklu Profile Eşleştirme

// "prod" profili aktif DEĞİLKEN — geliştirme araçları
@Configuration
@Profile("!prod")
public class DevToolsConfig {
    @Bean
    public CommandLineRunner seedDatabase(UserRepository repo) {
        return args -> {
            repo.save(new User("Test User", "test@example.com"));
            repo.save(new User("Admin", "admin@example.com"));
            System.out.println("🌱 Test verileri yüklendi");
        };
    }
}

// "dev" VEYA "test" profilinde
@Service
@Profile({"dev", "test"})
public class MockPaymentService implements PaymentService {
    @Override
    public PaymentResult charge(PaymentRequest request) {
        System.out.println("💳 Mock ödeme: " + request.getAmount());
        return PaymentResult.success("MOCK-TXN-" + System.currentTimeMillis());
    }
}

// "prod" VE "eu-west" profillerinin ikisi de aktifken
// NOT: @Profile tek annotation ile AND yapamaz
// Çözüm: @Conditional veya iç içe configuration kullanın
@Configuration
@Profile("prod")
public class ProdConfig {

    @Configuration
    @Profile("eu-west")
    static class EuWestConfig {
        @Bean
        public DataSource euWestDataSource() {
            // EU-West bölgesine özgü DataSource
            return createDataSource("eu-west-db.example.com");
        }
    }
}

Default Profile

Hiçbir profil aktif değilken default profili otomatik aktiftir:

@Service
@Profile("default")
public class DefaultStorageService implements StorageService {
    // Hiçbir profil belirtilmemişse bu bean oluşturulur
    @Override
    public String store(String filename, byte[] data) {
        System.out.println("⚠️ Default storage — hiçbir profil aktif değil!");
        // Basit yerel depolama
        return "/tmp/" + filename;
    }
}

⚠️ Dikkat: spring.profiles.active=dev belirlerseniz, default profili devre dışı kalır. @Profile("default") bean'leri sadece hiçbir profil aktif olmadığında oluşturulur.


Test'lerde Profile Kullanımı

// @ActiveProfiles ile test profilini aktifleştirin
@SpringBootTest
@ActiveProfiles("test")
class UserServiceTest {

    @Autowired
    private UserService userService; // Test profili bean'leri kullanılır

    @Test
    void shouldCreateUser() {
        // test profili aktif → H2 veritabanı, mock mail service...
    }
}

// Integration test için farklı profil
@SpringBootTest
@ActiveProfiles({"test", "integration"})
class IntegrationTest { ... }
# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
app.feature.email-verification=false

Yaygın Hatalar ve Çözümleri

Hata 1: Profile Oluşturmayı Unutmak

// ❌ @Profile("dev") ve @Profile("prod") var ama "test" profili için implementasyon yok
// Test'lerde: NoSuchBeanDefinitionException!

// ✅ Her profil için bean olduğundan emin olun veya @Profile("!prod") kullanın
@Service
@Profile({"dev", "test"}) // Hem dev hem test'te çalışır
public class LocalStorageService implements StorageService { ... }

Hata 2: application-profil.properties Dosya Adını Yanlış Yazmak

// ❌ YANLIŞ dosya adları
application.dev.properties     // Tire eksik
application-Dev.properties     // Büyük harf (case-sensitive!)
dev-application.properties     // Yanlış sıra

// ✅ DOĞRU format
application-dev.properties

Hata 3: Prod Şifreleri application.properties'e Yazmak

# ❌ TEHLİKELİ — Git'e commit edilir!
spring.datasource.password=SuperSecretPassword123

# ✅ GÜVENLİ — Environment variable kullanın
spring.datasource.password=${DB_PASSWORD}

Hata 4: Profil Test'te Kontrol Etmemek

// ❌ Profilin gerçekten aktif olduğunu test etmeyin → sessiz hata
// ✅ @ActiveProfiles mutlaka belirtin ve kontrol edin
@SpringBootTest
@ActiveProfiles("test")
class MyTest {
    @Autowired
    private Environment env;

    @Test
    void profileShouldBeActive() {
        assertTrue(Arrays.asList(env.getActiveProfiles()).contains("test"));
    }
}

Özet

  • Spring Profiles aynı kodu farklı ortamlarda farklı konfigürasyonlarla çalıştırır

  • `@Profile("dev")` ile bean bazında, `application-dev.properties` ile konfigürasyon bazında ortam ayrımı

  • Profil aktifleştirme: environment variable (SPRING_PROFILES_ACTIVE) production için en güvenli yoldur

  • Profile groups ile birden fazla profili tek isimle aktifleştirebilirsiniz

  • `!prod` negation ile "production hariç tüm ortamlarda" mantığı kurabilirsiniz

  • Test'lerde @ActiveProfiles("test") kullanın

  • Production şifrelerini asla properties dosyasına yazmayın — environment variable veya secret manager kullanın


Bütünleşik Gerçek Dünya Örneği: Bildirim Servisi

Farklı ortamlarda farklı bildirim stratejileri kullanan tam bir örnek:

// === Interface ===
public interface NotificationService {
    void sendNotification(String recipient, String message);
}

// === Development: Konsola yazdır ===
@Service
@Profile("dev")
@Slf4j
public class ConsoleNotificationService implements NotificationService {
    @Override
    public void sendNotification(String recipient, String message) {
        log.info("📧 [DEV] Bildirim → {}: {}", recipient, message);
        // Gerçek e-posta göndermez, sadece loglar
    }
}

// === Test: Hafızada sakla (doğrulama için) ===
@Service
@Profile("test")
public class InMemoryNotificationService implements NotificationService {
    private final List<Notification> sentNotifications = new ArrayList<>();

    @Override
    public void sendNotification(String recipient, String message) {
        sentNotifications.add(new Notification(recipient, message, Instant.now()));
    }

    // Test'lerde kullanmak için
    public List<Notification> getSentNotifications() {
        return Collections.unmodifiableList(sentNotifications);
    }

    public void clear() {
        sentNotifications.clear();
    }

    public record Notification(String recipient, String message, Instant sentAt) {}
}

// === Production: Gerçek e-posta gönder ===
@Service
@Profile("prod")
@RequiredArgsConstructor
@Slf4j
public class EmailNotificationService implements NotificationService {
    private final JavaMailSender mailSender;

    @Value("${app.email.from}")
    private String fromAddress;

    @Override
    public void sendNotification(String recipient, String message) {
        try {
            SimpleMailMessage mail = new SimpleMailMessage();
            mail.setFrom(fromAddress);
            mail.setTo(recipient);
            mail.setSubject("Bildirim");
            mail.setText(message);
            mailSender.send(mail);
            log.info("✅ E-posta gönderildi: {}", recipient);
        } catch (Exception e) {
            log.error("❌ E-posta gönderilemedi: {}", recipient, e);
            throw new NotificationException("Failed to send email", e);
        }
    }
}
// Bu service hangi profil aktif olursa olsun aynı şekilde çalışır
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final NotificationService notificationService; // Profil'e göre farklı impl

    @Transactional
    public Order placeOrder(OrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);

        // Dev'de konsola yazdırır, prod'da e-posta gönderir
        notificationService.sendNotification(
            request.getEmail(),
            "Siparişiniz alındı: " + order.getOrderNumber()
        );

        return order;
    }
}

Bu pattern'in güzelliği: OrderService hiçbir zaman hangi notification implementasyonunun kullanıldığını bilmez. Profile göre doğru bean otomatik enjekte edilir. Single Responsibility ve Open/Closed prensipleri mükemmel uygulanmış olur.


Profil Yönetimi Best Practices

  1. Profil adlarını kısa ve tutarlı tutun: dev, test, staging, prod

  2. Ortak ayarları application.properties'te, ortama özgü ayarları profil dosyalarında tutun

  3. Profile group kullanarak ilgili profilleri gruplayın: development = dev,swagger,debug

  4. Hassas bilgileri (şifre, API key) environment variable ile verin

  5. Her profili test edin — "prod'da çalışır dev'de çalışmaz" en tehlikeli durumlardan biridir

  6. Profil sayısını sınırlayın — 4-5 profil yeterli, fazlası yönetilemez karmaşıklık yaratır