Home > Architecture > πŸ—οΈ[Architecture] λ©€ν‹°ν…Œλ„ŒνŠΈ μ•„ν‚€ν…μ²˜(Multi-Tenant Architecture)λž€ λ¬΄μ—‡μΌκΉŒμš”?

πŸ—οΈ[Architecture] λ©€ν‹°ν…Œλ„ŒνŠΈ μ•„ν‚€ν…μ²˜(Multi-Tenant Architecture)λž€ λ¬΄μ—‡μΌκΉŒμš”?
Architecture

πŸ—οΈ[Architecture] λ©€ν‹°ν…Œλ„ŒνŠΈ μ•„ν‚€ν…μ²˜(Multi-Tenant Architecture)λž€ λ¬΄μ—‡μΌκΉŒμš”?

  • λ©€ν‹°ν…Œλ„ŒνŠΈ μ•„ν‚€ν…μ²˜(Multi-Tenant Architecture)λŠ” ν•˜λ‚˜μ˜ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μΈμŠ€ν„΄μŠ€μ™€ λ°μ΄ν„°λ² μ΄μŠ€κ°€ μ—¬λŸ¬ μ‚¬μš©μž κ·Έλ£Ή(ν…Œλ„ŒνŠΈ,Tenant) 간에 κ³΅μœ λ˜λ©΄μ„œ, 각 ν…Œλ„ŒνŠΈ(Tenant)κ°€ λ…λ¦½μ μœΌλ‘œ λ™μž‘ν•˜λ„λ‘ μ„€κ³„λœ μ†Œν”„νŠΈμ›¨μ–΄ μ•„ν‚€ν…μ²˜μž…λ‹ˆλ‹€.
    • μ΄λŠ” 클라우트 μ„œλΉ„μŠ€, SaaS(Software as a Service) μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ 자주 μ‚¬μš©λ©λ‹ˆλ‹€.

1️⃣ λ©€ν‹°ν…Œλ„ŒνŠΈ μ•„ν‚€ν…μ²˜(Muti-Tenant Architecture)의 핡심 κ°œλ….

1️⃣ ν…Œλ„ŒνŠΈ(Tenant)λž€?

  • ν…Œλ„ŒνŠΈ(Tenant)λŠ” ν•˜λ‚˜μ˜ λ…λ¦½λœ μ‚¬μš©μž κ·Έλ£Ήμ΄λ‚˜ 고객을 μ˜λ―Έν•©λ‹ˆλ‹€.
    • 예: νŠΉμ • νšŒμ‚¬, 쑰직 λ˜λŠ” μ‚¬μš©μž κ·Έλ£Ή.

      2️⃣ λ©€ν‹°ν…Œλ„ŒνŠΈμ˜ λͺ©μ .

  • μ—¬λŸ¬ ν…Œλ„ŒνŠΈκ°€ λ™μΌν•œ μ†Œν”„νŠΈμ›¨μ–΄ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‚¬μš©ν•˜λ©΄μ„œλ„ 데이터와 ν™˜κ²½μ΄ μ„œλ‘œ λ…λ¦½μ μœΌλ‘œ κ΄€λ¦¬λ˜λ„λ‘ ν•˜λŠ” 것 μž…λ‹ˆλ‹€.

2️⃣ λ©€ν‹°ν…Œλ„ŒνŠΈ μ•„ν‚€ν…μ²˜ μ’…λ₯˜

1️⃣ κ³΅μœ ν˜• λ°μ΄ν„°λ² μ΄μŠ€ 및 μŠ€ν‚€λ§ˆ.

  • νŠΉμ§•
    • λͺ¨λ“  ν…Œλ„ŒνŠΈκ°€ 같은 λ°μ΄ν„°λ² μ΄μŠ€μ™€ ν…Œμ΄λΈ”μ„ κ³΅μœ ν•©λ‹ˆλ‹€.
  • μž₯점
    • λ¦¬μ†ŒμŠ€ μ‚¬μš©μ΄ κ°€μž₯ νš¨μœ¨μ μž…λ‹ˆλ‹€.
    • ν™•μž₯이 μ‰½μŠ΅λ‹ˆλ‹€.
    • 관리가 κ°„λ‹¨ν•©λ‹ˆλ‹€.
  • 단점
    • 데이터 격리와 λ³΄μ•ˆ κ΅¬ν˜„μ΄ λ³΅μž‘ν•©λ‹ˆλ‹€.
    • νŠΉμ • ν…Œλ„ŒνŠΈμ˜ νŠΈλž˜ν”½ 증가가 λ‹€λ₯Έ ν…Œλ„ŒνŠΈμ˜ 영ν–₯을 쀄 수 μžˆμŠ΅λ‹ˆλ‹€.

2️⃣ μ˜ˆμ‹œ

CREATE TABLE message (
    id INT PRIMARY KEY AUTO_INCREMENT,
    tenant_id INT NOT NULL,
    sender_id INT NOT NULL,
    message TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
  • ν…Œμ΄λΈ”μ— tenant_idλ₯Ό μΆ”κ°€ν•˜μ—¬ 데이터λ₯Ό κ΅¬λΆ„ν•©λ‹ˆλ‹€.

2️⃣ λΆ„λ¦¬ν˜• λ°μ΄ν„°λ² μ΄μŠ€, 곡유 μŠ€ν‚€λ§ˆ.

  • νŠΉμ§•
    • λͺ¨λ“  ν…Œλ„ŒνŠΈκ°€ λ³„λ„μ˜ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό κ°€μ§€μ§€λ§Œ, λ™μΌν•œ ν…Œμ΄λΈ” ꡬ쑰(μŠ€ν‚€λ§ˆ)λ₯Ό κ³΅μœ ν•©λ‹ˆλ‹€.
  • μž₯점
    • 데이터가 물리적으둜 λΆ„λ¦¬λ˜μ–΄ λ³΄μ•ˆκ³Ό 데이터 격리가 용이.
    • νŠΉμ • ν…Œλ„ŒνŠΈμ˜ μ„±λŠ₯ μš”κ΅¬μ— 따라 κ°œλ³„μ μœΌλ‘œ μŠ€μΌ€μΌλ§ κ°€λŠ₯.
  • 단점
    • λ°μ΄ν„°λ² μ΄μŠ€κ°€ λŠ˜μ–΄λ‚ μˆ˜λ‘ 관리 λ³΅μž‘μ„±μ΄ 증가
    • λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° λΉ„μš© 증가.

3️⃣ μ™„μ „ λΆ„λ¦¬ν˜• λ°μ΄ν„°λ² μ΄μŠ€ 및 μŠ€ν‚€λ§ˆ.

  • νŠΉμ§•
    • 각 ν…Œλ„ŒνŠΈλ§ˆλ‹€ κ³ μœ ν•œ λ°μ΄ν„°λ² μ΄μŠ€μ™€ ν…Œμ΄λΈ” ꡬ쑰λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
  • μž₯점
    • μ™„λ²½ν•œ 데이터 격리와 λ³΄μ•ˆ 제곡.
    • ν…Œλ„ŒνŠΈλ³„λ‘œ λ§žμΆ€ν˜• μŠ€ν‚€λ§ˆμ™€ κΈ°λŠ₯ 제곡 κ°€λŠ₯.
  • 단점
    • ν…Œλ„ŒνŠΈ μˆ˜κ°€ λ§Žμ•„μ§€λ©΄ ν™•μž₯성이 떨어짐.
    • 관리와 배포가 맀우 볡작.

3️⃣ λ©€ν‹°ν…Œλ„ŒνŠΈ μ•„ν‚€ν…μ²˜μ˜ μž₯단점.

1️⃣ μž₯점.

  • 1. λΉ„μš© νš¨μœ¨μ„±
    • μ—¬λŸ¬ ν…Œλ„ŒνŠΈκ°€ λ™μΌν•œ 인프라λ₯Ό κ³΅μœ ν•˜λ―€λ‘œ λ¦¬μ†ŒμŠ€ ν™œμš©λ„κ°€ λ†’μŒ.
  • 2. 운영 νš¨μœ¨μ„±
    • ν•œ 번의 배포둜 λͺ¨λ“  ν…Œλ„ŒνŠΈμ— μ—…λ°μ΄νŠΈ κ°€λŠ₯.
  • 3. ν™•μž₯μ„±
    • ν…Œλ„ŒνŠΈ μˆ˜κ°€ 증가해도 μ„œλ²„λ₯Ό μΆ”κ°€ν•˜μ—¬ ν™•μž₯ κ°€λŠ₯(μˆ˜ν‰ ν™•μž₯).

2️⃣ 단점.

  • 1. λ³΅μž‘ν•œ 데이터 격리
    • ν…Œλ„ŒνŠΈλ³„ 데이터λ₯Ό λΆ„λ¦¬ν•˜κΈ° μœ„ν•œ 좔가적인 λ³΄μ•ˆ 및 둜직 ν•„μš”.
  • 2. λ¦¬μ†ŒμŠ€ 곡유 문제
    • νŠΉμ • ν…Œλ„ŒνŠΈμ˜ λ¦¬μ†ŒμŠ€ μ‚¬μš©λŸ‰μ΄ λ§Žμ„ 경우 λ‹€λ₯Έ ν…Œλ„ŒνŠΈμ˜ 영ν–₯을 쀄 수 있음.
  • 3. λ§žμΆ€ν™” μ œν•œ
    • κ°œλ³„ ν…Œλ„ŒνŠΈλ³„λ‘œ 맞좀 κΈ°λŠ₯을 μ œκ³΅ν•˜κΈ° μ–΄λ ΅κ±°λ‚˜ κ΅¬ν˜„μ΄ 볡작.

4️⃣ λ©€ν‹°ν…Œλ„ŒνŠΈ κ΅¬ν˜„ 방법.

1️⃣ λ°μ΄ν„°λ² μ΄μŠ€ 섀계.

κ³΅μœ ν˜• λ°μ΄ν„°λ² μ΄μŠ€ μ˜ˆμ‹œ

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    tenant_id INT NOT NULL,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL
)
  • tenant_idλ₯Ό 기반으둜 데이터 필터링.

λΆ„λ¦¬ν˜• λ°μ΄ν„°λ² μ΄μŠ€ μ˜ˆμ‹œ

  • 1. ν…Œλ„ŒνŠΈ 식별을 μœ„ν•œ Interceptor
    • μš”μ²­λ§ˆνƒ€ ν…Œλ„ŒνŠΈλ₯Ό μ‹λ³„ν•˜μ—¬ μ μ ˆν•œ λ°μ΄ν„°λ² μ΄μŠ€λ‘œ μ—°κ²°ν•©λ‹ˆλ‹€.
      ```java
      @Component
      public class TenantInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    String tenantId = request.getHeader(β€œX-Tenant-ID”);
    if (tenantId == null) {
    throw new RuntimeException(β€œTenant ID is not provided”);
    }
    TenantContext.setCurrentTenant(tenantId);
    return true
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handeler, Exception ex) throws Exception {
    TenantContext.clear();
    }
    }
    ```

  • 2. ν…Œλ„ŒνŠΈ μ»¨ν…μŠ€νŠΈ μ €μž₯μ†Œ
    • ν˜„μž¬ μš”μ²­μ˜ ν…Œλ„ŒνŠΈλ₯Ό μ €μž₯ν•˜κ³  κ΄€λ¦¬ν•©λ‹ˆλ‹€.
      ```java
      public class TenantContext {
      private static final ThreadLocal CURRENT_TENANT = new ThreadLocal<>();

    public static void setCurrentTenant(String tenant) {
    CURRENT_TENANT.set(tenant);
    }

    public static String getCurrentTenant() {
    return CURRENT_TENANT.get();
    }

    public static void clear() {
    CURRENT_TENANT.remove();
    }
    }
    ```

  • 3. 동적 데이터 μ†ŒμŠ€ ꡬ성
    • ν…Œλ„ŒνŠΈλ³„ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό λ™μ μœΌλ‘œ μ„ νƒν•©λ‹ˆλ‹€.
      ```java
      @Configuration
      public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
    return new AbstractRoutingDataSource() {
    @Override
    protected Object determineCurrentLookupKey() {
    return TenantContext.getCurrentTenant();
    }
    };
    }

    @Bean
    public DataSourceInitializer dataSourceInitializer(DataSource dataSource) {
    DataSourceInitializer initializer = new DataSourceInitializer();
    initializer.setDataSource(dataSource);
    return initializer;
    }

    @Bean
    public Map<Object, Object> tenantDataSources() {
    Map<Object, Object> dataSources = new HashMap<>();

      // λ°μ΄ν„°λ² μ΄μŠ€ A
      DataSource tenantADataSource = DataSourceBuilder.create()
          .url("jdbc:mysql://localhost:3306/tenant_a_db")
          .username("root")
          .password("password")
          .driverClassName("com.mysql.cj.jdbc.Driver")
          .build();
      dataSources.put("tenant_a", tenantADataSource);
    
      // λ°μ΄ν„°λ² μ΄μŠ€ B
      DataSource tenantBDataSource = DataSourceBuilder.create()
          .url("jdbc:mysql://localhost:3306/tenant_b_db")
          .username("root")
          .password("password")
          .driverClassName("com.mysql.cj.jdbc.Driver")
          .build();
      dataSources.put("tenant_b", tenantBDataSource);
    
      return dataSources;   } } ```
    
  • 4. μš”μ²­ 처리 흐름
    • ν΄λΌμ΄μ–ΈνŠΈκ°€ μš”μ²­μ„ 보낼 λ•Œ X-Tenant-ID 헀더λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.
      GET /api/resource HTTP/1.1
      Host: example.com
      X-Tenant-ID: tenant_a
      
  • TenantInterceptorκ°€ X-Tenant-IDλ₯Ό 읽어 TenantContext에 μ €μž₯ν•©λ‹ˆλ‹€.
  • AbstractRoutingDataSourceκ°€ TenantContextμ—μ„œ ν…Œλ„ŒνŠΈ 정보λ₯Ό 읽어 ν•΄λ‹Ή ν…Œλ„ŒνŠΈμ˜ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

λ°μ΄ν„°λ² μ΄μŠ€ ꡬ쑰

  • Tenant A: tenant_a_db
Table Columns
users id, name, email
orders id, user_id, order_data, total
products id, name, price, stock_quantity
  • Tenant B: tenant_b_db
Table Columns
users id, name, email
orders id, user_id, order_data, total
products id, name, price, stock_quantity

μž₯점.

    1. 데이터가 물리적으둜 λΆ„λ¦¬λ˜μ–΄ λ³΄μ•ˆμ΄ κ°•λ ₯
    1. ν…Œλ„ŒνŠΈ κ°„ 데이터 κ°„μ„­ κ°€λŠ₯성이 μ—†μŒ
    1. κ°œλ³„ ν…Œλ„ŒνŠΈλ³„λ‘œ λ°μ΄ν„°λ² μ΄μŠ€ μ„±λŠ₯을 λ…λ¦½μ μœΌλ‘œ 관리 κ°€λŠ₯

단점.

    1. λ°μ΄ν„°λ² μ΄μŠ€κ°€ λ§Žμ•„μ§ˆμˆ˜λ‘ 관리 λ³΅μž‘μ„±μ΄ 증가.
    1. μŠ€ν‚€λ§ˆ λ³€κ²½μ‹œ λͺ¨λ“  λ°μ΄ν„°λ² μ΄μŠ€μ— λ™μΌν•œ μž‘μ—…μ„ λ°˜λ³΅ν•΄μ•Ό 함.

2️⃣ Spring Boot κ΅¬ν˜„

  • μ—”ν‹°ν‹°
    @Entity
    public class User {
    
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      private Long tenantId;
    
      private String name;
    
      private String email;
    
      private String password;
    
      // Getters and setters
    }
    
  • Repository
    public interface UserRepository extends JpaRepository<User, Long> {
      List<User> findByTenantId(Long tenantId);
    }
    
  • Sevice
    @Service
    public class UserService {
    
      @Autowired
      private UserRepository userRepository;
    
      public List<User> getUsersByTenantId(Long tenantId) {
          return userRepository.findByTenantId(tenantId);
      }
    }
    
  • 컨트둀러
    @RestController
    @RequestMapping("/api/users")
    public class UserController {
    
      @Autowired
      private UserService userService;
    
      @GetMapping
      public List<User> getUsers(@RequestParam Long tenantId) {
          return userService.getUsersByTenantId(tenantId);
      }
    }
    

5️⃣ λ©€ν‹°ν…Œλ„ŒνŠΈμ™€ SaaS

  • SaaS(Software as a Service) μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ λ©€ν‹°ν…Œλ„ŒνŠΈλ₯Ό μ‚¬μš©ν•˜λŠ” λŒ€ν‘œμ μΈ μ‚¬λ‘€μž…λ‹ˆλ‹€.
    • 예: Slack, Salesforce, Google Workspace λ“±
      • 각 쑰직이 κ³ μœ ν•œ 데이터λ₯Ό κ°€μ§€μ§€λ§Œ, λ™μΌν•œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μΈμŠ€ν„΄μŠ€λ₯Ό μ‚¬μš©.

6️⃣ λ©€ν‹°ν…Œλ„ŒνŠΈ 섀계 μ‹œ κ³ λ € 사항.

  • 1. λ³΄μ•ˆ
    • ν…Œλ„ŒνŠΈ κ°„ 데이터 μ ‘κ·Ό 방지.
    • 인증 토근에 tenant_id 포함.
  • 2. μ„±λŠ₯
    • νŠΉμ • ν…Œλ„ŒνŠΈμ˜ κ³Όλ„ν•œ λ¦¬μ†ŒμŠ€ μ‚¬μš©μ΄ λ‹€λ₯Έ ν…Œλ„ŒνŠΈμ— 영ν–₯을 주지 μ•Šλ„λ‘ μ œν•œ μ„€μ •.
  • 3. μŠ€μΌ€μΌλ§
    • ν…Œλ„ŒνŠΈ μˆ˜κ°€ 증가할 λ•Œ μ„œλ²„, λ°μ΄ν„°λ² μ΄μŠ€μ˜ μˆ˜ν‰μ  ν™•μž₯이 κ°€λŠ₯ν•΄μ•Ό 함.

7️⃣ κ²°λ‘ .

  • λ©€ν‹°ν…Œλ„ŒνŠΈ μ•„ν‚€ν…μ²˜λŠ” ν•˜λ‚˜μ˜ μ‹œμŠ€ν…œμœΌλ‘œ μ—¬λŸ¬ μ‚¬μš©μž 그룹을 μ§€μ›ν•˜λ©΄μ„œ, 데이터와 ν™˜κ²½μ„ λ…λ¦½μ μœΌλ‘œ 관리할 수 μžˆλŠ” 효율적인 λ°©μ‹μž…λ‹ˆλ‹€.
    • κ΅¬ν˜„ 방식은 μ„œλΉ„μŠ€μ˜ μš”κ΅¬μ‚¬ν•­κ³Ό ν…Œλ„ŒνŠΈ μˆ˜μ— 따라 κ²°μ •λ˜λ©°, 섀계 λ‹¨κ³„μ—μ„œ 데이터 격리와 ν™•μž₯성을 μ² μ €νžˆ κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€.