Skip to content

김준기 5주차 학습 일지

June edited this page Jul 30, 2024 · 1 revision

Jar, War 파일

Jar는 라이브러리, 리소스, 클래스 파일 등 메타 데이터를 약속된 패키지 파일 포맷으로 압축한 파일이다.

# JarFile Structure

META-INF/
    MANIFEST.MF
woowa/
    camp/
        MyApplication.class
        MyJspCafe.class

War도 마찬가지이다. 다만 서블릿, JSP 등 웹 애플리케이션에 필요한 메타 데이터도 함께 압축한다는 점에서 차이가 있다.

META-INF/
    MANIFEST.MF
WEB-INF/
    web.xml
    jsp/
        helloWorld.jsp
    classes/
        static/
        templates/
        application.properties
    lib/
        // *.jar files as libs



War 배포와 Tomcat 디렉토리 구조

War를 외장 Tomcat에 배포했을 때, 클래스 / JSP / 설정 파일 / 정적 리소스 등의 메타 데이터들이 Tomcat의 어느 디렉토리에 위치해 있는지 숙지하는 것이 매우 중요하다.

결론부터 말하면 아래와 같이 정리할 수 있다.

  • (local) main/java → (tomcat) webapps/'application_context'/WEB-INF/classes
  • (local) main/resources → (tomcat) 동일 경로
  • (local) main/webapp/WEB-INF → (tomcat) 동일 경로
  • (local) main/webapp/static → (tomcat) webapps/'application_context'/static
image image

그림에서 표시된 것처럼 Tomcat의 WEB-INF는 private한 영역이다. 즉 클라이언트는 WEB-INF 경로로 요청을 보내도, 접근할 수는 없다. JSP는 컴파일하면 결국 어떤 로직이 담겨있는 Java 파일이다. 조금 더 보안성을 생각한다면, 로컬에서 JSP는 main/webapp/WEB-INF 경로의 하위에 위치하는 것이 안전할 것이다.



Application Context

Run/Debug Configurations - Deployment 에서 Application context 를 설정할 수 있다.



이는 웹 애플리케이션의 루트 URL 경로를 정의하는 것이다. 만약 Application context가 /test라면, 우리의 애플리케이션은 /test 하위에서 시작되는 것이다. IDE를 통해 로컬에 설치한 Tomcat으로 Deploy 하게되면, apache-tomcat/webapps/ 경로에 Application Context로 설정한 이름으로 디렉토리를 만들고, 그 하위에 파일 정보들이 있는 것을 확인할 수 있다.



JSP 에서는 절대경로를 권고하지 않는다. Application Context가 바뀌면 절대경로가 모두 깨지기 때문이다. JSP 에서는 PageContext를 기본으로 참조할 수 있는데, 여기서 Application Context 정보를 가져와서 href Prefix에 사용하자.

<form name="login" action="${pageContext.request.contextPath}/users/edit/${user.id}" method="post" class="login-form">

마찬가지로 애플리케이션 코드에서 정적 리소스에 대해 절대경로를 쓰고 있는 부분은 HttpServletRequest.getServletContext().getPathContext(); 를 활용하자.

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // something ..
    String contextPath = req.getContextPath();
    resp.sendRedirect(contextPath + "/users");
}



Servlet 객체 의존성 주입

Servlet 객체가 의존하고 있는 객체들에 대해 의존성 주입을 할 수 있는 방법에 대해 살펴보고자 한다.

ServletContextListener

ServletContextListener를 활용해서 의존성을 주입할 수 있다. 리스너는 톰캣 로드 시점에 실행되므로, 로드 시점에 ServletContext에 구현한 클래스의 인스턴스를 넣어둔다.

@WebListener
public class AppContextListener implements ServletContextListener {

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

    private static final UserRepository userRepository = new InMemoryUserRepository();
    private static final UserService userService = new UserService(userRepository);

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        log.debug("AppContextListener - contextInitialized start");
        ServletContext context = sce.getServletContext();
        context.setAttribute("userService", userService);
        log.debug("AppContextListener - contextInitialized end");
    }
}

서블릿 클래스의 init메서드 즉, 서블릿의 초기화 시점에 ServletContext에서 인스턴스를 가져와서 의존성을 주입할 수 있다.

@WebServlet(name = "userServlet", value = "/users")
public class UserServlet extends HttpServlet {

    private static final Logger log = LoggerFactory.getLogger(UserServlet.class);
    private UserRepository userRepository;

    @Override
    public void init(ServletConfig config) throws ServletException {
        ServletContext context = config.getServletContext();
        userRepository = (UserRepository) context.getAttribute("userRepository");
        if (this.userRepository == null) {
            throw new ServletException("UserRegistrationServlet -> UserRepository is not initialized");
        }
    }
}

한계점

  • 컴파일 타임에 의존성 주입 여부가 판단이 안되고, 런타임 시점에 파악이 된다.

  • 만약 init 과정에서 userRepository가 null이라서 ServletException을 throw했다고 해보자.

  • 그럼 해당 서블릿 객체를 생성하는게 실패한 것이지, 톰캣 (서블릿 컨테이너) 구동 자체가 실패한 것이 아니다.

  • 이후 해당 서블릿에 매핑된 URL로 요청을 보낼 때마다 init 초기화 메서드를 실행한다. 서블릿 컨테이너 생성을 못했기 때문에 계속 생성을 시도하는 것이다.



테스트 데이터 롤백 방법

테스트 데이터 롤백을 일괄적으로 처리하는 방법에 대해 고민이 많았다. 여러 래퍼런스를 참고하며 학습을 진행했고, 아래와 같은 코드를 구현했다. JUnit 프레임워크의 BeforeEachCallback, BeforeEachCallback 인터페이스를 구현한 코드이다.

public class ArticleDBSetupExtension implements BeforeEachCallback, BeforeEachCallback {

    private final DatabaseConnector connector;

    public ArticleDBSetupExtension() {
        this.connector = new DatabaseConnector();
    }

    @Override
    public void beforeEach(ExtensionContext extensionContext) throws Exception {
        createTables();
    }

    @Override
    public void afterEach(ExtensionContext extensionContext) throws Exception {
        dropTables();
    }

    private void createTables() {
        try (Connection connection = connector.getConnection();
             Statement statement = connection.createStatement()) {

            String createTableSQL = """
                    CREATE TABLE IF NOT EXISTS articles (
                        id BIGINT AUTO_INCREMENT PRIMARY KEY,
                        author_id BIGINT,
                        title VARCHAR(255) NOT NULL,
                        content VARCHAR(5000) NOT NULL,
                        hits INT DEFAULT 0,
                        created_at DATE NOT NULL
                    )
                    """;

            statement.execute(createTableSQL);
        } catch (Exception e) {
            throw new RuntimeException("Failed to create tables", e);
        }
    }

    private void dropTables() {
        try (var connection = connector.getConnection();
             var statement = connection.createStatement()) {

            String dropTableSQL = "DROP TABLE IF EXISTS articles";
            statement.execute(dropTableSQL);
        } catch (Exception e) {
            throw new RuntimeException("Failed to drop tables", e);
        }
    }
}

해당 클래스를 활용하고 싶다면, 테스트 클래스에 @ExtendWith 애노테이션으로 명시만 해주면 된다.

@Nested
    @DisplayName("Describe_게시글을 저장하는 기능은")
    @ExtendWith(ArticleDBSetupExtension.class)
    class SaveTest { ... }

👼 개인 활동을 기록합시다.

개인 활동 페이지

🧑‍🧑‍🧒‍🧒 그룹 활동을 기록합시다.

그룹 활동 페이지

🎤 미니 세미나

미니 세미나

🤔 기술 블로그 활동

기술 블로그 활동

📚 도서를 추천해주세요

추천 도서 목록

🎸 기타

기타 유용한 학습 링크

Clone this wiki locally