인프런 커뮤니티 질문&답변

wisehero님의 프로필 이미지
wisehero

작성한 질문수

Java/Spring 주니어 개발자를 위한 오답노트

객체 지향적인 코드 짜기 (1) : 객체의 종류, 행동

JPA 양방향 연관관계 관련하여 질문 드립니다.

해결된 질문

작성

·

3.3K

8

안녕하세요. 지식공유자님 강의 잘 듣고 있습니다. 순환참조 관련 설명을 해주시면서 외래키를 직접 들고 있는 편이 낫다고 하셨습니다. 실제로 최근에 최범균님의 JPA 강의를 들으면서 연관관계를 사용하지않고 저렇게 외래키를 들고 있는 코드를 보았는데요. 제가 여태껏 배운 것과는 많이 달라서 몇 가지 의문점이 듭니다.

 

  1. 외래키를 저렇게 직접적으로 들고있을 시엔 ORM을 사용함에도 불구하고 다시 데이터베이스에 가까운 엔티티 설계로 돌아간 것이 아닌가 하는 의문입니다.

  2. 양방향 연관관계를 사용하지 않을 경우 그에 따라 orphanRemoval나 cascade 옵션을 사용하지 않음에 따라 추가적인 로직 작성이 필요하지 않나요?? 그에 따른 추가작업이 생길 수 있는데 혹시 제가 잘못 생각하고 있는 것인지 여쭙고 싶습니다

  3. 양방향 연관관계를 걸었을때와 외래키를 직접 들고있는 것 중 CRUD 성능에 크게 차이가 있을까요?

  4. 혹시 현업에서는 어떻게 하고 있을까요? 팀마다 다를까요?

답변 1

16

김우근님의 프로필 이미지
김우근
지식공유자

안녕하세요. 제가 인프런 UX에 익숙하지 않아서 질문 게시판이 있는 줄도 몰랐고 질문을 남기신줄도 모르고 있었네요. 답변이 늦어서 미안합니다.

  1. 외래키를 저렇게 직접적으로 들고있을 시엔 ORM을 사용함에도 불구하고 다시 데이터베이스에 가까운 엔티티 설계로 돌아간 것이 아닌가 하는 의문입니다.

    '일단 이 질문을 이해하기로 외래키 값을 이용한 간접 참조를 사용할 경우, 매번 object를 불러오기 위해 repository에 쿼리를 던져야 하는데, 이럴거면 ORM은 왜 쓰지? 그리고 매번 DB랑 통신해야하니까 DB에 가까운 설계 아닌가?'라는 생각이 드셨다는 것으로 이해가 되는데요.

     

    일단 이걸 먼저 말씀드리겠습니다. 시장에서 워낙 Spring / Jpa 이 두 개의 키워드가 메인 스트림이다 보니, 개발자분들이 자꾸 어떻게하면 Spring, Jpa, ORM을 잘 다룰 수 있는지 이런 고민만 더 많이 하게 되는 것 같습니다. 그런데 실제로는 도메인이 훨씬 중요하고, 도메인을 잘 설계한 다음 Jpa를 나중에 갖다 붙이는 방식으로 개발이 되어야 합니다.

     

    양방향 연관 관계는 순환 참조를 써도 된다는 의미가 아니라, "도메인 설계를 하다가 어쩔 수 없이 나오는 순환 참조 문제는 이거라도 써서 해결하세요." 라는 의미로 나온겁니다

     

    Jpa는 Object - RDB 맵핑이 번거로우니 그거 도와주려고 나온 솔루션일 뿐입니다. 도메인 위주의 생각을 먼저 해야 합니다. Jpa랑 도메인이랑 분리해서 생각하는게 먼저고, 순환 참조 없이도 도메인을 만들 수 있는게 먼저이며, 그 다음 Jpa를 갖다 붙이는 겁니다. 그러니 오히려 데이터베이스와 거리가 먼 엔티티 설계인거고요.

     

    저는 그래서 개인적으로 도메인 모델에 Jpa 어노테이션이 덕지덕지 붙어있는 것을 좋아하지 않습니다. 도메인 엔티티와 영속성 엔티티를 구분하는 방식을 선호하는데요. 가정을 하나 해볼게요. 만약 도메인 엔티티와 Jpa 엔티티에 구분이 없는 상황에서, 프로젝트가 RDB에서 MongoDB로 메인 DB를 변경하게 됬다면 어떻게 하실건가요? 도메인에 붙어있던 Jpa 어노테이션 다 지우고 mongoDB 어노테이션으로 또 변경해야 겠죠? 서비스 로직도 DB랑 강결합 되어있었다면 건드리게 될 수 있고요. 이런게 '데이터베이스에 가까운 엔티티 설계'인거죠.

     

    염려하신 것처럼 간접 참조를 하면 쿼리 몇 줄이 더 추가될 수 있습니다. 그런데 순환 참조를 없애고 도메인과 DB의 연결을 끊었을 때 얻을 수 있는 혜택들이 너무나 명확합니다. 복잡도가 줄어들고 데이터 접근 경로가 한방향으로 통일됩니다. 규모가 있는 서비스를 다룰 때 시스템 복잡도는 굉장히 큰 문제입니다. 원하는 데이터를 접근해야 할 때 어떤 지점에서 시작해서 어떻게 접근하도록 만들 것이냐도 굉장히 큰 문제고요. 데이터 접근 경로가 다양해지면 마냥 좋을 것 같지만 경로가 여러 개면 오히려 콜스택이 복잡해져서 추적하기 더 힘들어집니다.

     

    저도 세미나 자료, 강의, 책들을 많이 봐왔기 때문에, Jpa를 알려줄 때 항상 양방향 관계 얘기가 빠지지 않고 나온다는 것을 알고 있습니다. 그리고 그 어떤 강의와 자료에서도 그럼에도 양방향 연관 관계는 가급적이면 쓰지 말아주세요 라고 말하지 않는 것도 알고 있고요. 그런데 그건 Jpa 강의니까 그럴 수 있다고 생각합니다.

     

    순환 참조가 해악이라는 건 CS 설계쪽에선 거의 정설에 가깝습니다. 가끔 일부 클래스에 한해서 개발자들이 '여기 정도는 순환 참조 넣어도 괜찮겠지? 그러니까 양방향 연관관계 넣어야지'하고 넣기도 합니다. 저도 그랬었고요. 그래서 그 코드는 볼 때마다 지금도 후회중입니다. 😭

     

    이건 경험의 문제라서 와 닿지 않을 수도 있겠네요. 본인이 생각한 바가 있으면 일단 소신대로 프로젝트 개발해보시고 느껴 보시길 바랍니다. 느끼는 바가 없다면... 좀 더 규모가 있는 서비스 조직으로 이직해보시는 것을 추천드리고요. 취준생이시라면, 면접 때 가서 솔직하게 "공부해서 알고는 있었는데 체감은 잘 못하겠더라."라고 말해도 아무도 뭐라 안 할 겁니다 :)

  2. 양방향 연관관계를 사용하지 않을 경우 그에 따라 orphanRemoval나 cascade 옵션을 사용하지 않음에 따라 추가적인 로직 작성이 필요하지 않나요?? 그에 따른 추가작업이 생길 수 있는데 혹시 제가 잘못 생각하고 있는 것인지 여쭙고 싶습니다

    단방향 맵핑을 사용하는 경우에도 cascade를 지원하는 옵션이 없을리가 없을 것 같아서 찾아보니 있는 듯 보입니다. https://stackoverflow.com/questions/7197181/jpa-unidirectional-many-to-one-and-cascading-delete

     

    제가 Jpa를 하드하게 쓰는 편이 아니라서, 이 옵션을 실제로 사용해본 것은 아니네요. 그리고 애초에 저 같은 경우는 DB의 cascade 옵션 자체를 잘 안쓰게 되는 것 같습니다. 대신 이야기 해주신대로 추가적인 로직을 그냥 작성하고 있고요.

     

    cascade 옵션을 선호하지 않는 데에는 여러 이유가 있는데요. 일단 DBA분들이 외래키를 선호하지 않기 때문에 cascade delete 이 어려운 점이 있다는 게 하나 있고요. orphanRemoval이나 cascade로 인해, 의도하지 않은 delete 쿼리가 나가는 상황을 피하고 싶기 때문입니다. 대표적인 게 삭제 대상을 단 건 조회해서 하나씩 지우는 쿼리가 나가는 거죠.
    ex. https://jojoldu.tistory.com/235

     

    그리고 또, 보통 delete 쿼리를 던질 때는 아래처럼 던지죠

    DELETE FROM tableA WHERE col1 = 1;

     

    그런데 대용량 데이터를 다룰 때, DML에 영향 받는 레코드가 10,000개 이상이면, 쿼리 속도에도 영향을 줘서 데드락이 발생할 수도 있습니다. 그래서 보통 아래처럼 delete 쿼리를 n개 정도로 끊어서 여러 번 던집니다. 부하를 줄이기 위해 쿼리 중간 중간 텀을 두기도 하고요

    DELETE FROM tableA WHERE col1 = 1 LIMIT 10,000;


    저도 정확히는 모르겠지만 orphanRemoval이나 cascade가 데이터를 삭제할 때 이런 기능을 제공하나요? 만약 그렇다면 다음 프로젝트에서는 orphanRemoval이나 cascade를 적극 사용하는 방향도 고민해봐야겠습니다. 저도 제 머리를 믿기보단 hibernate 팀을 믿고 싶습니다. (그런데 이것도 진리의 팀바팀 사바사라는 점)

  3. 양방향 연관관계를 걸었을때와 외래키를 직접 들고있는 것 중 CRUD 성능에 크게 차이가 있을까요?

    외래키를 들고 있는게 CRUD 성능 때문은 아닙니다. 순환 참조를 피하기 위해서 입니다. 양방향 연관관계를 걸었다해도 lazy loading을 한다면 불필요한 쿼리가 나가지 않을테니 외래키를 값으로 들고 있는거랑 CRUD 성능은 똑같겠죠.

  4. 혹시 현업에서는 어떻게 하고 있을까요? 팀마다 다를까요?

    이 질문에 대한 답은 "진리의 팀바팀 사바사, 회바회" 입니다.

     

    음... 그리고 굉장히 큰 오해가 있으신게요. 현업이라고 막 개발적으로 완벽한 분들만 모여서 항상 완벽한 코드를 짜는 게 아닙니다. 강의 내에서는 제가 트랜잭션 스크립트 방식을 그렇게 뭐라고 했지만, 여전히 트랜잭션 스크립트 방식을 쓰는 회사나 조직도 있어요. 그리고 애초에 설계적인 완성도를 그렇게 신경쓰지 않는 곳도 분명 많습니다.

얘기가 길었네요. 종종 이런 질의 응답을 하다보면, 시장에 스프링이랑 Jpa를 잘못 알려주는 곳이 많아서 생기는 문제같다는 생각이 듭니다.ㅠ 아카데미들은 왜 이렇게 스프링이랑 Jpa만을 강조하고 개념을 외우는 것에 혈안이 되어있는지... 저는 좀 더 노골적으로 말해서 Jpa를 쓰는 이유는 쿼리를 쉽게 만들어준다는 이유 외에는 없다고 봅니다.

다른 지식공유자를 언급하는건 매우 조심스러운 일이지만, 존경의 의미로 언급을 좀 하겠습니다. 이건 김영한님의 강의인데요. 커리큘럼을 보시면 도메인을 먼저 개발하고 '스프링으로 전환'합니다. 이게 자연스러운 흐름이에요.

한번 스프링, Jpa 위주의 사고에 물들으면 빠져나오기가 매우 어렵습니다. 보강을 좀 추가할까 고민중인데, 일단 그 전에 개인적으로는 DDD, 헥사고날 아키텍처, 클린 아키텍처 시리즈를 공부 해보시길 추천드립니다.

PS. 이 영상도 진짜 괜찮아요. https://www.youtube.com/watch?v=g6Tg6_qpIVc

wisehero님의 프로필 이미지
wisehero
질문자

취준생도 이해할 수 있을만큼 친절한 답변이었습니다. 감사합니다. 일부분은 제가 가졌던 오해를 말끔히 해소했고 또 생각해볼 지점을 만들어주셔서 감사합니다!

wisehero님의 프로필 이미지
wisehero

작성한 질문수

질문하기