Thread로 삽질을 하다가.....
알게 된....

우선 httpClient를 이용해서 웹스크래핑을 하는데요. Thread를 이용해서 5개정도 만들어서 5개를 스크래핑 시키면 더 빠르더라구요-_-; 그래서 Thread로 삽질을 하게 되었는데!

10분마다 도는 스케쥴러에 의해 실행되는 스케쥴러가 있다고 칩시다. 이 스케쥴러는 10분마다 해당 일을 실행합니다. 그 해당일은 스레드5개를 만들어서 5개를 웹스크래핑하는 겁니다.
이걸 그냥
[code]MyThread thread = new Thread();
thread.start();[/code]
이렇게 5개를 반복해서 넣어버리면......5개가 돌고 있는데 아직 끝나지 않았는데 이 스케쥴러는 다시 또 5개의 스레드를 만들어서 또 실행을 하게 됩니다-_-;
즉, 해당 메소드에서 해당 스레드가 끝날 때 까지 기다려주는 메소드인 것입니다.

[code]logger.info("DailySearch Start!");
        List<ScrapThread> threadList = new ArrayList<ScrapThread>();
       
        int count = bizNoDao.countBizListByStatus("T");
        for (int i=0; i<count / THREAD_SIZE; i++)
        {
            ScrapThread scrapThread = (ScrapThread)
                applicationContext.getBean("scrapThread");
            List<User> list = bizNoDao.selectUserBytStatusAndRow(
                    i * THREAD_SIZE, i * THREAD_SIZE + THREAD_SIZE, "T");
           
            scrapThread.setList(list);
            scrapThread.start();
           
            threadList.add(scrapThread);
        }
       
        for (int i=0; i<threadList.size(); i++)
        {
            try
            {
                threadList.get(i).join();
            }
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
        }
        logger.info("DailySearch End!");
[/code]
조낸 귀찮아서 그냥 소스 가져다 붙입니다-_-;
그냥 내용을 보면 ScrapThread라는 놈을 여러개 만들어서 Thrad start시켜놓고, 여러개가 돌게 한다음에 고 아래에서는 해당 스레드를 join을 시킵니다. 그럼 1번스레드가 끝날 때까지 웨이트 하다가 끝나면 다시 2번쓰레드를 join시키고, 그런식으로 다 끝날 때 까지 기다린 다음에 끝내야합니다.

그렇게 되면 이 해당 메소드가 끝나지 않았기 때문에 다 끝나고 난 다음에 DailySearch End라는 말이 호출이 되게 되는 겁니다-_-; join안하면 바로 DailySearch End찍고, 그냥 스케쥴러가 끝이 나는거죠.

이것도 장황하게 포스팅하는 이유는 반나절을 고생해서 입니다-_-;

 
Posted by 머드초보
,
 
Thread를 두개를 실행시켜서 DB에 동시에 INSERT를 시켜버리니 INSERT할 때 데이터가 AUTOINCRESEMENT가 아니라 ID부분을 직접 입력해서 하는 부분이면 문제가 발생합니다.
그래서 이 MERGE INTO문을 활용해서 데이터가 있으면 INSERT하고 없으면 UPDATE하는 구문을 만들었습니다.
이렇게 해도 제 생각이지만, Thread한 놈은 이미 데이터가 없는 것을 확인하고 insert를 시도 하려고 하고, 다른 Thread놈은 저 놈이 insert를 아직 하지 않았으니까 검색해서 안나오니 insert를 해보려고 하니 둘 다 insert를 시도하게 되더라구요.

이게 예제 입니다.
우선 테이블구조입니다.
테이블은 autoincreament가 아닌 수동적인 기본키를 사용합니다.
[code]CREATE TABLE "INSERTTABLE" ( "ID" NUMBER NOT NULL ENABLE, "DATA" VARCHAR2(4000) NOT NULL ENABLE, CONSTRAINT "INSERTTABLE_PK" PRIMARY KEY ("ID") ENABLE )
[/code]


applicationContext.xml
[code]<?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:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-2.5.xsd">

    <context:component-scan base-package="thread" />
   
    <!-- oracle용 -->
    <bean id="dataSource"
        class="org.springframework.jdbc.datasource.DriverManagerDataSource"
        p:driverClassName="oracle.jdbc.driver.OracleDriver"
        p:url="jdbc:oracle:thin:@localhost:1521:XE"
        p:username="mudchobo" p:password="1234" />
 
    <!-- SqlMap setup for iBATIS Database Layer -->
    <bean id="sqlMapClient"
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"
        p:dataSource-ref="dataSource"
        p:configLocation="classpath:config/SqlMapConfig.xml" />
   
    <bean id="sqlMapClientTemplete"
        class="org.springframework.orm.ibatis.SqlMapClientTemplate"
        p:sqlMapClient-ref="sqlMapClient"/>
           
</beans>
[/code]
SqlMapConfig.xml
[code]<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE sqlMapConfig     
    PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"     
    "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <sqlMap resource="config/Insert.xml" />
</sqlMapConfig>
[/code]
Insert.xml
[code]<?xml version="1.0" encoding="UTF-8" ?>

<!DOCTYPE sqlMap     
PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="Sqlmap">
       <insert id="insertData">
           MERGE INTO INSERTTABLE
           USING DUAL
           ON (ID = 1)
           WHEN MATCHED THEN
           UPDATE SET
           DATA = 'HERMUSSERI'
           WHEN NOT MATCHED THEN
           INSERT (ID, DATA)
           VALUES (1, 'MUDCHOBO')
       </insert>
      
</sqlMap>
[/code]
ThreadTestDao.java
[code]package thread;

public interface ThreadTestDao {
    public void insertData();
}
[/code]
ThreadTestDaoImpl.java
[code]package thread;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.ibatis.SqlMapClientTemplate;
import org.springframework.stereotype.Repository;

@Repository
public class ThreadTestDaoImpl implements ThreadTestDao {

    @Autowired
    private SqlMapClientTemplate sqlMapClientTemplate;

    @Override
    public void insertData() {
        sqlMapClientTemplate.insert("insertData");
    }
}
[/code]
TestThread.java
[code]package thread;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Scope("prototype")
@Component
public class TestThread extends Thread {

    @Autowired
    private ThreadTestDao threadTestDao;
   
    @Override
    public void run() {
        threadTestDao.insertData();
    }
}
[/code]
ThreadTest.java
[code]package thread;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class ThreadTest {

    public static void main(String[] args) {
        String configLocation = "config/applicationContext.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(
                configLocation);

        TestThread testThread1 = (TestThread) context
                .getBean("testThread");
        TestThread testThread2 = (TestThread) context
                .getBean("testThread");
       
        testThread1.start();
        testThread2.start();
    }
}
[/code]
실행할 때 계속 몇번을 실행해보면-_-; unique constraint 또는 ORA-00001: 무결성 제약 조건(MUDCHOBO.INSERTTABLE_PK)에 위배됩니다를 볼 수 있을 겁니다.

이게 제 생각인데, 첫번째 스레드가 돌면서 데이터를 확인하니 없길래 insert를 하려던 찰나에 두번째 스레드놈도 거의 동시에 데이터를 확인하니 없어서 insert를 시키려다가 에러가 나는 듯한데요.

트랜잭션도 먹이고 별 지롤을 다 했는데 안되길래 그냥 싱크방식으로 변경해버렸습니다.
insertData라는 메소드에
[code]@Override
    synchronized public void insertData() {
        sqlMapClientTemplate.insert("insertData");
    }
[/code]
메소드 앞에 synchronized를 붙입니다.
이건 어떤 스레드가 호출을 했으면 다음 스레드는 기다린 다음에 접근하게 되는 즉 싱크방식으로 처리를 시킬 수 있는 메소드가 됩니다.
이렇게 처리하면 효율은 좀 떨어지겠지만, 유니크 중복에러는 그나마 막을 수 있습니다.

다른 방법 있으면 좀 알려주세요 ㅠㅠ 고생하고 있습니다 ㅠ




 
Posted by 머드초보
,
 

클라이언트에서 ajax로 요청했을 때 자료를 손쉽게 파싱하기 위해서 json으로 클라이언트에 던져줄 때가 편할 때가 있습니다. 그래서 손쉽게 json으로 변환하는 라이브러리가 있습니다.

http://json-lib.sourceforge.net/

여기보면 json-lib라는 놈이 있는데 json으로 손쉽게 변환해주는 라이브러리입니다.

  • jakarta commons-lang 2.3
  • jakarta commons-beanutils 1.7.0
  • jakarta commons-collections 3.2
  • jakarta commons-logging 1.1
  • ezmorph 1.0.4

    이것들이 필요하다더군요. 위에 4개는 apache에 가면 있구요. ezmorph는 구글링해서 찾아서 받으세요.
    그리고 JSON-LIB인 json-lib-2.2.1-jdk15.jar가 필요합니다.

    사용법은 매우 간단합니다.
    [code]
    public class JsonTest {
     
     @Test
     public void Bean2Json()
     {
      MyBean myBean1 = new MyBean();
      myBean1.setId(1);
      myBean1.setName("mudchobo");
      MyBean myBean2 = new MyBean();
      myBean2.setId(2);
      myBean2.setName("shit");
     
      List<MyBean> mybeanList = new ArrayList<MyBean>();
      mybeanList.add(myBean1);
      mybeanList.add(myBean2);
     
      JSONArray jsonArray = JSONArray.fromObject(mybeanList);
      System.out.println("mybeanList - " + jsonArray);
     
      Map<String, Object> map = new HashMap<String, Object>();
      map.put("beanlist", jsonArray);
     
      JSONObject jsonObject = JSONObject.fromObject(map);
      System.out.println("json - " + jsonObject);
     }
    }
    [/code]
    Bean 2개를 List에 add를 한다음에 JSONArray라는 객체가 List를 배열로 만드는놈입니다.

    mybeanList - [{"id":1,"name":"mudchobo"},{"id":2,"name":"shit"}]

    이런식으로 만듭니다.
    저거를 JSONObject클래스를 이용해서 앞에 이름을 붙여줍니다. Map을 이용하면 됩니다
    Map을 이용해서 put에서 첫번째 인자에 이름을 넣고, 두번째 인자에 방금 생성한 Array를 넣으면 됩니다.
    그리고 JSONObject.fromObject메소드를 이용해서 생성하게 되면 이렇게 됩니다.

    json - {"beanlist":[{"id":1,"name":"mudchobo"},{"id":2,"name":"shit"}]}

    이상입니다-_-;

  •  
    Posted by 머드초보
    ,
     

    일주일에 한번, 하루에 한번 작업을 수행하는 프로세스를 실행하고 싶을 때가 있을 껍니다.
    한가한 시간에 자동으로 배치작업을 실행하는 등의 작업을 자바에서 수행할 수 있습니다.
    Timer라는 클래스에다가 시작날짜, 시간을 설정한 뒤, TimerTask클래스를 상속받은 클래스에서 run메소드를 구현하게 되면 설정된 시간에 run메소드가 자동으로 수행되게 됩니다.

    코드를 보도록 합시다.

    [code]
    package com.mudchobo.scheduler;

    import java.util.TimerTask;

    public class WeeklySearch extends TimerTask {

     @Override
     public void run() {
      System.out.println("WeeklySearch!");
     }
    }
    [/code]
    TimerTask를 상속받아서 run메소드를 구현했습니다. run메소드는 간단히 WeeklySearch라고 보여주는군요.

    그럼 메인을 보도록 합시다.
    [code]
    package com.mudchobo.scheduler;

    import java.util.Calendar;
    import java.util.Timer;

    public class Scheduler {

     public static void main(String[] args) {
      WeeklySearch weeklySearch = new WeeklySearch();
     
      Timer timer = new Timer();
      Calendar date = Calendar.getInstance();
      date.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
      date.set(Calendar.AM_PM, Calendar.PM);
      date.set(Calendar.HOUR, 11);
      date.set(Calendar.MINUTE, 29);
      date.set(Calendar.SECOND, 0);
      date.set(Calendar.MILLISECOND, 0);
     
      timer.scheduleAtFixedRate(weeklySearch, date.getTime(),
        1000 * 60 * 60 * 24 * 7);
     }
    }
    [/code]
    Timer객체, Calendar객체를 선언합니다. Calendar객체에는 이 스케쥴이 시작될 시간을 설정해서 넣습니다. 그 뒤에 timer에 있는 scheduleAtFixedRate메소드에 첫번째 인자는 맨 위에서 생성한 TimerTask객체를 넣으면 되구요. 두번째는 이 스케쥴이 시작될 시간을 설정해서 넣으면 되구요. 3번째는 얼마만큼의 주기로 실행될 지 기간을 설정하게 됩니다.
    밀리초여서 1000밀리초 * 60초 * 60분 * 24시간 * 7일 하게 되면 저것은 1주일에 한번 실행되게 됩니다.

    이상입니다!

     
    Posted by 머드초보
    ,
     

    로그 찍는 거 별로 안좋아하는데 로그를 찍어보니까 더 좋은 것 같아요 ^^
    게다가 log4j라는 매우 우수한 로그찍는 프로그램이 있습니다.
    sysout에서 벗어나봅시다-_-; 습관적으로 sysout을-_-(System.out.println()......-_-)

    우선 이클립스에서 프로젝트를 하나 만들어봅시다.
    log4j를 받아봅시다.
    http://logging.apache.org/log4j/1.2/download.html
    1.2버전입니다. 받아서 log4j-1.2.15.jar파일을 라이브러리에 추가합시다.

    log4j설정파일을 만들어봅시다.
    최상위 폴더에다가 log4j.properties파일을 만듭시다.
    [code]
    # Log4j Setting file
    log4j.rootLogger=INFO, console

    # Daily file log
    log4j.appender.logfile=org.apache.log4j.DailyRollingFileAppender
    log4j.appender.logfile.File=D:/mudchobo/Log/glv.log
    log4j.appender.logfile.DatePattern='.'yyyy-MM-dd
    log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
    log4j.appender.logfile.layout.ConversionPattern=[%d{HH:mm:ss}][%-5p](%F:%L) - %m%n

    # Console log
    log4j.appender.console=org.apache.log4j.ConsoleAppender
    log4j.appender.console.layout=org.apache.log4j.PatternLayout
    log4j.appender.console.layout.ConversionPattern=%-5p %l - %m%n

    # log level and appender
    log4j.logger.com.mudchobo=DEBUG, console
    log4j.logger.com.mudchobo.Test=INFO, logfile
    [/code]
    대략 내용을 살펴보면 log4j.rootLogger는 최상위 로거입니다.
    모든 INFO레벨이상의 로그는 다 console로 찍겠다는 겁니다.
    (레벨에는 DEBUG, INFO, WARN, ERROR, FATAL 순인데, 예를 들어 INFO레벨로 지정해두면 logger.debug로 찍는 로그는 나타나지 않습니다. INFO레벨 이상것만 나타납니다.)

    console은 아래 #Console log쪽에 보시면
    log4j.appender.console <- 요 이름입니다.
    요 console은 자세히보면 ConsoleAppender라는 클래스입니다. 이건 말그대로 콘솔에 로그를 찍어준다는 겁니다. layout에는 PatternLayout을 지정할 수 있는데 저 패턴은 뭐 레벨이 뭐고, 클래스가 뭐고, 메시지찍고 뭐 그런 내용입니다. 검색 고고싱-_-;

    그리고, 파일에다가 출력 할 수 있는데, DailyRollingFileAppender클래스를 이용합니다. 이눔은 말그대로 매일매일 다른로그를 사용하게 만듭니다. 로그이름이 위와 같이 glv.log라면, 해당로그가 어제날짜인데 로그를 찍으려고 하면 기존에 있던 파일은 glv.log.2008-04-17 이렇게 바꿔줍니다.

    아래부분에 보면 log4j.logger. 다음에 패키지명이나 클래스명을 지정해놓고, 로그레벨과 출력할 로그를 지정할 수 있는데요. 해당 클래스나 패키지의 로그는 저걸로 찍겠다는 겁니다. Test클래스는 logfile로 찍힌다는 겁니다.
    그리고, rootLogger가 colsole로 지정되어 있기 때문에 console에도 찍히겠죠? ^^

    로그를 찍어봅시다.
    TestLogging이라는 프로젝트 이름으로 만듭시다.

    Test클래스를 만들어봅시다.
    Test.java
    [code]
    package com.mudchobo;

    import org.apache.log4j.Logger;

    public class Test {

     private Logger logger = Logger.getLogger(getClass());
     
     public void println() {
      logger.info("안녕하세요! Test입니다");
     }
    }
    [/code]
    Test2클래스를 만들어봅시다.
    Test2.java
    [code]
    package com.mudchobo;

    import org.apache.log4j.Logger;

    public class Test2 {

    private Logger logger = Logger.getLogger(getClass());
     
     public void println() {
      logger.info("안녕하세요! Test2입니다.");
     }
    }
    [/code]
    TestLogging클래스를 만들어봅시다. 메인을 만들어야합니다.
    [code]
    package com.mudchobo;

    public class TestLogging {

     public static void main(String[] args) {
      Test test = new Test();
      Test2 test2 = new Test2();
     
      test.println();
      test2.println();
     }
    }
    [/code]
    자 그럼 콘솔에는
    INFO  com.mudchobo.Test.println(Test.java:10) - 안녕하세요! Test입니다.
    INFO  com.mudchobo.Test.println(Test.java:10) - 안녕하세요! Test입니다.
    INFO  com.mudchobo.Test2.println(Test2.java:10) - 안녕하세요! Test2입니다.
    INFO  com.mudchobo.Test2.println(Test2.java:10) - 안녕하세요! Test2입니다.
    이렇게 출력이 될 것이고 로그파일에는
    [19:56:35][INFO ](Test.java:10) - 안녕하세요! Test입니다.
    이것만 출력될 것입니다.
    위에 콘솔에 두번 찍힌 이유는 Rootlogger도 찍고, 아래 패키지를 지정한 로그도 찍었기 때문이죠.
    그리고, 파일에는 한번만 쓰여진 이유는 파일에 쓰는건
    log4j.logger.com.mudchobo.Test=INFO, logfile 여기 이 Test클래스 하나죠-_-;
    이상입니다-_-;


     
    Posted by 머드초보
    ,