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 |