당니의 개발자 스토리

회원 기능 테스트 본문

스프링/실전! 스프링 부트와 JPA 활용1

회원 기능 테스트

clainy 2024. 4. 27. 15:52

회원 기능 테스트

이번 시간에는 회원 기능이 잘 동작하는지 테스트 코드로 한번 검증해보겠습니다.

이제 테스트는 회원 가입을 성공해야 되고요. 그 다음에 회원을 가입할 때 같은 이름이 있으면 예외가 발생해야 되겠죠. 두 가지가 되게 중요한 테스트입니다.

자 그럼 한번 만들어 보겠습니다.

먼저 이 MemberService 클래스에서 테스트를 작성할 건데요. 여기서 단축키 Command + Shift + T를 누르시면 됩니다.

자 MemberServiceTest 라고 하고 저는 JUnit 4를 그대로 쓸 겁니다. 그렇게 해서 ok를 해놓고요.

먼저 두 가지를 테스트 하겠죠.

자 중요한 회원 가입 테스트를 만들고요. 그 다음에 중복 회원 예외가 정상적으로 동작하는지 확인하는 테스트를 작성할 겁니다.

자 그러면 먼저 테스트를 실행하려면 어쨌든 지금 이게 정말 순수한 단위 테스트를 만드는 게 아니라, 저는 JPA가 실제 DB까지 돌고 이런 걸 보여드리는 게 되게 중요해서 메모리 모드로 DB까지 다 엮어서 테스트를 해드리는 게 중요해요.

그래서 완전히 스프링이랑 Integration 해가지고 테스트를 할 거구요.

그래서 이 두 가지가 있어야 스프링이랑 Integration 해서 스프링 부트를 실제 딱 올려서 테스트를 할 수 있습니다.

그리고 지금 데이터를 변경해야 되기 때문에 @Transactional 이 있어야 롤백이 되죠.

이제 필요한 게 두가지가 있습니다.

이제 테스트 케이스니까 memberService는 다른 애들이 참조할 게 없잖아요. 가장 간단하게 이렇게 해주시면 됩니다.

이제 회원가입 테스트는 어떻게 하면 될까요. 그 전에 given-when-then은 뭐냐면 테스트라는 게 기본적으로 이런게 주어졌을 때(given) 이렇게 하면(when), 이렇게 된다(then) 검증해라. 이게 기본판 이거든요. 테스트를 많이 하시다 보면 느끼는 건데 '이런 게 주어졌을 때 이걸 실행하면 결과가 이게 나와야 돼' 이렇게 머리, 가슴, 배를 given, when, then으로 나누는 이 문법, 이 스타일로 쓰는게 기본적으로 테스트를 익히고 테스트를 유지, 보수하는 게 편합니다.

뭐 이 방법이 좋다 나쁘다 이렇게 이제 논란이 되기도 하는데 기본적으로 이렇게 쓰시면서 더 좋은 방법들을 여러분이 찾으셔서 응용하시면 될 것 같아요.

자 이렇게 있을 때, 회원을 join 하면 cmd + option + v 하면 반환 값으로 Id가 나오겠죠.

자 그럼 then에서 뭘 하면 되냐면,

assertEquals()를 쓸게요.

이게 org.junit.Assert.* 에 있는 거거든요. 그래서 여기서 새로 join한 member랑 memberRepository에서 찾아온 멤버랑 똑같은게 나와야 돼요. 그러면 같은 회원이겠죠. 가입이 정상적으로 됐다고 볼 수 있겠죠. join을 통해서 가입이 된 결과가 나온 거랑 지금 파라미터로 넣은 게 똑같으면, Equal이면 같다고 하겠죠.

이렇게 똑같은 게 가능한 이유는 사실 이 @Transactional 보이죠.

JPA에서 같은 트랜잭션 안에서 같은 엔티티, 그러니까 ID 값이 똑같으면, PK 값이 똑같으면 같은 영속성 컨텍스트에서 똑같은 애가 관리가 돼요. 그러니까 막 2개, 3개 생기지 않고 딱 하나로만 관리가 되거든요.

그래서 이게 true가 나올 겁니다.

이제 돌려봅시다.

예전에 만들어놓은 test 때문에 에러가 나네요. 지우고 다시 돌립니다.

결국 성공하는 것을 이 녹색불이 뜬걸 보실 수가 있죠.

자 지금 쿼리를 보면 굉장히 흥미진진한데 insert 문이 없어요. 그냥 member를 select하고 같은 쿼리인 걸 검사하고 끝나버려요.

왜 그러냐면 jpa을 많이 공부해보신 분들은 알텐데,

얘가 기본적으로 join을 하잖아요. 그러면 여기 member 객체에다가

save까지 들어가 볼게요.

그럼 엔티티 매니저에 persist를 해요. 여러분 persist를 한다고 해서 db에 insert문이 나갈까요, 안 나갈까요? DB마다 좀 다르긴 한, 전략마다 다르지만 특히 이 GenerateValue 전략에서는 기본적으로는 insert문이 안나가요.

왜냐하면 데이터베이스 트랜잭션이 커밋될 때 그때 Flush가 되면서 DB 인서트 쿼리가 쫙 나가는 거거든요. JPA가 그렇게 동작을 해요. 그래서 트랜잭션, 커밋이 엄청 중요해져요.

정리하면 데이터베이스 트랜잭션이 딱 정확하게 커밋을 하는 순간, Flush라는 게 되면서 JPA 영속성 컨텍스트에 있는 이 member 객체가 insert문이 만들어지면서 DB에 인서트가 딱 나간단 말이에요.

그런데 이 스프링에서 이 @Transactional은 기본적으로 뭘 해버리냐면, 트랜잭션 커밋을 안하고 롤백을 해버려요. '나 롤백 안할래. 그래도 눈으로 한번 직접 보고 싶어!' 라고 하면,

그때는 Rollback을 false로 주시면 Rollback을 안 하고 커밋 해버리거든요. 이렇게 하고 다시한번 돌려볼게요. 그러면 등록 쿼리랑 다 보실 수가 있어요. 이제 보면,

자 이제 insert문이 나가는 거를 볼 수가 있죠. 이거를 주의하셔야 됩니다. 처음에 보면 '어? 뭐지?' 막 이러실 수 있거든요. 그게 이제 @Transactional 스프링 어노테이션이 테스트 케이스에 있으면 기본적으로 롤백을 해버리기 때문에 그렇습니다.

여기서 @Rollback(false)를 지우고 다시 돌려보면,

여기 log에 뭐라 나오냐면 Rolled back transaction for test 라고 나와요. 그러니까 스프링이 롤백을 해버리면 JPA 입장에서는 DB에 인서트 쿼리를 날릴 이유 자체가 없겠죠. 그냥 아예 뭐 롤백을 해버린다는 말 자체가 DB에 있는걸 다 버린다는 거기 때문에, 그래서 아예 인서트 쿼리 조차 나가지가 않습니다. 정확하게 말씀드리면 영속성 컨텍스트가 flush를 안 해버립니다.

자 그래도 '롤백이지만 일단 DB에 쿼리 날리는 거 보고 싶어' 하시면 이렇게 하시면 됩니다.

@Autowired 하고 엔티티 매니저를 받으신 다음에 검증하기 전에 em.flush()를 한번 호출해주시면 됩니다.

그러면 일단 지금 영속성 컨텍스트에 이 member 객체가 들어가죠. 이게 쿼리로 db에 반영이 됩니다. flush랑 영속성 컨텍스트에 있는 어떤 변경이나 등록 내용을 데이터베이스에 반영하는 거니까요.

자 이렇게 해보시면 여기 insert문이 나가죠. insert문은 볼 수 있으나,

실제 트랜잭션은 롤백을 하게 되죠. em.flush()하면 일단 DB에 쿼리가 강제로 나가는 거거든요. DB의 영속성 컨텍스트에 있는 쿼리를 날리고, 그 다음 스프링 테스트가 끝날 때 이 @Transactional은 딱 회원가입이 끝날 때 롤백을 해버려요. 롤백을 하는데 그럼 이제 insert 됐다가 롤백이 되겠죠.

'기본적으로 @Transactional은 왜 롤백을 시키지?' 왜냐면 이 테스트가 반복해서 돼야 되잖아요. 사실 DB에 데이터가 남으면 안 되는 거예요.

자 그래서 이렇게 한번 살펴봤고요.

그 다음에 db를 통해서 지금 들어가고 있는데 사람이 하는 거기 때문에, 익숙해지면 모르겠는데 익숙해지기 전까지는 '얘가 진짜 DB 들어가는 거 맞아?' 눈으로 직접 보기 전까지는 의심이 되거든요. 그러면은 그냥 @Rollback(false) 하고 DB에 들어가 보세요.

데이터가 들어가 있죠. city, street, zipcode는 제가 안넣었으니까 다 null이고 name은 kim으로 정확하게 들어가 있는 걸 확인할 수가 있죠. id는 1번입니다. 다시 @Rollback(false)를 지워주고요.

이제 나중에는 테스트라는 것을 결국 제일 좋은 것은 메모리 db, 그러니까 WAS 띄울 때 같이 그 위에 조그마한 메모리 db를 띄워서 테스트하는게 제일 좋거든요. 그거는 또 뒤에서 설정하는거 설명 드릴게요.

그 때는 h2 db에서처럼 눈으로 확인을 못하겠죠. 여기에 하는게 아니고 메모리에다가 넣고 애플리케이션 종료되면 메모리 db가 내려가 버리거든요. 그건 뒤에 설명 드릴게요.

자 이제 기본적인 회원가입 테스트 해봤고 잘 되는 거 확인했고요. 그 다음에 중복,

Validate 했던 거 기억나시죠? 이 로직이 잘 동작하는지 검증해 봐야 되겠죠.

복붙할 때 주의하셔야 합니다. 이렇게 member1과 member2를 만들고 중복된 이름으로 세팅해줬습니다.

그리고나서, 둘 다 딱 join을 걸면 member2를 join할 때 예외가 발생해야죠. 왜냐면 똑같은 이름을 두 명 지금 넣었잖아요.

예외가 터지면 어떻게 됩니까?

지금 보면 이 validateDuplicateMember에서 이 !findMembers.isEmpty() 조건을 타서 exception이 터져버려요. 그럼 이 exception이 밖으로 날아오겠죠?

그럼 다시 여기로 exception이 튀어나오고, join에서 캐치를 안 하니까 또 join 밖으로 튀어나온단 말이에요.

그럼 여기서 exception이 튀어나와요. 그럼 어떻게 되나요? 튀어나와서 중복_회원_예외() 밖으로 나가게 됩니다. 혹시 밖으로 나가는 게 아니고 밑으로 로직이 나가버리면,

검사해보겠습니다. 그 전에 만약에

이렇게 되어있으면 어떻게 될까요? 그냥 member1만 join 한 거고 테스트코드를 잘못 작성한 거예요. 이 상태로 돌리면,

성공이라고 뜨겠죠.

자 그래서 어떤 게 있냐면 지금 JUnit이 기본으로 제공하는 Assert.fail이라는 게 있어요.

이건 뭐냐면 코드가 돌다가 여기에 오면 안 되는 거예요. 여기 오면 뭔가 잘못된 거여서 fail을 떨굽니다.

그럼 돌려볼게요. 지금 보면,

에러가 나면서 인텔리제이가 이유를 설명해주네요. 지금 보면,

"예외가 발생해야 된다"고 떠있죠. fail로 와버린 거에요. 원래 여기 오면 안 되는 거거든요.

자 왜냐?

여기서 지금 exception이 터져 가지고 여기서 제어가 끝나고 밖으로 나가야지, 이 밑에 fail이 있는 라인으로 가면 안 되는 거에요.

자 그럼 이렇게 작성하면 되겠죠. 그런데 여기서 예외가 터지니까 또 테스트가 실패할 겁니다.

이렇게 try-catch문을 써야죠. try문 안에서 예외가 터지는데 catch문에서 IllegalStateException 타입이 오면 정상적으로 return;을 하고, 그게 아니면 fail로 떨어질텐데 얘는 return;을 정상적으로 하기 때문에 테스트 케이스가 성공으로 떨어지겠죠.


try-catch 문의 구조


그래서 다시 돌려보면,

정상적으로 잘 동작합니다. 만약에 IllegalStateException 라고 안 적고, 다른 예외타입을 잘못 적으면, 캐치가 안돼서, 캐치를 못하고 예외가 터졌겠죠.

자 이렇게 하면 테스트가 성공인데 이건 너무 지저분하죠.

그래서 이걸 제공합니다. 이렇게 적어주시면,

이렇게 코드를 줄일 수 있습니다.

여기서 예외가 터져가지고 나가서 잡힌 애가 IllegalStateException이면 됩니다. 그러면 깔끔하게 여기 중복_회원_예외()에 대한 테스트를 작성할 수가 있습니다. 다시 돌려보겠습니다.

두 개 다 테스트가 성공한 걸 보실 수 있습니다.

자 그러면 이제 코드는 전반적으로 다 완성을 했구요.

그 다음에 이제 하나씩 설명을 드릴게요.

자 이거는 JUnit 실행할 때 '스프링이랑 같이 엮어서 실행할래' 라고 하면 이 @RunWith에 스프링 러너를 넣어 주시면 됩니다 아마 나중에 JUnit5 올라가고, 스프링 테스트 쪽도 버전 업이 되면 이것도 다 빠질 것 같은데 우선은 지금은 @RunWith가 필요합니다.

그리고 두 번째로 @SpringBootTest는 스프링 부트를 띄운 상태로 테스트를 하려면 이게 있어야 돼요. 이게 없으면 @Autowired가 다 실패합니다. 즉 스프링 컨테이너 안에서 이 테스트를 돌리는 거죠.

세 번째로 @Transactional은 일단 트랜잭션을 걸고 테스트를 돌린 다음에 테스트가 끝나면 다 롤백을 해버리는 거예요.

그래서 이 Annotation이 테스트 케이스에서 사용될 때는 기본적으로 롤백을 해버립니다. 이게 당연히 그냥 Service 클래스나 Repository 클래스나 이런 데에 붙이면 롤백하지 않아요. 롤백하면 큰일 나겠죠.

그 다음에 이제 뭘 해볼 거냐면 여러분 지금 이 테스트를 돌려봤는데 문제가 있어요.

지금 어쨌든 롤백이 되고 뭘 하고 했지만 어쨌든 실제 외부에 있는 DB를 사용했잖아요. 이런 테스트를 병렬로 여러 개, 막 세네개씩 돌리거나 아니면 외부에 DB를 설치를 또 해야 되잖아요. 테스트 케이스 하나 돌리는데 H2 데이터베이스 설치하듯이 하면 귀찮잖아요. 또 테스트라는 게 기본적으로 끝나면 다 데이터가 초기화 되는게 좋거든요.

그래서 테스트를 완전히 격리된 환경, 이 Java를 띄울 때 이 Java 안에 살짝 데이터베이스를 새로 만들어서 띄우는 방법이 있습니다.

이게 뭐냐면 이제 Memory DB를 쓰는 건데요. 놀랍게도 Spring Boot를 쓰면 이런 걸 다 거의 공짜로 할 수가 있습니다.

어떻게 하는지 설명드릴게요.

먼저 첫 번째로 여기 보면 기본적으로 main과 test, 이 두 가지로 폴더가 나뉩니다.

main은 실제 개발하는 운영 소스가 있는 거고요. 여기에 java랑 resources로 나뉘죠.

test도 java가 있는데 여기에다가 Directroy를 하나 만들어줍니다.

resources를 만들어 줍니다. 이게 뭐냐면,

이게 뭐냐면 기본적으로 운영 로직은 여기에 있는 게 우선권을 가져요.

근데 테스트는 여기에 있는 게 우선권을 가집니다. 무슨 말이냐?

이 main/resources에 있는 application.yml을 test/resources에다가 복사할게요.

자 그러면 test가 돌 때는 여기 test에 있는 application.yml이 있으면 얘가 우선권을 가지고 얘로 실행을 하고, main에 있는 application.yml는 무시됩니다.

자 그래서 이제 어떻게 하냐. 일단 이 url을 메모리로 바꾸면 돼요. 왜냐하면 우리가 지금 라이브러리에 뭐가 들어 있냐면 build.gradle을 보시면,

여기 h2 데이터베이스가 들어가 있죠. 자 이게 있으면 뭐가 되냐면, application.yml이 클라이언트 역할만 하는 게 아니라, 메모리 모드로 db를 그냥 잡아냅니다. 어차피 잡아도 h2가 돌거든요. 그래서 jvm 안에서 띄울 수가 있어요.

자 그래서 뭘 할 수 있냐,

이 URL에다가 메모리 모드로 띄우라고 하면 됩니다.

h2database.com/에 들어가시면,

Cheat Sheet이 있습니다. 여기 들어가셔서,

In Memory에 jdbc:h2:mem:test 라고 있죠.

여기다가 붙여넣기 하면 됩니다. 그럼 메모리 모드로 동작을 합니다.

자 돌려볼게요.

그럼 우리의 테스트가 main에 있는 application.yml이 아니라, test에 있는 application.yml로 돌아가게 됩니다.

진짜인가요? H2 데이터베이스 내려볼게요.

이렇게 하면,

사이트가 뻗어버리죠. 사이트 자체가 다 내려가고 DB도 다 내려가 버려요. 그러나 테스트를 다시 돌리면,

잘 동작합니다. 기가 막히죠?

그리고 지금 p6spy에서 커넥션을 얻는 url을 잘 보시면,

우리가 메모리 세팅했던 jdbc:h2:mem:test에서 가지고 오죠.

자 이렇게 하면 이제 정말 반복적으로 테스트를 딱딱 할 수가 있는 거죠. DB를 띄우는 거 없이, 바로 그냥 이것만 돌리면 그냥 한 방에 다 되는 거죠.

 

그런데 여러분 스프링 부트에는 놀라운 게 있습니다.

여러분 이게 다 없어도 됩니다. 왜냐면 스프링 부트가 기본적으로 별도의 설정이 없으면 메모리 모드로 돌려 버려요.

그렇기 때문에 이 설정이 다 없어도 됩니다.

한번 돌려 볼게요.

테스트가 성공하죠. 자 커넥션 뭘로 얻었는지 볼까요?

그러니까 이거는 스프링 부트가 기본적으로 세팅해서 가지고 올 때 이렇게 동작을 하게 됩니다.


그런데 나는 실제로 돌려보면,

여기서 커넥션을 얻어오는 걸로 보이는데, 이거에 대한 답변은

이렇다고 한다.


그래서 이렇게 세팅을 걸고 동작하게 됩니다. 사실 정말 극단적으로 얘기하면,

이게 다 없어도 돼요. 그렇게 해도 메모리 모드로 테스트하고 메모리 모드로 실행하는 것은 다 동작합니다. 기가 막히죠.

그리고 이제 test에 있는 yml과 운영의 yml을 실제 로컬에서 돌리든, 뭘 하든 당연히 분리하는 게 맞아요.

왜냐면 테스트 케이스를 돌릴 때 하고 싶은, 보고 싶은 설정이랑 운영에서의 설정은 다르거든요. 실제 로컬에 띄우든, 개발 DB에 띄우든, 띄워서 돌리는 건 다르거든요. 테스트 케이스라는 거는 그냥 돌리고 끝내고 싶은 거예요. 필요한 부분은 적절하게 모킹하는 것도 필요하고요. 그래서 설정을 따로 가져가는 게 좋습니다.

그리고 마지막으로 스프링 부트는 create가 아니고 create-drop으로 기본적으로 돌아가요.

drop은 뭐냐면,

여기 보시면 drop table 이라고 나오죠. create는 뭐냐면 내가 가지고 있는 엔티티를 먼저 다 drop한 다음에 create를 하고 애플리케이션을 딱 실행시켜요.

반면 create-drop은 create랑 똑같은데 마지막에 애플리케이션 종료 시점에 drop 쿼리를 다 날려줍니다. 그래서 완전히 깨끗하게 초기화하죠. 사실 메모리 모드는 이미 다 WAS가 내려가고 끝이 나기 때문에 뭐 이렇게까지 할 필요는 없을 것 같긴 한데, 어쨌든 이렇게 해서 깔끔하게 자원 정리까지 다 해줍니다.

그래서 이번 시간에는 테스트를 만드는 것까지 알아봤구요.

 

자 이렇게 해서 회원의 핵심 도메인에 대한 개발이 다 완료가 되었습니다.

다음 시간에는 상품, 그리고 또 그 다음 시간에는 주문, 이런 식으로 하나씩 쭉쭉 이와 동일한 라이프사이클로 개발을 하도록 하겠습니다.