Spring framework에서는 Dependency Injection으로 3가지 방식을 지원하고 있다.
본 포스트에서는 각 방식별 장단점에 대해서 서술한다.
@Controller
public class MemberController {
@Autowired
private MemberRepository memberRepository
}
외부에서 변경이 불가능해진다. (필드의 객체 수정이 불가능하다.)
특히, 테스트 코드 작성시 객체 수정이 불가능하므로 어려움을 겪게 됨.
(스프링 프레임워크에서는 빈이 싱글톤으로 생성되기에 테스트 코드 작성이 불가능 한 것은 아니나, DI 프레임워크에 의존적인 테스트 코드는 바람직하지 않은 구현이다.)
필드 주입 방식은 일반적으로 권장되지 않으나, 실무에서는 테스트 코드나 @Configuration 등에서는 편의를 위해 활용된다.
public class UserService {
private MemberRepository memberRepository;
private MemberService memberService;
@Autowired
public void setUserRepository(UserRepository userRepository){
this.memberRepository = memberRepository
}
}
만약 주입할 객체가 생성되기 전이라면 오류가 발생하므로, @Autowired(required=false)로 설정해줘야 한다.
생성자 주입은 생성자를 통해 의존 관계를 주입시키며, 생성자가 1개일 경우 @Autowired는 생략 가능하다. (가능하지만 생략하지 말자)
public class MemberService {
private MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository
}
}
스프링 프레임워크에서 Bean 간 Dependency가 변경되어야 하는 경우는 실제로 거의 없다. 그러므로 생성자 주입을 통해 변경 가능성을 배제하고 불변성을 보장하는 것이 좋다.
필드 주입 방식으로 Bean의 DI가 정의되었다면, 순수 자바코드로 단위 테스트를 작성할 수 없다.
테스트 코드는 Spring 프레임워크 상에서 동작하지 않으므로, 각 Bean들이 싱글톤으로 생성되어있음을 보장할 수 없다.
하지만 생성자 주입 방식을 사용한다면, 컴파일 시점에 객체를 주입받으므로 테스트 코드 작성이 가능하며, 컴파일 시점에 주입관계에서의 오류까지 잡아낼 수 있다.
생성자 주입 사용시, 필드 객체에 final을 사용할 수 있다. (필드 주입 방식에서는 DI가 객체 생성 이후에 수행되므로 final 사용 불가).
또한, final을 사용하면 Lombok을 활용할 수 있다.
Lombok: Annotation 기반 코드 자동완성 라이브러리
ex) @Getter, @Setter
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final MemberService memberService;
@Override public void register(String name) {
userRepository.add(name);
}
}
@RequiredArgsConstructor는 클래스의 필드들 중 @NonNull이 붙은 필드와 final로 생성된 필드들을 아규먼트로 하는 생성자를 자동으로 관리해주는 어노테이션이다,
@Getter @RequiredArgsConstructor public class Store extends Common { @NonNull private String companyName; // 상호명 private final String industryTypeCode; // 업종코드 private String businessCodeName; // 업태명 private String industryName; // 업종명(종목명) private String telephone; // 전화번호
Bean들이 필드/Setter 주입 방식으로 서로를 순환참조 하고 있다면, 서로를 순환해서 호출하며 CallStack이 계속 쌓여 Stackoverflow가 발생한다. 반면에 생성자 주입 방식의 경우, 같은 런타임이어도 애플리케이선 구동 시점에 에러가 발생하므로 해당 에러 발생을 사전에 방지할 수 있다.
주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
하지만, @Autowired 사용시 required=true가 디폴트로 처리되기에 DI 대상이 없으면 오류가 발생한다.
그 경우 자동 주입 대상이 없어도 오류가 발생하지 않도록 별도 처리를 해주어야 한다.
해당 방법은 아래 3가지가 있다.
@Test
void AutowiredOption(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean {
@Autowired(required=false)
public void setNoBean1(Member noBean1){
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2){
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3){
System.out.println("noBean3 = " + noBean3);
}
@Autowired
public void noBeanTest(){
Member test = new Member(1L,"hans", Grade.VIP);
setNoBean1(test);
}
}
}
//위 코드의 출력값
noBean2 = null
noBean3 = Optional.empty
noBean1 = group.core.member.Member@8c3619e