의존성 주입(Dependency Injection, DI)은 객체 간의 의존 관계를 외부에서 주입해 관리하는 설계 패턴
이를 통해 객체가 직접 필요한 의존성을 생성하거나 찾는 대신, 외부에서 주입받아 사용할 수 있습니다. DI는 코드의 결합도를 낮추고 테스트 가능성과 유지보수성을 높이는데 유용.
의존성(Dependency) : 한 객체가 다른 객체를 사용할 때 이를 '의존성'이라고 합니다. ex) A 객체가 B 객체를 사용하면, A는 B에 의존합니다.
의존성 주입 : 객체 내부에서 의존성을 생성하지 않고, 외부에서 제공받아 주입받는 방식입니다. 객체의 생성과 사용을 분리하여 결합도를 낮춥니다.
생성자 주입(Constructor Injection)
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)
public class Service {
@Autowired
private Repository repository;
}
결국, 의존성 주입을 사용하는 목적은 객체 간 강한 의존성을 없애고, 유연한 설계가 가능하며, 의존성 변경이나 교체가 용이하고 객체가 독립적으로 설계되어 다양한 환경에서 재사용할 수 있다.
동작 흐름 : 객체가 필요로 하는 의존성을 @Component나 @Bean으로 정의. Spring의 IoC(Inversion of Control) 컨테이너가 애플리케이션 컨텍스트에서 의존성을 관리. @Autowired나 @Inject 등을 통해 의존성을 주입받아 사용
DI의 세 가지 방식(필드 주입, 세터 주입, 생성자 주입)을 활용하여 간단한 CRUD 서비스를 만들어 보고, 각 방식의 장단점을 체감합니다.
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: 테스트 작성
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();
}
}
Spring 없이 DI를 수동으로 관리해 보면서, 프레임워크가 제공하는 편의성과 DI 방식의 차이를 이해합니다.
public class Main {
public static void main(String[] args) {
// 직접 의존성 생성 및 주입
Repository repository = new Repository();
Service service = new Service(repository);
service.process();
}
}
Spring Boot 기반으로 프로젝트를 생성하고, 생성자 주입만을 사용해 CRUD API를 개발합니다.
UserRepository, UserService, UserController를 모두 생성자 주입 방식으로 연결.필드 주입 방식으로 작성된 코드를 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();
}
}
Configuration 클래스 활용
@Bean을 사용해 객체를 명시적으로 생성하고 관리.Spring Context 없는 테스트
Integration Test
간단한 Spring Boot 프로젝트를 시작하며 DI의 중요성을 실감할 수 있는 구조를 설계합니다.
@Service 계층에서 Mock Repository를 활용한 단위 테스트 작성.@Controller 계층에서 MockMvc 테스트 작성.실습을 완료한 후, 다음 질문에 답을 작성하여 블로그나 학습 기록에 남깁니다.
이러한 실습은 신입 Java 백엔드 개발자로서 DI를 제대로 이해하고 활용하는 데 큰 도움이 될 것입니다.