스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 [김영한 강사님]
구현 기능
MemberRepository
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Repository //component 스캔에 의해 자동으로 빈으로 관리됨
public class MemberRepository {
@PersistenceContext //스프링이 EntityMnager를 만들어서 em에 주입해줌
private EntityManager em;
@PersistenceUnit
private EntityManagerFactory emf; // 직접 주입 -> 거의 사용할 일 없음
public void save(Member member) {
//영속화 한다. -> 영속성 컨텍스트에 객체를 넣는다.
em.persist(member);
}
public Member findOne(Long id) {
//id 값으로 멤버를 찾아서 리턴해준다. find(엔티티,기본키)
return em.find(Member.class, id);
}
public List<Member> findAll() {
//em.createQuery(jpql,반환타입) -> 엔티티 객체를 대상으로 쿼리를 날림(sql은 테이블을 대상으로 쿼리를 날림)
// jpql을 sql로 바꿔준다.
return em.createQuery("select m from Member m", Member.class)
.getResultList(); //member를 리스트로 변환해줌
}
public List<Member> findByName(String name) {
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name) //"name"은 위에 있는 name
.getResultList();
}
}
@Repository
: 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환@PersistenceContext
: 엔티티 매니저(EntityManager)를 주입@PsersistenceUnit
: 엔티티 매니저 팩토리(EntityManagerFactory) 주입save()
: em.persist()
는 영속성 컨텍스트를 통해서 엔티티를 영속화 한다.findOne()
findAll()
: JPQL을 호출하여 JPA에서 제공하는 메서드 호출만으로 작성할 수 없는 쿼리를 작성한다.findByName
MemberService
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true) // 데이터 변경할때는 기본적으로 transactional이 있어야 함 -> 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
public class MemberService {
@Autowired
private final MemberRepository memberRepository;
// 변경할 일이 없기 때문에 final로 하는 것을 권장 -> final을 해주면 생성자에 값 세팅을 안해주면 에러떠서 확인할 수 있음
public MemberSerivce(MemberRepository memberRepository){
this.memberRepository=memberRepository; //final을 썼을때 여기 부분을 작성 안해주면 에러 떠서 컴파일 시점을 확인할 수 있음
}
/**
* 회원가입
*/
@Transactional //변경
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()) { //db에 이름이 있으면
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
/**
* 전체 회원 조회
*/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
}
@Service
@Transactional
: 트랜잭션, 영속성 컨텍스트readOnly=true
: 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상(읽기 전용에는 다 적용)@Autowired
@Autowired
private MemberRepository memberRepository;
위와 같이 사용하면 테스트 등을 할때 변경해야하는데 필드에, private로 되어있어서 접근할 수가 없어서 변경할 수가 없다. -> 주입하기가 까다롭다.❗참고❗
실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다.
스프링 필드 주입 대신에 생성자 주입을 사용하자.
필드 주입
public class MemberService {
@Autowired
MemberRepository memberRepository;
...
}
생성자 주입
@Autowired
를 생략할 수 있다.final
키워드를 추가하면 컴파일 시점에 memberRepository
를 설정하지 않는 오류를 체크할 수 있다.(보통 기본 생성자를 추가할 때 발견)lombok
@RequiredArgsConstructor//final 있는 필드만 가지고 생성자를 만들어 준다.
public class MemberService {
private final MemberRepository memberRepository;
...
}
EntityManager
도 주입 가능@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
...
}
테스트 요구사항
회원가입 테스트 코드
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@RunWith(SpringRunner.class) //Junit이랑 스프링이랑 같이 실행
@SpringBootTest // 스프링 부트를 띄운 상태에서 테스트
@Transactional //테스트 돌린 다음에 rollback
public class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
//Given : 이런걸 줬을때
Member member = new Member();
member.setName("kim");
//When : 이렇게 하면
Long saveId = memberService.join(member);
//Then : 이렇게 된다
em.flush(); // 영속성 컨텍스트에 있는 어떤 변경이나 등록 내용을 데이터베이스에 반영하는 것이다.
assertEquals(member, memberRepository.findOne(saveId));
}
@Test(expected = IllegalStateException.class) //try 대신에 이렇게 써서 예외 작성 가능
public void 중복_회원_예외() throws Exception {
//Given
Member member1 = new Member();
member1.setName("kim");
Member member2 = new Member();
member2.setName("kim");
//When
memberService.join(member1);
try{
memberService.join(member2); //예외가 발생해야 한다.
}catch(IllegalStateException e){
return;
}
//Then
fail("예외가 발생해야 한다."); //
}
}
@RunWith(SpringRunner.class)
: 스프링과 테스트 통합@SpringBootTest
: 스프링 부트 띄우고 테스트(이게 없으면 @Autowired) 다 실패@Transactional
: 반복 가능한 테스트 지원, 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 드랜잭션을 강제로 롤백(이 어노테이션이 테스트 케이스에서 사용될 때만 롤백)테스트 케이스를 위한 설정
테스트는 케이스 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다. 그런 면에서 메모리 DB를 사용하는 것이 가장 이상적이다. 추가로 테스트 케이스를 위한 스프링 환경과, 일반적으로 애플리케이션을 실행하는 환경은 보통 다르므로 설정 파일을 다르게 사용하자. 다음과 같이 간단하게 테스트용 설정 파일을 추가하면 된다.
test/resource
에 application.yml 파일이 있으면 테스트에서 스프링을 실행하면 여기에 있는 파일을 읽고 없으면 src/resource
에 있는 yml 파일을 읽는다.https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-jpa