예제 도메인 모델
- 김영한님의 실전! Querydsl 강의를 바탕으로 스프링 데이터 JPA와 동일한 환경을 가진 예제 도메인 모델을 구축하고 데이터를 검증하는 테스트 과정을 정리함
도메인 모델 구조 요약
-
Member와 Team 엔티티 간의 다대일(N:1) 양방향 연관관계 구조

-
데이터베이스 관점의 테이블명 및 조인 식별자 구조 명시

- 연관관계의 주인은 물리 테이블에 외래키를 보유한
Member.team으로 지정 - DB 외래키 값 변경 제어권은
Member.team이 가지며, 역방향인Team.members는 읽기 전용 상태로 데이터 관리 역할을 수행함
- 연관관계의 주인은 물리 테이블에 외래키를 보유한
엔티티 코드 구성
- Member 엔티티 코드
- 연관관계 편의성을 지원하는 메서드를 함께 정의하여 단일 호출로 양측 데이터 수록을 보장함
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
package study.querydsl.entity; import lombok.*; import jakarta.persistence.*; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "username", "age"}) public class Member { @Id @GeneratedValue @Column(name = "member_id") private Long id; private String username; private int age; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "team_id") private Team team; public void changeTeam(Team team) { this.team = team; team.getMembers().add(this); } }
- JPA 스펙상 요구되는 기본 생성자는 유효성 검사 누락 방지를 위해
@NoArgsConstructor(access = AccessLevel.PROTECTED)수준으로 통제함 - 객체 로깅 시 발생할 수 있는 무한 루프를 막으려
@ToString에서 양방향 참조 필드(team)는 필히 명세에서 제외함

-
Team 엔티티 코드
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
package study.querydsl.entity; import lombok.*; import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "name"}) public class Team { @Id @GeneratedValue @Column(name = "team_id") private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); }
데이터 초기화 및 조회 확인 테스트
-
초기 데이터 등록 및 지연 로딩 관측용 테스트 코드
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 32 33 34
@SpringBootTest @Transactional @Commit public class MemberTest { @PersistenceContext EntityManager em; @Test public void testEntity() { Team teamA = new Team("teamA"); Team teamB = new Team("teamB"); em.persist(teamA); em.persist(teamB); Member member1 = new Member("member1", 10, teamA); Member member2 = new Member("member2", 20, teamA); em.persist(member1); em.persist(member2); // 영속성 컨텍스트 초기화 em.flush(); em.clear(); // 조회 및 확인 List<Member> members = em.createQuery("select m from Member m", Member.class) .getResultList(); for (Member member : members) { System.out.println("member=" + member); System.out.println("-> member.team=" + member.getTeam()); } } }
-
1차 캐시를 비워주는 영속성 초기화 과정(
em.flush(),em.clear())을 누락할 시, 조회가 메모리 상에서만 이루어져 실제 데이터베이스 I/O 쿼리를 관찰할 수 없으므로 필수로 명시해야 함
요약 정리
- Member와 Team 엔티티는 다대일 양방향 연관관계를 맺으며, 외래키가 위치한
Member.team을 연관관계의 주인으로 설정해 DB 외래키 변경 제어권을 부여함 - 연관관계 편의 메서드를
Member쪽에 두어 객체 상태 변환 시 양방향 데이터가 한 번에 매핑되도록 보장하고 불일치 문제를 예방함 - 순환 참조를 막고자
@ToString대상에서 연관 필드를 배제하고 무분별한 객체 생성을 막기 위해 최소 제어 수준인 Protected로 기본 생성자를 열어둠 - 초기 데이터 세팅 후 영속성 컨텍스트(1차 캐시)를 완전히 플러시 및 초기화해야 DB로 전달되는 실제 쿼리와 지연 로딩(
LAZY)의 작동 여부를 테스트 콘솔에서 명확히 확인할 수 있음