본문 바로가기

개인적으로 공부한 것을 정리해 놓은 블로그입니다 틀린 것이 있으면 댓글 부탁 드립니다!


JPA

JPA 공부 3 - 연관관계 매핑2 양방향 연관관계와 연관관계 주인

반응형

 

위 그림은 양방향 연관관계의 구조를 그려 놓은 것이다 . 

 

객체의 양방향관계는 team 클래스가 List<Member> members 필드를 갖고 , member는 Team team 객체를 갖음으로써 서로 양방향에서 참조가 가능하다 . 

 

하지만 테이블 연관관계는 foreign key 하나로 양쪽에서 join이 가능하기 떄문에 단방향일 때와 달라지는 것이 없다 . 

테이블의 연관관계는 사실 방향이라는 개념이 없는 것이다. 

 

 

객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개로 이어져 있는것이다. 

 

하지만 테이블은 외래 키 하나로 두  테이블의 연관관계를 관리 하기 떄문에 양쪽으로 조인이 가능하다 . 

 

select * from member m  join team t on t.team.id = m.team_id

select * from team t join member m on t.team_id = m.team_id 

 

 

 

그러면 누가 외래 키를 가지고 있어야 하는 걸까 ? 

 

다시 위 그림의 보자  객체 관계에서 서로 다른 두방향으로 양방향이 구현됬다 . 만약 멤버의 팀을 바꾸고싶다면 멤버의 팀을 바꿔야할까 팀의 멤버스를 바꿔야할까 복잡해진다

 반면에 DB입장에서는 외래키만 업데이트 되면 되기 때문에 상관이 없다.

 

위와 같은 차이를 해결하기 위해 둘중 하나만 외래 키를 관리해야 한다는 룰이 생겼다 이를 연관관계의 주인(Owner)를 정한다고 한다.

 

객체의 두 관계중 하나를 연관관계의 주인으로 지정하고 , 연관관계의 주인만이 외래 키를 관리(등록,수정) 가능하다 

주인이 아닌쪽은 일기만 가능하고 , 주인은 mappedBy 속성을 사용하지 않고 주인이아니면 mappedby로 주인을 지정해줘야한다 .

 

 

누구를 지정해줘야 하는건가.

 

외래 키가 있는 곳을 주인으로 정하는 것이 좋다고 한다 위의 그림의 경우 Member.team 이 연관관계의 주인이 된다 .

 

N쪽이 FK를 갖게되고 FK를 갖은 N쪽이 연관관계 주인이 된다 .  연관관계 주인은 JoinColumn으로 연관관계를 맺을려는 컬럼을 지정하고  주인이 아닌 곳은 mappedBy로 주인을 지정한다.  

(현재 이해가 잘되지 않기 떄문에 일단 외우자.)

 

 

그렇다면 이렇게 양방향으로 연결해놓은 객체에 값을 셋팅할때는 어떻게 해야할까 

 

주인이 아닌곳에서는 읽기만 가능하다 했으니 FK를 갖은 곳에서만 값을 셋팅해주면 되는걸까?

 

코드를 한번 확인해보자 

 

            //팀 저장
            Team team = new Team();
            team.setName("team1");
            em.persist(team);

            Member member = new Member();
            member.setName("member1");
            //멤버에 팀저장
            member.setTeam(team);
            em.persist(member);

            //팀에 멤버 저장
            team.getMembers().add(member);

            em.flush();
            em.clear();
            
            

            Team searchedTeam = em.find(Team.class, team.getId());

            List<Member> members = searchedTeam.getMembers();

            members.forEach(System.out::println);

            tx.commit();

 

이미 멤버에 팀을 저장해줬는데  팀에 멤버를 저장하는 부분이 필요한가 싶기도하다

하지만  위 코드에서 flush() clear()하는 부분이 없다고 생각해보자 .  그렇다면 1차캐시의  team은 

member가 추가 되지 않은 , 팀을 저장할 당시의 팀이 불려올 것이고 members는 비어있을 것이다.    

 

또 객체 입장에서 양쪽을 자유롭게 참조할 수 있어야하는데  한쪽에서 찾으면 값이나오고 한쪽에서 찾으면 값이 null 인 상황이 발생할 수도 있기 떄문에 객체 지향적으로 봤을때도 문제가 될 수 있다고 한다.

이런 문제를 해결하기 위해서는 양쪽에 값을 다 셋팅해 주는 것이 좋다 . 매번 직접 셋팅하기 보다는 

 

한쪽에 셋팅할때 다른쪽도 함께 셋팅하는 방식을 사용하면 더 좋다.

아래는 멤버에서 팀을 셋팅하는 setter메서드이다.

 

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

 

인스턴스가 생성될때 team을 셋팅하는 동시에 파라미터로 들어오는 팀에 멤버리스트에 자기자신의 인스턴스를 add해준다,

자바의 getter setter 관례는 getter / setter에 다른 코드를 넣지 않는 것을 추천하기 떄문에 

위와 같은 메서드는 changeTeam 등으로 이름을 변경하는 것도 방법이다.이렇게하면 메서드가 더 강조되어 보이게 한다. 

 

롬복을 사용하다보면  @Data 사용시 ToString을 자동으로 만들어 버리는데 양방향으로 연관관계가 걸려있을시에 

양쪽 ToString에 양쪽다 각각을 ToString 하기 때문에 무한루프에 빠질 수 있다 롬복에서 ToString은 왠만하면 쓰지 않는 것이 좋다 . 

 JSON 생성 라이브러리를 사용할 떄도 마찬가지이다 . 스프링  컨트롤러에서  엔티티로 바로 받아버리면 JSON으로 변환시에도 같은 현상이 일어나게된다 . 엔티티를 바로 반환할 경우 문제가 하나 더 있다 엔티티는 변경될 수 있기 떄문에 엔티티가 바뀌어 버리면 API스펙 자체가 바뀌어 버리고 해당 API를 사용하는 곳에서도 문제가 일어날 수 있다 . 이 두가지 문제를 해결하기 위해선 컨트롤러에서는 요청과 응답에 대하여 엔티티를 그대로 반환하지 않고 Dto로 변환하여 반환하는 것이 좋다 .

 

 

 단방향 매핑만으로도 설계를하고 양방향은 필요할 떄 추가해도 테이블에는 영향을 주지 않기 때문에 일단 단방향 맵핑을 최대한 활용하는 것이 좋다 .  

 

 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다.  연관관계의 주인은 외래 키의 위치를 기준으로 정해야한다. 

 

반응형