본문 바로가기
Framework and Tool/JPA

JPA - 연관관계 맵핑 - 단방향 연관

by ocwokocw 2021. 6. 30.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 연관관계

대부분의 엔티티들은 다른 엔티티와 관계가 있다. 주문은 상품과, 상품은 카테고리등 다른 엔티티와 관계를 갖는다. JPA 를 사용시 관계설정의 핵심은 객체 와 DB 가 다른 엔티티들과 관계를 맺을 때 연관방식이 다른다는데에 있다. DB 에서 다른 엔티티를 참조할 때 외래키를 사용하는데 JPA 를 이용하여 어플리케이션에서 어떻게 나타낼지 알아보자.

 

본격적으로 알아보기전 연관과 관련된 핵심 키워드를 알아본다.

  • 방향: 단방향과 양방향이 있다. 만약 멤버가 주문을 참조하기만 하면 단방향이며, 만약 주문도 멤버를 참조한다면 양방향 관계이다.
  • 다중성: 1:1, 1:N, N:1, M:N 관계를 나타낸다. 만약 어플리케이션에서 "회원은 여러 주문을 할 수 있으며, 1 주문에는 1 명의 회원만 관여한다." 라고 한다면 회원 대 주문은 1 : N 이 된다.
  • 연관관계의 주인: 객체가 양방향 연관일 때 연관관계의 주인을 정해야 한다.

인내심이 많다면 DB 의 ER 다이어그램이나 UML 의 association, directed association 의 개념을 간단하게라도 숙지하면 이해가 훨씬 수월할거라고 생각한다.


- 단방향 연관관계

연관관계중에서는 일반적으로 많이 발생하는 다대일(N:1)의 개념을 먼저 이해하는게 좋다. 만약 "회원과 팀이 있고, 회원은 하나의 팀에만 종속된다." 라고 한다면 이는 회원 : 팀 = N : 1 관계이다.

위의 다이어그램은 회원과 팀의 관계를 UML 로 나타낸것이다. 현재 User 에서 Team 을 참조할 수 있지만 Team 에서는 User 를 참조할 수 없기 때문에 단방향 연관으로 나타내었다. 객체를 양방향으로 참조할 수 있게 User 에서 Team 참조를, Team 에서 User 참조가 둘 다 존재할 때 양방향이라고 부르기는 하지만 실제로는 서로에 대한 단방향이라고 이해해야 정확하다.

반면 DB 관점에서는 단방향이라는 개념이 없다. TEAM 을 먼저 찾은 후 MEMBER 를 먼저 찾건 그 반대의 순서로 찾건간에 TEAM_ID 라는 컬럼으로 찾을 수가 있어서 무조건 양방향 연관이다.

 

이렇듯 DB 와 객체는 연관에서 차이가 존재한다. 객체는 참조를 통해서 그리고 DB 는 외래키를 통해서 연관을 표현한다. 


- 객체의 연관관계 표현(Java)

순수 Java 코드로 연관관계를 표현해보자. 우선 기본적인 User 와 Team 클래스를 선언한다.

 

public class User {

	private String id;
	private String userName;

	private Team team;
	
	public User(String id, String userName) {
		super();
		this.id = id;
		this.userName = userName;
	}
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public Team getTeam() {
		return team;
	}
	public void setTeam(Team team) {
		this.team = team;
	}
}

public class Team {

	private String id;
	private String name;

	public Team(String id, String name) {
		super();
		this.id = id;
		this.name = name;
	}
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}

 

2 명의 유저를 1 개팀에 소속시키는 코드를 작성해보자.

 

public static void main(String[] args) {
	
	User user1 = new User("USER_ID#1", "User 1");
	User user2 = new User("USER_ID#2", "User 2");
	
	Team team1 = new Team("TEAM_ID#1", "Team 1");
	
	user1.setTeam(team1);
	user2.setTeam(team1);
	
	Team teamOfUesr1 = user1.getTeam();
}

 

위의 코드처럼 각 User 의 Team setter 메소드를 이용하여 team1 의 소속이라는 것을 표현한다. 그리고 user1 의 Team getter 메소드를 이용하여 User1 이 속한 팀을 찾는다. 이를 "객체 그래프 탐색" 이라고 한다.


- 객체 관계 맵핑

DB 는 외래키를 이용하여 연관을 한다는것 그리고 객체는 참조를 통하여 연관을 표현한다는것을 알아보았다. 그래서 JPA 는 이 둘의 간격을 어떤 방식을 통해서 맵핑하고 있는지 알아보자.

 

우선 User 와 Team 클래스를 아래와 같이 수정하고 나머지 getter / setter 메소드를 추가해준다.

 

@Entity
public class User {

	@Id @GeneratedValue
	@Column(name = "USER_ID")
	private Long id;
	private String userName;

	@ManyToOne
	@JoinColumn(name = "TEAM_ID")
	private Team team;
	
	public User() {
		super();
	}

............

@Entity
public class Team {

	@Id @GeneratedValue
	@Column(name = "TEAM_ID")
	private Long id;
	private String name;

	public Team() {
		super();
	}

 

위의 코드에서 "JPA 를 이용하여 연관관계를 맵핑하였다." 함은 SQL 작성시 TEAM_ID 컬럼을 이용하여 User 와 Team 을 Join 하는 행위를 객체로는 User가 team 을 참조하는것으로 변환한것이다. 즉 TEAM_ID 와 Team 을 맵핑한것이다.

 

위에서 언급한 핵심 행위를 수행하는 어노테이션은 @ManyToOne 과 @JoinColumn 이다. 각 어노테이션은 다음과 같은 의미를 지닌다.

  • @ManyToOne: 다중성을 알려준다. 예제에서는 N : 1 임을 알려주고 있다.
  • @JoinColumn(name = "TEAM_ID"): 외래 키를 맵핑할 때 사용한다. name 속성에는 맵핑할 컬럼을 적으면 된다. 생략하면 [필드명]_[참조하는 테이블의 컬럼명] 을 사용한다. 예제같은 경우 team_TEAM_ID 가 된다.

- 연관관계 저장

User1 과 User 2 가 Team 1 에 속한다는것을 코드로 작성해보자.

 

public static void main(String[] args) {
	SpringApplication.run(JpaApplication.class, args);
	
	EntityManager em = emf.createEntityManager();
	
	EntityTransaction tx = em.getTransaction();
	tx.begin();
	
	User user1 = new User();
	user1.setUserName("User1");
	em.persist(user1);
	long user1Id = user1.getId();
	
	User user2 = new User();
	user2.setUserName("User2");
	em.persist(user2);
	
	Team team1 = new Team();
	team1.setName("Team1");
	em.persist(team1);
	
	user1.setTeam(team1);
	user2.setTeam(team1);

	tx.commit();

	User findUser1 = em.find(User.class, user1Id);
	System.out.println("teamOfUesr1: " + findUser1.getTeam());
	
	em.close();
	emf.close();
}

 

순수 자바코드에서 User 의 Team setter 메소드를 이용하여 연관지은것처럼 JPA 도 별반다르지 않다. 단 주의할점이 한 가지 있는데, user 의 team 을 설정할 때 해당 엔티티들이 모두 영속 상태여야 한다. user1.setTeam과 user2.setTeam 을 호출하기 전 엔티티매니저의 persist() 를 호출하여 모든 엔티티들을 영속상태로 만들어야 한다.

 

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    /* insert com.example.demo.example.User
        */ insert 
        into
            user
            (team_id, user_name, user_id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert com.example.demo.example.User
        */ insert 
        into
            user
            (team_id, user_name, user_id) 
        values
            (?, ?, ?)
Hibernate: 
    /* insert com.example.demo.example.Team
        */ insert 
        into
            team
            (name, team_id) 
        values
            (?, ?)
Hibernate: 
    /* update
        com.example.demo.example.User */ update
            user 
        set
            team_id=?,
            user_name=? 
        where
            user_id=?
Hibernate: 
    /* update
        com.example.demo.example.User */ update
            user 
        set
            team_id=?,
            user_name=? 
        where
            user_id=?
teamOfUesr1: Team [id=3, name=Team1]

 

코드를 실행하면 위처럼 결과가 나오는데 Team 1 과 User 1,2 를 영속화 시킬 때 key 채번을 3 번 진행한다. 그리고 user 의 team 을 설정할 때 user 테이블의 team_id 를 update 하는 SQL 문이 실행된다.

 

아래처럼 H2 서버에서 INNER JOIN SQL 을 실행하면 USER 와 TEAM 에 모두 데이터가 들어가있고, USER 테이블의 TEAM_ID 컬럼에도 데이터가 제대로 들어간것을 확인할 수 있다.


- 연관관계 조회

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

 

객체 그래프 탐색은 위의 예제코드에서 나온것과 같이 엔티티매니저의 find 메소드로 User 를 찾으면 해당 엔티티에서 Team의 참조를 얻어올 수 있는 getTeam() 메소드를 이용하는 방법이다.

 

객체지향 쿼리 사용은 SQL 에서 조인을 사용하여 조회한것처럼 JPQL 이 지원하는 조인 기능을 이용하여 조회하는 방법이다. Team 1 에 소속된 회원들을 조회하는 JPQL 예제를 작성해보자.

 

위의 예제에서 엔티티 매니저에서 Uesr 를 find 하던 find1User 코드 부분대신 아래와 같이 JPQL 을 작성해보자.

 

user1.setTeam(team1);
user2.setTeam(team1);

tx.commit();

String team1UserJpql = "select u from User u join u.team t where t.name = :teamName";
List<User> team1Uesrs = em.createQuery(team1UserJpql, User.class)
	.setParameter("teamName", "Team1")
	.getResultList();
	
team1Uesrs.forEach(System.out::println);
	
em.close();
emf.close();

 

문법이 SQL Join 과는 다르지만 그래도 비슷하게 생겼다. :teamName 부분은 파라미터 바인딩으로 Team의 이름값을 받기 위해 작성하였다.

 

Hibernate: 
    /* select
        u 
    from
        User u 
    join
        u.team t 
    where
        t.name = :teamName */ select
            user0_.user_id as user_id1_1_,
            user0_.team_id as team_id3_1_,
            user0_.user_name as user_nam2_1_ 
        from
            user user0_ 
        inner join
            team team1_ 
                on user0_.team_id=team1_.team_id 
        where
            team1_.name=?
User [id=1, userName=User1, team=Team [id=3, name=Team1]]
User [id=2, userName=User2, team=Team [id=3, name=Team1]]

 

코드를 실행해보면 위와 같이 inner join 을 사용한 익숙한 SQL 문이 출력된다. 그리고 그 결과로 반환되는 User 들도 Sysout 으로 출력해보면 기대한 결과가 나온다.


- 연관관계 수정

연관관계 수정은 엔티티 수정과 마찬가지로 엔티티 매니저가 따로 update 메소드 같은것을 호출하지 않으며, setter 메소드를 통해 연관 엔티티를 맵핑하면 commit() 시 update SQL 을 이용하여 외래키를 맵핑한다.

 

주의할점은 연관관계 저장에서 언급한것처럼 만약 연관 엔티티를 변경하고 싶다면 변경대상이 되는 엔티티도 영속상태이어야 하므로 persist() 로 영속화 시킨 후 참조를 변경해야 한다.


- 연관관계 제거

눈치가 조금 있는 사람이라면 예상했겠지만 연관관계를 제거한다고 해서 특별한 메소드를 이용하는것은 아니다. 연관관계 수정을 하는것처럼 setter를 이용한다. 다만 특정 엔티티가 아닌 user1.setTeam(null) 을 이용하여 연관관계를 제거한다.


- 연관된 엔티티 삭제

연관 엔티티를 삭제할 때에는 연관관계를 제거한 후 연관된 엔티티를 삭제 해야 한다.

 

user1.setTeam(team1);
user2.setTeam(team1);

em.remove(team1);

 

만약 위의 코드처럼 user1과 user2에 연관된 team1을 곧바로 삭제하려고 하면 에러가 발생한다.

 

2021-06-29 23:28:37.804  INFO 18008 --- [           main] o.h.e.j.b.internal.AbstractBatchImpl     : HHH000010: On release of batch it still contained JDBC statements
Exception in thread "main" javax.persistence.RollbackException: Error while committing the transaction
	at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:81)
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:104)
	at com.example.demo.JpaApplication.main(JpaApplication.java:47)
Caused by: javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:154)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
	at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:65)
	... 2 more
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
	at org.hibernate.exception.internal.SQLExceptionTypeDelegate.convert(SQLExceptionTypeDelegate.java:59)
...........

 

따라서 아래처럼 user1과 user2 의 Team 연관관계를 제거한 후 연관된 엔티티를 삭제해야한다.

 

EntityManager em1 = emf.createEntityManager();

EntityTransaction tx1 = em1.getTransaction();
tx1.begin();

User findUser1 = em1.find(User.class, (long) 1);
User findUser2 = em1.find(User.class, (long) 2);
Team findTeam1 = em1.find(Team.class, (long) 3);

findUser1.setTeam(null);
findUser2.setTeam(null);
em1.remove(findTeam1);

tx1.commit();
em1.close();

댓글