종속성 주입 지침Dependency injection guidelines

이 문서에서는 .NET 애플리케이션에서 종속성 주입을 구현하기 위한 일반적인 지침 및 모범 사례를 제공합니다.This article provides general guidelines and best practices for implementing dependency injection in .NET applications.

종속성 주입을 위한 서비스 디자인Design services for dependency injection

종속성 주입을 위한 서비스를 디자인하는 경우When designing services for dependency injection:

  • 상태 저장 정적 클래스 및 멤버를 사용하지 마세요.Avoid stateful, static classes and members. 대신 싱글톤 서비스를 사용하도록 앱을 설계하여 전역 상태를 만들지 않도록 합니다.Avoid creating global state by designing apps to use singleton services instead.
  • 서비스 내의 종속 클래스를 직접 인스턴스화하지 마세요.Avoid direct instantiation of dependent classes within services. 직접 인스턴스화는 코드를 특정 구현에 결합합니다.Direct instantiation couples the code to a particular implementation.
  • 서비스를 작고 잘 구성되고 쉽게 테스트할 수 있도록 만듭니다.Make services small, well-factored, and easily tested.

클래스에 주입된 종속성이 많은 경우 클래스가 역할이 너무 많고 SRP(단일 책임 원칙)을 위반하는 것일 수 있습니다.If a class has many injected dependencies, it might be a sign that the class has too many responsibilities and violates the Single Responsibility Principle (SRP). 해당 책임 몇 가지를 새로운 클래스로 이동하여 클래스를 리팩터링해 보세요.Attempt to refactor the class by moving some of its responsibilities into new classes.

서비스 삭제Disposal of services

컨테이너는 자신이 만든 형식을 정리하며 IDisposable 인스턴스에서 Dispose를 호출합니다.The container is responsible for cleanup of types it creates, and calls Dispose on IDisposable instances. 개발자는 컨테이너에서 확인된 서비스는 삭제해서는 안 됩니다.Services resolved from the container should never be disposed by the developer. 형식 또는 팩터리가 싱글톤으로 등록된 경우 컨테이너에서 싱글톤을 자동으로 삭제합니다.If a type or factory is registered as a singleton, the container disposes the singleton automatically.

다음 예제에서는 서비스가 서비스 컨테이너에 의해 만들어지고 자동으로 삭제됩니다.In the following example, the services are created by the service container and disposed automatically:

using System;

namespace ConsoleDisposable.Example
{
    public class TransientDisposable : IDisposable
    {
        public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
    }
}

위의 삭제 가능한 형식은 임시 수명을 갖도록 만들어진 것입니다.The preceding disposable is intended to have a transient lifetime.

using System;

namespace ConsoleDisposable.Example
{
    public class ScopedDisposable : IDisposable
    {
        public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
    }
}

위의 삭제 가능한 형식은 범위가 지정된 수명을 갖도록 만들어진 것입니다.The preceding disposable is intended to have a scoped lifetime.

using System;

namespace ConsoleDisposable.Example
{
    public class SingletonDisposable : IDisposable
    {
        public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
    }
}

위의 삭제 가능한 형식은 싱글톤 수명을 갖도록 만들어진 것입니다.The preceding disposable is intended to have a singleton lifetime.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ConsoleDisposable.Example
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using IHost host = CreateHostBuilder(args).Build();

            ExemplifyDisposableScoping(host.Services, "Scope 1");
            Console.WriteLine();

            ExemplifyDisposableScoping(host.Services, "Scope 2");
            Console.WriteLine();

            await host.RunAsync();
        }

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                    services.AddTransient<TransientDisposable>()
                            .AddScoped<ScopedDisposable>()
                            .AddSingleton<SingletonDisposable>());

        static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
        {
            Console.WriteLine($"{scope}...");

            using IServiceScope serviceScope = services.CreateScope();
            IServiceProvider provider = serviceScope.ServiceProvider;

            _ = provider.GetRequiredService<TransientDisposable>();
            _ = provider.GetRequiredService<ScopedDisposable>();
            _ = provider.GetRequiredService<SingletonDisposable>();
        }
    }

디버그 콘솔은 실행 후 다음 샘플 출력을 보여 줍니다.The debug console shows the following sample output after running:

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

서비스 컨테이너에서 만들지 않은 서비스Services not created by the service container

다음 코드를 살펴보세요.Consider the following code:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new ExampleService());

    // ...
}

위의 코드에서In the preceding code:

  • ExampleService 인스턴스가 서비스 컨테이너에서 만들어지지 않았습니다.The ExampleService instance is not created by the service container.
  • 프레임워크가 서비스를 자동으로 삭제하지 않습니다.The framework does not dispose of the services automatically.
  • 개발자가 서비스 삭제를 담당합니다.The developer is responsible for disposing the services.

임시 및 공유 인스턴스에 대한 IDisposable 지침IDisposable guidance for Transient and shared instances

임시적인 제한 수명Transient, limited lifetime

시나리오Scenario

앱에는 다음 시나리오 중 하나에 대해 임시 수명으로 IDisposable 인스턴스가 필요합니다.The app requires an IDisposable instance with a transient lifetime for either of the following scenarios:

  • 인스턴스가 루트 범위(루트 컨테이너)에서 확인됩니다.The instance is resolved in the root scope (root container).
  • 범위가 끝나기 전에 인스턴스를 삭제해야 합니다.The instance should be disposed before the scope ends.

해결 방법Solution

부모 범위 밖에서 인스턴스를 생성하려면 팩터리 패턴을 사용합니다.Use the factory pattern to create an instance outside of the parent scope. 이 경우 앱에는 일반적으로 최종 형식의 생성자를 직접 호출하는 Create 메서드가 있습니다.In this situation, the app would generally have a Create method that calls the final type's constructor directly. 최종 형식에 다른 종속성이 있는 경우 팩터리는 다음을 수행할 수 있습니다.If the final type has other dependencies, the factory can:

공유 인스턴스 및 제한 수명Shared instance, limited lifetime

시나리오Scenario

앱은 여러 서비스에서 공유 IDisposable 인스턴스가 필요하지만 IDisposable 인스턴스는 수명이 제한되어 있어야 합니다.The app requires a shared IDisposable instance across multiple services, but the IDisposable instance should have a limited lifetime.

해결 방법Solution

인스턴스를 범위가 지정된 수명으로 등록합니다.Register the instance with a scoped lifetime. IServiceScopeFactory.CreateScope을 사용하여 새 IServiceScope를 만듭니다.Use IServiceScopeFactory.CreateScope to create a new IServiceScope. 범위의 IServiceProvider를 사용하여 필요한 서비스를 가져옵니다.Use the scope's IServiceProvider to get required services. 더 이상 필요하지 않은 범위를 삭제합니다.Dispose the scope when it's no longer needed.

일반 IDisposable 지침General IDisposable guidelines

  • 임시 수명에 IDisposable 인스턴스를 등록하지 마세요.Don't register IDisposable instances with a transient lifetime. 대신 팩터리 패턴을 사용합니다.Use the factory pattern instead.
  • 루트 범위에서 임시 또는 범위가 지정된 수명으로 IDisposable 인스턴스를 확인하지 마세요.Don't resolve IDisposable instances with a transient or scoped lifetime in the root scope. 앱이 IServiceProvider를 생성/재생성 및 삭제하는 경우만 예외이지만, 이상적인 패턴이 아닙니다.The only exception to this is if the app creates/recreates and disposes IServiceProvider, but this isn't an ideal pattern.
  • DI를 통한 IDisposable 종속성 수신은 수신자가 자체적으로 IDisposable를 구현할 필요가 없습니다.Receiving an IDisposable dependency via DI doesn't require that the receiver implement IDisposable itself. IDisposable 종속성의 수신자는 해당 종속성에서 Dispose를 호출하지 않아야 합니다.The receiver of the IDisposable dependency shouldn't call Dispose on that dependency.
  • 범위를 사용하여 서비스 수명을 제어합니다.Use scopes to control the lifetimes of services. 범위는 계층적이지 않으며 범위 간 특수 연결이 없습니다.Scopes aren't hierarchical, and there's no special connection among scopes.

리소스 정리에 대한 자세한 내용은 Dispose 메서드 구현또는 DisposeAsync 메서드 구현을 참조하세요.For more information on resource cleanup, see Implement a Dispose method, or Implement a DisposeAsync method. 또한 리소스 정리와 관련하여 컨테이너가 삭제 가능한 임시 서비스를 캡처 시나리오를 살펴보세요.Additionally, consider the Disposable transient services captured by container scenario as it relates to resource cleanup.

기본 서비스 컨테이너 바꾸기Default service container replacement

기본 제공 서비스 컨테이너는 프레임워크 및 대부분의 소비자 앱의 요구를 충족하기 위한 것입니다.The built-in service container is designed to serve the needs of the framework and most consumer apps. 다음과 같이 지원하지 않는 특정 기능이 필요하지 않는 한 기본 제공 컨테이너를 사용하는 것이 좋습니다.We recommend using the built-in container unless you need a specific feature that it doesn't support, such as:

  • 속성 삽입Property injection
  • 이름에 기반한 삽입Injection based on name
  • 자식 컨테이너Child containers
  • 사용자 지정 수명 관리Custom lifetime management
  • 초기화 지연에 대한 Func<T> 지원Func<T> support for lazy initialization
  • 규칙 기반 등록Convention-based registration

ASP.NET Core 앱에서 사용할 수 있는 타사 컨테이너는 다음과 같습니다.The following third-party containers can be used with ASP.NET Core apps:

스레드로부터의 안전성Thread safety

스레드로부터 안전한 싱글톤 서비스를 만듭니다.Create thread-safe singleton services. 싱글톤 서비스가 Transient 서비스에 대한 종속성을 갖는 경우 Transient 서비스는 싱글톤에서 사용되는 방식에 따라 스레드 보안이 필요할 수 있습니다.If a singleton service has a dependency on a transient service, the transient service may also require thread safety depending on how it's used by the singleton.

AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>)의 두 번째 인수와 같은 싱글톤 서비스의 팩터리 메서드는 스레드로부터 안전하지 않아도 됩니다.The factory method of a singleton service, such as the second argument to AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), doesn't need to be thread-safe. 형식(static) 생성자와 같이 이 메서드는 단일 스레드에서 한 번만 호출됩니다.Like a type (static) constructor, it's guaranteed to be called only once by a single thread.

권장 사항Recommendations

  • async/awaitTask 기반 서비스 확인은 지원되지 않습니다.async/await and Task based service resolution isn't supported. C#은 비동기 생성자를 지원하지 않으므로, 서비스를 동기식으로 확인한 후 비동기 메서드를 사용합니다.Because C# doesn't support asynchronous constructors, use asynchronous methods after synchronously resolving the service.
  • 데이터 및 구성을 서비스 컨테이너에 직접 저장하지 마세요.Avoid storing data and configuration directly in the service container. 예를 들어 사용자의 쇼핑 카트는 일반적으로 서비스 컨테이너에 추가하지 말아야 합니다.For example, a user's shopping cart shouldn't typically be added to the service container. 구성은 옵션 패턴을 사용해야 합니다.Configuration should use the options pattern. 마찬가지로 다른 개체에 대한 액세스를 허용하기 위해서만 존재하는 “데이터 보유자” 개체를 사용하지 마세요.Similarly, avoid "data holder" objects that only exist to allow access to another object. DI를 통해 실제 항목을 요청하는 것이 좋습니다.It's better to request the actual item via DI.
  • 서비스에 정적 액세스를 사용하지 마십시오.Avoid static access to services. 예를 들어 다른 곳에 사용하기 위해 IApplicationBuilder.ApplicationServices를 정적 필드 또는 속성으로 캡처하지 마세요.For example, avoid capturing IApplicationBuilder.ApplicationServices as a static field or property for use elsewhere.
  • DI 팩터리를 빠르고 동기식으로 유지하세요.Keep DI factories fast and synchronous.
  • ‘서비스 로케이터 패턴’을 사용하지 마세요.Avoid using the service locator pattern. 예를 들어 DI를 대신 사용할 수 있는 경우 서비스 인스턴스를 가져오기 위해 GetService를 호출하지 마세요.For example, don't invoke GetService to obtain a service instance when you can use DI instead.
  • 피해야 하는 또 다른 서비스 로케이터 변형은 런타임에 종속성을 해결하는 팩터리를 주입하는 것입니다.Another service locator variation to avoid is injecting a factory that resolves dependencies at runtime. 이러한 두 가지 방법 모두 제어 반전 전략을 혼합합니다.Both of these practices mix Inversion of Control strategies.
  • ConfigureServices에서 BuildServiceProvider를 호출하지 마세요.Avoid calls to BuildServiceProvider in ConfigureServices. 일반적으로 BuildServiceProvider는 개발자가 ConfigureServices에서 서비스를 해결하려는 경우 호출합니다.Calling BuildServiceProvider typically happens when the developer wants to resolve a service in ConfigureServices.
  • 컨테이너가 삭제를 위해 삭제 가능한 임시 서비스를 캡처합니다.Disposable transient services are captured by the container for disposal. 따라서 최상위 컨테이너에서 해결할 경우 메모리 누수가 발생할 수 있습니다.This can turn into a memory leak if resolved from the top-level container.
  • 범위 유효성 검사를 사용하여 범위가 지정된 서비스를 캡처하는 싱글톤이 앱에 없는지 확인합니다.Enable scope validation to make sure the app doesn't have singletons that capture scoped services. 자세한 내용은 범위 유효성 검사를 참조하세요.For more information, see Scope validation.

모든 권장 사항과 마찬가지로, 권장 사항을 무시해야 하는 상황이 발생할 수 있습니다.Like all sets of recommendations, you may encounter situations where ignoring a recommendation is required. 예외는 드물게 발생하며 대부분 프레임워크 자체 내에서 특별한 경우에만 발생합니다.Exceptions are rare, mostly special cases within the framework itself.

DI는 정적/전역 개체 액세스 패턴의 ‘대안’입니다.DI is an alternative to static/global object access patterns. 고정 개체 액세스와 함께 사용할 경우 DI의 장점을 실현할 수 없습니다.You may not be able to realize the benefits of DI if you mix it with static object access.

안티 패턴 예제Example anti-patterns

이 문서의 지침 외에 ‘지양 해야 할’ 몇 가지 안티 패턴이 있습니다.In addition to the guidelines in this article, there are several anti-patterns you should avoid. 이러한 안티 패턴 중 일부는 런타임 자체를 개발하면서 배웁니다.Some of these anti-patterns are learnings from developing the runtimes themselves.

경고

다음은 안티 패턴 예제입니다. 코드를 복사하지 ‘말고’ 이러한 패턴을 사용하지 ‘않으며’ 어떤 경우에도 이러한 패턴을 피해야 합니다.These are example anti-patterns, do not copy the code, do not use these patterns, and avoid these patterns at all costs.

컨테이너가 삭제 가능한 임시 서비스를 캡처Disposable transient services captured by container

IDisposable을 구현하는 ‘임시’ 서비스를 등록하는 경우 기본적으로 DI 컨테이너는 이러한 참조를 유지하고 컨테이너에서 확인된 경우 애플리케이션이 중지하여 컨테이너가 삭제될 때까지 또는 범위에서 확인된 경우 범위가 삭제될 때까지는 Dispose()하지 않습니다.When you register Transient services that implement IDisposable, by default the DI container will hold onto these references, and not Dispose() of them until the container is disposed when application stops if they were resolved from the container, or until the scope is disposed if they were resolved from a scope. 따라서 컨테이너 수준에서 확인할 경우 메모리 누수가 발생할 수 있습니다.This can turn into a memory leak if resolved from container level.

static void TransientDisposablesWithoutDispose()
{
    var services = new ServiceCollection();
    services.AddTransient<ExampleDisposable>();
    ServiceProvider serviceProvider = services.BuildServiceProvider();

    for (int i = 0; i < 1000; ++ i)
    {
        _ = serviceProvider.GetRequiredService<ExampleDisposable>();
    }

    // serviceProvider.Dispose();
}

위의 안티 패턴에서는 1,000개의 ExampleDisposable 개체가 인스턴스화되고 루팅됩니다.In the preceding anti-pattern, 1,000 ExampleDisposable objects are instantiated and rooted. 이들은 serviceProvider 인스턴스가 삭제될 때까지 삭제되지 않습니다.They will not be disposed of until the serviceProvider instance is disposed.

메모리 누수를 디버깅하는 방법에 대한 자세한 내용은 .NET에서 메모리 누수 디버깅을 참조하세요.For more information on debugging memory leaks, see Debug a memory leak in .NET.

비동기 DI 팩터리에서 교착 상태가 발생할 수 있음Async DI factories can cause deadlocks

‘DI 팩터리’라는 용어는 Add{LIFETIME}을 호출할 때 존재하는 오버로드 메서드를 의미합니다.The term "DI factories" refers to the overload methods that exist when calling Add{LIFETIME}. Func<IServiceProvider, T>를 허용하는 오버로드가 있습니다. 여기서 T는 등록할 서비스이고, 매개 변수 이름은 implementationFactory입니다.There are overloads accepting a Func<IServiceProvider, T> where T is the service being registered, and the parameter is named implementationFactory. implementationFactory는 람다 식, 로컬 함수 또는 메서드로 제공될 수 있습니다.The implementationFactory can be provided as a lambda expression, local function, or method. 팩터리가 비동기식이고 Task<TResult>.Result를 사용하는 경우 교착 상태가 발생합니다.If the factory is asynchronous, and you use Task<TResult>.Result, this will cause a deadlock.

static void DeadLockWithAsyncFactory()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>(implementationFactory: provider =>
    {
        Bar bar = GetBarAsync(provider).Result;
        return new Foo(bar);
    });

    services.AddSingleton<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    _ = serviceProvider.GetRequiredService<Foo>();
}

위의 코드에서는 implementationFactory에 본문이 Task<Bar> 반환 메서드에서 Task<TResult>.Result를 호출하는 람다 식이 지정됩니다.In the preceding code, the implementationFactory is given a lambda expression where the body calls Task<TResult>.Result on a Task<Bar> returning method. 이로 인해 ‘교착 상태가 발생’합니다.This causes a deadlock. GetBarAsync 메서드는 단순히 Task.Delay를 사용하여 비동기 작업을 에뮬레이트한 다음 GetRequiredService<T>(IServiceProvider)를 호출합니다.The GetBarAsync method simply emulates an asynchronous work operation with Task.Delay, and then calls GetRequiredService<T>(IServiceProvider).

static async Task<Bar> GetBarAsync(IServiceProvider serviceProvider)
{
    // Emulate asynchronous work operation
    await Task.Delay(1000);

    return serviceProvider.GetRequiredService<Bar>();
}

비동기 지침에 대한 자세한 내용은 비동기 프로그래밍: 중요 정보 및 조언을 참조하세요.For more information on asynchronous guidance, see Asynchronous programming: Important info and advice. 교착 상태 디버깅에 대한 자세한 내용은 .NET에서 교착 상태 디버깅을 참조하세요.For more information debugging deadlocks, see Debug a deadlock in .NET.

이 안티 패턴을 실행 중이고 교착 상태가 발생하는 경우 Visual Studio의 병렬 스택 창에서 두 개의 스레드가 대기하는 것을 볼 수 있습니다.When you're running this anti-pattern and the deadlock occurs, you can view the two threads waiting from Visual Studio's Parallel Stacks window. 자세한 내용은 병렬 스택 창에서 스레드 및 작업 보기를 참조하세요.For more information, see View threads and tasks in the Parallel Stacks window.

조임 종속성Captive dependency

‘조임 종속성’Mark Seeman이 만든 용어로, 수명이 긴 서비스에서 수명이 짧은 서비스를 보유하는 잘못된 서비스 수명 구성을 의미합니다.The term "captive dependency" was coined by Mark Seeman, and refers to the misconfiguration of service lifetimes, where a longer-lived service holds a shorter-lived service captive.

static void CaptiveDependency()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    // Enable scope validation
    // using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);

    _ = serviceProvider.GetRequiredService<Foo>();

위의 코드에서 Foo는 싱글톤으로 등록되고 Bar는 범위가 지정되는데, 표면적으로는 유효한 것으로 보입니다.In the preceding code, Foo is registered as a singleton and Bar is scoped - which on the surface seems valid. 그러나 Foo의 구현을 생각해보세요.However, consider the implementation of Foo.

namespace DependencyInjection.AntiPatterns
{
    public class Foo
    {
        public Foo(Bar bar)
        {
        }
    }
}

Foo 개체에는 Bar 개체가 필요하며, Foo는 싱글톤이고 Bar는 범위가 지정되므로 이는 잘못된 구성입니다.The Foo object requires a Bar object, and since Foo is a singleton, and Bar is scoped - this is a misconfiguration. Foo가 한 번만 인스턴스화되고 해당 수명 동안 Bar를 유지해야 하는데 이 수명이 Bar의 의도된 범위가 지정된 수명보다 길기 때문입니다.As is, Foo would only be instantiated once, and it would hold onto Bar for its lifetime, which is longer than the intended scoped lifetime of Bar. validateScopes: trueBuildServiceProvider(IServiceCollection, Boolean)에 전달하여 범위 유효성 검사를 고려해야 합니다.You should consider validating scopes, by passing validateScopes: true to the BuildServiceProvider(IServiceCollection, Boolean). 범위의 유효성을 검사할 때 "싱글톤 'Foo'에서 범위가 지정된 서비스 'Bar'를 사용할 수 없음"과 비슷한 메시지가 포함된 InvalidOperationException이 발생합니다.When you validate the scopes, you'd get an InvalidOperationException with a message similar to "Cannot consume scoped service 'Bar' from singleton 'Foo'.".

자세한 내용은 범위 유효성 검사를 참조하세요.For more information, see Scope validation.

범위가 지정된 서비스를 싱글톤으로Scoped service as singleton

범위가 지정된 서비스를 사용할 때 범위를 만들지 않거나 기존 범위 안에 들어가는 경우 해당 서비스는 싱글톤이 됩니다.When using scoped services, if you're not creating a scope or within an existing scope - the service becomes a singleton.

static void ScopedServiceBecomesSingleton()
{
    var services = new ServiceCollection();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);
    using (IServiceScope scope = serviceProvider.CreateScope())
    {
        // Correctly scoped resolution
        Bar correct = scope.ServiceProvider.GetRequiredService<Bar>();
    }

    // Not within a scope, becomes a singleton
    Bar avoid = serviceProvider.GetRequiredService<Bar>();
}

위의 코드에서는 BarIServiceScope 내에서 검색되며 이는 올바른 패턴입니다.In the preceding code, Bar is retrieved within an IServiceScope, which is correct. 안티 패턴은 범위 밖에서 Bar를 검색하는 것입니다. 예제 검색이 잘못된 패턴임을 표시하기 위해 변수의 이름이 avoid로 지정되어 있습니다.The anti-pattern is the retrieval of Bar outside of the scope, and the variable is named avoid to show which example retrieval is incorrect.

참고 항목See also