Backend

JPA 5장

niahh 2025. 5. 23. 13:09

5.0 들어가며 

엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 

예를 들어 주문 엔티티는 어떤 상품을 주문했는지 알기 위해 상품 엔티티와 연관관계가 있고 상품 엔티티는 카테고리, 재고 등 또 다른 엔티티와 관계가 있다. 

 

반면,객체는 참조(주소)를 사용해 관계를 맺는다. 테이블은 외래 키를 사용해 관계 맺는다. 이둘은 다른 특징 가진다. 

 

이장의 목표는 객체의 참조와 테이블의 외래 키를 매핑하는 것이다. 

 

방향에는 단방향, 양방향이 있다. 단방향은 회원 -> 팀 팀 -> 회원

양방향은 회원<-> 팀 , 양쪽 모두 서로 참조하는 것이다. 

 

방향은 객체관계에서만 존재한다. 테이블 관계는 항상 양방향이기 때문이다. 

 

다중성 Multiplicity: 다대일, 일대다, 다대다 

 

연관관계의 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 함. 

5.1 단방향 연관관계 

다대일 단방향 관계에 대해 알아보자. 회원과 팀이있고 회원은 하나의 팀에만 소속된다. 회원 과 팀은 다대일 관계이다.

 

- 회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺음 

- 회원 객체와 팀 객체 : 단방향 관계 

member.getTeam()으로 팀은 알 수 있지만 반대방향인 team -> member접근 필드는 없다. 

 

회원 테이블은 TEAM_ID 외래키로 테이블과 연관관계를 맺는다. 회원 테이블과 팀 테이블은 양방향 관계이다. 

차이점은?

참조를 통한 연관관계는 언제나 단방향이다. 양방향처럼 보이는 것도 서로 다른 단방향 관계 2개이다. 

5.1.1 순수한 객체 연관관계

테이블 연관관계에서 조인 은 외래 키를 사용하여 연관관계를 탐색하는 것이다. 

 

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 void 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.....

}

 

객체 연관관계: 회원 객체의 Member.team 필드이다. 테이블 연관관계는 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼 사용했다. 

5.1.4 @JoinColum

@JoinColumn은 외래키를 매핑할때 사용한다. 

사용할 수 있는 속성은 아래와 같다. 

name

referenceColumnName

foriegnKey - DDL 사용할때만 

unique 

nullable

insertable

updatable 

columnDefinition

table 

 

만약 @JoinColumn을 생략하면 외래키 찾을때 기본 전략을 사용한다. 

5.1.5 @ManyToOne

optional : false로 설정하면 연관된 엔티티가 항상 있어야함. 기본값: true 

fetch

cascade

targetEntity : 거의 사용하지 않음 

 

참고**

@OneToOne 관계도 있음. 

단방향 관계 매핑 시, 어떤 것을 사용할지는 반대편 관계에 달려있있다. 반대편이 일대다 관계면 다대일 사용. 반대편이 일대일 관계면 일대일 사용. 

5.2 연관관계 사용

5.2.1 저장 

연관관계를 등록, 수정, 삭제, 조회하보며 사용법을 익히자. 

 

연관관계 매핑한 엔티티를 저장하는 코드  >>>

public void testSave() {

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

// Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team 1 
em.persist(member1);

// 회원2 저장 
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team 1
em.persist(member2);
}

>>> 회원과 팀을 저장하는 코드 

 

member1.setTeam(team1) // 회원 -> 팀 참조

em.persist(member1) // 저장

 

위 코드로 실행되는 sql 은 아래와 같다. 회원 테이블의 외래 키 값은 참조한 팀의 식별자 값 team1이다. 

5. 2. 2. 조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지이다. 첫번째는 객체 그래프 탐색(객체 연관관계를 사용한 조회)하는 것이도, 두번째는  객체 지향 쿼리 사용 (JPQL)하는 것이다.  

 

방금 저장한대로 회원1, 회원2가 팀1에 소속되어있다고 가정하자. 

member.getTeam()을 사용하여 member와 연관된 team 엔티티를 조회할 수 있다. 

 

Member member = em.find(Member.class, "member1");

Team team = member.getTeam(); // 객체 그래프 탐색

 

이처럼 객체를 통해 연관된 엔티티를 조회하는 것을 '객체 그래프 탐색'이라 함. 

5.2.3 . 수정

팀1 소속이던 회원을 팀2에 소속하도록 수정하자. 

private static void updateRelation(EntityManager em) {

	// 새로운 팀2 
    Team team2 = new Team("team2", "팀2");
    em.persist(team2);
    
    // 회원1을 팀2에 속하도록 
    Member member = em.find(Member.class, "memeber1");
    member.setTeam(team2);
}
update member 
set 
	team_id = 'team2', .... 
where
	id = 'member1'

 

수정은 em.update()같은 메서드가 없다. 단순히 불러온 엔티티 값만 변경하면, 트랜잭션을 커밋할때 플러시가 일어나면서 변경 감지 기능이 작동한다. 그리고 변경 사항을 자동으로 데이터 베이스에 반영한다. 

5.2.4 연관관계 제거 

회원1을 팀에 소속하지 않도록 변경한다면 코드는 아래와 같다. 

private static void deleteRelation(EntityManager em) {
	
    Member member1 = em.find(Member.class, "member1");
    member1.setTeam(null); // 연관관계 제거 

}

5.2.5 연관된 엔티티 삭제  

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 

그렇지 않으면, 외래키 제약조건으로 인해, 데이터 베이스에 오류가 발생한다. 

팀1에는 회원 1, 회원2가 소속되어있다. 팀1을 삭제하기 위해선 연관관계를 먼저 끊어줘야 한다. 

 

member1.setTeam(null)

member2.setTeam(null)

em.remove(team)

5.3 양방향 연관관계 

5.3.1 양방향 연관관계 매핑 

양방향 관계를 매핑해보자. 

회원 엔티티 클래스는 변경할 부분이 없다. 팀 엔티티는 변경 해야한다. 

 

@OneToMany(mappedBy="team")

private List<Member> members = new ArrayList<Member>();

 

@OneToMany 매핑 정보를

 

디비에서 회원 테이블을 조회해보면, TEAM_ID 외래 키에 팀의 기본 키값이 저장되어 있음. 

의미는 -> 양방향 연관관계에서는 연관관계 주인이 외래키를 관리함. 

그래서 주인이 아닌 방향은 값을 설정하지 안ㅇㅎ아도 디비에 외래키 값이 정상 입력된다. 

 

team1.getMembers().add(member1) // 무시, 연관관계 주인 아님 

team1.getMembers().add(member2) // 무시, 연관관계 주인 아님 

 

위같은 코드가 추가로 있어야 할 것같지만 Team.members는 연관관계 주인이 아님. 

주인이 아닌 곳에 입력된 값은 외래키에 영향 주지 않음. 디비에 저장할때 무시됨. 

 

member1.setTeam(team1) // 연관관계 설정됨. 연관관계 주인임 

member2.setTeam(team1) 

 

엔티티 매니저는 위 코드에서 입력된 값을 사용해 외래키 관리함. 

5.3.2. 일대다 컬렉션 조회 

public void biDirection() {

	Team team = em.find(Team.class, "team1");
    List<Member> members = team.getMembers();
    
    for(Member member : members) {
    	System.out.println("member.username = " + member.getUsername());
    }

}

팀에서 회원 컬렉션으로 객체 그래프 탐색을 사용해 조회한 회원들을 출력한다.

5.4. 연관관계의 주인 

5.4.1. 양방향 매핑의 규칙 : 연관관계의 주인 

연관관계 매핑시 지켜야할 규칙은 두 연관관계 중 하나를 연관관계의 주인으러 정해야 한다는 것이다. 연관관계 주인만 데이터 베이스 연관관계와 매핑되고 외래키를 관리(등록, 수정, 삭제)할 수 있음. 주인이 아닌쪽은 읽기만 가능하다. 

 

주인이 아닐 경우, mappedBy속성을 사용해 값으로 연관관계 주인을 지정한다. mappedBy 뒤에 오는 것이 연관관계 주인이다. 

 

연관관계의 주인을 정한다 = 외래키 관리자를 선택한다는 의미이다. 예제에서는, 회원 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택해야한다. 만약 회원 엔티티의 Member.team을 주인으로 선택하면, 자기 테이블에 있는 외래 키를 관리하면 된다. 
하지만 팀 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 함. ?

5.4.2 연관관계의 주인은 외래키가 있는 곳 

연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다. ????/

여기서는 회원 테이블이 외래 키를 가지고 있으므로 Member.team 이 주인이 됨. !!!!

주인이 아닌 Team.members 에는 mappedBy = "team" 속성을 사용해 주인이 아님을 설정함. 

mappedBy속성의 값은 연관관계의 w주인인 Member 엔티티의 team 필드를 주면 된다. 

 

참고**

테이블의 다대일, 일대일 관계에선 항상 '다' 쪽이 외래 키를 가진다. 

'다' 쪽인 ManyToOne 은 항상 연관관계의 주인임. mappedBy 속성 설정불가함. 그래서 ManyToOne에는 mappedBy 속성 없음. 

5.3 양방향 연관관계 저장

public void testSave() {

	Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    // 회원1저장 
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1); // 
    em.persist(member1);
    
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team1); 
    em.persist(member2);
    
    
}

 

 

5.6. 양방향 연관관계의 주의점 

연관관계 주인에는 값을 입력하지 않고, 

주인이 아닌 곳에만 값을 입력하는 실수!! 

 

회원1, 회원2를 저장하고 팀 컬렉션에 담은 후에 팀을 저장하면

team1.getMembers().add(member1) // 무시, 연관관계 주인 아님

team1.getMembers().add(member2) // 무시, 연관관계 주인 아님 

 

회원1, 회원2를 저장하고 팀 컬렉션에 담은 후 팀을 저장했다. 

디비에서 회원 테이블을 조회해보자 

 

외래키 TEAM_ID에 team1이 아닌 null 값이 입력되어있음. 

연관관계 주인이 아닌 Team.members 에만 값을 저장했기 때문임. 

5.6.1. 순수한 객체까지 고려한 양방향 연관관계 

정말 주인이 아닌 곳에는 값 저장안해도 될까???

객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전함. 

그렇지 않으면 jpa 를 사용하지 않는 순수한 객체 상태에서 심각한 문제 발생 가능성. 

5.6.2. 연관관계 편의 메서드

member.setTeam(team);

team.getMembers().add(member); 

 

위처럼 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있음. 

 

그래서 멤버 클래스를 리팩토링 하면: 

public class Member {

private Team team; 

	public void setTeam(Team team) {
    	this.team = team;
        team.getMembers().add(this);
    }
    
   // 연관관계 설정 
   member1.setTeam(team1);
   member2.setTeam(team1);
}

 

setTeam() 메서드 하나로 양방향 관계를 모두 설정하도록 변경했다. 

team의 관점에서 연관관계 설정하는 부분은 삭제하면 된다. 

-> teamA.getMembers().add(member1);

teamA.getMembers().add(member2);

 

이렇게 한번에 양방향 관계를 설정하는 메서드를 '연관관계 편의 메서드' 라고 함. 

 

5.6.3. 연관관계 편의 메서드 작성 시 주의사항 

연관관계를 변경할때 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야함. 

 

public void setTeam(Team team) {

	// if(this.team != null) {
    	this.team.getMembers().remove(this);
    }
    
    this.team = team;
    team.getMembers().add(this);
}

 

5.7 정리 

- 양방향 매핑에서는 연관관계의 주인을 정해야한다. 두개의 단방향 연관관계를 양방향으로 만들기 위한 로직도 잘 관리해야 한다. 

- 중요한 사실 - 연관관계가 하나인 단방향 매핑은 언제나 연관관계의 주인이ㅣ다.! 

- 단방향과 비교해서 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것 뿐이다. 

 

주인의 반대편은 mappedBy로 주인을 지정해야 한다. 

주인의 반대편은 단순히 보여주는일만 할 수 있다. 

 

- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 완료된다. 

- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다. 

- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다. 

 

연관관계의 주인을 정하는 기준!! 

- 예를 들어, 회원과 팀 엔 티티는 외래키가 있는 쪽인 회원이 연관관계의 주인이 된다.