본문 바로가기

JPA 실전

[JPA] JPA 실전 - 개발 기본

 

 

 

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;
    }
}

 

MemberTeam은 양방향 연관관계, `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)되며, 이 과정에서 프록시 객체가 사용됩니다.

프록시 객체와 쿼리

  1. 프록시 객체 생성:
    • member.team = Team(id=1, name=teamA)와 같이 Member 엔티티의 team 필드에 Team 객체가 설정될 때, Team 엔티티가 프록시로 생성됩니다.
    • 프록시 객체는 실제 엔티티가 로드될 때까지 데이터베이스 접근을 지연시키는 역할을 합니다.
  2. 쿼리 실행:
    • 실제로 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());
        }
    }


}