Web/Spring 2018. 5. 7. 19:47

Spring - Jsoup을 이용한 웹크롤링


채팅을 개발하던 도중에 간단히 챗봇 기능을 만들어보면 어떨까 하는 생각에 오늘 날씨를 물어보면 오늘 날씨에 대한 정보를 답장으로 보내주는 채팅을 구현하기 위해 Jsoup을 이용한 웹크롤링을 진행해보았습니다.




pom.xml



1
2
3
4
5
<dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.8.3</version>
</dependency>
cs





웹 크롤링 코드



1
2
3
4
5
String URL = "https://weather.naver.com/rgn/cityWetrMain.nhn";
Document doc = Jsoup.connect(URL).get();
Elements elem = doc.select(".tbl_weather tbody>tr:nth-child(1)");
String[] str = elem.text().split(" ");
Elements elem2=doc.select(".tbl_weather tbody>tr:nth-child(1) img");
cs


1. Document doc=Jsoup.connect(URL).get(); => 지정한 url의 html태그를 모두 가져옵니다. html태그를 문자열 형태로 다 가져옵니다.


2.Elements elem=doc.select("~"); => 지정한 url에서 가져온 html태그 중에 원하는 태그를 가져옵니다. 괄호 안에는 css 선택자를 지정하여 원하는 태그를 가져옵니다. 결과는 문자열로 html태그들을 모두 가져옵니다. 만약 태그들을 그대로 적용할 생각이라면 elem2를 jsp로 리턴하여 사용하면 됩니다.


3.String str=elem.text(); =>elem안에 담긴 html태그에서 문자열을 모두 가져오는 메소드입니다. 단순히 태그가 아니라 문자열들만 가져오고 싶다면 이메소드를 이용해서 문자열들을 가져오면 됩니다.


String str=elem.text( ); 가 가져온 텍스트

=>서울 경기 구름많음 기온 15.0℃ 강수확률 20% 구름조금 기온 25.0℃ 강수확률 10%


Elements elem=doc.select("~"); 가 가져온 텍스트

=>

<tr> 

 <th scope="row"> <a href="/rgn/cityWetrWarea.nhn?cityRgnCd=CT001000"> 서울<br> 경기 </a> </th> 

 <td> <p class="icon"><img src="https://ssl.pstatic.net/static/weather/images/w_icon/w_l21.gif" width="64" height="46" alt="구름많음"></p> 

  <ul class="text"> 

   <li class="nm">구름많음</li> 

   <li>기온 <span class="temp"><strong>15.0</strong>℃</span><br> 강수확률 <span class="rain"><strong>20</strong>%</span></li> 

  </ul> </td> 

 <td class="line"> <p class="icon"><img src="https://ssl.pstatic.net/static/weather/images/w_icon/w_l2.gif" width="64" height="46" alt="구름조금"></p> 

  <ul class="text"> 

   <li class="nm">구름조금</li> 

   <li>기온 <span class="temp"><strong>25.0</strong>℃</span><br> 강수확률 <span class="rain"><strong>10</strong>%</span></li> 

  </ul> </td> 

</tr>


Elements elem2=doc.select("~"); 가 가져온 텍스트

=>

<img src="https://ssl.pstatic.net/static/weather/images/w_icon/w_l21.gif" width="64" height="46" alt="구름많음">

<img src="https://ssl.pstatic.net/static/weather/images/w_icon/w_l2.gif" width="64" height="46" alt="구름조금">






결과 



텍스트들은 elem.text()를 이용하여 가져온 것들이고 사진은 elem2를 이용해 img태그를 그대로 이용한 것입니다.

posted by 여성게
:
Web/Spring 2018. 5. 6. 22:39

Spring AOP를 이용한 Xss 공격 방지


lucy-xss를 이용하여 간단하게 게시글이나 댓글에 XSS공격을 방지하는 기능을 구현하였습니다. 그런데 보통 홈페이지는 게시판과 댓글 기능이 하나만 들어가있는 경우는 드뭅니다. 그말은 즉, xss관련 코드가 2개 이상이 중복되어 구현됩니다. 그래서 spring AOP를 이용하여 XSS 관련 코드를 하나의 클래스 파일에 구현해 게시물,댓글 등의 코드에 중복되어 구현되지 않게 하였습니다.






pom.xml



1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- lucy xss  -->
<dependency>
    <groupId>com.navercorp.lucy</groupId>
    <artifactId>lucy-xss</artifactId>
    <version>1.6.3</version> 
</dependency>
 
<!-- aop weaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.8</version>
</dependency>
cs




lucy-xss-superset.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
<?xml version="1.0" encoding="UTF-8"?>
 
<config xmlns="http://www.nhncorp.com/lucy-xss"
    extends="lucy-xss-default.xml">
 
    <elementRule>
        <element name="body" disable="true" />
        <element name="embed" disable="true" />
        <element name="iframe" disable="true" />
        <element name="meta" disable="true" />
        <element name="object" disable="true" />
        <element name="script" disable="true" />
        <element name="style" disable="true" />
    </elementRule>
    
    <attributeRule>
        <attribute name="data" base64Decoding="true">
            <notAllowedPattern><![CDATA[(?i:s\\*c\\*r\\*i\\*p\\*t)]]></notAllowedPattern>
            <notAllowedPattern><![CDATA[&[#\\%x]+[\da-fA-F][\da-fA-F]+]]></notAllowedPattern>
        </attribute>
        <attribute name="src" base64Decoding="true">
            <notAllowedPattern><![CDATA[(?i:s\\*c\\*r\\*i\\*p\\*t)]]></notAllowedPattern>
            <notAllowedPattern><![CDATA[&[#\\%x]+[\da-fA-F][\da-fA-F]+]]></notAllowedPattern>
        </attribute>
        <attribute name="style">
            <notAllowedPattern><![CDATA[(?i:e\\*x\\*p\\*r\\*e\\*s\\*s\\*i\\*o\\*n)]]></notAllowedPattern>
            <notAllowedPattern><![CDATA[&[#\\%x]+[\da-fA-F][\da-fA-F]+]]></notAllowedPattern>
        </attribute>
    </attributeRule>
 
</config>
cs

차단 할 댓글,게시글의 패턴을 설정하는 파일입니다. 파일 위치는 src/main/resources 디렉토리 밑에 넣어주면 됩니다.






BeforeAdvice.java



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
@Service
@Aspect
public class BeforeAdvice {
    private static final Logger logger = LoggerFactory.getLogger(BeforeAdvice.class);
    
    @Before("PointcutCommon.insertReplyPointCut()")
    public void beforeInsertReplyAdvice(JoinPoint jp) {
        logger.info("[beforeAdviceLog] : "+jp.getSignature().getName()+"()호출");
        Object[] obj=jp.getArgs();
        for(int i=0;i<obj.length;i++) {
            if(obj[i] instanceof ReplyDTO) {
                ReplyDTO rdto=(ReplyDTO)obj[i];
                String commentContent=rdto.getR_content();
                XssFilter xssFilter=XssFilter.getInstance("lucy-xss-superset.xml");
                String filterCommentContent=xssFilter.doFilter(commentContent);
                Pattern p=Pattern.compile("<!-- Not Allowed Tag Filtered -->");
                Matcher m=p.matcher(filterCommentContent);
                String xssResult="";
                if(m.find()){
                    xssResult="비정상적인 댓글입니다.";
                }else{
                    xssResult=commentContent;
                }
                if(!commentContent.equals(xssResult)) {
                    logger.info("[beforeAdviceLog] : "+commentContent+"에서 "+xssResult+"로 댓글 내용 변경");
                }
                rdto.setR_content(xssResult);
            }else if(obj[i] instanceof ReplyPhotoDTO) {
                ReplyPhotoDTO rpdto=(ReplyPhotoDTO)obj[i];
                String commentContent=rpdto.getR_content();
                XssFilter xssFilter=XssFilter.getInstance("lucy-xss-superset.xml");
                String filterCommentContent=xssFilter.doFilter(commentContent);
                Pattern p=Pattern.compile("<!-- Not Allowed Tag Filtered -->");
                Matcher m=p.matcher(filterCommentContent);
                String xssResult="";
                if(m.find()){
                    xssResult="비정상적인 댓글입니다.";
                }else{
                    xssResult=commentContent;
                }
                if(!commentContent.equals(xssResult)) {
                    logger.info("[beforeAdviceLog] : "+commentContent+"에서 "+xssResult+"로 댓글 내용 변경");
                }
                rpdto.setR_content(xssResult);
            }
        }
    }
}
cs


@Before 어노테이션을 적용하여 프록시가 중간에 과정을 가로채게 됩니다. (Pointcut설정은 insertBoard,insertReply 등 게시물,댓글의 삽입 메소드의 이름을 통일하여 모두 적용되게 합니다. pointcut 소스는 생략합니다. 필요하시면 댓글 남겨주세요!) 만약 댓글을 삽입하던 게시물을 삽입하던 pointcut에 걸리게 되면 모두 이 AOP 메소드가 먼저 실행되게 됩니다.(@Before 어노테이션에 의해서 진짜 메소드가 실행되기 전에 먼저 aop의 메소드가 실행되게 된다.)


그렇다면 어떻게 사용자가 작성한 content를 가져오게 되는가? 보통 게시물을 쓰거나 댓글을 쓰게 되면 DB에 삽입하는 메소드의 매개변수로 작성한 content가 들어오게 됩니다. 그 매개변수를 JoinPoint의 객체의 getArgs()라는 메소드로 메소드의 매개변수를 가져오게 됩니다. getArgs( )는 object 형태로 매개변수를 가져오게 되므로 반복문을 통해 instanceof 로 원하는 매개변수를 얻어와서 만약 xss공격이 포함된 content 면 @Before클래스의 메소드 내에서 해당 매개변수를 바꿔서 다시 비즈니스 로직을 실행하는 것입니다. ex)<script>alert('xss attack !')</script>라는 댓글을 작성하면 이 클래스에서 '비정상적인 댓글입니다." 라고 매개변수를 바꿔줍니다. 그러면 비즈니스 로직에서는 변경된 댓글을 이용해 DB에 삽입되게 됩니다.



<실행과정>


1. XssFilter xssFilter=XssFilter.getInstance("lucy-xss-superset.xml"); => (싱글톤 패턴이 적용된) XssFilter 객체를 가져옵니다. resources 밑에 xml파일                                                                                             을 이용

2. String filterCommentContent=xssFilter.doFilter(commentContent); => 사용자가 작성한 댓글 내용에 doFilter( )를 이용하여 XSS코드가 포함 되어있는지                                                                                               filtering 합니다. 여기에서 '<' 같은 태그문자들은 '&lt','&gt' 등으로 변환됩니다.                                                                                               여기서 filtering되면 해당 content내에 <!-- Not Allowed Tag Filtered-->라는 문                                                                                               자가 포함된 문자열로 바뀌게 됩니다.

3. Pattern p=Pattern.complie("<!-- Not Allowed Tag Filtered -->"); => 정규식 패턴식을 정의


4. Matcher m=p.matcher(filterCommentContent); => 정의한 정규식패턴이 들어가 있다면 m.find( )는 true를 리턴하게 됩니다.




마치며..


혹시라도 읽어 보시다가 틀린 내용이 있다면 지적해주세요.. 혹시 이번 구현코드가 필요하시다면 여기서 생략한 pointcut코드 비즈니스 로직 코드등 댓글로 남겨주시면 보내드리겠습니다~!



posted by 여성게
:
Web/Spring 2018. 4. 22. 18:28

스프링 + 웹소켓을 이용한 간단한 실시간 채팅


우선 웹소켓이란 간단히 이야기하면 서버와 양방향 통신이 가능한 통신 방법이다. 그럼으로써 실시간 채팅 등이 구현이 가능한 것이다. 여기서 그러면 "ajax로 구현하면 되잖아? ajax도 서버와 통신이 되는데?" 생각을 하게된다. 나도 처음에는 그렇게 생각했는데, 생각해보면 ajax는 클라이언트가 서버로 데이터를 요청을 한다. 하지만 서버가 클라이언트에게 요청할 수 있는 방법이 없다. 하지만 웹소켓은 가능하다라는 것 ! ajax로 채팅을 구현한다면 클라이언트가 보낸 메시지를 서버가 받아서 그 메시지를 모든 사람에게 전송한다? ajax는 예를 들어 10초에 한번씩 서버에서 메시지를 뿌려주는 기능을 구현해야 할것이다. 왜냐하면 서버가 클라이언트에게 요청을 할수 있는 방법이 없기때문이다.


https://developer.mozilla.org/ko/docs/WebSockets/Writing_WebSocket_client_applications -> 참고




1. 시작전 설정


pom.xml


1
2
3
4
5
<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>${org.springframework-version}</version>
</dependency>
cs



web.xml

1
2
3
4
5
6
7
8
9
10
11
12
<servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                /WEB-INF/config/presentation-layer.xml
            </param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported> <!-- 웹소켓을 위한 설정(비동기지원) -->
</servlet>
cs




2.WebSocketChat


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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.web.rnb.controller;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.websocket.server.ServerEndpoint;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
 
import javax.websocket.RemoteEndpoint.Basic;
 
@Controller
@ServerEndpoint(value="/echo.do")
public class WebSocketChat {
    
    private static final List<Session> sessionList=new ArrayList<Session>();;
    private static final Logger logger = LoggerFactory.getLogger(WebSocketChat.class);
    public WebSocketChat() {
        // TODO Auto-generated constructor stub
        System.out.println("웹소켓(서버) 객체생성");
    }
    @RequestMapping(value="/chat.do")
    public ModelAndView getChatViewPage(ModelAndView mav) {
        mav.setViewName("chat");
        return mav;
    }
    @OnOpen
    public void onOpen(Session session) {
        logger.info("Open session id:"+session.getId());
        try {
            final Basic basic=session.getBasicRemote();
            basic.sendText("Connection Established");
        }catch (Exception e) {
            // TODO: handle exception
            System.out.println(e.getMessage());
        }
        sessionList.add(session);
    }
    /*
     * 모든 사용자에게 메시지를 전달한다.
     * @param self
     * @param message
     */
    private void sendAllSessionToMessage(Session self,String message) {
        try {
            for(Session session : WebSocketChat.sessionList) {
                if(!self.getId().equals(session.getId())) {
                    session.getBasicRemote().sendText(message.split(",")[1]+" : "+message);
                }
            }
        }catch (Exception e) {
            // TODO: handle exception
            System.out.println(e.getMessage());
        }
    }
    @OnMessage
    public void onMessage(String message,Session session) {
        logger.info("Message From "+message.split(",")[1+ ": "+message.split(",")[0]);
        try {
            final Basic basic=session.getBasicRemote();
            basic.sendText("to : "+message);
        }catch (Exception e) {
            // TODO: handle exception
            System.out.println(e.getMessage());
        }
        sendAllSessionToMessage(session, message);
    }
    @OnError
    public void onError(Throwable e,Session session) {
        
    }
    @OnClose
    public void onClose(Session session) {
        logger.info("Session "+session.getId()+" has ended");
        sessionList.remove(session);
    }
}
 
cs

@ServerEndpoint(value="/echo.do") 는 /echo.do 라는 url 요청을 통해 웹소켓에 들어가겠다라는 어노테이션입니다.

@onOpen 는 클라이언트가 웹소켓에 들어오고 서버에 아무런 문제 없이 들어왔을때 실행하는 메소드입니다.

@onMessage 는 클라이언트에게 메시지가 들어왔을 때, 실행되는 메소드입니다.

@onError 

@onClose 는 클라이언트와 웹소켓과의 연결이 끊기면 실행되는 메소드입니다.

sendAllSessionToMessage()는 어떤 누군가에게 메시지가 왔다면 그 메시지를 보낸 자신을 제외한 연결된 세션(클라이언트)에게 메시지를 보내는
메소드입니다.





3.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
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
<%@ page language="java" contentType="text/html; charset=EUC-KR"
    pageEncoding="EUC-KR"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>Insert title here</title>
</head>
<body>
    <div>
        <input type="text" id="sender" value="${sessionScope.member.m_id }" style="display: none;">
        <input type="text" id="messageinput">
    </div>
    <div>
        <button type="button" onclick="openSocket();">Open</button>
        <button type="button" onclick="send();">Send</button>
        <button type="button" onclick="closeSocket();">Close</button>
    </div>
    <!-- Server responses get written here -->
    <div id="messages"></div>
    <!-- websocket javascript -->
    <script type="text/javascript">
        var ws;
        var messages=document.getElementById("messages");
        
        function openSocket(){
            if(ws!==undefined && ws.readyState!==WebSocket.CLOSED){
                writeResponse("WebSocket is already opened.");
                return;
            }
            //웹소켓 객체 만드는 코드
            ws=new WebSocket("ws://localhost:9080/rnb/echo.do");
            
            ws.onopen=function(event){
                if(event.data===undefined) return;
                
                writeResponse(event.data);
            };
            ws.onmessage=function(event){
                writeResponse(event.data);
            };
            ws.onclose=function(event){
                writeResponse("Connection closed");
            }
        }
        
        function send(){
            var text=document.getElementById("messageinput").value+","+document.getElementById("sender").value;
            ws.send(text);
            text="";
        }
        
        function closeSocket(){
            ws.close();
        }
        function writeResponse(text){
            messages.innerHTML+="<br/>"+text;
        }
  </script>
</body>
</html>
cs







posted by 여성게
:
Web/Spring 2018. 3. 13. 16:07

스프링에서 어노테이션을 이용한 DI 방법



1.@Autowired,@Resource,@Qualfier 어노테이션


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
65
package com.web.nuri;
 
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
 
import javax.annotation.Resource;
 
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataAccessException;
 
import com.web.nuri.user.UserDTO;
import com.web.nuri.user.UserService;
 
//JUnit 확장기능들(import문은 수동으로 넣어줘야한다)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/test-applicationContext.xml"//테스트용 applicationContext.xml kodictest/kodictest
public class UserDAOTest {
    @Resource(name="userServiceImpl")
    UserService userService;
    @Autowired
    PlatformTransactionManager transactionManager;
    
    //트랜잭션 경계설정 코드
    @Test(expected=DataAccessException.class)
    public void insertUserTest() {
        //트랜잭션의 시작을 알린다.(트랜잭션을 얻어오는 시점이 시작)
        TransactionStatus status=this.transactionManager.getTransaction(new DefaultTransactionDefinition());
        UserDTO user1=new UserDTO();
        UserDTO user2=new UserDTO();
        UserDTO user3=new UserDTO();
        try {
            user1.setId("test@test.com");
            user1.setPw("1111");
            user1.setNickName("tester");
            user2.setId("test1@test.com");
            user2.setPw("1111");
            user2.setNickName("tester1");
            user3.setId("test@test.com");
            user3.setPw("1111");
            user3.setNickName("tester2");
        
            userService.insertUser(user1);
            userService.insertUser(user2);
            userService.insertUser(user3);
            this.transactionManager.commit(status);
        }catch (RuntimeException e) {
            // TODO: handle exception
            this.transactionManager.rollback(status);
            throw e;
        }
        //UserDTO user4=userService.getUser(user1);
        //assertThat(user2.getId(),is(user1.getId()));
        //userService.deleteUser(user);
    }
}
 
cs

위에서 보듯 어노테이션을 이용한 DI방법은 여러가지가 있지만 대표적으로 2가지를 설명하자면 @Autowired , @Resource 를 이용한 DI 주입 방법을 들수있다. @Autowired는 해당 변수의 인터페이스 타입으로 등록된 빈들에서 찾아서 DI를 주입해주는 방법이다. 만약 UserService 라는 인터페이스를 구현한 클래스가 2개 이상 빈으로 등록되어있다면 이름의 충돌이 발생해 제대로 DI가 되지 않는 경우가 있다.(에러가 뜬다. 하지만 에러를 발생하지 않게 하려면 @Autowired(require=false) 로주면 에러는 발생하지 않는다.) 변수의 타입으로 판별 할수 없다면 변수명을 이용하여 DI를 한다고 하지만 그 마저 마땅치 않으면 예외가 발생 할 수가 있다. 그래서 사용하는 어노테이션이 @Resource 어노테이션이다. @Resource(name="~") name이라는 속성안에 등록된 빈의 이름을 지정한다면 같은 타입의 인터페이스를 구현한 클래스들이 여러개 등록되어 있더라도 정확히 이름으로 골라서 등록이 가능하다. 하지만 만약 클래스를 @Service,@Component 등의 어노테이션으로 빈등록을 했다면? 방법이 있다. 예를 들어 UserServiceImpl 라는 UserService 인터페이스 구현클래스를 어노테이션으로 빈등록을 했다면 그 빈은 자동으로 userServiceImpl 라는 이름으로 빈등록이 된다. (앞글자를 소문자로) 그래서 내부적으로 지정된 userServiceImpl 라는 이름을 @Resource의 name 속성에 넣어주면 된다. @Autowired로 빈을 결정 지을 수 없을때, 밑에 @Qualifier("~")를 붙여서 특정빈을 선택한다.




2. DI 이전에 필요한 스프링설정


위의 어노테이션들을 사용하여 DI를 주입하기 위해서는 각각 어노테이션을 인식하기 위한 스프링 설정이 필요하다.


1
2
<!-- Root Context: defines shared resources visible to all other web components -->
    <context:component-scan base-package="com.web.nuri"></context:component-scan>
cs

posted by 여성게
:
Web/Spring 2018. 3. 7. 16:28

Spring Transaction(트랜잭션) 범위 설정하기



1.예외 상황(트랜잭션 범위설정이전)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void insertUserTest() {
        
        UserDTO user1=new UserDTO();
        UserDTO user2=new UserDTO();
        UserDTO user3=new UserDTO();
        
        user1.setId("test@test.com");
        user1.setPw("1111");
        user1.setNickName("tester");
        user2.setId("test1@test.com");
        user2.setPw("1111");
        user2.setNickName("tester1");
        user3.setId("test@test.com");
        user3.setPw("1111");
        user3.setNickName("tester2");
        
        userService.insertUser(user1);
        userService.insertUser(user2);
        userService.insertUser(user3);
        
    }
cs

예를 들어 이런 코드가 있다고 가정해보자. 여기서 보면 user3이라는 객체의 인스턴스변수를 설정해줄때, user3.setId() 메소드 부분에 user1과 같은 id가 들어가서 duplicationkey라는 예외가 발생하여 user3은 DB에 정상적으로 데이터가 삽입되지 않을 것이다. 여기서 transaction manager를 사용한다고 가정했을 때,



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    <!-- Transaction Manager -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- txManager Advice -->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="get*" read-only="true"/>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>    
    </tx:advice>
    <!-- txManagerAdvice aop -->
    <aop:config>
        <aop:pointcut expression="execution(* com.web.nuri..*(..))" id="txPointCut"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
    </aop:config>
cs

위에 보이는 대로 transaction manager를 설정하면 user1, user2는 정상적으로 DB에 삽입되고 user3는 DB에 저장되지 않고 rollback 될 것이다. 즉, UserDAO(userService)의 메소드 단위로 트랜잭션이 적용되어 user3에서 예외가 발생하여도 user1,user2는 정상적으로 DB에 삽입이 되는 것이다. 본인은 트랜잭션의 범위의 설정을 메소드 단위가 아닌 내가 직접 범위를 설정하여 user3에서 예외가 발생하였다고 하면 이전의 user1,user2의 데이터도 DB에 삽입되지 않게 하고 싶은 것이다.



2.트랜잭션 범위 설정


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
public class UserDAOTest {
    @Autowired
    UserService userService;
    @Autowired
    PlatformTransactionManager transactionManager;
    
    //트랜잭션 경계설정 코드
    public void insertUserTest() {
        //트랜잭션의 시작을 알린다.(트랜잭션을 얻어오는 시점이 시작)
        TransactionStatus status=this.transactionManager.getTransaction(new DefaultTransactionDefinition());
        UserDTO user1=new UserDTO();
        UserDTO user2=new UserDTO();
        UserDTO user3=new UserDTO();
        try {
            user1.setId("test@test.com");
            user1.setPw("1111");
            user1.setNickName("tester");
            user2.setId("test1@test.com");
            user2.setPw("1111");
            user2.setNickName("tester1");
            user3.setId("test@test.com");
            user3.setPw("1111");
            user3.setNickName("tester2");
        
            userService.insertUser(user1);
            userService.insertUser(user2);
            userService.insertUser(user3);
            this.transactionManager.commit(status);
        }catch (RuntimeException e) {
            // TODO: handle exception
            this.transactionManager.rollback(status);
            throw e;
        }        
    }
}
cs



여기에 적용되는 것은 스프링의 트랜잭션 서비스 추상화 기법이다. PlatformTransactionManager(트랜잭션매니저의 최상위 인터페이스) 라는 인터페이스에 각자의 DB에 해당되는 TransactionManager 클래스(여기서는 mybatis를 이용하므로 DataSourceTransactionManager가 된다)를 의존주입 해준 후에 TransactionStatus 객체를 생성해준다.( 이시점이 트랜잭션의 시작시점이 되는 것이다.)

그리고 예외없이 메소드가 잘 수행되면 transactionManager.commit(status), 예외가 발생하였다면 transactionManager.rollback(status) 메소드를 수행시킨다. 이 시점이 트랜잭션 범위의 끝지점이 되는 것이다. 이렇게 범위를 설정하게 되면 user3 객체의 삽입에서 예외가 발생하면 user1,user2의 객체는 이전에 수행됬던 것들이 모두 rollback 된다.




posted by 여성게
:
Web/Spring 2018. 2. 16. 00:30

Spring(스프링) 회원가입



1. 개발환경



1.eclipse

2.tomcat 8.0

3.spring 4.3.7

4.oracle






2. 프로젝트 파일구조









3. 관련 라이브러리 추가.



위에서 aop weaver 같은 경우는 pointcut에 걸렸을 때, advice 메소드를 호출해 실행하는 것이 weaver 라고 합니다. 그 기능을 하기 위해 weaver 라이브러리를 등록합니다.

parser 같은 경우는 repository가 있는 경로에 한글이 낄 경우 이상한 에러(log어쩌고 저쩌고)가 뜨게 됩니다.(ex:C:\Users\윤여성\.m2\repository....) 




그래서 한글 인코딩을 하기위한 라이브러리라고 생각하면 됩니다.






4.web.xml





처음에 있는 context-param의 경우는 서블릿이 컨트롤러를 호출하게 될때, 그 컨트롤러 코드에 있는 비즈니스단의 클래스의 빈생성(UserService)을 위해 미리 preload(ContextLoaderListner이용)하는 경우입니다. 그리고 그 다음은 presentation-layer.xml 파일을 로딩하여 해석한 servlet을 생성하는 코드입니다. 마지막으로 filter는 post방식으로 넘어오는 데이터의 한글 인코딩을 위한 코드입니다.



5. presentation-layer.xml




처음의 컴포넌트 스캔은 컨트롤러에 있는 어노테이션들을 인식해 빈 생성을 하기위한 코드이고, 그 다음의 리졸버는 컨트롤러에서 리턴하는 경로의 앞과 뒤에 붙여줄 문자를 설정하는 코드입니다.






6. applicationContext.xml




mybatis를 사용하기 위한 sql세션팩토리빈 생성과 세션템플릿 설정입니다. 그 다음은 트랜잭션 매니저를 빈등록하여 txAdvice를 등록해 밑의 pointcut에 걸려있는 메소들중 에러가 날 경우 자동으로 rollback을 하여 디비에 잘못된 칼럼이 등록되는 것을 방지하는 트랜잭션 매니저입니다. 여기서 advisor가 행위를 하기 위해 아까 라이브러리에 weaver라는 라이브러리를 등록한 것 입니다.




7. sql-map-config.xml , user-mapping.xml , database.properties



sql-map-config.xml 의 첫번째 alias는 user-mapper.xml에서 파라미터타입이나 리절트타입에 사용하기 위한 설정입니다. 그러면 user-mapper.xml 에서의 코드가 훨씬 간결해지는 것을 볼 수 있습니다.




8. UserDTO, UserDAO ,UserService , UserServiceImpl 구현



인스턴스 변수를 선언해주고 각각의 getter, setter 메소드를 선언해주면 됩니다. alt+shift+s 를 통해 겟터,셋터메소드 및 toString() 메소드를 쉽게 생성할 수 있습니다.


@Repository 어노테이션을 통해 applicationContext.xml 에 있는 context:component-scan 설정에 걸려 UserDAO 빈 생성을 하게합니다. 그리고 @Autowired 를 통해 applicationContext.xml 에 있는 sqlSessionTemplate 객체의 의존주입을 컨테이너에게 맡기게 됩니다. 그리고 각 메소드에 있는 mybatis.~은 모두 user-mapper.xml에 정의 해놓은 sql 구문들을 사용하는 sqlSessionTemplate의 메소드입니다.



UserService 클래스입니다. 보시면 UserDAO에 있는 메소드들이 추상메소드로 들어간 것이 보이시죠? 즉, UserService 객체의 다형성을 이용하기 위해 인터페이스 타입으로 클래스를 생성한 것입니다.(컨트롤러에서 직접 DAO에 접근하는 것이 아니라 Service 구현 클래스를 통해 DAO에 접근한다.) 하나 팁을 주자면 UserDAO에서 alt+shift+t 를하면 extract interface라는 기능이 있습니다. 그러면 UserDAO에 들어가있는 메소드를 모두 추상메소드 타입으로 갖고 있는 인터페이스를 쉽게 생성할 수 있습니다.(UserDAO에 생기게 되는 implements UserService는 꼭 지워줄것!)



UserService 인터페이스를 상속받아 구현한 클래스입니다.




9. UserController 구현



위에 @Autowired에 UserService 인터페이스 타입으로 의존주입을 받는 이유는 유지보수 이유때문입니다. 객체지향의 다형성을 이용하게 된다면 UserServiceImpl가 변경되어도 의존주입을 하게 되는 UserService와는 상속관계가 지속되기에 별도의 컨트롤러의 코드의 변경이 없어도 되게됩니다. 그리고 login.do 와 insertUser.do 매핑 메소드에 RequestMethod를 GET과 POST 두개를 나눈 이유는 index페이지에서 로그인 혹은 회원가입 버튼을 누르게 된다면 get방식으로 요청(진짜 로그인과 회원가입 같은 경우는 post 방식으로 데이터를 서버측으로 보내게 됩니다.)이 가서 각 페이지로 이동하게 됨으로 페이지 이동을 위한 메소드는 별도의 url매핑을 만들필요 없이 같은 url매핑으로 메소드 방식만 두개로 정해주면 됩니다. 




10. insertUser.jsp




<%@ page language="java" contentType="text/html; charset=EUC-KR"
 
    pageEncoding="EUC-KR"%>
 
<!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">
 
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css" rel="stylesheet">
 
    <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
 
    <script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/latest/js/bootstrap.min.js"></script>
 
    <style>
    body {
        background: #f8f8f8;
        padding: 60px 0;
    }
    
    #login-form > div {
        margin: 15px 0;
    }
 
 
 
</style>
 
<title>회원가입</title>
 
</head>
 
<body>
 
    <div class="container">
 
        <div class="col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2">
 
            <div class="panel panel-success">
 
                <div class="panel-heading">
 
                    <div class="panel-title">환영합니다!</div>
 
                </div>
 
                <div class="panel-body">
 
                    <form action="insertUser.do" id="login-form" method="post">
 
                        <div>
 
                            <input type="email" class="form-control id" name="id" placeholder="Email" oninput="checkId()" id="checkaa" autofocus>
 
                        </div>
 
                        <div>
 
                            <input type="password" class="form-control pass" name="pw" placeholder="Password" oninput="checkPwd()">
 
                        </div>
 
                        <div>
 
                            <input type="password" class="form-control pass" name="pwConfirm" placeholder="Confirm Password" id="repwd" oninput="checkPwd()">
 
                        </div>
 
                        <div>
 
                            <input type="text" class="form-control nickname" name="nickName" id="nickname" placeholder="Your Nickname" oninput="checkNick()" autofocus>
 
                        </div>
 
                        <div>
 
                            <button type="submit" class="form-control btn btn-primary signupbtn" disabled="disabled">회원가입</button>
 
                        </div>
 
                    </form>
 
                </div>
 
            </div>
 
        </div>
 
    </div>
 
    <script>
 
 
 
    //아이디와 비밀번호가 맞지 않을 경우 가입버튼 비활성화를 위한 변수설정
 
    var idCheck = 0;
 
    var nickCheck = 0;
 
    var pwdCheck = 0;
 
    //아이디 체크하여 가입버튼 비활성화, 중복확인.
 
    function checkId() {
 
        var inputed = $('.id').val();
 
        console.log(inputed);
 
        $.ajax({
 
            data : {
 
                id : inputed
 
            },
 
            url : "checkId.do",
 
            success : function(data) {
 
                if(inputed=="" && data=='0') {
 
                    $(".signupbtn").prop("disabled"true);
 
                    $(".signupbtn").css("background-color""#aaaaaa");
 
                    $("#checkaa").css("background-color""#FFCECE");
 
                    idCheck = 0;
 
                } else if (data == '0') {
 
                    $("#checkaa").css("background-color""#B0F6AC");
 
                    idCheck = 1;
 
                    if(idCheck==&& pwdCheck == 1) {
 
                        $(".signupbtn").prop("disabled"false);
 
                        $(".signupbtn").css("background-color""#4CAF50");
 
                    } 
 
                } else if (data == '1') {
 
                    $(".signupbtn").prop("disabled"true);
 
                    $(".signupbtn").css("background-color""#aaaaaa");
 
                    $("#checkaa").css("background-color""#FFCECE");
 
                    idCheck = 0;
 
                } 
 
            }
 
        });
 
    }
 
  //재입력 비밀번호 체크하여 가입버튼 비활성화 또는 맞지않음을 알림.
 
    function checkPwd() {
 
        var inputed = $('.pass').val();
 
        var reinputed = $('#repwd').val();
 
        console.log(inputed);
 
        console.log(reinputed);
 
        if(reinputed=="" && (inputed != reinputed || inputed == reinputed)){
 
            $(".signupbtn").prop("disabled"true);
 
            $(".signupbtn").css("background-color""#aaaaaa");
 
            $("#repwd").css("background-color""#FFCECE");
 
        }
 
        else if (inputed == reinputed) {
 
            $("#repwd").css("background-color""#B0F6AC");
 
            pwdCheck = 1;
 
            if(idCheck==&& pwdCheck == 1) {
 
                $(".signupbtn").prop("disabled"false);
 
                $(".signupbtn").css("background-color""#4CAF50");
 
            }
 
        } else if (inputed != reinputed) {
 
            pwdCheck = 0;
 
            $(".signupbtn").prop("disabled"true);
 
            $(".signupbtn").css("background-color""#aaaaaa");
 
            $("#repwd").css("background-color""#FFCECE");
 
            
 
        }
 
    }
 
    //닉네임과 이메일 입력하지 않았을 경우 가입버튼 비활성화
 
    function checkNick() {
 
        var nickname = $("#nickname").val();
 
        console.log(nickname);
 
        $.ajax({
 
            data : {
 
                nickName : nickname
 
            },
 
            url : "checkNickName.do",
 
            success : function(data) {
 
                if(nickname=="" && data=='0') {
 
                    $(".signupbtn").prop("disabled"true);
 
                    $(".signupbtn").css("background-color""#aaaaaa");
 
                    $("#nickname").css("background-color""#FFCECE");
 
                    nickCheck = 0;
 
                } else if (data == '0') {
 
                    $("#nickname").css("background-color""#B0F6AC");
 
                    nickCheck = 1;
 
                    if(idCheck==&& pwdCheck == 1) {
 
                        $(".signupbtn").prop("disabled"false);
 
                        $(".signupbtn").css("background-color""#4CAF50");
 
                    } 
 
                } else if (data == '1') {
 
                    $(".signupbtn").prop("disabled"true);
 
                    $(".signupbtn").css("background-color""#aaaaaa");
 
                    $("#nickname").css("background-color""#FFCECE");
 
                    nickCheck = 0;
 
                } 
 
            }
 
        });
 
    }
 
     
 
   </script>
 
</body>
 
</html>
cs





11. 마치며..


insertUser.jsp 와 UserController에 있는 코드 중 ajax를 이용한 실시간 아이디 중복체크 및 비밀번호 확인, 닉네임 체크 코드가 들어가있습니다. 그것은 별도로 따로 설명을 올리겠습니다.. 두서 없이 한 설명을 들어주셔서 감사합니다. 코드를 원하시는 분은 댓글로 남겨주시면 보내드리겠습니다.






posted by 여성게
: