반응형

Spting + Mybatis를 할려고한다 why? 잘까먹어서..

내가적었지만 뭔소리인가 싶기도 하며 맞는지도 의심스럽다.. 더열심이 해야겠다. 

select, insert부분만 구현했다 why? 두개면 나머지도 다할수 있다!!


시작하기 먼저 간단하게 순서를 소개하자면

 

1. pom.xml  mybatis 를하기위한 dependency 추가

 

2. root-context.xml 에 추가

3. bean 파일인 Member.java 생성 

 

4. 매핑하기위한 MemberMapper.interface 생성

5. MemberDAO.interface 와 DAO를 상속받은 MemberDAOService.java 생성

- 멤버DAO는 멤버Mapper와 내용은 같지만 Mapper만을 하기위한 인터페이스를 만드는 것이 좋다.

6. Service부분에서 값을 매핑한다 .

7. 컨트롤러 부분에서 값을 받아 화면으로 보내주면 화면 전환을 해준다.


이제 시작!! 설명은 주석으로 달려있다 천천이 보면 됨. 


1. pom.xml 에다가 mybatis 사용하기위한 아래의 3개의 dependency를  추가한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    <!-- myBatis -->        
    <dependency>
     <groupId>org.mybatis</groupId>
     <artifactId>mybatis</artifactId>
     <version>3.0.6</version>
 </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>1.0.2</version>
    </dependency>
    <!-- Spring -->
 <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${org.springframework-version}</version>
 </dependency>
 

 

 

 

 




2. root-context.xml <beans> </beans>사이에다가 다음과 같은 내용을 추가한다. 



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- Mybatis를 사용하는 패키지 경로를 적어준다.  -->    
    <context:component-scan base-package="com.test.mybatis" />
    <!-- MyBatis -->
    <bean id="dataSource"
        class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="oracle.jdbc.OracleDriver" />
        <!-- 아래의 부분은 오라클 버전이나 계정에따라 다르게적는다. -->
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
        <property name="username" value="java" />
        <property name="password" value="java" />
    </bean>
    
    <!-- 트랜잭션 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
 
    </bean>
 
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <!-- 아래부분은 매핑할 xml파일이 있는 패키지경로를 설정한다. -->
        <property name="typeAliasesPackage" value="com.test.mybatis" />
        <property name="mapperLocations" value="classpath:com/test/mybatis/*.xml" />
    </bean>
 
    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>


 

- pom.xml에서 메이븐을 추가 하였기에 root-context namespace탭에서 아래와 같이 이름뜬다 아래와같이 체크하여준다. 

 

 

 

 

 

 

 

 

 



3. Member.java 생성 (bean파일)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.test.mybatis;
 
public class Member {
    private String _name;
    private String _email;
    private String _phone;
    
    public String get_name() {
        return _name;
    }
 
    public void set_name(String _name) {
        this._name = _name;
    }
 
    public String get_email() {
        return _email;
    }
 
    public void set_email(String _email) {
        this._email = _email;
    }
 
    public String get_phone() {
        return _phone;
    }
 
    public void set_phone(String _phone) {
        this._phone = _phone;
    }
 
}//end of class
 



4. MemberMapper.interface 생성


1
2
3
4
5
6
7
8
9
10
11
12
13
package com.test.mybatis;
 
import java.util.ArrayList;
 
import com.test.mybatis.Member;
 
public interface MemberMapper {
    ArrayList<Member> getMembers();
    void insertMember(Member member);
    void updateMember(String name);
    void deleteMember(String name);
}
 

5. MemberMapper.xml 생성
- 위의 interface와 매핑한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
     PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 //여기는 매핑할 MemberMapper.java의 풀패키지명을 적어준다
<mapper namespace="com.test.mybatis.MemberMapper">
 
    <resultMap type="Member" id="MemberResultMap">
        <result property="_name" column="NAME" />
        <result property="_email" column="EMAIL" />
        <result property="_phone" column="PHONE" />
    </resultMap>
 
    <!-- id의 경우에는 Mapper.java에 동일하게 사용해야된다. -->
    <select id="getMembers" resultMap="MemberResultMap">
        SELECT * FROM tab_mybatis
    </select>
    
    <!-- parameterType 에는 구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래명이나 별칭이 들어간다  -->
    <insert id="insertMember" parameterType="com.test.mybatis.Member" >
        INSERT INTO tab_mybatis values(#{_name},#{_email},#{_phone})
    </insert>
    
    <!-- update와 delete는 생략한다. -->
    <update id=""></update>
    
    <delete id="deleteMember" parameterType="com.test.mybatis.Member">
        DELTE tab_mybatis WHERE(name=#{_name})
    </delete>
 
</mapper>
 


6. MemerDAO.java

- MemerMapper와 내용은 같으나 DAO를 Mapper의 역활로서 함께 사용시 문제가 발생하기에 따로 구분하여 사용하는 것이 좋다!!! 중요하다!! 이거때문에 시간 많이 잡아 먹었다!


1
2
3
4
5
6
7
8
9
10
11
12
13
package com.test.mybatis;
 
import java.util.ArrayList;
 
public interface MemberDAO {
 
    public ArrayList<Member> getMembers();
    public void insertMember(Member member);
    public void updateMember(String name);
    public void deleteMember(String name);
 
}
 



7. MemberDAOService.java

- DAO를 상속받아 SqlSession을 사용하여 MemberMapper.xml과 매핑시켜준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.test.mybatis;
 
import java.util.ArrayList;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
 //Service 클래스를 Repository로 등록함으로서 빈(bean) 클래스로 사용하능하게한다. 
@Repository
public class MemberDAOService implements MemberDAO {
 
 //Autowired를 사용하여 sqlSession을 사용할수 있다.
    @Autowired
    private SqlSession sqlSession;
    
    @Override
    public ArrayList<Member> getMembers() {
        ArrayList<Member> result = new ArrayList<Member>();
        //sqlSession을 통하여 매핑한다.
             MemberMapper memberMapper = sqlSession.getMapper(MemberMapper.class);
        //getMember()의 메소드명과 mapper.mxl과 id는 동일해야한다.
        result = memberMapper.getMembers();
        
        return result;
    }
    @Override
    public void insertMember(Member member) {
        MemberMapper memberMapper = sqlSession.getMapper(MemberMapper.class);
        memberMapper.insertMember(member);
    }
 //아래부분은 코딩하다 말았음
    @Override
    public void updateMember(String name) {
    }
    @Override
    public void deleteMember(String name) {
        MemberMapper memberMapper = sqlSession.getMapper(MemberMapper.class);
        memberMapper.deleteMember(name);
    }
}
 


8. MybatisController

- 컨트롤러 부분으로서 DB로부터 가져온값을 view화면으로 보내어준다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.test.controller;
 
import java.util.List;
import java.util.Locale;
 
import javax.servlet.http.HttpServletRequest;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
 
import com.test.mybatis.Member;
import com.test.mybatis.MemberDAOService;
 
@Controller
public class MybatisController {
    
    @Autowired
    private MemberDAOService memberDAOService;
    
    private static final Logger logger = LoggerFactory.getLogger(MybatisController.class);
    
    
    //시작 메인화면.
    @RequestMapping("/main")
    public ModelAndView main(Locale locale, Model model) {
        logger.info("Welcome main.", locale);
 
        // view 화면인 main.jsp에 DB로부터 읽어온 데이터를 보여준다.
        ModelAndView result = new ModelAndView();
        //addObject view에 넘어가는 데이터
        List<Member> memberList = memberDAOService.getMembers();
        result.addObject("result", memberList);
        result.setViewName("main");
        return result;
    }
    
    //insert 버튼 클릭시 값을 가져와서 result.jsp로 화면전환 해준다.
    @RequestMapping(value ="/insert", method = RequestMethod.POST)
    public ModelAndView insert(HttpServletRequest request){
        
        // HttpServletRequest를 이용하여 main.jsp로부터 값을 가져온다 getParameter로는 id값을 가져옴.
        Member member = new Member();
        member.set_name((String) request.getParameter("name"));
        member.set_email((String) request.getParameter("email"));
        member.set_phone((String) request.getParameter("phone"));
        
        memberDAOService.insertMember(member);
        System.out.println("insert complet");
        
        //아래부분은 select값을 result.jsp파일에 보여주기 위해 또사용.
        ModelAndView result = new ModelAndView();
        List<Member> memberList = memberDAOService.getMembers();
        result.addObject("result", memberList);
        result.setViewName("result");
        return result;
    }
}
 

9. result.jsp 생성
- 읽어온 값 을 화면에 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<%@ page language="java" contentType="text/html; charset=EUC-KR"
    pageEncoding="EUC-KR"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ page import ="java.util.*,com.test.mybatis.*" %>
    
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>결과창</title>
</head>
<body>
 
<table>
     <tr>
         <td width="50px" align="center">id</td>
         <td align="center">email</td>
         <td align="center">phone</td>
     </tr>
     <!-- result는 contoller의 addObject로 부터 가져온다. -->
     <c:forEach items="${result}" var="member">
         <tr>
             <td>${member._name}</td>
             <td>${member._email}</td>
             <td>${member._phone}</td>    
         </tr>
     </c:forEach>
 </table>
 </body>
</html>

 

 

10. DB

- 아래와같이 테이블을 형성하고 값을넣는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
create table tab_mybatis(
    name varchar2(10) primary key,
    email varchar2(100),
    phone varchar2(11)
);
 
insert into tab_mybatis 
values ('호랑이','thosit@naee.com','01044472343');
 
insert into tab_mybatis 
values ('말코임','qewdq@naee.com','1234654');
 
insert into tab_mybatis 
values ('커피임','thosit@naee.com','01044472343');
 
 
drop table tab_mybatis;



---
결과화면이다.


 


 

반응형
반응형

1. 공통 기능 

지난번 글에서 작성한 전자정부 프레임워크를 이용한 페이징과 마찬가지로 공통적으로 사용하는 기능을 먼저 만들도록 하자. 

거의 대부분은 비슷하지만 약간씩 다른데, 쿼리는 똑같기 때문에 다시 언급하지 않도록 하겠다.


1. 라이브러리 및 jsonView 설정

먼저 Ajax를 사용하여 클라이언트와 서버의 데이터 통신을 할때 사용할 jsonView를 설정하려고 한다. JSON이 어떤것인지는 대부분 알것이라 생각해서 특별히 언급을 할 필요는 없을것이라고 생각한다. 


1) 라이브러리 추가

먼저 pom.xml에 다음의 내용을 추가한다. 

 













<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.6.4</version>
</dependency>
 
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.6.4</version>
</dependency>

 


이 라이브러리를 이용하면 어떠한 형태의 데이터도 json 형식의 데이터로 자동으로 변환을 해준다. 이것은 나중에 어떻게 보여지는지 살펴보도록 하겠다.


2) jsonView 설정

위에서 추가한 라이브러리를 이용하여 jsonView를 설정할 차례이다. 

action-servlet.xml을 다음과 같이 변경한다.

 

<?xml version="1.0" encoding="UTF-8"?>
 
    <context:component-scan base-package="first" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
     
    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean class="first.common.resolver.CustomMapArgumentResolver"></bean>      
        </mvc:argument-resolvers>
    </mvc:annotation-driven>
     
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/**"/>
            <bean id="loggerInterceptor" class="first.common.logger.LoggerInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>
     
    <aop:aspectj-autoproxy/>
    <bean id="loggerAspect" class="first.common.logger.LoggerAspect" />
     
    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>
     
    <bean class="org.springframework.web.servlet.view.BeanNameViewResolver" p:order="0" />
    <bean id="jsonView" class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" />
     
    <bean
        class="org.springframework.web.servlet.view.UrlBasedViewResolver" p:order="1"
        p:viewClass="org.springframework.web.servlet.view.JstlView"
        p:prefix="/WEB-INF/jsp/" p:suffix=".jsp">
    </bean>
</beans>


36번째 줄에 jsonView라는것이 추가된것을 확인할 수 있다. 여기서 jsonView라는 것은 추후 Controller에서 사용될 것이다.


2. AbstractDAO

먼저 페이징을 처리하는 로직을 만들도록 하자. AbstractDAO.java에 다음의 내용을 작성하자.

 























@SuppressWarnings("unchecked")
public Object selectPagingList(String queryId, Object params){
    printQueryId(queryId);
    Map<String,Object> map = (Map<String,Object>)params;
     
    String strPageIndex = (String)map.get("PAGE_INDEX");
    String strPageRow = (String)map.get("PAGE_ROW");
    int nPageIndex = 0;
    int nPageRow = 20;
     
    if(StringUtils.isEmpty(strPageIndex) == false){
        nPageIndex = Integer.parseInt(strPageIndex)-1;
    }
    if(StringUtils.isEmpty(strPageRow) == false){
        nPageRow = Integer.parseInt(strPageRow);
    }
    map.put("START", (nPageIndex * nPageRow) + 1);
    map.put("END", (nPageIndex * nPageRow) + nPageRow);
     
    return sqlSession.selectList(queryId, map);
}

 

간단히 소스를 살펴보자.


먼저 6~16번째 줄이 현재 페이지 번호와 한 페이지에 보여줄 행의 개수를 계산하는 부분이다.

화면에서 PAGE_INDEX와 PAGE_ROW라는 값을 보내주도록 되어있지만, 혹시 모를 예외상황에 대비하여 해당 값을 각각 0과 20으로 설정하였다.

그 다음 17, 18번째 줄에서 페이징 쿼리의 시작과 끝값을 계산하도록 하였다. 

그 후 일반적인 리스트 조회를 호출하도록 하였다.


전자정부 프레임워크를 사용할때와는 다르게 계산할 것이 많이 없고, 반환값도 List인것을 확인할 수 있다. 


3. CSS

사실 여기서는 안해도 되는 부분이지만 나중에 페이징 결과를 좀 더 보기 쉽게 할수있게 스타일시트를 수정한다.

ui.css를 열어서 다음의 내용을 추가한다.

 



.pad_5 {padding5px;}

 


4. 자바스크립트

이제 이번글의 핵심인 스크립트의 작성 부분이다. 여기서는 크게 두가지에 대해서 이야기를 할것이다.

첫번째로는 Ajax 호출에 대한 내용이고 두번째는 jQuery를 이용한 페이징 태그를 만드는 함수이다.


1) Ajax

먼저 Ajax가 무엇인지 아주 간단히 이야기를 하려고 한다. 

Ajax는 Asynchronous JavaScript and Xml 의 약자로 클라이언트(웹브라우저)와 서버의 비동기적 통신을 통한 데이터 전송을 이용하는 방법이다. Ajax는 클라이언트와 서버가 내부적으로 데이터 통신을 하고 그 결과를 웹페이지에 프로그래밍적으로 반영한다. 그 결과 화면의 로딩없이 그 결과를 보여줄 수 있다.

jQuery에서는 Ajax 통신을 쉽게 하는 ajax() 함수를 제공하고 있다. 

인터넷에서 ajax 함수에 대한 기본적인 설명 및 사용법은 많이 나와있으니 넘어가고, 여기서는 기존의 ComSubmit과 같이 Ajax를 공통함수로 만들어서 사용하려고 한다.

common.js에 다음의 내용을 작성하자.

 















































var gfv_ajaxCallback = "";
function ComAjax(opt_formId){
    this.url = "";     
    this.formId = gfn_isNull(opt_formId) == true "commonForm" : opt_formId;
    this.param = "";
     
    if(this.formId == "commonForm"){
        var frm = $("#commonForm");
        if(frm.length > 0){
            frm.remove();
        }
        var str = "<form id='commonForm' name='commonForm'></form>";
        $('body').append(str);
    }
     
    this.setUrl = function setUrl(url){
        this.url = url;
    };
     
    this.setCallback = function setCallback(callBack){
        fv_ajaxCallback = callBack;
    };
 
    this.addParam = function addParam(key,value){
        this.param = this.param + "&" + key + "=" + value;
    };
     
    this.ajax = function ajax(){
        if(this.formId != "commonForm"){
            this.param += "&" + $("#" this.formId).serialize();
        }
        $.ajax({
            url : this.url,   
            type : "POST",  
            data : this.param,
            async : false,
            success : function(data, status) {
                if(typeof(fv_ajaxCallback) == "function"){
                    fv_ajaxCallback(data);
                }
                else {
                    eval(fv_ajaxCallback + "(data);");
                }
            }
        });
    };
}

 

간단히 소스를 보자. 

일단 기본적인 틀은 기존에 만들었던 ComSubmit 객체와 비슷한것을 알 수 있다. 

여기서 추가된것은 크게 setCallback이라는 함수와 submit() 대신에 ajax() 라는 함수가 추가되었다. 


먼저 setCallback은 ajax를 이용하여 데이터를 전송한 후 호출될 콜백함수의 이름을 지정하는 함수이다. 

Ajax는 클라이언트와 비동기적으로 수행되기 때문에 return을 받을수가 없다. 따라서 클라이언트가 서버에 어떠한 동작을 요청하고 그에 따른 결과가 다시 클라이언트측에 전달될 때 호출되는것이 콜백함수다. 여기서는 setCallback이라는 함수를 이용하여 ajax 요청 후 호출될 함수의 이름을 지정하는 것이다.

이에 대한 내용은 잠시 후 실제 소스에서 다시 보도록 하겠다. 


그 다음으로는 ajax() 함수이다. 이 함수가 실질적인 ajax 기능을 수행한다. 

submit의 경우 어떠한 요청을 하면 화면이 바뀌기때문에 그 안의 기능이 많지 않았지만, ajax의 경우는 설정을 해야할 게 몇가지가 있다. 

jQuery를 이용한 ajax는 여러가지 설정할 수 있는데, 여기서는 간단히 몇가지만 설정을 하였다. 

url은 호출할 url을 의미하고, type은 POST 또는 GET 방식의 통신을 설정한다. 여기서는 그냥 POST로 지정을 하였다. 

그리고 data 부분이 ajax를 이용하여 서버에 요청을 할 때 서버로 전달할 인자(Parameter)를 의미한다. 

원래는 저런 방식으로 하지않고 object 형식으로 data를 지정하지만, 여기서는 addParam또는 form 자체를 전송하기 때문에 저런식으로 하였다.

그 다음 async는 동기식과 비동기식의 통신방식을 의미한다. 

동기식은 클라이언트 -> 서버 -> 클라이언트의 과정에서 서버의 답변이 올때까지 다른 일을 수행하지 못하고 기다리기만 하는 방식이고,

비동기식은 요청을 보내고 다른일을 수행할 수 있다. (이에 대한 좀 더 자세한 설명은 쉽게 찾아볼 수 있으니 이정도만 이야기하려고 한다.)

여기서는 그냥 비동기식으로 설정을 하였다. 

여기서 나온 설정 및 다른 설정은 jQuery 공식홈페이지의 ajax 부분을 보면 자세하게 나와있다. (http://api.jquery.com/jquery.ajax/)


여기까지 해서 ajax 함수에 대한 설명은 간단히 끝났다. 아직은 무슨소리인지 이해가 가지 않는게 많을수도 있고 이걸 어떻게 쓰는지 모르겠지만, 나중에 화면에서 이를 어떻게 사용하는지를 보면서 다시 한번 설명을 하도록 하겠다.


2) 페이징 태그

 

다음으로는 페이징 태그를 만드는 부분이다. 

프로젝트에서는 페이징을 사용하는 화면이 여러개가 있기 때문에 JSP에서는 함수를 호출하여 간단히 페이징 태그 작성 및 기능을 수행하도록 공통함수로 만들었다.

 

/*
divId : 페이징 태그가 그려질 div
pageIndx : 현재 페이지 위치가 저장될 input 태그 id
recordCount : 페이지당 레코드 수
totalCount : 전체 조회 건수
eventName : 페이징 하단의 숫자 등의 버튼이 클릭되었을 때 호출될 함수 이름
*/
var gfv_pageIndex = null;
var gfv_eventName = null;
function gfn_renderPaging(params){
    var divId = params.divId; //페이징이 그려질 div id
    gfv_pageIndex = params.pageIndex; //현재 위치가 저장될 input 태그
    var totalCount = params.totalCount; //전체 조회 건수
    var currentIndex = $("#"+params.pageIndex).val(); //현재 위치
    if($("#"+params.pageIndex).length == 0 || gfn_isNull(currentIndex) == true){
        currentIndex = 1;
    }
     
    var recordCount = params.recordCount; //페이지당 레코드 수
    if(gfn_isNull(recordCount) == true){
        recordCount = 20;
    }
    var totalIndexCount = Math.ceil(totalCount / recordCount); // 전체 인덱스 수
    gfv_eventName = params.eventName;
     
    $("#"+divId).empty();
    var preStr = "";
    var postStr = "";
    var str = "";
     
    var first = (parseInt((currentIndex-1) / 10) * 10) + 1;
    var last = (parseInt(totalIndexCount/10) == parseInt(currentIndex/10)) ? totalIndexCount%10 : 10;
    var prev = (parseInt((currentIndex-1)/10)*10) - 9 > 0 ? (parseInt((currentIndex-1)/10)*10) - 9 : 1;
    var next = (parseInt((currentIndex-1)/10)+1) * 10 + 1 < totalIndexCount ? (parseInt((currentIndex-1)/10)+1) * 10 + 1 : totalIndexCount;
     
    if(totalIndexCount > 10){ //전체 인덱스가 10이 넘을 경우, 맨앞, 앞 태그 작성
        preStr += "<a href='#this' class='pad_5' onclick='_movePage(1)'>[<<]</a>" +
                "<a href='#this' class='pad_5' onclick='_movePage("+prev+")'>[<]</a>";
    }
    else if(totalIndexCount <=10 && totalIndexCount > 1){ //전체 인덱스가 10보다 작을경우, 맨앞 태그 작성
        preStr += "<a href='#this' class='pad_5' onclick='_movePage(1)'>[<<]</a>";
    }
     
    if(totalIndexCount > 10){ //전체 인덱스가 10이 넘을 경우, 맨뒤, 뒤 태그 작성
        postStr += "<a href='#this' class='pad_5' onclick='_movePage("+next+")'>[>]</a>" +
                    "<a href='#this' class='pad_5' onclick='_movePage("+totalIndexCount+")'>[>>]</a>";
    }
    else if(totalIndexCount <=10 && totalIndexCount > 1){ //전체 인덱스가 10보다 작을경우, 맨뒤 태그 작성
        postStr += "<a href='#this' class='pad_5' onclick='_movePage("+totalIndexCount+")'>[>>]</a>";
    }
     
    for(var i=first; i<(first+last); i++){
        if(i != currentIndex){
            str += "<a href='#this' class='pad_5' onclick='_movePage("+i+")'>"+i+"</a>";
        }
        else{
            str += "<b><a href='#this' class='pad_5' onclick='_movePage("+i+")'>"+i+"</a></b>";
        }
    }
    $("#"+divId).append(preStr + str + postStr);
}
 
function _movePage(value){
    $("#"+gfv_pageIndex).val(value);
    if(typeof(gfv_eventName) == "function"){
        gfv_eventName(value);
    }
    else {
        eval(gfv_eventName + "(value);");
    }
}
 

 

페이징 태그를 작성하는 함수는 두개로 구성이 되어있다. 

첫번째는 gfn_renderPaging이라는 함수로 페이징 태그를 작성하는 역할을 한다.

두번째로는 내부적으로 사용할 함수로 _movePage라는 함수가 있다. 이는 페이징 태그를 클릭하였을 경우 해당 페이지로 이동하는 역할을 한다. 


이제 소스를 간단히 살펴보도록 하자.

먼저 gfn_renderPaging 함수부터 보자.

1~7번째줄에는 주석이 달려있는것을 볼 수 있다. 이는 이 함수를 사용할 때 필요한 파라미터들을 적어놓은 것이다. 

함수에서 파라미터는 params라는 값 하나만 받는데 어떻게 저런 파라미터들을 받는지는 JSP에서 이야기를 할것이다. 여기서는 그냥 저런 이름들이 있다는 것만 확인하면 된다. 

이 함수에서 코드가 복잡한 부분은 없다. 단지 페이지당 레코드 수나 인덱스 수를 계산하거나, 아니면 태그를 만드는 부분이 살짝 복잡하다. 

27~29번째 줄에는 각각 3개의 변수가 선언되어 있는것을 볼 수 있다. 

이는 각각 맨앞으로 이동 태그,  1~10 등과 같은 인덱스 태그, 맨 뒤로 이동 태그를 담당한다. 

전체의 인덱스가 10을 초과할 경우 preStr 변수에는 맨앞, 앞 태그를 작성하고, 전체의 인덱스가 10 이하일 경우, 맨앞으로 이동 태그만 만들것이다.

마찬가지로 맨뒤, 뒤 태그도 postStr에 작성된다. 이는 전체의 인덱스에 따라 유동적으로 결정될것이다. 

그 다음 str 변수에는 인덱스가 담길것이다. 


각 태그는 <a>태그를 사용해서 작성했으며, 각 태그가 클릭되었을 때 _movePage라는 함수를 호출하게 되어있다.

_movePage는 해당 태그가 클릭되었을 때, JSP에서 선언한 함수를 호출하게끔 구성되어있다. 


지금 이러한 태그를 가지고 아무리 자세하게 설명한다고 하더라도 이해하기는 쉽지 않을것이다. 일단은 대충 이런 역할을 한다는것만 살펴보고 넘어가도록 하자.

다음에 나올 내용인 JSP 부분에서 다시 한번 살펴볼 것이다.


2. 개발 소스

이제 위에서 작성한 공통기능을 사용하여 페이징 기능을 사용할 차례이다. 위에서 작성한것을 바탕으로 기존의 게시판을 변경하도록 하겠다. (지난글에서 작성한 전자정부 프레임워크를 사용한 페이징이 아닌 일반 게시판을 기준으로 진행한다.)


1. JSP

먼저 jsp를 변경하자.

boardList.jsp를 다음과 같이 수정한다. 

 





















































































































<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<%@ include file="/WEB-INF/include/include-header.jspf" %>
</head>
<body>
    <h2>게시판 목록</h2>
    <table class="board_list">
        <colgroup>
            <col width="10%"/>
            <col width="*"/>
            <col width="15%"/>
            <col width="20%"/>
        </colgroup>
        <thead>
            <tr>
                <th scope="col">글번호</th>
                <th scope="col">제목</th>
                <th scope="col">조회수</th>
                <th scope="col">작성일</th>
            </tr>
        </thead>
        <tbody>
             
        </tbody>
    </table>
     
    <div id="PAGE_NAVI"></div>
    <input type="hidden" id="PAGE_INDEX" name="PAGE_INDEX"/>
     
    <br/>
    <a href="#this" class="btn" id="write">글쓰기</a>
     
    <%@ include file="/WEB-INF/include/include-body.jspf" %>
    <script type="text/javascript">
        $(document).ready(function(){
            fn_selectBoardList(1);
             
            $("#write").on("click", function(e){ //글쓰기 버튼
                e.preventDefault();
                fn_openBoardWrite();
            });
             
            $("a[name='title']").on("click", function(e){ //제목
                e.preventDefault();
                fn_openBoardDetail($(this));
            });
        });
         
         
        function fn_openBoardWrite(){
            var comSubmit = new ComSubmit();
            comSubmit.setUrl("<c:url value='/sample/openBoardWrite.do' />");
            comSubmit.submit();
        }
         
        function fn_openBoardDetail(obj){
            var comSubmit = new ComSubmit();
            comSubmit.setUrl("<c:url value='/sample/openBoardDetail.do' />");
            comSubmit.addParam("IDX", obj.parent().find("#IDX").val());
            comSubmit.submit();
        }
         
        function fn_selectBoardList(pageNo){
            var comAjax = new ComAjax();
            comAjax.setUrl("<c:url value='/sample/selectBoardList.do' />");
            comAjax.setCallback("fn_selectBoardListCallback");
            comAjax.addParam("PAGE_INDEX",pageNo);
            comAjax.addParam("PAGE_ROW", 15);
            comAjax.ajax();
        }
         
        function fn_selectBoardListCallback(data){
            var total = data.TOTAL;
            var body = $("table>tbody");
            body.empty();
            if(total == 0){
                var str = "<tr>" +
                                "<td colspan='4'>조회된 결과가 없습니다.</td>" +
                            "</tr>";
                body.append(str);
            }
            else{
                var params = {
                    divId : "PAGE_NAVI",
                    pageIndex : "PAGE_INDEX",
                    totalCount : total,
                    eventName : "fn_selectBoardList"
                };
                gfn_renderPaging(params);
                 
                var str = "";
                $.each(data.list, function(key, value){
                    str += "<tr>" +
                                "<td>" + value.IDX + "</td>" +
                                "<td class='title'>" +
                                    "<a href='#this' name='title'>" + value.TITLE + "</a>" +
                                    "<input type='hidden' name='title' value=" + value.IDX + ">" +
                                "</td>" +
                                "<td>" + value.HIT_CNT + "</td>" +
                                "<td>" + value.CREA_DTM + "</td>" +
                            "</tr>";
                });
                body.append(str);
                 
                $("a[name='title']").on("click", function(e){ //제목
                    e.preventDefault();
                    fn_openBoardDetail($(this));
                });
            }
        }
    </script>
</body>
</html>

 

지난번보다 좀 길어진것을 볼 수 있다. 

먼저 살펴봐야 할것은 29~30번째 줄이다. 

29번째 줄의 <div id="PAGE_NAVI"></div> 부분은 앞으로 페이징 태그가 그려질 부분이다. 

30번째 줄은 현재 페이지 번호가 저장될것이다.


그 다음으로는 38번째 줄에서 fn_selectBoardList(1)을 호출하는것을 봐야한다.

이것은 최초에 화면이 호출되면 1페이지의 내용을 조회하는 것을 의미한다.

그럼 fn_selectBoardList는 어떻게 되어있는지 확인해보자.


65번째 줄의 fn_selectBoardList를 살펴보면 파라미터로 pageNo를 받는것을 알 수 있다. 여기서 pageNo는 호출하고자 하는 페이지 번호를 의미한다.

위에서 만든 ComAjax를 사용하는것을 볼 수 있다.

대부분은 ComSubmit과 비슷하지만 setCallback이라는 함수가 추가된것을 확인할 수 있다. setCallback은 Ajax 요청이 완료된 후 호출될 함수의 이름을 지정하는 함수이다. 

여기서는 콜백함수로 fn_selectBoardListCallback 이라는 이름의 함수를 지정했는데, 이것은 74번째줄의 fn_selectBoardListCallback(data) 함수를 의미한다. 

ComAjax함수에서는 두개의 파라미터를 전송하고 있다. PAGE_INDEX와 PAGE_ROW가 그것인데 각각 현재 페이지 번호와 한 페이지에 보여줄 행(데이터)의 수를 의미한다. 

여기까지만 이야기하고 먼저 실행된 결과화면을 살펴보도록 하자.


 

나중에 서버단까지 개발을 완료하고 실행을 시키면 다음과 같은 화면을 볼 수 있다. 

한 페이지에 15개의 데이터를 보여주며 맨앞, 맨뒤를 의미하는 [<<], [>>] 버튼과 앞 뒤를 의미하는 [<], [>] 버튼, 그리고 1~10의 페이지 번호가 있는것을 볼 수 있다.

여기서 만들어진 페이징 태그가 앞에서 만들었던 gfn_renderPaging 함수에 의해서 만들어지는 부분이다. JSP에서는 gfn_renderPaging 함수를 fn_selectBoardListCallback 함수에서 호출했는데, 이 부분은 잠시 후에 살펴볼것이다. 

여기서는 JSP에서 단지 <div id="PAGE_NAVI"></div> 태그만 작성해 놓고 공통함수를 이용해서 페이징 태그가 작성되는것을 확인하면 된다.


그다음으로 fn_selectBoardListCallback 함수를 보자. 

이 함수는 ajax 호출이 되고 난 후 실행되는 콜백함수로 여기서는 화면을 다시 그리는 역할을 수행한다. 

Ajax는 기본적으로 비동기식호출이기 때문에 서버에 요청을 하고 그 결과값을 받더라도 화면의 전환이 일어나지 않는다. 따라서 결과값을 받은 후, 데이터의 갱신 등을 따로 해줘야한다. 이것은 submit을 할때와 다른 점으로, 화면 갱신이 일어나지 않기 때문에 JSTL등을 이용하여 목록 등을 만들수가 없다. 


그 과정을 이제 하나씩 살펴보도록 하겠다. 

여기서는 바로 전글(http://addio3305.tistory.com/89)에서 테이블의 <tbody> 부분과 비교를 하면서 보는것이 좋다.

가장 먼저 fn_selectBoardListCallback의 파라미터인 data는 서버에서 전송된 json 형식의 결과값이다. 이 값을 어떻게 보내주는지는 잠시 후에 보도록 하겠다. 

만약 조회된 결과가 0일 경우 (data.TATAL == 0) 조회된 결과가 없기 때문에 화면에는 조회된 결과가 없다고 표시한다. 

이것은 바로 이전글에서 <c:otherwise> 태그에 해당한다. 


반대로 데이터가 존재할 경우 84번째 줄부터가 실행된다. 

85~90번째줄은 앞에서 만든 gfn_renderPaging 함수를 수행하기 위해서 파라미터를 만드는 과정이다. Javascript에서 "var 변수명 = {} " 이렇게 선언을 하면 Object가 만들어지고, 거기에 각각 key와 value 형식으로 값을 추가할 수 있다.

divId, pageIndex, totalCount, eventName은 key가 되고, "PAGE_NAVI", "PAGE_INDEX", totla, "fn_selectBoardList"는 value가 된다. 

그 후 gfn_renderPaging 함수를 호출하면 object의 값을 이용하여 페이징 태그를 만들게 된다.


94번째 줄 부터는 이전글의 <c:forEach> 태그를 이용하여 테이블의 목록을 만든것과 같은 역할을 수행한다. 94번째 줄의 data.list 가 서버에서 보내준 데이터이고, 이를 이용해서 jQuery의 .each 함수를 사용하여 HTML 태그를 만들어주는것을 볼 수 있다.


그리고 마지막으로 새롭게 추가된 각각의 목록의 제목에 상세보기로 이동할 수 있도록 click 이벤트를 바인딩 해주게 된다. 


2. JAVA

1) Contoller

SampleContoller를 열어서 다음을 작성하자. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RequestMapping(value="/sample/openBoardList.do")
public ModelAndView openBoardList(CommandMap commandMap) throws Exception{
    ModelAndView mv = new ModelAndView("/sample/boardList");
     
    return mv;
}
 
@RequestMapping(value="/sample/selectBoardList.do")
public ModelAndView selectBoardList(CommandMap commandMap) throws Exception{
    ModelAndView mv = new ModelAndView("jsonView");
     
    List<Map<String,Object>> list = sampleService.selectBoardList(commandMap.getMap());
    mv.addObject("list", list);
    if(list.size() > 0){
        mv.addObject("TOTAL", list.get(0).get("TOTAL_COUNT"));
    }
    else{
        mv.addObject("TOTAL"0);
    }
     
    return mv;
}

 

지난글에서 작성한 것과는 약간 다른것을 확인할 수 있다.

지난글에서는 openBoardList에서 sampleService.selectBoardList를 호출했었는데(http://addio3305.tistory.com/89 의 2.Java > 1) Controller 참조), 이번에는 openBoardList는 단순히 boardList.jsp를 호출하는 역할만 수행한다.


그 다음으로 selectBoardList.do가 새로 생겼다. 

먼저 10번째 줄을 살펴보면 여태까지와는 약간 다른점을 확인할 수 있다. 

기존에는 ModelAndView에서 호출할 JSP 파일명이나 redirect를 수행했는데, 이번에는 jsonView라는 값이 들어가있는 것을 볼 수 있다. 

이는 앞에서 action-servlet.xml에 <bean id="jsonView" class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" /> 를 선언했었던것을 기억해야 한다. 여기서 bean id가 jsonView였는데, 여기서 선언된 bean을 사용하는 것이다. 

이 jsonView는 데이터를 json 형식으로 변환해주는 역할을 수행한다. 

주소창에서 http://localhost:8080/first/sample/selectBoardList.do를 한번 호출해보면 다음과 같은 화면을 볼 수 있다.


생소한 화면인데, 여기서 볼 수 있는 데이터가 바로 json 형식의 데이터다. 여기서는 데이터의 크기를 줄이기 위해서 Indent(띄어쓰기) 등의 처리가 안되어있어서 데이터의 형식을 보기가 힘들다. 이러한 JSON 형식의 데이터가 올바른지 여부를 검사하고 포맷팅도 해주는 사이트들이 있다. 

http://jsonlint.com/ 역시 그러한 사이트 중에 하나다. 여기에 들어가서 앞에서 조회한 데이터를 모두 복사해서 Validate를 눌러보면 다음과 같은 화면을 볼 수 있다.

 

아까의 데이터가 위에서 보는것과 같이 정렬되어서 나온다. 지금은 JSON에 대해서 이야기를 하는것이 아니기 때문에 간단히 이런게 있다는것만 살펴보고 넘어가도록 하겠다. 이에 대한 자세한 설명은 인터넷에서 많이 찾을수 있다.


다시 소스로 돌아와서 selectBoardList.do는 sampleService.selectBoardList를 호출하여 목록 정보를 조회하고 그 값을 화면에 전달하는 역할을 한다. 

여기서 mv.addObject에("list", list)와 mv.addObject("TOTAL", 어떤 값) 두가지의 값을 화면에 보내주는것을 확인하자. 

앞에서 ajax callback 함수에서 data.TOTAL과 data.list가 있었던 것을 다시 한번 찾아보자. Controller에서 json 형식의 데이터를 화면에 전달하는데 그 값은 data라는 이름으로 화면에 전달된다. (꼭 data일 필요는 없다. 그렇지만 필자는 ComAjax에서 callback을 수행할 때, data라는 이름으로 보내주도록 해놨다. ComAjax를 보면 확인할 수 있다.) 그리고 mv에는 각각 list와 TOTAL이라는 key로 값을 보내줬고 이는 다시 화면에서 각각 data.list, data.TOTAL이라는 형식으로 값에 접근할 수 있다.


그 이후 service, serviceImpl, DAO, sql은 지난번과 거의 같다. 


2) SampleService

SampleService.java에 다음을 작성한다.

 

1
List<Map<String, Object>> selectBoardList(Map<String, Object> map) throws Exception;

 

3) SampeServiceImpl

SampleServiceImpl.java에 다음을 작성한다.

 

1
2
3
4
@Override
public List<Map<String, Object>> selectBoardList(Map<String, Object> map) throws Exception {
    return sampleDAO.selectBoardList(map);
}

 

4) SampleDAO

SampleDAO.java에 다음을 작성한다.

 

1
2
3
4
@SuppressWarnings("unchecked")
public List<Map<String, Object>> selectBoardList(Map<String, Object> map) throws Exception{
    return (List<Map<String, Object>>)selectPagingList("sample.selectBoardList", map);
}

 

3. SQL

SQL은 지난글과 비교해서 바뀐게 없다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id="selectBoardList" parameterType="hashmap" resultType="hashmap">
    <include refid="common.pagingPre"/>
    <![CDATA[
        SELECT
            ROW_NUMBER() OVER (ORDER BY IDX DESC) RNUM,
            IDX,
            TITLE,
            HIT_CNT,
            TO_CHAR(CREA_DTM, 'YYYY.MM.DD'AS CREA_DTM
        FROM
            TB_BOARD
        WHERE
            DEL_GB = 'N'
    ]]>
    <include refid="common.pagingPost"/>
</select>

 

여기까지 하고 실행을 시키면 앞에서 봤던 화면을 볼 수 있다.

실행화면과 로그를 다시 한번 살펴보자.


 


페이징 태그의 버튼을 이것저것 누르면서 화면이 제대로 이동을 하는지, 쿼리 역시 제대로 동작하는지 살펴보면 된다.



반응형
반응형

1. JSP

먼저 boardWrite.jsp를 다음과 같이 수정하자.


 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<%@ include file="/WEB-INF/include/include-header.jspf" %>
</head>
<body>
    <form id="frm" name="frm" enctype="multipart/form-data">
        <table class="board_view">
            <colgroup>
                <col width="15%">
                <col width="*"/>
            </colgroup>
            <caption>게시글 작성</caption>
            <tbody>
                <tr>
                    <th scope="row">제목</th>
                    <td><input type="text" id="TITLE" name="TITLE" class="wdp_90"></input></td>
                </tr>
                <tr>
                    <td colspan="2" class="view_text">
                        <textarea rows="20" cols="100" title="내용" id="CONTENTS" name="CONTENTS"></textarea>
                    </td>
                </tr>
            </tbody>
        </table>
        <div id="fileDiv">
            <p>
                <input type="file" id="file" name="file_0">
                <a href="#this" class="btn" id="delete" name="delete">삭제</a>
            </p>
        </div>
         
        <br/><br/>
        <a href="#this" class="btn" id="addFile">파일 추가</a>
        <a href="#this" class="btn" id="write">작성하기</a>
        <a href="#this" class="btn" id="list">목록으로</a>
    </form>
     
    <%@ include file="/WEB-INF/include/include-body.jspf" %>
    <script type="text/javascript">
        var gfv_count = 1;
     
        $(document).ready(function(){
            $("#list").on("click", function(e){ //목록으로 버튼
                e.preventDefault();
                fn_openBoardList();
            });
             
            $("#write").on("click", function(e){ //작성하기 버튼
                e.preventDefault();
                fn_insertBoard();
            });
             
            $("#addFile").on("click", function(e){ //파일 추가 버튼
                e.preventDefault();
                fn_addFile();
            });
             
            $("a[name='delete']").on("click", function(e){ //삭제 버튼
                e.preventDefault();
                fn_deleteFile($(this));
            });
        });
         
        function fn_openBoardList(){
            var comSubmit = new ComSubmit();
            comSubmit.setUrl("<c:url value='/sample/openBoardList.do' />");
            comSubmit.submit();
        }
         
        function fn_insertBoard(){
            var comSubmit = new ComSubmit("frm");
            comSubmit.setUrl("<c:url value='/sample/insertBoard.do' />");
            comSubmit.submit();
        }
         
        function fn_addFile(){
            var str = "<p><input type='file' name='file_"+(gfv_count++)+"'><a href='#this' class='btn' name='delete'>삭제</a></p>";
            $("#fileDiv").append(str);
            $("a[name='delete']").on("click", function(e){ //삭제 버튼
                e.preventDefault();
                fn_deleteFile($(this));
            });
        }
         
        function fn_deleteFile(obj){
            obj.parent().remove();
        }
    </script>
</body>
</html>



파일 추가 버튼과 삭제버튼이 추가 되었다. 

먼저 파일 추가버튼을 누르면 fileDiv라는 파일 영역의 마지막에 새로운 파일 태그 및 삭제버튼을 추가하도록 하였다. 

그 후 추가된 삭제버튼에도 삭제 기능을 위한 클릭이벤트를 바인딩 하였다. 


여기서 살펴볼 것은 <input type='file'> 태그의 name이 동일할 경우, 서버에는 단 하나의 파일만 전송되는 문제가 발생한다. 따라서 gfv_count 라는 전역변수를 선언하고, 태그가 추가될때마다 그 값을 1씩 증가시켜서 name값이 계속 바뀌도록 하였다. 


사실 여기서는 핵심적인 부분만 설명하기 위해서 간략히 작성을 하였는데, 실제로는 좀 더 복잡한 처리가 여러가지 필요하다. 

예를 들어, 파일의 크기나 유효성 검사도 하지 않았으며, 추가할 수 있는 파일의 개수도 제한하지 않았다. 

또한 파일의 전송에 따라서 파일의 순서가 바뀔수도 있기 때문에, 첨부파일의 순서를 지정하는 작업도 필요하다. 

그렇지만 그런부분은 사람마다 구현하는 방식이 다르고, 또 여기서 그런것들을 모두 설명하기에는 양이 적지않아서 기본적인 내용만 보고 넘어가도록 하겠다.


그 다음으로 삭제버튼이다.

삭제버튼을 누르면 해당 버튼이 위치한 <p>태그 자체를 삭제하도록 구성하였다. 


그럼 실행을 시켜보자. 서버 및 쿼리는 지난글에서 다중 파일 업로드를 고려해서 작성을 했었기때문에, 다시 수정할 건 없다. 

최초로 실행시키면 다음과 같은 화면을 볼 수 있다.


여기서 파일 추가를 클릭하면 파일을 입력할 수 있는 새로운 태그가 생성된다.


여러개의 파일을 추가하고 삭제해보고 게시글을 등록하고 확인해보자.


3개의 파일 태그가 존재하고 1.PNG, 2.PNG라는 2개의 파일을 첨부하였다. 

이 상태로 작성하기를 누르면 게시글이 저장되고 목록으로 이동이 될 것이다. 바로 상세화면에서 파일이 제대로 올라갔는지 확인을 해 보자. 


위와 같이 두개의 파일이 정상적으로 등록이 된 것을 확인할 수 있다. 

(기존의 boardDetail.jsp 에서 첨부파일의 목록을 보여주던 <c:forEach>문 사이에 <p>태그를 추가하였다.)


다음으로는 이클립스의 로그를 확인해보자.

이클립스의 로그가 좀 잘렸지만, 필요한 내용은 다 확인할 수 있다. 

먼저 게시글을 등록한 다음, 첨부파일의 정보를 순차적으로 저장한 것을 확인할 수 있다. 


2. 첨부파일 수정

이번글의 가장 핵심적인 부분이다. 다음에 설명하는 부분에 대해서 한번 고민을 하는 시간을 가져보자.

지난 두개의 글에서 첨부파일의 등록 및 다운로드에 대해서 이야기를 했었고, 이번에는 첨부파일의 수정에 대해서 이야기를 할 차례이다. 


프로그램을 작성하기에 앞서 첨부파일의 수정을 어떻게 처리할 것인지 고민을 해야한다. 

게시글을 수정할 때 해당되는 첨부파일의 수정은 등록과 다르게 좀 복잡한 프로세스를 가진다. 

다음의 경우를 생각해보자.

1) 게시글의 내용만 수정을 하고, 첨부파일은 수정하지 않는다.

2) 첨부파일을 수정할 때 기존에 등록한 파일을 변경한다. 

3) 기존에 등록한 파일은 놔두고, 새로운 파일을 추가한다. 

4) 기존에 등록한 파일을 모두 삭제하고, 새로운 파일을 등록한다.

5) 기존에 등록한 파일의 일부를 삭제하고, 새로운 파일을 등록한다.


간단히 생각나는 경우의 수만 생각해도 이렇게 5개가 나왔다. 

첨부파일의 수정은 여러가지 경우의 수가 있기 때문에, 그 처리에 있어서 고민을 해야한다.


여기서는 필자가 사용하는 수정 프로세스를 먼저 이야기 하도록 하겠다. 

필자는 게시글 및 해당 첨부파일을 수정할 때, 먼저 해당 게시글의 첨부파일 목록을 모두 삭제처리한다. 

여기서 삭제처리의 의미는 실제 파일을 삭제하는것이 아니라, DEL_GB의 값을 모두 'Y"로 변경하는 것을 의미한다. 


그 다음으로 FileUtils클래스에서 파일정보를 list로 변경할 때, 기존에 첨부가 되어있던 파일의 정보와 신규로 입력된 파일 정보를 구분한다.

그 후, 기존에 첨부가 되어있던 파일은 DEL_GB값을 다시 N으로 변경(update)하고, 신규 추가된 파일 정보는 입력(insert)한다.


이렇게 하면 DB에는 기존에 등록했던 파일 정보까지 모두 남아있지만, 삭제된 파일과 현재 사용중인 파일을 DEL_GB값을 이용해서 구분할 수 있다.

단, 실제파일을 삭제하는것은 아니기 때문에 서버에는 모든 파일이 남아있게 된다. 


이 방식은 각각 장,단점이 존재한다. 

만약 실제 파일을 삭제하는것은 HDD를 사용하기 때문에, 서버의 부담이 커지고 당연히 속도도 저하될 수 밖에 없다. 그렇지만 서버에는 꼭 필요한 파일만 남아있기 때문에 서버의 용량관리에는 좀 더 유리하다.

반대로 필자의 경우, 파일을 삭제하지 않기 때문에 서버속도는 좀 더 빠르지만, 삭제처리된 파일을 그대로 가지고 있기 때문에 용량은 좀 더 많이 차지할수밖에 없다. 

따라서, 개발하는 시스템의 성격 및 개발 방식, 하드웨어의 상황에 따라서 유연하게 구성해야함을 잊지말자.

아직까지는 이게 무슨 이야기인지 바로 와닿지 않을것이라고 생각된다. 


이제, 위에서 이야기한 내용을 실제로 구현을 해 보자.


1. Controller

지난글에서 sampleService.selectBoardDetail 의 리턴값을 변경하였었다. 

기존에는 selectBoardDetail을 호출하면 게시글의 내용만 반환하였는데(http://addio3305.tistory.com/79 3. 상세페이지 > 2.Java > SampleServiceImpl 참조), 첨부파일을 추가하면서 게시글의 내용과 첨부파일 목록 2가지를 반환하도록 변경하였다. (http://addio3305.tistory.com/84 의 1.첨부파일 보여주기 > 2.java > 2) SampleServiceImpl 참조)



 

@RequestMapping(value="/sample/openBoardUpdate.do")
public ModelAndView openBoardUpdate(CommandMap commandMap) throws Exception{
    ModelAndView mv = new ModelAndView("/sample/boardUpdate");
     
    Map<String,Object> map = sampleService.selectBoardDetail(commandMap.getMap());
    mv.addObject("map", map.get("map"));
    mv.addObject("list", map.get("list"));
     
    return mv;
}



2. JSP

boardUpdate.jsp를 다음과 같이 수정하자.


 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<%@ include file="/WEB-INF/include/include-header.jspf" %>
</head>
<body>
    <form id="frm" name="frm" enctype="multipart/form-data">
        <table class="board_view">
            <colgroup>
                <col width="15%"/>
                <col width="35%"/>
                <col width="15%"/>
                <col width="35%"/>
            </colgroup>
            <caption>게시글 상세</caption>
            <tbody>
                <tr>
                    <th scope="row">글 번호</th>
                    <td>
                        ${map.IDX }
                        <input type="hidden" id="IDX" name="IDX" value="${map.IDX }">
                    </td>
                    <th scope="row">조회수</th>
                    <td>${map.HIT_CNT }</td>
                </tr>
                <tr>
                    <th scope="row">작성자</th>
                    <td>${map.CREA_ID }</td>
                    <th scope="row">작성시간</th>
                    <td>${map.CREA_DTM }</td>
                </tr>
                <tr>
                    <th scope="row">제목</th>
                    <td colspan="3">
                        <input type="text" id="TITLE" name="TITLE" class="wdp_90" value="${map.TITLE }"/>
                    </td>
                </tr>
                <tr>
                    <td colspan="4" class="view_text">
                        <textarea rows="20" cols="100" title="내용" id="CONTENTS" name="CONTENTS">${map.CONTENTS }</textarea>
                    </td>
                </tr>
                <tr>
                    <th scope="row">첨부파일</th>
                    <td colspan="3">
                        <div id="fileDiv">               
                            <c:forEach var="row" items="${list }" varStatus="var">
                                <p>
                                    <input type="hidden" id="IDX" name="IDX_${var.index }" value="${row.IDX }">
                                    <a href="#this" id="name_${var.index }" name="name_${var.index }">${row.ORIGINAL_FILE_NAME }</a>
                                    <input type="file" id="file_${var.index }" name="file_${var.index }">
                                    (${row.FILE_SIZE }kb)
                                    <a href="#this" class="btn" id="delete_${var.index }" name="delete_${var.index }">삭제</a>
                                </p>
                            </c:forEach>
                        </div>
                    </td>
                </tr>
            </tbody>
        </table>
    </form>
     
    <a href="#this" class="btn" id="addFile">파일 추가</a>
    <a href="#this" class="btn" id="list">목록으로</a>
    <a href="#this" class="btn" id="update">저장하기</a>
    <a href="#this" class="btn" id="delete">삭제하기</a>
     
    <%@ include file="/WEB-INF/include/include-body.jspf" %>
    <script type="text/javascript">
        var gfv_count = '${fn:length(list)+1}';
        $(document).ready(function(){
            $("#list").on("click", function(e){ //목록으로 버튼
                e.preventDefault();
                fn_openBoardList();
            });
             
            $("#update").on("click", function(e){ //저장하기 버튼
                e.preventDefault();
                fn_updateBoard();
            });
             
            $("#delete").on("click", function(e){ //삭제하기 버튼
                e.preventDefault();
                fn_deleteBoard();
            });
             
            $("#addFile").on("click", function(e){ //파일 추가 버튼
                e.preventDefault();
                fn_addFile();
            });
             
            $("a[name^='delete']").on("click", function(e){ //삭제 버튼
                e.preventDefault();
                fn_deleteFile($(this));
            });
        });
         
        function fn_openBoardList(){
            var comSubmit = new ComSubmit();
            comSubmit.setUrl("<c:url value='/sample/openBoardList.do' />");
            comSubmit.submit();
        }
         
        function fn_updateBoard(){
            var comSubmit = new ComSubmit("frm");
            comSubmit.setUrl("<c:url value='/sample/updateBoard.do' />");
            comSubmit.submit();
        }
         
        function fn_deleteBoard(){
            var comSubmit = new ComSubmit();
            comSubmit.setUrl("<c:url value='/sample/deleteBoard.do' />");
            comSubmit.addParam("IDX", $("#IDX").val());
            comSubmit.submit();
             
        }
         
        function fn_addFile(){
            var str = "<p>" +
                    "<input type='file' id='file_"+(gfv_count)+"' name='file_"+(gfv_count)+"'>"+
                    "<a href='#this' class='btn' id='delete_"+(gfv_count)+"' name='delete_"+(gfv_count)+"'>삭제</a>" +
                "</p>";
            $("#fileDiv").append(str);
            $("#delete_"+(gfv_count++)).on("click", function(e){ //삭제 버튼
                e.preventDefault();
                fn_deleteFile($(this));
            });
        }
         
        function fn_deleteFile(obj){
            obj.parent().remove();
        }
    </script>
</body>
</html>


먼저 실행된 화면을 확인한 후에, 하나씩 확인을 하자.


게시글 상세에서 수정하기 버튼을 눌러서 게시글 수정 화면을 보면 다음과 같다.


기존에 저장이 된 파일명과 해당 파일을 수정할 수 있는 파일 선택 버튼, 그리고 삭제버튼으로 구성하였다. 

파일추가 버튼은 boardWrite.jsp에서 만든 기능과 동일하게 첨부파일을 하나 추가하도록 구성하였다. 


여기서 하나 자세히 봐야하는것은 첨부파일 목록부분에서 만든 <input type="hidden" id="IDX" name="IDX_${var.index}"> 태그이다. 이 태그의 name 속성이 IDX_숫자 로 구성이 되어있는것을 기억해야 한다. 

위에서 파일수정 프로세스를 이야기할 때 "FileUtils클래스에서 파일정보를 list로 변경할 때, 기존에 첨부가 되어있던 파일의 정보와 신규로 입력된 파일 정보를 구분한다." 라는 이야기를 했었다. 

기존에 저장된 파일에서는 해당파일번호인 IDX 값이 존재하는데, 이를 이용해서 신규파일정보와 아닌것을 구분하려고 한다.

그 외에는 앞에서 이야기한 내용과 다른점이 없기 때문에 넘어가도록 하겠다.


3. Java

1) Controller

SampleController.java의 updateBoard.do를 다음과 같이 변경한다.


 @RequestMapping(value="/sample/updateBoard.do")

public ModelAndView updateBoard(CommandMap commandMap, HttpServletRequest request) throws Exception{
    ModelAndView mv = new ModelAndView("redirect:/sample/openBoardDetail.do");
     
    sampleService.updateBoard(commandMap.getMap(), request);
     
    mv.addObject("IDX", commandMap.get("IDX"));
    return mv;
}


기존의 updateBoard.do에서 첨부파일 정보를 포함한 HttpServletRequest를 추가하였다.


2) Service 및 ServiceImpl

먼저, SampleService.java의 updateBoard()를 다음과 같이 변경한다.

 

1
void updateBoard(Map<String, Object> map, HttpServletRequest request) throws Exception;

 

다음으로 중요한것은 ServiceImpl 영역이다. 

먼저 SampleServiceImpl.java의 updateBoard를 다음과 같이 변경하자.

 @Override

public void updateBoard(Map<String, Object> map, HttpServletRequest request) throws Exception{
    sampleDAO.updateBoard(map);
     
    sampleDAO.deleteFileList(map);
    List<Map<String,Object>> list = fileUtils.parseUpdateFileInfo(map, request);
    Map<String,Object> tempMap = null;
    for(int i=0, size=list.size(); i<size; i++){
        tempMap = list.get(i);
        if(tempMap.get("IS_NEW").equals("Y")){
            sampleDAO.insertFile(tempMap);
        }
        else{
            sampleDAO.updateFile(tempMap);
        }
    }
}


기존의 updateBoard에서는 sampleDAO.updateBoard(map) 한줄만 있었는데, 추가된 내용이 조금 있다. 

일단 이렇게 작성을 하게되면 

5번째 줄 dampleDAO.deleteFileList(map);

6번째 줄 fileUtils.parseUpdateFileInfo(map, request);

14번째 줄 sampleDAO.updateFile(tempMap); 

에서 에러가 발생할 것이다. 


일단 에러는 무시하고, 간단히 설명을 하도록 하겠다. 여기서는 위에서 첨부파일의 수정 프로세스를 염두해 두고서 생각하자.


먼저, 게시글의 내용을 수정하는 sampleDAO.updateBoard는 기존과 같다. 

그 다음으로 sampleDAO.deleteFileList(map)을 호출하는데, 이는 해당 게시글에 해당하는 첨부파일을 전부 삭제처리(DEL_GB = 'Y')를 하는 역할을 한다.

이렇게해서 해당 게시글의 첨부파일은 전부 삭제가 된 상황이다.

그 다음으로, fileUtils의 parseUpdateFileInfo 메서드를 이용해서 request에 담겨있는 파일 정보를 list로 변환한다. 이때, 기존에 저장된 파일 중에서 삭제되지 않은 파일정보도 함께 반환할 것이다. 

그 다음으로는 파일을 하나씩 입력(insert) 또는 수정(update)을 할 차례인데, 이는 list에 담긴 파일정보중에서 IS_NEW라는 값을 이용해서 판별할 계획이다.

IS_NEW라는 값이 "Y"인 경우는 신규 저장될 파일이라는 의미이고, "Y"가 아니면 기존에 저장되어 있던 파일이라는 의미이다.

그래서 신규저장은 sampleDAO.insertFile을 이용하여 파일정보를 저장하고, 기존에 저장된 파일정보는 다시 삭제처리를 바꿔주기만 할 계획이다.


그럼 이러한 기능을 수행할 FileUtils 클래스의 parseUpdateFileInfo 메서드를 살펴보자.


 

public List<Map<String, Object>> parseUpdateFileInfo(Map<String, Object> map, HttpServletRequest request) throws Exception{
    MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest)request;
    Iterator<String> iterator = multipartHttpServletRequest.getFileNames();
     
    MultipartFile multipartFile = null;
    String originalFileName = null;
    String originalFileExtension = null;
    String storedFileName = null;
     
    List<Map<String,Object>> list = new ArrayList<Map<String,Object>>();
    Map<String, Object> listMap = null;
     
    String boardIdx = (String)map.get("IDX");
    String requestName = null;
    String idx = null;
     
     
    while(iterator.hasNext()){
        multipartFile = multipartHttpServletRequest.getFile(iterator.next());
        if(multipartFile.isEmpty() == false){
            originalFileName = multipartFile.getOriginalFilename();
            originalFileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
            storedFileName = CommonUtils.getRandomString() + originalFileExtension;
             
            multipartFile.transferTo(new File(filePath + storedFileName));
             
            listMap = new HashMap<String,Object>();
            listMap.put("IS_NEW""Y");
            listMap.put("BOARD_IDX", boardIdx);
            listMap.put("ORIGINAL_FILE_NAME", originalFileName);
            listMap.put("STORED_FILE_NAME", storedFileName);
            listMap.put("FILE_SIZE", multipartFile.getSize());
            list.add(listMap);
        }
        else{
            requestName = multipartFile.getName();
            idx = "IDX_"+requestName.substring(requestName.indexOf("_")+1);
            if(map.containsKey(idx) == true && map.get(idx) != null){
                listMap = new HashMap<String,Object>();
                listMap.put("IS_NEW""N");
                listMap.put("FILE_IDX", map.get(idx));
                list.add(listMap);
            }
        }
    }
    return list;
}


기존에 작성한 parseInsertFileInfo 메서드를 기반으로 약간 변경하였다.

먼저 multipartFile이 비어있지 않은 경우, 즉 첨부파일이 있는 경우는 기존과 동일하다. 

첨부파일이 있다는 것은 해당 파일이 변경됨을 뜻하고 이는 새롭게 저장을 해야한다. 따라서 기존에 parseInsertFileInfo에서 한것과 동일하게 파일을 저장하고 그걸 정보를 list에 추가하였다.

여기서 다른점은 SampleServiceImpl 에서 사용할 "IS_NEW"라는 키로 "Y"라는 값을 저장하였다. 


그 다음으로 봐야할게 else 문, 즉 multipartFile이 비어있는 경우(multipartFile.isEmpty() == true)이다.

단순히 게시글을 작성할 경우에는 이는 무시하면 되는 부분이었다. 그렇지만 수정에서는 첨부파일이 없더라도 해당 파일은 저장이 될 수도 있고 아닐수도 있다는 것을 무시하면 안된다.

게시글에서 파일을 수정하지 않을 경우, 해당 multipartFile은 비어있다. 그렇지만 그대로 놔두면 이미 파일은 지워져있기 때문에, 최종적으로는 파일이 없다고 나오게 된다. 

따라서, 파일정보가 없는 경우에는 해당 파일정보가 기존에 저장이 되어있던 내용인지 아니면 단순히 빈 파일인지 구분해야한다. 

그것을 구분하는게 37, 38번째 줄이다. 

먼저 36번째 줄에서는 requestName = multipartFile.getName()이라고 되어있는데, 이는 html 태그에서 file 태그의 name 값을 가져오게 된다. 

아까 jsp에서 파일 태그를 <input type="file" id="file_숫자" name="file_숫자"> 로 만들었던 것을 다시 한번 확인하자. 

이 태그의 name값인 "file_숫자" 값을 가져오는 것이 multipartFile.getName() 메서드이다. 

이 name에서 뒤에 있는 숫자를 가져오게 되면, map에서 IDX_숫자 값이 있는지 여부를 판별할 수 있다.


기존에 저장이 되어있던 파일의 경우, <input type="hidden" id="IDX" name="IDX_숫자" value="파일번호"> 태그를 생성했던것을 기억해보자.

그리고 신규로 생성이 된 파일의 경우, 위의 태그를 만들어주지 않았었다. 

따라서, 위 태그의 값이 있을 경우가 기존에 저장된 파일임을 알 수 있다.

그래서 36번째 줄은 "IDX_" 라는 키 값에 해당 태그의 네임에서 숫자를 가져와서 합쳐준다. 그렇게 되면 IDX_1, IDX_2 등의 값을 가지게 되는 것이다.

그 다음 37번째 줄에서는 이제 화면에서 넘어온 값 중에서 IDX_숫자 키가 있는지를 확인하는 것이다.

그래서 IDX_숫자 키가 있다면 그것은 기존에 저장이 되어있던 파일 정보임을 의미하는 "N" 이라는 값을 "IS_NEW"키로 저장하게 된다.


4. DAO

이제 SampleDAO에 미완성된 기능을 추가하면 된다.

 public void deleteFileList(Map<String, Object> map) throws Exception{

    update("sample.deleteFileList", map);
}
 
public void updateFile(Map<String, Object> map) throws Exception{
    update("sample.updateFile", map);
}


5. SQL

이제 마지막으로 쿼리를 추가하면 된다.

다음 두 개의 쿼리를 추가하자.

 

<update id="deleteFileList" parameterType="hashmap">
    <![CDATA[
        UPDATE TB_FILE SET
            DEL_GB = 'Y'
        WHERE
            BOARD_IDX = #{IDX} 
    ]]>
</update>
 
<update id="updateFile" parameterType="hashmap">
    <![CDATA[
        UPDATE TB_FILE SET
            DEL_GB = 'N'
        WHERE
            IDX = #{FILE_IDX}  
    ]]>
</update>




6. 실행 및 결과 확인

이제 서버를 실행시키고 결과를 확인해 볼 차례이다. 

몇가지 상황을 가정하고 정확히 동작하는지 확인해보자. 


먼저 새로운 파일을 추가하는 경우이다. 

이미지에서 볼수 있듯이 기존의 게시글에서 3.PNG라는 파일을 새롭게 추가하였다. 이제 저장하기를 눌러서 결과를 확인해보자.



위에서 확인할 수 있듯이 3개의 파일이 정상적으로 저장된 것을 확인할 수 있다. 그럼 다음으로 이클립스의 로그를 살펴보자.



게시글을 수정하기 위해서 여러개의 쿼리가 실행된 것을 확인할 수 있다.

먼저 sample.updateBoard 쿼리가 수행되면서 게시글 정보가 변경된 것을 확인할 수 있다.

그 다음으로 sample.deleteFileList 쿼리를 이용하여 해당 게시글의 모든 파일을 삭제처리 하였다. 

그 다음으로 2번의 sample.updateFile 쿼리와 1번의 sample.insertFile 쿼리가 실행된 것을 확인할 수 있다. 

기존에 저장되어 있던 1.PNG, 2.PNG 파일의 IDX는 각각 6,7번 이었다. (필자의 환경에서만 그렇다.) 

따라서 두 개의 파일은 삭제여부만 다시 바꿔주면 되는 정보들이었고, DEL_GB = 'N' 으로 바뀌는것을 볼 수 있다.

그 후, 새롭게 추가한 3.PNG는 기존에 없었던 파일이었기 때문에 신규로 정보가 저장된 것을 확인할 수 있다. 

이렇게 해서 3개의 파일이 정상적으로 저장이 되었다. 


그럼 다른 상황을 살펴보자.


이번에는 2.PNG 파일을 4.PNG 파일로 변경하고, 빈 파일 태그를 하나 추가하였다. 

이 상태로 저장하기를 누른 후 결과를 확인해보자.

정상적으로 1.PNG, 3.PNG, 4.PNG 파일이 저장된 것을 볼 수 있다.

쿼리 로그는 생략하도록 하겠다.


마지막으로 파일의 변경 및 삭제가 동시에 일어나는 상황을 살펴보자.


이번에는 1.PNG 파일을 삭제하고, 3.PNG 파일을 2.PNG 파일로 변경, 그리고 빈태그 하나와 5.PNG 파일을 첨부하였다. 

이제 저장을 해보자.

결과는 정상적으로 2.PNG, 4.PNG, 5.PNG 3개가 저장된 것을 볼 수 있다. 

여기까지 하고 이클립스의 로그를 확인하는 것은 생략하고 이번에는 DB를 잠시 살펴보자.



TB_FILE 테이블에서 BOARD_IDX가 8인 정보만 조회를 하였다. (위에서 실험을 했던 게시글 번호가 8 이다.)

여기서 보면 그동안 수정을 했던 파일의 내역이 모두 남아있는 것을 확인할 수 있다. 그리고 현재 사용중인 파일은 DEL_GB의 값이 N인 파일만 사용중임을 알 수 있다. 


이렇게 하나의 테이블에서 파일의 변경내역을 관리를 할 수도 있지만, 로그파일 또는 로그 DB등을 이용하여 별도로 관리할 수도 있다.

그것은 해당 프로젝트마다 다르기 때문에, 프로젝트의 성격에 맞춰서 개발을 하면 된다.


------------------------------------------------------------------------------------

 





반응형
반응형

오늘은 Spring Maven 연동 및 설치에 대해서 알아보겠습니다.

1. 프레임워크란?

자바로 개발을 하기 위해 따르는 일련을 개발 구조라고 볼수 있습니다.

개발을 하면서 프로그램의 구조는 개발자의 성향에 따라 구조나 흐름이 달라질 수 있는데요.

프레임워크를 사용하게되면 프레임워크가 제공하는 가이드 라인과 라이브러리를 사용 하게 됩니다.

그래서 프레임워크를 사용하여 개발하는 프로그램은 생산성과 일관성을 얻을 수 있는 장점이 있습니다.

장점 : 구조적, 안정적, 유지보수 용이, 생산성 향상

단점 : 무분별한 프레임워크의 사용으로 인한 개발자 능력 저하

2. Spring 프레임 워크

1) 동적 웹 어플리케이션을 개발하기 위한 기능을 모아놓은 경량화 프레임워크로 우리나라에서 전자정부프레임워크로 불리고 있다.

<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-5595302733721535" data-ad-slot="5728147600" data-ad-format="auto"></ins> <script>(adsbygoogle = window.adsbygoogle || []).push({});</script>

2) 특징.

– IOC : 프로그램내에서 사용되는 객체를 사용자가 직접관리 하지 않고 컨테이너가 객체를 생성하고 관리한다 -> ApplicationContext.xml

– DI : 객체간의 의존관계를 외부에서 정의하고 주입한다. 불필요한 의존관계를 정리 할 수 있음

– AOP : 비지니스로직(개발해야 할 프로그램)과 공통모듈(트랜젝션, 로깅등 모든 프로그램에 있어야 할 기능)을 분리하여 관리하는 것

이후 비지니스 로직에 공통을 주입하여 기능의 구분이 가능함

3. Maven 이란

Spring 프레임워크를 사용하면서 다양한 라이브러리를 사용하게 되는데요.

db연결을 위한 ojdc나 mybatis등을 사용하기 위해 jar 파일을 구해 프로젝트에 추가시켜야 하는 단점이 있는데요.

Maven은 Pom.xml 에 해당 라이브러리를 작성함으로써 자동적으로 jar파일을 다운로드 하는 기능을 가집니다.

4. Spring 설치 및 Maven 연동 하기

4.1 Spring sts 설치

spring maven

 

우선 Spring 프레임워크를 설치해야 합니다.

이클립스를 실행후 help -> MarketPlace 로 이동합니다.

spring maven1

F

ind 에 sts로 검색하시면 Spring Tool Suite 이 뜨는데 이클립스 버전에 맞게 설치합니다.

spring maven2

 

Install과 Confirm 버튼을 통해 sts를 설치 합니다.

spring maven3

약관 동의를 하시고 Finish를 클릭합니다.

spring maven4

설치가 진행되는 모습입니다.

spring maven5

설치가 완료되면 이클립스를 재시작 합니다.

4.2 Maven 설치

spring maven6

Maven 다운로드 사이트로 접속하셔서 설치 파일을 다운을 받습니다.

그리고 C:에 압축을 풀어줍니다

spring maven7

 

이전에 소개했던 jdk 설정 방법과 마찬가지로 환경설정을 해줘야 합니다.

다음과 같이 MAVEN_HOME에 Maven 경로를 지정해 줍니다.

spring maven8

 

그리고 위와 같이 세부 경로인 bin을 path에 지정해 줍니다.

spring maven9

 

 

환경변수 등록 후 cmd창에 mvn -version을 쳤을때 다음과 같이 나오면 성공입니다.
spring maven10

 

 

다시 help -> MarketPlace로 이동하여 Maven을 검색하여 위와 같이 설치합니다.

설치가 완료되면 다시 이클립스를 재시작 하게 됩니다.

spring maven11

 

 

Maven을 이클립스에 등록하기 위해 Preferences로 이동합니다.

spring maven12

 

 

Maven탭의 Installations로 이동하여 Add를 눌러줍니다.

spring maven13

 

 

조금전 Maven을 설치했떤 경로를 지정해 줍니다.

spring maven14

 

위와 같이 경로가 정상적으로 입력되면 OK를 눌러줍니다.

4.3 연동 확인

spring maven15

 

 

이제 Spring과 Maven이 연동된 프로젝트를 만들기 위해 File – New -> Others 를 실행합니다.

 

spring maven16

 

Spring Project 가 없는 분들이 있을껀데요.

그럴경우 Spring Tool Suite 을 지우고 다시 설치해 주시면 됩니다.

 

spring maven17

 

프로젝트 이름을 지정하고 다음을 누릅니다.

 

spring maven18

 

패키지 경로 또한 지정해 줍니다.

 

spring maven19

 

그럼 위오 같이 프로젝트가 생성되는데요.

 

spring maven20

 

 

톰캣을 구동해 프로젝트를 실행하니 정상적으로 작동되었습니다.

이제 pom.xml에 사용할 라이브러리를 지정하여 사용할 수 있습니다.

Spring Maven 프로젝트 구조는 실무에서도 많이 사용되니 참고하시기 바랍니다.

 

 

 

여러분의 댓글이 빵찌니를 춤추게합니다.

반응형

+ Recent posts