본문 바로가기

개발/BACK

SpringBoot QueryDSL 환경에서 Servlet 구현 예제

728x90

SpringBoot에서 ServletConfig를 구현하는 방법을 알아보기 전에, 먼저 Servlet이 무엇인지 이해하고, web.xml을 사용하는 전통적인 방법과 Config.java로 구현하는 현대적인 방법 두 가지를 모두 살펴보겠습니다.


1. Servlet이란 무엇인가?

Servlet은 Java 웹 애플리케이션의 핵심 컴포넌트로, 클라이언트의 HTTP 요청을 처리하고 응답을 생성하는 Java 클래스입니다.

1.1 Servlet의 특징

  • 서버 사이드 컴포넌트: 웹 서버에서 실행되는 Java 프로그램
  • 요청-응답 모델: HTTP 요청을 받아 처리하고 응답을 반환
  • 멀티스레드: 서블릿 컨테이너가 여러 요청을 동시에 처리
  • 생명주기 관리: 서블릿 컨테이너가 생성, 초기화, 실행, 소멸을 관리

1.2 Servlet 생명주기

1. 서블릿 컨테이너 시작
   ↓
2. init() 메서드 호출 (한 번만 실행)
   ↓
3. service() 메서드 호출 (요청마다 실행)
   ├── doGet()
   ├── doPost()
   └── doPut(), doDelete() 등
   ↓
4. destroy() 메서드 호출 (서블릿 컨테이너 종료 시)

1.3 기본 Servlet 예제

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;

public class MyServlet extends HttpServlet {
    
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        // 초기화 로직
    }
    
    @Override
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) 
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("Hello Servlet!");
    }
    
    @Override
    public void destroy() {
        // 정리 로직
    }
}

2. ServletConfig란?

ServletConfig는 서블릿의 초기화 파라미터와 서블릿 컨텍스트 정보를 제공하는 인터페이스입니다.

2.1 ServletConfig의 주요 메서드

  • getInitParameter(String name): 초기화 파라미터 값 가져오기
  • getInitParameterNames(): 모든 초기화 파라미터 이름 가져오기
  • getServletContext(): 서블릿 컨텍스트 가져오기
  • getServletName(): 서블릿 이름 가져오기
💡 팁: ServletConfig는 각 서블릿 인스턴스마다 하나씩 존재하며, ServletContext는 웹 애플리케이션 전체에 하나만 존재합니다.

3. SpringBoot 프로젝트 설정

먼저 SpringBoot 프로젝트에 필요한 의존성을 설정합니다.

3.1 build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.0'
    id 'io.spring.dependency-management' version '1.1.0'
}

dependencies {
    // Spring Boot
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    
    // QueryDSL
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    implementation 'com.querydsl:querydsl-apt:5.0.0:jakarta'
    
    // Servlet API (Jakarta EE)
    compileOnly 'jakarta.servlet:jakarta.servlet-api:6.0.0'
    
    // Database
    runtimeOnly 'com.h2database:h2'
    
    // Lombok
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    
    // QueryDSL Annotation Processor
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}
⚠️ 중요: Spring Boot 3.x는 Jakarta EE를 사용하므로 javax.servlet 대신 jakarta.servlet을 사용합니다.

4. web.xml을 사용한 Servlet 설정 (전통적 방법)

전통적인 Java 웹 애플리케이션에서는 web.xml 파일을 사용하여 서블릿을 등록하고 설정했습니다.

4.1 프로젝트 구조

src/main/
├── java/
│   └── com/example/
│       └── servlet/
│           └── CustomServlet.java
└── webapp/
    └── WEB-INF/
        └── web.xml

4.2 CustomServlet.java

package com.example.servlet;

import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class CustomServlet extends HttpServlet {
    
    private String initParam;
    
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        // web.xml에서 설정한 초기화 파라미터 가져오기
        initParam = config.getInitParameter("customParam");
        System.out.println("Servlet 초기화: " + initParam);
    }
    
    @Override
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) 
            throws ServletException, IOException {
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        out.println("<html><body>");
        out.println("<h1>Custom Servlet</h1>");
        out.println("<p>초기화 파라미터: " + initParam + "</p>");
        out.println("<p>Servlet 이름: " + getServletConfig().getServletName() + "</p>");
        out.println("</body></html>");
    }
    
    @Override
    protected void doPost(HttpServletRequest request, 
                         HttpServletResponse response) 
            throws ServletException, IOException {
        doGet(request, response);
    }
    
    @Override
    public void destroy() {
        System.out.println("Servlet 종료");
    }
}

4.3 web.xml 설정

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
         https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <display-name>SpringBoot Servlet App</display-name>
    
    <!-- 서블릿 정의 -->
    <servlet>
        <servlet-name>customServlet</servlet-name>
        <servlet-class>com.example.servlet.CustomServlet</servlet-class>
        
        <!-- 초기화 파라미터 -->
        <init-param>
            <param-name>customParam</param-name>
            <param-value>Hello from web.xml</param-value>
        </init-param>
        
        <!-- 로드 순서 (낮을수록 먼저 로드) -->
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <!-- 서블릿 매핑 -->
    <servlet-mapping>
        <servlet-name>customServlet</servlet-name>
        <url-pattern>/custom/*</url-pattern>
    </servlet-mapping>
    
    <!-- 컨텍스트 파라미터 (전역 설정) -->
    <context-param>
        <param-name>appName</param-name>
        <param-value>My SpringBoot App</param-value>
    </context-param>

</web-app>

4.4 web.xml 주요 요소 설명

요소 설명
<servlet> 서블릿 클래스를 정의
<servlet-mapping> URL 패턴과 서블릿을 연결
<init-param> 서블릿 초기화 파라미터 설정
<load-on-startup> 서버 시작 시 서블릿 로드 순서
<context-param> 웹 애플리케이션 전체 파라미터
⚠️ 주의: Spring Boot는 기본적으로 web.xml을 사용하지 않습니다. web.xml을 사용하려면 WAR 패키징이 필요합니다.

5. Config.java를 사용한 Servlet 설정 (현대적 방법)

Spring Boot에서는 Java Config를 사용하여 서블릿을 등록하는 것이 권장됩니다. ServletRegistrationBean을 사용하여 서블릿을 등록할 수 있습니다.

5.1 ServletConfig.java (방법 1: ServletRegistrationBean 사용)

package com.example.config;

import com.example.servlet.CustomServlet;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ServletConfig {
    
    @Bean
    public ServletRegistrationBean<CustomServlet> customServletRegistration() {
        ServletRegistrationBean<CustomServlet> registration = 
            new ServletRegistrationBean<>(new CustomServlet(), "/custom/*");
        
        // 초기화 파라미터 설정
        Map<String, String> initParams = new HashMap<>();
        initParams.put("customParam", "Hello from Config.java");
        initParams.put("appVersion", "1.0.0");
        registration.setInitParameters(initParams);
        
        // 서블릿 이름 설정
        registration.setName("customServlet");
        
        // 로드 순서 설정 (낮을수록 먼저 로드)
        registration.setLoadOnStartup(1);
        
        return registration;
    }
}

5.2 ServletConfig.java (방법 2: @WebServlet 어노테이션 사용)

서블릿 클래스에 @WebServlet 어노테이션을 사용할 수도 있습니다:

package com.example.servlet;

import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebInitParam;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(
    name = "customServlet",
    urlPatterns = {"/custom/*", "/api/custom/*"},
    initParams = {
        @WebInitParam(name = "customParam", value = "Hello from @WebServlet"),
        @WebInitParam(name = "appVersion", value = "1.0.0")
    },
    loadOnStartup = 1
)
public class CustomServlet extends HttpServlet {
    
    private String initParam;
    
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        initParam = config.getInitParameter("customParam");
        System.out.println("Servlet 초기화: " + initParam);
    }
    
    @Override
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) 
            throws ServletException, IOException {
        
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        
        out.println("<html><body>");
        out.println("<h1>Custom Servlet</h1>");
        out.println("<p>초기화 파라미터: " + initParam + "</p>");
        out.println("</body></html>");
    }
}
💡 팁: @WebServlet을 사용하려면 메인 애플리케이션 클래스에 @ServletComponentScan을 추가해야 합니다.

5.3 메인 애플리케이션 클래스에 @ServletComponentScan 추가

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
@ServletComponentScan(basePackages = "com.example.servlet")
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

5.4 ServletRegistrationBean의 주요 메서드

메서드 설명
setUrlMappings() URL 패턴 설정
setInitParameters() 초기화 파라미터 설정
setLoadOnStartup() 로드 순서 설정
setName() 서블릿 이름 설정
setEnabled() 서블릿 활성화/비활성화

6. 실제 예제: 커스텀 Servlet 구현

QueryDSL과 함께 사용하는 실제 예제를 살펴보겠습니다.

6.1 FileDownloadServlet.java

package com.example.servlet;

import com.example.model.entity.FileEntity;
import com.example.repository.FileRepository;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Optional;

public class FileDownloadServlet extends HttpServlet {
    
    @Autowired
    private FileRepository fileRepository;
    
    private String uploadDir;
    
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        // Spring Bean 주입을 위한 설정
        SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
        
        // 초기화 파라미터에서 업로드 디렉토리 가져오기
        uploadDir = config.getInitParameter("uploadDir");
        if (uploadDir == null) {
            uploadDir = "./uploads";
        }
    }
    
    @Override
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) 
            throws ServletException, IOException {
        
        String fileId = request.getParameter("id");
        
        if (fileId == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "File ID is required");
            return;
        }
        
        try {
            Long id = Long.parseLong(fileId);
            Optional<FileEntity> fileEntityOpt = fileRepository.findById(id);
            
            if (fileEntityOpt.isEmpty()) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found");
                return;
            }
            
            FileEntity fileEntity = fileEntityOpt.get();
            File file = new File(fileEntity.getFilePath());
            
            if (!file.exists()) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found on disk");
                return;
            }
            
            // 응답 헤더 설정
            response.setContentType(fileEntity.getContentType());
            response.setHeader("Content-Disposition", 
                "attachment; filename=\"" + fileEntity.getOriginalFileName() + "\"");
            response.setContentLengthLong(file.length());
            
            // 파일 전송
            try (FileInputStream fis = new FileInputStream(file);
                 OutputStream os = response.getOutputStream()) {
                
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = fis.read(buffer)) != -1) {
                    os.write(buffer, 0, bytesRead);
                }
                os.flush();
            }
            
        } catch (NumberFormatException e) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid file ID");
        } catch (Exception e) {
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, 
                "Error downloading file: " + e.getMessage());
        }
    }
}

6.2 ServletConfig.java에 등록

package com.example.config;

import com.example.servlet.FileDownloadServlet;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class ServletConfig {
    
    @Value("${file.upload-dir:./uploads}")
    private String uploadDir;
    
    @Bean
    public ServletRegistrationBean<FileDownloadServlet> fileDownloadServlet() {
        ServletRegistrationBean<FileDownloadServlet> registration = 
            new ServletRegistrationBean<>(
                new FileDownloadServlet(), 
                "/download/*"
            );
        
        // 초기화 파라미터 설정
        Map<String, String> initParams = new HashMap<>();
        initParams.put("uploadDir", uploadDir);
        registration.setInitParameters(initParams);
        
        registration.setName("fileDownloadServlet");
        registration.setLoadOnStartup(1);
        
        return registration;
    }
}

6.3 사용 예제

# 파일 다운로드
curl http://localhost:8080/download?id=1

# 또는 브라우저에서
http://localhost:8080/download?id=1

7. Servlet vs Controller 비교

Spring Boot에서는 대부분의 경우 @RestController를 사용하는 것이 권장되지만, 특정 상황에서는 Servlet이 유용할 수 있습니다.

항목 Servlet @RestController
사용 시기 낮은 레벨 제어 필요 시 대부분의 REST API
Spring 통합 수동 설정 필요 자동 통합
의존성 주입 SpringBeanAutowiringSupport 필요 자동 주입
예외 처리 수동 처리 @ExceptionHandler 사용 가능
성능 약간 빠름 (오버헤드 적음) 약간 느림 (추가 기능)
유지보수 복잡함 간단함
💡 권장사항: 일반적으로는 @RestController를 사용하고, 특별한 요구사항(예: 레거시 시스템 통합, 특정 성능 최적화)이 있을 때만 Servlet을 사용하세요.

8. 주의사항 및 Best Practice

8.1 Spring Bean 주입 주의사항

Servlet은 일반적으로 Spring 컨테이너 밖에서 생성되므로, 의존성 주입을 위해서는 특별한 처리가 필요합니다:

// 방법 1: SpringBeanAutowiringSupport 사용
@Override
public void init(ServletConfig config) throws ServletException {
    super.init(config);
    SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
}

// 방법 2: ApplicationContext에서 직접 가져오기
@Autowired
private ApplicationContext applicationContext;

@Override
protected void doGet(...) {
    FileRepository repository = applicationContext.getBean(FileRepository.class);
    // ...
}

8.2 Best Practice

  • 리소스 관리: 파일 스트림, 데이터베이스 연결 등을 try-with-resources로 관리
  • 예외 처리: 적절한 HTTP 상태 코드 반환
  • 보안: 파일 경로 검증, 권한 체크 등 보안 고려
  • 로깅: 적절한 로깅으로 디버깅 용이하게
  • 성능: 대용량 파일 전송 시 스트리밍 사용

8.3 web.xml vs Config.java 선택 가이드

상황 권장 방법
Spring Boot JAR 패키징 Config.java
Spring Boot WAR 패키징 Config.java (권장) 또는 web.xml
레거시 프로젝트 마이그레이션 web.xml (임시), 점진적 마이그레이션
새로운 프로젝트 Config.java 또는 @WebServlet
✅ 핵심 요약:
  • Servlet은 HTTP 요청을 처리하는 Java 컴포넌트
  • ServletConfig는 서블릿 초기화 파라미터를 제공
  • web.xml은 전통적인 설정 방법 (WAR 패키징 필요)
  • Config.java는 현대적인 설정 방법 (권장)
  • @WebServlet 어노테이션으로도 설정 가능
  • Spring Bean 주입 시 SpringBeanAutowiringSupport 사용
  • 대부분의 경우 @RestController 사용 권장

추가로 궁금한 점이 있으시면 댓글로 남겨주세요! 🚀

 

카카오톡 오픈채팅 링크

https://open.kakao.com/o/seCteX7h


참고 자료

 

728x90