[SpringBoot] JPA (Java Persistence API)

김선형·2025년 9월 11일

Java

목록 보기
24/27

개요

RDBMS에 데이터를 CRUD (저장/조회/수정/삭제)할 수 있도록 표준화된 Java의 ORM 프레임워크

✏️ ORM (Object-Relational Mapping)?
객체 지향 프로그래밍 언어와 데이터베이스 간에 데이터를 매핑하는 기술로, 개발자는 SQL이 아닌 객체를 사용하여 데이터베이스에 접근할 수 있다.

✏️ JDBC (Java Database Connectivity)?
Java에서 데이터베이스에 접근하기 위한 표준 API로, Java에서 SQL을 직접 실행하여 DB와 연동하고 SQL을 직접 작성한다.

JPA를 위한 DB 정보 구성

Spring Boot에서 JPA 및 데이터베이스 연결 정보는 application.yml에 설정한다.

spring:
  datasource:
    url: jdbc:h2:mem:skala-stock
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
  h2:
    console:
      enabled: true

✏️ Spring Boot는 2.0 ver.부터 기본 데이터베이스 커넥션 풀로 HikariCP를 사용한다. 세밀한 커넥션 풀 설정 필요 시 hikari 속성으로 조정 가능하다.

spring:
  datasource:
    hikari:
      maximum-pool-size: 10
      minimum-idle: 2
      connection-timeout: 30000
      idle-timeout: 600000

주요 어노테이션

@Entity

해당 클래스를 JPA가 관리하는 엔터티 (테이블)로 지정한다. Java 클래스와 데이터베이스의 테이블을 1:1로 매핑한다.
@Entity가 선언된 클래스는 JPA의 CRUD 대상이 되며, 반드시 기본 생성자 (파라미터가 없는 생성자)가 필요하다. final, abstract, interface, inner class에는 선언할 수 없다.
@entity만 선언하면 클래스 이름이 그대로 테이블명으로 매핑되나, 실제 운영에서는 테이블명과 클래스명이 다를 수 있으므로 일반적으로 @Table을 함께 사용해서 명시적으로 테이블명을 지정하는 것이 안전하다.

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "stocks")
public class Stock {
    @Id
    private String ticker;
    
    private String name;
    
    private String market;
}

@Table

@Entity로 지정한 클래스가 어느 DB 테이블과 매핑될지 명시한다. 주로 테이블 이름이 클래스명과 다를 때 사용하거나, 스키마 지정 등 추가 설정이 필요할 때 사용한다.

속성

  • name: 매핑할 DB 테이블 이름을 지정한다.
  • schema: 사용할 DB 스키마를 지정한다. (DBMS가 지원하는 경우에만 해당)
  • catalog: DB 카탈로그를 지정한다. (거의 사용하지 않는다.)
  • uniqueConstraints: Unique 제약 조건을 지정한다.
@Entity
@Table(
    name = "stocks",
    uniqueConstraints = {
        @UniqueConstraint(columnNames = {"ticker", "market"})
    }
)
public class Stock {
    @Id
    private String ticker;
    private String name;
    private String market;
}

✏️ Unique 제약 조건: 단일 컬럼 vs 테이블 단위 복합 컬럼
@Column(unique = true): 한 컬럼이 중복되지 않아야 할 때 사용한다.
@Table(uniqueConstraints = ...): 두 개 이상 컬럼 조합이 중복되지 않도록 보장하고 싶을 때 사용한다.

@Id

엔터티의 Primary Key 필드를 지정한다. JPA의 각 엔터티는 반드시 하나 이상의 @Id가 필요하다.
@GeneratedValue(strategy = GenerationType.IDENTITY)를 사용하여 DBMS에서 auto_increment로 PK를 자동 할당하도록 할 수 있다.

// 기본키가 하나인 경우
@Entity
@Table(name = "stocks")
public class Stock {
    @Id
    private String ticker;
    
    private String name;
    private String market;
}
// 자동 생성되는 숫자 기본키
@Entity
@Table(name = "transactions")
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String ticker;
    private int quantity;
    private double price;
    private LocalDateTime tradedAt;
}

주로 두 개 이상의 컬럼 조합이 고유해야 하는 경우 복합키(Composite Key)로 구성한다.
@Embeddable 클래스로 복합키 타입을 정의하고, @EmbeddedId로 엔터티에 복합키를 사용한다.

@Embeddable
public class UserStockId implements Serializable {
    private Long userId;
    private String ticker;
}

@Entity
@Table(name= "user_stocks")
public class UserStock {
    @EmbeddedId
    private UserStockId id;

    private int quantity;
}

@GeneratedValue

Primary Key 값을 자동으로 생성하는 전략을 지정할 때 사용한다. 반드시 @Id와 함께 사용한다. 데이터베이스의 auto_increment, 시퀀스, 별도 테이블 등 다양한 자동 생성 방식을 지원한다.

속성

  • strategy: PK 자동 생성 방식 지정 (AUTO, IDENTITY, SEQUENCE)
  • generator: 시퀀스/테이블 방식일 때 커스텀 생성기 이름 지정

@Column

엔터티 필드를 데이터베이스의 컬럼과 매핑할 때 사용한다.

속성

  • name: 컬럼명 지정
  • nullable: null 허용 여부 (default: true)
  • length: 문자 길이 (문자열 필드에만 사용)
  • unique: Unique 제약 조건 부여 (단일 컬럼인 경우)
  • updatable: UPDATE 쿼리에서 수정 가능 여부
  • insertable: INSERT 쿼리에서 포함 여부
  • precision, scale: 소수점 숫자 자리수 지정 (숫자형 필드에만 사용)
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long Id;

    @Column(name = "user_name", nullable = false, length = 30)
    private String name; // DB 컬럼: user_name, NOT NULL, 최대 30자

    @Column(unique = true, length = 50)
    private String email; // DB 컬럼: email, UNIQUE, 최대 50자

    @Column(length = 100)
    private String password; // 길이 100자 제한

    @Column(updatable = false)
    private LocalDateTime createdAt; //수정 불가
}
@Entity
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long Id;

    @Column(precision = 12, scale=4)
    private BigDecimal price; // 최대 99,999,999.9999
}

@Enumerated

ENUM 타입 필드를 데이터베이스에 어떻게 저장할지 지정할 때 사용한다. Java의 enum 타입은 RDB에 직접 저장할 수 없으므로, ORDINAL (숫자)나 STRING (문자열)로 변환하여 저장한다.

✏️ 실무에서는 STRING 방식을 권장한다.

public enum StockStatus {
    ACTIVE,
    INACTIVE
}

@Entity
public class Stock {
    @Id
    private String ticker;

    @Enumerated(EnumType.STRING)
    @Column(length = 10)
    private StockStatus status;
}

@Lob

대용량 데이터를 데이터베이스에 저장할 때 사용한다. 필드 타입에 따라 자동으로 CLOB (텍스트) 또는 BLOB (바이너리)으로 매핑된다.

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @Lob
    private String description; // CLOB로 매핑

    @Lob
    private byte[] profileImage; // BLOB로 매핑
}

✏️ @Lob vs Column(columnDefinition = "TEXT")
@Lob은 JPA 표준 방식으로 DB 독립적이다. 유지보수 및 확장성에 유리하다.
columnDefinition은 DB 종속정이며, 유연성이 적다.

@Transient

엔터티 필드 중에서 DB 컬럼과 매핑하지 않을 필드에 사용한다. 해당 필드는 DB 테이블에 컬럼으로 생성되지 않으며, JPA의 영속화 (저장/조회) 대상이 아님을 명시한다. 계산값, 임시 데이터, 화면 출력 전용 속성 등에 활용한다.

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String password;

    @Transient
    private String passwordConfirm;
}

@{One/Many}To{One/Many}/@JoinColumn/@JoinTable

객체 간의 관계를 데이터베이스 테이블의 Foreign Key와 연결하는 방법을 정의한다. 다중성 (1:1, 1:N, N:1, N:M)과 방향성 (단방향, 양방향)을 조합하여 설정한다.

어노테이션관계
@ManyToOneN:1
@OneToMany1:N
@OneToOne1:1
@ManyToManyN:M
@JoinColumn외래키 지정
@JoinTable중간테이블 (N:M)
// Users.java
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "user") // mappedBy는 상대 엔터티에서 어떤 필드로 매핑되어 있는지 지정한다.
    private List<Transaction> transactions = new ArrayList<>();

    @OneToOne(mappedBy = "user")
    private UserProfile profile;

    @ManyToMany
    @JoinTable(name = "user_watchlist", joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "ticker"))
    private List<Stock> watchList = new ArrayList<>();
}
// Stocks.java
@Entity
@Table(name = "stocks")
public class Stock {
    @Id
    private String ticker;

    private String name;

    @Lob
    private String description;

    @OneToMany(mappedBy = "stock")
    private List<Transaction> transactions = new ArrayList<>();

    @ManyToMany(mappedBy = "watchList")
    private List<User> fans = new ArrayList<>();
}
// Transactions.java
@Entity
@Table(name = "transactions")
public class Transaction {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user; // 거래한 유저

    @ManyToOne
    @JoinColumn(name = "ticker")
    private Stock stock; // 거래한 주식

    private int quantity;

    @Column(precision = 15, scale = 4)
    private BigDecimal price;

    private LocalDateTime tradedAt;

    @Enumerated(EnumType.STRING)
    @Column(length = 10)
    private TransactionType type;
}
// UserProfiles.java
@Entity
@Table(name = "user_profiles")
public class UserProfile {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    @JoinColumn(name = "user_id", unique = true) // FK 컬럼 이름을 지정한다. 해당 필드가 다른 테이블의 PK를 참조함을 명시한다.
    private User user;

    @Lob
    private byte[] profileImage;
}
관계DBMLJPA 매핑
User-Transactionusers.id → transactions.user_id (1:N)User: @OneToMany(mappedBy="user")
Transaction: @ManyToOne(user_id)
Stock-Transactionstocks.ticker → transactions.ticker (N:1)Stock: @OneToMany(mappedBy="stock")
Transaction: @ManyToOne(ticker)
User-Profileusers.id → user_profiles.user_id (1:1)User: @OneToOne(mappedBy="user")
UserProfile: @OneToOne + @JoinColumn
User-Stock
(관심종목)
user_watchlist (user_id, ticker) (N:M)User: @ManyToMany + @JoinTable
Stock: @ManyToMany(mappedBy="watchList")

@Embedded/@Embeddable

@Embeddable는 값 타입 객체를 정의할 때 사용한다. 독립적인 엔터티가 아니라, 다른 엔터티에 포함되어 함께 저장되는 복합 값 객체임을 표시한다. DB에 별도 테이블이 생성되지 않고, 엔터티 테이블의 컬럼으로 들어간다.
@Embedded는 엔터티에서 값 타입, 즉 @Embeddable로 선언된 클래스를 필드로 사용할 때 설정한다. 실제로는 엔터티 테이블에 이 값 타입의 각 필드가 컬럼으로 생성된다.

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;

    // getter/setter 등
}
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @Embedded
    private Address address; // User 테이블에 city, street, zipcode 생성
}
profile
선형의 비선형적 기록 🐜

0개의 댓글