본문 바로가기
JAVA/JPA

[Spring + JPA] JPA 연관관계 정리 #2 - 양방향 매핑 시 주의사항

by 개미가되고싶은사람 2024. 11. 7.

목차

    양방향 매핑 시 가장 많이하는 실수

    양방향 연관관계를 설정할 때 아래와 같이 주의해야 하는 점이 있습니다.

    • 양방향 연관관계 주인 객체 필드에 값을 설정하지 않는 경우
    • 양방향 연관 관계 설정 시 무한 루프를 주의

     

    주인 객체에 필드 값을 설정하지 않는 경우 

    아래 코드는 Member 객체(테이블)에 주인 필드가 존재하는 상황입니다.

    tx.begin();
            try {
                Member2 member = new Member2();
                member.setUserName("member1");
                member.setAge(19);
                em.persist(member);
    
                Team team = new Team();
                team.setName("TeamA");
                team.getMembers().add(member);
                em.persist(team);
                
                Team findTeam = em.find(Team.class, team.getId());
                List<Member2> members = findTeam.getMembers();
                
                System.out.println("================================");
                for (Member2 member2 : members) {
                    System.out.println("memberUsername = " + member2.getUserName());
                }
                System.out.println("================================");
                
                tx.commit();
            } catch (Exception e) {
                tx.rollback();
            } finally {
                em.close();
            }

    결과

    해당 결과는 JPA의 영속성 컨텍스트 특징 때문입니다. JPA를 통해 데이터를 저장할 때, 데이터베이스에 직접적으로 INSERT가 이루어지는 것이 아니라, 먼저 영속성 컨텍스트의 1차 캐시에 저장됩니다. 이 1차 캐시는 JPA가 관리하는 메모리 공간으로, 영속성 컨텍스트가 활성화되어 있는 동안 엔티티 객체의 상태를 유지합니다.

    따라서, 자식 엔티티인 team 객체(테이블)를 추가한 후 이를 반영하지 않은 상태에서 부모 엔티티인 member 객체(테이블)를 추가하면, 추가했던 team 객체(테이블)는 아직 데이터베이스에 저장되지 않았기 때문에 member 객체(테이블)는 새로 추가한 team 객체(테이블)에 대한 값들을 가지지 않습니다.

     

     

    그렇다면 해당 방법을 어떻게 해결 할 수 있을까??

    해당 문제는 연관관계의 주인 필드를 가지고 있는 객체에 해당 필드 값을 입력하면 손쉽게 해결됩니다.

                Team team = new Team();
                team.setName("TeamA");
                em.persist(team);
    
                Member2 member = new Member2();
                member.setUserName("member1");
                member.setAge(19);
                member.setTeam(team);
                em.persist(member);

     

    위에 코드처럼 Team 객체를 생성하고 Member2 객체의 Team 필드에 해당 값을 설정하면 원하는대로 Member2 테이블에 외래키 값이 올바르게 저장됩니다. 하지만 객체 간의 관계를 명확히 하기 위해 Member2 객체의 Team 필드에 Team 객체를 설정하고, Team 객체의 members 리스트에 Member2 객체를 추가하는 것이 유지 보수, 테스트 코드 작성 시 큰 도움을 줄 수 있습니다.

     

    결론

    • 양방향 매핑 시 연관 관계의 주인 필드에 값을 설정해야 합니다.
    • 양방향 연관관계에서는 두 객체의 연관관계 관련된 필드 값을 모두 설정하는 것이 좋습니다.

     

     

    양방향 연관 관계 설정 시 무한 루프 상황

    양방향 연관 관계를 설정할 때, toString() 메서드나 JSON 응답 반환 과정에서 무한 루프가 발생할 수 있습니다. 예를 들어, Board와 User 엔티티가 서로를 참조하는 구조에서 Board의 toString()을 호출하면 User의 toString()이 호출되고, 다시 User의 toString()에서 Board의 toString()이 호출되는 식으로 무한히 반복되어 StackOverflowError가 발생할 수 있습니다. 이를 방지하기 위해서는 toString()을 오버라이드하지 않거나, 서로 참조하는 객체를 제외하거나, 한쪽에서만 참조하는 객체를 사용해야 합니다. 또한, 컨트롤러에서 JSON 형태로 응답을 반환할 때 엔티티를 직접 반환하는 것은 테이블 구조를 노출시키고, 엔티티 구조 변경 시 API 사양까지 영향을 미치는 문제를 초래할 수 있으므로, 별도의 DTO로 변환하여 반환하는 것이 바람직합니다.

    @Entity
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String name;
    
        @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
        private List<Board> boards;
    
        // toString() 메서드에서 boards를 제외하여 무한 루프 방지
        @Override
        public String toString() {
            return "User{id=" + id + ", name='" + name + "'}";
        }
    
    }
    @Entity
    public class Board {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String title;
    
        @ManyToOne
        @JoinColumn(name = "user_id")
        private User user;
    
        // toString() 메서드에서 user를 제외하여 무한 루프 방지
        @Override
        public String toString() {
            return "Board{id=" + id + ", title='" + title + "'}";
        }
    }

     

     

    그럼 양방향 연관 관계를 사용하는게 좋은 것인가??

    사실 양방향 연관 관계를 실제로 사용할 일은 많지 않다고 합니다. 양방향 연관 관계는 고려해야할 사항이 있는데다가, 부모 엔티티 객체에 해당하는 자식 엔티티 객체들을 확인하고 싶을 땐 굳이 양방향 연관 관계를 사용하지 않고 자식 테이블에서 외래키를 기준으로 조회하면 됩니다.

    그럼에도 불구하고 양방향 연관 관계를 사용하게 되는 경우는 자식 엔티티 객체에서 바로 부모 엔티티 객체를 조회하고 싶은 경우에 사용합니다. 그리고 양방향 연관관계는 테이블에 영향을 주지 않기 때문에 언제든지 변경이 가능합니다.

     

     

     

    참고

    https://colabear754.tistory.com/142#%EC%96%91%EB%B0%A9%ED%96%A5_%EC%97%B0%EA%B4%80_%EA%B4%80%EA%B3%84_%EC%84%A4%EC%A0%95_%EC%8B%9C_%EC%A3%BC%EC%9D%98_%EC%82%AC%ED%95%AD

    https://hyeonic.tistory.com/224

    https://ym1085.github.io/jpa/JPA-%EC%96%91%EB%B0%A9%ED%96%A5%EB%A7%A4%ED%95%91%EC%8B%9C-%EC%A3%BC%EC%9D%98%EC%A0%90/#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C