Spring Data JPA 를 이용한 DB 개발
JPA란 무엇인가?
JPA는 자바 진영의 ORM 기술 표준이다. 애플리케이션과 JDBC 사이에서 동작한다. 자바 퍼시스턴스 API(Java Persistence API,JPA)는 관계형 데이터베이스에 접근하기 위한 표준 ORM 기술을 제공하며, 기존에 EJB에서 제공되던 엔터티 빈(Entity Bean)을 대체하는 기술이다. 이를 구현한 구현체로는 Hibernate, OpenJPA, EclipseLink 와 같은것들이 있고 이에 따른 표준 인터페이스가 바로 JPA인 것이다.
ORM이란?
왜 JPA를 사용해야 하는가
JPA를 이용하여 객체 매핑을 할 경우 아래와 같은 어노테이션을 사용할 수 있다.
우리는 이전 강좌에서 H2 를 이용한 기본 설정(자동구성) 어플리케이션을 작성하였다. 그러나, 실무에서의 경우 h2 보다는 오라클이나 MySQL과 같은 상용 RDBMS를 사용하므로 새 어플리케이션을 작성하면서 MySQL로 연동을 해보도록 한다.
이번 챕터에서는 MySQL 을 이용하여 부서와 부서원 관리 어플리케이션을 작성해볼 예정이다. 이를 위해 DB 연동 설정과 Java 소스상에서 공통 반복 코드들을 어노테이션 기반으로 처리해줄 롬복 (lombok) 을 적용해볼 것이다. 롬복에 대해서 자세한 정보는 공식 사이트(https://projectlombok.org) 에서 확인하기 바란다. 이 내용들을 알기 위해서는 Persistance Layer의 개념과 Spring-Data-JPA와 Hibernate 의 개념, 그리고 영속화 연관관계에 대해서도 이해해야 하나, 이 내용들을 정리하기 위해서는 따로 상당한 양의 강의자료가 필요하므로 이 내용들은 Hibernate 관련 강의에서 재정리 하도록 하자.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driverClassName=org.h2.Driver
JDBC 연동을 위해서는 spring-boot-starter-jdbc 나 spring-boot-starter-data-jpa 스타터 번들이 추가 되어있어야 한다.
URL 에는 주소 + DB 스키마 명 (여기서는 test로 명명) 조합으로 구성되며 username 과 password는 해당 DB 스키마에 접근하는 사용자 계정 정보를 뜻한다.
이 밖에 톰캣에서 제공하는 Connection Pool 을 사용할 경우 아래와 같은 속성 항목을 설정해줄 필요도 있다. (참고 : https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html)
# Number of ms to wait before throwing an exception if no connection is available. spring.datasource.tomcat.max-wait=10000 # Maximum number of active connections that can be allocated from this pool at the same time. spring.datasource.tomcat.max-active=50 # Validate the connection before borrowing it from the pool. spring.datasource.tomcat.test-on-borrow=true
물론 JNDI를 사용하여 데이터소스에도 접근할 수 있다.
MySQL DB 연동을 위한 기본적인 설정
예제를 작성하기 위해, application.properties 파일에 아래와 같이 MySQL 접속 정보를 기술하도록 한다.
# Hibernate config
# Hibernate ddl auto (create, create-drop, update, validate, none): with "update" the database schema will be automatically updated accordingly to java entities found in the project
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=true
spring.jpa.database=MYSQL
# Show or not log for each sql query
spring.jpa.show-sql=true
# Allows Hibernate to generate SQL optimized for a particular DBMS
spring.jpa.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
spring.jpa.properties.hibernate.format_sql=true
spring.datasource.validation-query=SELECT 1
spring.datasource.test-on-borrow=false
spring.datasource.test-on-return=false // 반환시 커넥션 풀이 유효한지 검사
# JDBC Connection url for the database
spring.datasource.url=jdbc:log4jdbc:mysql://localhost:3306/howling?useUnicode=true&charaterEncoding=utf-8
spring.datasource.username=userId
spring.datasource.password=pass
spring.datasource.driverClassName=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
# message
howling.hello.message=[develop mode] Hello libqa.com!
# view Resolver cache config
environment.viewResolver.cached=false
# log level config
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=INFO
주목할 점은 하이버네이트 설정상에서의 옵션이 create, create-drop, update 등이 존재하는데 create 모드는 기존테이블 삭제 후 다시 생성하는 반면, create-drop 모드는 종료 시점에 테이블 DROP을 하고 update 모드의 경우는 변경분만 반영하는 특성이 있다. 나머지 validate는 엔티티와 테이블이 정상 매핑되었는지만 확인 하는 옵션이고 none의 경우는 ddl 옵션을 사용하지 않는 것으로 실 서비스 모드에서는 보통 none 으로 처리한다.
build.gradle 에 아래와 같이 관련 모듈을 추가해준다. 여기서는 thymeleaf 를 이용하여 뷰 영역을 표현할 예정이다. 자세한 스펙에 대해서는 공식 사이트를 참고한다. (http://www.thymeleaf.org)
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-tomcat')
compile('org.projectlombok:lombok:1.16.0')
compile('mysql:mysql-connector-java:5.1.28')
compile('org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc3:1.16')
compile('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
SQL 을 console에 출력하기 위해서 resources 하위에 log4jdbc.log4j2.properties 를 추가하고 로그 레벨을 조정하기 위해 logback.xml 을 추가해주도록 한다.
log4jdbc.log4j2.properties
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<!-- log4jdbc-log4j2 -->
<logger name="jdbc.sqlonly" level="ERROR"/>
<logger name="jdbc.sqltiming" level="INFO"/>
<logger name="jdbc.audit" level="INFO"/>
<logger name="jdbc.resultset" level="ERROR"/>
<logger name="jdbc.resultsettable" level="ERROR"/>
<logger name="jdbc.connection" level="INFO"/>
<logger name="com.libqa.web" level="DEBUG"/>
</configuration>
이제 도메인 파일을 작성할 예정인데, 패키지 경로는 com.libqa 하위에 entity를 선언하여 Member 엔티티와 Team 엔티티를 선언한다. 팀 엔티티는 1:N의 연관관계로 멤버 엔티티를 소유한다.
아래와 같이 엔티티를 작성한다.
Team.java
package com.libqa.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.Collection;
/**
* @Author : yion
* @Date : 2016. 12. 27.
* @Description :
*/
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Team {
@Id
@Column(name = "seq")
@GeneratedValue(strategy = GenerationType.AUTO)
private int seq;
@Column(name = "team_name")
private String teamName;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "team", fetch = FetchType.LAZY) // @OneToMany의 fetch 기본전략은 LAZY이다.
private Collection<Member> member;
public Team(String teamName) {
this.teamName = teamName;
}
}
Member.java
package com.libqa.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.Date;
/**
* @Author : yion
* @Date : 2016. 12. 19.
* @Description : 회원 정보 엔티티
*/
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Member {
@Id
@Column(name = "seq")
@GeneratedValue(strategy = GenerationType.AUTO)
private int seq;
@Column(name = "name")
private String name;
@ManyToOne(optional = false)
@JoinColumn(name = "team_seq") // @ManyToOne의 fetch 기본전략은 EAGER이다.
private Team team;
public Member(Team team, String name) {
this.team = team;
this.name = name;
}
}
이 엔티티를 접근하는 각각의 Repository를 작성한다.
TeamRepository.java
package com.libqa.web.member.repository;
import com.libqa.entity.Team;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @Author : yion
* @Date : 2016. 12. 27.
* @Description :
*/
public interface TeamRepository extends JpaRepository<Team, Integer> {
}
MemberRepository.java
package com.libqa.web.member.repository;
import com.libqa.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* @Author : yion
* @Date : 2016. 12. 20.
* @Description : User 리파지토리 인터페이스
*/
public interface MemberRepository extends JpaRepository<Member, Integer>{}
아직 아무론 비즈니스가 정의되어 있진 않지만, 해당 Repository를 호출하여 비즈니스 로직을 작성할 서비스를 정의해보도록 한다. 여기서는 RepositoryService라는 인터페이스로 정의 하였다.
public interface RepositoryService {
}
아래는 RepositoryServiceImpl 이다.
package com.libqa.web.member.service;
import com.libqa.entity.Member;
import com.libqa.entity.Team;
import com.libqa.web.member.repository.MemberRepository;
import com.libqa.web.member.repository.TeamRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.List;
/**
* @Author : yion
* @Date : 2016. 12. 27.
* @Description :
*/
@Slf4j
@Service
public class RepositoryServiceImpl implements RepositoryService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private TeamRepository teamRepository;
public RepositoryServiceImpl() {
}
}
이제 서버를 기동해보자.
하이버네이트 DDL모드를 update로 실행 하였는데, 서버 기동시 올라오는 Console의 로그상에서 아래와 같이 테이블 생성 스크립트가 실행됨을 확인할 수 있다.
2016-12-27 15:44:20.491 INFO 1886 --- [ main] jdbc.sqltiming : create table team (seq integer not null auto_increment, team_name varchar(255), primary key (seq)) 2016-12-27 15:44:20.480 INFO 1886 --- [ main] jdbc.sqltiming : create table member (seq integer not null auto_increment, name varchar(255), team_seq integer not null, primary key (seq)) alter table member add constraint FKhqim285vtvxxsoaqj6by2slc2 foreign key (team_seq) references ...
정상적으로 실행이 되었다면 DB 클라이언트등으로 설정한 DB에 접근해보도록 하자.
위 두개의 테이블이 제대로 생성 되었음을 확인 할 수 있다.
이제 html 폼을 이용하여 팀과 회원정보를 핸들링 해보도록 하자.
먼저 팀명을 입력해서 팀을 생성하는 폼을 호출 해본다.
DeptController.java
package com.libqa.web.member.controller;
import com.libqa.entity.Team;
import com.libqa.web.member.service.RepositoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @Author : yion
* @Date : 2016. 12. 19.
* @Description : 사용자 정보를 조회하는 controller
*/
@Controller
public class DeptController {
@Autowired
private RepositoryService repositoryService;
/**
* 팀 등록 폼 페이지
*
* @param model
* @return
*/
@GetMapping("/form")
public String form(Model model) {
model.addAttribute("title", "create your team!");
model.addAttribute("team", new Team());
return "form";
}
}
진입점은 "/form" 이라는 주소를 통해 GET방식으로 호출하며, 폼으로 표현할 객체를 생성해서 form.html 파일에 addAttribute 한 후 리턴해주었다. 이전의 자동설정 관련 내용에서 예제를 작성할 때 html 파일은 resources 하위에 templates 폴더 밑으로 작성했던것을 기억할 수 있을 것이다. form.html 파일을 작성해보자.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Handling Form Submission</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1><p th:text="${title}" /></h1>
<form action="#" th:action="@{/add}" th:object="${team}" method="post">
<p>seq: <input type="text" th:field="*{seq}" /></p>
<p>name: <input type="text" th:field="*{teamName}" /></p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
</body>
</html>
form action 으로 add URL을 정의하였고 실제 Team 엔티티에 대응하는 input 을 생성해주었다. (현재 seq 필드는 자동증가값이므로 사실 여기서 입력하는 것은 의미가 없다)
실제 저장 버튼을 누르면 컨트롤러에 정의한 "/add" 가 호출되어야 하므로 아래와 같이 저장로직이 들어가는 add 메서드를 구현해준다.
@PostMapping("/add")
public String add(@ModelAttribute Team team, Model model) {
Team entity = repositoryService.addTeam(team);
model.addAttribute("result", entity);
return "result";
}
여기에서는 서비스 레이어의 addTeam을 호출하므로 관련하여 interface를 작성하고, implements 를 작성해주도록 한다.
주요 로직은 해당 엔티티를 저장하는 것이므로 아래와 같이 간단하게 표현할 수 있다.
@Override
public Team addTeam(Team team) {
return teamRepository.save(team);
}
실행 후 결과를 보여주는 result.html 파일을 작성해보자.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Handling Form Submission</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Result</h1>
<p th:text="'seq: ' + ${result.seq}" />
<p th:text="'teamName: ' + ${result.teamName}" />
<a href="/form">팀 등록하기</a> |
<a th:href="@{|/addUser/${result.seq}|}">이 팀의 구성 원등록하기</a>
</body>
</html>
http://localhost:8080/form 에 접근하여 팀명을 입력 한 후 submit을 눌러보면 입력된 팀명이 제대로 결과 페이지에 출력되는 것을 확인 할 수 있을 것이다.
이때 로그를 잘 살펴보면 하이버네이트의 insert 쿼리가 동작하는 것을 확인 할 수 있다.
Hibernate: insert into team (team_name) values (?)
DB에 접근하여 살펴보면 입력된 값이 제대로 저장 된 것을 확인 할 수 있을 것이다.
이제 팀의 구성원을 등록하고, 팀 전체 목록을 조회하는 페이지를 작성해보도록 한다.
result 페이지에서 구성원 등록을 클릭하면 /addUser/{seq} 형태로 팀 고유번호를 같이 넘겨받아 팀에 해당하는 정보를 기반으로 사용자 정보를 작성할 수 있어야 한다.
@GetMapping("/addUser/{seq}")
public String addUser(@PathVariable int seq, Model model) {
Team team = repositoryService.findTeamBySeq(seq);
model.addAttribute("title", "add user");
model.addAttribute("team", team);
model.addAttribute("member", new Member());
return "addUser";
}
addUser.html 은 팀 정보 저장 페이지와 동일하되, 팀 번호를 hidden으로 물고 다니면서 해당 사용자가 현재 팀의 소속으로 저장된 다는 것을 알고 알려줘야 한다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Handling Form Submission</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1><p th:text="${team.teamName}" /> 에 구성원을 등록하세요. </h1>
<form action="#" th:action="@{/insert}" th:object="${member}" method="post">
<input type="hidden" th:value="${team.seq}" name="seq" />
<p>seq: <input type="text" th:field="*{seq}" /></p>
<p>name: <input type="text" th:field="*{name}" /></p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
</body>
</html>
저장로직은 /insert 를 호출하는데 소스는 아래와 같다.
@PostMapping("/insert")
public String insert(@ModelAttribute Member member, @RequestParam int seq, Model model) {
Member entity = repositoryService.addMember(member, seq);
model.addAttribute("result", entity);
return "user";
}
서비스 영역의 addMember 로직은 간단한 저장로직과 다른데, 이는 이 테이블 엔티티의 관계가 Team을 기준으로 연관관계에 있기 때문에 해당 Team 정보가 존재해야 저장이 되는 형태로 구현이 되기 때문에 아래와 같이 로직을 구현해주어야 한다.
@Override
public Member addMember(Member member, int seq) {
Team team = teamRepository.findOne(seq);
Member saveEntity = new Member(team, member.getName());
return memberRepository.save(saveEntity);
}
먼저 팀 Seq 번호로 팀 정보를 조회하고, 해당 팀과 연관관계라는 것을 Member에 세팅 한 후 member 엔티티를 저장해야 한다. 만약 이 정보를 가지고 있지 않을 경우, 팀 연관정보를 찾지 못해 에러가 발생하게 된다.
끝으로 저장된 유저 정보 결과를 보여주고 전체 팀 목록을 조회하는 페이지를 구현해주면 된다.
user.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Result</h1>
<p th:text="'사용자 번호: ' + ${result.seq}" />
<p th:text="'이름: ' + ${result.name}" />
<a href="/list">팀 목록 조회</a> |
</body>
</html>
팀 목록을 조회하면 Controller 에서는 service 에 find를 요청할 것이고, 서비스는 team 엔티티의 리스트 정보를 teamRepository 호출로 리턴 될 것이다.
DeptController.java
@GetMapping("/list")
public String list(Model model) {
List<Team> teamList = repositoryService.findTeamAll();
model.addAttribute("teamList", teamList);
return "list";
}
RepositoryServiceImpl.java
@Override
public List<Team> findTeamAll() {
return teamRepository.findAll();
}
최종적으로 list.html 에서는 리턴된 teamList 의 값을 출력하면 된다.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Handling Form Submission</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Team List</h1>
<ul>
<li th:each="team : ${teamList}" th:text="${team.teamName}"></li>
</ul>
<a href="/form">팀 등록하기</a>
</body>
</html>
결과는 아래와 같다.
이 전체 소스는 Github 의 chapter4 브랜치에 올라가 있으므로 전체 소스를 참고하도록 한다. (https://github.com/gliderwiki/SpringBootActuator)
CrudRepository 인터페이스 살펴보기
실제 Spring-Data-JPA 의 CrudRepository 를 살펴보면 크게 아래와 같은 메서드들을 정의 해 놓을 것을 확인 할 수 있다. 1번부터 6번까지 메서드 이외에도 기본적으로 제공되는 다른 인터페이스 들도 있지만 일단 제일 중요한 부분이므로 한번 살펴보도록 하자.
1: 주어진 엔티티를 저장한ㅇ다.
2: ID로 주어진 엔티티를 조회한다. 해당 키의 Entity 타입을 리턴한다.
3: 전체 엔티티를 리턴한다.
4: 엔티티의 갯수를 리턴한다.
5: 주어진 엔티티를 삭제한다.
6: 주어진 아이디가 존재하는지 리턴한다.
이러한 인터페이스는 각 DB에 의존적이지 않게 기본적인 퍼시스턴스 기술의 기능을 제공하게 된다.
개발자들이 흔히 궁금한 점은, 과연 저런 메서드가 제공 된다고 해서 원하는 수준의 쿼리를 뽑아 낼 수 있을 것인가일텐데, 이에 대해서는 아래의 쿼리 메서드를 살펴보면 어느정도 해답이 될거라고 본다.
기본적으로 알아야 할 쿼리 메서드 작성
만약 이정도의 쿼리 메서드로 해결하기 어려운 경우에는 JPQL(Java Persistence Query Language)이라던가, QueryDSL(http://www.querydsl.com)과 같은 관련 기술들의 도입을 검토해서 활용하면 충분히 대용량 서비스를 제공할 수 있는 수준의 잇점이 있을것이라고 판단한다.
링크 : http://www.libqa.com/wiki/730
'프로잭트(일기장) > 회사 공부 자료' 카테고리의 다른 글
WAS 란? WAS 뭐에요? (0) | 2019.02.26 |
---|---|
도로주소명 JSP 설정방법 (0) | 2019.02.26 |
JSON이란 무엇일까?? (0) | 2019.02.26 |
왕초보를 위한 JSON Parsing - 1 (JSON이란?) (0) | 2019.02.26 |
[Network] REST API / RESTFUL 이란? (0) | 2019.02.20 |