회원 엔티티 코드
package jpabook.jpashop.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
회원 리포지토리 개발
@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 : 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환
- @PersistenceContext : 엔티티 메니저( EntityManager ) 주입
- @PersistenceUnit : 엔티티 메니터 팩토리( EntityManagerFactory ) 주입
회원 서비스 개발
@Service
@Transactional(readOnly = true) // JPA에서의 데이터 변경에 관한 로직이 있을 시, Transaction이 필수적으로 요구됨.
//조회만 하는 service에서 readOnly설정을 해주면 좀 더 최적화와 성능개선을 기대할 수 있다.
@RequiredArgsConstructor
public class MemberService {
// @Autowired
// private MemberRepository memberRepository;
// 더 나은 Injection 방법 : Constructor Injection
private final MemberRepository memberRepository; //final을 넣으면 컴파일 시점에 주입이 제대로 되는지 확인이 가능하다.
/**
* 회원 가입
*/
@Transactional // Class 레벨의 어노테이션보다 Method 레벨의 어노테이션이 우선권을 갖는다.
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if(!findMembers.isEmpty()){
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
/**
* 회원 전체 조회
*/
public List<Member> findMembers(){
return memberRepository.findAll();
}
public Member findMember(Long memberId){
return memberRepository.findOne(memberId);
}
}
기술 설명
- @Service
- @Transactional : 트랜잭션, 영속성 컨텍스트
- readOnly=true : 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상(읽기 전용에는 다 적용)
- 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
- @Autowired
- 생성자 주입 방식을 권장
- 변경 불가능한 안전한 객체 생성 가능 생성자가 하나면, @Autowired 를 생략할 수 있다.
- final 키워드를 추가하면 컴파일 시점에 memberRepository 를 설정하지 않는 오류를 체크할 수 있다. (보통 기본 생성자를 추가할 때 발견)
※ 참고
실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다.
※ 참고
EntityManager 의 경우 의존성 주입 시 @PersistenceContext와 같은 표준이 존재하여 해당 어노테이션을 사용해야한다. 그러나 스프링 데이터 JPA를 사용하면 @Autowired로 EntityManager 도 주입 가능
회원 기능 테스트
테스트 요구사항
- 회원가입을 성공해야 한다.
- 회원가입 할 때 같은 이름이 있으면 예외가 발생해야 한다.
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em;
@Test
// @Rollback(false)
public void 회원가입() {
//given
Member member = new Member();
member.setName("kim");
//when
Long saveId = memberService.join(member);
//then
// em.flush();
Assertions.assertEquals(member, memberRepository.findOne(saveId));
}
@Test(expected = IllegalStateException.class)
public void 중복_회원_예외() {
//given
Member member1 = new Member();
member1.setName("kim");
Member member2 = new Member();
member2.setName("kim");
//when
memberService.join(member1);
memberService.join(member2); // 예외가 발생해야 한다!
// try {
// memberService.join(member2); // 예외가 발생해야 한다!
// } catch (IllegalStateException e) {
// return;
// }
//then
fail("예외가 발생해야 한다.");
}
}
기술 설명
- @RunWith(SpringRunner.class) : 스프링과 테스트 통합
- @SpringBootTest : 스프링 부트 띄우고 테스트(이게 없으면 @Autowired 다 실패)
- @Transactional : 반복 가능한 테스트 지원, 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백 (이 어노테이션이 테스트 케이스에서 사용될 때만 롤백)
- 만약 DB를 확인하고 싶다면 해당 메서드에 Rollback(false) annotation을 붙혀주면 된다
- 예외 발생 여부를 테스트하고싶을때는 @Test annotation의 expected = Exception.class 속성을 사용하면 된다. → @Test(expected = IllegalStateException.class)
※ 참고
테스트 케이스 작성 고수 되는 마법: Given, When, Then
(http://martinfowler.com/bliki/GivenWhenThen.html)
이 방법이 필수는 아니지만 이 방법을 기본으로 해서 다양하게 응용하는 것을 권장한다.
테스트 케이스를 위한 설정
테스트는 케이스 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다. 그런 면에서 메모리 DB를 사용하는 것이 가장 이상적이다.
추가로 테스트 케이스를 위한 스프링 환경과, 일반적으로 애플리케이션을 실행하는 환경은 보통 다르므로 설정 파일을 다르게 사용하자. 다음과 같이 간단하게 테스트용 설정 파일을 추가하면 된다.
spring:
# datasource:
# url: jdbc:h2:mem:test
# username: sa
# password:
# driver-class-name: org.h2.Driver
#
# h2:
# console:
# enabled: true
#
# jpa:
# hibernate:
# ddl-auto: create
# properties:
# hibernate:
## show_sql: true
# format_sql: true
logging:
level:
org.hibernate.SQL: debug
org.hibernate.type: trace
이제 테스트에서 스프링을 실행하면 이 위치에 있는 설정 파일을 읽는다. (만약 이 위치에 없으면 src/resources/application.yml 을 읽는다.)
스프링 부트는 datasource 설정이 없으면, 기본적을 메모리 DB를 사용하고, driver-class도 현재 등록된 라이브러를 보고 찾아준다. 추가로 ddl-auto 도 create-drop 모드로 동작한다. 따라서 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 된다