접근 스위치만 바꿔도 전역 설정의 상속이 끊겼다

AI Commons의 대상별 편집기는 접근 여부만 바꿔도 화면에 보이는 실효 기능 여섯 개를 모두 저장했다. 전역 기능을 상속하던 null 컬럼에 당시의 값이 들어가면서 직접 설정으로 고정됐다. 이후 전역 기능을 바꿔도 이 대상만 이전 값을 유지할 수 있었다.

숨긴 사용량 축도 null로 전송돼 기존 숫자 한도가 지워졌다. 서비스가 생략된 값을 ?? null로 바꾸면 “수정하지 않음”과 “비우기”를 구분할 수 없다. 읽기 계층은 행이 생겼다는 이유만으로 직접 설정이라 판단해 null을 무제한으로 해석하던 문제도 있었다.

초기 변경은 기능·접근·사용량의 diff를 나누고 바뀐 축만 보냈다. 사용량 update는 DTO 필드를 그대로 넘겨 생략한 축을 유지한다. 기능은 의존 보정에 전체가 필요해 여섯 필드를 모두 보내거나 모두 생략한다. 일부만 보낸 요청과 기능·접근이 모두 없는 요청은 거절한다.

전역 기본값의 복사본은 상속이 아니다

초기 변경은 새 배분 행에 전역 사용량 기본값을 복사했다. 행 생성 때문에 null이 무제한으로 읽히는 순간은 막을 수 있지만, 복사한 숫자는 다음 전역 변경을 따라가지 못한다. 바로 다음 변경에서 이 처방을 철회했다.

대신 사용량 컬럼 null을 상속으로 해석하도록 판정·표시·추가 배분 계산을 맞췄다. 직접 설정 여부도 행의 존재가 아니라 컬럼의 값으로 판단한다. 전역 기본값이 10에서 20으로 바뀌면 null인 대상은 다시 읽을 때 20을 사용한다.

이것은 기존의 “행이 있고 null이면 직접 무제한”이라는 의미를 바꾼 정책이다. 기존에 비워 둔 행도 전역 한도를 따르게 돼 허용 범위가 줄어들 수 있다. 단순 저장 방식 수정으로만 볼 수 없는 이유다.

상속과 대상별 무제한을 다른 값으로 표현한다

null을 상속에 쓰면 대상별 무제한을 별도로 표현해야 한다. 후속 변경은 slideImagePerUserUnlimited와 videoMinutesPerUserUnlimited를 기본 false인 boolean으로 추가했다. 읽기 순서는 플래그, 숫자, 전역 기본값이다.

사용량 저장 요청과 저장된 값의 의미
상태쓰기 또는 저장값결과
요청 필드 생략undefined기존 컬럼 유지
전역 상속무제한 false, 숫자 null라이선스 기본값 사용
직접 한도무제한 false, 숫자 지정지정한 숫자 사용
대상별 무제한무제한 true개인 한도 해제, 숫자는 보존

다음은 공용 resolveTargetPerUser의 슬라이드 축 부분이다.

if (axis === "slideImage") {
	if (row?.slideImagePerUserUnlimited) return null;
	return row?.slideImagePerUser ?? licenseDefault;
}

DB 컬럼 null은 상속이다. resolver가 반환한 null은 최종 개인 한도의 무제한이다. 계층에 따라 의미가 다르다. 무제한 플래그를 켜도 숫자는 남겨 해제하면 이전 값을 다시 쓸 수 있게 했다. 개인 한도를 해제해도 라이선스의 유한 총량과 미배분 표시 규칙까지 없어지지는 않는다.

부분 저장은 판정과 화면에서도 같은 뜻이어야 한다

서버 테스트는 접근만 저장할 때 기능 컬럼과 사용량을 건드리지 않는지, 생략 축 보존과 명시 null 저장, 부분 기능 요청 거절을 확인한다. resolver 테스트는 플래그 우선·숫자·상속·행 부재·기본값 null·축 독립과 플래그 없는 레거시 mock의 fallback을 다룬다. 클라이언트 테스트는 접근 스위치만 바꾼 payload에 기능 여섯 개와 별도 사용량 저장이 없는지 확인한다.

이 서비스 mock은 SQL 기본값이나 실제 HTTP 연결을 검증하지 않는다. 숫자 sentinel -1 대신 boolean을 선택하면 플래그가 없는 읽기는 기존 상속 규칙으로 돌아갈 수 있지만, 그 fallback의 허용 범위는 전역 기본값에 달려 있다. 최종 계약은 쓰기 생략, 저장된 null, 명시적 무제한을 구분하고 모든 읽기에서 같은 순서로 해석하는 데 있다.