단순한 CRUD 구현을 넘어 JPA의 연관관계 매핑과 영속성 컨텍스트를 활용하는 방법을 학습하였다. 반려동물 병원 예약 시스템을 구현하면서 @OneToMany, @ManyToOne, @OneToOne과 같은 연관관계 어노테이션을 적용하고, 트랜잭션과 더티 체킹, Soft Delete, JPQL, Fetch Join 등 JPA의 핵심 기능을 직접 구현해 볼 수 있었다.
이번 프로젝트에서 가장 먼저 학습한 내용은 Entity 간의 연관관계를 설정하는 방법이었다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "animal_id")
private Animal animal;
@ManyToOne은 여러 마리의 반려동물이 하나의 동물 종류(강아지, 고양이 등)에 속하는 관계를 표현한다.
@JoinColumn(name = "animal_id")을 사용하여 외래키(Foreign Key)를 지정하고, FetchType.LAZY를 적용하여 실제 Animal 객체는 필요한 시점에 조회하도록 설정하였다.
이를 통해 객체 간의 관계를 데이터베이스의 외래키와 자연스럽게 연결하는 방법을 학습하였다.
@OneToMany(mappedBy = "animal")
private List<Pet> pets;
@OneToMany는 하나의 Animal이 여러 마리의 Pet을 가지는 관계를 표현한다.
mappedBy는 연관관계의 주인이 아니라는 의미이며, 실제 외래키는 Pet 테이블이 관리한다.
이를 통해 JPA에서 연관관계의 주인과 주인이 아닌 객체를 구분하는 방법을 이해할 수 있었다.
연관관계를 선언하는 것뿐만 아니라 객체 간의 관계를 함께 변경하는 방법도 학습하였다.
public void addPet(Pet pet) {
pets.add(pet);
pet.changeAnimal(this);
}
public void changeAnimal(Animal animal) {
this.animal = animal;
}
연관관계는 한쪽 객체만 변경해서는 완전하게 유지되지 않는다.
addPet()에서는 Animal 객체의 리스트에도 Pet을 추가하고, 동시에 changeAnimal()을 호출하여 Pet 객체도 현재 Animal을 참조하도록 설정한다.
이를 통해 양방향 연관관계를 일관성 있게 관리하는 방법을 학습하였다.
이번 프로젝트에서는 1:1 관계도 구현하였다.
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pet_id")
private Pet pet;
PetHistory는 하나의 Pet만 관리하므로 @OneToOne을 사용하였다.
반려동물 한 마리마다 하나의 진료 이력을 연결할 수 있도록 설계하였으며, OneToOne 관계를 설정하는 방법과 외래키를 지정하는 방법을 학습하였다.
이번 프로젝트에서 가장 인상 깊었던 기능은 JPA의 더티 체킹(Dirty Checking)이었다.
@Transactional
public void deleteById(Long id) {
Pet pet = findById(id);
pet.changeDeleted();
}
@Transactional이 적용되면 메서드 시작 시점의 Entity 상태를 스냅샷으로 저장한다.
이후 Entity의 값이 변경되면 트랜잭션 종료 시점에 변경 내용을 자동으로 감지하여 UPDATE SQL을 실행한다.
따라서 별도의 update SQL을 직접 호출하지 않아도 데이터가 수정되는 더티 체킹의 동작 원리를 이해할 수 있었다.
삭제 기능도 새롭게 학습한 내용 중 하나였다.
private boolean isDeleted;
public void changeDeleted() {
isDeleted = true;
}
데이터를 실제로 삭제하는 대신 삭제 여부를 나타내는 isDeleted 값을 변경하는 방식을 사용하였다.
이러한 Soft Delete 방식은 삭제된 데이터를 복구하거나 이력을 관리할 수 있다는 장점이 있으며, 실무에서도 자주 사용하는 삭제 전략이라는 점을 알게 되었다.
이번 프로젝트에서는 SQL이 아닌 JPQL을 작성하는 방법도 학습하였다.
@Query("""
SELECT p
FROM Pet p
WHERE p.isDeleted = false
ORDER BY p.id DESC
""")
List<Pet> findAllDesc();
JPQL은 데이터베이스의 테이블이 아니라 Entity 객체를 대상으로 작성한다.
Pet Entity를 기준으로 조회하면서 삭제되지 않은 데이터만 조회하고, id를 기준으로 내림차순 정렬하였다.
이를 통해 SQL과 JPQL의 차이점과 Entity 중심으로 데이터를 조회하는 방식을 학습하였다.
이번 프로젝트에서는 Fetch Join을 이용하여 N+1 문제를 해결하는 방법도 학습하였다.
@Query("""
SELECT r
FROM Reservation r
JOIN FETCH r.doctor
JOIN FETCH r.pet
WHERE r.doctor.id = :doctorId
""")
예약 정보를 조회할 때 Doctor와 Pet이 모두 LAZY 전략으로 설정되어 있기 때문에 반복 조회 시 여러 개의 추가 SQL이 실행되는 N+1 문제가 발생할 수 있다.
JOIN FETCH를 사용하면 Reservation과 Doctor, Pet을 한 번의 조회로 함께 가져오기 때문에 불필요한 추가 쿼리를 줄일 수 있다.
이를 통해 성능을 고려한 JPA 조회 방법을 학습하였다.
예약 정보를 화면에 출력하기 위해 Entity를 그대로 사용하는 것이 아니라 DTO를 생성하였다.
public static DoctorReservationDTO fromEntity(Reservation reservation) {
return new DoctorReservationDTO(
reservation.getId(),
reservation.getDoctor().getName(),
reservation.getPet()
);
}
Entity 전체를 화면에 전달하지 않고 필요한 정보만 DTO로 변환하여 사용하였다.
이를 통해 화면에 필요한 데이터만 전달하는 DTO 패턴과 Entity를 직접 노출하지 않는 설계 방법을 학습하였다.
| 학습 내용 | 배운 내용 |
|---|---|
| JPA 연관관계 | @OneToMany, @ManyToOne, @OneToOne, @JoinColumn, mappedBy 사용 |
| 양방향 관계 | 객체 간 연관관계를 함께 관리하는 방법 |
| 영속성 컨텍스트 | @Transactional과 더티 체킹 동작 원리 |
| Soft Delete | 실제 삭제 대신 삭제 상태를 관리하는 방법 |
| JPQL | Entity를 대상으로 조회하는 객체지향 쿼리 작성 |
| Fetch Join | N+1 문제를 해결하는 조회 방법 |
| DTO 변환 | Entity를 DTO로 변환하여 필요한 데이터만 전달하는 방법 |
이번 프로젝트를 통해 JPA는 단순히 CRUD를 수행하는 기술이 아니라 객체 간의 관계를 효율적으로 관리하고, 영속성 컨텍스트를 활용하여 데이터 변경을 자동으로 반영하는 프레임워크라는 점을 이해할 수 있었다. 특히 연관관계 매핑과 더티 체킹, Soft Delete, JPQL, Fetch Join 등을 직접 구현하면서 객체 중심으로 데이터를 설계하고 조회하는 방식을 익힐 수 있었으며, 성능과 유지보수까지 고려한 JPA 활용 방법을 학습할 수 있었다.