| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- datastructure
- insertion sort
- list 컬렉션
- Stack
- C programming
- 윤성우의 열혈 자료구조
- 이것이 자바다
- 메모리구조
- JSON
- s
- buffer
- 혼자 공부하는 C언어
- stream
- R
- Algorithm
- ㅅ
- C 언어 코딩 도장
- 이스케이프 문자
- Serialization
- 윤성우 열혈자료구조
- Selection Sorting
- coding test
- 알기쉬운 알고리즘
- Graph
- Today
- Total
Engineering Note
JPA 일대다 List 컬렉션 사이즈 조회, 동작 방식 본문
결론
JPA본질 객체지향 => 객체지향적으로 이해를 하면 메서드라는 것 자체가 객체 인스턴스의 행위고 인스턴스는 반드시 특정되는거라서 근데 그 특정 인스턴스가 Entity는 PK로 식별된다.
객체지향의 본질
event.getParticipants().size();
이 코드에서:
1. event는 이미 특정 인스턴스
Event event = entityManager.find(Event.class, 1L);
// event는 "ID가 1인 Event" 라는 특정 객체
event.getParticipants();
// "이 특정 event(id=1)의 participants를 달라"는 의미
객체지향적으로 보면:
- event는 특정 인스턴스
- getParticipants()는 이 인스턴스의 행위
- 따라서 "이 event의 participants"가 당연한 것
2. Entity는 PK로 식별
일반 자바 객체:
Person person1 = new Person("홍길동");
Person person2 = new Person("홍길동");
// person1 != person2 (다른 인스턴스, 메모리 주소로 식별)
Entity 객체:
Event event1 = new Event(1L, "컨퍼런스");
Event event2 = new Event(1L, "컨퍼런스");
// PK가 같으면 같은 Entity! (영속성 컨텍스트에서 동일성 보장)
3. 연관관계도 객체 관점
class Event {
private Long id;
private List<EventParticipant> participants;
public List<EventParticipant> getParticipants() {
// "내(this Event)의 participants"
return this.participants;
}
}
객체지향 관점:
- this.participants는 "이 Event 인스턴스에 속한" participants
- JPA는 이걸 DB로 매핑할 때 PK를 사용
DB 매핑:
-- "이 Event(this)의 participants"를 DB로 표현
SELECT * FROM participants
WHERE event_id = ? -- this.id (특정 인스턴스의 PK)
---
Event event = eventRepository.findById(1L).orElseThrow();
int count = event.getParticipants().size();
```
**JPA 동작 순서**:
```
1. SELECT * FROM event_participant WHERE event_id = 1
↓
2. 모든 row를 EventParticipant 객체로 변환
↓
3. List<EventParticipant>에 담음 (메모리)
↓
4. Java List.size() 호출
↓
5. 결과 반환
event.getParticipants().size(); 이 자바코드가 카운트 쿼리를 수행하는 줄 알았다.
=> 내가 몰랐던 사실 나는 쿼리가 SELECT COUNT(*) FROM event_participant;
문제 : 전체 데이터 로딩
participants.size();
// SELECT * FROM event_participant WHERE event_id = ?
// 참여자 1000명이면 1000개 row 전부 메모리에!
비효율적:
- 카운트만 필요한데 전체 데이터 로딩
- SELECT COUNT(*) 쿼리가 훨씬 효율적
Before
public void vaidateAvialable() {
// 1. 상태 체크
if(this.status != EventStatus.ACTIVE) {
throw new IllegalStateException("진행 중인 이벤트가 아닙니다.");
}
// 2. 기간 체크
LocalDateTime now = LocalDateTime.now();
if(now.isBefore(this.startDate)) {
throw new IllegalStateException("이벤트가 아직 시작되지 않았습니다.");
}
if(now.isAfter(this.endDate)) {
throw new IllegalStateException("이벤트가 종료 되었습니다.")
}
// 3. 선착순 인원 체크
if(this.participants.size() >= maxParticipants) {
this.status = EventStatus.SOLD_OUT;
throw new IllegalStateException("선착순이 마감되었습니다.");
}
}
수정 코드
/**
* 이벤트 기간 및 상태 검증
* - 선착순 인원 체크는 Service에서!
*/
public void validateEventPeriodAndStatus() {
// 1. 상태 체크
if (this.status != EventStatus.ACTIVE) {
throw new IllegalStateException("진행 중인 이벤트가 아닙니다.");
}
// 2. 기간 체크
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(this.startDate)) {
throw new IllegalStateException("이벤트가 아직 시작되지 않았습니다.");
}
if (now.isAfter(this.endDate)) {
throw new IllegalStateException("이벤트가 종료되었습니다.");
}
}
@Transactional
public Long createOrder(Long eventId, Long memberId) {
// 1. 조회
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new IllegalArgumentException("회원을 찾을 수 없습니다."));
Event event = eventRepository.findById(eventId)
.orElseThrow(() -> new IllegalArgumentException("이벤트를 찾을 수 없습니다."));
// 2. 이벤트 검증 (도메인)
event.validateEventPeriodAndStatus();
// 3. 중복 주문 체크
if (participantRepository.existsByEventIdAndMemberId(eventId, memberId)) {
throw new IllegalStateException("이미 주문한 이벤트입니다.");
}
// 4. 선착순 인원 체크 (COUNT 쿼리)
long participantCount = participantRepository.countByEventId(eventId);
if (participantCount >= event.getMaxParticipants()) {
throw new IllegalStateException(
"선착순 마감되었습니다. (마감 인원: " + event.getMaxParticipants() + "명)"
);
}
// 5. 주문 생성 (Item 재고 차감)
Order order = Order.createEventOrder(member, event);
orderRepository.save(order);
// 6. 참여자 기록
EventParticipant participant = EventParticipant.create(event, member, order);
participantRepository.save(participant);
log.info("이벤트 주문 생성 완료. orderId={}, eventId={}, memberId={}",
order.getId(), eventId, memberId);
return order.getId();
}
내가 몰랐던 개념
JPA의 **지연 로딩(Lazy Loading)**과 프록시 객체의 동작 원리를 이해하면 명확해집니다.
왜 WHERE 조건에 eventId가 포함되는가?
event.getParticipants()를 호출할 때, JPA는 다음과 같이 동작합니다:
1. 컨텍스트 인식
JPA는 participants 컬렉션이 어떤 Event 객체에 속해있는지 알고 있습니다. 이미 event 객체를 영속성 컨텍스트에서 관리하고 있고, 그 event의 ID를 알고 있죠.
2. 효율적인 쿼리 생성
전체 participants 테이블을 조회할 이유가 없습니다. 이미 특정 event의 participants만 필요하다는 것을 JPA가 알기 때문에:
-- 비효율적 (당신의 예상)
SELECT * FROM participants;
-- 효율적 (실제 JPA의 동작)
SELECT * FROM participants WHERE event_id = ?;
3. 연관관계 매핑 정보 활용
@OneToMany(mappedBy = "event")
private List<EventParticipant> participants;
이 매핑은 JPA에게 다음을 알려줍니다:
- EventParticipant 테이블에 event_id 외래키가 있다
- 이 컬렉션은 특정 event에 속한 participants만 포함해야 한다
실제 동작 흐름
Event event = entityManager.find(Event.class, 1L); // event_id = 1
int count = event.getParticipants().size(); // 지연 로딩 트리거
내부 동작:
- getParticipants() 호출 → 프록시 컬렉션 반환
- size() 호출 → 실제 데이터 필요 → 지연 로딩 트리거
- JPA가 현재 event 객체의 ID(1L)를 확인
- SELECT * FROM participants WHERE event_id = 1 실행
만약 전체 조회를 원한다면?
전체 participants를 조회하려면 Repository를 통해 직접 조회해야 합니다:
// 전체 조회
List<EventParticipant> allParticipants = participantRepository.findAll();
// 특정 event의 participants (연관관계 활용)
int count = event.getParticipants().size(); // WHERE event_id = ?
핵심: JPA는 객체의 연관관계 매핑 정보를 활용해서, 필요한 데이터만 효율적으로 가져오도록 설계되어 있습니다. event.getParticipants()는 "이 event에 속한" participants를 의미하므로, WHERE 조건이 자동으로 추가되는 것이 당연한 동작입니다.
-> 영속성 컨텍스트는 Event Entity의 PK를 알고 있다.
영속성 컨텍스트의 구조
영속성 컨텍스트는 내부적으로 Map 구조로 되어 있습니다:
// 영속성 컨텍스트 내부 (개념적)
Map<EntityKey, Entity> persistenceContext = new HashMap<>();
// EntityKey 구조
class EntityKey {
Class<?> entityClass; // 예: Event.class
Object id; // 예: 1L (PK)
}
왜 PK가 필수인가?
1. 엔티티 식별
Event event = entityManager.find(Event.class, 1L);
// 내부적으로: persistenceContext.get(new EntityKey(Event.class, 1L))
영속성 컨텍스트에서 엔티티를 찾을 때:
- 엔티티 타입 + PK로 유일하게 식별
- PK가 없으면 같은 객체인지 구분 불가능
2. 연관관계에서의 활용
Event event = entityManager.find(Event.class, 1L);
// 영속성 컨텍스트: {(Event.class, 1L) -> Event객체}
event.getParticipants().size(); // 지연 로딩 발생
JPA의 내부 동작:
// 1. event 객체에서 PK 추출
Long eventId = event.getId(); // 1L
// 2. 이 정보로 쿼리 생성
SELECT * FROM participants WHERE event_id = 1;
3. 프록시 객체도 PK를 가짐
Event event = entityManager.getReference(Event.class, 1L);
// 아직 DB 조회 안 함, 프록시 객체
event.getParticipants();
// 프록시 객체도 ID(1L)는 알고 있음
// WHERE event_id = 1 쿼리 가능
실제 시나리오
@Entity
public class Event {
@Id
private Long id; // 이 값이 핵심!
@OneToMany(mappedBy = "event")
private List<EventParticipant> participants;
}
// 사용
Event event = entityManager.find(Event.class, 1L);
// 영속성 컨텍스트에 (Event.class, 1L) -> event 객체 저장됨
event.getParticipants().size();
// JPA: "이 event의 id는 1L이구나"
// SQL: WHERE event_id = 1
정리
- 영속성 컨텍스트는 (엔티티 타입, PK) 를 키로 사용
- 모든 영속 상태 엔티티는 반드시 PK 값을 가짐
- 연관관계 조회 시 이 PK 값을 활용해서 WHERE 조건 생성
- 그래서 event.getParticipants()가 "이 event(id=1)의 participants" 라는 것을 JPA가 정확히 알 수 있음
PK가 영속성 컨텍스트의 핵심이라는 당신의 이해가 정확합니다! 🎯
프록시 객체란?
프록시는 실제 엔티티를 상속받은 가짜 객체입니다. 겉모습은 같지만 내부는 텅 비어있죠.
Event event = entityManager.getReference(Event.class, 1L);
// 이때 반환되는 것은 Event가 아니라 Event$HibernateProxy 같은 것
프록시 객체의 구조
// 실제 Entity
@Entity
public class Event {
@Id
private Long id;
private String name;
private LocalDateTime startDate;
}
// JPA가 런타임에 생성하는 프록시 (개념적)
public class Event$HibernateProxy extends Event {
private Long id; // ⭐ PK만 가지고 있음!
private EntityEntry entityEntry; // 영속성 컨텍스트 참조
private boolean initialized = false; // 초기화 여부
@Override
public String getName() {
if (!initialized) {
// DB에서 실제 데이터 로딩
loadRealEntity();
}
return super.getName();
}
@Override
public Long getId() {
return this.id; // ⭐ DB 조회 없이 바로 반환!
}
}
왜 PK만 가지는가?
1. 프록시 생성 시점
Event event = entityManager.getReference(Event.class, 1L);
이 코드 실행 시:
- DB 조회 안 함
- PK(1L)만 받아서 프록시 생성
- 나머지 필드는 비어있음 (null)
'SW Engineering' 카테고리의 다른 글
| [SW Engineering] Redis 분산락을 통한 동시성 문제 해결 과정 (0) | 2025.12.25 |
|---|---|
| [SW Engineering] Redis 분산락을 사용하는 이유와 기본적인 사용 방법 (0) | 2025.12.25 |
| [SW Engineering] 동시성 이슈가 발생하는 이유와 비관적 락을 통해 동시성 문제 해결하기 (0) | 2025.12.24 |
| [SW Engineering] JWT 기반 인증의 한계와 보안·제어성 개선 전략 (0) | 2025.12.20 |
| [SW Engineering] 이커머스 동시성 문제 해결 (0) | 2025.12.19 |
