본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
JPA

연관관계 매핑 기초 - 1

by 방구석 대학생 2020. 9. 27.

"인프런 - 자바 ORM 표준 JPA 프로그래밍 강의를 듣고 작성한 글 입니다."

www.inflearn.com/course/ORM-JPA-Basic#

 

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

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

www.inflearn.com

 

이번엔 도메인 객체를 모델링 하는데 있어, 테이블에 맞춰 외래 키를 가져오는 방식으로 설계하는 것이 아니라

연관관계(Relationship) 를 맺어서 어떻게 좀 더 객체 지향스럽게 설계 하는지 알아보자.

 

연관관계(Relationship) 는 객체와 관계형 DB의 패러다임 차이에서 오는 것 중 가장 어려운 내용이라고 할 수 있다.

객체가 지향하는 패러다임과 관계형 DB 가 지향하는 패러다임이 다름으로 인해 생기는 어려움이 있는 것이다.

 

객체는 참조(reference) 를 통해 따라가는데 반해, 테이블은 외래 키 값을 이용한다.

여기서 객체의 참조와 테이블의 외래키를 어떻게 매핑할 수 있을까?

 

 

연관관계(Relationship) 은 어떤 이유 때문에 필요할까?

우선 다음과 같은 예제 시나리오를 토대로 객체를 테이블에 맞추어 모델링 해보자.

- 회원과 팀이 있다.

- 회원은 하나의 팀에만 소속될 수 있다.

- 회원과 팀은 다대일(N : 1) 관계이다.

 

위의 시나리오를 따라서 객체를 테이블 맞추어 모델링 해보면 아래와 같은 UML 다이어그램 을 얻을 수 있다.

 

그리고 위의 UML 다이어그램을 참조하여 도메인 클래스를 생성한다.

- Member.java

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

	@Column(name = "USERNAME")
	private String name;

	@Column(name = "TEAM_ID") // 참조 대신 외래 키를 그대로 사용한다.
	private Long teamId;
	//DB 테이블에 맞춰 모델링을 하고 있기 때문에 해당 필드를 직접 작성해주었다.(Team 클래스의 id 필드)

	// Getter, Setter ....
}

- Team.java

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

	// Getter, Setter ....
}

 

이후 외래 키 식별자를 직접 다루는 코드를 작성한다.

- JpaMain.java

// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team); // 영속성 컨텍스트에 올라갈 때 항상 기본 키가 같이 매핑된다.

// 회원 저장
Member member = new Member();
member.setName("member1"); 
member.setTeamId(team.getId()); // 객체지향 스러우려면 setTeam 이 되어야 하지 않을까?
em.persist(member);

Member findMember = em.find(Member.class, member.getId());

Long findTeamId = findMember.getTeamId(); 
Team findTeam = em.find(Team.class, findTeamId);
// 테이블 중심으로 설계하는 바람에 이번엔 팀 아이디를 찾으면서 또다시 기본 키를 찾는 과정을 반복하게 된다.
//기본 키 값을 한 번 찾는 걸로 팀을 알아낼 수는 없을까?

 

위의 예제 및 코드와 같이 연관관계가 없는 객체의 경우, 객체를 단순히 테이블에 맞추어 모델링 하여 표현한 UML 다이어그램에 따라 테이블에 맞추어 객체를 설계하면 teamId 와 같은 외래 키 값을 그대로 가져오게 된다.

 

아래는 이를 증명하는 hibernate message 및 SQL 이다.

Hibernate: 
    
    drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1 
// 내부적으로 시퀀스 전략을 활용하였기에 기본 키 값이 1로 매핑되었다.

Hibernate: 
    
    create table Member (
       MEMBER_ID bigint not null,
        TEAM_ID bigint, // Member 테이블에서 Team 테이블의 기본 키 값을 외래키 값으로 가져왔다.
        USERNAME varchar(255),
        primary key (MEMBER_ID)
    )
Hibernate: 
    
    create table Team (
       TEAM_ID bigint not null,
        name varchar(255),
        primary key (TEAM_ID)
    )

 

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

-> 테이블은 외래 키로 Join 을 사용해서 연관된 테이블을 찾고, 객체는 참조를 사용해서 연관된 객체를 찾는다는 큰 차이점이 있다.

 

 

객체를 객체 지향적으로 모델링을 할 경우 협력관계의 참조값을 그대로 들고 올 수 있다.

아래는 객체 연관관계를 활용하여 객체 지향적으로 모델링을 했을 경우 그릴 수 있는 UML 다이어 그램이다.

위의 다이어그램을 보면 Member 클래스에서 teadId(외래 키) 값을 가져왔던 이전과 달리 Team 클래스의 참조값을 그대로 가져오고 있다.

 

그런데 여기서 직접 코드로 작성할 때 Team 클래스의 객체를 도메인 클래스에 그냥 선언해두는 것 보다 연관관계가 무엇인지 JPA 에게 알려줘야 한다.(아니면 오류가 발생한다.)

일 대 다(1 : N) 관계의 경우 어느쪽이 일 이고 어느쪽이 다 인지가 중요하다.

(DB 관점으로 중요함, 어노테이션 들은 다 데이터베이스와 매핑하는 어노테이션 들이다.)

예제의 경우 team 이 1, member 가 N 이다.

 

- Member.java

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

	@Column(name = "USERNAME")
	private String username;

	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
	// team 참조값과 데이터베이스의 외래 키(TEAM_ID) 가 매핑되어야 한다.

	// Getter, Setter ....
}

위와 같이 객체 연관관계 매핑을 완료하고 나면 굳이 외래 키 값을 가져올 필요 없이 멤버 데이터에 setTeam 메소드를 이용해 Member 클래스 객체와 Team 클래스 객체간 단방향 연관관계 설정을 완료할 수 있다.

 

- JpaMain.java

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

Member member = new Member();
member.setName("member1");
member.setTeam(team); //  단방향 연관관계 설정, 참조 저장
// 이렇게 해두면 JPA 가 알아서 team 에서 기본 키를 꺼낸 후 insert 할 때 외래 키 값으로 사용한다.
em.persist(member);

Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam(); // teamId 값을 찾을 필요 없이 바로 Team 데이터를 꺼낼 수 있다.

 

위와 같이 객체지향 스럽게 코딩 했을 경우 hibernate SQL 은 다음과 같이 출력된다.

Hibernate: 
    /* insert hellojpa.Team
        */ insert 
        into
            Team
            (name, TEAM_ID) 
        values
            (?, ?)
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (TEAM_ID, USERNAME, MEMBER_ID) 
        values
            (?, ?, ?)

 

Setter 메소드를 통해 객체에 데이터를 insert 해준 이후 persist 메소드를 호출함으로 인해 Entity 가 1차 캐시에 올라가게 됨으로서, 이후 find 메소드를 통해 Member 데이터를 찾아와도 select 쿼리가 데이터베이스에 전달되지 않는다.

만약 select query 를 보고 싶거든 flush, clear 메소드를 활용해주면 된다.

 

다음으로 연관관계의 데이터를 수정하고 싶은 경우 아래와 같이 코드를 작성해 줄 수 있다.

- JpaMain.java

// 새로운 팀을 생성했다고 가정할 경우
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);

findMember.setTeam(teamB); // 찾아온 멤버 데이터의 팀을 변경해준다.

// 다른 팀이 이미 존재한다고 가정할 경우
Team newTeam = em.find(Team.class, 100L); // 키 값이 100 번에 해당하는 팀이 있다고 가정한다.
findMember.setTeam(newTeam); // 찾아온 멤버 데이터의 팀을 변경해준다.