Member 엔티티
package com.example.jpaspring.domain;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "taem_id")
private Team team;
public Member(String username){
this(username, 0);
}
public Member(String username, int age) {
this(username, age, null);
}
public Member(String username, int age, Team team) {
this.username = username;
this.age = age;
if(team !=null){
changeTeam(team);
}
}
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
@Setter 어노테이션을 실무에서 가급적 사용하지 않는 이유와 @NoArgsConstructor의 AccessLevel.PROTECTED 사용, @ToString의 사용 방법, 그리고 changeTeam() 메서드를 통한 양방향 연관관계 설정 방법에 대해 더 구체적으로 설명하겠습니다.
1. @Setter 사용 자제
- 이유: @Setter 어노테이션은 클래스의 모든 필드에 대해 setter 메서드를 생성합니다. 이는 객체의 상태가 외부에서 변경될 수 있음을 의미하며, 객체의 일관성을 유지하기 어렵게 만듭니다.
- 대안: 필요한 경우에만 명시적으로 setter 메서드를 작성하거나, 생성자를 통해 필드를 초기화합니다.
2. @NoArgsConstructor와 AccessLevel.PROTECTED
- 이유: JPA 스펙 상 기본 생성자가 필요하지만, 외부에서 객체 생성을 막기 위해 protected 접근 제어자를 사용합니다.
- 설명: 기본 생성자를 protected로 설정하면 JPA는 객체를 생성할 수 있지만, 다른 클래스에서 해당 객체를 직접 생성하지 못하게 할 수 있습니다.
3. @ToString 사용 방법
- 이유: @ToString 어노테이션은 객체의 문자열 표현을 생성할 때 사용됩니다. 연관 관계가 있는 필드를 포함하면 순환 참조 문제가 발생할 수 있습니다.
- 대안: 내부 필드만 포함하여 @ToString 어노테이션을 사용합니다.
4. changeTeam() 메서드를 통한 양방향 연관 관계 설정
- 이유: 양방향 연관 관계를 설정할 때, 두 객체 간의 관계를 일관되게 유지하기 위해 편의 메서드를 사용합니다.
- 설명: changeTeam() 메서드를 사용하여 Member 객체와 Team 객체 간의 관계를 설정합니다.
Team 엔티티
package com.example.jpaspring.domain;
import jakarta.persistence.*;
import lombok.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public Team(String name){
this.name = name;
}
}
Member와 Team은 양방향 연관관계, `Member.team` 이 연관관계의 주인, `Team.members` 는 연관관계의 주 인이 아님, 따라서 `Member.team` 이 데이터베이스 외래키 값을 변경, 반대편은 읽기만 가능
데이터 확인 테스트
package com.example.jpaspring.domain;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class MemberTest {
@PersistenceContext
EntityManager em;
@Test
@Transactional
@Rollback(value = false)
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);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
em.flush();
em.clear();
// 확인
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
List<Team> teams = em.createQuery("select t from Team t", Team.class).getResultList();
for(Member member : members){
System.out.println("member = " + member);
System.out.println("-> member.team = " + member.getTeam());
}
for(Team team : teams){
System.out.println("team = " + team);
System.out.println("-> team.members = " + team.getMembers());
}
}
}
로그를 보면 Hibernate가 엔티티를 로드할 때 프록시 객체를 생성하고 필요할 때 실제 쿼리를 실행하는 것을 알 수 있습니다. 예를 들어, Member 엔티티와 관련된 Team 엔티티를 로드할 때 Team 객체가 실제로 사용될 때까지 지연 로딩(Lazy Loading)되며, 이 과정에서 프록시 객체가 사용됩니다.
프록시 객체와 쿼리
- 프록시 객체 생성:
- member.team = Team(id=1, name=teamA)와 같이 Member 엔티티의 team 필드에 Team 객체가 설정될 때, Team 엔티티가 프록시로 생성됩니다.
- 프록시 객체는 실제 엔티티가 로드될 때까지 데이터베이스 접근을 지연시키는 역할을 합니다.
- 쿼리 실행:
- 실제로 Team 엔티티의 데이터가 필요할 때(team.getName() 등의 메서드 호출), Hibernate는 데이터베이스에 쿼리를 실행하여 필요한 데이터를 로드합니다.
- 로그에 select t1_0.team_id, t1_0.name from team t1_0 where t1_0.team_id=?와 같은 쿼리가 실행되는 것을 볼 수 있습니다.
builder 패턴으로 구현시
package com.example.jpaspring.domain;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "taem_id")
private Team team;
@Builder
public Member (String username, int age, Team team) {
this.username = username;
this.age = age;
if(team != null){
changeTeam(team);
}
}
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
package com.example.jpaspring.domain;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class MemberTest {
@PersistenceContext
EntityManager em;
@Test
@Transactional
@Rollback(value = false)
public void testEntity(){
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = Member.builder()
.username("member1")
.age(10)
.team(teamA)
.build();
Member member2 = Member.builder()
.username("member2")
.age(20)
.team(teamA)
.build();
Member member3 = Member.builder()
.username("member3")
.age(30)
.team(teamB)
.build();
Member member4 = Member.builder()
.username("member4")
.age(40)
.team(teamB)
.build();
Member member5 = Member.builder()
.username("member5")
.age(50)
.build();
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
em.persist(member5);
em.flush();
em.clear();
// 확인
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
List<Team> teams = em.createQuery("select t from Team t", Team.class).getResultList();
for(Member member : members){
System.out.println("member = " + member);
System.out.println("-> member.team = " + member.getTeam());
}
for(Team team : teams){
System.out.println("team = " + team);
System.out.println("-> team.members = " + team.getMembers());
}
}
}
'JPA 실전' 카테고리의 다른 글
[JPA] JPA 실전 - 확장 기능 (0) | 2024.07.02 |
---|---|
[JPA] JPA 실전 - 쿼리 메소드 (3) (0) | 2024.07.02 |
[JPA] JPA 실전 - 쿼리 메소드 (2) -페이징과 정렬 (0) | 2024.07.02 |
[JPA] JPA 실전 - 쿼리 메소드 (1) (0) | 2024.07.02 |
[JPA] JPA 실전 - 공통 인터페이스 (1) | 2024.06.30 |