스프링 프레임워크/IoC (Inversion of Control)

Composing Java-based Configurations

GLaDiDos 2024. 11. 19. 16:20

https://sundaland.tistory.com/481

 

▶ Composing Java-based Configurations

Spring의 Java 기반 구성 기능을 사용하면 어노테이션을 작성할 수 있어 구성의 복잡성을 줄일 수 있다.

 

▷ Using the @Import Annotation

<import/> 엘리먼트가 Spring XML 파일 내에서 구성의 모듈화를 돕기 위해 사용되는 것처럼, @Import 어노테이션을 사용하면 다음 예제와 같이 다른 구성 클래스에서 @Bean 정의를 로드할 수 있다.

@Configuration
public class ConfigA {

	@Bean
	public A a() {
		return new A();
	}
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

	@Bean
	public B b() {
		return new B();
	}
}

이제 컨텍스트를 인스턴스화할 때 ConfigA.class와 ConfigB.class를 모두 지정할 필요 없이 다음 예제와 같이 ConfigB만 명시적으로 제공하면 된다.

public static void main(String[] args) {
	ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

	// now both beans A and B will be available...
	A a = ctx.getBean(A.class);
	B b = ctx.getBean(B.class);
}

이 접근 방식은 컨테이너 인스턴스화를 간소화한다. 컨테이너를 생성하는 동안 잠재적으로 많은 수의 @Configuration 클래스를 기억할 필요가 없고, 하나의 클래스만 처리하면 되기 때문이다.

 

▷ Injecting Dependencies on Imported @Bean Definitions

앞의 예제는 작동하지만 단순하다. 대부분의 실제 시나리오에서 빈은 구성 클래스 간에 서로 종속성이 있다. XML을 사용하는 경우 컴파일러가 관여하지 않기 때문에 문제가 되지 않으며 ref="someBean"을 선언하고 컨테이너 초기화 중에 Spring이 이를 해결하도록 신뢰할 수 있다. @Configuration 클래스를 사용하는 경우 Java 컴파일러는 구성 모델에 제약 조건을 두므로 다른 빈에 대한 참조는 유효한 Java 구문이어야 한다.

@Bean 메서드는 빈 종속성을 설명하는 임의의 수의 파라미터를 가질 수 있다. 각각 다른 빈에 선언된 빈에 따라 달라지는 여러 @Configuration 클래스가 있는 다음과 같은 보다 현실적인 시나리오를 고려 해야한다.

@Configuration
public class ServiceConfig {

	@Bean
	public TransferService transferService(AccountRepository accountRepository) {
		return new TransferServiceImpl(accountRepository);
	}
}

@Configuration
public class RepositoryConfig {

	@Bean
	public AccountRepository accountRepository(DataSource dataSource) {
		return new JdbcAccountRepository(dataSource);
	}
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

	@Bean
	public DataSource dataSource() {
		// return new DataSource
	}
}

public static void main(String[] args) {
	ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
	// everything wires up across configuration classes...
	TransferService transferService = ctx.getBean(TransferService.class);
	transferService.transfer(100.00, "A123", "C456");
}

// SpringTransactionManager 참조

@Configuration 클래스는 궁극적으로 컨테이너의 또 다른 빈일 뿐이다. 즉, 다른 빈과 마찬가지로 @Autowired 및 @Value 주입과 다른 기능을 활용할 수 있다.

 

▼ 구성 클래스내에 @Autowired를 사용한 의존성 주입

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import javax.sql.DataSource;

@Configuration
public class ServiceConfig {

    @Autowired
    private AccountRepository accountRepository; // 자동 주입

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl(accountRepository); // 필드에서 가져온 빈 사용
    }
}

@Configuration
public class RepositoryConfig {

    @Autowired
    private DataSource dataSource; // 자동 주입

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource); // 필드에서 가져온 빈 사용
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // DataSource 구현체 반환
        // 예를 들어, HikariDataSource 등
    }
}

public class MainApp {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
        // 모든 설정 클래스가 적절하게 연결됨
        TransferService transferService = ctx.getBean(TransferService.class);
        transferService.transfer(100.00, "A123", "C456");
    }
}

 

@Configuration 클래스의 생성자 주입은 Spring Framework 4.3부터만 지원된다. 또한 대상 빈이 생성자를 하나만 정의하는 경우 @Autowired를 지정할 필요가 없다는 점에 유의해야 한다.

 

▷ Fully-qualifying imported beans for ease of navigation

이전 시나리오에서 @Autowired를 사용하면 잘 작동하고 원하는 모듈성을 제공하지만 자동 와이어링된 빈 정의가 정확히 어디에 선언되는지 확인하는 것은 다소 모호하다. Eclipse용 Spring Tools는 모든 것이 어떻게 와이어링되는지 보여주는 그래프를 렌더링할 수 있는 도구를 제공(스프링 부트 프로젝트에서만 지원)하며, 이것만으로도 충분할 수 있다. 또한 Java IDE는 AccountRepository 타입의 모든 선언과 사용을 쉽게 찾고 해당 타입을 리턴하는 @Bean 메서드의 위치를 ​​빠르게 보여줄 수 있다.

이러한 모호성이 허용되지 않고 IDE 내에서 한 @Configuration 클래스에서 다른 @Configuration 클래스로 직접 이동하려는 경우 구성 클래스 자체를 자동 와이어링하는 것을 고려해야 한다. 다음 예는 그 방법을 보여다.

@Configuration
public class ServiceConfig {

	@Autowired
	private RepositoryConfig repositoryConfig;

	@Bean
	public TransferService transferService() {
		// navigate 'through' the config class to the @Bean method!
		return new TransferServiceImpl(repositoryConfig.accountRepository());
	}
}

 이전 상황에서 AccountRepository가 정의된 위치는 완전히 명시적이다. 그러나 ServiceConfig는 이제 RepositoryConfig에 밀접하게 결합되었다. 이것이 트레이드오프다. 이러한 밀접 결합은 인터페이스 기반 또는 추상 클래스 기반 @Configuration 클래스를 사용하여 다소 완화할 수 있다.

@Configuration
public class ServiceConfig {

	@Autowired
	private RepositoryConfig repositoryConfig;

	@Bean
	public TransferService transferService() {
		return new TransferServiceImpl(repositoryConfig.accountRepository());
	}
}

@Configuration
public interface RepositoryConfig {

	@Bean
	AccountRepository accountRepository();
}

@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {

	@Bean
	public AccountRepository accountRepository() {
		return new JdbcAccountRepository(...);
	}
}

@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class})  // import the concrete config!
public class SystemTestConfig {

	@Bean
	public DataSource dataSource() {
		// return DataSource
	}

}

public static void main(String[] args) {
	ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
	TransferService transferService = ctx.getBean(TransferService.class);
	transferService.transfer(100.00, "A123", "C456");
}

이제 ServiceConfig는 구체적인 DefaultRepositoryConfig와 관련하여 느슨하게 결합되었으며, 내장된 IDE 툴링은 여전히 ​​유용하다. RepositoryConfig 구현의 타입 계층을 쉽게 얻을 수 있다. 이런 식으로 @Configuration 클래스와 해당 의존성을 탐색하는 것은 인터페이스 기반 코드를 탐색하는 일반적인 프로세스와 다르지 않다.'

 

특정 빈의 시작 시 생성 순서에 영향을 주고 싶다면, 일부 빈을 @Lazy(시작 시가 아닌 첫 번째 액세스 시 생성)로 선언하거나, 다른 특정 빈에 @DependsOn(현재 빈의 직접적인 의존성이 의미하는 것 이상으로, 다른 특정 빈이 현재 빈보다 먼저 생성되도록 함)으로 선언하는 것을 고려해야 한다.

 

▶ Conditionally Include @Configuration Classes or @Bean Methods

임의의 시스템 상태에 따라 전체 @Configuration 클래스 또는 개별 @Bean 메서드를 조건부로 활성화하거나 비활성화하는 것이 종종 유용하다. 이에 대한 일반적인 예 중 하나는 @Profile을 사용하여 Spring 환경에서 특정 프로필이 활성화된 경우에만 Bean을 활성화하는 것이다.

@Profile은 실제로 @Conditional이라는 훨씬 더 유연한 어노테이션을 사용하여 구현된다. @Conditional은 @Bean이 등록되기 전에 참조해야 하는 특정 org.springframework.context.annotation.Condition 구현을 나타낸다.

Condition 인터페이스의 구현은 true 또는 false를 반환하는 matches(…​) 메서드를 제공한다. 예를 들어, 다음 목록은 @Profile에 사용된 실제 Condition 구현을 보여다.

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
	// Read the @Profile annotation attributes
	MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
	if (attrs != null) {
		for (Object value : attrs.get("value")) {
			if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
				return true;
			}
		}
		return false;
	}
	return true;
}

 

이 코드는 @Profile 애노테이션을 사용해 특정 프로파일이 활성화된 경우에만 빈을 등록하는 방법을 예시로 들고, 나아가 사용자 정의 Condition 인터페이스를 구현하여 @Conditional을 사용하는 방법도 포함한다.

 

◎ 1. HikariCP 디펜던시 추가

pom.xml

	<dependency>
	    <groupId>com.zaxxer</groupId>
	    <artifactId>HikariCP</artifactId>
	    <version>5.0.1</version>
	 </dependency>

 

◎ 2. @Profile 애노테이션을 사용한 샘플 코드

AppConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import com.zaxxer.hikari.HikariDataSource;

@Configuration
public class AppConfig {

    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        // 개발 환경용 데이터소스 설정
    	SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
		
		dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class);
		dataSource.setUrl("jdbc:mysql://localhost:3306/sbdt_db?characterEncoding=UTF-8");
		dataSource.setUsername("root");
		dataSource.setPassword("1234");

		return dataSource;
    }

    @Bean
    @Profile("prod")
    public DataSource prodDataSource() {
        // 프로덕션 환경용 데이터소스 설정
        return new HikariDataSource(); // 다른 데이터베이스 설정
    }
}

 

MainApp.java

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MainApp {
    public static void main(String[] args) {
        // 프로파일 설정: "dev" 또는 "prod"
        System.setProperty("spring.profiles.active", "dev");

        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);

        DataSource dataSource = ctx.getBean(DataSource.class);
        System.out.println("Using DataSource: " + dataSource.getClass().getName());
    }
}

 

3. @Conditional 애노테이션을 사용한 사용자 정의 Condition 구현

CustomCondition.java

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class CustomCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 예를 들어 특정 시스템 속성이 존재하는 경우에만 true 반환
        String expectedProperty = "my.custom.property";
        String propertyValue = context.getEnvironment().getProperty(expectedProperty);
        return propertyValue != null && propertyValue.equals("enabled");
    }
}

 

AppConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    @Conditional(CustomCondition.class)
    public MyService myService() {
        return new MyServiceImpl();
    }
}

 

MainApp.java

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MainApp {
    public static void main(String[] args) {
        // 조건에 맞는 시스템 속성 설정
        System.setProperty("my.custom.property", "enabled");

        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);

        if (ctx.containsBean("myService")) {
            MyService myService = ctx.getBean(MyService.class);
            System.out.println("MyService Bean is available: " + myService.getClass().getName());
        } else {
            System.out.println("MyService Bean is not available");
        }
    }
}

 

◎ 4. 주요 클래스 정의

MyService.java

public interface MyService {
    void performService();
}

 

MyServiceImpl.java

public class MyServiceImpl implements MyService {

    @Override
    public void performService() {
        System.out.println("Service is being performed.");
    }
}

 

◎ 설명

1. @Profile 사용

  • AppConfig 클래스는 @Profile 애노테이션을 사용하여 특정 프로파일이 활성화된 경우에만 빈을 등록한다.
  • MainApp에서는 spring.profiles.active 속성을 설정하여 "dev" 또는 "prod" 프로파일을 활성화할 수 있다.

 

2. @Conditional 사용

  •  CustomCondition 클래스는 Condition 인터페이스를 구현하여, 특정 시스템 속성이 설정된 경우에만 빈을 등록하도록 한다.
  • AppConfig 클래스의 myService 빈은 @Conditional을 사용하여 CustomCondition이 참일 경우에만 등록된다.
  • MainApp에서 시스템 속성을 설정하고, 해당 조건에 맞는 빈이 등록되었는지 확인한다.

 

이 샘플 코드는 @Profile과 @Conditional 애노테이션을 사용하여 Spring에서 조건부로 @Configuration 클래스나 @Bean 메서드를 포함하거나 제외하는 방법을 보여준다. 이러한 기능을 사용하면 특정 환경이나 조건에 맞게 애플리케이션 구성을 유연하게 제어할 수 있다.