2026. 3. 26. 21:32ㆍ대우개발원 수업 내용/spring boot, framework
[TIL] Spring MVC & 데이터베이스 연동 핵심 기능 구현
1. 영속 계층 (Persistence Layer) 구축
- MyBatis 도입: 데이터베이스와의 직접적인 통신을 위해 MyBatis 설정
- 객체 매핑 (VO): DB 테이블 구조와 1:1로 매핑되는 TodoVO 클래스 생성
- Mapper 구성:
- 인터페이스: SQL을 호출할 자바 메서드(getTime, insert, selectAll) 정의
- XML: 실제 실행될 SQL 쿼리를 분리 작성. 전체 조회(selectAll) 시 최근 등록 순(ORDER BY ... DESC) 정렬 적용
- 검증: JUnit5를 활용해 DB 연결 및 쿼리 실행 정상 동작 여부 테스트 완료
2. 서비스 계층 (Service Layer) 구현
- 데이터 변환 (ModelMapper): 화면용 DTO(TodoDTO)와 DB용 VO(TodoVO) 간의 데이터 변환을 ModelMapper를 통해 자동화
- 핵심 비즈니스 로직: 신규 할 일 등록(register) 및 전체 목록 조회(getAll) 기능을 구현하여 컨트롤러와 영속 계층을 연결
3. 컨트롤러 및 데이터 검증 (Controller & Validation)
- 엄격한 유효성 검사 (Bean Validation): TodoDTO에 @NotEmpty(필수 입력), @Future(미래 날짜) 어노테이션을
적용해 잘못된 데이터 유입 원천 차단 - 에러 핸들링: BindingResult로 검증 에러를 포착. 에러 발생 시 RedirectAttributes의 addFlashAttribute를 사용해
기존 입력 폼으로 에러 정보와 함께 리다이렉트 처리 - PRG 패턴 적용: 데이터 등록 후 새로고침 시 데이터가 중복 등록되는 것을 막기 위해 Post-Redirect-Get 패턴을
적용하여 목록 페이지(/list)로 리다이렉트
4. 프론트엔드 및 사용자 경험 (JSP & UI)
- UI 구성: Bootstrap을 활용하여 깔끔한 입력 폼과 데이터 테이블 구성
- 동적 렌더링 (JSTL): 서버에서 전달받은 dtoList 데이터를 <c:forEach> 태그를 이용해 화면 테이블에 반복 출력
- 에러 피드백 (JavaScript): 서버 유효성 검사에서 발생한 에러 메시지를 JS 객체로 변환하여 브라우저 콘솔에 출력,
디버깅 환경 개선
5. 환경 설정 최적화 (Configuration)
- 의존성 관리 (build.gradle): Spring 6 및 Tomcat 10 (Jakarta EE) 환경에 맞춰 JSTL 3.0, Hibernate Validator 등
라이브러리 의존성 최적화 - 쿼리 로깅 (Log4j2): 로깅 레벨을 TRACE로 설정하여, MyBatis가 실행하는 실제 SQL 쿼리와
바인딩 파라미터 값을 콘솔에서 상세 추적할 수 있도록 설정
Directory structure:
└── ynkite-springex_web/
├── gradlew
├── gradlew.bat
├── gradle/
│ └── wrapper/
│ └── gradle-wrapper.properties
└── src/
├── main/
│ ├── java/
│ │ ├── lombok.config
│ │ └── com/
│ │ └── example/
│ │ └── springex_web/
│ │ ├── HelloServlet.java
│ │ ├── config/
│ │ │ └── ModelMapperConfig.java
│ │ ├── controller/
│ │ │ ├── SampleController.java
│ │ │ ├── TodoController.java
│ │ │ ├── exception/
│ │ │ │ └── CommonExceptionAdvice.java
│ │ │ └── formatter/
│ │ │ └── LocalDateFormatter.java
│ │ ├── domain/
│ │ │ └── TodoVO.java
│ │ ├── dto/
│ │ │ └── TodoDTO.java
│ │ ├── mapper/
│ │ │ ├── TimeMapper.java
│ │ │ ├── TimeMapper2.java
│ │ │ └── TodoMapper.java
│ │ └── service/
│ │ ├── TodoService.java
│ │ └── TodoServiceImpl.java
│ ├── resources/
│ │ ├── log4j2.xml
│ │ └── mapper/
│ │ ├── TimeMapper2.xml
│ │ └── TodoMapper.xml
│ └── webapp/
│ ├── index.jsp
│ ├── resources/
│ │ └── test.html
│ └── WEB-INF/
│ ├── root-context.xml
│ ├── servlet-context.xml
│ ├── web.xml
│ └── views/
│ ├── custom404.jsp
│ ├── hello.jsp
│ └── todo/
│ ├── list.jsp
│ └── register.jsp
└── test/
└── java/
└── com/
└── example/
└── springex_web/
├── mapper/
│ └── TodoMapperTests.java
└── service/
└── TodoServiceTests.java
Files Content:
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: src/main/java/lombok.config
================================================
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
================================================
FILE: src/main/java/com/example/springex_web/HelloServlet.java
================================================
package com.example.springex_web;
import java.io.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello World!";
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}
public void destroy() {
}
}
================================================
FILE: src/main/java/com/example/springex_web/config/ModelMapperConfig.java
================================================
package com.example.springex_web.config;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
// bean 설정에 대한 클래스다라는 것을 보여줌
public class ModelMapperConfig {
@Bean
public ModelMapper getMapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setMatchingStrategy(MatchingStrategies.LOOSE);
return modelMapper;
}
}
================================================
FILE: src/main/java/com/example/springex_web/controller/SampleController.java
================================================
package com.example.springex_web.controller;
import com.example.springex_web.dto.TodoDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.time.LocalDate;
@Controller
@Log4j2
public class SampleController {
@GetMapping("/hello")
public void hello() { log.info("hello..............");}
@GetMapping("/ex1")
public void ex1(String name, int age) {
log.info("ex1......");
log.info("name: " + name);
log.info("age: " + age);
}
@GetMapping("/ex2")
public void ex2(@RequestParam(name = "name", defaultValue = "AAA") String name,
@RequestParam(name = "age", defaultValue = "20")int age) {
log.info("ex2.........");
log.info("name: " + name);
log.info("age: " + age);
}
@GetMapping("/ex3")
public void ex3(@RequestParam(name = "dueDate", defaultValue = "2026-03-24") LocalDate dueDate) {
// public void ex3(@RequestParam("dueDate") LocalDate dueDate) {
log.info("ex3..........");
log.info("dueDate: " + dueDate);
}
@GetMapping("/ex4")
public void ex4(Model model) {
log.info(".........");
log.info("message" , "Hello World");
}
@GetMapping("/ex4_1")
public void ex4Extra(@ModelAttribute("dto") TodoDTO todoDTO, Model model) {
log.info(todoDTO);
}
@GetMapping("/ex5")
public String ex5(RedirectAttributes redirectAttributes) {
redirectAttributes.addAttribute("name", "ABC");
redirectAttributes.addFlashAttribute("result", "success");
return "redirect:/ex6";
}
@GetMapping("/ex6")
public void ex6() {
}
@GetMapping("/ex7")
public void ex7(@RequestParam("p1") String p1,@RequestParam("p2") int p2) {
log.info("p1............" +p1);
log.info("p2............" +p2);
}
}
================================================
FILE: src/main/java/com/example/springex_web/controller/TodoController.java
================================================
package com.example.springex_web.controller;
import com.example.springex_web.dto.TodoDTO;
import com.example.springex_web.service.TodoService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/todo")
@Log4j2
@RequiredArgsConstructor
public class TodoController {
private final TodoService todoService;
@RequestMapping("/list")
public void list(Model model){
log.info("todo list........");
model.addAttribute("dtoList", todoService.getAll());
}
// @RequestMapping(value ="/register", method= RequestMethod.GET) //GETMAPPING
@GetMapping("/register")
public void register() {
log.info("todo register........");
}
// @PostMapping("/register")
// public void registerPost(TodoDTO todoDTO) {
// log.info("POST todo register..........");
// log.info(todoDTO);
// }
// @PostMapping("/register")
// public String registerPost(TodoDTO todoDTO , RedirectAttributes redirectAttributes) {
// log.info("POST todo register..........");
// log.info(todoDTO);
// return "redirect:/todo/list";
// }
@PostMapping("/register")
public String registerPost(@Valid TodoDTO todoDTO ,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("POST todo register..........");
if(bindingResult.hasErrors()) {
log.info("has error.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
return "redirect:/todo/register";
}
log.info(todoDTO);
todoService.register(todoDTO);
return "redirect:/todo/list";
}
}
================================================
FILE: src/main/java/com/example/springex_web/controller/exception/CommonExceptionAdvice.java
================================================
package com.example.springex_web.controller.exception;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.Arrays;
@ControllerAdvice
@Log4j2
public class CommonExceptionAdvice {
@ResponseBody
@ExceptionHandler(NumberFormatException.class)
public String exceptNumber(NumberFormatException numberFormatException){
log.error("-----------------------------------");
log.error(numberFormatException.getMessage());
return "NUMBER FORMAT EXCEPTION";
}
@ResponseBody
@ExceptionHandler(Exception.class)
public String exceptCommon(Exception exception){
log.error("-----------------------------------");
log.error(exception.getMessage());
StringBuffer buffer = new StringBuffer("<ul>");
buffer.append("<li>" +exception.getMessage()+"</li>");
Arrays.stream(exception.getStackTrace()).forEach(stackTraceElement -> {
buffer.append("<li>"+stackTraceElement+"</li>");
});
buffer.append("</ul>");
return buffer.toString();
}
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String notFound() {
return "custom404";
}
}
================================================
FILE: src/main/java/com/example/springex_web/controller/formatter/LocalDateFormatter.java
================================================
package com.example.springex_web.controller.formatter;
import org.springframework.format.Formatter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class LocalDateFormatter implements Formatter<LocalDate> {
@Override
public LocalDate parse(String text, Locale locale) {
return LocalDate.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
@Override
public String print(LocalDate object, Locale locale) {
return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(object);
}
}
================================================
FILE: src/main/java/com/example/springex_web/domain/TodoVO.java
================================================
package com.example.springex_web.domain;
import lombok.*;
import java.time.LocalDate;
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TodoVO {
private Long tno;
private String title;
private LocalDate dueDate;
private boolean finished;
private String writer;
}
================================================
FILE: src/main/java/com/example/springex_web/dto/TodoDTO.java
================================================
package com.example.springex_web.dto;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TodoDTO {
private Long tno;
@NotEmpty
private String title;
@Future
private LocalDate dueDate;
private boolean finished;
@NotEmpty
private String writer;
}
================================================
FILE: src/main/java/com/example/springex_web/mapper/TimeMapper.java
================================================
package com.example.springex_web.mapper;
import org.apache.ibatis.annotations.Select;
public interface TimeMapper {
@Select("select now()")
String getTime();
}
================================================
FILE: src/main/java/com/example/springex_web/mapper/TimeMapper2.java
================================================
package com.example.springex_web.mapper;
public interface TimeMapper2 {
String getNow();
}
================================================
FILE: src/main/java/com/example/springex_web/mapper/TodoMapper.java
================================================
package com.example.springex_web.mapper;
import com.example.springex_web.domain.TodoVO;
import java.util.List;
public interface TodoMapper {
String getTime();
void insert(TodoVO todoVo);
List<TodoVO> selectAll(); //selectAll(); -> DB에 있는 모든 데이터 조회
}
================================================
FILE: src/main/java/com/example/springex_web/service/TodoService.java
================================================
package com.example.springex_web.service;
import com.example.springex_web.dto.TodoDTO;
import java.util.List;
public interface TodoService {
void register(TodoDTO todoDTO);
List<TodoDTO> getAll();
}
================================================
FILE: src/main/java/com/example/springex_web/service/TodoServiceImpl.java
================================================
package com.example.springex_web.service;
import com.example.springex_web.domain.TodoVO;
import com.example.springex_web.dto.TodoDTO;
import com.example.springex_web.mapper.TodoMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@Log4j2
@RequiredArgsConstructor
public class TodoServiceImpl implements TodoService{
private final TodoMapper todoMapper;
private final ModelMapper modelMapper;
public void register(TodoDTO todoDTO) {
log.info(modelMapper);
TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
log.info(todoVO);
todoMapper.insert(todoVO);
}
@Override
public List<TodoDTO> getAll() {
List<TodoDTO> dtoList = todoMapper.selectAll().stream()
.map(vo -> modelMapper.map(vo, TodoDTO.class))
.collect(Collectors.toList());
return dtoList;
}
}
================================================
FILE: src/main/resources/log4j2.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<logger name="org.springframework" level="INFO" additivity="false">
<appender-ref ref="Console" />
</logger>
<logger name="com.example" level="INFO" additivity="false">
<appender-ref ref="Console" />
</logger>
<logger name="com.example.springex_web.mapper" level="TRACE" additivity="false">
<appender-ref ref="Console" />
</logger>
<root level="INFO" additivity="false">
<AppenderRef ref="Console"/>
</root>
</Loggers>
</Configuration>
================================================
FILE: src/main/resources/mapper/TimeMapper2.xml
================================================
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springex.mapper.TimeMapper2">
<select id="getNow" resultType="string">
select now()
</select>
</mapper>
================================================
FILE: src/main/resources/mapper/TodoMapper.xml
================================================
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springex_web.mapper.TodoMapper">
<select id="getTime" resultType="string">
SELECT now()
</select>
<insert id="insert">
insert into tbl_todo(title, dueDate, writer) values (#{title}, #{dueDate}, #{writer})
</insert>
<select id="selectAll" resultType="com.example.springex_web.domain.TodoVO">
SELECT * FROM tbl_todo order by tno desc
</select>
</mapper>
================================================
FILE: src/main/webapp/index.jsp
================================================
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>JSP - Hello World</title>
</head>
<body>
<h1><%= "Hello World!" %>
</h1>
<br/>
<a href="hello-servlet">Hello Servlet</a>
</body>
</html>
================================================
FILE: src/main/webapp/resources/test.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<!--<h1>Hello, world!</h1>-->
<div class ="container-fluid">
<div class="row">
<!-- <h1>Header</h1>-->
<div class="col">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
<!-- header 끝-->
</div>
<div class="row content">
<!-- <h1>Content</h1>-->
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<p class="card-text">With supporting text below as a natural lead-in to additional content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
</div>
</div>
<!-- row content 끝 -->
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<!-- <h1>Footer</h1>-->
<div class="row fixed-botom" style="z-index: -100">
<footer class="py-1 my-1">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/root-context.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!--<beans xmlns="http://www.springframework.org/schema/beans"-->
<!-- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"-->
<!-- xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring.xsd">
<!-- <context:component-scan base-package="com.example.springex.sample"></context:component-scan>-->
<!-- <bean class="com.example.springex.sample.SampleDAO"></bean>-->
<!-- <bean class="com.example.springex.sample.SampleService"></bean>-->
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="org.mariadb.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mariadb://localhost:3306/webdb"></property>
<property name="username" value="webuser"></property>
<property name="password" value="1234"></property>
<property name="dataSourceProperties">
<props>
<prop key="cachePrepStmts">true</prop>
<prop key="prepStmtCacheSize">250</prop>
<prop key="prepStmtCacheSqlLimit">2048</prop>
</props>
</property>
</bean>
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:/mapper/*"></property>
</bean>
<mybatis:scan base-package="com.example.springex_web.mapper"></mybatis:scan>
<context:component-scan base-package="com.example.springex_web.config"></context:component-scan>
<context:component-scan base-package="com.example.springex_web.service"></context:component-scan>
</beans>
================================================
FILE: src/main/webapp/WEB-INF/servlet-context.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<mvc:annotation-driven></mvc:annotation-driven>
<mvc:resources mapping="/resources/**" location="/resources/"></mvc:resources>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<context:component-scan base-package="com.example.springex_web.controller"/>
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="formatters">
<set>
<bean class="com.example.springex_web.controller.formatter.LocalDateFormatter"/>
</set>
</property>
</bean>
<mvc:annotation-driven conversion-service="conversionService" />
</beans>
================================================
FILE: src/main/webapp/WEB-INF/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">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
WEB-INF/root-context.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>WEB-INF/servlet-context.xml</param-value>
</init-param>
<init-param>
<param-name>throwExceptionIfNoHandlerFound</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<servlet-name>appServlet</servlet-name>
</filter-mapping>
</web-app>
================================================
FILE: src/main/webapp/WEB-INF/views/custom404.jsp
================================================
<%--
Created by IntelliJ IDEA.
User: USER
Date: 2026-03-25
Time: 오후 7:35
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Oops! 페이지를 찾을 수 없습니다!</h1>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/views/hello.jsp
================================================
<%--
Created by IntelliJ IDEA.
User: USER
Date: 2026-03-24
Time: 오후 7:44
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>Hello JSP</h1>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/views/todo/list.jsp
================================================
<%--
Created by IntelliJ IDEA.
User: USER
Date: 2026-03-26
Time: 오후 9:04
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<!--<h1>Hello, world!</h1>-->
<div class ="container-fluid">
<div class="row">
<!-- <h1>Header</h1>-->
<div class="col">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
<!-- header 끝-->
</div>
<div class="row content">
<!-- <h1>Content</h1>-->
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<table class="table">
<thead>
<tr>
<th scope="col">Tno</th>
<th scope="col">Title</th>
<th scope="col">Writer</th>
<th scope="col">DueDate</th>
<th scope="col">Finished</th>
</tr>
</thead>
<tbody>
<c:forEach items="${dtoList}" var="dto">
<tr>
<th scope="row"><c:out value="${dto.tno}"/></th>
<td><c:out value="${dto.title}"/></td>
<td><c:out value="${dto.writer}"/></td>
<td><c:out value="${dto.dueDate}"/></td>
<td><c:out value="${dto.finished}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- row content 끝 -->
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<!-- <h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>
</html>
================================================
FILE: src/main/webapp/WEB-INF/views/todo/register.jsp
================================================
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<form action="/todo/register" method="post">
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control" placeholder="Title">
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control">
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control" placeholder="Writer">
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</div>
</form>
<script>
const serverValidResult = {}
<c:forEach items="${errors}" var="error">
serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
</c:forEach>
console.log(serverValidResult)
</script>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
<%-- <h1>Content</h1>--%>
</div>
<div class="row footer">
<h1>Footer</h1>
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
================================================
FILE: src/test/java/com/example/springex_web/mapper/TodoMapperTests.java
================================================
package com.example.springex_web.mapper;
import com.example.springex_web.domain.TodoVO;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.LocalDate;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
@Autowired(required = false)
private TodoMapper todoMapper;
@Test
public void testGetTime() {
log.info(todoMapper.getTime());
}
@Test
public void testInsert() {
TodoVO todoVO = TodoVO.builder()
.title("스프링 테스트")
.dueDate(LocalDate.of(2026, 03, 26))
.writer("user00")
.build();
todoMapper.insert(todoVO);
}
@Test
public void testSelectAll() {
List<TodoVO> voList = todoMapper.selectAll();
voList.forEach(vo -> log.info(vo));
}
}
================================================
FILE: src/test/java/com/example/springex_web/service/TodoServiceTests.java
================================================
package com.example.springex_web.service;
import com.example.springex_web.dto.TodoDTO;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.LocalDate;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoServiceTests {
@Autowired
private TodoService todoService;
@Test
public void testRegister() {
TodoDTO todoDTO = TodoDTO.builder()
.title("Test........")
.dueDate(LocalDate.now())
.writer("user1")
.build();
todoService.register(todoDTO);
}
}
MyBatis와 스프링을 이용한 영속 처리
• MyBatis를 이용해서 SQL을 처리하고 테스트
• 개발의 단계
1 • VO 클래스 개발
2 • Mapper인터페이스 개발
3 • XML을 이용해서 SQL 작성
4 • 테스트 코드의 개발
1 • VO 클래스 개발
domian 패키지 생성

TodoVo 클래스 만들기

package com.example.springex_web.domain;
import lombok.*;
import java.time.LocalDate;
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TodoVo {
private Long tno;
private String title;
private LocalDate dueDate;
private boolean finished;
private String writer;
}
TodoMapper 인터페이스 만듬

TodoMapper.xml 만듬

TodoMapperTests만듬




package com.example.springex_web.mapper;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.*;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
@Autowired(required = false)
private TodoMapper todoMapper;
@Test
public void testGetTime() {
log.info(todoMapper.getTime());
}
}
컨트롤러 자바 코드 오류나는거 수정

import com.example.springex_web.dto.TodoDTO;

import com.example.springex_web.dto.TodoDTO;
test 코드를 수정
package com.example.springex_web.mapper;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.*;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
@Autowired(required = false)
private TodoMapper todoMapper;
@Test
public void testGetTime() {
log.info(todoMapper.getTime());
}
}

로그가 뜨면서 정상적으로 실행
log4j2.xml코드의 로그 내용을 수정
로그를 찍는 레벨에 대한 정보를 제시해주는 코드 추가
TRACE에 대한 레벨을 추가
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<logger name="org.springframework" level="INFO" additivity="false">
<appender-ref ref="console" />
</logger>
<logger name="com.example" level="INFO" additivity="false">
<appender-ref ref="console" />
</logger>
<logger name="com.example.springex_web.mapper" level="TRACE" additivity="false">
<appender-ref ref="console" />
</logger>
<root level="INFO" additivity="false">
<AppenderRef ref="console"/>
</root>
</Loggers>
</Configuration>
TodoMapper.xml 수정

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springex_web.mapper.TodoMapper">
<select id="getTime" resultType="string">
SELECT now()
</select>
<insert id="insert">
insert into tbl_todo(title, dueDate, writer) values (#{title}, #{dueDate}, #{writer})
</insert>
</mapper>
TodoMapperTests 코드에 추가
package com.example.springex_web.mapper;
import com.example.springex_web.domain.TodoVO;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.LocalDate;
import static org.junit.jupiter.api.Assertions.*;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoMapperTests {
@Autowired(required = false)
private TodoMapper todoMapper;
@Test
public void testGetTime() {
log.info(todoMapper.getTime());
}
@Test
public void testInsert() {
TodoVO todoVO = TodoVO.builder()
.title("스프링 테스트")
.dueDate(LocalDate.of(2026, 03, 26))
.writer("user00")
.build();
todoMapper.insert(todoVO);
}
}
로그 확인

테이블에 데이터 추가

Todo 기능 개발(insert)
• 개발 순서
• TodoMapper -> TodoService -> TodoController -> JSP
TodoService 인터페이스 추가

TodoServiceImpl 클래스 추가
TodoService를 인플리먼츠 해줌
package com.example.springex_web.service;
public class TodoServiceImpl implements TodoService{
}
코드 추가

package com.example.springex_web.service;
import com.example.springex_web.domain.TodoVO;
import com.example.springex_web.dto.TodoDTO;
import com.example.springex_web.mapper.TodoMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
@Service
@Log4j2
@RequiredArgsConstructor
public class TodoServiceImpl implements TodoService{
private final TodoMapper todoMapper;
private final ModelMapper modelMapper;
public void register(TodoDTO todoDTO) {
log.info(modelMapper);
TodoVO todoVO = modelMapper.map(todoDTO, TodoVO.class);
log.info(todoVO);
todoMapper.insert(todoVO);
}
}
서비스 패키지 추가

TodoServiceTests를 만듬

로그가 제대로 안찍히는 문제가 있어서
plugins {
id 'java'
id 'war'
}
group 'com.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
ext {
junitVersion = '5.10.2'
springVersion = '6.1.6'
}
sourceCompatibility = '21'
targetCompatibility = '21'
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
dependencies {
compileOnly('jakarta.servlet:jakarta.servlet-api:6.0.0')
// testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
// testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
testImplementation platform("org.junit:junit-bom:${junitVersion}")
testImplementation "org.junit.jupiter:junit-jupiter"
// // https://mvnrepository.com/artifact/org.springframework/spring-core
// implementation group: 'org.springframework', name: 'spring-core', version: '6.2.15'
// // https://mvnrepository.com/artifact/org.springframework/spring-context
// implementation group: 'org.springframework', name: 'spring-context', version: '6.2.15'
// // https://mvnrepository.com/artifact/org.springframework/spring-test
// testImplementation group: 'org.springframework', name: 'spring-test', version: '6.2.15'
implementation "org.springframework:spring-core:${springVersion}"
implementation "org.springframework:spring-context:${springVersion}"
testImplementation "org.springframework:spring-test:${springVersion}"
implementation "org.springframework:spring-webmvc:${springVersion}"
implementation "org.springframework:spring-jdbc:${springVersion}"
implementation "org.springframework:spring-tx:${springVersion}"
implementation "org.mybatis:mybatis:3.5.15"
implementation "org.mybatis:mybatis-spring:3.0.3"
implementation("org.mariadb.jdbc:mariadb-java-client:3.3.3")
compileOnly("org.projectlombok:lombok:1.18.44")
annotationProcessor("org.projectlombok:lombok:1.18.44")
testCompileOnly("org.projectlombok:lombok:1.18.44")
testAnnotationProcessor("org.projectlombok:lombok:1.18.44")
// Source: https://mvnrepository.com/artifact/com.zaxxer/HikariCP
implementation("com.zaxxer:HikariCP:7.0.2")
// Source: https://mvnrepository.com/artifact/org.modelmapper/modelmapper
implementation("org.modelmapper:modelmapper:3.1.1")
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
// implementation("org.apache.logging.log4j:log4j-slf4j-impl:2.17.2")
implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.22.1'
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api
implementation("org.apache.logging.log4j:log4j-api:2.17.2")
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
implementation("org.apache.logging.log4j:log4j-core:2.17.2")
implementation("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2")
implementation("org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1")
implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'
}
test {
useJUnitPlatform()
}
추가

서비스 테스트 코드
package com.example.springex_web.service;
import com.example.springex_web.dto.TodoDTO;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.LocalDate;
@Log4j2
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/root-context.xml")
public class TodoServiceTests {
@Autowired
private TodoService todoService;
@Test
public void testRegister() {
TodoDTO todoDTO = TodoDTO.builder()
.title("Test........")
.dueDate(LocalDate.now())
.writer("user1")
.build();
todoService.register(todoDTO);
}
}

로그가 정상적으로 찍히고
테이블에도 데이터가 정상적으로 들어감

register.jsp 코드 수정
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 기존의 <h1>Header</h1> -->
<div class="row">
<div class="col">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" aria-current="page" href="#">Home</a>
<a class="nav-link" href="#">Features</a>
<a class="nav-link" href="#">Pricing</a>
<a class="nav-link disabled">Disabled</a>
</div>
</div>
</div>
</nav>
</div>
</div>
<!-- header end -->
<!-- 기존의 <h1>Header</h1>끝 -->
<div class="row content">
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<form action="/todo/register" method="post">
<div class="input-group mb-3">
<span class="input-group-text">Title</span>
<input type="text" name="title" class="form-control" placeholder="Title">
</div>
<div class="input-group mb-3">
<span class="input-group-text">DueDate</span>
<input type="date" name="dueDate" class="form-control">
</div>
<div class="input-group mb-3">
<span class="input-group-text">Writer</span>
<input type="text" name="writer" class="form-control" placeholder="Writer">
</div>
<div class="my-4">
<div class="float-end">
<button type="submit" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="row content">
<%-- <h1>Content</h1>--%>
</div>
<div class="row footer">
<h1>Footer</h1>
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1 ">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</body>
</html>
톰캣으로 실행하고
http://localhost:8080/todo/register 주소창에 입력

정상적으로 나옴
TodoController.java 코드 수정
package com.example.springex_web.controller;
import com.example.springex_web.dto.TodoDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/todo")
@Log4j2
public class TodoController {
@RequestMapping("/list")
public void list(Model model){
log.info("todo list........");
}
// @RequestMapping(value ="/register", method= RequestMethod.GET) //GETMAPPING
@GetMapping("/register")
public void register() {
log.info("todo register........");
}
// @PostMapping("/register")
// public void registerPost(TodoDTO todoDTO) {
// log.info("POST todo register..........");
// log.info(todoDTO);
// }
@PostMapping("/register")
public String registerPost(TodoDTO todoDTO , RedirectAttributes redirectAttributes) {
log.info("POST todo register..........");
log.info(todoDTO);
return "redirect:/todo/list";
}
}

서밋을 누르면

list로 연결되고

로그가 정상적으로 찍힘
web.xml에 추가

→ DTO가 서버로 옮겨지는 데이터
따라서 타이틀이 반드시 입력이 되어야함
아래는 그걸 수정한것
이 코드에서
package com.example.springex_web.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TodoDTO {
private Long tno;
private String title;
private LocalDate dueDate;
private boolean finished;
private String writer;
}
아래 코드로
Future 는 미래 값만 가져올 수 있음
(과거의 값만 가져올 수 있는 Past 어노테이션도 있음)
package com.example.springex_web.dto;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TodoDTO {
private Long tno;
@NotEmpty
private String title;
@Future
private LocalDate dueDate;
private boolean finished;
@NotEmpty
private String writer;
}
TodoControlller.java에 아래 코드 추가
@PostMapping("/register")
public String registerPost(@Valid TodoDTO todoDTO ,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("POST todo register..........");
if(bindingResult.hasErrors()) {
log.info("has error.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
return "redirect:/todo/register";
}
log.info(todoDTO);
return "redirect:/todo/list";
}
그래서 전체 코드는
package com.example.springex_web.controller;
import com.example.springex_web.dto.TodoDTO;
import jakarta.validation.Valid;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/todo")
@Log4j2
public class TodoController {
@RequestMapping("/list")
public void list(Model model){
log.info("todo list........");
}
// @RequestMapping(value ="/register", method= RequestMethod.GET) //GETMAPPING
@GetMapping("/register")
public void register() {
log.info("todo register........");
}
// @PostMapping("/register")
// public void registerPost(TodoDTO todoDTO) {
// log.info("POST todo register..........");
// log.info(todoDTO);
// }
// @PostMapping("/register")
// public String registerPost(TodoDTO todoDTO , RedirectAttributes redirectAttributes) {
// log.info("POST todo register..........");
// log.info(todoDTO);
// return "redirect:/todo/list";
// }
@PostMapping("/register")
public String registerPost(@Valid TodoDTO todoDTO ,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
log.info("POST todo register..........");
if(bindingResult.hasErrors()) {
log.info("has error.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
return "redirect:/todo/register";
}
log.info(todoDTO);
return "redirect:/todo/list";
}
}

값이 없는 상태에서 submit을 누르면 로그에 에러가 뜸

하지만 이렇게 하면 사용자가 에러를 확인할 수 없기 떄문에 프론트상에서 알아볼 수 있게
추가
register.jsp에서 에러를 확인할 수 있는 객체를 추가

/form끝나는 부분에 script 구문 추가
<script>
const serverValidResult = {}
<c:forEach items="${errors}" var="error">
serverValidResult['${error.getField()}'] = '${error.defaultMessage}'
</c:forEach>
console.log(serverValidResult)
</script>
위의 TodoController.java코드의 error

표시된 부분과 새로 추가한 구문을 확인
개발자 도구의 콘솔창을 열면 확인가능
title,writer는 필수 항목이라 입력하지 않으면 입력하라고 뜨고
날짜는 Futuer를 썼기 때문에 미래날짜를 입력하라고 뜬다


현재 TodoController에서 서비스를 받아주는 객체가 없기 때문에 추가

@RequiredArgsConstructor
private final TodoService todoService;
두 개 추가
아래 한 줄 추가
todoService.register(todoDTO);

이렇게 하면 데이터 베이스에 입력한 내용이 저장되어서 테이블에 반영됨


list를 조회하는 메서드만들기
TodoMapper.java에
List<TodoVO> selectAll(); //selectAll(); -> DB에 있는 모든 데이터 조회
추가
TodoMapper.xml에
<select id="selectAll" resultType="com.example.springex_web.domain.TodoVO">
SELECT * FROM tbl_todo order by tno desc
</select>
추가
확인을 위해 테스트 구문 추가
TodoMapperTests에 구문 추가
@Test
public void testSelectAll() {
List<TodoVO> voList = todoMapper.selectAll();
voList.forEach(vo -> log.info(vo));
}

정상적으로 실행되면서 로그가 제대로 뜸
TodoService.java에 아래 구문 추가

List<TodoDTO> getAll();
TodoServiceImpl에 아래 구문 추가

@Override
public List<TodoDTO> getAll() {
List<TodoDTO> dtoList = todoMapper.selectAll().stream()
.map(vo -> modelMapper.map(vo, TodoDTO.class))
.collect(Collectors.toList());
return dtoList;
}
TodoController 수정
현재는 로그만 찍는 메서드가 선언되어 있지만 서비스 객체를 가져오는 메서드를 추가

model.addAttribute("dtoList", todoService.getAll());
이후 뷰를 만들어야하는데
부트스트랩을 활용
아래 사진의 코드를 활용할 예정

기존의 test.html을 todo에다 list.jsp를 만듬

상단에 jstl 구문 추가

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
test.html에서 head와 body부분을 가져옴
아까 부트스트랩을 가져옴
<%--
Created by IntelliJ IDEA.
User: USER
Date: 2026-03-26
Time: 오후 9:04
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
</head>
<body>
<!--<h1>Hello, world!</h1>-->
<div class ="container-fluid">
<div class="row">
<!-- <h1>Header</h1>-->
<div class="col">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Features</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Pricing</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" aria-disabled="true">Disabled</a>
</li>
</ul>
</div>
</div>
</nav>
</div>
<!-- header 끝-->
</div>
<div class="row content">
<!-- <h1>Content</h1>-->
<div class="col">
<div class="card">
<div class="card-header">
Featured
</div>
<div class="card-body">
<h5 class="card-title">Special title treatment</h5>
<table class="table">
<thead>
<tr>
<th scope="col">Tno</th>
<th scope="col">Title</th>
<th scope="col">Writer</th>
<th scope="col">DueDate</th>
<th scope="col">Finished</th>
</tr>
</thead>
<tbody>
<c:forEach items="${dtoList}" var="dto">
<tr>
<th scope="row"><c:out value="${dto.tno}"/></th>
<td><c:out value="${dto.title}"/></td>
<td><c:out value="${dto.writer}"/></td>
<td><c:out value="${dto.dueDate}"/></td>
<td><c:out value="${dto.finished}"/></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- row content 끝 -->
<div class="row content">
<h1>Content</h1>
</div>
<div class="row footer">
<!-- <h1>Footer</h1>-->
<div class="row fixed-bottom" style="z-index: -100">
<footer class="py-1 my-1">
<p class="text-center text-muted">Footer</p>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
</body>
</html>
이렇게 하면
Submit을 눌렀을때 list화면이 정상적으로 뜨게 된다

'대우개발원 수업 내용 > spring boot, framework' 카테고리의 다른 글
| 자바프레임워크 9일차 springex_web 페이징 처리 (0) | 2026.03.30 |
|---|---|
| 자바 프레임워크 8일차 springex_web(Read, Delete, Update) (0) | 2026.03.27 |
| 자바프레임워크 6일차 springex 마무리 springex_web(자바 웹 개발 기본 환경 세팅 및 Servlet/JSP 구현) 만들기 (0) | 2026.03.25 |
| 자바 프레임워크 5 (Spring) (0) | 2026.03.24 |
| 자바 프레임워크 4 (0) | 2026.03.18 |