⚠️ 빌드 과정을 자세하게 다시 포스팅했다. (링크)
지금까지 스프링 부트의 Starter를 사용해서 편리한 빌드만 해 봤는데 이번에 듣게 된 강의에서 스프링 부트 없는 스프링 프로젝트인 Spring legacy proejct로 빌드를 해보게 되었다.
강의에서는 전자정부 표준프레임워크를 통해 쉽게 레거시 프로젝트의 기본적인 구조를 만들었는데, 나는 M2 맥북이라 전자정부 프레임워크를 설치할 수 없었다. 대신 이클립스에 STS4를 설치해 보았지만 어쩐지 레거시 프로젝트 생성 버튼은 뜨지 않았고 ... 어차피 나는 인텔리제이를 쓰고 싶었기 때문에 인텔리제이를 통한 레거시 프로젝트 빌드를 시도해 보았다.
.
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ │ └── kr
│ │ │ └── board
│ │ ├── resources
│ │ └── webapp
│ │ ├── WEB-INF
│ │ │ ├── spring
│ │ │ │ ├── appServlet
│ │ │ │ │ └── servlet-context.xml
│ │ │ │ └── root-context.xml
│ │ │ └── views
│ │ ├── resources
│ │ └── web.xml
│ └── test
│ ├── java
│ └── resources
└── target
만들어야 할 기본 구조는 위와 같다.
처음에는 이 구조가 스프링에서 정의한 표준이라고 생각해서 열심히 스프링 문서를 뒤졌는데 아무리 뒤져보아도 내가 원하는 설명이 없었다. 알고보니 이것은 스프링이라기 보다는 메이븐의 구조라고 보는 것이 더 적절했다.
메이븐은 약속된 디렉토리 구조와 pom.xml라는 핵심 설정 파일을 기반으로 프로젝트를 빌드해주는 빌드 자동화 도구이다.
핵심 디렉토리와 설정 파일들을 살펴보자. 참고로 이 프로젝트는 게시판을 만드는 프로젝트이며 MyBatis를 사용한다.
.
├── SpringMVC01.iml //IntelliJ
├── mvnw //Maven
├── mvnw.cmd //Maven
├── pom.xml --> Maven 프로젝트 설정 파일
├── src
│ ├── main
│ │ ├── java --> Java 소스 코드
│ │ │ └── kr
│ │ │ └── board
│ │ │ ├── controller
│ │ │ │ └── BoardController.java
│ │ │ ├── entity
│ │ │ │ └── Board.java
│ │ │ └── mapper --> 매퍼 인터페이스 & 매핑 XML
│ │ │ ├── BoardMapper.java
│ │ │ └── BoardMapper.xml
│ │ ├── resources --> 설정 파일
│ │ └── webapp --> 웹 애플리케이션 관련
│ │ ├── WEB-INF
│ │ │ ├── spring --> 스프링 설정
│ │ │ │ ├── appServlet
│ │ │ │ │ └── servlet-context.xml
│ │ │ │ └── root-context.xml
│ │ │ └── views
│ │ │ └── template.jsp
│ │ ├── resources --> 웹앱 정적 리소스(js, css)
│ │ └── web.xml --> 웹앱 배포 설명자 파일
│ └── test --> 테스트 코드 및 리소스
│ ├── java
│ └── resources
└── target --> 빌드, 패키징 관련 (빌드 도구에 의해 자동 생성)
pom.xml :pom.xml이다. 프로젝트 정보, 의존성, 플러그인 등의 정보를 갖고 있다.<properties>, <dependencies>, <plugins>src/main/java : 소스 코드(.java)src/main/java/kr/board/mapper 디렉토리의 경우 매퍼 인터페이스 소스 코드와 매핑 XML 파일이 위치할 수 있다.src/main/resources/src/main/webapp : 웹 애플리케이션 관련 디렉토리/src/main/webapp/WEB-INF : 웹 애플리케이션 관련 메타 데이터와 리소스 포함WEB-INF 내부에 접근할 수 있다./src/main/webapp/WEB-INF/spring : 스프링 관련 디렉토리 
root-context.xml : 루트 웹 애플리케이션 컨텍스트에 관한 스프링 빈 설정을 포함한다. 데이터소스, 서비스, 리포지토리 등 애플리케이션 전반에 걸친 설정과 관련이 있다../appServlet/servlet-context.xml : 웹 계층(서블릿 컨텍스트)에 관한 스프링 빈 설정을 포함한다. 대표적으로 뷰 리졸버가 있다./src/main/webapp/WEB-INF/views : 뷰 디렉토리/src/main/webapp/resources : 웹 애플리케이션의 정적 리소스 파일.js, .css 파일을 포함한다./src/main/webapp/web.xml : 웹 애플리케이션의 배포 설명자 파일DispatcherServlet 설정이 들어간다.src/target : 빌드, 배포 관련 디렉토리.java) 컴파일된 자바 바이트코드 파일(.class)이 위치한다.
New Project에서 Jakarta EE를 선택한다.
Template → Web applicationLanguage → JavaBuild system → MavenJDK → JDK 1.8참고) Jakarta EE는 자바의 엔터프라이즈 에디션(EE) 플랫폼이다. 표준인 SE보다 확장된 범위를 다루며 이전에는 J2EE, Java EE라고 불렸으나 2017년 오라클에서 이클립스 재단으로 관할이 넘어가면서 Jakarta EE로 명칭이 변경되었다.

나는 Java 8(JDK 1.8)을 사용했으므로 Version → Java EE 8을 선택했다. Dependencies에 기본 선택되어 있는 Servlet 설정 그대로 생성해준다.

이렇게 webapp, WEB-INF 디렉토리를 가진 올드한 구조가 만들어졌다. 톰캣 WAS 연결은 이전에 해둔게 있어서 자동 연결되었다.
만약 연결해 둔 것이 없다면 Run/Debug Configuration에서 + 버튼 → Tomcat Server - local 선택하고 톰캣이 설치된 경로를 찾아 libexec 디렉토리를 선택해주면 된다.
$ brew info tomcat@8 명령어로 설치 경로를 조회했다.
한 번 실행해 보니 다음과 같은 오류가 뜬다. IDE에서 포트가 충돌했을 수 있다고 경고가 나온다. 터미널에서 $ sudo lsof -i :8080로 8080 포트를 사용하고 있는 프로세스를 조회해 보았더니 java *:http-alt에서 사용중이라고 한다.
$ kill -9 PID로 포트를 강제 종료해보아도 계속 다시 생성된다. 🤔 아마도 다른 프로젝트에서 사용한 스프링 부트에 내장된 톰캣인 것 같다.

톰캣 연결 설정에서 포트를 8080 → 8081로 변경해보았다.

프로젝트를 생성할 때 만들어져 있던 기본 파일인 index.jsp가 실행되었다.
그런데 경로가 /가 아니라 /demo_war_exploded이다.

톰캣 연결 설정의 Deployment - Application context를 SpringMVC01_war_exploded → /로 바꿔준다.

잘 적용되었다.

아직 스프링 프레임워크는 추가한 적이 없기 때문에 라이브러리를 확인해보면 처음 프로젝트 생성 때 기본으로 삽입된 것만 존재한다.

MVN Repository에 가서 Sring Web MVC를 검색한다. 나는 최신 릴리즈 버전인 5.2.25.RELEASE로 선택했다.
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.25.RELEASE</version>
</dependency>
복사한 코드를 pom.xml에 붙여넣기 해 주고 빌드를 해 준다.

빌드가 완료된 뒤 라이브러리를 확인해보면 spring-webmvc 뿐 아니라 core와 기타 필요한 스프링 프레임워크가 들어와있는 것을 확인할 수 있다.

스프링 프레임워크가 성공적으로 추가되면 Spring Config 파일을 추가할 수 있게 된다.

웹 애플리케이션 디렉토리인 webapp을 만들어주고 하위 디렉토리와 XML 설정 파일들을 만들어주자.
resources, spring, WEB-INF 디렉토리spring/appServlet 디렉토리, spring/root-context.xml appServlet/servlet-context.xml WEB-INF/views 디렉토리, WEB-INF/web.xml template.jsp는 강의에서 만든 JSP 템플릿 파일이다.<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<filter>
<filter-name>encodingFilter</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>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<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/spring/appServlet/servlet-context.xml</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>
</web-app>
디스패처 서블릿 관련 설정과 root-context.xml, 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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
아직 추가한 설정은 없다.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/resources/**" location="/resources/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="kr.board.controller" />
</beans:beans>
뷰 리졸버와 컴포넌트 스캔 설정이 있다.
이제 프로젝트가 MVC 구조에 따라 잘 동작하는지 테스트해보자.
게시글의 정보를 담는 객체인 Board를 만들고 getter, setter도 만들어준다. 아직 DB 연결은 하지 않았으므로 컨트롤러에서 모델에 전달한 값인 List<Board> list를 만들어준다. 그리고 list를 모델에 담아 boardList 뷰에 전달하자.
@Controller //스프링에게 이 클래스가 컨트롤러임을 알려줌
public class BoardController {
@RequestMapping("/boardList.do")
public String boardList(Model model) {
System.out.println("boardList.do 실행");
Board vo = new Board(); //getter, setter 필수
vo.setIdx(1);
vo.setTitle("제목");
vo.setContent("내용");
vo.setWriter("작성자");
vo.setIndate("2021-01-01");
vo.setCount(0);
List<Board> list = new ArrayList<>(); //임의 리스트
list.add(vo);
list.add(vo);
list.add(vo);
model.addAttribute("list", list); //모델에 넣기
return "boardList"; //뷰의 논리적 이름
}
}
모델을 받아서 출력할 뷰 파일(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>
<title>Spring MVC 01</title>
<meta charset="utf-8">
<%-- 디바이스 크기 별로 화면 조정 --%>
<meta name="viewport" content="width=device-width, initial-scale=1">
<%-- 부트스트랩 제공 CSS 사용 --%>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<h2>Panel Heading</h2>
<div class="panel panel-default">
<div class="panel-heading">Spring MVC 01</div>
<div class="panel-body">
<table class="table table-bordered table-hover">
<tr>
<td>번호</td>
<td>제목</td>
<td>작성자</td>
<td>작성일</td>
<td>조회수</td>
</tr>
<c:forEach var="vo" items="${list}">
<tr>
<td>${vo.idx}</td>
<td>${vo.title}</td>
<td>${vo.writer}</td>
<td>${vo.indate}</td>
<td>${vo.count}</td>
</tr>
</c:forEach>
</table>
</div>
<div class="panel-footer">SP 1 </div>
</div>
</div>
모델에 list라는 이름으로 담아 전달한 값을 ${list}로 받아 반복문으로 필드 값을 출력한다. 디자인은 부트스트랩을 사용했다.

컨트롤러에서 지정한 @RequestMapping URL을 입력해보면 위와 같이 잘 출력된다. 적용한 스프링 MVC 프레임워크가 정상적으로 동작하는 것을 확인할 수 있다. 👍
시간이 꽤 많이 걸려서 그냥 이클립스로 방법을 찾는게 빨랐겠다 싶어서 후회할 뻔 했지만... 이 삽질 속에서 배운게 많았다. 스트링 부트로 쉽게 빌드할 때는 전혀 신경쓰지 않았던 것들이었는데 직접 하나하나 세팅하려니 '이거는 왜 필요한 거지?' 라는 궁금증이 들 수밖에 없었다. 덕분에 스프링을 더 잘 이해할수 있게 된 것 같다. 😀