Spring Boot와 JSP, MyBatis로 웹 프로젝트 구성하기
기존 프로젝트에 Spring Boot를 적용하고, JSP 화면과 PostgreSQL 데이터베이스를 연결해보았다.
이 포스팅 하나로 완벽히 Spring Boot 를 시작하기 위해 포스팅을 남긴다.
최대한 간략하고 보기 쉽게 단문 형식으로 기록.
이번 글에서는 pom.xml 설정부터 Controller, Service, MyBatis XML Mapper, JSP 화면 구성까지의 과정을 정리한다.
1. Spring Boot 프로젝트 구조
프로젝트는 Maven 멀티 모듈 구조로 구성했다.
sp-web
└── sp-front
└── src
└── main
├── java
│ └── com.sp
│ └── com.sp.front.controller
│ └── com.sp.mapper
│ └── com.sp.service
├── resources
│ ├── application.properties
│ ├── db.properties
│ ├── log4j2-spring.xml
│ └── mapper
└── webapp
└── WEB-INF
└── viewsp-web은 부모 프로젝트이고, 실제 웹 애플리케이션은 sp-front 모듈에서 동작한다.
2. pom.xml 설정
먼저 부모 pom.xml에 Spring Boot 부모를 지정했다.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>Java 버전은 21로 설정했다.
<properties>
<java.version>21</java.version>
</properties>웹 애플리케이션이므로 Spring MVC를 사용하기 위해 Web Starter를 추가한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>현재 프로젝트는 Log4j2를 사용하기 때문에 Spring Boot 기본 로깅인 Logback을 제외했다.
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>그리고 Log4j2를 추가했다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>완성된 메인 pom.xml 은 다음과 같이 간략하다.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath />
</parent>
<groupId>com.sp-test</groupId>
<artifactId>sp-web</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<jdk.version>21</jdk.version>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<modules>
<module>sp-front</module>
</modules>
</project>3. JSP 사용을 위한 설정(실제 웹서비스 maven 모듈)
현재 화면 기술은 JSP이므로 WAR 포장을 사용한다.
<packaging>war</packaging>내부 Tomcat에서 JSP를 실행하려면 Jasper가 필요하다.
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>JSP에서 JSTL 태그를 사용하기 위해 관련 의존성도 추가했다.
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
</dependency>다음은 완성된 모듈 pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.sp-test</groupId>
<artifactId>sp-web</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>sp-front</artifactId>
<packaging>war</packaging>
<build>
<finalName>sp-front</finalName>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>JSP 파일은 외부에서 직접 접근할 수 없도록 WEB-INF/view 아래에 배치한다.
src/main/webapp/WEB-INF/view/index.jsp
src/main/webapp/WEB-INF/view/db-test.jsp뷰 리졸버는 다음과 같이 설정했다.
spring.mvc.view.prefix=/WEB-INF/view/
spring.mvc.view.suffix=.jsp이제 Controller에서 다음과 같이 반환하면:
return "index";Spring은 자동으로 다음 JSP를 찾는다.
/WEB-INF/view/index.jsp4. Spring Boot 실행 클래스
Spring Boot 실행을 담당하는 클래스는 다음과 같다. 메인 클래스가 되며, 이 클래스로 인해서 굳이 톰캣에 연동하지 않고
Run As - Java application 으로 실행하면 내부 톰캣이 시작한다.
package com.sp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class SpFrontApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(SpFrontApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpFrontApplication.class);
}
}@SpringBootApplication은 다음 기능을 포함한다.
컴포넌트 스캔
자동 설정
설정 클래스 등록
또한 SpringBootServletInitializer를 상속하면 WAR 형태로 외부 Tomcat에도 배포할 수 있다.
5. Controller에서 JSP 화면 호출하기
첫 화면을 보여주는 Controller는 다음과 같이 작성할 수 있다.
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}return "index"는 JSP 파일명을 직접 반환하는 것이 아니다. 앞서 설정한 prefix와 suffix가 자동으로 적용된다.
/WEB-INF/view/ + index + .jsp결과적으로 다음 파일이 실행된다.
/WEB-INF/view/index.jsp6. PostgreSQL 연결 설정
PostgreSQL 연결 정보는 별도의 db.properties 파일로 분리했다.
spring.datasource.url=jdbc:postgresql://contabo:5432/hh
spring.datasource.username=hh
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=org.postgresql.Driverapplication.properties에서 해당 파일을 불러온다.
spring.config.import=optional:classpath:db.propertiesPostgreSQL JDBC 드라이버는 다음과 같이 추가한다.
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>비밀번호가 포함된 db.properties는 Git에 올리면 안 되므로 .gitignore에 추가한다.
sp-front/src/main/resources/db.properties실제 운영 환경에서는 환경변수나 외부 설정 서버를 사용하는 편이 더 안전하다.
7. MyBatis 설정
MyBatis Spring Boot Starter를 추가했다.
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>4.0.1</version>
</dependency>MyBatis XML 매퍼 위치는 다음과 같이 설정한다.
mybatis.mapper-locations=classpath:/mapper/*.xml
mybatis.configuration.map-underscore-to-camel-case=truemap-underscore-to-camel-case 옵션을 활성화하면 다음과 같은 컬럼을:
created_atJava 객체의 다음 필드로 자동 매핑할 수 있다.
private LocalDateTime createdAt;8. Mapper 인터페이스와 XML
Mapper 인터페이스에는 SQL을 작성하지 않고 메서드만 선언한다.
package com.sp.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TestMapper {
List<Map<String, Object>> findTop10();
}실제 SQL은 XML 파일에서 관리한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sp.mapper.TestMapper">
<select id="findTop10" resultType="map">
SELECT *
FROM tb_url
LIMIT 10
</select>
</mapper>XML의 namespace는 Mapper 인터페이스의 전체 경로와 같아야 한다.
com.sp.mapper.TestMapperselect의 id도 인터페이스의 메서드명과 같아야 한다.
findTop10이 규칙이 맞아야 MyBatis가 인터페이스 메서드와 XML SQL을 연결할 수 있다.
9. Service 계층의 역할
Service는 Mapper를 단순히 한 번 호출하는 역할만 하는 것이 아니다. 업무 로직과 트랜잭션 경계를 담당한다.
@Service
public class TestService {
private final TestMapper testMapper;
public TestMapper(TestMapper testMapper) {
this.testMapper = testMapper;
}
public List<Map<String, Object>> findTop10() {
return testMapper.findTop10();
}
}현재 기능만 보면 Controller에서 Mapper를 직접 호출해도 동작한다.
가벼운 로직은 Mapper 만 호출해도 된다.
Controller → Mapper → DB하지만 실제 업무 로직에서는 여러 작업을 묶는 경우가 많다.
@Transactional
public void updateUrl(...) {
if (...) {
testMapper.updateA(...);
testMapper.updateB(...);
}
}이처럼 여러 Mapper 호출을 하나의 업무 단위로 묶고, 하나라도 실패하면 전체를 롤백해야 한다면 Service 계층이 적합하다.
따라서 Service는 반드시 모든 경우에 필요한 것은 아니지만, 다음 상황에서 특히 유용하다.
여러 Mapper 호출 조합
트랜잭션 처리
데이터 검증
조회 결과 가공
외부 API 호출
권한 및 업무 규칙 처리
10. /db-test Controller
DB 조회 화면을 담당하는 Controller는 다음과 같다.
@Controller
public class DbTestController {
private final TestService testService;
public DbTestController(TestService testService) {
this.testService = testService;
}
@GetMapping("/db-test")
public String dbTest(Model model) {
List<Map<String, Object>> rows = testService.findTop10();
List<String> columns = rows.isEmpty()
? Collections.emptyList()
: new ArrayList<>(rows.get(0).keySet());
model.addAttribute("columns", columns);
model.addAttribute("rows", rows);
return "db-test";
}
}생성자가 하나뿐이면 @Autowired를 붙이지 않아도 Spring이 자동으로 의존성을 주입한다.
Lombok을 사용하면 다음처럼 줄일 수 있다.
@Controller
@RequiredArgsConstructor
public class DbTestController {
private final TestService testService;
@GetMapping("/db-test")
public String dbTest(Model model) {
List<Map<String, Object>> rows = testService.findTop10();
model.addAttribute("rows", rows);
return "db-test";
}
}@RequiredArgsConstructor는 final 필드를 받는 생성자를 자동으로 만들어준다.
11. JSP에서 조회 결과 출력
db-test.jsp에서는 JSTL을 사용해 결과를 출력한다.
<%@ taglib prefix="c" uri="jakarta.tags.core"%>
<h1>tb_url 최근 10건</h1>
<c:choose>
<c:when test="${empty rows}">
<p>조회 결과가 없습니다.</p>
</c:when>
<c:otherwise>
<table>
<thead>
<tr>
<c:forEach items="${columns}" var="column">
<th>
<c:out value="${column}" />
</th>
</c:forEach>
</tr>
</thead>
<tbody>
<c:forEach items="${rows}" var="row">
<tr>
<c:forEach items="${columns}" var="column">
<td>
<c:out
value="${row[column]}"
default="" />
</td>
</c:forEach>
</tr>
</c:forEach>
</tbody>
</table>
</c:otherwise>
</c:choose>접속 주소는 다음과 같다.
http://localhost:8080/db-test12. Log4j2 설정
Spring Boot 기본 Logback 대신 Log4j2를 사용했다.
logging.config=classpath:log4j2-spring.xml예를 들어 프레임워크 로그는 WARN, 애플리케이션 로그는 INFO로 설정할 수 있다.
<Loggers>
<Logger name="com.sp" level="INFO"/>
<Logger name="org.springframework" level="WARN"/>
<Logger name="org.apache.tomcat" level="WARN"/>
<Root level="WARN">
<AppenderRef ref="Console"/>
</Root>
</Loggers>이렇게 하면 불필요한 Spring 내부 로그는 줄이고 애플리케이션 로그는 확인할 수 있다.
마무리
이번 구성에서 각 계층의 역할은 다음과 같이 정리할 수 있다.
JSP
↓
Controller
↓
Service
↓
Mapper Interface
↓
Mapper XML
↓
PostgreSQLController는 HTTP 요청과 화면 이동을 담당하고, Service는 업무 로직과 트랜잭션을 담당한다.
Mapper 인터페이스는 Java와 SQL의 연결 지점이며, 실제 SQL은 XML에서 관리한다. JSP는 Controller가 전달한 데이터를 화면에 출력한다.
단순한 조회만 있다면 Controller에서 Mapper를 직접 호출할 수도 있다.
하지만 여러 SQL을 조합하거나 트랜잭션이 필요한 순간부터는 Service 계층이 유지보수에 큰 도움이 된다. 프로젝트 규모와 변경 가능성을 고려해 계층을 선택하는 것이 가장 중요하다.