STUDY/JPA

연관관계 매핑 기초 #1 단방향 연관관계

Anne of Green Galbes 2020. 1. 29. 14:02

이 글은 김영한의 자바 ORM 표준 JPA 프로그래밍을 보고 작성한 글임을 알려드립니다.

관련 강의 : 인프런스 자바 ORM 표준 JPA 프로그래밍 - 기본편

들어가기 전에

다음은 연관관계 매핑을 이해하기 위한 핵심 용어들이다.

  • 방향(Direction)
    • 단방향 관계 : 두 엔티티가 관계를 맺을 때, 한 쪽의 엔티티만 참조하고 있는 것을 의미한다.
    • 양방향 관계 : 두 엔티티가 관계를 맺을 때, 양 쪽이 서로 참조하고 있는 것을 의미한다.
    • 데이터 모델링에서는 관계를 맺으면 자동으로 양방향 관계가 되어서 서로 참조하지만, 객체 지향 모델링에서는 구현하고자 하느 서비스에 따라 단방향 관계인지 양방향 관계인지 적절한 선택을 해야한다.
    • 양방향 관계란 서로 다른 단방향 연관관계 2개를 로직으로 잘 묶어서 양방향인 것처럼 보이게 한 것이다.
      • 실제로 양방향 연관관계란 존재하지 않는다.
  • 다중성(Multiplicity)
    • Many To One : 다대일(N : 1)
    • One To Many : 일대다(1 : N)
    • One To One : 일대일(1 : 1)
    • Many To Many : 다대다(N : M)
    • 어떤 엔티티를 중심으로 상대 엔티티를 바라 보느냐에 따라 다중성이 달라진다.
      • 카테고리는 많은 서적을 가지고 있다 : 카테고리 입장 - 일대다, 서적 입장 - 다대일
  • 연관관계의 주인(Owner)
    • 연관관계에서 주인을 결정한다.
    • 객체를 양방향 연관관계로 만들려면 연관관계의 주인을 정해야 한다.
    • 연관관계를 갖는 두 테이블에 대해서 외래키를 갖는 테이블이 연관관계의 주인이 된다.
    • 연관관계의 주인만이 외래키를 관리(등록, 수정, 삭제) 할 수 있고, 주인이 아닌 엔티티는 읽기만 할 수 있다.

1. 단방향 연관관계

단방향 연관관계를 알아보기 전에 간단한 예제를 통해 객체 연관관계와 테이블 연관관계의 차이점을 먼저 알아보자.

 

다음 조건으로 테이블을 만들어보자

  • 회원과 팀이 있다.
  • 회원은 하나틔 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계이다.

다대일 연관관계

이제 위 그림을 객체 연관관계, 테이블 연관관계로 각각 분석을 해보자.

  • 객체 연관관계
    • 회원(Member) 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다.
    • 회원(Member) 객체와 팀(Team) 객체는 단방향 관계이다.
      • 회원(Member) 객체는 Member.team 필드로 팀을 알 수 있지만, 팀(Team)객체는 멤버를 알 수 없다.
      • member → team : member.getTeam()으로 가능하다.
      • team → member : 회원(Member) 객체에 접근할 수 있는 필드가 없다.
  • 테이블 연관관계
    • 회원(MEMBER) 테이블은 TEAM_ID를 외래키(FK)로 팀(TEAM) 테이블과 연관관계를 맺는다.
    • 회원(MEMBER) 테이블과 팀(TEAM) 테이블은 양방향 관계이다.
      • 회원(MEMBER)의 TEAM_ID로 회원(MEMBER) 테이블과 팀(TEAM) 테이블을 조인할 수 있다.
           SELECT *
        FROM MEMBER M
        JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
      • 회원(MEMBER)의 TEAM_ID로 팀(TEAM) 테이블과 회원(MEMBER) 테이블을 조인할 수 있다.
           SELECT *
        FROM TEAM T
        JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

참조를 통한 연관관계는 언제나 단방향이다. 객체간의 관계를 양방향으로 만들기 위해서는 반대편 객체에도 필드를 추가해야 한다. 즉, 연관관계를 하나 더 만들어 양쪽에서 서로 참조하도록 만들어야 한다. 이처럼 양족에서 서로 참조하는 것을 양방향 연관관계라 한다.

반면, 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.

[ 객체 연관관계 vs 테이블 연관관계 정리 ]
  • 객체는 참조(주소)로 연관관계를 맺는다.
  • 테이블은 외래 키로 연관관계를 맺는다.
  • 참조를 사용하는 객체의 연관관계는 단방향이다.
    • A → B
  • 외래키를 사용하는 테이블의 연관관계는 양방향이다.
    • A JOIN B가 가능하면 반대로 B JOIN A도 가능하다.
단방향 연관관계 vs 양방향 연관관계

1. 단방향 연관관계
class A {
	B b;
}
class B {}

2. 양방향 연관관계

class A {
	B b;
}
class B {
	A a;
}

 

1) 객체 연관관계

순순하게 객체만 사용한 연관관계에 대해 더 살펴보자. 다음은 JPA를 사용하지 않은 회원과 팀 클래스이다.

public class Member {
    private String id;
    private String username;
    
    //팀의 참조를 보관
    private Team team;
    
    public void setTeam(Team team) {
        this.team = team;
    }
    
    //getter, setter 생략
}

public class Team {
    private String id;
    private String name;
    
    //getter, setter 생략
}

이제 회원1과 회원2를 등록하고, 팀1에 소속시키는 작업을 하자.

public static void main(String[] args) {
    Member member1 = new Member("member1", "회원1");
    Member member2 = new Member("memebr2", "회원2");
    Team team = new Team("team1", "팀1");
    
    member1.setTema(team);
    member2.setTeam(team);
    
    //회원1에 속한 팀1 조회 → 객체 그래프 탐색
    Team findTeam = member1.getTeam();
}

 

객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이를 객체 그래프 탐색이라 한다. 위 코드에서는 Team findTeam = member1.getTeam() 부분이다.

 

2) 테이블 연관관계

그럼 이제 데이터베이스 테이블의 회원과 팀의 관계에 대해 살펴보자. 다음은 회원과 팀 테이블을 만드는 DDL이다.

CREATE TABLE MEMBER (
    MEMBER_ID VARCHAR(255) NOT NULL,
    TEAM_ID VARCHAR(255),
    USERNAME VARCHAR(255),
    PRIMARY KEY (MEMBER_ID)
);

CREATE TABLE TEAM (
    TEAM_ID VARCHAR(255) NOT NULL,
    NAME VARCHAR(255),
    PRIMARY KEY (TEAM_ID)
);

테이블을 만들었으면 이제 외래키를 설정 해 주자.

ALTER TABLE MEMBER 
ADD CONSTRAINT FK_MEMBER_TEAM
FOREIGN KEY (TEAM_ID)
REFERENCES TEAM;

마지막으로 회원1과 회원2를 팀1에 소속시켜준다.

INSERT INTO TEAM(TEAM_ID, NAME) VALUES ('team1', '팀1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ('member1', 'team1', '회원1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES ('member2', 'team1', '회원2');

 

3) 객체 관계 매핑

객체와 테이블의 연관관계를 알아봤으니 이제 JPA를 사용해서 둘을 매핑해보자.

@Entity
public class Member {
    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;

    //연관관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    //연관관계 설정
    public voud setTeam(Team team) {
        this.team = team;
    }
    
    //getter, setter 생략
}

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;
    
    private String name;

    //getter, setter 생략
}

연관관계를 매핑하기 위해 @ManyToOne 어노테이션과 @JoinColumn 어노테이션이 사용되었다. 

@ManyToOne 어노테이션은 다대일 관계라는 매핑 정보이며, @JoinColumn 어노테이션은 외래 키를 매핑할 때 사용한다. 두 어노테이션에 대한 설명은 글 하단에 정리 해 놓았으니 참고바란다.

 

4) 연관관계 저장

이제 매핑한 연관관계를 사용할 차례이다. 제일 먼저 연관관계를 매핑한 엔티티를 어떻게 저장하는지 알아보자.

//팀 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);

//회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); //연관관계 설절 memebr1 → team1
em.persist(member1);

//회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); //연관관계 설절 memebr2 → team1
em.persist(member2);

 

5) 연관관계 조회

연관관계를 조회하는 방법은 두 가지이다. 객체 그래프를 탐색하는 방법과 객체지향 쿼리(JPQL)을 사용하는 방법이다.

객체 그래프 탐색은 객체를 통해 연관된 엔티티를 조회하는 것을 말한다. 아래 예제를 살펴보자.

Member member = new Member(Member.class, "member1");

//객체 그래프 탐색
Team team = member.getTeam();
System.out.println("팀이름 = " + team.getName()); //출력 : 팀이름 = 팀1

member.getTeam()을 통해 member와 관련된 team 엔티티를 조회할 수 있다.

 

6) 연관관계 수정

수정은 em.update() 같은 메소드가 없다. 그저 엔티티의 값만 변경해두면 트랜잭션이 커밋할 때 플러시가 일어나면서 변경된 곳을 자동으로 감지하고, 변경사항을 데이터베이스에 자동으로 반영한다.

//새로운 팀2
Team team2 = new Team("team2", "팀2");
em.persist(team2);

//회원1에 새로운 팀2 설정
member1.setTeam(team2);

 

7) 연관된 엔티티 삭제

연관된 엔티티를 삭제하기 위해서는 기존에 있던 연관관계를 제거하고 삭제해야 한다. 외래 키 제약조건이 걸려있기 때문이다.

//연관관계 제거
member1.setTeam(null)
member2.setTeam(null)

//팀 엔티티 삭제
em.remove(team);

 

8) @JoinColumn, @ManyToOne

@JoinColumn

속성 기능 기본값
name 매핑할 외래 키 이름 필드명 +_+ 참조하는 테이블의 기본 키 컬럼명
referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본 키 컬럼명
foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다.  

unique

nullable

insertable

updatable

columnDefinition

table

@Column의 속성과 같다  

@ManyToOne

속성 기능 기본값
optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. true
fetch 글로벌 패치 전략을 설정한다.

@ManyToOne=FetchType.EAGER

@ManyToMany=FetchType.LAZY

cascade 영속성 전이 기능을 사용한다.  
targetEntity

연관된 엔티티의 타입 정보를 설정한다.

거의 사용하지 않는 기능이다.

컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.