본문으로 건너뛰기
최서희Frontend Engineer
← 블로그

Backend

[Backend] 초보 개발자를 위한 자바·스프링 핵심 가이드: DI/IoC와 Factory 이해

· 8분 읽기

📑 목차


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
DI

Spring IOC 컨테이너
Spring 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 계정으로 댓글을 남길 수 있어요.