Spring Core/자질구래

레거시 스프링 프로젝트 작성 절차

  • -

이번 포스트에서는 레거시 스프링 프로젝트를 구성하는 절차를 살펴보자.

이 페이지의 내용은 스프링 부트 애플리케이션의 구성을 연습하기 위한 것으로 비지니스 로직 처리는 적절치 않습니다.

 

아키타입을 생략한 새로운 Maven Project를 생성한다.

maven project 생성

group id(소속사 domain), artifact id(project 이름)을 기입하고 web 서비스를 위해 packaging을 war로 설정한다.

 

다음과 같이 pom.xml 파일을 수정해서 JDK와 필요한 의존성을 설정한다.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.doding</groupId> <artifactId>legacyproj</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.release>17</maven.compiler.release> <org.springframework.spring-context>6.1.13</org.springframework.spring-context> </properties> <dependencies> <!-- 가장 기본적인 스프링 모듈 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${org.springframework.spring-context}</version> </dependency> <!-- 단위테스트와 연결 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${org.springframework.spring-context}</version> <scope>test</scope> </dependency> <!--JUnit 단위테스트 --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.11.3</version> <scope>test</scope> </dependency> <!-- 로깅 프레임워크 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.11</version> </dependency> <!-- 롬복 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.34</version> <scope>provided</scope> </dependency> </dependencies> </project>

src/main/resources에 logback.xml 파일을 설정한다.

<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration> <configuration> <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder" /> <import class="ch.qos.logback.core.ConsoleAppender" /> <appender name="STDOUT" class="ConsoleAppender"> <encoder class="PatternLayoutEncoder"> <pattern>%d{HH:mm:ss} [%-5level] %logger{36}.%M.%L - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> <logger name="com.doding" level="trace" additivity="false"> <appender-ref ref="STDOUT" /> </logger> </configuration>

 

웹과 무관한 설정을 위해 ApplicationConfig 파일을 생성한다.

package com.doding.legacyproj.config; import org.springframework.context.annotation.Configuration; @Configuration public class ApplicationConfig { }

 

다음의 테스트를 통해서 ApplicationConfig가 잘 구성되는지 확인한다.

package com.doding.legacyproj.test; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import com.doding.legacyproj.config.ApplicationConfig; import com.doding.legacyproj.model.dto.User; import com.doding.legacyproj.model.service.UserService; import lombok.extern.slf4j.Slf4j; @ExtendWith(SpringExtension.class ) @ContextConfiguration(classes = {ApplicationConfig.class}) @Slf4j public class ProjectConfigTest { @Autowired ApplicationContext ctx; @Test public void 애플리케이션컨텍스트구성테스트() { log.debug("logger 동작 확인"); Assertions.assertNotNull(ctx); ApplicationConfig config = ctx.getBean(ApplicationConfig.class); Assertions.assertNotNull(config); } }

 

 

웹과 무관한 내용들(model/service, model/repo, model/dto, aspect), 웹과 유관한 내용들(controller, interceptor) 폴더를 생성한다.

 

ApplicationConfig에서 웹과 유관한 내용을 스켄하도록 처리한다.

package com.doding.legacyproj.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackages = { "com.doding.legacyproj.model", "com.doding.legacyproj.aspect" }) public class ApplicationConfig { }

 

DTO인 User와 UserService를 생성한다.

package com.doding.legacyproj.model.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @Builder public class User { private String id; private String name; private String pass; }
package com.doding.legacyproj.model.service; import org.springframework.stereotype.Service; import com.doding.legacyproj.model.dto.User; @Service public class UserService { public User login(User user) { return User.builder().id("hong").name("hong gil dong").pass("1234").build(); } }

 

@Autowired UserService service; @Test public void 서비스빈의생성_동작확인() { // given User user = User.builder().id("hong").pass("1234").build(); // when User loginUser = service.login(user); // then Assertions.assertEquals(loginUser.getName(), "hong gil dong"); }

 

 

mysql과 mybatis 사용을 위한 의존성을 추가한다.

<!-- mysql-connector-j --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.4.0</version> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <!-- mybatis-spring: mybatis와 Spring의 연동 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>3.0.4</version> </dependency> <!-- HikariCP : DataSource를 위한 Connection Pool --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>6.0.0</version> </dependency> <!-- spring-jdbc : 트랜젝션 처리에 필요 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${org.springframework.spring-context}</version> </dependency>

 

db 접속과 관련된 정보를 src/main/resources/db/dbinfo.properties로 만든다.

datasource.driver-class-name=com.mysql.cj.jdbc.Driver datasource.url=jdbc:mysql://localhost:3306/sampleDatabase?serverTimezone=UTC datasource.password=doding datasource.username=doding

 

DB와 관련된 빈들은 웹과 무관한 빈들이기 때문에 ApplicationConfig에 다음의 빈들을 명시적으로 구성한다. 추가로 annotation 3개(EnableTransactionManagement, PropertySource, MapperScan)을 추가해야 한다.

package com.doding.legacyproj.config; import java.io.IOException; import javax.sql.DataSource; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import com.zaxxer.hikari.HikariDataSource; @Configuration @ComponentScan(basePackages = { "com.doding.legacyproj.model", "com.doding.legacyproj.aspect" }) @EnableTransactionManagement // 트랜젝션 관리를 위함 @PropertySource("classpath:/db/dbinfo.properties") // DB 설정 불러오기 @MapperScan(basePackages = {"com.doding.legacyproj.model.repo"}) // Repository Interface 위치 public class ApplicationConfig { @Bean public DataSource datasource(@Value("${datasource.driver-class-name}") String driver, @Value("${datasource.url}") String url, @Value("${datasource.password}") String pass, @Value("${datasource.username}") String user) { HikariDataSource ds = new HikariDataSource(); ds.setDriverClassName(driver); ds.setJdbcUrl(url); ds.setUsername(user); ds.setPassword(pass); return ds; } @Bean public PlatformTransactionManager transactionManager(DataSource ds) { return new DataSourceTransactionManager(ds); } @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource ds) throws IOException { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(ds); // DTO의 위치 지정 bean.setTypeAliasesPackage("com.doding.legacyproj.model.dto"); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); // mapper xml 파일의 위치 지정 bean.setMapperLocations(resolver.getResources("classpath:/db/mapper/*.xml")); return bean; } }

 

위에서 생성한 DataSource가 잘 생성되었는지 확인하자.

@Autowired DataSource ds; @Test public void 데이터소스잘생성되었을까() { Assertions.assertNotNull(ds); Assertions.assertEquals(ds.getClass().getSimpleName(), "HikariDataSource"); }

 

사용자 정보를 관리하기 위한 UserRepo를 만들자.

package com.doding.legacyproj.model.repo; import com.doding.legacyproj.model.dto.User; public interface UserRepo { User select(String id); }

위 interface와 연관되는 mapper xml을 생성한다. 위치는 SqlSessionTemplateFactory를 구성하면서 전달했던 mppperLocation을 참조한다.

<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.doding.legacyproj.model.repo.UserRepo"> <select id="select" resultMap="userBase"> select * from user where id=#{userId} </select> <resultMap type="user" id="userBase"> <id column="id" property="id"/> <result column="name" property="name"/> <result column="pass" property="pass"/> </resultMap> </mapper>

 

테스트를 위해 데이터를 입력 후 다음의 테스트를 실행해보자.

insert into user values("hong", "hong gil dong", "1234"), ("jang", "jang gil san", "1234");
@Autowired UserRepo urepo; @Test public void UserRepo는잘생성되고동작하나() { // given, when User user = urepo.select("hong"); // then Assertions.assertEquals(user.getName(), "hong gil dong"); }

 

UserService에 UserRepo를 주입해서 동작시키고 모델영역 구성을 마무리 한다.

package com.doding.legacyproj.model.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.doding.legacyproj.model.dto.User; import com.doding.legacyproj.model.repo.UserRepo; import lombok.extern.slf4j.Slf4j; @Service @Slf4j public class UserService { @Autowired UserRepo urepo; public User login(User user) { User selected = urepo.select(user.getId()); log.debug("user: {}, selected: {}", user, selected); if (selected!=null && selected.getPass().equals(user.getPass())) { return selected; } else { throw new RuntimeException("ID/PASS 확인"); } } }
@Test public void 서비스와레포가잘연결되었나() { User user = User.builder().id("hong").pass("1234").build(); User loginUser = service.login(user); Assertions.assertEquals(loginUser.getName(), "hong gil dong"); user.setId("some"); Assertions.assertThrows(RuntimeException.class, ()->{service.login(user);}); }

 

 

AOP를 사용하기 위해 필요한 의존성을 추가한다.( spring-aop는 spring-context에 포함되어 있다.)

<!-- aspectjrt :aop 사용에 필요, scope runtime 지우기!!--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.22.1</version> </dependency>

 

proxy 사용 설정을 위해 @EnableAspectJAutoProxy를 추가한다.

@Configuration @ComponentScan(basePackages = { "com.doding.legacyproj.model", "com.doding.legacyproj.aspect" }) @EnableTransactionManagement // 트랜젝션 관리를 위함 @PropertySource("classpath:/db/dbinfo.properties") // DB 설정 불러오기 @MapperScan(basePackages = {"com.doding.legacyproj.model.repo"}) // Repository Interface 위치 @EnableAspectJAutoProxy // proxy 설정 public class ApplicationConfig { ... }

 

service가 호출되면 로그를 남길 aspect를 추가해보자.

package com.doding.legacyproj.aspect; import java.util.Arrays; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import lombok.extern.slf4j.Slf4j; @Aspect @Component @Slf4j public class ServiceParameterLoggingAspect { @Before("@within(org.springframework.stereotype.Service)") public void logging(JoinPoint jp) { log.debug("{}, {}", jp.getSignature(), Arrays.toString(jp.getArgs())); } }

 

서비스를 호출했을 때 AOP에 의한 로그가 나오는지 확인한다.

SLF4J(I): Connected with provider of type [ch.qos.logback.classic.spi.LogbackServiceProvider] 10:25:41 [DEBUG] c.d.l.a.ServiceParameterLoggingAspect.logging.19 > User com.doding.legacyproj.model.service.UserService.login(User), [User(id=hong, name=null, pass=1234)] 10:25:41 [INFO ] c.z.h.HikariDataSource.getConnection.109 > HikariPool-1 - Starting... 10:25:42 [INFO ] c.z.h.p.HikariPool.checkFailFast.572 > HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@52a33c3f 10:25:42 [INFO ] c.z.h.HikariDataSource.getConnection.122 > HikariPool-1 - Start completed. 10:25:42 [DEBUG] c.d.l.m.r.U.select.debug.135 > ==> Preparing: select * from user where id=? 10:25:42 [DEBUG] c.d.l.m.r.U.select.debug.135 > ==> Parameters: hong(String) 10:25:42 [TRACE] c.d.l.m.r.U.select.trace.141 > <== Columns: id, name, pass 10:25:42 [TRACE] c.d.l.m.r.U.select.trace.141 > <== Row: hong, hong gil dong, 1234 10:25:42 [DEBUG] c.d.l.m.r.U.select.debug.135 > <== Total: 1 ...

 

 

웹과 관련된 5개의 의존성(spring-webmvc, jakarta.servlet, jakarta.servlet.jsp, jakarta.servlet.jsp.jstl, org.glassfish.web)을 pom.xml에 추가한다.

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${org.springframework.spring-context}</version> </dependency> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>${jakarta.servlet-version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>jakarta.servlet.jsp</groupId> <artifactId>jakarta.servlet.jsp-api</artifactId> <version>${jakarta.servlet.jsp-version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>jakarta.servlet.jsp.jstl</groupId> <artifactId>jakarta.servlet.jsp.jstl-api</artifactId> <version>${jakarta.servlet.jsp.jstl-version}</version> </dependency> <dependency> <groupId>org.glassfish.web</groupId> <artifactI

 

웹과 관련된 설정을 추가하기 위해 WebContextConfig를 추가한다. 이때 component를 스캔할 위치를 주의한다. 또한 ViewResolver가 view를 찾기 위한 설정, 정적파일을 연결하기 위한 부분도 점검하자.

package com.doding.legacyproj.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @ComponentScan({"com.doding.legacyproj.controller", "com.doding.legacyproj.interceptor"}) @EnableWebMvc public class WebContextConfig implements WebMvcConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { registry.jsp("/WEB-INF/views/", ".jsp"); } @Override // 정적 리소스에 대한 경로 설정 public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**").addResourceLocations("/resources/"); } }

 

web.xml을 대신해서 관련 설정을 로딩할 WebApplicationInitializer를 작성한다.  이때 설정파일을 context에 넘겨주는 부분을 주의한다.

package com.doding.legacyproj.config; import org.springframework.web.WebApplicationInitializer; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.servlet.DispatcherServlet; import jakarta.servlet.FilterRegistration; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRegistration; public class CustomWebApplicationInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { // root-context AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(ApplicationConfig.class, WebContextConfig.class); // 웹과 무관한, 웹과 유관한 servletContext.addListener(new ContextLoaderListener(context)); // servlet-context DispatcherServlet servlet = new DispatcherServlet(context); ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet); registration.setLoadOnStartup(1); registration.addMapping("/"); } }

 

src/main/webapp/WEB-INF/views와 src/main/webapp/resources 폴더를 생성한다. 각각 폴더에 index.jsp와 /css/common.css를 작성한다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="jakarta.tags.core"%> <c:set value="${pageContext.servletContext.contextPath }" var="root"></c:set> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link rel="stylesheet" href="${root }/resources/css/common.css"> </head> <body> <h1>Hello WebMVC</h1> <form method="post" action="${root }/user/login"> <input type="text" name="id"> <input type="text" name="pass"> <button>login</button> </form> </body> </html>
h1{ color: blue; }

 

"/"를 호출했을 때 index.jsp를 연결하도록 Controller를 작성한다.

package com.doding.legacyproj.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class UserController { @GetMapping("/") public String index() { return "index"; } }

글자색이 파란색이어야 한다.!

 

post로 /user/login 요청이 왔을 때 Service를 연동해서 서비스하도록 Controller를 수정해본다.

@Autowired UserService service; @PostMapping("/login") public String login(@ModelAttribute User user, HttpSession session) { User selected = service.login(user); session.setAttribute("loginUser", selected); log.debug("selected: {}", selected); return "loginok"; }

현재는 loginok라는 jsp가 없으므로 404가 발생한다. 성공 여부는 log로 확인한다.

loginok.jsp는 없는게 맞다.

 

404, 500 오류를 처리하기 위한 ControllerAdvice를 작성한다.

package com.doding.legacyproj.controller; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.NoHandlerFoundException; import lombok.extern.slf4j.Slf4j; @ControllerAdvice @Slf4j public class ExceptionHandlingAdvice { @ExceptionHandler(value = NoHandlerFoundException.class) @ResponseStatus(code = HttpStatus.NOT_FOUND) public String handle404(NoHandlerFoundException e) { log.error("404", e); return "error/404"; } @ExceptionHandler(value = RuntimeException.class) @ResponseStatus(code=HttpStatus.INTERNAL_SERVER_ERROR) public String handle500(RuntimeException e) { log.error("500", e); return "error/500"; } }

 

404와 500 오류를 처리할 jsp페이지를 만들고 테스트 해본다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>앗!! 없는 파일</h1> </body> </html>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>이건 서버 오류</h1> </body> </html>

현재는 hong/12345로 로그인 시도하면 500 오류 페이지로 이동한다.

이 페이지의 내용은 스프링 부트 애플리케이션의 구성을 연습하기 위한 것으로 비지니스 로직 처리는 적절치 않습니다.

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.