인프런 커뮤니티 질문&답변

이용범님의 프로필 이미지

작성한 질문수

자바 ORM 표준 JPA 프로그래밍 - 기본편

수업 자료

연관 관계 저장 시 궁금한 점.

24.07.22 22:36 작성

·

132

0

영한님 안녕하세요. 강의를 잘 듣고 있습니다!

학습 중 궁금한 점이 있어 질문 드립니다.

엔티티를 저장할 때에는 연관된 엔티티가 모두 영속 상태여야 한다고 알고 있습니다. 다음 코드에서 현재 user와 연관된 teamB는 영속 상태가 아닙니다. 따라서 실행 시 에러가 발생할 것으로 예상했습니다. 그러나 에러 없이 잘 동작하였고, DB를 확인해 보니 user의 team이 현재 연관된 teamB가 아니라 teamA로 되어 있었습니다.

@Entity
@Table(name="users")
public class User {
    @Id @GeneratedValue
    @Column(name="user_id")
    private Long id;

    private String username;

    @ManyToOne
    @JoinColumn(name="team_id")
    private Team team;


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        this.team = team;
    }
}
@Entity
public class Team {
    @Id @GeneratedValue
    @Column(name="team_id")
    private Long id;

    private String teamName;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTeamName() {
        return teamName;
    }

    public void setTeamName(String teamName) {
        this.teamName = teamName;
    }
}
public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();
        try{
            Team teamA = new Team();
            teamA.setTeamName("teamA");

            System.out.println(" =====[1]===== ");
            System.out.println("teamA = " + teamA);
            System.out.println("teamA.getId() = " + teamA.getId());
            System.out.println("teamA.getTeamName() = " + teamA.getTeamName());
            System.out.println(" 영속? = " + em.contains(teamA));
            System.out.println(" ============= ");
            System.out.println();

            em.persist(teamA);

            System.out.println(" =====[2]===== ");
            System.out.println("teamA = " + teamA);
            System.out.println("teamA.getId() = " + teamA.getId());
            System.out.println("teamA.getTeamName() = " + teamA.getTeamName());
            System.out.println(" 영속? = " + em.contains(teamA));
            System.out.println(" ============= ");
            System.out.println();

            Team findMyTeam = em.find(Team.class, teamA.getId());

            System.out.println(" =====[3]===== ");
            System.out.println("findMyTeam = " + findMyTeam);
            System.out.println("findMyTeam.getId() = " + findMyTeam.getId());
            System.out.println("findMyTeam.getTeamName() = " + findMyTeam.getTeamName());
            System.out.println(" 영속? = " + em.contains(findMyTeam));
            System.out.println(" ============= ");
            System.out.println();

            Team teamB = new Team();
            teamB.setId(findMyTeam.getId());
            teamB.setTeamName("teamB");

            System.out.println(" =====[4]===== ");
            System.out.println("teamB = " + teamB);
            System.out.println("teamB.getId() = " + teamB.getId());
            System.out.println("teamB.getTeamName() = " + teamB.getTeamName());
            System.out.println(" 영속? = " + em.contains(teamB));
            System.out.println(" ============= ");
            System.out.println();

            User user = new User();
            user.setUsername("username");
            user.setTeam(teamB);

            System.out.println(" =====[5]===== ");
            System.out.println("user.getTeam() = " + user.getTeam());
            System.out.println("teamB = " + teamB);
            System.out.println("teamB.getId() = " + teamB.getId());
            System.out.println("teamB.getTeamName() = " + teamB.getTeamName());
            System.out.println(" 영속? = " + em.contains(teamB));
            System.out.println(" ============= ");
            System.out.println();

            em.persist(user);

            System.out.println(" =====[6]===== ");
            System.out.println("user.getTeam() = " + user.getTeam());
            System.out.println("teamB = " + teamB);
            System.out.println("teamB.getId() = " + teamB.getId());
            System.out.println("teamB.getTeamName() = " + teamB.getTeamName());
            System.out.println(" 영속? = " + em.contains(teamB));
            System.out.println(" ============= ");
            System.out.println();

            tx.commit();

            System.out.println(" =====[7]===== ");
            System.out.println("user.getTeam() = " + user.getTeam());
            System.out.println("teamB = " + teamB);
            System.out.println("teamB.getId() = " + teamB.getId());
            System.out.println("teamB.getTeamName() = " + teamB.getTeamName());
            System.out.println(" 영속? = " + em.contains(teamB));
            System.out.println(" ============= ");
            System.out.println();

        }catch(Exception e){
            e.printStackTrace();
            tx.rollback();
        }finally{
            em.close();
        }

        emf.close();
    }
 =====[1]===== 
teamA = hellojpa.Team@fa5f81c
teamA.getId() = null
teamA.getTeamName() = teamA
 영속? = false
 ============= 

 =====[2]===== 
teamA = hellojpa.Team@fa5f81c
teamA.getId() = 1
teamA.getTeamName() = teamA
 영속? = true
 ============= 

 =====[3]===== 
findMyTeam = hellojpa.Team@fa5f81c
findMyTeam.getId() = 1
findMyTeam.getTeamName() = teamA
 영속? = true
 ============= 

 =====[4]===== 
teamB = hellojpa.Team@dd4aec3
teamB.getId() = 1
teamB.getTeamName() = teamB
 영속? = false
 ============= 

 =====[5]===== 
user.getTeam() = hellojpa.Team@dd4aec3
teamB = hellojpa.Team@dd4aec3
teamB.getId() = 1
teamB.getTeamName() = teamB
 영속? = false
 ============= 

 =====[6]===== 
user.getTeam() = hellojpa.Team@dd4aec3
teamB = hellojpa.Team@dd4aec3
teamB.getId() = 1
teamB.getTeamName() = teamB
 영속? = false
 ============= 

 =====[7]===== 
user.getTeam() = hellojpa.Team@dd4aec3
teamB = hellojpa.Team@dd4aec3
teamB.getId() = 1
teamB.getTeamName() = teamB
 영속? = false
 ============= 
질문 사진.png

궁금한 점은 다음과 같습니다:

1. teamB가 영속 상태가 아닌데 어떻게 user가 저장될 수 있었는지 궁금합니다.

2. user는 현재 teamB와 연관되어 있는데, 왜 DB에는 teamA가 나오는지 궁금합니다.

좋은 강의 감사합니다!!

답변 2

1

y2gcoder님의 프로필 이미지

2024. 07. 23. 09:22

안녕하세요. 이용범님, 공식 서포터즈 y2gcoder입니다.

users 테이블의 관점으로 보시면 좋을 것 같습니다. users 테이블 입장에서는 team의 id가 외래키 필드로 존재합니다! 그리고 JPA 의 엔티티 매니저 또한 id를 기준으로 엔티티를 구별하고, 다른 엔티티와 연관관계를 맺어주기 때문에, 기존에 DB에 존재하는 team의 id를 연관관계로 맺어주면, 해당 team의 id를 연관관계 엔티티로 하여 저장됩니다 🙂

이 때 저희가 객체로 만든 teamB는 DB에도 저장되어있지 않은 상태지만 teamA와 같은 식별자를 사용했기 때문에, 만약 이를 저장하고자 한다면 새로운 teamB가 생기는게 아니라 teamA의 name이 변경될 것 입니다. 하지만 현재 작성하신 코드에서는 teamB라는 객체를 생성만 하고 영속화하지 않기 때문에 결국 teamA의 id만 유효한 값으로서 사용되는 것입니다. 그래서 변경되지 않은 teamA와 user가 서로 연관관계로 잡혀 저장되게 되고, teamB라는 새로운 객체의 내용은 버려지고, 기존 teamA의 내용만 남기 때문에 위와 같은 결과가 나온 것으로 보입니다 🙂

식별자가 결국 중요한 기준이 됩니다!

 

감사합니다.

이용범님의 프로필 이미지
이용범
질문자

2024. 07. 23. 11:07

친절한 답변 감사합니다!
"기존에 DB에 존재하는 team의 id를 연관관계로 맺어주면"
에 대하여 추가적인 궁금증이 있습니다! 엔티티 메니저가 team id의 존재 확인을 어떻게 하나요??

제 생각은

1. 1차 캐시에서 team의 id를 찾는다.

-> 확인하기 위해 유저를 저장하기 전 1차 캐시를 비워줬습니다. 제 생각이 맞을 경우, 1차 캐시에 teamA의 id가 없어 teamB를 연관관계 맺고 DB에 저장 시 유일키 문제로 에러를 발생할 것으로 예상했습니다.

em.flush();

em.clear();

em.persist(user);

에러 없이 잘 동작하였습니다.

2. 그래서 엔티티 메니저가 직접 DB에 쿼리를 날려 team의 id 가 존재하는지 확인한다고 생각했지만, 쿼리가 날라가지 않았습니다.

궁금한 점은 다음과 같습니다.

1. 엔티티 메니저는 team의 id가 DB에 존재하는지 어떻게 확인하고 teamB가 아니라 teamA를 연관 관계로 맺어주는지.

2. 현재 객체 user는 teamB를 참조하지만, 테이블에서 user는 teamA와 연관되어 있습니다.

개발을 할 때 이러한 차이가 문제를 유발할 수 있는지 궁금합니다.

y2gcoder님의 프로필 이미지

2024. 07. 23. 13:11

우선 질문 남겨주셔서 감사합니다!

기본적으로 엔티티들은 id가 있고 영속성 컨텍스트에 관리되지 않는 상태는 detached 상태로 간주하게 됩니다. 이러한 상황에서 연관관계 엔티티로 추가해주는 과정에서 JPA는 엔티티 식별자가 있으므로 DB에 해당 id를 가진 team 엔티티가 있다고 간주한 뒤 따로 DB에 질의하지 않고 외래키를 추가해주는 것으로 보입니다.

그러면 DB 입장에서는 이미 해당 id를 row가 존재합니다. 그냥 id를 연결만 해주면 되고, 그러면 결국 말씀하셨던 것과 같이 teamName이 "teamA"인 로우와 연결이 되는 것으로 보입니다!

제 실무 경험이 길지 않아 그런지는 몰라도 위와 같이 코드를 짜야하는 경우가 없었습니다. 그래서 저는 실무에서 이용범님께서 보여주시는 테스트 케이스가 나오면 코드를 잘못 짰다고 생각하고, 다시 짤 것 같습니다. 왜냐햐면 detached된 엔티티의 id만 가져와서 새로 엔티티를 만들어 데이터를 바꾸고 merge하지 않은 채로 연관관계를 맺어주는 로직을 짜야 할 상황이 있을까 싶기 때문입니다 🙂 

 

제 답변이 틀렸을 수도 있기 때문에 제가 다른 서포터즈 분들께도 질문을 열어놓겠습니다!

0

인프런 AI 인턴님의 프로필 이미지

2024. 07. 22. 22:41

안녕하세요, 인프런 AI 인턴입니다. 이용범님께 회신 드립니다.

질문주신 내용과 관련하여 직접 답변을 드리기는 어렵지만, 유사한 맥락에서 연관 관계 매핑과 영속성 컨텍스트 동작 방식에 대해 설명하는 링크를 안내해 드릴 수 있습니다. 이용범님이 질문하신 주제는 JPA에서 엔티티를 영속화할 때 발생할 수 있는 이슈와 직접적으로 관련이 있습니다. 특히, 연관된 엔티티를 영속화하는 과정에서의 우선 순위 및 영속성 컨텍스트가 어떻게 해당 엔티티들을 관리하는지에 관한 주제입니다.

김영한 강사님의 “연관관계매핑과 영속성컨텍스트에 관한 질문입니다.”에서 비슷한 문제에 대한 설명을 찾을 수 있을 것입니다. 이 글은 영속성 컨텍스트와 연관관계 매핑 동작에 대한 이해를 돕는데 유용합니다.

해당 답변에서는 Member와 Team 간의 다대일 양방향 연관관계를 설정한 후, 영속성 컨텍스트에서의 동작 원리와 Lazy loading으로 인해 발생하는 특정 쿼리의 실행 여부를 설명하고 있습니다. 비록 직접 질문에 대한 답변은 아니지만, 이에 대한 이해를 바탕으로 질문 하신 내용에 대한 답을 유추해 보는 것도 하나의 방법이 될 수 있습니다.

영속성 컨텍스트 동작 방식에 대한 기본적인 이해를 바탕으로, em.persist() 호출 시점과 영속 상태 여부, 그리고 등록/수정 시 고려해야 할 사항들에 비추어 볼 때, 연관관계가 영속 상태에 있지 않은 엔티티를 참조하는 경우와 영속성 컨텍스트 내에서 엔티티 간 관계가 어떻게 처리되는지에 대한 인사이트를 얻을 수 있습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.