// 컴포넌트 스캔에 의해 자동으로 빈으로 관리된다.@RepositorypublicclassMemberRepository {// 스프링이 EntityManager를 주입해준다. @PersistenceContextprivateEntityManager em;publicvoidsave(Member member) {em.persist(member); }publicMemberfindOne(Long id) {returnem.find(Member.class, id); }publicList<Member> findAll() {// 리스트는 jpql을 사용해야 한다.// sql은 테이블을 대상으로, jpql은 객체를 대상으로 쿼리한다는 점이 다르다.returnem.createQuery("select m from Member m",Member.class).getResultList(); }publicList<Member> findByName(String name) {returnem.createQuery("select m from Member m where m.name = :name",Member.class).setParameter("name", name) // :name 파라미터 바인딩.getResultList(); }}
@Repository
스프링 빈으로 등록된다.
JPA 예외를 스프링 기반 예외로 변환한다.
@PersistenceContext
EntityManager를 주입한다.
직접 긴 코드를 쓸 필요가 없어졌다.
@PersistenceUnit
@RepositorypublicclassMemberRepository {// EntityManagerFactory를 직접 주입받고 싶다면 아래를 사용한다. @PersistenceUnitprivateEntityManagerFactory emf;}
EntityManagerFactory를 주입한다.
회원 서비스 개발
// 컴포넌트 스캔에 의해 자동으로 빈으로 등록된다.@Service// JPA의 모든 데이터 변경과 로직은 트랜잭션 내에서 실행되어야 한다.@Transactional(readOnly =true)publicclassMemberService { @AutowiredprivateMemberRepository memberRepository; @TransactionalpublicLongjoin(Member member) {// 중복 회원 검증validateDuplicateMember(member);memberRepository.save(member);returnmember.getId(); }privatevoidvalidateDuplicateMember(Member member) {// WAS가 동시에 여러 개 떠서 동시에 validate를 시도하면 문제가 생긴다.// 실무에서는 이런 멀티 스레드 문제를 해결해줘야 한다.List<Member> findMembers =memberRepository.findByName(member.getName());if (!findMembers.isEmpty()) {thrownewIllegalStateException("이미 존재하는 회원입니다."); } }publicList<Member> findMembers() {returnmemberRepository.findAll(); }publicMemberfindOne(Long memberId) {returnmemberRepository.findOne(memberId); }}
@Transactional
javax와 springframework 두 가지 종류가 있다.
이미 스프링에 의존이 많이 되어있기 때문에 javax보다는 springframework를 import하는 게 좋다.
그래야 쓸 수 있는 옵션도 더 다양하다.
readOnly = true
더티 체킹을 안하거나 DB 옵션에 따라 읽기 전용 모드로 읽는 등 성능상 이점이 있다.
기본을 true로 두고 변경이 필요한 곳에만 false로 달아두면 된다.
조회가 아니라면 true일 때 데이터 변경이 안되므로 주의하자. 커맨드성이 강해서 조회가 거의 없다면 기본 값으로 두는 게 더 좋다.
@Autowired
@Autowired로 필드 주입을 하면 엑세스할 방법이 없어서 다른 의존성으로 바꿔치기 할 수가 없다.
@RunWith(SpringRunner.class)@SpringBootTest@TransactionalpublicclassMemberServiceTest { @AutowiredMemberService memberService; @AutowiredMemberRepository memberRepository; @Testpublicvoid회원가입() {// givenMember member =newMember();member.setName("kim");// whenLong savedId =memberService.join(member);// then// 같은 트랜잭션에 묶었으므로 동일한 영속성 컨텍스트에// 같은 PK를 가질 경우 똑같은 데이터로 취급한다.assertEquals(member,memberRepository.findOne(savedId)); }}
예제에서는 이해를 돕기 위해 통합적으로 테스트를 할 것이다.
@RunWith(SpringRunner.class)
스프링과 테스트를 통합한다.
@SpringBootTest
스프링 부트를 띄워서 테스트한다.
이게 없으면 @Autowired는 다 실패한다.
@Transactional
반복 가능한 테스트를 지원한다.
즉, 각 테스트 실행마다 테스트가 끝나면 트랜잭션을 롤백한다.
자동 롤백은 테스트 케이스에서만 적용된다.
select만 나가고 실제 회원 가입을 하면서 생겨야 할 insert 쿼리는 나가지 않았다.
@Repository@RequiredArgsConstructorpublicclassMemberRepository {publicvoidsave(Member member) {// insert 쿼리가 나가지 않은 이유em.persist(member); }}
DB 전략마다 다르지만 기본적으로 persist만 하면 쿼리가 나가지 않는다.
트랜잭션이 commit될 때 flush 되면서 쿼리가 나가는 것이기 때문이다.
@RunWith(SpringRunner.class)@SpringBootTest// 기본적으로 롤백이 동작한다.@TransactionalpublicclassMemberServiceTest { @Test @Rollback(value =false)publicvoid회원가입() {... }}
@Transactional이 자동으로 롤백을 하니까 당연히 insert 쿼리를 보내지 않는 것이다.
정확하게는 영속성 컨텍스트가 flush를 하지 않는다.
@Rollback에 false 옵션을 주어야 커밋이 실행된다.
정상적으로 insert 쿼리가 나간 것을 볼 수 있다.
중복 회원 예외
@RunWith(SpringRunner.class)@SpringBootTest@TransactionalpublicclassMemberServiceTest { @Test(expected =IllegalStateException.class)publicvoid중복_회원_예외() {// givenMember member1 =newMember();member1.setName("kim");Member member2 =newMember();member2.setName("kim");// whenmemberService.join(member1);// 똑같은 이름을 넣었으니 여기서 예외를 던져야 한다.memberService.join(member2);// then// 여기까지 오지 않고 위에서 예외가 이미 발생해야 할 때 쓴다.fail("예외가 발생해야 한다."); }}
expected에 터질 예정인 예외를 써준다.
테스트를 위한 설정
테스트는 완전히 격리된 환경에서 실행하는 것이 좋다.
끝나면 데이터를 초기화하기 위해 메모리 DB를 사용한다.
test에 resources를 따로 만들어두면 테스트 시에 main의 resources보다 우선권을 가진다.
spring:datasource:url:jdbc:h2:mem:test# 인 메모리 방식으로 바꿔준다.username:sapassword:driver-class-name:org.h2.Driverjpa:hibernate:ddl-auto:createproperties:hibernate:format_sql:truelogging.level:org.hibernate.SQL:debug
하이버네이트는 jvm 위에 띄워 인 메모리 방식으로도 사용할 수 있다.
spring:logging.level:org.hibernate.SQL:debug
심지어 이렇게 간단하게만 설정해도 돌아간다. 스프링 부트는 따로 설정이 없다면 자동으로 메모리 모드로 돌려주기 때문이다.