Spring - 의존성 주입

스프링의 원리

Posted by Yan on July 18, 2024

스프링의 핵심 원리 IoC (Inversion of Control)

  • IoC : 제어의 역전
  • 객체 생성과 의존 관계 설정을 개발자가 직접 관리하는 대신 스프링 컨테이너가 담당하도록 하는 것

기존 방식:

1
2
3
4
class Service {
    private Dao dao = new DaoImpl(); // 개발자가 직접 생성
    // ...
}

스프링 IoC 방식: 스프링 IoC를 사용하면 Service 클래스 내에서 Dao 객체를 직접 생성하지 않고, 스프링 컨테이너에 의존성을 주입받는다.

1
2
3
4
5
6
7
8
9
@Service
public class Service {
    private final Dao dao;

    @Autowired
    public Service(Dao dao) {
        this.dao = dao;
    }
}

의존성 주입을 받는 이유

  • 낮은 결합도:
    • 객체 간의 의존 관계를 명확히 분리하여 코드의 유연성을 높인다.
    • 특정 구현체에 의존하지 않고 인터페이스를 통해 의존하므로, 코드 변경 시 영향 범위를 최소화할 수 있다.
  • 테스트 용이성:
    • 의존 객체를 모킹 객체로 쉽게 교체하여 단위 테스트를 수행할 수 있다.
  • 재사용성:
    • 한 번 정의된 빈(Bean)을 다양한 곳에서 재사용할 수 있다.
  • 확장성:
    • 새로운 기능을 추가하거나 기존 기능을 변경할 때 유연하게 대처할 수 있다.
  • 스프링 생태계 활용:
    • 스프링이 제공하는 다양한 기능(AOP, 트랜잭션 관리 등)을 활용할 수 있다.

의존성 주입 방식

  • 생성자 주입:
    • 객체 생성 시 생성자를 통해 의존성을 주입하는 방식
    • 필수적인 의존성을 명확하게 표현하고, 불변성을 보장한다.
  • Setter 주입:
    • 객체 생성 후 setter 메서드를 통해 의존성을 주입하는 방식
    • 생성자 주입보다 유연하지만, 의존성이 필수적인지 명확하지 않을 수 있다.
  • 필드 주입:
    • 필드에 @Autowired 어노테이션을 사용하여 의존성을 주입하는 방식
    • 가장 간편하지만, 테스트하기 어렵고 의존성이 명확하지 않을 수 있다.

1. 생성자 주입

  • 생성자 파라미터를 통해 의존성을 주입: 객체 생성 시 필수적인 의존성을 명확하게 지정한다.
  • 장점:
    • 의존성이 필수적인지 명확하게 드러낸다.
    • 불변성을 보장하여 객체 상태를 안정적으로 유지할 수 있다.
    • 테스트하기 쉽다.
  • 단점: 코드가 다소 길어질 수 있다.
1
2
3
4
5
6
7
8
9
@Component
public class MyService {
    private final UserRepository userRepository;

    @Autowired
    public MyService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

2. @Autowired 어노테이션

  • 필드 또는 메소드에 직접 적용: 의존해야 할 객체의 타입을 명시하고, 스프링 컨테이너가 자동으로 해당 객체를 찾아 주입한다.
  • 장점: 간결하고 편리하게 의존성을 주입할 수 있다.
  • 단점:
    • 필드 주입의 경우, 의존성이 필수적인지 명확하지 않을 수 있다.
    • 테스트 시 의존성을 조작하기 어려울 수 있다.
1
2
3
4
5
@Component
public class MyService {
    @Autowired
    private UserRepository userRepository;
}

@Autowired vs 생성자 주입 비교

특징 @Autowired 생성자 주입
적용 위치 필드, 메소드 생성자
장점 간결, 편리 의존성 명확, 불변성, 테스트 용이
단점 의존성 필수 여부 불명확, 테스트 어려움 코드 길어짐
권장 선택적 의존성, 간단한 경우 필수 의존성, 불변성 보장, 테스트 중심 개발

왜 생성자 주입을 더 선호하는가?

명확한 의존성:

생성자를 통해 의존성을 주입하면 어떤 객체가 필요한지 명확하게 드러나므로 코드의 가독성이 높아진다.

1
2
3
4
5
6
7
8
9
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public User getUserById(Long id) {
        return userRepository.findById(id);
    }
}
  • UserService는 UserRepository에 의존하고 있지만, UserRepository가 언제, 어떻게 주입되는지 명확하지 않다.
  • 의존성 주입 시점에 문제가 발생할 경우 오류를 찾기 힘들다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id);
    }
}

생성자 주입을 사용하면 UserService가 생성될 때 반드시 UserRepository가 필요하다는 것을 명확하게 보여준다. 의존성 관리가 보다 쉽다.

불변성:

생성자를 통해 주입된 의존성은 변경될 수 없으므로 객체 상태를 안정적으로 유지할 수 있다. 객체 생성 후 의존성이 변경될 가능성이 줄어들기 때문이다.

테스트 용이성:

의존성을 모킹하기 쉽기 때문에 단위 테스트를 효과적으로 수행할 수 있다.

  • @Autowired 필드 주입을 받을 때: UserService를 테스트하기 위해 UserRepository를 모킹하려면, 스프링 테스트 컨텍스트를 설정하고 @MockBean 어노테이션을 사용해야 한다. 이는 테스트 코드를 복잡하게 만들고, 의도하지 않은 사이드 이펙트가 발생할 가능성을 높인다.

  • 생성자 주입을 할 때: UserService를 생성할 때 모킹한 UserRepository를 전달하여 쉽게 테스트할 수 있다.

1
2
3
4
5
6
7
8
@Test
public void getUserById_success() {
    UserRepository userRepository = mock(UserRepository.class);
    when(userRepository.findById(1L)).thenReturn(Optional.of(new User()));

    UserService userService = new UserService(userRepository);
    User user = userService.getUserById(1L);
}

SonarLint가 @Autowired를 지양하는 이유

Autowired를 사용했을 때, SonarLint에서 경고가 나왔다. 왜 지양하는지 이유를 알고 싶었다.

알고보니 스프링 공식 문서에서도 생성자 주입을 권장하고 있었다.

스프링에서 의존성 주입은 IoC라는 핵심 원리에 기반하여 객체 간의 결합도를 낮추고 유연한 시스템을 구축하는 데 필수적이다. 특히, 생성자 주입 방식은 의존성을 명확하게 표현하고 테스트하기 쉽다는 장점이 있어 권장된다.

생성자 주입이 일반적으로 더 선호되는 방식이지만, @Autowired도 간단한 경우나 선택적 의존성일 때 유용하게 사용될 수 있다. 프로젝트의 특성과 개발 팀의 규약에 따라 적절한 방식을 선택하는 것이 좋다.

핵심은 의존성을 명확하게 관리하고, 코드의 가독성과 유지보수성을 높이는 것이다.

요약:

  • 스프링 IoC는 객체 생성과 의존 관계 설정을 자동화한다.
  • 의존성 주입은 코드의 유연성, 테스트 용이성, 재사용성을 높인다.
  • 생성자 주입이 가장 권장되는 방식이다.