개요 

여러 서비스들의 배치 수행시간을 컨트롤하고 배치 성공, 실패 로그를 수집해야하는 작업이 주어졌다.

해당 업무를 진행 하기 전 테스트를 위하여 진행하고 있던 웹 포트폴리오에서 테스트를 진행하고자 한다.


구성 및 설계

기존은 @shceduled를 사용하여 특정 시간에 배치를 수행하였지만 배치 수행시간을 변경하려면 소스의 수정 및 배포가 강제되었다. 이를 해결하기위해 해당 배치들을 API 형식으로 수정하고 Jenkins의 스캐줄을 사용하여 배치수행을 진행한다.

 

또한 단일 서비스가 아닌 여러 서비스의 배치 통합이 이루어져야 하기 때문에 각각의 Batch 수행에 관련된 로그를 수집하여야 한다. 이를 해결하기위해 Elasticsearch와 Logstash를 사용한다.

 

 


Elasticsearch, Logstash 설치

Docker를 사용하여 설치

# ELK가 세팅된 git 프로젝트 clone
git clone https://github.com/ksundong/docker-elk-kor.git

# logstash 데이터를 확인하기위한 volum 디렉토리 생성
mkdir logs

 

 

logstash데이터 확인을 위한 logs 볼륨 추가

docker-compose.yml

      context: logstash/
      args:
        ELK_VERSION: $ELK_VERSION
    volumes:
      - type: bind
        source: ./logstash/config/logstash.yml
        target: /usr/share/logstash/config/logstash.yml
        read_only: true
      - type: bind
        source: ./logstash/pipeline
        target: /usr/share/logstash/pipeline
        read_only: true
        # logstash input을 확인하기위한 볼륨
      - type: bind
        source: /home/k/ELK/docker-elk-kor/logs
        target: /logs
    ports:
      - "5044:5044"
      - "5000:5000/tcp"
      - "5000:5000/udp"
      - "9600:9600"
    environment:
      LS_JAVA_OPTS: "-Xmx256m -Xms256m"
    networks:
      - elk
    depends_on:
      - elasticsearch

  kibana:
    build:
      context: kibana/
      args:
        ELK_VERSION: $ELK_VERSION
    volumes:
      - type: bind
        source: ./kibana/config/kibana.yml
        target: /usr/share/kibana/config/kibana.yml
        read_only: true
    ports:
      - "5601:5601"
    networks:
      - elk
    depends_on:
      - elasticsearch

networks:
  elk:
    driver: bridge

volumes:
  elasticsearch:

 

 

 

 

logstash 파일 출력 및 기타설정

logstash/pipeline/logstash.conf

input {
# beats 미사용
       # beats {
        #        port => 5044
       # }

        tcp {
                port => 5000
        }
}

## Add your filters / logstash plugins configuration here

output {
        elasticsearch {
                hosts => "elasticsearch:9200"
                index => "logs"
                user => "username"
                password => "password"
                ecs_compatibility => disabled
        }
		# 파일 출력을 위한 설정
        file {
                path => "/logs/test.log"
        }

}

 

 

Logstash, Elasticsearch 기동

docker-compose build && docker-compose up -d

 

 

 

키바나 확인 

http://localhost:5601/app/home#/ 

 

 


Spring boot의 Logback 세팅

module-common의 build.gradle

dependencies {
    // 이미 board에 포함된 의존성임으로 추가하지 않음
    implementation project(":module-common")
    implementation project(":module-normal-board")

	// logback을 사용하여 logstash로 전달하기 위하여 필요한 의존성
    implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
    implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.5.6'
}

 

Logback 세팅

module-main의 ressources에 logback.xml 생성

 

logback.xml

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

<!-- 콘솔에 로그를 출력하는 콘솔 앱렌더 설정 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <!-- 로그 출력을 위한 레이아웃 설정 -->
    <layout class="ch.qos.logback.classic.PatternLayout">
        <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
    </layout>
</appender>

<!-- 파일에 로그를 저장하는 파일 앱렌더 설정 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 일별로 로그 파일 롤링 -->
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 보관할 로그 파일의 최대 개수 -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
        </layout>
    </appender>

	<!-- 로그스태시 세팅, 로컬의 5000포트 사용-->
    <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
        <destination>127.0.0.1:5000</destination>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
        <layout class="net.logstash.logback.layout.LogstashLayout">
            <timestampPattern>yyyy-MM-dd' 'HH:mm:ss.SSS</timestampPattern>
        </layout>
    </appender>


<!-- 루트 로거 설정 -->
<root level="INFO">
    <appender-ref ref="CONSOLE" />
<!--    <appender-ref ref="FILE" />-->
</root>

<!-- 로그스태시 사용 -->
<logger name="LOGSTASH">
    <appender-ref ref="LOGSTASH"/>
</logger>

</configuration>

 

 

module-main의 @EnableAsync 설정

API를 호출 했을 때 배치가 오래 걸리린다면(예를들어 1시간 돌아가는배치) request에 대한 timeOut이 발생하기 때문에 비동기로 동작할 수 있도록 세팅

 

package com.portfolio;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication(scanBasePackages = "com.portfolio")
@EnableAsync
public class PortfolioWasApplication {
    public static void main(String[] args) {

        SpringApplication.run(PortfolioWasApplication.class, args);
    }
}

 

 

modue-normal-board의 TestController

package com.portfolio.controller;

import com.portfolio.entity.NormalBoard;
import com.portfolio.repo.NormalBoardRepo;
import com.portfolio.service.NormalBoardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@RequestMapping("/test")
@RestController
@RequiredArgsConstructor
@Slf4j
public class TestController {
    private final NormalBoardService normalBoardService;
    @PostMapping("/jpaInsertTest")
    @Tag(name = "jpa insert Test")
    @Operation(summary = "jpa insert Test")
    public String jpaInsertTest() {

        NormalBoard board = NormalBoard.builder()
                .title("testTitle")
                .content("testContent")
                .firstRegTime(LocalDateTime.now())
                .lastUpdTime(LocalDateTime.now())
                .firstRegUser("patrache")
                .lastUpdUser("patrache")
                .build();

        log.info("controller start");
        normalBoardService.insertNormalBoard(board);
        return board.toString();
    }

    @GetMapping("/jpaSelectTest")
    @Tag(name = "jpa select Test")
    @Operation(summary = "jpa select Test")
    public List<NormalBoard> jpaSelectTest() {
        List<NormalBoard> boardList = normalBoardService.selectAllBoardList();
        return boardList;
    }
}

 

 

module-normal-board의 NormalBoardServiceImpl

해당 service는 배치가 아니지만 배치라 가정해고 작성, Async가 동작하는것을 확인하기위해 Thread.sleep으로 테스트

package com.portfolio.serviceimpl;

import com.portfolio.entity.NormalBoard;
import com.portfolio.repo.NormalBoardRepo;
import com.portfolio.service.NormalBoardService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
@RequiredArgsConstructor
public class NormalBoardServiceImpl implements NormalBoardService {
    private final NormalBoardRepo boardRepo;
    private Logger logstash = LoggerFactory.getLogger("LOGSTASH");

    @Override
    @Async
    public void insertNormalBoard(NormalBoard board) {
        try {
            Thread.sleep(30000);
            boardRepo.save(board);
        } catch (Exception e) {
            logstash.error("BAT0001||테스트 배치||FAIL||{}", e.toString());
            e.printStackTrace();
        }
        logstash.info("BAT0001||테스트 배치||SUCCESS||ETC");

    }

    @Override
    public List<NormalBoard> selectAllBoardList() {
        return boardRepo.findAll();
    }
}

 

Async 동작 확인

Controller start 로그 30초 이후 Logstash 로그 확인

2024-06-05 16:10:08 INFO  c.p.controller.TestController - controller start
2024-06-05 16:10:09 INFO  c.p.controller.TestController - controller start

2024-06-05 16:10:38 INFO  LOGSTASH - BAT0001||테스트 배치||SUCCESS||ETC
2024-06-05 16:10:39 INFO  LOGSTASH - BAT0001||테스트 배치||SUCCESS||ETC

 


Kibana에서 데이터 확인

http://localhost:5601/
목록 > devtool

 

GET {index}/_search 

 

 


Jenkins 설치 및 배포 

docker run  -p 8088:8080 -p 50000:50000 --restart=on-failure -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk17

 

 

 

 

WSL 세팅

WSL은 window에서 리눅스를 사용할 수 있도록 해준다.

리눅스 기반에서 docker를 설치할 예정

https://velog.io/@darktrace1/%EC%9C%88%EB%8F%84%EC%9A%B011%EC%97%90-UbuntuWSL2-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0

 

윈도우11에 Ubuntu(WSL2) 설치하기

윈도우11에 Ubuntu(WSL2) 설치하기

velog.io


Docker를 사용한 Mysql 세팅


#의존성 패키지 설치
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common

#Docker 공식 GPG 키 추가 
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

#docker Rpository 추가 
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

#Docker 설치
sudo apt update
sudo apt install docker-ce

# 설치 확인
sudo docker --version

#docker 시작 
sudo service docker start

 

docker ps 

 

 

Docker hub에서 mariaDB 검색

 

#도커 이미지 다운로드
docker pull mariadb

# 도커 이미지 확인
docker images

#이미지를 사용하어 컨테이너 생성
docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=123 -p 3306:3306 mariadb:latest

#docker container 상태 확인
docker ps

 

# 컨테이너에 접속
docker exec -it some-mariadb bash 

#컨테이너에서 mariadb DBMS 접근(위 설정에서 비밀번호는 123)
mariadb -u root -p

# 데이터 베이스 조회
show databases;

# 데이터베이스 생성
create database portfolio;

# 데이터베이스 접속
use portfolio;

# 쿼리 실행 확인
select now()

 

 

# timezone 세팅
SET global TIME_ZONE='+09:00';
SET session TIME_ZONE='+09:00';

# 확인 
select now();

 

 

 

# 계정생성
CREATE USER 'adm'@'%' IDENTIFIED BY '123';

# 권한부여
GRANT ALL PRIVILEGES ON portfolio.* TO 'adm'@'%';

# 권한 부여 반영
FLUSH PRIVILEGES;

Docker volume 

Volume을 설정하지 않는다면 도커의 컨테이너를 삭제 시 mariaDB의 데이터 또한 모두 삭제 된다.

Volume을 사용하여 컨테이너 내부와 host의 디렉토리를 공유 하는것으로 container가 삭제 되더라도 volume을 사용한다면 신규 컨테이너에서 host의 파일을 읽기 때문에 데이터의 손실이 없다.

 

Ex 

docker-compose

services:
  mariadb:
    container_name: mariadb
    image: mariadb:(버전 기재)
    restart: always
    # 환경에 따라 변경
    expose:
      - "3306"
    volumes:
      # DB 데이터 저장 디렉터리
      - ./data:/var/lib/mysql
      # 설정 파일 저장될 위치
      - ./config:/etc/mysql/conf.d
    environment:
      # 정의하지 않으면 실행 시, 에러가 발생합니다.
      - "MYSQL_ROOT_PASSWORD=(root 계정의 PASSWORD 정의)"
      # 컨테이너 내의 시스템 타임존을 우리나라에 맞게 설정합니다.
      - "TZ=Asia/Seoul"
    command:
      # 위 명령어를 사용하지 않으면, 일부 설정이 latin으로 설정됩니다.
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci

 

 


https://github.com/Kmmanki/portfolio_was

 

GitHub - Kmmanki/portfolio_was: portfolio

portfolio. Contribute to Kmmanki/portfolio_was development by creating an account on GitHub.

github.com

 

멀티모듈이란?

하나의 프로젝트를 여러개의 독립적인 모듈로 분리하여 구성하는 방식으로 여러 모듈간 의존성 관리를 할 수 있다.

Ex)

login 모듈(로그인 관련 api)은 auth와 common 모듈을 포함한다. 

board 모듈(게시판 관련 api)은 auth와 common 모듈을 포함한다.

auth 모듈(authentication 관련 설정) Oautn, jwt 관련 라이브러리를 포함한다.

 

위와같이 모듈을 분리함으로써 auth가 필요 없는 모듈은 auth를 가져가지 않고 추후 다수의 개발자가 개발 시 보다 용이한 버전관리를 할 수 있다.

 


프로젝트 우클릭 -> New -> Module

 

 

module-common, module-normal-board, modue-main 추가

기존의 src 제거

 

src 제거, 각 모듈 생성

ps . 모듈 이름 오타남.

  modue-noraml-board -> module-normal-baord

 

settings.gradle(intelij에서 자동으로 작성해줌)

rootProject.name = 'portfolio_was'
include 'module-common'
include 'modue-normal-board'
include 'module-main'

 

build.gradle(프로젝트 최상위)

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.4'
}



java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}


// 모든 모듈에 적용되는 사항
subprojects {
	apply plugin: "org.springframework.boot"
	apply plugin: "io.spring.dependency-management"
	apply plugin: "java"

	group = 'com.portfolio'
	version = '0.0.1-SNAPSHOT'
    
    // 라이브러리 및 프레임워크 저장소
	repositories {
		mavenCentral()
	}

	configurations {
		compileOnly {
        	// 롬복 관련 설정
			extendsFrom annotationProcessor
		}
	}

	dependencies {
		implementation 'org.springframework.boot:spring-boot-starter-web'
		compileOnly 'org.projectlombok:lombok'
		developmentOnly 'org.springframework.boot:spring-boot-devtools'
		annotationProcessor 'org.projectlombok:lombok'
		testImplementation 'org.springframework.boot:spring-boot-starter-test'
		
        // OpenAPI의 swager 기능 사용하기 위해
		implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.5.0'
	}

	tasks.named('test') {
		useJUnitPlatform()
	}


}

 

 

module-normal-board의 build.gradle

// 빌드 시 bootJar로 생성하지않음
bootJar { enabled = false }
// 다른 모듈의 라이브러리 형태가 될 것이기 때문에 jar 형태로 진행
jar { enabled = true }

dependencies {
	// common 모듈에 의존
    implementation project(":module-common")
}

 

module-common의 build.gradle

bootJar { enabled = false }
jar { enabled = true }

dependencies {

}

 

module-main의 build.gradle

dependencies {
    // 이미 board에 포함된 의존성임으로 추가하지 않음
//    implementation project(":module-common")
    implementation project(":module-normal-board")
}

 


module-main 의 main 함수 추가

 

PortfolioWasApplication.java

package com.portfoio;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication(scanBasePackages = "com.portfoio")
public class PortfolioWasApplication {
    public static void main(String[] args) {

        SpringApplication.run(PortfolioWasApplication.class, args);
    }
}

 

구동 확인

 


모듈 의존성 적용 확인

 

module-common에 TestController.java 추가

 

OpenAPI 접속

  • http://localhost:8080/swagger-ui/index.html

 

 

main 모듈에 없는 Controller 적용 확인

 


https://github.com/Kmmanki/portfolio_was

 

GitHub - Kmmanki/portfolio_was: portfolio

portfolio. Contribute to Kmmanki/portfolio_was development by creating an account on GitHub.

github.com

 

Spring Boot 프로젝트 생성

Spring Boot initializr 를 사용하여 스프링 부트 프로젝트를 생성 후 IDE에서 열어보기

https://start.spring.io/

 

 

java, Spring Boot 버전에 맞게 설정 후 사용할 디펜던시 추가

  • Lombok: Getter, Setter 등 어노테이션 사용을 위해 추가
  • Spring Web: Spring의 MVC 패턴을 사용하기 위해 추가
  • Spring Boot Dev Tools: 소스 변경시 재기동 등 다양한 개발 편의를 위해 추가

프로젝트 실행

생성한 프로젝트를 VSCode에서 open 후  실행 확인

 

 


Swagger 라이브러리 추가

https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui/2.5.0

 

Swagger를 사용하기위한 springdoc 라이브러리 추가

 ps. Spring Boot 3.x 이상부터는 openAPI Starter WebMVC UI로 적용해야한다.

 

maven repository에서 springdoc을 찾아서 gradle 클릭 후 카피

 

 

build.gradle에 추가

 

접속

테스트 컨트롤러 및 openAPI 설정 

SwaggerConfig.java

package com.portfolio.portfolio_was.config;

import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * SwaggerConfig
 */
@Configuration
public class SwaggerConfig {

  
  @Bean
  public GroupedOpenApi api() {
    String[] paths = {"/**"};
    // String[] pakcagesToScan = {"com.portfolio.portfolio_was"};
    return GroupedOpenApi.builder().group("springdoc-openapi")
    .pathsToMatch(paths)
    // .packagesToScan(pakcagesToScan)
    .build();
  }
}

 

TestController.java

package com.portfolio.portfolio_was.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;


@RequestMapping("/test")
@RestController
public class TestController {
 
  @GetMapping("/api1")
  @Tag(name = "test api")
  @Operation(summary = "test")
  public String test22() {
    return "test";
  }
}

 

 


https://github.com/Kmmanki/portfolio_was

 

GitHub - Kmmanki/portfolio_was: portfolio

portfolio. Contribute to Kmmanki/portfolio_was development by creating an account on GitHub.

github.com

 

+ Recent posts