자바 프레임워크 7일차 springex_web(Spring MVC & 데이터베이스 연동 핵심 기능 구현)

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화면이 정상적으로 뜨게 된다