내일배움캠프

[내일배움캠프] IoC, DI, Bean

munsik22 2026. 4. 6. 19:49
 

스파르타클럽 | AI시대, 미래를 돌파하는 힘

누구나 잠재력을 깨워 나아가도록. IT 커리어의 모든 성장 과정을 스파르타클럽에서

academia.spartaclub.kr

🧩 3 Layer Architecture

현재 메모장 프로젝트의 문제점

  • Controller 클래스 하나로 모든 API를 처리하고 있음
  • 현재는 API 수가 적고 기능이 단순하여 코드가 복잡해 보이지 않을 수 있지만 앞으로 기능이 추가되고 복잡해진다면 문제가 발생할 수 있음
    • 한 개의 클래스에 너무 많은 양의 코드가 존재하기 때문에 코드를 이해하기 어려움
    • 현업에서는 코드의 추가 혹은 변경 요청이 계속 생길 수 있음
    • 문제가 발생했는데 해당 Controller 클래스를 구현한 개발자가 퇴사한다면???

Spring의 3 Layer Architecture

  1. Controller
    • 클라이언트의 요청을 받음
    • 요청에 대한 로직 처리는 Service에게 전달함
    • Request 데이터가 있으면 Service에 같이 전달함
    • Serivce에서 처리 완료된 결과를 클라이언트에게 응답함
  2. Service
    • 사용자의 요구사항을 처리하는 비즈니스 로직
    • 현업에서는 계속해서 비대해지는 부분
    • DB 저장 및 조회가 필요할 때는 Repository에게 요청함
  3. Repository
    • DB 관리 및 CRUD 작업을 처리함

역할 분리하기

  • Controller → Service 분리: MemoController, MemoService
  • Service → Repository 분리: MemoService, MemoRepository

🧩 IoC(제어의 역전)와 DI(의존성 주입)

Spring의 IoC와 DI

  • IoC, DI는 객체지향의 SOLID 원칙과 GoF의 디자인 패턴과 같은 설계 원칙 및 디자인 패턴
  • IoC는 설계 원칙에 해당 (예: 김치 볶음밤을 맛있게 만드는 방법)
  • DI는 디자인 패턴에 해당 (예: 김치 볶음밥 레시피)

의존성

public class Consumer {

    void eat() {
        Chicken chicken = new Chicken();
        chicken.eat();
    }

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        consumer.eat();
    }
}

class Chicken {
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}
  • 위 코드에서 Consumer와 Chicken은 강하게 결합되어 있음
  • Consumer가 치킨이 아니라 피자가 먹고 싶다면 많은 수의 코드 변경이 필요함
  • 이러한 관계를 강한 결합 및 강한 의존성이라고 함
public class Consumer {

    void eat(Food food) {
        food.eat();
    }

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        consumer.eat(new Chicken());
        consumer.eat(new Pizza());
    }
}

interface Food {
    void eat();
}

class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }
}

 

  • Java의 Interface를 활용하면 이를 해결할 수 있음
  • Interface 다형성의 원리를 사용해서 구현하면 고객이 어떤 음식을 요구해도 쉽게 대처 가능함
  • 이러한 관계를 약한 결합 및 약한 의존성이라고 함

주입

  • 여러 방법을 통해 필요로 하는 객체를 해당 객체에 전달하는 것
  • 필드에 직접 주입: Food를 Consumer에 포함시킴
public class Consumer {

    Food food;

    void eat() {
        this.food.eat();
    }

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        consumer.food = new Chicken();
        consumer.eat();

        consumer.food = new Pizza();
        consumer.eat();
    }
}

interface Food {
    void eat();
}

class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }
}
  • 메서드를 통한 주입: set 메서드 사용
public class Consumer {

    Food food;

    void eat() {
        this.food.eat();
    }

    public void setFood(Food food) {
        this.food = food;
    }

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        consumer.setFood(new Chicken());
        consumer.eat();

        consumer.setFood(new Pizza());
        consumer.eat();
    }
}

interface Food {
    void eat();
}

class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }
}

 

  • 생성자를 통한 주입
public class Consumer {

    Food food;

    public Consumer(Food food) {
        this.food = food;
    }

    void eat() {
        this.food.eat();
    }

    public static void main(String[] args) {
        Consumer consumer = new Consumer(new Chicken());
        consumer.eat();

        consumer = new Consumer(new Pizza());
        consumer.eat();
    }
}

interface Food {
    void eat();
}

class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }
}

 

제어의 역전

  • 이전에는 Consumer가 직접 Food를 만들어 먹었기 때문에 새로운 Food를 만들려면 추가적인 코드 변경이 불가피했음
    • 제어의 흐름: Consumer → Food
  • 이를 해결하기 위해 만들어진 Food를 Consumer에게 전달해주는 식으로 변경함으로써 Consumer는 추가적인 코드 변경 없이 어느 Food가 되었든지 전부 먹을 수 있게 되었음
    • 제어의 흐름: Food → Consumer으로 역전됨

메모장 프로젝트의 IoC와 DI

  • MemoService: 객체 중복 생성 문제 해결
/* 수정 전 */
public class MemoService {

    private final JdbcTemplate jdbcTemplate;

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

    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public List<MemoResponseDto> getMemos() {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long updateMemo(Long id, MemoRequestDto requestDto) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long deleteMemo(Long id) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }
}
/* 수정 후 */
public class MemoService {
   
    private final MemoRepository memoRepository;

    public MemoService(JdbcTemplate jdbcTemplate) {
        this.memoRepository = new MemoRepository(jdbcTemplate);
    }
    
    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        Memo memo = new Memo(requestDto);

        Memo saveMemo = memoRepository.save(requestDto);

        MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);

        return memoResponseDto;
    }
	...
}
  • 제어의 흐름: Controller → Service → Repository (강한 결합)
    • Controller가 Service를 직접 만들고 Service도 Repository를 직접 만듦
    • Repository1 생성자 변경에 의해 모든 Controller와 모든 Service의 코드 변경이 필요함
  • 강한 결합 해결책
    • 각 객체에 대한 객체 생성은 1번씩만
    • 생성된 객체를 모든 곳에서 재사용
    • 생성자 주입을 사용해 필요로 하는 객체 주입
  • MemoController 수정: 만들어진 memoService를 받아오도록
public class MemoController {

    private final MemoService memoService;

    public MemoController(MemoService memoService) {
        this.memoService = memoService;
    }
    
    /* ... */
}
  • MemoService 수정: 만들어진 memoRepository를 받아오도록
public class MemoService {

    private final MemoRepository memoRepository;

    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }
    
    /* ... */
}
  • 외부에서 미리 만든 객체를 주입하는 것이 DI 패턴이라고 이해했고 그렇게 코드를 수정했는데, 그렇다면 도대체 MemoRepository 등의 객체들은 언제 어디서 누가 만들어주고 넣어주는 거지?

🧩 IoC Container와 Bean

  • DI를 사용하기 위해서는 객체 생성이 우선 되어야 한다. 그렇다면 언제 어디서 누가 객체를 생성하는가?
    👉 Spring 프레임워크가 필요한 객체를 생성하고 관리하는 역할을 대신 해준다.
    • Bean: Spring이 관리하는 객체
    • Spring IoC Container: Bean을 모아둔 컨테이너

Spring Bean 등록 방법

  • @Component
@Component
public class MemoService {
    /* ... */
}
@Component
public class MemoRepository {
    /* ... */
}
  • @ComponentScan: Spring Boot 애플리케이션이 자체적으로 @Component 애노테이션이 붙은 클래스들을 Bean으로 등록한다.

기본적으로 ComponentScan이 설정되어 있다

  • 코드 좌측에 초록색 커피콩 아이콘이 있다면 Bean으로 등록이 되었다는 뜻이다.

여태까지 🚫인줄 알았는데...

  • Bean을 사용하려면 @Autowired을 달아줘야 한다.
    • Spring 4.3 버전부터 생성자가 하나만 선언한 경우 생략 가능함
    • 주입이 잘 된 경우 커피콩 아이콘 위에 화살표가 붙음

  • 메서드로 주입하는 방법
    • memoRepository가 final이 될 수 없고, @Autowired 애노테이션을 반드시 붙여야 Bean으로 사용 가능함
    • private임에도 불구하고 Spring에 의해 외부에서 Bean 객체를 주입 가능함
    • 하지만 객체 불변성을 지키기 위해 현업에서는 거의 생성자 주입 방식을 사용함
@Component
public class MemoService {

    private MemoRepository memoRepository;

    // final 사용 불가
    @Autowired
    public void setDi(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }
    
    /* ... */
}
  • Lombok 사용: @RequiredArgsConstructor가 final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성함
@Component
@RequiredArgsConstructor
public class MemoService {

    private final MemoRepository memoRepository;

    /* ... */

}
  • @Autowired는 Spring IoC Container에 의해서 관리되고 있는 Bean class에서만 사용 가능함

ApplicationContext

  • BeanFactory 등을 상속하여 기능을 확장한 Container
  • BeanFactory: Bean의 생성, 관계 설정 등의 제어를 담당하는 IoC 객체
  • 스프링 IoC 컨테이너에서 Bean을 수동으로 가져오는 방법
@Component
public class MemoService {

    private final MemoRepository memoRepository;

    public MemoService(ApplicationContext context) {
        // 1.'Bean' 이름으로 가져오기
        MemoRepository memoRepository = (MemoRepository) context.getBean("memoRepository");

        // 2.'Bean' 클래스 형식으로 가져오기
        // MemoRepository memoRepository = context.getBean(MemoRepository.class);

        this.memoRepository = memoRepository;
    }

    /* ... */		
}

3 Layer Annotation

  • Controller, Service, Repository의 역할로 구분된 클래스들을 Bean으로 등록할 때 해당 Bean 클래스의 역할을 명시하기 위해 사용됨
    • @Controller, @RestController
    • @Service
    • @Repository