메일 클라이언트의 다중 스레드 지원
다음과 같은 메일 클라이언트 ImapClient 및 Pop3Client 사용자가 멀티스레드 환경에서 이를 사용할 수 있도록 합니다. 메일 클라이언트는 서버와 하나 이상 연결을 가질 수 있습니다. 클라이언트 내부에서 연결 집합을 관리하기 위해 연결 풀을 사용합니다. 동시에 생성·사용 가능한 연결 수는 CredentialsByHostClient.MaxConnectionsPerServer 속성에 의해 제한됩니다. 이 속성은 1 이상의 값으로 설정할 수 있으며, 기본값은 10입니다. 연결에 대해 멀티스레드 작업을 지원하기 위해 명령 큐가 구현되었습니다. 명령은 Noop, Authenticate 등 프로토콜에서 정의된 가장 간단한 작업을 구현합니다. 사용자는 사용 가능한 연결 수보다 더 많은 명령을 실행하려 할 수 있지만, 실제로 실행되는 것은 클라이언트가 해당 작업을 위한 연결을 생성할 수 있을 때만입니다.
멀티스레드 환경에서 메일 클라이언트 사용 방법
이메일 클라이언트는 다음과 같은 동작을 합니다:
1. MaxConnectionsPerServer = 1인 경우, 클라이언트는 1개의 연결을 생성하고 인증 및 인가를 수행합니다. 이 연결은 클라이언트가 해제될 때까지 작업 상태를 유지합니다. 서로 다른 스레드의 모든 작업은 기본 연결에 배치된 하나의 명령 큐로 전달됩니다.
2. MaxConnectionsPerServer > 1인 경우, 클라이언트는 필요량만큼 연결을 생성하고 각 연결에 대해 인증 및 인가를 수행합니다. 하나의 연결은 기본 연결로 예약됩니다. 이 연결은 클라이언트가 해제될 때까지 작업 상태를 유지합니다. 다른 연결은 필요에 따라 생성·해제됩니다. 이러한 연결의 최대 수는 MaxConnectionsPerServer 속성으로 정의되며, 예를 들어 MaxConnectionsPerServer = 2이면 하나는 기본 연결로 예약되고, 두 번째 연결은 다른 스레드에서 실행되는 작업을 위한 추가 연결로 사용됩니다. MaxConnectionsPerServer = 3이면 첫 번째 연결이 기본 연결로 예약되고, 두 개의 추가 연결이 다른 스레드 작업에 사용됩니다. 새로운 스레드에서 연결 요청이 들어오고 모든 연결이 사용 중이면, 사용 중인 연결 수가 감소할 때까지 클라이언트는 대기합니다. 이는 연결을 올바르게 해제하는 것이 왜 중요한지를 명확히 하는 매우 중요한 상황입니다.
예제
사용자는 여러 방법으로 다른 스레드에서 작업을 실행할 수 있습니다. 이는 두 가지 유형으로 구분됩니다:
1. 사용자는 클라이언트에 정의된 비동기(Begin/End) 메서드를 사용합니다. 이 경우 메일 클라이언트는 필요에 따라 새로운 스레드를 시작합니다. 클라이언트에는 작업 큐가 구현되어 있는데(연결의 명령 큐와 혼동하지 마세요), 연결이 사용 가능하면 작업이 실행됩니다. 사용 중인 연결 수가 제한값보다 적어지면 클라이언트는 새 연결을 만들고 현재 작업을 위한 스레드를 생성하여 해당 작업을 실행합니다. 비동기 작업 사용 예시:
// Create an imapclient with host, user and password
ImapClient client = new ImapClient();
client.setHost("domain.com");
client.setUsername("username");
client.setPassword("password");
client.selectFolder("InBox");
ImapMessageInfoCollection messages = client.listMessages();
IAsyncResult res1 = client.beginFetchMessage(messages.get_Item(0).getUniqueId());
IAsyncResult res2 = client.beginFetchMessage(messages.get_Item(1).getUniqueId());
MailMessage msg1 = client.endFetchMessage(res1);
MailMessage msg2 = client.endFetchMessage(res2);
2. 사용자는 Thread, ExecutorService 등과 같은 객체 또는 이 목적을 위한 기타 객체를 사용하여 스레드를 생성할 수 있습니다. 또한 써드파티 코드에서 생성된 스레드를 사용할 수도 있습니다. 이때 클라이언트는 두 가지 동작 모델을 가집니다
a. 사용자가 스레드 내 작업을 위해 추가 연결을 만들지 않은 경우, 해당 스레드의 모든 작업은 명령 큐를 통해 기본 연결로 전송됩니다. 새로운 연결을 만들지 않고 추가 스레드에서 수행되는 작업 예시로, 모든 트랜잭션이 기본 연결을 통해 이루어집니다:
/**
* Creates an executor service with a fixed pool size, that will time
* out after a certain period of inactivity.
*
* @param poolSize The core- and maximum pool size
* @param keepAliveTime The keep alive time
* @param timeUnit The time unit
* @return The executor service
*/
private static ExecutorService createFixedTimeoutExecutorService(
int poolSize, long keepAliveTime, TimeUnit timeUnit)
{
ThreadPoolExecutor e =
new ThreadPoolExecutor(poolSize, poolSize,
keepAliveTime, timeUnit, new LinkedBlockingQueue<Runnable>());
e.allowCoreThreadTimeOut(true);
return e;
}
final List<MailMessage> list = new ArrayList<MailMessage>();
ExecutorService executor = createFixedTimeoutExecutorService(1, 1000, TimeUnit.MILLISECONDS);
executor.execute(new Runnable() {
public void run() {
client.selectFolder("folderName");
ImapMessageInfoCollection messageInfoCol = client.listMessages();
for (ImapMessageInfo messageInfo : messageInfoCol) {
list.add(client.fetchMessage(messageInfo.getUniqueId()));
}
}
});
b. 사용자가 추가 스레드용 새로운 연결을 생성하는 메서드를 실행하면, 새 연결을 허용하도록 연결 할당량 값이 변경될 때까지 해당 스레드가 차단됩니다. 그 후 새 연결이 생성됩니다. 이 연결은 해당 스레드의 모든 작업에 대한 기본 연결로 설정됩니다. 스레드의 모든 작업이 완료된 후 연결을 해제해야 합니다. 새로운 연결을 만들려면 CredentialsByHostClient.CreateConnection 메서드를 사용하십시오. 이 메서드는 IDisposable 인터페이스를 구현하는 객체를 반환합니다. 연결을 해제하려면 Dispose 메서드를 호출해야 합니다. 연결 생성 및 해제는 메일 작업이 실행되는 스레드 내에서 수행되어야 합니다. 메일 클라이언트가 생성된 스레드에서 새로운 연결을 만들려고 하면 오류가 발생하는데, 해당 스레드는 현재 새로운 연결을 생성하는 데 사용할 수 없기 때문입니다. 또한 MaxConnectionsPerServer = 1인 경우 새로운 연결을 만들 수 없습니다. 추가 스레드에서 새로운 연결을 생성하는 코드 예시:
final List<MailMessage> list = new ArrayList<MailMessage>();
ExecutorService executor = createFixedTimeoutExecutorService(1, 1000, TimeUnit.MILLISECONDS);
executor.execute(new Runnable() {
public void run() {
IConnection connection = client.createConnection();
try {
client.selectFolder(connection, "FolderName");
ImapMessageInfoCollection messageInfoCol = client.listMessages(connection);
for (ImapMessageInfo messageInfo : messageInfoCol)
list.add(client.fetchMessage(connection, messageInfo.getUniqueId()));
} finally {
connection.dispose();
}
}
});
권장 사항
사용자가 모든 명령을 기본 연결에 보낼 경우, 서로 다른 스레드의 명령이 섞이는 상황이 발생할 수 있다는 점에 유의해야 합니다. 사용자는 어떤 명령이 순서에 의존하는지 이해하고, 이러한 명령들의 동기화를 위해 조치를 취해야 합니다. 또한 다른 세션(IMAP/POP3)에서 명령을 실행하는 가능성도 고려해야 합니다. FetchMessage, AppendMessage, Send와 같은 가장 시간이 많이 소요되는 작업은 새 스레드와 새 연결을 사용하여 수행하는 것이 바람직합니다. 하지만 Delete와 같은 빠른 작업은 기본 연결에서 수행하는 것이 적합합니다. 새 연결 초기화 자체가 충분히 시간이 소요되는 작업임을 기억하십시오.