Spring

DI(Dependency Injection)

변위니 2025. 1. 7. 03:16

객체 간의 의존성을 외부에서 주입하여,객체 간의 결합도를 낮추고, 코드의 재사용성과 유연성을 높이는 패턴

 

객체가 직접 의존성을 생성하거나 관리하는 대신, 외부에서 필요한 의존성을 주입받아서 사용하기 때문에, 사용할 객체의 구현을 몰라도 되고, 인터페이스나 추상 타입에 의존한다.

 

 

 


강합 결합

한 클래스가 다른 클래스의 구현에 직접적으로 의존하는 상황으로, 두 클래스 간의 관계를 변경하거나 유지보수가 어렵다.

public class Engine {
    public void start() {
        System.out.println("Engine start!");
    }
}

public class ElectricEngine extends Engine {
    @Override
    public void start() {
        System.out.println("Electric engine started");
    }
}

public class Car {
    private Engine engine;
    
    public Car() {
        this.engine = new Engine();
    }
    
    public void startCar() {
        engine.start();
        System.out.println("Car started!");
    }
}

Car 클래스는 Engine 객체의 구현에 의존한다. 이런 경우 Engine이 변경되거나 다른 종류의 엔진을 사용해야 하는 경우, Car의 코드를 변경해야 한다. 테스트를 할 경우에도 실제 Engine 객체가 필요하기 때문에 테스트가 복잡해질 수 있다.

 


구체적인 구현

클래스의 실제 행동이나 기능을 수행하는 코드를 말한다.

 

Engine는 기본 엔진을 나타내는 클래스이고, ElectricEngine은 전기 엔진을 나타내는 구체적인 구현이다.

 

구현한 클래스는 start() 메서드를 다르게 구현할 수 있다.

public class Car {
    private Engine engine;
    
    public Car() {
        this.engine = new ElectricEngine();
    }

   public void startCar() {
      engine.start();
      System.out.println("Car started");
   }
}

위의 방식은 Car 클래스는 항상 ElectricEngine클래스만 사용하도록 한다. 만약 다른 엔진(GasolineEngine)을 사용하고 싶다면, Car클래스를 수정해야 한다.

 

새로운 엔진이 생길 때마다 해당 엔진으로 변경하기 위해 Car 클래스를 고치는 것은 개방-폐쇄 원칙을 위반하는데, 이 문제를 해결하기 위해 의존성 주입을 사용할 수 있다.

public class Car {
    private Engine engine;
    
    public Car(Engine engine) {
        this.engine = engine;
    }
    
    public void startCar() {
        engine.start();
        System.out.println("Car started");
    }
}

Car 클래스는 Engine 인터페이스에 의존하며, 실제 구현체를 생성자를 통해 주입받는다.

ElectricEngine 등 다양한 종류의 엔진을 사용할 수 있고, 테스트 시 모의 객체(Mock Object)등을 주입하여 테스트를 할 수 있어 단위 테스트 작성에 용이하다.

Engine regularEngine = new Engine();
Car car1 = new Car(regularEngine);

Engine electricEngine = new ElectricEngine();
Car car2 = new Car(electricEngine);

car1.startCar();
car2.startCar();

 

 

 


의존성 주입 방법

1. 생성자 주입

public class UserService {

    // final로 선언해 불변성을 보장한다.
    private UserRepository userRepository;
    
    // 의존성을 생성자를 통해 주입
    public UserService(UserRepository userRepository) {
       this.userRepository = userRepository;
    }
}
  • 의존성을 생성자에서 받아오기 때문에, 불변성이 보장되고, 테스트가 용이하다.
  • 생성자 주입은 의존성 주입 실패 시 컴파일 에러를 감지할 수 있다.

2. Setter 주입

public class UserService {
    
    private UserRepository userRepository;
    
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}
  • 선택적인 의존성 주입이 가능하다.
  • 의존성을 주입받을 필드에 대한 수정 가능성이 필요할 때 유용하지만, 의존성을 변경할 수 있는 상태를 갖게 된다.
  • 객체가 생성된 후, Setter 메서드를 호출하지 않으면 의존성이 설정되지 않아 불완전한 상태가 될 수 있다.

3. 필드 주입

@Autowired 어노테이션을 사용하여 의존성을 필드에 직접 주입하는 방식으로 스프링이 자동으로 해당 타입이 빈을 찾아서 주입한다.

public class UserService {
    @Autowired
    private UserRepository userRepository;
}
  • 필드에 직접 의존성을 주입한다.
  • 필드 주입은 테스트나 유지보수 시 유연성이 떨어질 수 있어, 일반적으로 생성자 주입이 권장된다.

 

@Autowired

의존성 주입을 처리하는 어노테이션으로 스프링 컨테이너가 클래스의 필드, 생성자, 또는 메서드에 필요한 의존성으로 자동으로 주입한다.

 


 

의존성 주입의 동작 원리

  • 스프링은 컨테이너에서 관리되는 빈들 간의 의존성을 자동으로 주입한다.
  • @Autowired는 스프링의 IoC 컨테이너에서 적합한 빈을 찾아 주입한다.
  • 타입 기반 주입 방식으로, @Autowired가 선언된 필드나 생성자에 타입이 일치하는 빈을 찾아 자동으로 주입한다.

 


 

각 요청을 다음 계층(layer)인 서비스로 전달하기 위해, 서비스 멤버 변수를 private final로 선언하고 생성자를 통해 주입받는다

 

 

왜 private final로 선언할까?

  • final로 선언된 필드는 한 번 초기화된 이후 변경할 수 없다.
    • 즉, 서비스나 레포지토리 객체를 주입받을 때 한 번 설정되면 그 이후로 변경될 위험이 없다.
    • 예를 들어, 잘못된 코드나 외부 요인으로 서비스 객체가 바뀌어버리는 일을 방지할 수 있다. (안정성)
  • 의존성이 고정되고 명확해진다.
    • 의존성을 생성자 주입 방식으로 받기 때문에, 객체가 생성될 때 주입된 의존성은 변경될 수 없고 고정적이다.
    • 이런 방식은 해당 클래스가 어떤 의존성에 의존하는지 명확히 보여줘 코드의 가독성과 유지보수성을 높인다.
  • 생성자 주입 방식과의 결합
    • final로 선언된 필드는 반드시 생성자에서 초기화해야 한다. 따라서 컴파일 시점에서 의존성을 보장한다.
    • 또한, 생성자 주입 방식은 의존성 주입을 강제하므로 테스트가 쉬워진다.

 

 

final로 선언된 필드는 생성자에서 반드시 초기화되어야 한다.

생성자에서 주입받는 의존성이 고정되고 그 의존성이 절대 변하지 않기 때문에, 해당 클래스가 무엇을 의존하는지 명확하게 보여준다.

public class UserService {
    private final UserRepository userRepository; // 의존성 고정

    public UserService(UserRepository userRepository) { // 생성자 주입
        this.userRepository = userRepository;
    }
}

UserService가 생성될 때, 생성자 주입을 통해 UserRepository가 주입된다.

이때 userRepository는 final로 선언되어 있기 때문에, 객체가 생성된 이후에는 변경될 수 없다. 따라서 UserService는 UserRepository에 고정된 의존성을 갖기 때문에, 코드의 안정성을 보장할 수 있다.

 

final로 선언된 필드는 한 번 주입된 의존성이 변경되지 않기 때문에 의도치 않은 변경과 버그를 방지할 수 있다. 반면, final이 아닌 필드는 코드 중간에 의존성이 바뀔 가능성이 있어 런타임 에러나 예상치 못한 동작이 발생할 수 있다.

 

생성자 주입은 스프링이 컨트롤러를 생성할 때 자동으로 필요한 서비스를 주입하는 방식으로, 컨트롤러와 서비스의 생명주기를 연결하여 한 번 매핑된 의존성을 바꾸지 않도록 보장한다. 이를 통해 서비스와 컨트롤러가 함께 생성되고 주입되어, 안전한 객체 관리가 가능해진다.

 

 

 

 

 


 

정리

의존성 주입은 클래스가 특정 구현체가 아닌 인터페이스나 추상 클래스에 의존하도록 함으로써 클래스 간 결합도를 낮춘다.

이를 통해 코드의 유연성과 확장성을 높이고, 테스트 시 Mock 객체를 주입하여 단위 테스트를 용이하게 해준다. 객체의 생성과 사용을 분리함으로써 코드의 유지보수성과 재사용성을 높일 수 있다.

 

 

 

'Spring' 카테고리의 다른 글

Controller, RestController 어떻게 다를까  (0) 2025.02.02
Spring과 Spring Boot 동작이 어떻게 다른거야~  (1) 2025.01.20
JSON Web Token  (0) 2025.01.07