Spring Initializer로 Spring Boot 프로젝트를 쉽게 생성하는데, 어딘지 모르게 죄책감이 든다.

나는 과연 Spring과 Spring Boot의 차이를 알고 매일 이렇게 쉽게 이용하는 것인가.

누군가 물어보면 쉽게 대답할 수 있는가? 했을 때 자신이 없어 이참에 알아보기로 하였다. 

 

Spring

  • 자바를 더 쉽게 사용할 수 있게 해주는 오픈소스 프레임워크 
    • 중복코드의 사용률을 줄여주고 비즈니스 로직을 더 간단하게 한다. 
      • 프레임워크? 자주 쓰일만한 기능들을 한데 모아 놓은 클래스들의 집합으로 기본적인 설계나 필요한 라이브러리를 제공한다. 
  • 특징
    • 의존성 주입(Dependency Injection)
      • 구성요소 간의 의존 관계가 소스코드 내부가 아닌 외부의 설정 파일을 통해 정의되는 방식
      • 어노테이션, 설정파일을 통해 
    • 제어의 역전(IOC, Inversion Of Control)
      • 객체 생명주기 관리를 개발자가 하지 않고 컨테이너가 대신 해주는 것
        • 필요한 부분을 적절한 상황에 따라 자유롭게 교체할 수 있음
          • MyBatic를 쓰던 JPA를 쓰던 Oracle Database를 쓰던...
    • 관점 지향 프로그래밍 (Aspect Object Programming)
      • 로깅, 트랜잭션, 보안 등 여러 모듈에서 공통적으로 사용하는 기능을 분리하여 관리
    • POJO (Plain Old Java Object)
      • 객체 지향 원리에 충실하되, 특정 환경에 종속되지 않고 순수 자바로 설계된 객체

Spring Boot

  • 스프링 프레임워크를 기반으로 바로 실행가능한 애플리케이션을 쉽게 만들도록 도와줍니다. 스프링 관련된 복잡한 설정을 자동으로 처리해서 개발자는 최소한의 설정만 진행하도록 할 수 있게 하는 스프링 프레임워크의 서브 프로젝트
    •  (단독 실행 가능 스프링애플리케이션 생성 + 최소한의 초기 스프링 구성)
  • 생성 배경
    • 스프링으로 애플리케이션을 개발하려면 사전에 많은 작업을 해야 했음 (비즈니스 로직에만 더 집중하고 싶음)
    • 웹 애플리케이션 개발에 필요한 라이브러리와 그 라이브러리에 종속된 라이브러리들을 개발자가 일일이 추가해야만 했음
      • 버전이 다르거나 충돌이 나는 경우가 있어 개발 환경 구성에 어려움이 존재하였음
    • 비즈니스 로직보다 개발환경 구성 및 스프링의 기능 구성에 시간을 쏟게 됨
      • Transaction Manager
      • Hibernate - spring-boot-starter-data-jpa
      • Entity Manager
      • Spring MVC
  • 장점
    • 프로젝트에 따라 자주 사용되는 라이브러리들이 미리 조합됨
      • Spring MVC, Jackson Databind, Hibernate 코어 및 Log4j
    • 복잡한 설정을 자동으로 처리 
    • 내장 서버를 포함해서 톰캣과 같은 서버를 추가로 설치하지 않아도 바로 개발이 가능
    • JAR파일로 웹 애플리케이션 개발이 가능 
      • 스프링의 jar 파일이 클래스 패스에 있는 경우 Spring Boot는 Dipatcher Servlet으로 자동 구성

 

 

출처 

책 - 스프링부트 시작하기 

https://goddaehee.tistory.com/238

https://jerryjerryjerry.tistory.com/62

 

 

  • Batch 
    • 일괄처리
  • 사용 예
    • 엄청나게 큰 데이터를 가공해야 하는데 예를 들어 그 작업을 하루에 1번 정도 해줘야 한다. 
      • 엄청나게 큰 데이터를 가공해야 한다 ? 
        • CPU, I/O 등의 자원 처리에 서버 부하가 발생 
        • Request 처리를 올바르게 할 수 없다. 
        • 데이터 처리 중에 실패 발생하면, 다시 처음부터 하는게 아니라 끝난 지점부터하는게 좋음 (5만1번째)
      • 하루에 딱 1번 한다 ?
        • 하루에 한번만 돌게 하는 것을 구현하려고 API를 구성하는 것은 낭비일 수 있음 
        • 다른 사람이 한번 더 돌리는 실수를 막아야 함
  • SpringBatch는
    • '단발성으로 대용량의 데이터를 처리하는 어플리케이션' 이다.
      • Spring MVC를 사용함으로써 비즈니스 로직에 최대한 집중할 수 있었던 것 처럼 Spring Batch를 이용하면 배치 어플리케이션을 동작하는 데 필요한 로직에만 집중할 수 있다.
      • 예를 들어, 이미 실행된 경우 재실행을 불가능 하게 한다거나, 실패된 지점에서 부터 다시 동작시키게 할 수 있다거나... 이런 기능들을 SpringBatch가 쉽게 할 수 있도록 도와준다. 
      •  
  • Batch Application의 조건
    • 대용량 데이터 - 대용량의 데이터를 가져오거나 전달, 계산 등의 처리를 함 
    • 자동화 - 사용자 개입을 최소로 줄여 최대한 자동으로 동작될 수 있도록 한다.
    • 견고성 - 잘못된 데이터를 충돌/중단 없이 처리
    • 신뢰성 - 잘못된 부분을 추적할 수 있음 (로깅 / 알림)
    • 성능 - 지정된 시간 내에 처리, 다른 어플리케이션을 방해하지 않는다.

아래의 그림과는 표현한 방식이 살짝 다르다.
Job과 Step은 작업의 최소 단위,  Step을 통해 데이터 읽기/가공/결과기록 등의 로직을 묶어서 관리한다. Chunk는 한 번의 operation을 통해 다룰 데이터의 집합으로 각 Step은 설정에 정의된 Chunk 단위에 따라 데이터를 읽어 들이고 가공한 후 기록한다. Step의 설정에서 Chunk의 크기를 결정하는데 이는 데이터베이스의 한 row일 수도 있고, CSV 파일의 한 줄일 수도 있다. Chunk는 트랜잭션 관리 단위이기도 하여 마지막 처리 시점에 TX를 커밋하거나 롤백한다. ItemReader<Input> 은 작업 대상이 될 데이터를 읽어 들이는 컴포넌트이다. ItemProcessor<Input,Output>은 로직에 따라 가공하고, 변경된 데이터를 리턴한다. ItemWriter<Output>은 완성된 데이터를 정해진 곳에 저장/색인한다.

 

  • Batch Application 생성 - Simple ver.

Spring Initializer를 이용해 위의 세팅으로 프로젝트 생성

 

SpringBatch의 여러 기능을 쓰려면 필수로 추가해야하는 어노테이션
Job 생성과 Step 생성

run을 해보면..

 

log.info(">>>>> This is Step1") 가 잘 수행되어있음

 

 

 

 

 

  • Batch Application 생성 - hard ver.
    • PostgreSQL 환경에서 Spring Batch 실행해보기 
    • 먼저, Spring Batch를 작동하기 위해 필요한 메타 데이터 테이블을 세팅한다. 
      • Spring Batch 의 메타데이터는..
        • 이전에 실행한 Job에 대한 기록
        • 실패한 Batch Parameter 기록, 성공한 Job 기록
        • 재실행시 시작해야 될 포인트 기록
        • Job이 갖고 있는 Step에 대한 Histroy tracking (성공/실패 Step 기록)

소스 라이브러리에서 spring-batch-core 디렉토리 안에 있다.

 

더보기

-- Autogenerated: do not edit this file

CREATE TABLE BATCH_JOB_INSTANCE (
JOB_INSTANCE_ID BIGINT NOT NULL PRIMARY KEY ,
VERSION BIGINT ,
JOB_NAME VARCHAR(100) NOT NULL,
JOB_KEY VARCHAR(32) NOT NULL,
constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
) ;

CREATE TABLE BATCH_JOB_EXECUTION (
JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY ,
VERSION BIGINT ,
JOB_INSTANCE_ID BIGINT NOT NULL,
CREATE_TIME TIMESTAMP NOT NULL,
START_TIME TIMESTAMP DEFAULT NULL ,
END_TIME TIMESTAMP DEFAULT NULL ,
STATUS VARCHAR(10) ,
EXIT_CODE VARCHAR(2500) ,
EXIT_MESSAGE VARCHAR(2500) ,
LAST_UPDATED TIMESTAMP,
JOB_CONFIGURATION_LOCATION VARCHAR(2500) NULL,
constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)
) ;

CREATE TABLE BATCH_JOB_EXECUTION_PARAMS (
JOB_EXECUTION_ID BIGINT NOT NULL ,
TYPE_CD VARCHAR(6) NOT NULL ,
KEY_NAME VARCHAR(100) NOT NULL ,
STRING_VAL VARCHAR(250) ,
DATE_VAL TIMESTAMP DEFAULT NULL ,
LONG_VAL BIGINT ,
DOUBLE_VAL DOUBLE PRECISION ,
IDENTIFYING CHAR(1) NOT NULL ,
constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)
references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ;

CREATE TABLE BATCH_STEP_EXECUTION (
STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY ,
VERSION BIGINT NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
JOB_EXECUTION_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP DEFAULT NULL ,
STATUS VARCHAR(10) ,
COMMIT_COUNT BIGINT ,
READ_COUNT BIGINT ,
FILTER_COUNT BIGINT ,
WRITE_COUNT BIGINT ,
READ_SKIP_COUNT BIGINT ,
WRITE_SKIP_COUNT BIGINT ,
PROCESS_SKIP_COUNT BIGINT ,
ROLLBACK_COUNT BIGINT ,
EXIT_CODE VARCHAR(2500) ,
EXIT_MESSAGE VARCHAR(2500) ,
LAST_UPDATED TIMESTAMP,
constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID)
references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ;

CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT (
STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
SHORT_CONTEXT VARCHAR(2500) NOT NULL,
SERIALIZED_CONTEXT TEXT ,
constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)
references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)
) ;

CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT (
JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
SHORT_CONTEXT VARCHAR(2500) NOT NULL,
SERIALIZED_CONTEXT TEXT ,
constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)
references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
) ;

CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ MAXVALUE 9223372036854775807 NO CYCLE;
CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ MAXVALUE 9223372036854775807 NO CYCLE;
CREATE SEQUENCE BATCH_JOB_SEQ MAXVALUE 9223372036854775807 NO CYCLE;

위의 쿼리문을 이용해 table을 생성해준다. 나는 AWS RDS를 이용해 PostgreSQL을 설치해 주었다. 

 

application.yml에 datasource 설정 완료

 

 

참조

medium.com/myrealtrip-product/spring-batch-%EC%B2%98%EC%9D%8C%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-3c6a5db0646d

 

Spring Batch, 처음부터 시작하기

커머스 서비스 개발자를 위한 Spring Batch 입문

medium.com

jojoldu.tistory.com/325?category=902551

 

2. Spring Batch 가이드 - Batch Job 실행해보기

이번 시간에는 간단한 Spring Batch Job을 생성 & 실행하면서 전반적인 내용을 공부해보겠습니다. 작업한 모든 코드는 Github에 있으니 참고하시면 됩니다. 2-1. Spring Batch 프로젝트 생성하기 기본적인

jojoldu.tistory.com

 

현재 개발 중인 어드민 사이트는 다국어 지원이 되어야 한다. 

내가 맡은 부분은 서버 즉 백엔드 부분이므로, 설정한 언어에 맞게 Error Message를 클라이언트로 전달해야 했다.

 

기존 구 어드민 사이트에서는 .properties 파일에 Message들을 저장했지만 나는 DB에 저장하여 관리키로 함.

전략이라 할 것도 없지만.. 어쨌든 내가 가고자 하는 방향에 맞는 전략은 아래와 같다.

(내 전략의 방향이 맞는지, 그리고 이것을 구현한 방법이 맞는지 아직도 의문이다. 따라서 혹시라도 이것을 보시고 더 좋은 방법이나 틀린 부분이 있다고 느끼시는 분들이 있다면 Comment로 남겨주시면 제가 그것을 감사히 읽어보고 수정하도록 하겠습니다.)

 

- 에러 메시지는 DB에 저장

- 에러 메시지를 클라이언트로 전달할 때는 Caching되어진 데이터를 전달하고, 데이터는 locale과 messageKey 값으로 찾음 

- Exception은 @RestControllerAdvice로 지정한 class에서 처리

 

@RestContorllerAdvice로 설정된 전역 ExceptionHandler가 클라이언트에서 전달한 Accept-Language Header값에 맞는 Exception Message로 전달하는 역할을 담당한다. 각 메소드는 Exception에 대한 MessageKey를 가지고 있어 Caching되어진 Exception Data에서 MessageKey와 Locale 정보에 맞는 Exception Message를 불러와서 Client로 전달한다. 단, 어떤 Exception 은 구체화된 에러 메시지를 내려줄 필요성이 있기에 이런 경우에는 exception을 throw할 때 overriding한 CustomException의 messageKey를 set해서 전달한다. 아래 로직은 @ExceptionHandler가 Exception을 Client에 전달하기 전에 수행하는 작업에 대한 내용이다. 

@RestControllerAdvice
@Slf4j
public class AdminExceptionHandler {
    @ExceptionHandler(ForbiddenRequestException.class)
    public ResponseEntity<?> handleForbiddenRequestException(ForbiddenRequestException e, Locale locale) {
        log.error(ExceptionUtils.getExceptionMessage(e));
        String messageKey;
        if (e.getMsgKey() != null) {
        	messageKey = e.getMsgKey();
        } else {
            	messageKey = "forbidden.request";
        }
        return ApiMessage.builder().errorMessage(resource.getMessage(CacheKeys.builder().messageKey(messageKey).locale(locale.toString()).build())).status(e.getStatus()).build().toEntity();
    }
}

 

그리고 아래 부분은 DefaultLocale 값을 설정하고, Accept-Language 값을 Locale로 설정하기 위해 구현한 내용이다. 

나는 CookieLocaleResolver를 사용했다. 아래 설정을 마치면 @ExceptionHandler로 지정한 메소드로 전달한 Locale값이 Accept-Language 헤더값으로 잘 전달된다. 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Arrays;
import java.util.Locale;

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }


    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setDefaultLocale(Locale.KOREA);
        return localeResolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
        lci.setParamName("lang");
        return lci;
    }
}

 

다국어 데이터를 Caching 하여 필요할 때 꺼내쓰려면 messageKey와 locale 두개의 값이 Caching 데이터의 Key가 되어야 했고 이를 위해 아래 CacheKeys라는 아래 class를 만들었다. 

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;
import java.util.Objects;

@Getter
@Setter
@Builder
public class CacheKeys implements Serializable {
    private String messageKey;
    private String locale;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CacheKeys cacheKeys = (CacheKeys) o;
        return Objects.equals(messageKey, cacheKeys.messageKey) &&
                Objects.equals(locale, cacheKeys.locale);
    }

    @Override
    public int hashCode() {
        return Objects.hash(messageKey, locale);
    }
}

 

getMessage 메소드가 캐싱하는 대상 메소드이다.

보시다시피 key는 위에 정의한 CacheKey 클래스의 messageKey와 locale 값이다. 

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;

import java.util.List;

@Slf4j
@Component
public class ErrorMessageComponent {

    @Autowired
    ErrorMsgI18nRepository errorMsgI18nRepository;

    @Cacheable(value = "errorMsgCache", key = "#cacheKeys")
    public String getMessage(CacheKeys cacheKeys) {
        log.debug("by getMessage method -> not by cache");
        return errorMsgI18nRepository.findByMsgKeyAndLocale(cacheKeys.getMessageKey(), cacheKeys.getLocale()).orElse("CACHE KEYS ERROR");
    }
}

 

class가 key로 되게 하기 위해서는 우선 내가 사용한 ehcache의 xml파일에 정의를 해주어야 한다.

key가 바로 CacheKeys라는 것을 명시해주고, value는 String임을 정의해준다.

<expiry>를 보면 서비스가 run하는 중에는 만료되지 않게 하기 위해 <none/>를 해준 것을 알 수 있다. 

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
        xsi:schemaLocation="
            http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
            http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">

    <cache alias="errorMsgCache"> 
        <key-type>com.test.entity.CacheKeys</key-type> 
        <value-type>java.lang.String</value-type>
        <expiry>
            <none/>
        </expiry>

        <resources>
            <heap unit="entries">10</heap> 
            <offheap unit="MB">10</offheap>
        </resources>
    </cache>
</config>

 

 

  • Ehcache
    • Hibernate와 캐시 제공자 사이의 다리 역할
    • redis / memchached와 달리 별도 서버를 가지지 않으며, Spring 내부적으로 동작하여 캐싱함
    • 캐시하고 싶은 메소드에 @Cacheable(value = "xxx")를 붙여주면 된다. 
      • Second-Level Cache란?
        • 세션 단위의 1차 레벨 캐시라는 개념을 적용한게 Hibernate / 세션이 종료되면 First-Level Cache도 종료
        • Second-Level Cache는 SessionFactory 범위이므로 모든 세션에서 같은 SessionFactory를 공유한다. 
        • 엔티티가 Second-Level Cahing 활성화 상태가 되면? 
          • 인스턴스가 First-Level-Cache에 있는 경우엔 First-Level-Cache에서 반환된다.
          • First-Level-Cache에서 인스턴스를 찾을 수 없고 해당 인스턴스 상태가 Second-Level에 Cache된 경우 거기서 데이터를 가져온다. 
          • 그렇지 않으면 필요한 데이터가 데이터베이스에서 로드되고 Instance가 assemble되고 반환된다.

 

 

일정에 쫓기어 글을 완성도 있게 마무리 하고 싶었지만 우선 이정도로 마무리.. 

언제쯤이면 일도 척척 하고 포스팅도 여유롭게 할 수 있는 경지에 다다를 수 있을런지

얼른 끝내고 다시 돌아오겠습니다~

 

 

 

 

 

참고.

gs.saro.me/dev?tn=488

프론트 개발자분과 함께 작업하면서 백엔드 api서버를 로컬에 띄워 작업하는 데 한계를 느껴

아직 개발 중이지만 지금까지 만들어 둔 것만 EC2에 올려두기로 했다.

 

그런데 이미 EC2에 올려놓은 코드를 수정해야 할 작업이 계속 생기고,

그때마다 배포를 하는 것은 말도 안된다 생각하여 자동배포를 하기로 결심! 

 

동료의 도움과 아래 잘 정리된 포스팅으로 생각보다 빠르게 (반나절) 끝이 났다.

 

 

https://twofootdog.tistory.com/37

 

AWS CodeBuild로 빌드 후 S3에 빌드 결과파일 업로드

이번 글에서는 AWS CodeCommit에 있는 스프링부트 소스코드를 AWS CodeBuild를 통해서 빌드를 수행한 후 빌드된 결과파일(아티팩트(JAR))을 S3에 업로드하는 실습을 진행해볼 것이다. 이 글의 순서는 다음과 같다...

twofootdog.tistory.com

https://jojoldu.tistory.com/281

 

1) AWS로 배포하기 시리즈 - 1. Code Deploy 사용하기

AWS로 전체 시스템 구축해야 할 일이 생겨 AWS 배포 환경 시리즈를 시작합니다. 시리즈 과정은 CodeDeploy -> Code Pipeline -> ELB & Auto Scaling Group -> Beanstalk 으로 진행될 예정입니다. Code Pipeline vs..

jojoldu.tistory.com

일단 돌아가게는 만들었지만, 다시 정리할 필요를 느껴 짧게 내 방식대로 정리해보려 한다. 

 

 

 

 

 

AWS에서 배포하는 데 필요한 서비스

 

  • Code Commit
    • 깃허브와 같은 코드 저장소의 역할
  • Code Build
    • 빌드용 서비스, 규모가 있는 데에선 젠킨스/팀시티 등 이용
    • 소스코드 컴파일, 단위테스트, 배포 준비된 artifact file 생성
  • Code Deploy
    • 배포 서비스, 위 2개의 서비스는 대체재가 있으나 CodeDeploy는 대체재가 없다.
    • 오토 스케일링 그룹 배포, 블루 그린 배포, 롤링 배포, EC2 단독 배포의 기능을 지원한다. 

 

내가 이해한 Code Pipeline이 빌드 및 배포하는 과정 

 

Code Commit에 올린 내 소스가 업데이트(커밋)되면 파이프라인이 실행된다. (커밋 = 트리거)

소스 단계의 artifact가(빌드 준비가 완료된 파일)이 S3에 저장된다.  (빌드 준비가 완료된 파일)

CodeBuild는 이 artifact를 빌드한 다음, 빌드된 jar파일을 다시 S3에 저장하고, CodeDeploy에게 배포 명령을 내림

CodeDeploy는 CodeBuild가 빌드한 jar파일을 가져와서 애플리케이션(EC2)에 배포한다. 

 

 

 

 

자동배포 작업순서 

 

  • Code Commit에 내 스프링부트 소스 올려두기
  • AWS S3 버킷 생성하기
  • AWS CodeBuild 프로젝트 생성 
    • 소스공급자 - CodeCommit
    • 리포지토리 - CodeCommit 내 나의 프로젝트 
    • 브랜치 - master
    • 운영체제 - Amazon Linux 2
    • 아티팩트 - Amazon S3 (버킷 연결)
      • 패키징 : zip 
      • 캐시 : 빌드에 필요한 의존성들을 캐시하는 건데, 빌드할 때마다 새로 받는게 아니라 S3에 올려놓고 이미 있는 건 새로 받지 않도록 하는 것
  • 프로젝트 Root 디렉토리에 buildspec.yml 파일 생성
    • artifacts작성하면서 jar파일 어디에 저장하고 Deploy때 필요한 appspec.yml과 배포스크립트 파일 어디있는지 작성하기
  • commit & push 후 CodeBuild에서 빌드 시작 클릭해서 잘 되는지 테스트
  • AWS CodeDeploy용 IAM 역할 생성 (AWSCodeDeployRole)
  • AWS EC2 인스턴스에서 S3 접근을 위한 IAM 역할 생성 (AmazonEC2RoleforAWSCodeDeploy)해서 EC2에 역할주기
  • EC2 인스턴스에 CodeDeployAgent 설치
$ sudo yum update
$ sudo yum install ruby
$ sudo yum install wget

$ cd /home/ec2-user
$ wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
$ chmod +x ./install
 

최신 버전의 CodeDeploy 에이전트를 설치하려면 다음을 수행합니다.

$ sudo ./install auto

서비스가 실행 중인지 확인하려면 다음 명령을 실행합니다.

$ sudo service codedeploy-agent status

"error: No AWS CodeDeploy agent running"와 같은 메시지가 표시되면 서비스를 시작하고 다음 두 명령을 한 번에 하나씩 실행합니다.

$ sudo service codedeploy-agent start
$ sudo service codedeploy-agent status

 

출처 : https://docs.aws.amazon.com/ko_kr/codedeploy/latest/userguide/codedeploy-agent-operations-install-linux.html

 

  • AWS CodeDeploy 애플리케이션 생성
    • AWS CodeDeploy용 IAM 역할 붙여주기
    • 애플리케이션 배포 방법 선택 (현재위치, 블루/그린)
    • 연결할 ec2를 tag를 입력하여 알려주기
  • 프로젝트 Root 디렉토리에 appspec.yml 파일 생성
    • 어떤 파일을 어느 위치에 배포하고 어떤 스크립트 실행할 건지 관리한다.
  • 배포스크립트 start.sh 생성 (아래 참고)
  • commit & push 후 CodeDeploy에서 배포 생성해서 잘 되는지 테스트해보기

+ deploy할 때 에러가 발생할 때는 ? 

  • cd /var/log/aws/codedeploy-agent 로 가서 codedeploy-agent.log 파일을 확인해보자.

 

 

CI/CD 흐름

설정한 배포 프로세스

 

작성파일 

 

  • buildspec.yml
version: 0.2

phases:
  install:
    runtime-versions:
      java: corretto8
  build:
    commands:
      - echo starting build stage
      - mvn package
  post_build:
    commands:
      - pwd
artifacts:
  files:
    - target/*.jar
    - appspec.yml
    - scripts/**
  discard-paths: yes
 cache:
  paths:
    - '/root/.m2/**/*'

artifacts는 빌드된 빌드된 WAR/JAR를 보관할 위치를 나타낸다.

artifacts 파일에 있는 appspec.yml과 scripts/** 는 CodeBuild가 .zip으로 패키징 할 때 jar파일에 appspec.yml, start.sh를 포함시킨다. CodeDeploy는 S3에 있는 .zip파일 압축을 풀어서 appspec.yml파일을 참고해서 배포한다. 

cache 경로에 있는 파일들이 S3 캐시파일로 등록된다.

 

 

  • appspec.yml
version: 0.0
os: linux
files:
  - source: /
    destination: /home/ec2-user/target/
permissions:
  - object : /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks:
  AfterInstall:
    - location: start.sh
      timeout: 60
      runas: ec2-user

 

  • 배포스크립트 (.sh)
#!/bin/bash
BUILD_JAR=$(ls /home/ec2-user/target/*.jar)
JAR_NAME=$(basename $BUILD_JAR)
echo "> build 파일명: $JAR_NAME" >> /home/ec2-user/deploy.log
echo "> debugging : $BUILD_JAR"    >> /home/ec2-user/deploy.log

echo "> build 파일 복사" >> /home/ec2-user/deploy.log
DEPLOY_PATH=/home/ec2-user/target/
# cp $BUILD_JAR $DEPLOY_PATH

echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ec2-user/deploy.log
CURRENT_PID=$(pgrep -f $JAR_NAME)
echo "> debugging : $JAR_NAME"    >> /home/ec2-user/deploy.log

if [ -z $CURRENT_PID ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ec2-user/deploy.log
else
  echo "> kill -15 $CURRENT_PID"
  kill -15 $CURRENT_PID
  sleep 5
fi

DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME
echo "> DEPLOY_PATH, JAR_NAME : $DEPLOY_PATH$JAR_NAME"    >> /home/ec2-user/deploy.log

echo "> DEPLOY_JAR 배포"    >> /home/ec2-user/deploy.log
nohup java -Xms1024m -Xmx2048m -XX:NewRatio=2 -server -Dspringi.profiles.active=product -jar $DEPLOY_JAR >> /home/ec2-user/deploy.log 2>/home/ec2-user/deploy_err.log &

 

Code Pipeline으로 배포환경을 구축할 때 발생했었던 에러

  • 평소 Maven Wrapper을 사용해서 ./mvnw로 빌드했는데, 이게 안먹는거 아닌가;
    • ./mvnw은 안되는 걸로... (아무리 검색해봐도 관련 자료가 없다 ㅠㅠ)
    • mvn package로 하자..
  • '기본 manifest 속성이 없습니다' 오류
    • Code Pipeline에서는 모든 과정이 오류 없이 Success가 떴는데, 서버가 실행되지 않았다.
    • 문제는  jar파일이 처음부터 잘못 만들어진 것인데
    • 원인은 'spring-boot-maven-plugin' 플러그인 추가를 하지 않았던 것
      • spring-boot-maven-plugin
        • 실행 가능한 jar 파일을 만들어 준다. 
          • 실행가능한 jar파일
            • 컴파일된 클래스 뿐만 아니라 코드 실행에 필요한 모든 jar dependency들을 포함하여 압축한다. 
        • 의존하고 있는 라이브러리 들을 추가해준다. 
        • BOOT-INF에 컴파일된 class 파일을 저장하는데, JarLauncher는 이 BOOT-INF 구조에 있는 class파일과 .jar파일을 로딩한다. 

 

 

오늘 사수님께 공유받은 이직과 관련된 포스팅을 보고 급 반성을 하게 되었다.

아직 신입 꼬리표도 못뗀 주제에 너무 안일하게 시간을 보낸 것 같다는 생각이 들며,

그동안의 나를 되돌아보며 스스로를 질책했다. 

 

우선, 자바 기본책을 다시 정독하기 위해 자바의 정석 3rd edition을 새로 구입하여 두세번 읽어보기로 다짐. 

기본도 정확히 모르는 주제에 무슨 개발을 하겠다고 설쳐댄건지.

 

내가 정확하게 다 알 수 있다고 자신있게 말하려면

최소한 기본책에 나온 어떤 내용에 대한 질문에도 짧고 간결하게 답할 수 있는 정도. (구구절절? -> NO)

그 정도가 되었다는 자신이 생겼다면 지금 사두고 잘 읽지 못하고 있는 '모던 자바 인 액션'을 읽기로 하겠다. 

 

 

Anyway,

지금 어쨌든 회사에서 당장 개발해야 될 것들이 있기 때문에 책이 오기 전까지 Spring이 무엇인지, 그리고 동작원리는 어떻게 되는지 짧게라도 짚고 개발을 해야 죄책감이 아주 조금이라도 덜 것 같기에 내용이 잘 정리된 포스팅을 찾고, 그 내용을 기반으로 내 방식대로 다시 정리해보았다. 아래 나온 모든 내용은 맨 하단에 나온 출처로 부터 나온 내용!

 

 

 

  • 스프링이란?
    • IoC와 AOP를 지원하는 경량의 컨테이너
  • 컨테이너란?
    • 객체의 생성, 관리를 담당하고, 객체를 운용하는 데 필요한 기능을 제공한다. 
  • 스프링에서 컨테이너 역할을 하는 것은 ?
    • BeanFactory
    • ApplicationContext (BeanFactory 상속)
  • 두 컨테이너의 차이점은?
    • BeanFactory 
      • applicationContext.xml (스프링 설정 파일) 에 등록된 bean 객체를 생성 및 관리
      • 클라이언트로부터 요청이 들어올 때만 객체를 생성한다. 
    • ApplicationContext
      • BeanFactory의 객체 생성 및 관리 + 트랜잭션 관리 / 메시지 기반의 다국어처리 지원 
      • 컨테이너가 구동되는 시점에 bean에 등록된 클래스를 객체화 한다. 
      • GenericXmlApplicationContext -> ApplicationContext를 구현한 대표 클래스
  • 스프링의 특징
    • POJO (Plain old java object)
      • 예전에는 자바로 웹애플리케이션을 개발하려면 Servlet클래스를 상속받아 구현해야 했는데 이 Servlet이 POJO가 아니었다. 그런데 스프링을 사용하면 POJO만으로 웹 애플리케이션을 구축할 수 있게 되었다. Servlet클래스를 모두 추상화하여 라이브러리로 들어갔기 때문에. XML이나 다른 설정으로 Servlet을 이요하면 된다.  
    • IoC (Inversion of Control)
      • 객체 생성을 자바 코드로 직접 처리하는 것이 아니라, 컨테이너가 대신 처리한다. 
        • 제어권이 사용자가 아니라 프레임워크에 있다.
      • 객체와 객체 사이의 의존 관계 또한 컨테이너가 대신 처리한다. 
    • DI (Dependency Injection)
      • 의존성
        • A 객체에서 B 객체의 변수나 메소드를 사용해야 할 경우,  A라는 객체 생성자에서 new B();를 해야한다. 이 때 A는 B에 의존한다고 볼 수 있다. 
      • 의존성 주입
        • A라는 객체에서 B를 생성하는 것이 아니라 외부에서 생성된 B를 A에 주입함으로써 의존 관계를 없앤다.
      • 의존성 주입 방법
        • XML 방법
          • 생성자<constructor-arg> 태그 + ref 속성
          • 속성 <property> + name 속성  
        • Annotation 방법
          • @Autowired / @Resource 
    • AOP (Aspect of Programming)
      • 트랜잭션이나 로깅, 보안과 같이 여러 모듈에서 공통적으로 사용하는 기능의 경우, 핵심로직에서 분리하여 관리한다. 
      • AOP 방법
        • XML 방법
        • AOP 방법 
          • @Aspect / @Before
    • 스프링 동작 원리 (출처에 표시된 URL에 가시면 자세한 설명 보실 수 있습니다!!!)

웹어플리케이션 실행 -> WAS가 web.xml 실행 -> ApplicationContext를 생성하는 ContextLoaderListener가 web.xml에 등록된 내용에 따라 생성 -> root-context.xml에 등록된 Spring Container가 구동되며 필요한 객체들 생성 -> client요청 들어옴 -> DipatcherServlet 생성 -> 알맞은 Page Controller에게 전달하고 응답을 어떻게 할지 결정 -> Dispatcher Servlet은 servlet-context.xml을 loading -> 두번째 Spring Container가 구동될 때 첫번째 Container가 구동되며 생성된 DAO, VO, ServiceImple클래스들과 협업하여 작업 처리 

https://asfirstalways.tistory.com/334

 

Spring 의 시작, 프레임워크의 구성요소와 동작원리

Spring Framework의 구성요소와 동작원리 POJO 스프링의 특징을 살펴보면 POJO라는 단어가 등장한다. POJO란 Plain Old Java Object로 직역하자면 평범한 옛날 자바 객체이다. 말 그대로 자바 객체인 것이다. 이..

asfirstalways.tistory.com

 

 

+ Recent posts