본문 바로가기
Spring Boot (프로젝트)

[쇼핑몰 프로젝트] 4. Spring Boot, 엔티티 기본 설정

by Hoozy 2023. 11. 6.
  • 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들을 자동화 시켜주는 빌드 도구이다.
    1. Compile
    2. Test
    3. Packaging
    4. Deploy / Run
  • gradle의 장점
    1. 직관적인 코드와 자동완성
    2. 다양한 Repository 사용 가능
    3. 각 작업에 필요한 라이브러리들만을 가져오는 작업

쇼핑몰 프로젝트의 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가지 상태가 존재한다.
    1. 비영속(new / transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
    2. 영속(managed) : 영속성 컨텍스트에 저장된 상태
    3. 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
    4. 삭제(removed) : 삭제된 상태

자바 ORM 표준 JPA 프로그래밍 p.93

  • 비영속 : 엔티티 객체가 생성된 순간으로 순수한 객체 상태이며 저장하지 않은 상태이다.
  • 영속 : em.persist(생성), em.merge(병합(이미 존재하는 엔티티)), em.find(조회) 하고 나서 영속성 컨텍스트가 관리하는 상태이다.
    • 위의 메소드 모두 실제 DB에는 flush() 메소드로 저장된다.
  • 준영속 : 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 em.detach(분리), em.close(영속성 컨텍스트 닫기), em.clear(영속성 컨텍스트 초기화) 하고 난 상태이다.
  • 삭제 : em.remove(삭제)하여 엔티티를 영속성 컨텍스트와 DB에서 삭제한 상태이다.
  • 엔티티 조회 과정
    1. em.find()로 조회한다.
    2. 1차 캐시에서 엔티티 식별 값에 해당하는 엔티티를 찾는다.
    3. 있다면 캐시 값을 조회해서 반환한다.
      • 없다면 DB를 조회한다.
    4. 조회한 데이터로 엔티티 식별 값에 해당하는 엔티티를 생성해서 1차 캐시에 저장한다.(영속 상태)
    5. 조회한 엔티티를 반환한다.
  • 엔티티 등록 과정
    1. en.persist()로 엔티티를 저장한다.
    2. 1차 캐시에 @Id와 엔티티를 저장한다.
      • 동시에 쓰기 지연 SQL 저장소에 INSERT SQL 문을 저장한다.
    3. 트랜잭션을 커밋한다.
    4. 엔티티 매니저는 영속성 컨텍스트를 flush() 해서 영속성 컨텍스트의 내용을 DB에 동기화한다.
  • 엔티티 수정
    1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 flush()가 호출된다.
    2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
    3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소로 보낸다.
    4. 쓰기 지연 저장소의 SQL을 DB로 보낸다.
    5. 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

댓글