코드로 배우는 스프링 웹 프로잭트 Part3
Chapter07 스프링 MVC 프로젝트의 기본 구성
예제를 작성하기에 앞서서 전체 데이터의 흐름을 보기 위해 스프링 MVC를 이용하는 프로젝트의 구성을 이해해보도록 하겠습니다.
브라우저에서 전송한 데이터를 스프링 MVC의 어떤 단계를 거쳐서 실행되는지를 이해한다면 문제가 발생했을 때 빠른 대처와 대안을 찾을 수 있기 때문입니다.
일반적으로 웹 프로젝트는 33-tier(티어)방식으로 구성합니다.
Presentation Tier(화면 계층) : 화면에 보여주는 기술을 사용하는 영역
책의 예제에서는 Servlet/JSP나 스프링 MVC가 담당하는 영역입니다.
프로젝트의 성격에 맞게 앱으로 제작 하거나, CS(Client-Server)로 구성되는 경우도 있습니다.
이전 파트에서 학습한 스프링 MVC와 JSP를 이용한 화면 구성이 이에 속합니다.
Business Tier(비즈니스 계층) : 순수한 비즈니스 로직을 담고 있는 영역
고객이 원하는 요구 사항을 반영하는 계층입니다.
이 영역의 설계는 고객의 요구사항과 정확히 일치해야 합니다.
이 영역은 주로 xxxService와 같은 이름으로 구성하고, 메서드의 이름 역시 고객들이 사용하는 용어를 사용하는것이 좋습니다.
Persistence Tier(영속 계층 혹은 데이터 계층) : 데이터를 보관하고 사용하는 방식에 대한 설계가 들어가는 계층입니다.
일반적인 경우에는 데이터베이스를 많이 이용 하지만, 경우에 따라서 네트워크 호출이나 원격 호출 등의 기술이 접목될 수 있습니다.
이 영역은 Mybatis와 mybatis-spring을 이용해서 구성했던 파트 1을 이용합니다.
계층에 대한 설명을 스프링 MVC와 맞춰보면 다음과 같은 구조가 됩니다.
스프링 MVC 영역은 Presentation Tier를 구성하게 되는데, 각 영역은 사실 별도의 설정을 가지는 단위로 볼 수 있습니다.
이전 예제에서는 root-context.xml, servlet-context.xml 등의 설정 파일이 해당 영역의 설정을 담당 하였습니다.
스프링 Core 영역은 흔히 POJO(Plain-Old-Java-Object)의 영역입니다.
스프링의 의존성 주입을 이용해서 객체간의 연관구조를 완성해서 사용 합니다.
Mybatis영역은 현실적으로는 mybatis-spring을 이용해서 구성하는 영역입니다.
SQL에 대한 처리를 담당하고 있습니다.
7.1 각 영역의 Naming Convention(명명 규칙)
프로젝트를 위와 같이 3-tier로 구성하는 일반적인 이유는 유지보수에 대한 필요성 때문입니다.
각 영역은 독립적으로 설계되어 나중에 특정한 기술이 변경되도라도 필요한 부분만을 전자 부품처럼 쉽게 교환할수 있습니다.
따라서 설계당시부터 각 영역을 구분하고, 해당 연결 부위는 인터페이스를 이용해서 설계하는 것이 일반적인 구성 방식입니다.
프로젝트를 진행할 때에는 다음과 같은 네이밍 규칙을 가지고 작성 합니다.
- xxxController : 스프링 MVC에서 동작하는 Controlloer 클래스를 설계할 떄 사용 합니다.
- xxxService,xxxServiceImpl : 비즈니스 영역을 담당하는 인터페이스는 xxxService라는 방식을 사용하고, 인터페이스를 구현한 클래스는 xxxServiceImpl이라는 이름을 사용합니다.
- xxxDAO, xxxRepository : DAO(Data-Access-Object), Repository(저장소)라는 이름으로 영역을 따로 구성하는 것이 보편적입니다. 다만 이 책의 예제는 별도로 DAO를 구성하는 대신에 Mybatis의 Mapper 인터페이스를 활용 합니다.
- VO,DTO : VO와 DTO는 일반적으로 유사한 의미로 사용하는 용어로, 데이터를 담고 있는 객체를 의미한다는 공통점이 있습니다. 다만 VO의 경우는 주로 Read Only의 목적이 강하고, 데이터 자체도 Immutable(불편)하게 설계하는 것이 정석입니다. DTO는 주로 데이터 수집의 용도가 좀 더 강합니다. 예를 들어 웹 화면에서 로그인 하는 정보를 DTO로 처리하는 방식을 이용합니다. 이 책에서는 테이블과 관련된 데이터는 VO라는 이름을 사용하겠습니다.
7.1.1 패키지의 Naming Convention
패키지의 구성은 프로젝트의 크기나 구성원들의 성향으로 결정합니다.
예를 들어 규모가 작은 프로젝트는 Controller 영역을 별도의 패키지로 설계하고, Service 영역 등을 하나의 패키지로 설계할 수 있습니다.
반면에 프로젝트의 규모가 커서 많은 Service 클래스와 Controller들이 혼재할 수 있다면 비즈니스를 단위별로 구분하고(비즈니스 단위 별로 패키지를 작성하고) 다시 내부에서 Controller 패키지, Service 패키지 등으로 나누는 방식을 이용합니다.
이런 방식은 프로젝트 담당자가 명확해지고, 독립적인 설정을 가지는 형태로 개발하기 떄문에 큰 규모의 프로젝트에 적합합니다.
다만 패키지가 많아지고, 구성이 복잡하게 느껴질 수 있습니다.
이 책의 예제 구성은 PART 3 이후로는 다음과 같은 패키지를 구성할 것입니다.
7.2 프로젝트를 위한 요구사항
프로젝트를 진행하기 전에 고객의 요구사항을 인식하고, 이를 설계한느 과정이 필요합니다.
흔히 이를 요구사항 분석 설계 라고 하는데, 고객이 원하는 내용이 무엇이고, 어느 정도 까지 구현할 것인지에 대한 프로젝트의 범위를 정하는 것을 목표로 합니다.
요구사항은 실제로 상당히 방대해 질 수 있으므로, 프로젝트에서는 단계를 정확하게 구분해 주는 것이 좋습니다.
요구사항은 온전한 문장으로 정리 하는 것이 좋습니다.
주어는 고객 이고 목적어는 대상이 됩니다.
여기서 대상은 결국 데이터베이스 설계와 시스템 설계에서 가장 중요한 용어가 됩니다.
예를들어 게시판의 경우 다음과 같이 요구사항을 정리 할 수 있습니다.
- 고객은 새로운 게시물을 등록할 수 있어야 한다.
- 고객은 특정한 게시물을 조회할 수 있어야 한다.
- 고객은 작성한 게시물을 삭제할 수 있어야 한다.
이 경우 대상은 게시물이 되므로 게시물이라는 용어가 필요하게 되고, 게시물의 구조를 판단해서 데이터베이스 테이블을 설계합니다.
예를 들어 게시물의 경우 'tbl_board'라는 테이블을 설계하고
테이블과 관련된 VO 클래스를 org.zerock.domain.BoardVO와 같은 이름으로 설계될 수 있습니다.
게시물과 관련된 로직은 org.zerock.service.BoardService가 될 수 있고, org.zerock.controller.BoardController라는 이름의 클래스를 생성하는 연속적인 과정을 거치게 됩니다.
7.2.1 요구사항에 따른 화면 설계
요구사항에서 나오는 용어를 기준으로 테이블이나 클래스의 이름이 정해지듯이, 요구사항은 화면에서도 영향을 미치게 됩니다.
고객이 새로운 게시물은 등록할 수 있어야 한다면 당연히 그에 해당한느 화면을 구성해야 합니다.
이 구성은 어떤 내용들을 입력하게 될 것인가에 세부적인 설계가 되고, 이를 기준으로 테이블이나 클래스의 맴버 변수(인스턴스 변수)들을 설계하게 됩니다.
실제 프로젝트에서는 결과로 스토리 보드를 만들게 됩니다.
화면 설계를 할 때는 주로 Mock-Up(목업) 툴을 이용하는 경우가 많습니다.
대표적으로 Power Point, Balsamiq studio, Pencil Mockup 등의 SW를 이용해 작성합니다.
각 화면 을 설계하는 단계에서는 사용자가 입력해야 하는 값과 함께 전체 페이지의 흐름이 설계 됩니다.
이 화면의 흐름을 URL로 구성하게 되는데 이 경우 GET/POST 방식에 대해 같이 언급해두는것이 좋습니다.
7.3 예제 프로젝트 구성
예제를 위한 프로젝트는 ex02 이름으로 생성하고 Spring Legacy Project로 생성합니다.
프로젝트르 생성한 이후에는 pom.xml의 수정, 데이터베이스 관런 처리, 스프링 MVC 처리와 같은 순으로 진행 합니다.
먼저 프로젝트를 생성 해 줍니다.
7.3.1 pom.xml 수정
프로젝트를 생성하고 pom.xml의 설정을 수정해줍니다.
추가된 사항 : spring-tx, spring-jdbc,spring-test, HikariCP, MyBatis, mabatis-spring,Log4jdbc, LomBok,jUnit
Servlet 버전 : 2.5 -> 3.0, Maven Version : 1.8
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.zerock</groupId>
<artifactId>controller</artifactId>
<name>ex02</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>1.8</java-version>
<org.springframework-version>5.0.7.RELEASE</org.springframework-version>
<org.aspectj-version>1.6.10</org.aspectj-version>
<org.slf4j-version>1.6.6</org.slf4j-version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>2.7.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.bgee.log4jdbc-log4j2</groupId>
<artifactId>log4jdbc-log4j2-jdbc4</artifactId>
<version>1.16</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
수정이 끝났으면 Maven > Update project를 실행 합니다.
마지막으로 Oracle JDBC Driver를 프로젝트의 Build Path에 추가하고 Deployment Assembly에도 추가 합니다.
7.3.2 테이블 생성과 Dummy(더미) 데이터 생성
SQL Developer를 이용해 PART1 에서 작성한 book_ex 계정을 이욯애서 테이블을 생성 합니다.
게시물은 각 게시물별로 고유 번호가 필요 합니다.
오라클의 경우 시퀀스(sequence)를 이용해서 처리 합니다.
create sequence seq_board;
create table tbl_board (
bno number(10,0),
title varchar2(200) not null,
content varchar2(2000) not null,
writer varchar2(50) not null,
regdate date default sysdate,
updatedate date default sysdate
);
alter table tbl_board add constraint pk_board
primary key (bno);
시퀀스를 생성할 때에는 데이터베이스의 다른 오브젝트들과 구분하기 위해 seq_와 같이 시작하는것이 일반적입니다.
테이블을 생성할때는 tbl_로 시작 하거나 t_와 같이 구분 가능 한 단어를 앞에 붙여 주는 것이 좋습니다.
tbl_board 테이블은 고유의 번호를 가지기 위해서 bno 칼럼을 생성 했고, 제목(title), 내용(contectent), 작성사(writer)를 칼럼으로 지정 합니다.
테이블을 설계할 때는 가능하면 레코드의 생성 시간(regdate)과 최종 수정 시간(updatedate)칼럼을 지정 합니다.
이때 기본값으로 sysdate를 지정해서 레코드가 생성 된 시간을 자동으로 기록 되게 합니다.
테이블 생성 이후에는 alter table.... 을 이용해서 테이블에 primary key(이하 PK)를 지정해 주었습니다.
PK를 지정할 때 pk_board 이름을 부여 했는데 이 이름은 뒤에 중요하게 사용 되므로 반드시 구분이 가능하게 생성 해 주는 것 이 좋습니다.
더미데이터 추가
테이블을 생성하고 나면 여러 개의 데이터를 추가해 주는데 이런 의미 없는 데이터를 흔히 토이 데이터(toy data) 혹은 더미 데이터(dummy data) 라고 부릅니다.
다음과 같은 방법으로 데이터를 추가 해줍니다.
insert into tbl_board(bno,title,content,writer)
values(seq_board.nextval,'테스트 제목','테스트 내용','user00');
bno 칼럼에는 매번 새로운 값이 들어가야 하므로 seq_board.nextval을 이용해 새로운 번호를 채번 했습니다.
오라클 데이터베이스에서는 결과를 저장하기 위해서는 commit을 해주어야 합니다.
insert를 여러 번 실행 하고 commit을 한 후에 정상적으로 데이터가 처리 되었는지 select를 해주겠습니다.
7.4 데이터베이스 관련 설정 및 테스트
root-context.xml에는 mybatis-spring 네임스페이스를 추가하고, PART1 에서 작성한 DataSource의 설정과 MyBatis의 설정을 추가 합니다.
<?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:context="http://www.springframework.org/schema/context"
xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.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:orcl"></property>
<property name="username" value="c##book_ex"></property>
<property name="password" value="book_ex"></property>
</bean>
<!-- HikariCP configuration -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
destroy-method="close">
<constructor-arg ref="hikariConfig" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
</bean>
<mybatis-spring:scan base-package="org.zerock.mapper"/>
<context:component-scan base-package="org.zerock.sample">
</context:component-scan>
</beans>
root-contect.xml은 내부적으로 Log4jdbc를 이용하는 방식으로 구성되어 있으므로 PART1에서 작성된 log4jdbc.log4j2.properties 파일도 추가 해줍니다.
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
프로젝트가 정상적으로 실행 하려면 먼저 DataSource와 MyBatis의 연결이 반드시 필요하므로 PART 1 에서 작성했던 DataSourceTests 클래스와 JDBCTest 클래스를 패키지에 추가 합니다.
package org.zerock.persistence;
import static org.junit.Assert.fail;
import java.sql.Connection;
import javax.sql.DataSource;
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 lombok.Setter;
import lombok.extern.log4j.Log4j;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
//Java 설정을 사용할 경우
//@ContextConfiguration(classes= {RootConfig.class})
@Log4j
public class DataSourceTests {
@Setter(onMethod_ = { @Autowired })
private DataSource dataSource;
@Test
public void testConnection() {
try (Connection con = dataSource.getConnection()){
log.info(con);
}catch(Exception e) {
fail(e.getMessage());
}
}
}
package org.zerock.persistence;
import static org.junit.Assert.fail;
import java.sql.Connection;
import java.sql.DriverManager;
import org.junit.Test;
import lombok.extern.log4j.Log4j;
@Log4j
public class JDBCTests {
static {
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void testConnection() {
try (Connection con = DriverManager.getConnection("jdbc:oracle:thin:@172.30.1.74:1521:orcl", "c##book_ex",
"book_ex")) {
log.info(con);
} catch (Exception e) {
fail(e.getMessage());
}
}
}
JDBCTests와 DataSourceTests는 개발 이전에 반드시 테스트를 해서 DB연결이 정상적인지 확인 해야 합니다.
'개발관련 > 코드로 배우는 스프링 웹 프로젝트(개정판)' 카테고리의 다른 글
Part3 - 비즈니스 계층 Chapter09 (0) | 2023.03.19 |
---|---|
Part3 - 영속/비즈니스 계층의 CRUD 구현 Chapter08 (0) | 2023.03.09 |
Part2 - 스프링 MVC Controller Chapter06 (0) | 2023.02.20 |
Part2 - 스프링 MVC의 기본 구조 Chapter05 (1) | 2023.02.19 |
Part1 - 스프링 개발 환경 구축 Chapter04 (0) | 2022.12.27 |