개발은 언제나 즐겁다.

V20 쿼드덱 좋네~

후비고!

개발폰겸 음악도 들을겸 구입한 V20. 첨에 그냥 저가형 커널 이어폰 끼고 V20의 쿼드덱을 켜고 들으니 오잉! 음질 괜찮네.

음..그래서 집에서 굴러다니던 구매후 아내에게 대따 혼났던 오픈형 이어폰을 수리하기로 맘먹고 수리.. 오 소리가 더 좋아!!

근데...문제는 서비스센터에서 샘플로 놓여있던 Bose헤드폰의 음질이 더 좋았던..아 또 혼날 각오하고 질러야 하나..끙.



살까..살까..살까...




상품이미지

네이버 카페 자동 댓글 남기기

후비고!

아내가 특정 카페들에서 댓글을 빨리 달면 저렴하게 물건을 살 수 있다고 신나 하다가...oTL


아무리 해도 "새로고침->새글확인->원하는상품명의경우->클릭->댓글폼->댓글->저장" 이 사이클에서 자신은 결코 빠르게 댓글을 달 수 없다고한다. 즉, 자기가 댓글을 남기면 이미 20개가 넘는 댓글이 남겨져 있다고... 그러면서  본인 손은 저주받은 손이고... 아이패드로 너무 새로고침을 했더니 지문이 없어졌단다. ㅡㅡ;


대략 보니 이대로면 아내의 스트레스 지수가 높아지고 그건 곧 가정의 안녕이 파괴되는 일이라고 판단. 

"새로고침->새글확인->원하는상품명의경우->클릭->댓글폼->댓글->저장" 이 사이클을 자동화 해야겠다는 생각이 들었다.

 

어떤식으로 풀까?

- swt(eclipse rcp)로 해볼까..음 이건 해본지 너무 오래되서 아무것도 생각이 안나 포기.

- 스프링웹이나 부트 + 쿼츠로 같은걸로 해볼까... 음 이건 naver api도 연동해야 해서 포기.

- 음..아하! Chrome Extension이 있구나!!! 




그래서 Chrome Extension 으로 작업을 시작.


첫번째로 workspace 폴더를 만든다. Path 구조는 다음과 같다.


"/workspaces/chrome-ext/naver_cafe_mywife"


나의 계획을 만들기위한 Chrome Extension의 기본 파일을 생성한다.




background.js : 백그라운드에서 사용될 javascript로 여기서는 사용하지 않음.

contents.js : 카페 글의 목록 분석과 댓글 남기기의 실제 javascript

icon.png : 크롬 상단에 표시될 아이콘 이미지

manifest.json : 설정 정보가 담긴 json 파일.

popup.html : 확장 프로그램 gui(?)

reload.js : 초기 popup.html과 contents.js의 연계를 위한 javascript



위 파일중 가장 중요하기 기본이 되는 설정정보 json 파일은 다음과 같다.

manifest.json

{
"manifest_version": 2,

"name": "naver_cafe_mywife_chromeExtension",
"description": "아내의 스트레스를 줄여주는 확장 프로그램",
"version": "1.0.0",

"browser_action": {
"default_icon": "icon.png",
"default_popup": "popup.html"
},

"background": {
"scripts": ["background.js"]
},

"permissions": [
...
]
}

permissions 은 알맞게 추가 하면된다.


위 부분에서 browser_action을 보면 된다. icon을 지정하고 icon을 클릭하면 popup.html이 뜨도록 한 것이다.

popup.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
BODY {width : 300px; min-height:250px;}
span .label-txt {width : 150px;}
</style>
</head>
<body>
GUI
</body>
</html>


실행 화면 아래 화면과 같다.


이후 각종 입력받을 폼을 생성하고 reload.js를 로드 하도록 한다.

..
<body>
<div class="label-txt">내카페별명 : </div><input id="myName" value=""/><br/><br/>
<div class="label-txt">매칭검색어 : </div><input id="wantMatchKeyword" value=""/><br/><br/>
<div class="label-txt">댓글템플릿 : </div><input id="commentText" value=""/><br/><br/>
<div class="label-txt">리로드타임 : </div><input id="intervalSec" value=""/><br/><br/>
<div class="label-txt">마지막게시 : </div><input id="lastArticleId" value=""/><br/><br/>
<button class=".btn">Run!</button>
<script src="reload.js"></script>
</body>
..


자~ 다시 보자.


이제 'Run!' 버튼의 이벤트를 reload.js에 추가한다.

function actionMethod(){
chrome.tabs.executeScript(null,
{
code: "넘길변수등을지정한다;"
},
function(){
chrome.tabs.executeScript(null,{file: "contents.js"});
}
);
}

document.querySelector('button').addEventListener('click', function () {
// 입력받은 각종값들 변수 처리.
....
// 입력받은 리로드 타임에 맞춰 자동 리로드
setInterval(actionMethod::실행할함수명, 리로드타임*1000);
});


그럼 버튼 클릭시 actionMethod()가 실행되고 활성화 되어 있는 tabs의 페이지에 contents.js가 주입된다.

대략 감이 오겠지만, "활성화 되어 있는 tabs의 페이지에 contents.js가 주입" 이게 포인트다.


즉, 카페의 목록 페이지에서 contents.js를 주입시켰으니 마치 내가 만든 페이지 처럼 제어 할 수 있다는 것이다. 

가령 글의 아이디를 뽑아내고 싶다면 이렇게 넣으면 된다.

var readLastArticleId = document.querySelector("#itemList li a").getAttribute("data-article-id").trim();


얼마나 쉬운가~?


https://developer.mozilla.org/ko/docs/Web/JavaScript

https://developer.chrome.com/extensions/getstarted


이렇게 해서 아내가 몇개의 신발을 아주 저렴하게 사더니.. 얼굴에 화색이 돌기 시작했다.ㅎㅎ


Slf4j Log Marker를 이용한 로그 분리

Programming!

좀 오래된 사항이기는 한데.. 여튼


O2O의 예약 관련 사항을 작업하던중 처음 단일 벤더에서, 멀티 벤더를 허용하게 된다. (익xx디아, 핏x즈 등등)


기존에는 file appender를 하나로 처리해서 예약 관련 로그를 file로 남겼는데, 벤더사가 늘어나다 보니 좀 분리할 필요가 생겼다.


해서 기존 logback-spring.xml에 Marker와 Filter를 적용하여 로그파일을 분리 시킴.


각 Filter에 대한 설명은 여기서 확인하면 된다.

https://logback.qos.ch/manual/filters.html


기존 logback-spring.xml


...

    <springProfile name="stage, prod, production">

    <logger name="com.tistory.eclipse4j" level="INFO" />

        <include resource="file-appender-vendor1.xml" />

        <include resource="file-appender-vendor2.xml" />

        <include resource="file-appender.xml" />


        <root level="INFO">

            <appender-ref ref="CONSOLE" />

            <appender-ref ref="FILE" />

            <appender-ref ref="vendor1-file" />

            <appender-ref ref="vendor2-file" />

        </root>

    </springProfile>

...


file-appender-expedia.xml

<?xml version="1.0" encoding="UTF-8"?>

<included>

<appender name="expedia-file"

class="ch.qos.logback.core.rolling.RollingFileAppender">

<filter class="ch.qos.logback.core.filter.EvaluatorFilter"> 

<evaluator class="ch.qos.logback.classic.boolex.OnMarkerEvaluator"> 

<marker>VENDOR1</marker> 

</evaluator> 

<OnMatch>ACCEPT</OnMatch>

            <OnMismatch>DENY</OnMismatch>

</filter> 

<encoder>

<pattern>${FILE_LOG_PATTERN}</pattern>

</encoder>

<file>logs/spring-expedia.log</file>

<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">

<fileNamePattern>VENDOR1_${LOG_FILE}.%i</fileNamePattern>

</rollingPolicy>

<triggeringPolicy

class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">

<MaxFileSize>100MB</MaxFileSize>

</triggeringPolicy>

</appender>

</included>



MyLogMarker.java

public class MyLogMarker {

    public static Marker VENDOR1 = MarkerFactory.getMarker("VENDOR1");

    public static Marker VENDOR2 = MarkerFactory.getMarker("VENDOR2");

}


Log남기기 

log.info(MyLogMarker.VENDOR1, "이 로그는 {} 로그입니다.", "Vendor1");



eclipse(sts)에서 m2 repository 따로 지정하기

Programming!

어느 순간 잘 사용하던 플젝이 build path오류를 내기 시작함.


음.. 누군가 설정을 변경했을 거 같고, IntelliJ를 사용하는 다른 개발자들은 문제없이 사용하는 거보니 뭔가 설정 자체가 IntelliJ에 맞춰진듯 하다.


오류 내용을 보니 build libs를 못찾고있다. 아마 eclipse 의 maven local repository path 문제일꺼라 생각하고 eclipse에서 Maven설정을 들여다 봄.



음 역시 Local Repository가 문제였다. 어느 개발자분이 settings.xml을 프로젝트 내에 두고, repository도 프로젝트 내에 둔 것이다.

문제가된 settings.xml은 {PROJECT_HOME}/.m2/ 여기에 있었다.

<settings>
    <localRepository>.m2/repository</localRepository>
</settings>


위와 같이 .m2/repository로 로컬 저장소를 지정하니까 전혀 엉뚱한 곳으로 path가 잡히고 있다. 된장..

해서 절대 경로로 바꿔 주려다가 해당 settings.xml은 팀 공유 파일이라 별도 settings.xml을 만들기로 결정함.


위 이미지에서는 "Users/Grissom/.m2/settings.xml"이지만 해당 xml은 global 설정이니 건드리지 말고 ProjectHome이나 또는 "Users/Grissom/.m2/settings.myproject.xml"을 하나 더 만들어서 아래와 같이 localRepository 를 절대경로로 지정해준다.

<settings>
    <localRepository>/Users/Grissom/Development/workspaces/java/myproject/.m2/repository</localRepository>
</settings>


이렇게 하니 정상적으로 build path 가 잡힌다. 뭐 eclipse 의 문제이기는 하지만... 플젝별로 localRepository이 필요할까도 싶고.. 플젝별로 한다면 빌드 실행시 옵션처리를 하는 게 좋지 않았을까 싶기도 하고.. 이렇게.

<settings>
    <localRepository>{project.home.root}/.m2/repository</localRepository>
</settings>

여튼 성공.

Spring Boot - Kafka 연계

Programming!

Kafka Zookeeper 설치


1. Docker-Compose 설치

https://docs.docker.com/compose/install/


2. kafka-docker 설치

$ git clone https://github.com/wurstmeister/kafka-docker


3. 설정 수정

$ vi docker-compose.yml

KAFKA_ADVERTISED_HOST_NAME : {본인ip}



4. 도커실행

$ docker-compose up -d


5. 들어가서 확인해보기

$ docker ps -a

CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS                                                NAMES
e391cac38357        kafkadocker_kafka        "start-kafka.sh"         27 hours ago        Up 27 hours         0.0.0.0:32769->9092/tcp                              kafkadocker_kafka_1


$ docker exec -it e391cac38357 /bin/bash


// 컨테이너 진입 - Topic 목록 보기

$ bash-4.3# kafka-topics.sh --list --zookeeper zookeeper



Spring Boot - Kafka 연계


1. 토픽만들기

bash-4.3# kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 1 --topic mytopic



2. Spring Boot Configuration 설정

buildscript {
    ext {
        springBootVersion = '1.5.9.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

group = 'com.tistory.eclipse4j'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.kafka:spring-kafka')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}


@EnableKafka
@Configuration
public class KafkaConfiguration {

    @Value(value = "${kafka.bootstrapAddress}")
    private String bootstrapAddress;

    public ProducerFactory<String, MyTopic> topicProducerFactory() {
        Map<String, Object> configProps = new HashMap<>();
        configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
        return new DefaultKafkaProducerFactory<>(configProps);
    }

    @Bean
    public KafkaTemplate<String, MyTopic> topicKafkaTemplate() {
        return new KafkaTemplate<>(topicProducerFactory());
    }

    public ConsumerFactory<String, MyTopic> topicListenerFactory() {
        Map<String, Object> props = new HashMap<>();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "mytopic");
        return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(),
                new JsonDeserializer<>(MyTopic.class));
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory<String, MyTopic> topicKafkaListenerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, MyTopic> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(topicListenerFactory());
        return factory;
    }
}


3. Sender 만들기

@Component
public class MyTopicKafkaSender {

    @Autowired
    private KafkaTemplate<String, MyTopic> topicKafkaTemplate;

    public void send(String topic, MyTopic payload) {
        log.info("Send : Key={}, Topic ==================== > {}", topic, payload);
        topicKafkaTemplate.send(topic, payload);
    }
}


4. Listener 만들기

@Slf4j
@Component
public class MyTopicKafkaListener {

    private CountDownLatch latch = new CountDownLatch(1);

    public CountDownLatch getLatch() {
        return latch;
    }

    @KafkaListener(topics = "mytopic", containerFactory = "topicKafkaListenerFactory")
    public void reciver(MyTopic topic) {
        log.info("MyTopic => {}", topic);
    }
}


5. Test 돌려보기

@RunWith(SpringRunner.class)
@SpringBootTest
public class KafkaSenderSpringTest {

    @Autowired
    private MyTopicKafkaSender sender;
    @Autowired
    private MyTopicKafkaListener listener;
   
    @Test
    public void testSend() throws Exception {
        MyTopic myTopic = new MyTopic();
        myTopic.setKey("TopicKey");
        myTopic.setData("Data Topic");
        sender.send("mytopic", myTopic);
        listener.getLatch().await(10000, TimeUnit.MILLISECONDS);
    }

}


6. 결과

2017-12-05 00:09:34.810  INFO 8684 --- [ntainer#0-0-C-1] c.t.e.service.MyTopicKafkaListener       : MyTopic => MyTopic(key=TopicKey, data=Data Topic)


Git : https://github.com/eclipse4j/spring-boot-kafka