일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 가상화
- Java
- 스프링 부트
- JPA
- Spring Security
- spring boot
- computer science
- Container
- spring batch
- ORM
- 스프링 배치
- 스프링 시큐리티
- 데이터베이스
- 스프링
- web server
- HTTP
- 배포
- 도커
- 자바
- 컨테이너
- CI/CD
- vm
- Spring
- spring cloud
- mysql
- 영속성 컨텍스트
- 웹 서버
- 백엔드
- virtualization
- CS
- Today
- Total
개발 일기
[섹션 8] 프록시와 연관관계 관리 본문
프록시
프록시를 이해하고 아래의 즉시로딩 지연로딩에 대해서 이해할 수 있다.
우선 왜 프록시가 쓰이는가?
Member를 조회할 때 Team도 함께 조회해야 하는가?

// 1. 회원과 팀 함께 출력
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());
}
// 2. 회원만 출력
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());
}
첫번째와 같이 회원을 조회하고 팀을 쓰는 경우에는 상관없지만 두번째와 같이 회원을 조회하지만은 팀은 따로 출력하지 않는 상황에 굳이 회원을 조회할 때 팀을 같이 조회해야할지에 대한 고민이 있을 수 있다.
후자와 같은 상황에서 DB에서 회원을 끌어오면서 팀도 같이 끌고 오는 것은 분명 비효율적이라고 볼 수 있을 것이다.
이때 프록시를 사용하여 Team의 조회를 미룰 수 있다. 어떻게 하는지 구체적으로 이해해보자.
프록시(Proxy)
실제 클래스를 상속받아서 만들고 실제 클래스와 겉 모양이 같다. 그래서 개발자 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 된다.
프록시 객체는 실제 객체의 참조(target)를 보관하며 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출!
그렇기 때문에 em.getReference()하는 시점에서는 SELECT 쿼리가 나가지 않는다. 왜냐하면 가짜(프록시) 엔티티를 조회하기 때문이다. 그러다가 실제로 그 객체를 사용할때 쿼리가 나간다. 아래의 그림을 보면 알 수 있다.

기존에 학습한 바로는 em.find()로 데이터베이스를 통해서 실제 엔티티 객체를 조회했지만
em.getReference()를 통해 데이터베이스 조회를 미루는 가짜(프록시) 엔티티 객체를 조회한다.

위 그림의 흐름
- em.getReference로 조회한 Member Proxy Entity 객체의 target은 사용하기 전이라 Null 이다.
(처음에만 Null이고 한번 target이 실제 객체를 참조하게 되면 아래 2~4번의 과정을 거치지 않는다.)
그런 상황에서 getName()으로 요청이 들어오게 된다. - target을 확인하여 실제 객체로 참조가 되어 있지 않았다면 JPA가 영속성 컨텍스트에 진짜 객체를 알려달라고 요청을 하게 된다.
- 만약 영속성 컨텍스트에 없다면 DB 조회를한다.
- 그렇게하여 영속성 컨텍스트가 실제 Member Entity 객체를 생성하고 Member Proxy Entity의 target이 실제 객체를 참조하게 된다.
- 그렇게 되면 실제 target의 getName()이 호출되고 유저에게 반환된다.
- 프록시 객체는 처음 사용할 때 한번만 초기화
- 프록시 객체를 초기화할때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님(참조하는 것임), 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근 가능
- 프록시 객체는 원본 엔티티를 상속받음, 따라서 타입 체크시 주의해야함 (== 비교 실패, 대신 instance of 사용)
- 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
em.find()하여 실제 객체를 불러오고 em.getReferenc()를 하면 이때 프록시 객체가 아닌 실제 객체가 조회된다.
이미 실제 객체가 영속성 컨텍스트가 있으니 굳이 프록시 객체로 대체할 필요가 없다. - 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생
(하이버네이트는 org.hibernate.LazyInitializationException 예외를 터트림)
즉시 로딩과 지연 로딩
지연 로딩
위에서 얘기했듯이 단순히 맴버 정보만 사용하는 비즈니스 로직해서 팀에 관한 정보를 함께 조회하지 않기 위해서 지연 로딩을 사용한다.
@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;
..
}


프록시와 즉시로딩 주의점!
- 가급적 지연 로딩만 사용하자(특히 실무에서)
- 즉시 로딩 적용 시 JPQL에서 N+1 문제 등 예상치 못한 SQL이 발생할 수 있다.
- @ManyToOne, @OneToOne은 기본이 즉시 로딩이기 떄문에 LAZY 설정 필요
- @OneToMany, @ManyToMany는 기본이 지연 로딩
지연 로딩 활용 - 실무
- 모든 연관관계에서 지연 로딩 사용하기, 실무에서 즉시 로딩 사용 X
- 해결책으로 JPQL fetch 조인이나, 엔티티 그래프 기능 사용하기(후에 배움)
- 즉시 로딩으로 인한 상상치 못한 쿼리가 발생할 수 있음
영속성 전이: CASCADE
특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을때 쓴다.


위의 그림과 같이 parent만 persist하면 List<Child> childList의 child 엔티티들을 모두 같이 persist되게 된다.
- 이는 연관관계를 매핑하는 것과 아무 관련이 없다.
- 엔티티를 영속화할 때 동시에 연관된 엔티티도 함께 영속화하는 편리함을 제공
CASCADE 종류
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- 그 외 MERGE, REFRESH, DETACH가 있음
그래서 언제 써야하는데?
의문이 들 수 있다. 언제 써야하는가? @OneToMany에는 모두 다 써야하는가?
예를 들어 child의 소유자가 하나인 경우 써도되지만 parent에서도 관리하고 grandparent에서도 관리하게 되면 쓰면 안된다.
단일 엔티티에 대해서 종속적인 경우에만 쓰자.
고아 객체
고아 객체 제거
- 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제
- orphanRemoval = true -> @OneToMany에 걸어주면 됨
- 예를들어 em.find()로 부모 엔티티를 끌고와서 parent1.getChildren().remove(0)으로 자식 엔티티 중 하나를 컬렉션에서 제거하게 되면 `DELETE FROM CHILD WHERE ID=?` 쿼리가 발생한다.
주의할 점
- 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능
- 참조하는 곳이 하나일 때 사용해야함! 특정 엔티티가 개인 소유할 때 사용(CASCADE와 동일)
- @OneToOne, @OneToMany만 가능
- 참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고 아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.
ex) em.remove()로 parent 엔티티를 제거하면 child 엔티티 들이 모두 같이 삭제된다.
영속성 전이 + 고아 객체, 생명주기
CascadeType.ALL + orphanRemoval=true와 같이 영속성 전이와 고아 객체 제거를 동시에 설정한다면?
- 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거
- 두 옵션을 모두 활성화 하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
- 도메인 주도 설계(DDD)의 Aggregate Root개념을 구현할 때 유용
'Back-End > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[섹션 10] 객체지향 쿼리 언어(JPQL) - 1 (1) | 2024.04.10 |
---|---|
[섹션 9] 값 타입 (0) | 2024.04.09 |
[섹션 7] 고급 매핑 (0) | 2024.04.02 |
[섹션 6] 다양한 연관관계 매핑 (0) | 2024.04.01 |
[섹션5] 연관관계 매핑 기초 (0) | 2024.04.01 |