Spring Core/자질구래

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

  • -

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

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

기본 프로젝트 구성

 

maven project 구성

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

maven project 생성

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

 

pom.xml에 JDK 설정 및 필요한 의존성 설정

다음과 같이 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);
	}
}

 

빈들을 위한 폴더 구조 생성 및 hello 빈 등록

 

폴더 구조 생성

웹과 무관한 내용들(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 {

}

 

User와 UserService 생성

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과 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 접속을 위한 속성 설정

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");
	}

 

Repository 인터페이스 생성 및 연관 xml 파일 작성

사용자 정보를 관리하기 위한 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>

 

repository 동작 확인

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

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 설정하기

 

의존성 추가

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 사용 설정

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 {
  ... 
}

 

aspect 추가

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 추가

웹과 관련된 설정을 추가하기 위해 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/");
    }
}

 

WebApplicationInitializer 작성

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;
}

 

Controller 작성

"/"를 호출했을 때 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";
	}
}

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

 

Service 연결

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는 없는게 맞다.

 

ControllerAdvice를 통한 예외 처리

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 오류 페이지로 이동한다.

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

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

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