김영한 님의 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 강의를 보고 작성한 내용입니다.
@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();
}
}
persist()
는 저장할 때 사용하는데 persist()
를 하면 영속성 컨텍스트에 Member Entity 를 넣고, 나중에 트랜잭션이 commit 되는 시점에 DB 에 반영됩니다.
find()
는 단건을 조회할 때 사용하는데 첫 번째 인자로 반환 타입을 지정하고, 두 번째 인자에 PK 를 넣어주면 됩니다.
여러 건을 조회할 때는 createQuery()
를 사용하는데 첫 번째 인자에는 JPQL 을 넣어주고, 두 번째는 반환 타입을 지정해줍니다.
SQL 은 테이블을 대상으로 쿼리를 하는 반면, JPQL 은 Entity 객체를 대상으로 쿼리를 합니다.
조건을 사용해서 쿼리를 하는 경우 where 절에서 :파라미터
형태로 작성한 후, setParameter()
를 통해 어떤 파라미터에 어떤 값이 들어갈 지를 지정합니다.
참고로 @Persistence
를 사용하지 않고, @RequiredArgsConstructor
와 final
키워드를 이용해서 EntityManager 를 주입 받는 방법도 가능합니다.
@Service
@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();
}
// 나머지 코드는 생략
}
JPA 의 모든 데이터 변경이나 로직들은 가급적이면 트랜잭션 안에서 실행되어야 합니다. 그래서 @Transactional
이 필요하며, 이 어노테이션이 있어야 지연로딩과 같은 기능들이 동작합니다.
readOnly = true
라는 옵션을 주면 JPA 가 조회하는 곳에서는 성능을 최적화 합니다. 읽기가 아닌 쓰기에서 적용하면 데이터 변경이 일어나지 않습니다.
위에서는 생략된 부분에 조회 관련 코드가 있기 때문에 전체적으로 readOnly = true
를 걸어주었고, 변경이 일어나는 메서드에는 @Transactional
을 사용하여 변경이 일어날 수 있도록 하였습니다.
persist()
를 통해 Entity 를 영속성 컨텍스트에 올리는데 영속성 컨텍스트는 key 와 value 를 가지고 있습니다.
영속성 컨텍스트에 엔티티를 관리하려면 그 순간 바로 PK 인 ID가 필요합니다. 왜냐하면 PK인 ID를 기준으로 영속성 컨텍스트가 관리되기 때문입니다.
그래서 영속성 컨텍스트는 DB 에 들어간 시점이 아닌 persist()
를 호출하는 순간에 바로 PK인 ID를 획득하게 되고 해당 ID를 엔티티의 PK ID에 넣어둡니다.
그렇기 때문에 member.getId()
를 했을 때 항상 값이 존재한다는 것이 보장됩니다.
@SpringBootTest
@Transactional
class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입() {
Member member = new Member();
member.setName("김");
Long savedId = memberService.join(member);
assertEquals(member, memberRepository.findOne(savedId));
}
}
JPA 에서 같은 트랜잭션 안에서 같은 Entity 인, 즉 영속성 컨텍스트에서 Entity 를 하나로만 관리하기 때문에 PK( ID ) 값이 똑같으면 같은 Entity 입니다.
그래서 회원가입 테스트를 진행했을 때 저장한 회원과 repository 를 통해 조회한 회원이 동일하게 됩니다.
GeneratedValue 전략에서는 persist()
를 호출해도 DB 에 INSERT 문이 전달되지 않습니다.
Insert 쿼리는 DB 트랜잭션이 Commit 될 때 flush()
되면서 DB 에 쿼리가 나가게 됩니다.
@Transactional
이 테스트에 있을 때 트랜잭션을 Commit 하지 않고 Rollback 하기 때문에 해당 테스트의 로그를 살펴보면 INSERT 쿼리가 없는 것을 확인할 수 있습니다.
EntityManager 를 주입받고, join()
이후에 em.flush()
를 작성하면 Insert 쿼리가 수행되는 것을 확인할 수 있고, @Transactional
이 있기 때문에 Rollback 되어 실제로 저장되진 않습니다.
테스트를 진행할 때는 현재처럼 외부 DB 를 사용하는 것이 아닌 완전히 격리된 환경에서 진행하는 것이 좋은데 Java 를 띄울 때 안에 DB 를 새로 만들어서 띄우는 방법이 존재합니다.
test 아래 resources 폴더를 만들고 내부에 application.yml
을 복사해서 넣습니다. 이렇게 하면 테스트 할 때는 TEST 하위에 있는 application.yml
을 가지고 실행을 하게 됩니다.
그 후 url 을 jdbc:h2:mem:test
로 변경하게 되면 H2 DB 가 메모리 모드로 동작하게 됩니다.
참고로 스프링부트는 datasource 설정이 없으면, 기본적을 메모리 DB를 사용하고, driver-class도 현재 등록된 라이브러를 보고 찾아줍니다.
또 ddl-auto 도 create-drop 모드로 동작하기 때문에 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 됩니다.
public abstract class Item {
...
private int stockQuantity;
// 비즈니스 로직
public void addStockQuantity(int stockQuantity) {
this.stockQuantity += stockQuantity;
}
public void removeStockQuantity(int stockQuantity) {
int restStock = this.stockQuantity - stockQuantity;
if (restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity -= stockQuantity;
}
}
데이터를 가지고 있는 쪽에 비즈니스 메서드가 있는 것이 응집력이 있고 좋은 형태입니다. 그래서 재고를 늘리고, 줄이는 로직은 Item 이 가진 stockQuantity 정보를 사용하기 때문에 Item 에 작성하였습니다.
내부 값을 변경할 때 Setter 함수를 사용하는 것이 아닌 위처럼 핵시 비즈니스 메서드를 사용해서 변경하는 것이 좋은 방법입니다.
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
public Item findOne(Long id) {
return em.find(Item.class, id);
}
public List<Item> findAll() {
return em.createQuery("select i from Item i", Item.class).getResultList();
}
}
아예 새로운 Item 이라면 저장되기 전에 Item 은 id 값이 없기 때문에 persist()
를 사용하고, id 값이 있는 경우에는 merge()
를 사용합니다.
merge()
는 JPA 에서 엔티티를 영속성 컨텍스트에 병합하는 데 사용되는 메서드이며 엔티티의 변경 내용을 데이터베이스에 동기화하기 위해 주로 사용됩니다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void saveItem(Item item) {
itemRepository.save(item);
}
public List<Item> findItems() {
return itemRepository.findAll();
}
public Item findOne(Long itemId) {
return itemRepository.findOne(itemId);
}
}