웹 프로그래밍/Spring JPA

[Spring JPA] 방명록 애플리케이션 (3) - EntityManager와 CRUD

빅토리_ 2018. 4. 28. 13:42

2020.03.06 수정


Mybatis 버전의 방명록 애플리케이션( 링크 )을 JPA 버전으로 만드는 주제입니다.


JPA는 SQL 쿼리를 직접 작성하지 않고, 메서드 호출만으로 DB를 조작하는 기술입니다.

  • JPA의 역할
    • 엔티티와 DB 테이블을 Mapping
    • 엔티티 매니저가 매핑된 엔티티를 사용( find, persist, remove 메서드 등을 호출하여 데이터 조작 )

이번 엔티티와 테이블을 매핑하는 방법에 대해서는 이전글에서 알아보았습니다.

이번 글에서는 Entity Manager가 무엇인지 알아보겠습니다.






1. 엔티티 매니저 팩토리 ( EntityManagerFactory )

EntityManagerFactory는 말 그대로 EntityManager를 만들어내는 곳입니다.


<!-- JPA 설정 ( 엔티티 매니저 팩토리 등록 ) -->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
...
</bean>

환경 설정을 할 때 applicationContext.xml 파일에서 EntityManagerFactory bean을 등록했던 것을 기억하시나요?

EntityManagerFactory 객체를 생성하는 비용은 상당히 크기 때문에, 이 객체는 딱 하나만 만들어서 애플리케이션 전체에서 공유하도록 설계되어 있습니다.





2. 엔티티 매니저 ( EntityManager )

EntityManager 역시 이름 그대로 엔티티를 관리하는 객체이며, EntityManager를 통해 테이블과 매핑된 엔티티를 실제로 사용하게 됩니다. ( CRUD )

코드 상으로도 Repository 계층에서 엔티티 매니저를 주입 받아 사용하게 되는데 감이 잘 안오실겁니다.


개발자 입장에서 EntityManager는 가상의 데이터베이스라고 생각해도 무방하며, 아직 언급되지 않은 Persistence Context와 같다고 봐도 됩니다.

( Entity Manager  ==  Persistence Context  ==  Virtual Database )


Persistence Context는 중요한 개념인데, Persistence Context와 라이프 사이클은 나중에 여기를 참고하시길 바랍니다.

지금은 Persistence Context를 가상의 데이터베이스이며, 영속화된 환경이라고 정의하겠습니다.

즉, 객체를 영속화 시켜야 데이터베이스의 조작이 가능해집니다.





이제 CRUD를 하기 앞서 스프링 MVC 및 3 Layer Architecture에 대한 내용을 이해하고 있다고 가정하겠습니다.


3. 조회 ( SELECT )

1) com.victolee.guestbook.controller.GuestbookController.java

@Controller
@RequestMapping("")
public class GuestbookController {
@Autowired
private GuestbookService guestbookService;

@RequestMapping(value="/", method=RequestMethod.GET)
public String index(Model model) {
List<Guestbook> guestbookList = guestbookService.getMessageList();
model.addAttribute("guestbookList", guestbookList);
return "index";
}
}
  • 방명록의 모든 글 목록을 조회하는 메서드만 정의된 컨트롤러입니다.



2) com.victolee.guestbook.service / GuestbookService.java

@Service
public class GuestbookService {
@Autowired
private GuestbookRepository guestbookRepository;

public List<Guestbook> getMessageList(){
return guestbookRepository.findAll();
}
}
  • 서비스 계층에서는 repository 메서드를 호출하여 방명록을 조회해달라고 요청합니다.



3) com.victolee.guestbook.Repository.GuestbookRepository.java

@Repository
public class GuestbookRepository {

@PersistenceContext // EntityManagerFactory DI 할 수 있도록 어노테이션 설정
private EntityManager em;

public List<Guestbook> findAll(){
String jpql = "SELECT gb FROM Guestbook gb ORDER BY gb.regDate DESC";
TypedQuery<Guestbook> query = em.createQuery(jpql, Guestbook.class);
return query.getResultList();
}
}

처음에 말씀드렸던 EntityManager가 나왔습니다!

  • EntityManager는 엔티티를 관리하며, 이 객체를 사용하기 위해서는 스프링으로부터 주입 받습니다.
    • 이 때 주입 받기 위해 @Autowired가 아닌 @PersistenceContext 어노테이션을 작성했습니다.
    • PersistenceContext는 영속화된 환경이라고 했었고, 엔티티 매니저는 이 환경에 있는 엔티티들을 관리합니다.
    • 또한 PersistenceContext를 가상의 데이터베이스라고 생각하면 좋다고 했는데, 그렇기 때문에 패키지 이름을 Repository( 저장소 )로 작성한 것입니다. ( DB 접근 계층을 일반적으로 Repository라는 이름으로 사용합니다. )


코드를 보시면 jpql이라는 변수가 있는데, SQL과 비슷해 보입니다.

JPQL은 JPA에서 메서드만으로 복잡한 쿼리를 실행하기 까다로워서 제공하는 기능입니다.

이 문법에 대해서는 이글에서 알아보겠습니다.

당장 JPQL을 모르더라도 SQL과 비슷하므로, 코드를 이해하는데 크게 어려움은 없을 것입니다.





이제 뷰 페이지를 작성하겠습니다.
4) WEB-INF/views/index.jsp
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<% pageContext.setAttribute("newLine", "\n"); %>
<body>
<form action="/guestbook/" method="post">
<table border="1" width="500">
<tr>
<td>이름</td><td><input type="text" name="name"></td>
<td>비밀번호</td><td><input type="password" name="pwd"></td>
</tr>
<tr>
<td colspan=4><textarea name="message" cols=60 rows=5></textarea></td>
</tr>
<tr>
<td colspan=4 align=right><input type="submit" VALUE=" 확인 "></td>
</tr>
</table>
</form>

<br>

<c:set var="count" value="${fn:length(guestbookList)}" />
<c:forEach items="${guestbookList }" var="guestbook" varStatus="status" >
<table width="510" border="1">
<tr>
<td>[${count - status.index}]</td>
<td>${guestbook.name }</td>
<td>${guestbook.regDate }</td>
<td><a href="/guestbook/delete?no=${guestbook.no }">삭제</a></td>
</tr>
<tr>
<!-- 개행(\n) JSTL에서 사용할 수 없어서 page context에 다른 변수로 추가해줘야함 -->
<td>${fn:replace(guestbook.message, newLine, "<br>") }</td>
</tr>
</table>
<br>
</c:forEach>
</body>
</html>

JSTL이 익숙하지 않다면 이글을 참고해주세요.




이상으로 SELECT 구현을 마치겠습니다.

아직 데이터가 아무것도 없지만 서버를 실행 시키면, 콘솔 창에 아래와 같이 hibernate가 guestbook이라는 테이블을 생성하게 됩니다.



<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
...
<property name="jpaProperties">
<props>
...
<prop key="hibernate.hbm2ddl.auto">create</prop> <!-- DDL 자동 생성 -->
</props>
</property>
</bean>

물론 applicationContext.xml 파일에서 위와 같이 설정이 되어 있어야 합니다.

create 방식은 테이블이 존재하면 삭제하고, 새롭게 정의하는 방식입니다.

실제 DB에서 테이블이 생성되었는지 확인해보세요 !



다음으로 브라우저에서 http://localhost:8080/guestbook/로 접근하면, 아래와 같은 SELECT 쿼리가 콘솔창에 출력될 것입니다.



GuestbookRepository에서 작성한 JPQL이 SQL로 바뀐 것입니다.

JPA가 해석할 수 있는 언어는 JPQL이고 내부적으로 JDBC API를 사용하기 때문에 결과적으로 SQL이 실행되는 것을 확인할 수 있습니다.


아직은 데이터가 없으므로 이렇게 쿼리가 출력되는 것까지만 확인하면 되고, 이어서 INSERT 작업을 진행해보겠습니다.





2. 삽입 ( INSERT )

1) GuestbookController.java

@RequestMapping(value="/", method=RequestMethod.POST)
public String add(Guestbook guestbook) {
guestbookService.insertMessage(guestbook);
return "redirect:/";
}
  • 기존의 Controller에 add() 메서드를 추가하여, 방명록을 추가할 수 있도록 합니다.



2) GuestbookService.java

public void insertMessage(Guestbook guestbook) {
guestbook.setRegDate(new Date());
guestbookRepository.save(guestbook);
}
  • Service 계층에서 주의해야 할 점이 있습니다.
    • INSERT , UPDATE , DELETE 쿼리를 수행 할 때는 트랜잭션을 필수적으로 명시를 해줘야 합니다.
    • JPA에서 트랜잭션을 제공하므로, 클래스 선언부 위에 @Transactional 어노테이션을 추가해주면 해당 서비스 내에서 트랜잭션이 걸리게 됩니다.
      • 단, 클래스 위에 트랜잭션을 걸어주는 것보다 메서드 단위로 해주는 것이 좋습니다.




3) GuestbookRepository.java

public void save(Guestbook guestbook) {
em.persist(guestbook);
}
  • 데이터를 추가하는 메서드는 EntityManager의 persist() 메서드입니다.
  • persist는 영속화 시킨다는 의미인데, 정확히는 데이터를 추가하는 것이 아니지만, 편의상 INSERT 쿼리가 수행되므로 데이터를 추가하는 메서드라 표현하겠습니다. 이는 좀 어려운 내용이므로 나중에 이글에서 알아가면 됩니다.



이제 서버를 재실행 하고, 방명록을 작성해보겠습니다.

그러면 아래와 같은 쿼리가 콘솔에 출력 되고, 실제로도 DB에 데이터가 저장되는 것을 확인할 수 있습니다.






3. 삭제 ( DELETE )

이어서 삭제도 바로 진행해볼게요.


1) GuestbookController.java

@RequestMapping(value="/delete", method=RequestMethod.GET)
public String deleteform() {
return "deleteform";
}


@RequestMapping(value="/delete", method=RequestMethod.POST)
public String delete(Guestbook guestbook) {
guestbookService.deleteMessage( guestbook );
return "redirect:/";
}



2) GuestbookService.java

public void deleteMessage(Guestbook guestbook) {
boolean result = guestbookRepository.remove(guestbook);
}



3) GuestbookRepository.java

public boolean remove(Guestbook guestbook) {
String jpql = "SELECT gb from Guestbook gb WHERE gb.no = :no AND gb.pwd = :pwd";
TypedQuery<Guestbook> query = em.createQuery(jpql, Guestbook.class);
query.setParameter("no", guestbook.getNo());
query.setParameter("pwd", guestbook.getPwd());

List<Guestbook> guestbookList = query.getResultList();
if( guestbookList.size() != 1 ) {
return false;
}

em.remove(guestbookList.get(0));
return true;
}
  • 작성한 JPQL은 방명록을 조회하는 쿼리이며, 실제 DB DELETE 쿼리를 수행하는 부분은 em.remove() 메서드입니다.



4) WEB-INF/views/deleteform.jsp

마지막으로 삭제 페이지를 구현하겠습니다.

<form action="/guestbook/delete?no=${ param.no }" method="post">
<table>
<tr>
<td>비밀번호</td>
<td><input type="password" name="pwd"></td>
<td><input type="submit" value="확인"></td>
</tr>
</table>
</form>

<a href="/guestbook/">메인으로 돌아가기</a>



이제 서버를 재실행하여, 글을 다시 작성한 후 삭제까지 해보겠습니다.

삭제를 하면 아래와 같은 쿼리가 출력됩니다.







이상으로 EntityManager 객체를 통한 CRUD를 구현해보았습니다.

( 과거에 작성했던 글이라 예제가 미흡하네요... 그래도 이 글의 내용을 깊게 이해하려고 하지 않으셔도 됩니다. 이어지는 글들이 훨씬 더 중요해요. )

  • INSERT
    • em.persist()
  • DELETE
    • em.persist()
  • UPDATE는 다루지 않았는데 DELETE 할 때와 같이 엔티티를 조회한 후, setter()를 통해 값을 변경하면 UPDATE 쿼리가 수행됩니다.


지금까지의 JPA를 보면, Mybatis 대신 JPA를 사용해야 하는 이유를 잘 못느끼실 수 있을 것 같습니다.

QueryDSL이라는 것을 사용하면 Mybatis와의 차이점을 느낄 수 있고,

더 나아가서 Spring Data JPA를 사용하면 "이래서 JPA를 사용하는구나!"를 느낄 수 있습니다.


그렇다고 바로 Spring Data JPA로 넘어가면 안되고,

JPQL, QueryDSL 모두 JPA를 잘 다루기 위해 필요한 기술이기 때문에 학습을 해두는 것이 좋습니다!