JPA 프로그래밍 기본편

이주인·2023년 8월 2일
0

스프링 공부

목록 보기
11/11

엔티티 매핑

객체와 테이블 매핑

@Entity가 붙은 클래스는 JPA가 관리한다.
엔티티라고 함

  • jpa를 사용하여 테이블과 매핑할 클래스는 @Entity 어노테이션이 필수
  • 기본 생성자 필수
  • final, enum, interface, inner 클래스를 사용해선 안된다
  • db에 저장할 필드에 final을 붙이면 안된다

@Table : 엔티티와 매핑할 테이블을 지정

데이터베이스 스키마 자동 생성

데이터베이스 테이블을 실행시점에 자동으로 생성하는 기능이 존재.
-> 실행할 경우 @Entity가 붙은 클래스와 매핑된 데이터베이스 테이블을 생성

필드와 컬럼 매핑

@Entity 
public class Member { 
 	@Id 
 	private Long id; 
 	//컬럼매칭 - db 컬럼에는 'name'으로 생성됨
 	@Column(name = "name") 
 	private String username; 
 	
    private Integer age; 
 
 	//enum 타입을 사용하고 싶은 경우 - STRING 타입을 사용하는 것이 좋음
 	@Enumerated(EnumType.STRING) 
 	private RoleType roleType; 
 	
    //Date 타입을 사용하고 싶은 경우
    //LocalDate 타입을 사용할 경우, 이 어노테이션은 필요 없음
    @Temporal(TemporalType.TIMESTAMP) 
 	private Date createdDate; 
 
 	@Temporal(TemporalType.TIMESTAMP) 
 	private Date lastModifiedDate; 
 
 	//매우 큰 값을 넣고 싶은 경우
 	@Lob 
 	private String description; 
    
    //메모리 상에서만 사용하고, DB에 매핑하고 싶지 않은 경우
    @Transient
	int temp;
}

@Column의 세부 기능

기본 키 매핑

	@Id 
 	private String id; 
 	
 	@Column(name = "name") 
 	private String username; 

직접 할당 방법

  • @id만 사용

자동 생성(@GeneratedValue)

  • 사용할 수 있는 전략은 3가지이다
/*
기본키 생성을 데이터베이스에 위임
mysql의 경우 auto_increase와 같음
*/
@GeneratedValue(strategy = GenerationType.IDENTITY) 

/*
SEQUENCE 오브젝트를 통해 값을 세팅
*/
@GeneratedValue(strategy = GenerationType.SEQUENCE) 

/*
키생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략
*/
@GeneratedValue(strategy = GenerationType.TABLE) 

권장하는 식별자 전략

  • null이 아닐 것
  • 변하면 안될 것
  • Long형 + 대체키(uuid, sequence 등을 사용)

IDENTITY 전략의 특징

JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행
그러나 AUTO_ INCREMENT는 데이터베이스에 INSERT SQL을 실행
한 이후에 ID 값을 알 수 있음

따라서 예외적으로 IDENTITY 전략을 사용할 때만 em.persist(entity) 호출한 시점에서 데이터베이스에 insert 쿼리를 날린다.

[IDENTITY 전략의 동작 과정]

em.persist(entity) 호출

-> pk값이 null인 상태로 1차 캐시에 저장불가

-> 호출 시점한 시점에서 Insert 쿼리를 DB에 전달

-> DB에서 PK 값 생성

-> PK값을 조회해서(내부적으로 select 쿼리를 보냄) 영속성 컨텍스트의 1차 캐시에 저장, 
PK 값이 적용된 영속 엔티티가 초기값일때 스냅샷으로 사용


연관관계 매핑

연관관계 매핑 기초

객체의 참조와 테이블의 외래 키를 매핑하는 법

단방향 연관관계

 @Entity
 public class Member { 
 	@Id @GeneratedValue
 	private Long id;
 	@Column(name = "USERNAME")
 	private String name;
 	private int age;
	
    /*
    일대 다 관계에서 누가 1이고 누가 다인지 알려줘야 함
    이경우 팀은 하나이고 , 팀에 소속된 팀원이 다 이다.
    
    @ManyToOne을 붙여 표시한다
    */	
	@ManyToOne
    // 조인해야 하는 컬럼이 무엇인지 선언
 	@JoinColumn(name = "TEAM_ID")
 	private Team team;
 	…

실사용 예시

//팀 저장
 Team team = new Team();
 team.setName("TeamA");
 em.persist(team);//알아서 Team의 pk값을 찾아 DB에 삽입함
 
 //회원 저장
 Member member = new Member();
 member.setName("member1");
 member.setTeam(team); //단방향 연관관계 설정, 참조 저장
 em.persist(member);
 
 //조회
 Member findMember = em.find(Member.class, member.getId()); 
//참조를 사용해서 연관관계 조회
 Team findTeam = findMember.getTeam();

양방향 연관관계

 @Entity
 public class Team {
	 
     @Id @GeneratedValue
	 private Long id;
	 private String name;
	 
     @OneToMany(mappedBy = "team")
	 List<Member> members = new ArrayList<Member>();
 … 
 }

mappedBy를 사용하는 이유

  • 객체가 가지고 있는 연관관계는 단방향 2개(객체 1 -> 객체 2, 객체 2 -> 객체 1)
  • 테이블의 연관관계는 양방향 1개(객체1 <-> 객체2)

DB 테이블의 경우 한번의 join으로 사용하면 조인된 두 테이블의 데이터를 자유롭게 사용할 수 있다

객체의 경우 연관관계가 두번 필요하다 -> 따라서 누군가가 외래 키를 관리해야 한다

양방향 연관관계의 주인이란

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정해야 한다.
  • 주인 만이 외래키를 관리(등록 및 수정) 할 수 있으며, 주인이 아닌 쪽은 읽기만 가능하다
  • 따라서 mappedBy를 사용하여 누가 주인인가 정해야 한다.
// 주인이 아니면 mappedBy를 선언하여 '주인'의 어떤 칼럼과 join할 것인지 알려야함
 @OneToMany(mappedBy = "주인")
  • 보통 외래키가 있는 곳을 주인으로 정함

주의점

1.양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.

  • 순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.

    Team team = new Team();
    team.setName("TeamA");
    em.persist(team);
    Member member = new Member();
    member.setName("member1");
    //역방향(주인이 아닌 방향)만 연관관계 설정
    team.getMembers().add(member);
    em.persist(member)

    옳은 코드

    Team team = new Team();
    team.setName("TeamA");
    em.persist(team);
    Member member = new Member();
    member.setName("member1");
    //없어도 되긴함
    team.getMembers().add(member); 
    
    //연관관계의 주인에 값 설정
    member.setTeam(team); 
    em.persist(member);

    '연관관계 편의매핑' 방식으로 Setter 메소드를 만들면
    team.getMembers().add(member); 코드를 사용할 필요 없이
    항상 양쪽에 값을 입력할 수 있다.(권장)

    참고

    public void setTeam(Team team){
    	this.team=team;
       team.getMember().add(this);
    }
  1. 양방향 매핑시에 무한 루프를 조심하자
  • 예: toString(), lombok, JSON 생성 라이브러리

정리

  • 단방향 매칭만으로 이미 연관관계 매핑은 완료되어야 함
  • 양방향 매핑은 반대방향으로 조회 기능을 추가된것
  • 따라서 양방향은 필요할때 추가해도 상관없다


다양한 연관관계 매핑

테이블

  • 외래 키 하나로 양쪽 조인 가능
  • 방향이라는 개념이 없음

객체

  • 참조용 필드가 있는 쪽으로만 참조 가능

연관관계의 주인

  • 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음

  • 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데 있으므로
    둘중 테이블의 외래 키를 관리할 곳을 지정해야함

  • 연관관계의 주인: 외래 키를 관리하는 참조

  • 주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능

다대일 관계(@ManyToOne)

외래 키가 있는 쪽이 연관관계의 주인

  • 양쪽을 서로 참조하도록 개발

일대다 관계(@OneToMany)

일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인

  • 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하
    는 특이한 구조
  • @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블
    방식을 사용함(중간에 테이블을 하나 추가함)

연관관계 관리를 위해 추가로 UPDATE SQL 실행해야 하는 단점이 있으므로,
일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하는 것이 좋다

일대일 관계(@OneToOne)

주 테이블이나 대상 테이블 중에 외래키 선택 가능

  • 주 테이블/대상 테이블 어디든 외래키 지정 가능

  • 외래키에 데이터베이스 유니크 제약조건이 추가되어야 함

  • 다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인

  • 반대편은 mappedBy 적용해야 함

• 주 테이블에 외래 키
	• 주 객체가 대상 객체의 참조를 가지는 것 처럼
	  주 테이블에 외래 키를 두고 대상 테이블을 찾음
	• 객체지향 개발자 선호
	• JPA 매핑 편리
	• 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
	• 단점: 값이 없으면 외래 키에 null 허용

• 대상 테이블에 외래 키
	• 대상 테이블에 외래 키가 존재
	• 전통적인 데이터베이스 개발자 선호
	• 장점: 주 테이블과 대상 테이블을 일대일에서 
      일대다 관계로 변경할 	때 테이블 구조 유지
	• 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨

다대다 관계(@ManyToMany)

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를
표현할 수 없음

  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함
  • 연결 테이블이 단순히 연결만 하고 끝나지 않으며,
    주문시간, 수량 같은 데이터가 들어올 수 있음
  • @ManyToMany 대신 @OneToMany, @ManyToOne을 사용하는 것이 좋음


고급 매핑

상속관계 매핑

객테의 상속과 구조와 DB의 수퍼타입 서브타입 관계를 매핑

조인전략

@Inheritance(strategy=InheritanceType.JOINED)

장점

  • 테이블 정규화
  • 외래 키 참조 무결성 제약조건 활용가능
  • 저장공간 효율화

단점

  • 조회시 조인을 많이 사용, 성능 저하
  • 조회 쿼리가 복잡함
  • 데이터 저장시 INSERT SQL 2번 호출

단일 테이블 전략

@Inheritance(strategy=InheritanceType.SINGLE_TABLE)

장점

  • 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
  • 조회 쿼리가 단순함

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null 허용
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상
    황에 따라서 조회 성능이 오히려 느려질 수 있다.

구현 클래스마다 테이블 전략

사용을 추천하지 않는 전략

장점

  • 서브 타입을 명확하게 구분해서 처리할 때 효과적
  • not null 제약조건 사용 가능

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION SQL 필요)
  • 자식 테이블을 통합해서 쿼리하기 어려움

@MappedSuperclass란

공통으로 사용할 매핑 정보가 있을 경우

  • 이 경우 중복되어 사용되는 id, name 필드를 BaseEntity 라는 상위 클래스에 담은 후, @MappedSuperclass를 선언해준다.

오직 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공

  • 상속관계 매칭도 아니고, 엔티티도 아니다.
  • 조회, 검색 불가(em.find(BaseEntity) 불가)
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장

@MappedSuperclass의 역할

  • 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑
    정보를 모으는 역할
  • 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통
    으로 적용하는 정보를 모을 때 사용
  • 참고) @Entity 클래스는 엔티티나 @MappedSuperclass로 지
    정한 클래스만 상속 가능


프록시와 연관관계

프록시란

em.find(): 데이터베이스를 통해서 실제 엔티티 객체 조회

em.getReference(): 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체 조회
-> 이 코드는 바로 값을 조회하는 것이 아닌,
조회한 값이 사용이 되는 시점에서 DB에서 객체를 조회

특징

  • 실제 클래스를 상속 받아서 만들어짐
  • 실제 클래스와 겉 모양이 같다
  • 프록시 객체는 실제 객체의 참조(target)를 보관
  • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출

  • 프록시 객체는 처음 사용할 때 한 번만 초기화
  • 초기화시 프록시 객체를 통해 실제 엔티티에 접근 가능
  • 프록시 객체는 원본 엔티티를 상속 받으므로, 타입 체크시 주의 해야 함
    (== 연산자가 아닌 instanceOf를 사용해야함)
  • 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티를 반환
  • 준영속 상태일 때, 프록시를 초기화 할 경우 에러 발생

유용한 기능들

  • 프록시 인스턴스의 초기화 여부 확인
    PersistenceUnitUtil.isLoaded(Object entity)

  • 프록시 클래스 확인 방법
    entity.getClass().getName() 출력(..javasist.. or
    HibernateProxy…)

  • 프록시 강제 초기화
    org.hibernate.Hibernate.initialize(entity);

즉시 로딩과 지연 로딩

// 맴버와 팀을 따로따로 조회하는 경우가 많을 떄

 @Entity
 public class Member {
 	@Id
 	@GeneratedValue
 	private Long id;
 
 	@Column(name = "USERNAME")
 	private String name;
 	
    /*
    프록시를 사용하여, 지연로딩을 선언
    -> 이 값이 실제로 사용되는 시점에서 조회
    */
    @ManyToOne(fetch = FetchType.LAZY) //**
 	@JoinColumn(name = "TEAM_ID")
 	private Team team;
 ..
 }

//맴버와 팀을 같이 조회하는 경우가 많을 때

@Entity
 public class Member {
 	@Id
 	@GeneratedValue
 	private Long id;
 
 	@Column(name = "USERNAME")
 	private String name;
 	
    /*
    프록시를 사용하여, 즉시로딩을 선언
    */
    @ManyToOne(fetch = FetchType.EAGER) //**
 	@JoinColumn(name = "TEAM_ID")
 	private Team team;
 ..
 }

실무에선 가급적 지연 로딩을 사용하는 것이 좋다.

  • 즉시 로딩을 적용시 예상치 못한 SQL이 발생(수많은 select문이 발생할 수 있음)
  • 즉시로딩은 JPQL에서 N+1 문제를 일으킨다.
  • @ManyToOne, OneToOne의 경우 기본 설정이 즉시 로딩으로 설정되어 있음
  • @OneToMany, @ManyToMany는 기본설정이 지연로딩임

영속성 전이란

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용
    -> 연관관계 매핑과 관계 없으며, 그냥 같이 영속상태로 변경하는 것뿐
/*
이런 식으로 연관된 엔티티를 em.persist()를 여러번 사용하여 저장하는 대신,
한번에 영속 상태로 변경가능

*/
@OneToMany(mappedBy="parent", cascade=CascadeType.PERSIST)

고아객체 란

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제

  • 참조하는 곳이 하나일 때 사용
  • 특정 엔티티가 개인 소유할 때 사용
  • @OneToOne, @OneToMany만 가능
//cascade=CascadeType.REMOVE(또는 ALL) 처럼 사용할 수 있음
@OneToMany(mappedBy="parent", orphanRemoval = true)

영속성 전이 + 고아객체

스스로 생명주기를 관리하는 엔티티는 sm.persist()로 영속화, em.remove()로 제거할 수 있음

  • 두 옵션을 모두 활성화 할 경우, 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다.


값 타입

값 타입은 다음과 같이 분류된다.

기본값 타입

  • 자바 기본 타입(int, double)
  • 래퍼 클래스(Integer, Long)
  • String

임베디드 타입(embedded type, 복합 값 타입)

  • 컬렉션 값 타입(collection value type

기본 값 타입

엔티티 타입

  • @Entity로 정의하는 객체
  • 데이터가 변해도 식별자로 지속해서 추적 가능

값 타입

  • 단순히 값으로 사용하는 자바 기본 타입이나 객체
  • 식별자가 없으므로, 변경시 추적 불가

특징

  • 생명주기를 엔티티에 의존
    -> 회원을 삭제시 이름, 나이 필드도 함꼐 삭제

  • 값 타입은 공유되서는 안된다
    -> 회원 이름 변경시 다른 회원의 이름도 함께 변경되선 안됨

  • primitve 타입은 값을 공유하는 것이 아닌 복사하는 것

  • Wrapper 클래스는 값을 공유할 수 있으나 변경을 되지 않는다

임베디드 타입(복합 값 타입)

  • 새로운 값 타입을 직접 정의할 수 있음
  • 주로 기본 값 타입을 모아서 만드므로, 복합 값 타입이라고 함
    -> 객체를 생각하면 편할 듯

장점

  • 재사용 가능
  • 높은 응집도
  • 해당 값 타입만 사용하는 의미있는 메소드를 만들 수 있다
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존함

사용법

  • @Embeddable: 값 타입을 정의하는 곳에 표시
  • @Embedded: 값 타입을 사용하는 곳에 표시
  • 기본 생성자 필수
  • 임베디드 타입을 포함한 모든 값 타입은, 값 타입을 소유한 엔티티에 생명주기를 의존

한 엔티티에서 같은 값 타입을 사용해야 하는 경우
@AttributeOverrides, @AttributeOverride를 사용해서 컬럼 명 속성을 재정의

// 임베디드 타입 사용
@Entity
public class Member {
  
  @Id @GeneratedVAlue
  private Long id;
  private String name;
  
  @Embedded
  private Period workPeriod;	// 근무 기간
  
  @Embedded
  private Address homeAddress;	// 집 주소
}
// 기간 임베디드 타입
@Embeddable
public class Peroid {
  
  @Temporal(TemporalType.DATE)
  Date startDate;
  @Temporal(TemporalType/Date)
  Date endDate;
  // ...
  
  public boolean isWork (Date date) {
    // .. 값 타입을 위한 메서드를 정의할 수 있다
  }
}

값 타입과 불변객체

임베디드 타입을 여러 엔티티에서 공유할 경유, 부작용이 발생할 수 있음
-> 값 변경시 특정 엔티티만 변경되는 것이 아닌, 여러 엔티티의 값이 동시에 변경될 수 있다.

따라서 값을 복사해서 사용하여야 한다.

//값을 복사해서 사용
Address a = new Address(“Old”); 
Address b = a; //객체 타입은 참조를 전달
b. setCity(“New”)

그러나 컴파일러 레벨에서, 객체타입은 참조값을 직접 대입하는 것을 막을 방법이 없다.
-> 객체의 공유 참조는 피할 수 없다.

불변 객체

객체 타입을 수정할 수 없게 만드는 방법

  • 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 전달하고, Setter를 만들지 않으면 됨
  • EX)Wrapper 클래스는 자바가 제공하는 대표적인 불변 객체이다

값 타입의 비교

인스턴스가 달라도 그 안의 값이 같으면 같은 것으로 봐야함

  • 값 타입은 a.equals(b)를 사용해서 동등성 비교를 해야 함

값 타입 컬랙션

  • 값 타입을 하나 이상 저장할 때 사용
  • @ElementCollection, @CollectionTable 사용
  • 데이터베이스는 컬렉션을 같은 테이블에 저장할 수 없다.
  • 컬렉션을 저장하기 위한 별도의 테이블이 필요함

제약사항

  • 값 타입은 엔티티와 다르게 식별자 개념이 없다.
  • 값은 변경하면 추적이 어렵다.
  • 값 타입 컬렉션에 변경 사항이 발생하면, 주인 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다.
  • 값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본
    키를 구성해야 함: null 입력X, 중복 저장X

따라서 실무에서는 상황에 따라 값 타입 컬렉션 대신에 일대다 관계를 고려하는 것이 좋다

  • 일대다 관계를 위한 엔티티를 만들고, 여기에서 값 타입을 사용
  • 영속성 전이(Cascade) + 고아 객체 제거를 사용해서 값 타입 컬렉션 처럼 사용
profile
소프트웨어공

0개의 댓글