Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Hotfix]: 프로덕션 DB Connection Pool 과부화 방지 & Admin 이미지 원본링크 이슈 수정 #29

Merged
merged 4 commits into from
Feb 5, 2025
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
286 changes: 140 additions & 146 deletions src/main/java/sopt/org/homepage/aws/s3/S3ServiceImpl.java
Original file line number Diff line number Diff line change
@@ -1,164 +1,158 @@
package sopt.org.homepage.aws.s3;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Service
@Slf4j
@RequiredArgsConstructor
public class S3ServiceImpl implements S3Service {
private final S3Presigner s3Presigner;
private final S3Client s3Client;

@Value("${aws.bucket.image}")
private String bucket;

@Value("${aws.bucket.dir}")
private String baseDir;

public String generatePresignedUrl(String fileName, String path) {
try {
String contentType = getContentTypeFromFileName(fileName);
String key = baseDir + path + fileName;

PutObjectRequest objectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(contentType)
.build();

PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(5))
.putObjectRequest(objectRequest)
.build();

PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
return presignedRequest.url().toString();
} catch (Exception e) {
log.error("Error generating presigned URL for file: {}", fileName, e);
throw new RuntimeException("Failed to generate presigned URL", e);
}
}

public String getOriginalUrl(String presignedUrl) {
try {
URL url = new URL(presignedUrl);
System.out.println(url);
String key = url.getPath().substring(1);
if (key.startsWith(this.bucket + "/")) {
key = key.substring(this.bucket.length() + 1); // 버킷 제거
}

return getFileUrl(key);
} catch (MalformedURLException e) {
log.error("Error parsing presigned URL: {}", presignedUrl, e);
throw new RuntimeException("Failed to parse presigned URL", e);
}
}
public String uploadFile(MultipartFile file, String path) {
String fileName = createFileName(file.getOriginalFilename());

String key = baseDir + path + fileName;
try {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(file.getContentType())
.build();

s3Client.putObject(putObjectRequest,
RequestBody.fromInputStream(file.getInputStream(), file.getSize()));

return getFileUrl(key);
} catch (IOException e) {
log.error("Error uploading file: {}", fileName, e);
throw new RuntimeException("Failed to upload file", e);
}
}

public String getFileUrl(String fileKey) {
try {
GetObjectPresignRequest request = GetObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(10))
.getObjectRequest(b -> b
.bucket(bucket)
.key(fileKey)
.build())
.build();

return s3Presigner.presignGetObject(request).url().toString();
} catch (Exception e) {
log.error("Error getting file URL: {}", fileKey, e);
throw new RuntimeException("Failed to get file URL", e);
}
}

public void deleteFile(String fileUrl) {
try {
String fileName = extractKeyFromUrl(fileUrl);
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
.bucket(bucket)
.key(fileName)
.build();

s3Client.deleteObject(deleteObjectRequest);
} catch (Exception e) {
log.error("Error deleting file: {}", fileUrl, e);
throw new RuntimeException("Failed to delete file", e);
}
}

private String extractKeyFromUrl(String fileUrl) {
try {
String[] parts = fileUrl.split("/");
return String.join("/", Arrays.copyOfRange(parts, 4, parts.length));
} catch (Exception e) {
throw new RuntimeException("Invalid S3 URL format", e);
}
}

private String createFileName(String originalFileName) {
return UUID.randomUUID().toString() + "_" + originalFileName;
}

private String getContentTypeFromFileName(String fileName) {
Map<String, String> contentTypeMap = new HashMap<>();
contentTypeMap.put("jpg", "image/jpeg");
contentTypeMap.put("jpeg", "image/jpeg");
contentTypeMap.put("png", "image/png");
contentTypeMap.put("gif", "image/gif");
contentTypeMap.put("pdf", "application/pdf");
contentTypeMap.put("txt", "text/plain");
contentTypeMap.put("html", "text/html");
contentTypeMap.put("json", "application/json");
// 필요한 파일타입 추가

String extension = getFileExtension(fileName);
return contentTypeMap.getOrDefault(extension, "application/octet-stream");
}

private String getFileExtension(String fileName) {
int lastIndexOfDot = fileName.lastIndexOf(".");
return (lastIndexOfDot == -1) ? "" : fileName.substring(lastIndexOfDot + 1).toLowerCase();
}
private final S3Presigner s3Presigner;
private final S3Client s3Client;

@Value("${aws.bucket.image}")
private String bucket;

@Value("${aws.region}")
private String region;

@Value("${aws.bucket.dir}")
private String baseDir;

public String generatePresignedUrl(String fileName, String path) {
try {
String contentType = getContentTypeFromFileName(fileName);
String key = baseDir + path + fileName;

PutObjectRequest objectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(contentType)
.build();

PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
.signatureDuration(Duration.ofMinutes(5))
.putObjectRequest(objectRequest)
.build();

PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
return presignedRequest.url().toString();
} catch (Exception e) {
log.error("Error generating presigned URL for file: {}", fileName, e);
throw new RuntimeException("Failed to generate presigned URL", e);
}
}

public String getOriginalUrl(String presignedUrl) {
try {
URL url = new URL(presignedUrl);
System.out.println(url);
String key = url.getPath().substring(1);
if (key.startsWith(this.bucket + "/")) {
key = key.substring(this.bucket.length() + 1); // 버킷 제거
}

return getFileUrl(key);
} catch (MalformedURLException e) {
log.error("Error parsing presigned URL: {}", presignedUrl, e);
throw new RuntimeException("Failed to parse presigned URL", e);
}
}

public String uploadFile(MultipartFile file, String path) {
String fileName = createFileName(file.getOriginalFilename());

String key = baseDir + path + fileName;
try {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(file.getContentType())
.build();

s3Client.putObject(putObjectRequest,
RequestBody.fromInputStream(file.getInputStream(), file.getSize()));

return getFileUrl(key);
} catch (IOException e) {
log.error("Error uploading file: {}", fileName, e);
throw new RuntimeException("Failed to upload file", e);
}
}

public String getFileUrl(String fileKey) {
return String.format("https://s3.%s.amazonaws.com/%s/%s",
region,
bucket,
fileKey);
}

public void deleteFile(String fileUrl) {
try {
String fileName = extractKeyFromUrl(fileUrl);
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
.bucket(bucket)
.key(fileName)
.build();

s3Client.deleteObject(deleteObjectRequest);
} catch (Exception e) {
log.error("Error deleting file: {}", fileUrl, e);
throw new RuntimeException("Failed to delete file", e);
}
}

private String extractKeyFromUrl(String fileUrl) {
try {
String[] parts = fileUrl.split("/");
return String.join("/", Arrays.copyOfRange(parts, 4, parts.length));
} catch (Exception e) {
throw new RuntimeException("Invalid S3 URL format", e);
}
}

private String createFileName(String originalFileName) {
return UUID.randomUUID().toString() + "_" + originalFileName;
}

private String getContentTypeFromFileName(String fileName) {
Map<String, String> contentTypeMap = new HashMap<>();
contentTypeMap.put("jpg", "image/jpeg");
contentTypeMap.put("jpeg", "image/jpeg");
contentTypeMap.put("png", "image/png");
contentTypeMap.put("gif", "image/gif");
contentTypeMap.put("pdf", "application/pdf");
contentTypeMap.put("txt", "text/plain");
contentTypeMap.put("html", "text/html");
contentTypeMap.put("json", "application/json");
// 필요한 파일타입 추가

String extension = getFileExtension(fileName);
return contentTypeMap.getOrDefault(extension, "application/octet-stream");
}

private String getFileExtension(String fileName) {
int lastIndexOfDot = fileName.lastIndexOf(".");
return (lastIndexOfDot == -1) ? "" : fileName.substring(lastIndexOfDot + 1).toLowerCase();
}
}
6 changes: 6 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ spring:
url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC;
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 5
minimum-idle: 2
idle-timeout: 300000
connection-timeout: 20000
max-lifetime: 1200000

jpa:
hibernate:
Expand Down
13 changes: 9 additions & 4 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ spring:
url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC;
username: ${DB_USERNAME}
password: ${DB_PASSWORD}

hikari: # t3.micro 환경에 맞게 hikari pool 설정
maximum-pool-size: 5 # vCPU * 2 + 1 = 5
minimum-idle: 2 # 최소한의 idle 커넥션만 유지
idle-timeout: 300000 # 5분 (기본값 10분에서 줄임)
connection-timeout: 20000 # 20초
max-lifetime: 1200000 # 20분 (기본값 30분에서 줄임)
jpa:
hibernate:
ddl-auto: none
Expand All @@ -16,9 +21,9 @@ spring:

properties:
hibernate:
show_sql: true
format_sql: true
use_sql_comments: true
show_sql: false # Production 환경에서 로깅 OFF
format_sql: false
use_sql_comments: false
default_schema: public

springdoc:
Expand Down
Loading