인프런/스프링 입문

9)순수 JDBC, DI(dependency injection)과 다형성의 장점 , 스프링통합테스트

backend dev 2022. 11. 27.

어플리케이션에서 DB를 연결해서 DB에 저장시킬수있게끔 하자.

jdbc와 h2를 사용하기위해 dependency 추가

 

resource 아래 application.properties에 데이터베이스 관련 정보를 적어준다.

 

 

예전에는 요즘 jdbc를 사용하지않고 개발할때는

데이터베이스를 연결하기 위해 jdbc 리포지토리를 하나 만들었다.

이런식으로 개발을 했었다.

 

상당히 코드가 길고 복잡했음.

 

그렇게

 JdbcMemberRepository를 개발하고

Spring Config에서 멤버리포지토리 객체를 생성하는곳에서 return new JdbcMemberRepository로 바꿔주면된다.

@Configuration
public class SpringConfig {

    @Bean //@Bean 어노테이션 -> 스프링빈을 등록할거다 라는 의미
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

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


}

이렇게 DI(의존성주입)과 인터페이스를 이용한 다형성이 없었더라면

 

리포지토리를 하나 바꾸려면 많은 코드수정이 필요했을것이다.

 

 

https://dahye-jeong.gitbook.io/java/java/undefined/java-interface

 

DataSource dataSource

@Autowired
DataSource dataSource;

스프링빈에 들어가있다면 이런식으로 @Autowired 해서 가져와도 된다. (간단한버전)

 

일반적으로는

private final DataSource dataSource;

@Autowired
public SpringConfig(DataSource dataSource) {
    this.dataSource = dataSource;
}

이런식으로 

 

@Configuration
public class SpringConfig {

    private final DataSource dataSource;

    @Autowired
    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean //@Bean 어노테이션 -> 스프링빈을 등록할거다 라는 의미
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

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


}

 

 

JdbcMemberRepository라는 구현체를 추가하고 난뒤  모습

 

 

원래는 위의 그림처럼 멤버서비스가 멤버리포지토리를 가리키고있었는데

SpringConfig에서 jdbc레포지토리로 리포지토리 스프링빈을 등록했기에

이제는 jdbc리포지토리쪽을 가리키게된다.

 

 

 

 

 

스프링통합테스트

스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행해보자.

지금까지 테스트작성한것은 순수한 자바코드로 테스트한것이다. 스프링과 관련이 없었다.

 

 

테스트와 spring을 엮어서 테스트하는법을 알아보자. (편하게 들으면된다) 

MemberServiceTest와 똑같은걸 복사해서 MemberServiceIntegrationTest라는 파일을 만들자.

@Springboot를 추가해주고, @Transactional을 추가해준다.

이 테스트를 할떄는 직접 리포지토리를 생성해서 넣어줬는데 지워준다.

그리고 이제 스프링빈에 있는 멤버서비스와 멤버리포지토리를 가져와야하는데 

 

생성자 의존성주입 보다는 필드 의존성주입으로 간단하게 할 수 있다.( 테스트에서는 주입되는게 바뀌지 않을거고 , 가져오기만 하면되니까)

 

@BeforeEach 부분삭제해주고

@AfterEach 부분도 삭제해준다. (@Transactioanl로 인해 데이터의 변화가 없을것이므로 없어도된다)

 

각 객체에 @Autowired 붙여주었다.

그리고 메모리멤버리포지토리를 직접 생성해서 넣어줬기에 메모리멤버리포지토리 객체를 생성하는 부분이였는데 

그 부분을 멤버리포지토리(인터페이스) 변수로 바꾸어준다.

@Autowired
MemberService memberService; //멤버 서비스 선언
@Autowired
MemberRepository memberRepository;//멤버 저장소 선언

 

 

 

@SpringBootTest

 

h2에 있는 데이터를 지우고 회원가입 테스트를 돌려보면 

스프링이 실행되면서 테스트가 실행되는것을 확인할 수 있다.  (@SpringBootTest 덕분!)

테스트가 끝나면 테스트를 위해 켜진 스프링이 꺼진다!

 

 

 

 

@Transactional

만약 위에서 진행한 회원가입 테스트를 @Transactional 어노테이션없이 진행했다면 어떻게될까?

똑같이 통과는 하지만

테스트를 위해 넣어본 spring 멤버가 데이터베이스에 저장되어있게 된다.

이렇게되면 다음에 또 회원가입을 테스트할때 중복회원이 있는지 검사하는부분에서 걸리게되어 테스트를 통과하지못하게된다.

 

이런 불상사를 없애기위에 예전에는 AfterEach를 이용하여 리포지토리안에 데이터를 없애는 메소드를 만들고 사용했는데

 

그건 불편하므로

@Transactional로 쉽게 간다.

 

트랜잭션으로 어떻게?

 -> sql문을 실행한 뒤 변경사항에 대한 커밋을 안하고 롤백해버려서 원래 상태대로 돌릴 수 있었던것!

 

정리!

결론

 

MemberServiceTest.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;

import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Optional;

import static org.assertj.core.api.Assertions.*;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService; //멤버 서비스 선언
    MemoryMemberRepository memberRepository;//멤버 저장소 선언

    @BeforeEach // beforeEach는 각 테스트메소드가 실행되기전에 실행되는 메소드를 설정하기위한 어노테이션
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository(); // 새로 저장소를 생성해서 초기화해주고
        memberService = new MemberService(memberRepository); // 새로 생성한 저장소를 전달해서 멤버서비스까지 생성 및 초기화 해준다.
        //이렇게 함으로서 모든 테스트케이스가 시작전에 새로운 멤버저장소,멤버서비스를 가지기 떄문에 다른 테스트 메소드의 영향을 받을일이 없어진다.
        //테스트 메소드는 독립적으로 실행이 되야하기 때문에 다음과 같은 설정을 한다.
    }

    @AfterEach //하나의 테스트 메소드가 동작이 끝날때마다 해당 어노테이션이 붙은 메소드를 동작하라는뜻.
    public void afterEach()
    {
        memberRepository.clearStore(); // 저장소에있는 내용 삭제
        //저장소역할을 하는 store 변수와 멤버아이디를 만들기위한 sequence 변수는 static 이므로
        //위에처럼 새롭게 저장소객체를 만들어도 static 변수는 공용이기떄문에 접근가능하다.
    }


    @Test
    void 회원가입() { //테스트 메소드는 한글로도 이름을 정할 수 있음.
        //given 값이 주어졌을때
        Member member = new Member();
        member.setName("spring");

        //when  실행했을때
        Long saveId = memberService.join(member);

        //then 결과가 이렇게 나와야한다.
        Member findMember = memberService.findOne(saveId).get(); // .get을 이용해서 optional을 벗긴 멤버값을 받을 수 있다.
        assertThat(member.getName()).isEqualTo(findMember.getName()); //방금만든 멤버의 이름이 나와야한다.
        //그러므로 멤버의 이름이 실제값(actual) findmember 했을때 나오는 이름이 예상값(expected)

    }

    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");
        //when
        memberService.join(member1); // member1을 회원가입
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));// 메시지검증을 하고싶다면 리턴값을 저장해서
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다"); //검증한다.

//        try {
//            memberService.join(member2); // member2를 회원가입 시킬때 중복된 이름으로 인해 오류가 발생해야함
//            //이 다음줄로 넘어간다는것은 오류가 발생해야하는데 catch로 가지않았다는것이므로
//            fail();// 테스트 실패!
//
//        }catch (IllegalStateException e )
//        {
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다"); //catch에 아무것도 안적어도 try catch로 잘넘어갔기때문에 테스트하면 성공이 뜬다.
//        }

        //then
    }


    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

다음과 같은 테스트는 자바코드로서 동작하는것이므로 엄청 빠르게 테스트가 된다.

(통합테스트는 스프링서버를 띄우고까지의 시간이 추가되므로 오래걸린다)

 

이러한 테스트는 단위테스트라고 부른다.

 

순수한 단위테스트가 훨씬 좋은 테스트이다.

단위로 잘쪼개서 테스트할 수 있도록 하고,  스프링 컨테이너 없이 테스트 할 수 있도록 훈련을 해야한다.

 

 

아까 위에서 한 MemberServiceIntegrationTest.java는 통합테스트이다.

스프링컨테이너를 어쩔 수 없이 올려서 테스트를 해야하는것은 

테스트설계부터 잘못됬을 확률이 높다.

 

 

댓글