#12 - JPA 실무 가이드 1부 — 입문자 기준 쉽게 이해하기(30분 JPA 정복)
중년개발자
@loxo
약 8시간 전
JPA 실무 가이드 1부 — 입문자 기준 쉽게 이해하기
SQL을 직접 작성하던 시절, 개발자는 항상 DB 구조에 맞춰 움직여야 했습니다. 그래서 JPA는 복잡하고 어려운 기술처럼 느껴질 수 있습니다. 하지만 실무에서 제대로 사용해보면, JPA는 객체 중심으로 개발을 단순화해주는 도구입니다. 이 문서는 불필요한 이론을 걷어내고, 실무에 꼭 필요한 핵심만 정리한 30분 요약 가이드입니다.
0. JPA가 무엇인가요?
JPA (Java Persistence API, 자바 영속성 API)는
자바 객체(Object)를 데이터베이스(DB)에 저장하고 관리하는 기술입니다.
조금 더 쉽게 말하면,
- 우리가 객체(Object)를 만들고
- 값을 바꾸면
- JPA가 알아서 SQL을 만들어 DB에 반영해 줍니다.
여기서 중요한 개념이 바로
- ORM (Object Relational Mapping, 객체-관계 매핑)
입니다.
객체(Object)와 테이블(Table)을 연결해주는 기술이 ORM입니다.
1. 엔티티(Entity, 테이블과 매핑되는 클래스)
엔티티(Entity)는
데이터베이스 테이블과 1:1로 연결되는 자바 클래스입니다.
예시:
@Entity
@Getter
@Table(name = "table_name") // 1. 테이블명 소문자 명시
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 2. 기본 생성자 막기
public class DefaultEntity extends BaseEntity { // 3. BaseEntity 상속
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "id")
private Long id;
@Column(nullable = false)
private String name;
// 4. @Builder: 생성자 대신 빌더 사용
@Builder
public DefaultEntity(String name) {
this.name = name;
}
// 5. 비즈니스 로직: Setter 대신 메서드
public void changeName(String name) {
this.name = name;
}
}여기서 중요한 개념들:
- @Entity → 이 클래스는 테이블과 연결됨
- @Id → 기본 키(Primary Key, PK)
- @GeneratedValue → PK 자동 생성 전략
1-1. 왜 @Setter를 지양(권장하지 않음)할까요?
가장 많이 실수 하는것이 바로 @Setter입니다.
하지만 엔티티(Entity)에 무분별하게 @Setter를 열어두면 문제가 생깁니다.
예를 들어:
order.setPrice(1000);이 코드는 단순히 "값을 바꿨다"는 것만 보여줍니다.
나중에 이런 질문이 생깁니다.
- 언제 가격이 바뀌었지?
- 왜 바뀌었지?
- 할인인가?
- 환불인가?
의도가 코드에 드러나지 않습니다.
❌ 좋지 않은 예
order.setStatus(CANCEL); // 그냥 값 변경⭕ 좋은 예
order.cancel(); // "주문 취소"라는 행위가 명확함이처럼
엔티티는 데이터 덩어리(Data Holder)가 아니라
"행위(Behavior)를 가진 객체"로 설계해야 합니다.
이 방식의 장점:
- 비즈니스 규칙을 메서드 안에 넣을 수 있음
- 잘못된 상태 변경을 막을 수 있음
- Dirty Checking(변경 감지) 의도가 명확해짐
따라서 실무에서는:
- 엔티티에는 무분별한 @Setter 사용을 피하고
- 의미 있는 메서드로 상태를 변경하도록 설계합니다.
2. 기본 키 전략 (Primary Key Strategy, 기본 키 생성 방식)
PostgreSQL에서는 보통 두 가지 전략을 많이 사용합니다.
2-1. SEQUENCE (시퀀스 기반 생성)
@GeneratedValue(strategy = GenerationType.SEQUENCE)특징:
- DB의 시퀀스(Sequence, 번호 생성기)를 사용
- 여러 건을 한 번에 저장할 때(batch insert, 일괄 저장) 성능이 좋음
- 실무에서 가장 많이 사용됨
2-2. IDENTITY (자동 증가 컬럼)
@GeneratedValue(strategy = GenerationType.IDENTITY)특징:
- DB가 insert 할 때 자동으로 번호 생성
- 단순하지만 batch insert가 어려움
📌 정리
PostgreSQL 실무에서는 보통
SEQUENCE 전략을 더 많이 사용합니다.
3. 숫자 PK는 보안상 위험한가요?
많이 오해하는 부분입니다.
문제는:
- PK가 숫자라서가 아니라
- 그 숫자를 외부 API에 그대로 노출하기 때문입니다.
해결 방법:
- 내부 PK → Long + SEQUENCE
- 외부 노출 ID → UUID (Universally Unique Identifier, 범용 고유 식별자)
즉, 보안은 전략 문제가 아니라 설계 문제입니다.
4. UUID (범용 고유 식별자)
Hibernate 6에서는
@UuidGenerator(style = UuidGenerator.Style.TIME)처럼 시간 기반 UUID (time-ordered UUID, 시간 순 정렬 UUID)를 지원합니다.
UUID v4는 완전 랜덤(Random)이라
- 인덱스(Index, 색인)
- B-Tree 구조
에서 성능이 조금 떨어질 수 있습니다.
그래서 최근에는 시간 순 UUID를 더 선호합니다.
5. Dirty Checking (변경 감지)
JPA의 가장 중요한 개념입니다.
Dirty Checking(변경 감지)이란:
객체의 값이 바뀌면 자동으로 UPDATE SQL을 생성하는 기능
예시:
@Transactional
public void changePrice(Long id) {
Order order = repository.findById(id).orElseThrow();
order.changePrice(1000);
}- 트랜잭션(Transaction, 작업 단위) 시작
- 엔티티(Entity) 조회
- 값 변경
- commit 시점에 자동 UPDATE 실행
이때 내부적으로
- 영속성 컨텍스트(Persistence Context, 엔티티 관리 공간)
- 스냅샷(Snapshot, 초기 상태 저장)
을 사용해 변경 여부를 판단합니다.
6. save()는 언제 필요할까요?
Spring Data JPA에서
repository.save(entity);동작 방식:
- id == null → persist (신규 저장)
- id != null → merge (병합)
💡 INSERT는 왜 save()를 해야 하나요?
Dirty Checking은 "이미 있는 것(Managed)"이 "바뀌었는지(Dirty)" 검사하는 기능입니다.
새로 만든 객체(new Order())는 아직 JPA가 모르는 녀석(Transient)입니다.
그래서 repository.save()를 호출해서 "이것도 관리해줘(Persist)"라고 알려줘야 합니다.
📌 핵심 정리
- 새로 만든 객체는 save() 필요
- 이미 조회한 객체는 save() 없이도 변경 반영
7. Auditing (자동 시간 기록)
생성일(createdAt)과 수정일(updatedAt)을 자동으로 넣어주는 기능입니다.
7-1. 공통 부모 클래스(BaseEntity) 만들기
@Getter
@MappedSuperclass // "나는 테이블은 아니고, 자식들에게 필드만 물려줄게"
@EntityListeners(AuditingEntityListener.class) // "누가 건드리는지 감시해줘"
public abstract class BaseEntity {
@CreatedDate // 생성될 때 시간 자동 저장
@Column(updatable = false) // 수정 불가 (태어난 시간은 안 바뀌니까)
private LocalDateTime createdAt;
@LastModifiedDate // 변경될 때마다 시간 자동 갱신 (Dirty Checking 시점)
private LocalDateTime updatedAt;
}설명:
- @MappedSuperclass → 테이블로 생성되지 않고, 필드만 상속됨
- @EntityListeners → Auditing 기능 활성화
- @CreatedDate → INSERT 시 자동 입력
- @LastModifiedDate → UPDATE 시 자동 갱신
7-2. 엔티티에서 상속받기
@Entity
public class Order extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private int price;
}이제 Order를 저장하거나 수정하면
createdAt / updatedAt이 자동으로 들어갑니다.
7-3. 반드시 필요한 설정
① 메인 애플리케이션 클래스
@SpringBootApplication
@EnableJpaAuditing
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}② 의존성 확인 (Gradle 예시)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}7-4. 동작 예시
@Transactional
public void createOrder() {
Order order = new Order();
repository.save(order);
}저장 후 DB를 보면:
- created_at → 현재 시간 자동 입력
- updated_at → 현재 시간 자동 입력
수정하면:
- updated_at → 자동 갱신
이것이 Auditing(자동 기록) 기능입니다.
8. @Transactional (트랜잭션 관리)
@Transactional의 의미:
이 메서드 안의 작업은 하나의 묶음으로 처리된다.
기본 동작:
- RuntimeException → rollback (되돌림)
- Checked Exception → 기본적으로 rollback 안 됨
9. 입문자 기준 최종 정리
실무에서 가장 안전한 기본 구조:
- PK → Long + SEQUENCE, UUID
- 외부 노출 ID → UUID 별도 사용
- 엔티티는 setter 대신 의미 있는 메서드 사용
- 트랜잭션은 Service 레이어에서 관리
- Dirty Checking 이해가 가장 중요
📚 참고 공식 문서 (Reference URLs)
-
Jakarta Persistence (JPA) Specification
https://jakarta.ee/specifications/persistence/ -
Hibernate ORM 공식 문서
https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html -
Spring Data JPA Reference
https://docs.spring.io/spring-data/jpa/reference/ -
PostgreSQL Documentation
https://www.postgresql.org/docs/ -
UUID RFC (RFC 4122)
https://www.rfc-editor.org/rfc/rfc4122