oguri's garage

DB부터 시작하는 JPA Entity 매핑 정리 본문

개발하다/Spring

DB부터 시작하는 JPA Entity 매핑 정리

oguri 2025. 10. 20. 19:40

 

 

 

레거시 프로젝트에서 이미 만들어진 DB에 Entity를 매핑하는 작업을 했다.
Entity를 먼저 작성하고 DB와 안 맞아서 삽질한 경험이 있어서, 이번엔 DB를 먼저 확인하고 매핑하는 방식으로 진행했다.

 

 

 




DB 테이블 확인부터 시작

테이블 정보 확인

DB 테이블을 확인할 때 주로 사용한 쿼리들이다.

-- 테이블 기본 정보 확인
DESC your_database.users;

-- 테이블 생성 DDL 확인
SHOW CREATE TABLE your_database.users;

-- 컬럼 상세 정보 조회
SELECT 
    COLUMN_NAME,
    DATA_TYPE,
    CHARACTER_MAXIMUM_LENGTH,
    IS_NULLABLE,
    COLUMN_KEY,
    COLUMN_DEFAULT,
    EXTRA
FROM INFORMATION_SCHEMA.COLUMNS 
WHERE TABLE_SCHEMA = 'your_database'
  AND TABLE_NAME = 'users'
ORDER BY ORDINAL_POSITION;

 

이 쿼리로 확인한 정보들:

  • 컬럼명: 정확한 대소문자 (DB는 대소문자 구분 안 할 수 있지만 명시적으로 확인)
  • 데이터 타입: VARCHAR, INT, DATETIME, TEXT 등
  • 길이 제약: VARCHAR(255), CHAR(1) 등
  • NULL 허용 여부: NOT NULL 제약조건
  • 기본값: DEFAULT 값 설정 여부
  • Primary Key: PK 컬럼 확인
  • Auto Increment: ID 자동 생성 여부



Primary Key 생성 전략

MySQL은 AUTO_INCREMENT, Oracle/PostgreSQL은 SEQUENCE를 주로 사용한다.

-- AUTO_INCREMENT 확인
SHOW CREATE TABLE your_database.users;

-- 결과 예시:
-- PRIMARY KEY (`user_id`),
-- `user_id` bigint(20) NOT NULL AUTO_INCREMENT

 

DB별 전략:

  • AUTO_INCREMENT 여부 → GenerationType.IDENTITY 사용
  • Sequence 사용 여부 → GenerationType.SEQUENCE 사용
  • 복합키 여부 → @EmbeddedId 또는 @IdClass 사용
  • UUID/문자열 PK → 수동 할당 방식

 

 

제약 조건

-- Foreign Key 확인
SELECT 
    CONSTRAINT_NAME,
    COLUMN_NAME,
    REFERENCED_TABLE_NAME,
    REFERENCED_COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE 
WHERE TABLE_SCHEMA = 'your_database'
  AND TABLE_NAME = 'users'
  AND REFERENCED_TABLE_NAME IS NOT NULL;

-- Unique 제약 확인
SELECT 
    CONSTRAINT_NAME,
    CONSTRAINT_TYPE
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS 
WHERE TABLE_SCHEMA = 'your_database'
  AND TABLE_NAME = 'users';

 

 

 

 




필수 어노테이션

JPA Entity는 @Entity와 기본 생성자가 필수다.

클래스 레벨 어노테이션

@Entity
@Table(name = "users", schema = "your_database")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class User {
    // 필드 정의
}

 

어노테이션 정리:

  • @Entity: JPA Entity 선언 (필수)
  • @Table: 테이블명과 스키마 명시
  • @Getter: Lombok을 통한 Getter 생성
  • @NoArgsConstructor(access = AccessLevel.PROTECTED): JPA가 요구하는 기본 생성자 (외부 생성 방지)
  • @AllArgsConstructor: Builder와 함께 사용
  • @Builder: Builder 패턴 적용 (권장)



Primary Key 매핑

@Id와 @GeneratedValue로 PK를 정의한다.
AUTO, IDENTITY, SEQUENCE, TABLE 네 가지 전략이 있다.

// Case 1: MySQL AUTO_INCREMENT
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long userId;

// Case 2: Oracle/PostgreSQL SEQUENCE
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
@SequenceGenerator(name = "user_seq", sequenceName = "user_seq", allocationSize = 1)
@Column(name = "user_id")
private Long userId;

// Case 3: UUID 수동 할당
@Id
@Column(name = "user_id")
private String userId;

// Case 4: 복합 키
@EmbeddedId
private UserCompositeKey id;

 

전략 선택 기준:

  • MySQL → IDENTITY
  • Oracle, PostgreSQL → SEQUENCE 권장
  • 범용적 사용 → AUTO (하지만 명시적 지정 권장)
  • UUID/수동 할당 → @GeneratedValue 없이 사용

 

 

 




타입별 매핑

자주 사용한 타입 매핑들을 정리했다.

문자열 타입

// VARCHAR
@Column(name = "username", nullable = false, length = 100)
private String username;

// TEXT
@Column(name = "description", columnDefinition = "TEXT")
private String description;

// CHAR(1) - 플래그 필드
@Column(name = "is_active", nullable = false, columnDefinition = "CHAR(1)")
private String isActive = "Y";



숫자 타입

// INT
@Column(name = "age")
private Integer age;

// BIGINT
@Column(name = "phone_number")
private Long phoneNumber;

// DECIMAL
@Column(name = "price", precision = 10, scale = 2)
private BigDecimal price;



날짜/시간 타입

JPA 3.1부터는 LocalDate, LocalTime, LocalDateTime 등 java.time 패키지를 지원한다.

// DATETIME
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;

// DATE
@Column(name = "birth_date")
private LocalDate birthDate;

// TIMESTAMP
@Column(name = "updated_at")
private Instant updatedAt;



Boolean 타입

// TINYINT(1)
@Column(name = "is_deleted", nullable = false)
private Boolean isDeleted = false;

 

 

 




Naming Convention

Spring Boot는 Java camelCase를 DB snake_case로 자동 변환한다.
하지만 레거시 DB에서는 PascalCase 컬럼을 종종 만난다.

자동 변환

// Java: camelCase
private String userName;      
// DB: user_name (자동 변환)

// 하지만 명시하는 것을 권장
@Column(name = "user_name")
private String userName;



레거시 DB

PascalCase 컬럼은 명시적으로 매핑해야 한다.

// DB 컬럼이 PascalCase인 경우 반드시 명시
@Column(name = "UserId")
private Long userId;

@Column(name = "UserName")
private String userName;

@Column(name = "CreateDateTime")
private LocalDateTime createDateTime;

 

정리:

  • Java 필드명은 항상 camelCase를 따른다
  • DB 컬럼명은 @Column의 name 속성으로 명시한다
  • 자동 변환에 의존하지 말고 명시적으로 작성하는 것이 안전하다

 

 

 




Lombok 사용

Entity 클래스는 final이면 안 되고, 기본 생성자가 필요하다.

조회 전용 Entity

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "products", schema = "store")
public class Product {
    @Id
    @Column(name = "product_id")
    private Long productId;

    @Column(name = "product_name", nullable = false, length = 200)
    private String productName;

    @Column(name = "price", nullable = false)
    private Integer price;
}



일반 CRUD Entity

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = "orders", schema = "store")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_id")
    private Long orderId;

    @Column(name = "order_date", nullable = false)
    private LocalDateTime orderDate;

    @Column(name = "total_amount", nullable = false)
    private Integer totalAmount;

    @Column(name = "status", nullable = false, length = 20)
    private String status;
}



@Data 사용 주의

// ❌ 피해야 할 패턴
@Data
@Entity
public class BadEntity {
    // @ToString, @EqualsAndHashCode가 자동 포함되어
    // 연관관계에서 무한루프 또는 성능 문제 발생 가능
}

// ✅ 권장 패턴
@Getter
@Setter  // 필요한 경우만
@Entity
public class GoodEntity {
    // 필요한 것만 명시적으로
}



예시: 전체 과정

실제 작업했던 흐름이다.

코드는 예시 코드로 수정하였다.

Step 1: DB 테이블 확인

mysql> DESC store.products;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| product_id    | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| product_name  | varchar(200) | NO   |     | NULL    |                |
| category      | varchar(50)  | YES  |     | NULL    |                |
| price         | int(11)      | NO   |     | 0       |                |
| stock_qty     | int(11)      | NO   |     | 0       |                |
| is_available  | tinyint(1)   | NO   |     | 1       |                |
| created_at    | datetime     | NO   |     | NULL    |                |
| updated_at    | datetime     | NO   |     | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+



Step 2: Entity 작성

package com.example.store.entity;

import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;

@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Table(name = "products", schema = "store")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "product_id")
    private Long productId;

    @Column(name = "product_name", nullable = false, length = 200)
    private String productName;

    @Column(name = "category", length = 50)
    private String category;

    @Column(name = "price", nullable = false)
    private Integer price;

    @Column(name = "stock_qty", nullable = false)
    private Integer stockQty = 0;

    @Column(name = "is_available", nullable = false)
    private Boolean isAvailable = true;

    @Column(name = "created_at", nullable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at", nullable = false)
    private LocalDateTime updatedAt;
}



Step 3: Repository 작성

package com.example.store.repository;

import com.example.store.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long> {

    List<Product> findByCategory(String category);

    List<Product> findByIsAvailableTrue();

    List<Product> findByPriceBetween(Integer minPrice, Integer maxPrice);
}

 

 

 




체크리스트

작업할 때 확인하는 항목들이다.

DB 확인 사항

  • 테이블 구조 확인 (DESC 또는 SHOW CREATE TABLE)
  • 컬럼명의 정확한 대소문자 확인
  • 데이터 타입과 길이 확인
  • NULL 허용 여부 확인
  • Primary Key 확인
  • AUTO_INCREMENT 또는 SEQUENCE 여부 확인
  • 기본값(DEFAULT) 확인
  • Foreign Key 관계 확인
  • UNIQUE 제약 확인



Entity 작성 사항

  • @Entity 선언
  • @Table(name, schema) 매핑
  • @Id 선언
  • @GeneratedValue 전략 선택 (필요시)
  • 모든 필드에 @Column 명시 (권장)
  • nullable 설정
  • length 설정 (VARCHAR)
  • columnDefinition 설정 (특수 타입)
  • Java Naming Convention (camelCase) 준수
  • @NoArgsConstructor 추가
  • @Getter 추가
  • @Builder 추가 (권장)
  • @AllArgsConstructor 추가 (Builder와 함께)

 

 


 


참고 자료: