[Spring] 의존성 주입 방법

이신영·2023년 6월 28일
1

Spring

목록 보기
1/16
post-thumbnail

의존성 주입?

객체 지향 프로그래밍에서는 하나의 객체가 다른 객체를 사용하여 작업을 수행할 수 있다.
이때 다른 객체를 해당 객체의 의존성이라고 부른다.
예를 들어, 커피 주문을 처리하는 객체(Order)는 커피를 제조하는 객체(CoffeeMaker)에 의존한다.

이때 DI는 객체를 생성하는 시점이나 사용하는 시점이 아니라,
외부에서 객체를 생성하고 의존성을 주입하는 방식으로 의존성을 관리한다.
이를 통해 객체 간의 결합도를 낮추고 유지보수성을 높이는 효과를 얻을 수 있다.


다소 부끄러운 현재의 지식상태

SOLID 원칙중에 SRP에 대한 예시를 보고있다가 문득 떠오른게, 그간 나는 Service에서 늘 @Autowired로 DI를 했다. new도 쓰지않고 편리하기 때문이다. 그러다가

인텔리제이에서는 추천되는방식이 아니라길래 수정해보니까

    private final BoardRepository boardRepository;
    private final BoardLikeRepository boardLikeRepository;
    private final UserSecurityService userSecurityService;
    private final UserRepository userRepository;
    private final DeletedBoardRepository deletedBoardRepository;
    private final CommentRepository commentRepository;

    public BoardService(BoardRepository boardRepository, BoardLikeRepository boardLikeRepository, UserSecurityService userSecurityService, UserRepository userRepository, DeletedBoardRepository deletedBoardRepository, CommentRepository commentRepository) {
        this.boardRepository = boardRepository;
        this.boardLikeRepository = boardLikeRepository;
        this.userSecurityService = userSecurityService;
        this.userRepository = userRepository;
        this.deletedBoardRepository = deletedBoardRepository;
        this.commentRepository = commentRepository;
    }

라는 수정을 해준 기억이 난다. 그때당시에는 매번 추가해야하니까 저게 @Autowired보다 귀찮아보여서 매번 이거만 썼었는데 위의 생성자를 간추려주는게 lombok의 @RequiredArgsConstructor를 사용하는거였더라구요?

@RequiredArgsConstructor
public class BoardService {
    private final BoardRepository boardRepository;
    private final BoardLikeRepository boardLikeRepository;
    private final UserSecurityService userSecurityService;
    private final UserRepository userRepository;
    private final DeletedBoardRepository deletedBoardRepository;
    private final CommentRepository commentRepository;

이렇게 쓰면 위와 똑같아짐 이러면 이걸 써도되겠다~ 싶은데 결정적으로 생성자주입이 좋다! 라고만 들어왔지 @Autowired를 통한 주입과 @RequiredArgsConstructor를 통한 주입에서 생기는 차이점은 잘 모르고있었다 😅
명확하게 짚고넘어가면서 또 다른 방식의 DI가 있으니 다양한 DI 방법에 대해 정리해볼까 하는 마음에 포스팅하나 해보도록 하겠다!


의존성 주입의 종류

의존성을 주입(DI)하는 방법은 3가지가 있다.

  • 필드 주입(Field Injection)
  • 생성자 주입(Constructor based Injection)
  • 세터 주입(Setter based Injection)

생성자주입이 좋다는데 왜좋은지는 모르겠네? 일단 셋의 차이를 보도록하자


1. 필드 주입

public class UserService {
    @Autowired
    private UserRepository userRepository;

    // ...
}

내가 평소에 쓰던 주입방식이다. 의존성을 주입하고자 하는 필드에 @Autowired만 붙이면 되는 간결한 장점이 있다. 하지만 내가 몰랐던건 이것이다.

필드 주입에서 주의할 것

필드주입시에 자칫하면 NullPointerException이 생긴다는거였다. 생기는 이유에 대한 과정을 위의 코드를 통해 설명해보겠다.

  1. UserService클래스에서 @Autowired가 필드에 추가되어있으므로 스프링 컨테이너는 해당 필드에 자동으로 UserRepository의 인스턴스(userRepository)를 주입함
  2. 주입될 인스턴스를 찾아봄. 찾으려는 인스턴스는 스프링 컨테이너에 등록된 빈 중에서 타입이 UserRepository인 빈이다.

여기서 두번째 과정에서 빈을 찾는데 UserRepository클래스가 컴포넌트스캔 등을 통해서 @Repository으로 등록되지않았거나 빈의 이름이 다른경우, 스프링 컨테이너에서는 해당 타입의 빈이 존재하지않게된다. 이럴경우에 NPE가 발생한다.

즉, 필드주입은 @Autowired로 자동주입해주는 방식이기 때문에 주입순서를 제어할수없는 문제가 있고, 필드주입에 참여하는 모든 의존성이 동시에 주입되는게 아니라 하나씩 주입되는거라서 어느 의존성이 먼저 주입될지 모른다. 따라서 필드주입을 쓸때는 의존성 사이에 순서를 조심하도록하자~


2. Setter 주입

public class UserService {
    private UserRepository userRepository;

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

    // ...
}

언뜻보면 필드주입과 유사하지만 다른점이 있다면 필드에 바로 주입하는게 아니라 빈 객체의 Setter를 호출하여 주입한다는 점이다.

물론 필드주입과 동일하게 주입할 빈이 없더라도 해당빈이생성되어 NPE 문제가 생길 수 있다.

사실 처음봄 ㅋㅋ;


3. 생성자 주입

public class UserService {
    private UserRepository userRepository;

//생성자를 선언하고 의존성을 매개변수로 선언
    public UserService(UserRepository userRepository) {
    	//의존성을 필드에 저장
         this.userRepository = userRepository;
    }

    // ...
}

주입과정은 이렇다.

  1. 의존성을 주입받을 클래스의 생성자를 선언
  2. 생성자의 매개변수로 주입받을 의존성을 선언
  3. 의존성을 필드에 저장

그래서 이게 필드주입과 달라지는건 뭘까?


필드주입과의 차이

  • 의존성을 선언할때 해당 의존성이 누락되면 컴파일시점에서 오류가 발생함 -> 필수적인 의존성에 대한 안정적인 선언이 가능
  • final 선언이 가능해서 코드 변경이 안됨 -> 불변성을 가짐
  • 테스트가 용이함
  • 순환참조를 방지함

첫번째랑 두번째는 코드에서 이해가 가는데 그럼 테스트 용이함과 순환참조방지는 뭐야? 할수있으니 좀 더 알아보자

테스트에 용이함

생성자 주입일 때

public class UserService {
    private final UserRepository userRepository;

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

    // ...
}

public class UserServiceTest {
    @Test
    public void testSomeMethod() {
        UserRepository mockRepository = Mockito.mock(UserRepository.class);
        UserService userService = new UserService(mockRepository);

        // 테스트 로직 작성
    }
}

필드 주입일 때

public class UserService {
    @Autowired
    private UserRepository userRepository;

    // ...
}

public class UserServiceTest {
    @Autowired
    private UserService userService;

    // 테스트 로직 작성
}

코드로 설명하자면 이렇다.

테스트 환경에서 생성자주입은 UserRepository의 mock 객체를 만들어서 이를 생성자를 통해서 UserService에 주입한다면 독립적으로 테스트할 수 있다. 반면, 필드주입은 @Autowired를 통해 주입된 빈을 이용해서 테스트를 해야한다. 즉, 모의 객체를 생성하기에는 까다롭다는 점이다.

순환참조 방지

public class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    // ...
}
public class UserRepository {
    private final UserService userService;
    
    public UserRepository(UserService userService) {
        this.userService = userService;
    }
    
    // ...
}

생성자 주입을 사용한 두 클래스간 동시에 의존하는 순환참조상황이 발생하고있다고 가정한다면,

public class Main {
    public static void main(String[] args) {
        UserRepository userRepository = new UserRepository(null);
        UserService userService = new UserService(userRepository);
        
        userRepository.setUserService(userService); // 순환 참조가 발생하는 코드
        
        // ...
    }
}

위 시점에서 이미 서로를 주입하는 시도를 하였지만 생성자 주입을 사용하였기에 의존성을 객체 생성시점에 한번에 주입받는다. 그렇기때문에, 순환참조가 발생할 수 있는 시점에서 이미 의존성이 주입되어있어 이를 방지할 수 있다.

만약 필드주입이나 Setter주입을 사용했다면 위의 코드에서 순환참조가 발생하여 런타임시에 문제가 생길 수 있다!


요약

필드 주입 : @Autowired로 필드에서 UserRepository를 주입
Setter 주입 : @Autowired로 Setter 메서드를 통해 UserRepository를 주입
생성자주입 : 생성자를 통해 UserRepository를 주입

앵간하면 생성자주입을 쓰자~! TEST에 용의하니깐 ㅎ

profile
후회하지 않는 사람이 되자 🔥

0개의 댓글