728x90

자바라 쓰고 Springframework를 공부한다.

Spring의 주요 3가지 컨셉은 아래와 같습니다.

1) IOC : 제어의 역전 / 의존성 주입
2) AOP : 관심의 분리
3) PSA : 일관성 있는 추상화

그럼 실제로 어떻게 사용될까?

IOC : 제어의 역전 / 의존성 주입 사용 예제

Bean은 IoC 컨테이너 안에 등록된 객체들을 의미 합니다.
모든 클래스의 객체가 Bean으로 등록되지 않습니다. @Repository, @Component, @Service, @Bean 등의 어노테이션을 통해서 Bean으로 등록 가능합니다.

사용 예로는 @Autowired 어노테이션을 이용해서 Bean을 주입할 수 있습니다.
Bean 주입이 주는 이점은, 객체의 관리를 스프링 컨테이너가 하기에 개발자가 언제 빈을 생성하고 소멸시킬지 신경쓰지 않아도 됩니다.

AOP : 관심의 분리 예제

가장 큰 예로 @Transctional을 이야기 할 수 있습니다.
connection에 대하여 rollback, commit등을 신경쓰지 않고 처리 하게 됩니다.
결국 트렌젝션 처리는 위임하고 구현 기능에 집중하는 코드를 작성하면 됩니다.

PSA : 일관성 있는 추상화

Service Abstraction으로 제공되는 기술을 다른 기술 스택으로 간편하게 바꿀 수 있는 확장성을 갖고 있는 것이 Portable Service Abstraction. 줄여서 PSA라고 합니다.
예로는 서블릿을 직접 사용하는 것이 아니라, Controller의 @GetMapping이나 @PostMapping을 통해 특정 url로 요청이 들어왔을 때, 해당 블록이 요청을 처리하도록 구현 되어 있습니다.
이렇게 추상화 계층을 사용해 어떤 기술을 내부에 숨기고 개발자에게 편의성을 제공하는 것을 Service Abstraction이라고 합니다.

스프링은 MVC라는 추상화 기법을 사용. Spring Web MVC를 사용하면 서블릿을 직접 구현할 필요가 없습니다.

참고 자료

Posted by lahuman

댓글을 달아 주세요

728x90

couchDB의 변경내역이 발생할 경우 kafka로 해당 데이터를 전송해야 하는 연계성 요청이 들어왔습니다.

여러 가지 방안을 고민하였으나, 확장성과 유연성을 위해서 logstash를 선택하였습니다.

실제로 가능한지 테스트 하기 위해서 개발 환경을 docker 기반으로 구성하여 테스트 했습니다.

시작하기 전에

docker 간의 통신을 위해서 docker network를 추가 합니다.

myHome 이름으로 네트워크를 생성 합니다.

$ docker network create myHome

자세한 docker network 설명은 Docker 네트워크 사용법을 참조하셔요.

couchdb 설정

couchdb는 docker에서 bitnami 에서 제공하는 이미지로 생성하였습니다.

다음 명령어로 image를 다운받고 실행합니다.

$ docker run --network myHome  -p 5984:5984 --name  couchdb bitnami/couchdb:latest

추후 테스트를 위해서 5984 port를 연결합니다.

docker가 온전하게 기동되었다면, http://localhost:5984 에 접근하면 아래와 같은 내용을 확인 할 수 있습니다.

{
  "couchdb": "Welcome",
  "version": "3.1.1",
  "git_sha": "ce596c65d",
  "uuid": "b7449ad8eda14515e4e0782a84c41f5d",
  "features": [
    "access-ready",
    "partitioned",
    "pluggable-storage-engines",
    "reshard",
    "scheduler"
  ],
  "vendor": {
    "name": "The Apache Software Foundation"
  }
}

kafka 설정

kafka의 경우 기본적으로 zookeeper가 필요 하게 됩니다.
이를 쉽게 처리 하기 위해서 docker-compose를 이용합니다.

docker-compose.yml 파일을 생성합니다.

version: "2"

networks:
  default:
    external: true
    name: myHome

services:
  zookeeper:
    image: docker.io/bitnami/zookeeper:3
    ports:
      - "2181:2181"
    volumes:
      - "zookeeper_data:/bitnami"
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes
  kafka:
    image: docker.io/bitnami/kafka:2
    ports:
      - "9092:9092"
    volumes:
      - "kafka_data:/bitnami"
    environment:
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
      - ALLOW_PLAINTEXT_LISTENER=yes
    depends_on:
      - zookeeper

volumes:
  zookeeper_data:
    driver: local
  kafka_data:
    driver: local

해당 파일의 생성이 완료 되었다면, 다음 명령어로 기동 합니다.

$ docker-compose up

기동이 문제 없이 되었다면 아래와 같은 로그가 확인 됩니다.

kafka_1      | [2021-05-17 03:39:13,937] INFO [GroupMetadataManager brokerId=1001] Finished loading offsets and group metadata from __consumer_offsets-48 in 103 milliseconds, of which 102 milliseconds was spent in the scheduler. (kafka.coordinator.group.GroupMetadataManager)

logstash 설정

먼저 pipline 디렉토리를 생성한 후 logstash.conf 파일을 생성합니다.

$ mkdir pipline
$ vi logstash.conf
# logstash.conf 파일 내용
input {
  couchdb_changes {
    id => "my_plugin_id"
    host => "couchdb"
    username => "admin"
    password => "couchdb"
    port => "5984"
    db => "test"
  }
}

output {
    stdout { codec => "rubydebug" }

     kafka {
       codec => "json"
       topic_id => "test_topic"
       bootstrap_servers => "logstash_kafka_1:9092"
     }
}

주요 정보는 input과 output에 대한 정의 이며, 이후 db 종류가 많이 생기면, input에 추가 하면 됩니다.
kafka 나, couchdb의 host는 docker ps 정보에서 이름을 활용하면 됩니다.

$ docker ps
CONTAINER ID   IMAGE                                            COMMAND                  CREATED          STATUS          PORTS                                                                     NAMES
03bb311bf431   docker.elastic.co/logstash/logstash-oss:7.12.1   "/usr/local/bin/dock…"   50 seconds ago   Up 48 seconds   5044/tcp, 9600/tcp                                                        compassionate_yonath
28a0315d2422   bitnami/kafka:2                                  "/opt/bitnami/script…"   4 days ago       Up 4 minutes    0.0.0.0:9092->9092/tcp, :::9092->9092/tcp                                 logstash_kafka_1
fa1b5e62784b   bitnami/zookeeper:3                              "/opt/bitnami/script…"   4 days ago       Up 4 minutes    2888/tcp, 3888/tcp, 0.0.0.0:2181->2181/tcp, :::2181->2181/tcp, 8080/tcp   logstash_zookeeper_1
786c603b9ae2   bitnami/couchdb:latest                           "/opt/bitnami/script…"   4 days ago       Up 3 hours      4369/tcp, 9100/tcp, 0.0.0.0:5984->5984/tcp, :::5984->5984/tcp             couchdb

준비가 다 되었다면, pipeline 디렉토리 주소를 알맞게 설정후 다음 명령어로 docker 를 기동 합니다.

$ docker run --rm -it -v /Users/admin/DEV/test/logstash/pipeline/:/usr/share/logstash/pipeline/ --network myHome  docker.elastic.co/logstash/logstash-oss:7.12.1

테스트

couchdb의 utils를 이용해서 데이터를 적제 할 수 있습니다.

접근 주소 : http://localhost:5984/_utils/

해당 주소에 접근하여 test database를 생성하고 documents를 생성합니다.

데이터를 생성 하였으면, kafka에 제대로 적재 되었는지 확인해봅니다.

docker exec 명령어를 이용해서 kafka 내부로 접근합니다.

$ docker exec -it  logstash_kafka_1 /bin/bash

첫번째로 topic의 목록을 조회 해봅니다.

$ kafka-topics.sh --list --bootstrap-server localhost:9092
__consumer_offsets
mytopic
test_topic

logstash를 이용하여 생성한 test_topic 이 확인되었다면, 데이터도 console로 확인해봅니다.

$ kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test_topic --from-beginning
{"@timestamp":"2021-05-12T09:22:28.111Z","doc_as_upsert":true,"doc":{"test":"test"},"@version":"1"}
{"@timestamp":"2021-05-17T00:35:33.135Z","doc_as_upsert":true,"doc":{"helo":"kafka"},"@version":"1"}

이로서 연동 테스트가 완료 되었습니다.

참고 자료

Posted by lahuman

댓글을 달아 주세요

728x90

프로젝트 내에 여러 profile이 있을 경우 Mavne 빌드시 하나를 선택해야 합니다.

Maven 프로젝트의 Root 디렉토리에서 다음 명령어로 packaging 을 하면 TEST 코드에 대하여 profile을 적용하게 됩니다.

# dev profile을 사용하여 packaging 진행
./mvnw clean package -Dspring.profiles.active=dev

이후 결과 jar 파일을 실행시 profile을 설정하는 것도 동일 합니다.

# dev profile을 사용하여 packaging 진행
java -jar -Dspring.profiles.active=dev result.jar 
Posted by lahuman

댓글을 달아 주세요

728x90

회사내에서 배치 작업을 잠시 하게 되었습니다. 오랜만에 자바를 사용해보는거라 재미있네요. 스프링 설정은 이젠 javaconfig 만으로 웬만한 설정은 다 할수 있습니다.

추가 정보 : 스프링 배치에 대한 문서는 토리맘님의 한글 라이즈 프로젝트에 한글로 번역된 문서가 있습니다.

spring initializr를 활용한 프로젝트 생성

SpringBatch의 초기 프로젝트를 spring initializr에서 생성 합니다.

제가 준 옵션은 아래와 같습니다.

Gradle을 사용하고, Jdk는 1.8을 지정하였습니다.
Mysql은 Spring Batch의 Meta데이터를 저장하는 용도이며, H2 DB는 개발용으로, Oracle는 배치 데이터의 조회 & 적재용으로 사용하게 됩니다.
개발툴은 intellij 기준으로 진행합니다.

초기 환경 구성 진행

  1. spring initializr 에서 프로젝트 정보를 입력하고 생성 & 다운로드합니다.

  2. 다운로드한 파일의 압축을 풉니다.

  3. intellij에서 open 을 선택하고 압축 푼 디렉토리를 선택합니다.

  4. Gradle에서 관련 모듈 다운로드가 완료되기를 기다립니다.

Spring Batch 설정

기본적인 Spring Batch에 대한 내용은 Spring Batch Introduction을 읽어보시기를 권장합니다.

Spring Batch는 Job, Step, 사용자가 개발하는 처리 유닛으로 나누어져 있습니다. 이렇게 구현함으로 다음과 같은 장점을 얻을 수 있습니다.

  • 명확한 관심사 분리
  • 인터페이스로 제공하는 명확한 아키텍처 레이어와 서비스
  • 빠르게 적용하고 쉽게 응용할 수 있는 간단한 디폴트 구현체
  • 크게 향상된 확장성

Creating a Batch Service를 기준으로 코드를 작성 할 예정입니다.

src/main/resources/sample-data.cvs에 아래 내용을 작성하고 저장합니다.

Jill,Doe
Joe,Doe
Justin,Doe
Jane,Doe
John,Doe

데이터가 적재될 table을 생성하는 스크립트를 src/main/resources/schema-all.sql에 아래 내용을 작성하고 저장합니다.

DROP TABLE people IF EXISTS;

CREATE TABLE people  (
    person_id BIGINT IDENTITY NOT NULL PRIMARY KEY,
    first_name VARCHAR(20),
    last_name VARCHAR(20)
);

Spring Boot는 schema-@@platform@@.sql 파일을 시작시 실행합니다. -all은 모든 플랫폼을 의미합니다.

저장 데이터 구조가 되는 Person.java를 생성합니다.

package com.gsshop.batch.work;

public class Person {

    private String lastName;
    private String firstName;

    public Person() {
    }

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "firstName: " + firstName + ", lastName: " + lastName;
    }

}

데이터를 받아서 대문자로 처리 하는 PersonItemProcessor을 생성합니다.

package com.gsshop.batch.work;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.batch.item.ItemProcessor;

public class PersonItemProcessor implements ItemProcessor<Person, Person> {

    private static final Logger log = LoggerFactory.getLogger(PersonItemProcessor.class);

    @Override
    public Person process(final Person person) throws Exception {
        final String firstName = person.getFirstName().toUpperCase();
        final String lastName = person.getLastName().toUpperCase();

        final Person transformedPerson = new Person(firstName, lastName);

        log.info("Converting (" + person + ") into (" + transformedPerson + ")");

        return transformedPerson;
    }

}

작업에 대한 처리가 완료 되었을대 확인이 가능한 리스너를 제작합니다.
해당 리스너의 역할은 단순히 작업이 완료 되었을 경우 저장된 데이터를 출력하게 됩니다.

package com.gsshop.batch.work;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

@Component
public class JobCompletionNotificationListener extends JobExecutionListenerSupport {

    private static final Logger log = LoggerFactory.getLogger(JobCompletionNotificationListener.class);

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JobCompletionNotificationListener(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if(jobExecution.getStatus() == BatchStatus.COMPLETED) {
            log.info("!!! JOB FINISHED! Time to verify the results");

            jdbcTemplate.query("SELECT first_name, last_name FROM people",
                    (rs, row) -> new Person(
                            rs.getString(1),
                            rs.getString(2))
            ).forEach(person -> log.info("Found <" + person + "> in the database."));
        }
    }
}

Batch의 시작점인 BatchConfiguration을 작성합니다. 해당 내용에는 reader, processor, writer, 조립 설정 등이 포함 됩니다.

package com.gsshop.batch.work;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import javax.sql.DataSource;

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {
    @Autowired
    public JobBuilderFactory jobBuilderFactory;
    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    @Bean
    public FlatFileItemReader<Person> reader() {
        return new FlatFileItemReaderBuilder<Person>()
                .name("personItemReader")
                .resource(new ClassPathResource("sample-data.csv"))
                .delimited()
                .names(new String[]{"firstName", "lastName"})
                .fieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{
                    setTargetType(Person.class);
                }})
                .build();
    }

    @Bean
    public PersonItemProcessor processor() {
        return new PersonItemProcessor();
    }

    @Bean
    public JdbcBatchItemWriter<Person> writer(DataSource dataSource) {
        return new JdbcBatchItemWriterBuilder<Person>()
                .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
                .sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
                .dataSource(dataSource)
                .build();
    }

    @Bean
    public Job importUserJob(JobCompletionNotificationListener listener, Step step1) {
        return jobBuilderFactory.get("importUserJob")
                .incrementer(new RunIdIncrementer())
                .listener(listener)
                .flow(step1)
                .end()
                .build();
    }

    @Bean
    public Step step1(JdbcBatchItemWriter<Person> writer) {
        return stepBuilderFactory.get("step1")
                .<Person, Person> chunk(10)
                .reader(reader())
                .processor(processor())
                .writer(writer)
                .build();
    }
}

여기까지 설정하고 실행하면 잘 동작하는 것을 확인 할 수 있습니다.

Multi Datasoruce 설정

Multi datasource는 springboot의 multi Datasource를 사용하는 것과 동일합니다.

Datasource 생성을 하는 설정파일을 생성합니다.

package com.gsshop.batch.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.autoconfigure.batch.BatchDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.transaction.ChainedTransactionManager;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class DatabaseConfig {
    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.default")
    DataSource springBatchDb(){
        DataSourceBuilder builder = DataSourceBuilder.create();
        builder.type(HikariDataSource.class);
        return builder.build();
    }

    @Bean
    @BatchDataSource
    @ConfigurationProperties("spring.datasource.work")
    DataSource workDb(){
        DataSourceBuilder builder = DataSourceBuilder.create();
        builder.type(HikariDataSource.class);
        return builder.build();
    }

    // Transaction Setting

    @Bean
    PlatformTransactionManager springBatchTxManager() {
        return new DataSourceTransactionManager(springBatchDb());
    }

    @Bean
    PlatformTransactionManager workTxManager() {
        return new DataSourceTransactionManager(workDb());
    }

    @Bean
    PlatformTransactionManager chainTxManager() {
        ChainedTransactionManager txManager = new ChainedTransactionManager(springBatchTxManager(), workTxManager());
        return txManager;
    }

}
  • @Primary는 기본으로 DatasSource를 이용할 경우 자동으로 주입되는 DataSource 입니다.
  • @BatchDataSource 는 SpringBatch의 Meta정보를 저장할 때 사용하는 DataSsource 입니다.

특정 DataSource 사용할 경우 @Qualifier를 이용합니다. 다음 예제를 확인하셔요.

@Bean
public JdbcBatchItemWriter<Person> writer(@Qualifier("workDb") DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder<Person>()
            .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
            .sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
            .dataSource(dataSource)
            .build();
}

전체 코드 확인하기

참고자료

Posted by lahuman

댓글을 달아 주세요

728x90

React 기반의 Storybook에서 css module 사용 설정

기간계 디자인 시스템을 진행하면서 storybook 을 이용하고 있습니다.
이번에 css module을 사용하기 위해서 몇가지 설정을 했는데, 검색에 많은 시간이 걸렸습니다.

들어가기에 앞서서

크게 storybook 설정과 typescript + rollup 설정으로 나뉩니다.
typescript + rollup의 경우 배포와 관련이 있습니다.

storybook 설정하기

sotrybook설정은 Add loader for .module.css to load CSS modules 내용을 참고하면 쉽게 됩니다.

//  ./storybook/main.js 

module.exports = {
  stories: ['../stories/**/*.stories.js'],
  addons: ['@storybook/addon-actions', '@storybook/addon-links'],
  webpackFinal: async (config, { configType }) => {

    // get index of css rule
    const ruleCssIndex = config.module.rules.findIndex(rule => rule.test.toString() === '/\\.css$/');

    // map over the 'use' array of the css rule and set the 'module' option to true
    config.module.rules[ruleCssIndex].use.map(item => {
      if (item.loader && item.loader.includes('/css-loader/')) {
        item.options.modules = {
          mode: 'local',
          localIdentName: configType === 'PRODUCTION' ? '[local]--[hash:base64:5]' : '[name]__[local]--[hash:base64:5]',
        };
      }
      return item;
    })

    // Return the altered config
    return config;
  }
};

typescript + rollup 설정

먼저 typescript 설정을 위해서 typescript-plugin-css-modules 모듈을 사용하여야 합니다.

# 설치 
yarn add -D typescript-plugin-css-modules

tsconfig.json 파일에 아래 설정을 추가 합니다.

{
  "compilerOptions": {
    "plugins": [{ "name": "typescript-plugin-css-modules" }]
  }
}

이것으로 typescript 설정이 완료 되었습니다.

그리고 rollup 설정은 Bundle Libraries With SCSS and CSS Modules Using Rollup 를 따라서 설정하면 쉽게 됩니다.

제 경우는 postcss의 옵션만 추가 해서 설정을 완료 하였습니다.

// rollup.config.js
import babel from 'rollup-plugin-babel';
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import postcss from 'rollup-plugin-postcss';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';

export default {
  input: './src/index.js',

  output: [
    {
      name: 'comlib',
      sourcemap: true,
      file: './dist/index.js',
      format: 'umd',
      globals: { react: 'React' },
    },
  ],

  plugins: [
    peerDepsExternal(),
    postcss({
      // 옵션 추가 처리
      extract: false,
      modules: true,
      use: ['sass'],
    }),
    babel({ exclude: 'node_modules/**' }),
    resolve(),
    commonjs(),
  ],

  external: ['react', 'react-dom'],
};

이후 배포를 하면 css module도 함께 처리가 된 것을 확인 할 수 있습니다.

css module이 적용된 예제

위의 그림과 같이 GuiButton-module_formWrap__2zPLQ가 적용된 것을 확인 할 수 있습니다.

마치며

정리해놓은 자료가 다른 누군가에게 도움이 되기를 바랍니다.
저와 같이 하루종일 고생하지 않으시길...

참고자료

Posted by lahuman

댓글을 달아 주세요

728x90

오랜만에 MongoDB를 설치해야 할 일이 생겼습니다.

기존의 MongoDB를 이전설치해야 할 일이 발생했습니다. 기존의 경우 shard와 mongos(router)로 사용했지만, sharding을 하지 않아서 P(Primary)-S(slave)-S(slave) 형식으로 구성하기로 했습니다.

하위 내용을 4.4 번의 AWS LINUX 기준으로 작성되었습니다.

1. repo 파일 생성

root 계정으로 mongodb-org-4.4.repo파일을 생성하고 다음 내용을 작성합니다.

vi /etc/yum.repos.d/mongodb-org-4.4.repo

[mongodb-org-4.4]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/4.4/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-4.4.asc

2. mongoDB 설치

yum 명령어로 mongoDB 4.4를 설치 합니다.

yum install -y mongodb-org

3. mongodb 설정파일 셋팅

설치 기준 디렉토리를 /applications/으로 잡아서 진행하였습니다.

기준 디렉토리 생성

mkdir /applications
cd /applications
mkdir mongodb
cd mongodb
mkdir config
mkdir log
mkdir data

mongod.conf 설정 파일 생성

replication > replSetName 은 Replica Set 구성을 위해서 P-S 모두 동일 해야 합니다. 접근 port는 27019로 설정했습니다.

vi /applications/config/mongod.conf

systemLog:
   destination: file
   path: "/applications/mongodb/log/mongod.log"
   logAppend: true
   logRotate: rename
storage:
   engine: wiredTiger
   directoryPerDB: true
   wiredTiger:
      engineConfig:
         journalCompressor: snappy
      collectionConfig:
         blockCompressor: snappy
      indexConfig:
         prefixCompression: true
   dbPath: "/applications/mongodb/data"
   journal:
      enabled: true
      commitIntervalMs: 300
processManagement:
   fork: true
   pidFilePath: "/tmp/mongod.pid"
net:
   port: 27019
   bindIpAll: true
   maxIncomingConnections: 20000
   unixDomainSocket:
      enabled: false
replication:
    oplogSizeMB: 10240
    replSetName: "replset"

4. mongodb 시작하기

다음 명령어로 MongoDB 데몬을 실행합니다.

mongod -f /applications/mongodb/config/mongod.conf 

만약 아래와 같은 경고가 발생한다면,

MongoDB server version: 4.4.3
---
The server generated these startup warnings when booting:
        2021-02-08T11:18:39.873+09:00: Access control is not enabled for the database. Read and write access to data and configuration is unrestricted
        2021-02-08T11:18:39.873+09:00: Soft rlimits too low
        2021-02-08T11:18:39.873+09:00:         currentValue: 8192
        2021-02-08T11:18:39.873+09:00:         recommendedMinimum: 64000

root 계정으로 /etc/security/limits.conf 파일에 다음과 같이 변경하세요.

vi /etc/security/limits.conf

*               soft    nproc   1024000
*               hard    nproc   1024000
*               -       nofile  64000

변경이 완료 되었다면 재기동을 하여서 변경내역을 반영하세요.

shutdown -r now

5. Replica Set 구성하기

Primary로 생각한 서버에서 다음의 명령어로 mongo shell에 접근하세요

mongo localhost:27019/admin

다음 명령어로 Replica Set을 설정합니다.

rs.initiate(
           {
      _id: "replset", // config 에서 설정했던, replSetName 입니다.
      version: 1,
      members: [
         { _id: 0, host : "<primaryIP>:<PORT>" },
         { _id: 1, host : "<SecondaryIP>:<PORT>" },
         { _id: 2, host : "<SecondaryIP>:<PORT>" },
      ]
   }
)

# 결과 

{
    "ok" : 1,
    "$clusterTime" : {
        "clusterTime" : Timestamp(1612755725, 1),
        "signature" : {
            "hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
            "keyId" : NumberLong(0)
        }
    },
    "operationTime" : Timestamp(1612755725, 1)

성공이후에는 mongo shell에서 "replset:PRIMARY > " 혹은 "replset:SECONDARY> " 으로 표기 됩니다.

6. 계정 생성하기

이후 로그인 처리를 위해서 root 권한을 가진 계정을 mongo shell에서 생성합니다.

use admin
db.createUser(
   {
     user: "rootUser",
     pwd: "rootPass",
     roles: ["root"]
   }
)

7. mongoDB 인증모드 설정

꼭 계정을 생성 후에 인증모드를 설정해야 합니다.

7.1 key 파일 생성하기

key 파일이 없을 경우 올바른 인증이 되지 않아서 Replica Set에 참여가 되지 않습니다.
openssl 명령어를 이용해서 key 파일을 생성합니다.
생성된 키는 최소한의 권한만 가지도록 합니다.

openssl rand -base64 756 > <path-to-keyfile>
chmod 400 <path-to-keyfile>

생성된 파일은 모든 서버에 복제합니다.

7.2 key 파일 설정과 security 활성화 하기

/applications/mongodb/config/mongod.conf 파일 하단에 아래 내용을 추가 합니다.

그리고 모든 mongoDB 서버를 재기동 합니다.

/applications/mongodb/config/mongod.conf

security:
    authorization: enabled
    keyFile: <path-to-keyfile>

7.3 로그인 해보기

모든 처리가 완료 되었다면, 로그인을 해봅니다.

mongo --port 27019 -u "rootUser" -p

또는 

mongo localhost:27019/admin
db.auth('rootUser', 'rootPass');

마치며

이전에 keyFile을 설정 하지 못해서 삽질을 한적이 있는데.. 이번에 다시 정리하면서 확실하게 이해했습니다.

참고자료

Posted by lahuman

댓글을 달아 주세요

728x90

wsl2를 이용시 localhost:port로 접근 해결방법

오늘 갑자기 node로 띄운 후 접근이 안되었습니다.

해결 방법을 찾아보았는데 WSL2, 외부 네트워크와 연결하기를 보고 따라 해보니 잘되었습니다.
내용 자체는 외부와 연결이지만, 저의 경우 내부와의 접근도 안되어 시도 하였습니다.

먼저 아래의 내용을 가진 wsl2-networks.ps1을 생성합니다.

$remoteport = bash.exe -c "ifconfig eth0 | grep 'inet '"
$found = $remoteport -match '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}';

if( $found ){
  $remoteport = $matches[0];
} else{
  echo "The Script Exited, the ip address of WSL 2 cannot be found";
  exit;
}

#[Ports]

#All the ports you want to forward separated by coma
$ports=@(80,443,10000,3000,5000);


#[Static ip]
#You can change the addr to your ip config to listen to a specific address
$addr='0.0.0.0';
$ports_a = $ports -join ",";


#Remove Firewall Exception Rules
iex "Remove-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' ";

#adding Exception Rules for inbound and outbound Rules
iex "New-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' -Direction Outbound -LocalPort $ports_a -Action Allow -Protocol TCP";
iex "New-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock' -Direction Inbound -LocalPort $ports_a -Action Allow -Protocol TCP";

for( $i = 0; $i -lt $ports.length; $i++ ){
  $port = $ports[$i];
  iex "netsh interface portproxy delete v4tov4 listenport=$port listenaddress=$addr";
  iex "netsh interface portproxy add v4tov4 listenport=$port listenaddress=$addr connectport=$port connectaddress=$remoteport";
}

위의 스크립트에서 port는 80,443,10000,3000,5000를 얼여 두었는데요 포트를 변경하려면 다음 라인(14)을 수정하면 됩니다.

#All the ports you want to forward separated by coma
$ports=@(80,443,10000,3000,5000);

해당 파일을 시작 스크립트로 추가 하면 됩니다.

추가를 하기 위해서는 먼저 batch 파일(.bat)을 생서하고 바로가기를 추가로 생성합니다.
생성한 바로가기 파일의 속성에서 고급을 버튼을 눌러서 관리자 권한으로 실행을 체크 합니다.

이후 해당 파일을 시작 디렉토리에 추가합니다. 참고

시작 스크립트 등록 방법은 Win+R 키를 눌러 실행 창에 shell:startup 을 입력 후 엔터를 누릅니다.
해당 디렉토리에 파일을 넣으면 됩니다.

만약 메뉴얼로 진행할 경우 다음 명령어를 관리자 모드로 실행한 CMD에서 실행하세요.

PowerShell.exe -ExecutionPolicy Bypass -File .\wsl2-forward-server.ps1

처음 powershell을 실행하면 파일이 없다는 오류가 발생하지만, 동작 됩니다.

첫 실행 오류

Remove-NetFireWallRule : 'WSL 2 Firewall Unlock'과(와) 같은 'DisplayName' 속성을 가진 MSFT_NetFirewallRule 개체가 없습
니다. 속성 값을 검증하고 다시 시도하십시오.
위치 줄:1 문자:1
+ Remove-NetFireWallRule -DisplayName 'WSL 2 Firewall Unlock'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (WSL 2 Firewall Unlock:String) [Remove-NetFirewallRule], CimJobException
    + FullyQualifiedErrorId : CmdletizationQuery_NotFound_DisplayName,Remove-NetFirewallRule



Name                  : {1c1f5350-1d56-4715-ae96-79bf9cb4bf84}
DisplayName           : WSL 2 Firewall Unlock
Description           :
DisplayGroup          :
Group                 :
Enabled               : True
Profile               : Any
Platform              : {}
Direction             : Outbound
Action                : Allow
EdgeTraversalPolicy   : Block
LooseSourceMapping    : False
LocalOnlyMapping      : False
Owner                 :
PrimaryStatus         : OK
Status                : 저장소에서 규칙을 구문 분석했습니다. (65536)
EnforcementStatus     : NotApplicable
PolicyStoreSource     : PersistentStore
PolicyStoreSourceType : Local

Name                  : {b6435941-bbbb-4754-8b5c-0a0526592ecf}
DisplayName           : WSL 2 Firewall Unlock
Description           :
DisplayGroup          :
Group                 :
Enabled               : True
Profile               : Any
Platform              : {}
Direction             : Inbound
Action                : Allow
EdgeTraversalPolicy   : Block
LooseSourceMapping    : False
LocalOnlyMapping      : False
Owner                 :
PrimaryStatus         : OK
Status                : 저장소에서 규칙을 구문 분석했습니다. (65536)
EnforcementStatus     : NotApplicable
PolicyStoreSource     : PersistentStore
PolicyStoreSourceType : Local

지정된 파일을 찾을 수 없습니다.



지정된 파일을 찾을 수 없습니다.



지정된 파일을 찾을 수 없습니다.



지정된 파일을 찾을 수 없습니다.



지정된 파일을 찾을 수 없습니다.

두번째 실행시

Name                  : {57c6491d-13f9-4d76-944f-7efd5624efd8}
DisplayName           : WSL 2 Firewall Unlock
Description           :
DisplayGroup          :
Group                 :
Enabled               : True
Profile               : Any
Platform              : {}
Direction             : Outbound
Action                : Allow
EdgeTraversalPolicy   : Block
LooseSourceMapping    : False
LocalOnlyMapping      : False
Owner                 :
PrimaryStatus         : OK
Status                : 저장소에서 규칙을 구문 분석했습니다. (65536)
EnforcementStatus     : NotApplicable
PolicyStoreSource     : PersistentStore
PolicyStoreSourceType : Local

Name                  : {982f1982-0a78-42ad-b2c6-eb8a54302847}
DisplayName           : WSL 2 Firewall Unlock
Description           :
DisplayGroup          :
Group                 :
Enabled               : True
Profile               : Any
Platform              : {}
Direction             : Inbound
Action                : Allow
EdgeTraversalPolicy   : Block
LooseSourceMapping    : False
LocalOnlyMapping      : False
Owner                 :
PrimaryStatus         : OK
Status                : 저장소에서 규칙을 구문 분석했습니다. (65536)
EnforcementStatus     : NotApplicable
PolicyStoreSource     : PersistentStore
PolicyStoreSourceType : Local

참고 자료

Posted by lahuman

댓글을 달아 주세요

728x90

Nginx location 설정 옵션

Nginx를 사용하다가 특정 접근 경로에 대하여 다른 정적 페이지를 바라보게 처리하는 일이 생겼습니다.

이때, html을 보도록 설정하였는데 파일이 다운로드 되는 현상을 겪었습니다.

이유는 default_type을 설정하지 않아서 였습니다.

샘플

location = /approval {
        default_type "text/html";
        alias /home/ubuntu/approval/index.html;
        index index.html;
}

이 외에도 location 설정시 =, ~, -*, ^- 등을 uri 전에 추가 할 수 있습니다.

문법

Syntax:    location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }
Default:    —
Context:    server, location

뜻은 다음과 같습니다.

# 정확하게 일치 
location = / {
    [ configuration A ]
}

# 지정한 패턴으로 시작
location / {
    [ configuration B ]
}

# 지정한 패턴으로 시작
location /documents/ {
    [ configuration C ]
}

# 지정한 패턴으로 시작 패턴이 일치 하면 다른 패턴 탐색 중지( 정규식 아님 )
location ^~ /images/ {
    [ configuration D ]
}

# 정규식 표현 일치 - 대소문자 구분
location ~ \.(gif|jpg|jpeg)$ {
    [ configuration E ]
}

# 정규식 표현 일치 - 대소문자 구분 안함
location ~* \.(gif|jpg|jpeg)$ {
    [ configuration F ]
}

별겨 아니지만 삽질 하지 않도록 기억해 두어야겠네요! :)

참고자료

Posted by lahuman

댓글을 달아 주세요

728x90

node:Alpine 에서 puppeteer 기동 하고 한글 깨짐 처리 하기

GitOps 스타일의 지속적인 배포를 구축하여 자동 배포 시스템을 Git ==> jenkins ==> Kubernetes로 구축하여 사용하고 있습니다.

배포가 문제 없이 되었으나 puppeteer를 사용하는 순간 다음과 같은 오류를 만나게 되었습니다.

....
Error: Failed to launch chrome! spawn /app/node_modules/puppeteer/.local-chromium/linux-609904/chrome-linux/chrome ENOENT
...

원인 해결 방법으로 chrome을 설치하고 실행 위치를 강제로 잡아주면 된다.

# Dockerfile 내부
# chromium 설치
RUN apk add --no-cache udev ttf-freefont chromium

# npm 설치 시 chromium 다운하지 않도록 설정
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
# 설치된 위치를 환경 변수로 설정(node에서 사용)
ENV CHROMIUM_PATH /usr/bin/chromium-browser

이후 puppeteer를 실행하는 곳에서 다음과 같이 처리 해줍니다.

puppeteer.launch({
  executablePath: process.env.CHROMIUM_PATH,
  args: ['--no-sandbox'], // This was important. Can't remember why
});

이후 다음과 같은 오류가 발생할 수 있습니다.

Protocol error (IO.read): Invalid parameters handle: string value expected

이유는 node:12-alpine를 사용할 경우 발생하는 오류로 node:13-alpine를 이용하면 해결 됩니다.

마지막으로 이렇게 처리 했음에도 한글이 깨지고 맙니다.

한글 폰트를 다음과 같이 설치하여 줍니다.

# Dockerfile
# 한글 폰트 설치
RUN mkdir /usr/share/fonts/nanumfont
RUN wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip
RUN unzip NanumFont_TTF_ALL.zip -d /usr/share/fonts/nanumfont
RUN fc-cache -f -v

추가 : 전체 Dockerfile을 예제

Alpine 이란? 알파인 리눅스는 보안, 단순성 및 자원 효율성을 높이는 고급 사용자를 위해 설계된 범용 리눅스 배포판으로 musl libc와 busybox를 기반으로 하기에 GNU/Linux 배포판 보다 작고 가벼우며 yum이나 apt 같은 패키지 관리자가 아닌 apk라는 자체 패키지 관리자를 가지며 보안에 염두해두고 설계되었기에 모든 바이너리는 독립적인 실행 파일(PIE)로 컴파일 됩니다.

FROM node:13.12.0-alpine3.11 as build

USER root

# 앱 디렉터리 생성
WORKDIR /usr/src/app

COPY . .
# chromium 설치
RUN apk add --no-cache udev ttf-freefont chromium

# 한글 폰트 처리
RUN mkdir /usr/share/fonts/nanumfont
RUN wget http://cdn.naver.com/naver/NanumFont/fontfiles/NanumFont_TTF_ALL.zip
RUN unzip NanumFont_TTF_ALL.zip -d /usr/share/fonts/nanumfont
RUN fc-cache -f -v

# 앱 디렉토리로 다시 이동
RUN cd /usr/src/app
# npm install 시 Chromium 다운로드 제외 처리
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
# chromium-browser 설치 위치를 환경 변수에 저장
ENV CHROMIUM_PATH /usr/bin/chromium-browser

# 시간을 서울로 변경 처리
RUN apk --no-cache add tzdata && \
        cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \
        echo "Asia/Seoul" > /etc/timezone

# Set the lang
ENV LANG=ko_KR.UTF-8 \
    LANGUAGE=ko_KR.UTF-8

# node module 설치
RUN npm  install --unsafe-perm 

# docker 외부 OPEN 포트
EXPOSE 3000
CMD ["npm", "start"]

아마 나중에 다시 만날 문제일꺼 같습니다.

한글 꺠짐 문제는 늘 한번씩 돌아오네요.

참고자료

Posted by lahuman

댓글을 달아 주세요

728x90

[Mac TIP] OS 업데이트 이후 다시 설치해야 하는 xcode 재설치 하는 법!

Mac의 OS를 업데이트를 하고 나면 꼭 다음과 같은 오류를 만나게 됩니다.

xcode CommandLineTools을 분명히 설치 했음에도 발생합니다.

이럴때는 아래의 명령어로 삭제후 재설치 하여야 합니다.

$> sudo rm -rf /Library/Developer/CommandLineTools
$> sudo xcode-select --install

MAC OS 종특인듯...

Posted by lahuman

댓글을 달아 주세요