DI (Dependency Injection)

parkrootseok·2025년 1월 28일

스프링

목록 보기
3/12
post-thumbnail

DI(의존성 주입)란?

DI란 객체를 직접 생성하는 것이 아닌 외부에서 생성 후 주입하는 것을 말합니다. 즉, 객체간의 관계를 결정하는 주체가 자신(개발자)이 아닌 외부(프레임워크)에 있다는 것을 의미합니다. 이를 통해, 객체간의 느슨한 결합 형태를 만들 수 있습니다.

느슨한 결합이란?

느슨한 결합은 한 클래스가 다른 클래스에 대해 낮은 의존성을 가지는 상태입니다. 즉, 한 클래스의 변경이 다른 클래스에 최소한의 영향을 주도록 설계된 구조입니다.

DI는 어떻게 사용하나요?

DI를 사용하지 않는 코드에서 부터 DI를 사용하는 코드로 변경하면서 Spring에서 DI를 사용하기 위한 방법과 사용했을 때 이점을 살펴보겠습니다.

DI 미사용

일반적으로, Java의 경우 객체를 사용할 때 다음과 같이 직접 생성하여 클래스 내부에 있는 메소드나 변수에 접근할 수 있습니다.

이러한 경우 어떠한 문제가 발생할 수 있을까요?

첫 번째, 객체간 높은 의존성을 가질 수 있습니다. 만약, MemberService가 기본 생성자가 아닌 매개 변수 생성자를 사용한다면, MemberController의 코드 또한 수정이 발생할 수 있습니다.

두 번째, 불필요한 리소스 소모가 발생할 수 있습니다. MemberController 객체를 새롭게 생성할 때 MemberService 객체도 새롭게 생성해야 합니다. 이런 작업이 빈번하게 수행된다면 불필요한 리소스가 소모될 수 있습니다.

DI 사용

Spring에서는 이러한 단점을 극복하기 위해 DI를 지원합니다. 다음 사진과 같이 의존성 주입을 사용할 수 있습니다. 이때, 의존성 주입 방법은 3가지(Constructor, Setter, Field) 방법이 존재합니다. 가장 권장하는 방법인 Constructor 방식을 사용하도록 하겟습니다.

왜 생성자 방식을 권장하나요?

  • 객체 생성 시 생성자는 반드시 호출됨
    • Null Pointer Exception 방지
  • 컴파일 단계에서 순환 참조 오류를 미리 파악할 수 있음
    • Setter, Field 방식의 경우 런타임에서 확인 가능
  • final 키워드를 사용하여 불변성 보장 가능

@Autowired 어노테이션을 활용하여 다음과 같이 의존성을 주입할 수 있습니다. 이를 통해, MeberService 클래스에 대한 수정을 MemberController는 신경쓰지 않아도 됩니다.

정리

이를 정리하면 다음과 같습니다.

  • DI (Dependency Injection, 의존성 주입)
    • 개발자가 객체를 직접 생성하는 것이 아니고, 프레임워크에서 생성된 객체를 주입하는 것
  • DI를 활용하는 방법
    • @AutoWired 어노테이션 사용
    • 3가지 방식 존재
      • Constructor
        • 가장 권장하는 방식 (NPE 방지, 순환 참조 방지, 불변성)
      • Setter
      • Field

추가 학습

테스트 용이성 증가

DI는 작성한 코드에 대한 테스트에도 긍정적인 영향을 미칩니다. DI를 사용하면 클래스를 직접 생성하지 않기 때문에, 다음과 같이 외부에서 생성한 Mock 객체를 쉽게 주입받아 사용할 수 있기 때문입니다.

이로 인해, 외부 리소스(DB, 네트워크)에 의존하지 않고 독립적으로 수행할 수 있으며 특정 의존성의 구체적인 구현에 의존하지 않아 단위 테스트를 효율적으로 작성할 수 있습니다.

Mock이란?

테스트 시 실제 객체를 대신하여 동작을 시뮬레이션하는 가짜 객체를 말합니다.

생성자 방식을 사용하는 것이 테스트 용이성을 극대화한다?

Field와 Setter방식은 생성자 방식 보다 Mock 객체를 주입하는 방식이 번거롭다는 단점이 있습니다. 특히, Field 방식은 Reflection을 사용하기 때문에 더욱 어렵습니다.

예상 질문

DI란 무엇인가요?

DI란 객체를 개발자가 직접 생성하는 것이 아닌 프레임워크에서 생성한 객체를 주입해 사용하는 것을 말합니다.

DI는 어떻게 사용할 수 있나요?

@Autowired 어노테이션을 활용하여 주입할 수 있으며 수정자, 생성자, 필드 총 3가지 방식이 존재합니다. 이중 가장 권장하는 방법은 생성자입니다. 해당 방식은 NPE 방지, 순환 참조 오류 방지, 불변성 등의 특징을 가지고 있기 때문입니다.

DI는 어떠한 장점이 있나요?

객체간 낮은 결합도 유지 가능

DI를 사용하지 않으면 아래와 같이 MySQLUserRepository과 강한 결합을 이루게 됩니다.

public class UserService {
    private final UserRepository userRepository;

    // 의존성을 변경할 때 직접적인 코드 수정 필요 -> 결합도 증가
    public UserService() {
        this.userRepository = new MySQLUserRepository();
    }

    public void registerUser(User user) {
        userRepository.save(user);
    }
}

하지만, 의존성 주입을 통해 의존 관계를 정의하면 이에 대한 결합도를 낮출 수 있습니다.

@Repository
public class MySQLUserRepository implements UserRepository {}

@Repository
public class RedisUserRepository implements UserRepository {}

@Service
public class UserService {
    private final UserRepository userRepository;

	// UserService 클래스는 UserRepository에 대한 정보를 알 필요가 없음 (결합도 감소)
    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void registerUser(User user) {
        userRepository.save(user);
    }
}

테스트 용이성

DI를 사용하지 않는 경우, 테스트 시 UserServiceUserRepository를 직접 생성하기 때문에 Mock 객체를 넣을 수 없습니다.

public class UserService {
    private final UserRepository userRepository;

    public UserService() {
    	// 직접 객체를 생성함
        this.userRepository = new UserRepository();
    }

    public void registerUser(String username) {
        userRepository.save(username);
    }
}

public class UserServiceTest {
    @Test
    public void testRegisterUser() {
        UserService userService = new UserService();
        // userService는 내부적으로 UserRepository를 new로 생성하므로,
        // Mocking이 불가능하다.
        userService.registerUser("John Doe");
    }
}

DI를 사용하면 다음과 같이 간단하게 Mock 객체를 주입할 수 있습니다.

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
}

@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;

    // Spring이 관리하는 Bean에 Mock 객체 주입
    @MockBean
    private UserRepository userRepository;
}

DI 없이 Mock 객체를 주입할 수 있는 방법은 없나요?

수정자 주입

아래와 같이 수정자 주입을 통해 할 수 있지만 권장하는 방법은 아닙니다.

public class UserServiceTest {
    @Test
    public void test() {
        // 1. Mock 객체 생성
        UserRepository mockRepository = mock(UserRepository.class);

        // 2. UserService에 주입 (DI 없이 수동으로 Setter 사용)
        UserService userService = new UserService();
        userService.setUserRepository(mockRepository);
    }
}

위 같은 경우는 의존성이 1개지만, 더 많아지거나 의존성을 주입하는 시점이 달라질 경우 더욱 복잡해질 수 있기 때문입니다.

Reflection API 활용

아래와 같이 Reflection API를 활용해 필드를 강제로 변경할 수 있습니다.

public class UserServiceTest {
    @Test
    public void test() throws Exception {
        // 1. Mock 객체 생성
        UserRepository mockRepository = mock(UserRepository.class);

        // 2. UserService 생성
        UserService userService = new UserService();

        // 3. Reflection을 통해 private 필드에 접근
        Field field = UserService.class.getDeclaredField("userRepository");
        field.setAccessible(true);
        field.set(userService, mockRepository);
    }
}

하지만, 필드명이 변경될 때마다 Reflection 코드 수정도 일어나고, Context에 등록되지 않아 Spring에서 제공하는 기능을 사용할 수 없습니다.

profile
동료들의 시간과 노력을 더욱 빛내줄 수 있는 개발자가 되고자 노력합니다.

0개의 댓글