diff --git a/src/main/java/sopt/org/homepage/aws/s3/S3ServiceImpl.java b/src/main/java/sopt/org/homepage/aws/s3/S3ServiceImpl.java index 9200c67..211b8f8 100644 --- a/src/main/java/sopt/org/homepage/aws/s3/S3ServiceImpl.java +++ b/src/main/java/sopt/org/homepage/aws/s3/S3ServiceImpl.java @@ -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 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 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(); + } } \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 69ff56c..ee48a4c 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -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: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 21bca93..599cb71 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -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 @@ -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: