[Spring Quick Start] 프로젝트 세팅 ① - 전반적인 프로세스

김희정·2023년 2월 14일
1

Spring

목록 보기
3/16
post-thumbnail

💎 들어가며

이번 포스팅에서는 크게 프로젝트 구성 시에 고려해야될 사항과 Spring MVC 세팅방법에 대해 정리해보았습니다. 전반적인 프로세스에 대해 설명해야되기 때문에 상세 내용은 생략하고 큰 개요만 내용에 담도록 하였습니다.

상세 내용에 대해서는 추가적으로 포스팅하겠습니다.🤗


1. 프로젝트 스펙

우선, 프로젝트 세팅에 앞서, 프로젝트 구현에 필요한 기술 스펙을 정리해야 합니다.
"내가 어떤 기술을 사용해 어떻게 구성할것인지"에 대해 세팅에 앞서 고민해야 합니다.

아래는 자주 사용해왔던 프로젝트 환경 및 기술 스펙입니다.
(다소 레거시한 부분이 있습니다.😅)

Back-End: Java 1.8, Template(jsp, tiles), Spring MVC,
Persistence: DB(PostgreSQL), Framework(MyBatis) 등
Front-End: jQuery, Tabulator, 그밖의 JS 라이브러리 등
WAS: Tomcat
Build Tool: Maven, IDE: STS, DVCS: git

이는 개발자로서 초창기 처음 접하는 프로젝트들에게 가장 영향을 많이 받았습니다.
환경 구성을 처음 시작해볼 때는 주로 유지보수하던, 기존에 미완성된 프로젝트를 이어받는 것으로 시작하던 것이기 때문이죠.

하지만, 어떤 기술을 사용할 것인지 Back-End 부터 세세하게는 WAS, Build Tool 까지 모두가 선택 사항입니다.

개발자로서 성장해나가려면 이러한 기술들을 스스로 결정할줄 알아야 하고, 기술을 선택할 수 있도록 바운더리를 넓혀가는 것이 중요한게 아닐까 생각이 듭니다.


근래에는 새로운 Framework, 새로운 IDE 등 새로운 것에 대한 관심이 늘어, 아래와 같이 기술 스펙을 사용해보았습니다. (프론트 단은 그대로 냅뒀습니다🤣)

환경구성

Back-End: Java 1.8, Template(Thymeleaf), Spring Boot
Persistence: DB(MariaDB), Framework(JPA) 등
Build Tool: Gradle, IDE: IntelliJ

이들은 별차이가 없는거 같지만, 사실 엄청난 큰 차이가 있습니다. 프레임워크 자체가 달라지는 것(Spring MVCSpring Boot)이기 때문에 세팅방식이 전혀 다릅니다. 이와 같이 프로젝트 기술이 달라지면 프로젝트 세팅 방식이 크게 달라지기 때문에, 프로젝트 세팅에 앞서 기술 스펙 정리는 필수적입니다.


2. Spring MVC Project

우선, 앞선 포스팅과도 연결되며 기존에 자주 사용해왔던 프로젝트인 Spring MVC Project에 대해 세팅을 진행해볼까합니다.

Back-End: Java 1.8, Template(jsp, tiles), Spring MVC,
Persistence: DB(PostgreSQL), Framework(MyBatis) 등
Front-End: jQuery, Tabulator, 그밖의 JS 라이브러리 등
WAS: Tomcat
Build Tool: Maven, IDE: STS, DVCS: git


Spring MVC 세팅할 때 고려해야될 사항을 보면 아래와 같습니다.

  1. 스프링 빈 설정
  2. 애플리케이션 설정 파일
  3. 라이브러리 관리
  4. Persistence 및 DB 세팅
  5. 패키지 구조 설정

3. 스프링 빈 설정

※ 스프링 빈 설정은 앞선 포스팅에서 이야기했듯이, 스프링 컨테이너에 객체(bean)을 로드할 때 사용합니다.

3.1 파일 생성

  1. 우선 WEB-INF/spring 폴더의 내용을 src/main/resources로 옮기고, 해당 폴더를 삭제합니다.
  2. web.xml의 contextConfigLocation의 내용을 바뀐 경로로 업데이트합니다.
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:spring/context-root.xml</param-value>
</context-param>

<!-- 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>classpath:spring/context-servlet.xml
    </param-value>
  </init-param>
</servlet>

3.2 빈 설정

  1. context-root.xml 파일에 component-scan을 추가합니다.
    이때, Controller 타입을 제외합니다.
<!-- Root Context: defines shared resources visible to all other web components -->
<context:component-scan base-package="com.khjdev.blog">
	<context:exclude-filter type="annotation"
		expression="org.springframework.stereotype.Controller" />
	<context:exclude-filter type="annotation"
		expression="org.springframework.web.bind.annotation.RestController" />
</context:component-scan>
  1. context-servlet.xml 파일에 component-scan을 수정합니다.
    이때, Controller 타입만 포함하도록 합니다.
<context:component-scan base-package="com.khjdev.blog" use-default-filters="false">
	<context:include-filter type="annotation"
		expression="org.springframework.stereotype.Controller" />
	<context:include-filter type="annotation"
		expression="org.springframework.web.bind.annotation.RestController" />
</context:component-scan>


4. 애플리케이션 설정 파일

애플리케이션을 개발하다보면 가변적인 요소들은 DB에 저장하거나 설정파일에 저장합니다. 예를 들어 데이터베이스 정보가 될수도 있고, 각 배포 사이트별 특별한 요청을 설정할 수 있도록 옵션처리를 제공하는 정보가 될 수 있습니다.

이러한 정보를 저장할 때 properties 파일에 저장하며, 해당정보에 접근하기 위해 Property 객체를 bean으로 설정합니다.

4.1 환경 별 파일 생성

src/main/resourcesconfig 폴더를 생성합니다.

그리고 아래와 같이 각 환경 별로 환경 파일을 생성합니다. ⇒ 공통(config-common), 로컬(config-local), 개발(config-dev), 운영(config-real)

4.2 PropertiesFactoryBean 설정

그런 다음, 해당 properties를 전역적으로 접근할 수 있도록 context-root.xml에 아래와 같이 빈을 선언합니다.

<bean id="config" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
	<property name="locations">
		<list>
			<value>classpath:config/config-common.properties</value>
			<value>classpath:config/config-#{systemProperties['spring.profiles.active']}.properties</value>
		</list>
	</property>
	<property name="fileEncoding" value="UTF-8" />
</bean>

4.3 Tomcat Launch 옵션

Tomcat Launch 옵션에서 -Dspring.profiles.active=local 환경을 설정해줍니다.
ㄴ sts에서는 Tomcat Server 더블 클릭 > Open launch configuration > Arguments > VM arguments에 해당 옵션을 추가해주면 됩니다.

내용 자체가 길어지는 것을 방지하기 위해 최대한 자세한 내용 설명을 생략하고 있습니다. 운영 환경 분리에 대해서는 따로 포스팅할 계획입니다.


5. 라이브러리 관리

라이브러리 관리는 종속성(dependency) 관리입니다. Maven에서는 pom.xml에서 라이브러리 스펙을 변경하고, Maven update 명령을 내리면 자동으로 라이브러리를 다운로드 받아줍니다.

5.1 pom.xml

패키지 관리를 위해 pom.xml를 엽니다. 해당 파일을 보면 아래와 같은 내용이 있습니다.

<properties>
	<java-version>1.6</java-version>
	<org.springframework-version>3.1.1.RELEASE</org.springframework-version>
	<org.aspectj-version>1.6.10</org.aspectj-version>
	<org.slf4j-version>1.6.6</org.slf4j-version>
</properties>

Legacy Project로 생성해서 그런가 버전이 상당이 옛날 버전인 것을 볼 수 있습니다.

버전을 좀 변경하고, 추가적으로 필요한 라이브러리들을 설치해줍니다. 이런식으로 properties 안에 버전을 명시해두면 좀 더 수월하게 변경할 수 있습니다.

<java-version>1.8</java-version>
<org.springframework-version>5.3.20</org.springframework-version>
<org.tiles-version>3.0.7</org.tiles-version>
<org.aspectj-version>1.9.9.1</org.aspectj-version>
<org.slf4j-version>1.7.36</org.slf4j-version>
<org.log4j-version>2.18.0</org.log4j-version>
<jackson.version>2.13.3</jackson.version>

5.2 버전 관리의 필요성

라이브러리의 버전은 프로젝트가 오래되면 될수록 업데이트하지 않기 마련입니다.

하지만 해당 라이브러리 자체는 종속성이라고 표현하는데, 이 종속성이 가진 문제는 꽤나 상당합니다. 예를 들어, 작년에 핫이슈였던 log4j의 보안취약점 (원격 제어명령 실행)이 엄청난 문제가 됐고, Apache 재단에서는 해당 라이브러리를 일정 수준 이상으로 패치할 것을 권고했습니다.

따라서 주기적으로 관심을 갖고 라이브러리들을 관리해줄 필요성이 있습니다. Maven Repository 홈페이지에서 검색할 수 있으며, 해당 라이브러리의 어떤 버전이 보안취약점이 있었는지 까지 확인가능합니다.

But, 항상 최신라이브러리를 사용할 수 있다면 좋겠지만, 해당 버전에는 일정 수준 이상의 기술 제한이 있습니다.

예를 들면 어떤 라이브러리는 Java 11부터 지원한다거나, Boot 3버전 부터 지원한다거나 각 차이가 있을 수 있습니다. 프로젝트 기술 스펙에 맞는 버전인지, 정말 필요하다면 다른 버전들을 업그레이드하는 방향까지 체크해야 합니다.


6. Persistence 및 DB 세팅

6.1 관련 라이브러리 추가

사용할 Persistence인 MyBatis 라이브러리, 데이터베이스 postgresql 라이브러리, 데이터베이스의 Connection Pool을 사용하기 위해 Commons DBCP 등 데이터베이스 관련 라이브러리를 추가해줍니다.

<!-- Database -->
<dependency>
	<groupId>org.postgresql</groupId>
	<artifactId>postgresql</artifactId>
	<version>9.4.1208</version>
</dependency>
<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis</artifactId>
	<version>3.5.6</version>
</dependency>
<dependency>
	<groupId>org.mybatis</groupId>
	<artifactId>mybatis-spring</artifactId>
	<version>1.3.2</version>
</dependency>
<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
	<version>${org.springframework-version}</version>
</dependency>
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-dbcp2</artifactId>
	<version>2.9.0</version>
</dependency>

DBCP란?

데이터베이스와 애플리케이션을 효율적으로 연결을 관리하는 커넥션 풀(connection pool) 라이브러리
Apache의 Commons DBCP와 Tomcat-JDBC, BoneCP, HikariCP 등이 있습니다.

아래 내용을 참고해서 보면 좋을 거같습니다.
[참조] Commons DBCP 이해하기


6.2 DB 커넥션 정보 저장

DB정보는 보통 애플리케이션 설정파일에 저장하며, 운영 환경(로컬, 개발, 운영)마다 바뀔수 있기 때문에 config-[ local | dev | real ].properties 중에 저장합니다.

⇒ 저는 config-local.properties 에 DB 정보를 써보겠습니다.

# DB info
db.url=jdbc:postgresql://127.0.0.1:5432/blog
db.driverClass=org.postgresql.Driver
db.username=postgres
db.password=postgres

6.3 Bean 파일 생성 및 web.xml에 추가

DB와 관련된 객체를 생성하기 위해 Spring Bean 파일 생성을 생성해줍니다.

MyBatis에서는 dataSource, sqlSessionFactory, sqlSessionTemplate 세가지 빈을 선언해줘야합니다. 아래는 해당 객체별 역할입니다.

객체 사이의 관계

  • Spring Container의 역할 : IOC, AOP, MVC를 지원한다.
  • DBCP : DataBase Connection Pool이다.
  • SqlSessionFactoryBean : Bean - 자바의 컴포넌트, SqlSessionFactory 생성
  • SqlSessionFactory : MyBatis의 전역 정보를 가지고 실행을 제어, SqlSession 생성
  • SqlSession : 쿼리를 실행한다 (작업 단위 별로 factory에서 생성됨)
  • SqlSessionTemplate : Template Pattern 중 하나이다. template은 sql 실행만 담당한다.
    나머지 일은 SqlSessionFactoryBean, Mapper.xml 정보를 DI로 주입받아 사용한다.
    MemberDAOImpl : template을 주입받아 사용한다. (return template.selectOne(""))

[참조] [MyBatis/Spring] 스프링에 마이바티스 적용해보기

src/main/resources/spring 아래에 context-db.xml을 추가하여 다음과 같은 내용을 추가합니다.
그 후에는 web.xmlContext Rootcontext-db.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:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans.xsd
						http://www.springframework.org/schema/aop
                        http://www.springframework.org/schema/aop/spring-aop.xsd
                        http://www.springframework.org/schema/tx
                        http://www.springframework.org/schema/tx/spring-tx.xsd">

  	<!-- dataSource -->
	<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="${db.driverClass}" />
		<property name="url" value="${db.url}" />
		<property name="username" value="${db.username}" />
		<property name="password" value="${db.password}" />
	</bean>
  
	<!-- sqlSessionFactory -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
      	<!-- Mapper 위치 지정 -->
		<property name="mapperLocations" value="classpath:/mapper/*.xml" />
      	<property name="configuration">
			<bean class="org.apache.ibatis.session.Configuration">
				<property name="cacheEnabled" value="false" />
				<property name="mapUnderscoreToCamelCase" value="true" />
			</bean>
		</property>
	</bean>
  
  	<!-- sqlSessionTemplate -->
	<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>

	<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="locations">
			<list>
				<value>classpath:config/config-common.properties</value>
				<value>classpath:config/config-#{systemProperties['spring.profiles.active']}.properties</value>
			</list>
		</property>
	</bean>
</beans>

6.4 mapper의 위치

<!-- sqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dataSource" />
    
    <!-- Mapper 위치 지정 -->
	<property name="mapperLocations" value="classpath:/mapper/*.xml" />
    <property name="configuration">
		<bean class="org.apache.ibatis.session.Configuration">
			<property name="cacheEnabled" value="false" />
			<property name="mapUnderscoreToCamelCase" value="true" />
		</bean>
	</property>
</bean>

mapper의 위치는 위와 같이 sqlSessionFactory에서 지정한 경로에 위치합니다.
src/main/resources/mapper 폴더를 생성해줍니다.



7 게시판 컴포넌트 구현

이제 마지막 단계인 웹 요청을 처리하는 실제 구현과정입니다.

하나의 도메인, 컴포넌트(Controller, Service, DAO, VO) 구조를 실습해보고자 합니다.
아래는 게시판 목록 조회하는 기능을 구현하였습니다.


7.1 Database Post Table

우선 데이터베이스에 CRUD가 가능하게 하도록 하려면 데이터베이스에 게시판 테이블을 생성해야 합니다.

(아래는 PostgreSQL 쿼리입니다.)


-- 시퀀스(sequence) 생성
CREATE SEQUENCE post_seq
  INCREMENT 1
  MINVALUE 1
  MAXVALUE 9223372036854775807
  START 1
  CACHE 1;

-- 테이블(table) 생성
CREATE TABLE post_tbl (
  rec_key integer NOT NULL DEFAULT nextval('post_seq'::regclass) primary key,
  subject text,
  content text,
  writer_id character varying(20),
  writer_name character varying(20),
  log_date timestamp without time zone NOT NULL
)

-- 예시 데이터 넣기
INSERT INTO post_tbl(subject, content, writer_id, writer_name, log_date)
VALUES('Hello', '안녕하세요. 첫번째 블로그 글입니다~', 'developer_khj', '김희정', now())

7.2 PostVO, PostDAO, PostService

위와 같이 PostService 인터페이스, PostVO, PostDAO, PostServiceImpl 클래스를 생성합니다.
( 순서는 상관없지만 제 경험상, 보통은 VOServiceDAO 순으로 생성되는 것 같습니다. )

mapper 폴더에는 Mapper 파일인 게시판 SQL문을 작성할 mapper-post.xml을 생성합니다.


7.3 PostVO

Post 테이블 정보를 저장 PostVO를 만듭니다.

public class PostVO {
	private long recKey;
	private String subject;
	private String content;
	private String writerId;
	private String writerName;
	private Date logDate;
	
	public PostVO(long recKey, String subject, String content, 
    					String writerId, String writerName, Date logDate) {
		this.recKey = recKey;
		this.subject = subject;
		this.content = content;
		this.writerId = writerId;
		this.writerName = writerName;
		this.logDate = logDate;
	}

	... (중략)
}

7.4 PostDAO

데이터베이스와 연계할 PostDAO를 생성합니다. PostDAO에서는 실제 DB 객체와 연결되서 SQL문을 실행하고 PostVO를 결과값으로 받습니다.

@Repository
public class PostDAO {
	@Autowired
	@Resource(name = "sqlSessionTemplate")
	private SqlSession sqlSession;
	
	private static class SQL {
		public static String SELECT = "post.getPostList";
	}
	
	public List<PostVO> selectList(Map<String, Object> paramMap) {
		return sqlSession.selectList(SQL.SELECT, paramMap);
	}
}

7.5 PostService

PostService는 게시판 서비스로 게시판의 요청을 위임받는 역할을 담당할 예정입니다.
역할을 위임받아 DB 작업이 필요한 경우 DAO들과 연계합니다.

  • PostService Interface: 인터페이스
public interface PostService {
	public List<PostVO> getPostList(Map<String, Object> paramMap); 
}
  • PostService Class: 인터페이스 구현체
@Service
public class PostServiceImpl implements PostService {
	private final PostDAO PostDAO;
	
	public PostServiceImpl(PostDAO PostDAO) {
		this.PostDAO = PostDAO;
	}
	
	@Override
	public List<PostVO> getPostList(Map<String, Object> paramMap) {
		return PostDAO.selectList(paramMap);
	}
}

7.6 mapper-post.xml

이제 게시판 CRUD를 위해 사용할 SQL문을 매퍼 파일에 작성합니다.

<?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="post">
	<cache />
	
	<!-- [조회] 코드 목록 조회 -->
	<select id="getPostList" parameterType="map"
		resultType="com.khjdev.blog.post.PostVO">
		SELECT
			rec_key
			,subject
			,content
			,writer_id
			,writer_name
			,log_date
		FROM post_tbl
		<where>
			<if test="subject != null and subject != ''">
				AND subject LIKE '%'||#{group}||'%'
			</if>
		</where>
	</select>
</mapper>

7.7 API 테스트

앞서 작성한 게시판 목록을 불러오는 테스트 API를 개발해봅시다.

개발 순서는 아래와 같습니다.

  1. 먼저 ApiController Class를 생성하고, @RestController 어노테이션을 붙입니다.
  2. 그 다음 응답 메소드를 작성하고, url을 매핑(@GetMapping)합니다.
@RestController
public class ApiController {
	private final PostService postService;

	@Autowired
	public ApiController(PostService postService) {
		this.postService = postService;
	}

	@GetMapping("/posts")
	public ResponseEntity<Object> getPostList(HttpServletRequest request,
			@RequestBody Map<String, Object> paramMap) {
		Object result = postService.getPostList(paramMap);
		return new ResponseEntity<Object>(result, HttpStatus.OK);
	}
}

  1. 이제 웹브라우저에서 /posts로 요청을 보내 목록을 받아봅시다.

💎 마치며

이번 포스팅에서는 프로젝트 구성 시에 고려해야될 사항과 Spring MVC 프로젝트의 전반적인 설정에 대해 살펴보았습니다. 또한 데이터베이스 연결하여 게시판 컴포넌트를 생성하고 목록을 조회하는 간단한 API을 개발하는 것까지 실습하였습니다.

profile
Java, Spring 기반 풀스택 개발자의 개발 블로그입니다.

0개의 댓글