본문 바로가기

TIL & WIL

TIL_220629_양방향 연관관계를 이해해보자!

항해 99 실전프로젝트가 시작되었다.

하나하나 기록하면서 가고자 하는 마음을 다잡아야 할 때가 된 듯 하다.

생각보다 미니 프로젝트, 클론 코딩 프로젝트 주간이 굉장히 타이트했고 쉽지 않았다.

특히 클론 코딩 프로젝트 주간에는 웹소켓, Stomp, Redis를 이용한 Slack Clone 코딩을 진행했었는데,

일주일동안 정말 매일같이 조원들과 새벽까지 매달렸던 것 같다.

그러다보니 소홀하게 된 TIL 작성 ㅠㅠ..

아직까지는 기억 속에 있으니 괜찮다지만 나중을 위해서는 하나하나 기록하는 습관을 들여야 하겠다.

 

오늘은 양방향 연관관계 매핑에 대해서 이해해보는 글을 작성한다.

김영한 님의 강의 및 책을 보고 깔끔하게 정리하신 블로거 분의 글이 있어 그 글을 참고하였음을 밝힌다.

**참고 블로그 URL**

 


참고 블로그에서 가져온 이 예시 이미지에서 볼 수 있는 것이 양방향 객체 연관관계이다.

실제로 지금 내가 하고 있는 실전 프로젝트에서의 코드를 잠시 가져와보자면,

 

<Dog Entity>

    @ManyToOne
    @JsonIgnore
    @JoinColumn(name="MEMBER_ID")
    private Member member;

 

<Member Entity>

    @OneToMany(mappedBy = "member")
    private List<Dog> dogList = new ArrayList<>();

 

반려견 산책 관련 매칭서비스를 구현하다보니 생긴 테이블인데, 

회원 1명이 여러 마리의 반려견을 키울 수 있다보니 저렇게 설정하게 되었다.

 

위의 이미지에서 Member 엔티티의 Team team 부분이

첨부한 코드에서 보면 Dog 엔티티의 Member member라고 볼 수 있다.

 

반대로 위의 이미지에서 Team 엔티티의 List members 부분이

첨부한 코드에서 보면 Member 엔티티의 List<Dog> dogList = new ArrayList<>(); 라고 볼 수 있다.

 

객체 간의 연관관계는 다음과 같다.

* 회원 >> 반려견 : Member.dogList

* 반려견 >> 회원 : Dog.member

 

 

그리고 객체 간의 연관관계는 단방향 연관관계가 두개가 존재한다고 보는 것이 편하다.

쉽게 생각해보면 회원 입장에서는 회원 1명 당 여러마리의 반려견을 키울 수 있으므로

@One(회원)ToMany(반려견) 어노테이션을 사용하면 될 것이고,

반대로 반려견 입장에서 회원을 바라볼 때는 @Many(반려견)ToOne(회원) 어노테이션을 사용하면 적절할 것이다.

 


 

Member Entity에서 속성값으로 쓰고 있는 'mappedBy=member'  라는 부분은

연관관계의 주인을 설정해줄 때 쓰는 속성이다.

  • 연관관계의 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아닌 Entity에서 mappedBy 속성을 사용하며, 연관관계의 주인 Entity와 매핑되었음을 보여준다.

예시 코드에서보면 Dog Entity는 연관관계의 주인이 된 것임을 알 수 있고, MEMBER_ID 라는 외래키를

직접 관리하게 됨을 알 수 있다. 주인이 아닌 쪽 즉, Member 엔티티에서는 '읽기(member.getDogList())'만 가능하고

외래키를 관리하지는 못하게 된다.

 

데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다(Many) 쪽이 외래 키를 가진다.
다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy 를 설정할 수 없다.
따라서 @ManyToOne에는 mappedBy 속성이 없다.

 


이 이미지는 위에서 보여준 객체간의 연관관계가 아니라 DB에 저장되어지는 테이블간의 연관관계를 보여준다.

내가 소속된 팀 이름을 알고 싶으면 TEAM 테이블의 TEAM_ID로 MEMBER 테이블에 조인하면 된다.
팀에 소속된 멤버들을 알고 싶으면 MEMBER의 TEAM_ID로 TEAM 테이블에 join하면 된다.
따라서 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다.

객체 간의 연관관계와는 다르게 테이블 연관관계는 외래키(FK)하나로 양방향을 조회할 수 있음을 알 수 있다.

(이 부분도 중요하니 메모메모)

 


자, 그렇다면 예를들어 Dog Entity를 이용하여 DB에 회원이 키우고 있는 반려견 1마리의 정보를 저장한다고 하자.

이때 해당 반려견이 어떤 회원의 반려견인지 데이터를 잘 매핑하여 저장해야 할 것이다.

 

이것이 위에서 말했던 외래키(FK)의 개념이며, Dog Entity에 있는 Member member라는 컬럼이 외래키 역할을 할 것이고,

그 컬럼이 DB에서 표시될 때는 name="MEMBER_ID" 속성에 의해 MEMBER_ID라는 컬럼명으로 매핑이 될 것이다.

 

<DB 테이블 예시>

 

실질적으로, Spring Boot 프로젝트 > Controller > Service 부분에서

클라이언트 측으로부터 들어온 반려견 정보 등록(POST) 요청이 실행될 때,

어떤식으로 반려견과 회원이 정보를 매핑해주는 지 살펴보자.

 

public DogProfileResponseDto addProfile(MultipartFile multipartFile,
                                            DogProfileRequestDto requestDto) throws IOException {

        String username = SecurityUtil.getCurrentMemberUsername();
        Member member = memberRepository.findByUsername(username).orElseThrow(
                () -> new IllegalArgumentException("해당하는 ID의 회원이 존재하지 않습니다."));

        // 멍 프로필이 이미 3개가 등록되었을 경우
        if(member.getDogList().size() == 3) {
            return new DogProfileResponseDto("false", "멍 프로필 등록은 최대 3개까지만 가능합니다.");
        }

        // 처음 멍프로필 등록 시 첫 강아지가 자동으로 대표강아지로 등록
        if(member.getDogList().size() == 0) {
            requestDto.setIsRepresentative(true);
        }

        // Dog 엔티티에 image 빼고 정보 저장
        Dog dog = new Dog(requestDto);
        dog.setMember(member); // 연관관계 편의 메소드
        dogRepository.save(dog);

해당 코드에서 맨 아래쪽 두 줄을 살펴보면, dog라는 객체에 setMember(member) 해주는 코드를 볼 수 있고,

그 다음 JPA repository를 이용하여 DB에 dog 객체를 저장해 줌을 알 수 있다.

그럼 여기서 dog.setMember(member)가 무엇인지 코드를 보자면,

 

    public void setMember(Member member) {
        this.member = member;
        member.getDogList().add(this);
    }

이렇게 구성이 되어있다. 여기에서 매개변수(파라미터)로 받아오는 member라는 객체가 특정 회원인 것이고,

해당 member의 정보를 this.member = member를 통해 dog 객체에 member를 세팅해주는 것을 볼 수 있다.

 

그리고 반대 입장으로도 정보의 저장이 필요한데, member 입장에서도 어떤 강아지가 나의 반려견으로

등록되었는지 알 수 있어야 하기 때문에

member.getDogList().add(this)를 통해 Member Entity에 있었던 List<Dog> dogList 라는 ArrayList에

dog 객체를 등록해준 것이다.

 

객체 관점에서 보았을 때는 이런 식으로 양쪽 방향에 모두 값을 입력해주는 것이 매우 중요하다.

어느 한쪽만 입력했을 경우는 Dog DB 테이블에 MEMBER_ID 외래키가 null 값으로 들어가게 되거나,

Member Entity에서 member.getDogList()를 통해 등록된 반려견 리스트를 조회하려고 했을 때

해당 리스트에 값이 아무것도 없을 수 있기 때문이다.

 

그것을 편하게 해줄 수 있는 코드 묶음이 바로 위에서 본 setMember 메소드이고,

이러한 메소드를 '연관관계 편의 메소드'라고 부른다.

 

 

 

오늘의 TIL은 여기서 마무리해본다.

끝.