C
Spring Boot/Core Concepts/Lesson 04

IoC · DI · Bean — The *Heart* of Spring

45 min·theory

IoC · DI · Bean — The *Heart* of Spring

🎯 What You'll Be Able to Do After This Lesson

After finishing this lesson, you'll be able to confidently handle the following three things.

  • How @Autowired injects Beans (3 approaches)
  • ✅ Resolving Bean conflicts of the same type with @Qualifier / @Primary
  • ✅ Registering external library classes via @Configuration + @Bean

Keep these learning goals as a checklist — once you can answer all of them, close the lesson.

IoC (Inversion of Control) — *Receive, Don't Create*

Core Idea

IoC (Inversion of Control) = instead of creating objects yourself, you let the framework create them and hand them to you. The name reflects that the flow of control is inverted.

The Old Way — Creating Everything Yourself

java
public class OrderService {
    private UserRepository userRepo = new UserRepository();     // Created directly
    private EmailService email = new EmailService();             // Created directly

    public void order(Long userId) {
        User u = userRepo.findById(userId);
        email.send(u.email(), "Order complete");
    }
}

It looks clean on the surface, but there are serious problems:

  • Hard to test: You only want to test OrderService, but the real EmailService runs with it — actual emails get sent
  • Hard to change: If EmailService is swapped for SmsService, every call site must be updated
  • Hard to trace dependencies: No way to track who creates whom

The New Way — Receive and Use

java
@Service
public class OrderService {
    private final UserRepository userRepo;
    private final EmailService email;

    public OrderService(UserRepository userRepo, EmailService email) {
        this.userRepo = userRepo;        // Spring injects it
        this.email = email;
    }

    public void order(Long userId) {
        User u = userRepo.findById(userId);
        email.send(u.email(), "Order complete");
    }
}

Now OrderService doesn't care where its dependencies came from — it just receives and uses them. Who creates them is Spring's responsibility.

That's IoC. The control over object creation has been handed to the framework.

Why This Is Better

1. Easier to test:

java
@Test
void order_test() {
    EmailService fakeEmail = new MockEmailService();   // Fake
    OrderService svc = new OrderService(userRepo, fakeEmail);
    svc.order(42L);
    // Verify fakeEmail call — no actual email sent
}

2. Easy to swap implementations: To replace EmailService with KakaoMessageService, change one line in the Spring configuration. Calling code stays the same.

3. Dependencies are explicit: Just reading the constructor signature tells you exactly what the class needs.

The IoC Container — Spring's Factory

The core component of Spring that creates, stores, and wires objects together is called the IoC container, or ApplicationContext.

How it works:
1. On startup, Spring scans all classes annotated with @Component, @Service, @Repository, and @Controller
2. It creates one instance of each discovered class and stores it in the container
3. When another component needs one of those objects, Spring injects it via constructor or @Autowired

> 💡 Objects stored in the container are called Beans — a metaphor for small unit objects, like beans.

Quick Summary

  • IoC = delegating object creation to the framework
  • Benefits = testing, swapping, and tracing all become easier
  • Spring container = the factory that creates and connects Beans

This is the most fundamental idea in Spring. DI, AOP, and transactions all operate on top of IoC.

DI (Dependency Injection) — *Three Approaches*

The Relationship Between DI and IoC

IoC is the broader concept, and DI (Dependency Injection) is one of its concrete implementations. It is the specific technique of injecting the dependencies an object needs from the outside.

3 Injection Approaches

1. Constructor Injection (Recommended)

java
@Service
public class OrderService {
    private final UserRepository userRepo;

    public OrderService(UserRepository userRepo) {
        this.userRepo = userRepo;
    }
}

Advantages:

  • Allows final → guarantees immutability
  • All dependencies are guaranteed at object creation time
  • Circular dependencies are caught at compile time
  • Easy to inject mock objects in tests

As of Spring 4.3+, @Autowired can be omitted. Combined with Lombok's @RequiredArgsConstructor, it becomes even more concise:

java
@Service
@RequiredArgsConstructor
public class OrderService {
    private final UserRepository userRepo;
    private final EmailService email;
    // Constructor auto-generated
}

2. Setter Injection

java
@Service
public class OrderService {
    private UserRepository userRepo;

    @Autowired
    public void setUserRepo(UserRepository userRepo) {
        this.userRepo = userRepo;
    }
}

Advantage: Can express optional dependencies (may or may not be present)
Disadvantage: Cannot use final. Null if setter is never called. Rarely used.

3. Field Injection (Discouraged)

java
@Service
public class OrderService {
    @Autowired private UserRepository userRepo;       // ❌
}

Looks concise but:

  • Hard to test (injection only possible via Reflection)
  • Circular dependencies unknown until runtime
  • Dependencies can be added carelessly because it's too convenient

> 💡 Industry consensus: Always use constructor injection. Field injection is only seen in legacy code.

When There Are Multiple Beans of the Same Type

java
@Service public class EmailService implements MessageService { }
@Service public class SmsService   implements MessageService { }

@Service
public class OrderService {
    public OrderService(MessageService msg) { }   // ❌ ambiguous
}

Spring doesn't know which one to inject. Three solutions:

1. @Primary — designate a default:

java
@Service
@Primary
public class EmailService implements MessageService { }

2. @Qualifier — explicit selection:

java
public OrderService(@Qualifier("emailService") MessageService msg) { }

3. Receive as a List — get all implementations:

java
public OrderService(List<MessageService> all) {
    // All MessageService implementations
}

Common Pitfalls

Circular dependency: A injects B, and B injects A. With constructor injection, this causes an error at startup — a signal that your design is wrong. The fix is to extract the shared part into a new class.

@Autowired vs @Resource vs @Inject: Knowing Spring's @Autowired is sufficient. The other two are Java standards but are rarely used in practice.

Quick Summary

  • Constructor injection is the standard. Combine with Lombok's @RequiredArgsConstructor
  • When multiple Beans of the same type exist, use @Primary or @Qualifier
  • Circular dependencies are a design signal — refactoring is needed

Bean — *Objects Managed by Spring*

What Is a Bean?

A Bean is an object registered and managed by the Spring IoC container. Think of it like a small-unit object among a collection of beans. What distinguishes it from an ordinary Java object is that Spring manages its creation, lifecycle, and injection entirely.

Ways to Register a Bean

1. @Component Family (Most Common)

java
@Component        // General
@Service          // Business logic
@Repository       // DB access
@Controller       // Web controller
@RestController   // REST API

The names differ, but they all register a Bean at their core. They serve as semantic distinctions with some behavioral differences (@Repository provides exception translation).

java
@Service
public class UserService {
    // Automatically registered as a Bean
}

2. @Bean (Method-Level)

The return value of a method in a configuration class becomes a Bean:

java
@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}

Used to register classes from external libraries (RestTemplate, ObjectMapper, etc.) as Beans — classes you can't annotate yourself.

3. Auto-configuration — Spring Boot Starters

Starter dependencies like spring-boot-starter-data-jpa automatically register many Beans (DataSource, EntityManager, TransactionManager, etc.).

Bean Scope — When Is a New Instance Created?

The default is singleton — only one instance per container, shared by all. But other options exist.

ScopeMeaningUse Case
singleton (default)1 per containerAlmost all cases
prototypeNew object per requestStateful objects
request1 per HTTP requestRequest-scoped state
session1 per HTTP sessionSession-scoped state
application1 per ServletContextGlobal
java
@Service
@Scope("prototype")
public class StatefulProcessor { }

Most common pitfall: Keeping mutable fields in a singleton Bean. In a multi-threaded environment, this causes data corruption. Always use immutable state or an external store (DB, Redis).

Bean Lifecycle

A Bean goes through the following stages:

1. Instantiation (constructor call)
2. Dependency injection (@Autowired, etc.)
3. Initialization (@PostConstruct or InitializingBean.afterPropertiesSet())
4. In use
5. Destruction (@PreDestroy or DisposableBean.destroy())

java
@Service
public class CacheManager {
    @PostConstruct
    public void init() {
        // Executed once after Bean creation. Cache loading, etc.
    }

    @PreDestroy
    public void cleanup() {
        // Executed once on app shutdown. Resource cleanup
    }
}

@PostConstruct is used frequently — for preparing resources, warming caches, etc. @PreDestroy is important for graceful shutdown.

@Configuration vs @Component

Both are registered as Beans, but they serve different roles.

  • @Configuration — a configuration class. Its @Bean methods are wrapped in a proxy to guarantee the same instance is returned
  • @Component — a regular Bean

When @Bean methods inside a @Configuration class call each other, they return the same instance. With @Component, there's a risk of a new object being created each time.

Quick Summary

  • Bean = an object managed by Spring
  • Register via @Service and similar annotations, or a @Bean method
  • Default scope is singleton (appropriate in most cases)
  • Lifecycle hooks are available via @PostConstruct and @PreDestroy

@Bean · @Configuration · Resolving @Qualifier · @Primary Conflicts

@Component vs @Bean — When to Use Which

  • @Component (including @Service, @Repository, @Controller) — for classes you write yourself. Spring scans them automatically.
  • @Bean — when you need to register external library objects or conditionally.

@Configuration + @Bean Example

java
@Configuration
public class AppConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
            .setConnectTimeout(Duration.ofSeconds(3))
            .setReadTimeout(Duration.ofSeconds(5))
            .build();
    }

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

External library classes (RestTemplate, ObjectMapper) can't be annotated directly, so they're registered with @Bean.

Two Beans of the Same Type — Conflict

java
@Bean PaymentService cardPayment() { ... }
@Bean PaymentService kakaoPayment() { ... }

@Autowired
PaymentService paymentService;   // ❌ NoUniqueBeanDefinitionException

Multiple Beans of the same type → Spring doesn't know which one to inject.

Solution 1 — Specify a Name with @Qualifier

java
@Autowired
@Qualifier("kakaoPayment")
PaymentService paymentService;

The Bean name defaults to the method name (cardPayment, kakaoPayment).

Solution 2 — Set a Default with @Primary

java
@Bean @Primary
PaymentService cardPayment() { ... }   // Default value

@Bean
PaymentService kakaoPayment() { ... }

@Autowired PaymentService p;   // ✅ cardPayment injected (Primary)

Which One to Use?

  • A clear default/primary implementation@Primary
  • Inject different ones depending on context@Qualifier
  • Auto-matching by name without either is also possible (@Autowired PaymentService cardPayment;)

Conditional Registration — @ConditionalOnProperty

java
@Bean
@ConditionalOnProperty(name = "payment.provider", havingValue = "kakao")
public PaymentService kakaoPayment() { ... }

The Bean is only registered when payment.provider=kakao is set in application.yml. This cleanly separates different implementations per profile.

🤖 Try Asking AI This Way

Knowing the concepts from this lesson lets you give AI specific, precise instructions. Instead of a vague "fix this," you can make requests with vocabulary — that's the starting point for saving tokens.

  • "There are two implementations of this PaymentService — branch them using @Qualifier"
  • "Register this RestTemplate as a Bean using @Configuration + @Bean"
  • "Conditionally activate the Kakao payment module using @ConditionalOnProperty"

Why This Saves Tokens

Without understanding the concepts, you have to follow up every AI answer with "what does that mean?" — and that follow-up costs tokens. Learn the concept once, and the conversation wraps up in a single exchange.

IoC · DI · Bean — The Heart of Spring - Spring Boot