iOS 중앙 집중식 및 분리형 네트워킹: 싱글톤 클래스가 포함된 AFNetworking 자습서
게시 됨: 2022-03-11iOS 아키텍처 패턴과 관련하여 MVC(Model-View-Controller) 디자인 패턴은 애플리케이션 코드베이스의 수명과 유지 관리 가능성에 매우 좋습니다. 클래스를 서로 분리하여 다양한 요구 사항을 지원하기 위해 쉽게 재사용하거나 교체할 수 있습니다. 이는 객체 지향 프로그래밍(OOP)의 장점을 극대화하는 데 도움이 됩니다.
이 iOS 애플리케이션 아키텍처는 미시적 수준(앱의 개별 화면/섹션)에서 잘 작동하지만 앱이 성장함에 따라 여러 모델에 유사한 기능을 추가하는 자신을 발견할 수 있습니다. 네트워킹과 같은 경우 공통 논리를 모델 클래스에서 싱글톤 도우미 클래스로 옮기는 것이 더 나은 접근 방식이 될 수 있습니다. 이 AFNetworking iOS 자습서에서는 마이크로 수준 MVC 구성 요소에서 분리되고 분리된 아키텍처 응용 프로그램 전체에서 재사용할 수 있는 중앙 집중식 싱글톤 네트워킹 개체를 설정하는 방법을 알려 드리겠습니다.
iOS 네트워킹 문제
Apple은 사용하기 쉬운 iOS SDK에서 모바일 하드웨어 관리의 많은 복잡성을 추상화하는 훌륭한 작업을 수행했지만 네트워킹, Bluetooth, OpenGL 및 멀티미디어 처리와 같은 일부 경우에는 클래스를 유지하려는 목표 때문에 번거로울 수 있습니다. SDK는 유연합니다. 고맙게도 풍부한 iOS 개발자 커뮤니티는 애플리케이션 디자인과 구조를 단순화하기 위한 노력의 일환으로 가장 일반적인 사용 사례를 단순화하는 고급 프레임워크를 만들었습니다. ios 앱 아키텍처 모범 사례를 사용하는 훌륭한 프로그래머는 어떤 도구를 사용해야 하는지, 왜 사용해야 하는지, 언제 자신만의 도구와 클래스를 처음부터 작성하는 것이 더 좋은지 알고 있습니다.
AFNetworking은 훌륭한 네트워킹의 예이며 개발자의 일상적인 작업을 단순화하는 가장 일반적으로 사용되는 오픈 소스 프레임워크 중 하나입니다. RESTful API 네트워킹을 단순화하고 성공, 진행 및 실패 완료 블록이 있는 모듈식 요청/응답 패턴을 생성합니다. 이렇게 하면 개발자가 구현한 대리자 메서드 및 사용자 지정 요청/연결 설정이 필요하지 않으며 모든 클래스에 매우 빠르게 포함될 수 있습니다.
AFNetworking의 문제
AFNetworking은 훌륭하지만 모듈화로 인해 단편적인 방식으로 사용될 수도 있습니다. 일반적으로 비효율적인 구현에는 다음이 포함될 수 있습니다.
단일 보기 컨트롤러에서 유사한 메서드 및 속성을 사용하는 여러 네트워크 요청
동기화되지 않을 수 있는 분산된 공통 변수로 이어지는 여러 뷰 컨트롤러에서 거의 동일한 요청
해당 클래스와 관련이 없는 데이터에 대한 클래스의 네트워크 요청
제한된 수의 보기, 구현할 API 호출이 거의 없고 자주 변경되지 않는 애플리케이션의 경우 이는 큰 문제가 되지 않을 수 있습니다. 그러나 더 큰 생각을 하고 수년간의 업데이트를 계획하고 있을 가능성이 더 큽니다. 귀하의 경우가 후자라면 결국 다음을 처리해야 할 것입니다.
여러 세대의 애플리케이션을 지원하는 API 버전 관리
기능 확장을 위해 시간이 지남에 따라 새 매개변수 추가 또는 기존 매개변수 변경
완전히 새로운 API 구현
네트워킹 코드가 코드베이스 전체에 흩어져 있다면 이것은 이제 잠재적인 악몽입니다. 바라건대, 공통 헤더에 정적으로 정의된 매개변수 중 일부가 적어도 일부는 있지만 가장 사소한 변경에도 수십 개의 클래스를 건드릴 수 있습니다.
AFNetworking 제한 사항을 어떻게 해결합니까?
요청, 응답 및 해당 매개변수의 처리를 중앙 집중화하는 네트워킹 싱글톤을 만듭니다.
싱글톤 개체는 해당 클래스의 리소스에 대한 전역 액세스 지점을 제공합니다. 싱글톤은 일반적인 서비스나 리소스를 제공하는 클래스와 같이 이 단일 제어 지점이 필요한 상황에서 사용됩니다. 팩토리 메소드를 통해 싱글톤 클래스에서 전역 인스턴스를 얻습니다. - 사과
따라서 싱글톤은 애플리케이션의 수명 동안 존재하는 애플리케이션에서 단 하나의 인스턴스만 가질 수 있는 클래스입니다. 또한 인스턴스가 하나만 있다는 것을 알고 있기 때문에 해당 메서드나 속성에 액세스해야 하는 다른 클래스에서 쉽게 액세스할 수 있습니다.
네트워킹에 싱글톤을 사용해야 하는 이유는 다음과 같습니다.
정적으로 초기화되므로 일단 생성되면 액세스를 시도하는 모든 클래스에서 동일한 메서드와 속성을 사용할 수 있습니다. 이상한 동기화 문제가 발생하거나 클래스의 잘못된 인스턴스에서 데이터를 요청할 가능성이 없습니다.
속도 제한 미만으로 유지되도록 API 호출을 제한할 수 있습니다(예: API 요청을 초당 5개 미만으로 유지해야 하는 경우).
호스트 이름, 포트 번호, 끝점, API 버전, 장치 유형, 영구 ID, 화면 크기 등과 같은 정적 속성을 함께 배치할 수 있으므로 하나의 변경 사항이 모든 네트워크 요청에 영향을 줍니다.
많은 네트워크 요청 간에 공통 속성을 재사용할 수 있습니다.
싱글톤 개체는 인스턴스화될 때까지 메모리를 차지하지 않습니다. 이는 기기가 없는 경우 Chromecast로의 비디오 전송을 처리하는 것과 같이 일부 사용자가 필요로 하지 않을 수 있는 매우 구체적인 사용 사례가 있는 싱글톤에 유용할 수 있습니다.
네트워크 요청은 뷰와 컨트롤러에서 완전히 분리되어 뷰와 컨트롤러가 파괴된 후에도 계속될 수 있습니다.
네트워크 로깅은 중앙 집중화되고 단순화될 수 있습니다.
경고와 같은 실패에 대한 일반적인 이벤트는 모든 요청에 대해 재사용할 수 있습니다.
이러한 싱글톤의 주요 구조는 간단한 최상위 정적 속성 변경으로 여러 프로젝트에서 재사용할 수 있습니다.
싱글톤을 사용하지 않는 몇 가지 이유:
단일 클래스에서 여러 책임을 제공하기 위해 남용될 수 있습니다. 예를 들어, 비디오 처리 방법은 네트워킹 방법 또는 사용자 상태 방법과 혼합될 수 있습니다. 이는 잘못된 설계 방식으로 코드를 이해하기 어렵게 만들 수 있습니다. 대신 특정 책임이 있는 여러 싱글톤을 만들어야 합니다.
싱글톤은 하위 분류할 수 없습니다.
싱글톤은 종속성을 숨길 수 있으므로 덜 모듈화됩니다. 예를 들어, 싱글톤이 제거되고 클래스에 싱글톤이 가져온 가져오기가 누락된 경우 향후 문제가 발생할 수 있습니다(특히 외부 라이브러리 종속성이 있는 경우).
클래스는 다른 클래스에서 예상치 못한 긴 작업 동안 싱글톤의 공유 속성을 수정할 수 있습니다. 이에 대한 적절한 고려가 없으면 결과가 달라질 수 있습니다.
싱글톤 자체가 할당 해제되지 않기 때문에 싱글톤의 메모리 누수는 중요한 문제가 될 수 있습니다.
그러나 iOS 앱 아키텍처 모범 사례를 사용하면 이러한 단점을 완화할 수 있습니다. 몇 가지 모범 사례는 다음과 같습니다.
각 싱글톤은 단일 책임을 처리해야 합니다.
높은 정확도가 필요한 경우 여러 클래스 또는 스레드에 의해 빠르게 변경될 데이터를 저장하는 데 싱글톤을 사용하지 마십시오.
사용 가능한 종속성을 기반으로 기능을 활성화/비활성화하는 싱글톤을 빌드합니다.
싱글톤 속성에 많은 양의 데이터를 저장하지 마세요(수동으로 관리하지 않는 한).
AFNetworking을 사용한 간단한 싱글톤 예제
먼저 전제 조건으로 AFNetworking을 프로젝트에 추가하십시오. 가장 간단한 접근 방식은 Cocoapods를 사용하는 것이며 지침은 GitHub 페이지에서 찾을 수 있습니다.
거기에 있는 동안 UIAlertController+Blocks
및 MBProgressHUD
(CocoaPods로 다시 쉽게 추가됨)를 추가하는 것이 좋습니다. 이것들은 분명히 선택 사항이지만 AppDelegate 창의 싱글 톤에서 구현하려는 경우 진행률과 경고를 크게 단순화합니다.
AFNetworking
이 추가되면 NSObject
의 하위 클래스로 NetworkManager
라는 새 Cocoa Touch 클래스를 생성하여 시작합니다. 관리자에 액세스하기 위한 클래스 메서드를 추가합니다. NetworkManager.h
파일은 아래 코드와 같아야 합니다.
#import <Foundation/Foundation.h> #import “AFNetworking.h” @interface NetworkManager : NSObject + (id)sharedManager; @end
다음으로 싱글톤에 대한 기본 초기화 방법을 구현하고 AFNetworking 헤더를 가져옵니다. 클래스 구현은 다음과 같아야 합니다(참고: 자동 참조 계산을 사용한다고 가정).
#import "NetworkManager.h" @interface NetworkManager() @end @implementation NetworkManager #pragma mark - #pragma mark Constructors static NetworkManager *sharedManager = nil; + (NetworkManager*)sharedManager { static dispatch_once_t once; dispatch_once(&once, ^ { sharedManager = [[NetworkManager alloc] init]; }); return sharedManager; } - (id)init { if ((self = [super init])) { } return self; } @end
엄청난! 이제 우리는 요리를 하고 속성과 메서드를 추가할 준비가 되었습니다. 싱글톤에 액세스하는 방법을 이해하기 위한 빠른 테스트로 다음을 NetworkManager.h
에 추가해 보겠습니다.
@property NSString *appID; - (void)test;
그리고 다음을 NetworkManager.m
에 추가합니다.
#define HOST @”http://www.apitesting.dev/” static const in port = 80; … @implementation NetworkManager … //Set an initial property to init: - (id)init { if ((self = [super init])) { self.appID = @”1”; } return self; } - (void)test { NSLog(@”Testing out the networking singleton for appID: %@, HOST: %@, and PORT: %d”, self.appID, HOST, port); }
그런 다음 기본 ViewController.m
파일(또는 가지고 있는 모든 파일)에서 NetworkManager.h
를 가져온 다음 viewDidLoad
에 다음을 추가합니다.
[[NetworkManager sharedManager] test];
앱을 실행하면 출력에 다음이 표시되어야 합니다.
Testing our the networking singleton for appID: 1, HOST: http://www.apitesting.dev/, and PORT: 80
좋습니다. 따라서 이와 같이 #define
, static const 및 @property
를 한 번에 모두 혼합하지 않고 명확성을 위해 옵션을 표시하기만 하면 됩니다. "정적 const"는 형식 안전성을 위한 더 나은 선언이지만 #define
은 매크로를 사용할 수 있으므로 문자열 작성에 유용할 수 있습니다. 가치를 위해 이 시나리오에서는 간결함을 위해 #define
을 사용하고 있습니다. 포인터를 사용하지 않는 한 이러한 선언 방식 간에 실질적인 차이는 없습니다.
이제 #defines
, 상수, 속성 및 메서드를 이해했으므로 이러한 항목을 제거하고 더 관련성 있는 예제로 넘어갈 수 있습니다.
네트워킹의 예
사용자가 무엇이든 액세스하려면 로그인해야 하는 애플리케이션을 상상해 보십시오. 앱 실행 시 인증 토큰을 저장했는지 확인하고 저장했다면 API에 GET 요청을 수행하여 토큰이 만료되었는지 여부를 확인합니다.
AppDelegate.m
에서 토큰의 기본값을 등록해 보겠습니다.
+ (void)initialize { NSDictionary *defaults = [NSDictionary dictionaryWithObjectsAndKeys:@"", @"token", nil]; [[NSUserDefaults standardUserDefaults] registerDefaults:defaults]; }
NetworkManager에 토큰 검사를 추가하고 완료 블록을 통해 검사에 대한 피드백을 받습니다. 원하는 대로 이러한 완성 블록을 디자인할 수 있습니다. 이 예에서는 응답 개체 데이터로 성공을 사용하고 오류 응답 문자열과 상태 코드로 실패를 사용하고 있습니다. 참고: 분석에서 값을 증가시키는 것과 같이 수신측에 중요하지 않은 경우 실패는 선택적으로 생략될 수 있습니다.
네트워크매니저.h
@interface
위:
typedef void (^NetworkManagerSuccess)(id responseObject); typedef void (^NetworkManagerFailure)(NSString *failureReason, NSInteger statusCode);
@인터페이스에서:
@property(비원자, 강력) AFHTTPSessionManager *networkingManager;
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;
NetworkManager.m:
BASE_URL 정의:
#define ENABLE_SSL 1 #define HOST @"http://www.apitesting.dev/" #define PROTOCOL (ENABLE_SSL ? @"https://" : @"http://") #define PORT @"80" #define BASE_URL [NSString stringWithFormat:@"%@%@:%@", PROTOCOL, HOST, PORT]
인증된 요청과 구문 분석 오류를 더 간단하게 만들기 위해 몇 가지 도우미 메서드를 추가합니다(이 예제에서는 JSON 웹 토큰을 사용함).

- (AFHTTPSessionManager*)getNetworkingManagerWithToken:(NSString*)token { if (self.networkingManager == nil) { self.networkingManager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASE_URL]]; if (token != nil && [token length] > 0) { NSString *headerToken = [NSString stringWithFormat:@"%@ %@", @"JWT", token]; [self.networkingManager.requestSerializer setValue:headerToken forHTTPHeaderField:@"Authorization"]; // Example - [networkingManager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; } self.networkingManager.requestSerializer = [AFJSONRequestSerializer serializer]; self.networkingManager.responseSerializer.acceptableContentTypes = [self.networkingManager.responseSerializer.acceptableContentTypes setByAddingObjectsFromArray:@[@"text/html", @"application/json", @"text/json"]]; self.networkingManager.securityPolicy = [self getSecurityPolicy]; } return self.networkingManager; } - (id)getSecurityPolicy { return [AFSecurityPolicy defaultPolicy]; /* Example - AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone]; [policy setAllowInvalidCertificates:YES]; [policy setValidatesDomainName:NO]; return policy; */ } - (NSString*)getError:(NSError*)error { if (error != nil) { NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; NSDictionary *responseObject = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil]; if (responseObject != nil && [responseObject isKindOfClass:[NSDictionary class]] && [responseObject objectForKey:@"message"] != nil && [[responseObject objectForKey:@"message"] length] > 0) { return [responseObject objectForKey:@"message"]; } } return @"Server Error. Please try again later"; }
MBProgressHUD를 추가했다면 여기에서 사용할 수 있습니다.
#import "MBProgressHUD.h" @interface NetworkManager() @property (nonatomic, strong) MBProgressHUD *progressHUD; @end … - (void)showProgressHUD { [self hideProgressHUD]; self.progressHUD = [MBProgressHUD showHUDAddedTo:[[UIApplication sharedApplication] delegate].window animated:YES]; [self.progressHUD removeFromSuperViewOnHide]; self.progressHUD.bezelView.color = [UIColor colorWithWhite:0.0 alpha:1.0]; self.progressHUD.contentColor = [UIColor whiteColor]; } - (void)hideProgressHUD { if (self.progressHUD != nil) { [self.progressHUD hideAnimated:YES]; [self.progressHUD removeFromSuperview]; self.progressHUD = nil; } }
그리고 토큰 확인 요청:
- (void)tokenCheckWithSuccess:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSString *token = [defaults objectForKey:@"token"]; if (token == nil || [token length] == 0) { if (failure != nil) { failure(@"Invalid Token", -1); } return; } [self showProgressHUD]; NSMutableDictionary *params = [NSMutableDictionary dictionary]; [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { [self hideProgressHUD]; if (success != nil) { success(responseObject); } } failure:^(NSURLSessionTask *operation, NSError *error) { [self hideProgressHUD]; NSString *errorMessage = [self getError:error]; if (failure != nil) { failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode); } }]; }
이제 ViewController.m viewWillAppear 메서드에서 이 싱글톤 메서드를 호출합니다. 요청의 단순성과 View Controller 측의 작은 구현에 주목하십시오.
[[NetworkManager sharedManager] tokenCheckWithSuccess:^(id responseObject) { // Allow User Access and load content //[self loadContent]; } failure:^(NSString *failureReason, NSInteger statusCode) { // Logout user if logged in and deny access and show login view //[self showLoginView]; }];
그게 다야! 이 스니펫이 시작 시 인증을 확인해야 하는 모든 애플리케이션에서 가상으로 어떻게 사용될 수 있는지 주목하십시오.
마찬가지로 로그인에 대한 POST 요청을 처리할 수 있습니다. NetworkManager.h:
- (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure;
NetworkManager.m:
- (void)authenticateWithEmail:(NSString*)email password:(NSString*)password success:(NetworkManagerSuccess)success failure:(NetworkManagerFailure)failure { if (email != nil && [email length] > 0 && password != nil && [password length] > 0) { [self showProgressHUD]; NSMutableDictionary *params = [NSMutableDictionary dictionary]; [params setObject:email forKey:@"email"]; [params setObject:password forKey:@"password"]; [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { [self hideProgressHUD]; if (success != nil) { success(responseObject); } } failure:^(NSURLSessionTask *operation, NSError *error) { [self hideProgressHUD]; NSString *errorMessage = [self getError:error]; if (failure != nil) { failure(errorMessage, ((NSHTTPURLResponse*)operation.response).statusCode); } }]; } else { if (failure != nil) { failure(@"Email and Password Required", -1); } } }
여기에서 멋지게 꾸며서 AppDelegate 창에서 AlertController+Blocks로 경고를 추가하거나 단순히 실패 개체를 뷰 컨트롤러로 다시 보낼 수 있습니다. 또한 여기에 사용자 자격 증명을 저장하거나 대신 뷰 컨트롤러에서 처리하도록 할 수 있습니다. 일반적으로 NetworkManager와 직접 통신할 수 있는 자격 증명 및 권한을 처리하는 별도의 UserManager 싱글톤을 구현합니다(개인 취향).
다시 한 번, 뷰 컨트롤러 쪽은 매우 간단합니다.
- (void)loginUser { NSString *email = @"[email protected]"; NSString *password = @"SomeSillyEasyPassword555"; [[NetworkManager sharedManager] authenticateWithEmail:email password:password success:^(id responseObject) { // Save User Credentials and show content } failure:^(NSString *failureReason, NSInteger statusCode) { // Explain to user why authentication failed }]; }
앗! API 버전을 지정하고 기기 유형을 보내는 것을 잊었습니다. 또한 엔드포인트를 "/checktoken"에서 "/token"으로 업데이트했습니다. 네트워킹을 중앙 집중화했기 때문에 업데이트하기가 매우 쉽습니다. 우리는 코드를 파헤칠 필요가 없습니다. 모든 요청에 대해 이러한 매개변수를 사용하므로 도우미를 생성합니다.
#define API_VERSION @"1.0" #define DEVICE_TYPE UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? @"tablet" : @"phone" - (NSMutableDictionary*)getBaseParams { NSMutableDictionary *baseParams = [NSMutableDictionary dictionary]; [baseParams setObject:@"version" forKey:API_VERSION]; [baseParams setObject:@"device_type" forKey:DEVICE_TYPE]; return baseParams; }
향후에 여기에 임의의 수의 공통 매개변수를 쉽게 추가할 수 있습니다. 그런 다음 다음과 같이 토큰 확인 및 인증 방법을 업데이트할 수 있습니다.
… NSMutableDictionary *params = [self getBaseParams]; [[self getNetworkingManagerWithToken:token] GET:@"/checktoken" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) { … … NSMutableDictionary *params = [self getBaseParams]; [params setObject:email forKey:@"email"]; [params setObject:password forKey:@"password"]; [[self getNetworkingManagerWithToken:nil] POST:@"/authenticate" parameters:params progress:nil success:^(NSURLSessionTask *task, id responseObject) {
AFNetworking 튜토리얼 마무리하기
여기서 멈출 것이지만 보시다시피 공통 네트워킹 매개변수와 메서드를 싱글톤 관리자에 중앙 집중화하여 뷰 컨트롤러 구현을 크게 단순화했습니다. 향후 업데이트는 간단하고 빠를 것이며 가장 중요한 것은 사용자 경험에서 네트워킹을 분리한다는 것입니다. 다음에 디자인 팀이 UI/UX 정밀 검사를 요청할 때 네트워킹 쪽에서 이미 작업이 완료되었음을 알게 될 것입니다!
이 기사에서는 네트워킹 싱글톤에 중점을 두었지만 다음과 같은 다른 많은 중앙 집중식 기능에도 동일한 원칙을 적용할 수 있습니다.
- 사용자 상태 및 권한 처리
- 앱 탐색에 대한 터치 작업 라우팅
- 비디오 및 오디오 관리
- 해석학
- 알림
- 주변기기
- 그리고 훨씬 더…
우리는 또한 iOS 애플리케이션 아키텍처에 중점을 두었지만 이것은 Android 및 JavaScript로도 쉽게 확장될 수 있습니다. 보너스로, 고도로 정의되고 기능 지향적인 코드를 생성함으로써 앱을 새로운 플랫폼으로 이식하는 작업을 훨씬 더 빠르게 만듭니다.
요약하자면, 위의 네트워킹 예제와 같이 주요 싱글톤 방법을 설정하기 위해 초기 프로젝트 계획에 약간의 추가 시간을 투자하면 미래의 코드가 더 깨끗하고 간단하며 유지 관리가 쉬워질 수 있습니다.