[JPA] 연관관계 매핑 - 다대일 ( @ManyToOne )
JPA는 객체지향설계와 관계형DB의 패러다임 불일치를 해결하는 프레임워크이다.
객체지향설계의 가장 대표적인 특성이 '연관관계'이다. 객체는 주소를 참조하여 다른 객체에 접근할 수 있다. A객체 ➟ B객체 ➟ C객체 .. 주소에 주소를 타고 여러 객체에 연속으로 접근할 수 있다. 반면 관계형DB는 다르다. 관계형DB는 오로지 '외래키'로 JOIN 연산이 수행되어야 다른 테이블에 데이터를 가져올 수 있다. 객체지향만큼 데이터 접근이 자유롭지 못하다.
연관관계와 외래키JOIN방식의 간극은 JPA에 의해 해결된다. 개발자는 JPA가 처리할 수 있도록 어노테이션으로 '표시'만 남기면 된다. 그럼 개발자는 어떤 어노테이션을 남겨야 할까?
다대일 관계 [ N : 1 ] ( @ManyToOne )
가장 많이 사용되는 연관관계이다.
여러 선수는 하나의 팀에 소속된다. 선수는 팀을 참조하여 관계를 맺는데, 이것이 다대일관계이다. 다대일 관계가 가장 많이 사용되는 이유는 관계형DB 패러다임과 가장 일치하기 때문이다.
관계형DB는 외래키JOIN방식으로 접근하는데, 외래키는 항상 '다' 쪽이 갖는다.
Team은 모든 Player의 외래키를 유지할 수 없다. 그래서 Player(다) 쪽에 외래키를 두어 Team(일)에 접근한다. 객체지향에서도 다대일 관계는 '다'가 주소로 '일'을 참조함을 의미한다. 이와같이, 패러다임이 일치하기에 다대일 관계는 가장 많이 사용되는 연관관계이다.
그럼 이를 JPA로 구현해보자.
Player 엔티티
@Entity
@Data
public class Player {
@Id @GeneratedValue
@Column(name = "PLAYER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne // 다대일 관계
@JoinColumn(name = "TEAM_ID") //외래키 지정
private Team team;
}
일반적인 클래스로 보면 Player는 인스턴수변수로 Team 참조변수를 가진다. Player와 Team은 연관관계이다. 여기서 어노테이션이 붙으면 엔티티로 거듭난다. Player는 '다'이므로 외래키를 가져야 한다. JPA를 사용하지 않았던 과거에는 진짜 외래키인 TEAM_ID를 변수로 가졌다.
과거 : private String team_id ; // 외래키 ( 객체지향의 특성을 잃어버림 )
현재 : private Team team ; // 객체지향을 특성을 살릴수 있음
JPA를 사용하는 현재는 어노테이션으로 외래키를 지정한다. JPA는 지정된 외래키와 필드를 매핑한다. 이로써 객체지향 특성도 살리면서 외래키와도 매핑이 가능해진다.
Team 엔티티
@Entity
@Data
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
'일'에 해당하는 Team엔티티도 만들어준다. 그럼 클라이언트를 하나 만들어 테스트 해보자.
Main 클래스
public class JpaMain {
public static void main(String[] args) {
//EntityManagerFactory 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("h2");
//EntityManager 생성
EntityManager entityManager = emf.createEntityManager();
//EntityTransaction 생성
EntityTransaction tx = entityManager.getTransaction();
tx.begin(); //트랜잭션 시작
try{
//Team 생성
Team team = new Team();
team.setName("TeamA");
entityManager.persist(team);
//Member 생성
Player player = new Player();
player.setUsername("member1");
player.setTeam(team);
entityManager.persist(player);
entityManager.flush(); // SQL 쿼리 실행
entityManager.clear(); // 영속성 컨텍스트 초기화 ( DB에서 데이터를 조회하기 위해 )
Player findplayer = entityManager.find(Player.class,player.getId());
System.out.println("=================");
System.out.println("player = " + findplayer);
System.out.println("=================");
tx.commit();
}catch (Exception e){
tx.rollback(); // 트랜잭션 롤백
}finally {
entityManager.close();
}
}
}
Player 테이블
Team 테이블
Player 테이블을 보면 실제로 TEAM_ID(외래키)가 잘 저장되었음을 알 수 있다. 그럼 DB에 저장된 Player를 조회해보자. 캐시에서 엔티티를 가져오는 것을 방지하기 위해 clear()로 영속성컨텍스트를 초기화 한다.
select
player0_.PLAYER_ID as PLAYER_I1_1_0_,
player0_.TEAM_ID as TEAM_ID3_1_0_,
player0_.USERNAME as USERNAME2_1_0_,
team1_.TEAM_ID as TEAM_ID1_2_1_,
team1_.name as name2_2_1_
from
Player player0_
left outer join
Team team1_
on player0_.TEAM_ID=team1_.TEAM_ID
where
player0_.PLAYER_ID=?
find 메소드로 Player를 조회하자 SELECT문이 실행되었다. 주목할 점은 LEFT OUTER JOIN문이 생성되었다는 점이다. JPA는 매핑정보를 참고하여 다대일관계임을 확인하고 자동으로 JOIN문을 생성하였다.
Player 엔티티를 조회하였는데 JOIN으로 Team 엔티티도 자동생성하였다. 이로써 Player는 Team을 참조할 수 있게 된다. 이렇듯, JPA는 객체지향 App과 관계형DB 사이에서 패러다임 불일치를 해결한다.
다대일 양방향 관계
지금까지는 Player가 Team을 참조하는 단방향 관계를 보았다. 반대로 Team에서도 Player에 접근할 필요가 있다. 여기서 패러다임 불일치가 발생한다. '일'이 '다'로의 접근은 객체지향에서만 필요하다. 비즈니스 로직상 Team이 Player의 정보에 접근할 필요가 있다. 그러나 관계형DB는 아니다. 관계형DB에서 외래키는 반드시 '다'쪽에 있어야 한다. '일'에서 '다'로는 접근하지 못한다.
JPA는 이런 불일치를 해결해야 한다.
Team 엔티티
@Entity
@Data
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // 양방향 관계
private List<Player> players = new ArrayList<>();
}
다대일 양방향 관계는 객체지향에서만 의미있다. 그러므로 객체지향만 적용되면 된다.
Team 엔티티가 Player를 참조할 수 있도록 참조변수를 만들었다. Player는 '다'이므로 List로 자료구조를 사용한다. 다대일 양방향 관계에서 '일'은 @OneToMany 어노테이션을 사용한다. 이때 속성을 mappedBy를 사용한다. mappedBy는 외래키가 자신한테 없고 다른 애한테 있다는 표시이다.
@OneToMany(mappedBy = "team")는 외래키는 Player의 team 참조변수가 외래키이고 players는 그저 양방향 관계를 만들기 위한 참조변수이므로 Team 테이블에 해당 변수에 대한 컬럼을 만들필요 없음을 JPA에게 알려주는 것이다. JPA는 엔티티에 있는 필드는 모두 컬럼으로 생성하는데, @OneToMany(mappedBy = "team")로 표시된 필드는 컬럼으로 추가하지 않는다.
이와같이, 테이블에는 어떤 컬럼도 생성하지 않았다. 다대일 양방향 관계에서 '일'의 참조변수는 객체지향 비즈니스 로직 때문에 존재하므로 JPA가 이를 무시할 수 있도록 @OneToMany 어노테이션으로 정보를 전달해야 한다.
참고자료