연관관계 매핑 기초 #1 단방향 연관관계
자바 ORM 표준 JPA 프로그래밍 - 기본편
이 글은 김영한의 자바 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
- 회원(MEMBER)의 TEAM_ID로 회원(MEMBER) 테이블과 팀(TEAM) 테이블을 조인할 수 있다.
참조를 통한 연관관계는 언제나 단방향이다. 객체간의 관계를 양방향으로 만들기 위해서는 반대편 객체에도 필드를 추가해야 한다. 즉, 연관관계를 하나 더 만들어 양쪽에서 서로 참조하도록 만들어야 한다. 이처럼 양족에서 서로 참조하는 것을 양방향 연관관계라 한다.
반면, 테이블은 외래 키 하나로 양방향으로 조인할 수 있다.
[ 객체 연관관계 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 |
연관된 엔티티의 타입 정보를 설정한다. 거의 사용하지 않는 기능이다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다. |