[스프링 핵심 원리] 의존관계 자동 주입

JUJU·2024년 2월 19일
0

Spring

목록 보기
7/21
본 포스트는 김영한 개발자님의 스프링 핵심 원리 강의를 듣고 정리한 것입니다.
※ 코드는 강의에서 사용된 것과 다릅니다.
jaewon-ju Github Address

✏️ 의존관계 주입의 종류

의존관계 주입은 크게 4가지 종류가 있다.

  1. 생성자 주입
  2. Setter 주입
  3. 필드 주입
  4. 일반 메소드 주입

1. 생성자 주입

지금까지 진행했던 방법이 바로 생성자 주입이다.

  • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
  • 필드에 final 키워드를 적용할 수 있다. (불변)
@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 어노테이션을 생략할 수 있다.


2. Setter 주입

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;
    } ...

3. 필드 주입

필드에 바로 의존관계를 주입하는 방법이다.

  • 코드가 간결하다.
  • 외부에서 변경이 불가능하다.
  • 순수 자바 코드로는 아무것도 할 수 없다.
  • 쓰지말자!
@Component
public class BoardServiceImpl implements BoardService{

    @Autowired private MemberRepository memberRepository;
    @Autowired private BoardPolicy boardPolicy;
    
    ...

위의 코드를 보면, 생성자도 없고 setter도 없다.
즉 스프링의 @Autowired 기능이 없으면 필드에 접근조차 할 수 없다.
필드 주입 방식은 최대한 지양하는 것이 좋다.


4. 일반 메소드 주입

일반 메서드에 @Autowired 어노테이션을 붙이면 의존관계 주입 기능을 사용할 수 있다.

  • 거의 사용되지 않는다.
@Component
public class BoardServiceImpl implements BoardService{

    @Autowired
    public void init(MemberRepository memberRepository, BoardPolicy boardPolicy) {
        this.memberRepository = memberRepository;
        this.boardPolicy = boardPolicy;
    }
    ...



✏️ 권장되는 방식

생성자 주입 방식을 사용하는 것이 좋다!

이유는 다음과 같다.

1. 불변

  • 의존관계는 불변해야 한다.
  • setter 주입을 사용하면 setter를 public으로 열어두어야 한다. 따라서, 수정가능성이 있다.

2. 누락

  • 생성자 주입 방식은 객체를 생성할 때 반드시 의존관계를 주입해야 하지만, setter 주입 방식은 객체 생성 후에 주입하는 코드를 누락할 수 있다.
  • 위의 문제는 스프링 프레임워크를 사용하지 않고, 자바 코드로만 테스트를 실행할 때 발생한다.
  • 생성자 주입은 누락 시에 컴파일 오류가 발생해서 누락을 방지할 수 있다.

3. final

  • final 키워드는 의존관계를 불변하게 만들뿐만 아니라, 생성자에서 의존관계를 설정하지 않은 경우 컴파일 시점에 오류를 발생시켜 준다.
  • 오직 생성자 주입 방식만 final 키워드를 쓸 수 있다.



✏️ 옵션처리

의존관계를 주입할 빈이 없어도 동작해야 할 때가 있다.

@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은 의존관계 주입 코드를 간단하게 만들어주는 라이브러리다.

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);
}



✏️ 의존관계 주입 - 자동? 수동?

자동 의존관계 주입과 수동 의존관계 주입, 어떤 것을 써야할까?

편리한 자동 기능을 기본으로 사용하자.
직접 등록하는 기술 지원 객체는 수동으로 등록하여 설정 정보에 바로 보이게 하는 것이 좋다.
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고려해보자.




REFERENCE

스프링 핵심 원리 - 김영한 개발자님

profile
개발자 지망생

0개의 댓글

관련 채용 정보