spring
에서 DI(Dependency Injection)
를 하는 방법에는 여러가지가 있다.
각 방법의 장단점을 알아보자!
필드 주입은 테스트할 때가 아니면 사용하면 안된다.
@Controller
public class Controller {
@Autowired private Service service;
}
}
1. 코드가 간결해진다. (하지만 장점은 이것 뿐,,)
1. 테스트하기 어렵다.
DB에 접근하는 Repository
객체를 참조하는 Service
클래스가 있다고 하자.
@Component
public class MemberServiceImpl implements MemberService {
@Autowired private MemberRepository memberRepository;
}
테스트할 때 @Autowired
가 동작하지 않으므로 memberRepository
를 주입받을 수 없다.
결국 @SpringBootTest
를 사용해야만 테스트할 수 있다는 의미이다.
그리고 Repository
객체를 사용해서 DB에 직접 접근하지 않고, Mock
객체를 만들어서 테스트하고 싶은 경우가 생길 수 있다.
하지만 필드 주입은 스프링 컨테이너에 있는 빈을 주입받기 때문에 Mock
객체로 테스트하는 것은 불가능하다.
2. 순환참조를 찾아내기 힘들다.
순환 참조 예시를 살펴보자. CourseService
와 StudentService
사이에 순환 참조가 일어나는 코드이다.
@Service
public class CourseServiceImpl implements CourseService {
@Autowired
private StudentService studentService;
@Override
public void courseMethod() {
studentService.studentMethod();
}
}
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private CourseService courseService;
@Override
public void studentMethod() {
courseService.courseMethod();
}
}
이렇게 서로 계속 참조를 하면 StackOverflowError
가 발생한다.
필드 주입을 받으면 객체를 로드 할 시점이 되어서야 순환 참조 에러가 발생한다. (생성자 주입을 쓰면 더 빨리 순환참조를 알아낼 수 있다. 뒤에서 설명...)
1. 객체를 중간에 변경하고 싶다면 변경할 수 있다.
만약에 의존성 객체를 이미 주입받은 상태일 때, 중간에 변경하고 싶다면 setter
를 호출해서 객체를 다시 주입받을 수 있다.
하지만 객체가 바뀌어야 하는 상황은 거의 없으며 중간에 의도치 않게 객체가 바뀔 가능성이 있기 때문에 이 장점은 단점으로 이어질 수 있다.
2. 선택적으로 객체를 주입 받을 수 있다.
@Component
public class OrderService {
private MemberRepository memberRepository;
@Autowired(required = false)
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
setter
는 생성자와 달리 호출하고 싶으면 하고, 안하고 싶으면 안하면 된다.
호출을 하면 의존성 객체를 주입 받을 수 있고, 호출 안하면 주입받지 않게 된다.
따라서 선택적으로 의존성 객체 주입을 받고 싶을 때 사용할 수 있다.
spring
에서 setter
주입을 사용할 때 의존성 객체를 무조건 주입받지 않아도 된다면 @Autowired(required = false)
로 지정해주면 된다.
1. setter
은 public
으로 열어두기 때문에 누군가 의존성 객체를 바꿀 수도 있다. (불변 보장 X)
2. 의존성 주입 받지 않은 시점에 객체를 사용할 수 있어서 NullPointerException
이 발생할 수 있다.
생성자를 이용하면 객체 생성과 동시에 주입을 받는다.
반면에 setter
로 주입을 받는 경우,
new
로 객체 생성setter
로 의존성 주입
이런 과정이 가능해진다. 이 말은 객체 생성 후 의존성 주입을 받기 전에도 그 객체를 사용할 수 있다는 의미이다.
@Component
public class Controller {
private Service Service;
@Autowired
public void setService(Service service) {
this.service = service;
}
public void hello(){
service.hello();
}
}
Service
객체를 setter
로 주입 받는 Controller
클래스에서, controller
객체가 만들어지면 (service 객체를 사용하는) hello()
를 사용할 수 있게 된다.
근데 service
를 주입 받지 않은 상황에서 hello()
를 호출하면 NullPointerException
이 발생한다.
3. (필드 주입과 같이) 순환참조를 찾아내기 힘들다.
필드 주입 처럼 객체를 로드 할 시점이 되어서야 순환 참조 에러가 발생한다.
--
@Component
public class OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
잘 사용하지 않지만 일반 메서드로 주입받을 수도 있다.
setter
주입과 비슷하지만 한 번에 여러 필드를 주입받을 수 있다.
@Component
public class OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderService(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
1. 생성자 호출 시점에 딱 한 번만 호출되고, 그 객체는 변하지 않는다. (불변 보장)
final
키워드를 사용할 수 있다.
객체가 만들어짐 == 의존성 주입 받음
이기 때문에 NullPointerException
을 방지할 수 있다.
2. 생성자에는 null
값을 허용하지 않기 때문에 필수 의존관계에 사용하면 좋다.
3. 순환참조를 방지할 수 있다.
생성자 주입을 이용하면 컴포넌트 스캔에 의해 빈이 등록될 때 생성자가 호출되어 그 시점에 의존성 객체 주입을 받게 된다.
그래서 빈 객체 등록 시점에 순환참조를 바로 찾아낼 수 있기 때문에 순환참조 되고있는 있는 지점을 스프링이 바로 알려준다.
4. 테스트하기 쉽다.
직접 객체를 주입할 수 있기 때문에 DI 프레임워크 없이도 테스트 가능하다.
결론 : 생성자 주입을 사용하자 ^0^