Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
Tags
- static-factory-method
- 3계층 아키텍처
- 클린 아키텍처
- 레벨1
- DevOps
- 다짐글
- 가용영역
- design-pattern
- 회고
- 프로그래머스
- 클라우드아키텍처
- 글또10기
- object-creation
- QueryDSL
- constructor
- 코딩테스트
- axios
- SpringBoot
- React
- 코엑스그랜드볼룸
- ReverseNested
- builder-pattern
- Level2
- 글또
- UserLand
- 포트앤어댑터 아키텍처
- 헥사고날 아키텍처
- 글쓰기세미나
- HashMap
- OpenSearch
Archives
- Today
- Total
oguri's garage
DB부터 시작하는 JPA Entity 매핑 정리 본문
레거시 프로젝트에서 이미 만들어진 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와 함께)
참고 자료:
'개발하다 > Spring' 카테고리의 다른 글
| Spring DTO 설계 시 기본 생성자만 두는 이유 (0) | 2025.10.22 |
|---|---|
| Spring Data JPA Native Query에서 만난 InvalidDataAccessApiUsageException (0) | 2025.10.21 |
| JPA에서 DISTINCT가 필요한 경우와 필요하지 않은 경우 (0) | 2025.10.18 |
| Spring에서 실행 시간 측정하기 (0) | 2025.10.17 |
| @ControllerAdvice와 @ExceptionHandler로 전역 예외 처리하기 (0) | 2025.10.16 |