디버깅과 모니터링을 위한 로깅 추가하기
디버깅과 모니터링을 위한 로깅 추가하기
Dart 애플리케이션에 로깅을 추가해 디버깅과 모니터링에 도움을 주는 방법을 배워 봐요.
본문
이 장에서는 Dart 애플리케이션에 로깅을 추가하는 방법을 배워요. 로깅은 다양한 환경에서 애플리케이션의 동작을 디버깅하고 모니터링하며 이해하는 데 아주 중요한 도구예요.
logging패키지를 추가하고 배워 봐요.- 로그 레벨과 파일 출력을 구성해요.
- CLI 명령에 로깅을 통합해요.
사전 준비 (Prerequisites)
이 장을 시작하기 전에 다음을 확인해 주세요:
- 11장을 마치고
dartpedia프로젝트가 있는, 동작하는 Dart 개발 환경을 갖췄는지 확인해요. - 디버깅의 기본과, 애플리케이션에서 오류와 이벤트를 추적하는 것이 왜 중요한지 이해하고 있는지 확인해요.
작업 (Tasks)
이 장에서는 wikipedia 패키지 명령을 통합하고, 오류를 추적하고 동작을 모니터링하는 데 도움이 되도록 로깅을 추가해서 dartpedia CLI 애플리케이션을 완성해요. 여기에는 패키지 의존성 추가, CLI 명령 만들기, Logger 인스턴스 구성, 그리고 완성된 애플리케이션 실행이 포함돼요.
작업 1: cli 패키지에 의존성 추가하기 (Add dependencies to the cli package)
먼저 wikipedia 패키지(이전 장에서 만든 것)와 logging 패키지를 CLI 프로젝트의 의존성에 추가해 주세요.
-
cli/pubspec.yaml파일을 열어 주세요. -
dependencies섹션을 찾아 주세요. -
wikipedia와logging패키지를 의존성에 추가해 주세요:
dependencies:
http: ^1.3.0
command_runner:
path: ../command_runner
# Add the following lines
wikipedia:
path: ../wikipedia
logging: ^1.2.0
cli디렉터리에서dart pub get을 실행해 새 의존성을 가져와 주세요.
작업 2: 로거 만들기 (Create a logger)
다음으로 Logger 인스턴스를 만들고 로그 메시지를 파일에 쓰도록 구성해요. 로거용 새 파일을 만들고 필요한 import를 설정하는 작업이 포함돼요.
-
cli/lib/src/logger.dart라는 새 파일을 만들어 주세요. -
필요한 import를 추가하고
initFileLogger함수를 정의해 주세요:
import 'dart:io';
import 'package:logging/logging.dart';
Logger initFileLogger(String name) {
// Enables logging from child loggers.
hierarchicalLoggingEnabled = true;
// Create a logger instance with the provided name.
final logger = Logger(name);
final now = DateTime.now();
// The rest of the function will be added below.
// ...
return logger;
}
- 프로젝트의 루트 디렉터리를 찾고,
logs디렉터리가 없으면 만들고, 고유한 로그 파일을 만드는 코드를 추가해 주세요:
Logger initFileLogger(String name) {
hierarchicalLoggingEnabled = true;
final logger = Logger(name);
final now = DateTime.now();
// Get the path to the project directory from the current script.
final scriptFile = File(Platform.script.toFilePath());
final projectDir = scriptFile.parent.parent.path;
// Create a 'logs' directory if it doesn't exist.
final dir = Directory('$projectDir/logs');
if (!dir.existsSync()) dir.createSync();
// Create a log file with a unique name based on
// the current date and logger name.
final logFile = File(
'${dir.path}/${now.year}_${now.month}_${now.day}_$name.txt',
);
// The rest of the function will be added below.
// ...
return logger;
}
- 로거의 레벨을 구성하고 로그 메시지를 파일에 쓰는 리스너를 설정해 주세요:
Logger initFileLogger(String name) {
hierarchicalLoggingEnabled = true;
final logger = Logger(name);
final now = DateTime.now();
final scriptFile = File(Platform.script.toFilePath());
final projectDir = scriptFile.parent.parent.path;
final dir = Directory('$projectDir/logs');
if (!dir.existsSync()) dir.createSync();
final logFile = File(
'${dir.path}/${now.year}_${now.month}_${now.day}_$name.txt',
);
// Set the logger level to ALL, so it logs all messages regardless of severity.
// Level.ALL is useful for development and debugging, but you'll likely want to
// use a more restrictive level like Level.INFO or Level.WARNING in production.
logger.level = Level.ALL;
// Listen for log records and write each one to the log file.
logger.onRecord.listen((record) {
final msg =
'[${record.time} - ${record.loggerName}] ${record.level.name}: ${record.message}';
logFile.writeAsStringSync('$msg \n', mode: FileMode.append);
});
return logger;
}
이 코드는 다음을 수행해요:
hierarchicalLoggingEnabled = true로 계층적 로깅(hierarchical logging)을 활성화해요.- 주어진 이름으로
Logger인스턴스를 만들어요. Platform.script.path에서 프로젝트 디렉터리를 가져와요.logs디렉터리가 없으면 만들어요.- 현재 날짜와 로거 이름으로 로그 파일을 만들어요.
- 로거 레벨을
Level.ALL로 설정해 모든 메시지를 기록하게 해요. 개발과 디버깅에 유용하지만, 프로덕션에서는Level.INFO나Level.WARNING같은 더 제한적인 레벨을 쓰고 싶을 거예요. - 로그 레코드를 듣고 로그 파일에 기록해요.
작업 3: SearchCommand 명령 만들기 (Create the SearchCommand command)
CLI의 핵심 기능은 명령들에 있어요. SearchCommand와 GetArticleCommand 파일을 만들고, 로깅과 오류 처리를 포함한 필요한 코드를 추가해 주세요.
-
cli/lib/src/commands/search.dart라는 새 파일을 만들어 주세요. -
import와 기본 클래스 구조를 추가해 주세요. 이
SearchCommand클래스는Command를 확장하며, 생성자는Logger인스턴스를 받아요. 생성자에서 로거를 받는 것은 의존성 주입(dependency injection)이라는 흔한 패턴으로, 명령이 자체 로거를 만들 필요 없이 이벤트를 기록할 수 있게 해 줘요.
import 'dart:async';
import 'dart:io';
import 'package:command_runner/command_runner.dart';
import 'package:logging/logging.dart';
import 'package:wikipedia/wikipedia.dart';
class SearchCommand extends Command {
SearchCommand({required this.logger}) {
addFlag(
'im-feeling-lucky',
help:
'If true, prints the summary of the top article that the search returns.',
);
}
final Logger logger;
@override
String get description => 'Search for Wikipedia articles.';
@override
bool get requiresArgument => true;
@override
String get name => 'search';
@override
String get valueHelp => 'STRING';
@override
String get help =>
'Prints a list of links to Wikipedia articles that match the given term.';
@override
FutureOr<String> run(ArgResults args) async {
// Command logic will be added below.
// ...
return '';
}
}
- Wikipedia를 검색하고 결과를 형식화하는 명령 로직을 구현해 주세요:
// ...
@override
FutureOr<String> run(ArgResults args) async {
if (requiresArgument &&
(args.commandArg == null || args.commandArg!.isEmpty)) {
throw ArgumentException('Please include a search term', name);
}
final buffer = StringBuffer('Search results:\n');
final SearchResults results = await search(args.commandArg!);
if (args.flag('im-feeling-lucky')) {
final title = results.results.first.title;
final Summary article = await getArticleSummaryByTitle(title);
buffer.writeln('Lucky you!');
buffer.writeln(article.titles.normalized.titleText);
if (article.description != null) {
buffer.writeln(article.description);
}
buffer.writeln(article.extract);
buffer.writeln();
buffer.writeln('All results:');
}
for (var result in results.results) {
buffer.writeln('${result.title} - ${result.url}');
}
return buffer.toString();
}
// ...
- 마지막으로 주요 로직을
try/catch블록으로 감싸 주세요. 이렇게 하면 네트워크 문제나 데이터 형식화 문제에서 발생할 수 있는 잠재적 예외를 처리할 수 있어요. 주입된logger를 사용해 이런 오류를 로그 파일에 기록해요.
import 'dart:async';
import 'dart:io';
import 'package:command_runner/command_runner.dart';
import 'package:logging/logging.dart';
import 'package:wikipedia/wikipedia.dart';
class SearchCommand extends Command {
SearchCommand({required this.logger}) {
addFlag(
'im-feeling-lucky',
help:
'If true, prints the summary of the top article that the search returns.',
);
}
final Logger logger;
@override
String get description => 'Search for Wikipedia articles.';
@override
bool get requiresArgument => true;
@override
String get name => 'search';
@override
String get valueHelp => 'STRING';
@override
String get help =>
'Prints a list of links to Wikipedia articles that match the given term.';
@override
FutureOr<String> run(ArgResults args) async {
if (requiresArgument &&
(args.commandArg == null || args.commandArg!.isEmpty)) {
throw ArgumentException('Please include a search term', name);
}
final buffer = StringBuffer('Search results:\n');
try {
final SearchResults results = await search(args.commandArg!);
if (args.flag('im-feeling-lucky')) {
final title = results.results.first.title;
final Summary article = await getArticleSummaryByTitle(title);
buffer.writeln('Lucky you!');
buffer.writeln(article.titles.normalized.titleText);
if (article.description != null) {
buffer.writeln(article.description);
}
buffer.writeln(article.extract);
buffer.writeln();
buffer.writeln('All results:');
}
for (var result in results.results) {
buffer.writeln('${result.title} - ${result.url}');
}
return buffer.toString();
} on HttpException catch (e) {
logger
..warning(e.message)
..warning(e.uri)
..info(usage);
return e.message;
} on FormatException catch (e) {
logger
..warning(e.message)
..warning(e.source)
..info(usage);
return e.message;
}
}
}
작업 4: GetArticleCommand 명령 만들기 (Create the GetArticleCommand command)
이제 GetArticleCommand 파일을 만들고 필요한 코드를 추가해 주세요. 이전 SearchCommand와 비슷하며, 역시 try/catch 블록으로 잠재적인 네트워크나 데이터 오류를 처리해요.
-
cli/lib/src/commands/get_article.dart라는 새 파일을 만들어 주세요. -
get_article.dart에 다음 코드를 추가해 주세요:
import 'dart:async';
import 'dart:io';
import 'package:command_runner/command_runner.dart';
import 'package:logging/logging.dart';
import 'package:wikipedia/wikipedia.dart';
class GetArticleCommand extends Command {
GetArticleCommand({required this.logger});
final Logger logger;
@override
String get description => 'Read an article from Wikipedia';
@override
String get name => 'article';
@override
String get help => 'Gets an article by exact canonical wikipedia title.';
@override
String get defaultValue => 'cat';
@override
String get valueHelp => 'STRING';
@override
FutureOr<String> run(ArgResults args) async {
try {
var title = args.commandArg ?? defaultValue;
final List<Article> articles = await getArticleByTitle(title);
// API returns a list of articles, but we only care about the closest hit.
final article = articles.first;
final buffer = StringBuffer('\n=== ${article.title.titleText} ===\n\n');
buffer.write(article.extract.split(' ').take(500).join(' '));
return buffer.toString();
} on HttpException catch (e) {
logger
..warning(e.message)
..warning(e.uri)
..info(usage);
return e.message;
} on FormatException catch (e) {
logger
..warning(e.message)
..warning(e.source)
..info(usage);
return e.message;
}
}
}
방금 추가한 코드를 살펴보세요. SearchCommand와 GetArticleCommand는 이제:
command_runner,logging,wikipedia같은 필요한 패키지를 import해 그 클래스와 함수를 사용해요.- 생성자를 통해
Logger인스턴스를 받아요. 이는 의존성 주입이라는 흔한 패턴으로, 명령이 자체 로거를 만들 필요 없이 이벤트를 기록할 수 있게 해 줘요. - 명령의 로직을 정의하는
run메서드를 구현해요. 이 메서드는 적절한 wikipedia API를 호출하고 출력을 형식화해요. - 네트워크 오류(
HttpException)와 데이터 파싱 오류(FormatException)를 우아하게 처리하고 디버깅을 위해 기록하는try/catch블록을 포함해요.
작업 5: 명령 export하고 cli.dart 연결하기 (Export commands and wire up cli.dart)
이제 cli 라이브러리에서 로거와 명령을 export하고, cli/bin/cli.dart에서 연결해 완전한 CLI 애플리케이션을 만들어 봐요.
cli/lib/cli.dart파일을 열어 주세요. 자리 표시자 내용을 로거와 명령에 대한 export로 교체해 주세요:
export 'src/commands/get_article.dart';
export 'src/commands/search.dart';
export 'src/logger.dart';
이 파일은 라이브러리의 공개 인터페이스 역할을 하며, initFileLogger, SearchCommand, GetArticleCommand를 export해서 cli/bin/cli.dart가 package:cli/cli.dart에서 import할 수 있게 해 줘요.
-
cli/bin/cli.dart파일을 열어 주세요. -
cli와command_runner에 대한 import를 추가해 주세요:
import 'package:cli/cli.dart';
import 'package:command_runner/command_runner.dart';
main함수를 수정해 로거를 초기화하고CommandRunner에 명령들을 등록해 주세요:
import 'package:cli/cli.dart';
import 'package:command_runner/command_runner.dart';
void main(List<String> arguments) async {
final errorLogger = initFileLogger('errors');
final app =
CommandRunner(
onOutput: (String output) async {
await write(output);
},
onError: (Object error) {
if (error is Error) {
errorLogger.severe(
'[Error] ${error.toString()}\n${error.stackTrace}',
);
throw error;
}
if (error is Exception) {
errorLogger.warning(error);
print(error);
}
},
)
..addCommand(HelpCommand())
..addCommand(SearchCommand(logger: errorLogger))
..addCommand(GetArticleCommand(logger: errorLogger));
app.run(arguments);
}
이 코드는 다음을 수행해요:
initFileLogger('errors')로Logger인스턴스를 초기화해요.- 로거 인스턴스를
SearchCommand와GetArticleCommand에 전달해요. - 모든 명령을
CommandRunner에 등록해요.
작업 6: 애플리케이션 실행하고 로그 확인하기 (Run the application and check the logs)
이제 CLI 명령을 구현하고 bin/cli.dart에서 CommandRunner를 연결했으며 로깅을 구성했으니, 터미널에서 애플리케이션을 테스트해 봐요. 이 명령들은 반드시 cli 디렉터리(/dartpedia/cli)에서 실행해야 해요.
- 문서를 검색하도록 CLI 애플리케이션을 실행해 주세요:
dart run bin/cli.dart search "Dart programming"
검색어와 일치하는 Wikipedia 문서를 나열하는 터미널 출력이 보여야 해요.
--im-feeling-lucky플래그로 명령을 실행해 보세요:
dart run bin/cli.dart search "Dart" --im-feeling-lucky
- 오류 로깅을 테스트하려면 필수 search 인자 없이 애플리케이션을 실행해 주세요:
dart run bin/cli.dart search
터미널에 오류가 출력되는 게 보여야 해요:
ArgumentException: Please include a search term
-
프로젝트의
cli/logs디렉터리를 확인해 주세요. 현재 날짜의 이름을 가진 파일이 있어야 해요. 예:<year>_<month>_<day>_errors.txt. -
로그 파일을 열어 오류 메시지가 기록됐는지 확인해 주세요:
[2025-02-20 16:23:45.678 - errors] WARNING: ArgumentException: Please include a search term
참고:
SearchCommand나GetArticleCommand가 잡은 네트워크 오류(예: 기기가 오프라인이거나 Wikipedia에 도달할 수 없을 때)도WARNING레벨 항목으로 이 파일에 기록되고, 그 뒤에 명령 사용법이 담긴INFO항목이 이어져요.
- 작업공간 구조가 완성된 프로젝트와 일치하는지 확인해 주세요:
dartpedia/
├── pubspec.yaml # Workspace root configuration
├── cli/
│ ├── bin/
│ │ └── cli.dart # Application entrypoint
│ ├── lib/
│ │ ├── cli.dart # Library exports
│ │ └── src/
│ │ ├── commands/
│ │ │ ├── get_article.dart
│ │ │ └── search.dart
│ │ └── logger.dart # Logging configuration
│ ├── logs/
│ │ └── <date>_errors.txt # Generated error logs
│ └── pubspec.yaml
├── command_runner/
│ ├── lib/
│ │ ├── command_runner.dart
│ │ └── src/
│ │ ├── arguments.dart
│ │ ├── command_runner_base.dart
│ │ ├── console.dart
│ │ ├── exceptions.dart
│ │ └── help_command.dart
│ └── pubspec.yaml
└── wikipedia/
├── lib/
│ ├── wikipedia.dart
│ └── src/
│ ├── api/
│ │ ├── get_article.dart
│ │ ├── search.dart
│ │ └── summary.dart
│ └── model/
│ ├── article.dart
│ ├── search_results.dart
│ ├── summary.dart
│ └── title_set.dart
└── pubspec.yaml
복습 (Review)
배운 내용 (What you accomplished)
이번 강의에서 만들고 배운 내용을 정리해 드릴게요.
- logging 패키지 추가하고 배우기: 의존성에
logging을 추가하고 CLI에 import했어요. 이 패키지는 구성 가능한 심각도 레벨로 구조화된 로깅을 위한Logger,Level,LogRecord클래스를 제공해요. - 로그 레벨과 파일 출력 구성하기:
initFileLogger()를 만들어logs/디렉터리의 타임스탬프 파일로 계층적 로깅을 설정했어요. 이렇게 하면 이후에 애플리케이션 동작을 검토할 수 있어요. - CLI 명령에 로깅 통합하기: 의존성 주입을 통해 로거를 명령에 전달한 다음,
logger.warning(),logger.severe(),logger.info()로 관련 컨텍스트와 함께 다른 레벨의 오류를 기록했어요. 이 구성은 개발과 프로덕션 모두에서 문제를 더 쉽게 디버깅하고 필터링하는 데 도움을 줘요.
퀴즈 (Quiz)
이해도를 확인해 봐요 (Check your understanding)
Q1. Dart에서 logging 패키지의 목적은 무엇인가요?
- 애플리케이션에서 이벤트와 오류를 기록하는 방법을 제공하는 것. — 맞아요!
logging패키지는 애플리케이션 실행 중 이벤트, 경고, 오류, 그리고 다른 메시지를 기록하는 유연한 시스템을 제공해요. - HTTP 요청을 처리하는 것. — 아니에요. HTTP 요청은
package:http가 처리해요.logging패키지는 다른 목적을 가져요. - 프로젝트에서 의존성을 관리하는 것. — 아니에요. 의존성은
pubspec.yaml에서 관리해요.logging패키지는 프로젝트 설정이 아니라 런타임 동작에 관한 거예요. - 명령줄 인터페이스를 만드는 것. — 아니에요. CLI 생성은
args같은 패키지나 사용자 코드를 사용해요.logging패키지는 애플리케이션의 다른 측면을 도와줘요.
Q2. hierarchicalLoggingEnabled = true; 줄은 무엇을 하나요?
- 로거가 자식 로거의 이벤트를 포착할 수 있게 해요. — 맞아요! 계층적 로깅을 활성화하면 부모 로거가 자식 로거의 이벤트를 받아 처리할 수 있어요.
- 계층적 파일 시스템으로 로깅을 활성화해요. — 아니에요. 이 설정은 파일 시스템이나 로그가 저장되는 위치에 영향을 주지 않아요. "계층적(hierarchical)"은 완전히 다른 것을 가리켜요.
- 콘솔 로깅을 비활성화해요. — 아니에요. 콘솔 출력은 리스너가 제어하지, 이 설정이 제어하지 않아요. 이 설정은 로거들이 서로 상호작용하는 방식에 영향을 줘요.
- HTTP 요청 로깅을 활성화해요. — 아니에요. HTTP 로깅은 HTTP 코드에서 특정 구성이 필요해요. 이 설정은 로깅 시스템의 내부 구조에 영향을 줘요.
Q3. 이 강의는 logger.severe(), logger.warning(), logger.info()를 사용해요. 그냥 print()를 쓰는 대신 다른 로그 레벨을 쓰는 이유는 무엇인가요?
- 심각도별로 로그를 필터링할 수 있어요. 개발 중에는 모든 것을 보면서 프로덕션에서는 경고와 오류만 보여 줄 수 있어요. — 맞아요! 로그 레벨로 임계값을 설정할 수 있어요. 프로덕션에서는 경고 이상만 기록하고, 개발에서는 info와 debug 메시지까지 볼 수 있어요.
- 다른 레벨이 콘솔에서 다른 색을 사용해요. — 아니에요. 색상은 표현 방식의 선택이지 핵심 이유가 아니에요. 실제 이점은 겉모습보다 더 근본적이에요.
- 각 레벨이 자동으로 다른 파일에 기록돼요. — 아니에요. 파일 대상은 별도로 구성해야 해요. 레벨이 자동으로 로그가 저장되는 곳을 결정하지 않아요.
- Dart에서 레벨 사용이 필수예요.
print()는 프로덕션에서 작동하지 않아요. — 아니에요.print()는 Dart 어디서든 작동해요. 레벨은 선택이지만print()가 제공할 수 없는 중요한 이점을 줘요.