본문 바로가기

공부 자료/Spring

[JPA] N:1, 1:N, 1:1, M:N 연관관계 매핑 (단방향/양방향 연관관계)

객체와 테이블 매핑 이해하기

 

 

 

JPA에서 테이블을 한 개만 사용하는 것이 아니라면 필요한 것은 연관관계 매핑이다.

 

객체 지향 프로그래밍에서는 객체간 상태를 쉽게 참조하고 호출해 상호작용이 가능하지만,

RDBS에서는 외래 키와 조인을 사용해 관계를 표현한다.

따라서 이 두개의 간극이 존재하기에 이를 해결하기 위한 것이 바로 JPA이며, 연관관계 매핑인 것이다.

 

객체와 DB 간의 매핑을 통해 둘의 간격을 줄일 수 있으며,

객체 지향 관점에서 연관관계를 설정하고, DB 스키마에 맞게 매핑해 데이터를 효율적으로 관리할 수 있다.

 

그렇기에 우리는 연관관계 매핑을 사용하는 것이 중요하다.

 

 


 

연관관계를 매핑할 때에 생각해야 3가지 있는데, 아래와 같다.

 

1. 방향 : 단방향, 양방향

 

DB 테이블은 외래 키를 통해 양방향 테이블 조인이 가능하기 때문에 단방향 / 양방향을 나눌 필요가 없지만,

객체의 경우 참조 필드가 있어야 다른 객체 참조가 가능하다.

 

따라서, 두 객체가 있을 때 하나의 객체만 참조용 필드를 가지고 참조하면 단방향,

두 객체 모두가 참조용 필드를 가지고 참조하면 양방향이라고 한다.

 

그럼 단방향, 양방향 어떤것을 사용해야 할지 선택해야 하는데,

이는 비즈니스 로직에서 참조가 필요한지 여부를 생각해보면 된다.

 

예를들어, Member와 Team이 있다고 했을 때

 

회원이 어떤 팀인지는 알 수 있지만, 팀 이름을 통해 회원이 누가 있는지 알고 싶지 않다면

Member만 팀을 참조하는 단방향 매핑을 사용하면 됩니다.

 

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    // 팀과의 단방향 매핑
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

@Entity
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
}

 

 

하지만 해당 팀에 소속된 회원들도 누가 있는지가 궁금하다면, 양방향 매핑을 사용하면 될 것이다.

 

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    // 양방향 매핑
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    // 팀 엔티티와 양방향 연관관계 설정
    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
 }
 
 @Entity
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // 양방향 매핑
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

 

 

그럼 무조건 양방향 관계를 하여 일단 정보를 가지고 있는것이 좋지 않을까 라고 생각할 수 있지만,

모든 엔티티가 양방향 관계로 설정하게 되면 불필요한 연관관계 매핑으로 복잡성이 증가할 수 있기 때문에,

기본적으로 단방향 매핑을 하되 추후 역방향이나 객체가 필요하다고 느낄 때 추가해도 된다.

 


 

 

2. 연관 관계 주인 : 양방향일 경우 관리의 주체

 

양방향일 경우에는 두 객체에서 모두 참조 필드를 가지게 되는데,

이 때 연관관계의 주인을 정해 실질적인 관계가 어떤 것인지 JPA에게 알려줘야 하는데,

이를 mappedBy 속성을 이용해서 지정해준다.

 

연관관계의 주인은 두 객체 사이에서 CRUD가 가능하지만, 주인이 아니라면 조회만 가능하고,

주인이 아닌 객체에 mappedBy 속성으로 주인이 어떤 것인지를 지정해 주는 것이다.

* 외래키가 있는 곳을 연간 관계의 주인으로 지정

 

이것도 예를 들어 설명을 해보자면 Member와 Team이 양방향 관계를 가지고 있다고 한다면,

JPA는 Member에서 Team을 수정할 때 FK를 수정할지, Team에서 Membere를 수정할 때 FK를 수정할 지 결정하기 어렵기에

주인을 정해 Team에서 Member를 수정할 때에만 FK를 수정하는 것 처럼 주인이 누구인지를 명확히 정해주면 되는 것이다.

* Team에서는 Member를 조회할 수 있지 Member를 변경할 수는 없지만, Member는 팀을 변경할 수 있다.

* 코드는 위의 코드를 참고하면 된다.

 


 

 

3. 다중성 : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)

 

다중성의 경우에 객체를 기준으로 하는 것이 아닌 DB를 기준으로 결정한다.

 

 

* 다대일, 일대다, 다대다의 경우 Member와 Team의 관계를 예시

* 일대일의 경우 Member와 UserNum(회원번호)의 관계를 예시

* 단방향/양방향의 경우에는 앞에서 설명했으므로 다중성에 초점을 맞추어 이야기 할 예정이다.

 

 

[ 다대일(N:1) ]

 

요구사항이 아래와 같다면, Member과 Team는 다대일(N:1) 양방향 관계를 가진다.

- Member는 하나의 팀만 가질 수 있다.

- Team은 여러명의 Member를 가질 수 있다.

- Member에서 팀을 조회할 수 있고, Team에서 Memeber를 조회할 수 있다.

 

즉, 외래키를 Member(N)가 관리하는 일반적인 형태이다.

 

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    // 양방향 매핑
    // Team:Member = 1:N이기에 @ManyToOne
    @ManyToOne
    @JoinColumn(name = "team_id") // FK 관리
    private Team team;

    // 팀 엔티티와 양방향 연관관계 설정
    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
 }
 
 @Entity
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // 양방향 매핑
    // Team:Member = 1:N이기에 @OneToMany
    @OneToMany(mappedBy = "team") // 주인을 지정
    private List<Member> members = new ArrayList<>();
}

 


 

[ 일대다(1:N) ]

일대다가 다대일의 반대 입장이 될 수 있지만, 다르다.

다대일(N:1)에서는 주인을 (N)에 두었지만, 일대다(1:N)에서는 주인을 (1)에 두는 것을 말하는 것이다.

즉, (1) 쪽에서 (N) 쪽 객체를 조작하는 방법이 되는 것이다.

 

* 이는 실무에서는 거의 쓰지 않으며, 만약 사용을 해야하는 경우, 다대일(N:1) 양방향 매핑을 하는 것을 권장한다.

 

그렇다면 예시를 보면서 설명을 같이 보길 바란다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

 }
 
 @Entity
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // 단방향 매핑
    // Team:Member = 1:N이기에 @OneToMany
    @OneToMany // (1)
    @JoinColumn(name="ID) // (1)
    private List<Member> members = new ArrayList<>();
}

 

(1) 양방향일 경우 mappedBy를 통해 주인을 명시해 주었는데, 양방향이 아니기에 @JoinColumn을 이용해 조인을 하게된다.

 

그렇다면 왜 사용하지 않는걸까?

쿼리문을 보면 이해가 갈 것이다.

 

Member member = new Member();
member.setName("홍길동");

entityManager.persist(member); // member 저장

 

 

쿼리문에서 member을 저장할 때에는 정상적으로 insert가 나가는 것을 볼 수 있지만, 문제는 다음에서 발생하게 된다.

 

Team team = new Team();
team.setName("홍길동즈");
team.setMember().add(team);

entityManager.persist(team); // team 저장

 

team을 저장할 때에는 Team을 insert하는 커리가 나간 뒤 member를 update하는 쿼리가 실행되는데,

Team 엔티티는 Team 테이블에 매핑되어 Team 테이블에 직접 저장이 가능하나, 

Member의 경우 FK를 저장할 방법이 없기 때문에 조인 및 업데이트 쿼리를 날려야 하는 문제가 발생하게 된다.

 

즉, Team을 저장했는데 Member가 수정이 되는 경우가 되어 버린다.

따라서 일대다 단방향 연관관계 매핑이 필요한 경우 그냥 다대일 양방향 연관관계 매핑을 사용하는 것이

유지보수에 수월하게 되는 것이다.

 


 

[ 일대일(1:1) ]

일대일의 경우에는 뒤집어도 1:1이 되기에 주인이 누가되어도 상관이 없다. 

즉, FK를 어디에 두어도 상관이 없다는 말과 동일하다.

하지만 일대일이기 때문에 Member(회원)와 UserNum(회원번호)가 있다면 주 테이블인 Member 테이블이 대상 테이블이 된다.

        

일대일 단방향일 때 FK를 주 테이블인  Member 테이블이 가진다면? 아래와 같이 나오게 된다.

(즉, Member가 한개의 회원번호만 가질 수 있다고 가정한다)

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    
    @OneToOne
    @JoinColumn(name="UserNum")
    private UserNum userNum;

 }
 
 @Entity
 public class UserNum{
 	@Id
    private Long id;
}

 

 

일대일 양방향이라면, 위와 동일하지만 똑같이 @OneToOne 설정 후 mapped 설정을 통해 양방향도 간단하게 만들어 줄 수 있다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    
    @OneToOne
    @JoinColumn(name="UserNum")
    private UserNum userNum;

 }
 
 @Entity
 public class UserNum{
 	@Id
    private Long id;
    
    @OneToOne(mappedBy="userNum")
    private Member member;
}

 

 

여기서 알고 있어야 한다는 사실은 일대일을 할 때에는 단방향을 지원하지 않음(대상 테이블에서 FK를 갖는 경우)을 알고 있어야 한다.

이것은 간단히 예를 들어보자면 BackNum 테이블에서 Member 테이블의 FK를 가지고 있다고 한다면,

이는 JPA에서 지원이 불가능하다.

이럴 때에는 일대일이므로 양방향 설정을 해주면 되는 것인데

이 또한 주의해서 사용해야 할 점은 외래키를 주 테이블에서 관리할 것인지 대상 테이블에서 관리할 것인지 생각해야 하며,

테이블은 한 번 생성시 변경이 어렵지만 비즈니스 로직은 언제든 변경될 수 있기 때문이다.

 

즉, 한 회원이 한 홈페이지에 여러 아이디를 가질 수 있지만,

한 아이디가 여러명의 회원을 가지기는 어렵기 때문에 다(N)가 될 수 있는 주 테이블 쪽에 FK가 있는 것이 변경에 유연하게 된다.

(물론 이게 100%의 정답은 될 수 없고, 종합적인 판단과 결정을 단순화 했을 때 그렇다는 것이다.) 

 

 


 

 

N:1, 1:N, 1:1, M:N 연관관계 매핑 (단방향/양방향 연관관계)에 대해서 정리해 보았는데,

내용도 내용이지만 생각해야 할 것이 많은 부분이었다.

내가 어떻게 설계를 하느냐는 매우 중요하기 때문에 명확히 알고

어떤 상황에 어떤 매핑이 필요한지에 대해서 충분히 생각하고 알맞은 방법을 선택하는 것이 중요할 것 같다.