[JPA] 연관 관계 매핑 ( 양방향 )

해당 내용은 이영한님의 자바 ORM표준 JPA프로그래밍 책을 공부하면서 가볍게 정리한 내용입니다. 책과 영상으로 공부하면서 현업에서 너무 잘 사용하고 있어서 복습할겸 이렇게 정리하게되었습니다. 꼭 영상과 도서로 보시길 바랍니다.

전에 포스팅했던 연관 관계 매핑 (단방향)편을 보면 당연히 단방향이 있다면 양방향도 있다는것을 추측할 수 있을 것입니다.

이번 포스팅은 양방향 연관관계에 대해 알아보도록 하겠습니다.

기존에 소스는 Member -> Team 으로만 단방향 매핑을 하였기 때문에 Member만이 Team을 알 수 있었습니다. 하지만 양방향으로 설정하기 위해서는 Team -> Member로도 알수 있어야 하기 때문에 Team.class에 Member에대해 알수있도록 매핑해주도록 하겠습니다.

// Team.class 에 Member에 대한 필드 추가
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {

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

    private String name;

    // 추가된 부분 
    @OneToMany(mappedBy = "team")
    private List<Member> member = new ArrayList<>();


    public Team(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Team{");
        sb.append("id=").append(id);
        sb.append(", name='").append(name).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

기존의 소스와 비슷해보이지만 중간에 @OneToMany를 사용하여 Team에서도 Member를 알수 있도록 했습니다.

팀과 회원은 1:N관계 입니다, 따라서 Team.class에서는 N을 표현하기 위해서 컬렉션은 List를 사용하였습니다. 추가적으로 @OneToMany를 사용하여 One인 Team쪽에서 Many인 Member와 연관관계를 맺어주는 것입니다.

따라서 mappedBy="team"을 설정 함으로써 Member.team과 연관관계를 맺게됩니다.

Team team = new Team("토트넘");
teamRepository.save(team);

Member member = new Member("손흥민", team);
Member member2 = new Member("박지성", team);
memberRepository.save(member);
memberRepository.save(member2);

Member findMember = memberRepository.findById(1L).get();
Team findTeam = findMember.getTeam();
findTeam.getMember().stream().forEach(System.out::println);

// 출력결과 
없습니다.

여기 까지 설정을 하였다면 Team.getMember()를 통해 컬렉션으로 객체 그래프 탐색을 할 수 있을거라고 생각했지만 처음 출력해보면 아무것도 출력이 되지 않습니다. 그이유는 아래에서 보겠습니다.

☝ 연관관계의 주인이 뭐일까 ??

@OneToMany 만 보면 말그대로 누가 One이고 누가 Many인지 쉽게 알수 있습니다
그렇다면 @ManyToOne에 보이지 않았던 mappedBy 속성은 왜있는것을까요 .

mappedBy속성은 연관관계의 주인을 정해줄때 사용하는 속성입니다.

연관관계의 주인은 DB 연관관계와 매핑되고 외래키를 관리(등록,수정,삭제)할 수 있고 주인이 아닌쪽은 읽기만 가능합니다.

주의사항은 mappedBy옵션은 주인이 가지고 있는것이 아닌 주인을 가리키고자 하는곳에 속성을 넣어주면 됩니다.

연관관계의 주인은 주로 1:N중에서도 N쪽이 연관관계의 주인이 됩니다. 따라서 Member가 연관관계의 주인이 되기 때문에 Team에서 mappedBy로 Member가 가지고 있는 team 필드를 가리키게 됩니다.

양방향 연관관계사용시 주의해야할 점은 연관관계의 주인이 아닌 Team.class에서 getMember().add()를 통해 추가를 하였으니 연관관계가 추가되었을거라고 생각할 수 있지만 주인이 아닌 곳에서만 추가를 한다면 정상적으로 추가가 되지 않을 것 입니다..

예시 코드를 보겠습니다.

 Team team = new Team("토트넘");

Member member = new Member("손흥민");
Member member2 = new Member("박지성");
memberRepository.save(member);
memberRepository.save(member2);

team.getMember().add(member);
team.getMember().add(member2);
teamRepository.save(team);

해당 소스를 보면 member, member2가 영속상태이고 서로 양방향이니 컬렉션에 add를 해서 team에 member를 등록한다고 볼수 있지만 막상 저장되고 난 상태를 보면 다음과 같습니다.

서로 양방향관계 설정했고 컬렉션에 추가했는데 왜 안되냐?? 라고 생각할 수 있지만 , 연관관계의 주인이 아닌 쪽에서 값을 저장했기 때문입니다, 위에 나와있는것처럼 연관관계의 주인만이 외래키의 값을 변경할 수 있습니다. 따라서 key값에 null이 위치하게 됩니다.

그렇다면 이 문제를 어떻게 해결해야 할까요 .

그 해결은 객체 관점에서 바라보고 양쪽 방향에서 모두 값을 입력해주는것이 가장 안전하다 입니다. 양방향 관계 양쪽에서 값을 입력하지 않는다면 순수한 객체상태에서 문제가 발생할 수 있습니다. 따라서 편의 메소드를 양쪽에 작성해주면 됩니다.


☝ 양방향 연관관계 편의 메소드

양방향 연관관계는 결국 양쪽다 저장할 수있도록 신경써줘야 합니다.
따라서 아래처럼 코드가 작성될 수 있습니다.

Team team = new Team("토트넘");

Member member = new Member("손흥민");
Member member2 = new Member("박지성");
memberRepository.save(member);
memberRepository.save(member2);

member.setTeam(team);
member2.setTeam(team);

team.getMember().add(member);
team.getMember().add(member2);
teamRepository.save(team);

위처럼 코드를 작성 하고 실행해본다면 DB에 연관관계가 제대로 작성되어 들어간것을 확인 할 수 있습니다.

그런데 문제가 하나있을 수 있습니다. 만약 둘중 한곳에서 연관관계를 추가해주는것을 깜빡하게 된다면 제대로 추가가 안될 수 있습니다.

이러한 문제를 해결하기 위해 리펙토링 하자면 다음과 같이 변경될 수 있습니다 .


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


// After
public void setTeam(Team team) {
	this.team = team;
    team.getMember().add(this);
}

이렇게 변경해주게 된다면 setTeam()만 호출해도 알아서 양방향 연관관계를 설정하게 됩니다.

리펙토링이 완료되었습니다. 문제가 없을까요??

안타깝게도 하나의 문제가 더있습니다.
바로 Member 객체에서 2번의 setTeam이 이뤄졌을 경우입니다.

실제로 DB상에는 team2와 연관관계가 맺어져 있지만 보이지 않지만 team과도 컬렉션을 통해 연관관계가 맺어져 있는것입니다.

이런것을 감안하여 team이 변경될때는 기존의 관계를 제거하고 새로 연관관계를 맺도록 다시한번 소스코드를 리펙토링 해줘야 합니다.

// Before
public void setTeam(Team team) {
	this.team = team;
    team.getMember().add(this);
}

// After
public void setTeam(Team team) {
	if(this.team != null) {
    	this.team.getMember().remove(this);
    }
	this.team = team;
    team.getMember().add(this);
}

이 문제는 실제 Database에는 문제를 일으키거나 저장된 이후에 불러온다 한들 크게 문제는 없습니다. 이 문제는 영속성 컨텍스트가 끝나지 않고 저장하기 직전에 getMember()를 통해 코드를 호출하게 되었을때 team조회시 member2가 검출되어 나온다는 점에서 문제가 되는것입니다. 크게 문제가 될것은 아니지만 좀더 안전하게 코드를 작성하기 위해 다음과 같이 작성해 주면 됩니다.

항상 이 책을 보면서 현업에서 일을하고 다시 복습하면서 생각하지만 보면볼수록 더 깊은 고민을 할 수 있게 되는것 같습니다.

좋은 웹페이지 즐겨찾기