대표적인 JPA 매핑 어노테이션연관관계양방향, 단방향 연관관계다중성상속 관계 매핑@MappedSuperclass복합 키와 식별 관계 매핑@IdClass : 복합키 식별자 클래스를 하나 만든 후 @IdClass를 붙인다.@EmbeddedId조인 테이블프록시FetchCasCade(영속성 전이)임베디드 타입(복합 값 타입)JPQLCriteria 쿼리QueryDSL스프링 데이터 JPA공통 인터페이스 기능쿼리 메서드 기능JPA NamedQuery@Query페이징과 정렬
대표적인 JPA 매핑 어노테이션
- @Entity
- JPA를 사용해 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 한다.
- JPA가 엔티티 객체를 생성할 때 기본 생성자를 사용하므로, 이 생성자는 반드시 있어야 한다.
- @Table(객체와 테이블 매핑)
- 엔티티와 매핑할 테이블을 지정한다.
- @Id(기본 키 매핑)
- @Column(필드와 컬럼 매핑)
- @ManyToOne, @JoinColumn(연관관계 매핑)
연관관계
- 연관간계
- 객체와 관계형 데이터베이스 테이블을 매핑해 주어야 한다.
- 연관 관계 정의 규칙
- 방향
- 단방향, 양방향
- 두 객체 모두 참조용 필드가 있으면 양방향, 한 객체만 참조용 필드가 있으면 단방향 연관관계라고 부른다.
- 무조건 양방향 관계를 맺으면 된다고 생각할 수 있지만, 모든 엔티티를 양방향으로 하면 너무 복잡해진다.
- 따라서, 기본적으로 단방향 매핑을 하고 꼭 필요한 경우 양방향 매핑을 설정해주자.
- 연관 관계의 주인
- 양방향인 경우, 연관 관게에서의 관리 주체
- 외래키가 있는 곳을 연관관계의 주인으로 정한다.
- 설정 이유 - 연관 관계의 주인을 명확히 설정해 주지 않으면 JPA 입장에서 혼란을 주기 때문
- 다중성
- 다대일(N:1)
- 일대다(1:N)
- 일대일(1:1)
- 다대다(N:M)
양방향, 단방향 연관관계
- 양방향 연관관계
- 양쪽 엔티티가 서로의 상태를 자주 참조할 때 사용한다.
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 양방향 연관관계 설정
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 양방향 연관관계 설정
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
- 연관관계의 주인 : A→B, B→A 중 제어의 권한을 갖는 실직적인 관계가 어떤 것인지 Jpa에게 알려줌 →
mappedBy()
속성을 통해 지정
- 단방향 연관관계
- 연관 엔티티가 간단한 구조로, 읽기 중심인 경우 단방향 연관관계 사용
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 단방향 연관관계 설정
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "parent_id")
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
다중성
참고로, 다대다(N:M)는 유지보수, 복잡한 쿼리의 이유로 실무에서 사용하지 않는다고 한다.
- 다대일(N:1)
- 하나의 게시판(Board)과 게시글(Post) 예시
- 단방향
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
//... getter, setter
}
출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그:티스토리]
@ManyToOne
만 할당하였다.
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "board")
List<Post> posts = new ArrayList<>();
//... getter, setter
}
출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그:티스토리]
@ManyToOne
만 할당하였다.@OneToMany
가 할당되었고, Post가 ArrayList로 담겨진다.- 일대일(1:1)
- 하나의 게시글(Post)와 첨부파일(Attach)로 예시
- 단방향
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@OneToOne
@JoinColumn(name = "ATTACH_ID")
private Attach attach;
//... getter,setter
}
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
//... getter, setter
}
출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그:티스토리]
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@OneToOne
@JoinColumn(name = "ATTACH_ID")
private Attach attach;
//... getter,setter
}
@Entity
public class Attach {
@Id @GeneratedValue
@Column(name = "ATTACH_ID")
private Long id;
private String name;
@OneToOne(mappedBy = "attach")
private Post post;
//... getter, setter
}
출처: https://jeong-pro.tistory.com/231 [기본기를 쌓는 정아마추어 코딩블로그:티스토리]
상속 관계 매핑
조인 전략과 단일 테이블 전략 중 선택한다.
- 조인 전략 -
@Inheritance(strategy = InheritanceType.
JOINED
)
- 단일 테이블 전략 -
@Inheritance(strategy = InheritanceType.SINGLE_TABLE
- 상속 관계 예시
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
@Getter @Setter
public class Item {
@Id @GeneratedValue
@Column
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("A")
@Getter @Setter
public class Album extends Item{
private String artist;
}
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
@Getter @Setter
public class Book extends Item{
private String author;
private String isbn;
}
@Inheritance
: 상속 관계 매핑은 부모 클래스에 @Inheritance를 사용하여야 한다. 조인 전략에 따라 Type을 명시해 준다.
@DiscriminatorColumn(name = "DTYPE")
: 부모 클래스에 구분 칼럼을 명시, 기본값은 DTYPE 이다.
@DiscriminatorValue("A")
: 자식 타입에 엔터티 구분 칼럼 값을 명시한다. 부모 클래스의@DiscriminatorColumn
칼럼에 “A”가 들어가게 된다.
조인 전략은 Item과 Album/Book 테이블이 따로 생성된다. 단일 테이블 전략은 item 테이블에 Album/Book 칼럼들이 모두 포함되고, 빈 값은 null이 들어가게 된다.
@MappedSuperclass
- 부모 클래스는 테이블과 매핑하지 않고, 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶다면 @MappedSuperclass를 사용한다.
- 하단 예시에서, Member와 Seller 테이블에 id, name 칼럼이 추가된다.
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
}
@Entity
public class Member extends BaseEntity {
private String email;
}
@Entity
public class Seller extends BaseEntity {
private String shopName;
}
- @AttributeOverrid 로 매핑 정보를 재정의 할 수 있다.
- @AssocitaionOverride 로 연관관계를 재정의 할 수 있다.
- BaseEntity 클래스는 테이블에 매핑되지 않고, 자식 클래스 엔티티의 매핑 정보 상속을 위해 사용한다.
복합 키와 식별 관계 매핑
- 복합키: 두 가지 이상의 칼럼을 기본키로 지정하는 것
- 식별 관계 : 부모 테이블의 기본 키를 내려받아 자식 테이블의 기본키 + 외래키로 사용한다.
- 비식별 관계 : 부모 테이블의 기본 키를 내려받아 자식 테이블의 외래키로만 사용한다.
- 다음 이유로, 식별 관계보다는 비식별 관계를 주로 사용한다.
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서, 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 또 복합키별로 복합키 클래스를 만들어야 한다. → 복잡도가 늘어남
- 비식별 관계의 기본 키는 주로 대리키를 사용하고 JPA는 @GeneratedValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.
- → 될 수 있으면 비실별 관계를 사용하고, 기본 키는 Long 타입의 대리키를 사용하자.
- 복합키를 사용할 때, 그냥 @Id를 두 가지 이상 칼럼에 지정하면 에러가 발생한다.
- JPA는 복합키를 지원하기 위해 @IdClass와 @EmbeddedId 두 가지 방법을 제공한다.
@IdClass : 복합키 식별자 클래스를 하나 만든 후 @IdClass를 붙인다.
@Entity
@IdClass(ParentId.class)
@Setter @Getter
public class Parent {
@Id
@Column
private String id1;
@Id
@Column
private String id2;
@Column
private String name;
}
- @IdClass(ParentId.class) 를 통해 식별자 클래스인 ParentId 명시를 해 주어야 한다.
- 복합키로 세팅하고 싶은 key들에게 @Id를 붙인다.
@EqualsAndHashCode
@RequiredArgsConstructor
public class ParentId implements Serializable {
@EqualsAndHashCode.Include
public String id1;
@EqualsAndHashCode.Include
public String id2;
}
- 식별가 클래스 ParentId는 Serializable를 상속 받아야 한다.
- equal, hashCode method를 구현해야 한다. 위에선 lombok의 @EqualsAndHashCode로 구현하였다.(@EqualsAndHashCode.Include 사용)
public interface ParentRepository extends JpaRepository<Parent, ParentId> {
}
- Repo는 다음과 같이 구현한다. JpaRepository<Parent, ParentId>
@EmbeddedId
- @IdClass보다 더 객체지향적인 방법이다.
@Entity
@Setter @Getter
public class Parent {
@EmbeddedId
private ParentId parentId;
@Column
private String name;
}
@EqualsAndHashCode
@Embeddable
@RequiredArgsConstructor
public class ParentId implements Serializable {
@EqualsAndHashCode.Include
public String id1;
@EqualsAndHashCode.Include
public String id2;
}
- @IdClass 대신 @EmbeddedId, @Embeddable을 붙여 구현한다.
- @Embedded의 경우, 쿼리조회 같은 경우 약간 더 쿼리문이 길어질 수 있다.
- @IdClass의 경우에는 member.getId()와 같이 사용
- @EmbeddedId의 경우에는 member.getParentId().getId()와 같이 사용해야 한다.
- Embedded 타입의 JPA Query
- 다음과 같이 EmbeddedId 클래스 내의 필드로 JPA query를 조회할 수 있다.
findByParentIdId1
// ParentId -> EmbeddedId
// ID1 -> query하고싶은 embedded 클래스 내의 필드
조인 테이블

테이블 간 연관관계는, 조인 컬럼과 조인 테이블 두 가지 설계 방법이 있다.
- 조인 칼럼
- 조인 컬럼이라고 부르는 외래 키 컬럼을 사용해 관리한다.
- 연관 테이블에 연관관계가 맺어지지 않으면, 외래키에 null이 저장되어 있게 된다. (단일 테이블 전략)
- → 아주 가끔씩 데이터가 연간관계를 맺을 경우에는 비효율적이다.
- 조인 테이블
- 조인 테이블이라는 별도의 테이블을 사용하여 연관관계를 관리한다.
- 조인 테이블에 두 테이블간의 외래 키를 가지고 연관관계를 관리한다.
- 관리해야 할 테이블이 하나 늘어나고, 조인 시 조인 테이블까지 추가로 조인해야 한다.
→ 기본적으로 조인 칼럼을 사용하고, 필요하다고 생각되면 조인 테이블을 사용하자.
프록시
- 지연 로딩 : Entity를 조회할 때, 연관된 Entity들은 조회 될 수도, 안 될 수도 있다. 불필요한 낭비를 줄이기 위해, JPA는 실제 Entity의 조회가 일어날 때까지 조회를 지연한다. 이를 지연 로딩이라고 한다.
- 하단의 Fetch 참고
- 프록시 객체 : 지연 로딩을 사용하려면 실제 Entity 대신 DB 조회를 지연할 수 있는 가짜 객체가 필요한데, 이를 프록시 객체라 한다.
→ JPA의 경우, 지연 로딩 구현 방법을 JPA 구현체에 위임했기 때문에, 직접 설정할 필요는 없다.
Fetch
- 애플리케이션이 DB로부터 데이터를 가지고 오는것
- DB에서 데이터를 가져오는 방법은, EAGER(즉시로딩), LAZY(지연로딩)이 있다.
FetchType.EAGER(즉시로딩)
- 연관관계에 있는 Entity를 전부 로딩해서 영속성 컨텍스트에 가져온다.
@ManyToOne, @OneToOne
에서 Default로 세팅된다.FetchType.LAZY(지연로딩)
- 연견관계에 있는 Entity를 모두 가져오는게 아닌, 호출 시 그때 가져온다.
- 위의
프록시 객체
가 이때 사용된다. @OneToMany, @ManyToMany
에서 Default로 세팅된다.
- N+1 문제
- 연관관계가 설정된 엔티티를 조회하는 경우, 조회된 데이터의 개수만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 것
- ex) 모든 팀들을 쿼리하는데(1), 해당 팀에 속한 멤버들의 쿼리(N)가 계속 반복되는 경우
- 이 경우 성능 저하의 원인이 된다.
- 해결 방법?
FetchType.LAZY
- 연관관계의 Entity에 접근하는 경우가 아니라면, N+1 문제를 해결할 수 있다.
- 하지만 연관관계 객체가 호출이 된다면, 해결이 되지 않는다.
- Fetch Join 사용
- JPQL 쿼리를 사용하여 Fetch 조인
List<Team> teams = entityManager.createQuery(
"SELECT t FROM Team t JOIN FETCH t.members", Team.class).getResultList();
CasCade(영속성 전이)
- 특정 Entity를 영속 상태로 만들 때 연관된 Entity도 함께 영속 상태로 만드는 기능
- 부모 Entity를 저장할 때 자식 Entity도 저장할 수 있다. → 부모 Entity를 삭제할 때 자식 Entity도 삭제할 수 있다.
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- 주로 @OnetoMany에서 다음과 같이 사용한다.
@OneToMany(mappedBy = "parent",cascade = CascadeType.ALL)
- User와 Authority가 있다고 할 때
public class USERTEST{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
private List<AUTHORITYTEST> authorities = new ArrayList<>();
}
public class AUTHORITYTEST {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@ManyToOne
@JoinColumn(name= "user_id")
private USERTest user;
}
- User을 생성하고, 해당 유저의 Authority들을 부여하는 상황일 때
void jpaTest4() {
USERTest user = new USERTest();
user.setUsername("test");
AUTHORITYTEST authority1 = new AUTHORITYTEST();
...
AUTHORITYTEST authority2 = new AUTHORITYTEST();
...
List<AUTHORITYTEST> authorities = new ArrayList<>();
list.add(authority1);
list.add(authority2);
user.setAuthorities(authorities);
userTestRepository.save(user);
}
- cascade 옵션이 없는 경우
- User 테이블의 test 데이터는 db에 저장되고, authority 테이블에는 데이터자 저장되지 않는다.
- cascade = CascadeType.PERSIST 인 경우
- User 테이블의 test 데이터는 db에 저장되고, authority 테이블에도 authority1, authority2가 저장된다.
- test 데이터가 삭제되도, authority1, authority2는 삭제되지 않는다.
- cascade = CascadeType.ALL 인 경우
- PERSIST의 경우 뿐만 아니라, 모든 경우에 영속성이 적용된다.
- test 데이터를 삭제하면, authority1, authority2도 삭제된다.
- CasCade는 자식 엔티티가 하나의 부모 엔티티에 종속적일때만 사용해야 한다.
- CasCade = ALL로 되어있을 경우 자식 엔티티의 DELETE 쿼리 등이 작동하지 않을 수 있다.
임베디드 타입(복합 값 타입)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@Embedded Period workPeriod;
@Embedded Address homeAddress;
}
@Embeddable
public class Period { // 필드 }
@Embeddable
public class Address { // 필드 }
- @Embedded : 어노테이션으로 필드를 개별 클래스로 가질 수있다.
- @Embeddable : 값 타입을 정의하는 곳에 표시
- @Embeddable 클래스에도 연관관계 정의가 가능하다.
JPQL
- Entity객체를 조회하는 객체지향 쿼리이다. SQL과 유사하지만 더 간결하다.
- 특정 데이터베이스에 의존하지 않는다.
Criteria 쿼리
- JPQL 빌더 클래스이다.
- 컴파일 시점에 오류를 발견할 수 있고, IDE 자동완성이 되기 때문에 더 간편하다.
- 동적 쿼리를 작성하기 편하다.
select m from Member as m where m.username = 'kim'
위 JPQL를 Criteria 쿼리로 바꾸면
/ ...
// CriteriaBuilder, CriteriaQuery 객체 생성 코드 필요
/ ...
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("username"), "kim"))
List<Member> resultList = em.createQuery(cq).getResultList();
QueryDSL
- Criteria처럼 JPQL 빌더 역할, Criteria보다 훨신 단순하고 사용하기 쉽다.
- JPA 표준이 아닌 오픈소스 프로젝트이다.
- 위 동일한 JPQL문 예시
JPAQuery query = new JPAQuery(em);
List<Member> members = query.from(member).where(member.username.eq("kim")).list(member);
스프링 데이터 JPA
- 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트
- 지루하게 반복되는 CRUD 문제를 세련된 방법으로 처리할 수 있다.
- 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
- 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해주기 때문
공통 인터페이스 기능
- JPARepsotiroy → 제네릭에 Entity 클래스와 식별자의 타입을 지정한다.
publid interface ExamRepository extends JPARepository<Example, Long> {}
- JpaRepository→ PagingAndSortingRepository → CrudRepository → Repository

- JpaRepository 인터페이스의 주요 메서드들
- save(S)
- delete(T)
- findOne(ID)
- getOne(ID) : Entity를 프록시로 조회한다.
- findAll()
쿼리 메서드 기능
- 컬럼&메서드 이름을 조합하여, 인터페이스에 등록만으로 사용할 수 있다.
- 컬럼이 변하면, 메서드의 이름도 수정해 주어야 한다.
Keyword | Sample | JPQL snippet |
Distinct | findDistinctByLastnameAndFirstname | select distinct … where x.lastname = ?1 and x.firstname = ?2 |
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is, Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull, Null | findByAge(Is)Null | … where x.age is null |
IsNotNull, NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection<Age> ages) | … where x.age in ?1 |
JPA NamedQuery
- 쿼리에 이름을 보여하여 사용하는 방법
- @NamedQuery를 사용하여 name과 query를 지정
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NamedQuery(
name = "User.findByUseranme",
query = "select u from User u where u.username = :username"
)
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
}
- 생성한 쿼리는 EntityManager의 createNamedQuery로 사용할 수 있다.
public List<User> findByUsername(String username) {
return entityManager.createNamedQuery("User.findByUsername", User.class)
.setParameter("username", "이름")
.getResultList();
}
@Query
- @Query를 사용하면 레퍼지토리 메서드에 직접 쿼리를 정의할 수 있다.
public interface AuthorRepository extends JpaRepository<User, Long> {
List<User> findByFirstNameAndLastName(String firstName, String lastName);
@Query("select u from user u where u.firstName = :firstName and u.lastName = :lastName")
List<User> findByName(String firstName, String lastName);
}
- findByFirstNameAndLastName와 findByName는 동일한 역할을 수행한다.
페이징과 정렬
- 페이징 : org.springframework.data.domain.Pageable (내부에 sort 포함)
- PageRequest로 정렬 및 페이징 조건을 설정할 수 있다.
- 정렬 : org.springframework.data.domain.Sort
- name이 김씨로 시작하는 회원을 내림차순으로 10개씩 페이징하는 예제
// findByNameStartingWith는 인터페이스에 등록되어 있어야 한다.
PageRequest pageRequest = new PageRequest(0,10,new Sort(Sort.Direction.DESC, "name"));
Page<Member> result = memberRepository.findByNameStartingWith("김", pageRequest);
List<Member> members = result.getContent(); // 조회된 데이터
int totalPages = result.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = result.hasNext(); // 다음 페이지 존재 여부
TroubleShooting
JPA 양방향 순환 참조save the transient instance before flushingShare article