의존성 주입(Dependency Injection)이란 무엇인가요?

김상욱·2024년 11월 20일

의존성 주입(Dependency Injection)이란 무엇인가요?

의존성 주입(Dependency Injection, DI)은 객체 간의 의존 관계를 외부에서 주입해 관리하는 설계 패턴

이를 통해 객체가 직접 필요한 의존성을 생성하거나 찾는 대신, 외부에서 주입받아 사용할 수 있습니다. DI는 코드의 결합도를 낮추고 테스트 가능성과 유지보수성을 높이는데 유용.

의존성(Dependency) : 한 객체가 다른 객체를 사용할 때 이를 '의존성'이라고 합니다. ex) A 객체가 B 객체를 사용하면, A는 B에 의존합니다.
의존성 주입 : 객체 내부에서 의존성을 생성하지 않고, 외부에서 제공받아 주입받는 방식입니다. 객체의 생성과 사용을 분리하여 결합도를 낮춥니다.

생성자 주입(Constructor Injection)

  • 의존성을 생성자의 매개변수로 전달받는 방식
  • 장점 : 의존성이 불변(immutable)으로 설정될 수 있음.
    : 생성자를 통해 전달된 의존성은 이후 수정(set)되지 않기 때문에 객체의 상태가 예측 가능하며 또한 생성 시 모든 의존성을 확정하므로 객체의 상태가 일정하게 유지되어 스레드 안정성(Thread Safety)도 보장될 수 있음. 추가적으로 세터 주입처럼 의존성을 수정할 수 없기에 의도치 않게 의존성이 바뀌는 것도 방지.
public class Service {
    private final Repository repository;

    public Service(Repository repository) {
        this.repository = repository;
    }
}

세터 주입(Setter Injection)

  • 의존성을 세터 메서드를 통해 전달받는 방식.
  • 선택적인 의존성을 주입하거나 런타임에 변경이 가능
public class Service {
    private Repository repository;

    public void setRepository(Repository repository) {
        this.repository = repository;
    }
}

필드 주입(Field Injection)

  • 객체의 필드에 직접 의존성을 주입받는 방식.
  • 주로 @autowired나 @inject를 사용하여 주입.
public class Service {
    @Autowired
    private Repository repository;
}
  • 테스트와 리팩토링이 어렵고, 의존성이 숨겨져 가독성이 낮아질 수 있음.
    : 보통 어노테이션을 통해 주입되므로 DI 컨테이너가 동작해야만 의존성이 주입되므로, 단독으로 객체를 생성할 수 없음. 또한 생성자를 사용하지 않으므로, 객체 생성 시 필요한 의존성이 무엇인지 명확하지 않음.
    : @Autowired가 많아지면, 어떤 의존성이 필수적이고 어떤 것이 선택적인지 알기 어려워짐.

결국, 의존성 주입을 사용하는 목적은 객체 간 강한 의존성을 없애고, 유연한 설계가 가능하며, 의존성 변경이나 교체가 용이하고 객체가 독립적으로 설계되어 다양한 환경에서 재사용할 수 있다.

동작 흐름 : 객체가 필요로 하는 의존성을 @Component나 @Bean으로 정의. Spring의 IoC(Inversion of Control) 컨테이너가 애플리케이션 컨텍스트에서 의존성을 관리. @Autowired나 @Inject 등을 통해 의존성을 주입받아 사용


1. DI 방식별로 간단한 서비스 구현

DI의 세 가지 방식(필드 주입, 세터 주입, 생성자 주입)을 활용하여 간단한 CRUD 서비스를 만들어 보고, 각 방식의 장단점을 체감합니다.

실습 목표

  • DI 방식별 코드 구조 이해.
  • 생성자 주입과 필드 주입의 테스트 난이도 차이 경험.

실습 코드 예제

Step 1: 필드 주입 방식 구현

@Component
public class Repository {
    public void save() {
        System.out.println("Data saved!");
    }
}

@Service
public class Service {
    @Autowired
    private Repository repository;

    public void process() {
        repository.save();
    }
}

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);
        Service service = context.getBean(Service.class);
        service.process();
    }
}

Step 2: 생성자 주입 방식 구현

@Service
public class Service {
    private final Repository repository;

    // 생성자를 통해 의존성 주입
    public Service(Repository repository) {
        this.repository = repository;
    }

    public void process() {
        repository.save();
    }
}

Step 3: 테스트 작성

  • 필드 주입은 테스트 시 DI 컨테이너(Spring Context)가 필요하지만, 생성자 주입은 Mock 객체를 직접 주입할 수 있어 테스트가 간단합니다.
public class ServiceTest {
    @Test
    public void testProcess() {
        // 생성자 주입을 활용한 테스트
        Repository mockRepo = Mockito.mock(Repository.class);
        Service service = new Service(mockRepo);

        service.process();

        Mockito.verify(mockRepo, times(1)).save();
    }
}

2. DI 컨테이너 없이 객체 직접 관리

Spring 없이 DI를 수동으로 관리해 보면서, 프레임워크가 제공하는 편의성과 DI 방식의 차이를 이해합니다.

실습 코드 예제

public class Main {
    public static void main(String[] args) {
        // 직접 의존성 생성 및 주입
        Repository repository = new Repository();
        Service service = new Service(repository);

        service.process();
    }
}

3. Spring Boot 프로젝트에서 생성자 주입만 사용해보기

Spring Boot 기반으로 프로젝트를 생성하고, 생성자 주입만을 사용해 CRUD API를 개발합니다.

실습 과제

  • API 요구사항: 간단한 사용자 관리 시스템 (회원 가입, 조회, 삭제).
  • 구현: UserRepository, UserService, UserController를 모두 생성자 주입 방식으로 연결.
  • 테스트: Controller와 Service를 MockMvc와 Mock 객체로 테스트.

4. Reflection을 사용하여 필드 주입의 테스트 단점 체감하기

필드 주입 방식으로 작성된 코드를 Reflection을 활용해 Mock 객체로 테스트하며, 복잡성과 유지보수성 문제를 직접 경험해봅니다.

실습 코드 예제

public class ServiceTest {
    @Test
    public void testProcessWithReflection() throws Exception {
        // 필드 주입 방식의 테스트
        Repository mockRepo = Mockito.mock(Repository.class);
        Service service = new Service();

        Field field = service.getClass().getDeclaredField("repository");
        field.setAccessible(true); // private 필드 접근
        field.set(service, mockRepo);

        service.process();

        Mockito.verify(mockRepo, times(1)).save();
    }
}

5. DI와 테스트를 강화하는 Best Practices 실습

  • 목표: 실무에서 사용하는 DI와 테스트의 Best Practices를 익힙니다.

실습 과제

  1. Configuration 클래스 활용

    • @Bean을 사용해 객체를 명시적으로 생성하고 관리.
  2. Spring Context 없는 테스트

    • Spring의 의존성을 배제한 순수 단위 테스트 환경 구성(Mock 객체 사용).
  3. Integration Test

    • Spring Context를 사용하여 실제 의존성을 주입한 상태로 통합 테스트 작성.

6. 미니 프로젝트

간단한 Spring Boot 프로젝트를 시작하며 DI의 중요성을 실감할 수 있는 구조를 설계합니다.

프로젝트 아이디어

  • 간단한 도서 관리 시스템
    • 구현 목표:
      • 도서 추가, 조회, 삭제 API.
      • 의존성을 생성자 주입 방식으로 연결.
      • @Service 계층에서 Mock Repository를 활용한 단위 테스트 작성.
      • @Controller 계층에서 MockMvc 테스트 작성.

7. 느낀 점 정리

실습을 완료한 후, 다음 질문에 답을 작성하여 블로그나 학습 기록에 남깁니다.

  • 필드 주입, 세터 주입, 생성자 주입 각각의 장단점은 무엇인가?
  • 테스트와 유지보수 측면에서 어떤 방식이 가장 적합하다고 느꼈는가?
  • DI를 활용하면서 Spring의 IoC 컨테이너가 어떤 편의를 제공했는지?

이러한 실습은 신입 Java 백엔드 개발자로서 DI를 제대로 이해하고 활용하는 데 큰 도움이 될 것입니다.

0개의 댓글