[Section2] Spring Framework 핵심 - DI(Dependency Injection) 1
🧑🏻💻 TIL(Today I Learned)
✔️ Spring Framework 핵심 개념 실습
💡 스프링 컨테이너와 빈
package com.codestates.burgerqueenspring;
import com.codestates.burgerqueenspring.order.Order;
import com.codestates.burgerqueenspring.order.OrderApp;
import com.codestates.burgerqueenspring.product.ProductRepository;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
public static void main(String[] args) {
// 스프링 컨테이너의 관리 하에 있는 AppConfigurer
// (1) 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
// (2)스프링 빈 조회
ProductRepository productRepository = applicationContext.getBean("productRepository", ProductRepository.class);
Menu menu = applicationContext.getBean("menu", Menu.class);
Cart cart = applicationContext.getBean("cart", Cart.class);
Order order = applicationContext.getBean("order", Order.class);
// (3)의존성 주입
OrderApp orderApp = new OrderApp(
productRepository,
menu,
cart,
order
);
// (4)프로그램 실행
orderApp.start();
}
}
(1) 스프링 컨테이너 생성
➡️ ApplicationContext 인터페이스를 일반적으로 스프링 컨테이너라고 부름
- BeanFactory는 스프링 컨테이너의 최상위 인터페이스, 스프링 빈을 관리하고 조회하는 역할
- ApplicationContext는 위 그림처럼 BeanFactory만 상속받는 것은 아님
- 실제로 BeanFactory 말고도 다양한 기능의 인터페이스들을 상속받아 사용하고 있음
- 즉 ApplicationContext는 빈을 관리하고 조회하는 기능뿐 아니라 웹 애플리케이션을 개발하는 데 필요한 다양한 부가 기능 제공함
- AnnotationConfigApplicationContext는 구현 객체이며 매개 변수로 구성 정보(AppConfigurer.class) 넘겨줌
- 스프링 컨테이너는 넘겨받은 구성 정보를 가지고 메서드를 호출하여 빈을 생성하고, 빈들 간의 의존 관계 설정함
→ 빈을 생성하는 과정에서 스프링 컨테이너는 호출되는 메서드의 이름을 기준으로 빈의 이름을 등록함
@Configuration // 스프링 컨테이너가 만들어질 때 AppConfigurer 클래스를 스프링 컨테이너의 구성 정보로 사용한다는 의미
public class AppConfigurer { // 객체를 생성하고 의존 관계를 연결시키는 역할
// private Cart cart = new Cart(productRepository(), menu());
@Bean // 스프링이 실행되었을 떄 빈으로 등록된 메서드들을 모두 호출하여 반환된 객체를 스프링 컨테이너에 등록하고 관리하겠다는 의미
public Menu menu() {
return new Menu(productRepository());
}
}
}
- 위와 같은 경우 @Bean 애너테이션이 붙은 menu()라는 이름의 메서드의 호출 결과로 반환된 new Menu(productRepository()) 객체 빈은 menu 라는 이름으로 스프링 컨테이너의 빈 리스트에 저장이 되는 방식
- Menu menu = applicationContext.getBean("menu", Menu.class);
→ 빈 조회(저장된 빈 객체 불러오기) - getBean(빈 이름, 타입)
@Configuration
public class AppConfigurer {
@Bean(name="cart2")
public Cart cart() {
return new Cart(productRepository(), menu());
}
}
- 이름을 위와 같이 다르게 설정해 줄 수도 있음, 하지만 같은 이름으로 등록되는 경우 에러가 나기 때문에 주의할 것
(2) 스프링 빈 조회
➡️ 스프링 컨테이너가 관리하는 자바 객체 : 스프링 빈(Bean)
➡️ 빈은 클래스의 등록 정보, getter/setter 메서드를 포함하며 구성 정보(설정 메타 정보)를 통해 생성됨
➡️ 실제 getBean() 메서드를 통해 불러온 빈들을 출력해 보면 객체 인스턴스의 참조값이 찍히는 것을 확인할 수 있음
➡️ 기본적인 빈 조회 방법
- getBean(빈 이름, 타입)
- ex) applicationContext.getBean("menu", Menu.class);
- getBean(타입)
- ex) applicationContext.getBean(Menu.class);
➡️ 모든 빈 조회하기
public class Main {
public static void main(String[] args) {
// (2) 모든 빈 조회
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = applicationContext.getBean(beanDefinitionName);
System.out.println("beanName=" + beanDefinitionName + " object=" + bean);
}
}
➡️ 각각의 빈은 해당 빈에 대한 메타 정보를 가지고 잇고 이 정보들도 조회가 가능함
→ 스프링 컨테이너는 메타 정보(BeanDefinition)를 기반으로 스프링 빈 생성
public class Main {
public static void main(String[] args) {
// (2) 빈 메타정보 조회
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = applicationContext.getBeanDefinition(beanDefinitionName);
if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
System.out.println("beanDefinitionName" + beanDefinitionName + " beanDefinition = " + beanDefinition);
}
}
}
(3) 의존성 주입 & 프로그램 실행
➡️ 스프링 컨테이너 하의 빈 객체들을 성공적으로 불러왔다면 해당 객체의 참조값을 의존성 주입을 사용하여 OrderApp 객체에 전달하여 프로그램을 실행할 수 있음
💡 테스트 케이스 작성 기초
✔️ 지금까지 작성한 코드가 의도한 값을 바르게 도출하고 있는지 여부를 판단하기 위해 주로 System.out.println() 메서드를 활용하여 콘솔에 값을 출력해 왔음, 간편하고 빠르게 테스트하기에 유용한 방법이지만 가장 최선의 방법이라고 하기는 어려움
개발자 입장에서 번거롭기도 하지만 비용과 성능이 측면에서도 딱히 좋지 못함
➡️ 좋은 테스트를 잘 설계하는 일은 잠재적으로 발생할 수 있는 코드 상의 문제와 성능적인 비용을 최소화할 수 있음
🔎 단위 테스트(Unit Test)
➡️ 작은 단위의 어떤 특정한 기능을 테스트하고 검증하기 위한 도구
➡️ 다른 말로 테스트 케이스(Test Case)를 작성한다고도 표현함, 그 과정에는 입력 데이터, 실행 조건 그리고 기대 결과에 대한 값 포함
➡️ 스프링에서는 단위 테스트를 간편하고 효과적으로 수행할 수 있도록 JUnit이라는 오픈 소스 테스트 프레임워크 제공함
➡️ JUnit을 사용하는 테스트는 기본적으로 test 디렉토리 안에서 작성되는 것이 원칙이며 디렉토리 구조 또한 main 패키지 안에 작성한 디렉토리 구조와 동일하게 작성하는 것을 권장함
➡️ 기본 구조
import org.junit.jupiter.api.Test;
public class JunitDefaultStructure {
@Test
public void test1() {
// 테스트하고자 하는 대상에 대한 테스트 로직 작성
}
@Test
public void test2() {
// 테스트하고자 하는 대상에 대한 테스트 로직 작성
}
@Test
public void test3() {
// 테스트하고자 하는 대상에 대한 테스트 로직 작성
}
}
// JUnit 5 이하 버전에서는 public static 키워드가 필수였지만 Junit 5 부터는 생략 가능
🔎 테스트 로직 작성
package com.codestates.burgerqueenspring;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainTest {
// 스프링 컨테이너 생성
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
// 빈 조회 테스트케이스
@Test
void findBean() {
// (1) given => 초기화 또는 테스트에 필요한 입력 데이터
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
// (2) when => 테스트 할 동작
Cart cart = applicationContext.getBean("cart", Cart.class);
// (3) then => 검증
Assertions.assertThat(cart).isInstanceOf(Cart.class);
}
}
- 주석으로 표시한 given - when - then 은 BDD(Behavier Driven Development)라고 부르는 테스트 방식에서 사용되는 방법
- given : 입력 데이터
- 테스트에 필요한 초기화 값 또는 입력 데이터
- when : 실행 동작
- 테스트할 실행 동작 지정
- then : 결과 검증
- 테스트의 결과를 최종적으로 검증하는 단계
- 일반적으로 테스트 결과 예상되는 기대값과 실제 실행 결과의 값을 비교하여 테스트 검증함
AssertJ 라이브러리의 Assertions 클래스의 메서드인 assertThat() 을 사용한 검증 방법
- AssertJ는 메서드 체이닝(Method Chaining) 지원하기 때문에 스트림과 유사하게 여러 메서드들을 연속하여 호출하고 간편하게 사용 가능
- AssertJ에서 모든 테스트 코드는 assertThat() 사용하고 테스트를 실행할 대상을 파라미터로 전달하여 호출
- 아래와 같이 작성한 코드를 보면 Assertions.assertThat() 메서드에 테스트를 실행할 참조 변수 cart를 전달인자로 전달
그다음 메서드 체이닝을 사용하여 isInstanceOf() 메서드 사용 → 대상 타입이 주어진 유형의 인스턴스인지 검증
Assertions.assertThat(menu).isInstanceOf(Cart.class);
💡 스프링 컨테이너 = 싱글톤 컨테이너
싱글톤 패턴(Singleton Pattern)
: 인스턴스가 단 한 번만 생성되게 만들어 객체의 참조값을 공유할 수 있도록 하는 것
: 동시다발적인 고객의 요청을 처리해야 하는 경우 매번 new 연산자를 사용하여 객체를 생성해야 한다면 매번 이를 위한 메모리 영역을 할당받아야 하는데 이것은 큰 메모리 낭비를 초래할 수 있음
: 그래서 싱글톤 패턴을 사용하여 단 하나의 객체를 두고 요청이 돌아올 때마다 같은 객체를 공유하는 방법으로 메모리 낭비 최소화
➡️ 스프링 컨테이너는 싱글톤 패턴 코드, 즉 결합도가 높은 상태를 야기하는 코드를 직접적으로 작성하지 않아도 내부적으로 객체 인스턴스를 싱글톤으로 관리함으로 싱글톤 패턴이 가지는 모든 잠재적인 단점들을 효과적으로 극복할 수 있음
package com.codestates.burgerqueenspring.singleton;
import com.codestates.burgerqueenspring.AppConfigurer;
import com.codestates.burgerqueenspring.Cart;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class CartSingletonTest {
@Test
void checkCartSingleton() { // 내부적으로 객체들을 싱글톤으로 관리하는지 확인하기
// given - 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
// when - 빈 조회
Cart cart = applicationContext.getBean("cart", Cart.class);
Cart cart2 = applicationContext.getBean("cart", Cart.class);
// 출력
System.out.println("cart = " + cart);
System.out.println("cart2 = " + cart2);
// then - 검증
Assertions.assertThat(cart).isSameAs(cart2);
}
}
➡️ 위 결과를 통해 스프링 컨테이너가 내부적으로 객체들을 싱글톤으로 관리한다는 사실을 알 수 있음
➡️ 즉, 스프링 컨테이너는 싱글톤 컨테이너 역할을 수행한다!
➡️ 싱글톤 레지스트리(Singleton Registry) : 싱글톤으로 객체를 생성하고 관리하는 기능
→ 스프링은 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 싱글톤 레지스트리를 가능하게 함
@Bean
public Cart cart() { // CGLIB 내부 동작 의사 코드
if(cart가 이미 스프링 컨테이너에 있는 경우) {
return 이미 있는 객체를 찾아서 반환
} else {
새로운 객체를 생성하고 스프링 컨테이너에 등록
return 생성한 객체 반환
}
}