본문 바로가기

학습 노트/iOS (2021)

196. Data Validation

Data Validation


Core Data에 CRUD 되는 상황에서 데이터의 무결성을 유지하기 위해
저장이나 업데이트를 시도하는 데이터의 종류나 형식에 제약을 걸 수 있다.

Data Model에서 Inspector를 사용해 간단하게 검증 방식을 설정하고,
NSManagedObject를 사용해 검증코드를 구현하는 방식으로 구현한다.

Data Model에 검증방식 설정하기

xcdatamodeld > Person Entity > name Attribute

  • Optional
    해당 Attribute에 nil을 저장할 수 있도록 하는 속성이다.
    비활성화하면 false에 해당하고 nil을 저장할 수 있고,
    활성화하면 true에 해당하고 필수 nil을 저장할 수 없어 반드시 입력값이 존재해야 하는
    필수 Attribute가 된다.
  • Validation
    문자열 Attribute에서는 문자열의 길이를 제한하는 용도로 작동한다.
    Min Length는 최소 길이를,
    Max Length는 최대 길이를 설정한다.
  • Reg. Ex.
    형식 검증용 정규식을 지정할 수 있다.

xcdatamodeld > Person Entity > age Attribute

  • Default Value
    데이터가 입력되지 않았을 경우 기본값으로 사용될 값을 지정한다.
  • Validation
    Integer Attribute에서는 입력 값의 범위를 제한하는 용도로 작동한다.
    Minimum은 최솟값을, Macimum은 최댓값을 설정한다.

검증 코드 작성하기

ValidationViewContoller.swift > viewDidLoad()

override func viewDidLoad() {
      super.viewDidLoad()
      DataManager.shared.mainContext.undoManager = UndoManager()
   }

   deinit {
      DataManager.shared.mainContext.undoManager = nil
   }
}

iOS의 Coredata는 undoManager를 기본으로 제공하지 않는다.
따라서 위와 같이 View를 불러오는 시점에 undoManager를 사용하도록 설정해야 하며,
이렇게 되면 undo, redo, rollback 등이 사용 가능해진다.

ValidationViewController.swift > validate()

   @IBAction func validate(_ sender: Any) {
      guard let name = nameField.text else { return }
      let age = Int16(ageSlider.value)
      let context = DataManager.shared.mainContext

      let newEmployee = EmployeeEntity(context: context)
	   newEmployee.name = name
	   newEmployee.age = age
	   newEmployee.department = selectedDepartment
	   
	   do {
		   try newEmployee.validateForInsert()
	   } catch let error as NSError {
		   print("Domain =============")
		   print(error.domain)
		   print("Code ===============")
		   print(error.code)
		   print("Description ========")
		   print(error.localizedDescription)
		   print("User Info ==========")
		   print(error.userInfo)
	   }
       
       context.rollback()
   }

입력받은 데이터를 새로운 Context에 저장하고,
해당 Context를 CoreData에 저장하기 전 검증하도록 구현했다.

NSManagedObject는 여러 Validate 메서드를 제공한다.

  • ValidateForInsert
    새로운 Entity를 저장할 때 검증을 시도한다.
  • ValidateForDelete
    Entity를 삭제할 때 무결성을 검증한다.
  • ValidateForUpdate
    새 Attribute 값을 저장할 수 있는지 판단한다.

이러한 Validate 메서드는 검증에 성공하면 문제없이 실행되고, 검증에 사용한 context를 파기하지만,
검증에 실패하게 되면 NSError 형태의 error를 반환하도록 되어있다.

따라서 검증에 실패하게 되면
catch 블록의 코드들이 실행되게 되는데

결과

Domain =============

NSCocoaErrorDomain

Code ===============

1670

Description ========

name is too short.

User Info ==========

["NSValidationErrorValue": , "NSLocalizedDescription": name is too short., "NSValidationErrorKey": name, "NSValidationErrorObject": <EmployeeEntity: 0x6000009bcaf0> (entity: Employee; id: 0x600002a686e0 <x-coredata:///Employee/t780F3425-BEB5-473F-93C9-19608B0A417F2>; data: {

    address = nil;

    age = 20;

    contact = nil;

    department = nil;

    name = "";

    salary = 0;

})]

검증에 실패했을 시 콘솔에 출력되는 에러는 위와 같다.

  • Domain
    어떤 Framework나 Class에서 발생한 오류인지에 대한 정보를 담고 있다.
  • Code
    발생한 오류의 식별 번호를 담고 있다.
  • Description
    발생한 오류의 내용을 담고 있다.
  • User Info
    오류가 발생했을 시점의 사용자 환경에 대한 정보를 담고 있다.
    여기서는 검증에 사용된 Attribute,
    실제 검증에 사용된 값,
    검증에 실패한 객체에 관한 정보를 확인할 수 있다.

ValidationViewController.swift > validate()

   @IBAction func validate(_ sender: Any) {
      guard let name = nameField.text else { return }
      let age = Int16(ageSlider.value)
      let context = DataManager.shared.mainContext

      let newEmployee = EmployeeEntity(context: context)
	   newEmployee.name = name
	   newEmployee.age = age
	   newEmployee.department = selectedDepartment
	   
	   do {
		   try newEmployee.validateForInsert()
	   } catch let error as NSError {
		   switch error.code{
		   case NSValidationStringTooLongError, NSValidationStringTooShortError:
			   if let attr = error.userInfo[NSValidationKeyErrorKey] as? String, attr == "name" {
				   showAlert(message: "Check the name has correct length")
			   } else {
				   showAlert(message: "Check it is valid value")
			   }
		   default:
			   break

		   }
		}
   }

보통은 위와 같이 NSError로 전달된 내용을 기반으로 사용자에게 안내 팝업을 띄우는 방식으로 구현한다.


결과


 

Custom Validation

검증 조건을 직접 지정하는 것도 가능하다.

xcmodeld > 대상 Entity

Codegen을 Class Definition에서 Manual/None으로 바꿔
직접 작성하도록 변경한다.

Editor > Create NSManagedObject Subclass를 선택해
대상 Entity를 선택하고 파일을 생성하면

새로운 파일 두 개가 생성된다.

Properties 파일은 Entity가 업데이트될 때마다 자동으로 업데이트되지만,
Class 파일은 유지된다.
즉, Entity가 업데이트돼도 동일하게 검증 방식을 유지할 수 있도록 Class 파일에 Validation을 구현하게 된다.

Custom Validation을 구현하는 방식에는 두 가지 방식이 있다.
이는 CoreData에서 실제로 데이터를 주고받는 대상이 Entity와 Class 두 개로 존재하기 때문으로
이들에 따라 구현 패턴이 바뀌게 된다.

Entity가 추가될 때 검증

EmployeeEntity+CoreDataClass.swift

	public override func validateForInsert() throws {
		try super.validateForInsert()
	}

'validateFor'가 접두로 붙는 메서드는 모두 하위 구현에서 상위 구현을 반드시 호출해야 한다.
따라서 'try super.validateForInsert()' 라인은 반드시 존재해야 한다.

Attribute가 추가될 때 검증

EmployeeEntity+CoreDataClass.swift

	public override func validateValue(_ value: AutoreleasingUnsafeMutablePointer<AnyObject?>, forKey key: String) throws {
		
	}

Attribute가 추가되는 경우에는 위의 validateValue 등의 메서드가 이용되지만,
이들 메서드는 Override가 불가한다.
따라서 해당 메서드가 값을 검증하는 과정에서 호출하는 메서드를 Override 하는 방식으로 구현한다.

EmployeeEntity+CoreDataClass.swift

	@objc func validateAge(_ value: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
		guard let ageValue = value.pointee as? Int else { return }
		
		if ageValue < 20 || ageValue > 50 {
			let msg = "Age value must be between 20 and 50"
			let code = ageValue < 20 ? NSValidationNumberTooSmallError : NSValidationNumberTooLargeError
			let error = NSError(domain: NSCocoaErrorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: msg])
			throw error
		}
	}

Attribute의 검증 조건 구현에는 몇 가지 주의 사항이 존재한다.

  • @objc 특성이 반드시 요구된다.
  • throws가 반드시 존재해야 한다.
  • 이름은 'validate + Attribute 이름'의 형식을 가져야 하며,
    파라미터의 형식도 AutoreleasingUnsafeMutablePointer<AnyObject?> 형식이어야 한다.

위의 주의사항을 모두 만족해야 validateValue 메서드가 검증을 시도할 때
새롭게 정의한 메서드를 자동으로 호출할 수 있다.

이렇게 만든 Custom Validation인 validateAge에서는 위에서 NSError를 확인할 때 볼 수 있었던
msg(Description), code, error(domain)을 지정하고,
이를 error에 담아 반환하는 방식으로 구현됐다.

이렇게 만든 메서드는 validateValue 메서드가 검증을 시도할 때,
자동으로 호출돼 정의된 조건으로 값의 유효성을 검증하게 된다.

ValidationViewController.swift > validate()

   @IBAction func validate(_ sender: Any) {
      guard let name = nameField.text else { return }
      let age = Int16(ageSlider.value)
      let context = DataManager.shared.mainContext

      let newEmployee = EmployeeEntity(context: context)
	   newEmployee.name = name
	   newEmployee.age = age
	   newEmployee.department = selectedDepartment
	   
	   do {
		   try newEmployee.validateForInsert()
	   } catch let error as NSError {
		   switch error.code{
		   case NSValidationStringTooLongError, NSValidationStringTooShortError:
			   if let attr = error.userInfo[NSValidationKeyErrorKey] as? String, attr == "name" {
				   showAlert(message: "Check the name has correct length")
			   } else {
				   showAlert(message: "Check it is valid value")
			   }
		   case NSValidationNumberTooSmallError, NSValidationNumberTooLargeError:
			   if let msg = error.userInfo[NSLocalizedDescriptionKey] as? String {
				   showAlert(message: msg)
			   } else {
				   showAlert(message: "Check it is valid value")
			   }
		   default:
			   break

		   }
		}
   }

case를 추가해 반환되는 NSError의 정보를 이용,
팝업을 표시하도록 구현했다.


결과


 

부서 인원 추가 조건 검증하기.

개발부서에는 20대 직원이 소속될 수 없다.

EmployeeEntity+CoreDataClass.swift

public let NSValidationInvalidAgeAndDepartment = Int.max - 100

해당 조건은 보편적으로는 사용되지 않는 조건이기 때문에
해당되는 에러 코드 또한 제공되지 않는다.
따라서 위와 같이 새롭게 선언해 줄 필요가 있다.

EmployeeEntity+CoreDataClass.swift

public class EmployeeEntity: PersonEntity {
	@objc func validateAge(_ value: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
		guard let ageValue = value.pointee as? Int else {
			return
		}
		if ageValue < 20 || ageValue > 50 {
			let msg = "Age value must be between 20 and 50"
			let code = ageValue < 20 ? NSValidationNumberTooSmallError : NSValidationNumberTooLargeError
			let error = NSError(domain: NSCocoaErrorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: msg])
			throw error
		}
	}
	func validateAgeWithDepartment() throws {
		guard let deptName = department?.name, deptName == "Development" else {
			return
		}
		guard age < 30 else {
			return
		}
		
		let msg = "Development Dept must over 20"
		let code = NSValidationInvalidAgeAndDepartment
		let error = NSError(domain: NSCocoaErrorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: msg])
		throw error
	}
}

public let NSValidationInvalidAgeAndDepartment = Int.max - 100

Department Entity와 Employee Entity를 동시에 다뤄야 한다.
또한 추가되거나 업데이트되는 모든 경우에 검증할 수 있도록 조치해야 한다.

EmployeeEntity+CoreDataClass.swift

@objc(EmployeeEntity)
public class EmployeeEntity: PersonEntity {
	@objc func validateAge(_ value: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
		guard let ageValue = value.pointee as? Int else {
			return
		}
		if ageValue < 20 || ageValue > 50 {
			let msg = "Age value must be between 20 and 50"
			let code = ageValue < 20 ? NSValidationNumberTooSmallError : NSValidationNumberTooLargeError
			let error = NSError(domain: NSCocoaErrorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: msg])
			throw error
		}
	}
	func validateAgeWithDepartment() throws {
		guard let deptName = department?.name, deptName == "Development" else {
			return
		}
		guard age < 30 else {
			return
		}
		
		let msg = "Development Dept must over 30's"
		let code = NSValidationInvalidAgeAndDepartment
		let error = NSError(domain: NSCocoaErrorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: msg])
		throw error
	}
	
	public override func validateForInsert() throws {
		try super.validateForInsert()
		try validateAgeWithDepartment()
	}
	public override func validateForUpdate() throws {
		try super.validateForUpdate()
		try validateAgeWithDepartment()
	}
}

따라서 위와 같이 validateForInsert 메서드와 validateForUpdate 메서드에서
새로 정의한 validateAgeWithDepartment 메서드를 검증에 사용할 수 있도록 추가해 준다.

ValidationViewController.swift

   @IBAction func validate(_ sender: Any) {
      guard let name = nameField.text else { return }
      let age = Int16(ageSlider.value)
      let context = DataManager.shared.mainContext

      let newEmployee = EmployeeEntity(context: context)
	   newEmployee.name = name
	   newEmployee.age = age
	   newEmployee.department = selectedDepartment
	   
	   do {
		   try newEmployee.validateForInsert()
	   } catch let error as NSError {
		   switch error.code{
		   case NSValidationStringTooLongError, NSValidationStringTooShortError:
			   if let attr = error.userInfo[NSValidationKeyErrorKey] as? String, attr == "name" {
				   showAlert(message: "Check the name has correct length")
			   } else {
				   showAlert(message: "Check it is valid value")
			   }
		   case NSValidationNumberTooSmallError, NSValidationNumberTooLargeError:
			   if let msg = error.userInfo[NSLocalizedDescriptionKey] as? String {
				   showAlert(message: msg)
			   } else {
				   showAlert(message: "Check it is valid value")
			   }
		   case NSValidationInvalidAgeAndDepartment:
			   if let msg = error.userInfo[NSLocalizedDescriptionKey] as? String {
				   showAlert(message: msg)
			   } else {
				   showAlert(message: "Check it is valid value")
			   }
		   default:
			   break

		   }
		}
	   context.rollback()
   }

새로 만든 Error Code에도 대응할 수 있도록 case를 수정한다.


결과


 

'학습 노트 > iOS (2021)' 카테고리의 다른 글

198. Concurrency with Context  (0) 2022.06.09
197. Batch Processing with CoreData  (0) 2022.06.03
195. Faulting & Uniquing  (0) 2022.05.24
194. Transformable  (0) 2022.05.20
193. Fetched Result Controller  (0) 2022.04.09