본문 바로가기

CS

[JPA] 연관관계 매핑 기초

연관관계 매핑에서는 방향, 다중성, 그리고 연관관계의 주인을 명확하게 설정해야 합니다. 방향은 단방향양방향으로 나뉘며, 다중성은 일대일, 일대다, 다대일, 다대다로 구분됩니다. 특히 양방향 연관관계에서는 반드시 연관관계의 주인을 정해야만 올바르게 매핑이 이루어집니다.

 


단방향 연관관계

 

객체 연관관계테이블 연관관계는 서로 차이가 있습니다. 객체는 참조를 사용해 연관관계를 맺고, 기본적으로 연관관계는 단방향입니다. 만약 객체 간 양방향 연관관계를 만들고 싶다면 단방향 연관관계를 두 개 만들어야 합니다. 반면, 테이블은 외래 키를 사용해 연관관계를 설정하며, 이 관계는 기본적으로 양방향입니다.

 

 

객체의 순수한 연관관계에서는 참조를 통해 연관관계를 탐색합니다. 이를 객체 그래프 탐색이라고 부릅니다.

public class Member {

    private String id;
    private String username;

    private Team team; // 팀의 참조를 보관

    public void setTeam(Team team) {
         this.team = team;
     }
}

 

public class Team {

    private String id;
    private String name;

    // Getter, Setter
}

 


 

아래는 위의 코드처럼 단방향 연관관계에서 객체 그래프를 탐색하는 예제입니다.

public static void main(String[]args){
    Member member1 = new Member("member1","회원1");
    Member member2 = new Member("member2","회원2");
    Team team1 = new Team("team1","팀1");

    member1.setTeam(team1);
    member2.setTeam(team1);

    Team findTeam = member1.getTeam(); //팀1 조회
}



반면, 테이블 연관관계에서는 외래 키를 사용하여 연관관계를 탐색합니다. SQL에서는 다음과 같은 방식으로 연관관계를 조회합니다.

SELECT T.*
FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.ID 
WHERE M.MEMBER_ID = 'member1' // member1이 속한 팀 조회

 

 


객체 관계 매핑

 

JPA에서는 객체의 필드와 데이터베이스 테이블의 외래 키를 매핑해야 합니다. 이를 위해 @ManyToOne과 @JoinColumn 어노테이션을 사용합니다.

@Entity
public class Member {

    @Id
    @Column(name= "MEMBER_ID")
    private String id;
    private String username;

    // 연관 관계 매핑
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // Getter, Setter
}

@Entity
public class Team {

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

    // Getter, Setter
}

 


연관관계 사용

 

JPA에서 엔티티를 저장할 때는 연관된 모든 엔티티가 영속 상태여야 합니다.

public void testSave(){
    Team team1 = new Team("team1","팀1");
    em.persist(team1);

    Member member1 = new Member("member1","회원");
    member1.setTeam(team1); // 팀 참조
    em.persist(member1); // 저장

    Member member2 = new member("member2","회원2");
    member2.setTeam(team1);
    em.persist(member2);
}


연관관계가 있는 엔티티를 조회하는 방법은 두 가지가 있습니다. 첫 번째는 객체 그래프 탐색을 사용하는 것입니다.

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


 
두 번째는 JPQL을 사용하는 것입니다. 참고로 해당 코드는 팀 1에 소속된 회원들만 조회하는 것으로 해당 JPQL에서 : 로 시작하는 것은 파라미터를 바인딩받는 문법입니다.

String jpql = "select m from Member m join m.team where" + "t.name=:teamName";

List<Member> resultList = em.createQuery(jpql, Member.class)
    .setParameter("teamName", "팀1")
    .getResultList();

 

 

연관관계를 수정하거나 제거할 때는 기존 연관관계를 명확하게 관리해야 합니다. 단순히 불러온 엔티티의 값만 변걍을 해주어도 저번 포스팅에서 봤듯, 스냅샷 비교를 통해 자동으로 업데이트 쿼리가 나가 DB에 자동 반영 되게 됩니다.

private static void updateRelation(EntityManager em){
    Team team2 = new Team("team2","팀2");
    em.persist(team2);

    Member member = em.find(Member.class,"member1");
    member.setTeam(team2); // 수정
}


 
연관관계를 지워야 할 때에는 단순하게 null로 설정하면 됩니다.

private static void deleteRelation(EntityManager em){
    Member member1 = em.find(Member.class,"member1");
    member1.setTeam(null);
}



연관된 엔티티를 아예 삭제를 할려면, 먼저 관계를 위에서 본 것 처럼 다 지우는 정리 작업을 하여 삭제해야 합니다.그렇지 않으면 외래 키 제약 조건으로 인해 DB에서 오류가 발생하게 됩니다.

 

여기서 제약 조건이란 데이터 무결성을 보장하기 위한 것으로 참조 무결성을 의미합니다. 즉 외래 키가 참조하는 대상의 데이터가 항상 유효해야 한다는 것입니다.

member1.setTeam(null);
member2.setTeam(null);
em.remove(team);

 


양방향 연관관계와 연관관계의 주인

 

양방향 연관관계에서는 한 쪽이 연관관계의 주인이 되어야 합니다. 주인은 데이터베이스의 외래 키를 관리하며,mappedBy를 사용하지 않습니다. 또한 DB의 테이블은 위 객체와는 달리 외래 키 하나로 양방향 조회가 가능하므로 DB에 추가해야 할 내용은 없습니다.

 

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    // Getter, Setter
}

@Entity
public class Team {

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

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    // Getter, Setter
}



테이블은 외래 키 하나로 두 테이블의 연관관계를 관리합니다. 하지만 객체에서는 양방향 연관관계를 설정하면 회원 → 팀, 팀 → 회원 두 곳에서 서로를 참조하게 되어 객체의 참조는 두 개가 되지만, 실제 데이터베이스에서는 외래 키가 하나만 존재하는 차이가 발생합니다.

 

따라서 두 객체 연관관계 중 하나를 연관관계의 주인으로 정해 테이블의 외래 키를 관리해야 합니다. 연관관계의 주인은 데이터베이스의 외래 키를 직접 관리하며, 등록, 수정, 삭제를 수행할 수 있습니다. 반면 주인이 아닌 쪽은 읽기만 가능합니다.

 

연관관계의 주인은 mappedBy 속성을 사용하지 않으며, 주인이 아닌 쪽은 mappedBy를 사용해 주인을 명시해야 합니다. 일반적으로 일대다, 다대일 관계에서는 항상 '다(N)' 쪽이 외래 키를 가지기 때문에 @ManyToOne이 항상 연관관계의 주인이 됩니다. 참고로 @ManyToOne은 mappedBy 자체가 사용이 불가합니다.


양방향 연관관계 저장

 

회원은 "다"이고, 팀은 "일" 관계인데, 항상 연관관계의 주인은 다 쪽인 것이고, 그러한 연관관계의 주인이 외래키를 등록, 수정, 삭제 작업을 해주기에 다를 기준으로 영속화 시켜야 합니다.

 

public void testSave(){
    Team team1 = new Team("team1","팀1");
    em.persist(team1);

    Member member1 = new Member("member1","회원");
    member1.setTeam(team1);
    em.persist(member1);

    Member member2 = new member("member2","회원2");
    member2.setTeam(team1);
    em.persist(member2);
}



즉 주인이 아닌 곳에 입력된 값은 외래키에 영향을 주지 않습니다. 만일 team1.getMembers().add(member1) 코드가 있다고 한다면 Team.members는 연관관계의 주인이 아니므로 DB에 저장할 때 해당 코드는 무시됩니다.

 

하지만 개발을 하다보면 가장 흔히 보이게 되는 실수로 주인이 아닌 곳에 값을 입력하는 것으로 외래키의 작업이 무시당하게 하는 것입니다. 이렇게 되면 DB 저장 시 해당 코드는 무시되므로 외래 키의 값도 null이 저장됩니다.
 


 

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

 

  

JPA에서는 연관관계의 주인인 "다" 쪽(@ManyToOne)에서만 연관관계를 관리해도 데이터베이스에는 문제가 발생하지 않습니다. 하지만 이는 JPA가 제공하는 영속성 컨텍스트와 데이터베이스의 외래 키를 통해 데이터가 관리되기 때문이며, 순수한 객체 상태에서는 문제가 발생할 수 있습니다.

 

여기서 말하는 순수한 객체 상태란 JPA의 영속성 컨텍스트나 데이터베이스에 의존하지 않고, 객체 자체만으로 관계가 논리적으로 일관성을 유지해야 하는 상태를 의미합니다.

 

예를 들어, Member 객체에서 Team 객체를 참조하도록 설정했지만, Team 객체의 members 리스트에 해당 Member가 추가되지 않은 경우, Team 객체는 자신이 어떤 Member를 가지고 있는지 알 수 없습니다. 이 상태는 JPA 영속성 컨텍스트에서는 문제가 없지만, 객체 그래프를 직접 탐색해야 하는 상황에서는 심각한 논리적 오류를 일으킬 수 있습니다.

 

따라서 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주는 것이 가장 안전합니다. Member가 Team을 참조할 때, 동시에 Team의 members 리스트에도 해당 Member를 추가해주는 것이 필요합니다. 이를 위해서는 setTeam과 같은 연관관계 편의 메서드를 사용해 양쪽 객체 관계를 일관되게 관리해야 합니다.

 

이러한 방식은 JPA를 사용하지 않는 상황에서도 객체의 논리적 일관성을 유지할 수 있으며, 양방향 연관관계에서 발생할 수 있는 오류를 방지할 수 있습니다. 결론적으로, 객체의 양방향 연관관계는 양쪽 모두 관계를 명확하게 설정해 주어야 합니다.



public void Teamtest(){
    Team team1=new Team("team1","팀1");
    em.persist(team1);

    Member member1=new Member("member1","회원1");

    //양방향 연관관계 설정
    member1.setTeam(team1);
    team1.getMembers().add(member1);
    em.persist(member1);

    //양방향 연관관계 설정
    Member member2=new Member("member2","회원2");
    member2.setTeam(team2);
    team1.getMembers().add(member2);
    em.persist(member2);
}



위 코드처럼 양 쪽 방향 모두 영속시키는 건 객체적으로 아주 좋은 코드이지만, 개발자도 사람이다보니 저렇게 긴 코드를 필요한 매 순간마다 하게 되면 실수하게 될 것 입니다. 그래서  모두 관계를 맺는 코드를 하나의 메소드로 묶어서 사용하면 실수할 일이 줄어들고 개발의 편리성이 크게 증가하게 되죠. 이를 연관관계 편의 메소드라고 합니다.

public class Member {

    private Team team;

    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
}


member1.setTeam(teamA);를 설정하고 teamA를 teamB로 변경하고 싶어 member1.setTeam(teamB); 를 설정하면 teamA -> member1 관계를 제거되지 않은 상태에서 teamB와 연결되게 된다. 따라서 연관관계를 변경할 때, 기존 팀이 있으면 삭제하는 코드를 추가해주는 조건문을 추가해야 한다.

 

 



public void setTeam(Team team){

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

      this.team = team;
      team.getMembers().add(this);
}

 

'CS' 카테고리의 다른 글

HTTPS란?  (0) 2025.01.13
[JPA] 다양한 연관관계 매핑  (1) 2024.12.25
[JPA] JPA는 무엇이고 사용하는 이유는 무엇일까?  (1) 2024.12.03
TCP/IP 4계층 모델  (1) 2024.10.07
도커란 무엇일까?  (4) 2024.09.26