[Java/servlet POJ] 서블릿 초기화 / 필드 초기화

이혜윤·2024년 4월 1일

JAVA

목록 보기
5/9

📌 1. 기존 코드

원래 의도: Listener를 통해 서버 실행을 감지, 서버 실행 즉시 mainDao, reviewDao, userDao 객체 생성되어 DB에 video DB에 영상 정보 삽입되도록 의도함

MainController.java

@WebServlet("/main")
public class MainController extends HttpServlet {

	MainDao mainDao = (MainDao) getServletContext().getAttribute("mainDao");
    ReviewDao reviewDao = (ReviewDao) getServletContext().getAttribute("reviewDao");
    UserDao userDao = (UserDao) getServletContext().getAttribute("userDao");

// 이하 service 함수부터는 생략

DBListener.java


import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class DBListener implements ServletContextListener {

	@Override
	public void contextInitialized(ServletContextEvent sce) {
	    // DAO 인스턴스 생성
	    MainDao mainDao = MainDaoImpl.getInstance();
	    ReviewDao reviewDao = ReviewDaoImpl.getInstance();
	    UserDao userDao = UserDaoImpl.getInstance();

	    // 서블릿 컨텍스트에 DAO 인스턴스 저장
	    sce.getServletContext().setAttribute("mainDao", mainDao);
	    sce.getServletContext().setAttribute("reviewDao", reviewDao);
	    sce.getServletContext().setAttribute("userDao", userDao);
	}
}

MainDaoImpl.java


import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.*;

import com.ssafy.model.dto.Video;
import com.ssafy.util.DBUtil;

public class MainDaoImpl implements MainDao {

	private static final DBUtil util = DBUtil.getInstance();
	private static MainDaoImpl instance;
	private static List<Video> list;

	private MainDaoImpl() {
		System.out.println("created.");
		
	    list = new ArrayList<Video>();
		list.add(new Video(1, "gMaB-fG4u4g", "ThankyouBUBU", 10, "전신", "전신 다이어트 최고의 운동 [칼소폭 찐 핵핵매운맛]"));
		list.add(new Video(2, "swRNeYw1JkY", "ThankyouBUBU", 12, "전신", "하루 15분! 전신 칼로리 불태우는 다이어트 운동"));
		list.add(new Video(3, "54tTYO-vU2E", "ThankyouBUBU", 20, "상체", "상체 다이어트 최고의 운동 BEST [팔뚝살/겨드랑이살/등살/가슴어깨라인]"));
		list.add(new Video(4, "QqqZH3j_vH0", "ThankyouBUBU", 2, "상체", "상체비만 다이어트 최고의 운동 [상체 핵매운맛]"));
		list.add(new Video(5, "tzN6ypk6Sps", "김강민", 17, "하체", "하체운동이 중요한 이유? 이것만 보고 따라하자 ! [하체운동 교과서]"));
		list.add(new Video(6, "u5OgcZdNbMo", "GYM종국", 120, "하체", "저는 하체 식주의자 입니다"));
		list.add(new Video(7, "PjGcOP-TQPE", "ThankyouBUBU", 1, "복부", "11자복근 복부 최고의 운동 [복근 핵매운맛]"));
		list.add(new Video(8, "7TLk7pscICk", "SomiFit", 0, "복부", "(Sub)누워서하는 5분 복부운동!! 효과보장! (매일 2주만 해보세요!)"));

	    addVideoList(list);
	    System.out.println(list.toString());
	}

	public void addVideoList(List<Video> videoList) {
		System.out.println("add video list");
	    try (Connection conn = util.getConnection();
	         PreparedStatement pstmt = conn.prepareStatement(
	             "INSERT INTO videos (videoId, youtubeId, channelName, viewCnt, fitPartName, title) VALUES (?, ?, ?, ?, ?, ?)")) {
	        
	        for (Video video : videoList) {
	            pstmt.setInt(1, video.getVideoId());
	            pstmt.setString(2, video.getYoutubeId());
	            pstmt.setString(3, video.getChannelName());
	            pstmt.setInt(4, video.getViewCnt());
	            pstmt.setString(5, video.getFitPartName());
	            pstmt.setString(6, video.getTitle());
	            pstmt.addBatch(); // 일괄 삽입을 위해 배치에 추가
	        }
	        
	        pstmt.executeBatch(); // 일괄 삽입 실행
	    } catch (SQLException e) {
	        e.printStackTrace();
	    }
	}
	
	public static MainDao getInstance() {
		if (instance == null)
			instance = new MainDaoImpl();
		return instance;
	}

// 이하 생략

📌 2. 문제점

getServletContext().getAttribute("mainDao"), getServletContext().getAttribute("reviewDao"), getServletContext().getAttribute("userDao")를 호출하는 방식으로 mainDao, reviewDao, userDao 인스턴스를 초기화하려고 하고 있습니다.

하지만, 이 코드는 서블릿의 생명주기 메서드나 요청 처리 메서드가 아닌 필드 초기화 시점에서 실행되므로, getServletContext() 호출이 NullPointerException을 발생시키는 원인입니다. 서블릿 컨텍스트는 서블릿이 초기화되는 init() 메서드 실행 시점 이후에만 사용할 수 있기 때문입니다.

코드를 수정하여, init() 메서드를 오버라이드하고 해당 메서드 내에서 DAO 인스턴스를 초기화하도록 변경해야 합니다

📌 3. 수정 코드

MainController.java

@WebServlet("/main")
public class MainController extends HttpServlet {

    private MainDao mainDao;
    private ReviewDao reviewDao;
    private UserDao userDao;

    @Override
    public void init() throws ServletException {
        super.init();
        // 서블릿 컨텍스트에서 DAO 인스턴스를 가져옵니다.
        this.mainDao = (MainDao) getServletContext().getAttribute("mainDao");
        this.reviewDao = (ReviewDao) getServletContext().getAttribute("reviewDao");
        this.userDao = (UserDao) getServletContext().getAttribute("userDao");
    }

// 이하 service 함수부터는 생략

서블릿이 초기화될 때 ServletContext에서 DAO 인스턴스를 안전하게 가져올 수 있으며, NullPointerException 문제를 해결할 수 있습니다. init() 메서드는 서블릿 생명주기에 따라 서블릿 객체가 생성된 후 한 번만 호출되므로, 이 시점에서 필요한 초기화 작업을 수행하는 것이 적합합니다.

📌 4. 필요 개념 (몰랐던 개념)

💡 4.1 super.init()의 역할

super.init(); 호출은 서블릿의 init() 메서드 내에서 부모 클래스의 init() 메서드를 호출하는 것을 의미합니다. 서블릿 생명주기에서 init() 메서드는 서블릿이 처음 생성될 때 한 번만 호출되어 초기 설정을 수행하는 데 사용됩니다. 이 때 super.init();을 사용하면, 현재 서블릿 클래스가 확장하고 있는 상위 클래스(대부분의 경우 javax.servlet.GenericServlet 또는 javax.servlet.http.HttpServlet)의 초기화 로직을 실행할 수 있습니다.

그러나 실제로 HttpServlet 클래스의 init() 메서드는 기본적으로 아무런 작업을 수행하지 않는 빈 메서드입니다. 따라서 super.init(); 호출은 필수적이지 않으며, 많은 경우 생략됩니다. 그럼에도 불구하고 이를 호출하는 것은 좋은 습관입니다. 왜냐하면 이는 상위 클래스에서 일부 중요한 초기화 작업을 수행할 가능성을 염두에 두고, 클래스 계층구조에서 변경이 발생할 경우를 대비하는 것이기 때문입니다.

예를 들어, 사용자가 HttpServlet을 확장하는 서블릿을 작성하고 init() 메서드를 오버라이드하는 경우, 사용자 정의 초기화 코드를 실행하기 전이나 후에 super.init();를 호출함으로써 상위 클래스의 초기화 코드를 실행할 수 있는 옵션을 남겨두는 것입니다. 이러한 접근 방식은 코드의 확장성과 유지 보수성을 향상시키며, 향후 서블릿 API가 업데이트되어 init() 메서드에 새로운 기능이 추가되더라도 이러한 변경사항을 자연스럽게 수용할 수 있게 합니다.

요약하자면, super.init(); 호출은 서블릿의 초기화 과정에서 상위 클래스의 초기화 로직을 명시적으로 실행하기 위해 사용되며, 현재의 HttpServlet 구현에서는 필수적이지 않지만, 코드의 확장성과 유지 보수성 측면에서 권장되는 사례입니다.

💡 4.2 필드 초기화 시점과 서블릿 초기화 시점 개념

필드 초기화 시점과 서블릿 초기화 시점 사이의 관계를 설명하자면, 서블릿 초기화 시점은 필드 초기화 시점 이후에 발생

  • 여기서 "필드 초기화"란 클래스의 인스턴스 변수(필드)에 대한 초기화 작업을 의미하며, 서블릿 클래스가 인스턴스화될 때 발생합니다.
  • 반면, "서블릿 초기화" 시점은 서블릿 인스턴스가 생성된 후, 서블릿 컨테이너에 의해 서블릿의 init() 메서드가 호출될 때를 말합니다.

💬 [클래스의 인스턴스가 생성되는 과정]

1) 인스턴스 변수(필드) 초기화: 서블릿 인스턴스가 메모리에 할당되면, 선언된 필드에 대해 기본값(숫자형의 경우 0, 객체의 경우 null 등)이 할당되고, 이어서 명시적으로 초기화된 코드(예: private int number = 10;)가 실행됩니다. 이 단계는 생성자가 실행되기 전에 이루어집니다.

2) 생성자 실행: 필드 초기화가 완료된 후, 클래스의 생성자가 실행됩니다. 생성자 내에서는 일반적으로 인스턴스 변수의 초기화 또는 다른 초기화 작업을 수행합니다.

3) 서블릿 init() 메서드 호출: 모든 생성자가 실행된 후, 서블릿 인스턴스가 완전히 준비되면, 서블릿 컨테이너는 서블릿의 init(ServletConfig config) 메서드를 호출하여 서블릿을 초기화합니다. 이 때 ServletConfig 객체가 전달되며, 서블릿은 이 객체를 사용하여 서블릿 초기화 파라미터 등의 구성 정보에 접근할 수 있습니다.

따라서, 필드 초기화는 서블릿 인스턴스 생성 과정의 초기 단계에서 발생하며, init() 메서드 호출은 그 이후에 이루어집니다. 이는 서블릿 생명주기의 일부로서, init() 메서드는 서블릿이 올바르게 구성되고 초기화될 수 있도록 서블릿 컨테이너에 의해 호출됩니다. 그러므로, init() 메서드 내에서 초기화 작업을 수행하는 것이 안전하며, 필드를 사용하여 서블릿 컨텍스트 등에 접근하는 작업은 init() 메서드가 호출된 후에 수행되어야 합니다.

💬 [서블릿 초기화 과정]

1) 컨테이너가 servlet instance 생성 시점에서 해당 servlet에 배포 서술자나 어노테이션을 통해 정의된 초기화 파라미터가 있는지를 살핀다.

2) 컨테이너는 이 초기화 파라미터에 있는 값을 이름/값 쌍의 문자열로 읽어들이고 servletConfig 객체를 생성해서 파라미터 값을 저장.

3) servlet instance를 생성한 후 servletConfig 객체를 init() 메소드의 참조인자로 넘긴다.

4) servletConfig 는 이 때부터 servlet과 생사를 함께함. servlet 객체가 소멸하면 참조 변수가 사라진 servletConfig 역시 garbage collection의 대상이 됨.

결국, servlet 에 관한 정보를 저장하는 servletConfig는 servlet이 생성될 때 함께 만들어져서 servlet이 소멸되고 나면 함께 사라진다. 만약 모든 웹 애플리케이션에서 이용해야 하는 정보라면 servletConfig 객체에 저장하는 것은 소용이 없을 것이다.

그렇다고 해서 모든 servlet마다 초기화 파라미터 값을 다 설정하고 수정할 때는 또 모든 서블릿의 초기화 파라미터를 다 수정할 순 없으니.. 이 문제를 해결하기 위해서 컨텍스트 초기화 파라미터를 이용

💬 getServletContext()
: ServletConfig 객체에는 servletContext에 접근 가능한 getServletContext() method를 통해 접근. servlet이 초기화를 거치게 되면 컨텍스트 초기화 파라미터에도 접근이 가능.

profile
구르미 누나

0개의 댓글