@Repository
public class MemberRepository {
@PersistenceContext
private EntityManager em;
public void save(Member member) {
em.persist(member);
}
// (1)
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();
}
}
객체화된 기본 자료형 대신 기본자료형을 사용하라
ATDD 강의를 들으며 받았던 리뷰 중 하나이다. 이에 대한 내용은 객체화된 기본 자료형 대신 기본자료형을 사용하라에 잘 나와있다.
(1)
코드를 보면 원래 다음과 같았다.
public Member findOne(Long id) {
return em.find(Member.class, id);
}
여기서 Long을 long으로 바꿔준 것 뿐이다.
변경된 사항은 되게 미미하나 이유에 대해서는 공부를 할 것을 추천한다.
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public Long join(Member member) {
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
// (1)
private void validateDuplicateMember(Member member) {
List<Member> findMember = memberRepository.findByName(member.getName());
if(!findMember.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Member findOne(long id) {
return memberRepository.findOne(id);
}
}
이 어노테이션이 있어야 JPA가 작동한다 -> 하나의 트랜잭션 안에서 작동한다.
org.springframework.transaction.annotation.Transactional을 사용하는 것이 좋다.
readOnly = true
Lombok 라이브러리가 지원하는 어노테이션
생성자 주입을 지원한다 -> final을 붙인 변수에 대해 생성자 주입
생성자 주입을 하면 변경 불가능한 안전한 객체 생성이 가능하다.
final 키워드를 추가하면 memberService
가 memberRepository
를 설정하지 않는 오류를 컴파일 시점에 체크할 수 있다.
실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다.
만약 멀티쓰레드 환경에서 동시에 같은 이름을 입력해 가입한다면 검증 로직에 안걸릴 수 있기 때문이다.
@SpringBootTest // Junit5부터 @Runwith이 필요없다.
@Transactional
public class MemberServiceTest {
@Autowired
MemberService memberService;
@Test
void 회원가입() throws Exception {
// given
Member member = new Member();
member.setName("park");
// when
Long savedId = memberService.join(member);
// then
assertThat(savedId).isNotNull();
}
@Test
void 중복회원_예외() throws Exception {
// given
Member member1 = new Member();
member1.setName("park");
memberService.join(member1);
Member member2 = new Member();
member2.setName("park");
// when, then
assertThatThrownBy(() ->
memberService.join(member2))
.isInstanceOf(IllegalStateException.class);
}
}
반복 가능한 테스트를 지원한다.
각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백 시킨다.
당연히 Test 코드에서만 이런 기능을 한다.
Junit4 : org.junit.Test
Junit5 : org.junit.jupiter.api.Test
엄연히 다르니 버전에 맞춰서 import 잘할 것!
Junit4에만 지원하는 기능이다.
Junit5에서는 Assertions.assertThatThrownBy
를 이용한다.
@DataJpaTest의 경우 Spring Data JPA의 Repository만 @Autowired 할 수 있다.
이 포스트의 Repository의 경우 Spring Data JPA의 Repository가 아닌 우리가 직접 만든 Repository이고 @Repository로 등록한 경우이다. 이의 경우 @DataJpaTest는 이를 단순히 @Component로 여겨 빈으로 등록하지 않는다. 참고할 것!
테스트 케이스는 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다.
그런면에서 메모리 DB를 사용하는 것이 가장 이상적이다.
추가로 테스트 케이스를 위한 스프링 환경과 일반적으로 어플리케이션을 실행하는 환경은 보통 다르므로 설정파일을 따로 두어 다르게 사용하는 것이 좋다.
test/resources/application.yml을 따로 만들어두기!
spring:
# datasource:
# url: jdbc:h2:tcp://localhost/~/jpashop
# username: sa
# password:
# driver-class-name: org.h2.Driver
#
# jpa:
# hibernate:
# ddl-auto: create
# properties:
# hibernate:
# #show_sql: true #이것은 sout으로 프린트
# 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 관련 별도의 추가 설정을 하지 않아도 된다.