영속성 컨텍스트

 

※ 영속성이라는게 뭘까?

  • 오래도록 계속 유지되는 성질.
  • 트랜잭션의 지속성(durability)은 영속성이라고도 하는데 트랜잭션이 성공적으로 완료된 후 데이터베이스에 반영한 수행 결과는 어떠한 경우에도 손실되지 않고 영구적이어야 함을 의미한다. 즉, 시스템에 장애가 발생하더라도 트랜잭션 작업 결과는 없어지지 않고 데이터베이스에 그대로 남아있어야 한다는 의미다.

 

① 영속성 컨텍스트란 ? 

 

출처 자바 ORM 표준 JPA - https://product.kyobobook.co.kr/detail/S000000935744

 

  • 영속성 컨텍스트란 엔티티를 영구 저장 하는 환경 이라는 뜻
  • 어플리케이션(자바 코드 그 자체)이 데이터베이스에서 꺼내온 데이터 객체를 보관하는 역할을 한다.
  • 영속성 컨텍스트는 엔티티 매니저를 통해 엔티티를 조회하거나 저장할때 엔티티를 보관하고 관리한다.
  • 엔티티 매니저마다 개별적으로 부여되는, 어떠한 논리적 공간같은 개념으로 비유적으로 이해할 수 있다.
  • 자바의 엔티티 객체를 엔티티 매니저마다 가지고 있는 영속성 컨텍스트라는 공간에다 넣고 빼고 하면서 사용하는 것
  • “영속화 한다” 라는 말을 “엔티티 매니저가 자기의 영속성 컨텍스트에 넣어준다”로 이해할 수 있다.

 

JPA 엔티티의 상태

 

 

  • 비영속(New) : 영속성 컨택스트와 관계가 없는 새로운 상태. 해당 객체의 데이터가 변경되거나 말거나 실제 DB의 데이터와는 관련없고, 그냥 Java 객체인 상태
// 엔티티를 생성
Member minsook = new Member();
member.setId("minsook");
member.setUsername("민숙");
  • 영속(Managed) : 엔티티 매니저를 통해 엔티티가 영속성 컨텍스트에 저장되어 관리되고 있는 상태. 이와 같은 경우 데이터의 생성, 변경등을 JPA가 추적하면서 필요하면 DB에 반영한다.
// 엔티티 매니저를 통해 영속성 컨텍스트에 엔티티를 저장
em.persist(minsook);
  • 준영속(Detached) : 영속성 컨택스트에서 관리되다가 분리된 상태
// 엔티티를 영속성 컨택스트에서 분리
em.detach(minsook);
// 영속성 컨텍스트를 비우기
em.clear();
// 영속성 컨택스트를 종료
em.close();
  • 삭제(Removed) : 영속성 컨택스트에서 삭제된 상태
em.remove(minsook)

 

영속성 컨텍스트는 어떻게, 왜 이렇게 설계되어있을까?

 

① 1차 캐시라는 것을 가지고 있다.

  • DB를 이용하는 작업은 상대적으로 부하와 비용이 심한 작업이다. 자바 어플리케이션 상에서 데이터를 조회 사용할일이 아주 잦은데, 그럴때마다 DB로 “SELECT * FROM….”과 같은 SQL쿼리를 내는 일은 막아야 한다. 굳이 DB에 접근하지 않아도 요청을 보다 가볍게 처리할 수 있도록 하기위해 영속성 컨텍스트 내부에 1차캐시를 둔다.

 

 

  1. find(”memberB”)와 같은 로직이 있을 때 먼저 1차 캐시를 조회한다.
  2. 있으면 해당 데이터를 반환한다.
  3. 없으면 그 때 실제 DB로 “SELECT * FROM….” 의 쿼리를 내보낸다. (DB접근)
  4. 그리고 반환하기 전에 1차캐시에 저장하고 반환해준다.

 

② “쓰기 지연 SQL 저장소”가 있다.

  • 비슷한 맥락으로 MemberA, MemberB를 생성할 때 마다 DB를 다녀오는건 비효율적이기 때문에, 굳이 여러번 DB를 방문하지 않도록 내부에 “쓰기 지연 SQL 저장소”를 두고 있다.

 

 

  1. memberA, memberB를 영속화 하고
  2. entityManager.commit() 메서드를 호출하면
  3. 내부적으로 쓰기 지연 SQL 저장소에서 Flush가 일어나고
  4. “INSERT A”, “INSERT B”와 같은 쓰기 전용 쿼리들이 DB로 흘러들어간다.

 

③ DirtyChecking을 통해 데이터의 변경을 감지해서 자동으로 수정해준다.

  • JPA는 1차캐시와 쓰기지연 SQL 저장소를 이용해서 변경과 수정을 감지해준다.

 

 

1. 사실 1차 캐시에는 DB의 엔티티의 정보만 저장하는것이 아니다.

2. 해당 엔티티를 조회한 시점의 데이터의 정보를 같이 저장해둔다.

3. 그리고 엔티티객체와 조회 시점의 데이터가 다르다면 변경이 발생했다고 감지한다.

4. 해당 변경 부문을 반영 할 수 있는 UPDATE 쿼리를 작성해둔다.

 

 

④ 데이터의 어플리케이션 단의 동일성을 보장해준다.

  • 값이 같은 데이터가 들어오면 같은 데이터로 취급한다.

 

엔티티 매핑 심화 - 기본 엔티티 매핑 관련

 

@Entity 
@Table (name="USER") 
public class Member { 
	
	@Id 
	@Column (name = "user_id") 
	private String id; 
	 
	private String username; 
	
	private Integer age; 

	@Enumerated (EnumType. STRING) 
	private RoleType userRole;

//	@Enumerated (EnumType. ORDINAL) 
//	private RoleType userRole;

	@Temporal (TemporalType. TIMESTAMP) 
	private Date createdDate;

	@Temporal (TemporalType. TIMESTAMP)  
	private Date modifiedDate;
 
}

@Entity

  1. 기본 생성자는 필수!!
  2. final 클래스, enum, interface 등에는 사용 할 수 없다.
  3. 저장할 필드라면 final을 사용할 수 없다.

@Table

  1. 엔티티와 매핑할 테이블의 이름

@Column

  1. 객체 필드를 테이블 컬럼에 매핑하는데 사용
  2. 생략 가능하다.
  3. 속성들은 자주 쓸 일이 없고, 특정 속성은 무시무시한 effect가 있으니 이름을 지정 할 때 아니고는 보통 생략하기도 한다.

@Enumerated

  1. Java Enum을 테이블에서 사용한다고 생각하면 된다.
  2. 속성으로는 Ordinal, String이 있는데, String인경우 해당 문자열 그대로 저장해서 비용은 많이 들지만, 나중에 Enum이 변경되어도 위험할일이 없기 때문에 일반적으로는 String을 사용한다.

 

연관관계 관련 심화

단방향 연관관계

 

① @ManyToOne

  • 이름 그대로 다대일(N:1) 관계라는 매핑 정보. ( “한명의 유저가 여러개의 주문” )
  • 주요 속성으로는 optional, fetch, cascade가 있다.
  • optional은 말 그대로 false로 설정하면 항상 연관된 엔티티가 있어야 생성할 수 있다는 뜻.

@JoinColumn(name="food_id") 

  • 외래 키를 매핑할 때 사용 (실제 데이터베이스에는 객체필드에는 해당 객체 테이블의 외래키가 들어간다)
  • 기본적으로 @Column이 가지고 있는 필드 매핑관련 옵션 설정들과, 외래키 관련 몇가지 옵션이 추가되어있는 옵션

 

양방향 연관관계

@Getter
@Entity
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
		@Column(nullable = false)
    private String memberName;

    @OneToMany(mappedBy = "member", fetch = FetchType.EAGER)
    private List<Orders> orders = new ArrayList<>();

    public Member(String memberName) {
        this.memberName = memberName;
    }
}
@Getter
@Entity
@NoArgsConstructor
public class Orders {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "food_id")
    private Food food;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    public Orders(Food food, Member member) {
        this.food = food;
        this.member = member;
    }
}
@Getter
@Entity
@NoArgsConstructor
public class Food {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String foodName;
    @Column(nullable = false)
    private int price;

    @OneToMany(mappedBy = "food",fetch = FetchType.EAGER)
    private List<Orders> orders = new ArrayList<>();

    public Food(String foodName, int price) {
        this.foodName = foodName;
        this.price = price;
    }
}

 

  • 객체에는 사실 양방향 연관관계라는 것이 없다. 서로 다른 단방향으로 조회하는 로직 2개를 잘 묶어서 양방향인 것처럼 보이게 한 것 뿐
  • 더 정확히는 멤버객체에 주문객체의 주소값을, 주문객체에는 멤버객체의 주소값을 가지고 있는 것
  • 외래키는 연관관계가있는 두개의 테이블 중에서 하나의 테이블에만 있으면 충분하다.
  • 따라서. 이런 차이로 인해 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 합니다.
  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제) 하게 되어있다.
  • 반면에 주인이 아닌 쪽은 읽기만 할 수 있다. 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것!
  • 연관관계의 주인에 의해 mappedBy 된다.

 

양방향 연관관계의 주의점

  • 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하기. 데이터베이스에 외래 키값이 정상적으로 저장되지 않으면 이것부터 의심해봐야 한다.
  • 해결 : 순수한 객체까지 고려한 양방향 연관관계 - 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전
  • 해결 2 : 연관관계 편의 메소드

 

프록시

  • 엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다. 연관관계의 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만 그렇지 않을 때도 있다. 실제 사용하다보면 유저의 선택이나, 특정 상황에 따라 연관관계로 맺어진 정보들이 전혀 필요 없을때가 많다.
  • JPA는 굳이 필요없는 DB 조회를 줄이면서 성능을 최적화한다. 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라 한다.
  • 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대상에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라고 한다.

즉시 로딩 : 엔티티를 조회할 때 연관된 엔티티도 함께 조회 @ManyToOne(fetch = FetchType.EAGER)

지연 로딩 : 연관된 엔티티를 실제 사용할 때 조회, 설정 방법 : @ManyToOne(getch = FetchType.LAZY)

 

@ManyToOne, @OneToOne: 즉시 로딩(FetchType.EAGER)

@OneToMany, @ManyToMany: 지연 로딩(FetchType.LAZY)

 

  • 기본적으로 “즉시로딩”은 연관된 엔티티를 조인해서 다 긁어와버리는 것이고, ”지연로딩”은 실제로 가짜 객체를 이용하면, 그때 별도의 쿼리가 나간다고 생각하면 된다. 
  • 하지만 즉시로딩은, 처음부터 모든 테이블에 조인을 걸어버리고 별도로 쿼리가 나가는 경우가 생기기에, 연관관계가 많고 복잡할수록 비용이 기하급수적으로 늘어나기에, 정확하게 이해하고 필요한 상황이 아니라면, 가급적으로 모두 지연로딩을 걸어두는게 일반적이다.
  • 그렇다면 굳이 필요가 없다면, @ManyToOne(FetchType.Lazy)를 사용하면 된다.

영속성 전이?

  • 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이기능을 사용하면 된다.
  • JPA는 cascade 옵션으로 영속성 전이를 제공한다
  • 예를들어 유저테이블과 메모 테이블이 있는데, 영속화한 유저객체가 있으면, 메모 테이블도 같이 영속화되어 같이 관리되는 것을 영속성 전이라고 한다.
  • 키워드 : CASCADE
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
private List<Address> addresses;

 

 

자료 출처 : https://teamsparta.notion.site/JPA-c7d93ee983be486ebee3bcd279e58e48

+ Recent posts