0. 개요
안녕하세요! 오늘은 SpringSecurity, JPA 환경에서 서로 다른 2종류의 회원을 설계하면서 느꼈던 고민들에 대해서 작성해보려고 합니다. 이 글을 작성하는 이유는 '내가 이렇게 구현했으니 참고하세요!' 보다는 '다른 사람들은 이런 상황에서 어떻게 구현할까?' 가 궁금해서에요. 제가 어떠한 이유로 이렇게 구현했는지 설명해 드리고, 여러분이라면 어떤 식으로 구현할지 의견을 들어보고 싶습니다. 제가 의도한 대로 구현하긴 했지만, 이게 최선이라는 확신도 들지 않고 다른 사람이라면 어떻게 이 문제를 풀었을까? 라는 궁금증이 풀리질 않아서 이 글을 쓰게 되었네요.
시작하기에 앞서, 2가지의 양해드리는 부분이 있습니다. 첫 번째로는 글을 작성하기에 제 프로젝트의 코드들은 이미 구현이 끝난 코드들이라, 글을 작성하면서 코드를 다시 작성했습니다. 정말 간단한 예시 코드의 사진이 들어가는 경우도 있어요.
두 번째는 각 전략에 대한 자세한 개념설명은 이미 다른 블로그에 너무 잘 되어 있으니, 구현했던 것 위주로 설명하겠습니다. (아마 다른 글도 읽어보셨다면 제가 실제 사용해본 위주로 글을 작성한다는 것을 아실 거예요!) 이 점 양해해 주시면 감사하겠습니다. 그럼 시작해 보겠습니다!
1. 너무 다른 2종류의 회원?
그래서, 제가 제목에 언급한 너무 다른 2종류의 회원이 뭘까요? 바로 '병원' 과 '회원'입니다. 둘이 가지고 있는 칼럼이 너무나도 달랐기에 이러한 고민이 시작되었어요.
먼저, 보통 다른 역할의 회원을 보통 이런식으로 구현하실 거라고 생각합니다.
보통의 다른 '역할' 정도라면 이런 식으로 Role을 부여해서 사용하고, Security에도 이를 바탕으로 권한을 부여해서 사용해요. 그런데, 문제는 제가 구현해야 하는 '병원' 과 '회원' 이 너무 다른 칼럼을 가지고 있다는 것이었어요.
이게 프로젝트 설계를 하면서 만들었던 회원 초안인데요, 서로 다른 필드들이 많이 보여요. 거기에 더불어서 병원은 의사, 운영 시간, 상담... 등등 여러 테이블과 연관관계를 맺어야 했어요.
단순히 구현, 관리를 쉽게 하기 위해 Member라는 Entity에 모든 Column을 넣어서 관리할 경우에 생기는 불만은
1. '회원' '병원' 에 따라서 null값을 가지는 Column이 너무 많이 생긴다 (설계 자체를 잘 못한 거라고 생각함)
2. 실제 코드로 작성할 시 '병원', '회원'과 연관관계를 맺는 필드들도 작성해야 할 텐데, 결국 구현하면서 너무 난잡해질 것이다.
3. 데이터의 불안정함, 일관성, 복잡성, 확장성... 등등
단순하게 생각해도 너무 많은 불만이 떠올랐어요. 그래서 이를 추상화를 통해 해결해보자 했습니다.
2. 사용해본 전략들
제가 원하는 구현의 형태는 공통된 정보들을 관리하는 부모 클래스를 하나 만들어서 이에 BaseEntity와 Spring Security의 UserDetails를 상속시키고, 이를 각각의 회원과 병원이 상속받는 형태였습니다.
2-1. SingleTable 전략
우선, JPA에서 제공하는 연관관계 전략 중 SingleTable 전략을 사용해 보았습니다.
이렇게 구현하고, 실행해 보면 제가 원하는 형태의 엔티티가 생성되었을까요?
제가 글의 처음에 우려하던 형태의 entity 하나만 만들어졌습니다. 짧게 SingleTable 전략에 대해서 설명해 보면,
이 전략을 사용하면 부모 클래스와 모든 자식 클래스의 속성이 하나의 테이블에 모두 저장돼요. 테이블에는 구분자 컬럼이 추가되어 각 레코드가 어떤 자식 클래스에 속하는지를 알려줍니다.
제가 위에서 작성한 코드와 같이 생각해보면, 부모의 entity에 자식의 속성들이 모두 포함돼서 생성됐죠? 저는 이것과 더불어 별도의 엔티티를 원했고 글 처음에 언급한 null값에 대해서도 안전하지 못하기 때문에 이 전략은 실패입니다.
2-2. Joined 전략
이번엔 Joined 전략을 사용해 보겠습니다.
단순하게 strategy만 바꿔주었습니다.이렇게 변경하고 실행해 보면?
방금 전의 SingleTable 전략과 반대로 어느 정도 제가 원하는 형태의 entity들이 만들어졌습니다. 이 전략에 대해 짧게 설명해 보자면, 부모 클래스와 자식 클래스의 별도의 데이터베이스 테이블이 생성되는 방식이에요. 이때, 자식 테이블은 부모 테이블의 pk값을 외래키로 사용하여 연결됩니다.
entity가 실제로 만들어진 것을 보시면 바로 이해하실 수 있겠죠?
이 방법은 썩 괜찮았지만, 마음에 완전히 들지는 않았어요. 왜냐하면, 저는 UserInfo라는 부모 클래스를 별도의 엔티티로 만들고 싶지 않았고, 그저 공통된 사항들을 추상화하여 반복되는 코드들을 단순화하고 유연하게 코드를 작성하고 싶었거든요. 또 부모가 가지고 있는 칼럼들을 조회해 오려면 조인을 사용해야 하는 부분도 썩 마음에 들지는 않았어요.
2-3. 문제의 발견
이 방법들 외에도 TABLE_PER_CLASS 전략이 존재하지만 제가 원하는 형태의 설계는 이루어지지 않을 것이라고 생각했습니다. 위에서 2가지의 방법을 시도해 보고, 곰곰이 생각해보면서 원인은 결국 부모클래스에 붙어있는 @Entity라고 생각했거든요. 이것을 제거하지 않는 이상 부모클래스의 entity를 생성할 것이었고, 이것은 제가 원하는 형태의 구현이 아니었어요. 어떻게 할지 곰곰히 생각해 보다가, 너무 어이없게도 제가 작성한 코드에서 힌트를 찾았습니다.
힌트가 뭐냐면... 바로 BaseEntity입니다.
2-4. MappedSuperClass
지겹도록 BaseEntity라는 것을 사용했으면서 왜 진작에 생각하지 못했을까요? UserInfo를 Entity가 아닌 MappedSuperClass로 선언하면 제가 원하는 형태의 구현이 될 것 같다고 확신했습니다.
1. 부모 클래스의 entity가 생성되지 않음
2. 공통된 로직과 컬럼을 부모에 넣으므로써 추상화
3. 테이블 설계가 단순해짐
4. null값에 대한 문제 해결 (자식들이 다른 자식의 컬럼을 가지지 않음)
5. 확장성 (새로운 종류의 회원이 추가된다면 이를 상속받기만 하면 됨)
드디어 원하는 형태의 entity가 생성되었고, 코드 상으로도 원하는 코드가 구현이 되었습니다!
3. 문제점
제가 원하는 형태의 구현을 해서 너무 마음에 들지만, 하나 불안한 점이 있었습니다. 지금까지 구현해 본 것을 나타내보면
이러한 형태가 만들어지는데요, 저는 제가 구현했기에 이 코드가 어떻게 동작할지 알 수 있지만, 팀원들이나 다른 사람들이 이 구조를 보면 쉽게 이해를 하지 못할 수도 있겠다는 생각이 들었습니다. 쉽게 말하면, 복잡도가 올라간듯한 느낌이 들었어요. 이 부분에 대해서는 제가 설명을 잘하고, 가독성 있게 코드를 짜야겠다고 생각하게 됐습니다. 그런데,
JWT를 사용한 인증 부분에서도 불만족스러운 부분이 생겼습니다.
위의 코드는 예시 코드가 아닌 제가 실제 프로젝트에서 작성한 코드인데요, 보시다시피 Spring Security를 위해 UserDetails를 구현하고 있습니다. 문제는요, JWT Token을 바탕으로 유저를 찾을 때에요.
UserDetailService의 구현체입니다. loadUserByUsername의 내용을 보면요, 우선 회원을 조회해서 찾았다면 업캐스팅 후 반환합니다. 반환하지 못했다면, 병원을 조회해서 찾아서 반환하거나 찾지 못했다면 에러를 던지는 모습이에요.
즉, 병원이 어떠한 요청을 보낼 때마다 JWT Filter에서 병원을 찾기 위해 항상 2번의 조회쿼리가 발생하게 되는 거에요.
사실 단순 조회쿼리가 얼마나 성능에 영향을 미칠까? 라고 생각할 수도 있지만 저는 이러한 부분이 마음에 들지 않았습니다. 그래서, 캐싱을 사용해서 cache hit이 발생했다면 이 2번의 조회쿼리를 발생시키지 않는 방법을 생각해 봤어요. 이 부분은 바로 이어서 다음 글에 작성해야 할 것 같습니다. 다음 글은 캐싱을 통한 성능향상이 되겠네요!
제가 고민했던 과정을 최대한 글로 녹여보려 했는데, 쉽지 않네요.. 제가 작성한 글이 잘 이해가 되실지 모르겠습니다.
결과적으로 @MappedSuperClass를 이용해서 추상화를 했구요, 이렇게 설계하고 구현하면서 만족스럽게 프로젝트를 진행한 것 같습니다.
여러분들은 이런 상황이라면 어떤 식으로 구현하실 것 같으신가요? 제 방법에 대한 조언이나 새로운 방식에 대한 제안과 같은 의견에 대해서 댓글로 남겨주시면 감사하게 듣겠습니다.
부족한 글 읽어주셔서 감사합니다!
'SPRING' 카테고리의 다른 글
[SPRING + REDIS] 유저 정보 캐싱으로 성능 개선해보기 (10) | 2023.08.23 |
---|---|
[SPRING + JPA + Thymeleaf] 게시판에 해시태그 기능을 구현해보자 (7) | 2023.04.08 |