예시를 통해 알아보는 Spring Boot의 의존성 주입(DI)

2025. 1. 19. 13:26·Spring/기초 개념

의존성 주입( Dependency Injection )

의존성 주입이라는 개념은 사실 어려운 개념은 아니다.

정말 쉽게 말하자면, 필요한 기능을 갖는 객체를 수정자, 생성자 등을 통해 주입받는 것이다.

 

일단 Spring Boot의 의존성 주입(DI)에 대해서 알아보기 전, 의존성 주입 자체에 대해서 알아볼 것이다.

 

의존성 주입의 필요성

 

그렇다면 의존성 주입은 언제, 왜 사용하는 것일까?

의존성 주입은 객체 간 결합을 약하게 하여 상황에 따라 필요한 객체를 주입하여 사용할 수 있도록 해준다.

 

이 글만 보고 이해하는 것은 매우 어려운 일이다. 아래 코드를 통해 그 필요성을 알아보자

import java.util.*;
import java.lang.*;
import java.io.*;

class Dog{
    public void bark(){
        System.out.println("Dog Bark");
    }
}

class Cat{
    public void bark(){
        System.out.println("Cat Bark");
    }
}

class DogSound{    
    void printBarkSound(){
        new Dog().bark();
    }
}

class CatSound{    
    void printBarkSound(){
        new Cat().bark();
    }
}

class Main {
    public static void main(String[] args) {
        DogSound ds = new DogSound();
        CatSound cs = new CatSound();
        ds.printBarkSound();
        cs.printBarkSound();
    }
}

위 코드는 서로 다른 종의 동물 클래스를 생성하고, 추가로 각 동물 클래스마다 printBarkSound 메서드를 갖는 클래스를 추가로 생성하고 있다.

이러한 코드는 만약 동물 클래스의 종류가 많아진다면 그 구조를 복잡하게 만들고 매번 코드를 수정해야하는 문제가 발생한다.

 

DI는 이러한 상황에서 채택할 수 있는 수단으로, 객체 간 결합을 약하게 하여 유연성을 확보하고 확장성을 높인다.

그렇다면, 이 DI라는 것은 무엇이며 어떤 방식으로 사용해야 할까

 

의존성 주입의 방법

 

일단 의존성 주입은 아래 세 가지 방법으로 나뉜다.

  • Setter(수정자)를 통한 DI
  • 생성자를 통한 DI
  • 필드 주입을 통한 DI

예시를 통해 하나씩 알아보도록 하자.

Setter를 통한 DI

이 방법은 클래스에 필드를 생성하고, setter를 통해 의존성을 주입받는 방법이다.

 

아래는 기존 코드를 수정자 DI 방식에 맞게 바꾼 것이다.

import java.util.*;
import java.lang.*;
import java.io.*;

interface Animal{
    void bark();
}

class Dog implements Animal{
    public void bark(){
        System.out.println("Dog Bark");
    }
}

class Cat implements Animal{
    public void bark(){
        System.out.println("Cat Bark");
    }
}

class AnimalSound{
    private Animal animal;
    public void setAnimal(Animal animal){
        this.animal = animal;
    }
    void printBarkSound(){
        animal.bark();
    }
}

class Main {
    public static void main(String[] args) {
        AnimalSound as_dog = new AnimalSound();
        AnimalSound as_cat = new AnimalSound();
        as_dog.setAnimal(new Dog());
        as_cat.setAnimal(new Cat());
        as_dog.printBarkSound();
        as_cat.printBarkSound();
    }
}

이 코드에서 주의 깊에 봐야할 점은 생성자를 통해서 관계를 맺는 객체를 주입받는다는 것이다.

 

이를 통해서 아래 효과를 얻는다.

  • 외부 객체를 인터페이스를 통해 주입받기 때문에 클래스 간 의존도를 낮추고 확장성을 챙긴다.
  • 만약 다른 객체와의 관계를 원한다면, 코드 수정 없이 setter로 다른 객체를 전달하면 되기에 개방 폐쇄 법칙에 부합한다.

하지만 의존성 주입을 강제하지 않기에 setter로 Animal가 설정되지 않닸다면 NullPointException이 발생한다.

따라서 주로 생성자를 통한 DI를 많이 사용한다.

생성자를 통한 DI

이번에는 생성자를 통해서 필요한 객체를 주입받는 방식에 대해서 알아본다.

 

기존 코드에서 AnimalSound 클래스가 객체를 받는 방법만 수정했다.

import java.util.*;
import java.lang.*;
import java.io.*;

interface Animal{
    void bark();
}

class Dog implements Animal{
    public void bark(){
        System.out.println("Dog Bark");
    }
}

class Cat implements Animal{
    public void bark(){
        System.out.println("Cat Bark");
    }
}

class AnimalSound{
    private final Animal animal;
    public AnimalSound(Animal animal){
        this.animal = animal;
    }
    void printBarkSound(){
        animal.bark();
    }
}

class Main {
    public static void main(String[] args) {
        AnimalSound as_dog = new AnimalSound(new Dog());
        AnimalSound as_cat = new AnimalSound(new Cat());
        as_dog.printBarkSound();
        as_cat.printBarkSound();
    }
}

이 방식은 기존 수정자 DI의 장점들은 유지하면서 단점들은 보완한다.

  • 의존성 주입을 강제하기에 의존성 주입이 발생하지 않아 생기는 문제를 예방한다.
  • 유일하게 final 키워드를 사용할 수 있다. 따라서 중간에 관계를 맺은 객체가 수정되는 문제를 예방한다.

이러한 이유로 생성자 DI는 가장 선호되는 의존성 주입 방식이다.

필드 주입을 통한 DI

마지막으로 필드 주입을 통한 DI를 알아보자.

일단 이 것을 이해하기 위해서 Spring에서 제공하는 @Autowired 어노테이션이 필요하다.

 

해당 어노테이션은 이름과 타입을 기반으로 필드에 Spring Bean을 주입해주는 방식이다.

@Component
class AnimalSound{
    @Autowired
    private Cat cat;
    
    void printBarkSound(){
        animal.bark();
    }
}

여기서 Spring Bean은 스프링 프레임워크가 관리해주는 객체라고 보면 된다.

사용자가 직접 특정 클래스를 Bean으로 등록할 수 있다.

 

이러한 방식은 코드가 간결해지지만, 단점이 많기에 사용을 자제해야 한다.

  • final 키워드를 사용할 수 없기에 관계를 맺는 객체가 수정될 수 있음
  • 클래스 내부에서 직접 의존성을 관리하기에 객체 간 결합도가 높다
  • 테스트 과정에서 관계를 맺는 객체 대신 테스트용 객체를 주입해야하는 상황이 있는데, 이러한 상황에서 어려움이 발생한다.

따라서 필드 주입 방식은 되도록 지양하는 것이 좋다.

 

Spring Boot의 의존성 주입(DI)과 빈 등록

 

Spring Boot에서는 객체들을 Spring Container가 Spring Bean의 형태로 관리하다.

그렇다면, Spring에서는 어떤 방식으로 Bean이 등록되고 DI를 통해 관계를 형성하는지 알아보자. 

Spring Container와 Spring Bean

일단 먼저 Spring Container와 Spring Bean에 대해서 간단하게 알아보자.

 

Spring Container(컨테이너)는 자바 객체의 생성, 제거를 포함한 전체 생명주기를 관리한다.

그리고 스프링 컨테이너에 의해 관리되는 재사용 가능한 컴포넌트, 즉 객체를 Spring Bean(빈)이라고 부른다.

 

우선 @Configuration 어노테이션을 활용하여 빈을 수동 등록하는 방법에 대해서 알아보자.

@Configuration
public class AppConfig{
	@Bean
	public UserDao userDao(){
		return new UserDao();
	}
	
	@Bean
	public UserService userService(){
		return new UserService(userDao());
	}
}

이 코드는 @Bean이라는 어노테이션을 활용하여 userDao 빈과 userService 빈을 등록하는 과정이다.

정확히는 @Configuration 어노테이션을 활용하여 빈을 수동 등록하는 경우, 메서드 이름이 빈의 이름이 되고 싱글톤 패턴으로 생성되는 것을 보장한다

 

다음으로 아래는 @Component 어노테이션을 활용하여 빈을 등록하는 방법이다.

@Repository
class UserRepository{...}

@Service
class Service1{
	public Service1(UserRepository repo){
		System.out.println(repo);
	}
}

@Service
class Service2{
	public Service2(UserRepository repo){
		System.out.println(repo);
	}
}

 

그런데 이들을 @Component 어노테이션이 없는데 어떻게 빈으로 등록되는 것일까?

Spring Boot는 @Component 어노테이션을 상속받는 어노테이션을 갖는 객체들을 빈으로 등록한다.

여기서 @Repository, @Service, @Configuration 어노테이션은 @Component 어노테이션을 상속받는 어노테이션들이다.

따라서 이들은 빈으로 등록된다.

 

이 외에도 다양한 방법과 용도의 빈 등록 및 관리 방법이 있으니 추후 알아보도록 하자.

컴포넌트 스캔을 통한 빈 등록

그렇다면 이렇게 등록된 빈을 Spring Boot에서는 어떻게 인지할까?

결론부터 말하자면, @ComponentScan의 basePackages에 설정한 하위 패키지들 중 @Component 어노테이션을 갖는 객체들을 빈으로 등록한다.

 

그런데 처음 Spring Boot 프로젝트에 들어가보면, @ComponentScan 어노테이션은 찾을 수 없고 대신 @SpringBootApplication 어노테이션이 있음을 확인할 수 있다.

@SpringBootApplication
public class OnlinecompilerApplication {
	public static void main(String[] args) {
		SpringApplication.run(OnlinecompilerApplication.class, args);
	}

}

 

여기서 @SpringBootApplication는 @ConponentScan을 상속받는 어노테이션이다.

따라서 Spring Boot는 해당 어노테이션이 위치한 패키지의 하위 패키지들에 대해 컴포넌트 스캔을 한다.

 

이렇게 Spring Boot가 빈을 탐색하는 방법에 대해서 알아보았다.

Spring Boot에서 탐색한 객체들을 순서대로 빈으로 등록하는 방법

위 생성자 DI에서 사용한 예제 코드를 조금 다듬어서 가져왔다.

@Component
class Dog implements Animal{
    public void bark(){
        System.out.println("Dog Bark");
    }
}

@Component
class Cat implements Animal{
    public void bark(){
        System.out.println("Cat Bark");
    }
}

@Component
class AnimalSound{
    private final Animal animal;
    public AnimalSound(Animal animal){
        this.animal = animal;
    }
    void printBarkSound(){
        animal.bark();
    }
}

일단 지금은 @Component 어노테이션이 붙은 객체들은 빈으로 등록되었다고 알아두자.

 

그런데 만약 Spring이 AnimalSound를 먼저 빈으로 등록한다면 어떻게 될까?

또한, 빈으로 등록된 객체의 생성자가 빈이 아닌 객체를 요구한다면 어떻게 될까?

 

이를 위해서 Spring Boot의 빈 생성 순서와 주의 사항에 대해서 알아볼 것이다.

  1. 우선 패키지를 차례대로 확인하며 패키지에 존재하는 순서대로 빈의 인스턴스를 생성함
  2. 생성자를 통한 DI를 사용하는 경우, 이전에 필요한 빈 객체가 있다면 해당 빈을 먼저 생성한다.
  3. 주의 사항 : DI는 빈 객체 사이에서만 발생한다.

정리하자면, Spring Boot 내부적으로 DI는 빈 객체 사이에서만 발생하고 만약 생성자 주입 시 필요한 빈이 있다면 해당 빈을 우선적으로 생성해준다.

Spring에서의 빈 등록

마지막으로 Spring Boot가 아닌 Spring에서 빈을 등록하는 방법에 대해 간단하게 설명하고 넘어가겠다.

상당히 어려운 내용이므로 Spring Boot와의 차이만을 알고 넘어가자.

  빈 관리 빈 탐색 빈 생성
Spring 스프링 컨테이너가 빈을 관리 설정 파일에 빈과 관계 등록 설정 파일을 탐색하며 생성
Spring Boot 스프링 컨테이너가 빈을 관리 @ComponentScan으로 탐색 컴포넌트 스캔한 빈들을 생성

 

마무리

 

이렇게 DI에 대한 기본적인 개념에 대해서 알아보았고, Spring에서 빈을 생성할 때 어떻게 DI가 사용되는지에 대해서 알아보았다.

DI라는 개념은 단순히 빈을 생성할 때만 사용되는 개념이 아니다. 보다 유연하고 확장성 있는 코드를 작성하는데 자주 사용되는 개념이기에 잘 알아두도록 하자.

'Spring > 기초 개념' 카테고리의 다른 글

Access Token와 Refresh Token을 활용한 인증과 인가  (0) 2025.02.07
Spring Boot에서 테스트코드 작성하기  (0) 2025.02.01
Spring Security로 JWT 토큰 인증 방식 구축하기  (1) 2025.01.27
실습을 통해서 알아보는 Spring Security 기초  (0) 2025.01.25
Spring의 제어역전(IoC)  (0) 2024.12.26
'Spring/기초 개념' 카테고리의 다른 글
  • Spring Boot에서 테스트코드 작성하기
  • Spring Security로 JWT 토큰 인증 방식 구축하기
  • 실습을 통해서 알아보는 Spring Security 기초
  • Spring의 제어역전(IoC)
코드래곤
코드래곤
코드래곤 님의 블로그 입니다.
  • 코드래곤
    코드래곤 님의 블로그
    코드래곤
  • 전체
    오늘
    어제
    • 분류 전체보기 (61)
      • 알고리즘 (3)
        • 그리디 (1)
        • 그래프 (2)
      • 시스템 설계 (6)
      • CS 및 기본 개념 (17)
      • Docker (5)
      • Spring (23)
        • 백준 서비스 구현하기 (1)
        • 기초 개념 (14)
        • MSA (2)
        • JPA (1)
      • Dart (3)
      • Flutter (1)
      • Kubernetes (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
코드래곤
예시를 통해 알아보는 Spring Boot의 의존성 주입(DI)
상단으로

티스토리툴바