Lee's Grow up

[JAVA/JPA] 연관관계 매핑 본문

PROGRAMMING/JAVA

[JAVA/JPA] 연관관계 매핑

효기로그 2019. 12. 9. 22:36
반응형

해당 내용은 인프런의 자바 ORM 표준 JPA 프로그래밍 - 기본편, 김영한 의 내용을 기반으로 정리해서 작성한 글입니다.
자세한 내용은 해당 강의 또는 책을 구매하시는걸 추천합니다.

테이블과 객체 사이의 간격


TEAM테이블과 MEMBER테이블이 있다고 가정하고, MEMBER테이블은 TEAM을 기본키를 외래키로 가지고 있는 N:1의 관계일 때 기존의 방식으로 객체를 테이블의 데이터 기준으로 작성하면 아래와 같이 사용함 ( Mybaits 등)

public class Member{

    private Long memberId;
    private Long teamId;
    private String memberName;
}

public class Team{ 

    private Long teamId;
    private String teamName;
}

위와 같은 경우 객체끼리의 참조가 끊김, 예를들어 Member에서 member.getTeam()등과 같이 연관 관계가 있는 객체끼리 참조가 불가능함. 이는 객체적인 모델링이 아니다. 이와 같은 이격이 존재하는 이유는 테이블과 객체의 아래와 같은 차이점 때문

  • 테이블은 외래키로 조인( 키값의 데이터 )을 사용해서 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해서 연관된 객체를 찾는다

위와 같은 차이점말고, 테이블은 사실상 기본키 값이든 외래키 값이든 키값을 통해 방향에 상관없이 테이블에서 식별이 가능하다.
그러나 객체의 경우 참조는 단방향일 수 밖에 없다. 예를들어 아래와 같이 객체가 존재할 경우

public class Member{

    private Long memberId;
    private Team team;
    private String memberName;

    public Team getTeam(){ this.team }
}

public class Team{ 

    private Long teamId;
    private String teamName;
}

이제는Member에 단순 키값 Long teamId가 아닌 Team 객체 자체를 넣었기 때문에 Member.getTeam()을 통해서 객체의 참조가 가능하다. 그러나 Team의 경우 Member을 참조할 수 없다. 이것이 일반적인 객체의 참조이고, 객체의 참조는 테이블과 다르게 단방향으로만 가능

그렇다면 객체는 양쪽으로는 참조가 불가능할까? 아니다 각 테이블의 관계를 고려해서 아래와 같이 수정하면 양방향으로도 객체의 참조가 가능해진다. 예제는 MemberTeam이 N:1의 관계라고 가정해본다.

public class Member{

    private Long memberId;
    private Team team;
    private String memberName;

    public Team getTeam(){ return this.team }
}

public class Team{ 

    private Long teamId;
    private String teamName;
    private List<Member> members = ArrayList<>();

    public List<Member> getMembers() { return this.members; } 
}

위와 같이 객체를 설계할 경우, 단방향이 서로 설정이 되며, 이를 양방향이라고 표현한다. 이제 기존의 데이터 지향에서 객체지향으로 설계를 바꿨고, 이제 JPA의 입장에서 생각을 해보자. 만일 MemberteamTeammembers 중 어느 값이 변경 되었을 경우 테이블에 영향을 주어야 할까? 이를 설정해주는 것이 이번장의 핵심 연관관계 주인을 지정해주는 것이다.

연관관계 주인

기존의 MemberTeam을 엔티티로 변경하면 아래와 같이 변경할 수 있다.

@Enity
public class Member{

    @Id @GeneratedValue
    @Column( name = "member_id")
    private Long id;

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

    // 기타 필요 메소드 getter(), setter() 등
}

@Enity
public class Team{ 

    @Id @GeneratedValue
    @Column( name = "team_id")
    private Long id;
    private String teamName;

    @OneToMany(mappedBy = "team")
    private List<Member> members = ArrayList<>();

    // 기타 필요 메소드 getter(), setter() 등
}

주목해야 할 어노테이션은 @ManyToOne, @OneToMany, @JoinColumn 이 3가지이다. @ManyToOne, @OneToMany의 경우 이름처럼 테이블의 관계가 1:N인지 N:1인지 N:M인지 등 관계를 표현하는 어노테이션이고, @JoinColumn 어노테이션을 통해 해당 연관관계에서의 주인을 지정해주는 어노테이션이다.

또한 현재 양방향 연관관계이기 때문에 @OneToManymappedBy 속성을 통해서 연관관계의 주인이 아닌 반대 방향이라는 설정을 해줘야 한다.

위와 같이 설정하게 되면 연관관계의 주인을 설정할 경우에만 JPA가 테이블의 값을 변경하고, mappedBy로 설정된 값은 조회만 가능하게 된다.

누구를 연관관계 주인으로?

외래키가 있는 곳을 연관관계 주인으로 지정해라.

사실 객체의 입장에서 누구든 연관관계의 주인이 될 수 있지만, 테이블 기준 외래키가 들어가는 엔티티를 연관관계의 주인으로 설정하기를 권장한다. 아닐 경우 아래와 같은 문제가 발생

  • team을 연관관계 주인으로 설정할 경우
    • 성능상의 이슈, Team을 변경해도 Member의 update 쿼리가 발생한다.
    • team을 변경해도 member 테이블을 조작하는 쿼리가 발생해 개발 시 헷갈리게 된다. 즉 직관적이지가 않다.
주의할 점

위와 같이 연관관계 주인을 설정하고, mappedBy 속성을 통해 양방향 연관관계를 설정할 수 있게 되었다.
그렇지만 양방향 연관관계의 경우 아래와 같은 주의점이 발생한다.

  • toString(), lombok 사용시 무한 루프에 빠지게 된다. 재정의가 필요할 경우 양방향 연관관계를 고려해서 재정의 할 것
  • JSON 라이브러리를 통해 생성시에도 무한루프에 빠진다. 그래서 컨트롤러에서 entity를 반환하지 말고 DTO를 사용해라

비지니스 로직상 add할때 내가 있는지 체크 후 빼고, 추가 이런 로직이 필요하면 추가로 changeTeam에 설정..

실제는 단방향으로 모든 엔티티를 설계하고 필요에 의해서만 양방향을 고려해라.. 추가로 수정해도 기존 로직에 영향이 없음.. 그러나 JPQL에서 역방향으로 탐색할 일이 많기 때문에

가장 많이 하는 실수

연관관계 주인이 아닌곳은 조회만 가능한 점을 잘 생각하고 아래와 같은 로직이 있을 때 어떤 문제가 발생할까?

Team team = new Team();
em.persist(team);

Member member = new Member();

// 주인이 아닌 역방향만 연관관계 설정
team.getMembers().add(member);

em.persist(member);

위와 같이 작성시 mappedBy로 설정된 읽기전용 필드에만 값을 설정 시 MEMBER 테이블에 TEAM의 키 값이 null로 등록되는 현상이 발생, 그렇기 때문에 아래와 같이 수정

Team team = new Team();
em.persist(team);

Member member = new Member();
member.setTeam(team); // !!! 중요 연관관계 주인에 값을 넣어야함 !!!

// team.getMembers().add(member); 없어도 되지만 둘다 설정하는게 좋다.

em.persist(member);

team.getMember()의 경우 사실상 값을 설정해주지 않아도, JPA가 알아서 지연로딩 기법을 사용해 값을 참조하는 순간 알아서 값이 불러온다. 그치만 이와 같이 사용하면 JPA 메커니즘에 의해 오류가 발생 했을 때 값이 테이블과 동일하지 않는 문제가 발생할 수 있기 때문에 양쪽에 다 넣어주는게 유리하다. 또한 테스트 케이스 작성 시 한쪽으로만 데이터가 있는 경우가 발생하기 때문에 둘다 값을 넣어주는걸 추천

JPA 메커니즘에 의한 오류
Team team = new Team();
em.persist(team);

Member member = new Member();
member.setTeam(team); 

em.persist(member);

Team findTeam = em.find(Team.class, team.getId()); // 1차 캐쉬
List<Member> member = findTeam.getMembers();

위와 같이 동작할때 JPA 메커니즘에 의해 em.persist(team)로 등록된 엔티티는 DB에 저장되는게 아닌, 영속 컨텍스트에 저장되기 때문에 당연히 em.find(Team.class, team.getId()를 통해 불러와도 당연히 DB가 아닌 영속 컨텍스트에서 값일 받아옴.
그럼 해당 객체는 team.getMembers().add(member)를 실행하지 않았기 때문에 당연히 null을 반환 em.find()도 같은 객체를 반환하기 때문에 null 문제가 발생 그렇기 때문에 양쪽에 넣어주는게 좋다.

위와 같은 방법이 아니면 연관관계 편의 메소드를 만들것을 고려해라

// Member Entity
public void change(Team team) {
    this.team = team;
    team.getMembers().add(this);
}

또는 Team 엔티티에 설정해도 상관은 없다. 단 둘중 한곳에만 설정할 것!
또한 List를 사용하기 때문에 로직에 따라 기존의 List에 값이 있으면 찾아서 제거하고, 추가하기 등 필요한 로직은 비니니스 로직에 따라 추가해줘야 하는 경우도 생긴다.

정리

  • 객체와 테이블의 차이점 이해
  • 연관관계 설정 ( 연관관계 주인 파악하기 )
    • 외래키를 가진 테이블의 엔티티를 연관관계 주인으로
    • 연관관계에서 데이터 조작시 JPA 메커니즘을 이해하고 조작하기 ( 주인만 테이블을 조작 )
  • toString, lombok, JSON 사용시 무한참조 주의
  • 엔티티는 단방향으로 전부 설계, 필요 시 양방향을 추가할 것 그래도 기존의 테이블에 수정이 없기 때문에 필요시에 추가해도 충분하다.
반응형
Comments