JSP와 서블릿, Spring에서 발생할 수 있는 문제점


JSP와 Servlet의 기본적인 동작원리를 알아보자.
일반적으로 JSP와 같은 웹 화면단을 처리하는 부분에서 소요되는 시간은 많지 않다.
JSP의 경우 가장 처음에 호출되는 경우에만 시간이 소요되는 이후에는 컴파일된 서블릿 클래스가 수행되기 때문이다.

JSP 라이프 사이클

(1) JSP URL 호출
(2) 페이지 번역
(3) JSP 페이지 컴파일
(4) 클래스 로드
(5) 인스턴스 생성
(6) jspInit 메서드 호출
(7) _jspService 메서드 호출
(8) jspDestory 메서드 호출

해당 JSP 페이지가 이미 컴파일되어 있고 클래스 로드되어 있고
JSP 파일이 변경되지 않았다면 가장 많은 시간이 소요되는 (2)~(4) 프로세스는 생략된다.


서블릿 라이프 사이클

WAS의 JVM이 시작한 후에는,

Servlet 객체가 자동으로 생성되거나 초기화 되거나
사용자가 해당 Servlet을 처음으로 호출했을 때 생성되고 초기화 된다.

KakaoTalk_Photo_2021-07-09-14-25-47


그 다음 계속 사용 가능 상태로 대기한다.
중간에 예외가 발생하면 사용 불가능 상태로 빠졌다가 다시 사용 가능 상태로 변환되기도 한다.
해당 서블릿이 더 이상 필요 없을 때는 파기 상태로 넘어간 후 JVM에서 제거 된다.

서블릿은 JVM에 여러 객체로 생성되지 않는다.
다시 말해 WAS가 시작하고, 사용 가능 상태가 된 이상 대부분의 서블릿은 JVM에 살아있고,
여러 스레드에서 해당 서블릿의 service() 메서드를 호출하여 공유한다.

만약 서블릿 클래스이 메서드 내에 선언한 지역 변수가 아닌 멤버 변수 (인스턴스 변수)를 선언하여
service() 메서드에서 사용하면 어떤 일이 벌어질까?

static을 사용하는 것과 거의 동일한 결과를 나타낸다.
service() 메서드를 구현할 때는 멤버 변수나 static한 클래스 변수를 선언하여
지속적으로 변경하는 작업은 피해야한다.


- 스프링 프레임워크 간단 정리

스프링 프레임워크는 데스크톱과 웹 어플리케이션, 작고 간단한 애플리케이션부터
여러 서버와 연동하여 동작해야 하는 엔터프라이즈 애플리케이션도 범용적인 애플리케이션 프레임워크이다.

Spring의 가장 큰 특징은 복잡한 애플리케이션도 POJO(Plain Old Java Object)로 개발할 수 있다는 점이다.
서블릿을 개발하려면 반드시 HttpServlet이라는 클래스를 상속해야 한다.
하지만 스프링을 사용하면 HttpServlet을 확장하지 않아도 웹 요청을 처리할 수 있는 클래스를 만들 수 있다.


스프링의 핵심 기술

Dependency Injection,
Aspect Oriented Programming,
Portable Service Abstraction 으로 함축할 수 있다.

(1) Dependency Injection
‘의존성 주입’ 이라고 한다.
객체간의 관계를 관리하는 기술로 생각하면 된다.
어떤 객체가 필요로 하는 객체를 자기 자신이 직접 생성하여 사용하는 것이 아니라
외부에 있는 다른 무언가로부터 필요로 하는 객체를 주입 받는 기술이다.

(2) AOP (Aspect Oriented Programming)
‘관점 지향 프로그래밍’ 이라고 부른다.
대부분은 비슷한 코드가 중복되고 코드를 읽는 데 방해가 된다.
이런 코드를 실제 비즈니스 로직과 분리할 수 있도록 도와주는 것이 바로 AOP이다.

자바에서 가장 유명한 AOP 프레임워크로는 AspectJ가 있다.

(3) PSA (Portable Service Abstraction)
스프링은 비슷한 기술을 모두 아우를 수 있는 추상화 계층을 제공하여, 사용하는 기술이 바뀌더라도
비즈니스 로직의 변화가 없도록 도와준다.


스프링 프레임워크를 사용하면서 발생할 수 있는 문제점들

스프링 프레임워크를 사용할 때 성능 문제가 가장 많이 발생하는 부분은 프록시(proxy) 와 관련이 있다.
스프링 프록시는 기본적으로 실행 시에 생성된다.
따라서 요청량이 많은 운영 상황으로 넘어가면 문제가 나타날 수 있다.

스프링이 프록시를 사용하게 하는 주요 기능은 트랜잭션이다.
@Transaction 어노테이션을 사용하면 해당 어노테이션을 사용한 클래스의 인스턴스를 처음 만들 때 프록시 객체를 만든다.
개발자가 직접 스프링 AOP를 사용해서 별도의 기능을 추가하는 경우에도 프록시를 사용하는데,
이 부분에서 문제가 많이 발생한다.

따라서, 간단한 부하 툴을 사용해서라도 성능적인 면을 테스트해야만 한다.

추가로, 스프링이 내부 매커니즘에서 사용하는 캐시도 조심해야 한다.
예를들어 스프링 MVC에서 작성하는 메서드의 리턴 타입으로 다음과 같은 문자열을 사용할 수 있다.
이 때 매번 동일한 문자열에 대한 뷰 객체를 새로 찾기 보다는 이미 찾아본 뷰 객체를 캐싱해두면
다음에도 동일한 문자열이 반환됐을 때 훨씬 빠르게 뷰 객체를 찾을 수 있다.

스프링이 제공하는 ViewResolver 중에 자주 사용되는 InternalResourceViewResolver에는 그러한 캐싱 기능이 내장되어 있다.
(ViewResolver는 뷰 이름과 지역화를 위한 Locale을 파라미터로 전달받으며,
매핑되는 View 객체를 리턴한다. 만약, 매핑되는 View 객체가 존재하지 않으면 null을 리턴한다.)

만약 매번 다른 문자열이 생성될 가능성이 높고, 상당히 많은 수의 키 값으로 캐싱 값이 생성될 여지가 있는 상황에서는
문자열을 반환하는 게 메모리에 치명적일 수 있다.

이런 상황에서는 뷰 이름을 문자열로 변환하기보다는 뷰 객체 자체를 반환하는 방법이 메모리 릭을 방지하는 데 도움이 된다.

@GetMapping("/members/{id}")
public View hello(@PathVariable int id) {
    return new RedirectView("/members/" + id);
}