프로세스와는 무관한 작업으로 인한 지연 현상이 발생하여 비동기 방식 처리를 고민하게되었다.


로그 히스토리를 DB에 기록하는 행위들인데 이 부분들에서 INSERT 시 지연이 발생하여 파일로 쌓았다가 긁어가야 하나.. 어떻게해야 하나 고민 하던 중 WAS 에서 제공하는 JMS 를 사용할까 하다가 ActiveMQ, RabbitMQ 를 셈플링 해보았다.


선택은 Cluster, Mirroring 설정이 간편한 RabbitMQ!!


[Cluster] https://www.rabbitmq.com/clustering.html

[Mirroring] https://www.rabbitmq.com/ha.html


그대로 따라 하기만 하면 잘 된다.


[설치]

esl-erlang_17.5-1~centos~6_amd64.rpm

rabbitmq-server-3.5.1-1.noarch.rpm


우선 서버당 /var/lib/rabbitmq/.erlang.cookie 값을 동일 하게 일치 시킨다.


하지만 난 하나의 물리 서버에 MQ서버 NODE 두개씩 올려보았다.


[rabbitmq@dev01]

   - mq11 - /usr/local/mq/domain/mq11

   - mq12 - /usr/local/mq/domain/mq12

[rabbitmq@dev02]

   - mq21 - /usr/local/mq/domain/mq21

   - mq22 - /usr/local/mq/domain/mq21



관리를 편리하게 하기 위해서 NODE 기반 명령어 스크립트를 만들어 둔다.

env.sh 에서 RabbitMQ 포트 번호 및 웹 관리 포트번호를 설정 하게 한다.


각각 디렉토리에 들어가서 각각 start.sh 를 실하면 알아서 뜬다.


env.sh - 기본 동적 설정

#!/bin/sh

NODE_PORT=각각 다르게 포트번호
NODE_MANAGEMENT_PORT=각각 다르게 포트번호(웹 관리 포트)
NODE_NAME=각각 NODE 이름


start.sh - MQ NODE 시작

#!/bin/sh

. ./env.sh


export RABBITMQ_NODE_PORT=$NODE_PORT 
export RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,$NODE_MANAGEMENT_PORT}]"
export RABBITMQ_NODENAME=$NODE_NAME
export RABBITMQ_MNESIA_DIR=/usr/local/mq/domains/$NODE_NAME/mnesia
export RABBITMQ_LOG_BASE=/usr/local/mq/domains/$NODE_NAME/logs
export RABBITMQ_CONFIG_FILE=/usr/local/mq/domains/$NODE_NAME/rabbitmq
/usr/sbin/rabbitmq-server -detached

echo "============================================================="
echo "   RabbitMQ Server 3.5.1-1.noarch           ddakker@gmail.com"
echo "-------------------------------------------------------------"
echo "RABBITMQ_NODENAME=$RABBITMQ_NODENAME"
echo "RABBITMQ_NODE_PORT=$RABBITMQ_NODE_PORT"
echo "NODE_MANAGEMENT_PORT=$NODE_MANAGEMENT_PORT"
echo "RABBITMQ_MNESIA_DIR=$RABBITMQ_MNESIA_DIR"
echo "RABBITMQ_LOG_BASE=$RABBITMQ_LOG_BASE"
echo "RABBITMQ_CONFIG_FILE=$RABBITMQ_CONFIG_FILE.config"
echo "============================================================="


stop.sh - MQ NODE 중지

#!/bin/sh

. ./env.sh

rabbitmqctl -n $NODE_NAME stop


app_start.sh - MQ NODE 관리앱 시작

#!/bin/sh

. ./env.sh

rabbitmqctl -n $NODE_NAME start_app


app_stop.sh - MQ NODE 관리앱 중지

#!/bin/sh

. ./env.sh

rabbitmqctl -n $NODE_NAME stop_app


ctl.sh - rabbitmqctl NODE 옵션 지정한 명령어

#!/bin/sh

. ./env.sh

echo "rabbitmqctl -n $NODE_NAME $1 $2 $3"
rabbitmqctl -n $NODE_NAME $1 $2 $3


status.sh - 상태 정보

#!/bin/sh

. ./env.sh

rabbitmqctl -n $NODE_NAME status
rabbitmqctl -n $NODE_NAME cluster_status


rabbitmq.config - 기본 설정 파일

[
   {mnesia, [{dump_log_write_threshold, 1000}]},
   {rabbit, [
        {log_levels, [{connection, info}]}
   ]},
   {rabbitmq_management, [
       {redirect_old_port, false}
   ]}
].


[NODE 별로 기동]

[rabbitmq@dev01 mq11]$ ./start.sh

[rabbitmq@dev01 mq12]$ ./start.sh

[rabbitmq@dev02 mq21]$ ./start.sh

[rabbitmq@dev02 mq22]$ ./start.sh


[Clustering] - mq11 을 기준으로 한다.(mq11 에서는 안 해도 됨)

[rabbitmq@dev01 mq12]$ ./app_stop.sh

[rabbitmq@dev01 mq12]$ ./ctl.sh join_cluster mq11@dev01

[rabbitmq@dev01 mq12]$ ./app_start.sh



[rabbitmq@dev02 mq21]$ ./app_stop.sh

[rabbitmq@dev02 mq21]$ ./ctl.sh join_cluster mq11@dev01

[rabbitmq@dev02 mq21]$./app_start.sh


[rabbitmq@dev02 mq22]$ ./app_stop.sh

[rabbitmq@dev02 mq22]$ ./ctl.sh join_cluster mq11@dev01

[rabbitmq@dev02 mq22]$ ./app_start.sh


[Mirroring] - Cluster 되면 아무곳에서 해도 됨

./ctl.sh set_policy 사용해도 되고, 그냥 "admin Tab" -> "Policies" 메뉴에서 UI상으로 하는게 편리!!

애래 Node 부분에 "+3" 부분이 보여야 함(정상/비정상 여부에 따라서 복제된 정보가 파랑숫자, 빨강숫자로 표현됨)

장애 후 복구된 서버에 이전데이터 동기(복제) 처리는 하지 않았음




[웹 접속 계정 생성 및 권한 부여] - Cluster 되면 아무곳에서 해도 됨

[rabbitmq@dev01 mq11]$ ./ctl.sh add_user test 1234

[rabbitmq@dev01 mq11]$ ./ctl.sh set_user_tags test administrator

그냥 "admin Tab" -> "Users" 메뉴에서 UI상으로 하는게 편리!!



앞단에 L4 를 붙이면 좋겠음








BindResult 삽질기!!!


회사 동료가 Spring Validation 기능을 활용해보자고 해서 해보는데 엄청난 시간 소모가...

결론은 BindResult 파라미터 선언을 @Valid 바로 뒤에 해야 한다는...


[TestWeb.java]

import javax.validation.Valid;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("test")
public class TestWeb {
	private static Logger log = LoggerFactory.getLogger(TestWeb.class);

	@RequestMapping(value="case1", method=RequestMethod.GET)
	public String test(@Valid TestVo testVo, BindingResult bindResult, Model model) {
		log.debug("bindResult.hasErrors(): {}", bindResult.hasErrors());
		if (bindResult.hasErrors()) {
			throw new RuntimeException("파라미터 설정이 잘 못 되었습니다..");
			//return "coupon/mainError";	// 공통 Exception 처리 Throws 하거나 에러 뷰 return
		}
		return "test/case1";
	}
	
	
	
}

[TestVo.java]

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class TestVo {
	@Size(min=0, max=5)
	private String test1;
	
	@NotNull
	private String test2;
}

[build.gradle]

dependencies {
	compile 'spring.... 4.1.x...'
	compile 'org.hibernate:hibernate-validator:5.1.3.Final'
}

최근 인터넷 곳곳에서 ORM 관련 내용들이 많이 쏟아져나오고 있다.


예전에도 Spring 과 Hibernate를 이용해서 내부 프로젝트를 진행 한적이 있었는데, 그때는 단순한것은 Criteria를 사용하고, 조금이라도 복잡한것은 HQL을 사용했던 기억인데 개념적으로 잘 이해되지 않아 초반에 힘들었던거 같은데...


이번에도 역시 많은 삽질을 해야 했다.. 개념 이해 뿐만 아니라 이래저래 많은 변화가 있었던것 같다.

Hibernate에서 JPA, Spring Data JPA, QueryDSL, Spring Data JPA & QueryDSL 등 표준이니 뭐니...


단순 셈플링이다.

아직 알지도 못하는 많은 방법들이 존재해서 뭐가 어떻게 되는건지 심도 있는 학습이 필요할 것 같다.

작은 내부 프로젝트를 진행하려고 하는데 이번 기회에 사용해봐야겠다.


[build.gradle]


apply plugin: 'java'
apply plugin: 'idea'

ext {
    javaVversion            = '1.7'
    servletVersion          = '3.0.1'
    springframeworkVersion  = '4.1.6.RELEASE'
    hibernateJpaVersion        = '1.0.1.Final'
    hibernateEntitymanagerVersion = '4.1.9.Final'
    hibernateCommonsAnnotationsVersion = '4.0.1.Final'
    hibernateValidatorVersion = '4.3.1.Final'
    aspectjVersion = '1.6.8'
    queryDslVersion = '3.2.0'
    springDataJpaVersion = '1.8.0.RELEASE'
}

List loggerSlf4jAndLogback = [
        "ch.qos.logback:logback-classic:1.0.13",
        "org.slf4j:jcl-over-slf4j:1.7.5"
]

sourceCompatibility = javaVversion
version = '1.0'

task wrapper(type: Wrapper) {
  gradleVersion = '2.3'
  distributionUrl = 'http://services.gradle.org/distributions/gradle-2.3-bin.zip'
}

repositories {
    mavenCentral()
}

dependencies {
    compile loggerSlf4jAndLogback

    compile "org.springframework:spring-webmvc:" + springframeworkVersion
    compile "org.springframework:spring-orm:" + springframeworkVersion
    compile "org.springframework:spring-aspects:" + springframeworkVersion

    compile "org.springframework.data:spring-data-jpa:" + springDataJpaVersion
    compile 'com.h2database:h2:+'
    //compile 'mysql:mysql-connector-java:5.1.23'

    compile "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:" + hibernateJpaVersion
    compile "org.hibernate:hibernate-entitymanager:" + hibernateEntitymanagerVersion
    compile "org.hibernate.common:hibernate-commons-annotations:" + hibernateCommonsAnnotationsVersion
    compile "org.hibernate:hibernate-validator:" + hibernateValidatorVersion

    compile "com.mysema.querydsl:querydsl-core:" + queryDslVersion
    compile "com.mysema.querydsl:querydsl-apt:" + queryDslVersion
    compile "com.mysema.querydsl:querydsl-jpa:" + queryDslVersion
    compile "com.mysema.querydsl:querydsl-sql:" + queryDslVersion


    compile "org.aspectj:aspectjrt:" + aspectjVersion
    compile "org.aspectj:aspectjweaver:" + aspectjVersion


    compile 'javax:javaee-api:7.0'

    //providedCompile 'javax.servlet:javax.servlet-api:3.1.0'

    testCompile group: 'junit', name: 'junit', version: '4.11'
    testCompile "org.springframework:spring-test:" + springframeworkVersion
}

sourceSets {
    generated {
        java {
            srcDirs = ['src/main/generated']
        }
    }
}

task generateQueryDSL(type: JavaCompile, group: 'build') {
    source = sourceSets.main.java
    classpath = configurations.compile
    options.compilerArgs = [
            "-proc:only",
            "-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
    ]
    destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}

compileJava {
    dependsOn generateQueryDSL
    source generateQueryDSL.destinationDir
}
compileGeneratedJava {
    dependsOn generateQueryDSL
    options.warnings = false
    classpath += sourceSets.main.runtimeClasspath
}

clean {
    delete sourceSets.generated.java.srcDirs
}

idea {
    module {
        sourceDirs += file('src/main/generated')
    }
}

[Config AppConfig.java]

package pe.kr.ddakker.jpa;

import org.h2.tools.Server;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.aspectj.EnableSpringConfigured;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.beans.PropertyVetoException;
import java.sql.SQLException;
import java.util.Properties;

/**
 * Created by ddakker on 2015-04-09.
 */
@Configuration
@ComponentScan("pe.kr.ddakker.jpa")
@EnableTransactionManagement
@EnableSpringConfigured
@EnableJpaRepositories("pe.kr.ddakker.jpa")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public DataSource testDataSource() {
        return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
    }

    @Bean
    public HibernateJpaVendorAdapter jpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws SQLException, PropertyVetoException{
        LocalContainerEntityManagerFactoryBean emf =
                new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(testDataSource());
        emf.setJpaVendorAdapter(jpaVendorAdapter());
        emf.setPersistenceUnitName("rss");
        emf.setPackagesToScan("pe.kr.ddakker.jpa.domain");
        Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        properties.put("hibernate.hbm2ddl.auto", "create-drop");
//        properties.put("hibernate.show_sql","true");
//        properties.put("hibernate.format_sql", "true");
        emf.setJpaProperties(properties);
        return  emf;
    }

    @Bean
    public JpaTransactionManager transactionManager() throws PropertyVetoException, SQLException{
        JpaTransactionManager trans = new JpaTransactionManager();
        trans.setEntityManagerFactory(entityManagerFactory().getObject());
        return trans;
    }

}

[Domain User.java]

package pe.kr.ddakker.jpa.domain;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;




@Entity
public class User implements Serializable {
    @Id
    @Column(name = "USER_ID")
    private Long id;

    @Column(name = "USER_NM")
    private String name;

    @Column(name = "USER_AGE")
    private Integer age;

    @Temporal(TemporalType.DATE)
    @Column(name = "REG_DT")
    private Date regDt;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Date getRegDt() {
        return regDt;
    }

    public void setRegDt(Date regDt) {
        this.regDt = regDt;
    }
}

[Repository UserRepository.java]

package pe.kr.ddakker.jpa.repository;

import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import pe.kr.ddakker.jpa.domain.User;

import java.util.List;

/**
 * Created by ddakker on 2015-04-09.
 */
public interface UserRepository extends CrudRepository, QueryDslPredicateExecutor {

    public List findByName(String name);
}

[Test UserRepositoryTest.java]

package pe.kr.ddakker.jpa.repository;

import com.mysema.query.jpa.impl.JPAQuery;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import pe.kr.ddakker.jpa.AppConfig;
import pe.kr.ddakker.jpa.domain.QUser;
import pe.kr.ddakker.jpa.domain.User;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Date;
import java.util.List;

import static org.junit.Assert.assertEquals;

/**
 * Created by ddakker on 2015-04-09.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppConfig.class})
@Transactional
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class UserRepositoryTest {
    @Autowired private UserRepository userRepository;

    @PersistenceContext private EntityManager entityManager;

    /**
     * Spring JPA CRUD 기능을 이용한다면
     */
    @Before
    public void create() {
        User inputUser = new User();
        inputUser.setId(System.currentTimeMillis());
        inputUser.setName("ddakker");
        inputUser.setAge(33);
        inputUser.setRegDt(new Date());
        userRepository.save(inputUser);

        inputUser = new User();
        inputUser.setId(System.currentTimeMillis());
        inputUser.setName("petitjjang");
        inputUser.setAge(33);
        inputUser.setRegDt(new Date());
        userRepository.save(inputUser);

        inputUser = new User();
        inputUser.setId(System.currentTimeMillis());
        inputUser.setName("sisigi");
        inputUser.setAge(1);
        inputUser.setRegDt(new Date());
        userRepository.save(inputUser);

        assertEquals("전체 사이즈", 3, userRepository.count());
    }

    /**
     * Method Name Query를 사용한다면
     */
    @Test
    public void testRepository_methodNameQuery() {
        List userList = userRepository.findByName("ddakker");
        assertEquals("갯수는", 1, userList.size());
    }
    /**
     * Spring Data JPA & QueryDSL Predicate 사용한다면
     */
    @Test
    public void testRepository_Predicate() {
        String name = "dda%";
        int age = 33;

        QUser user = QUser.user;
        Page page =  userRepository.findAll(user.name.like(name).and(user.age.eq(age)), new PageRequest(0,10));
        assertEquals("검색 결과", 1, page.getNumberOfElements());

        Iterable users = userRepository.findAll(user.age.eq(age));
        for (User u : users) {
            System.out.println("iterable user: " + u.getId() + ", " + u.getName() + ", " + u.getAge() + "," + u.getRegDt());
        }
    }


    /**
     * Spring Data JPA & QueryDSL 확장 기능을 이용한다면
     * @throws Exception
     */
    @Test
    public void testRepository_support() throws Exception {
        UserRepositorySupport userRepositorySupport = new UserRepositorySupport(User.class);
        userRepositorySupport.setEntityManager(entityManager);
        assertEquals("큰 나이", 33, userRepositorySupport.getMaxAge());
        assertEquals("작은 나이", 1, userRepositorySupport.getMinAge());

    }

    /**
     * 직접 QueryDSL을 써본다면..
     *      - 이것을 어느 영역에 둬야 할까...
     */
    @Test
    public void test_Dsl() {
        JPAQuery query = new JPAQuery(entityManager);
        QUser qUser = QUser.user;
        List userList = query.from(qUser).where(qUser.name.eq("ddakker")).list(qUser);

        for (User u : userList) {
            System.out.println("list user: " + u.getId() + ", " + u.getName() + ", " + u.getAge() + "," + u.getRegDt());
        }
    }
}

[Repository UserRepositorySupport.java]

package pe.kr.ddakker.jpa.repository;

import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport;
import pe.kr.ddakker.jpa.domain.QUser;
import pe.kr.ddakker.jpa.domain.User;

/**
 * Created by ddakker on 2015-04-09.
 */
public class UserRepositorySupport extends QueryDslRepositorySupport {
    public UserRepositorySupport(Class user) {
        super(user);
    }

    public int getMinAge() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.age.min());
    }

    public int getMaxAge() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.age.max());
    }
}

[참고] https://www.youtube.com/watch?v=ho0fQt8v_HA


Slack 라는 업무용 메신저를 회사 동료가 알려줘서 우선 기존 모니터링 SMS 발송 부분을 Slack 으로 부분적으로 대체 해보기로 했다.


JAVA Client API로는 여러가지가 있고, 메시지 발송용으로는 아래 두가지 아무거나 사용하면 될 것 같다.

(https://api.slack.com/community)


SimpleSlackAPI - https://github.com/Ullink/simple-slack-api

SlackSDK - https://github.com/estebanwasinger/slack-sdk


SimpleSlackAPI 의 경우는 본인 계정 Token 기반으로만 글을 남길 수 있고, 내용 이외에 스타일 구조 메시지를 넣을 수 있다.(제목, 들여 쓰기 내용 등)

SlackSDK 의 경우는 본인 계정 Token 기반으로는 인증 하되 전송자명, 전송자 이미지를 수동으로 조정 가능하다.


각각 단순 셈플링시의 차이점이며 자세한 API는 아직 잘 모름!!

하지만 이정도만 해도 현재로서는 충분함!!


[SimpleSlackAPI]

package com.ullink.slack;


import org.junit.Test;

import com.ullink.slack.simpleslackapi.SlackAttachment;
import com.ullink.slack.simpleslackapi.SlackChannel;
import com.ullink.slack.simpleslackapi.SlackSession;
import com.ullink.slack.simpleslackapi.impl.SlackChannelImpl;
import com.ullink.slack.simpleslackapi.impl.SlackSessionFactory;

public class SlasckTest {
	@Test
	public void create() {
		final SlackSession session = SlackSessionFactory.createWebSocketSlackSession("개인 계정 Token 문자열");
		
		SlackChannel channel = new SlackChannelImpl("#cms-batch", null, null, null);
		session.sendMessage(channel, "내용이여", new SlackAttachment("title", "fallback", "text", "pretext"));
	}
}


[SlackSDK]

package msg.test;

import org.junit.Test;
import org.stevew.SlackClient;

public class SlackTest {
	@Test
	public void create1() {
		SlackClient sc = new SlackClient("개인 계정 Token 문자열");
		sc.sendMessage("내용이여", "#channel-name", "전송자명(쓰는대로 됨)", "http://../logo.png");
	}
}



로그성 DB INSERT로 인해서 전체적인 DB성능 이슈가 발생하면서 어플리케이션까지 서능에 영향이 미치는 상황이 발생하였다.


처음에는 Logging 라이브러리를 활용하여 Text형태로 쌓아놓고 한가한 시간에 배치로 DB에 INSERT하려고 하다가 공부 겸 JMS(java message service)를 적용해보기로 하였다.


Message Queue 관련 방법은 여러가지가 있던데 선택은 Apache ActiveMQ 를 선택 하였다.

관련 장단점에 대해서는 인터넷 참고!!





우선 Apache ActiveMQ 설치


context-jms.xml


	



	



	



	
    




	
	
	



	
	
	
		
	


MemberMessageConverter.java

package pe.kr.ddakker.jms;

import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.Session;

import org.springframework.jms.support.converter.MessageConversionException;
import org.springframework.jms.support.converter.MessageConverter;

public class MemberMessageConverter implements MessageConverter {

	public MemberMessageConverter(){}
	
	@Override
	public Object fromMessage(Message message) throws JMSException,
			MessageConversionException {
		if(!(message instanceof MapMessage)){
			throw new MessageConversionException("Messaeg isn't a MapMessage");
		}
		
		MapMessage mapMessage = (MapMessage) message;
		Member member = new Member();
		member.setName(mapMessage.getString("name"));
		member.setEmail(mapMessage.getString("email"));
		return member;
	}

	@Override
	public Message toMessage(Object object, Session session) throws JMSException,
			MessageConversionException {
		if(!(object instanceof Member)){
			throw new MessageConversionException("Messaeg isn't a Member");
		}
		
		Member member = (Member) object;
		MapMessage message = session.createMapMessage();
		message.setString("name", member.getName());
		message.setString("email", member.getEmail());
		return message;
	}

}


ReceiverMessageListenerImpl.java

package pe.kr.ddakker.jms;

import javax.annotation.Resource;
import javax.jms.Message;
import javax.jms.MessageListener;

import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.stereotype.Component;

@Component
public class ReceiverMessageListenerImpl implements MessageListener {


	@Resource
	private MessageConverter memberConverter;

	@Override
	public void onMessage(Message message) {
		Member member = null;
		try {
			member = (Member) memberConverter.fromMessage(message);

			if (member != null) {
				System.out.println("member.getEmail(): " + member.getEmail());
			}
		} catch (Exception e) {
			System.err.println(e);
		}

	}

}


JmsTest .java

package pe.kr.ddakker.jms;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath*:config/spring/context-jms.xml"})
public class JmsTest {
	@Resource
	private JmsTemplate jmsTemplate;
	
	@Test
	public void test() {
		
		for (int i = 0; i < 5; i++) {
			Member member = new Member();
			member.setName("ddakker" + i);
			member.setEmail("ddakker" + i + "@gmail.com");
			
			jmsTemplate.convertAndSend(member);
		}
	}
}


Redis Cluster 셈플링 좀 하다보니 Cluster 기능이 Ruby 기반으로 작동하고 있네..


Ruby 를 안 해본 입장에서 설치 좀 해보려니 좀 삽질..


[환경]

CentoOs 5.9 64Bit


해당 환경에서 yum으로 설치해보니 ruby 1.8.5가 설치되는 것 같고, Gem이 포함되지 않은 버전이여서 Gem도 수동으로 설치하고.. 안되서 버전 이것저것 삽질을 하다가 RVM 발견..


아주 간단하게 설치..


[RVM 설치] http://rvm.io/

설명대로 설치

$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
$ \curl -sSL https://get.rvm.io | bash -s stable

[RUBY 설치]

$ rvm install 2.2.0

Gem 포함 설치


* Redis Cluster 구동해보려 하니 ruby 관련 redis가 필요하단다.


[RUBY Redis Lib 설치]

$ gem install redis


* /usr/bin/env: ruby: No such file or directory .. 없단다..

ruby 실행 파일을 /usr/bin/ruby 에 심볼릭 링크 걸어 해결

ln -s /usr/local/rvm/rubies/default/bin/ruby /usr/bin/ruby


Redis Cluster 실행 성공!!

VMWare 기반 Mint 해상도 설정 리스트에 존재 하지 않는 사이즈로 변경하기



$ cvt 1920 1080 엔터

--------------------------------------------------------------------------------------------------------
# 1920x1080 59.96 Hz (CVT 2.07M9) hsync: 67.16 kHz; pclk: 173.00 MHz
Modeline "1920x1080_60.00"  173.00  1920 2048 2248 2576  1080 1083 1088 1120 -hsync +vsync
--------------------------------------------------------------------------------------------------------

위에 나온 정보 입력
$ xrandr --newmode "1920x1080_60.00"  173.00  1920 2048 2248 2576  1080 1083 1088 1120 -hsync +vsync
$ xrandr
--------------------------------------------------------------------------------------------------------
Screen 0: minimum 1 x 1, current 800 x 600, maximum 8192 x 8192
Virtual1 connected 800x600+0+0 (normal left inverted right x axis y axis) 0mm x 0mm
   800x600        60.0*+   60.3 
생략
--------------------------------------------------------------------------------------------------------
$ xrandr --addmode Virtual1 "1920x1080_60.00"

# Virtuals1 부분은 xrandr 명령어 처보면 확인 가능 VGA, DVI, HDMI 등??

Gradle

[build.gradle]

dependencies {
	compile 'com.yahoo.platform.yui:yuicompressor:2.4.8'
}

apply plugin: 'js'
apply plugin: 'css'
buildscript {
    repositories {
    	jcenter()
    }
    dependencies {
       classpath 'com.eriwen:gradle-js-plugin:1.12.1'
       classpath 'com.eriwen:gradle-css-plugin:1.11.1'
    }
}

javascript.source {
	def targetDir = "$project.projectDir" + File.separatorChar + "webapp" + File.separatorChar + "resources" + File.separatorChar + "js"
    dev {
        js {
            srcDir targetDir
            include "**/*.js"
            exclude "**/*.min.js"
        }
    }
}
css.source {
	def targetDir = "$project.projectDir" + File.separatorChar + "webapp" + File.separatorChar + "resources" + File.separatorChar + "css"
    dev {
        css {
            srcDir targetDir
            include "**/*.css"
            exclude "**/*.min.css"
        }
    }
}

javascript.source.dev.js.files.eachWithIndex { jsFile, i ->
	tasks.create(name: "minifyJs${i}", type: com.eriwen.gradle.js.tasks.MinifyJsTask) {
		source 	= jsFile
		dest 	= jsFile
		//dest = jsFile.getAbsolutePath().replace('.js', '.min.js')
	}
}
task allMinifyJs(dependsOn: tasks.matching { Task task ->
		if( "$targetServer" != "local" ){
			task.name.startsWith("minifyJs")
		} else {
			println '로컬에서는 minify 하지 않음'
		}
	}
)

css.source.dev.css.files.eachWithIndex { cssFile, i ->
	tasks.create(name: "minifyCss${i}", type: com.eriwen.gradle.css.tasks.MinifyCssTask) {
		source 	= cssFile
		dest 	= cssFile
    	//dest = cssFile.getAbsolutePath().replace('.css','.min.css')
    	yuicompressor {
    		lineBreakPos = -1
    	}
    }
}
task allMinifyCss(dependsOn: tasks.matching { Task task ->
		if ( "$targetServer" != "local" ){
			task.name.startsWith("minifyCss")
		} else {
			println '로컬에서는 minify 하지 않음'
		}
	}
)



Ant

https://github.com/yui/yuicompressor

[build.xml]

	<path id="yui.classpath">
		<fileset dir="./lib">
			<include name="ant-yui-compressor-taskdef-1.0.jar" />
			<include name="yuicompressor-2.4.8.jar" />
		</fileset>
	</path>

	<taskdef resource="yuicompressor.tasks" classpathref="yui.classpath" />

	<target name="compress" description="Compress JS files using YUI Compressor">
		<!-- Compress JS files using YUI Compressor -->
		<ant-yui-compressor todir="./webapp/resources/js" deleteOriginal="false">
			<fileset dir="./webapp/resources/js">
				<include name="**/*.js" />
				<include name="**/*.min.js" />
				<exclude name="ez-common.js" />
				<exclude name="IBSheetPro3/IBSheetInfo_UTF8.js" />
			</fileset>
			<mapper type="glob" from="*.js" to="*.js" />
		</ant-yui-compressor>

		<!-- Compress CSS files using YUI Compressor -->
		<ant-yui-compressor todir="./webapp/resources/css" deleteOriginal="false">
			<fileset dir="./webapp/resources/css">
				<include name="**/*.css" />
				<include name="**/*.min.css" />
			</fileset>
			<mapper type="glob" from="*.css" to="*.css" />
		</ant-yui-compressor>
	</target>


web.xml 의 Filter 부분을 동적으로 처리해야 할 일이 생겼다.


Spring 3.1 이상을 사용하게 되면 org.springframework.web.WebApplicationInitializer 를 이용하게되면 더 심플하겠지만 현재 상황에서는 Spring 2.5 환경이여서 불가능!!


Servlet 3.x (javax.servlet.ServletContainerInitializer) 만으로 처리해보자.



유의할 점은 Tomcat 8.x 에서는 아래와 같이 그대로 해도 정상적으로 작동하지만 JBoss와 같이 Java EE 환경에서는 

jar cvf webxml.jar META-INF/services/javax.servlet.ServletContainerInitializer pe.kr.ddakker.WebXml.class

와 같이 jar 생성 후 WEB-INF/lib 하위에 위치 시켜야 정상 작동합니다.


build.gradle

dependencies {
	providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}


WebXml.java

package pe.kr.ddakker;

import java.util.EnumSet;
import java.util.Set;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;

import pe.kr.ddakker.filter.TestFilter;

public class WebXml implements ServletContainerInitializer {

	@Override
	public void onStartup(Set> c, ServletContext servletContext) throws ServletException {
		System.out.println("ServletContainerInitializer");


		TestFilter testFilter = new TestFilter();
        FilterRegistration.Dynamic test = servletContext.addFilter("testFilter", testFilter);
        test.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");

	}
}


TestFilter.java

package pe.kr.ddakker.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class TestFilter implements Filter {

    public TestFilter() {
    }

	public void destroy() {
	}

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		// pass the request along the filter chain
		System.out.println("TestFilter S");
		chain.doFilter(request, response);
		System.out.println("TestFilter E");
	}

	public void init(FilterConfig fConfig) throws ServletException {
	}

}


classpath 하위 META-INF/services/javax.servlet.ServletContainerInitializer

pe.kr.ddakker.WebXml


Redis 를 활용해서 Cache를 적용해보자..


우선 Redis Client는 http://www.redis.io/clients 에서 확인되는바와 같이 종류가 여러가지다.

그런데 Spring Data Redis 를 사용하면 별도의 Client가 필요 없네...(Interface만 있고 구현체는 가져다 쓰는줄 알았더니 아니네요.)


Source Code에서 사용법과 Method Annotataion 사용법을 보자.


[build.gradle]

dependencies {
	compile springDependencies // Spring 3.2.x
	compile 'org.springframework.data:spring-data-redis:1.3.4.RELEASE'
}


[context-cache.xml]



 
	
 
	
		
		
		
	
 
	
		
		
		
	
 
	
		
	
	
	
	
		
	
 
	
		
		
	    
	    
	    
		    
				
				
			
		
		
		
			
	            BANNER
	            EVENT
	        
		
	
 
	
	    
	    
	
 
	
	    
	    
	
 


[RedisTestCase.java]

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
		"classpath*:config/spring/context-common.xml",
		"classpath*:config/spring/context-cache.xml",
		"classpath*:config/spring/context-datasource.xml"
})
public class RedisTestCase {
    @Resource Cache cacheEvent;
    @Resource Cache cacheBanner;
 
    @Resource AuthService authService; 
 
    @Test
    public void test() {
    	User user = new User();
		user.setUserNm("ddakker");
	 
		cacheEvent.put("a", "zzz");
		String a = cacheEvent.get("a");
		System.out.println("a: " + a);
	 
		cacheBanner.put("user", user);
		User resultUser = cacheBanner.get("user");
		System.out.println("user: " + resultUser);
    }
 
}
 
@Test
public void testAnnotation() {
    // DB 쿼리 호출
    List authList = authService.getList();
    System.out.println("authList: " + authList);
 
    // DB 쿼리 호출 않음
    authList = authService.getList();
    System.out.println("authList: " + authList);
 
    Auth auth = new Auth();
    auth.setSearchKey("AUTH_CD");
    auth.setSearchKwd("ROLE_USER");
 
    // DB 쿼리 호출
    authList = authService.getList(auth);
    System.out.println("authList user: " + authList);
 
    // DB 쿼리 호출 않음
    authList = authService.getList(auth);
    System.out.println("authList user: " + authList);
}


[AuthService.java]

// @Cacheable 의 key나 조건의 디테일한 제어에 대한 설멍은 - http://dev.anyframejava.org/docs/anyframe/plugin/optional/cache/1.0.3/reference/html/ch01.html
@Service
public class AuthService {
	@javax.annotation.Resource AuthMapper authMapper;
 
	@Cacheable(value="BANNER")
	public List getList() {
		return authMapper.getList(new Auth());
	}
 
	@Cacheable(value="EVENT")
	public List getList(Auth authVo) {
		return authMapper.getList(authVo);
	}
}

+ Recent posts