ποΈ[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 ThreadLocalCURRENT_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
- ν΄λΌμ΄μΈνΈκ° μμ²μ λ³΄λΌ λ X-Tenant-ID ν€λλ₯Ό μΆκ°ν©λλ€.
- 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 |
μ₯μ .
-
- λ°μ΄ν°κ° 물리μ μΌλ‘ λΆλ¦¬λμ΄ λ³΄μμ΄ κ°λ ₯
-
- ν λνΈ κ° λ°μ΄ν° κ°μ κ°λ₯μ±μ΄ μμ
-
- κ°λ³ ν λνΈλ³λ‘ λ°μ΄ν°λ² μ΄μ€ μ±λ₯μ λ 립μ μΌλ‘ κ΄λ¦¬ κ°λ₯
λ¨μ .
-
- λ°μ΄ν°λ² μ΄μ€κ° λ§μμ§μλ‘ κ΄λ¦¬ 볡μ‘μ±μ΄ μ¦κ°.
-
- μ€ν€λ§ λ³κ²½μ λͺ¨λ λ°μ΄ν°λ² μ΄μ€μ λμΌν μμ μ λ°λ³΅ν΄μΌ ν¨.
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 λ±
- κ° μ‘°μ§μ΄ κ³ μ ν λ°μ΄ν°λ₯Ό κ°μ§μ§λ§, λμΌν μ ν리μΌμ΄μ μΈμ€ν΄μ€λ₯Ό μ¬μ©.
- μ: Slack, Salesforce, Google Workspace λ±
6οΈβ£ λ©ν°ν λνΈ μ€κ³ μ κ³ λ € μ¬ν.
-
1. 보μ
- ν λνΈ κ° λ°μ΄ν° μ κ·Ό λ°©μ§.
- μΈμ¦ ν κ·Όμ tenant_id ν¬ν¨.
-
2. μ±λ₯
- νΉμ ν λνΈμ κ³Όλν 리μμ€ μ¬μ©μ΄ λ€λ₯Έ ν λνΈμ μν₯μ μ£Όμ§ μλλ‘ μ ν μ€μ .
-
3. μ€μΌμΌλ§
- ν λνΈ μκ° μ¦κ°ν λ μλ², λ°μ΄ν°λ² μ΄μ€μ μνμ νμ₯μ΄ κ°λ₯ν΄μΌ ν¨.
7οΈβ£ κ²°λ‘ .
- λ©ν°ν
λνΈ μν€ν
μ²λ νλμ μμ€ν
μΌλ‘ μ¬λ¬ μ¬μ©μ κ·Έλ£Ήμ μ§μνλ©΄μ, λ°μ΄ν°μ νκ²½μ λ
립μ μΌλ‘ κ΄λ¦¬ν μ μλ ν¨μ¨μ μΈ λ°©μμ
λλ€.
- ꡬν λ°©μμ μλΉμ€μ μꡬμ¬νκ³Ό ν λνΈ μμ λ°λΌ κ²°μ λλ©°, μ€κ³ λ¨κ³μμ λ°μ΄ν° 격리μ νμ₯μ±μ μ² μ ν κ³ λ €ν΄μΌ ν©λλ€.