Study/Mastering-Spring-5-Study

ch02. 의존 관계 주입 및 단위 테스트하기

hongeeii 2022. 12. 31.
728x90
반응형

의존 관계 주입은 스프링 프레임워크의 가장 중요한 기능으로 느슨하게 연결된 애플리케이션을 쉽게 개발할 수 있다.
느슨하게 연결된 애플리케이션은 단위 테스트가 쉬우므로 유지 관리가 훨씬 쉽다.
스프링 프레임워크에서 의존 관계 주입은 스프링 IOC(Inversion of Control)컨테이너에서 구현된다.


클래스의 의존 관계는 역할을 수행하기 위해 의존하는 다른 클래스다.

  public class BusinessServiceImpl {
    DataServiceImpl dataService = new DataServiceImpl();
  }

DataServiceImpl은 BusinessServiceImpl의 의존 관계다.

중요한 설계 목표 중 하나는 SRP(Single Responsibilty Principle)다.
애플리케이션의 각 클래스와 컴포넌트는 명확하게 정의된 특정 책임을 갖는 것이 좋다.
애플리케이션을 쉽게 유지 관리하려면 SRP를 준수해야 한다.
SRP를 잘 구현하면 많은 의존 관계를 가진 여러 개의 작은 클래스가 생긴다.

의존 관계 주입의 개념

설계 목표의 핵심은 느슨한 결합이다.

각 구성요소와 클래스는 서로 느슨하게 연결돼야 한다.
다른 클래스와 구성요소에 영향을 주지 않고 클래스 및 구성요소를 변경할 수 있게 한다.

    DataServiceImpl dataService = new DataServiceImpl();

BusinessServiceImpl이 DataServiceImpl 인스턴스를 직접 생성하는데 두 클래스 사이에 단단한 결합을 만든다.
다른 DataService를 구현할 수 있게 하려면 BusinessServiceImpl 코드를 바꿔야 한다.
단단하게 결합된 코드는 유지 관리가 어렵고 단위 테스트도 어렵다.

인터페이스를 사용하면 느슨하게 연결된 코드를 만드는 데 도움이 된다.

    DataService dataService = new DataServiceImpl();

DataService 인터페이스를 사용하고 있지만 BusinessServiceImpl은 DataServiceImpl 인스턴스를 생성함으로써 여전히 밀접하게 결합돼 있다.

로직을 이동해서 다른 곳에 DataServiceImpl을 생성하고 BusinessServiceImpl에서 사용 가능하게 만들면 어떨까?

  public class BusinessServiceImpl {
    private DataService dataService;
    public BusinessServiceImpl(DataService dataService){
      this.dataService = dataService;
    }
  }

BusinessServiceImpl은 더이상 DataServiceImpl과 단단하게 연결돼 있지 않다.
DataService를 구현한 후 생성자 인수로 전달해 BusinessServiceImpl을 작성할 수 있게 됐다.
코드를 더 느슨하게 결합하려면 BusinessService의 인터페이스를 만들어야한다.

    DataService dataService = new DataServiceImpl(); //생성하기 
    
    BusinessService businessService = new BusinessServiceImpl(dataService); //생성하고 연결

누가 DataServiceImpl 클래스의 인스턴스를 생성해 BusinessServiceImpl 클래스의 인스턴스를 만들것인가에 대한 질문은 스프링 프레임워크가 해결한다.

Spring framework 주요 용어

  • Bean(빈)
    • Spring IoC 컨테이너가 관리하는 자바 객체
    • ApplicationContext.getBean()으로 얻어질 수 있는 객체
    • 이전 예제에서의 dataService와 businessService라는 인스턴스
  • 와이어링
    • 의존성 주입을 통해 빈을 연결하는 것
    • dataService를 생성해 BusinessServiceImpl 생성자의 인수로 제공한 것
  • 의존 관계 주입(DI, Dependency Injection)
    • bean 식별, 생성 및 와이어링 의존 관계 프로세스를 의존 관계 주입이라고 함
    • DI(의존성 주입)를 통해서 모듈 간의 결합도가 낮아지고 유연성이 높아진다.
  • IOC(Inversion of Control)
    • 제어의 역전
    • 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라, 외부에서 결정되는 것을 의미
    • 의존성을 역전시켜 객체 간의 결합도를 줄이고 유연한 코드를 작성
    • 가독성 및 코드 중복, 유지 보수를 편하게 함
    • BusinessServiceImpl은 이제 DataServiceImpl을 생성할 책임이 없어졌다.
    • 의존 관계를 만드는 책임을 프레임워크로 옮기는 것.

스프링 프레임워크의 역할

스프링 IOC 컨테이너라고 하는 스프링 프레임워크는 의존 관계를 식별하고 함계 인스턴스화 하고 연결하는 역할을 해야 한다.

  • 스프링 IOC 컨테이너가 어떤 빈을 생성해야 하는지 어떻게 알까?
    • BusinessServiceImpl과 DataServiceImpl 클래스에 빈을 생성하는 방법을 알고 있을까?
  • 스프링 IOC 컨테이너는 빈을 서로 연결하는 방법을 어떻게 알까?
    • DataServiceImpl 클래스의 인스턴스를 BusinessServiceImpl 클래스의 인스턴스에 주입하는 방법을 어떻게 알까?

스프링 IOC 컨테이너가 어떤 빈을 생성해야 하는지 어떻게 알까?

@Component어노테이션을 사용하면 스프링 IOC 컨테이너는 어떤 빈을 생성해야 하는지 알 수 있다.

@Component
public class DataServiceImpl implements DataService

@Component
public class BusinessServiceImpl implements BusinessService

스프링 IOC 컨테이너는 클래스에서 어노테이션을 보고 클래스의 인스턴스를 만든다.
이러한 인스턴스를 빈(Bean)이라고 한다.

@Component는 bean을 정의하는 가장 일반적인 방법이다.
@Service 어노테이션은 비즈니스 서비스 구성요소에 사용된다.
@Repository어노테이션은 DAO(Data Access Object) 구성요소에서 사용된다.

추가 기능을 제공한다면 일반 @Component 어노테이션보다 @Service, @Repository와 같은 특수 어노테이션을 사용하는 것이 좋다.
예를 들어 @Repository는 데이터 레이어에서 자동 트랜잭션 관리를 제공함.

스프링 IOC 컨테이너는 빈을 서로 연결하는 방법을 어떻게 알까?

DataServiceImpl 클래스의 빈은 BusinessServiceImpl 클래스의 빈에 주입해야 한다.
BusinessServiceImpl 클래스에서 @Autowired 어노테이션을 지정해 주입할 수 있다.

  @Service
  public class BusinessServiceImpl implements BusinessService{
    @Autowired
    private DataService dataService;
  }

스프링 IOC 컨테이너 생성

스프링 IOC 컨테이너를 생성하는 데는 두 가지 방법이 있다.

  • BeanFactory
  • ApplicationContext

BeanFactory를 직접 사용하는 일은 거의 없다. 주로 부가 기능이 추가된ApplicationContext를 사용한다.

스프링 프레임워크 초기에는 ApplicationContext의 모든 구성이 xml파일로 지정됐다.
현재의 애플리케이션은 자바 어노테이션 기반 구성을 사용한다.
자바 컨텍스트 구성은 @Configuration 어노테이션이다.
스프링 IOC 컨테이너는 컴포넌트 스캔을 정의해 검색할 패키지를 스프링 IOC 컨테이너에 알린다.

  @Configuration
  @ComponentScan(basePackages = {"com.mastering.spring"})
  public class SpringContext{}

java 구성을 기반으로 ApplicationContext를 가져와서 Bean을 가져오는 방법

  ApplicationContext context = new AnnotationConfigApplicationContext(SpringContext(@Configuration 붙어있는 class).class);
  BusinessService(빈 class) service = context.getBean(BusinessService(빈 class));
  1. com.mastering.spring 패키지를 스캔하고 BusinessServiceImpl와 DataServiceImpl 빈을 찾는다.
  2. DataServiceImpl에는 의존 관계가 없으므로 빈이 생성된다.
  3. BusinessServiceImpl은 DataService에 의존하고, DataServiceImpl은 DataService인터페이스의 구현체임으로 오토와이어링 기준과 일치한다. BusinessServiceImpl 빈이 생성되고, DataServiceImpl로 생성된 빈이 setter를 통해 자동으로 연결된다.

빈과 의존 관계를 식별하기 위해 컴포넌트 스캔이 수행 된다.
빈이 생성되고 필요에 따라 의존 관계가 연결된다.(Autowiring)

스프링 프레임워크

컨테이너 관리 빈

빈을 생성하고 관리하는 기능을 컨테이너에 위임하면 이로운 점이 많다.

  • 클래스는 의존 관계 형성에 책임이 없기 때문에 느슨하게 결합돼 테스트할 수 있다. 디자인은 좋아지고 결함은 적어진다.
  • 컨테이너가 빈을 관리하므로 빈주위에 몇 개의 기능을 더 일반적인 방식으로 도입할 수 있다. 로깅, 캐싱, 트랜잭션 관리 및 예외 처리와 같은 크로스 컷팅을 AOP를 사용해 빈 중심으로 구성할 수 있다.

static이 bean보다 먼저 메모리 적재됨!

DI(Dependency Injection)의 3가지 방법

  1. 필드 주입 (@Autowired)
  2. setter 주입
  3. 생성자 주입

필드 주입 - @Autowired 어노테이션

@Autowired를 의존 관계에 사용하면 ApplicationContext는 일치하는 의존 관계를 검색한다.

  • 기본적으로 오토와이어링된 모든 의존 관계가 필요하다.
  • DI Container와 강한 결합을 가져 외부(spring 제외) 사용이 용이하지 않는다. 단 테스트는 용이함
  • 의존성 주입 대상 필드가 final 선언 불가
  • 순환 의존성이 발생 할 수 있음

※ 순환 의존성 : A Class가 B Class를 참조하는데 B Class가 다시 A Class를 참조할 경우,
A Class가 B Class를 참조하고, B Class가 C Class를 참조하고 C Class가 A Class를 참조하는 경우 이를 순환 의존성(Circular Dependency)이라고 부른다.

setter 주입

  • Setter 주입은 생성자 주입과 다르게 주입받는 객체가 변경될 가능성이 있는 경우에 사용한다. (실제로 변경이 필요한 경우는 극히 드물다.)
@Service
public class BusinessServiceImpl implements BusinessService{
 private DataService dataService;

@Autowired
public void setDataService(DataService dataService){
this.dataService = dataService;
}
}

생성자 주입

  • Spring Framework Reference에서 권장하는 방법은 생성자를 통한 주입이다.
  • null을 주입하지 않는 한 NullPointerException 은 발생하지 않는다.
  • 생성자를 사용하는 방법이 좋은 이유는 필수적으로 사용해야하는 의존성 없이는 Instance를 만들지 못하도록 강제할 수 있기 때문이다.
  • final 을 사용할 수 있다.(객체의 불변성)
  • 순환 의존성을 알 수 있다.
@Service
public class BusinessServiceImpl implements BusinessService{
  private DataService dataService;

@Autowired
public BusinessServiceImpl(DataService dataService){
 super();
 this.dataService = dataService;
}
}

스프링 프레임워크는 생성자주입을 적극 지원하기 때문에 생성자가 1개만 있을 경우에 @Autowired를 생략해도 주입이 가능하도록 편의성을 제공하고 있다.

@Service
public class BusinessServiceImpl implements BusinessService{
 private DataService dataService;

//  @Autowired
public BusinessServiceImpl(DataService dataService){
super();
this.dataService = dataService;
}
}

Lombok을 쓰게 된다면 이를 간소화 할 수 있는데 @RequiredArgsConstructor를 사용하면 된다.

@RequiredArgsConstructor
@Service
public class BusinessServiceImpl implements BusinessService{
  private final DataService dataService;

}

의존 객체 탐색 순서

  1. 타입이 같은 빈 객체를 검색한다. 객체가 한개면 그 빈 객체를 사용한다.
  2. 타입이 같은 빈 객체가 두개 이상이면, @Primary나 @Qualifier와 같은 값을 갖는 빈 객체를 찾고 존재하면 그 객체를 사용한다.
  3. 타입이 같은 빈 객체가 두개 이상 있고, @Qualifier나 @Primary가 없다면 이름이 같은 빈 객체를 찾는다. 존재하면 그 객체를 사용한다. 1-3 의 경우를 모두 만족하지 않는다면 IOC 컨테이너는 Exception을 발생시킨다.

둘 이상의 후보가 발견되면 두 가지 방법으로 해결할 수 있다.

  • 후보 중 하나만 사용하려면 @Primary 어노테이션 사용
  • 오토와이어링을 더욱 강화하려면 @Qualifier 어노테이션 사용
    • 스프링 빈에 오토와이어링할 필요가 있는 의존관계를 규정하는데 사용

스프링 빈 스코프 사용자 정의

스프링의 기본 스코프는 싱글톤(Singleton)이다.
일반적으로 싱글톤이랑 JVM당 하나의 인스턴스만 있는 자바 클래스지만, 스프링 컨텍스트의 싱글톤은 스프링 ApplicationContext당 하나의 인스턴스를 가진 클래스
싱글톤 빈의 인스턴스는 하나뿐이므로 요청(request)와 관련된 데이터를 포함할 수 없다. 스코프는 모든 스프링 빈에서 @Scope 어노테이션과 함께 제공된다.

@Service
@Scope("singleton")
public class BusinessServiceImpl implements BusinessService{}
  • 싱글톤
    • 기본적으로 모든 빈의 스코프는 싱글톤
    • 빈의 인스턴스는 스프링 IOC 컨테이너의 인스턴스당 하나만 사용
    • 빈에 여러 참조가 있어도 컨테이너당 한 번만 생성
  • 프로토타입
    • 스프링 컨테이너에서 빈을 요청할 때마다 새 인스턴스가 생성
    • 빈에 상태가 포함돼 있는 경우 좋다.
  • 리퀘스트
    • 스프링 웹 컨텍스트만 사용 가능
    • 모든 HTTP 요청마다 빈의 새 인스턴스가 생성
    • 빈은 요청 처리가 완료되는 즉시 폐기
    • 단일 요청과 관련된 데이터를 보유하고 있는 빈에 이상적
  • 애플리케이션
    • 스프링 웹 컨텍스트에서만 사용가능
    • 웹 애플리케이션당 하나의 빈 인스턴스로 특정 환경의 애플리케이션 구성에 적합
    기타 중요한 스프링 어노테이션
    • @ScopedProxy
      • 요청이나 세션 스코프의 빈을 싱글톤-스코프의 빈에 주입해야 할 경우에 스마트 프록시를 제공
    • @Component, @Service, @Controller, @Repository
      • @Component : 스프링 빈을 정의하는 가장 일반적인 방법, 다른 어노테이션은 이와 관련된 특정 컨텍스트를 갖고 있음.
      • @Service : 비즈니스 서비스 레이어에서 사용
      • @Repository : 데이터 액세스 객체(DAO)에서 사용
      • @Controller : 프레젠테이션 구성요소에 사용
    • @PostConstruct
      • 모든 스프링 빈에서 post construct 메소드를 제공
      • 빈이 의존 관계로 완전히 초기화된 후에 호출
      • 빈 생명 주기 동안 한 번만 호출
    • PreDestroy
      • 모든 스프링 빈에서 predestroy 메소드 제공
      • 컨테이너에서 빈이 제거되기 전에 호출
      • 빈이 보유한 모든 리소스를 해제하는 데 사용

Bean의 Lifecycle

스프링 빈 라이프 사이클 메소드 실행 순서

Bean에 대해 여러 라이프 사이클 메커니즘이 구성되고 각 메커니즘이 다른 메소드 이름으로 구성된 경우 구성된 각 메소드는 아래 나열된 순서대로 실행됩니다.

초기화 방법의 경우

  • @PostConstruct 로 주석이 달린 메소드
  • InitializingBean 콜백 인터페이스 에 의해 정의된 afterPropertiesSet()
  • 사용자 정의 구성된 init() 메서드

Destroy 메소드는 같은 순서로 호출됩니다.

  • @PreDestroy 주석이 달린 메소드
  • DisposableBean 콜백 인터페이스 에 의해 정의된 destroy()
  • 사용자 정의 구성 destroy() 메소드---- 기본적으로 spring boot는 위에서 아래로 bean등록이 된다. ----
    1. 생성자가 먼저 생성됨 (Component가 붙은)
    2. autoWired 주입
    3. PostConstructor
    4. Bean 생성

    스프링 ApplicationContext의 단위 테스트

    TDD(Test-Driven-Development)를 사용해 코드 이전에 테스트를 작성하는 것이 좋다.
    TDD를 사용하면 간단하고 유지 관리가 가능하며 테스트할 수 있는 코드가 된다.단위 테스트는 배포된 전체 애플리케이션을 테스트하는 대신 각 클래스와 각 메소드의 책임에 자동화된 테스트를 작성하는 데 중점을 둔다.
    단위 테스트에는 다음과 같은 이점이 있다.
    • 미래 결함에 대비한 안정망이다.
    • 결함의 조기 발견이 가능하다.
    • TDD가 더 나은 디자인을 만든다.
    • BDD(Given-When-Then) , 잘 작성된 테스트는 코드와 기능의 문서화 역할을 한다.
    스프링 프레임워크를 사용하면서 느슨하게 결합된 코드로 단위 테스트를 쉽게 할 수 있다.
    스텁(stub)이나 모킹 의존 관계를 사용할 수 있으며 스프링 프레임워크에 의존해 와이어링할 수도 있다.
    @RunWith(SpringJUnit4ClassRunner.class) // ApplicationContext를 시작하기 위함
    @ContextConfiguration(classes = {SpringContext.class}) // ApplicationContext 정의
    public class BusinessServiceJavaContextTest{
      @Autowired
      private BusinessService service;
    
      @Test
      public void testCalculateSum(){
        long sum = service.calculateSum(Dummy_User);
        assertEquals(30, sum);
      }
    }
  • ApplicationContext를 사용해 JUnit 작성
  • 단위 테스트의 개념
  • Bean 생성순서!!!!!!!!!!!!!!!!!!!!

위의 테스트가 만약 데이터베이스 테스트였다면 데이터들이 달라질 수 있다.
BusinessServiceImpl이 의존하는 DataService를 가져와야 한다.

  1. DataService의 스텁을 구현 1-1. @DirtiesContext 를 사용해 별도의 테스트 컨텍스트 구성을 사용해야 함.
  2. DataService의 모크(mock)를 생성하고 모크를 BusinessServiceImpl에 오토와이어링 함.

mock을 사용한 단위 테스트

mocking은 실제 객체의 동작을 시뮬레이션하는 객체를 만든다.
스텁과 달리 모크는 런타임에 동적으로 생성될 수 있다.
가장 인기있는 모크 프레임워크는 Mockito를 사용한다.

  • DataService의 모크를 생성하기 위해 @Mock 어노테이션을 사용한다.
  • BusinessServiceImpl로 모크를 만들고 싶다면 @InjectMocks 어노테이션을 사용한다.
    • @InjectMocks는 서비스의 모든 의존 관계를 오토와이어링 한다.

@Mock

@Mock 으로 만든 mock 객체는 가짜 객체이며 그 안에 메소드 호출해서 사용하려면 반드시 스터빙(stubbing)을 해야함.
만약, 스터빙을 하지 않고 그냥 호출한다면 primitive type은 0, 참조형은 null을 반환.
Mock으로 만든 객체의 메소드가 리턴 값을 가진다면 테스트 데이터를 리턴하는 모크를 만들어야한다.
Mockito가 제공하는 BDD스타일로 메소드를 사용한다.

@RunWith(MockitoJUitRunner.class) // Mockito를 사용하기 위함
public class BusinessServiceJavaContextTest{
  @Mock
  private DataService dataServiceMock;

  @InjectMocks
  private BusinessService service = new BusinessServiceImpl();

  @Test
  public void testCalculateSum(){
    BDDMockito
          .givin(dataServiceMock.retrieveData(Matchers.any(User.class))
          .willReturn(Arrays.asList(.....)); 
    long sum = service.calculateSum(Dummy_User);
    assertEquals(30, sum);
  }
}

@Spy

@Spy로 만든 mock 객체는 진짜 객체이며 메소드 실행 시 스터빙을 하지 않으면 기존 객체의 로직을 실행한 값을, 스터빙을 한 경우엔 스터빙 값을 리턴.

@InjectMocks

@InjectMock은 DI를 @Mock이나 @Spy로 생성된 mock 객체를 자동으로 주입해주는 어노테이션

스터빙(Stubbing)

만들어진 mock 객체의 메소드를 실행했을 때 어떤 리턴 값을 리턴할지를 정의하는 것

스터빙을 할 수 있는 방법은 OngoinStubbing, Stubber를 쓰는 방법 2가지가 있다.

OngoingStubbing 메소드
OngoingStubbing 메소드란 when에 넣은 메소드의 리턴 값을 정의해주는 메소드

when({스터빙할 메소드}).{OngoingStubbing 메소드};

Stubber 메소드
스터빙이 반드시 실행되야 하는 경우 사용하는 메소드

{Stubber 메소드}.when({스터빙할 클래스}).{스터빙할 메소드}

Stubber 메소드를 사용해야 하는 이유

 또한 리턴 타입인 void 메소드 테스트가 가능해짐.

스터빙한 메소드를 검증하는 방법

verify 메소드를 이용해서 스터빙한 메소드가 실행됐는지, n번 실행됐는지, 실행이 초과되지 않았는지 등 다양하게 검증해볼 수 있다.

verify(T mock, VerificationMode mode);

ex) verify(userService, times(2)).getUser();

위와 같이 쓰이며 VerificationMode는 검증할 값을 정의하는 메소드이다.

 

728x90
반응형

추천 글