• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

양방향 연관관계에서 관계를 바꿀 때

22.11.27 02:08 작성 조회수 302

0

[질문 내용]

안녕하세요. 예전 부터 궁금했던 내용인데, 혹시 나중에 나오지 않을까 해서 미루다가 결국 질문합니다..!

 

 

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

예전 강의에서는 양방향 연관 관계에서 주인이 관계를 변경할 때 위처럼 Member Entity는 자신의 필드에 새로운 Team을 세팅하고 Team은 Member를 list에 추가하는 식으로만 마무리했던 것으로 기억합니다.

지나가는 말로 영한님께서 기존 Member와 연관 관계에 있던 Team list에서 Member를 remove하는 작업도 해야 하는데, 연습하는 거니까 빼셨다고 하셨던 것 같습니다.

 

이 부분이 조금 궁금했습니다.

관계가 바뀌기 이전의 기존 Member의 Team list에서 Member를 지워 주는 부분을 어떤식으로 작성해야 할 지 조금 감이 안 잡힙니다..ㅠㅠ

고려할 사항이 너무나도 많다고 해야 할까요..

 

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

위처럼 단순히 remove()를 사용하기에는 몇 가지 고려할 사항들이 있었습니다.

첫 번째 문제는 remove()의 경우 add와 달리 list의 Member들을 필요로 하는 로직이 들어 있기 때문에 DB로부터 Team의 Member list 정보를 불러온 다음에 동작한다는 점입니다.

특정 로직은 Team의 Members를 사용할 일이 없어 굳이 DB와 Entity그래프를 일치시킬 필요가 없는데, remove()로직이 들어가면서 DB를 조회하는 일이 생긴다는 점입니다.

이로 인한 성능상 문제는 정말 미세하겠지만, 뭔가 조금 걸리는 느낌입니다..

 

두 번째는 remove()를 할 때에 eqauls()를 사용한다는 점입니다.

단순히 equals()를 오버라이딩하여 구현하면 될 줄 알았지만, 이 부분도 고려할 부분이 생각 보다 많았습니다.

크게 세 가지로 나뉘는 것 같았습니다.

  1. pk를 이용한 equals()오버라이딩

  2. pk와 연관관계 필드를 제외한 필드로 eqauls()를 오버라이딩

  3. Business-Id를 이용한 eqauls()오버라이딩

이렇게 세 가지 사항 정도가 고려되는 것 같았습니다.

각각의 장단점이 있어 보였는데, 3번이 제일 괜찮은 방식으로 보였습니다.

1번은 pk가 GenerateValue방식일 경우 Entity가 persist되기 이전에는 pk를 초기화하지 못 해, set과 같은 Collection을 사용할 때 제한이 생긴다는 점이나 NPE발생 가능성 내재, pk값이 null인 객체가 같은 객체로 인식이 되는 위험, 비영속 상태 객체와 영속 상태 객체의 eqauls연산 시 일치 불가 등이 있는 것 같았습니다.

사실 위의 문제점들이 발생할 만한 로직을 실제 작성하게 될 일이 많지는 않을 것 같지만 뭔가 내재된 위험이 많아 보여 패스했습니다.

2번은 pk와 연관 관계 필드를 제외한 모든 필드들이 합쳐서 Unique한 값을 갖지 못할 경우 중복이 발생하는 문제점이 있어 보였습니다.

3번이 제일 적절해 보였지만 Business-Id로 사용할 만한 데이터가 없을 경우 문제가 있을 것 같았습니다.

특정 글에서는 UUID와 같이 Business-Id를 일부로 두기도 한다는 것 같은데 괜찮은 방식인 지는 모르겠습니다.

 

내용이 길어졌는데 결론은 양방향 연관 관계에서 관계를 바꿔 줄 때 Entity 그래프를 일치시키기 위해서 어떤 형태로 로직을 작성하는 지 궁금하다는 것입니다.

이렇게 보니 제가 너무 이상한 방식으로 접근한 게 아닌가 생각이 듭니다ㅠㅠ

현업에서는 어떤 식으로 구현하는 지 궁금한데 혼자 공부하다 보니 마땅히 예시를 볼만한 곳이 없어 질문드립니다. 긴 글 읽어 주셔서 감사합니다..! (_ _)

 

 

답변 1

답변을 작성해보세요.

2

y2gcoder님의 프로필

y2gcoder

2022.11.27

안녕하세요. 공부용님, 공식 서포터즈 David입니다.

1번 문제에서는 편의메서드에 해당 멤버가 팀을 갖고 있는지 체크하는 로직을 추가해주시면 될 것 같습니다.

 

public void changeTeam(Team team) {
    if (this.team != null) { // 해당 멤버가 기존 팀이 존재한다면
        this.team.getMembers().remove(this); // 그 팀의 멤버 리스트에 해당 멤버 객체를 삭제해주면 됩니다. 
    }
    this.team = team;
    team.getMembers().add(this);
}

 

그리고 2번 문제에서는 remove 작업 때 우려되는 점들에 대해 질문해주셨습니다.

우선 저는 경험이 미천해서 그럴 수도 있지만 실무에서 다대일 양방향 연관관계를 사용하면서 equals를 오버라이딩하는 경우가 없었습니다.

양방향에서 연관관계 편의 메서드가 필요한 이유는 DB와 영속성 컨텍스트로 불러온 객체들 사이의 일치를 위해서입니다.

영한님이 굳이 연관관계 편의 메서드에서 기존 team list에서 member를 삭제하는 로직을 추가하지 않으셨던 것은 해당 로직을 넣지 않더라도 DB에는 영향을 주지 않기 때문입니다. 다만 저희가 Team 을 조회해와서 사용하는 트랜잭션 안에서 Member 객체의 팀 변경이 일어났을 때 DB 와 현재 영속성 컨텍스트로 불러온 객체들 간의 불일치가 발생하기 때문에 발생할 수 있는 문제가 있을 뿐입니다.

그리고 Member에 대한 팀을 DB에서 설정해주는 것은 결국

this.team = team;

해당 부분입니다. member에 외래키가 있기 때문입니다.

  1. 신규 Member일 때는 트랜잭션 종료 시점에 해당 team(얘는 이미 DB에도 존재합니다)의 pk를 가져와서 저장해줍니다.

  2. 이미 있던 Member의 Team 변경일 때는 트랜잭션 종료 시점에 더티체킹을 통해 바뀐 team의 pk를 가져와서 update 문을 DB로 요청할 것입니다.

     

주로 다대일 양방향 연관관계에서 @OneToMany쪽은 읽기 를 위해서 주로 사용된다고 생각하시면 됩니다.

또한 remove를 한다는 것은 member라는 자바 객체에 대한 것에 국한되어있습니다. Team이 그로 인해 지워지거나 변경이 일어나지는 않습니다. team에서의 members에서 remove해주는 것은 결국 해당 컬렉션에서 this에 해당하는 member 객체의 참조값 삭제일 뿐이라고 생각합니다.

그래서 pk가 없다는 문제점도 해당되지 않습니다. 위에 보시면 pk로 체크하는 게 아니라 해당 객체의 주소값(this)로 체크하고 있습니다. ㅎㅎ

 

마지막으로 business-id 를 사용하는 것은 혹시 pk를 따로 두고 business-id를 사용하신다는 말씀이실까요? 아마 말씀하신 특정 글에서는 business-id를 따로 두는 이유가 있기 때문인 것으로 보입니다. 고민하시는 부분을 위해서만 business-id를 따로 두는 것은 추천드리지 않습니다. 정확히는 불필요합니다. (사실 저도 따로 뒀다가 조언을 듣고 고쳤던 터라 그 조언에 대해서 생각나지 않습니다. 죄송합니다. 생각나는대로 다시 추가 댓글로 답변드리겠습니다.)

 깔끔하게 답변드리지 못한 것 같아 죄송합니다. 혹시나 이해가지 않으시는 부분이나 제가 미처 생각지 못했던 부분이 있다면 말씀해주시면 저도 같이 고민해보고 답변드리겠습니다. :)



감사합니다.

공부용님의 프로필

공부용

질문자

2022.11.28

준영속 상태의 Entity와 영속 상태의 Entity의 레퍼런스가 다를 때 서로 간에 equal연산을 수행하는 경우 발생할 수 있는 문제점에 대해서만 고민하다가 정작 중요한 부분을 놓치고 있었던 것 같네요..!

로직상에서 준영속 상태의 Entity와 영속 상태의 Entity의 레퍼런스 차이가 있지만 동일 Entity처럼 동작하게 처리해야 하는 경우 equals()와 hashCode()를 오버라이딩 하는 것을 고려할 수 있을 것 같은데, 제가 연관 관계를 변경하는 부분을 그런 경우의 수라고 오해하고 있었던 것 같습니다..

답변해 주신 내용을 읽다 보니 연관 관계를 변경하는 부분은 애초에 준영속 상태의 Entity와 영속 상태의 Entity를 이용해서 비교해서 처리하는 로직이 있을 리가 없을텐데 말이죠 ㅠㅠ

그래도 해당 내용을 고민해 보면서 equals()와 hashCode()에 대해 공부해 보는 좋은 기회가 되었던 것 같습니다.

2번 내용은 정리가 좀 된 것 같습니다 ㅎㅎ

 

제시해 주신 1번 내용에 대한 문제 해결 방법에서 다시 의문이 들었는데,

Public List<Member> members = new ArrayList<>;

이런 식으로 필드에 미리 List를 초기화 해 놓는 식으로 사용하고 있을 경우, 또는 그렇지 않더라도

지연 로딩을 위한 프록시 객체가 생성되면서 null이 아니게 될 것 같은데

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

해당 조건문은 항상 참이 되어 실행되는 게 아닌 지 궁금합니다..!

 

매번 엉뚱한 질문에 자세하게 답변해 주셔서 감사합니다 :)

y2gcoder님의 프로필

y2gcoder

2022.11.28

아닙니다 공부용님 질문을 제가 답변드릴 때마다 항상 저도 많은 도움을 받습니다.

말씀해주신 문제점은 member 객체의 team 필드가 null인지 확인하는 부분입니다.

근데 걱정하시는 부분은 team 객체의 List<Member> members 가 초기화되어있는 부분이라 서로 상관이 없을 것 같습니다!

공부용님의 프로필

공부용

질문자

2022.11.28

아..! 제가 질문을 잘못 드렸네요!

Member입장에서 team을 지연 로딩 할 경우에, team변수에 프록시 객체가 초기화되기 때문에 null이 아니게 되어 항상 참이 되는 것이 아닌 지에 대한 내용이었습니다.

또, 기존에 team이 null이 아닌 상태에서, 즉, 기존에 특정 team과 연관 관계가 이미 있는 상태에서 다른 team으로 연관 관계로 변경할 때에는 remove()작업이 일어나게 되는데, 이 때, 연관 관계를 변경하기만 하고 종료되는 로직 처럼 Entity 그래프를 굳이 맞추지 않아도 되는 케이스에서는 remove()작업을 수행하기 위해 기존 team의 members가 전부 호출되는 것이 조금 비효율적으로 보였습니다!

물론 Entity 그래프를 맞추는 것을 기본으로 하는 것이 다른 로직에서 문제를 일으키지 않을 수 있으니 좋은 방식 같아 보입니다.

이런 상황을 고려하여 Entity그래프를 맞추면서 연관 관계를 변경하는 메소드와 연관 관계만 변경하는 메소드를 따로 만들까 했는데, 너무 나간 생각이 아닌가 싶었습니다.

아니면 혹시 제가 놓치고 있는 부분이 있어 전제 자체가 틀린 내용일까요? 질문이 길어져서 죄송합니다..ㅠㅠ

 

y2gcoder님의 프로필

y2gcoder

2022.11.28

질문 감사합니다 :)

Member입장에서 team을 지연 로딩 할 경우에, team변수에 프록시 객체가 초기화되기 때문에 null이 아니게 되어 항상 참이 되는 것이 아닌 지에 대한 내용이었습니다.

해당 부분은 사실 해당 관계에서는 Member를 저장할 때 항상 Team과의 관계도 같이 저장해주면서 외래키를 등록합니다. 그래서 Member를 조회할 때 프록시로 불러올 것이라 생각하고, 그게 또 요구사항에 따르면 맞는 것 같습니다. 사실 외래키에 null을 허용하면 말씀하신 부분이 항상 참이 되지는 않을 것 같습니다. 근데 또 Member를 조회해와서 해당 객체를 조작하는 과정에서 잠시 null인 경우가 발생할 수는 있을 것 같고(현재 요구사항에서는 개발자의 실수로 발생할 것 같습니다. changeTeam이라는 메서드 명은 사실 원래 팀이 있고, 다른 팀으로 바꾼다는 말을 전제로 지은 메서드명이라고 생각하기 때문입니다.) 그 때 해당 체크 로직이 유효할 수는 있을 것 같습니다.

또, 기존에 team이 null이 아닌 상태에서, 즉, 기존에 특정 team과 연관 관계가 이미 있는 상태에서 다른 team으로 연관 관계로 변경할 때에는 remove()작업이 일어나게 되는데, 이 때, 연관 관계를 변경하기만 하고 종료되는 로직 처럼 Entity 그래프를 굳이 맞추지 않아도 되는 케이스에서는 remove()작업을 수행하기 위해 기존 team의 members가 전부 호출되는 것이 조금 비효율적으로 보였습니다!

 

이 부분은 저는 오히려 불러오는게 필요하다고 생각이 드는게, 이미 DB와 객체 사이의 괴리를 줄이기 위해 설정하는 부분이기 때문입니다. 다른 때는 오히려 비효율적으로 보일 수는 있으나, 동일 트랜잭션 안에서 팀을 바꾸는 등의 로직을 수행할 때는 remove작없이 없다고 가정하면 영속성 컨텍스트에서 다시 조회해오기 전까지는 객체와 실제 데이터 간의 불일치가 발생하고 이로 인해 사이드 이펙트가 생길 수 있기 때문입니다. 또한 team.getMembers()를 해오더라도 초기화나 페치조인을 하지 않은 이상 앞선 질문에서 말씀하신 것처럼 프록시를 호출해오기 때문에 성능적인 부분에 대한 부담이 적다면 오히려 성능적인 부분을 감수하고 명확함을 가져올만 하다고 생각합니다.

이런 상황을 고려하여 Entity그래프를 맞추면서 연관 관계를 변경하는 메소드와 연관 관계만 변경하는 메소드를 따로 만들까 했는데, 너무 나간 생각이 아닌가 싶었습니다.

 

따로 만드는 것에 대해서는 제가 생각해보지 못했습니다. 고민하실 때 트레이드 오프를 잘 생각하시고 적용해보시면 좋을 것 같습니다. ㅎㅎ

 

공부용님의 프로필

공부용

질문자

2022.11.28

헉..제가 미처 고려하지 못 한 부분도 많았네요..

덕분에 매번 혼자 해결하기 힘들었던 문제들을 해소하는 데에 큰 도움을 받고 있습니다. 감사합니다!