Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
20344ab
refactor: Request 및 Response 구성 클래스 분리
helenason Sep 12, 2024
8614dad
refactor: controller 및 mapper 클래스 분리
helenason Sep 12, 2024
4f6bb60
chore: 패키지 구조 변경
helenason Sep 12, 2024
8ac813b
test: helper 패키지
helenason Sep 12, 2024
d9853da
refactor: helper 클래스 싱글톤 패턴 적용
helenason Sep 12, 2024
c3c2714
refactor: 응답 바디 파싱 로직 수정
helenason Sep 12, 2024
0fce7f5
refactor: SessionManager 싱글톤 패턴 적용
helenason Sep 12, 2024
417fad9
style: 불필요한 주석 제거
helenason Sep 12, 2024
fa79e2c
refactor: 상수 처리
helenason Sep 12, 2024
789ed95
refactor: 파일 경로 찾는 메서드 분리
helenason Sep 12, 2024
3c7cc1f
test: helper 패키지
helenason Sep 13, 2024
5d80027
test: response 패키지
helenason Sep 13, 2024
bf5c2c6
feat: thread pool 적용
helenason Sep 13, 2024
4f644b9
feat: session 동시성 컬렉션 적용
helenason Sep 13, 2024
407c475
test: thread 학습테스트 0단계
helenason Sep 13, 2024
dd1fa88
test: thread 학습테스트 1단계
helenason Sep 13, 2024
f66a521
test: thread 학습테스트 2단계
helenason Sep 13, 2024
5aebdbf
refactor: 헤더 키 enum 클래스 분리
helenason Sep 16, 2024
021b899
style: 불필요한 주석 제거
helenason Sep 16, 2024
000b871
refactor: LoginCookie Optional로 감싸 반환
helenason Sep 16, 2024
3a6ad60
feat: 유효하지 않은 경로 입력 시 404 페이지 반환
helenason Sep 16, 2024
697e51d
refactor: 로그인 성공 여부에 따른 행위 메서드 분리
helenason Sep 16, 2024
2516bc7
refactor: findFirst -> findAny
helenason Sep 16, 2024
0e482c6
chore: 패키지 위치 설정
helenason Sep 16, 2024
52f0b32
refactor: SessionManager 저장 위치 변경
helenason Sep 16, 2024
9690f33
chore: 테스트 패키지 위치 설정
helenason Sep 16, 2024
ad8e324
test: Http11Processor
helenason Sep 16, 2024
8ebc973
refactor: 불필요한 출력문 제거
helenason Sep 16, 2024
8aed891
Merge branch 'helenason' into step3
helenason Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions study/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ handlebars:

server:
tomcat:
accept-count: 1
max-connections: 1
accept-count: 10 # 요청의 대기열(queue) 길이 -> http call count 관련
max-connections: 2 # 동시에 처리 가능한 서버의 연결 개수 -> 테스트 결과와 관련

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하루 10만건 정도의 요청이 들어오는 서비스라면 스레드 개수를 얼마나 잡아보면 좋을까요? 같이 고민해볼까요? 🤭

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 고민 거리 주셔서 감사해요! 😃

알아보니 적정 스레드 수는 CPU 코어 수 × (1 + (대기 시간 / 처리 시간))로 계산하곤 하더라구요. CPU 코어의 수와 요청 처리 시간 및 대기 시간에 따라 설정해야 할 스레드 개수가 달라질 것 같아요! 또, 하루 10만건의 요청이 어느 분포로 들어오는지도 측정해본 후 개수를 설정해볼 것 같아요. 동시에 들어오는 요청의 수가 중요하다고 생각되어서요! 켈리는 어떻게 생각하시나요? 혼자서 나름 고민하다보니 켈리의 생각도 궁금해지네요 🤔

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 에버의 의견에 동의합니다! 하루 10만건이 특정 시간대에 몰려서 오는건지, 혹은 하루동안 균일하게 요청이 들어오는건지에 따라 적정 스레드 풀 계수를 산정할거 같아요! 물론 저희가 취업전에 이정도 트래픽을 받아볼 일이 없..겠지만..! 가끔 이런 공상을 해보는것도 재밌더라구요 🤭

threads:
min-spare: 2
max: 2
min-spare: 2 # 스레드풀에서 최소로 유지할 스레드 개수
max: 2 # 스레드풀에서 사용할 최대 스레드 개수 (== 동시 처리 가능한 최대 개수)
compression:
enabled: true
min-response-size: 10
27 changes: 17 additions & 10 deletions study/src/test/java/thread/stage0/SynchronizationTest.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package thread.stage0;

import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* 다중 스레드 환경에서 두 개 이상의 스레드가 변경 가능한(mutable) 공유 데이터를 동시에 업데이트하면 경쟁 조건(race condition)이 발생한다.
Expand All @@ -18,6 +21,8 @@
*/
class SynchronizationTest {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 테스트에서 사용된 synchronized는 정확히 어떤 역할을 해줄까요?!

Copy link
Member Author

@helenason helenason Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하나의 스레드가 calculate 로직을 수행하고 있으면 다른 스레드가 해당 로직을 수행하지 못하도록 합니다. 즉, 여러 스레드가 공유하고 있는 sum 필드에 동시에 접근하지 못하게 함으로써, 동시성에 의한 예상치 못한 에러를 방지합니다!


private static final Logger log = LoggerFactory.getLogger(SynchronizationTest.class);

/**
* 테스트가 성공하도록 SynchronizedMethods 클래스에 동기화를 적용해보자.
* synchronized 키워드에 대하여 찾아보고 적용하면 된다.
Expand All @@ -27,12 +32,12 @@ class SynchronizationTest {
*/
@Test
void testSynchronized() throws InterruptedException {
var executorService = Executors.newFixedThreadPool(3);
var synchronizedMethods = new SynchronizedMethods();
ExecutorService executorService = Executors.newFixedThreadPool(3);
SynchronizedMethods synchronizedMethods = new SynchronizedMethods();

IntStream.range(0, 1000)
.forEach(count -> executorService.submit(synchronizedMethods::calculate));
executorService.awaitTermination(500, TimeUnit.MILLISECONDS);
.forEach(i -> executorService.submit(synchronizedMethods::calculate)); // 실행 (비동기로 요청 시도)
executorService.awaitTermination(500, TimeUnit.MILLISECONDS); // 정상적인 종료, 혹은 타임아웃 발생 여부 확인 후 대기

assertThat(synchronizedMethods.getSum()).isEqualTo(1000);
}
Expand All @@ -41,15 +46,17 @@ private static final class SynchronizedMethods {

private int sum = 0;

public void calculate() {
public synchronized void calculate() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스레드 동기화는 정확히 어떤 개념일까요? 면접에 자주 등장하는 단골 질문이라는데 에버의 설명이 궁금하네요! 🧑‍💻

Copy link
Member Author

@helenason helenason Sep 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2개 이상의 스레드가 사용되는 환경, 즉 멀티스레드 환경에서, 여러 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제를 방지하기 위해 사용되는 기술입니다. 하나의 스레드가 특정 로직을 수행하고 있을 때 다른 스레드의 요청이 들어올 경우, 해당 로직을 동시에 수행하는 것이 아닌, 하나의 스레드가 일을 마무리할 때까지 기다리도록 하는 방식입니다. 이를 통해 공유 자원의 일관성을 유지하고 race condition을 방지합니다.

스레드 동기화가 무엇인지 추상적으로만 알고 있었는데, 이렇게 정리할 수 있는 기회가 되니 좋네요 :)

log.info("before calculate: {}", sum);
setSum(getSum() + 1);
log.info("after calculate: {}", sum);
}

public int getSum() {
public /*synchronized*/ int getSum() {
return sum;
}

public void setSum(int sum) {
public /*synchronized*/ void setSum(int sum) {
this.sum = sum;
}
}
Expand Down
37 changes: 27 additions & 10 deletions study/src/test/java/thread/stage0/ThreadPoolsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,46 @@ class ThreadPoolsTest {

@Test
void testNewFixedThreadPool() {
final var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
// corePoolSize = 2
// maximumPoolSize = 2
// keepAliveTime = 0

executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
// logWithoutSleep 진행 시 queue에 쌓이지 X => 3, 0

// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 0;
final int expectedQueueSize = 0;
final int expectedPoolSize = 2;
final int expectedQueueSize = 1; // holding tasks 저장

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
assertThat(executor.getPoolSize()).isEqualTo(expectedPoolSize);
assertThat(executor.getQueue().size()).isEqualTo(expectedQueueSize);
}

@Test
void testNewCachedThreadPool() {
final var executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
// corePoolSize = 0
// maximumPoolSize = MAX_VALUE
// keepAliveTime = 60s

executor.submit(logWithSleep("hello cached thread pools"));
executor.submit(logWithSleep("hello cached thread pools"));
executor.submit(logWithSleep("hello cached thread pools"));
// 실험) maximumPoolSize 이상 횟수로 실행시키면? -> OutOfMemoryError

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스레드 개수에 따라 여러 문제가 발생할 수 있기 때문에 적절한 스레드풀의 개수를 맞추는건 아주 중요한 작업이라고해요! 그럼...

  • 스레드가 너무 적을때 어떤 문제가 발생할까?
  • 스레드가 너무 많을때 어떤 문제가 발생할까?
  • 스레드 개수를 늘리거나 혹은 줄일 상황은 어떤 지표를 보고 판단할 수 있을까?
  • 우리가 개발하고 있는 서비스에서 스레드풀 개수를 책정할 지점은 어디일까?

같이 고민 해볼까요? 🤔🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스레드가 너무 적을 때 어떤 문제가 발생할까?

동시에 들어오는 요청의 수보다 스레드의 수가 한없이 적을 경우, 항상 모든 스레드가 점유되어 있기 때문에 스레드의 대기 시간이 길어질 수 있습니다. 또한 대기 중인 스레드가 큐를 모두 채울 경우 일부 요청은 거절될 수 있습니다.

스레드가 너무 많을 때 어떤 문제가 발생할까?

반대로 동시에 들어오는 요청의 수보다 스레드의 수가 한없이 많다면, 쉬고 있는 스레드가 많아져 불필요하게 메모리를 차지하고 있을 수 있습니다.

스레드 개수를 늘리거나 혹은 줄일 상황은 어떤 지표를 보고 판단할 수 있을까?

큐에 쌓이는 요청의 개수를 보고 판단할 수 있을 것 같습니다. 큐에 너무 많은 요청이 쌓이는 경우 스레드 수를 늘려야 할 것이고, 큐에 요청이 전혀 들어오지 않는 경우는 쉬고 있는 스레드가 많다 판단하여 스레드의 수를 줄여야 할 것입니다.

우리가 개발하고 있는 서비스에서 스레드풀 개수를 책정할 지점은 어디일까?

각 HTTP 요청 별로 스레드가 생성되기 때문에 HTTP 요청이 들어오는 Connector 클래스에서 책정하면 될 것 같습니다!


모두 저의 개인적인 생각이므로, 혹시 켈리가 다르게 생각하고 계신다면 공유해주시면 감사하겠습니다 😁


// 올바른 값으로 바꿔서 테스트를 통과시키자.
final int expectedPoolSize = 0;
final int expectedQueueSize = 0;
final long expectedPoolSize = 3;
final long expectedQueueSize = 0;

assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
assertThat(executor.getPoolSize()).isEqualTo(expectedPoolSize);
assertThat(executor.getQueue().size()).isEqualTo(expectedQueueSize);
}

private Runnable logWithSleep(final String message) {
// 1초 후 로깅
return () -> {
try {
Thread.sleep(1000);
Expand All @@ -63,4 +74,10 @@ private Runnable logWithSleep(final String message) {
log.info(message);
};
}

private Runnable logWithoutSleep(final String message) {
return () -> {
log.info(message);
};
}
}
18 changes: 14 additions & 4 deletions study/src/test/java/thread/stage0/ThreadTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ void testExtendedThread() throws InterruptedException {
Thread thread = new ExtendedThread("hello thread");

// 생성한 thread 객체를 시작한다.
thread.start();
// log.info("before start");
thread.start(); // ! run() 실행 !
// log.info("after start");

// thread의 작업이 완료될 때까지 기다린다.
thread.join();
// log.info("before join");
thread.join();
// log.info("after join");
}

/**
Expand All @@ -46,10 +50,16 @@ void testRunnableThread() throws InterruptedException {
Thread thread = new Thread(new RunnableThread("hello thread"));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread객체를 생성시 파라미터로 왜 RunnableThread를 넣어줘야할까요? 파라미터로 들어간 RunnableThread 객체의 인스턴스는 어떤 행위를 해줄까요?

가볍게 찾아보면 재밌을거 같아요! (저 좀 알려주세요! 😎)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Runnable 인터페이스를 구현함으로써, 스레드가 실행될 때 수행할 역할을 정의하기 위함입니다!

아래는 Thread 클래스에 정의되어있는 run 메서드인데, Runnable 인터페이스를 구현한 클래스를 생성자 파라미터로 전달하는 경우 아래 메서드가 재정의됩니다.

@Override
public void run() {
    Runnable task = holder.task;
    if (task != null) {
        Object bindings = scopedValueBindings();
        runWith(bindings, task);
    }
}


// 생성한 thread 객체를 시작한다.
thread.start();
// log.info("before start");
thread.start();
// log.info("after start");

// ! run() 실행 !

// thread의 작업이 완료될 때까지 기다린다.
thread.join();
// log.info("before join");
thread.join();
// log.info("after join");
}

private static final class ExtendedThread extends Thread {
Expand Down
1 change: 1 addition & 0 deletions study/src/test/java/thread/stage1/ConcurrencyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ void test() throws InterruptedException {
// 하지만 디버거로 개별 스레드를 일시 중지하면 if절 조건이 true가 되고 크기가 2가 된다. 왜 그럴까?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

왜 그럴까요..!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

디버거로 두 개의 스레드를 일시 중지한 후 동시에 실행시키면 users 리스트가 비어있는 상태에서 동시에 users.contains문이 실행되고 두 스레드 모두에서 user는 리스트에 포함되어있지 않다고 출력됩니다. 따라서 두 스레드 모두에서 user 데이터 추가를 시도하게 되고 결과적으로 리스트 사이즈는 2가 됩니다. 이를 해결하기 위해 synchronized 키워드를 붙여 동기적으로 차례차례 실행되도록 하였습니다!

assertThat(userServlet.getUsers()).hasSize(1);
}
// 각 스레드 중단점 걸고 동시에 실행 -> synchronized 붙이면 동기로 처리하여 해결 (두번째 스레드는 아예 중단점 안 걸림)
}
2 changes: 1 addition & 1 deletion study/src/test/java/thread/stage1/UserServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public void service(final User user) {
join(user);
}

private void join(final User user) {
private synchronized void join(final User user) {
if (!users.contains(user)) {
users.add(user);
}
Expand Down
8 changes: 5 additions & 3 deletions study/src/test/java/thread/stage2/AppTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,22 @@ void test() throws Exception {
var threads = new Thread[NUMBER_OF_THREAD];

for (int i = 0; i < NUMBER_OF_THREAD; i++) {
threads[i] = new Thread(() -> incrementIfOk(TestHttpUtils.send("/test")));
threads[i] = new Thread(() -> incrementIfOk(TestHttpUtils.send("/test"))); // 요청 10번
}

// 완전히 동시에 요청을 보내지는 않는 환경임
for (final var thread : threads) {
thread.start();
thread.start(); // 요청
Thread.sleep(50);
}

for (final var thread : threads) {
thread.join();
thread.join(); // 완료될 때가지 대기
}

assertThat(count.intValue()).isEqualTo(2);
}
// 해결: max-connections => 2

private static void incrementIfOk(final HttpResponse<String> response) {
if (response.statusCode() == 200) {
Expand Down
28 changes: 21 additions & 7 deletions tomcat/src/main/java/org/apache/catalina/connector/Connector.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
package org.apache.catalina.connector;

import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Connector implements Runnable {

private static final Logger log = LoggerFactory.getLogger(Connector.class);

private static final int DEFAULT_PORT = 8080;
private static final int DEFAULT_ACCEPT_COUNT = 100;
private static final int DEFAULT_MAX_THREADS = 250;

private final ServerSocket serverSocket;
private final ThreadPoolExecutor executor;
private boolean stopped;

public Connector() {
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT);
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, DEFAULT_MAX_THREADS);
}

public Connector(final int port, final int acceptCount) {
public Connector(final int port, final int acceptCount, final int maxThreads) {
this.serverSocket = createServerSocket(port, acceptCount);
this.stopped = false;
this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(maxThreads);
}

private ServerSocket createServerSocket(final int port, final int acceptCount) {
Expand Down Expand Up @@ -67,7 +71,9 @@ private void process(final Socket connection) {
return;
}
var processor = new Http11Processor(connection);
new Thread(processor).start();
// Thread thread = new Thread(processor);
// thread.start(); // 매 요청마다 스레드 생성
executor.submit(processor);
}

public void stop() {
Expand All @@ -92,4 +98,12 @@ private int checkPort(final int port) {
private int checkAcceptCount(final int acceptCount) {
return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT);
}

public int getExecutorPoolSize() {
return executor.getPoolSize();
}

public int getExecutorQueueSize() {
return executor.getQueue().size();
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.apache.catalina.session;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class SessionManager {

private static SessionManager INSTANCE;

private final List<Session> sessions = new ArrayList<>();
private final List<Session> sessions = new CopyOnWriteArrayList<>();

public static SessionManager getInstance() {
if (INSTANCE == null) {
Expand All @@ -19,14 +19,14 @@ public static SessionManager getInstance() {
private SessionManager() {
}

public String create(String key, Object value) {
public synchronized String create(String key, Object value) {
Session session = new Session();
session.addAttribute(key, value);
sessions.add(session);
return session.getId();
}

public boolean contains(String id) {
public synchronized boolean contains(String id) {
return sessions.stream()
.anyMatch(session -> session.hasId(id));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.apache.catalina.connector;

import static org.assertj.core.api.Assertions.assertThat;

import java.net.http.HttpResponse;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class ConnectorTest {

private static final Logger log = LoggerFactory.getLogger(ConnectorTest.class);
private static final AtomicInteger count = new AtomicInteger(0);

@DisplayName("거의 동시에 300개의 요청이 들어오면?")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트도 꼼꼼하게.. 굳 👍

@Test
void test_concurrentRequest() {
Connector connector = new Connector();
connector.start();

for (int i = 0; i < 300; i++) {
incrementIfOk(TestHttpUtils.send("/"));
// log.info("pool size = {}", connector.getExecutorPoolSize());
// log.info("queue size = {}", connector.getExecutorQueueSize());
}

assertThat(connector.getExecutorPoolSize()).isEqualTo(250);
// assertThat(connector.getExecutorQueueSize()).isEqualTo(50);
connector.stop();
}

private static void incrementIfOk(final HttpResponse<String> response) {
if (response.statusCode() == 200) {
count.incrementAndGet();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.apache.catalina.connector;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class TestHttpUtils {

private static final HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.connectTimeout(Duration.ofSeconds(1))
.build();

public static HttpResponse<String> send(final String path) {
final var request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080" + path))
.timeout(Duration.ofSeconds(1))
.build();

try {
return httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}