Backend
[Backend] 초보 개발자를 위한 자바·스프링 핵심 가이드: DI/IoC와 Factory 이해
· 8분 읽기
📑 목차
- 1) 자바 기본 용어
- 2) OOP(객체지향) 핵심
- 3) Factory 패턴 (생성 책임 분리)
- 4) DI/IoC 개념
- 5) 스프링 매핑
- 6) 어노테이션 → 컨테이너 → 인젝션 (역할 매핑)
- 7) 스프링 빈(Spring Bean)
- 8) 스프링 IoC 컨테이너(ApplicationContext)
- 9) 주입 방식 비교
- 10) Factory vs DI (언제 무엇을?)
- 11) 마무리
1) 자바 기본 용어
- 클래스(Class): 객체를 만들기 위한 설계도
- 객체(Object, 인스턴스): 클래스로부터 생성된 실체
- 생성자(Constructor): 객체 생성 시 1회 호출되는 초기화 전용 메서드
- 메서드(Method): 동작(기능)
- Setter: 값 변경·검증에 활용
- 인터페이스(Interface): 규약/계약 정의(구현 클래스는 이를 반드시 충족)
public class Car {
String color;
int speed;
void run() { System.out.println("달립니다!"); }
}
// 사용
Car myCar = new Car();2) OOP(객체지향) 핵심
- 추상화: 필요한 인터페이스만 보이게 설계
- 캡슐화: 내부 상태 보호, Setter/검증으로 일관성 유지
- 단일 책임 원칙(SRP): 클래스는 한 가지 역할에 집중
- 다형성: 인터페이스로 호출 → 구현을 바꿔도 사용 코드는 동일
public interface Book { int calculatePrice(); }
public class NoDiscountBook implements Book {
public int calculatePrice() { return 12000; }
}3) Factory 패턴 (생성 책임 분리)
문제
핵심 로직이 new 구현체()를 직접 만들면 구현 교체가 어렵고 테스트가 불편.
해법
“무엇을 만들지”에 대한 분기/정책을 Factory에 집중.
public class PeriodDiscountBook implements Book {
public int calculatePrice(){ return 10000; }
}
public class PercentDiscountBook implements Book {
public int calculatePrice(){ return 9000; }
}
enum DiscountType { PERIOD, PERCENT, NONE }
public class BookFactory {
public static Book create(DiscountType type) {
return switch (type) {
case PERIOD -> new PeriodDiscountBook();
case PERCENT -> new PercentDiscountBook();
default -> () -> 12000; // NoDiscount (람다)
};
}
}
// 사용
public class Manager {
public void printPrice(DiscountType type) {
Book book = BookFactory.create(type);
System.out.println(book.calculatePrice());
}
}요약: Factory는 “무엇을 만들지”의 생성 정책/분기를 한 곳에 모아 유연성과 테스트 용이성을 높인다.
4) DI/IoC 개념
- IoC(Inversion of Control): 객체 생성·연결·수명 관리의 주도권을 애플리케이션 코드가 아닌 컨테이너가 가진다.
- DI(Dependency Injection): 의존 객체를 **외부(컨테이너)**가 주입하는 방식(= IoC 구현 기법).


DI의 본질(핵심 흐름)
[Injector] --injects--> [Client] --uses--> [Service]- Injector: 의존성 생성/초기화/선정/관리
- Service: 기능 제공 구현체
- Client: 주입받은 Service를 사용
순수 자바 예시
public interface MessageService { void sendMessage(String msg); }
public class EmailService implements MessageService {
public void sendMessage(String msg) { System.out.println("이메일: " + msg); }
}
public class NotificationClient {
private final MessageService service;
public NotificationClient(MessageService service) { this.service = service; } // 생성자 주입
public void notify(String msg) { service.sendMessage(msg); }
}
public class AppInjector {
public static void main(String[] args) {
MessageService svc = new EmailService(); // Service 생성
NotificationClient client = new NotificationClient(svc); // 주입
client.notify("안녕하세요!");
}
}한 줄 정리: DI는 생성(Injector), 제공(Service), **사용(Client)**을 분리해 변경과 테스트를 쉽게 한다.
5) 스프링 매핑
- Injector = ApplicationContext (스프링 IoC 컨테이너)
- Service/Client = 스프링 빈(Bean)
- 주입 방식 권장도: 생성자 주입 ▶ 세터(선택 의존) ▶ 필드(지양)
public interface MessageService { void send(String to, String msg); }
@Service
class EmailMessageService implements MessageService {
public void send(String to, String msg) { /* 이메일 발송 */ }
}
@Service // Client
@RequiredArgsConstructor
class NotificationClient {
private final MessageService messageService; // 인터페이스 의존, 생성자 주입
public void notify(String to, String msg) { messageService.send(to, msg); }
}
// 구현이 여러 개라면 @Primary 또는 @Qualifier("beanName")6) 어노테이션 → 컨테이너 → 인젝션 (역할 매핑)
<table style="width: 100%;" border="1" data-ke-align="alignLeft"><tbody><tr><td><b>컨테이너 부팅/설정</b></td><td>@SpringBootApplication</td><td>부팅 + 컴포넌트 스캔 + 자동설정</td></tr><tr><td> </td><td>@Configuration, @ComponentScan</td><td>자바 설정/스캔 범위 지정</td></tr><tr><td><b>빈 등록(제공자)</b></td><td>@Component · @Service · @Repository · @Controller</td><td>클래스를 <b>빈</b>으로 등록</td></tr><tr><td> </td><td>@Bean (메서드)</td><td>수동 빈 등록(복잡 생성/외부 라이브러리)</td></tr><tr><td><b>주입(소비자)</b></td><td>@Autowired (단일 생성자면 생략 가능)</td><td>의존성 주입(생성자/세터/필드)</td></tr><tr><td><b>충돌 해결</b></td><td>@Primary, @Qualifier("name")</td><td>동일 타입 다수 빈 중 선택/우선순위</td></tr><tr><td><b>스코프/수명</b></td><td>@Scope(<code>"singleton"</code> | <code>"prototype"</code> | <code>"request"</code> | <code>"session"</code>)</td><td>빈 수명/개수 규칙 지정</td></tr><tr><td><b>라이프사이클</b></td><td>@PostConstruct, @PreDestroy</td><td>초기화/소멸 콜백</td></tr><tr><td><b>설정값</b></td><td>@Value, @ConfigurationProperties</td><td>환경·프로퍼티 주입</td></tr></tbody></table>
포인트: 어노테이션은 선언이고, **실제 주입은 컨테이너(ApplicationContext)**가 수행한다.
7) 스프링 빈(Spring Bean)
등록
- 자동 등록: @Component 계열 + 컴포넌트 스캔
- 수동 등록: @Configuration + @Bean
스코프(개수·수명)
<table style="width: 100%;" border="1" data-ke-align="alignLeft"><tbody><tr><td><b>singleton(기본)</b></td><td>컨테이너 시작</td><td>앱 전체</td><td>대부분의 서비스·리포지토리</td></tr><tr><td><b>prototype</b></td><td>매 주입/조회</td><td>생성만 컨테이너 관리(소멸은 개발자)</td><td>짧게 쓰고 버리는 객체</td></tr><tr><td><b>request</b></td><td>HTTP 요청마다</td><td>요청~응답</td><td>요청별 상태</td></tr><tr><td><b>session</b></td><td>세션마다</td><td>로그인~만료</td><td>사용자 상태</td></tr></tbody></table>
@RequiredArgsConstructor
class UseProtoService {
private final ObjectProvider<TempCalculator> calcProvider;
public int run() {
TempCalculator c = calcProvider.getObject(); // prototype 매번 새 인스턴스
return c.calculate();
}
}라이프사이클
- @PostConstruct 초기화, @PreDestroy 소멸
- @Bean(initMethod="init", destroyMethod="close")로 지정 가능
8) 스프링 IoC 컨테이너(ApplicationContext)
- 역할: 빈 검색/생성/초기화/주입/소멸, 환경/프로퍼티, 이벤트, AOP 연계
- 구성:
- 컴포넌트 스캔
- 자바 설정(@Configuration/@Bean)
- 프로파일(@Profile("dev"))로 환경별 빈 분기
- 주의: 컨테이너 밖에서
new하면 AOP/트랜잭션/주입이 깨진다.
@Configuration
@ComponentScan("com.example")
class AppConfig { }
public class Main {
public static void main(String[] args) {
try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
var client = ctx.getBean(NotificationClient.class);
client.notify("kim@ex.com", "안녕하세요!");
}
}
}9) 주입 방식 비교
<table style="width: 100%;" border="1" data-ke-align="alignLeft"><tbody><tr><td><b>생성자 주입</b></td><td>불변성, 테스트·리팩토링 유리, 순환참조 조기 감지</td><td>생성자 길어질 수 있음</td><td>⭐ 기본</td></tr><tr><td><b>세터 주입</b></td><td>선택 의존/런타임 교체 용이</td><td>null 위험, 불변성 약화</td><td>△ 조건부</td></tr><tr><td><b>필드 주입</b></td><td>코드 짧음</td><td>숨은 의존성, 테스트/리팩토링 불편</td><td>❌ 지양</td></tr></tbody></table>
보일러플레이트는 Lombok @RequiredArgsConstructor 사용.
10) Factory vs DI (언제 무엇을?)
<table style="width: 100%;" border="1" data-ke-align="alignLeft"><tbody><tr><td>프레임워크 없는 순수 자바에서 <b>생성 정책/분기</b> 관리</td><td><b>Factory</b></td></tr><tr><td>스프링에서 <b>생성·주입·수명·AOP</b>를 일관 관리</td><td><b>DI</b></td></tr></tbody></table>
함께 사용: 전반은 DI, 복잡한 생성 분기는 Factory로 캡슐화.
11) 마무리
- 자바 기본 → OOP → Factory → DI/IoC → 스프링 매핑·빈·컨테이너
- 실무 기본값은 DI(생성자 주입), 복잡한 생성 정책은 Factory로 보완
- 유지보수 핵심: 불변 객체 + 책임 분리
댓글
GitHub 계정으로 댓글을 남길 수 있어요.