안녕하세요!
이번 시간에는 JPA 를 활용한 예제 프로젝트를 생성해보겠습니다.
들어가기 전에
JPA의 특징으로는 SQL문을 자동생성하는 것이 특징입니다.
💡 프로젝트 준비 및 환경설정
프로젝트 준비시 스프링부트의 종속성을 추가합니다.
생성 후 시간이 소요됩니다.
한글 깨짐 방지를 위해 설정 - 에디터 - 파일 인코딩 부분에 UTF-8로 변경해줍니다.
build.gradle 파일의 dependencies 부분에 오라클과 logback, log4jdbc 종속성을 추가해줍니다.
// todo: 오라클 라이브러리( 19c )
implementation 'com.oracle.database.jdbc:ucp:19.14.0.0'
implementation 'com.oracle.database.security:oraclepki:19.14.0.0'
implementation 'com.oracle.database.security:osdt_cert:19.14.0.0'
implementation 'com.oracle.database.security:osdt_core:19.14.0.0'
// todo: logback , log4jdbc 설정
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
implementation 'ch.qos.logback:logback-classic:1.2.11'
implementation 'org.slf4j:slf4j-api:1.7.36'
implementation 'org.slf4j:jcl-over-slf4j:1.7.36'
그리고 main > resources > application.properties 파일에 설정을 추가합니다.
# 서버 포트
server.port=8000
# todo : docker db 설정
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
# todo : spring.datasource.url=jdbc:log4jdbc:oracle:thin:@ip주소:db포트번호/db이름
spring.datasource.url=jdbc:log4jdbc:oracle:thin:@localhost:1521/xepdb1
spring.datasource.username=scott
spring.datasource.password=!Ds1234567890
# todo : jpa 설정
# todo : ddl-auto - create(모든 테이블 재생성)/update(수정된 테이블만 생성)/none(안함)
spring.jpa.hibernate.ddl-auto=create
#spring.jpa.hibernate.ddl-auto=update
#spring.jpa.hibernate.ddl-auto=none
# todo : db 제품 연결 ( oracle 12 이상 : Oracle12cDialect )
spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect
# todo : generate-ddl=true (테이블 자동 생성 옵션)
spring.jpa.generate-ddl=true
# todo : sql log 보기 (true/false)
spring.jpa.show-sql=true
# 1) resource/data.sql 자동 실행 ( DML 실행 )
# -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
# todo : DML, DDL 스크립트 (실습용 샘플 테이블) 실행을 위한 옵션
spring.jpa.defer-datasource-initialization=true
# todo : sql log 좀 더 정리된 형태로 찍기
spring.jpa.properties.hibernate.format_sql=true
# todo : 로깅 레벨 : error < info < debug < trace (정보 많은 순서)
logging.level.org.hibernate=info
# 2) resource/data.sql 자동 실행 ( DML 실행 )
# -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.sql.init.mode=always
# sql 에러 무시하고 스프링 서버 로딩
spring.sql.init.continue-on-error=true
# 자바 소스 변경시 스프링 서버 자동 재시작
spring.devtools.restart.enabled=true
# PUT , DELETE 방식도 form 에서 사용할 수 있게 만들어줌 : jsp에서 사용함
spring.mvc.hiddenmethod.filter.enabled=true
실습을 위해 resources 폴더 아래에 4가지 파일을 추가합니다.
📂 data.sql
INSERT INTO TB_DEPT VALUES (10,'ACCOUNTING','NEW YORK', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_DEPT VALUES (20,'RESEARCH','DALLAS', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_DEPT VALUES (30,'SALES','CHICAGO', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_DEPT VALUES (40,'OPERATIONS','BOSTON', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_EMP VALUES (7369,'SMITH','CLERK', 7902,TO_CHAR(to_date('17-12-1980','dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),800, NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7499,'ALLEN','SALESMAN', 7698,TO_CHAR(to_date('20-2-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1600, 300,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7521,'WARD','SALESMAN', 7698,TO_CHAR(to_date('22-2-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1250, 500,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7566,'JONES','MANAGER', 7839,TO_CHAR(to_date('2-4-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),2975,NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7654,'MARTIN','SALESMAN',7698,TO_CHAR(to_date('28-9-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1250,1400,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7698,'BLAKE','MANAGER', 7839,TO_CHAR(to_date('1-5-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),2850,NULL,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7782,'CLARK','MANAGER', 7839,TO_CHAR(to_date('9-6-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),2450,NULL,10, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7788,'SCOTT','ANALYST', 7566,TO_CHAR(to_date('13-07-1987','dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),3000,NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7839,'KING','PRESIDENT', NULL,TO_CHAR(to_date('17-11-1981','dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),5000,NULL,10, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7844,'TURNER','SALESMAN',7698,TO_CHAR(to_date('8-9-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1500, 0,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7876,'ADAMS','CLERK', 7788,TO_CHAR(to_date('13-07-1987','dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1100,NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7900,'JAMES','CLERK', 7698,TO_CHAR(to_date('3-12-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),950, NULL,30, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7902,'FORD','ANALYST', 7566,TO_CHAR(to_date('3-12-1981', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),3000,NULL,20, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_EMP VALUES (7934,'MILLER','CLERK', 7782,TO_CHAR(to_date('23-1-1982', 'dd-mm-yyyy'), 'YYYY-MM-DD HH24:MI:SS'),1300,NULL,10, TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), NULL);
INSERT INTO TB_FAQ VALUES (1,'제목','해결방법', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_FAQ VALUES (2,'제목2','해결방법2', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_FAQ VALUES (3,'제목3','해결방법3', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
INSERT INTO TB_FAQ VALUES (4,'제목4','해결방법4', TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'), null);
COMMIT;
📂log4jdbc.log4j2.properties
# sql \uB85C\uADF8 \uC124\uC815
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
# \uCD5C\uB300 \uBA87 \uB77C\uC778\uAE4C\uC9C0 \uCD9C\uB825\uD560 \uAC83\uC778\uAC00\uB97C \uACB0\uC815 : 0 \uC774\uBA74 \uC81C\uD55C\uC5C6\uC74C
log4jdbc.dump.sql.maxlinelength=0
📂 logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- Appenders -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>%d %5p [%c] %m%n</Pattern>
</encoder>
</appender>
<appender name="console-infolog" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>%d %5p %m%n</Pattern>
</encoder>
</appender>
<!-- Logger -->
<logger name="com.example.jpaexam" level="DEBUG" appender-ref="console" />
<logger name="jdbc.sqlonly" level="INFO" appender-ref="console-infolog" />
<logger name="jdbc.resultsettable" level="INFO" appender-ref="console-infolog" />
<!-- Root Logger : 기본 OFF -->
<!-- 하위 로거에서 재정의해서 사용함 : INFO, DEBUG 등으로 -->
<root level="off">
<appender-ref ref="console" />
</root>
</configuration>
📂 shema.sql
DROP SEQUENCE SQ_DEPT;
CREATE SEQUENCE SQ_DEPT START WITH 50 INCREMENT BY 10;
DROP SEQUENCE SQ_EMP;
CREATE SEQUENCE SQ_EMP START WITH 8000 INCREMENT BY 1;
DROP SEQUENCE SQ_FAQ;
CREATE SEQUENCE SQ_FAQ START WITH 1 INCREMENT BY 1;
DROP TABLE TB_EMP CASCADE CONSTRAINT;
DROP TABLE TB_DEPT CASCADE CONSTRAINT;
DROP TABLE TB_FAQ CASCADE CONSTRAINT;
CREATE TABLE TB_DEPT (
DNO NUMBER NOT NULL PRIMARY KEY,
DNAME VARCHAR2(255),
LOC VARCHAR2(255),
INSERT_TIME VARCHAR2(255),
UPDATE_TIME VARCHAR2(255)
);
CREATE TABLE TB_EMP
(ENO NUMBER NOT NULL PRIMARY KEY,
ENAME VARCHAR2(255),
JOB VARCHAR2(255),
MANAGER NUMBER,
HIREDATE VARCHAR2(255),
SALARY NUMBER,
COMMISSION NUMBER,
DNO NUMBER,
INSERT_TIME VARCHAR2(255),
UPDATE_TIME VARCHAR2(255)
);
CREATE TABLE TB_FAQ (
NO NUMBER NOT NULL PRIMARY KEY,
TITLE VARCHAR2(255),
CONTENT VARCHAR2(255),
INSERT_TIME VARCHAR2(255),
UPDATE_TIME VARCHAR2(255)
);
그리고 java > com.example.jpaexam 폴더 아래에 다음과 같이 폴더를 생성합니다.
💡 model 폴더에 공통모델 파일 생성
model 폴더 아래에 생성일자/수정일자를 만들어주는 추상클래스를 생성합니다.
실습에서는 파일명을 BaseTimeEntity 로 생성하였습니다.
📂 model > BaseTimeEntity.java
1) 먼저 추상클래스 위에 어노테이션 3개를 추가합니다.
package com.example.jpaexam.model;
import lombok.Getter;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
/**
* packageName : com.example.jpaexam.model
* fileName : BaseTimeEntity
* author : GGG
* date : 2023-10-16
* description : JPA 에서 자동으로 생성일자/수정일자를 만들어주는 클래스
* 요약 :
* <p>
* ===========================================================
* DATE AUTHOR NOTE
* —————————————————————————————
* 2023-10-16 GGG 최초 생성
*/
@Getter
// todo : 자동으로 생성일자/수정일자 컬럼을 sql 문에 추가시키는 어노테이션 2개
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
// todo) 공통 속성
private String insertTime;
private String updateTime;
}
여기서 @MappedSuperclass 어노테이션의 의미는
생성일자/수정일자를 모든 엔티티에 공통으로 가져가야하는 상황에서 위의 코드처럼
BaseTimeEntity를 정의해서 활용할 때 사용합니다.
2) 공통 속성의 형태를 변경하는 함수 추가
위의 코드는 time의 형태가 yyyy-MM-dd HH:mm:ss 형태가 아닌 기본 형태로 보이게 됩니다.
가독성을 좋게하기 위해 공통속성 코드 아래에 함수를 추가합니다.
...
private String updateTime;
// todo) 해당 테이블에 데이터가 만들어 질 때(insert 문) 실행되는 이벤트 함수
@PrePersist
void OnPerPersist(){
this.insertTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
// todo) 해당 테이블에 데이터가 수정될 때(update 문) 실행되는 이벤트 함수
@PreUpdate
void OnPreUpdate(){
this.updateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
this.insertTime = this.updateTime; // 생성일시 == 수정일시 동일하게 처리
}
💡 Dept 모델 생성
model 폴더 아래에 Dept 클래스를 생성합니다.
JPA에서 사용하는 어노테이션의 설명은 아래 코드에 todo로 표시되어있습니다.
📂 model > Dept.java
package com.example.jpaexam.model;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import javax.persistence.*;
/**
* packageName : com.example.jpaexam.model
* fileName : Dept
* author : GGG
* date : 2023-10-16
* description : 부서 모델 클래스 ( 엔티티(entity) )
* 요약 :
* <p>
* ===========================================================
* DATE AUTHOR NOTE
* —————————————————————————————
* 2023-10-16 GGG 최초 생성
*/
// todo) @Entity - JPA 기능을 클래스에 부여하는 어노테이션
@Entity
// todo) @Table(name = "생성될 테이블명")
@Table(name = "TB_DEPT")
// todo 사용법 : @SequenceGenerator(
// name = "시퀀스 함수 이름"
// , sequenceName = "DB에 생성된 시퀀스 이름"
// , initialValue = 시작값
// , allocationSize = jpa에서 관리용숫자(성능지표)
// )
@SequenceGenerator(
name = "SQ_DEPT_GENERATOR"
, sequenceName = "SQ_DEPT"
, initialValue = 1
, allocationSize = 1
)
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
// todo) JPA 어노테이션 SQL 자동 생성시 null 값 컬럼은 제외하고 생성하는 어노테이션
// 예) insert into 테이블명(컬럼1, 컬럼2, 컬럼3) values(1, 2, null)
// => insert into 테이블명(컬럼1, 컬럼2) values(1, 2)
@DynamicInsert
@DynamicUpdate
public class Dept extends BaseTimeEntity{
@Id // todo) 기본키임을 알려주는 어노테이션
// todo) @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "시퀀스함수이름")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SQ_DEPT_GENERATOR") // 시퀀스 기능 부여
// todo : @Column(columnDefinition = "DB컬럼 자료형")
@Column(columnDefinition = "NUMBER")
private Integer dno; // 부서번호(기본키) - 시퀀스 기능 부여
@Column(columnDefinition = "VARCHAR2(255)")
private String dname; // 부서명
@Column(columnDefinition = "VARCHAR2(255)")
private String loc; // 부서위치
}
repository 파일 만들기
DB 접속 함수들(CRUD)을 만들어주기 위한 인터페이스 파일을 생성합니다.
📂 repository > DeptRepository.java
package com.example.jpaexam.repository;
import com.example.jpaexam.model.Dept;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* packageName : com.example.jpaexam.repository
* fileName : DeptRepository
* author : GGG
* date : 2023-10-16
* description : JPA 레포지토리 인터페이스 (DB 접속 함수들(CRUD) 있음)
* == DAO랑 비슷함
* 요약 :
* <p>
* ===========================================================
* DATE AUTHOR NOTE
* —————————————————————————————
* 2023-10-16 GGG 최초 생성
*/
// todo) @Repository - 클래스위에 붙이고, 스프링서버가 실행될 때 자동으로 객체 1개를 만들어줌( IOC )
// 사용법 : 인터페이스명 extends JpaRepository<모델클래스명, 기본키의 자료형>
@Repository
public interface DeptRepository extends JpaRepository<Dept, Integer> {
}
JpaRepository를 상속받으면 DB접속에 사용할 수 있는 CRUD함수 기능을 사용할 수 있습니다.
💡 업무 service 클래스 생성
📂 service > DeptService.java
package com.example.jpaexam.service;
import com.example.jpaexam.model.Dept;
import com.example.jpaexam.repository.DeptRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* packageName : com.example.jpaexam.service
* fileName : DeptService
* author : GGG
* date : 2023-10-16
* description : 부서 업무 서비스
* 요약 :
* <p>
* ===========================================================
* DATE AUTHOR NOTE
* —————————————————————————————
* 2023-10-16 GGG 최초 생성
*/
@Service
public class DeptService {
@Autowired
DeptRepository deptRepository; // DI 객체 가져오기
/** 전체 조회 */
public List<Dept> findAll(){
List<Dept> list = deptRepository.findAll(); // db 전체조회 함수 호출
return list;
}
}
💡 controller 생성
📂 controller > DeptController
package com.example.jpaexam.controller.exam01;
import com.example.jpaexam.model.Dept;
import com.example.jpaexam.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* packageName : com.example.jpaexam.controller.exam01
* fileName : DeptController
* author : GGG
* date : 2023-10-16
* description : 부서 컨트롤러 : @RestController 사용
* 요약 :
* <p>
* ===========================================================
* DATE AUTHOR NOTE
* —————————————————————————————
* 2023-10-16 GGG 최초 생성
*/
@Slf4j
@RestController
@RequestMapping("/exam01")
public class DeptController {
@Autowired
DeptService deptService; // 객체 가져오기 (DI)
/** 전체조회 함수 */
@GetMapping("/dept")
public ResponseEntity<Object> getDeptAll() {
try {
// todo) 전체조회 함수 호출
List<Dept> list = deptService.findAll();
if (list.isEmpty() == false) {
// todo) 성공
return new ResponseEntity<>(list, HttpStatus.OK);
} else {
// todo) 데이터 없음
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
REST API 테스트를 위해 아래 그림의 빨간색 박스를 누릅니다.
테스트 창에서 ▶ 버튼을 눌러 실행합니다.
실행결과
GET http://localhost:8000/exam01/dept
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 16 Oct 2023 08:15:45 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[
{
"insertTime": "2023-10-16 08:15:40",
"updateTime": null,
"dno": 10,
"dname": "ACCOUNTING",
"loc": "NEW YORK"
},
{
"insertTime": "2023-10-16 08:15:40",
"updateTime": null,
"dno": 20,
"dname": "RESEARCH",
"loc": "DALLAS"
},
{
"insertTime": "2023-10-16 08:15:40",
"updateTime": null,
"dno": 30,
"dname": "SALES",
"loc": "CHICAGO"
},
{
"insertTime": "2023-10-16 08:15:40",
"updateTime": null,
"dno": 40,
"dname": "OPERATIONS",
"loc": "BOSTON"
}
]
응답 파일이 저장되었습니다.
> 2023-10-16T171545.200.json
Response code: 200; Time: 297ms (297 ms); Content length: 397 bytes (397 B)
JPA에서 자동으로 만든 select 문을 확인해봅시다.
Hibernate:
select
dept0_.dno as dno1_0_,
dept0_.insert_time as insert_time2_0_,
dept0_.update_time as update_time3_0_,
dept0_.dname as dname4_0_,
dept0_.loc as loc5_0_
from
tb_dept dept0_
'Spring Boot > 스프링부트 예제' 카테고리의 다른 글
JPA - JPQL(Java Persistence Query Language) (1) | 2023.10.17 |
---|---|
JPA 상세조회, 저장함수, 수정함수, 삭제함수 (0) | 2023.10.17 |
JSTL 라이브러리 (0) | 2023.10.06 |
파라미터 방식의 매개변수 전달 어노테이션 (0) | 2023.10.05 |
객체 형태로 변환하는 어노테이션 (0) | 2023.10.05 |