스프링 컨테이너는 자바 객체의 생명 주기를 관리하며, 생성된 객체들에게 추가적인 기능을 제공하는 역할을 한다. 이 때 생성된 객체들을 Bean이라고 부른다.
스프링 컨테이너는 객체들 간의 의존 관계를 런타임에서 알아서 만들어주며, 객체들의 생성/소멸까지도 알아서 관리해준다.
스프링 컨테이너의 최상위 인터페이스.
BeanFactory는 빈을 등록/생성/조회/반환해주는 등 Bean을 관리해주는 역할을 한다. getBean() 메소드를 통해 빈을 인스턴스화할 수 있다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
BeanFactory 방식에선 아래와 같이 컨테이너를 통해 getBean(이름,타입)을 호출하여 필요한 Bean 객체를 찾을 수 있다.
public class Main {
public static void main(String[] args) {
final BeanFactory beanFactory = new AnnotationConfigApplicationContext(SpringConfig.class);
final MemberService memberService = beanFactory.getBean("MemberService", MemberService.class);
Member member1 = memberService.findOne(1);
System.out.println(member1.getId());
}
}
ApplicationContext도 BeanFactory와 같이 빈을 관리할 수 있다. ApplicationContext는 BeanFactory를 상속받은 자손이기 때문이다.
그 외 주요 기능
ApplicationContext의 인터페이스
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
...
}
main에서의 활용
public class Main {
public static void main(String[] args) {
final ApplicationContext beanFactory = new AnnotationConfigApplicationContext(SpringConfig.class);
final MemberService memberService = beanFactory.getBean("MemberService", MemberService.class);
Member member1 = memberService.findOne(1);
System.out.println(member1.getId());
}
}
BeanFactory는 getBean()메소드 호출 시점에서야 해당 빈을 생성하나 ApplicationContext는 Context 초기화 시점에 모든 싱글톤 빈을 미리 로드한 후, 애플리케이션 가동 후에는 빈을 지연없이 받을 수 있으므로 빈을 지연없이 받을 수 있다는 장점으로 실무에서 많이 활용된다.
스프링은 스프링 컨테이너에 빈을 등록할 때, 특별한 상황이 아니라면 기본적으로 싱글톤으로 등록한다. 그에 따라 같은 스프링 빈이면 모두 같은 인스턴스다.
프로젝트 실행시 Spring에게 Spring Application이라고 인식시키고 의존성 관계를 주입시키는 방법은 두 가지가 있다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방식이 있다.
필드 주입의 경우에는 자유도가 떨어지고, setter 주입의 경우에는 동적으로 Bean의 인스턴스가 변경될 수 있을 위험이 있으므로 생성자 주입 방식이 권장된다.
컨테이너 클래스의 getBeanDefinitionNames 메소드를 통해 등록된 Bean들을 Key-value 형태로 반환받을 수 있다.
또한 getBeanDefinition 메소드를 통해 BeanDefinition 객체에 접근하고 빈의 정보를 확인할 수 있다.
ex) getRole() 메소드로 해당 빈이 Infrastructure인지 Application인지 확인할 수 있다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
// Role ROLE_APPLICATION: Spring 내부에서 등록한게 아니라 내가 개발하기 위해 등록된 빈듯
// Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
Object bean = ac.getBean(beanDefinitionName);
System.out.println("App bean / name = " + beanDefinitionName + " object =" + bean);
}
if (beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE){
Object bean = ac.getBean(beanDefinitionName);
System.out.println("Infra bean / name = " + beanDefinitionName + " object =" + bean);
}
}
}
하나의 타입에 대해서 여러 개의 Bean이 생성될 수 있으며, 해당 케이스에서는 NoUniqueBeanDefinitionException이 발생한다. 그럴 때에는 Bean의 이름을 지정하여 선택해주도록 한다.
BeanDefinition 그 자체는 인터페이스고, 런타임에서 형태에 따라 다른 구현으로 동작한다.
BeanDefinition은 아래의 설정 값들이 있다.
(더 많은 설정들이 있을 수 있으나, implements가 많이 나뉘어져있어 정확히 파악하긴 어려운 것 같다.)
BeanDefinition의 설정 정보들을 통해 Bean을 좀 더 구체적으로 관리할 수 있다.
테스트 코드들을 생성할 클래스에서 shift+command+T를 통해 테스트 코드들을 한번에 생성할 수 있다.
테스트 함수들은 함수 위에 @Test 어노테이션이 추가되며, 테스트 코드들은 빌드시 포함되지 않으므로 한글로 메소드명을 표기하는 것이 가독성에 도움된다.
테스트 함수들은 순서가 무작위로 수행된다.
실행 순서에 따라 함수들의 테스트함수간 의존성이 생기지 않도록 인스턴스를 초기화해주는 용도로 활용할 수 있다.
해당 어노테이션을 통해 테스트를 스프링 프레임워크 위에서 수행할 수 있다.
참고 - 테스트에는 단위/통합/인수 3가지 종류가 있다.
그 중 통합 테스트는 외부 라이브러리와 같이 개발자가 접근할 수 없는 부분까지 함께 테스트를 하기 위해 수행되는 테스트이다. 만약 코드를 테스트해야되는데 단위 테스트로는 테스트가 불가능하고 반드시 특정 프레임워크 위에서 수행이 되어야한다면, 해당 테스트영역은 설계부터 잘못 설계됐을 소지가 있다.
테스트코드들 사이에서 @Transactional이 사용될 경우, 디비의 변경사항들을 commit하지 않는다. (매 테스트마다 서로간에 독립적일 수 있도록)
테스트 코드 내에서 given, when, then을 나누어 표기하여 직관적으로 이해가 쉽도록 한다.
테스트함수는 함수의 기본 로직 수행여부만을 검증하는 것에서 더 나아가, 해당 함수에서 구현된 예외들이 정상적으로 발생되는지까지 검증되어야한다.
assertThrows(예외클래스, 예외발생로직 람다식)를 통해 예외 발생 여부를 검증할 수 있다.
@Test
public void 중복_회원_예외(){
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
참고블로그: https://steady-coding.tistory.com/459
참고자료: inflearn 강의 (스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술)