Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ RUN chmod +x ./gradlew && ./gradlew --no-daemon clean bootJar -x test
FROM eclipse-temurin:17-jre

WORKDIR /app
ENV JAVA_TOOL_OPTIONS="-Xmx384m -XX:MaxRAMPercentage=75"
ENV JAVA_TOOL_OPTIONS="-Xms64m -Xmx256m -XX:+UseSerialGC -XX:ActiveProcessorCount=1 -XX:MaxMetaspaceSize=128m -XX:ReservedCodeCacheSize=64m -XX:MaxDirectMemorySize=32m -Xss256k -XX:+ExitOnOutOfMemoryError"
COPY --from=build /app/build/libs/*.jar app.jar

EXPOSE 8080
Expand Down
10 changes: 8 additions & 2 deletions docs/RENDER_FREE_DEPLOYMENT_RUNBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ spring:
```env
DB_MAX_POOL_SIZE=2
DB_MIN_IDLE=1
JAVA_TOOL_OPTIONS=-Xmx384m -XX:MaxRAMPercentage=75
JAVA_TOOL_OPTIONS=-Xms64m -Xmx256m -XX:+UseSerialGC -XX:ActiveProcessorCount=1 -XX:MaxMetaspaceSize=128m -XX:ReservedCodeCacheSize=64m -XX:MaxDirectMemorySize=32m -Xss256k -XX:+ExitOnOutOfMemoryError
SPRING_MAIN_LAZY_INITIALIZATION=true
SPRINGDOC_ENABLED=false
HIBERNATE_DEFAULT_BATCH_FETCH_SIZE=100
```

## DB 이전
Expand Down Expand Up @@ -168,9 +171,12 @@ CHARACTER_IMAGE_QUALITY=medium
CHARACTER_IMAGE_WIDTH=1024
CHARACTER_IMAGE_HEIGHT=1024
CHARACTER_IMAGE_COUNT=1
JAVA_TOOL_OPTIONS=-Xmx384m -XX:MaxRAMPercentage=75
JAVA_TOOL_OPTIONS=-Xms64m -Xmx256m -XX:+UseSerialGC -XX:ActiveProcessorCount=1 -XX:MaxMetaspaceSize=128m -XX:ReservedCodeCacheSize=64m -XX:MaxDirectMemorySize=32m -Xss256k -XX:+ExitOnOutOfMemoryError
DB_MAX_POOL_SIZE=2
DB_MIN_IDLE=1
SPRING_MAIN_LAZY_INITIALIZATION=true
SPRINGDOC_ENABLED=false
HIBERNATE_DEFAULT_BATCH_FETCH_SIZE=100
```

Google OAuth를 Render 백엔드로 사용할 경우:
Expand Down
18 changes: 10 additions & 8 deletions src/main/java/com/kw/readwith/config/AsyncConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ public class AsyncConfig implements AsyncConfigurer {
public Executor imageGenerationExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

// 코어 스레드: 동시에 2개의 이미지만 생성
executor.setCorePoolSize(2);
// Render Free 512MB 환경에서는 이미지 생성을 한 번에 하나만 실행한다.
executor.setCorePoolSize(1);

// 최대 스레드: 부하가 높을 때 최대 3개까지 허용
executor.setMaxPoolSize(3);
executor.setMaxPoolSize(1);

// 대기 큐: 50개까지 대기 가능
executor.setQueueCapacity(50);
executor.setQueueCapacity(10);

// 스레드 이름 prefix (로그 추적 용이)
executor.setThreadNamePrefix("image-gen-");
executor.setAllowCoreThreadTimeOut(true);
executor.setKeepAliveSeconds(60);

// 큐가 가득 찰 경우 호출한 스레드에서 직접 실행
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
Expand All @@ -50,9 +50,11 @@ public Executor imageGenerationExecutor() {
public Executor normalizationJobExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(20);
executor.setMaxPoolSize(1);
executor.setQueueCapacity(5);
executor.setThreadNamePrefix("normalization-job-");
executor.setAllowCoreThreadTimeOut(true);
executor.setKeepAliveSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ public GeneratedCharacterImage generateTextImage(Character character) throws IOE
}

public String uploadGeneratedImage(Character character, byte[] imageData, String s3KeyName) {
String base64Image = Base64.getEncoder().encodeToString(imageData);
return s3Manager.uploadFileFromBase64(s3KeyName, base64Image, "image/png");
s3Manager.uploadBytes(s3KeyName, imageData, "image/png");
return s3Manager.getObjectUrl(s3KeyName);
}

public String buildReferenceCandidateS3KeyName(Character character, Long assetId) {
Expand Down
10 changes: 9 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ server:
port: ${PORT:8080}

spring:
main:
lazy-initialization: ${SPRING_MAIN_LAZY_INITIALIZATION:true}
datasource:
url: ${DATABASE_URL:jdbc:mysql://${AWS_DB_URL}:3306/readwith_db?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&useSSL=true&requireSSL=true}
username: ${DATABASE_USERNAME:${AWS_DB_USERNAME}}
Expand Down Expand Up @@ -30,7 +32,7 @@ spring:
use_sql_comments: true
hbm2ddl:
auto: validate
default_batch_fetch_size: 1000
default_batch_fetch_size: ${HIBERNATE_DEFAULT_BATCH_FETCH_SIZE:100}
# 배치 INSERT 최적화 (성능 개선)
jdbc:
batch_size: 50
Expand Down Expand Up @@ -69,6 +71,12 @@ spring:
quality: ${CHARACTER_IMAGE_QUALITY:medium}
n: ${CHARACTER_IMAGE_COUNT:1}

springdoc:
api-docs:
enabled: ${SPRINGDOC_ENABLED:false}
swagger-ui:
enabled: ${SPRINGDOC_ENABLED:false}

epub:
normalization:
enabled: ${EPUB_NORMALIZATION_ENABLED:true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ void generateAndSaveImage_succeeds() throws Exception {
Files.write(tempImage, new byte[]{1, 2, 3, 4});

when(characterRepository.findByIdWithBook(testCharacter.getId())).thenReturn(Optional.of(testCharacter));
when(s3Manager.uploadFileFromBase64(anyString(), anyString(), eq("image/png")))
when(s3Manager.getObjectUrl("character-images/1/10.png"))
.thenReturn("https://cdn.readwith.store/character-images/1/10.png");

ImageResponse imageResponse = mock(ImageResponse.class);
Expand All @@ -183,7 +183,7 @@ void generateAndSaveImage_succeeds() throws Exception {
assertThat(imageOptions.getWidth()).isEqualTo(1024);
assertThat(imageOptions.getHeight()).isEqualTo(1024);
assertThat(imageOptions.getN()).isEqualTo(1);
verify(s3Manager).uploadFileFromBase64(eq("character-images/1/10.png"), anyString(), eq("image/png"));
verify(s3Manager).uploadBytes(eq("character-images/1/10.png"), any(byte[].class), eq("image/png"));
verify(transactionService).updateImageAndStatus(
testCharacter.getId(),
"https://cdn.readwith.store/character-images/1/10.png",
Expand All @@ -197,7 +197,7 @@ void generateAndSaveImage_uploadsBase64JsonResponse() {
byte[] generatedImage = new byte[]{9, 8, 7, 6};

when(characterRepository.findByIdWithBook(testCharacter.getId())).thenReturn(Optional.of(testCharacter));
when(s3Manager.uploadFileFromBase64(anyString(), anyString(), eq("image/png")))
when(s3Manager.getObjectUrl("character-images/1/10.png"))
.thenReturn("https://cdn.readwith.store/character-images/1/10.png");

ImageResponse imageResponse = mock(ImageResponse.class);
Expand All @@ -210,13 +210,13 @@ void generateAndSaveImage_uploadsBase64JsonResponse() {

characterImageService.generateAndSaveImage(testCharacter.getId());

ArgumentCaptor<String> base64Captor = ArgumentCaptor.forClass(String.class);
verify(s3Manager).uploadFileFromBase64(
ArgumentCaptor<byte[]> imageCaptor = ArgumentCaptor.forClass(byte[].class);
verify(s3Manager).uploadBytes(
eq("character-images/1/10.png"),
base64Captor.capture(),
imageCaptor.capture(),
eq("image/png")
);
assertThat(Base64.getDecoder().decode(base64Captor.getValue())).containsExactly(generatedImage);
assertThat(imageCaptor.getValue()).containsExactly(generatedImage);
verify(transactionService).updateImageAndStatus(
testCharacter.getId(),
"https://cdn.readwith.store/character-images/1/10.png",
Expand Down
Loading