0+ 스프링/0 + 스프링 ORM(JPA)

[JPA] 연관관계 매핑 개념(패러다임 불일치 해결)

힘들면힘을내는쿼카 2023. 2. 18. 01:41
728x90
반응형

[JPA] 연관관계 매핑 개념

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

테이블은 외래 키를 이용하여 자신과 연관관계가 있는 테이블을 탐색 할 수 있습니다.
하지만 객체는 다릅니다.
객체는 레퍼런스를 이용해서 자신과 연관관계가 있는 객체를 탐색할 수 있습니다.

다듬어서 이야기 하면 다음과 같습니다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾습니다.
객체는 참조를 사용해서 연관된 객체를 찾습니다.

이처럼 테이블과 객체사이의 패러다임의 불일치가 존재하게 되는데,
이를 해결한 기술이 바로 ORM 입니다.

 

패러다임 불일치 해결!!!

ORM을 사용하면, 객체의 참조를 이용해서 테이블의 외래키를 매핑 할 수 있습니다.
연관관계를 매핑을 하기위해서는 다음과 같은 용어가 등장합니다.

 

  • 방향
    • 단방향, 양방향
  • 다중성
    • 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
  • 연관관계 주인
    • 객체 양방향 연관관계는 관리 주인이 필요함

 

이러한 용어는 계속 등장하게 되니 너무 큰 걱정 안하셔도 됩니다.
이제 연관관계를 매핑하는 방법에 대해서 설명드리겠습니다.

 

 

연관관계가 필요한 이유

매핑하는 방법을 설명하기에 앞서
연관관계가 필요한 이유에 대해서 고민해봅시다.
불편하기 때문에 연관관계라는 개념을 넣었겠죠?
테이블에 맞추어 객체를 모델링하면 불편함을 피부로 느낄 수 있을것 같습니다.😎

 

객체지향의 사실과 오해 - YES24

『객체지향의 사실과 오해』는 객체지향이란 무엇인가라는 원론적면서도 다소 위험한 질문에 답하기 위해 쓰여진 책이다. 안타깝게도 많은 사람들이 객체지향의 본질을 오해하고 있다. 가장

www.yes24.com

 

객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.
조영호(객체지향의 사실과 오해)

라는 말이 있습니다.

 

협력 공동체 라는 의미를 다시한번 생각하면서 테이블에 맞추어 객체를 모델링 해봅시다.

 

테이블의 관계는 다음과 같습니다.

  • 회원과 팀이 존재
  • 회원은 하나의 팀에만 소속될수 있음
  • 회원과 팀은 다대일 관계

 

테이블에 맞추어 객체를 모델링

Member 객체에 Team객체의 id에 해당하는 teamId를 생성했습니다!(외래키를 직접 사용합니다.)

 

Member

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    @Column(name = "team_id")
    private Long teamId;
}

 

Team

@Entity
@Getter @Setter
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
}

 

TeamMember를 저장합니다.

// 팀 저장
Team team = new Team();
team.setName("SK T1");

em.persist(team);

// 회원 저장
Member member = new Member();
member.setName("Faker");
member.setTeamId(team.getId());

em.persist(member);

 

이와 같이 외래 키를 직접 사용하면…
다시 식별자로 조회해야 합니다.
객체 지향적인 방법은 아니네요!

// 조회
// Member와 Team은 연관관계가 없음
Member findMember = em.find(Member.class, member.getId());
Team findTeam = em.find(Team.class, team.getId());

 

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없습니다.


테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾습니다.
객체는 참조를 사용해서 연관된 객체를 찾습니다.
이것이 "패러다임의 불일치" 입니다.

 

이러한 불일치를 해결하기 위해서 등장한 ORM 기술 중 하나인 JPA는 객체 지향 모델링을 할수 있게 해줍니다.

 

객체 지향 모델링(양방향 매핑)

MemberteamId가 아닌 Team으로 매핑합니다.


이때, 테이블 연관관계는 전혀 변화가 없는것을 알수 있습니다.

 

Member

@Entity
@Getter @Setter
public class Member {
    @Id @GeneratedValue
    private Long id;
    @Column(name = "username")
    private String name;
    /**
     * 다대일 관계
     * 여러명의 회원은 하나의 팀에 소속 될 수 있음
       * 연관관계 주인
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
}

 

Team

@Entity
@Getter @Setter
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    /**
     * 일대다 관계
     * 하나의 팀에 여러명의 회원이 있음
     */
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

 

각 객체의 연관관계를 생각하며 저장합니다.

// 팀 저장
Team team = new Team();
team.setName("SK T1");
em.persist(team);

// 회원 저장
Member member = new Member();
member.setName("Faker");

// 연관관계 설정
member.setTeam(team);
team.getMembers().add(member);

em.persist(member);

 

연관관계를 이용하여 매핑했기 때문에 객체 탐색이 가능합니다.

// 조회
// Team을 Member의 연관관계로 조회 가능
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();

// 양방향 관계이기 때문에 Team에서도 Member를 조회할수 있습니다.
Team findTeam2 = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size();

 

연관관계 주인과 mappedBy

MemberTeam을 매핑할때 Team에서 mappedBy를 사용한것을 볼수 있습니다.
mappedBy를 설명하기 전에 객체와 테이블이 관계를 맺는 차이에 대해서 알아봅시다.

 

객체의 양방향 관계

객체의 양방향 관계는 단방향 연관관계 2개가 있다고 생각하면 이해하기 쉽습니다.
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들면 됩니다…!

  • 회원 ➡️ 팀
    • 연관관계 1개(단방향)
  • 팀 ➡️ 회원
    • 연관관계 1개(단방향)

 

테이블의 양방향 관계

테이블은 외래 키 1개로 2개의 테이블의 연관관계를 관리 할 수 있습니다.
외래 키 1개로 양방향 연관관계를 가질수 있습니다.

  • 회원 ↔️ 팀
    • 연관관계 1개(양방향)

 

객체의 양방향 관계

객체 입장에서 양방향 관계로 설정하면 Member에도 Team이 있고, Team에도 Member가 있습니다.
테이블 입장에서는 외래키는 한 곳에서만 관리되어야 합니다.

 

객체 입장에서도 1곳에서 외래키를 관리해야 하는데,
Member에서 Team으로 외래 키를 관리 할지
Team에서 Member로 외래 키를 관리 할지를 정해야합니다.


이렇게 외래 키를 관리(등록, 수정)할 곳을 정하는 것을 연관관계의 주인을 정한다라고 표현 합니다.
이때 mappedBy를 사용하여 연관관계 주인인지 아닌지 구분 합니다.
(주인이 아닌경우에 mappedBy를 사용함)

 

정리

  • 객체의 두 관계중 하나를 연관관계 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 연관관계의 주인이 아닌곳은 읽기만 가능
  • 주인은 mappedBy 속성 사용하지 않음
    • 외래키가 있는 곳
  • 주인이 아니면 mappedBy 속성으로 주인 지정

 

외래키가 존재하는 곳을 연관관계 주인으로 정하면 됩니다.
DB입장에서 외래 키가 있는 곳이 무조건 다(N) 입니다.
외래 키가 없는 곳이 무조건 일(1) 입니다.

 

따라서 다(N)쪽을 연관관계의 주인으로 정하면 됩니다.

 

양방향 매핑시 주의 사항

양방향 매핑을 하게되면 항상 연관관계를 고려해야 합니다.

 

연관 관계 주인에 값을 넣지 않는 경우

// 팀 저장
Team team = new Team();
team.setName("SK T1");
em.persist(team);

// 회원 저장
Member member = new Member();
member.setName("Faker");

// 연관관계 설정
team.getMembers().add(member);

em.persist(member);

 

Hibernate: 
    /* insert com.study.purejpa.entity.Team
        */ insert 
        into
            Team
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert com.study.purejpa.entity.Member
        */ insert 
        into
            Member
            (username, team_id, id) 
        values
            (?, ?, ?)

 

insert SQL은 2개가 실행되지만,
DB를 확인해보면 MEMBERTEAM_IDnull 입니다.

연관관계의 주인은 Member에 있는 Team이기 때문 입니다.
따라서 반드시 member.setTeam(team);을 해야 합니다.

 

 

연관 주인이 아닌 곳에 값을 넣지 않는 경우

그러면 연관관계 주인이 아닌곳에 값을 넣지 않으면 어떻게 될까요?

// 팀 저장
Team team = new Team();
team.setName("SK T1");
em.persist(team);

// 회원 저장
Member member = new Member();
member.setName("Faker");

// 연관관계 설정
member.setTeam(team);
//team.getMembers().add(member);

em.persist(member);

// 1차 캐시
Team findTeam = em.find(Team.class, team.getId());
for (Member m : findTeam.getMembers()) {
    // 값이 없음
    System.out.println("m="+m.getName());
}

 

for문에 대한 결과가 출력되지 않는것을 확인 할 수 있습니다.
왜 이러한 결과가 발생한 것일까요?
객체지향적으로 생각해보면 당연한 결과 입니다.
team.getMembers().add(member);를 하지 않았기 때문이죠!!! 😊

 

결론

순수 객체 상태를 고려해서 항상 양쪽에 값을 설정

member.setTeam(team);
team.getMembers().add(member);

 

연관관계 편의 메소드를 생성

public class Member {
    // 연관관계 편의 메소드
    public void changeTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}

 

// 팀 저장
Team team = new Team();
team.setName("SK T1");
em.persist(team);

// 회원 저장
Member member = new Member();
member.setName("Faker");

// 연관관계 편의 메소드 사용
member.changeTeam(team);

em.persist(member);

 

연관관계 편의 메소드는 일(1)에 넣어도 되고 다(N)에 넣어도 됩니다.
상황에 맞춰 개발하시면 됩니다.^^

 

양방향 매핑시 무한 루프 주의
(toString(), lombok, JSON 생성 라이브러리)

@Entity
@Getter @Setter
public class Team {

    @Override
    public String toString() {
        return "Team{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", members=" + members + // member의 toString() 호출
                '}';
    }
}

 

@Entity
@Getter @Setter
public class Member {

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", team=" + team + // team의 toString() 호출
                '}';
    }
}

 

 

이렇게 되면 양쪽으로 toString()을 무한으로 호출하게 됩니다.

Team findTeam = em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();

System.out.println("findTeam="+findTeam);

 

무한으로 호출하게 되면 java.lang.StackOverflowError를 발생하게 됩니다.
따라서 lombok으로 toString을 생성하는 것을 지양하는 것을 추천 드립니다.
그리고 Entity를 절대로 Controller에서 반환하는 것을 지양하셔야 합니다.

마지막으로!! 반드시 DTO를 사용해서 반환하시기 바랍니다.
그렇게 하면 JSON 생성 라이브러리로 무한 호출되는 문제가 발생할 경우는 거의 없습니다.

 

정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료 상태!
    • 먼저 단방향 매핑으로 설계를 완료하는 것을 권장 👍
    • 양방향 매핑을 할수록 개발자의 고민거리만 늘어간다..! 😭
    • 필요한 경우에 양방향 매핑 추가하는 방향으로 개발 진행 👩‍💻
      • 테이블에 영향을 주지 않음
      • 왜냐? 양방향은 테이블은 그대로 두고 java 코드만 추가하는 것 이기때문^^
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐임을 명심!
  • 연관관계의 주인은 외래 키의 위치를 기준으로 선정!

 

 

 

 

728x90
반응형