
* 개념 복습과 학습 정도를 파악하고자 포스팅합니다!
* Claude, ChatGPT를 활용하여 이미지를 생성하고 활용합니다.
IoC/DI 코드로 실습하며 파헤치기
1. 문제 상황
시나리오: 알림 발송 시스템
주문이 완료되면 고객에게 알림을 보내는 서비스를 만들어주세요.
처음엔 이메일로 보내다가, 나중에는 SMS로 바꿔달라는 요청 수신.
2. DI 없는 코드
아래 코드를 보면 문제점이 보입니다.
// 이메일 발송 클래스
public class EmailNotifier {
public void send(String message) {
System.out.println("[EMAIL] " + message);
}
}
// 주문 서비스 — EmailNotifier를 직접 생성
public class OrderService {
// 문제 1: new로 직접 생성 → OrderService가 EmailNotifier에 강하게 묶임
private EmailNotifier notifier = new EmailNotifier();
public void order(String item) {
System.out.println(item + " 주문 완료");
notifier.send(item + " 주문 알림"); // 이메일만 보낼 수 있음
}
}
// 실행
public class Main {
public static void main(String[] args) {
OrderService orderService = new OrderService();
orderService.order("노트북");
}
}
[EMAIL] 노트북 주문 알림
해당 코드에서 "SMS로 바꿔주세요"라는 요청이 오면 어떻게 해야 하냐면요..
// SMS 발송 클래스를 새로 만들었다
public class SmsNotifier {
public void send(String message) {
System.out.println("[SMS] " + message);
}
}
// OrderService 내부를 직접 수정해야 함 ← 문제!
public class OrderService {
// private EmailNotifier notifier = new EmailNotifier(); // 이걸 지우고
private SmsNotifier notifier = new SmsNotifier(); // 이걸 직접 바꿔야 함
...
}
이렇게 해야 합니다.
위 코드가 왜 문제가 되냐면, OrderService처럼 EmailNotifer를 쓰는 클래스가 50개라면 50곳을 다 바꿔야 하는 상황이 발생해요.
그리고 바꾸는 과정에서 실수가 생기고, 테스트도 어려워집니다.
이걸 강한 결합(Strong Coupling)이라고 합니다.

3. 순수 Java로 DI 구현하기
Spring없이 DI 구조를 직접 만들어보겠습니다.
Spring이 뒤에서 이걸 대신 해준다는 걸 체감하려면 직접 코드를 짜서 이해해보는 수 밖에 없을 것 같아서요!!
인터페이스로 추상화하기
// Notifier 인터페이스 - "알림을 보낸다"는 행위만 정의
public interface Notifier {
void send(String message);
}
구현체 두 가지
// 이메일 구현체
public class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("[EMAIL] " + message);
}
}
// SMS 구현체
public class SmsNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("[SMS] " + message);
}
}
OrderService: 인터페이스에만 의존
public class OrderService {
private final Notifier notifier; // 인터페이스 타입으로 선언
// 생성자로 외부에서 주입받음 — 직접 생성 X
public OrderService(Notifier notifier) {
this.notifier = notifier;
}
public void order(String item) {
System.out.println(item + " 주문 완료");
notifier.send(item + " 주문 알림");
}
}
외부에서 조립(DI)
public class Main {
public static void main(String[] args) {
// 이메일로 주입
Notifier emailNotifier = new EmailNotifier();
OrderService orderService = new OrderService(emailNotifier);
orderService.order("노트북");
System.out.println("---");
// SMS로 교체 — OrderService 코드는 한 줄도 안 바꿈
Notifier smsNotifier = new SmsNotifier();
OrderService orderService2 = new OrderService(smsNotifier);
orderService2.order("마우스");
}
}
OrderService는 한 줄도 바꾸지 않고 동작이 바뀌게됩니다. 이게 DI입니다!!
그런데 지금 Main 클래스에서 객체를 직접 만들고 연결하고 있습니다. 실제 서비스에선 클래스가 수십, 수백 개가 될텐데요.
이 조립을 Spring이 대신 해주는거죠!
4. Spring으로 전환하기
이제 Spring에게 조립을 맡겨볼건데요. new와 생성자 호출을 걷어내기만 하면 됩니다!
구현체에 @Component 붙이기
// EmailNotifier — Spring Bean으로 등록
@Component
public class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("[EMAIL] " + message);
}
}
// SmsNotifier — 일단 주석처리 (두 개 동시 등록하면 충돌)
// @Component
public class SmsNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("[SMS] " + message);
}
}
OrderService: @Service로 Bean 등록, 생성자 주입
@Service
public class OrderService {
private final Notifier notifier;
// Spring이 Notifier 타입 Bean을 찾아서 자동으로 주입
// 생성자가 하나면 @Autowired 생략 가능 (Spring 4.3 이상부터)
public OrderService(Notifier notifier) {
this.notifier = notifier;
}
public void order(String item) {
System.out.println(item + " 주문 완료");
notifier.send(item + " 주문 알림");
}
}
실행 확인
@SpringBootApplication
public class NotifierApplication {
public static void main(String[] args) {
// ApplicationContext = IoC 컨테이너
ApplicationContext ctx =
SpringApplication.run(NotifierApplication.class, args);
// 컨테이너에서 Bean 꺼내기
OrderService orderService = ctx.getBean(OrderService.class);
orderService.order("노트북");
}
}
노트북 주문 완료
[EMAIL] 노트북 주문 알림
new가 단 하나도 없죠?
Spring이 EmailNotifer를 만들고, OrderService를 만들고, 둘을 연결까지 해줬습니다.
5. 구현체 교체하기
문제 상황: 구현체가 두 개라면?
@Component
public class EmailNotifier implements Notifier { ... }
@Component
public class SmsNotifier implements Notifier { ... }
둘다 @Component면 Spring이 어느 걸 주입해야 할지 몰라서 아래와 같은 오류가 납니다.
NoUniqueBeanDefinitionException:
expected single matching bean but found 2: emailNotifier, smsNotifier
위 문제를 해결할 방법이 두 가지가 있습니다.
방법1 : @Primary - 기본값 지정
@Component
@Primary // 같은 타입 Bean이 여러 개일 때 이게 기본값
public class EmailNotifier implements Notifier {
...
}
@Component
public class SmsNotifier implements Notifier {
...
}
// OrderService는 그대로 — EmailNotifier가 자동 주입됨
방법2: @Qualifier - 주입할 Bean 직접 지정
@Component("emailNotifier") // Bean 이름 명시
public class EmailNotifier implements Notifier { ... }
@Component("smsNotifier")
public class SmsNotifier implements Notifier { ... }
@Service
public class OrderService {
private final Notifier notifier;
public OrderService(@Qualifier("smsNotifier") Notifier notifier) {
this.notifier = notifier; // SmsNotifier가 주입됨
}
}

6. 실전에서 사용하는 방식
실제 Spring Boot 프로젝트에서 주로 어떤식으로 사용하는지 알기 위해서 Controller → Service → Repository 3계층이 DI로 연결되는 코드를 작성해보겠습니다.
// 1. Repository — DB 접근
@Repository
public class UserRepository {
public String findNameById(Long id) {
return "홍길동"; // 실제론 JPA같은 ORM사용
}
}
// 2. Service — 비즈니스 로직, UserRepository 주입받음
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository; // Spring이 주입
}
public String getUserName(Long id) {
return userRepository.findNameById(id);
}
}
// 3. Controller — 요청 처리, UserService 주입받음
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService; // Spring이 주입
}
@GetMapping("/{id}")
public String getUser(@PathVariable Long id) {
return userService.getUserName(id);
}
}
위처럼 개발자는 @Service, @Repository 등 어노테이션만 붙여주면 Spring이 모든 연결을 처리할 수 있게 됩니다.
'Backend > Spring' 카테고리의 다른 글
| [Spring] 간단한 게시판 기능 구현 - 1 (0) | 2026.04.25 |
|---|---|
| [Java] Spring Boot와 Web MVC 파헤치기 (0) | 2026.04.25 |
| [Spring] AOP 원리와 코드로 실습하며 파헤치기 (0) | 2026.04.25 |
| [Spring] Spring 기본 동작 원리 파헤치기 (0) | 2026.04.24 |
| [Spring] Spring, Spring Boot는 무엇일까? (0) | 2026.04.24 |