인프런에서 주최하는 Warming-up 클럽 0기 백엔드 스터디에 참여하고 있다.
스터디에 참여하면서 배우게 된 내용을 전체적으로 정리하고, 참여하면서 느낀 부분을 회고해 보고자 한다.
(1) 6일 차 : 2024-02-26(Mon)
1. @RestController
(1) 해당 어노테이션은 관련 클래스를 API의 진입 지점으로 설정한다.
(2) 또한 관련된 컨트롤러를 스프링 빈(Spring Bean)에 등록한다.
2. 스프링 빈(Spring Bean) & 스프링 컨테이너(Spring IoC Container)
2-1. 스프링 빈이란?
(1) 스프링 빈이란 스프링 컨테이너에서 직접적으로 관리되는 객체를 의미한다. 스프링 부트 서버가 시작되면 스프링 IoC 컨테이너(Spring IoC Container)를 올리게 되고 내부에 스프링 빈을 넣는다.
(2) 스프링 컨테이너로 스프링 빈들이 들어가게 되는데 이때 빈이 되는 대상 객체의 다양한 정보들도 함께 들어가게 된다.(클래스 타입, 이름 등...) 이후 스프링 빈들에 대한 인스턴스화도 이루어진다.
2-2. 스프링 컨테이너
(1) 스프링 컨테이너의 역할은 서로 필요한 관계에 있는 스프링 빈끼리 의존성을 확인하고, 의존성을 주입, 스프링 빈의 생성과 소멸 등 전반적인 라이프 사이클을 관리하는 역할을 수행한다.
(2) 스프링 컨테이너가 생성되면 기본적으로 다양한 의존성(프레임워크나 라이브러리)들에 의해 많은 스프링 빈이 컨테이너에 등록된다.
(3) 이후 개발자가 직접 별도의 어노테이션을 줘서 설정된 스프링 빈도(UserController 등)이 컨테이너에 등록된다.
(4) (3)번 과정에서 스프링 빈으로 등록된 클래스들에 대해 의존관계가 별도로 확인된다면, 컨테이너에 스프링 빈으로 등록되고 의존성 주입을 통해 필요한 객체를 주입시켜준다.
2-3. 스프링 컨테이너는 왜 사용할까?
(1) 아래의 예시를 통해 스토리로 이해해보자. 도서 상품을 메모리에 저장하는 API를 구현해야 한다고 해보자. 아래와 같은 계층 구조를 갖게 된다. (단 스프링 컨테이너, 빈을 전혀 사용하지 않고 구현한다.)
public class BookController {
private final BookService bookService = new BookService();
@GetMapping("/book")
public void saveBook() {
bookService.saveBook();
}
}
public class BookService {
private BookRepository bookRepository = new BookMemoryRepository();
public void saveBook() {
bookRepository.saveBook();
}
}
public class BookMemoryRepository {
// private final List<Book> books = new ArrayList<>();
public void saveBook() {
}
}
(2) 그렇다면 Controller, Service, Repository는 위와 같은 코드로 작성된다.
(3) 스프링 컨테이너를 사용하지 않고 있으니 각 계층에서 필요한 객체들을 직접 new를 통해 인스턴스화가 필요하다.
(4) 이 상황에서 새로운 요구사항이 추가되었다.
- 데이터를 메모리에 저장하지 않고 MySQL 데이터베이스에 저장한다.
(5) 그럼 아래와 같이 요구사항이 변경된다.
public class BookMysqlRepository {
public void saveBook() {
}
}
public class BookService {
private BookMysqlRepository bookMysqlRepository = new BookMysqlRepository();
public void saveBook() {
bookMysqlRepository.saveBook();
}
}
(6) 그렇다면 서비스 계층의 객체를 인스턴스화하는 코드를 아예 변경해 줘야 한다.
(7) 즉 Repository를 의존하는 Service 계층의 코드까지 변경해 줘야 한다. 위와 같이 각 객체들이 강하게 결합되어 있는 경우 요구사항이 변경되면 의존하는 객체쪽은 코드 변경을 피할 수 없는 상태가 된다.
(8) 이 문제를 어떻게 해결할까?
2-4. 스프링 컨테이너는 왜 사용할까? - 개선 : 강하게 결합된 부분을 인터페이스로 고쳐보자.
public interface BookRepository {
void saveBook();
}
(1) 위와 같이 BookRepository 인터페이스, saveBook() 추상 메서드를 만든다.
public class BookMemoryRepository implements BookRepository {
// private final List<Book> books = new ArrayList<>();
public void saveBook() {
}
}
public class BookMysqlRepository implements BookRepository {
public void saveBook() {
}
}
(2) 인터페이스를 만들고 BookMysqlRepository, BookMemoryRepository가 해당 인터페이스의 구현체로 설정한다.
public class BookService {
private BookRepository bookRepository = new BookMysqlRepository();
// private BookRepository bookRepository = new BookMemoryRepository()
public void saveBook() {
bookRepository.saveBook();
}
}
(3) 이렇게 설정한다면 BookMemoryRepository를 사용하다가 추후 요구사항 변경에 의해 BookMysqlRepository를 사용한다고 해도 위와 같이 구현체만 바꿔서 수정해주면 되지만 아직도 문제가 여전히 존재한다. 바로 코드 변경을 피할 수 없다는 것이다.
(4) 이러한 의존관계로 인한 강한 결합성 문제들을 스프링 컨테이너가 해결해 준다. 스프링 컨테이너를 도입해 보자.
2-5. 스프링 컨테이너를 사용하는 이유? : 문제 해결 : 스프링 컨테이너 사용
@RestController
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/book")
public void saveBook() {
bookService.saveBook();
}
}
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public void saveBook() {
bookRepository.saveBook();
}
}
@Repository
public class BookMemoryRepository implements BookRepository {
// private final List<Book> books = new ArrayList<>();
public void saveBook() {
System.out.println("Memory Repository");
}
}
@Repository
public class BookMysqlRepository implements BookRepository {
@Override
public void saveBook() {
System.out.println("MySql Repository");
}
}
(1) @RestController, @Service, @Repository를 붙여주면 스프링 컨테이너에서 관리되는 스프링 빈으로 설정된다. Controller, Service, Repository의 코드를 수정하자.
(2) 스프링 빈으로 등록하는 부분에 대해선 아래에서 조금 더 자세히 알아보자.
(2) 이렇게 되면 컨테이너가 스프링 빈들의 각 의존관계를 직접 확인한다. 이후 특정 객체의 필드에 관련된 객체만 명시하면 런타임 시점에 컨테이너가 필요한 빈을 직접 관련 객체에 주입시켜준다. 이를 의존성 주입(Dependency Injection)이라고 한다. 따라서 컨테이너가 런타임 시점에 빈을 생성하고, 생성자의 파라미터로 해당 생성된 빈이 들어가게 되면서 의존성 주입이 이루어진다.
(3) 또한 개발자가 직접 코드로 애플리케이션의 객체 라이프 사이클을 관리하지 않고 스프링 컨테이너가 이를 담당하게 된다. 이 부분을 제어권이 개발자에서 스프링 컨테이너로 넘어갔기 때문에 제어가 역전되었다고 해서 제어의 역전(IoC, Inversion of Control)이라고 한다.
@Repository
public class BookMemoryRepository implements BookRepository {
// private final List<Book> books = new ArrayList<>();
public void saveBook() {
System.out.println("Memory Repository");
}
}
@Primary // 동일한 빈 타입에 대해 의존성 주입에 대한 우선권이 생긴다.
@Repository
public class BookMysqlRepository implements BookRepository {
@Override
public void saveBook() {
System.out.println("MySql Repository");
}
}
(4) 여기서 한 가지 의문이 생긴다. BookMysqlRepository, BookMemoryRepository는 같은 인터페이스를 구현했기 때문에 둘의 타입이 동일하다. 스프링 컨테이너도 동일한 빈 타입에 대해선 어떤 빈을 주입해야 할지 모호함이 생기는데 이 부분은 @Primary 어노테이션으로 해결한다. @Primary 어노테이션으로 동일한 빈에 대해 어떤 빈을 주입할지 우선권을 먼저 부여한다.
2-6. 3단으로 분리되었던 영역에 Controller - Service - Repository 영역에 각 @RestController(@Controller), @Service, @Repository 어노테이션을 추가하면 스프링 컨테이너에서 자동으로 관리되는 스프링 빈으로 등록된다.
(1) 서버 어플리케이션을 시작한다.
(2) 서버 어플리케이션이 시작되면 스프링 컨테이너에 JdbcTemplate, DataSource, Environment 등 의존성에 의해 추가되었던 여러 클래스들이 스프링 빈으로 등록된다.
(3) 스프링 컨테이너가 빈으로 설정된 객체들의 의존관계를 모두 검사하여 초기에 등록된 빈들을 의존하는 클래스들을 모두 탐색하여 스프링 빈으로 등록한다.
- 이번 프로젝트의 경우 빈으로 설정된 JdbcTemplate을 의존하는 클래스(UserRepository)가 빈으로 등록되었다.
(4) 이에 따라서 컨테이너가 UserRepository를 의존하는 클래스를 탐색해 UserService가 스프링 빈으로 등록된다.
(5) Controller도 위와 같은 메커니즘으로 스프링 빈에 등록된다.
(6) 이러한 스프링 컨테이너를 조금 더 자세하게 다루는 방법에 대해 알아보자.
3. 스프링 컨테이너를 다루는 방법
3-1. 특정 객체를 스프링 빈으로 설정하는 방법
(1) @Configuration
- 클래스 레벨에 붙이는 어노테이션
- @Bean을 사용할 때 함께 사용되어야 한다.
(2) @Bean
- 메서드 레벨에 붙이는 어노테이션
- 메서드에서 반환되는 객체를 스프링 빈으로 등록한다.
3-2. @RestController(Controller), @Service, @Repository는 언제 사용할까?
(1) 개발자가 직접 정의한 클래스들을 스프링 빈으로 등록해야 하는 경우
3-3. @Configuration + @Bean은 언제 사용할까?
(1) 외부 라이브러리, 프레임워크에서 개발한 클래스들을 직접 사용할 때
3-4. @Component
(1) 해당 어노테이션을 클래스에 적용하면 "컴포넌트"로 간주한다.
(2) 컴포넌트로 설정된 클래스들은 애플리케이션 서버가 실행될 때 자동으로 스프링 빈으로 등록된다.
(3) 지금까지 사용되었던 @RestController, @Service, @Repository 어노테이션 모두 @Component를 포함하고 있다. 따라서 스프링 컨테이너에서 관리되는 스프링 빈으로 설정될 수 있었다.
3-4. @Component는 언제 사용할까?
(1) Controller, Service, Repository를 제외하고 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용할 수 있다.
4. 스프링 빈을 주입받는 방법
4-1. Constructor injection (가장 권장되는 방법 중 하나)
@Service
public class UserService {
private final UserRepository userRepository;
// @Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
(1) 관련된 스프링 빈 객체를 위와 같이 필드에 선언한다.
(2) 그 다음 생성자를 별도로 만들어서 생성자의 파라미터로 컨테이너가 주입한 스프링 빈 인스턴스를 넣는다.
(3) 기존에는 의존성 주입에 사용되는 @Autowired 어노테이션(컨테이너가 관련된 스프링 빈을 직접 찾아서 필요한 객체에 직접적으로 주입시키라는 의미를 가진 어노테이션)을 명시해야 했지만 스프링이 버전업되면서 @Autowired 어노테이션을 생략해도 된다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
}
(3) 추가적으로 위와 같이 Lombok 라이브러리의 @RequiredArgsConstructor를 선언하면 생성자를 직접 사용하지 않아도 상수 타입으로 선언된 객체를 기반으로 생성자를 생성하여 생성자 주입이 가능하도록 해준다.
(4) 코드 가독성 덕분에 위와 같은 방식을 많이 사용한다.
4-2. Setter injection
@Service
public class UserService {
private UserRepository userRepository;
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
(1) Setter를 통해 직접 의존성을 주입하는 방법이다.
4-3. Field injection
@Service
public class UserService {
private UserRepository userRepository;
}
(1) 필드에서 직접 의존성을 주입하는 방법이다.
4-4. 불변 타입의 필드와 함께 의존성 주입은 Constructor injection을 사용하도록 한다.
(1) 불변 타입(final) 필드 Constructor injection을 추천하는 이유는 아래와 같다.
(2) 주입된 객체가 final인 경우 생성자를 통해 한 번만 초기화되고 이후부터는 값이 변경되지 않음을 보장받기 때문이다.
(3) final으로 선언할 경우 초기화되지 않은 객체에 대해 컴파일 오류를 발생시켜서 기존에 코드를 미리 수정할 수 있다.
(4) Setter injection의 경우 코드 어딘가에서 해당 setter 메서드를 호출함으로써 코드가 오작동할 여지가 있기 때문에 추천되지 않는다.
(5) Field injection의 경우 의존성을 명시하지 않고 내부에서 직접 의존성을 주입하기 때문에 해당 클래스가 정확히 어떤 의존성을 필요로 하는지 모르고 객체를 생성할 때마다 의존성 주입이 내부적으로 일어나기 때문에 테스트와 같은 외부 환경에서 mock 객체를 주입하는 데 어려움이 있고 이는 테스트에서 의존성을 제어하는 데 문제를 일으킬 수 있다. 따라서 테스트를 어렵게 만드는 요인이 되기도 하기 때문에 Field injection은 추천되지 않는다
5. @Qualifier, @Primary?
(1) @Qualifier, @Primary 모두 동일한 타입 빈들이 여러 개 존재할 때, 어떤 빈을 의존성 주입의 대상으로 선정할지 결정하는 어노테이션이다.
(2) 두 개 모두 우선권을 지정하는 어노테이션인데 두 개 어노테이션 중 우선순위가 더 높은 것은 바로 @Qualifier 어노테이션이다.
- 스프링의 특성 중 하나인데 대부분 직접 이름이나 값을 명시해준 곳에서 우선순위가 높도록 스프링은 설계되어 있다.
(3) 빈을 주입받아서 사용하는 쪽에서 @Qualifier를 사용하는 경우 주입받는 빈의 이름을 위와 같이 명시해야 한다.(카멜 케이스 적용)
6. 개인 회고
(1) 스프링 빈, 스프링 컨테이너, Dependency Injection, Inversion of Control 등 스프링을 이루고 있는 핵심 원리에 대해 복습할 수 있었다.
(2) @Component, @Configuration + @Bean은 언제 사용되는지 애매했었는데 이번 기회에 감을 잡을 수 있었다.
(3) 스프링을 이루고 있는 핵심 원리인만큼 관련 내용을 깊게 이해할 필요성을 느꼈다.
7. Reference
(1) 관련 레퍼런스는 인프런에서 활동하고 계시는 최태현 강사님의 온라인 강의를 듣고 내용을 정리했습니다.
※ 해당 포스팅에 대해 내용 추가가 필요하다고 생각되면 기존 포스팅 내용에 다른 내용이 추가될 수 있습니다.
개인적으로 공부하며 정리한 내용이기에 오타나 틀린 부분이 있을 수 있으며, 이에 대해 댓글로 알려주시면 감사하겠습니다
'기록, 회고 > InFlearn Warming-up 0기 BE' 카테고리의 다른 글
[7일 차] - 내용 정리, 개인 회고 (2) | 2024.02.24 |
---|---|
[6일 차] - 과제 수행 : Controller - Service - Repository 분리 (0) | 2024.02.23 |
[5일 차] - 과제 수행 : 클린 코드, 코드 리팩토링 (0) | 2024.02.21 |
[5일 차] - 내용 정리, 개인 회고 (0) | 2024.02.21 |
[4일 차] - 과제 수행 : API 개발 연습 (0) | 2024.02.21 |
댓글