Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags more
Archives
Today
Total
관리 메뉴

ultra_dev

Schema 여정기 Multi-Tenancy(feat. PostgreSQL) 본문

SPRING&JAVA

Schema 여정기 Multi-Tenancy(feat. PostgreSQL)

ultra_dev 2023. 11. 1. 21:07

* PostgreSQL에는 Schema라는게 존재한다.

MySQL에서는 Schema가 테이블을 의미하지만,

PostgreSQL에서는 테이블의 집합을 의미, 하나의 데이터베이스를 논리적으로 나누는 개념이다.

-> MySQL의 논리 데이터베이스와 유사하다고 볼 수 있다.

 

새로 만드는 서비스에서

회사별로 Schema를 통해 논리적으로 데이터베이스를 구분한다고 한다.

 

화면설계서 만들고 db가 나오려면 시간이 좀 걸린다고 하니까

스키마를 통한 회사 데이터베이스간 완전 격리를 예상하고 공통 코드를 설계했다.

 

처음엔

한번만 스키마를 바꿔주면 이후에 바꿀 필요가 없을 것이라고 예상했다.

그러라고 있는게 스키마니까..

 

그 방법으로 찾은 것이 MultiTenancy

https://www.baeldung.com/hibernate-6-multitenancy

 

 

동적으로 스키마를 바꿔주는게 가능하다.

@Component
public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver, HibernatePropertiesCustomizer {

    private final ThreadLocal<String> currentTenant = ThreadLocal.withInitial(() -> "public");

    public void setCurrentTenant(String tenant) {
        currentTenant.set(tenant.toLowerCase());
    }

    @Override
    public String resolveCurrentTenantIdentifier() {
        return currentTenant.get();
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return false;
    }

    @Override
    public void customize(Map<String, Object> hibernateProperties) {
        hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
    }

}

 

주의 : currentTenant를 그냥 String으로 선언하면 동시성 문제가 발생하므로 ThreadLocal로 묶어 줘야한다!

 

 

이 가정하에 밑에 느낌으로 Security 필터도 새로 만들고(당연히 회사 코드 아님! 예시로 만든 코드일 뿐 완전 다름!!

@RequiredArgsConstructor
public class CompanySchemaFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final TenantIdentifierResolver tenantIdentifierResolver;
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        @NonNull HttpServletResponse response,
        @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        if (!isPublicSchemaProviderUri(requestURI)) {
            String companySchema = jwtUtil.getCompanySchema(request);
            tenantIdentifierResolver.setCurrentTenant(companySchema); // 회원 회사별 스키마 분기
        }
        filterChain.doFilter(request, response);
    }

    private boolean isPublicSchemaProviderUri(String requestURI) {
        return Arrays.stream(WHITE_LIST_SCHEMA_FILTER)
            .anyMatch(pattern -> antPathMatcher.match(pattern, requestURI));
    }
}

 

이런 식으로 Jwt 토큰 안에 회사 타입을 넣어주고 로그인 시점에 해당 회사 스키마로 동적으로 변경하는 방식으로 설정했다.

 

하지만 erd 설계가 진행되면서 

내가 생각했던 방향과는 많이 달라진다는 걸 깨달았다.

 

1번 문제 

public 스키마에 공통으로 두는 테이블들이 존재하고 join이 필요!!

 

해결 :

 

1. public Entity들은 어떤 상황에서도 public만 바라보도록 지정해서 이후 멀티테넌시를 이용해서 데이터소스를 변경해도 public으로 쿼리가 날아가게 만들었다.

@Table(name = "example", schema = "public")

 

2. public이 아닌 엔티티들은 멀티테넌시에 맞는 식별자를 파라미터로 받고, 해당 스키마로 변경하도록 수정

(-> 반복되는 코드로 인해 추후 aop 처리)

 

 

2번 문제 

제일 머리를 싸맸던 문제였다.

 

모든 스키마를 돌면서 값을 얻어오고 그 데이터를 바탕으로 하는 로직들이 존재한다.

 

jpa든 jpql이든

연결된 데이터소스 스키마에 해당하는 테이블과 매핑이 될 뿐 

schema1.team, schema2.team 이런식으로 인식 시키게 할 순 없다.

 

네이티브 쿼리를 쓴다면 가능하지만, 많은 코드를 그렇게 짠다면 유지보수 측면이나 휴먼 에러나

네이티브 쿼리는 최대한 지양을 해야할 뿐더러 

설령 네이티브 쿼리를 쓴다고 쳐도 아래의 문제가 발생한다. 

 

1.현재 고객사 생성 시, 해당 고객사에 해당하는 스키마를 추가로 생성해준다.

 

2.현재 백엔드 서버는 1개이다.

 

3.모든 고객사 스키마를 돌면서 값을 조회하고 그걸 바탕으로 만드는 로직들이 존재한다.

 

4.네이티브 쿼리로 schema1.team, schema2.team 같은 방식으로 짜서 값을 얻어온다 쳐도 고객사가 새로 생기는 순간

코드를 수정해야 하고

그러면 1개 뿐인 백엔드 서버는 내려갔다 올라가야 한다. 무중단이 불가능해진다..

 

5. 그렇다면 고객사 스키마가 생겨도 동적으로 해결 할 수 있는 방법이 없을까?


 

해결책

여러 스키마에 대한 값이 필요한 경우를 뷰테이블로 만들어서 entity에 매핑..!! 

모든걸 JPA와 JPQL만으로는 해결 불가능하다는 걸 인지하고, 프로시저를 써서 스키마 갯수만큼 반복문 돌면서 Union All을 통해 뷰테이블을 생성하는 로직을 만들었다. 

 

이후 뷰테이블을 엔티티와 매핑하여 필요 값들을 얻어올 수 있도록 설정해서 문제를 해결했다.

@Entity
@Getter
@Immutable
@Table(name = "example_view", schema = "public")
public class ExampleView {

 

 

'SPRING&JAVA' 카테고리의 다른 글

AOP, Custom Annotation 활용기  (0) 2023.11.07
View Table + JPA 매핑  (0) 2023.11.06
체크 예외, 언체크 예외  (0) 2023.05.27
JPA 기본  (0) 2023.04.06
배열 <깊은 복사, 얕은 복사>  (0) 2023.01.13
Comments