TIL - JAVA spring DAY 14

jihan kong·2022년 1월 21일
0

JAVA spring 입문

목록 보기
16/20
post-thumbnail

어느덧 인프런 Spring 입문 강의의 2/3 정도를 듣게 되었다. 강의를 들으면서 느끼는 것은 back-end. 즉, 뒷단에서 고려해야할 부분들이 상당히 많다는 것이다. Repository를 구성하고 service를 구축하고 그것이 잘 만들어졌는지 테스트하고 또 서버를 DB와 연결하고... 등등 전체적인 시각에서 바라보아야할 뿐만 아니라 각 클래스 구현에서의 디테일 또한 중요하다. 즉, 어떻게 DI를 구성할 것인지... Http method는 get을 사용할 지, Post를 사용할 지 등에 관한 것이다.

서두가 길었다. 오늘은 스프링을 전체적으로 통합해서 테스트하는 것을 학습하였다. 우리가 만든 JdbcmemberRepository 는 Jdbc 기술을 통해 DB까지 연결이 되는 상태이다. 그렇다면 지금까지는 사실 java내에서 코드를 통해 메모리상에서만 테스트를 진행했지만 아예 스프링과 연동해서 DB까지 연결하여 동작하는 테스트를 진행할 수 있다.

이른바, 통합 테스트(IntegrationTest) 이다. 이를 위해, 먼저 service package에 MemberServiceIntegrationTest class를 하나 만든다.

MemberServiceIntegrationTest class는 일단은 전에 만들었던 MemberServiceTest 를 기반으로 수정해서 만들 계획이므로, 복사해서 붙여넣기로 만든다.

MemberServiceIntegrationTest (미완)

class MemberServiceIntegrationTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void 회원가입() {
        // given
        Member member = new Member();
        member.setName("spring");

        // when
        Long saveId = memberService.join(member);

        // then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @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("이미 존재하는 회원입니다.");

        /*
        try {
            memberService.join(member2);
            fail();
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
        */

        // then
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

그 후, class 명 위에 annotation을 추가하는데 다음과 같다.

@SpringBootTest
@Transactional

못보던 친구들이다. 하나씩 알아보자.

@SpringBootTest

여기서 사실 강사님께서 감탄하셨는데 (이는 나의 감탄이기도 했다) 지금까지 예전에는 테스트 코드를 작성하기 위해 많은 노력이 필요했었다. 그러나 이제 저 한줄로 test를 진행할 수 있다니... (spring의 위대함이다) 스프링 컨테이너와 테스트를 함께 실행할 수 있는, 다시 말해 스프링 기반의 테스트가 작동됨을 의미한다.

그럼 이제, MemberServiceTest 를...

어떻게 수정하면 될까?

  1. BeforeEach() 메소드를 모두 지운다. 왜냐하면 지금까지는 MemberRepository, MemberService 객체를 직접 생성해서 만들었는데, 이제는 spring container로 부터 객체들을 받아서 사용하기 때문이다.
  2. Injection을 할 때는 강사님께서 test의 경우, 가장 끝단에서 실행되기 때문에 그냥 @Autowired로 편하게 코딩해도 된다고 하셨다.
    그런 다음 MemoryMemberRepositoryMemberRepository 로 바꿔주기만 하면 된다.
  3. @AfterEach 메소드를 지운다. 메모리에 저장된 데이터를 다음 테스트를 진행할 때, 영향을 주지 않기 위해서 이 코드를 작성했었는데, 이제는 메모리를 사용하지 않기 때문에 필요가 없기 때문이다.

이러한 수정사항들을 반영하여 완성된 MemberServiceIntegrationTest 는 다음과 같다.

MemberServiceIntegrationTest (완)

@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {

    @Autowired
    MemberService memberService;
    @Autowired
    MemoryMemberRepository memberRepository;

    @Test
    void 회원가입() {
        // given
        Member member = new Member();
        member.setName("spring");

        // when
        Long saveId = memberService.join(member);

        // then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

    @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("이미 존재하는 회원입니다.");

	}
}

이제 테스트를 실행시켜보자. 다음과 같이 spring을 통해 test가 성공적으로 완료되었음을 확인할 수 있다.

그런데 테스트라함은 반복적으로 실행할 수 있어야 한다. 여기서 회원가입 test를 다시 실행하면 어떤 일이 벌어질까?

이미 setName("spring");으로 값을 저장했는데, 같은 값을 또 저장하려니 "이미 존재하는 회원입니다." 라고 하면서 오류가 발생했다. 그렇다면, 전처럼 BeforeEach 문을 작성하고 데이터베이스 값을 지워주는 번잡스러운 작업을 또 해야할까...?

@Transactional

Spring은 이에 대해 새로운 해결책을 제공해준다. 데이터베이스에는 트랜잭션(Transaction) 이라는 개념이 존재한다. 기본적으로 Query를 Insert 했다면, Commit 연산을 진행해야 DB에 반영이 되며, 이것이 트랜잭션의 수행 완료 과정이다.
근데 만약 테스트가 끝난 후, Rollback 을 하게 된다면 어떻게 될까?
객체를 insert 하는 Query를 날리고 findOne, findMember 와 같은 검증 과정을 거친 후, Rollback 을 실행하면 데이터가 반영이 되지않고 마치 없던 일처럼 전으로 돌아가게 된다.

테스트 케이스에 @Transactional 이 있으면, 테스트 시작 전에 트랜잭션을 시작하게 되고, 테스트가 끝나면 항상 Rollback을 해준다. 이렇게 하면 DB에는 데이터가 남아있지 않게 되므로 다음 테스트에 영향을 주지 않는다. 다음의 과정을 통해 실제로 검증해보자.

먼저, 위와 같이 h2 콘솔창에서 delete from member;member table에 있는 값들을 모두 제거한다. 그 후, 회원가입 테스트를 실행한다.

테스트 성공
그리고, 또 다시 돌려보게 되면....

이렇게 성공하는 것을 볼 수 있다.

DB

대학 학부과정 중 SQL 데이터베이스 관련 수업을 들은 적이 있다. 그 때, 트랜잭션의 특징이라고 해서 Atomicity(원자성), Consistency(일관성), Isolation(격리성), Durability(지속성) 를 앞글자만 따서 ACID 라고 외우곤 했다.
서두에서도 이야기 했지만 백엔드 개발자라면 DB에 대한 개념이 잡혀있어야하고, SQL과 같은 데이터베이스 문법과 친숙해야한다. 당장 오늘 배운 트랜잭션과 관련된 skill(?)을 사용할 때에도 그렇다. 당연한 이야기겠지만 모르면 알 수 없는 이야기이다. 부족한 부분이 있을 때, 오히려 기뻐하고 또 그것을 공부하고 배워서 채워나갈때의 보람을 느끼는 개발자가 되기를 희망한다.

profile
학습하며 도전하는 것을 즐기는 개발자

0개의 댓글