- JPA는 자바의 클래스와 엔티티인 관계형 DB의 테이블을 매칭시키는 JAVA 진영의 ORM 기술 표준이다.
JPA 기초 설정
1. Application.yml
- yaml(yml) 파일은 Yaml Ain't Markup Lanuage 라는 직역하면, YAML은 마크업 언어가 아니다 라는 뜻을 가지고 있다.
- 이는 핵심이 마크업이 아니라 데이터가 중심이라는 것을 보여주기 위해 저 뜻이 되어 버렸다.
# properties 예시
server.url=127.0.0.1
server.port=8080
# yaml 예시
server:
url: 127.0.0.1
port: 8080
- 위의 예시에서 보았듯이 코드의 중복을 최소화하고, 가독성을 크게 증가시킨 계층식 구조를 가지고 있다.
- properties는 유사한 속성값이 섞여 있다면 동일한 값으로 인지하여 잘못된 데이터를 호출하는 위험을 가질 수 있는데, yaml의 경우 이런 위험을 방지한다.
쇼핑몰 프로젝트의 application 설정
server:
address: 0.0.0.0
# 포트 번호
port: 8080
servlet:
# 서블릿 통신 설정
encoding:
charset: UTF-8
enabled: true
force: true
spring:
devtools:
# 정적 소스(classpath) 파일 변경 시 자동 브라우저 재실행
livereload:
enabled: true
# 서버 측 파일이 변경되면 재실행 안함(false)
restart:
enabled: false
# 객체를 json으로 변환하는 과정을 serialize이라 하고, 변환할 때 객체를 못가져와도 변환에 실패하지 않는다는 뜻
jackson:
serialization:
fail-on-empty-beans: false
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver # mysql
url: jdbc:mysql://localhost:3306/{스키마 명}?createDatabaseIfNotExist=true%useUnicode=true&serverTimezone=Asia/Seoul
username: # 접속명
password: # 접속명의 비밀번호
jpa: # jpa 설정
database: oracle
show-sql: true
hibernate:
ddl-auto: validate
# create : 기존 테이블 삭제 후 다시 생성
# create-drop : 테이블을 생성한다. 종료 시점에 생성한 테이블읈 삭제한다.
# update : 변경된 것만 반영 한다.
# validate : 엔티티와 테이블이 정상 매핑되었는지 확인
# none : 사용하지 않는다.
properties:
hibernate:
show_sql: true
format_sql: true
jwt:
header: Authorization
# jwt의 시크릿 키 : HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.
secret:
token-validity-in-seconds: 86400 # 토큰 유효기한(초)
2. build.gradle
- gradle은 CI/CD를 위한 아래 TASK들을 자동화 시켜주는 빌드 도구이다.
- Compile
- Test
- Packaging
- Deploy / Run
- gradle의 장점
- 직관적인 코드와 자동완성
- 다양한 Repository 사용 가능
- 각 작업에 필요한 라이브러리들만을 가져오는 작업
쇼핑몰 프로젝트의 build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.3'
}
group = 'com.hoozy'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
allprojects {
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-web-services'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.oracle.database.jdbc:ojdbc8'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
// json 파싱을 위한 gson
implementation 'com.google.code.gson:gson:2.10.1'
// log4j2
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
// 포트원 API
implementation 'com.github.iamport:iamport-rest-client-java:0.2.21'
}
tasks.named('test') {
useJUnitPlatform()
}
3. Entity 설정
- 엔티티는 테이블을 구성하는 객체 구성 성분으로, JPA에서는 객체를 개체(엔티티)로 설계하여 객체 간의 관계를 설정할 수 있다.
- 객체 간의 관계는 객체 관계 어노테이션으로 설정할 수 있으며, 외래키를 가지고 있는 엔티티는 @JoinColumn 어노테이션으로 외래키를 설정, 참조되는 테이블은 객체 관계 어노테이션의 속성인 mappedBy 속성으로 서로 관계를 설정할 수 있다.
- 초기 설정과 엔티티의 개념을 익히기가 어렵지만, 익히면 SQL 문을 일일이 작성하지 않고 자동으로 관계를 매핑시켜주기 때문에 SQL 문 의존도를 낮추고, 객체지향적 코드를 늘려 코드의 가독성도 높아지는 장점이 있다.
EntityManager
- EntityManager(EM)는 엔티티를 CRUD(저장, 조회, 수정, 삭제)를 하는 등 엔티티와 관련된 모든 일을 처리하는 매니저이다.
- 이름 그대로 엔티티를 저장하는 가상의 DB라고 보면 된다.
- 이 EM 은 EntityManagerFactory라는 곳에서 생산할 수 있는데, factory는 여러 스레드가 동시에 접근해도 안전하지만, EM은 하나의 스레드만 사용해야 한다.
- JPA 구현체들은 EntityManagerFactory를 생성할 때 커넥션 풀도 함께 만드는데 이 정보는 Application.yml에 있는 datasource이다. 이 벙조들을 통해 생성된 엔티티는 영속성 컨텍스트(Persistence Context)에 저장한다.
- Persistence Context : 엔티티를 영구 저장하는 환경
- 영속성 컨텍스트에서 관리하기 위해서 엔티티의 4가지 상태가 존재한다.
- 비영속(new / transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 삭제된 상태
- 비영속 : 엔티티 객체가 생성된 순간으로 순수한 객체 상태이며 저장하지 않은 상태이다.
- 영속 : em.persist(생성), em.merge(병합(이미 존재하는 엔티티)), em.find(조회) 하고 나서 영속성 컨텍스트가 관리하는 상태이다.
- 위의 메소드 모두 실제 DB에는 flush() 메소드로 저장된다.
- 준영속 : 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 em.detach(분리), em.close(영속성 컨텍스트 닫기), em.clear(영속성 컨텍스트 초기화) 하고 난 상태이다.
- 삭제 : em.remove(삭제)하여 엔티티를 영속성 컨텍스트와 DB에서 삭제한 상태이다.
- 엔티티 조회 과정
- em.find()로 조회한다.
- 1차 캐시에서 엔티티 식별 값에 해당하는 엔티티를 찾는다.
- 있다면 캐시 값을 조회해서 반환한다.
- 없다면 DB를 조회한다.
- 조회한 데이터로 엔티티 식별 값에 해당하는 엔티티를 생성해서 1차 캐시에 저장한다.(영속 상태)
- 조회한 엔티티를 반환한다.
- 엔티티 등록 과정
- en.persist()로 엔티티를 저장한다.
- 1차 캐시에 @Id와 엔티티를 저장한다.
- 동시에 쓰기 지연 SQL 저장소에 INSERT SQL 문을 저장한다.
- 트랜잭션을 커밋한다.
- 엔티티 매니저는 영속성 컨텍스트를 flush() 해서 영속성 컨텍스트의 내용을 DB에 동기화한다.
- 엔티티 수정
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 flush()가 호출된다.
- 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소로 보낸다.
- 쓰기 지연 저장소의 SQL을 DB로 보낸다.
- DB 트랜잭션을 커밋한다.
Users (회원)
// 회원
@Table(name = "users")
public class Users {
@Id // 기본키가 될 변수를 의미
// generator는 시퀀스 생성하는 제너레이터 이름, SEQUENCE 전략 : DB SEQUENCE를 사용해 기본 키를 할당하는 전략
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "USERS_SEQ_GENERATOR")
// 오라클은 Sequence 전략을 지원하기 때문에, 오라클 DBMS에서 sequence를 생성하고 똑같은 이름, 설정으로 sequence를 생성하는 제너레이터를 설정해야 한다.
// sequenceName : 오라클에서 생성한 sequence 이름, initialValue : 초기값, allocationSize : 호출 시 증가하는 수
@SequenceGenerator(name = "USERS_SEQ_GENERATOR", sequenceName = "USERS_SEQ", initialValue = 1, allocationSize = 1)
private Long id; // PK
@Column(nullable = false) // 이 필드명과 컬럼명이 다르다거나 특징을 명시
private String email; // 이메일
@Column(nullable = false)
private String pwd; // 비밀번호
@Enumerated(EnumType.STRING)
private Auth auth; // 권한 enum
@Column(nullable = false, name = "coupon_count")
private int couponCount; // 보유하고 있는 쿠폰 수
@OneToMany(mappedBy = "user") // 참조되는 필드명
private List<Payment> payments = new ArrayList<>(); // 결제내역에서 참조할 회원 엔티티
public Users(Long id, String email, String pwd, Auth auth) {
this.id = id;
this.email = email;
this.pwd = pwd;
this.auth = auth;
}
}
// 권한
public enum Auth {
ROLE_USER, ROLE_TEST, ROLE_ADMIN
}
- 회원의 엔티티로 결제내역 엔티티인 'payment'의 참조테이블이다.
- @GeneratedValue 어노테이션은 PK 자동 증가 전략을 설정하는 것으로, MYSQL처럼 AUTO_INCREMENT 기능을 제공하면 IDENTITY 전략으로 DB에 자동으로 맡길 수가 있다. 하지만, 오라클은 SEQUENCE 전략을 지원해 SEQUENCE 전략을 사용해야 한다.
- @SequenceGenerator 어노테이션은 SEQUENCE전략에 쓰일 SEQUENCE를 설정하는 제너레이터이다. 현재 회원 엔티티의 PK는 id는 오라클의 Sequence 전략으로 초기값이 1이고 호출할 때마다 1씩 증가하는 것으로 설정했다.
- 이때 오라클 DBMS에서도 SEQUENCE를 같은 이름으로 설정해야 한다.
- 하나의 회원은 여러 개의 결제내역을 가질 수 있고 참조테이블로 @OneToMany 어노테이션과 mappedBy 속성으로 객체 관계를 설정했다.
- 회원은 권한을 하나만 가질 수 있게 기획해서 권한인 Auth를 enum 클래스로 생성하여 ROLE_TEST, ROLE_USER, ROLE_ADMIN을 설정했다.
- @Column 어노테이션은 말그대로 현재 필드와 관계된 DB의 컬럼의 특성을 적는것이다.
- nullable : not null 유무와 같고, name : DB의 컬럼명이다.
Product (상품)
// 상품
@Table(name = "product")
@IdClass(ProductID.class) // 복합키 매핑
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "PRODUCT_SEQ_GENERATOR")
@SequenceGenerator(name = "PRODUCT_SEQ_GENERATOR", sequenceName = "PRODUCT_SEQ", initialValue = 1, allocationSize = 1)
private Long id; // PK
@Column(nullable = false)
private String title; // 상품명
@Column(nullable = false)
private int price; // 상품 가격
@Column(nullable = false)
private int stock; // 상품 재고
// 단방향
@Id // PFK는 PK임과 동시에 외래키이므로, 연관관계를 맺음과 동시에 @Id 어노테이션으로 기본키임을 명시해야한다.
// Fetch는 로딩 방식이다.
// EAGER(즉시 로딩) : 연관 테이블을 모두 Product 엔티티를 호출할 때 join문을 이용하여 한 번에 가져온다. -> 연관 테이블이 쓸모 없어도 가져온다.
// LAZY(지연 로딩) : 연관 테이블을 호출하지 않는 이상 Product 엔티티만 호출한다. 연관 테이블이 필요할 때만 호출할 수 있다.
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "img_id")
public Img img; // 이미지 엔티티 참조 필드
// 단방향
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cate_id")
private Category category; // 카테고리 엔티티 참조 필드
}
// 카테고리
@Table(name = "category")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CATEGORY_SEQ_GENERATOR")
@SequenceGenerator(name = "CATEGORY_SEQ_GENERATOR", sequenceName = "CATEGORY_SEQ", initialValue = 1, allocationSize = 1)
private Long id;
@Column(nullable = false)
private String cate; // 카테고리 명 -> API에서 가져왔다.
}
// 이미지
@Table(name = "img")
public class Img {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "IMG_SEQ_GENERATOR")
@SequenceGenerator(name = "IMG_SEQ_GENERATOR", sequenceName = "IMG_SEQ", initialValue = 1, allocationSize = 1)
private Long id;
@Column(nullable = false)
private String link; // 이미지 링크 -> API에서 가져와서 링크가 존재한다.
}
- 네이버 검색(쇼핑) API를 활용하여 가져온 상품 엔티티이며 복합키를 가진다.
- 복합키 : PK가 2개 이상을 묶어 하나의 PK로 두는 키이다.
- 회원 엔티티와 마찬가지로 PK는 SEQUENCE 전략을 활용했고, 각 필드도 컬럼에 맞춰 nullable을 false로 했다.
- 상품 엔티티는 상품의 고유 이미지 엔티티, 상품의 카테고리 엔티티의 외래키를 가지고 참조하며 LAZY 방식으로 로딩한다.
- EAGER(즉시 로딩) : 연관 테이블을 모두 현재 엔티티를 호출할 때 join문을 이용하여 한 번에 가져온다. -> 연관 테이블이 쓸모 없어도 가져온다.
- LAZY(지연 로딩) : 연관 테이블을 호출하지 않는 이상 현재 엔티티만 호출한다. 연관 테이블이 필요할 때만 호출할 수 있다.
- 이미지 링크는 현재 상품만이 가지는 유일한 링크로 이미지 엔티티와 식별관계로 이루어져 있어서 외래키인 img_id를 PKF로 가진다.
- 엔티티는 PFK를 가지면 PK가 2개 이상이기 때문에 각 PK마다 @Id 어노테이션으로 PK를 나타내야 한다.
- 카테고리 엔티티는 API에서 가져와서 중복 없이 넣어서 사용하는 카테고리이다.
- 이미지 엔티티는 API에서 가져와서 링크를 저장해서 사용하는 이미지이다.
ProductID (상품 복합키)
@Builder
public class ProductID implements Serializable {
private Long id;
private Long img; // PFK여서 테이블을 참조하므로 참조하는 테이블의 PK타입과 참조하는 필드명으로 적어야한다.
public static ProductID toProductID(long id) {
return ProductID.builder()
.id(Long.valueOf(id))
.img(Long.valueOf(id))
.build();
}
}
- JPA에서 복합키를 구성하는 방법은 아래 2가지가 있다.
// 1. @IdClass
// 복합키 클래스
public class EntityID implements Serializable {
private Long id;
private Long tableId;
}
// 엔티티
@IdClass(EntityID.class)
public class Entity {
@Id
private Long id;
@Id
@Column(name = "table_id")
private Long tableId;
}
// 2. @EmbeddedId
// 복합키 클래스
@Embeddable
public class EntityID implements Serializable {
private Long id;
@Column(name = "table_id")
private Long tableId;
}
// 엔티티
public class Entity {
@EmbeddedId
private EntityID entityId;
}
- IdClass 는 엔티티에 PK 필드가 바로 나와있어 사용하기 편해서 IdClass를 사용하기로 했다.
- 상품의 복합키 클래스로 ProductID 가 있고, 상품은 PK를 2개 가지지만 한개가 이미지 엔티티의 PFK여서 필드 명인 img를 참조하기 때문에 ProductID에 필드명을 img로 썼다.
Payment (결제내역)
// 결제내역
@Table(name = "payment")
public class Payment {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "PAYMENT_SEQ_GENERATOR")
@SequenceGenerator(name = "PAYMENT_SEQ_GENERATOR", sequenceName = "PAYMENT_SEQ", initialValue = 1, allocationSize = 1)
private Long id; // PK
@Column(nullable = false, name = "imp_uid")
private String impUid; // 결제 고유 아이디
@Column(nullable = false, name = "merchant_uid")
private String merchantUid; // 상품 고유 아이디(결제)
@Column(nullable = false)
private int price; // 결제 최종 가격
@Column(nullable = false)
private String method; // 결제 수단
@Column(nullable = false, name = "payment_date")
private Timestamp paymentDate; // 결제 날짜
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id") // manytomany 는 중간에 mapper를 둬서 onetomany - mapper - manytoone 으로 해야한다
private Users user; // 회원 엔티티 참조 필드
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id") // 복합키를 pk로 가지는 product여서 payment의 외래키 또한 2개여서 2개를 적어줘야한다.
@JoinColumn(name = "img_id")
private Product product; // 상품 엔티티 참조 필드
// 단방향
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "cancel_id")
private PayCancel payCancel; // 취소 내역 엔티티 참조 필드
}
// 취소내역
@Table(name = "pay_cancel")
public class PayCancel {
@Id
private Long id; // 1이면 취소, 0이면 취소안함
@Column(nullable = false)
private int status;
}
- 결제내역을 담을 엔티티이다.
- 결제를 이상 없이 완료하게 되면 내역을 담아 DB에 저장할 엔티티이다. 결제한 회원, 결제된 상품, 만약 취소했다면 취소여부를 참조하여 저장한다.
- 결제 고유 아이디와 상품 고유 아이디는 결제를 하기 위한 필수요소로 둘 다 고유해야하며 따로 결제 고유 아이디는 PG 사에서 제공해주고, 상품 고유 아이디는 결제 요청할 때 생성해서 요청한다.
- 취소 내역 엔티티는 id가 1이면 취소완료, 0이면 취소안하고 결제가 성공된 상태로 설정했다.
Cart (장바구니)
@Table(name = "cart")
public class Cart {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CART_SEQ_GENERATOR")
@SequenceGenerator(name = "CART_SEQ_GENERATOR", sequenceName = "CART_SEQ", initialValue = 1, allocationSize = 1)
private Long id; // PK
@Column(nullable = false)
private int count; // 구매하는 개수
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id") // manytomany 는 중간에 mapper를 둬서 onetomany - mapper - manytoone 으로 해야한다
private Users user; // 회원 엔티티 참조 필드
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id") // 복합키를 pk로 가지는 product여서 payment의 외래키 또한 2개여서 2개를 적어줘야한다.
@JoinColumn(name = "img_id")
private Product product; // 상품 엔티티 참조 필드
}
- 장바구니 엔티티로 상세보기 페이지에서 장바구니에 넣고, 장바구니 페이지에서 사용되는 엔티티이다
- 장바구니에는 회원마다 6개까지 담을 수 있고, 상품당 3개까지 구매가 가능하다.
참고 자료
https://needjarvis.tistory.com/590
https://sweets1327.tistory.com/60
'Spring Boot (프로젝트)' 카테고리의 다른 글
[쇼핑몰 프로젝트] 3. ERD 기획 (2) | 2023.11.06 |
---|---|
[쇼핑몰 프로젝트] 2. 리액트 화면 구현 (0) | 2023.10.29 |
[쇼핑몰 프로젝트] 1. 페이지 기획(feat. 피그마) (0) | 2023.10.28 |
[쇼핑몰 프로젝트]PG 시스템을 공부하기 위한 쇼핑몰 토이 프로젝트 기획 (0) | 2023.10.28 |
[스프링 부트] 프로젝트 6 S3 백엔드 구현 (0) | 2023.04.17 |
댓글