TeamFactory#signup missing ActiveRecord::RecordInvalid rescue
RCA: team signup failed: Validation failed: Domain has already been taken
Error Log#
team signup failed: Validation failed: Domain has already been taken
Impact#
- Service:
cupixvista-api - 발생 횟수: 2
- 최초 발생: 2026-04-10T13:06:46.788Z
- 최근 발생: 2026-04-10T13:06:46.788Z
Root Cause Summary#
사용자가 POST /api/v1/teams/signup API를 통해 team_domain: "testing"으로 팀 생성을 시도했으나, 해당 domain이 이미 존재하여 Team 모델의 validates :domain, uniqueness: { case_sensitive: false } 검증에서 실패했습니다. TeamFactory.signup 메서드의 validate_team_domain은 tmp- 접두사만 검사하고, 도메인 중복 여부를 사전에 확인하지 않습니다. 따라서 중복 도메인은 ActiveRecord::RecordInvalid 예외가 발생할 때까지 감지되지 않으며, 이 예외가 ENT10005 에러 코드로 변환되어 HTTP 400으로 반환됩니다. 이는 정상적인 validation 동작이며, 동일 사용자(Android Chrome)가 이미 존재하는 "testing" 도메인으로 두 번 연속 가입을 시도한 것이 원인입니다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/teams_controller.rb:18—signupaction이TeamFactory.signup호출
# app/controllers/api/v1/teams_controller.rb:18-27
def signup
access_token = request.headers['X-CUPIX-AUTH']
access_token = access_token.split(' ').last if access_token.present? && access_token.include?(' ')
team = TeamFactory.signup(params, access_token)
render_api Renderable.new({
contents: team,
serializer_option: @serializer_option
})
end
- Signup factory:
app/factories/team_factory.rb:87—signup메서드가 파라미터 검증 후 팀 생성 시작
# app/factories/team_factory.rb:87-112
def signup(params, access_token = nil)
find_user_email(params, access_token)
validate_required_signup_params(params)
validate_team_domain(params[:team_domain])
params = params.merge(
name: params[:team_name],
domain: params[:team_domain]
)
current_user = ::User.find_by(email: params[:email])
user_firstname, user_lastname = extract_user_names(current_user, access_token)
team, super_admin = self.create_team_with_admin(params, user_firstname, user_lastname)
# ...
rescue StandardError => e
Cupix::Logger.error("team signup failed: #{e.message}", class: self.class.name, function: __method__, error: e)
raise e if e.is_a?(Cupix::Errors::BaseError)
raise Cupix::Errors::System.new(code: 'SYS10000', reason: e.to_s, message: e.message)
end
- Domain validation (insufficient):
app/factories/team_factory.rb:136-138—tmp-접두사만 검사하고 중복 검사는 하지 않음
# app/factories/team_factory.rb:136-138
def validate_team_domain(team_domain)
raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'team_domain cannot start with tmp-') if team_domain.start_with?('tmp-')
end
- Team creation path:
app/factories/team_factory.rb:163-175—find_or_create_team_and_superadmin에서 임시 팀(tmp-prefix)이 없으면TeamFactory.create!호출
# app/factories/team_factory.rb:163-175
def find_or_create_team_and_superadmin(params, user_firstname, user_lastname)
temp_team = ::Team.joins(:user)
.where('teams.domain LIKE ?', 'tmp-%')
.where(users: { email: params[:email], state: :active })
.first
if temp_team.present?
temp_team.update!(domain: params[:team_domain], name: params[:team_name])
superadmin = User.find_by(email: params[:email], team: temp_team, state: :active)
return [temp_team, superadmin]
end
team = TeamFactory.create!(params)
# ...
end
- Failure point:
app/factories/base_factory.rb:130-131—model.save!에서ActiveRecord::RecordInvalid발생 후ENT10005로 변환
# app/factories/base_factory.rb:123-131
begin
self.model.save!
self.model
rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
raise e
rescue NoMethodError => e
raise Cupix::Errors::System.new(code: 'SYS10003', reason: e.message)
rescue ActiveRecord::RecordInvalid => e
raise Cupix::Errors::Entity.new(code: 'ENT10005', reason: e.message)
end
- Model validation:
app/models/team.rb:68-71— domain uniqueness 제약 조건
# app/models/team.rb:68-71
validates :domain,
uniqueness: { case_sensitive: false },
format: { with: /\A[a-z0-9]([-](?![-])|[a-z0-9])*[a-z0-9]\z/ },
length: { in: 3..20 }
기대 동작: 사용자가 이미 존재하는 도메인으로 가입 시도 시 명확한 에러 메시지와 함께 HTTP 400 반환.
실제 동작: 기대 동작과 동일. HTTP 400과 ENT10005 에러 코드가 반환됨. 다만 validate_team_domain에서 사전 중복 검사 없이 DB 레벨 validation에 도달한 후 에러가 발생하며, 에러 메시지가 error 레벨로 로깅됨.
Log Evidence#
사용한 Datadog 쿼리:
service:cupixvista-api status:error "team signup failed"
Time range: 2026-04-10T12:06:00Z to 2026-04-10T13:36:00Z
service:cupixvista-api "Domain has already been taken"
Time range: 2026-04-10T12:06:00Z to 2026-04-10T13:36:00Z
총 6개의 로그 항목이 발견되었으며, 약 4.5초 이내(13:06:42~13:06:46 UTC)에 2개의 요청에서 발생했습니다.
요청 1 (Request ID: 1e87a63c-bccb-424b-bbbd-978d01068245)
HTTP request log (info):
{
"timestamp": "2026-04-10T13:06:42.215Z",
"status": "info",
"message": "[400] POST /api/v1/teams/signup (Api::V1::TeamsController#signup)",
"duration": "27.26ms",
"db": "7.02ms",
"remote_ip": "130.176.220.116",
"auth_method": "COGNITO",
"params": { "team_domain": "testing", "fields": ["id", "name", "domain"] },
"error_response": { "code": "ENT10005", "class": "Cupix::Errors::Entity", "reason": "Validation failed: Domain has already been taken" }
}
Model validation log (warn):
{
"timestamp": "2026-04-10T13:06:46.788Z",
"status": "warn",
"message": "Validation failed: Domain has already been taken",
"class": "Team",
"function": "write_log",
"model_attributes": { "domain": "testing", "name": "Testing", "state": "initializing", "users_count": 0 }
}
Signup error log (error):
{
"timestamp": "2026-04-10T13:06:46.788Z",
"status": "error",
"message": "team signup failed: Validation failed: Domain has already been taken",
"error": { "code": "ENT10005", "reason": "Validation failed: Domain has already been taken" },
"function": "signup",
"class": "Class"
}
요청 2 (Request ID: 3e157728-7c78-447d-b9df-6896a0e63ab7) — 동일한 패턴, remote IP 130.176.220.115, 2초 후 재시도.
핵심 관찰:
- 동일 사용자(Android Chrome User-Agent)가 약 2초 간격으로
team_domain: "testing"으로 두 번 가입 시도 - 두 요청 모두 HTTP 400으로 정상 거부됨
- 두 IP(
130.176.220.115,130.176.220.116)는 CloudFront 등 CDN edge를 통한 동일 사용자 요청으로 추정
Fix Recommendation#
즉시 조치 (Critical)#
이 에러는 즉시 수정이 필요한 버그가 아닙니다. 이미 존재하는 도메인으로 가입을 시도하면 HTTP 400이 정상적으로 반환되고 있습니다. 다만 두 가지 개선이 가능합니다:
-
로그 레벨 조정 —
app/factories/team_factory.rb:108: 사용자 입력 오류(중복 도메인)로 인한 validation 실패는 error가 아닌 warn 레벨이 적절합니다. 현재Cupix::Logger.error로 로깅되어 불필요한 에러 알림을 유발합니다.Cupix::Errors::Entity(validation 에러)인 경우 warn 레벨로 변경하면 noise를 줄일 수 있습니다. -
사전 도메인 중복 검사 —
app/factories/team_factory.rb:136-138:validate_team_domain메서드에서Team.exists?(domain: team_domain)검사를 추가하면, DB save 전에 명확한 에러 메시지(ARG코드)를 반환할 수 있습니다. 이는 사용자 경험 개선에 해당합니다.
단기 개선 (1주 이내)#
validate_team_domain에 도메인 중복 검사 추가하여tmp-prefix 검사와 함께Team.exists?(domain: team_domain)확인. race condition 가능성이 있으나, 기존 DB uniqueness 제약이 최종 방어선 역할을 하므로 문제 없음.signuprescue 블록에서Cupix::Errors::Entity타입 에러(validation 실패)는 warn 레벨로 로깅하도록 분기 처리.
장기 개선 (재발 방지)#
- 프론트엔드에서 가입 폼 제출 전
/api/v1/teams/find_by_domainAPI를 호출하여 도메인 사용 가능 여부를 사전 확인하는 UX 개선 검토. - 사용자 입력 validation 실패를 error 레벨로 로깅하는 패턴이 다른 factory에도 존재하는지 점검하여 일괄 개선.
Monitoring#
현재 발생 빈도(2건)가 낮아 별도 알림이 필요하지는 않으나, 반복적으로 발생할 경우를 대비한 쿼리:
service:cupixvista-api status:error "team signup failed" "Domain has already been taken"
로그 레벨이 warn으로 변경된 후에는 아래 쿼리로 모니터링:
service:cupixvista-api status:warn "Domain has already been taken"
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial