순수 자바 코드로 구현한 프로그램을 스프링 프레임워크를 사용하여 전환해보자.
DI를 통해 객체들 간 의존 관계를 연결하고 있는 AppConfigurer 클래스가 있다.
package com.codestates.burgerqueenspring;
import com.codestates.burgerqueenspring.discount.Discount;
import com.codestates.burgerqueenspring.discount.discountCondition.CozDiscountCondition;
import com.codestates.burgerqueenspring.discount.discountCondition.DiscountCondition;
import com.codestates.burgerqueenspring.discount.discountCondition.KidDiscountCondition;
import com.codestates.burgerqueenspring.discount.discountPolicy.FixedAmountDiscountPolicy;
import com.codestates.burgerqueenspring.discount.discountPolicy.FixedRateDiscountPolicy;
import com.codestates.burgerqueenspring.order.Order;
import com.codestates.burgerqueenspring.product.ProductRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfigurer {
// private Cart cart = new Cart(productRepository(), menu()); // 기존 싱글톤 패턴을 구현한 부분을
@Bean
public Menu menu() {
return new Menu(productRepository());
}
@Bean
public ProductRepository productRepository() {
return new ProductRepository();
}
@Bean
public Cart cart() {
return new Cart(productRepository(), menu()); // 다른 메서드와 마찬가지로 new 를 통해 객체를 생성하도록 바꿔줌
}
@Bean
public Order order() {
return new Order(cart(), discount());
}
// 여기 코드를 변경하여 손쉽게 할인 정책을 추가하거나 의존 관계를 변경 가능
@Bean
public Discount discount() {
return new Discount(new DiscountCondition[] {
new CozDiscountCondition(new FixedRateDiscountPolicy()),
new KidDiscountCondition(new FixedAmountDiscountPolicy())
});
}
}
여기에 클래스 레벨에 @Configuration 애너테이션이, 그리고 내부 각각의 메서드에 @Bean 애너테이션이 달려 있다.
@Configuration: 스프링 컨테이너가 만들어질 때 AppConfigurer 클래스를 스프링 컨테이너의 구성 정보로 사용한다는 뜻
@Bean: 스프링이 실행되었을 때, @Bean으로 등록된 메서드들을 모두 호출하여 반환된 객체를 스프링 컨테이너에 등록하고 관리하겠다는 뜻
스프링 컨테이너가 관리하는 객체를 스프링 빈이라고 한다.
스프링은 스프링 컨테이너가 기본적으로 싱글톤으로 빈 객체들을 관리해주기 때문에 주석 처리된 부분처럼 기존의 싱글톤 패턴을 다른 메서드와 마찬가지로 cart() 메서드 내부에서 new 키워드를 통해 객체를 생성하여 반환하도록 코드를 변경하였다.
다음은 스프링 컨테이너의 생성과 사용에 대해 적용해보자.
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 객체 생성 및 조립
// AppConfigurer appConfigurer = new AppConfigurer();
//
// OrderApp orderApp = new OrderApp(
// appConfigurer.productRepository(),
// appConfigurer.menu(),
// appConfigurer.cart(),
// appConfigurer.order()
// );
// 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
// 스프링 빈 조회
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);
// 불러온 빈의 사용
OrderApp orderApp = new OrderApp(
productRepository,
menu,
cart,
order
);
orderApp.start();
}
}
위 코드는 기존에 개발자가 AppConfigurer 클래스를 생성하여 객체를 직접 생성하고, DI를 통해 연결해주었던 역할을 스프링 컨테이너가 대신 가져가겠다는 의미를 가지고 있다.
여기서 ApplicationContext 를 스프링 컨테이너라 하며 인터페이스로 구현되어 있다.
그 구현 객체는 AnnotationConfigApplicationContext 이며, 매개변수로 AppConfigurer.class 를 구성 정보로 넘겨주고 있다.
이렇게 전달 받은 클래스 구성 정보를 바탕으로 스프링 컨테이너는 빈 객체들을 생성하고, 객체들 간의 의존 관계를 연결한다.
스프링 설정 작업이 모두 완료되면, getBean() 메서드를 활용하여 필요한 객체(스프링 빈)를 필요할 때마다 스프링 컨테이너에서 불러 사용할 수 있다.
위의 로그들은 앞서 AppConfigurer 클래스에서 @Bean 애너테이션을 통해 등록한 각각의 객체들이 스프링 컨테이너를 통해 정상적으로 생성되고 관리되었음을 알려주고 있다.
DI(Dependency Injection)은 자바 클래스들 간의 관계를 느슨하게 하기 위해 인터페이스를 사용하여 의존성을 주입해주는 것을 말한다. 여기서의 가장 큰 특징 두 가지는 1) 외부로부터 객체를 주입 받아 this 키워드를 통해 내부 필드에 할당하여 값을 사용하고 있다는 사실과 2) 모두 생성자를 통해 객체를 주입받고 있다는 점이다.
사실 생성자를 주입 받는 방법은 크게 3가지가 있다.
1) 생성자 주입
2) setter 주입
3) 필드 주입
하지만 공식 문서에서도 생성자 주입 방식을 사용할 것을 권장하고 있기 때문에 생성자 주입 방식을 사용하자.
public class KidDiscountCondition implements DiscountCondition {
--- 생략 ---
private DiscountPolicy discountPolicy;
public KidDiscountCondition(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
--- 생략 ---
}
그리고 각 필드의 접근 제어자가 private 키워드를 통해 선언되었다는 특징이 있다.
그 이유는 최초에 객체 인스턴스가 생성될 때 생성자를 통해 주입된 각각의 필드의 값은 다시 수정될 방법이 없기 때문이다.
이는 객체지향의 4대 특징 중, 캡슐화를 구현하기 위함이다. (외부로의 노출을 방지하고, 값이 변경되지 않도록 하기 위해)
같은 맥락에서 생성자 주입의 경우 private 접근 제어자와 함께 final 키워드를 함께 종종 사용하여 사용자 값의 변경을 원천적으로 차단하기도 한다. final 키워드를 사용할 경우, 해당값이 들어오지 않을 경우 바로 에러를 띄어주기 때문에 개발자의 실수에 대한 오류를 방지할 수 있다는 장점이 있다.
public class Discount {
private DiscountCondition[] discountConditions;
public Discount(DiscountCondition[] discountConditions)
{
this.discountConditions = new DiscountCondition[]{
new CozDiscountCondition(new FixedAmountDiscountPolicy()),
new KidDiscountCondition(new FixedRateDiscountPolicy())
};
}
... 생략 ...
}
Discount 클래스의 생성자는 객체가 최초 생성될 때, 손님의 조건에 따라 정률 할인(FixedRateDiscountPolicy)과 정액 할인(FixedAmountDiscountPolicy)이 적용될 수 있도록 객체들 간에 의존 관계를 구성하는 역할을 수행하고 있다.
여기서 각 조건에 따른 정률할인과 정액할인을 바꾸고 싶다면, 언제든지 바꿔줄 수 있다.
- Coz 수강생인 경우 정률 할인을 적용한다.
- CozDiscountCondition → FixedRateDiscountPolicy
- 미성년자의 경우 정액 할인을 적용한다.
- KidDiscountCondition → FixedAmountDiscountPolicy
@Configuration
public class AppConfigurer {
... 생략 ...
@Bean
public Discount discount() {
return new Discount(new DiscountCondition[] {
new CozDiscountCondition(new FixedAmountDiscountPolicy()),
new KidDiscountCondition(new FixedRateDiscountPolicy())
});
}
}
이처럼 객체지향 프로그래밍에서 가장 중요한 확장에 유연한 코드를 설계하는 것이며, 그 중심에는 DI가 있다.
스프링 컨테이너와 빈
(1) 스프링 컨테이너 생성
public class Main {
public static void main(String[] args) {
// (1) 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
... 생략 ...
}
}
ApplicationContext 인터페이스를 일반적으로 스프링 컨테이너라고 부른다.
더 정확히는 스프링 컨테이너를 말할 때 ApplicationContext가 상속하고 있는 BeanFactory와 구분해서 사용하나, BeanFactory를 직접 사용하는 경우는 거의 없기 때문에 일반적으로 ApplicationContext를 스프링 컨테이너라고 부른다.
BeanFactory는 스프링 컨테이너의 최상위 인터페이스로 스프링 빈을 관리하고 조회하는 역할을 담당한다.
다음으로 AnnotationConfigApplicationContext는 그 구현 객체이며 매개변수로 구성 정보(AppConfigurer.class)를 넘겨주고 있습니다.
스프링 컨테이너는 넘겨받은 구성 정보를 가지고 메서드를 호출하여 빈을 생성하고, 빈들 간의 의존 관게를 설정한다.
이렇듯 빈을 생성하는 과정에서, 스프링 컨테이너는 호출되는 메서드 이름을 기준으로 빈의 이름을 등록한다.
@Configuration
public class AppConfigurer {
@Bean
public Cart cart() {
return new Cart(productRepository(), menu());
}
--- 생략 ---
}
만약 new Cart(productRepository(), menu()) 객체 빈은 cart라는 이름으로 스프링 컨테이너의 빈 리스트에 저장되는 방식이다.
- 빈 조회 - getBean(빈 이름, 타입) Cart cart = applicationContext.getBean("cart", Cart.class);
2) 빈 조회
스프링 컨테이너가 관리하는 자바 객체를 스프링 빈(Bean) 이라 한다.
public class Main {
public static void main(String[] args) {
// (2) 빈 조회
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
Menu menu = applicationContext.getBean("menu", Menu.class);
Cart cart = applicationContext.getBean("cart", Cart.class);
Order order = applicationContext.getBean("order", Order.class);
System.out.println("productRepository = " + productRepository);
System.out.println("menu = " + menu);
System.out.println("cart = " + cart);
System.out.println("order = " + order);
}
}
스프링 컨테이너가 빈을 생성하고 의존 관계를 연결해주면, 이제 스프링 컨테이너 관리 하에 있는 객체 빈들을 위와 같이 getBean() 메서드로 조회할 수 있다.
가장 기본적인 빈 조회 방법은 다음과 같다.
- getBean(빈 이름, 타입)
- 예시) applicationContext.getBean("menu", Menu.class);
- getBean(타입)
- 예시) applicationContext.getBean(Menu.class);
3) 의존성 주입 & 프로그램 실행
앞의 과정을 통해 스프링 컨테이너 관리 하의 빈 객체들을 성공적으로 불러왔다면,
해당 객체의 참조값을 앞서 배웠던 의존성 주입을 사용하여 OrderApp 객체에 전달하여 프로그램을 실행할 수 있다.
테스트 케이스 작성 기초
단위 테스트에 대한 기초적인 내용을 학습해보자.
단위 테스트란?
작은 단위의 어떤 특정한 기능을 테스트하고, 검증하기 위한 도구를 의미한다.
다른 말로 테스트 케이스 (Test Case)를 작성한다고 표현할 수 있으며, 이 과정에는 주로 입력 데이터, 실행 조건, 기대 결과에 대한 값이 포함된다.
스프링에서는 JUnit 이라는 오픈 소스 테스트 프레임워크를 제공하는데, 각각의 단위 테스트는 메서드 단위로 작성된다.
테스트 케이스 작성을 위한 디렉토리 구조는 main 패키지 안에서 작성한 구조와 동일하게 작성하는 것을 권장한다.
1) MainTest 클래스 - 빈 조회 단위 테스트 작성
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();
// 빈 조회 테스트케이스
// (1) 잘못된 이름의 빈 조회
@Test
void findBean() {
//given
// 초기화 또는 테스트에 필요한 입력 데이터
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
//when
// 테스트 할 동작
Menu menu = applicationContext.getBean("menu", Menu.class);
//then
// 검증
Assertions.assertThat(menu).isInstanceOf(Menu.class);
}
}
BDD(Behavior Driven Development) 테스트 방식
(given - when - then)
(1) given: 입력 데이터
테스트에 필요한 초기화 값 또는 입력 데이터를 의미합니다. 위의 예제의 경우, 빈 조회 테스트에 필요한 초기화 세팅은 AppConfigurer 클래스를 구성 정보로 하는 스프링 컨테이너를 생성하는 것입니다.
(2) when: 실행 동작
테스트할 실행 동작을 지정합니다. 일반적으로 단위 테스트에서는 메서드 호출을 통해 테스트를 진행하므로 한 두줄 정도로 작성이 끝납니다. 빈 조회 테스트에서 실행할 동작은 getBean() 메서드를 사용하여 빈을 불러오는 것입니다.
(3) then: 결과 검증
테스트의 결과를 최종적으로 검증하는 단계입니다.
일반적으로 테스트 결과 예상되는 기대값(expected)과 실제 실행 결과의 값을 비교하여 테스트를 검증합니다.
주로 JUnit 또는 AssertJ 라이브러리에서 제공하는 Assertions 클래스의 기능들을 필요에 따라 사용하여 검증을 진행합니다.
위의 코드는 AssertJ 라이브러리의 Assertions 클래스의 메서드인 assertThat()을 사용한 검증 방법을 보여주고 있습니다.
AssertJ는 메서드 체이닝(method chaining)을 지원하기 때문에 스트림과 유사하게 여러 메서드들을 연속하여 호출하여 간편하게 사용이 가능합니다.
AssertJ에서 모든 테스트코드는 assertThat()을 사용하고, 테스트를 실행할 대상을 파라미터로 전달하여 호출합니다. 호출의 결과로 ObjectAssert 타입의 인스턴스를 반환하는데, 이를 사용하여 isInstanceOf() , isSameAs() , isNotNull() , isNotEmpty() 등 다양한 검증을 편리하게 실행할 수 있습니다.
우리가 작성한 코드를 해석해 보면, 먼저 Assertions.assertThat() 메서드에 테스트를 실행할 참조변수 cart를 전달인자로 전달하고, 메서드 체이닝을 사용하여 isInstanceOf() 메서드를 사용하고 있습니다. isInstanceOf() 메서드는 대상 타이 주어진 유형의 인스턴스인지 검증할 때에 사용합니다. 자바의 instanceOf() 메서드와 유사하다고 할 수 있습니다.
2) MainTest 클래스 - 잘못된 이름의 빈 조회
package com.codestates.burgerqueenspring;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainTest {
// 스프링 컨테이너 생성
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
// 빈 조회 테스트케이스
// (2) 존재하지 않는 빈 검증
@Test
@DisplayName("빈이 존재하지 않는 경우")
void findBeanX2() {
//given
// 초기화 또는 테스트에 필요한 입력 데이터
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);
//when ==> 불필요
//then
// 검증
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean("xxx", Menu.class));
}
}
스프링 컨테이너 = 싱글톤 컨테이너
빈 생명주기와 범위
컴포넌트 스캔과 의존성 자동 주입
@Autowired
'CodeStates 45th' 카테고리의 다른 글
Section 4. [인증/보안] 기초 (0) | 2023.07.17 |
---|---|
[Spring MVC] API 계층 (0) | 2023.06.19 |
[Section 2] SQL, MySQL Command practice (0) | 2023.05.25 |
[Section 2] Spring Framework 기본 (0) | 2023.05.21 |
[Section 1] 회고 (1) | 2023.05.09 |