// 컴포넌트 스캔에 의해 자동으로 빈으로 관리된다.
@Repository
public class MemberRepository {
// 스프링이 EntityManager를 주입해준다.
@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() {
// 리스트는 jpql을 사용해야 한다.
// sql은 테이블을 대상으로, jpql은 객체를 대상으로 쿼리한다는 점이 다르다.
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) // :name 파라미터 바인딩
.getResultList();
}
}
@Repository
스프링 빈으로 등록된다.
JPA 예외를 스프링 기반 예외로 변환한다.
@PersistenceContext
EntityManager를 주입한다.
직접 긴 코드를 쓸 필요가 없어졌다.
@PersistenceUnit
@Repository
public class MemberRepository {
// EntityManagerFactory를 직접 주입받고 싶다면 아래를 사용한다.
@PersistenceUnit
private EntityManagerFactory emf;
}
EntityManagerFactory를 주입한다.
회원 서비스 개발
// 컴포넌트 스캔에 의해 자동으로 빈으로 등록된다.
@Service
// JPA의 모든 데이터 변경과 로직은 트랜잭션 내에서 실행되어야 한다.
@Transactional(readOnly = true)
public class MemberService {
@Autowired
private MemberRepository memberRepository;
@Transactional
public Long join(Member member) {
// 중복 회원 검증
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
// WAS가 동시에 여러 개 떠서 동시에 validate를 시도하면 문제가 생긴다.
// 실무에서는 이런 멀티 스레드 문제를 해결해줘야 한다.
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
javax와 springframework 두 가지 종류가 있다.
이미 스프링에 의존이 많이 되어있기 때문에 javax보다는 springframework를 import하는 게 좋다.
그래야 쓸 수 있는 옵션도 더 다양하다.
readOnly = true
더티 체킹을 안하거나 DB 옵션에 따라 읽기 전용 모드로 읽는 등 성능상 이점이 있다.
기본을 true로 두고 변경이 필요한 곳에만 false로 달아두면 된다.
조회가 아니라면 true일 때 데이터 변경이 안되므로 주의하자. 커맨드성이 강해서 조회가 거의 없다면 기본 값으로 두는 게 더 좋다.
@Autowired
@Autowired로 필드 주입을 하면 엑세스할 방법이 없어서 다른 의존성으로 바꿔치기 할 수가 없다.
@Service
@Transactional(readOnly = true)
public class MemberService {
private MemberRepository memberRepository;
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
setter 주입을 쓰면 직접 mock 등을 주입해줄 수 있는 장점이 있다.
애플리케이션 동작 시점에 누군가가 setter에 접근해서 바꿀 수 있는 위험이 있다.
@Service
@Transactional(readOnly = true)
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
생성자 주입을 하면 생성된 이후에는 바꿀 수 없어서 좋다.
객체 생성 시 의존성을 넣어주도록 컴파일 타임에 강제한다.
주입해야 한다는 사실을 안 놓치고 안전하게 개발할 수 있다.
스프링 최신 버전에서는 생성자가 하나만 있는 경우 @Autowired가 없어도 알아서 생성해준다.
이때 private final로 선언해주는 것이 좋다.
값 세팅을 안하면 컴파일 시점에 체크해주기 때문이다.
@Service
@Transactional(readOnly = true)
@AllArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
}
롬복 @AllArgsConstructor를 쓰면 생성자를 대체할 수 있다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
}
@RequiredArgsConstructor는 final인 필드만 생성자를 만들어준다.
@AllArgsConstructor보다 추천한다.
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
}
스프링 데이터 JPA 덕분에 EntityManager도 생성자 주입으로 받을 수 있다.
회원 기능 테스트
회원 가입
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Test
public void 회원가입() {
// given
Member member = new Member();
member.setName("kim");
// when
Long savedId = memberService.join(member);
// then
// 같은 트랜잭션에 묶었으므로 동일한 영속성 컨텍스트에
// 같은 PK를 가질 경우 똑같은 데이터로 취급한다.
assertEquals(member, memberRepository.findOne(savedId));
}
}
예제에서는 이해를 돕기 위해 통합적으로 테스트를 할 것이다.
@RunWith(SpringRunner.class)
스프링과 테스트를 통합한다.
@SpringBootTest
스프링 부트를 띄워서 테스트한다.
이게 없으면 @Autowired는 다 실패한다.
@Transactional
반복 가능한 테스트를 지원한다.
즉, 각 테스트 실행마다 테스트가 끝나면 트랜잭션을 롤백한다.
자동 롤백은 테스트 케이스에서만 적용된다.
select만 나가고 실제 회원 가입을 하면서 생겨야 할 insert 쿼리는 나가지 않았다.
@Repository
@RequiredArgsConstructor
public class MemberRepository {
public void save(Member member) {
// insert 쿼리가 나가지 않은 이유
em.persist(member);
}
}
DB 전략마다 다르지만 기본적으로 persist만 하면 쿼리가 나가지 않는다.
트랜잭션이 commit될 때 flush 되면서 쿼리가 나가는 것이기 때문이다.
@RunWith(SpringRunner.class)
@SpringBootTest
// 기본적으로 롤백이 동작한다.
@Transactional
public class MemberServiceTest {
@Test
@Rollback(value = false)
public void 회원가입() {
...
}
}
@Transactional이 자동으로 롤백을 하니까 당연히 insert 쿼리를 보내지 않는 것이다.
정확하게는 영속성 컨텍스트가 flush를 하지 않는다.
@Rollback에 false 옵션을 주어야 커밋이 실행된다.
정상적으로 insert 쿼리가 나간 것을 볼 수 있다.
중복 회원 예외
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {
@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);
// then
// 여기까지 오지 않고 위에서 예외가 이미 발생해야 할 때 쓴다.
fail("예외가 발생해야 한다.");
}
}
expected에 터질 예정인 예외를 써준다.
테스트를 위한 설정
테스트는 완전히 격리된 환경에서 실행하는 것이 좋다.
끝나면 데이터를 초기화하기 위해 메모리 DB를 사용한다.
test에 resources를 따로 만들어두면 테스트 시에 main의 resources보다 우선권을 가진다.
spring:
datasource:
url: jdbc:h2:mem:test # 인 메모리 방식으로 바꿔준다.
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
logging.level:
org.hibernate.SQL: debug
하이버네이트는 jvm 위에 띄워 인 메모리 방식으로도 사용할 수 있다.
spring:
logging.level:
org.hibernate.SQL: debug
심지어 이렇게 간단하게만 설정해도 돌아간다. 스프링 부트는 따로 설정이 없다면 자동으로 메모리 모드로 돌려주기 때문이다.