
김영한 강사님 [실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 강의 참조
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//스프링 빈으로 등록
public class MemberRepository {
//스프링이 entitymanager 빈으로 만들어서 주입
@PersistenceContext
private EntityManager em;
//JPA가 entity를 저장하는 로직
public void save(Member member){
em.persist(member);
}
//JPA가 제공하는 find메소드로 id값을 넘기면 member찾아서 반환
public Member findOne(Long id){
return em.find(Member.class, id);
}
//List 조회 (JPQL을 실행하기 위한 쿼리객체 생성 후 결과 반환)
public List<Member> findAll(){
return em.createQuery("select m from Member m", Member.class).getResultList();
}
//이름으로 회원을 검색하여 List 조회
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 : JPA의 EntityManager를 빈으로 주입할 때 사용하는 어노테이션
- 스프링에서는 영속성 관리를 위해 EntityManager가 존재(스프링 컨테이너가 시작될 때 EntityManager를 만들어서 빈으로 등록)
- EntityManagerFactory에서 새로운 EntityManager를 생성하거나 Transaction에 의해 기존에 생성된 EntityManager를 반환
@PersistenceContext private EntityManager em;
- EntityManager : 내부적으로 DB connection pool을 사용해서 DB에 접근
- Transaction 단위를 수행할 때마다 생성 (고객의 요청이 올 때마다 사용했다가 닫는다.)
- Transaction 수행 후에는 반드시 EntityManager 를 닫는다. (내부적으로 DB Connection 을 반환)
- 영속성 컨텍스트(Persistence Context) : Entity를 영구 저장하는 환경
- EntityManager를 통해서 영속성 컨텍스트에 접근
- EntityManager가 생성되면 1:1로 영속성 컨텍스트가 생성
- EntityManager.persist(entity) : DB에 저장하는 것이 아니라 영속성 컨텍스트를 통해서 Entity를 영속화한다는 뜻 (persist() 시점에는 Entity를 영속성 컨텍스트에 저장하는 것)
- 트랜잭션 커밋되는 시점에 DB에 반영(DB에 insert쿼리 전달)
- entity값의 @Id가 키(영속성 콘텍스트에는 키와 값이 있음)가 됨(DB pk와 매핑) → id값이 항상 생성(보장됨)
- EntityManager.find(type, pk) : 단건조회(pk값을 넘기면 알맞은 type조회)
- 테이블이 아닌 엔티티 객체를 대상으로 검색하는 객체지향 쿼리
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않음
- JPA는 JPQL을 분석한 후 적절한 SQL을 만들어 데이터베이스를 조회
- 기본문법 : String jpql = "select m from Member m where m.name = 'c'";(별칭은 필수)
typedQuery : JPQL을 실행하려면 쿼리 객체를 만들어야 한다. 쿼리 객체로는 TypedQuery와 Query가 있는데 반환할 타입을 명확하게 지정할 수 있으면 TypedQuery 객체를, 명확하게 지정할 수 없으면 Query 객체를 사용
public static void typedQuery(EntityManager em) {
String jpql = "select m from Member m";
TypedQuery<Member> query = em.createQuery(jpql, Member.class);
List<Member> list = query.getResultList();
for( Member member : list) {
System.out.println("Member : " + member);
}
}
ex. 이름으로 회원을 검색하여 List를 조회하는 메소드 작성
- :name : parameter를 바인딩한것
public List<Member> findByName(String name){ return em.createQuery(qlString: "select m from Member m where m.name=:name", Member.class).setParameter(name:"name", name).getResultList(); }
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor//final이 있는 필드만을 가지고 생성자 만듦
public class MemberService {
private final MemberRepository memberRepository;
//회원 가입
@Transactional
public Long join(Member member){
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();//값이 있다는 것이 보장됨(id가 키값)
}
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();
}
//id에 해당하는 회원 조회(단건조회)
public Member findOne(Long memberId){
return memberRepository.findOne(memberId);
}
}
- @Service : 스프링 빈으로 등록
- @Autowired : 의존성 주입(스프링이 스프링빈에 등록되어있는 멤버레퍼지토리를 인젝션)
- 필드 인젝션 : 필드를 변경 할 수 없다는 단점
@Autowired private MemberRepository memberRepository;
- setter 인젝션 : 바로 주입하는 것이 아니라 setter로 들어와서 주입
- test code작성 시 mock(실제 객체를 만들기엔 비용과 시간이 많이 들거나 의존성이 길게 걸쳐져 있어 제대로 구현하기 어려울 경우, 가짜 객체를 만들어 사용하는 것)을 주입가능
- but, setRepository를 호출해서 개발 중간에 변경할 일이 없음
@Autowired public void setMemberRepository(MemberRepository memberRepository) { this.memberRepository = memberRepository; }
- 생성자 인젝션 : 생성시점에 생성자에서 인젝션
- 테스트 케이스 작성시 직접 주입해야해서 놓치지 않고 작성 가능
- 생성자가 하나만 있을 경우 자동으로 인젝션 해줌
@Autowired public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; }
- @Transactional : 메서드가 포함하고 있는 작업 중에 하나라도 실패할 경우 전체 작업을 취소(데이터 변경 시 필수)
- @Transactional(readOnly = true) : 로 설정하면 JPA가 조회시 성능 최적화된 결과 반환(조회 메소드에 넣을 것을 권장)
- 클래스에 @Transactional을 넣으면 전체(public)메소드에 적용
- 메소드에 따로 넣으면 우선순위로 실행.
memberRepository.findByName(member.getName());List<Member> findMembers = memberRepository.findByName(member.getName());이렇게 만들수 있습니당!아이디 중복 회원 검증처리를 했어도 멀티스레드를 고려해서 DB멤버의 이름을 unique 제약조건 해주는 것을 권장
인젝션 필드 주입 시 final로 작성할 것을 권장(변경할 일이 없으므로)
private final MemberRepository memberRepository테스트 요구사항
스프링과 통합해서 테스트(jpa가 실제 db까지 도는것까지)
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)
@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("여기까지 에러가 오면 안된다.");
}
}
회원가입 테스트 실행시 insert문은 롤백되어 실행되지 않음

insert문이 어떻게 실행되는지 보고싶으면
1) @Rollback(value = false)를 추가하면 롤백처리 하지 않음
정확하게 확인하고 싶을 경우 h2 콘솔을 이용해서 확인 가능
2) @Autowired EntityManager em; 으로 EntityManager를 주입 후 em.flush 호출하면 영속성 컨텍스트의 변경 내용을 DB 에 반영 후 롤백 처리 되어 확인 가능
영속성 컨텍스트의 변경 내용을 DB 에 반영하는 것(DB에 동기화)
Transaction commit 이 일어날 때 flush가 동작하는데, 이때 쓰기 지연 저장소에 쌓아 놨던 INSERT, UPDATE, DELETE SQL들이 DB에 날라간다. (영속성 컨텍스트를 비우는 것이 아니라 영속성 컨텍스트의 변경 사항들과 DB의 상태를 맞추는 작업)
플러시의 동작 과정
flush가 발생한다고 해서 commit이 이루어지는 것이 아니고 flush 다음에 실제 commit이 일어난다.
[참고] https://gmlwjd9405.github.io/2019/08/07/what-is-flush.html
- assert.fail("message") : 무조건 실패로 AssertionError가 발생하고 다음 라인부터 실행하지 않음
- 여기까지 에러가 온 것은 중복으로 인한 예외를 잡아주지 못한 것 (test code 잘못 작성한 것)
- 예외 발생시 try~catch로 잡아주지 않아서 예외가 튕겨 나가 발생
@Test
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("여기까지 에러가 오면 안된다.");
}

- Ctrl + Shift + T를 통해서 test 생성 가능 (service class test)
- @RunWith(SpringRunner.class) : 스프링과 테스트 통합 (junit 실행할 때 스프링이랑 엮어서 실행할래)
- @SpringBootTest : 스프링 부트 띄우고 테스트(없으면 @Autowired 다 실패-container안에서 test실행)
- @Transactional : 반복 가능한 테스트 지원, 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백 (이 어노테이션이 테스트 케이스에서 사용될 때만 롤백)
test는 given(이렇게 주어졌을 때) when(이렇게하면) then(이렇게 된다)형식으로 작성
assertEquals(expected, actual) : junit4에서 예상값이 실제값과 같은지 확인
@Test(expected = IllegalStateException.class) : 실행결과 IllegalStateException예외 발생시 예외 처리
테스트는 케이스 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다. 그런 면에서 메모리DB를 사용하는 것이 가장 이상적
테스트 케이스를 위한 스프링 환경과, 일반적으로 애플리케이션을 실행하는 환경은 보통 다르므로 설정 파일을 다르게 사용하자
- test안에 resources dir생성 후 main에 있는 application.yml복사해서 붙여넣기
- test실행 시에는 test디렉토리안의 파일이 우선권을 가지므로 main에 있는 yml파일은 무시됨
- https://www.h2database.com → Cheat Sheet → In-Memory → jdbc:h2:mem:test복사
- application.yml파일의 url에 붙여넣기 후 실행하면 test가능
create : 내가 가진 entity를 다 drop 후 create한 다음 application실행
create-drop : 내가 가진 entity를 다 drop 후 create한 다음 application실행 후 마지막에 application 종료 시점에 drop query재실행(완전한초기화)