일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 웹개발
- 카프카
- Kafka
- 스프링의 정석
- 패스트캠퍼스
- Spring
- 생성자 주입
- CentOS
- DB
- JWT
- emqx
- 남궁성과 끝까지 간다
- 스웨거
- 개인프로젝트
- EC2
- 프로그래머스
- MYSQL
- 스파르타코딩클럽
- AWS
- visualvm
- WEB SOCKET
- java
- 데이터베이스
- Spring Security
- @jsonproperty
- 항해99
- 시큐리티
- docker
- 쇼트유알엘
- JavaScript
- Today
- Total
Nellie's Blog
[항해99][강의정리] Spring 숙련주차(W4) - JPA 심화 정리 본문
[목차]
Ⅰ.영속성 컨텍스트
00. 들어가기 전에
01. 영속성 컨텍스트란?
02. 영속성 컨텍스트는 어떻게, 왜 이렇게 설계되어있을까요?
Ⅱ. 엔티티 매핑 심화
01. 기본 엔티티 매핑 관련
02. 연관관계 관련 심화
03. 양방향 연관관계의 주의점
04. 프록시
Ⅰ.영속성 컨텍스트
00. 들어가기 전에
우리가 Spring Data JPA로 해오던 방식
// Entity를 생성!
Member minsook = new Member();
member.setId("abcd1234");
member.setUsername("민숙");
// 아래의 내용은 똑같은 과정!
memberRepository.save(minsook);
memberRepository.find();
우리가 Spring Data JPA를 사용하지 않았다면.. ?
// Entity를 생성!
Member minsook = new Member();
member.setId("abcd1234");
member.setUsername("민숙");
// EntityManager를 생성해줄 EntityManagerFactory를 만들어야합니다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa심화주차");
// Entity를 관리해줄 EntityManager를 EntityManagerFactory에서 생성!
EntityManager em = emf.createEntityManager();
// 엔티티를 영속화(저장)
em.persist(minsook);
// 엔티티를 찾기
em.find(Member.class, 100L);
EntityManager가 Entity를 관리해준다는 것은 이름에서 짐작이 가능한데, 왜 굳이 저걸 EntityManagerFactory에서 또 생성하게 만들게끔 구현하나요?
프로그램 내부에 “스레드”라는 일꾼이 있습니다.
최근의 프로그램들은 성능을 위해 여러 스레드들이 일을 같이하게 되어 있는데,
하나의 큰 일을 동시에 처리하려다 보면 동시성 문제가 생길 수 있습니다.
그런 일을 방지하기 위해, 특정 리소스나 정보는 공유하지 못하게 하는등의 처리가 필요합니다.
엔티티매니저에는 공유하면 안되는 특정 리소스나 정보가 있고, 여러 스레드가 하나의 엔티티 매니저를 이용 할 수 없도록 처리해야 합니다.
그래서 엔티티 매니저 팩토리에서 필요 할 때 마다 여러개의 엔티티매니저를 생성하는 구조로 설계한 것 입니다.
쉽게 말해,
모종의 이유로 여러개의 엔티티 매니저가 필요하고, 매번 모든과정을 다시하기에는 비용이 많이 들어 대부분 비용이 많이 드는 일을 엔티티 매니저 팩토리를 생성하면서 하고, 엔티티 매니저를 여러개 생성하는 일의 비용을 줄였다고 이해하시면 좋을 것 같습니다.
01. 영속성 컨텍스트란?
영속성 컨텍스트란 엔티티를 영구 저장 하는 환경 이라는 뜻 입니다. 어플리케이션(🤔 지금은 여러분의 자바 코드 그 자체라고 생각하시면 좋을 것 같습니다)이 데이터베이스에서 꺼내온 데이터 객체를 보관하는 역할을 합니다. 영속성 컨텍스트는 엔티티 매니저를 통해 엔티티를 조회하거나 저장할때 엔티티를 보관하고 관리합니다.
엔티티 매니저마다 개별적으로 부여되는, 어떠한 논리적 공간같은 개념으로 비유적으로 이해하셔도 좋을 것 같습니다.
자바의 엔티티 객체를 엔티티 매니저마다 가지고 있는 영속성 컨텍스트라는 공간에다 넣고 빼고 하면서 사용하는거죠.
“영속화 한다” 라는 말을 “엔티티 매니저가 자기의 영속성 컨텍스트에 넣어준다”로 이해하는 것 처럼 말이죠
- 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)
02. 영속성 컨텍스트는 어떻게, 왜 이렇게 설계되어있을까요?
1. 1차 캐시라는 것을 가지고 있습니다
DB역시 저장공간, 연산능력과 같은 컴퓨팅 리소스를 가진 프로그램 같은 것이라고 말씀드렸죠? 그리고 보통은 서버가 떠있는 컴퓨터(AWS라고 치면 여러분의 ec2 인스턴스)가 아닌 다른 곳에 떠있는 경우가 많습니다.
그리고 다른 여러가지 이유와 함께 DB를 이용하는 작업은 상대적으로 부하와 비용이 심한 작업입니다.
그래서 부하가 심한 작업을 자주하는 것을 줄여야 할 필요가 있었습니다.
자바 어플리케이션 상에서 데이터를 조회 사용할일이 아주 잦은데, 그럴때마다 DB로 “SELECT * FROM….”과 같은 SQL쿼리를 내는 일은 막아야 한다는 거죠!
그러기 위해서 영속성 컨텍스트 내부에 1차캐시를 둡니다.
- find(”memberB”)와 같은 로직이 있을 때 먼저 1차 캐시를 조회합니다.
- 있으면 해당 데이터를 반환합니다.
- 없으면 그 때 실제 DB로 “SELECT * FROM….” 의 쿼리를 내보냅니다.
- 그리고 반환하기 전에 1차캐시에 저장하고 반환해줍니다.
이제 memberB를 find 하는 요청이 다시 들어와도 굳이 DB로 다녀올 필요가 전혀 없겠죠?
2. “쓰기 지연 SQL 저장소”가 있습니다!
비슷한 맥락입니다. MemberA, MemberB를 생성할 때 마다 DB를 다녀오는건 비효율적이겠죠?
굳이 여러번 DB를 방문하지 않도록 내부에 “쓰기 지연 SQL 저장소”를 뒀습니다.
1. memberA, memberB를 영속화 하고
2. entityManager.commit() 메서드를 호출하면
3. 내부적으로 쓰기 지연 SQL 저장소에서 Flush가 일어나고
4. “INSERT A”, “INSERT B”와 같은 쓰기 전용 쿼리들이 DB로 흘러들어갑니다.
3. DirtyChecking을 통해 데이터의 변경을 감지해서 자동으로 수정해줍니다.
사실 똘똘한 JPA는 1차캐시와 쓰기지연 SQL 저장소를 이용해서 변경과 수정을 감지해줍니다.
1. 사실 1차 캐시에는 DB의 엔티티의 정보만 저장하는것이 아닙니다.
2. 해당 엔티티를 조회한 시점의 데이터의 정보를 같이 저장해둡니다.
3. 그리고 엔티티객체와 조회 시점의 데이터가 다르다면 변경이 발생했다고 알 수 있겠죠?
4. 해당 변경 부문을 반영 할 수 있는 UPDATE 쿼리를 작성해둡니다.
4. 데이터의 어플리케이션 단의 동일성을 보장해줍니다!
Member member1 = em.find(Member.class, "minsook");
Member member2 = em.find(Member.class, "minsook");
System.out.println(member1 == member2) => true
Ⅱ. 엔티티 매핑 심화
01. 기본 엔티티 매핑 관련
@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” 관련!
- 기본 생성자는 필수입니다.(생성자가 하나도 없으면 자바가 만들어주겠지만 파라미터가 있는 생성자가 하나라도 있다면 기본생성자를 꼭 작성해줘야 한다. 주의!)
- final 클래스, enum, interface 등에는 사용 할 수 없어요.
- 저장할 필드라면 final을 사용하시면 안됩니다.
“@Table”관련!
- 엔티티와 매핑할 테이블의 이름입니다. 생략하는경우 어떻게되는지 찾아보시면 좋을 것 같아요!
“@Column”관련!
- 객체 필드를 테이블 컬럼에 매핑하는데 사용합니다.
- 생략이 가능합니다!
- 속성들은 자주 쓸 일이 없고, 특정 속성은 무시무시한 effect가 있으니(
검색해보세요) 이름을 지정 할 때 아니고는 보통 생략하기도 합니다.
“@Enumerated”관련!
- Java Enum을 테이블에서 사용한다고 생각하면 좋을 것 같습니다.
- 속성으로는 Ordinal, String이 있는데, String인경우 해당 문자열 그대로 저장해서 비용은 많이 들지만, 나중에 Enum이 변경되어도 위험할일이 없기 때문에 일반적으로는 String을 사용합니다.
02. 연관관계 관련 심화
1. 단방향 연관관계
@Entity
@Getter
@Setter
public class Member {
@Id
@Column(name = "member_id")
private String id;
private String username;
@ManyToOne
@JoinColumn(name="team_id")
private Team team;
public void setTeam(Team team) {
this.team = team;
}
}
@Entity
@Getter
@Setter
public class Team {
@Id
@Column (name = "TEAM_ID")
private String id;
private String name;
}
@ManyToOne : 이름 그대로 다대일(N:1) 관계라는 매핑 정보였습니다. “한명의 유저가 여러개의 주문” 기억나시죠? 주요 속성으로는 optional, fetch, cascade가 있습니다. optional은 말 그대로 false로 설정하면 항상 연관된 엔티티가 있어야 생성할 수 있다는 뜻 입니다. fetch와 cascade는 뒤에 조금 더 설명하겠습니다
@JoinColumn(name="food_id") : 외래 키를 매핑할 때 사용했습니다. (실제 데이터베이스에는 객체필드에는 해당 객체 테이블의 외래키가 들어간다고 말씀드렸었죠?) 기본적으로 @Column이 가지고 있는 필드 매핑관련 옵션 설정들과, 외래키 관련 몇가지 옵션이 추가되어있는 옵션입니다.
2. 양방향 연관관계
@OneToMany는 양방향을 표기해주기 위해 해주는게 딱 봐도 느낌이 오는데 mappedBy는 뭔가요?
객체에는 사실 양방향 연관관계라는 것이 없습니다. 서로 다른 단방향으로 조회하는 로직 2개를 잘 묶어서 양방향인 것처럼 보이게 한 것 뿐이죠! 더 정확히는 멤버객체에 주문객체의 주소값을, 주문객체에는 멤버객체의 주소값을 가지고 있는 것 입니다!
외래키는 연관관계가있는 두개의 테이블 중에서 하나의 테이블에만 있으면 충분합니다!
따라서. 이런 차이로 인해 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데, 이것을 연관관계의 주인이라 합니다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제) 하게 되어있습니다.
반면에 주인이 아닌 쪽은 읽기만 할 수 있죠.
연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것 입이다.
📌 연관관계의 주인에 의해 mappedBy 된다!
03. 양방향 연관관계의 주의점
연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하기.
데이터베이스에 외래 키값이 정상적으로 저장되지 않으면 이것부터 의심해봐야 합니다.
Order order = new Order ("order", "order”);
em.persist(order);
Order order2 = new Order (”order2", "order2”);
em.persist(order2);
Member member = new Member("member", ”member”);
//여기가 실수 포인트!!!
member.getOrders().add(order);
member.getOrders().add(order2);
em.persist(member);
memberId | |
order | null |
order2 | null |
여기는 연관관계의 주인이 아닌 member.order에만 값을 저장했기 때문입니다.
예제 코드는 연관관계의 주인인 order.member에 아무 값도 입력하지 않았죠.
따라서 memberId 외래 키의 값도 null이 저장됩니다.
해결 : 순수한 객체까지 고려한 양방향 연관관계
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전합니다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있죠.
order.setMember(member)
member.getOrders().add(order);
해결 2 : 연관관계 편의 메소드
위의 방법은 번거로울 수 있으니, 아래 코드 처럼 Order안에서 Member를 넣어줄 때 Order엔티티(this)를 넣어주면 더 안전하게 코딩할 수 있다.
private Order order;
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
...
}
04. 프록시
프록시 개념
엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아닙니다. 연관관계의 엔티티는 비즈니스 로직에 따라 사용될 때도 있지만 그렇지 않을 때도 있죠. 실제 사용하다보면 유저의 선택이나, 특정 상황에 따라 연관관계로 맺어진 정보들이 전혀 필요 없을때가 많습니다.
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;
'회고록 > 항해99' 카테고리의 다른 글
[항해99][과제] Spring 심화주차 - Spring Security를 적용한 나만의 항해 블로그 백엔드 서버 만들기 (0) | 2022.12.19 |
---|---|
[항해99] Spring 숙련주차 강의 - 회원가입 / 로그인 정리(JWT사용) (0) | 2022.12.06 |
[항해99][과제] Spring 숙련주차 - 스프링 부트로 회원가입, 로그인, 댓글 작성/조회/수정/삭제 기능이 추가된 블로그 백엔드 서버 만들기(JWT사용) (0) | 2022.12.05 |
[항해99][과제] Spring 입문주차 - 스프링 부트로 로그인 기능이 없는 블로그 백엔드 서버 만들기 (0) | 2022.12.04 |
[항해99] Spring 입문주차 1주차 정리 - JPA기초, 메모장 만들기 (0) | 2022.11.30 |