Book Review

자바 ORM 표준 JPA 프로그래밍 - 연관관계 매핑

빈코 2022. 10. 23. 22:52

엔티티들은 대부분 다른 엔티티와 연관관계가 있다. 하지만 객체는 참조(주소)를 사용해서 관계를 맺고, 테이블은 외래 키를 사용해서 관계를 맺는다. 이 둘은 완전히 다른 특징을 가진다. 객체 관계 매핑에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일이다.

 

로고
연관관계 매핑

 

개요

이전 포스팅에서는 엔티티 매핑에 대해서만 알아보았고, 이번에는 다양한 연관관계를 간단한 예시와 함께 포스팅하려 한다. 연관관계의 사용부터 관계를 맺었을 때 주인은 누구로 설정해야 하는지, 또 주의할 점편리하게 사용하는 방법등을 알아보려 한다.

 

예시
예시

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계다.

 

연관관계를 사용하기에 앞서 서두에서 언급한 객체와 테이블의 차이를 알아야 한다. 객체 그림을 보면 회원 객체와 팀 객체는 단방향 관계임을 알 수 있다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다.

 

예를 들어, member -> team 조회는 member.getTeam()으로 가능하지만 반대 방향인 team -> member를 접근하는 필드가 없다.

 

하지만 테이블에서는 가능하다. 회원 테이블과 팀 테이블은 양방향 관계로 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있다. 

 

결론적으로 객체와 테이블 관계는 서로 다르다는 점을 알고 넘어가자😄

 

Entity 설계📙

위 예시 그림을 참고해서 회원 엔티티와 팀 엔티티를 설계하였다.

 

@Entity
@Getter @Setter
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;
    }
}

 

회원 엔티티를 먼저 설계하였다. 연관관계 매핑에 필요한 어노테이션의 사용법은

  • @ManyToOne : 이름 그대로 다대일 관계라는 매핑 정보다. 회원과 팀은 다대일 관계이며, 연관관계를 매핑할 때 이렇게 다중성을 타나태는 어노테이션을 필수로 사용해야 한다.
  • @JoinColumn(name = "TEAM_ID") : 조인 칼럼은 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하면 된다.

 

@Entity
@Getter @Setter
public class Team {
	
    @Id
    @Column(name = "TEAM_ID")
    private String id;
    
    private String name;
}

 

그다음으로는 팀 엔티티를 설계하였다. 현재는 양방향 관계가 아니므로 조인하는 멤버 필드는 지정하지 않았다. 양방향 관계는 아래에서 좀 더 자세히 다뤄보려고 한다.

 

연관관계 사용📒

연관관계를 등록, 수정, 삭제, 조회하는 간단한 예제를 통해 사용법을 알아보려고 한다.

 

public void testSave() {
	
    // Team1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    // 회원1 저장
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1); // 연관관계 설정
    em.persist(member1);
    
    // 회원2 저장
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team1); // 연관관계 설정
    em.persist(member2);
}

 

첫 번째로 저장하는 로직을 구성하였다. 팀을 생성한 후 각 멤버에게 해당 팀을 연관관계로 설정하였다. em.persist()을 이용하여 영속성 컨텍스트에 SQL문들을 차곡차곡 저장하고, 트랜잭션이 끝났을 경우에 해당 쿼리를 DB에 전달한다. 데이터베이스로 확인해 보면 아래 표와 같이 값이 저장된 걸 확인할 수 있다.

 

MEMBER_ID NAME TEAM_ID TEAM_NAME
member1 회원1 team1 팀1
member2 회원2 team1 팀1

연관관계가 있는 엔티티를 조회하는 방법은 크게 객체 그래프 탐색 방법객체지향 쿼리 사용(JPQL) 으로 총 2가지로 나뉜다. 객체 그래프 탐색은 member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있다. JPQL 방식은 아래 코드를 참고하자.

 

private static void queryLogicJoin(EntityManager em) {
	
    String jpq1 = "select m from Member m join m.team t where t.name=:teamName";
    
    List<Member> resultList = em.createQuery(jpq1, Member.class)
    		.setParameter("teamName", "팀1")
            .getResultList();
            
    for (Member member : resultList) {
    	System.out.println(" member.username=)" + member.getUsername());
    }
    
    // 결과: member.username=회원1
    // 결과: member.username=회원2
}

📌JPQL을 모르신다면 JPQL의 사용법에 대해서는 포스팅 주제와 다르기 때문에 링크를 클릭하셔서 확인해주세요😄


마지막으로 수정, 삭제 부분은 따로 예제 코드를 작성해 보지 않았다. JPA를 공부해보신 분이라면 수정하는 방법이 따로 없고, 영속 상태인 엔티티에 set을 이용해 값을 변경하면 자동으로 업데이트 쿼리문을 DB에 보내는 것을 알고 있을 것이다.

member1.setTeam(team2); // team2로 수정

 

삭제 또한 null 값으로 세팅해주어 연관관계를 제거해주면 된다.

member1.setTeam(null) // 연관관계 제거

 

양방향 연관관계 매핑📗

@Entity
@Getter @Setter
public class Team {
	
    @Id
    @Column(name = "MEMBER_ID")
    private String id;
    
    private String name;
    
    //==추가==//
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}

 

팀과 회원은 일대다 관계다. 따라서 팀 엔티티에 컬렉션인 List<Member> members를 추가했다. 그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다. mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다. 반대쪽 메핑이 Member.team이므로 team을 값으로 주었다. 

 

양방향 매핑을 하였으므로 이제는 팀 엔티티를 통해 해당 팀이 보유한 회원들을 꺼내올 수 있다. 테스트 코드를 작성해서 확인해보면 원하는 결과를 추출할 수 있다.

 

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());
    }
    
    // 결과
    // member.username = 회원1
    // member.username = 회원2
}

 

연관관계의 주인📔

이전에 양방향 연관관계에서 mappedBy 속성은 왜 설정하는 것일까? 해답은 객체에는 양방향 연관관계라는 것이 없기 때문이다. 객체를 테이블처럼 양방향으로 관계를 맺으려면 설정하려면 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 하는 것이다.

 

📌데이터베이스 테이블은 앞서 설명했듯이 외래 키 하나로 양쪽이 서로 조인할 수 있다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.

 

엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 되지만, 양방향으로 매핑할 경우에는 회원 -> 팀, 팀 -> 회원 두 곳에서 서로를 참조한다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳으로 늘어나는 셈이다.

 

이런 문제로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데 이것을 연관관계의 주인이라고 한다.

 

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있기 때문에 주인을 설정할 때는 로직의 흐름을 잘 파악해야 한다. 주인을 정할 때는 mappedBy 속성을 이용하면 된다.

 

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다. 위 예시에서는 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다. 주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정한다.

 

양방향 연관관계의 주의점📗

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다. 실수하는 예제 코드를 살펴보자.

 

public vpoid testSaveNonOwner() {
	
    //회원1 저장
    Member member1 = new Member("member1", "회원1");
    em.persist(member1);
    
    Team team1 = new Team("team1", "팀1");
    // 주인이 아닌 곳에만 연관관계 설정
    team1.getMembers().add(member1);
    em.persist(team1);
}

 

데이터베이스 결과를 살펴보면 아래 표와 같이 나오게 될 것이다.

 

MEMBER_ID USERNAME TEAM_ID
member1 회원1 null

 

양방향 연관관계이므로 member 엔티티에도 해당 팀을 추가로 관계를 맺어주어야 한다. 잘 된 예시를 살펴보자.

 

public void testORM_양방향() {
	
    //팀1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    Member member1 = new Member("member1", "회원1");
    
    //양방향 연관관계 설정
    member1.setTeam(team1);
    team1.getMembers().add(memeber1);
    em.persist(member1);
}

 

양 쪽 모두 연관관계를 설정했기 때문에, 순수한 객체 상태에서도 동작하며 테이블의 외래 키도 정상 입력되는 것을 확인할 수 있다.

 

연관관계 편의 메소드📘

위 예시 처럼 계속해서 양방향으로 연관관계를 맺다 보면 실수로 누락하는 경우가 생길 수도 있다. Member 클래스의 setTeam() 메서드를 수정해서 편리하게 관계를 맺을 수 있게 변경해 보려 한다.

 

public class Member {
	
    private Team team;
    
    public void setTeam(Team team) {
    	this.team = team;
        //추가
        team.getMembers.add(this);
    }
}

 

setTeam 메소드 한 번으로 양쪽 연관관계를 매핑시킬 수 있게 되었다.

 

 


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

마치며

지금까지 양방향 연관관계 매핑에 대해 알아보았다. 다음 포스팅은 다대일, 일대다, 일대일 매핑 등 다양한 연관관계 매핑에 대해 알아보겠다😁

반응형