Search

3. 역할의 분리와 스프링 컨테이너

1. 좋은 코드가 왜 중요한지 이해하고, 원래 있던 Controller 코드를 보다 좋은 코드로 리팩토링한다.
2. 스프링 컨테이너와 스프링 빈이 무엇인지 이해한다.
3. 스프링 컨테이너가 왜 필요한지, 좋은 코드와 어떻게 연관이 있는지 이해한다.
4. 스프링 빈을 다루는 여러 방법을 이해한다

좋은 코드(Clean Code)는 왜 중요한가?

CODE

코드는 요구사항을 표현하는 언어이다.
개발자는 요구사항을 구현하기 위해 코드를 읽고 작성한다. 코드를 읽는 것은 필수적이고 피할 수 없다
위에 소스는 가독성도 안좋고 지저분하다
밑의 소스처럼 변경하면
코드만 보고도 의미를 파악할 수 있다.

그렇다면 Controller에서 모든 기능을 구현하면 왜 안될까?!

<Clean Code>
함수는 최대한 작게 만들고 한 가지 일만 수행하는 것이 좋다.
클래스는 작아야 하며 하나의 책임만을 가져야 한다.

우리가 작성한 Controller 함수 1개가 3000줄을 넘으면?!

1. 그 함수를 동시에 여러 명이 수정할 수 없다.
2. 그 함수를 읽고, 이해하는 것이 너무 어렵다.
3. 그 함수의 어느 부분을 수정하더라도 함수 전체에 영향을 미칠 수 있기 때문에 함부로 건들 수 없게 된다.
4. 너무 큰 기능이기 때문에 테스트도 힘들다.
5. 종합적으로 유지보수성이 매우 떨어진다.
3가지 역할을 하고 있다.
이 함수를 3가지 역할에 맞게 3단 분리 하도록 하자.

Controller를 3단 분리하기 - Service와 Repository

Controller의 함수 1개가 하고 있던 역할

1. API의 진입 지점으로써 HTTP Body를 객체로 변환하고 있다.
Controller 역할
2. 현재 유저가 있는지, 없는지 등을 확인하고 예외 처리를 해준다.
Service 역할
3. SQL을 사용해 실제 DB와의 통신을 담당한다.
Repository 역할

Service 역할

package com.group.libraryapp.controller.user; @RestController public class UserController { . . private final UserService userService = new UserService(); . . @PutMapping("/user") public void updateUser(@RequestBody UserUpdateRequest userUpdateRequest){ userService.updateUser(jdbcTemplate,userUpdateRequest); } . . . }
Java
복사
package com.group.libraryapp.service.user; public class UserService { public void updateUser(JdbcTemplate jdbcTemplate, UserUpdateRequest userUpdateRequest){ //조회 String readSql = "SELECT * FROM user WHERE id = ?"; //해당 id로 조회된 데이터 있는지 확인 boolean isUserNotExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, userUpdateRequest.getId()).isEmpty(); if(isUserNotExist){ throw new IllegalArgumentException(); } //업데이트 String sql = "UPDATE user SET name = ? WHERE id = ?"; jdbcTemplate.update(sql, userUpdateRequest.getName(), userUpdateRequest.getId()); } }
Java
복사

Repository 역할

package com.group.libraryapp.service.user; public class UserService { private final UserRepository userRepository = new UserRepository(); public void updateUser(JdbcTemplate jdbcTemplate, UserUpdateRequest userUpdateRequest){ if(userRepository.isUserNotExist(jdbcTemplate, userUpdateRequest.getId())){ throw new IllegalArgumentException(); } userRepository.updateUserName(jdbcTemplate, userUpdateRequest.getName(), userUpdateRequest.getId()); } }
Java
복사
package com.group.libraryapp.repository; public class UserRepository { //해당 id로 조회된 데이터 있는지 확인 public boolean isUserNotExist(JdbcTemplate jdbcTemplate, long id){ String readSql = "SELECT * FROM user WHERE id = ?"; return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty(); } //업데이트 public void updateUserName(JdbcTemplate jdbcTemplate, String name, long id){ String sql = "UPDATE user SET name = ? WHERE id = ?"; jdbcTemplate.update(sql, name, id); } }
Java
복사

3가지 역할로 구분된 구조

어려운 용어로 Layered Architecture라고 한다!

하나 문제점이 있다

바로 JdbcTemplate은 UserRepository에서만 쓰는데 자꾸 컨트롤러나 서비스에서 해당 JdbcTemplate 받아서 넘겨주고 있다.
UserRepository
public class UserRepository { private final JdbcTemplate jdbcTemplate; public UserRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } . . }
Java
복사
UserService
package com.group.libraryapp.service.user; public class UserService { private final UserRepository userRepository; public UserService(JdbcTemplate jdbcTemplate) { userRepository = new UserRepository(jdbcTemplate); } . . . }
Java
복사
package com.group.libraryapp.controller.user; @RestController public class UserController { . . private final UserService userService ; public UserController(JdbcTemplate jdbcTemplate){ this.jdbcTemplate = jdbcTemplate; this.userService = new UserService(jdbcTemplate); } . . }
Java
복사
그러고 파라매터로 넘겨준 JdbcTemplate를 다빼주면 된다
@PutMapping("/user") public void updateUser(@RequestBody UserUpdateRequest userUpdateRequest){ userService.updateUser(userUpdateRequest); }
Java
복사
public void updateUser(UserUpdateRequest userUpdateRequest){ if(userRepository.isUserNotExist(userUpdateRequest.getId())){ throw new IllegalArgumentException(); } userRepository.updateUserName(userUpdateRequest.getName(), userUpdateRequest.getId()); }
Java
복사
//해당 id로 조회된 데이터 있는지 확인 public boolean isUserNotExist(long id){ String readSql = "SELECT * FROM user WHERE id = ?"; return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty(); } //업데이트 public void updateUserName(String name, long id){ String sql = "UPDATE user SET name = ? WHERE id = ?"; jdbcTemplate.update(sql, name, id); }
Java
복사

이제 DELETE, POST, GET API도 변경해보자!

UserController
@DeleteMapping("/user") public void deleteUser(@RequestParam String name){ userService.deleteUser(name); }
Java
복사
UserService
public void deleteUser(String name){ //조회 String readSql = "SELECT * FROM user WHERE name = ?"; //해당 name 로 조회된 데이터 있는지 확인 boolean isUserNotExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, name).isEmpty(); if(isUserNotExist){ throw new IllegalArgumentException(); } String sql = "DELETE FROM user WHERE name = ?"; jdbcTemplate.update(sql, name); }
Java
복사
서비스로 옮긴 소스를 이제 또 레파지토리로 옮겨주자
UserService
public void deleteUser(String name){ if(userRepository.isUserNotExist(name)){ throw new IllegalArgumentException(); } userRepository.deleteUser(name); }
Java
복사
UserRepository
public boolean isUserNotExist(String name){ //조회 String readSql = "SELECT * FROM user WHERE name = ?"; //해당 name 로 조회된 데이터 있는지 확인 return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, name).isEmpty(); } public void deleteUser(String name){ String sql = "DELETE FROM user WHERE name = ?"; jdbcTemplate.update(sql, name); }
Java
복사

생성 API 수정

UserController
@PostMapping("/user") // POST /user public void saveUser(@RequestBody UserCreateRequest request) { userService.saveUser(request); }
Java
복사
UserService
public void saveUser(UserCreateRequest userCreateRequest){ String sql = "INSERT INTO user(name, age) VALUES (?,?)"; jdbcTemplate.update(sql, request.getName(),request.getAge()); }
Java
복사
이렇게 가져오고 다시 레포지토리에 해당 부분을 옮겨주고
레파지토리 호출로 바꿔준다
public void saveUser(UserCreateRequest userCreateRequest){ userRepository.saveUser(userCreateRequest.getName(), userCreateRequest.getAge()); }
Java
복사
UserRepository
public void saveUser(String name, Integer age){ String sql = "INSERT INTO user(name, age) VALUES (?,?)"; jdbcTemplate.update(sql, name, age); }
Java
복사

조회 API 수정

UserController
@GetMapping("/user") public List<UserResponse> getUser(){ return userService.getUsers(); }
Java
복사
UserService
public List<UserResponse> getUsers(){ return userRepository.getUsers(); }
Java
복사
UserRepository
public List<UserResponse> getUsers(){ String sql = "SELECT * FROM user"; return jdbcTemplate.query(sql, (rs, rowNum) -> { long id = rs.getLong("id"); String name = rs.getString("name"); int age = rs.getInt("age"); return new UserResponse(id, name, age); }); }
Java
복사
서버 재시동하고 사이트에서 정상 작동하는지 확인해주자.

한가지 의문점

컨트롤러 생성자에서
public UserController(JdbcTemplate jdbcTemplate){ // JDBC 템플릿을 받아서 생성자를 만듦. this.jdbcTemplate = jdbcTemplate; this.userService = new UserService(jdbcTemplate); }
Java
복사
우리는 Controller controller = new Controller(jdbcTemplate)를 하지않는데
어디서 JdbcTemplate를 받아오는걸까?
그리고 지금은 컨트롤러 → 서비스 → 레파지토리로 주고 있는데
그냥 바로 레파지토리에서 JdbcTemplate를 가져올 수 없을까?

UserController와 스프링 컨테이너

UserController의 의아한 점

[1] static이 아닌 코드를 사용하려면 인스턴스화가 필요하다.
도대체 누가 UserController를 인스턴스화 하고 있는것인가?
[2] UserController는 JdbcTemplate이 필요하다.
즉, UserController는 JdbcTemplate에 의존하고 있다.

UserController는 JdbcTemplate에 의존하고 있다.

UserController는 JdbcTemplate이 없으면 동작하지 않는다!
그런데 우리는 JdbcTemplate이란 클래스를 설정해준 적이 없다. 인스턴스화 하지도 않았다.
UserController는 어떻게 JdbcTemplate을 가져올 수 있었을까?
UserController 클래스를 API의 진입지점으로 만들 뿐 아니라,
UserController 클래스를 스프링 빈으로 등록시킨다!

스프링 빈이란?

서버가 시작되면, 스프링 서버 내부에 거대한 컨테이너를 만들게 된다!
컨테이너 안에는 여러 클래스가 들어가게 된다!!
이때 다양한 정보도 함께 들어있고, 인스턴스화도 이루어진다!
즉, UserController를 인스턴스화 한 적이 없고 생성자를 한번도 호출해준 적이 없는 부분이 해결이 된다.
서버가 시작될 때, 거대한 컨테이너 공간이 생기고, 그 공간 안에 여러 클래스들이 들어가는데 그 클래스를 spring bean 이라고 한다.
잠깐! 그런데 UserController를 인스턴스화 하려면 JdbcTemplate이 필요하다!
사실 이 JdbcTemplate도 스프링 빈으로 등록되어 있다!
JdbcTemplate도 컨테이너 안에 있고 UserController도 컨테이너 안에 있으니까 UserController 인스턴스화 할 때 JdbcTemplate도 넣어주는거다.

JdbcTemplate은 누가 스프링 빈으로 등록해주었지?!

비밀은 바로 설정해놓은 의존성 덕분이다.
옆에 화살표 쪽에 콩 모양의 이미지를 보여준다. 이걸 누르면
JdbcTemplate을 누군가가 코딩한 소스를 볼 수 있다.
이 파일의 위치가
spring boot autoconfigure에 있다.
build.gradle에 있는 start들에 의해서 프로젝트 외부 의존성(라이브러리나 프레임워크)으로 설정된거다.
dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' . . . }
XML
복사
즉 결국, build.gradle를 통해서 dependency, 즉 의존성을 설정했고 이 의종성에 의해 JdbcTemplate이 누군가 코딩해놓은 스프링 빈으로 등록하라는 코딩에 의해 스프링 컨테이너 안에 JdbcTemplate이 들어가게 된거다.

스프링 컨테이너는 필요한 클래스를 연결해준다!

build.gradle에 설정해서 넣어준 Jdbc Template과
직접 @RestController 어노테이션을 붙여서 스프링컨테이너 안에 들어가 있다.
스프링컨테이너의 역할은 서로 필요한 관계가 있는 스프링 빈 끼리 연결을 시켜준다.

정리

서버가 시작되면 다음과 같은 일이 일어난다.

[1] 스프링 컨테이너(클래스 저장소)가 시작된다!
[2] 기본적으로 많은 스프링 빈들이 등록된다
[3] 우리가 설정해준 스프링 빈이 등록된다!
[4] 이때 필요한 의존성이 자동으로 설정된다!

그렇다면 왜 UserRepository는 JdbcTemplate을 가져오지 못할까?!

JdbcTemplate을 가져오려면 UserRepository가 스프링 빈이어야 하는데 UserRepository는 스프링 빈이 아니다!!!
그래서 UserRepository를 spring bean으로 등록해보자.
아직 콩 모양이 없을 때
@Repository 추가해주니까 좌측에 콩 모양이 생겼다. 그러면 스프링 빈으로 등록된거다.
UserService에는 @Service를 붙여주자.
UserRepository에는 @Repository 붙여주고 나서 JdbcTemplate을 바로 갖고 올 수 있다.
UserService에는 @Service를 붙여주면 이제 UserRepository를 만들 때 굳이 JdbcTemplate을 가져다 줄 필요가 없다.
그래서 UserService의 소스가
//변경 전 public UserService(JdbcTemplate jdbcTemplate) { userRepository = new UserRepository(jdbcTemplate); } //변경 후 public UserService(UserRepository userRepository ){ this.userRepository = userRepository; }
Java
복사
이렇게 변경된다.
UserController도 변경해주면
//변경전 private final UserService userService ; private final JdbcTemplate jdbcTemplate; public UserController(JdbcTemplate jdbcTemplate){ this.jdbcTemplate = jdbcTemplate; this.userService = new UserService(jdbcTemplate); } //변경 후 private final UserService userService ; public UserController(UserService userService){ this.userService = userService; }
Java
복사

이제 3개의 클래스는 서버가 시작할 때 다음 순서를 따른다!

스프링 컨테이너가 시작되고 기본적인 스프링 빈들이 등록된다.
JdbcTemplate을 의존하는 UserRepository가 스프링 빈으로 등록된다. 그러면서 인스턴스화 된다.
UserRepository를 의존하는 UserService가 스프링 빈으로 등록된다.
UserService를 의존하는 UserController가 스프링 빈으로 등록된다.

자 그런데 한 가지 궁금한 점이 자연스럽게 떠오른다.

뭔가 더 좋아진 것 같긴한데... 스프링 컨테이너를 왜 사용하는 걸까?! 그냥 new 연산자를 쓰면 안되나?!

스프링 컨테이너를 왜 사용할까?

다음 요구사항을 생각해보자.

책 이름을 메모리에 저장하는 API를 매우 간단하게 구현하라!
Service, Repository는 Spring Bean이 아니어야 한다.

구현된 그림

book 패키지를 생성해서 컨트롤러, 서비스, 레파지토리 생성
package com.group.libraryapp.controller.book; public class BookController { }
Java
복사
package com.group.libraryapp.repository.book; public class BookRepository { }
Java
복사
package com.group.libraryapp.service.book; public class BookService { }
Java
복사
이제 간단하게 만들어보자.
BookController
package com.group.libraryapp.controller.book; @RestController public class BookController { private final BookService bookService = new BookService(); @PostMapping("/book") public void saveBook(){ bookService.saveBook(); } }
Java
복사
BookService
package com.group.libraryapp.service.book; public class BookService { private final BookMemoryRepository bookRepository = new BookMemoryRepository(); public void saveBook(){ bookRepository.saveBook(); } }
Java
복사
BookMemoryRepository
package com.group.libraryapp.repository.book; public class BookMemoryRepository { //private final List<Book> books = new ArrayList<Book>(); public void saveBook(){ //books.add(new Book()); } }
Java
복사

추가 요구사항을 구현하는 상상

이제 Memory가 아니라 MySQL과 같은 DB를 사용해야 한다!
JdbcTemplate은 Repository가 바로 설정할 수 있다고 해보자!

구현된 그림

지금은 현재 이렇게 구현되어 있는데 추가요건에 따라서 MtySql 레파지토리로
연결해주면
기존 Memory부분은 주석처리되고 MySql에 대한 레파지토리로 넣어주면 된다

어딘가 부족하다!!

데이터를 메모리에 저장할지~ MySQL에 저장할지~ Repository의 역할에 관련된 것만 바꾸고 싶은데 BookService까지 바꿔야 한다!
어떻게 하면 Repository를 다른 Class로 바꾸더라도 BookService를 변경하지 않을 수 있을까?!
Java의 interface를 활용해보자!!

구현된 그림

BookRepository를 생성
package com.group.libraryapp.repository.book; public interface BookRepository { void saveBook(); }
Java
복사
BookMemoryRepository
package com.group.libraryapp.repository.book; public class BookMemoryRepository implements BookRepository { //private final List<Book> books = new ArrayList<Book>(); public void saveBook(){ //books.add(new Book()); } }
Java
복사
BookService
package com.group.libraryapp.service.book; public class BookService { private final BookRepository bookRepository = new BookMemoryRepository(); public void saveBook(){ bookRepository.saveBook(); } }
Java
복사
만약 이상태에서 MySql로 교체해줘야 한다면
BookMySqlRepository를 생성하고
package com.group.libraryapp.repository.book; public class BookMySqlRepository implements BookRepository{ @Override public void saveBook() { } }
Java
복사
BookService 에서 구현체만 바꿔주면 된다
package com.group.libraryapp.service.book; public class BookService { private final BookRepository bookRepository = new BookMySqlRepository(); public void saveBook(){ bookRepository.saveBook(); } }
Java
복사
BookService의 변경 범위가 줄어들긴 했지만, 아직은 아쉽다!
그래서 등장한 스프링 컨테이너!

스프링 컨테이너 사용하게 된다면!

스프링 컨테이너를 사용하면 컨테이너가 BookService를 대신 인스턴스화 하고, 그 때 알아서 BookRepository를 결정해준다!
두 개의 클래스가 스프링 컨테이너 안에 들어 있다.
이때 서비스도 컨테이너 안에 있는데
이때 북서비스를 인스턴스화 하는 과정에서 어떤 클래스를 쓸지 스프링 컨테이너가 선택한다면
코드를 한 줄도 안바꾸도 다른 클래스로 바꿀 수 있다.
이제 서비스에 @Service를 붙여주고
컨트롤러에는 생성자를 만들어서 스프링 빈을 주입받게 한다
BookController
private final BookService bookService; public BookController(BookService bookService) { this.bookService = bookService; }
Java
복사
BookMemoryRepository이랑 BookMySqlRepository에 @Repository를 붙여준다.
그러면 서비스에서도 생성자를 통해서 레파지토리를 주입받게 해준다
BookService
private final BookRepository bookRepository; public BookService(BookRepository bookRepository) { this.bookRepository = bookRepository; }
Java
복사
이제 스프링 컨테이너가 대신 결정해서 쓴다
이런 방식을 제어의 역전(IoC, Inversion of Control)이라 한다.
컨테이너가 선택해 BookService에 넣어주는 과정을 의존성 주입(DI, Dependency Injection)라고 한다.

그렇다면 둘 중 어떤 Repository가 주입될까?!

둘다 레파지토리를 붙여주는 순간 서비스쪽에 에러가 났다. 스프링도 어떤걸 써야할지 몰라서 오류가 나는거다.
하나를 선택해서 알려줘야한다. 써야하는 레파지토리가 있으면 @Primary 사용하면 된다.
우리가 @Primary를 활용해서 조절할 수 있다!!
@Primary : 우선권을 결정하는 어노테이션
BookMemoryRepository에다가 @Primary를 붙여주고 sysout 찍어보자
package com.group.libraryapp.repository.book; @Primary @Repository public class BookMemoryRepository implements BookRepository { public void saveBook(){ System.out.println("BookMemoryRepository"); } }
Java
복사

스프링 컨테이너를 다루는 방법

빈을 등록하는 방법

@Configuration

클래스에 붙이는 어노테이션
@Bean을 사용할 때 함께 사용해 주어야 한다!

@Bean

메소드에 붙이는 어노테이션
메소드에서 반환되는 객체를 스프링 빈에 등록한다

UserRepository에 @Bean을 사용해보자!

UserRpository에 @Repository 준거를 없애주자
UserConfiguration을 config 패키지 안에 생성하고 JdbcTemplate 반환하는 메소드를 만들어주면서
위에 @Bean 어노테이션을 붙여주면
package com.group.libraryapp.config; @Configuration public class UserConfiguration { @Bean public UserRepository userRepository(JdbcTemplate jdbcTemplate){ return new UserRepository(jdbcTemplate); } }
Java
복사
좌측에 바깥 화살표 콩이랑 들어오는 화살표 콩 이미지가 표시된다
들어오는 표시의 화살표는 스프링 컨테이너가 관리하는 JdbcTemplate이 들어온다는 의미고
빠져 나간다는 의미는 UserService의 생성자에 스프링 컨테이너가 빈 어노티에션을 통해 등록 UserRepository를 주입해준다는 의미

언제 @Service, @Repository를 사용해야 할까?!

개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때!

언제 @Configuration + @Bean을 사용해야 할까?!

외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때!
JdbcTemplate 보면 @Bean하고 @Configuration으로 만들어짐

그렇다면 UserService, UserRepository는?!

@Service, @Repository를 사용하는 것이 좋다

다음으로 살펴볼 어노테이션은

@Component

주어진 클래스를 컴포넌트로 간주한다.
이 클래스들은 스프링 서버가 뜰 때 자동으로 감지된다.

사실 이 @Component는 지금까지 숨어있었다!!

@Component 덕분에 우리가 사용했던 어노테이션이 자동감지 되었다!
레파지토리 어노테이션을 타고 가면 @Component가 있다
서비스 어노테이션도 마찬가지
@RestController 안에 있는 @Controller 안에 Component가 있다

@Component는 언제 사용하는가?!

1) 컨트롤러, 서비스, 리포지토리가 모두 아니고
2) 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용되기도 한다.

이제 스프링 빈을 주입 받는 방법을 살펴보자!

스프링 빈을 주입 받는 몇 가지 방법

(가장 권장) 생성자를 이용해 주입받는 방식
@Autowired //생략가능 public UserController(UserService userService){ this.userService = userService; }
Java
복사
두 번째 방법 – setter와 @Autowired 사용
세 번째 방법 – 필드에 직접 @Autowired 사용
1. 생성자 사용 (@Autowired 생략 가능)
2. setter 사용
누군가 setter를 사용하면 오작동할 수 있다
3. 필드에 바로 사용
테스트를 어렵게 만드는 요인이다.

마지막으로 @Qualifier를 알아보자!

package com.group.libraryapp.service.fruit; public interface FruitService { }
Java
복사
package com.group.libraryapp.service.fruit; @Service public class AppleService implements FruitService{ }
Java
복사
package com.group.libraryapp.service.fruit; @Service public class BananaService implements FruitService{ }
Java
복사
package com.group.libraryapp.service.fruit; @Service public class OrangeService implements FruitService{ }
Java
복사
이렇게 만들고 UserController에
private final UserService userService ; private final FruitService fruitService; public UserController(UserService userService, FruitService fruitService){ this.userService = userService; this.fruitService = fruitService; }
Java
복사
이렇게 해주면 오류난다.. 왜냐면 @Service가 3개니까 어떤걸 선택할지 몰라서 나는 오류인데
이럴때 @Qualifier를 사용
public UserController(UserService userService, @Qualifier("appleService") FruitService fruitService){ this.userService = userService; this.fruitService = fruitService; }
Java
복사
여러개 후보 중에 선택한 서비스를 쓰게 된다
그럼 이번에는 서비스에 직접 줘보자
BananaService
package com.group.libraryapp.service.fruit; @Service @Qualifier("main") public class BananaService implements FruitService{ }
Java
복사
UserController
public UserController(UserService userService, @Qualifier("main") FruitService fruitService){ this.userService = userService; this.fruitService = fruitService; }
Java
복사
둘다 main을 주면
BananaService를 갖고온다
스프링 빈을 사용하는 쪽, 스프링 빈을 등록하는 쪽 모두 @Qualifier를 사용할 수 있다!
스프링 빈을 사용하는 쪽에서만 쓰면, 빈의 이름을 적어주어야 한다. 양쪽 모두 사용하면, @Qualifier 끼리 연결된다!

@Primary vs @Qualifier

사용하는 쪽에서 직접 적어준 @Qualifier가 이긴다

정리

1. 좋은 코드가 왜 중요한지 이해하고, 원래 있던 Controller 코드를 보다 좋은 코드로 리팩토링한다.
2. 스프링 컨테이너와 스프링 빈이 무엇인지 이해한다.
3. 스프링 컨테이너가 왜 필요한지, 좋은 코드와 어떻게 연관이 있는지 이해한다.
4. 스프링 빈을 다루는 여러 방법을 이해한다.

*참고