Open skydreamer21 opened 5 months ago
java 8, 11, 17 버전의 주요 특징에 대해서 설명해주세요.
스프링 프레임워크와 스프링 부트의 차이에 대해 설명해주세요.
Bean 생명주기에 대해 설명해주세요.
스프링이 여러 요청을 처리하는 방법을 설명해주세요
java 8, 11, 17 버전의 주요 특징에 대해서 설명해주세요.
stream.parallelStream
Java8부터는 Permanent Generation이 Metaspace로 대체되었습니다. Metaspace는 런타임 시 메모리 요구 사항에 따라 자체 크기를 조정하며, 필요하다면 MaxMetaspaceSize 매개변수를 설정하여 Metaspace의 양을 조절할 수 있습니다.
📝 Permanent Generation
📝 Metaspace
스프링 프레임워크와 스프링 부트의 차이에 대해 설명해주세요.
스프링 프레임워크 IoC 컨테이너와 자체적은 MVC 프레임워크, 광범위한 모듈 등을 제공함으로써 웹 어플리케이션의 효과적인 개발을 돕습니다. 이렇게 Spring 프레임워크가 제공하는 수많은 기능을 이용해서 유연하게 어플리케이션을 개발할 수 있지만 이에 따라 수많은 설정과 구성이 필요합니다. 스프링 부트는 스프링 프레임워크를 기반으로 개발자가 어플리케이션을 빠르게 시작하고 실행할 수 있도록 기본값과 내장 서버를 포함한 자동 구성을 제공합니다. 이를 통해 개발자를 어플리케이션을 빠르게 개발하고 배포할 수 있게 됩니다.
정리하면, 스프링 프레임워크는 복잡한 애플리케이션 개발 및 커스터마이징에 적합하며, 스프링 부트는 빠른 개발과 설정의 간소화에 중점을 둡니다.
Bean 생명주기에 대해 설명해주세요.
먼저 스프링 컨테이너는 Bean의 설정과 구성을 파악하여 Bean을 생성합니다. 이 후 생성한 Bean에 대해 의존성을 주입합니다. 이후 Bean 의 초기화 콜백이 수행되면서 @PostConstruct 어노테이션이 붙은 메서드나 사전 설정한 초기화 메서드가 실행됩니다. 이후 Bean 의 소멸시점에서 소멸 전 콜백함수가 수행됩니다. @PreDestory 어노테이션이 붙은 메서드 혹은 사전에 등록한 메서드가 실행되고 Bean이 소멸됩니다.
스프링이 여러 요청을 처리하는 방법을 설명해주세요
웹 서버로부터 클라이언트의 HTTP 요청을 Dispatcher Servlet이 받습니다. Dispatcher Servlet은 요청을 어떤 컨트롤러가 처리할지 결정하기 위해 HandlerMapping을 사용합니다. HandlerMapping은 URL과 매칭되는 컨트롤러를 선택해 Dispatcher Servlet에게 다시 전달합니다. Dispatcher Servlet은 해당 컨트롤러의 비지니스 로직 실행 작업을 Handler Adapter에게 위임합니다. HandlerAdapter는 컨트롤러의 비지니스 로직을 호출하고 결과를 ModelAndView 객체에 담아서 Dispatcher Servlet에게 보냅니다. Dispatcher Servlet은 ViewResolver를 이용하여 결과를 보여줄 View를 가져오고 View에서 생성된 응답을 웹서버를 통해 클라이언트에게 반환합니다.
java 8, 11, 17 버전의 주요 특징에 대해서 설명해주세요.
java8
java11
java 17
Stream.toList()
제공스프링 프레임워크와 스프링 부트의 차이에 대해 설명해주세요.
- spring boot 전용 dependency 제공 (여러 라이브러리를 통합하여 제공)
- AutoConfiguration기능을 사용하여 Component Scan 대상인 클래스들을 Bean에 등록
- 내장 WAS를 가지고 있기 때문에, war가 아닌 jar 파일만으로도 간편하게 배포 가능
Bean 생명주기에 대해 설명해주세요.
- 스프링 컨테이너 생성
- 스프링 Bean 생성
- 의존관계 주입
- 초기화 콜백 (
@PostConstruct
)- Bean 사용
- 소멸 전 콜백 (
@PreDestroy
)- 스프링 종료
스프링이 여러 요청을 처리하는 방법을 설명해주세요.
Spring MVC
DispatcherServlet
이 요청을 가로챈다.HandlerMapping
은 어떤 컨트롤러에게 가로챈 요청을 위임하면 좋을지 판단한다.@RequestMapping
을 통하여 요청을 처리할 메서드에 도달한다. (HandlerAdapter
)DispatcherServlet
에게 보낸다.DispatcherServlet
은 ViewResolver
에게 뷰에 대한 정보를 넘긴다.ViewResolver
는 해당 View를 찾아 DispatcherServlet
에게 알려준다. (suffix, prefix를 붙이는 것도 여기서 수행)DispatcherServlet
은 응답할 View에게 Render를 지시하고 View는 응답 로직을 처리한다.DispatcherServlet
이 클라이언트에게 렌더링된 View를 응답한다.Spring REST API
DispatcherServlet
이 요청을 가로챈다.HandlerMapping
은 어떤 컨트롤러에게 가로챈 요청을 위임하면 좋을지 판단한다.@RequestMapping
을 통하여 요청을 처리할 메서드에 도달한다. (HandlerAdapter
)ResponseEntity
에 이를 담아 DispatcherServlet
에게 보낸다.스프링 부트는
와 같은 특징이 있습니다.
의 생명주기를 가집니다.
스프링 부트의 내장 톰캣은 다중 요청 처리를 위해 쓰레드풀을 생성합니다. 이후 유저 요청이 들어오면 쓰레드 풀에서 하나씩 쓰레드를 할당하고, 해당 쓰레드에서 Dispatcher Servlet을 거쳐 유저 요청을 처리합니다. 작업을 모두 수행하고 나면 쓰레드는 쓰레드풀로 반환됩니다.
java 8, 11, 17 버전의 주요 특징에 대해서 설명해주세요.
Java 8에는 대표적으로 람다 표현식, default 메서드, 옵셔널, 스트림 API 등이 추가되었습니다. Java 11에는 대표적으로 String 클래스에 여러 메서드가 추가되고 javac를 통해 컴파일하지 않고도 java 파일을 바로 실행할 수 있는 환경으로 변경되었습니다. Java 17에는 sealed class가 추가되었고 프리뷰 단계인 패턴 매칭과 인큐베이터 단계인 외부 함수 및 메모리 api가 있습니다.
스프링 프레임워크와 스프링 부트의 차이에 대해 설명해주세요.
스프링(Spring)은 프레임워크이며, 스프링 부트(Spring Boot)는 스프링 프레임워크를 기반으로 한 도구입니다. 스프링은 설정 파일을 작성해야 하지만, 스프링 부트는 자동 설정을 제공하여 간편하게 개발할 수 있습니다. 또한, 스프링 부트는 내장 서버를 제공하여 쉽게 웹 애플리케이션을 실행할 수 있습니다. Spring은 스프링 프레임워크를 보다 세밀하게 제어하고자 하는 경우에, Spring Boot는 빠르고 간단하게 스프링 애플리케이션을 개발하고자 하는 경우에 사용됩니다.
Bean 생명주기에 대해 설명해주세요.
스프링 내부에서는 스프링의 라이프 사이클과 관련하여 이벤트가 있는데 순서는 다음과 같습니다. 스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 앱 동작 수행 → 소멸 전 콜백 → 스프링 종료
스프링이 여러 요청을 처리하는 방법을 설명해주세요
스프링부트는 내장 서블릿 컨테이너인 Tomcat을 이용합니다. Tomcat은 다중 요청을 처리하기 위해서, 부팅할 때 스레드의 컬렉션인 Thread Pool을 생성합니다. 이후 유저 요청(HttpServletRequest)가 들어오면 Thread Pool에서 하나씩 Thread를 할당합니다. 해당 Thread에서 스프링부트에서 작성한 Dispatcher Servlet을 거쳐 유저 요청을 처리합니다. 작업을 모두 수행하고 나면 스레드는 스레드 풀로 반환됩니다.
server:
tomcat:
threads:
max: 200 # 생성할 수 있는 thread의 총 개수
min-spare: 10 # 항상 활성화 되어 있는 thread의 개수
max-connections: 8192 # 수립 가능한 connection의 총 개수
accept-count: 100 # 작업 큐의 사이즈
connection-timeout: 20000 # timeout 판단 기준 시간, 20초
port: 8080 # 서버를 띄울 포트번호
지금까지 알아본 바에 의하면, 유저 요청이 들어올 때(Connection)마다 스레드가 하나씩 할당될 것이고, 작업 큐가 가득 차면 스레드가 늘어날 것이고, 스레드도 가득 차면 유저 요청이 거절될 것이다.
테스트로 스프링 프로젝트를 하나 만드록, application.yml에 다음과 같이 옵션을 주었다.
server:
tomcat:
threads:
max: 2
min-spare: 2
accept-count: 1
port: 5000
그 후 3초를 대기하는 api를 하나 만들었다.
@Controller
public class HelloController {
private Logger log = LoggerFactory.getLogger(HelloController.class);
@RequestMapping("/hello")
public ResponseEntity<Void> hello() throws InterruptedException {
log.info("start");
Thread.sleep(3000);
log.info("end");
return ResponseEntity.ok().build();
}
}
이 프로젝트가 감당할 수 있는 요청은 동원할 수 있는 스레드 2개, 그리고 작업 큐 1개에서 대기할 요청까지 최대 3개이다.
이를 확인하기 위해 또 다른 스프링 프로젝트를 만들어, 요청을 5번 보내기로 했다.
@Controller
public class PowerSmashController {
private Logger log = LoggerFactory.getLogger(PowerSmashController.class);
@GetMapping("/")
@ResponseBody
public ResponseEntity<Void> 요청5개쏘는메소드() {
RestTmeplate restTemplate = new RestTemplate();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
log.info("발사!");
String result = restTemplate
.getForObject("http://localhost:5000/hello", String.class);
log.info(result);
});
thread.start();
}
return ResponseEntity.ok().build();
}
}
해당 코드는 밀리세컨드 단위로 5번의 요청을 한번에 보내게 된다. 2개의 요청이 2개의 활성 스레드에서 각각 3초동안 block되고, 3번째 요청은 작업 큐에서 대기할 것이므로 4, 5번째 요청은 거절되어야 한다.
요청은 동시에 갔지만 응답은 3초 단위로 텀을 두고 순차적으로 처리되는 모습을 볼 수 있다.
서버 측도 확인해보면
2개의 활성 스레드가 차근차근, 3초 간격으로 작업을 처리한 걸 볼 수 있다. 작업 큐는 1칸 이므로 2개의 4, 5번째 요청은 받을 수 없었을텐데 어떻게 가능했을까?
일단 자바는 C/C++과 달리 직접 메모리를 관리하고 OS 레벨의 시스템 콜을 직접 사용하기는 어렵다. JNI를 사용하는 것은 여기서는 배제하도록 하자. 자바는 JVM 위에서 동작하므로 주로 C/C++에 비하면 느리다고 인식되고 실제로도 그렇다.
자바가 특별히 성능이 좋지 않은 부분은 IO다. IO 성능 문제를 개선하는 것이 바로 java.nio 패키지이다.
자바 4부터 새로운 입출력(NIO: New Input/Output)이라는 뜻에서 java.nio 패키지가 포함되었는데, 자바 7부터 자바 IO와 자바 NIO 사이의 일관성 없는 클래스 설계를 바로 잡고, 비동기 채널 등의 네트워크 지원을 대폭 강화한 NIO.2 API가 추가되었다. NIO.2는 java.nio2 패키지로 제공되지 않고 기존 java.nio의 하위 패키지 java.nio.channels, java.nio.charset, java.nio.file에 통합되어 있다.
IO 는 스트림 기반이다. 스트림은 입력 스트림과 출력 스트림으로 구분되어 있기 때문에 데이터를 읽기 위해서는 입력 스트림을 생성해야 하고, 데이터를 출력하기 위해서는 출력 스트림을 생성해야 한다. NIO는 채널 기반. 채널은 스트림과 달리 양방향으로 입력과 출력이 가능하다. 그렇기 때문에 입력과 출력을 위한 별도의 채널을 만들 필요가 없다.
IO에서는 출력 스트림이 1바이트를 쓰면 입력 스트림이 1바이트를 읽는다. 이것보다는 버퍼를 사용해서 복수 개의 바이트를 한꺼번에 입력받고 출력하는 것이 빠른 성능을 낸다. 그래서 IO는 버퍼를 제공해주는 보조 스트림인 BufferedInputStream, BufferedOutputStream을 연결해서 사용하기도 한다. NIO는 기본적으로 버퍼를 사용해서 입출력을 하므로 IO 보다는 성능이 좋다. 채널은 버퍼에 저장된 데이터를 출력하고, 입력된 데이터를 버퍼에 저장한다.
또한 IO는 스트림에서 읽은 데이터를 즉시 처리하기 때문에 입력된 전체 데이터를 별도로 저장하지 않으면, 입력된 데이터의 위치를 자유롭게 이용할 수 없다. NIO는 읽은 데이터를 무조건 버퍼에 저장하기 때문에 버퍼 내에서 데이터의 위치를 이동해 가면서 필요한 부분만 읽고 쓸 수 있다.
IO는 블로킹(blocking)이 된다. 입력 스트림의 read() 출력 스트림의 write() 메소드를 호출하면 블로킹 된다. IO 스레드가 블로킹되면 다른 일을 할 수 없고 블로킹을 빠져나오기 위해 인터럽트도 할 수 없다. 블로킹을 빠져나오는 방법은 스트림을 닫는 것이다.
NIO는 블로킹와 논블로킹 특징을 모두 가지고 있다. IO 블로킹과의 차이점은 NIO 블로킹은 스레드를 인터럽트 함으로써 빠져나올 수가 있다는 것이다. NIO의 논블로킹은 입출력 작업 준비가 완료된 채널만 선택해서 작업 스레드가 처리하기 때문에 작업 스레드가 블로킹 되지 않는다. NIO 논블로킹의 핵심 객체는 멀티플렉서인 Selector이다. 셀렉터는 복수 개의 채널 중에서 이벤트가 준비 완료된 채널을 선택하는 방법을 제공해준다.
소켓을 통해 non-blocking read를 할 수 있도록 지원하는 Connection.
읽기, 쓰기 하나씩 쓸 수 있는 스트림은 단방향식, 채널은 읽기 쓰기 둘 다 가능한 양방향식 입출력 클래스이며 네이티브 IO, Scatter/Gather 구현으로 효율적인 IO 처리 (시스템 콜 수 줄이기, 모아서 처리하기)
커널에 의해 관리되는 시스템 메모리를 직접 사용할 수 있는 채널에 의해 직접 read 되거나 write 될 수 있는 배열과 같은 객체.
네트워크 프로그래밍의 효율을 높이기 위한 것
클라이언트 하나 당 스레드 하나를 생성해서 처리하기 때문에 스레드가 많이 생성될수록 급격한 성능 저하를 가졌던 단점을 개선하는 Reactor 패턴의 구현체
Selector는 어느 channel set이 IO event를 가지고 있는지를 알려준다. Selector.select()는 I/O 이벤트가 발생한 채널 set을 return 한다. return할 channel이 없다면 계속 block 된다. 이 block 된 것을 바로 return 시켜주는 것이 Selector.wakeup()이다.
Selector.selectedKeys()는 Selection Key를 return 해 준다. Reactor는 이 Selection Key를 보고 어떤 handler로 넘겨줄지를 결정한다.
Selector와 Channel 간의 관계를 표현해주는 객체이다. Selector가 제공한 Selection Key를 이용해 Reactor는 채널에서 발생하는 I/O 이벤트로 수행할 작업을 선택할 수 있다. ServerSocketChannel에 selector를 등록하면 key를 준다. 이 key가 SelectionKey이다.
캐릭터셋을 나타낸다. 바이트 데이터와 문자 데이터를 인코딩/디코딩 할 때 사용된다.
NIO는 다수의 연결이나 파일들을 논블로킹이나 비동기 처리할 수 있어서 많은 스레드 생성을 피하고 스레드를 효과적으로 재사용한다는 장점이 있다. 그래서 NIo는 연결 수가 많고 하나의 입출력 처리 작업이 오래 걸리지 않는 경우에 사용하는 것이 좋을 것이다. 스레드에서 입출력 처리가 오래 걸린다면 대기하는 작업의 수가 늘어나게 되므로 장점이 사라진다.
많은 데이터 처리의 경우 IO가 좋을 수 있다. NIO는 버퍼 할당 크기가 문제가 되고, 모든 입출력 작업에 버퍼를 무조건 사용해야 하므로 즉시 처리하는 IO보다 성능 저하가 있을 수 있다. 연결 클라이언트 수가 적고 전송되는 데이터가 대용량이면서 순차적으로 처리될 필요성이 있는 경우 IO로 구현하는 것이 좋은 선택일 수 있다.
비밀은 Connector에 있다. 위 얘기는 BIO(Blocking I/O) Connector일 때 유효한 얘기이다. 그러나 톰캣 8.0부터 NIO(Nonblockig I/O) Connector가 기본으로 채택되고, 9.0부터는 BIO Connector가 deprecate 됨으로써 다른 방식으로 진행되게 된다.
Connector는 소켓 연결을 수입하고 데이터 패킷을 획득하여 HttpServletRequest 객체로 변환하고 Servlet 객체에 전달하는 역할을 한다.
BIO Connector는 Socket Connection을 처리할 때 Java의 기본적인 I/O 기술을 사용한다.
thread pool에 의해 관리되는 thread는 소켓 연결을 받고 요청을 처리하고 요청에 대해 응답한 후 소켓 연결이 종료되면 pool에 다시 돌아오게 된다.
즉, connection이 닫힐 때까지 하나의 thread는 특정 connection에 계속 할당되어 있을 것이다.
이러한 방식으로 thread를 할당하여 사용할 경우, 동시에 사용되는 thread 수가 동시 접속할 수 있는 사용자의 수가 될 것이다. 그리고 이러한 방식을 채택해서 사용할 경우 thread들이 충분히 사용되지 않고 idle 상태로 낭비되는 시간이 많이 발생한다. 이러한 문제점을 해결하고 리소스를 효율적으로 사용하기 위해 NIO Connector가 등장했다.
NIO Connector는 I/O가 아니라 Http11NioProtocol을 사용한다.
NIO Connector에선 Poller라고 하는 별도의 스레드가 커넥션을 처리한다. Poller는 Socket들을 캐시로 들고 있다가 해당 Socket에서 data에 대한 처리가 가능한 순간에만 thread를 할당하는 방식을 사용해서 thread가 idle 상태로 낭비되는 시간을 줄여준다.
Acceptor는 이름 그대로 Socket Connection을 accept한다. serverSocket.accept() 방식을 사용하고 있다. 소켓에서 Socket Channel 객체를 얻어서 톰캣의 NioChannel 객체로 변환한다. 그리고 추가로 NioChannel 객체를 PollerEvent라는 객체로 한 번 더 캡슐화해서 event queue에 넣게 된다. Acceptor는 Event Queue의 공급자, Poller thread는 Event Queue의 사용자이다.
Poller는 NIO의 Selector를 가지고 있다. Selector에는 다수의 채널이 등록되어 있고, select 동작을 수행하여 데이터를 읽을 수 있는 소켓을 얻는다. 그리고 Worker Thread Pool에서 이용할 수 있는 Worker Thread를 얻어서 해당 소켓을 worker thread에 넘긴다.
Java Nio Selector를 사용해서 data 처리가 가능할 때만 Thread를 사용하기 때문에 idle 상태로 낭비되는 Thread가 줄어들게 된다.
Poller에선 Max Connection까지 연결을 수락하고, 셀렉터를 통해 채널을 관리하므로 작업 큐 사이즈와 관계 없이 추가로 커넥션을 refuse하지 않고 받아놓을 수 있다.
스레드 또한 모자라다면 max 사이즈까지 스레드를 추가하는 것을 볼 수 있다.
Java & Spring 관련 면접 질문 달아주세요!
이전 질문들