어느덧 인프런 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
를...
BeforeEach()
메소드를 모두 지운다. 왜냐하면 지금까지는 MemberRepository
, MemberService
객체를 직접 생성해서 만들었는데, 이제는 spring container로 부터 객체들을 받아서 사용하기 때문이다.MemoryMemberRepository
만 MemberRepository
로 바꿔주기만 하면 된다.@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에 있는 값들을 모두 제거한다. 그 후, 회원가입 테스트를 실행한다.
테스트 성공
그리고, 또 다시 돌려보게 되면....
이렇게 성공하는 것을 볼 수 있다.
대학 학부과정 중 SQL 데이터베이스 관련 수업을 들은 적이 있다. 그 때, 트랜잭션의 특징이라고 해서 Atomicity(원자성), Consistency(일관성), Isolation(격리성), Durability(지속성) 를 앞글자만 따서 ACID 라고 외우곤 했다.
서두에서도 이야기 했지만 백엔드 개발자라면 DB에 대한 개념이 잡혀있어야하고, SQL과 같은 데이터베이스 문법과 친숙해야한다. 당장 오늘 배운 트랜잭션과 관련된 skill(?)을 사용할 때에도 그렇다. 당연한 이야기겠지만 모르면 알 수 없는 이야기이다. 부족한 부분이 있을 때, 오히려 기뻐하고 또 그것을 공부하고 배워서 채워나갈때의 보람을 느끼는 개발자가 되기를 희망한다.