기록

[자바 ORM 표준 JPA 프로그래밍] JPA 연관관계 매핑 기초: 단방향과 양방향 연관관계 (5장) 본문

교육/책

[자바 ORM 표준 JPA 프로그래밍] JPA 연관관계 매핑 기초: 단방향과 양방향 연관관계 (5장)

youngyin 2024. 11. 23. 01:04

이번 글에서는 JPA의 엔티티 매핑에 대해 정리해보았습니다. 이 글은 '자바 ORM 표준 JPA 프로그래밍'의 5장 '연관관계 매핑 기초' 부분을 읽고, 제 경험과 함께 정리한 내용입니다. Java ORM 표준인 JPA를 사용하다 보면, 객체 사이의 관계를 데이터베이스 테이블로 표현하는 일이 자주 발생합니다. 이번 포스팅에서는 JPA의 연관관계 매핑을 이해하기 위해 단방향과 양방향 연관관계의 개념을 살펴보고, 실제 예제를 통해 이를 어떻게 구현하는지에 대해 설명해 보겠습니다. 연관관계 매핑은 객체 모델과 데이터베이스 모델 간의 연결을 효과적으로 관리할 수 있게 도와주는 핵심적인 요소입니다.

1. 객체와 테이블의 연관관계

객체와 데이터베이스의 연관관계는 표현 방식에서 차이가 있습니다.

  • 객체 연관관계: 객체 지향의 세계에서는 객체 간의 관계를 참조를 통해 쉽게 표현할 수 있습니다. 예를 들어, 한 객체가 다른 객체를 필드로 포함하는 방식(member.getTeam(), team.getMembers())으로 관계를 나타냅니다.
  • 데이터베이스 연관관계: 반면, 관계형 데이터베이스에서는 테이블 간의 관계를 정의하기 위해 외래 키를 사용하고, 데이터를 조회할 때는 조인(join) 연산을 통해 테이블을 연결합니다.

예를 들어, 회원과 팀 테이블 간의 관계를 조회하기 위해 다음과 같은 SQL 쿼리를 사용할 수 있습니다.

SELECT m.*
FROM Member m
JOIN Team t ON m.team_id = t.team_id
WHERE t.teamName = 'team1';

이 쿼리는 MemberTeam 테이블을 외래 키로 연결하고, 특정 팀에 속한 모든 회원을 조회하는 방식입니다. 이러한 SQL 조인을 통해 테이블 간의 관계를 탐색하고 필요한 데이터를 가져올 수 있습니다.

이러한 차이는 객체 모델과 관계형 모델 간의 간극을 의미하며, JPA는 이 간극을 해소하기 위해 설계되었습니다. JPA를 사용하면 @ManyToOne, @OneToMany와 같은 어노테이션을 활용하여 객체 간의 관계를 데이터베이스 테이블의 관계로 쉽게 매핑할 수 있습니다.

예시: 팀과 회원 관계

아래와 같은 Team과 Member 모델이 있다고 가정해 봅시다. 팀과 회원 간에는 다대일 관계가 성립됩니다. 이를 단방향 연관관계로 표현하면 다음과 같습니다.

@Getter
@Setter
@NoArgsConstructor
@Entity
class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;
    private String memberName;

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

@Getter
@Setter
@NoArgsConstructor
@Entity
class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long teamId;
    private String teamName;
}

이 예시에서 Member 객체는 Team 객체를 참조하고 있으며, @JoinColumn을 통해 팀의 외래 키(team_id)가 설정됩니다. 이를 통해 데이터베이스에서 회원과 팀이 어떻게 연결되어 있는지를 나타낼 수 있습니다.

2. 연관관계 사용 방법

2.1 연관관계 저장

객체 간 연관관계를 매핑한 후에는 데이터를 저장할 수 있습니다. 다음 코드를 통해 팀과 회원을 저장하는 예를 살펴보겠습니다.

Team team1 = new Team();
team1.setTeamName("team1");
entityManager.persist(team1);

Member member1 = new Member();
member1.setMemberName("member1");
member1.setTeam(team1);
entityManager.persist(member1);

Member member2 = new Member();
member2.setMemberName("member2");
member2.setTeam(team1);
entityManager.persist(member2);

여기서 중요한 점은 member1.setTeam(team1)과 같이 객체 간의 참조 관계를 설정해준다는 것입니다. 이를 통해 JPA가 자동으로 외래 키를 관리하게 됩니다.

2.2 연관관계 조회

저장된 연관관계를 조회할 때는 객체 그래프 탐색을 사용하거나 JPQL을 활용할 수 있습니다.

객체 그래프 탐색이란 객체 간의 연관관계를 따라가며 필요한 데이터를 조회하는 방식입니다. 객체 그래프 탐색을 사용하는 경우, 다음과 같이 간단하게 팀 정보를 조회할 수 있습니다.

Member member = entityManager.find(Member.class, "member1");
Team team = member.getTeam();

또는 JPQL을 사용해 좀 더 복잡한 조건을 통해 데이터를 조회할 수도 있습니다.

String jpql = "select m from Member m join m.team t where t.teamName = :teamName";
List<Member> members = entityManager.createQuery(jpql, Member.class)
                                     .setParameter("teamName", "team1")
                                     .getResultList();

3. 양방향 연관관계

3.1 양방향 연관관계 매핑

양방향 연관관계는 객체 간 참조가 서로를 가리키는 구조를 의미합니다. 예를 들어, Team이 자신에게 속한 Member들을 참조하려면 아래와 같은 구조로 구현할 수 있습니다.

@Getter
@Setter
@NoArgsConstructor
@Entity
class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long teamId;
    private String teamName;

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

@Getter
@Setter
@NoArgsConstructor
@Entity
class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long memberId;
    private String memberName;

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

여기서 mappedBy 속성은 양방향 관계에서 연관관계의 주인이 아닌 곳을 명시하는데 사용됩니다. 주인은 외래 키를 관리하며, 주인이 아닌 쪽은 읽기 전용으로 동작합니다.

3.2 연관관계의 주인

양방향 연관관계를 설정할 때 가장 중요한 것은 "연관관계의 주인"을 정하는 것입니다. JPA에서는 연관관계의 주인만이 외래 키를 업데이트할 수 있습니다.

  • 연관관계의 주인: 연관관계의 주인은 외래 키를 가진 엔티티입니다. 주인을 정하는 기준은 실제 데이터베이스의 외래 키를 누가 관리하느냐에 달려 있습니다. 예를 들어, MemberTeam 간의 관계에서 Member가 외래 키(team_id)를 가지기 때문에 Member가 연관관계의 주인이 됩니다. 연관관계의 주인은 데이터 변경 시 외래 키 값을 업데이트하거나 저장하는 역할을 수행합니다.
  • 주인이 아닌 쪽: 반면에 주인이 아닌 쪽은 단순히 읽기 전용으로 작동합니다. 이 예제에서는 Member가 연관관계의 주인이고, Team은 읽기 전용으로 설정됩니다.

이로 인해 데이터를 저장할 때에는 반드시 연관관계의 주인 쪽에 값을 설정해줘야 합니다. 그렇지 않으면 데이터베이스의 외래 키가 제대로 설정되지 않으며, 연관관계에 문제가 발생할 수 있습니다.

Member member = new Member();
Team team = new Team();

member.setTeam(team); // 연관관계의 주인에 값을 설정해야 DB에 반영됩니다.
team.getMembers().add(member); // 이 부분은 선택적이며 객체의 일관성을 위해 사용하는 것이 좋습니다.

4. 연관관계 편의 메소드

양방향 연관관계를 설정할 때는 객체의 일관성을 유지하기 위해 연관관계 편의 메소드를 작성하는 것이 좋습니다. 예를 들어, Member 클래스에 setTeam 메소드를 정의하여 Team에도 회원을 자동으로 추가하도록 할 수 있습니다.

public void setTeam(Team team) {
    // 기존 팀과의 관계를 제거
    if (this.team != null) {
        this.team.getMembers().remove(this);
    }
    // 새로운 팀으로 설정
    this.team = team;
    team.getMembers().add(this);
}

이렇게 작성하면 양쪽의 연관관계가 항상 일치하게 되어 객체의 일관성을 유지할 수 있습니다. 이를 통해 실수로 인해 양방향 관계가 깨지는 것을 방지할 수 있습니다.

5. 정리

주요 개념 요약

  • 단방향 vs 양방향 연관관계: 단방향은 한쪽 객체만 참조하고, 양방향은 서로를 참조하는 구조입니다. 양방향 매핑 시에는 연관관계의 주인을 정하는 것이 중요합니다.
  • 객체와 테이블의 차이: 객체는 참조를 통해 관계를 표현하고, 데이터베이스는 외래 키와 조인(join)을 사용해 관계를 표현합니다. 이를 JPA가 매핑해주는 역할을 합니다.
  • 연관관계의 주인: 외래 키를 가진 쪽이 연관관계의 주인이 됩니다. 주인은 외래 키를 업데이트할 수 있으며, 데이터의 변경을 반영합니다.

단방향과 양방향 매핑의 개념을 이해하고, 연관관계의 주인을 잘 설정하는 것은 데이터의 일관성을 유지하는 데 필수적입니다. 또한, 연관관계 편의 메소드를 사용해 객체 간의 관계를 명확히 설정해 주는 것이 좋습니다.

실무에서는 데이터베이스 설계 시 외래 키를 적극적으로 사용하지 않는 경우도 종종 있습니다. 특히 비즈니스 요구사항이 자주 변경되는 상황에서는 외래 키 제약 조건이 많은 부분을 복잡하게 만들기 때문에, 이를 선언하더라도 제약 조건을 엄격하게 두지 않는 것이 일반적입니다. 이런 이유로 비즈니스 변경이 필요할 때, 데이터베이스 구조를 대폭 수정하지 않고도 대응할 수 있게 됩니다.

JPA에서도 이러한 제약 조건을 반드시 명시해야 하는 것은 아닙니다. JPA는 객체 간의 연관관계를 기반으로 매핑을 관리하며, 데이터베이스 수준에서의 제약 조건 설정은 개발자의 선택에 달려 있습니다. 따라서 비즈니스의 유연성을 높이기 위해, 상황에 따라 제약 조건을 추가하거나 생략하는 방식으로 설계를 조정하는 것이 중요합니다.

Comments