OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000fbe00000, 27262976, 0) failed; error='Not enough space' (errno=12)
위에 러는 말그대로 메모리가 충분하지 않아 발생한다.
해결책으로는 여러가지 방법이 있는걸로 화인되었다.
시스템의 메모리 로드 줄이기
물리적 메모리 또는 SWAP 공간 늘리기
SWAP 백킹 저장소가 가득차 있는지 확인
64비트 OS 64비트 Java 사용
JAVA 힙 크기 줄이기 (-Xmx/ -Xms)
java 스레드 수 줄이기
java 스레드 스택 크기 줄이기 (-Xss)
다양한 방법이 있는데 나의 겨우 swap 공간을 확인후 할당하는 방향으로 해결하기로 했다.
swap 이란?
스왑은 Linux 기반 운영 체제에서 가상 메모리로 작동하는 저장 장치(예: HHD, SSD, 가상 저장 장치)의 전용 공간이다. 시스템의 사용 가능한 메모리가 부족할 때 물리적 RAM(Random Access Memory)을 보충하는 데 사용된다. 스왑 공간을 통해 운영 체제는 덜 자주 사용되는 데이터를 RAM에서 스왑 영역으로 이동하여 더 중요하거나 자주 액세스하는 데이터를 위해 RAM의 공간을 확보할 수 있다.
스왑 공간은 스왑 파티션 또는 스왑 파일의 형태일 수 있다. 스왑 파티션은 저장 장치의 전용 파티션인 반면 스왑 파일은 기존 파일 시스템 내의 파일이다. 둘 다 동일한 목적을 수행한다.
이전에 다니던 회사에서는 배포시 test 코드 실행에 대해 큰 걱정을 하지 않았는데 야믈 파일에 환경변수가 설정이 되어 배포 되는 서버에서 환경번수를 넣어주기떄문에
개발 서버 빌드시 개발 config 환경변수가 들어가고 운영서버 빌드 시에는 운영 config 환경변수가 들어오는
현재는 빌드시 해당 지정 profile을 읽고 있다.
이떄 문제가 빌드시에는 default profile 을 읽고 있기떄문에 test 코드 실행시 문제가 되기 때문에 test 코드 실행 시 동적으로 읽을 필요가 있다.
아래는 실제 Test 실패로 나온 경우다.
> Task :test
AsyncExceptionHandlingTest > 슬랙 알람 확인 FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:658
Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:185
Caused by: org.redisson.client.RedisConnectionException at ConnectionPool.java:153
Caused by: java.util.concurrent.CompletionException at CompletableFuture.java:368
Caused by: io.netty.channel.ConnectTimeoutException at AbstractNioChannel.java:261
HowserPartnerApiApplicationTests > contextLoads() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:658
Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:185
Caused by: org.redisson.client.RedisConnectionException at ConnectionPool.java:153
Caused by: java.util.concurrent.CompletionException at CompletableFuture.java:368
Caused by: io.netty.channel.ConnectTimeoutException at AbstractNioChannel.java:261
HowserTemplateTest > 쿠팡 OpenApiKey 유효성 테스트 FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:658
Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:185
Caused by: org.redisson.client.RedisConnectionException at ConnectionPool.java:153
Caused by: java.util.concurrent.CompletionException at CompletableFuture.java:368
Caused by: io.netty.channel.ConnectTimeoutException at AbstractNioChannel.java:261
문제를 해결할 방법을 찾아보니 여러 방법이 있지만
일단 현 시점에 가장 맞는 2가지 방법을 찾았다.
1. ActiveProfilesResolver를 이용하여 test 코드 빌드 시 동적 profile을 적용하도록 변경
먼저 profile 을 동적으로 적용해주기 위해 ActiveProfilesResolver를 구현
import org.springframework.test.context.ActiveProfilesResolver;
public class SpringActiveProfilesResolver implements ActiveProfilesResolver {
private static final String SPRING_PROFILES_ACTIVE = "spring.profiles.active";
@Override
public String[] resolve(Class<?> testClass) {
String property = System.getProperty(SPRING_PROFILES_ACTIVE);
return new String[] {property};
}
}
//위 파일을 넣기 위해서는 dependency spring test 추가 필요
이는 setTimeout 함수에 대해 타이머 작업 완료 여부를 신경 쓰지 않고 바로 그 다음 콘솔 작업을 수행하였기 때문이다. 그리고 setTimeout 함수의 타이머 작업 완료 알람을 콜백 함수를 통해 값을 받아 출력하였다. 따라서 setTimeout 은 비동기(Asynchronouse)이다.
setTimeout 함수는 자신의 타이머 작업을 수행하기 위해 메인 함수를 블락하지 않고 백그라운드에서 별도로 처리되었다. 메인 함수를 블락하지 않으니 setTimeout 함수를 호출하고 바로 그 다음 콘솔 함수를 호출한 것이다. 따라서 setTimeout 은 논블로킹(Non-blocking)이다.
3. 동기/비동기 + 블로킹 논블로킹
Sync Blocking(동기 + 블로킹)
Async Blocking(비동기 + 블로킹)
Sync Non-Blocking(동기 + 논블로킹)
Async Non-Blocking(비동기 + 논블로킹)
Sync Blocking
다른 작업이 진행되는 동안 자신의 작업을 처리하지 않고(Blocking) 다른 작업의 완료 여부를 바로 받아 순차적으로 처리하는 방식
Async Non-Blocking
다른 작업이 진행되는 동안에도 자신의 작업을 처리하고( Non Blocking), 다른 작업의 결과를 바로 처리하지 않아 작업 순서가 지켜지지 않는 Async 방식
Sync Non-Blocking
다른 작업이 진행되는 동안에도 자신의 작업을 처리하고( Non Blocking)
다른 작업의 결과를 바로 처리하여 작업을 수낯대로수행하는 sync 방식
async Blocking
다른 작업이 진행되는 동안 자신의 작업을 멈추고 기다리는( Blocking), 다른 작업의 결과를 바로 처리하지 않아 순서대로 작업을 수행하지 않는 Async 방식 (실제 로 sync blocking 과 큰 차이가 없고 작업할때 볼일이 없을 듯 하다).
spring @Transaction,@Async 2가지 어노테이션을 사용법이나 주의 사항을 정리하고자 한다.
@Transactional, @Async 흔하게 많이 쓰는 어노테이션이다.
Trasction 이란 데이터베이스의 상태를 변화시키기 위해서 수행하는 작업의 단위를 뜻한다.
Spring에서 트랜잭션 관련된 3가지 핵심
1. 동기화
2. 추상화
3. AOP이용한 트랜잭션 분리
동기화
트랜잭션을 시작하기 위한 connection 객체를 특별한 저장소에 보관해두고 필요할때 꺼내쓸 수 있도록 하는 기술
//트랜잭션 매니저 시작
TransactionSynchronizeManager.initSynchronization();
Connection c = DataSourceUtils.getConnection(dataSource);
//종료
DataSourceUtils.releaseConnection(c, dataSource);
TransactionSynchronizeManager.unbindResource(dataSource);
TransactionSynchronizeManager.clearSynchronization();
트랜잭션 동기화 저장소는 작업 쓰레드마다 Connection 객체를 독립적으로 관리, 멀티쓰레드 환경에서도 충돌이 발생할 여지가 없다.
*Hibernate 에서는 Connection이 아닌 Session 을씀 이로 인한 JDBC 종속적인 트랜잭션 동기화 코드들을 문제를 유발
이를 해결하기 위해 아래 기술할 추상화 기술 제공.
추상화
Spring은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술 제공. 이를 통해 JDBC, JPA, Hobernate 등 종속적인 코들르 이용하지 않고 일관되게 트랜잭션을 처리 가능.
트랜잭션 경계설정을 위한 추상 인터페이스는 Platform TransactionManager 이다.
AOP
흔히 우리가 쓰는 @Transactional 어노테이션이다. 이 어노테이션을 사용 함으로써 마치 트랜젹션 코드가 존재하지 않는 것 처럼 보이지만 실제로 트랜잭션이 적용이 되는것이다.
트랜잭션 세부 설정
Spring의 DefaultTrasactionDefinition이 구현하고 있는 TrasctionDefinition 인터페이스는 트랜잭션의 동작방식에 영향을 줄 수 있는 네 가지 속성을 정의 하고 있다. 아래 4가지 속성은 세부적으로 이용할 수 있게 도와주며, @Transactional 어노테이션에도 공통적으로 적용할 수 있다.
1. 트랜잭션 전파
2. 격리수준
3. 제한시간
4. 일기전용
트랜잭션 전파
트랜잭션 전파란 트랜잭션의 경계에서 이미 진행중인 트랜잭션이 있거나 없을 때 어떻게 동작할 것인가를 결정하는 방식을 의미.
예를 들어 A 작업에 대한 트랜잭션이 진행중이고 B 작업에 대한 트랜잭션을 어떻게 처리할까에 대한 부분이다.
A의 트랜잭션 참여 (PROPAGATION_REQUIRED)
B의 코드는 새로운 트랜잭션을 만들지 않고 A에서 진행중인 트랜잭션에 참여할 수 있다. 이 경우 B의 작업이 마무리 되고 나서, 남은 A의 작업(2)를 처리할 때 예외가 발생하면 A와 B의 작업이 모두 취소된다. 왜냐하면 A와 B의 트랜잭션이 하나로 묶여있기 떄문이다.
독립적인 트랜잭션 생성(PROGATION_REQUIRES_NEW)
반대로 B의 트랜잭션은 A의 트랜잭션과 무관하게 만들 수 있다. 이 경우 B의 트랜잭션 경계를 빠져나오는 순간 B의 트랜잭션은 독자적으로 커밋 또는 롤백되고, 이것은 A에 어떠한 영향도 주지 않는다. 즉 이후 A가 (2)번 작ㅇ버을 하면서 예외가 발생해 롤백되어도 B의 작업에는 영향을 주지 못한다.
트랜잭션 없이 동작(PROGATION_NOT_SUPPORTED)
B의 작업에 대해 트랜잭션을 걸지 않을 수 있다. B의 작업이 단순 데이터 조회라면 굳이 트랜잭션이 필요 없을 것이다.
격리 수준
모든 DB 트랜잭션은 격리 수준을 가지고 있어야한다. 서버에는 여러 개의 트랜잭션이 동시에 진행 될 수 있는데, 모든 트랜잭션을 독립적으로 만들고 순차 진행 한다면 안전하지만 성능이 떨어짐. 따라서 적절하게 격리수준을 조정해서 가능한 많은 트랜잭션을 동시에 진행시키면서 문제가 발생하지 않도록 제어해야 한다. 이는 JDBC 드라이버나 DataSource 등에서 재설정하고 트랜잭션 단위 로 격리 수준을 조정할 수 있다.
DefualtTrasactionDefinition에 설정된 격리 수준은 ISOLATION_DEFAULT로 DataSource에 정의된 격리 수준을 따른다는 것이다.
기본적으로는 기본 격리 수준을 따르는 것이 좋지만. 특별한 작업을 수행하는 메소드라면 독자적으로 지정해줄 필요가 있다.
제한시간
트랜잭션을 수행하는 제한시간을 설정할 수 있다. 제한시간의 설정은 트랜잭션을 직접 시작하는 PROGATION_REQUIRED나 PROGATION_REQUIRES_NEW의 경우에 사용해야만 의미가 있다.
읽기전용
읽기전용으로 설정해두면 트랜잭션 내에서 데이터를 조작하는 시도를 막아주며 데이터 엑세스 기술에 따라 성능 향상이 된다.
(재직중인 곳이 아니더라도 보통 SELECT 문 을 처리할떄 slaveDB에 읽기전용으로 붙어서 쓰고 있다.)
@Async
@Async는 spring 에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 어노테이션이다.
@Async로 annotationg된 bean 의 메소드는 별도의 스레드에서 실행
기존 JAVA 에서 비동기 방식으로 메서드를 구현할때 java.util.concurrent.ExecutorService을 활용해서 비동기 방식의 method를 정의 할 때마다, Runnable의 run()을 재구현해야 하는 등동일한 작업들의 반복이 잦았다.
그러나 @Async 사용 시 손쉽게 비동기 메서드 작성 가능
spring의 Async 지원은 JDK Proxy 또는 CGlib와 같은 객체를 사용하여 Async가 정의된 객체를 생성
그 후 Spring은 메소드의 로직을 별도의 실행 경로로 제출하기 위해 컨텍스트와 연결된 스레드 풀을 찾으려고 합니다. 구체적으로는, 고유한 TaskExcutor 빈이나 taskExcutor로 이름 지정된 빈을 검색
이러한 빈이 없으면 기본적으로 SimpleAsyncTaskExcutor를 사용
SimpleAsyncTaskExcutor는 스레드 풀이 아닙니다. 그렇기 때문에 스레드를 관리하고 재사용하는 것이 아니라 계속 만들어 냅니다.
스레드 자원이 많이 들기 때문에 SimpleAsyncTaskExcutor를 쓰지 말아야 합니다.
따라서 TaskExecutor를 빈으로 등록하여 사용하는것이 옮은 방법.
아래는 async config 중 일부이다.
@Configuration
@EnableAsync
public class AsyncThreadConfiguration {
@Bean
public Executor asyncThreadTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(5);
threadPoolTaskExecutor.setMaxPoolSize(10);
threadPoolTaskExecutor.setThreadNamePrefix("Executor-");
return threadPoolTaskExecutor;
}
}
스레드 생성 하는 경우
Rules for creating Threads internally by SUN:
the number of threads 가 corePoolSize보다 작으면 새로운 스레드를 생성
the number of threads 가 corePoolSize 같거나(또는 크다면), task를 큐에 넣습니다.
큐가 다 찼을 때 the number of threads가 maxPoolSize보다 작으면 새로운 스레드를 생성
큐가 the number of treads가 maxpoolsize보다 같거나(또는 크다면) 해당 task를 reject 한다.(RejectExcutionException)
주의사항
1. private 메서드 적용 불가 proxy로 만들 수 없기 떄문에
2. self-invocation 해서는 안됨 proxy로 만ㄷ루 서 없기 떄문에
3. return 값은 void 혹은 CompleteableFuture<>
간략하게나마 트랜잭션과 async 처리에 대해서 정리해보았는데 앞으로는 좀더 알고 사용 알 수 있을것 같다.
현재 스웨거 문서로만 적용이 되어 있는데 아무래도 문서 명세서로 보기에는 restdocs이 편할 듯하여 내가 주로 담당하는 리소스에 적용할려고 한다.
일단 스웨거와 restDocs 의 차이
Spring Rest Dcos
장점
실제 코드에 영향이 없음
테스트코드 성공시에만 문서작성이 완료됨.
단점
적용하기 어려움
Swagger
장점
API 테스트화면 제공
적용이 매우 쉬운편
단점
실제 코드(프로덕션)에 어노테이션 추가
실제코드와 동기화 안될 수 있음
와 같은 특징이 있지만 실제로 써본 결과 프론트 엔드 쪽 작업자가 있는 경우 스웨거가 있는것이 좀더 편하다고 하는사람이 많았다.
그러나 퇴사자 혹은 입사자가 많이 생기는 경우 restDoc 문서를 만들어 두는것이 파악하기에는 훨씬 좋다고 느껴져
2가지 다 적용이 되면 좋을 것 같다.
restDocs 작성 방법
작성에 앞서 몇까지 선택 해야하는 것들이 있는데
1. AsciiDoc VS Markdown => 일단 가이드 문서에 있는 adoc을 사용 했는데 markdown도 사용이 가능 한걸로 확인했다.
장점은 작성이 쉽다는데 공식문서를 따르기로 했다.
2. MockMvc(@WebMvcTest) VS Rest Assured(@SpringBootTest) => 문서 작성시 Mocking을 사용하여 작성
Rest Assured 는 BDD 스타일로 직관적이지만 별도의 구성없이는 @SpringBootTest 로 수행
전체 컨테스트를 로드하여 빈을 주입하기에 속도가 많이 느림.
MockMvc 는 @WebMvcTest 수행 controller Layer만 테스트하여 속도가 빠름
단순 컨트롤러 테스트만 할 경우 MockMvc가 좋고 아닌 경우 는 Rest Assured가 좋음
JUNIT 외에 Spock라는게 있는데 작업을 할 프로젝트에는 Jnut 이기때문에 고려하지 않기로 한다.
적용 Project 버전 확인
Sprting boot 2.6.7
java 17
gradle 7
build.gradle 설정 추가
실제 적용된 코드
id "org.asciidoctor.jvm.convert" version "3.3.2" //asciidoc 파일 변환 및 복사 플러그인
configurations {
asciidoctorExt
}
ext {// snippets 파일이 저장될 경로 변수 설정
snippetsDir = file('build/generated-snippets')
}
//dependency 추가
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.6.RELEASE'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.6.RELEASE'
test {// 테스트 실행 시 snippets를 저장할 경로로 snippetsDir 사용
outputs.dir snippetsDir
}
// API 문서 생성
asciidoctor {
dependsOn test
inputs.dir snippetsDir
attributes 'snippets': snippetsDir
}
tasks.register('copyDocument', Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
build {
dependsOn copyDocument
}
bootJar {
dependsOn asciidoctor
copy {
from "${asciidoctor.outputDir}"
into 'src/main/resources/static/docs'
}
}