의존관계 주입은 크게 4가지 종류가 있다.
지금까지 진행했던 방법이 바로 생성자 주입이다.
@Component
public class BoardServiceImpl implements BoardService{
private final MemberRepository memberRepository;
private final BoardPolicy boardPolicy;
// 생성자 주입은 final 키워드를 사용할 수 있다.
// 생성자 주입
@Autowired
public void BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
this.memberRepository = memberRepository;
this.boardPolicy = boardPolicy;
} ...
⚠️ 생성자가 딱 1개만 있으면 @Autowired
어노테이션을 생략할 수 있다.
setter 메소드를 사용해서 의존관계를 주입하는 방법이다.
@Component
public class BoardServiceImpl implements BoardService{
private MemberRepository memberRepository;
private BoardPolicy boardPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setBoardPolicy(BoardPolicy boardPolicy) {
this.boardPolicy = boardPolicy;
} ...
필드에 바로 의존관계를 주입하는 방법이다.
@Component
public class BoardServiceImpl implements BoardService{
@Autowired private MemberRepository memberRepository;
@Autowired private BoardPolicy boardPolicy;
...
위의 코드를 보면, 생성자도 없고 setter도 없다.
즉 스프링의 @Autowired
기능이 없으면 필드에 접근조차 할 수 없다.
필드 주입 방식은 최대한 지양하는 것이 좋다.
일반 메서드에 @Autowired
어노테이션을 붙이면 의존관계 주입 기능을 사용할 수 있다.
@Component
public class BoardServiceImpl implements BoardService{
@Autowired
public void init(MemberRepository memberRepository, BoardPolicy boardPolicy) {
this.memberRepository = memberRepository;
this.boardPolicy = boardPolicy;
}
...
생성자 주입 방식을 사용하는 것이 좋다!
이유는 다음과 같다.
의존관계를 주입할 빈이 없어도 동작해야 할 때가 있다.
@Autowired
public void BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
this.memberRepository = memberRepository;
this.boardPolicy = boardPolicy;
}
예를 들어, memberRepository가 아직 준비되지 않았는데 BoardServiceImpl 객체를 생성해야 하는 상황이 생길 수 있다.
이때, 옵션을 사용해서 오류를 피할 수 있다.
@Autowired(required = false)
: 주입할 대상이 없으면 메소드 자체가 호출되지 않음 (생성자에는 적용 불가)@Nullable
: 주입할 대상이 없으면 null을 주입Optional<>
: 주입할 대상이 없으면 Optional.empty를 주입@Autowired(required = false)
public void setBeanOption1(Member member) {
this.member = member
}
// member 객체는 빈이 아니다!
// 따라서 @Autowired로 주입할 대상이 없다.
// 주입할 대상이 없으므로 위의 메소드는 아예 호출되지 않는다.
@Autowired
public void setBeanOption1(@Nullable Member member) {
this.member = member
}
// member 객체는 빈이 아니다!
// 따라서 @Autowired로 주입할 대상이 없다.
// 주입할 대상이 없으므로 null이 주입된다.
@Autowired(required = false)
public void setBeanOption1(Optional<Member> member) {
this.member = member
}
// member 객체는 빈이 아니다!
// 따라서 @Autowired로 주입할 대상이 없다.
// 주입할 대상이 없으므로 Optional.empty가 주입된다.
lombok은 의존관계 주입 코드를 간단하게 만들어주는 라이브러리다.
lombok의 @RequiredArgsConstructor
어노테이션을 사용하면, final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
// 원래의 코드
@Component
public class BoardServiceImpl implements BoardService{
private final MemberRepository memberRepository;
private final BoardPolicy boardPolicy;
@Autowired
public void BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
this.memberRepository = memberRepository;
this.boardPolicy = boardPolicy;
} ...
lombok을 적용해보자.
// lombok이 적용된 코드
@Component
@RequiredArgsConstructor
public class BoardServiceImpl implements BoardService{
private final MemberRepository memberRepository;
private final BoardPolicy boardPolicy;
...
생성자 코드가 사라진 것처럼 보이지만, 컴파일 시점에 lombok이 생성자 코드를 넣어준다.
이전 포스팅에서 학습한 @Qualifier
어노테이션에는 컴파일시 오류 체크를 못한다는 문제가 존재한다.
예를 들어, @Qualifier(ssubBoardPolicy)
같이 타이핑 실수가 났을 때, 컴파일 오류가 발생하지 않는다.
이러한 문제를 해결하기 위해 어노테이션을 직접 만들 수 있다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("subBoardPolicy")
public @interface SubBoardPolicy {
}
이제, @SubBoardPolicy
라는 어노테이션을 사용할 수 있다. 어노테이션은 컴파일 시 오류가 체크되기 때문에 위의 문제를 막을 수 있다.
@Component
@SubBoardPolicy // @SsubBoardPolicy 같이 타이핑 실수가 발생해도 컴파일 오류로 알려준다.
public class ReadWritePolicy implements BoardPolicy{ ... }
⚠️ 어노테이션에는 상속이라는 개념이 없다.
@Qualifier(SubBoardPolicy)
같이 여러 어노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.
의도적으로 해당 타입의 스프링 빈이 다 필요한 경우가 있다.
이러한 경우, Map 또는 List를 사용해서 중복되는 스프링 빈을 모두 주입 받을 수 있다.
클라이언트(학생)이 글쓰기 권한을 선택할 수 있다고 가정해보자.
// 새로 등록할 빈
@Component
static class BoardService {
private final Map<String, BoardPolicy> policyMap;
@Autowired
public BoardService(Map<String, BoardPolicy> policyMap) {
this.policyMap = policyMap;
}
public boolean getAuthority(Member member, String policyCode){
// PolicyCode로 빈의 이름을 넘겨 받는다.
BoardPolicy boardPolicy = policyMap.get(policyCode);
// 주입된 빈들 중에서 policyCode를 찾는다.
return boardPolicy.returnAuthority(member);
}
}
BoardService 클래스는 BoardPolicy 타입 + 하위 타입을 주입받는다.
BoardPolicy의 하위 타입은 ReadOnlyPolicy와 ReadWritePolicy가 있으므로 중복되는 두 빈이 모두 Map에 저장된다. <키: 빈 이름, 값: 빈 타입>
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, BoardService.class);
// AutoAppConfig와 BoardService를 설정정보로 전달 + 빈으로 등록
BoardService boardService = ac.getBean(BoardService.class);
Member member = new Member(1L, "studentA", Position.STUDENT);
// 클라이언트가 readOnlyPolicy 빈을 선택한다.
boolean authority = boardService.getAuthority(member, "readOnlyPolicy");
// readOnlyPolicy는 학생의 권한을 false로 설정한다.
Assertions.assertThat(authority).isEqualTo(false);
}
자동 의존관계 주입과 수동 의존관계 주입, 어떤 것을 써야할까?
편리한 자동 기능을 기본으로 사용하자.
직접 등록하는 기술 지원 객체는 수동으로 등록하여 설정 정보에 바로 보이게 하는 것이 좋다.
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고려해보자.