네트워크 애플리케이션 개발 시 가장 중요한 부분 중 하나는 네트워크 통신 중 발생할 수 있는 다양한 예외 상황을 처리하는 것이다. Dart에서 네트워크 예외 처리는 HTTP 요청, WebSocket 통신, 네트워크 연결 상태 등 다양한 상황에서 발생할 수 있으며, 이를 적절하게 처리하지 않으면 애플리케이션의 신뢰성에 큰 문제가 발생할 수 있다.

네트워크 예외의 종류

네트워크 예외는 주로 다음과 같은 상황에서 발생할 수 있다:

  1. 네트워크 연결 오류: 클라이언트가 서버에 연결하지 못하는 경우 발생.
  2. 타임아웃 예외: 요청이 특정 시간 내에 응답을 받지 못할 때 발생.
  3. HTTP 오류 코드: 서버가 클라이언트의 요청을 정상적으로 처리하지 못하고 4xx, 5xx 등 오류 상태 코드를 반환할 때 발생.
  4. 데이터 전송 중단: 데이터가 부분적으로 전송되다가 중단되는 경우 발생.
  5. DNS 오류: 서버의 도메인 이름을 IP 주소로 변환하지 못하는 경우 발생.

이러한 예외를 처리하는 방식은 각 네트워크 프로토콜과 통신 방식에 따라 조금씩 다르지만, 기본적으로 try-catch-finally 구문을 사용하여 예외를 포착하고 대응할 수 있다.

Dart에서의 예외 처리 기초

Dart에서는 예외가 발생할 수 있는 코드 블록을 try 블록으로 감싸고, 발생한 예외는 catch 블록에서 처리할 수 있다. finally 블록은 예외의 발생 여부와 상관없이 마지막에 항상 실행되는 코드 블록이다.

try {
  // 네트워크 요청 수행
  var response = await http.get(Uri.parse('https://example.com'));
  // 성공적 응답 처리
  print('Response status: ${response.statusCode}');
} catch (e) {
  // 예외 처리
  print('네트워크 요청 중 오류 발생: $e');
} finally {
  // 최종 정리 작업
  print('요청 완료');
}

위 코드에서 네트워크 요청 도중 예외가 발생하면 catch 블록에서 이를 포착하고, 적절한 오류 메시지를 출력한다. finally 블록에서는 네트워크 요청이 성공했든 실패했든 상관없이 최종 정리 작업을 수행한다.

HTTP 요청에서 발생할 수 있는 예외 처리

Dart에서 HTTP 요청을 수행할 때 가장 흔히 발생하는 예외 중 하나는 타임아웃이다. 서버 응답이 일정 시간 내에 도착하지 않으면 타임아웃 예외가 발생할 수 있으며, 이를 처리하는 방법은 다음과 같다.

try {
  var response = await http
      .get(Uri.parse('https://example.com'))
      .timeout(Duration(seconds: 5)); // 타임아웃 설정
  print('Response status: ${response.statusCode}');
} catch (e) {
  if (e is TimeoutException) {
    print('요청이 타임아웃되었다.');
  } else {
    print('네트워크 오류 발생: $e');
  }
}

위 코드에서 .timeout 메서드를 사용하여 5초 내에 응답이 도착하지 않으면 TimeoutException이 발생하고, catch 블록에서 이를 처리한다.

HTTP 상태 코드 예외 처리

HTTP 요청을 보냈을 때, 서버가 반환하는 상태 코드에 따라 성공 여부를 확인할 수 있다. 2xx 코드는 성공을 의미하지만, 4xx (클라이언트 오류)나 5xx (서버 오류)와 같은 코드가 반환되면 오류가 발생한 것이다. 이를 처리하는 방법은 다음과 같다.

try {
  var response = await http.get(Uri.parse('https://example.com'));
  if (response.statusCode == 200) {
    print('요청 성공');
  } else {
    throw HttpException('HTTP 오류 발생: 상태 코드 ${response.statusCode}');
  }
} catch (e) {
  print('HTTP 요청 오류: $e');
}

위 코드에서 상태 코드가 200이 아닌 경우, HttpException을 발생시키고 이를 catch 블록에서 처리한다.

DNS 오류 처리

서버에 요청을 보낼 때, 도메인 이름이 IP 주소로 변환되지 않는 경우 DNS 오류가 발생할 수 있다. 이러한 오류는 보통 인터넷 연결이 불안정하거나 잘못된 URL을 사용했을 때 발생한다. Dart에서는 이와 같은 네트워크 연결 오류를 SocketException으로 처리할 수 있다.

try {
  var response = await http.get(Uri.parse('https://invalid-domain.com'));
  print('Response status: ${response.statusCode}');
} catch (e) {
  if (e is SocketException) {
    print('DNS 오류 발생: 서버를 찾을 수 없다.');
  } else {
    print('네트워크 오류 발생: $e');
  }
}

위 코드에서는 도메인 이름을 IP 주소로 변환하는 과정에서 SocketException이 발생할 수 있으며, 이를 포착해 "DNS 오류" 메시지를 출력한다.

데이터 전송 중단 처리

데이터가 서버로 전송되거나 서버로부터 데이터를 수신하는 과정에서 연결이 중단될 수 있다. 이때 Dart는 SocketException을 발생시키며, 이를 통해 연결 중단 상황을 처리할 수 있다.

try {
  var request = await http.post(
    Uri.parse('https://example.com/upload'),
    body: {'file': 'large_data_chunk'},
  );
  print('파일 업로드 성공');
} catch (e) {
  if (e is SocketException) {
    print('데이터 전송 중단: 네트워크 연결이 끊어졌다.');
  } else {
    print('네트워크 오류 발생: $e');
  }
}

위 코드에서는 서버로 파일을 업로드하는 중 연결이 중단되었을 때 SocketException을 통해 예외를 처리한다.

네트워크 재시도 로직

네트워크 요청이 실패했을 때, 자동으로 재시도를 수행하는 로직은 안정적인 애플리케이션 개발에서 중요한 부분이다. Dart에서 이를 구현하기 위해 재시도 횟수를 제한하고, 네트워크 오류가 발생할 때마다 다시 요청을 보내는 방식을 사용할 수 있다.

int retryCount = 0;
const maxRetries = 3;

Future<void> makeRequest() async {
  while (retryCount < maxRetries) {
    try {
      var response = await http.get(Uri.parse('https://example.com'));
      print('Response status: ${response.statusCode}');
      return; // 성공 시 함수 종료
    } catch (e) {
      retryCount++;
      print('네트워크 오류 발생: 재시도 $retryCount회 시도 중...');
      if (retryCount == maxRetries) {
        print('최대 재시도 횟수를 초과하였다.');
        return;
      }
    }
  }
}

이 코드에서는 네트워크 요청이 실패할 경우 최대 3번까지 재시도를 시도하고, 성공하면 재시도를 중지한다. 재시도 횟수가 초과되면 오류 메시지를 출력하고 함수를 종료한다.

네트워크 상태 확인

네트워크 요청 전에 사용자의 네트워크 연결 상태를 확인하는 것도 중요하다. Dart의 connectivity 패키지를 사용하면 장치가 인터넷에 연결되어 있는지 확인할 수 있다. 이를 통해 네트워크 연결이 없는 상황에서 불필요한 요청을 방지할 수 있다.

import 'package:connectivity/connectivity.dart';

Future<void> checkNetworkAndRequest() async {
  var connectivityResult = await (Connectivity().checkConnectivity());
  if (connectivityResult == ConnectivityResult.none) {
    print('네트워크에 연결되어 있지 않는다.');
    return;
  }

  // 네트워크에 연결되어 있을 때만 요청 수행
  try {
    var response = await http.get(Uri.parse('https://example.com'));
    print('Response status: ${response.statusCode}');
  } catch (e) {
    print('네트워크 요청 오류: $e');
  }
}

이 코드에서는 Connectivity 클래스를 사용하여 네트워크 연결 상태를 확인하고, 네트워크에 연결되어 있지 않으면 요청을 보내지 않는다.

WebSocket 예외 처리

WebSocket은 클라이언트와 서버 간의 양방향 통신을 실시간으로 처리할 수 있는 프로토콜로, Dart에서는 web_socket_channel 패키지를 사용하여 WebSocket을 쉽게 구현할 수 있다. 그러나 WebSocket 통신 중에도 다양한 예외 상황이 발생할 수 있으며, 이를 적절히 처리해야 한다.

WebSocket 연결 예외 처리

WebSocket을 열 때 서버에 연결하지 못하거나, 네트워크 오류로 인해 연결이 실패하는 경우가 있을 수 있다. 이러한 예외는 SocketException 또는 WebSocketChannelException으로 처리할 수 있다.

import 'package:web_socket_channel/web_socket_channel.dart';

try {
  final channel = WebSocketChannel.connect(
    Uri.parse('ws://example.com/socket'),
  );
  print('WebSocket 연결 성공');
} catch (e) {
  if (e is WebSocketChannelException) {
    print('WebSocket 연결 오류: $e');
  } else {
    print('네트워크 오류 발생: $e');
  }
}

위 코드는 WebSocket 연결을 시도할 때 발생할 수 있는 오류를 포착하고, 해당 오류가 WebSocket 관련 오류인 경우와 일반 네트워크 오류인 경우를 구분하여 처리한다.

WebSocket 통신 중단 처리

WebSocket 통신 중 연결이 끊기거나 서버가 더 이상 응답하지 않는 경우 onDone 콜백을 사용하여 통신 중단을 처리할 수 있다. 또한, onError 콜백을 사용하면 통신 중 발생한 오류를 처리할 수 있다.

import 'package:web_socket_channel/web_socket_channel.dart';

final channel = WebSocketChannel.connect(
  Uri.parse('ws://example.com/socket'),
);

channel.stream.listen(
  (message) {
    print('메시지 수신: $message');
  },
  onError: (error) {
    print('WebSocket 오류 발생: $error');
  },
  onDone: () {
    print('WebSocket 연결이 종료되었다.');
  },
);

이 코드에서는 WebSocket 스트림을 통해 수신된 메시지를 처리하고, 오류가 발생하거나 연결이 종료되었을 때 각각 onErroronDone 콜백을 사용하여 해당 상황을 처리한다.

WebSocket 자동 재연결

WebSocket 통신 중 연결이 끊길 경우 자동으로 재연결을 시도하는 로직을 구현할 수 있다. 이는 네트워크 상태가 일시적으로 불안정할 때 유용하며, WebSocket 통신의 안정성을 높여줄 수 있다.

import 'dart:async';
import 'package:web_socket_channel/web_socket_channel.dart';

WebSocketChannel? channel;
Timer? reconnectTimer;

void connectWebSocket() {
  try {
    channel = WebSocketChannel.connect(Uri.parse('ws://example.com/socket'));
    print('WebSocket 연결 성공');

    channel!.stream.listen(
      (message) {
        print('메시지 수신: $message');
      },
      onDone: () {
        print('WebSocket 연결 종료, 재연결 시도 중...');
        reconnect();
      },
      onError: (error) {
        print('WebSocket 오류 발생: $error, 재연결 시도 중...');
        reconnect();
      },
    );
  } catch (e) {
    print('WebSocket 연결 오류: $e');
    reconnect();
  }
}

void reconnect() {
  reconnectTimer = Timer(Duration(seconds: 5), () {
    connectWebSocket();
  });
}

void main() {
  connectWebSocket();
}

위 코드에서는 WebSocket 연결이 끊기거나 오류가 발생할 경우 5초 후에 자동으로 재연결을 시도하는 로직을 구현했다. Timer를 사용하여 일정 시간이 지난 후에 다시 connectWebSocket 함수를 호출하는 방식이다.

비동기 네트워크 예외 처리

Dart에서 네트워크 통신은 비동기로 이루어지기 때문에, 예외 처리는 주로 Future와 함께 사용된다. asyncawait 키워드를 사용하면 비동기 네트워크 요청과 관련된 예외를 간편하게 처리할 수 있다. 하지만, 비동기 통신에서 발생하는 예외는 동기 코드와 다르게 처리되어야 하며, 이를 Future.catchError를 사용하여 추가적으로 처리할 수 있다.

Future<void> fetchData() async {
  await http.get(Uri.parse('https://example.com')).then((response) {
    print('응답 수신: ${response.statusCode}');
  }).catchError((e) {
    print('비동기 네트워크 오류 발생: $e');
  });
}

위 코드에서는 thencatchError를 사용하여 비동기 요청 중 발생한 예외를 포착하고 처리한다. asyncawait를 함께 사용하지 않는 방식에서도 예외를 처리할 수 있는 패턴이다.