티스토리 뷰
스프링 시큐리티 완전 처음부터 8편 (Kotlin)
멀티 테넌트(Multi-Tenant) 환경에서 인증/인가 설계 – B2B SaaS 실무 구조
이쯤 되면 질문이 하나 바뀐다.
“한 서비스에 여러 회사(조직) 가 들어오면
이 시큐리티 구조, 그대로 써도 되나요?”
정답부터 말하면
그대로 쓰면 100% 사고 난다.
왜냐면 지금까지 만든 구조는
‘사용자 단위’ 보안까지만 커버하고 있기 때문이다.
B2B, SaaS, 내부 업무 시스템에서는
보안의 기준이 하나 더 생긴다.
“이 사용자는 누구인가?” +
“어느 조직(테넌트)에 속해 있는가?”
이번 글은
멀티 테넌트 환경에서의 인증/인가를 어떻게 설계해야 하는지를
진짜 실무 관점에서 풀어본다.
0. 멀티 테넌트, 말부터 정리하자
멀티 테넌트란 이거다.
하나의 서비스에 여러 ‘조직(회사)’이 존재하고,
각 조직의 데이터와 권한이 서로 완전히 분리된 구조
예시:
- 사내 협업툴
- 전자계약 서비스
- 회계/ERP SaaS
- 교육/관리 플랫폼
여기서 가장 위험한 사고는 단 하나다.
A 회사 사용자가
B 회사 데이터에 접근하는 사고
이건 장애가 아니라 보안 사고다.
1. 멀티 테넌트에서 절대 피해야 할 오해
❌ “ROLE 만 잘 나누면 되지 않나요?”
안 된다.
ROLE_ADMIN
이 정보만으로는:
- 어느 회사의 관리자?
- 어떤 조직 범위의 관리자?
를 알 수 없다.
👉 테넌트 정보는 ROLE과 다른 축이다.
2. 멀티 테넌트 보안의 핵심 원칙 (이건 외워야 한다)
🔒 원칙 1
모든 인증 정보에는 ‘tenant 식별자’가 포함되어야 한다
🔒 원칙 2
모든 데이터 접근은 tenant 기준으로 필터링된다
🔒 원칙 3
인가 판단은 (user + role + tenant) 조합으로 한다
이 원칙이 깨지는 순간
“우연히 다른 회사 데이터가 보였다” 같은 사고가 난다.
3. 테넌트 모델링부터 잡자 (DB 설계 관점)
3-1. Tenant 테이블
@Entity
@Table(name = "tenants")
class Tenant(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(nullable = false, unique = true)
val tenantKey: String, // 예: companyA, corp-123
val name: String
)
3-2. User 테이블에 tenant 연결
@Entity
@Table(name = "users")
class User(
val username: String,
val password: String,
val role: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tenant_id")
val tenant: Tenant,
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
)
📌 중요 포인트
- User는 반드시 하나의 Tenant에 속한다
- “전사 관리자” 같은 케이스는 별도 설계가 필요 (나중 이야기)
4. JWT에 tenant 정보를 반드시 포함시켜야 하는 이유
지금까지 JWT에는 보통 이 정도만 넣었다.
{
"sub": "username",
"role": "ADMIN"
}
멀티 테넌트에서는 절대 부족하다.
4-1. JWT Payload – 최소 권장 형태
{
"sub": "username",
"role": "ADMIN",
"tenantId": 3
}
또는
{
"sub": "username",
"role": "ADMIN",
"tenantKey": "companyA"
}
👉 tenant 식별자는 JWT 안에 반드시 있어야 한다.
4-2. JwtProvider 수정 (개념 코드)
fun generateToken(username: String, role: String, tenantId: Long): String {
return Jwts.builder()
.setSubject(username)
.claim("role", role)
.claim("tenantId", tenantId)
.setExpiration(expiry)
.signWith(secretKey)
.compact()
}
5. JwtAuthenticationFilter – tenant 정보까지 SecurityContext에 올리기
이제 인증 객체에는 이런 정보가 있어야 한다.
- username
- authorities (ROLE)
- tenantId
5-1. Custom Principal 만들기 (실무 핵심)
class TenantUserPrincipal(
val username: String,
val role: String,
val tenantId: Long
)
5-2. 인증 객체 생성 시 principal로 사용
val principal = TenantUserPrincipal(
username = username,
role = role,
tenantId = tenantId
)
val authentication = UsernamePasswordAuthenticationToken(
principal,
null,
listOf(SimpleGrantedAuthority("ROLE_$role"))
)
SecurityContextHolder.getContext().authentication = authentication
👉 이제 모든 요청에서 tenantId를 꺼낼 수 있다.
6. 컨트롤러 / 서비스에서 tenant 기준 데이터 접근
6-1. tenantId 꺼내기 유틸
fun currentTenantId(): Long {
val auth = SecurityContextHolder.getContext().authentication
val principal = auth.principal as TenantUserPrincipal
return principal.tenantId
}
6-2. 서비스 레벨에서 필터링 (이게 정석)
@Service
class ContractService(
private val contractRepository: ContractRepository
) {
fun findContracts(): List<Contract> {
val tenantId = currentTenantId()
return contractRepository.findByTenantId(tenantId)
}
}
📌 절대 하지 말아야 할 것
findAll() // ❌
멀티 테넌트에서 findAll() 은 거의 범죄다.
7. @PreAuthorize + tenant 조건 결합 (고급 패턴)
@PreAuthorize("hasRole('ADMIN') and #tenantId == principal.tenantId")
fun adminOnlyAction(tenantId: Long) {
...
}
👉 ROLE + tenant 조건을 동시에 체크
이 패턴은:
- 관리자 API
- 조직 설정 변경
- 과금/결제 관련 API
에서 자주 쓰인다.
8. 실무에서 가장 많이 터지는 멀티 테넌트 사고 Top 5
❌ 1. JWT에 tenant 정보 없음
→ 인증은 됐는데, 데이터 분리가 안 됨
❌ 2. 컨트롤러에서만 tenant 체크
→ 다른 서비스에서 우회 호출 가능
❌ 3. Repository에 tenant 조건 빠짐
→ 실수 한 번으로 전사 데이터 노출
❌ 4. 관리자 계정 예외 처리 남발
→ “임시로” 만든 코드가 영원히 남음
❌ 5. 테스트 데이터가 tenant 1개뿐
→ 사고는 항상 두 번째 테넌트에서 터진다
9. 이 구조의 장점 (현실적인 평가)
- 테넌트 단위 데이터 완전 분리
- 인증/인가/데이터 접근 책임 명확
- JWT 기반이라 확장성 좋음
- 나중에 DB 분리(tenant 별)도 가능
👉 B2B SaaS에서 바로 써먹을 수 있는 구조
10. 여기까지 이해했다면, 이건 분명히 말할 수 있다
- “멀티 테넌트 보안 구조 설계 경험 있습니다”
- “JWT에 tenant 정보 포함해서 인증 구현했습니다”
- “서비스 레벨에서 테넌트 격리 처리합니다”
- “B2B SaaS 보안 사고 포인트 이해하고 있습니다”
이건 주니어 레벨에서 거의 안 나오는 경험이다.
출처
- Spring Security Reference
- JWT RFC 7519
- 실무 B2B SaaS 보안 설계 경험 정리
스프링시큐리티, springsecurity, 코틀린, kotlin, 멀티테넌트, saas보안, jwt, 인증인가, 백엔드개발, 개발블로그
'study > kotlin' 카테고리의 다른 글
| 스프링 시큐리티 완전 처음부터 11편 (Kotlin) (0) | 2026.01.04 |
|---|---|
| 스프링 시큐리티 완전 처음부터 9편 (Kotlin) (0) | 2025.12.31 |
| 스프링 시큐리티 완전 처음부터 7편 (Kotlin) (0) | 2025.12.26 |
| 스프링 시큐리티 완전 처음부터 6편 (Kotlin) (0) | 2025.12.22 |
| 스프링 시큐리티 완전 처음부터 5편 (Kotlin) (0) | 2025.12.19 |
- Total
- Today
- Yesterday
- Docker
- 개발블로그
- DevOps
- Express
- ai철학
- nextJS
- seo 최적화 10개
- JWT
- llm
- fastapi
- node.js
- JAX
- 웹개발
- NestJS
- Redis
- Next.js
- CI/CD
- SEO최적화
- 프론트엔드개발
- 압박면접
- flax
- kotlin
- 백엔드개발
- rag
- Python
- 쿠버네티스
- 딥러닝
- PostgreSQL
- REACT
- Prisma
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 31 |

