๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

๐Ÿ—๏ธ IoC Container

IoC(Inversion of Control) ์ปจํ…Œ์ด๋„ˆ๋Š” Sprout Framework์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ๊ฐ์ฒด ์ƒ์„ฑ, ์˜์กด์„ฑ ์ฃผ์ž…, ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๊ฐœ์š”โ€‹

Sprout์˜ IoC ์ปจํ…Œ์ด๋„ˆ๋Š” ๋‹ค์Œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

  • ์ปดํฌ๋„ŒํŠธ ์Šค์บ”: Reflections ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•œ ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ํด๋ž˜์Šค ์ž๋™ ๊ฐ์ง€
  • ์ƒ์„ฑ์ž ์ฃผ์ž…: ํƒ€์ž… ์•ˆ์ „ํ•œ ์˜์กด์„ฑ ํ•ด๊ฒฐ (ํ•„๋“œ ์ฃผ์ž… ๋ฏธ์ง€์›)
  • ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ: ๋นˆ ์ƒ์„ฑ, ์ดˆ๊ธฐํ™”, ์†Œ๋ฉธ
  • ์ˆœํ™˜ ์˜์กด์„ฑ ๊ฐ์ง€: BeanGraph๋ฅผ ํ†ตํ•œ ์œ„์ƒ ์ •๋ ฌ๊ณผ ์ˆœํ™˜ ์ฐธ์กฐ ๊ฐ์ง€
  • ์ˆœ์„œ ์ง€์›: @Order๋ฅผ ํ†ตํ•œ ๋นˆ ์ดˆ๊ธฐํ™” ๋ฐ ์ปฌ๋ ‰์…˜ ์ˆœ์„œ ์ œ์–ด
  • CGLIB ํ”„๋ก์‹œ: @Configuration ํด๋ž˜์Šค์˜ ์‹ฑ๊ธ€ํ†ค ๋ณด์žฅ

์ปจํ…Œ์ด๋„ˆ ์•„ํ‚คํ…์ฒ˜โ€‹

ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธโ€‹

Sprout์˜ IoC ์ปจํ…Œ์ด๋„ˆ๋Š” ๋‹ค์Œ ์ฃผ์š” ํด๋ž˜์Šค๋“ค๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค.

  • SproutApplicationContext: ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ
  • DefaultListableBeanFactory: ํ•ต์‹ฌ ๋นˆ ํŒฉํ† ๋ฆฌ ๊ตฌํ˜„
  • ClassPathScanner: ํด๋ž˜์ŠคํŒจ์Šค ์Šค์บ” ๋ฐ ๋นˆ ์ •์˜ ์ƒ์„ฑ
  • BeanGraph: ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„์™€ ์œ„์ƒ ์ •๋ ฌ

์ปจํ…Œ์ด๋„ˆ ์ดˆ๊ธฐํ™” ๊ณผ์ •โ€‹

public class SproutApplication {
public static void run(Class<?> primarySource) throws Exception {
// 1. ํŒจํ‚ค์ง€ ์Šค์บ” ์„ค์ •
List<String> packages = getPackagesToScan(primarySource);

// 2. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
ApplicationContext applicationContext =
new SproutApplicationContext(packages.toArray(new String[0]));

// 3. ์ปจํ…์ŠคํŠธ ์ดˆ๊ธฐํ™” (refresh)
applicationContext.refresh();

// 4. ์„œ๋ฒ„ ์‹œ์ž‘
HttpServer server = applicationContext.getBean(HttpServer.class);
server.start(port);
}
}

์ปดํฌ๋„ŒํŠธ ์Šค์บ”โ€‹

์ง€์›๋˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜โ€‹

Sprout๋Š” ๋‹ค์Œ ์ปดํฌ๋„ŒํŠธ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ธ์‹ํ•ฉ๋‹ˆ๋‹ค:

@Component         // ์ผ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ
@Service // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ณ„์ธต
@Repository // ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ณ„์ธต
@Controller // ์›น ๊ณ„์ธต
@Configuration // ๊ตฌ์„ฑ ํด๋ž˜์Šค
@Aspect // AOP ์• ์ŠคํŽ™ํŠธ
@ControllerAdvice // ๊ธ€๋กœ๋ฒŒ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
@WebSocketHandler // WebSocket ํ•ธ๋“ค๋Ÿฌ

์Šค์บ” ๊ณผ์ •โ€‹

// ClassPathScanner์˜ ์Šค์บ” ๋กœ์ง
public Collection<BeanDefinition> scan(ConfigurationBuilder configBuilder,
Class<? extends Annotation>... componentAnnotations) {
// 1. Reflections๋ฅผ ์ด์šฉํ•œ ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜ ํด๋ž˜์Šค ํƒ์ƒ‰
Set<Class<?>> componentCandidates = new HashSet<>();
for (Class<? extends Annotation> anno : componentAnnotations) {
componentCandidates.addAll(r.getTypesAnnotatedWith(anno));
}

// 2. ๊ตฌ์ฒด ํด๋ž˜์Šค๋งŒ ํ•„ํ„ฐ๋ง (์ธํ„ฐํŽ˜์ด์Šค, ์ถ”์ƒํด๋ž˜์Šค ์ œ์™ธ)
Set<Class<?>> concreteComponentTypes = componentCandidates.stream()
.filter(clazz -> !clazz.isInterface() &&
!clazz.isAnnotation() &&
!Modifier.isAbstract(clazz.getModifiers()))
.collect(Collectors.toSet());

// 3. @Bean ๋ฉ”์„œ๋“œ ๊ธฐ๋ฐ˜ ๋นˆ ํƒ์ƒ‰
Set<Class<?>> configClasses = r.getTypesAnnotatedWith(Configuration.class);
for (Class<?> configClass : configClasses) {
for (Method method : configClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(Bean.class)) {
beanMethodReturnTypes.add(method.getReturnType());
}
}
}
}

์ปดํฌ๋„ŒํŠธ ์Šค์บ” ํ™œ์„ฑํ™”โ€‹

๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํด๋ž˜์Šค์— @ComponentScan์„ ์‚ฌ์šฉํ•˜์„ธ์š”:

@ComponentScan("com.myapp")  // ํŠน์ • ํŒจํ‚ค์ง€ ์Šค์บ”
@ComponentScan({"com.myapp.web", "com.myapp.service"}) // ์—ฌ๋Ÿฌ ํŒจํ‚ค์ง€
public class Application {
public static void main(String[] args) throws Exception {
SproutApplication.run(Application.class);
}
}

์˜์กด์„ฑ ์ฃผ์ž…โ€‹

์ƒ์„ฑ์ž ์ฃผ์ž… ์ „๋žตโ€‹

Sprout๋Š” ์ƒ์„ฑ์ž ์ฃผ์ž…๋งŒ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€์žฅ ๋งŽ์€ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์ง„ ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•œ ์ƒ์„ฑ์ž๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.

// ์ƒ์„ฑ์ž ํ•ด๊ฒฐ ๋กœ์ง
private Constructor<?> resolveUsableConstructor(Class<?> clazz, Set<Class<?>> allKnownBeanTypes) {
return Arrays.stream(clazz.getDeclaredConstructors())
.filter(constructor -> Arrays.stream(constructor.getParameterTypes())
.allMatch(param -> isResolvable(param, allKnownBeanTypes)))
.max(Comparator.comparingInt(Constructor::getParameterCount))
.orElseThrow(() -> new NoSuchMethodException("No usable constructor"));
}

์˜์กด์„ฑ ํ•ด๊ฒฐ ๊ทœ์น™โ€‹

private boolean isResolvable(Class<?> paramType, Set<Class<?>> allKnownBeanTypes) {
// 1. List ํƒ€์ž…์€ ํ•ญ์ƒ ํ•ด๊ฒฐ ๊ฐ€๋Šฅ
if (List.class.isAssignableFrom(paramType)) {
return true;
}

// 2. ์ธํ”„๋ผ ํƒ€์ž… ํ™•์ธ (ApplicationContext, BeanFactory ๋“ฑ)
if (isKnownInfrastructureType(paramType)) {
return true;
}

// 3. ์•Œ๋ ค์ง„ ๋นˆ ํƒ€์ž… ์ค‘์—์„œ ํ• ๋‹น ๊ฐ€๋Šฅํ•œ ํƒ€์ž… ์ฐพ๊ธฐ
return allKnownBeanTypes.stream()
.anyMatch(knownType -> paramType.isAssignableFrom(knownType));
}

์˜ˆ์ œ: ๊ธฐ๋ณธ ์˜์กด์„ฑ ์ฃผ์ž…โ€‹

@Service
public class UserService {
private final UserRepository repository;
private final EmailService emailService;

// ์ƒ์„ฑ์ž ์ฃผ์ž… - @Autowired ๋ถˆํ•„์š”
public UserService(UserRepository repository, EmailService emailService) {
this.repository = repository;
this.emailService = emailService;
}
}

@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;

public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}

์ปฌ๋ ‰์…˜ ์ฃผ์ž…โ€‹

ํŠน์ • ํƒ€์ž…์˜ ๋ชจ๋“  ๋นˆ์„ List๋กœ ์ฃผ์ž…๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

public interface EventHandler {
void handle(Event event);
}

@Component
@Order(1)
public class EmailEventHandler implements EventHandler {
public void handle(Event event) { /* ์ด๋ฉ”์ผ ์ฒ˜๋ฆฌ */ }
}

@Component
@Order(2)
public class LogEventHandler implements EventHandler {
public void handle(Event event) { /* ๋กœ๊ทธ ์ฒ˜๋ฆฌ */ }
}

@Service
public class EventProcessor {
private final List<EventHandler> handlers;

// ๋ชจ๋“  EventHandler ๋นˆ์ด @Order ์ˆœ์„œ๋Œ€๋กœ ์ฃผ์ž…๋จ
public EventProcessor(List<EventHandler> handlers) {
this.handlers = handlers;
}

public void processEvent(Event event) {
handlers.forEach(handler -> handler.handle(event));
}
}

์ปฌ๋ ‰์…˜ ์ฃผ์ž… ์ฒ˜๋ฆฌ ๋กœ์งโ€‹

// DefaultListableBeanFactory์˜ ์ปฌ๋ ‰์…˜ ์ฃผ์ž… ํ›„์ฒ˜๋ฆฌ
protected void postProcessListInjections() {
for (PendingListInjection pending : pendingListInjections) {
Set<Object> uniqueBeansForList = new HashSet<>();
for (Object bean : singletons.values()) {
if (pending.getGenericType().isAssignableFrom(bean.getClass())) {
uniqueBeansForList.add(bean);
}
}

// @Order ์–ด๋…ธํ…Œ์ด์…˜์— ๋”ฐ๋ผ ์ •๋ ฌ
List<Object> sortedBeansForList = uniqueBeansForList.stream()
.sorted(Comparator.comparingInt(bean -> {
Class<?> clazz = bean.getClass();
Order order = clazz.getAnnotation(Order.class);
return (order != null) ? order.value() : Integer.MAX_VALUE;
}))
.toList();

pending.getListToPopulate().clear();
pending.getListToPopulate().addAll(sortedBeansForList);
}
}

๋นˆ ์ •์˜์™€ ์ƒ์„ฑโ€‹

๋นˆ ์ •์˜ ํƒ€์ž…โ€‹

Sprout๋Š” ๋‘ ๊ฐ€์ง€ ๋นˆ ์ƒ์„ฑ ๋ฐฉ์‹์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

  1. ์ƒ์„ฑ์ž ๊ธฐ๋ฐ˜ ๋นˆ (ConstructorBeanDefinition)
  2. ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๋นˆ (MethodBeanDefinition)

์ƒ์„ฑ์ž ๊ธฐ๋ฐ˜ ๋นˆโ€‹

@Component
public class NotificationService {
private final EmailService emailService;
private final SmsService smsService;

public NotificationService(EmailService emailService, SmsService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
}

ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๋นˆโ€‹

@Configuration
public class DataSourceConfig {

@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/myapp");
return new HikariDataSource(config);
}

@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}

@Configuration ํ”„๋ก์‹œโ€‹

@Configuration ํด๋ž˜์Šค๋Š” CGLIB์„ ์ด์šฉํ•ด ํ”„๋ก์‹œ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์‹ฑ๊ธ€ํ†ค์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

@Configuration(proxyBeanMethods = true)  // ๊ธฐ๋ณธ๊ฐ’
public class AppConfig {
@Bean
public ServiceA serviceA() {
return new ServiceA(serviceB()); // ๋™์ผํ•œ serviceB ์ธ์Šคํ„ด์Šค ๋ฐ˜ํ™˜
}

@Bean
public ServiceB serviceB() {
return new ServiceB();
}
}
// ํ”„๋ก์‹œ ์ƒ์„ฑ ๋กœ์ง
if (def.isConfigurationClassProxyNeeded()) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(def.getType());
enhancer.setCallback(new ConfigurationMethodInterceptor(this));
beanInstance = enhancer.create(def.getConstructorArgumentTypes(), deps);
}

์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌโ€‹

์ปจํ…Œ์ด๋„ˆ ์ดˆ๊ธฐํ™” ๊ณผ์ •โ€‹

@Override
public void refresh() throws Exception {
// 1. ๋นˆ ์ •์˜ ์Šค์บ”
scanBeanDefinitions();

// 2. ์ธํ”„๋ผ ๋นˆ ๋จผ์ € ์ƒ์„ฑ (BeanPostProcessor ๋“ฑ)
instantiateInfrastructureBeans();

// 3. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋นˆ ์ƒ์„ฑ
instantiateAllSingletons();

// 4. ์ปจํ…์ŠคํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ ํ›„ ์ฝœ๋ฐฑ
List<ContextInitializer> contextInitializers = getAllBeans(ContextInitializer.class);
for (ContextInitializer initializer : contextInitializers) {
initializer.initializeAfterRefresh(this);
}
}

๋นˆ ์ƒ์„ฑ ์ˆœ์„œโ€‹

Sprout๋Š” BeanGraph๋ฅผ ์ด์šฉํ•ด ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์œ„์ƒ ์ •๋ ฌํ•˜์—ฌ ์˜ฌ๋ฐ”๋ฅธ ์ˆœ์„œ๋กœ ๋นˆ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

private void instantiateGroup(List<BeanDefinition> defs) {
// ์œ„์ƒ ์ •๋ ฌ๋กœ ์˜์กด์„ฑ ์ˆœ์„œ ๊ฒฐ์ •
List<BeanDefinition> order = new BeanGraph(defs).topologicallySorted();

// ์ˆœ์„œ๋Œ€๋กœ ๋นˆ ์ƒ์„ฑ
order.forEach(beanFactory::createBean);

// ์ปฌ๋ ‰์…˜ ์ฃผ์ž… ํ›„์ฒ˜๋ฆฌ
beanFactory.postProcessListInjections();
}

@Primary์™€ ๋นˆ ์„ ํƒโ€‹

๋™์ผํ•œ ํƒ€์ž…์˜ ๋นˆ์ด ์—ฌ๋Ÿฌ ๊ฐœ์ผ ๋•Œ @Primary๋กœ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

private String choosePrimary(Class<?> requiredType, Set<String> candidates) {
// 1. @Primary๊ฐ€ ๋ถ™์€ ๋นˆ ์ฐพ๊ธฐ
List<String> primaries = candidates.stream()
.filter(name -> {
BeanDefinition d = beanDefinitions.get(name);
return d != null && d.isPrimary();
})
.toList();

if (primaries.size() == 1) return primaries.get(0);
if (primaries.size() > 1)
throw new RuntimeException("@Primary beans conflict for type " + requiredType.getName());

return null;
}

๋นˆ ํ›„์ฒ˜๋ฆฌ (Bean Post Processing)โ€‹

// ๋นˆ ์ƒ์„ฑ ํ›„ BeanPostProcessor ์ ์šฉ
Object processedBean = beanInstance;
for (BeanPostProcessor processor : beanPostProcessors) {
Object result = processor.postProcessBeforeInitialization(def.getName(), processedBean);
if (result != null) processedBean = result;
}
for (BeanPostProcessor processor : beanPostProcessors) {
Object result = processor.postProcessAfterInitialization(def.getName(), processedBean);
if (result != null) processedBean = result;
}

์ˆœํ™˜ ์˜์กด์„ฑ ๊ฐ์ง€โ€‹

Sprout๋Š” ์‹œ์ž‘ ์‹œ BeanGraph๋ฅผ ํ†ตํ•ด ์ˆœํ™˜ ์˜์กด์„ฑ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ๋ฐœ๊ฒฌ๋˜๋ฉด ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘์„ ์ค‘๋‹จํ•ฉ๋‹ˆ๋‹ค.

@Component
public class ServiceA {
public ServiceA(ServiceB serviceB) { /* ... */ }
}

@Component
public class ServiceB {
public ServiceB(ServiceC serviceC) { /* ... */ }
}

@Component
public class ServiceC {
public ServiceC(ServiceA serviceA) { /* ... */ } // ์ˆœํ™˜ ์˜์กด์„ฑ!
}

// ์œ„์ƒ ์ •๋ ฌ ์‹œ ์ˆœํ™˜ ์˜์กด์„ฑ์ด ๊ฐ์ง€๋˜์–ด ์‹œ์ž‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ

๋นˆ ๋“ฑ๋ก๊ณผ ๊ฒ€์ƒ‰โ€‹

ํƒ€์ž…๋ณ„ ๋นˆ ๋งคํ•‘โ€‹

// ํƒ€์ž…๋ณ„ ๋นˆ ์ด๋ฆ„ ๋งคํ•‘ (์ธํ„ฐํŽ˜์ด์Šค, ์ƒ์œ„ํด๋ž˜์Šค ํฌํ•จ)
private void registerInternal(String name, Object bean) {
singletons.put(name, bean);

Class<?> type = bean.getClass();
primaryTypeToNameMap.putIfAbsent(type, name);
typeToNamesMap.computeIfAbsent(type, k -> new HashSet<>()).add(name);

// ์ธํ„ฐํŽ˜์ด์Šค ๋“ฑ๋ก
for (Class<?> iface : type.getInterfaces()) {
primaryTypeToNameMap.putIfAbsent(iface, name);
typeToNamesMap.computeIfAbsent(iface, k -> new HashSet<>()).add(name);
}

// ์ƒ์œ„ ํด๋ž˜์Šค ๋“ฑ๋ก
for (Class<?> p = type.getSuperclass();
p != null && p != Object.class;
p = p.getSuperclass()) {
primaryTypeToNameMap.putIfAbsent(p, name);
typeToNamesMap.computeIfAbsent(p, k -> new HashSet<>()).add(name);
}
}

๋นˆ ๊ฒ€์ƒ‰โ€‹

@Override
public <T> T getBean(Class<T> requiredType) {
// 1. ์ด๋ฏธ ์ƒ์„ฑ๋œ ๋นˆ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
T bean = getIfPresent(requiredType);
if (bean != null) return bean;

// 2. ํ›„๋ณด ์ˆ˜์ง‘
Set<String> candidates = candidateNamesForType(requiredType);
if (candidates.isEmpty()) {
throw new RuntimeException("No bean of type " + requiredType.getName() + " found");
}

// 3. Primary ์„ ํƒ
String primary = choosePrimary(requiredType, candidates);
if (primary == null) {
if (candidates.size() == 1) primary = candidates.iterator().next();
else throw new RuntimeException("No unique bean of type " + requiredType.getName());
}

// 4. ํ•„์š”์‹œ ์ƒ์„ฑ ํ›„ ๋ฐ˜ํ™˜
return (T) createIfNecessary(primary);
}

๋ชจ๋ฒ” ์‚ฌ๋ก€โ€‹

1. ์ƒ์„ฑ์ž ์ฃผ์ž… ์‚ฌ์šฉโ€‹

// ๊ถŒ์žฅ: ์ƒ์„ฑ์ž ์ฃผ์ž…์œผ๋กœ ๋ถˆ๋ณ€์„ฑ ๋ณด์žฅ
@Service
public class UserService {
private final UserRepository repository;

public UserService(UserRepository repository) {
this.repository = repository;
}
}

2. ์ธํ„ฐํŽ˜์ด์Šค ๊ธฐ๋ฐ˜ ์„ค๊ณ„โ€‹

// ๊ถŒ์žฅ: ์ธํ„ฐํŽ˜์ด์Šค์— ์˜์กด
@Service
public class OrderService {
private final PaymentProcessor paymentProcessor;

public OrderService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
}

@Component
public class StripePaymentProcessor implements PaymentProcessor {
// ๊ตฌํ˜„
}

3. ์ˆœํ™˜ ์˜์กด์„ฑ ๋ฐฉ์ง€โ€‹

// ๊ถŒ์žฅ: ์ด๋ฒคํŠธ๋กœ ์ˆœํ™˜ ๋Š๊ธฐ
@Service
public class OrderService {
private final EventPublisher eventPublisher;

public void processOrder(Order order) {
// ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ
eventPublisher.publish(new OrderProcessedEvent(order));
}
}

@Component
public class InventoryService {
@EventListener
public void handleOrderProcessed(OrderProcessedEvent event) {
// ์žฌ๊ณ  ์—…๋ฐ์ดํŠธ
}
}

4. @Order๋กœ ์ˆœ์„œ ์ œ์–ดโ€‹

@Component
@Order(1)
public class ValidationFilter implements Filter {
// ๋จผ์ € ์‹คํ–‰
}

@Component
@Order(2)
public class AuthenticationFilter implements Filter {
// ๊ฒ€์ฆ ํ›„ ์‹คํ–‰
}

์„ฑ๋Šฅ ์ตœ์ ํ™”โ€‹

์ง€์—ฐ ๋กœ๋”ฉ๊ณผ ์ฆ‰์‹œ ๋กœ๋”ฉโ€‹

Sprout๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ์‹ฑ๊ธ€ํ†ค ๋นˆ์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์žฅ์ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  • ์‹œ์ž‘ ์‹œ ์„ค์ • ์˜ค๋ฅ˜ ์กฐ๊ธฐ ๋ฐœ๊ฒฌ
  • ๋Ÿฐํƒ€์ž„ ์„ฑ๋Šฅ ํ–ฅ์ƒ
  • ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์˜ˆ์ธก ๊ฐ€๋Šฅ

๋นˆ ์Šค์ฝ”ํ”„โ€‹

ํ˜„์žฌ Sprout๋Š” ์‹ฑ๊ธ€ํ†ค ์Šค์ฝ”ํ”„๋งŒ ์ง€์›ํ•˜๋ฉฐ, ๋ชจ๋“  ๋นˆ์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด์—์„œ ํ•˜๋‚˜์˜ ์ธ์Šคํ„ด์Šค๋งŒ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

ํ™•์žฅ ํฌ์ธํŠธโ€‹

BeanDefinitionRegistrarโ€‹

์‚ฌ์šฉ์ž ์ •์˜ ๋นˆ ์ •์˜๋ฅผ ๋™์ ์œผ๋กœ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

public class MyFeatureAutoConfiguration implements BeanDefinitionRegistrar {
@Override
public Collection<BeanDefinition> registerAdditionalBeanDefinitions(
Collection<BeanDefinition> existingDefs) {
// ์กฐ๊ฑด๋ถ€ ๋นˆ ๋“ฑ๋ก ๋กœ์ง
return additionalBeans;
}
}

BeanPostProcessorโ€‹

๋นˆ ์ƒ์„ฑ ๊ณผ์ •์— ๊ฐœ์ž…ํ•˜์—ฌ ์ถ”๊ฐ€ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Component
public class TimingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(String beanName, Object bean) {
if (bean.getClass().isAnnotationPresent(Timed.class)) {
return createTimingProxy(bean);
}
return bean;
}
}

Sprout์˜ IoC ์ปจํ…Œ์ด๋„ˆ๋Š” Spring๊ณผ ์œ ์‚ฌํ•˜์ง€๋งŒ ๋” ๋‹จ์ˆœํ•˜๊ณ  ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋กœ ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒ์„ฑ์ž ์ฃผ์ž…๋งŒ์„ ์ง€์›ํ•˜๊ณ , ๋ช…ํ™•ํ•œ ๋นˆ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ์ œ๊ณตํ•˜์—ฌ ๋””๋ฒ„๊น…๊ณผ ์ดํ•ด๊ฐ€ ์‰ฌ์šด ๊ฒƒ์ด ํŠน์ง•์ž…๋‹ˆ๋‹ค.