
서론
커리어 전반적으로 다양한 언어, 프레임워크를 배포 및 운영해왔습니다.
가장 많은 것이 Java + SpringBoot 였음에도 정확히 설명하기 어려웠습니다.
“DevOps 엔지니어로서 SpringBoot를 얼마나 이해하고 있을까?”에 대한 답을 위해,
이번 문서를 작성하면서 Java 아키텍처, Java 빌드툴, Spring 생태계 등의 기본적인 개념을 확립할 수 있었습니다.
🚫
이 문서는 단순히 "겉햝기"에 치중한 문서일 확률이 높으며, 최대한 많고 다양한 참고자료를 확인하였음에도 진위여부를 확신할 수 없습니다.따라서 전체적인 개요라고 이해하고 이 문서를 읽어주시면 감사드리겠습니다.
Spring
Spring Ecosystem이란?
Spring Framework란?
Spring이란?
Spring Framework, SpringBoot 전후
Spring Ecoystem이란?
Spring은 여러 가지 Spring Project들의 모음을 의미합니다.
모든 Spring Project들은 Spring Framework를 기반으로 동작합니다.
Spring Framework란?
공식 문서에서는 SpringBoot Framework의 2가지 이점을 소개하고 있습니다.
Java 기반의 어플리케이션을 만들 수 있다.
개발자들은 비즈니스 로직에 집중할 수 있다.
Spring Framework는 초기 설정*만 잘 해두면
비즈니스 로직과 어플리케이션의 분리를 편리하게 할 수 있는 장점이 있습니다.
2000년대 초반의 엔터프라이즈 급 프레임워크인 EJB*는
비싼 비용과 강한 종속성으로 인한 객체 지향 적용이 어려운 문제가 있었습니다.이후 무료고 객체 지향 적용이 원활한 Spring Framework가 주류가 되어갔습니다.
하지만 Ecosystem이 거대해지고 오픈소스를 같이 사용하게 되면서
초기/추가 설정이 많아지고 복잡해지게 되었습니다.
SpringBoot란?
공식 문서에서는 SpringBoot를 다음과 같이 소개하고 있습니다.
SpringBoot를 사용하면 Spring 기반 어플리케이션을 쉽게 만들 수 있다.
Spring Framework의 복잡한 설정*을 SpringBoot가 해준다.
내장 서버
편리한 의존성 & 권장 버전 관리
자동 설정
Spring Framework, SpringBoot 전후
외장/내장 서버
AS-IS
WAS를 설치한다.
개발한 웹 어플리케이션 코드를 WAR로 빌드한다.
빌드한 WAR 파일을 WAS 폴더 하위에 넣는다.
WAS를 실행한다.
TO-BE
JAR 파일로 패키징하여 main 메소드로 바로 실행 가능
코드 보기
@SpringBootApplication public class MySpringApplication { public static void main(String[] args) { SpringApplication.run(MySpringApplication.class,args); } }
편리한 의존성 & 권장 버전 관리
AS-IS
코드보기
dependencies { // Spring Web MVC implementation 'org.springframework:spring-webmvc:6.0.4' // Tomcat implementation 'org.apache.tomcat.embed:tomcat-embed-core:10.1.5' // ... }TO-BE
코드 보기
plugins { id 'io.spring.dependency-management' version '1.1.0' } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-jdbc' }
자동 구성
TO-BE : 일반적으로 자주 사용하는 수많은 빈들을 자동으로 등록해줌
코드 보기
@Repository public class ProductDao { private final JdbcTemplate jdbcTemplate; public ProductDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } }
Build Tool
Java에서 사용 가능한 Build Tool 3가지를 소개합니다.
(재직 중에는 대부분 Gradle Wrapper를 사용하였다…)
Maven이란?
Gradle이란?
Gradle Wrapper란?
Maven이란?
Java의 대표적인 관리 도구인 Ant를 대체하기 위해 개발됨
프로젝트 외부 라이브러리를 쉽게 참조할 수 있게 pom.xml 파일로 명시하여 관리
참조한 외부 라이브러리에 연관된 다른 라이브러리도 자동으로 관리됨
Maven 대표 태그 설정
- modeVersion : maven의 버전을 의미
- groupld : 프로젝트 그룹 id를 뜻하며, 일반적으로 대표하는 사이트 도메인을 역순으로 적어 사용
(ex: thinkground.studio -> studio.thinkground) - artifactid : groupId외에 다른 프로젝트와는 구분될 수 있는 프로젝트의 Id를 작성
- version : 프로젝트의 버전을 의미하며 개발 단계에 따라 구분하여 작성
- name: 프로젝트의 이름
- description : 해당 프로젝트의 간략한 설명을 작성
- properties : pom.xml 파일 내에서 빈번하게 사용되는 중복 상수를 정의하는 영역
해당 영역의 상수를 사용하기 위해서는 $[태그명)} 의 형태로 사용하면 됨 - dependendies : 해당 프로젝트에서 의존성을 가지고 사용하는 라이브러리를 정의하는 영역 각 라이브러리마다
태그를 사용하여 구분 - build : 프로젝트 빌드와 관련된 정보를 설정하는 영역
Gradle이란?
Groovy 스크립트를 활용한 빌드 관리 도구
안드로이드 프로젝트의 표준 빌드 시스템으로 채택
멀티 프로젝트(Multi-project)의 빌드에 최적화 되어 설계됨
Maven에 비해 더 빠른 처리 속도를 가지고 있음
Maven에 비해 더 간결한 구성이 가능함
Gradle 대표 용어 설명
- repositories : 라이브러리가 저장된 위치 등 설정
- mavenCentral : 기본 Maven Repository
- dependencies : 라이브러리 사용을 위한 의존성 설정
Gradle Wrapper란?
Gradle을 각 개발자나 CI 서버에 설치하지 않고 프로젝트에 포함시켜 배포하는 것
./gradlew (Linux/MacOS) 및 ./gradlew.bat (Windows)를 실행하면 필요한 종속성을 알아서 설치하고 빌드가 가능하다.
명령어들
Python의 pip,pip3와 Node.js npm,yarn,pnpm은 일반적으로 아래로 실행합니다.
yarn install
yarn build
yarn start
하지만 Maven, Gradle, Gradle Wrapper는 모두 자동으로 종속성을 설치합니다.
예를 들어 Gradle Wrapper의 경우 아래 구문에 설치도 모두 포함됩니다.
./gradlew bootJar
아래는 개발용 실행, 클린 빌드, War/Jar 생성, War/Jar 실행 등의 구문 모음입니다.
사실과 다른 부분이 있을 수 있어 반드시 확인 절차 이후 실행해주세요.
목적 | Maven | Gradle | Gradle Wrapper |
---|---|---|---|
개발용 실행 | mvn spring-boot:run | gradle bootRun | ./gradlew bootRun |
클린 빌드 | mvn clean | gradle clean | ./gradlew clean |
배포용 War 생성 | mvn package -P war | gradle bootWar | ./gradlew bootWar |
배포용 Jar 생성 | mvn package | gradle bootJar | ./gradlew bootJar |
War 실행하기 | cp target/*.war $TH/webapps/ | cp build/libs/*.war $TH/webapps/ | cp build/libs/*.war $TH/webapps/ |
Jar 실행하기 | java -jar target/*.jar | java -jar build/libs/*.jar | java -jar build/libs/*.jar |
의존성 트리 확인하기 | mvn dependency:tree | gradle dependencies | ./gradlew dependencies |
Bin/Lib [Adv.]
SpringBoot를 사용하더라도 다양한 bin(lib)을 추가로 설치하게 됩니다.
요구사항에 맞춰 SpringBatch, SpringSecurity, SpringCloud, Spring LDAP
네트워크 연결을 위해 WebClient, WebSockets, Reactive, WebFlux
[project] SpringBatch, Quartz Scheduler
Spring Batch는 대용량 데이터 처리에 사용되며 스케줄링 기능은 지원하지 않습니다.
Quartz Shceduler 등을 통해서 스케줄링 기능을 추가할 수 있습니다.
💡
인프라 레이어에서 스케줄링을 제어하는 것도 합리적으로 보입니다. 예를 들어, Linux cron 이나 Kubernetes CronJob을 사용할 수 있습니다. 다만, SpringBatch Warm-Up 단계의 리소스가 반복적으로 소모되기에, 요구사항을 확인하고 배치의 유후 및 실행 빈도에 따라 결정할 수 있어 보입니다.
용어 설명을 덧붙이면 Job, JobInstance, JobExecution 등이 존재합니다.
Job : Job 이름 정의 > Step 정의 > Step 순서 정의 > Job 재사용 가능 정의
1개 혹은 그 이상의 Step으로 구성됨, Step은 StepExecution을 가짐
Job Instance : 논리적으로 Job 실행
Job Parameter를 통해서 Job Instance 들을 구분
Job Instance = Job + identifying Job Parameter
Job Execution
Job을 실행하는 단일 시도
실패했던 Job Instance에 대해 새로운 실행을 하면
새로운 Job Execution이 실행됨
JobExecution에서는 몇가지 Properties를 통해서 상태를 표기하고 있습니다.
(Batch) Status | EXIT_CODE | EXIT_MESSAGE |
---|---|---|
FAILED | FAILED | org.springframework.dao.InvalidDataAccess… |
FAILED | FAILED | org.springframework.dao.InvalidDataAccess… |
COMPLETED | COMPLETED | - |
COMPLETED | NOOP | All steps already completed or no steps config… |
StepExecution에서는 몇가지 Propreties를 통해서 상태를 표기하고 있습니다.
Key | Example Value |
---|---|
STEP_EXECUTION_ID | 1 |
VERSION | 3267 |
STEP_NAME | crawlingStep |
JOB_EXECUTION_ID | 1 |
START_TIME | 2020-09-14 18:29:53 |
END_TIME | - |
STATUS | STARTED | FAILED |
COMMIT_COUNT | 325 |
그 외에는 JobRepository, JobLauncher 등을 가지고 있습니다.
JobRepository : Job, Step을 구현하기 위한 CRUD 작업 제공
JobLauncher : Job을 시작하기 위한 간단한 인터페이스
[project] SpringSecurity
Spring Security는 인증/인가, 데이터 보호 및 방어 등을 지원하는 프레임워크입니다.
*Filter라는 개념을 사용해서 Request에 대한 인증/인가 등의 기능을 제공합니다.
일반적인 경우 : Request → Spring Controller
Spring Security의 경우 : Request → Filter → Spring Controller
[project] SpringCloud
Spring Cloud는 클라우드 네이티브 어플리케이션을 만들기 위한 프로젝트입니다.
💡
인프라 레이어에서 이를 구축하는 것이 합리적인 것 같습니다. Kubernetes Service, Headless Service 등으로 다양한 서비스를 노출시키고 복잡한 L4, L7 라우팅 전략이 필요한 경우 Istio, Envoy를 사용할 수 있습니다.
분산 시스템을 구축하기 위한 Service Discovery/Registry를 포함한 다양한 기능이 어플리케이션 레벨에서 이루어지게 할 수 있습니다.
Service Registry : 마이크로서비스들의 위치를 등록하고 검색하도록 지원
API Gateway : 클라이언트 요청을 적절한 서비스로 라우팅
Config Server : 마이크로서비스 환경설정을 외부 저장소에 저장
[network] Dispatcher Servlet
Spring DispatcherServlet은 HTTP 프로토콜의 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러(Front Controller)입니다.
[network] WebSockets
Spring WebSockets은 실시간 양방향 통신을 제공하며 Socket 통신을 사용합니다.
WebSocket을 사용할 수 없는 경우, SockJS 기반 폴링 기반 대체 통신이 가능합니다.
STOMP*과 함꼐 사용하여메세지 브로커 기반의 구조를 사용할 수도 있습니다.
💡
STOMP*(Simple Text Oriented Messaging Protocol) 메세지 큐, 브로커 종류의 서비스들 중에서 STOMP을 지원하는 대상만 사용 가능 RabbitMQ, ActiveMQ 등이 대표적으로 STOMP을 지원하는 브로커 서비스
[network] WebFlux
Spring WebFlux는 Spring 5에서 추가된 Reactive Programming 기반의 Non-blocking Web Framework로서 대규모 동시 요청 처리에 적합합니다. Network I/O, Aggregation I/O가 많은 경우에 적합합니다.
[db] JDBC
Spring Data JDBC는 데이터베이스와 통신하는 표준 API입니다.
Spring Data JDBC는 ORM 없이 간단하게 SQL 기반 데이터 액세스를 지원한다.
도메인 객체와 데이터베이스 테이블 간의 단순한 매핑에 집중한다.
복잡한 연관 관계보다는 명시적 SQL 쿼리와 트랜잭션 관리에 최적화되어 있다.
[db] JPA
Spring Data JPA는 ORM 기반 프레임워크로 Hibernate 등을 활용합니다.
Spring Data JPA는 JPA 사양을 따르는 ORM 프레임워크로, 객체와 관계형 데이터베이스를 매핑한다.
Hibernate 같은 구현체를 활용해 복잡한 연관 관계와 캐싱, 지연 로딩 등의 기능을 제공한다.
강력한 기능을 지원하지만 설정과 관리가 다소 복잡할 수 있다.
[db] R2DBC
Spring Data R2DBC는 Reactive Programming을 지원하는 비동기 논블로킹 방식의 데이터베이스 접근 기술입니다.
Spring Data R2DBC는 리액티브 프로그래밍을 위한 비동기, 논블로킹 데이터베이스 액세스 기술이다.
리액티브 스트림을 활용해 효율적인 리소스 관리와 고성능 처리를 지원한다.
주로 마이크로서비스나 고부하 환경에서 동기 방식의 한계를 극복하기 위해 사용된다.
💡
Reactive Programming이란 방식 자체가 공감이 크게 되지 않는다. Node.js에서는 대부분의 친구들이 비동기, 논 블로킹 방식으로 작동하는데 Java에서는 데이터 처리 방식, 스레드 동작 방식의 기본값이 다른가 보다.
Theory [Adv.]
추가로 Java Application 운영을 위해서 몇 가지 심층적인 개념이 필요합니다.
💡
.java 파일 작성 → .class 파일 생성 (javac compile) → JVM process...
Java 동기화 비동기
JVM 메모리 구조
JVM 병렬성 설정과 ActiveProcessorCount
GC(Garbage Collection) 튜닝
Java 아키텍처
Java는 기본적으로 싱글 스레드 기반 동기 아키텍처에서 작동합니다.
대량의 작업 처리를 위해서는 멀티 스레딩을 활용할 수 있습니다.
스레드를 다루기 위해서 Thread 클래스, Runnable 인퍼테이스를 활용하거나
Executor Framework를 활용할 수도 있습니다. 다만 SpringBoot에서는 대부분 완성된 어노테이션을 통해서 활용할 수 있지 않을까 예상해 봅니다.
💡
Node.js가 이벤트 기반 비동기 아키텍처에서 작동하는 것과 상반됩니다. 이는 작업 효율성을 위해 EventLoop + Callback/Promise를 사용하지만, 작업의 효율성을 위해서 Event Loop + Callback/Promise를 사용하지만,
Java 동기와 비동기
Java는 기본적으로 동기적으로 작동하지만, 비동기 또한 지원하고 있습니다.
(Feature, CompletableFuture, @Async Annoation 등)
SpringBoot에서 지원하는
Reactive Programming은 비동기에 Non-Blocking I/O를 더한 기술이며
Aggregation, Network I/O가 많은 경우에 효율적으로 작동합니다.
JVM(자바 가상 머신) 동작 방식
JVM은 Java 앱을 Class Loader로 읽고 Java API와 함꼐 실행합니다.
Class Loader : 동적 로딩을 통해 필요한 클래스들을 로딩 및 링크하여 Runtime Data Area에 올린다.
Runtime Data Area : 실질적인 메모리 할당 및 관리 영역으로 Byte Code가 저장되어 있습니다.
Execution Engine : Runtime Data Area에 올라간 Byte Code를 해석합니다.
[사진 9] Java Workflow
Class Loader, Runtime Data Area, Execution Engine은 세분화할 수 있습니다.
위 도식에 나와있는 모든 항목을 간단하게 요약하면 다음과 같습니다.
런타임 환경에서 선형적 부하를 일으킬 가능성이 있는 친구들을 경계해야 하며,
일반적으로 Heap, Garbage Collector에 대해서 확인할 필요가 있어 보입니다.
💡
1번의 요청에 1개의 클래스가 생성되어 Heap에 저장되고 1번의 응답 이후에도 Garbage Collector가 이를 회수하지 못한다면 N번의 요청이 오면 N개의 클래스가 Heap에 저장되는 선형적 부하가 발생한다.
💡
Runtime Data Areas는 일반적으로 1개만 존재하지만 PC Register, JVM Stack, Native Method Stack은 Thread 별로 존재합니다. 즉, 멀티스레딩을 통해 N개의 스레드를 만들면 세 개 모두 N 배율로 생성됩니다.
Component | Type | 설명 | |
---|---|---|---|
Class Loader | Loading | .class 파일을 JVM 메모리에 로드 | |
Linking | Verifying | 로드된 .class이 JVM 명세와 일치 여부 확인 | |
Preparing | 로드된 .class에 필요한 상수들을 메모리에 할당 | ||
Resolving | 로드된 .class의 모든 심볼릭 레퍼런스를 다이내믹 레퍼런스로 변경 | ||
Initializing | .class 변수들을 적절한 값으로 초기화 | ||
Runtime Data Areas | Method Area | JVM에서 읽어들인 | |
Runtime Constant Pool | JVM에서 읽어들인 아래 항목의 실제 주소값 | ||
Heap | 런타임에서 동적으로 데이터를 할당받는 위치 | ||
PC Register [Thread] | 실행 중인 JVM 주소를 가지고 있으며 | ||
JVM Stack [Thread] | 스레드 수행 정보를 저장하는 선입후출(FILO) | ||
Native Method Stack [Thread] | 자바 외의 네이티브 코드를 위한 메모리 | ||
Execution Engine | JIT Compiler | Byte Code를 컴파일해 Native Code로 변환 | |
Interpreter | Byte Code 명령어를 하나씩 읽고 해석하여 실행 | ||
Garbage Collector | Heap에서 사용하지 않는 메모리 회수 |
Java에서는 타입의 종류에 따라서 서로 다른 장소에 저장이 됩니다.
Primitive Type : JVM Stack에 데이터가 저장됨, Heap을 참조하기도 함
Reference Type : Heap에 데이터가 저장됨
💡
Java은 Primitive Type, Reference Type, Null Type이 있고 이 문서에서는 Primitive, Reference 만을 포함한 설명을 하고 있습니다.
Heap에 저장된 Primitive Type은 JVM Stack의 참조 유무로 사용이 결정되며,
참조 유무 등에 따라서 Heap의 5가지 공간 중 하나에 저장되게 됩니다.
이때 Garbage Collector는 Heap의 Old Generation을 주기적으로 청소합니다.
나아가서 Heap과 JVM Stack의 관계에 대해서도 생각할 필요가 있습니다.
Primitive Type이 회수되기 위해 JVM Stack에서 일정 시간 참조되지 않아야 합니다.
하지만 멀티 스레딩 환경에서 Java를 사용하면
실행한 스레드의 숫자만큼 JVM Stack 등이 생성되게 됩니다.
즉, 잘못 작성된 코드는 JVM Stack 간에서 동일한 Heap을 가리킬 수 있고
Garbage Collector에 회수되지 못하는 Primitive Type이 생길 수 있습니다.
JVM 병렬성 설정과 ActiveProcessorCount
JVM은 병렬성 상한을 위한 파라미터(ActiveProcessorCount)를 가집니다.
구현체나 프레임워크에 따라 이 파라미터는 vCPU 수량과 비례하는 자동값을 가지기도 합니다. 즉, 프로세스의 작업 속도를 최대한 보장하기 위해서 병렬성 상한을 높이는 기법으로 보입니다.
따라서 특정한 운영환경*에서는 병렬성 설정의 값을 별도로 지정함이 좋습니다.
💡
Kubernetes에서 컨테이너의 Request를 2, Limits 4로 할당했으며 스케쥴링된 워커 노드의 vCPU가 20이라고 생각해보고 ActiveProcessorCount의 값이 20이라고 설정되어 있는 경우 대량의 Throtlling을 유발할 수 있어 보입니다.
References.
Spring Ecosystem
Build Tool
Bin/Lib
Project
Network
DB
Theory