Controller <-> Service <-> Repository로 만들어진 구조에서 Controller는 JdbcTemplate에 의존하는 중이다. 그러나, JdbcTemplate은 Reposirtory에서만 사용한다. 현재는 Controller에서 JdbcTemplate를 Repository까지 전달하는 모양새인데, Repository가 바로 JdbcTemplate를 가져올 순 없을까?
Controller가 Controller의 인스턴스가 없이도 동작할 수 있었던 것은 우리가 이 클래스를 @RestController 어노테이션을 통해 스프링 빈으로 등록을 해주었기 때문이다.
스프링 빈이란 무엇인가 함은 스프링 컨테이너 내에 있는 클래스들을 의미한다. 스프링 서버가 실행되면, 서버는 JdbcTemplate, DataSource 등 기본적인 클래스들(스프링 빈)의 인스턴스들을 생성하여 이것들을 실행하고 난 후, 우리가 추가로 설정한 스프링 빈의 인스턴스를 생성하여 이를 실행하게 된다. 이때 Controller는 JdbcTemplate에 의존하는데, 서버는 이 작업을 자동으로 설정해준다.
그래서 Controller 클래스는 JdbcTemplate을 바로 가져올 수 있었지만, Repository 클래스는 스프링 빈으로 등록되어 있지 않기에 스프링 컨테이너가 의존 관계를 자동으로 설정하지 않게 되고, 이런 이유로 Repository 클래스는 JdbcTemplate라는 스프링 빈을 바로 가져올 수 없었던 것이다.
Repository에서 JdbcTemplate을 바로 가져오기 위해서는 이 클래스도 스프링 빈으로 등록해주면 된다.
Repository는 @Repository, Service는 @Service 어노테이션을 클래스 위에 넣어주게 되면 이 클래스들도 스프링 빈으로 등록이 된다. 스프링 빈으로 등록이 되었다는 얘기는 서버가 실행될 때 스프링 컨테이너 내에서 자동으로 의존 관계들을 설정해줄 수 있다는 말이다.
우리는 그래서 JdbcTemplate은 Repository에서만 사용하기 때문에 Repository가 JdbcTemplate를 생성자 단계에서 외부에서 가져오는 것이 아니라 클래스에서 바로 가져올 수 있게 했고, Service에서도 this.userRepository가 JdbcTemplate을 인자로 가지는 Repo 객체가 아닌 바로 받아온 userRepository를 쓸 수 있고, Controller에서도 this.userService = userService로 바로 쓸 수 있는 것이다. 이 모든 것은 스프링 빈으로 등록이 되었기 때문에 서버가 자동으로 의존 관계들을 설정해주기 때문에 가능한 일이다.
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// this.userRepository = new userRepository(jdbcTemplate); 였다.
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService){
this.userService = userService;
}
//this.userService = new userService(jdbcTemplate)였다.
그렇다면 왜 굳이 스프링 컨테이너를 사용해서 의존성을 설정해줄까? 원래대로 new를 사용해서 연결해주면 안되는건가?
우선 Service와 Repository에 스프링 빈을 등록시키지 않고 Book을 저장하는 간단한 API를 구현해보겠다. 구조는 BookController, BookService, BookMemoryRepository이다.
@RestController
public class BookController {
private final BookService = new BookService();
@PostMapping("/book")
public void saveBook(@RequestBody SaveBookRequest request) {
bookService.saveBook(request);
}
}
public class BookService {
private final BookMemoryRepository bookRepository = new BookMemoryRepository();
public void saveBook(SaveBookRequest request) {
bookRepository.save(request.getName();
}
}
public class BookMemoryRepository {
private final List<String> books = new ArrayList();
public void save(String bookName) {
books.add(bookName);
}
}
이렇게 코드가 있을 때, 우리는 Memory에 Book을 저장하는 방식이 아닌 SQL을 사용해서 Book을 저장하는 Repository를 추가하려고 한다.
이때, 문제가 생긴다. 우리는 Repository의 역할만 바꾸고 싶은데, 이걸 바꾸기 위해서는 현재 구조에서 Service의 코드도 바꿔줘야 한다.
private final BookMemoryRepository bookMemoryRepository = new BookMemoryRepository();
에서
private final BookMySqlRepository bookMySqlRepository = new BookMySqlRepository();
로 바꿔줘야 한다.
지금은 Repository를 사용하는 곳이 BookService 한 곳이지만 이 Repository를 사용하는 곳이 매우 많다면 그 코드를 하나하나 다 찾아서 바꿔줘야 한다.
이를 해결하기 위해 인터페이스를 도입해본다. BookRepository라는 인터페이스를 만들고 void saveBook();을 메소드로 가지게 한다. 기존 Repository들은 이 BookRepository를 implements로 받고 saveBook()을 오버라이딩한다.
public class BookService {
private final BookRepository bookRepository = new BookMemoryRepository();
}
public class BookMemoryRepository implements BookRepository {
private final List<String> books = new ArrayList();
@Override
public void save(String bookName) {
books.add(bookName);
}
}
public class BookMySqlRepository implements BookRepository {
@Override
public void save(String bookName) {
// jdbcTemplate.....
}
}
하지만, 여전히 BookService에서 new 다음의 코드를 바꿔줘야 한다. 이러면 Repository가 사용된 곳의 코드를 바꿔야 한다는 것은 여전하다.
이러한 문제를 스프링 빈을 통해 해결할 수 있다. 스프링 컨테이너는 스프링 빈들의 의존 관계를 자동으로 설정해줄 수 있다고 했다. 그래서 @Service와 @Repository를 모두 붙이게 되면 스프링 컨테이너가 알아서 어떤 Repository를 가져오게 할 지 정해준다. 이런 방식을 제어의 역전(IoC)라고 하고, 이렇게 스프링 컨테이너가 어떤 스프링 빈을 의존관계로 설정할 것인지 결정하고 설정하는 과정을 의존성 주입(DI)라고 한다.
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public void saveBook(){
bookRepository.saveBook();
}
}
// 생성자 부분의 변경
이때, BookMemoryRepository와 BookMySqlRepository 둘 다에게 @Repository가 붙어있는데, 스프링 컨테이너는 어떤 기준으로 의존관계를 설정할까?
단순히 둘다 @Repository만 붙이게 되면 스프링 컨테이너도 어떤 Repository를 선택할 지 모르지만, @Primary 를 추가로 붙여주게 되면, 이 클래스를 우선해서 설정해라 라는 의미를 가지게 할 수 있다. 스프링 컨테이너는 이 @Primary가 붙은 Repository를 Service가 의존하게 만들 수 있다.
이렇게 스프링 컨테이너를 사용하게 되면 Service의 코드를 전혀 바꾸지 않고도 Repository의 기능을 바꾸게 할 수 있다.
@Configuration
@Bean
//Configuration은 config 패키지 내에서 만든다.
@Configuration
public class UserConfiguration {
@Bean
public UserRepository userRepository(JdbcTemplate jdbcTemplate){
return new UserRepository(jdbcTemplate);
}
@Bean
public UserService userService(UserRepository userRepository){
return new UserService(userRepository);
}
}
이렇게 만들면 userRepository 메소드가 UserRepository를 반환할 때 @Bean이 이 객체를 스프링 빈에 등록한다. 그래서 @Bean 옆의 아이콘을 누르면 UserService의 생성자에 있는 UserRepository로 이동하게 된다. 이는 곧 userRepository가 UserService로 의존 관계가 설정되었다는 말이다. userService도 아이콘을 눌러보면 userController로 가기에 Controller로 의존성이 잘 연결되었다는 것을 알 수 있다.
그렇다면 언제 @Service, @Repository를 쓰고 언제 @Configuration, @Bean을 쓸까?
@Service와 @Repository는 개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때 사용한다. 우리가 만든 Service와 Repository는 @Service와 @Repository를 다는 게 올바르다.
@Configuration과 @Bean은 외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용한다.
다음으로 다룰 어노테이션은 @Component이다.
@Component
@Repository나 @Service, @RestController의 속으로 들어가보면 @Component가 들어있다. 이 어노테이션 덕분에 자동으로 감지될 수 있었다.
이런 @Component는 1) 컨트롤러, 서비스, 리포지토리가 모두 아니고 2) 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용하기도 한다.
@RestController
public class UserController {
@Autowired
private UserService userService;
...
이렇게 필드에 바로 @Autowired를 붙여서 사용할 수도 있지만, 테스트를 어렵게 만드는 요인이 될 수 있다.
마지막으로 @Qualifier를 알아본다.
@Qualifier는 @Primary와 유사하게 여러 개의 스프링 빈이 있고, 이를 IoC 해야하는 상황에서 어떤 스프링 빈을 연결할 것인지를 고민할 때 사용하는 어노테이션이다. @Qualifier는 스프링 빈을 사용하는 쪽에서 등록된 스프링 빈 이름으로 직접 지정하거나(
@RestController
public class Controller{
public UserController(@Qualifier("UserService")){
...
)
사용하는 쪽, 등록하는 쪽 모두에서 특별한 이름을 같이 써서 서로 연결되게 할 수 있다.
@RestController
public class Controller{
public UserController(@Qualifier("main")){
...
@Qualifier("main")
@Service
public class UserService{
public UserService(){
...
@Primary VS @Qualifier
그렇다면 한쪽에서는 @Primary가 달려있고, 다른 한 쪽에서는 @Qualifier로 연결시켜놨다면 스프링 컨테이너는 어느 것을 우선시할까?
정답은 사용하는 쪽에서 직접 적어준 @Qualifier가 이긴다.