기존의 UserController의 과도한 역할을 3단 분리를 통해 Controller -> Service -> Repository 형태로 만들었다.
메서드마다 JdbcTemplate를 매개변수로 넣어줘야 하는 번거로움은 생성자를 통해 해결하였다.
각 클래스마다 생성자에 new 연산자를 통해 Service는 Repository의 인스턴스를, Controller는 Service의 인스턴스를 받았다.
그렇다면, Controller의 인스턴스는 어떻게 생성되었을까?
그리고 Controller는 JdbcTemplate가 없으면 작동할 수 없는데 JdbcTemplate이라는 클래스를 설정해준 적이 없다. 어떻게 JdbcTemplate를 가져온 것일까?
Spring Bean을 통해 가능했던 것이다.
서버가 시작이 되면 스프링 컨테이너가 만들어지는데 내부에는 여러 클래스들이 들어가게 된다.
이때, 스프링 컨테이너 내부에 있는 클래스를 Spring Bean이라고 한다.

스프링 컨테이너의 역할은 이 Spring Bean들끼리 연결시켜주는 것으로, JdbcTemplate을 자동으로 UserController와 연결시킨 것이다.
@RestController 에너테이션이 API의 진입지점이라고 앞에서 설명한 적이 있다.
이 에너테이션이 바로 Spring Bean으로 등록해주는 역할도 한다.
Controller 외에도 Service, Repository도 에너테이션을 사용하면 Spring Bean으로 등록이 가능하다.
UserService와 UserRepository에 각각 @Service, @Repository를 추가해주면 된다.
Spring Bean으로 등록되었다는 것은 서로 연결이 가능하다는 것이기 때문에 기존의 new 연산자는 필요 없어진다. 스프링 컨테이너가 자동으로 연결해주기 때문이다.
그리고 Repository가 JdbcTemplate을 가지고 있기 때문에 생성자를 통해 넘겨받는 Service,Controller는 JdbcTemplate을 가지고 있을 필요가 없어진다.
코드를 수정해보자.
//UserController
@RestController
public class UserController {
private final UserService userService;
public UserController(JdbcTemplate jdbcTemplate) {
this.userService = new UserService(jdbcTemplate);
}
}
//UserService
public class UserService {
UserRepository userRepository;
public UserService(JdbcTemplate jdbcTemplate){
this.userRepository = new UserRepository(jdbcTemplate);
}
}
//UserRepository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
Spring Bean 등록하면
//UserController
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
//UserService
@Service
public class UserService {
UserRepository userRepository;
public UserService(UserRepository userRepository){
this.userRepository = userRepository;
}
}
//UserRepository
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
자, 정리해보자.
기존에는 모든 클래스가 JdbcTemplate을 필요로 하였고 Controller를 제외하고는 Spring Bean으로 등록하지 않았기 때문에 new 연산자를 통해 인스턴스를 생성해야 했다.
Spring Bean으로 등록하고 난 후에는 자동으로 연결되기 때문에 new 연산자가 필요없어졌고, Controller -> Service -> Repository -> JdbcTemplate 순으로 의존하기 때문에 Controller와 Service는 JdbcTemplate을 가지고 있을 필요가 없어졌다.
스프링 컨테이너를 사용하면 Spring Bean으로 등록된 각 클래스들을 연결할 수 있다.
BookController, BookService, BookRepository를 통해 책을 저장하는 API를 만들고 스프링 컨테이너를 활용하는 과정을 알아보자.
User와 비슷하게 Book에 대한 클래스와 save 메서드를 만들어보자.
BookService
BookMemoryRepository
SaveBookRequest : PostMapping 간 request 객체를 받기 위해 만든 SaveBookRequest 클래스
이 saveBook API도 Controller -> Service -> Repository 순으로 분리하였고 여기서 Repository로 MemoryRepository를 사용하였는데 DB를 사용하지 않고 List에 bookName을 저장하는 방식이다.
이 방식을 MySQL에 저장하는 방식으로 바꿔보자.
가장 먼저 BookMemoryRepository에서 BookMysqlRepository로 변경되야 한다.
그러면 BookService에서도
private final BookMemoryRepository bookMemoryRepository = new BookMemoryRepository();
이 부분을 바꿔줘야 한다.
private final BookMysqlRepository bookMysqlRepository = new BookMysqlRepository();
여기서 문제는 Repository를 Memory로 사용할지, MySQL로 할지에 따라 BookService를 계속 변경해줘야 한다는 것이다.
BookService 변경을 최소화 할 수는 없을까?
Interface를 사용하면 가능하다. Interface의 다형성을 기억하는가?
같은 인터페이스를 구현하고 있으면 참조변수 타입으로서 하나의 인터페이스로 전부 사용할 수 있다.
(BookRepository를 구현한 클래스들은 save 메서드를 구현해야 한다)
BookMemoryRepository로 인터페이스 구현
BookMysqlRepository로 인터페이스 구현
마찬가지로 Repository를 인터페이스로 만들고 MemoryRepository든, MysqlRepository든 이 인터페이스를 구현하면 BookService의 변경을 다음과 같이 최소화가 가능하다.
//BookMemoryRepository를 사용 시
private final BookRepository bookRepository = new BookMemoryRepository();
//BookMysqlRepository를 사용 시
private final BookRepository bookRepository = new BookMysqlRepository();
하지만 아직 부족하다. 여전히 BookServiced에서 new 연산자를 변경해야 하는 소요가 있다. 지금이야 Repository가 2개지만 몇 100개가 되면 감당이 안된다.
BookService의 코드를 바꾸지 않은채 Repository를 변경하려면 스프링 컨테이너가 답이다.
스프링 컨테이너를 사용한다면 BookMemoryRepository와 BookMysqlRepository 중 하나를 선택하여 BookService의 인스턴스를 만들어 줄 것이다.
이를 제어의 역전이라고 한다.
이 때 컨테이너가 두 Repository 중 하나를 선택하여 BookService에 넣어주는 과정을 의존성 주입이라고 한다.
우선 Servie와 Repository에 에너테이션을 추가하여 Spring Bean으로 등록하자.
각각 @Service, @Repository를 추가해준다.
컨테이너 입장에서 Repository를 선택하려면 우선 순위가 필요하며, 기준이 된다.
우선권은 동시에 부여가 되지 않으므로 테스트로 MemoryRepository에 우선권을 부여해보자.
BookMemoryRepository
BookMysqlRepository
BookController와 BookService도 Spring Bean으로 등록되었기 때문에 new 연산자 방식이 아닌 생성자 방식으로 의존하도록 변경해야 한다.
//BookController
@RestController
public class BookController {
private final BookService bookService;
public BookController(BookService bookService){
this.bookService = bookService;
}
@PostMapping("/book")
public void saveBook(@RequestBody SaveBookRequest request){
bookService.saveBook(request);
}
}
//BookService
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository){
this.bookRepository = bookRepository;
}
public void saveBook(SaveBookRequest request){
bookRepository.save(request.getName());
}
}
앞에서 @RestController, @Service, @Repository 에너테이션을 추가하여 스프링 빈으로 등록하고 의존성을 주입했었다.
이 외에도 몇 가지 에너테이션을 통해 Spring Bean으로 등록이 가능하다.
@Configuration, @Bean 에너테이션은 이미 만들어져 있는 외부 라이브러리나 프레임워크에서 만든 클래스를 등록할 때 사용한다.
@Configuration은 클래스에 추가하는 에너테이션으로 @Bean과 함께 사용된다. @Bean은 메서드에 추가되어 메서드에서 반환하는 객체를 Bean으로 등록한다.
다음 예시를 보자.
@Configuration
public class UserConfiguration {
@Bean
public UserRepository userRepository(JdbcTemplate jdbcTemplate) {
return new UserRepository(jdbcTemplate);
}
}
UserConfiguration이라는 클래스에 @Configuration을 추가하고 UserRepository 객체를 반환하는 메서드에 @Bean을 추가하여 반환하는 객체를 Bean으로 등록할 수 있다.
그렇다면 UserController, UserService, UserRepository 모두 이 방식으로 등록해야 할까?
이 세 클래스는 개발자가 직접 만든 클래스들이기 때문에 @RestController, @Service, @Repository로 사용하는 것이 더 좋다.
JdbcTemplate 같은 외부에서 만들어진 클래스는 @Configuration, @Bean으로 등록되어 있다.
@Component가 추가된 클래스는 컴포넌트로 간주되어 스프링 서버가 뜰 때 자동으로 감지된다.
앞에서 사용한 @RestController, @Service, @Repository에도 @Component를 가지고 있어 지금까지 스프링 서버 실행 시 자동으로 감지되었던 것이다.

주입받는 쪽에서 특정 클래스를 선택할 수 있게 해주는 에너테이션이다.
@Primary를 언급했었는데 기능이 유사하다.
이해하기 쉽게 간단히 예시를 들겠다.
Movable 인터페이스를 구현하는 Car, Plane, Train 클래스가 있다고 가정하자.
EquipmentController 클래스는 Movable 인터페이스를 주입받고자 한다.
생성자 매개변수로 Movable을 넣었지만 컴파일 에러가 발생한다. 왜일까?
이를 구현하는 클래스가 3개이기 때문에 어느 것을 주입받을 지 선택하지 못했기 때문이다. 이 때 컴파일 에러를 해결하는 방법은 3가지 있다.
첫 번째는 @Primary를 사용하는 것이다. 이건 앞에서 다뤘으니 생략하겠다.
두 번재는 @Qulifier를 추가하고 클래스 이름을 지정하는 것이다.
@RestController
class EquipmentController{
private final Movable movable;
public EquipmentController(@Qulifier("Car") Movable movable){
this.movable = movable;
}
}
Car 클래스를 지정하였기 때문에 Movable에는 Car 클래스가 들어오게 되어 Bean으로 등록된다.
세 번째 방법은 두 번째와 비슷한데 사용하는 쪽과 등록되는 쪽 모두 같은 특정 이름을 지정하여 에너테이션을 추가하는 것이다.
@RestController
class EquipmentController{
private final Movable movable;
public EquipmentController(@Qulifier("main") Movable movable){
this.movable = movable;
}
}
@Service
@Qulifier("main")
class Car implements Movable{}
Car 클래스와 EquipmentController 모두 Qulifier에 같은 이름을 지정하였기 때문에 Car 클래스가 등록된다.
그렇다면 @primary가 있는 클래스와 @Qulifier가 있는 클래스가 존재할 경우 어느 것을 우선할까?
스프링은 사용자가 직접 명시한 것에 우선순위를 두기 때문에 @Qulifier가 우선하게 된다.
Spring Bean을 주입받는 방법에는 대표적으로 3가지가 있다. 생성자 사용, Setter 사용, 필드에 직접 주입하는 방법이 있는데 생성자 사용에 의한 주입을 권장한다.
지금까지 사용해왔던 방법이 생성자를 이용한 방법이다.
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
두 번째 방법인 Setter에 의한 방법은 생성자를 사용하지 않고 클래스에 Setter메서드를 생성하고 @Autowired를 추가하는 것이다.
public class UserRepository {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate){
this.jdbcTemplate = jdbcTemplate;
}
}
이때 필드의 final 제어자는 제거해주어야 한다.
마지막 방법은 필드에 직접 주입받는 방법이다.
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
}
Setter가 비슷하게 필드의 final을 제거하고 @Autowired를 필드에 직접 추가해주면 된다.
Setter에 의한 방법은 타인이 Setter를 사용해 다른 인스턴스로 교체하여 동작에 문제가 생길 수 있고, 필드에 직접 주입받는 방법은 테스트가 어렵기 때문에 권장되지 않는다.