@Repository
public class MemberRepository {
@PersistenceContext
private EntityManager em;
public void save(Member member){
em.persist(member);
}
public Member findOne(Long id){
return em.find(Member.class, id);
}
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public List<Member> findByName(String name){
return em.createQuery("select m from Member m where m.name = :name",Member.class)
.setParameter("name",name)
.getResultList();
}
}
@Repository
스프링 빈으로 등록
@PersistenceContext
애노테이션에 의해 스프링이 EntityManager를 만들어서 주입해준다.
findAll, findByName은 JPQL을 사용한다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// 회원 가입
@Transactional(readOnly = false)
public Long join(Member member){
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
//EXCEPTION
List<Member> findMembers = memberRepository.findByName(member.getName());
if (!findMembers.isEmpty()){
throw new IllegalStateException("이미 존재하는 회원입니다");
}
}
// 회원 전체 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
public Member findOne(Long memberId){
return memberRepository.findOne(memberId);
}
}
@Transactional
기본적으로 트랜잭션 안에서 데이터를 변경해야 한다.
클래스 레벨에 @Transactional
애노테이션을 쓰면 public 메소드들에는 기본적으로 트랜잭션이 걸린다.
@Transactional
에는 readOnly 속성이 있다.
readOnly = true
: JPA가 읽기전용으로 성능을 최적화해준다. 읽기에는 가급적 넣어주는게 좋다
readOnly = false
: 읽기전용이 아니다. 데이터를 쓸 수 있다.
위 예시와 같이 클래스단에는 readOnly=false
로 놓고 (읽기전용인 메소드가 많기 때문에) 읽기전용이 아닌 메소드에 readOnly=true
로 설정해주면 메소드에 달린 애노테이션이 우선순위를 갖는다.
@RequiredArgsConstructer
로 final이 있는 필드에 대해서만 생성자를 만든다. 스프링은 생성자가 하나만 있는 경우 자동으로 생성자 주입을 해준다.
실무에서는 회원가입 중복 체크를 위해 DB의 member의 name을 unique 제약조건으로 거는 것이 좋다.
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
public void save(Member member){
em.persist(member);
}
public Member findOne(Long id){
return em.find(Member.class, id);
}
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public List<Member> findByName(String name){
return em.createQuery("select m from Member m where m.name = :name",Member.class)
.setParameter("name",name)
.getResultList();
}
}
Spring Data Jpa를 사용하면 위와 같이 리포지토리에서 EntityManager에도 생성자 주입을 사용할 수 있다!(@Autowired
로 주입이 가능하게 해줌)
요구사항
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception{
// given
Member member = new Member();
member.setName("kim");
// when
Long savedId = memberService.join(member);
// then
assertEquals(member, memberRepository.findOne(savedId));
}
@Test(expected = IllegalStateException.class)
public void 중복_회원_예외() throws Exception{
// given
Member member1 = new Member();
member1.setName("kim");
Member member2= new Member();
member2.setName("kim");
// when
memberService.join(member1);
memberService.join(member2); // 예외가 발생해야 한다
// then
fail("예외가 발생해야 한다.");
}
}
@RunWith(SprngRunner.class)
애노테이션은 Junit을 실행할 때 스프링과 엮어서 실행한다는 뜻이다.
@SPringBootTest
로 스프링을 띄운 채로 테스트한다. 이게 있어야 @Autowired
등이 동작한다.
@Transactional
로 트랜잭션을 걸고 테스트를 돌리고, 테스트가 끝나면 롤백을 해주도록 한다. (테스트가 아닌 경우에는 롤백하지 않음)
@Transactional
이 있기 때문에 JPA에서 같은 트랙잭션 안에서 PK 값이 같으면 같은 영속성 컨텍스트에서 똑같이 관리된다.
테스트에서 @Transactional
이 있기 때문에 회원가입 테스트의 경우 INSERT 쿼리가 나가지 않는 것을 확인할 수 있다.
then 절에서 flush 해서 쿼리는 보고 롤백도 할 수 있다
또는 @Rollback(false)
애노테이션을 붙여줄 수도 있다
fail()
은 코드가 돌다가 fail
에 도착하면 테스트가 실패한다.
fail과 try , catch를 사용해서 예외 테스트를 할 수도 있다.
하지만 @Test(expected = IllegalStateException.class)
와 같이 작성하는 편이 더 깔끔하다.
테스트를 Memory DB를 사용해서 할 수 있다.
위와 같이 test 디렉토리에도 resources 디렉토리와 application.yml 파일을 만들면 테스트 할 때는 이 설정파일을 사용하게 된다.
spring: #띄어쓰기 없음
datasource: #띄어쓰기 2칸
url: jdbc:h2:mem:test #4칸
username: sa
password:
driver-class-name: org.h2.Driver
jpa: #띄어쓰기 2칸
hibernate: #띄어쓰기 4칸
ddl-auto: create #띄어쓰기 6칸
properties: #띄어쓰기 4칸
hibernate: #띄어쓰기 6칸
# show_sql: true #띄어쓰기 8칸
format_sql: true #띄어쓰기 8칸
logging.level: #띄어쓰기 없음
org.hibernate.SQL: debug #띄어쓰기 2칸
org.hibernate.type: trace #띄어쓰기 2칸
url 부분을 메모리를 사용하도록 바꿔준다 (h2 db에서 지원함)
이렇게 하면 h2 db를 내리고 테스트를 돌려도 테스트가 동작한다.
하지만 스프링부트에서는 아래와 같이
#spring: #띄어쓰기 없음
# datasource: #띄어쓰기 2칸
# url: jdbc:h2:mem:test #4칸
# username: sa
# password:
# driver-class-name: org.h2.Driver
# jpa: #띄어쓰기 2칸
# hibernate: #띄어쓰기 4칸
# ddl-auto: create #띄어쓰기 6칸
# properties: #띄어쓰기 4칸
# hibernate: #띄어쓰기 6칸
# # show_sql: true #띄어쓰기 8칸
# format_sql: true #띄어쓰기 8칸
logging.level: #띄어쓰기 없음
org.hibernate.SQL: debug #띄어쓰기 2칸
org.hibernate.type: trace #띄어쓰기 2칸
db 관련 설정드을 지워버리면 메모리 모드로 돌려버린다