Spring Boot 기초 - Container와 Bean, 테스트 코드 작성법

Hansu Kim·2022년 1월 31일
0

Spring boot

목록 보기
2/10

웹 애플리케이션의 기본 구조

  • Controller: 웹 MVC의 컨트롤러 역할
  • Service: 핵심 비지니스 로직 (회원가입, 탈퇴 등)
  • Repository: DB에 접근, 도매인 객체를 DB에 저장/관리
  • Domain: 비지니스 도메인 객체 (회원, 주문, 상품 등등)

Spring Container와 Bean

스프링 컨테이너는 자바 객체의 생명 주기를 관리하며, 생성된 객체들에게 추가적인 기능을 제공하는 역할을 한다. 이 때 생성된 객체들을 Bean이라고 부른다.
스프링 컨테이너는 객체들 간의 의존 관계를 런타임에서 알아서 만들어주며, 객체들의 생성/소멸까지도 알아서 관리해준다.

Spring Container의 종류

BeanFactory

스프링 컨테이너의 최상위 인터페이스.
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


ApplicationContext도 BeanFactory와 같이 빈을 관리할 수 있다. ApplicationContext는 BeanFactory를 상속받은 자손이기 때문이다.

  • 그 외 주요 기능

    • MessageSource: 국제화 기능
    • EnvironmentCapable: 환경 변수를 통한 로컬/개발/운영 구분 처리
    • ApplicationEventPublisher: 이벤트를 발행하고 구독하는 모델을 편리하게 지원 (중요해 보인다)
    • ResourceLoader: 파일, 클래스경로, 외부 등에서 리소스를 편리하게 조회
  • 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 초기화 시점에 모든 싱글톤 빈을 미리 로드한 후, 애플리케이션 가동 후에는 빈을 지연없이 받을 수 있으므로 빈을 지연없이 받을 수 있다는 장점으로 실무에서 많이 활용된다.

Dependency Injection - Spring Bean 등록 및 의존관계 설정

스프링은 스프링 컨테이너에 빈을 등록할 때, 특별한 상황이 아니라면 기본적으로 싱글톤으로 등록한다. 그에 따라 같은 스프링 빈이면 모두 같은 인스턴스다.
프로젝트 실행시 Spring에게 Spring Application이라고 인식시키고 의존성 관계를 주입시키는 방법은 두 가지가 있다.

  • 컴포넌트 스캔을 통한 자동 의존관계 설정
    • 컴포넌트별 클래스에 @Controller, @Service, @Repository, @Component등의 어노테이션으로 해당 클래스의 기능을 인식시킨다.
    • 클래스 생성자에 @Autowired 어노테이션으로 해당 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입한다.
      • 생성자가 1개만 있으면 AutoWired 생략 가능
  • 자바 코드로 직접 스프링 빈 등록
  @Configuration
  public class SpringConfig {
      @Bean
      public MemberService memberService(){
          return new MemberService(memberRepository());
      }

      @Bean
      public MemberRepository memberRepository(){
          return new MemoryMemberRepository();
      }
  }

DI 권장 설정 방법

DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방식이 있다.
필드 주입의 경우에는 자유도가 떨어지고, setter 주입의 경우에는 동적으로 Bean의 인스턴스가 변경될 수 있을 위험이 있으므로 생성자 주입 방식이 권장된다.

상세 포스팅: https://velog.io/@zihs0822/DI%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%A3%BC%EC%9E%85-%EB%B0%A9%EC%8B%9D%EB%B3%84-%EC%9E%A5%EB%8B%A8%EC%A0%90

컨테이너에 등록된 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의 이름을 지정하여 선택해주도록 한다.

Bean Definition 정보

BeanDefinition 그 자체는 인터페이스고, 런타임에서 형태에 따라 다른 구현으로 동작한다.
BeanDefinition은 아래의 설정 값들이 있다.
(더 많은 설정들이 있을 수 있으나, implements가 많이 나뉘어져있어 정확히 파악하긴 어려운 것 같다.)

BeanDefinition의 설정 정보들을 통해 Bean을 좀 더 구체적으로 관리할 수 있다.

  • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름
    • ex) appConfig
  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정
    • 예) memberService
  • Scope: 싱글톤(기본값)
  • lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
  • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
  • Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)

테스트 케이스 작성법

간단한 Test code generating

테스트 코드들을 생성할 클래스에서 shift+command+T를 통해 테스트 코드들을 한번에 생성할 수 있다.
테스트 함수들은 함수 위에 @Test 어노테이션이 추가되며, 테스트 코드들은 빌드시 포함되지 않으므로 한글로 메소드명을 표기하는 것이 가독성에 도움된다.

@AfterEach와 @BeforeEach

테스트 함수들은 순서가 무작위로 수행된다.
실행 순서에 따라 함수들의 테스트함수간 의존성이 생기지 않도록 인스턴스를 초기화해주는 용도로 활용할 수 있다.

@SpringBootTest

해당 어노테이션을 통해 테스트를 스프링 프레임워크 위에서 수행할 수 있다.

참고 - 테스트에는 단위/통합/인수 3가지 종류가 있다.
그 중 통합 테스트는 외부 라이브러리와 같이 개발자가 접근할 수 없는 부분까지 함께 테스트를 하기 위해 수행되는 테스트이다. 만약 코드를 테스트해야되는데 단위 테스트로는 테스트가 불가능하고 반드시 특정 프레임워크 위에서 수행이 되어야한다면, 해당 테스트영역은 설계부터 잘못 설계됐을 소지가 있다.

@Transactional

테스트코드들 사이에서 @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 접근 기술)

0개의 댓글