(JPA) Java 오브젝트 관계와 DB 테이블 관계는 어떻게 다른가?

  • by

Java 객체와 DB 테이블의 각 관련 관계는 어떻게 다른가요?
또한 JPA는 어떻게 이러한 불일치를 조정할 것인가?

ORM 기술은 DB의 테이블 중심 패러다임과 Java의 객체 지향 패러다임의 불일치를 해결하기 위해 도입되었다.

인플론 김영한 씨의 강의를 듣고 JPA에 접해 프로젝트를 진행했지만, 각 연관과 그 설정이 실제 코드에 어떻게 반영되는지, 테이블 구조가 어떻게 적용되는지 신경이 쓰여 이번 기회에 직접 코드를 써 DB의 상태를 확인해 보았다.

관련 관계

ORM 기술은 객체와 테이블 중심의 각각 다른 패러다임에서 발생하는 문제를 해결하기 위해 등장하여 개발자는 서비스 로직을 결합하면서 객체에 완전히 집중하여 개발할 수 있게 되어 DB 테이블 에 대하여 말썽을 극소화할 수 있다 있다.

다른 패러다임이지만 둘 다 연관관계존재합니다.

OOP에서의 연관은 오브젝트 간의 협력을 목표로 하고, DB의 테이블에서는 효율적으로 데이터를 적재/관리하기 때문입니다.

한 팀에서 여러 회원을 가질 수 있다면 팀과 회원의 관계는 1:N(일대다)이라고 할 수 있다.

이러한 관계를 각각의 입장에서 구현하면 다음과 같다.

테이블 세계의 관계

  • Member 테이블
MEMBER_ID NICKNAME TEAM_ID
0 10

  • 팀 테이블
TEAM_ID NAME
10 알파

각 테이블에서 TEAM_ID라는 외래 키(FK)를 기반으로 JOIN하여 관련 테이블을 찾습니다.


오브젝트 세계의 관계

public class Member {
    private Long id;
    private String nickname;
    // ...
}

public class Team {
    private Long id;
    private String name;
    Member() members;
    // ...
}

개체는 참조를 통해 관련 개체를 찾습니다.


그렇다면 JPA를 사용한 관련 관계 매핑

@Entity
public class Member {
    @Id @GeneratedValue
    private Long memberId;

    private String name;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID") 
    private Team team; // Team 객체
}

@Entity
public class Team {
    @Id @GeneratedValue
    private Long teamId;
    private String nickname;
}
Team team = new Team();
team.setName("Alpha");
em.persist(team);

Member member = new Member();
member.setUserName("John");
member.setTeam(team); // Member에 직접 Team객체 주입
em.persist(member);

Member findMember = em.find(Member.class, member.getId());
//Team findTeam = em.find(Team.class, findTeamId);
Team findTeam = findMember.getTeam();

@ManyToOne, @JoinColumn 를 통해 관련 관계를 명시한다.


그러나 이때 Member에서는 Team에서 액세스가 가능하지만 Team에서는 소속 Member에 액세스할 수 없다.


이와 같이 한쪽만 참조/액세스가 가능한 관계를 단방향 관련 관계라고 한다.

위의 코드를 활용한 테이블 상황

  • Member 테이블
MEMBER_ID AGE NICKNAME TEAM_ID
1 20 1

  • 팀 테이블
TEAM_ID NAME
1 알파


양방향 관련 관계 매핑

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String nickname;

    @ManyToOne // Member의 입장에서 Many가 된다.

@JoinColumn(name = "TEAM_ID") private Team team; // Team 객체 } @Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long teamId; @Column(name = "USERNAME") private String name; @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); // 추가된 필드 }

1 : N 관계에서 1에 해당하는 팀에 @OneToMany를 통해 Member 객체(테이블)를 매핑합니다.


mappedBy속성을 통해 해당 외부 오브젝트의 필드 이름을 지정합니다.

Team team = new Team();
team.setName("Alpha");
em.persist(team);

Member member = new Member();
member.setUsername("John");
member.setTeam(team);
em.persist(member);

em.flush();
em.clear();

Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers(); // 해당 팀에 속한 모든 멤버 로드

위의 예에 따른 데이터베이스 상태

  • Member 테이블
MEMBER_ID AGE NICKNAME TEAM_ID
1 20 1

  • 팀 테이블
TEAM_ID NAME
1 알파

앞의 예제 코드와 다른 점은 Team에서 Members가 추가되었다는 것입니다.


Members에 직접 추가하지 않았지만, mappedBy옵션을 사용하면 실제 쿼리에서 join을 통해 검색할 수 있습니다.

select
        m1_0.member_id,
        m1_0.nickname,
        t1_0.team_id,
        t1_0.name 
    from
        member m1_0 
    left join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        m1_0.member_id=?

위의 코드는 팀 구성원을 쿼리하는 SQL 쿼리입니다.


관련 관계 소유자(Owner) 및 mappedBy

오브젝트에서 양방향 연관은 실제로 두 개의 단방향 연관으로 구현됩니다.

class A {
    B b;
}

class B {
    A a;
}

A a = new A();
B b = new B();
a.b = b;
b.a = a;

반면에 테이블 구조에서는 양방향 연결이 외래 키를 통해 한 번에 구현됩니다.

SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

SELCT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

외래 키 관리

  • in 테이블
    Member의 TEAM_ID(FK)만을 변경하면 된다.

  • in 객체
    개체에서 memberA가 Team A에 속할 때 memberA의 Team B로 변경하려면 Team의 List 필드를 변경하거나 MemberA의 Team 필드를 변경해야 합니다.

이 차이가 있습니다.

DB의 입장에서는, 외래 키가 존재하는 쪽이 무조건인 (Many) 상태이며, 참조하는 쪽은 일 (One)일 가능성이 높다.

따라서 외래 키를 관리하는 소유자는 Many (위의 예에서는 Member)가됩니다.

  • 양방향 매핑 규칙에서 ‘김용한 강의’
    • 객체의 두 관계 중 하나를 관련 관계의 소유자로 지정
    • 관련 관계의 소유자는 외래 키의 위치를 ​​기반으로 합니다.

      -> Many 쪽이 주인
    • 관련 관계의 소유자만 외래 키 관리(등록 및 수정)
    • 소유자 이외의 분은 읽고 참조하십시오
    • 소유자는 mappedBy 속성을 사용합니다.

    • 소유자가 아닌 경우 mappedBy 속성으로 소유자 지정

Bad Practice

Team team = new Team();
team.setName("Alpha");
em.persist(team);

Member member = new Member();
member.setName("John");

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

위 코드에 대한 DB 상태

  • Member 테이블
MEMBER_ID NICKNAME TEAM_ID
1

  • 팀 테이블
TEAM_ID NAME
1 알파

상기의 예 코드를 실행하면, 실제의 DB에 Member 테이블에 TEAM_ID(FK)는 null가 들어가게 된다.

member에서 team에 대한 지정을 해주지 않았기 때문이다.

Recommend Practice

Team team = new Team();
team.setName("Alpha");
em.persist(team);

Member member = new Member();
member.setName("John");
member.setTeam(team); // Member-Team간의 연관관계를 맺도록 하는 코드 부분
em.persist(member);

team.getMembers().add(member); // 연관관계 매핑에 대해 명시적으로 표현하기 위함
// in Team 클래스
// 메소드에서 양방향 지정하는 것이 좋다.

public void addMember(Member member) { member.setTeam(this); this.members.add(member); }

Java(JPA) 레벨에서는 단방향 맵핑만으로 이미 관련 관계가 맵핑되었으며 DB에서 FK를 통해 관계가 확립되었습니다.

양방향 매핑과의 차이점은 그래프 검색 기능을 사용하여 Java 레벨에서 반대 방향(Team->Member)을 쿼리할 수 있다는 것입니다.

따라서, 양방향 맵핑은 필요에 따라 반대 방향 조회가 필요한 경우에 적용해야 합니다.

이것은 테이블에 영향을 미치지 않습니다.

📝 가능하면 양방향 매핑을 피하십시오.

  • 양방향 매핑은 순환을 유발할 수 있습니다.

  • 양방향으로 설정하면 엔티티 간의 관계 복잡성을 높일 수 있습니다.

아 그래.

애매하게 일어나고 있는 상황이 직접 코드로 작성해, DB를 바라보면서 보다 명확하게 이해할 수 있었다.

역시 영한센세가 언제나 말씀하던 시라몬이 불필요하다.

‘스프링 딥 다이브’ 스터디를 진행하면서 다시 한번 정리해 직접 샘플 코드를 만들어 진행했다.

시간이 꽤 들렸지만 ‘나’의 것으로 완전히 만들 수 있는 시간으로 남았다.

다음으로 스터디에서 주어진 질문에 대해 분석한 내용에 대해 적어 보겠습니다.