Back-end/Spring

[Spring] Spring Data JPA 어노테이션 정리 (초기화 전략,연관관계,Repository)

랑 이 2025. 11. 6. 00:37
반응형

Spring의 대표적인 ORM Spring Data JPA에 대해서 작성해보려고 합니다

 

먼저 ORM은 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 테이블 간의 데이터를 자동으로 변환해 주는 기술입니다

간단하게 말하면 DB의 테이블을 클래스 형태로 표현하고 이를 자동을 테이블에 매핑 해주는 기술이라고 보면 됩니다

 

이게 되게 편리하다고 느끼는 부분이 직접 SQL을 통해서 테이블을 정의하지 않고 클래스 형태로 정의가 가능하기 때문에 훨씬 간편하고 가독성도 되게 좋다고 느끼고 있습니다 또한 기본적인 쿼리문(CRUD)도 지원하고 쿼리메서드도 지원하고 있어 메서드의 이름을 통해서 쿼리문 역할을 할 수 있도록 구현하기도 합니다


JPA(Java Persistence API)

자바에서 객체와 관계형 데이터베이스를 매핑(ORM) 하기 위한 표준 인터페이스 입니다

객체(클래스) 와 관계형 데이터베이스에 간의 불일치를 해결하기 위한 기술이라고 볼 수 있습니다 

구성요소

- Entity: DB 테이블과 매핑되는 클래스

- EntityManager: 엔티티를 저장,수정,삭제,조회하는 역할

- EntityTransacton: 트랜잭션 관리

- Persistence Context: 영속성 컨텍스트 (엔티티 상태를 관리하는 캐시 역할)

- JPQL: 객체를 대상으로 하는 쿼리 언어 (SQL과 유사하지만 엔티티 기준)

Hibernate

자바 애플리케이션을 위한 객체 관계 매핑(ORM) 프레임워크

JPA 구현체이며 앞서 말했던 것처럼 SQL을 작성하지 않고 데이터베이스에 저장하고 조회할 수 있는 기능을 제공합니다

Spring Data JPA 

Spring 프레임워크에서 JPA(Java Persistence API)를 더 쉽고 편리하게 사용할 수 있도록 도와주는 모듈입니다 

 

개발자는 직접 SQL을 작성하는 대신 Repositroy 인터페이스를 정의하면

Spring Data JPA가 자동으로 구현 객체를 생성하고 기본적인 CRUD(Create, Read, Update, Delete) 기능을 제공합니다 

 

이는 개발자가 객체와 데이터베이스 관계에 집중하도록 하여 생산성을 높여줍니다 

 

다음으로 Spring Data JPA의 어노테이션에 대해 설명합니다


 [1] 엔티티 | 테이블 매핑

1. @Entity

해당 클래스를 영속 엔티티로 지정 

- 옵션: name(JPQL에서 사용할 엔티티명, 기본값 = 클래스명)

- 기본 생성자가 필요하며 final,enum,interface 불가능

"영속이란, JPA가 객체를 관리하는 상태를 의미"
@Entity(name = "MembmerEntity")
public class Member {...}

2. @Table

해당 엔티티가 매핑될 실제 테이블의 정보

- @Entity 위에 함께 사용함

 

옵션

name: 매핑할 테이블 이름을 지정합니다

schema: DB의 스키마를 지정합니다 

catalog: 카탈로그를 지정합니다 (MySQL은 schema 사용)

 

uniqueConstraints: 하나 이상의 컬럼에 대해 고유(UNIQUE) 제약조건을 설정할 때 사용합니다

uniqueConstraints = {
    @UniqueConstraint(name = "UK_MEMBER_EMAIL", columnNames = {"email"})
}

- name: 제약조건 이름 (DB 제약조건 이름으로 반영)

- columnNames: 유니크 제약조건으로 적용할 컬럼 목록(하나 이상 가능)

 

복합 유니크 제약조건

@Table(
    uniqueConstraints = {
        @UniqueConstraint(
            name = "UK_MEMBER_NAME_EMAIL",
            columnNames = {"name", "email"}
        )
    }
)

name 과 email의 조합이 중복되면 DB에 오류 발생 두 컬럼이 동시에 같은 값을 가질 수 없도록 제약조건을 거는 것

복합 유니크 제약조건은 많이 사용되기 때문에 개념을 익혀 두는 게 좋을 거 같습니다

 

예) 

1. 하나의 유저는 하나의 게시글에 하나의 댓글만 작성할 수 있다

→  같은 유저가 동일한 게시글에 두 번 이상 댓글을 달 수 없음을 보장해야 하는 요구사항이 있음

  DB에서는 댓글 테이블에서 (user_id,post_id)를 복합 유니크 제약조건을 설정하여 구현할 수 있습니다

 

2. 하나의 유저는 하나의 게시글에 한 번만 좋아요를 누를 수 있다 

→ 같은 유저가 동일한 게시글에 여러 번 좋아요를 누를 수 없음을 보장해야 하는 요구사항이 있음
DB에서는 좋아요(Like) 테이블에서 (user_id, post_id)를 복합 유니크 제약조건으로 설정하여 구현할 수 있습니다

 

3. @Id

엔티티의 기본 키(Primary Key) 를 지정

- 모든 JPA 엔티티는 반드시 @Id로 기본키를 가져야 함

 

4. @GeneratedValue

기본키의 값을 자동 생성할 때 사용

- JPA 자동으로 기본키를 생성하도록 지정

 

strategy 기본키 생성 전략

전략 설명 동작 방식
AUTO (기본값) DB 방언에 따라 자동 선택 H2는 SEQUENCE, MySQL은 IDENTITY
IDENTITY DB의 AUTO_INCREMENT 사용 MySQL 등에서 insert 시 자동 증가
SEQUENCE DB의 시퀀스 객체 사용 Oracle, PostgreSQL 등에서 사용
TABLE 별도 키 생성용 테이블 사용 모든 DB에서 호환 가능 (비추천)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

주로 MySQL은 AUTO_INCREMENT를 사용하여 관리합니다

 

5. @Column

엔티티 클래스의 필드(변수)를 DB 컬럼에 매핑

- 컬럼 이름,제약조건,정밀도,DDL 정의 등 

 

자주 쓰는 옵션 정리

옵션 설명 부가 설명 / 예시
name 매핑할 컬럼 이름 지정 클래스 필드명과 DB 컬럼명이 다를 때 사용
예) @Column(name = "user_name")
nullable NOT NULL 여부 설정 (기본값 = true) nullable = false → NOT NULL 제약조건 추가
unique 컬럼 단위 유니크 제약조건 단일 컬럼만 적용됨
복합 유니크는 @Table(uniqueConstraints = …) 사용
length 문자 타입(String)의 최대 길이 지정 기본값 255
예) @Column(length = 100) → VARCHAR(100)
precision 숫자 전체 자리수 지정 (BigDecimal 전용) 예) precision = 10 → 전체 10자리
scale 소수점 자리수 지정 (BigDecimal 전용) 예) scale = 2 → 소수점 이하 2자리
insertable INSERT 시 해당 컬럼 포함 여부 제어 false로 설정 시 DB 기본값 사용 가능
updatable UPDATE 시 해당 컬럼 포함 여부 제어 예) 생성일(createDate) 등 변경 불가 필드에 updatable = false
columnDefinition DDL 직접 지정 (DB 의존적) 예) @Column(columnDefinition = "VARCHAR(20) DEFAULT 'N'")
table 세컨더리 테이블 컬럼 매핑 시 사용  
@Entity
@Table(name = "member")
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 이름: "user_name" null 허용 X 길이 50 제한
    @Column(name = "user_name", nullable = false, length = 50)
    private String name;

    // 유니크 키 설정
    @Column(unique = true)
    private String email;

    // 자리수: 10 소수점 자리수: 2
    @Column(precision = 10, scale = 2)
    private BigDecimal salary;
    
    // 컬럼 UPDATE 제외 컬럼 INSERT 제외 DDL 직접 지정 
    @Column(updatable = false, insertable = true, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
    private LocalDateTime createdAt;
}

 

6. @Lob

CLOB/BLOB 대용량 타입 지정

- 필드 타입에 따라 CLOB/BLOB 매핑 결정(String -> CLOB, byte[] BLOB)

@Lob
private String description; // → CLOB 매핑

@Lob
private byte[] fileData;    // → BLOB 매핑

 

 

7. @Enumerated

Enum 타입 필드를 DB 컬럼에 매핑

- 자바의 enum (열거형) 타입은 기본적으로 DB에서 인식되지 않아 지정해야 함

- DB에서 숫자(ORDINAL) 또는 문자열(STRING) 형태로 저장 가능

- 주로 User 테이블에서 권한 또는 삭제 여부에 사용됨

public enum Status {
    ACTIVE, INACTIVE, DELETED
}

@Entity
public class Member {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Enumerated(EnumType.STRING)
    private Status status;
}

 

9. @Transient

비영속/비매핑 필드로 지정

- DB 컬럼 매핑으로 매핑되지 않으며 JPA의 영속성 컨텍스트에서도 관리되지 않음

- 해당 필드는 엔티티 내부에서만 일시적으로 사용하는 값에 주로 사용됨

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String firstName;
    private String lastName;

    @Transient
    private String fullName;  // DB에 저장되지 않음

    public String getFullName() {
        return firstName + " " + lastName;
    }
}

 

9. @Version

낙관적 락(Optimistic Lock)을 구현하기 위한 버전 관리 컬럼 지정

- JPA가 제공하는 동시성 제어(Concurrency Control) 기능

- 한 엔티티를 여러 트랜잭션이 동시에 수정할 때 마지막 커밋된 변경만 반영되도록 하여 데이터 무결성을 보장

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Version
    private int version;  // 낙관적 락 버전 컬럼
}

낙관적 락(Optimistic Lock)과 관련된 동작 원리는 나중에 별도로 자세히 알아보겠습니다


[2] 연관관계 매핑 

FetchType (연관관계 조회 전략)

연관된 엔티티를 언제(DB에서) 조회할지 결정하는 설정 옵션

- JPA는 엔티티 간 연관관계를 매핑할 때 연관된 엔티티를 즉시(EAGER) 또는 지연(LAZY)으로 로딩 가능

- 한 엔티티를 조회할 때 연관 객체를 함께 가져올지 필요할 때만 가져올지 결정하는 설정

 

기본 설정 규칙

관계 어노테이션 기본 FetchType 설명
@ManyToOne EAGER (즉시 로딩) 엔티티 조회 시 연관된 엔티티도 즉시 함께 조회
@OneToOne EAGER (즉시 로딩) 마찬가지로 즉시 함께 조회
@OneToMany LAZY (지연 로딩) 연관된 컬렉션은 실제 사용 시점에 조회
@ManyToMany LAZY (지연 로딩) 연결 테이블을 거치는 관계는 기본적으로 지연 로딩
일반적으로 @ManyToMany 관계는 거의 사용되지 않습니다
@ManyToMany는 중간 조인 테이블을 자동으로 생성하지만 조인 테이블에 추가 컬럼을 넣을 수 없으며 연관관계 제어(삭제,수정)가 복잡하기 때문에 중간 엔티티를 직접 만들어 각각 @ManyToOne 관계로 풀어서 사용하는 방식이 일반적입니다
모든 연관관계에 대해 지연로딩(LAZY) 방식을 사용하는 것이 좋다
- EAGER는 즉시 조회 -> N+1 문제 발생 위험이 높다
- 쿼리 제어가 어렵다 (JOIN이 자동으로 붙음)
- 서비스 로직에서 필요한 데이터만 선택적으로 가져오는 것이 더 효율적이다

 

💢 N+1 문제? 

→ 한 번의 조회 후, 연관된 데이터를 조회할 때마다 추가 쿼리가 발생하여
결과적으로 총 N+1번의 쿼리가 실행되는 성능 저하 문제를 의미합니다

 

예시 (서비스 로직 기반 N+1 문제)

// MemberService.java
@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    public void printMemberTeams() {
        // JPQL로 전체 회원 조회
        List<Member> members = memberRepository.findAllMembers();

        // 각 회원의 팀 이름 출력
        for (Member m : members) {
            System.out.println(m.getTeam().getName()); // 여기서 N+1 발생
        }
    }
}
// MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {

    @Query("SELECT m FROM Member m")  // JPQL: 단순 회원 조회
    List<Member> findAllMembers();
}

1. SELECT * FROM member; - 회원 전체 조회 (1번)

2. 각 회원이 속한 팀(m.getTeam()) 조회 시마다 (SELECT * FROM team WHERE id = ?;) 반복 실행 (N번)

3. 총 N+1번의 쿼리 발생 

 

해결 방법 (Fetch Join 사용)

@Query("SELECT m FROM Member m JOIN FETCH m.team")
List<Member> findAllMembersWithTeam();
public void printMemberTeams() {
    List<Member> members = memberRepository.findAllMembersWithTeam();

    for (Member m : members) {
        System.out.println(m.getTeam().getName()); // 추가 쿼리 발생 X
    }
}

 

실행 쿼리 (1번만 실행)

SELECT m.*, t.* 
FROM member m 
JOIN team t ON m.team_id = t.id;

Member와 Team을 한 번의 쿼리로 함께 조회하므로 N+1 문제 해결

 

N + 1 문제 해결은 성능 개선의 첫 번째라고 할 수 있는 만큼 쿼리 발생 횟수를 줄여 DB 부하를 크게 낮추는 최적화 방법입니다

성능 최적화 방법도 더 자세히 다뤄보고 싶네요

 

[공통 옵션] 

(1) cascade (영속성 전이)

부모 엔티티의 상태 변화(저장,삭제)를 자식 엔티티에도 함께 전이시키는 기능

- 한 번의 엔티티 조작으로 연관된 엔티티까지 함께 처리

- 엔티티 간 연관관계(1:N,N:1)가 있을 때 사용

 

주요 옵션

옵션 설명
CascadeType.PERSIST 부모 저장 시 자식도 함께 저장
CascadeType.REMOVE 부모 삭제 시 자식도 함께 삭제
CascadeType.MERGE 부모 병합 시 자식도 병합
CascadeType.DETACH 부모가 영속성 컨텍스트에서 분리될 때 자식도 분리
CascadeType.REFRESH 부모 새로고침 시 자식도 새로고침
CascadeType.ALL 위 모든 옵션을 포함 (실무에서 자주 사용)

 

(2) optional

연관관계의 필수 여부(Null 허용 여부)를 설정하는 옵션

- @ManyToOne,@OneToOne 관계에서 사용

- 기본값은 True → 연관된 엔티티가 없어도(null) 저장 가능

 

(3) orphanRemoval

고아 객체 자동삭제 기능 옵션 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동 삭제

- 부모 엔티티에서 자식 엔티티의 참조가 제거된 상태를 "고아 상태" 라고 함

- @OneToOne,@OneToMany 관계에서 사용

 

1. @ManyToOne

다대일(N:1) 관계 매핑 어노테이션

- 여러 엔티티가 하나의 엔티티를 참조할 때 사용

- 관계형 데이터베이스는 N 쪽 테이블이 외래 키(FK)를 가짐

- 해당 어노테이션이 선언된 엔티티가 연관관계의 주인(Owner)이 됨

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "team_id", nullable = false)
private Team team;

 

2. @OneToMany

일대다(1:N) 관계 매핑 어노테이션 

- 하나의 엔티티가 여러 엔티티를 참조할 때 사용

- mappedBy 속성으로 연관관계의 주인을 지정해야 합니다

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;

    // 하나의 팀에 여러 멤버
    @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Member> members = new ArrayList<>();
}
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id") // FK 위치 → Member 테이블
    private Team team;
}

 

3. @OneToOne

일대일(1:1) 관계 매핑 어노테이션 하나의 엔티티가 하나의 엔티티만 참조할 때 사용

- 양쪽 엔티티 중에서 접근 빈도가 높은 엔티티가 외래 키(FK)를 가짐

@Entity
public class User {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "profile_id") // 외래키 (FK) 소유
    private Profile profile;
}
@Entity
public class Profile {

    @Id @GeneratedValue
    private Long id;

    private String bio;

    @OneToOne(mappedBy = "profile") // 비주인 (읽기 전용)
    private User user;
}

 

4. @JoinColumn

외래 키(FK) 컬럼을 지정할 때 사용하는 어노테이션

- 외래 키(FK)를 가지는 엔티티 필드에 선언하여 사용합니다

 

주요 속성

속성 설명  기본값
name 외래 키 컬럼명 참조필드명_참조PK명
referencedColumnName 참조 대상 엔티티의 컬럼명 (기본키가 아닐 경우 지정) 참조 엔티티의 PK
nullable NOT NULL 제약조건 여부 true
unique UNIQUE 제약조건 여부 (1:1 관계 시 자주 사용) false
insertable, updatable 쓰기 가능 여부 (읽기 전용 컬럼 만들 때 사용) true
foreignKey FK 제약조건 이름 지정 (@ForeignKey 객체 사용) DB 자동 생성 이름

 

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(
        name = "team_id",                // 외래 키 컬럼명
        nullable = false,                // NOT NULL
        foreignKey = @ForeignKey(name = "fk_member_team") // FK 이름 지정
    )
    private Team team;
}

 

5. @JoinColumns

복합 외래 키를 매핑할 때 사용하는 어노테이션

- 두 개 이상의 컬럼으로 이루어진 복합 외래키를 지정할 때 사용합니다

@Entity
public class OrderItem {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "order_id", referencedColumnName = "id"),
        @JoinColumn(name = "order_code", referencedColumnName = "code")
    })
    private Order order;
}

[3] Repository 계층

1. JpaRepository <T, ID>

Spring Data JPA에서 제공하는 기본 CRUD 및 페이징/정렬 기능을 가진 인터페이스

- CrudRepository + PagingAdSortingRepository를 확장한 상위 인터페이스

- 엔티티에 대한 표준 데이터 접근 계층(DAO)을 자동 생성

public interface MemberRepository extends JpaRepository<Member, Long> {
}

일반적으로 (엔티티명 Repository) 명으로 인터페이스를 정의하여 JpaRepository를 상속받아 구현합니다

<T, ID> 제네릭 타입은 T = 엔티티, ID = 엔티티의 기본키 타입을 지정

 

기본 제공 CRUD 메서드

- save(entity): 저장 또는 수정

- findById(id): 단건 조회

- findAll(): 전체 조회

- deleteById(id): 삭제

 

페이징/정렬

Page<Member> findAll(Pageable pageable);
List<Member> findAll(Sort sort);

 

2. Query Method (쿼리 메서드)

메서드 이름만으로 쿼리를 자동 생성하는 기능

- 규칙 기반 쿼리 생성기능을 통해 별도의 JPQL 없이 조회 가능

List<Member> findByEmail(String email);
List<Member> findTop10ByStatusOrderByCreatedAtDesc(Status status);

- findBy,countBy,existsBy 등 접두어 사용

- And,Or,Between,Like,OrderBy 등 키워드 조합

 

3. @Query

JPQL 또는 Natice SQL을 직접 정의할 때 사용하는 어노테이션

- 복잡한 조건,커스텀 조회 로직,성능 튜닝에 사용됨

@Query("SELECT m FROM Member m WHERE m.status = :status")
List<Member> findByStatus(@Param("status") Status status);

 

4. @Modifying

@Query와 함께 사용하여 INSERT,DELETE,UPDATE 쿼리를 실행할 때 필요

- 반환 타입은 int (영향받은 행 수)

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE Member m SET m.name = :name WHERE m.id = :id")
int rename(@Param("id") Long id, @Param("name") String name);

 

주요 옵션

속성 설명
clearAutomatically 쿼리 실행 후 영속성 컨텍스트 자동 초기화
flushAutomatically 쿼리 실행 전 자동 flush 수행

JPA의 핵심 매핑 어노테이션과 Reposiroy 계층의 주요 기능을 간단하게 정리했습니다

평소에 JPA를 많이 사용하긴 했지만 정확한 개념을 모르는 경우도 있어서 개념 위주로 작성했습니다 

 

어노테이션의 역할과 특징을 파악하면 엔티티 설계부터 데이터 접근 계층까지 안정적이고 효율적으로 JPA를 사용할 수 있을 것 같습니다 이번 포스팅에서 다루긴 했지만 더 자세히 다뤄볼 내용도 많으니 하나씩 작성해보려 합니다

반응형