프록시
- 프록시를 사용하면 연관된 객체를 처음부터 데이터베이스에서 조회하는 것이 아니라,
실제 사용하는 시점에 데이터베이스에서 조회하므로 객체 그래프로 연관된 객체들을 마음껏 탐색 가능하며
이러한 방법을 지연 로딩이라고 하며,
지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신 데이터베이스 조회를 지연할 수 있는 가짜 객체인 프록시 객체가 필요
// 회원 엔티티
@Entity
public class Member {
private String username;
@ManyToOne
private Team team;
public Team getTeam() {
retrun team;
}
public String getUsername() {
return username;
}
...
}
// 팀 엔티티
@Entity
public class Team {
private String name;
public String getName() {
return name;
}
...
}
// 회원과 팀 정보를 출력하는 비즈니스 로직
// 회원 엔티티를 찾아서 회원은 물론이고 회원과 연관된 팀의 이름도 출력
public void printUserAndTeam(String memberId) {
Member member = em.find(Member.class, memberId);
Team team = member.getTeam();
System.out.println("회원 이름: " + member.getUsername());
// 팀 엔티티의 값을 실제 사용하는 시점에 데이터베이스에서 팀 엔티티에 필요한 데이터를 조회
System.out.println("소속팀: " + team.getName());
}
// 회원 정보만 출력하는 비즈니스 로직
// 회원 엔티티만 출력하는데 사용하고 회원과 연관된 팀 엔티티는 전혀 사용하지 않음
// 회원 엔티티를 조회할 때 회원과 연관된 팀 엔티티까지 데이터베이스에서 함께 조회하는 것은 비효율적
// 이런 문제를 해결하기 위해 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 지연 로딩 제공
public void printUser(String memberId) {
// 회원 데이터만 데이터베이스에서 조회
Member member = em.find(Member.class, memberId);
System.out.println("회원 이름: " + member.getUsername());
}
- 프록시 기초
/* JPA에서 식별자로 엔티티 하나를 조회할 때는 EntityManager.find() 메소드를 사용해
영속성 컨텍스트에 엔티티가 없으면 데이터베이스를 조회함
이렇게 엔티티를 직접 조회하면 조회한 엔티티를 실제 사용하든 사용하지 않든 데이터베이스를 조회 */
Member member = em.find(Member.class, "member1");
/* 엔티티를 실제 사용하는 시점까지 데이터베이스 조회를 미루고 싶으면
EntityManager.getReference() 메소드를 사용하며
이 메소드를 호출하면 JPA는 데이터베이스를 조회하지 않고 실제 엔티티 객체를 생성하지 않는 대신
데이터베이스 접근을 위임한 프록시 객체를 반환 */
Member member = em.getReference(Member.class, "member1");
- 프록시 클래스
실제 클래스를 상속 받아서 만들어지므로 실제 클래스와 겉 모양이 같아 진짜인지 프록시인지 구분하지 않고 사용하며
프록시 객체는 실제 객체에 대한 참조를 보관하므로 프록시 객체의 메소드를 호출하면 프록시 객체는 실제 객체 메소드 호출 - 프록시 객체의 초기화
프록시 객체는 실제 사용될 때 데이터베이스를 조회해서 실제 엔티티 객체를 생성하는데 이를 프록시 객체의 초기화라고 함
// 프록시 초기화 예제
// MemberProxy 반환
Member member = em.getReference(Member.class, "id1");
member.getName(); // 1. getName();
// 프록시 클래스 예상 코드
class MemberProxy extends Member {
Member target = null; // 실제 엔티티 참조
public String getName() {
if(target == null) {
// 2. 초기화 요청
// 3. DB 조회
// 4. 실제 엔티티 생성 및 참조 보관
this.target = ...;
}
// 5. target.getName();
return target.getName();
}
}
- 프록시 초기화 과정 분석
1) 프록시 객체에 member.getName()을 호출해서 실제 데이터를 조회
2) 프록시 객체는 실제 엔티티가 생성되어 있지 않으면 영속성 컨텍스트에 실제 엔티티 생성을 요청하는 초기화 작업 진행
3) 영속성 컨텍스트는 데이터베이스를 조회해서 실제 엔티티 객체를 생성
4) 프록시 객체는 생성된 실제 엔티티 객체의 참조를 Member target 멤버 변수에 보관
5) 프록시 객체는 실제 엔티티 객체의 getName()을 호출해서 결과를 반환 - 프록시의 특징
1) 프록시 객체는 처음 사용할 때 한 번만 초기화됨
2) 프록시 객체를 초기화한다고 프록시 객체가 실제 엔티티로 바뀌는 것은 아니며, 단지 이를 통해 실제 엔티티에 접근 가능
3) 프록시 객체는 원본 엔티티를 상속받은 객체이므로 타입 체크 시 주의해서 사용해야 함
4) 영속성 컨텍스트에 이미 찾는 엔티티가 있으면 데이터베이스를 조회할 필요 없으므로
em.getReference() 를 호출해도 프록시가 아닌 실제 엔티티를 반환
5) 초기화는 영속성 컨텍스트의 도움이 필요하므로 도움을 받을 수 없는 준영속 상태의 프록시를 초기화하면 문제 발생 - 준영속 상태와 초기화
// MemberProxy 반환
Member member = em.getReference(Member.class, "id1");
transaction.commit();
em.close(); // 영속성 컨텍스트 종료
/* meber.getName()을 호출하면 프록시를 초기화해야 하는데
영속성 컨텍스트가 없으므로 실제 엔티티를 조회할 수 없어 예외 발생 */
member.getName(); // 준영속 상태 초기화 시도 -> org.hibernate.LazyInitializationException 예외 발생
- 프록시와 식별자
// 엔티티를 프록시로 조회
// 식별자(PK) 값을 파라미터로 전달하며 프록시 객체는 이 식별자 값을 보관
/* 엔티티 접근 방식을 프로퍼티(@Access(AccessType.PROPERTY))로 설정한 경우 식별자 값을 가지고 있으므로
식별자 값을 조회하는 team.getId()를 호출해도 프록시를 초기화하지 않음.
단 엔티티 접근 방식을 필드(@Access(AccessType.FIELD))로 설정하면
JPA는 getId() 메소드가 id만 조회하는 메소드인지
다른 필드까지 활용해서 어떤 일을 하는 메소드인지 알지 못하므로 프록시 객체를 초기화 */
Team team = em.getReferenace(Team.class, "team1"); // 식별자 보관
team.getId(); // 초기화되지 않음
// 프록시를 사용한 연관관계 설정
// 프록시는 연관관계를 설정할 때 유용하게 사용
// 연관관계를 설정할 때는 식별자 값만 사용하므로 프록시를 사용하면 데이터베이스 접근 횟수를 줄일 수 있음
// 또한 연관관계를 설정할 때는 엔티티 접근 방식을 필드로 설정해도 프록시를 초기화하지 않음
Member member = em.find(Member.class, "member1");
Team team = em.getReference(Team.class, "team1"); // SQL을 실행하지 않음
member.setTeam(team);
- 프록시 확인
JPA가 제공하는 PersistenceUnitUtil.isLoaded(Object entity) 메소드를 사용하면 프록시 인스턴스의 초기화 여부 확인 가능
// 프록시 확인
boolean isLoad = em.getEntityManagerFactory().getPersistenceUnitUtil().isLoaded(entity);
// 또는 boolean idLoad = emf.getPersistenceUnitUtil().isLoaded(entity);
// 초기화 여부 확인
// 이미 초기화되었거나 프록시 인스턴스가 아니면 true 반환
// 초기화되지 않은 프록시 인스턴스는 false를 반환
System.out.println("isLoad = " +isLoad);
// 조회한 엔티티가 진짜 엔티티인지 프록시로 조회한 것인지 확인하는 방법
// 클래스 명을 직접 출력해보기 - 클래스 명 뒤에 ..javassist..라 되어 있을 경우 프록시
System.out.println("memberProxy = " + member.getClass().getName());
// 결과 : memberProxy = jpabook.domain.Member_$$_javassist_0
+) 하이버네이트의 initialize() 메소드를 사용하면 프록시를 강제 초기화할 수 있음
org.hibernate.Hibernate.initialize(order.getMember()); // 프록시 초기화
즉시 로딩과 지연 로딩
- JPA는 즉시 로딩과 지연 로딩이라는 방법을 모두 지원하며 지연 로딩할 때는 프록시 객체를 사용
- 즉시 로딩
엔티티를 조회할 때 연관된 엔티티도 함께 조회
연관된 엔티티를 즉시 조회하며 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회
// 즉시 로딩 설정
// 즉시 로딩을 사용하려면 @ManyToOne의 fetch 속성을 FetchType.EAGER로 지정
@Entity
public class Member {
// ...
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}
// 즉시 로딩 실행 코드
// 회원을 조회하는 순간 팀도 함께 조회
// 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
// 즉시 로딩 실행 SQL
// 회원과 팀을 조인해서 쿼리 한 번으로 조회
// 이후 member.getTeam()을 호출하면 이미 로딩된 팀1 엔티티를 반환
SELECT
M.MEBER_ID AS MEMBER_ID,
M.TEAM_ID AS TEAM_ID,
M.USERNAME AS USERNAME,
T.TEAM_ID AS TEAM_ID,
T.NAME AS NAME
FROM
MEMBER M LEFT OUTER JOIN TEAM T
ON M.TEAM_ID=T.TEAM_ID
WHERE
M.MEMBER_ID='member1'
/* 위의 경우 TEAM_ID 외래 키는 NILL 값을 허용하므로 팀에 소속되지 않은 회원이 있을 수 있으므로
팀에 소속하지 않은 회원과 팀을 내부 조인하면 팀은 물론이고 회원 데이터도 조회할 수 없음.
그러므로 JPA는 이런 상황을 고려해서 외부 조인을 사용함.
(JPA는 선택적 관계면 외부 조인 사용, 필수 관계면 내부 조인 사용)
하지만 외부 조인보다 내부 조인이 성능과 최적화에 더 유리하므로
외래 키에 NOT NULL 제약 조건을 설정해 내부 조인을 사용하도로 함
@JoinColumn(nullable = true) : NULL 허용 (기본값), 외부 조인 사용
@JoinColumn(nullable = false) : NULL 허용하지 않음, 내부 조인 사용
@ManyToOne(fetch = FetchType.EAGER, optional = false) : 내부 조인 사용 */
- 지연 로딩
연관된 엔티티를 실제 사용할 때 조회
연관된 엔티티를 프록시로 조회하며, 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회
// 지연 로딩 설정
// 지연 로딩을 사용하려면 @ManyToOne의 fetch 속성을 FetchType.LAZY로 지정
@Entity
public class Member {
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// ...
}
// 지연 로딩 실행 코드
// 회원만 조회하고 팀은 조회하지 않음
// 대신 조회한 회원의 team 멤버 변수에 프록시 객체를 넣어둠
// 반환된 팀 객체는 프록시 객체이며, 이는 실제 사용될 때까지 데이터 로딩을 미루며
// 실제 데이터가 필요한 순간이 되어서야 데이터베이스를 조회해서 프록시 객체를 초기화
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색, 프록시 객체
team.getName(); // 팀 객체 실제 사용
// 지연 로딩 실행 SQL
// em.find(Member.class, "member1") 호출 시 실행되는 SQL
SELECT * FROM MEMBER
WHERE MEMBER_ID = 'member1'
// team.getName() 호출로 프록시 객체가 초기화되면서 실행되는 SQL
SELECT * FROM TEAM
WHERE TEAM_ID = 'team1'
+) 만약 조회 대상이 영속성 컨텍스트에 이미 있으면 프록시 객체를 사용하지 않고 실제 객체를 사용
지연 로딩 활용
- 예) 사내 주문 관리 시스템 개발
애플리케이션 로직 분석 및 로딩 설정
1) Member와 연관된 Team은 자주 함께 사용 → 즉시 로딩 설정
2) Member와 연관된 Order는 가끔 사용 → 지연 로딩 설정
3) Order와 연관된 Product는 자주 함께 사용 → 즉시 로딩 설정
// 회원 엔티티
@Entity
public class Member {
@Id
private String id;
private String username;
private Integer age;
// 회원 엔티티를 조회하면 연관된 팀 엔티티도 즉시 조회 (즉시 로딩)
@ManyToOne(fetch = FetchType.EAGER)
private Team team;
// 회원 엔티티를 조회하면 연관된 주문내역 엔티티는 프록시로 조회해서
// 실제 사용될 때까지 로딩을 지연 (지연 로딩)
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<Order> orders'
// Getter, Setter
...
}
// 회원을 조회할 때 실행된 SQL
// 회원과 팀은 즉시 로딩으로 설정했으므로, 회원을 조회할 때 연관된 teamA도 함께 조회
SELECT
MEMBER.ID AS MEMBERID,
MEMBER.AGE AS AGE,
MEMBER.TEAM_ID AS TEAM_ID
MEMBER.USERNAME AS USERNAME,
TEAM.ID AS TEAMID,
TEAM.NAME AS NAME
FROM
MEMBER MEMBER
// 조인 쿼리를 만들어 회원과 팀을 한 번에 조회
// 반면 회원과 주문내역은 프록시를 조회하므로 SQL에 전혀 나타나지 않음
LEFT OUTER JOIN
TEAM TEAM ON MEMBER.TEAM_ID=TEAM1_.ID
WHERE
MEMBER0_.ID='member1'
- 프록시와 컬렉션 래퍼
지연 로딩으로 설정하면 실제 엔티티 대신 프록시 객체를 사용하며, 이는 자신이 사용될 때까지 데이터베이스를 조회하지 않음
// 주문 내역 조회
Member member = em.find(Member.class, "member1");
List<Order> orders = member.getOrders();
System.out.println("orders = " + orders.getClass().getName());
// 결과 : orders = org.hibernate.collection.internal.PersistentBag 이므로 컬렉션 래퍼 반환
하이버네이트는 엔티티를 영속 상태로 만들 때 엔티티에 컬렉션이 있으면 컬렉션을 추적하고 관리할 목적으로
원본 컬렉션을 하이버네이트가 제공하는 내장 컬렉션으로 변경하며 이를 컬렉션 래퍼라고 함
엔티티를 지연 로딩하면 프록시 객체를 사용해서 지연 로딩을 수행하지만
주문 내역 같은 컬렉션은 컬렉션 래퍼가 지연 로딩을 처리해주며, 컬렉션에 대한 프록시 역할을 하므로 프록시라고 부름
// 컬렉션 초기화 X
member.getOrders();
// 컬렉션 초기화 O
// 컬렉션에서 실제 데이터를 조회할 때 데이터베이스를 조회해서 초기화
member.getOrders().get(0);
- JPA 기본 패치 전략
JPA의 기본 패치 전략은 연관된 엔티티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용
컬렉션을 로딩하는 것은 비용이 많이 들고 잘못하면 너무 많은 데이터를 로딩할 수 있기 때문에 지연 로딩을 사용
예) 특정 회원이 연관된 컬렉션이 데이터를 수만 건 등록한 경우 해당 회원을 로딩하는 순간 수만 건의 데이터도 함께 로딩
// fetch 속성의 기본 설정가뵤
@ManyToOne, @OneToOne : 즉시 로딩 (FetchType.EAGER)
@OneToMany, @ManyToMany : 지연 로딩 (FetchType.LAZY)
추천하는 방법은 모든 연관관계에 지연 로딩을 사용한 후,
애플리케이션 개발이 어느 정도 완료단계에 왔을 때 실제 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화
- 컬렉션에 FetchType.EAGER 사용 시 주의점
- 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않음
너무 많은 데이터를 반환할 수 있고 결과적으로 애플리케이션 성능이 저하될 수 있으므로
2개 이상의 컬렉션을 즉시 로딩으로 설정하는 것을 권장하지 않음 - 컬렉션 즉시 로딩은 항상 외부 조인을 사용
다대일 관계인 회원 테이블과 팀 테이블을 조인할 때
회원 테이블의 외래 키에 not null 제약 조건을 걸어두면 모든 회원은 팀에 소속되므로 항상 내부 조인을 사용해도 되지만,
팀 테이블에서 회원 테이블로 일대다 관계를 조인할 때 회원이 한 명도 없는 팀을 내부 조인하면 팀까지 조회되지 않으므로
JPA는 일대다 관계를 즉시 로딩할 때 항상 외부 조인을 사용 - FetchType.EAGER 설정과 조인 전략
- 컬렉션을 하나 이상 즉시 로딩하는 것은 권장하지 않음
@ManyToOne, @OneToOne
- (optional = false) : 내부 조인
- (optional = true) : 외부 조인
@OneToMany, @ManyToMany
- (optional = false) : 외부 조인
- (optional = true) : 외부 조인
영속성 전이 : CASCADE
- JPA는 연관된 객체를 함께 저장하거나 함께 삭제할 수 있는 영속성 전이 기능 제공하며
이는 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들어 줌
// 영속성 전이가 없는 부모 엔티티
@Entity
public class Parent {
@Id @GeneratedValue
private Lond id;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<Child>();
...
}
// 자식 엔티티
@Entity
public class Child {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Parent parent;
...
}
// 영속성 전이가 없는 부모 자식 저장
// 예) 부모 1명에 자식 2명 저장
// JPA는 엔티티를 저장할 때 연관된 모든 에티티는 영속 상태여야 함
// 따라서 부모 엔티티를 영속 상태로 만들고 자식 엔티티도 각각 영속 상태로 만듦
private static void saveNoCascade(EntityManager em) {
// 부모 저장
Parent parent = new Parent();
em.persist(parent); // 영속 상태
// 1번 자식 저장
Child child1 = new Child();
child1.setParent(parent); // 자식 -> 부모 연관관계 설정
parent.getchild().add(child1); // 부모 -> 자식 연관관계 설정
em.persist(child1); // 영속 상태
// 2번 자식 저장
Child child2 = new Child();
child2.setParent(parent); // 자식 -> 부모 연관관계 설정
parent.getchild().add(child2); // 부모 -> 자식 연관관계 설정
em.persist(child2); // 영속 상태
}
위와 달리 영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있음
- 영속성 전이 : 저장
// 영속성 전이를 활성화한 부모 엔티티
@Entity
public class Parent {
@Id @GeneratedValue
private Lond id;
// 부모를 영속화할 때 자식들도 함께 영속화하라고 옵션 설정
// 이를 통해 간편하게 부모와 자식 엔티티를 함께 영속화할 수 있음
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSEIST)
private List<Child> children = new ArrayList<Child>();
...
}
// CASCADE 저장 코드
private static void saveWithCascade(EntityManager em) {
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
child1.setParent(parent); // 연관관계 추가
child2.setParent(parent); // 연관관계 추가
parent.getChildren().add(child1);
parent.getChildren().add(child2);
// 부모 저장, 연관된 자식들 저장
// 엔티티를 영속화할 때 연관된 엔티티도 같이 영속화하는 편리함 제공
em.persist(parent);
}
// 부모만 영속화하면 CascadeType.PERSIST로 설정한 자식 엔티티까지 함께 영속화해서 저장
// 데이터베이스에 입력된 데이터를 확인하는 쿼리
SELECT * FROM CHILD
id | parent_id |
1 | 1 |
2 | 1 |
- 영속성 전이 : 삭제
부모와 자식 엔티티를 모두 제거하려면 각각의 엔티티를 하나씩 제거해야 하지만,
영속성 전이를 사용하여 CascadeType.REMOVE로 설정하면 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제됨
// 영속성 전이 사용 전
Parent findParent = em.find(Parent.class, 1L);
Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);
em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);
// 영속성 전이 사용 후
// DELETE SQL을 3번 실행한 후 부모와 부모와 연관된 자식도 모두 삭제됨
// 삭제 순서는 외래 키 제약조건을 고려해서 자식을 먼저 삭제하고 부모를 삭제
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);
/* CascadeType.REMOVE 없이 부모 로우를 삭제하려고 한다면,
자식 테이블에 걸려 있는 외래 키 제약조건으로 인해,
데이터베이스에서 외래키 무결성 예외가 발생 */
- CASCADE의 종류
// CascadeType 코드
public enum CascadeType {
ALL, // 모두 적용
PERSIST, // 영속 - 플러시를 호출할 때 전이 발생
MERGE, // 병합
REMOVE, // 삭제 - 플러시를 호출할 때 전이 발생
REFRESH, // REFRESH
DETACH // DETACH
}
// 여러 속성을 같이 사용 가능
cascade = {CascadeType.PERSIST, CascadeType.REMOVE}
고아 객체
- JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 고아 객체 제거 기능을 제공
즉, 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이며
만약 삭제한 엔티티를 다른 곳에서 참조한다면 문제가 발생할 수 있으므로, @OneToOne, @OneToMany에서만 사용 가능
또한 부모를 제거하면 자식은 고아가 되므로, 부모를 제거하면 자식도 같이 제거되므로 CascadeType.REMOVE 설정과 동일 - 예) 고아 객체 제거 기능을 사용해 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제
// 고아 객체 제거 기능 설정
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
// 고아 객체 기능을 활성화
// 컬렉션에서 제거한 엔티티는 자동으로 삭제됨
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
...
}
// 사용 코드
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); // 첫 번째 자식 엔티티를 컬렉션에서 제거
// 모든 자식 엔티티 제거
parent1.getChildren().clear();
// 실행 결과 SQL
// 컬렉션에서 엔티티를 제거하면 데이터베이스의 데이터도 삭제됨
// 영속성 컨텍스트를 플러시할 때 적용되므로 플러시 시점에 DELETE SQL이 실행
DELETE FROM CHILD WHERE ID=?
영속성 전이 + 고아객체, 생명주기
- 엔티티는 EntityManger.persist()로 영속화되고 EntityManager.remove()로 제거되며 엔티티 스스로 생명 주기를 관리함
- 그러므로 CascadeType.ALL + orphanRemoval = true를 동시에 사용하면 부모 엔티티를 통해 자식의 생명주기 관리 가능
// 부모 엔티티를 통해 자식의 생명주기를 관리
// 자식을 저장하려면 부모에 등록만 하면 됨 (CASCADE)
Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);
// 자식을 삭제하려면 부모에서 제거하면 됨
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);
'Java-Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 값 타입 (0) | 2022.04.19 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 프록시와 연관관계 관리 - 실전 예제 (0) | 2022.04.15 |
[자바 ORM 표준 JPA 프로그래밍] 고급 매핑 - 실전 예제 (0) | 2022.04.12 |
[자바 ORM 표준 JPA 프로그래밍] 고급 매핑 (0) | 2022.04.12 |
[자바 ORM 표준 JPA 프로그래밍] 다양한 연관관계 매핑 - 실전 예제 (0) | 2022.04.04 |