객체 지향 프로그래밍에서는 하나의 객체가 다른 객체를 사용하여 작업을 수행할 수 있다.
이때 다른 객체를 해당 객체의 의존성이라고 부른다.
예를 들어, 커피 주문을 처리하는 객체(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가지가 있다.
생성자주입이 좋다는데 왜좋은지는 모르겠네? 일단 셋의 차이를 보도록하자
public class UserService {
@Autowired
private UserRepository userRepository;
// ...
}
내가 평소에 쓰던 주입방식이다. 의존성을 주입하고자 하는 필드에 @Autowired
만 붙이면 되는 간결한 장점이 있다. 하지만 내가 몰랐던건 이것이다.
필드주입시에 자칫하면 NullPointerException이 생긴다는거였다. 생기는 이유에 대한 과정을 위의 코드를 통해 설명해보겠다.
UserService
클래스에서 @Autowired
가 필드에 추가되어있으므로 스프링 컨테이너는 해당 필드에 자동으로 UserRepository
의 인스턴스(userRepository)를 주입함UserRepository
인 빈이다.여기서 두번째 과정에서 빈을 찾는데 UserRepository
클래스가 컴포넌트스캔 등을 통해서 @Repository
으로 등록되지않았거나 빈의 이름이 다른경우, 스프링 컨테이너에서는 해당 타입의 빈이 존재하지않게된다. 이럴경우에 NPE가 발생한다.
즉, 필드주입은
@Autowired
로 자동주입해주는 방식이기 때문에 주입순서를 제어할수없는 문제가 있고, 필드주입에 참여하는 모든 의존성이 동시에 주입되는게 아니라 하나씩 주입되는거라서 어느 의존성이 먼저 주입될지 모른다. 따라서 필드주입을 쓸때는 의존성 사이에 순서를 조심하도록하자~
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
// ...
}
언뜻보면 필드주입과 유사하지만 다른점이 있다면 필드에 바로 주입하는게 아니라 빈 객체의 Setter를 호출하여 주입한다는 점이다.
물론 필드주입과 동일하게 주입할 빈이 없더라도 해당빈이생성되어 NPE 문제가 생길 수 있다.
사실 처음봄 ㅋㅋ;
public class UserService {
private UserRepository userRepository;
//생성자를 선언하고 의존성을 매개변수로 선언
public UserService(UserRepository userRepository) {
//의존성을 필드에 저장
this.userRepository = userRepository;
}
// ...
}
주입과정은 이렇다.
그래서 이게 필드주입과 달라지는건 뭘까?
첫번째랑 두번째는 코드에서 이해가 가는데 그럼 테스트 용이함과 순환참조방지는 뭐야? 할수있으니 좀 더 알아보자
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에 용의하니깐 ㅎ