스프링 MVC 프로젝트 기본 구성

cy8erpsycho·2023년 8월 24일
0

스프링

목록 보기
9/29
post-thumbnail

스프링 MVC 프로젝트 기본 구성


3-tier 구조

  • Presentation Tier(화면 계층) : 화면에 보여주는 기술을 사용하는 영역이다. Servlet/JSP나 스프링 MVC가 담당한다.
  • Business Tier(비즈니스 계층) : 순수한 비즈니스 로직을 담고 있는 영역이다.
  • Persistence Tiser(데이터 계층) : 데이터를 어떤 방식으로 보관하고, 사용하는가에 대한 설계가 들어가는 계층이다. 일반적인 경우 데이터 베이스를 이용한다.

스프링 MVC를 이용하는 구조

Naming Convention(명명규칙)

  • xxxController: 스프링 MVC에서 동작하는 Controller 클래스

  • xxxSerivce, xxxServiceImpl: 비즈니스 영역을 담당하는 인터페이스는 ‘xxxService’라는 방식을 사용하고, 인터페이스를 구현한 클래스는 ‘xxxServiceImpl’이라는 이름을 사용(실제 reference 변수는 'xxxService'의 변수 사용)

  • xxxDAO, xxxRepository: DAO(Data-Access-Object)나 Repository(저장소)라는 이름으로 영역을 따로 구성하는 것이 보편적. 예제에서는 별도의 DAO를 구성하는 대신에 MyBatis의 Mapper 인터페이스를 활용.

  • VO, DTO: VO의 경우는 주로 Read Only의 목적이 강하고, 데이터 자체도 Immutable(불변)하게 설계. DTO는 주로 데이터 수집의 용도
    VO : 객체를 상수화 시켜 변하지 않게 한다. 그럼 왜씀? 메소드간 데이터를 ㄱ전달할떄 덩어리로 묶어서 전달하는 용도
    DTO : DB에서 데이터 값 받아서 전달하는 용도(DB쪽 용도)

패키지의 Naming Convention

비즈니스를 단위별로 구분하고 다시 내부에서 Controller 패키지, Service 패키지 등으로 다시 나누는 방식을 이용한다.

CRUD 흐름

R-C-(UD)


예제 프로젝트 구성

프로젝트의 생성 및 준비

  • Spring Legacy Project의 생성
  • pom.xml에서 스프링 버전 변경
  • spring-test,spring-jdbc(회사에 맞는 jdbc를 가져와야함),
    spring-tx 추가
  • junit버전, Log4j 변경
  • Servlet 버전 변경
  • HikariCP, MyBatis, mybatis-spring, Log4jdbc 추가(DB관련)
  • JDBC드라이버 프로젝트내 추가
  • 기타 Lombok의 설정 등

Project name : ex02
패키지의 마지막 단어가 context root가 된다.
컨텍스트의 이름은 겹치지 않도록 웹프로젝트의 이름으로 하는 경우가 많다.


📘 프로젝트 생성 및 환경설정

프로젝트를 생성한 후 제일 먼저 Framwork의 버전을 맞추는 것이 원칙이다.

web.xml에 인코딩 필터 매핑 추가

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

.jsp 에 한글 설정 코드 추가

<%@ page session="false" language="java" contentType="text/html; charset=UTF-8" 
	pageEncoding="UTF-8"%>

설정을 마친 후 기본적으로 동작하는지 확인한다.
(HomeController.java와 home.jsp는 환경설정이 모두 끝난 후 지우도록 한다)

✅ pom.xml 수정

<java-version>1.6</java-version> → 11
3.1.1 REALEASE → 5.0.7 REALEASE

<source>11</source>
<target>11</target>

Maven>Project Update 후 버전이 제대로 수정되었는지 다음과 같이 확인한다.

✅ pom.xml <dependency> 수정

junit 버전 변경

javax.servlet 버전 변경

<artifactId>javax.servlet-api</artifactId> 로 변경
<version>3.1.0</version> 로 변경

✅ 필요한 <Dependency> 추가

롬복추가

https://mvnrepository.com/artifact/org.projectlombok/lombok/1.18.0

depency를 복사해서 pom.xml에 추가

log4j 버전 변경

<version>1.2.17</version>로 변경
<scope>runtime</scope> 삭제 또는 추석처리

spring-test 추가

https://mvnrepository.com/artifact/org.springframework/spring-test/5.0.7.RELEASE

		<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${org.springframework-version}</version>
			<scope>test</scope>
		</dependency>

spring-jdbc 추가

https://mvnrepository.com/artifact/org.springframework/spring-jdbc/5.0.7.RELEASE

mybatis 추가

https://mvnrepository.com/artifact/org.mybatis/mybatis/3.4.6

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>

spring-mybatis 추가

https://mvnrepository.com/artifact/org.mybatis/mybatis-spring/1.3.2

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.3.2</version>
</dependency>

hikari-cp 추가

<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>2.7.8</version>
</dependency>

log4jdbc 추가

https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4/1.16

<!-- https://mvnrepository.com/artifact/org.bgee.log4jdbc-log4j2/log4jdbc-log4j2-jdbc4 -->
<dependency>
    <groupId>org.bgee.log4jdbc-log4j2</groupId>
    <artifactId>log4jdbc-log4j2-jdbc4</artifactId>
    <version>1.16</version>
</dependency>

log4jdbc.log4j2.properties 파일 추가

src/main/resources 에 추가한다. 밑에도 src폴더가 있으니 올바른 경로를 찾아서 추가하도록 한다.

파일내용

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator

ojdbc8 추가

(hikari오류때문에 추가한건데 왜 난 오류가 안났지)

<!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 -->
<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc8</artifactId>
    <version>19.3.0.0</version>
</dependency>


✅ root-context에 <bean> 추가하기

<?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 https://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<!-- Root Context: defines shared resources visible to all other web components -->
	
	<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
		<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property>
		<property name="jdbcUrl" value="jdbc:log4jdbc:oracle:thin:@localhost:1521:XE"></property>
        
        <property name="username" value="book_ex"></property>
		<property name="password" value="book_ex"></property>
	</bean>
		
</beans>

오류가 없다면 다음과 같이 Beans Graph에 작성한 bean들이 보여야한다.

dataSource추가(2.7.8 버전)

destroy-method에 close추가

위와같이 자동으로 추가된 것을 확인할 수 있다.

<?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 https://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<!-- Root Context: defines shared resources visible to all other web components -->
	
	<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
		<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property>
		<property name="jdbcUrl" value="jdbc:log4jdbc:oracle:thin:@localhost:1521:XE"></property>
		
		<property name="username" value="book_ex"></property>
		<property name="password" value="book_ex"></property>
	</bean>
		
	<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
		destroy-method="close" primary="false">
		<constructor-arg ref="hikariConfig"></constructor-arg>
	</bean>
</beans>

sqlSessionFactory <bean> 추가

위에서 Name의 dataSource는 내부적으로 정해져서 변경할 수 없다.
Ref는 이 부분의 dataSource를 참조한다는 뜻이다.

	<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
		destroy-method="close" primary="false">
		<constructor-arg ref="hikariConfig"></constructor-arg>
	</bean>

💡 dataSource란?

STS(Spring Tool Suite)는 Spring Framework를 위한 특화된 IDE(Integrated Development Environment)입니다. "dataSource"는 데이터베이스와의 커넥션을 관리하는 객체로, 일반적으로 Spring Framework 내에서 JDBC(Java Database Connectivity)나 JPA(Java Persistence API) 등을 사용할 때 설정합니다. 이 객체는 데이터베이스와의 실제 연결을 캡슐화하고 커넥션 풀링 같은 고급 기능을 제공합니다.

dataSource의 역할은 크게 아래와 같습니다:

  1. 커넥션 풀링: 여러 사용자가 동시에 데이터베이스에 접근할 때, 매번 새로운 연결을 생성하는 것은 비효율적입니다. dataSource는 여러 커넥션을 미리 만들어 놓고, 필요할 때 커넥션을 제공하고 반환받는 역할을 합니다.

  2. 트랜잭션 관리: 데이터베이스 작업을 트랜잭션 단위로 처리할 수 있도록 도와줍니다.

  3. 추상화: dataSource는 데이터베이스 엔진에 대한 실제 연결 정보를 추상화하여, 애플리케이션 코드에서는 데이터베이스 연결을 직접 다루지 않아도 됩니다. 이로 인해 데이터베이스가 변경되더라도 코드의 큰 수정 없이 dataSource 설정만 변경하면 됩니다.

  4. 에러 핸들링: 연결 실패, 쿼리 오류 등 다양한 데이터베이스 관련 오류를 캡쳐하고 적절히 처리할 수 있습니다.

Spring Framework에서 dataSource는 보통 XML 설정 파일, 자바 설정 파일, 또는 애노테이션을 통해 설정됩니다. 예를 들면, 아래와 같이 XML 파일에서 dataSource를 설정할 수 있습니다:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
    <property name="username" value="root"/>
    <property name="password" value="password"/>
</bean>

여기에서 BasicDataSource는 Apache Commons DBCP 라이브러리에서 제공하는 데이터 소스 클래스입니다. 이 설정을 통해 MySQL 데이터베이스에 연결을 설정합니다. 이후에 이 dataSource 빈은 JdbcTemplate 나 JPA의 EntityManager와 같은 데이터 액세스 객체에 주입될 수 있습니다.


	<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
		destroy-method="close" primary="false">
		<constructor-arg ref="hikariConfig"></constructor-arg>
	</bean>

이 설정은 HikariCP라는 커넥션 풀 라이브러리를 사용하여 dataSource를 설정한 예시입니다. HikariCP는 고성능 데이터베이스 커넥션 풀 라이브러리로 널리 사용되고 있습니다.

설정 내용을 간단히 설명하면 다음과 같습니다:

  • <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close" primary="false">: 이 부분에서 Spring Bean을 정의하고 있습니다. id는 Bean의 식별자이며, class는 이 Bean이 어떤 클래스의 인스턴스를 생성할 것인지 명시합니다. destroy-method="close"는 이 Bean이 소멸될 때 실행할 메소드를 지정하고, primary="false"는 이 dataSource가 primary bean이 아니라는 것을 명시합니다. 여러 개의 dataSource를 가질 경우 어느 것이 주된(primary) dataSource인지 설정할 수 있습니다.
  • <constructor-arg ref="hikariConfig"></constructor-arg>: HikariDataSource의 생성자에 주입할 인자를 설정합니다. ref="hikariConfig"는 다른 Bean이나 설정에서 정의된 hikariConfig를 참조하여 생성자에 주입하는 것을 의미합니다. 즉, HikariDataSource의 설정은 hikariConfig라는 다른 Bean에서 가져옵니다.

이렇게 설정하면 Spring 애플리케이션 내에서 dataSource라는 이름으로 이 Bean을 참조할 수 있습니다. 이 Bean은 일반적으로 JdbcTemplate, EntityManager 등과 같은 데이터베이스 작업을 수행하는 다른 객체에 주입됩니다. 이를 통해 데이터베이스 연결과 관련된 모든 세부 설정이나 커넥션 관리는 HikariCP에 위임하고, 애플리케이션 로직에서는 비즈니스 로직에만 집중할 수 있습니다.


⚠ 오류 발생시 해결

STS 종료후 문제가 발생한 부분의 repository를 날리고 재시작
.m2\repository\

위와 같이 버전을 지정해서 변경하면 메이븐은 이 버전이 레퍼지토리에 있는지 확인하고 없으면 해당 버전을 다운받고 적용한다. 충돌 방지를 위해 버전을 변경한 후에는 서버를 닫고 clean하고 (캐싱 삭제) 재시작을 하도록 한다.


테이블 생성과 더미 데이터 생성

데이터베이스 내 테이블 생성

더미 데이터 추가

반복적인 실행으로 여러 개의 데이터 생성 및 확인 및 commit

데이터베이스 설정 및 테스트

  • root-context.xml
    DataSource의 설정
    sqlSessionFactory 설정

mybatis-spring scan

Namespaces 에서 mybatis-spring 체크
체크를 하면 <mybatis-spring:scan base-package=""/>을 쓸 수 있다.

<mybatis-spring:scan base-package="com.zerock.mapper"/>을 추가하면 com.zerock.mapper를 스캔해서 mapper를 찾는다.


영속/비즈니스 계층의 CRUD 구현

영속 계층의 구현 준비

프로젝트에 com.zerock.domain패키지 생성, BoardVO 클래스 정의한다.

BoardMapper.java

package com.zerock.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Select;

import com.zerock.domain.BoardVO;

public interface BoardMapper {	
	@Select("select * from tbl_board")
	public List<BoardVO> getList();
}

BoardMapper 인터페이스를 작성할 때는 이미 작성된 BoardVO 클래스를 적극적으로 활용해서 필요한 SQL을 어노테이션의 속성값으로 처리할 수 있다. SQL을 작성할 때는 반드시 ';'이 없도록 작성해야 한다.

작성된 BoardMapper 인터페이스를 테스트 할 수 있게 테스트 환경인 'src/test/java'에 'com.zerock.mapper'패키지를 작성하고 BoardMapperTests 클래스를 추가한다.

package com.zerock.mapper;

import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.zerock.domain.BoardVO;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j
public class BoardMapperTests {
	
	@Setter(onMethod_=@Autowired)
	private BoardMapper mapper;

	@Test
	public void testGetList() {
		mapper.getList().forEach(board -> log.info(board));		
	}

}

BoardMapperTests 클래스는 스프링을 이용해서 BoardMapper 인터페이스의 구현체를 주입받아서 동작한다.

정상적으로 테스트가 진행되면 위와 같은 결과가 나온다.

Mapper.XML 파일

mapper의 쿼리부분을 삭제한다.

패키지 생성 후 xml파일을 생성한다.

<?xml version="1.0" encoding="${encoding}"?>

<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

window>preference>templet

xml파일을 설정할때 틀리지 않고 기본적인 코드를 작성할 수 있도록 돕는 설정이다.

id - 어떤 메서드와 연결되어 있는지 알려주는 속성

완성된 BoardMapper.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.zerock.mapper.BoardMapper">
	<select id="getList" resultType="com.zerock.domain.BoardVO">
	<![CDATA[
	select * from tbl_board
	]]>
	</select>
</mapper>

XML 작성 주의사항

<mapper>의 namespace 속성값을 Mapper 인터페이스와 동일한 이름을 주는 것에 주의한다. <select> 태그의 id 속성값은 메서드의 이름과 일치하게 작성한다. resultType 속성값은 select 쿼리의 결과를 특정 클래스의 객체로 만들기 위해서 설정한다. XML에 사용한 <![CDATA[]]> 부분은 XML에서 부등호를 사용하기 위해서 사용한다

XML에 SQL문 처리 후에는

public interface BoardMapper {	
	//@Select("select * from tbl_board")
	public List<BoardVO> getList();
}

위와 같이 BoardMapper 인터페이스의 SQL을 제거한다.


영속 영역의 CRUD 구현

MyBatis는 내부적으로 JDBC의 PreparedStatement를 활용하고 필효한 파라미터를 처리하는 '?'에 대한 치환은 '#{속성}'을 이용해서 처리한다.

create(insert) 처리

무조건 시퀀스를 pk로 쓰진 않는다.

insert

	<insert id="insert">
	<![CDATA[
	insert into tbl_board (bno, title, content, writer)
	values (seq_board.nextval, #{title}, #{content}, #{writer})
	]]>
	</insert>

insertSelectKey

	<insert id="insertSelectKey">
		<selectKey keyProperty="bno" order="BEFORE"
			resultType="long">
			<![CDATA[
			select seq_board.nextval from dual 
			]]>
		</selectKey>
		
		<![CDATA[
		insert into tbl_board (bno, title, content, writer)
		values (#{bno}, #{title}, #{content}, #{writer})
		]]>
	</insert>

read(select) 처리

BoardMapper.java

public interface BoardMapper {	
	//@Select("select * from tbl_board")
	public List<BoardVO> getList();
	public void insert(BoardVO board);
	public void insertSelectKey(BoardVO board);
	public BoardVO read(Long bno);
}

BoardMapper.xml

	<select id="read" resultType="com.zerock.domain.BoardVO">
	<![CDATA[
	select * from tbl_board where bno = #{bno}
	]]>
	</select>

BoardMapperTests

	@Test
	public void testRead() {
		BoardVO board = mapper.read(10L);
		log.info(board);
	}


delete 처리

영향을 받은 갯수만큼의 리턴이 돌아온다.

BoardMapper.java

public interface BoardMapper {	
	//@Select("select * from tbl_board")
	public List<BoardVO> getList();
	public void insert(BoardVO board);
	public void insertSelectKey(BoardVO board);
	public BoardVO read(Long bno);
	public int delete(Long bno);
}

BoardMapper.xml

	<delete id="delete">
	<![CDATA[
	delete from tbl_board where bno = #{bno}
	]]>

BoardMapperTests

	@Test
	public void testDelete() {
		log.info("delete lines : " + mapper.delete(10L));
	}

만약 위의 테스트를 한번 더 진행하면, DB에 데이터가 없기 때문에 다음과 같이 출력된다.


update 처리

update 처리를 할 때 미리 where절을 설정하도록 한다.

BoardMapper.java

public interface BoardMapper {	
	//@Select("select * from tbl_board")
	public List<BoardVO> getList();
	public void insert(BoardVO board);
	public void insertSelectKey(BoardVO board);
	public BoardVO read(Long bno);
	public int delete(Long bno);
	public unt update(BoardVO board);
}

BoardMapper.xml

	<update id="update">
	<![CDATA[
	update tbl_board
	set title = #{title},
	content = #{content},
	writer = #{writer},
	updateDate = sysdate
	where bno = #{bno}
	]]>	
	</update>

BoardMapperTests

	@Test
	public void testUpdate() {
		BoardVO board = new BoardVO();
		board.setBno(6L);
		board.setTitle("제목 수정");
		board.setContent("내용 수정");
		board.setWriter("이름 수정");
		int count = mapper.update(board);
		log.info("UPDATE COUNT : " + count);
	}

0개의 댓글