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

JSP의 라이프 사이클은 다음의 단계를 거친다.

  1. JSP URL 호출
  2. 페이지 번역
  3. JSP 페이지 컴파일
  4. 클래스 로드
  5. 인스턴스 생성
  6. jspInit 함수 호출
  7. _jspService 함수 호출
  8. jspDestroy 함수 호출

여기서 해당 JSP 페이지가 이미 컴파일되어 있고, 클래스가 로드외어 있고, JSP 파일이 변경되지 않았다면, 가장 많은 시간이 소요되는 2~4 프로세스는 생략된다. 서버의 종류에 따라서 서버가 기동될 때 컴파일을 미리 수행하는 Precompile 옵션이 있다. 이 옵션을 선택하면 서버에 최신 버전을 반영한 이후에 처음 호출되었을 때 응답 시간이 느린 현상을 방지할 수 있다.

이번에는 서블릿의 라이프 사이클을 살펴보자. WAS의 JVM이 시작한 후에는,

servlet-life-cycle

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

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

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

public class DontUserLikeThisServlet extends HttpServlet {
  private static final long serialVersionUID = 1L;
  String successFlag = "N";

  public DontUserLikeThisServlet() {
    super();
  }

  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    successFlag = request.getParameter("successFlag");
  }
}

successFlag 값은 여러 스레드에서 접근하면서 계속 바뀔 것이다. static을 사용하는 것과 거의 동일한 결과를 나타낸다. 그러므로, service() 함수를 구현할 때는 멤버 변수나 static한 클래스 변수를 선언하여 지속적으로 변경하는 작업은 피하기 바란다.

적절한 include 사용하기

JSP에서 사용할 수 있는 include 방식은 정적진 방식(include directive)과 동적인 방식(include action)이 있다. 정적인 방식은 JSP의 라이플 사이클 중 JSP 페이지 번역 및 컴파일 단계에서 필요한 JSP를 읽어서 메인 JSP의 자바 소스 및 클래스에 포함을 시키는 방식이다. 이와 반대로, 동적인 방식은 페이지가 호출될 때마다 지정된 페이지를 불러들여서 수행하도록 되어 있다.

동적인 방식이 정직인 방식보다 느릴 수밖에 없다. 정적인 방식과 동적인 방식의 응답 속도를 비교해보면 동적인 방식이 약 30배 더 느리게 나타난다. 즉, 성능을 더 빠르게 하려면 정적인 방식을 사용해야 한다는 의미다. 하지만 모든 화면을 정적인 방식으로 구성하면 잘 수행되던 화면에서 오류가 발생할 수 있다. 정적인 방식을 사용하면 메인 JSP에 추가되는 JSP가 생긴다. 이 때 추가된 JSP와 메인 JSP에 동일한 이름의 변수가 있으면 심각한 오류가 발생할 수 있다.

자바 빈즈, 잘 쓰면 약 못 쓰면 독

자바 빈즈(Java Beans)는 UI에서 서버 측 데이터를 담아서 처리하기 위한 컴포넌트이다. 자바 빈즈를 통하여 userBean을 하면 성능에 많은 영향을 미치지는 않지만, 너무 많이 사용하면 JSP에서 소요되는 시간이 증가될 수 있다.

한 두 개의 자바 빈즈를 사용하는 것은 상관없지만, 10~20개의 자바 빈즈를 사용하면 성능에 영향을 주게 된다. 그러므로 TO(Transfer Object) 패턴을 사용하도록 하자.

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

스프링의 핵심 기술은 바로 Dependency Injection, Aspect Oriented Programming, Portable Service Abstraction으로 함축할 수 있다.

spring-triangle

Dependency Injection(의존성 주입)은 객체 간의 의존 관계를 관리하는 기술 정도로 생각하면 된다. 객체는 보통 혼자서 모든 일을 처리하지 않고, 여러 다른 객체와 협엽하여 일을 처리한다. 이때 자신과 협업하는 객체와 자신과의 의존성을 가능한 낮춰야 유리하다. 다시 말해서, 어떤 객체가 필요로 하는 객체를 자기 자신이 직접 생성하여 사용하는 것이 아니라 외부에 있는 다른 무언가로부터 필요로 하는 객체를 주입 받는 기술이다. 스프링은 이렇게 의존성을 쉽게 주입하는 틀을 제공한다. XML이나 어노테이션 등으로 의존성을 주입하는 방법을 제공하며 생성자 주입, 세터 주입, 필드 주입 등 다양한 의존성 주입 방법을 제공하고 있다.

AOP(Aspect Oriented Programming)는 우리나라 말로 ‘관점 지향 프로그래밍’이라고 부른다. 이 기술은 OOP를 보다 더 OOP스럽게 보완해주는 기술이다. 트랜잭션, 로깅, 보완 체크 코드 등은 대부분 비슷한 코드가 중복된다. 이런 코드를 실제 비즈니스 로직과 분리할 수 있도록 도와주는 것이 바로 AOP이다. 이 기술을 잘 활용하면 핵심 비즈니스 코드의 가독성을 높여준다.

마지막으로 스프링이 제공하는 핵심 기술로 PSA를 꼽을 수 있다. 사용 중인 라이브러리나 프레임워크를 바꿔야할 때 심각한 문제가 발생할 수 있어서 추상화가 중요하다. 스프링은 그런 일이 생기지 않도록 비슷한 기술을 모두 아우를 수 있는 추상화 계층을 제공하여, 사용하는 기술이 바뀌더라도 비즈니스 로직의 변화가 없도록 도와준다.

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

빈 설정을 잘못해서 발생하는 문제도 있을 수 있고, 스프링의 동작 원리를 이해하지 않고서는 해결되지 않는 문제도 발생할 수 있다.

스프링 프레임워크를 사용할 때 성능 문제가 가장 많이 발생하는 부분은 ‘프록시(proxy)’와 관련되어 있다. 스프링 프록시는 기본적으로 실행 시에 생성된다. 따라서, 개발할 때 적은 요청에는 이상이 없다가 요청량이 많은 운영 상황으로 넘어가면 문제가 나타날 수 있다. 스프링이 프록시를 사용하게 하는 주요 기능은 바로 트랜잭션이다. \@Transactional 어노테이션을 사용하면 해당 어노테이션을 사용한 클래스의 인스턴스를 처음 만들 때 프록시 객체를 만든다. 이밖에도, 개발자가 직접 스프링 AOP를 사용해서 별도의 기능을 추가하는 경우에도 프록시를 사용하는데, 이 부분에서 문제가 가장 많이 발생한다. \@Transactional처럼 스프링이 자체적으로 제공하는 기능은 이미 상당히 오랜 시간 테스트를 거치고 많은 사용자에게 검증을 받았지만, 개발자가 직접 작성한 AOP 코드는 예상하지 못한 성능 문제를 보일 가능성이 매우 높다. 따라서, 간단한 부하 툴을 사용해서라도 성능적인 면을 테스트해야만 한다.

추가로, 스프링이 내부 매커니즘에서 사용하는 캐시도 조심해서 써야 한다.

public class SampleController {
  @RequestMapping("/member/{id}")
  public String hello(@PathVariable int id) {
    return "redirect:/member/" + id;
  }
}

이렇게 문자열 자체를 리턴하면 스프링은 해당 문자열에 해당하는 실제 뷰 객체를 찾는 매커니즘을 사용하는데, 이 때 매번 동일한 문자열에 대한 뷰 객체를 새로 찾기 보다는 이미 찾아본 뷰 객체를 캐싱해두면 다음에도 동일한 문자열이 반환됐을 때 훨씬 빠르게 뷰 객체를 찾을 수 있다. 스프링에서 제공하는 ViewResolver 중에 자주 사용되는 InternalResourceViewResolver에는 그러한 캐싱 기능이 내장되어 있다.

만약 매번 다른 문자열이 생성될 가능성이 높고, 상당히 많은 수의 키 값으로 캐시 값이 생성될 여지가 있는 상황에서는 문자열을 반환하는 게 메모리에 치명적일 수 있다. 따라서 이런 상황에서는 뷰 이름을 문자열로 반환하기보다는 뷰 객체 자체를 반환하는 방법이 메모리 릭을 방지하는 데 도움이 된다.

public class SampleController {
  @RequestMapping("/member/{id}")
  public View hello(@PathVariable int id) {
    return new RedirectView("/member/" + id);
  }
}