본문 바로가기
Java/Spring Boot

JPA

by curious week 2025. 3. 31.

 

1. @Entity, @Table → CREATE TABLE

@Entity
@Table(name = "users")
public class User {
    ...
}
CREATE TABLE users (
  ...
);
  • @Entity가 붙으면 해당 클래스는 DB 테이블로 변환
  • @Table(name = "...") 없으면 → 클래스명이 소문자로 변환됨 (User → user)

2. @Id, @GeneratedValue → PRIMARY KEY, AUTO_INCREMENT

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
id BIGINT PRIMARY KEY AUTO_INCREMENT

 

IDENTITY AUTO_INCREMENT (MySQL), SERIAL (PostgreSQL)
SEQUENCE CREATE SEQUENCE ... + DEFAULT nextval(...)
AUTO 벤더 따라 자동 결정

3. @Column → 각 컬럼 제약 조건

@Column(nullable = false, unique = true, length = 100)
private String email;
email VARCHAR(100) NOT NULL UNIQUE

옵션 SQL 문 의미

nullable = false NOT NULL 제약 조건
unique = true UNIQUE 제약 조건
length = 100 VARCHAR(100)
name = "email" 컬럼명 강제 지정 (생략 시 필드명)

4. @Enumerated → 문자열 vs 숫자 저장

@Enumerated(EnumType.STRING)
private Role role;
role VARCHAR(255) -- 예: 'ADMIN', 'USER'
  • EnumType.STRING: 문자열 그대로 저장 (실무에서 추천)
  • EnumType.ORDINAL: 숫자 인덱스 저장 → 위험 (enum 순서 바뀌면 오류 발생)

5. @Lob, @Column(columnDefinition = "...")

@Lob
private String content;

@Column(columnDefinition = "TEXT")
private String description;
content TEXT,
description TEXT
  • @Lob은 TEXT 또는 BLOB 타입으로 변환됨 (255자 이상을 사용할 때는 사용을 고려할 것)
  • columnDefinition을 쓰면 직접 SQL 타입을 명시할 수 있음

6. @Transient → DB에 컬럼 생성 안 됨

@Transient
private String tempData;

이 필드는 CREATE TABLE 쿼리에서 제외됨.


예시: JPA 엔티티 → SQL 테이블

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 100)
    private String email;

    @Enumerated(EnumType.STRING)
    private Role role;

    @Lob
    private String bio;

    @Transient
    private String tempToken;
}
CREATE TABLE users (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  email VARCHAR(100) NOT NULL UNIQUE,
  role VARCHAR(255),
  bio TEXT
);
-- tempToken은 저장되지 않음

 


관계 매핑 어노테이션 (SQL FOREIGN KEY 대응)


@ManyToOne + @JoinColumn → FOREIGN KEY 생성

@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
user_id BIGINT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)

어노테이션 SQL 의미

@ManyToOne 다대일 관계
@JoinColumn 외래키 컬럼명 + 제약조건 지정
nullable = false NOT NULL 제약 조건

  • 지연 로딩 (fetch = LAZY) 기본 설정 강력 추천!
  • 외래키 필드는 기본적으로 인덱스를 자동 생성함

@OneToMany(mappedBy = ...) → 반대방향 참조 (조인 테이블 없음)

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Post> posts;
  • mappedBy = "user"는 Post 엔티티에서 외래키가 있음을 의미
  • 이쪽에서는 외래키 컬럼이 없음 (SQL로는 표현되지 않음)

→ 이건 양방향 관계에서 읽기용으로만 사용돼.

 

  • @JoinColumn → 연결의 주인 (DB에 FK 생성)
  • mappedBy = "user" → 상대편 필드명 (내가 DB 제어는 안 해)

 


@OneToOne → 유니크 외래키

@OneToOne
@JoinColumn(name = "profile_id", unique = true)
private Profile profile;
profile_id BIGINT UNIQUE,
FOREIGN KEY (profile_id) REFERENCES profiles(id)
  • unique = true가 붙으면 1:1 관계를 DB에서도 강제

@ManyToMany → 조인 테이블 자동 생성

@ManyToMany
@JoinTable(
  name = "user_roles",
  joinColumns = @JoinColumn(name = "user_id"),
  inverseJoinColumns = @JoinColumn(name = "role_id")
)
private List<Role> roles;
CREATE TABLE user_roles (
  user_id BIGINT,
  role_id BIGINT,
  PRIMARY KEY(user_id, role_id),
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (role_id) REFERENCES roles(id)
);
  • 다대다 관계에서는 중간 테이블을 자동 생성
  • 실무에선 거의 사용 ❌ → 대신 엔티티로 중간 테이블 분리 추천

Fetch 전략

LAZY (지연 로딩) 객체를 사용할 때 쿼리 실행 (실무 기본)
EAGER (즉시 로딩) 객체를 불러올 때 자동으로 JOIN 실행됨 (주의!)
@ManyToOne(fetch = FetchType.LAZY)

EAGER는 예기치 않은 N+1 문제성능 저하 원인이 되므로 실무에서 LAZY 기본 설정이 좋다.


Cascade 옵션 (연관된 객체까지 함께 처리)

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
PERSIST 저장 시 연관 객체도 저장
REMOVE 삭제 시 연관 객체도 삭제
ALL 모두 적용 (persist + merge + remove 등)
DETACH, MERGE 특정 상황에서 사용됨

실무에선 CascadeType.ALL + orphanRemoval = true 조합으로
"내가 삭제하면 관련 데이터도 자동 정리"되도록 많이 사용


정리: User → Post

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

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
}

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

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Post> posts = new ArrayList<>();
}
-- Post 테이블에만 FK 존재
user_id BIGINT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)

3. 임베디드/값 타입 관련 어노테이션


@Embeddable + @Embedded → 복합 타입 한 덩어리로 매핑

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

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

  @Embedded
  private Address address;
}
CREATE TABLE user (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  city VARCHAR(255),
  street VARCHAR(255),
  zipcode VARCHAR(255)
)
  • Address 필드 3개가 그대로 User 테이블에 포함됨
  • 코드상에선 한 덩어리 객체로 관리 가능 → 재사용성 굿 👍

@Embedded + @AttributeOverrides → 컬럼명 변경

@Embedded
@AttributeOverrides({
  @AttributeOverride(name = "city", column = @Column(name = "home_city")),
  @AttributeOverride(name = "street", column = @Column(name = "home_street")),
})
private Address homeAddress;
  • 같은 @Embeddable 클래스를 여러 필드에 사용할 때 컬럼명 중복을 방지

4. @ElementCollection → 값 타입 리스트 (JPA 전용)

@ElementCollection
@CollectionTable(name = "user_favorites", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "favorite")
private List<String> favorites;
CREATE TABLE user_favorites (
  user_id BIGINT,
  favorite VARCHAR(255)
)
  • 엔티티가 아닌 문자열, 숫자 같은 값 타입 리스트를 저장할 수 있음
  • 실제로는 JPA가 별도 테이블 만들어 관리함
  • 단점: 조회만 좋고, 변경은 비효율적 (delete 후 insert)

5. 엔티티 생명주기 콜백

@PrePersist INSERT 직전
@PostPersist INSERT 직후
@PreUpdate UPDATE 직전
@PostUpdate UPDATE 직후
@PreRemove DELETE 직전
@PostRemove DELETE 직후
@PostLoad 엔티티 조회 후
@PrePersist
public void setCreatedAt() {
    this.createdAt = LocalDateTime.now();
}
  • 실무에서 가장 많이 쓰는 건 @PrePersist로 createdAt, updatedAt 설정!

6. @Access(AccessType.FIELD | PROPERTY)

@Access(AccessType.FIELD)
private String name;
  • JPA가 필드에 직접 접근할지, getter/setter 통해 접근할지 설정
  • 거의 안 쓰지만, 특정 필드만 설정 다르게 하고 싶을 때 유용함

잘 쓰면 좋은 상황

어노테이션 언제 쓰면 좋은가?

@Embeddable 주소, 이름 등 묶어서 쓰고 싶을 때
@ElementCollection 취미, 태그 같은 값만 갖는 리스트
@PrePersist 등 createdAt, updatedAt 자동처리
@AttributeOverrides 동일 타입을 여러 번 쓸 때

 

4. N+1 문제란?

정의

"한 번의 쿼리를 날렸는데, 연관된 엔티티 때문에 N번 추가 쿼리가 발생하는 문제"


예시

List<User> users = userRepository.findAll(); // 사용자 목록 조회

for (User user : users) {
    System.out.println(user.getPosts().size()); // 각각의 게시글 접근
}

실행된 SQL

SELECT * FROM users;             -- 1번
SELECT * FROM posts WHERE user_id = 1; -- N번
SELECT * FROM posts WHERE user_id = 2;
...

유저가 100명이면 → 총 1 + 100 = 101번 쿼리 발생


원인

  • 연관관계의 기본 설정은 fetch = FetchType.LAZY
  • 즉시 불러오지 않고 필요할 때 쿼리 날림
  • 루프 돌며 연관 필드 접근 시 쿼리 N번

해결 방법 1: @EntityGraph (간단한 경우)

@EntityGraph(attributePaths = "posts")
List<User> findAll(); // posts를 함께 join fetch함

→ 자동으로 JOIN FETCH 처리됨


해결 방법 2: JPQL + join fetch

@Query("SELECT u FROM User u JOIN FETCH u.posts")
List<User> findAllWithPosts();

결과

SELECT u.*, p.* 
FROM users u 
JOIN posts p ON p.user_id = u.id
  • 한 번에 조인해서 가져옴 → N+1 제거

해결 방법 3: QueryDSL

List<User> users = queryFactory
  .selectFrom(user)
  .leftJoin(user.posts, post).fetchJoin()
  .fetch();
  • fetchJoin()이 핵심!
  • 실무에서 복잡한 조건, 동적 쿼리 처리에 매우 유리

해결 방법 4: DTO Projection으로 직접 매핑

@Query("SELECT new com.example.dto.UserDto(u.id, u.name, COUNT(p)) " +
       "FROM User u LEFT JOIN u.posts p GROUP BY u")
List<UserDto> fetchUserSummary();
  • 조회 성능 극대화
  • 단점: DTO 전용 생성자 필요

실무 추천 전략

단순 조회 @EntityGraph, join fetch
동적 조건 많음 QueryDSL
트래픽 많고 조회 전용 DTO Projection
조건부로 fetch 하고 싶음 QueryDSL + where 조건

⚠️ 주의사항: 컬렉션 페치 조인 제한

  • @OneToMany 같은 컬렉션에 join fetch 쓰면 → 페이징 불가
// ⚠️ 이건 페이징 안 됨!
SELECT u FROM User u JOIN FETCH u.posts
  • 해결: Batch Size or @OneToMany 대신 DTO 조회

batch-size 설정 예시

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100

→ 지연 로딩에서도 in 쿼리로 묶어서 최적화

'Java > Spring Boot' 카테고리의 다른 글

Optional<T>  (0) 2025.03.31
JPQL과 QueryDSL  (0) 2025.03.31
OAuth 구현  (0) 2025.03.28
Redis, kafka  (0) 2025.03.27
HTTP 상태 코드 종류  (0) 2025.03.27