์๊ฐ ๐ฏ
์ด๋ฒ ํฌ์คํ ์์๋ Spring Boot ์ ํ๋ฆฌ์ผ์ด์ ์์ Google Cloud Storage(GCS)๋ฅผ ์ฌ์ฉํ์ฌ ํ์ผ์ ์ ๋ก๋, ๋ค์ด๋ก๋, ์ญ์ ํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด๊ฒ ์ต๋๋ค. GCS๋ Google Cloud Platform์์ ์ ๊ณตํ๋ ํ์ฅ์ฑ ์๋ ๊ฐ์ฒด ์คํ ๋ฆฌ์ง ์๋น์ค์ ๋๋ค.
๋จผ์ ์๋์ ๊ตฌ๊ธ ํด๋ผ์ฐ๋ ์ฝ์์ ๋ค์ด๊ฐ์ ํ๋ก์ ํธ๋ฅผ ์์ฑํ๋ฉด
https://cloud.google.com/cloud-console
https://cloud.google.com/cloud-console
cloud.google.com
์๋์ ์ฌ์ง ์ฒ๋ผ 90์ผ๋์ ๋ฌด๋ฃ ํฌ๋ ๋ง์ ์ฌ์ฉ๊ฐ๋ฅํฉ๋๋ค. ์ ํฌ๋ ์ด ํฌ๋ ๋ง์ ์ฌ์ฉํ ๊ฒ๋๋ค.
์ ๊ทธ๋ฆฌ๊ณ ์ผ๋ฐ ๊ณ์ ํ์ฑํ๋ ๋ฌด๋ฃ ํฌ๋ ๋ง์ ์ ๋ถ ์ฌ์ฉํ๋ฉด ์ดํ์ ์ฌ์ฉํ ๋งํผ ๋น์ฉ์ ์ง๋ถํ๋ ๋ฐฉ์์ด๋ผ ๋ฐ๋ก ์ค์ ์ ์ํ์ ๋ ๋ฉ๋๋ค.(90์ผ ๋๋ ํฌ๋ ๋ง ์ ๋ถ ๋ค ์ฌ์ฉ ํ ์ด์ด์ ๊ณ์ ์ฌ์ฉํ์๊ณ ์ถ์ ๋ถ๋ค๋ง)
๊ทธ๋ฆฌ๊ณ ์ข์ธก ์๋จ์ ๋ฉ๋ด๋ฐ๋ฅผ ํด๋ฆญํด์ ํด๋ผ์ฐ๋ ์คํ ๋ฆฌ์ง๋ฅผ ํด๋ฆญ ํ ๋ฒํท์ผ๋ก ์ด๋ํฉ๋๋ค.
๋ฒํท์ ๋๋ฌ์ ๋ค์ด์ค์๋ฉด ์๋์ ํ๋ฉด์ด ๋จ๋๋ฐ ๋ฒํท ๋ง๋ค๊ธฐ๋ฅผ ์ ํํด์ฃผ์๋ฉด ๋ฉ๋๋ค.
์๋ ํ๋ฉด์์ ์ด๋ฆ ์ ํด์ฃผ์๊ณ , ๋ฐ์ดํฐ ์ ์ฅ ์์น๋ Region ์์ธ ์ ํํ์๋ฉด ๋ฉ๋๋ค. ๋ฐ์ดํฐ ์คํ ๋ฆฌ์ง ํด๋์ค๋ Standard, ๊ทธ๋ฆฌ๊ณ ์๋์ ํ๋ฉด์ ํ๋์ ํ์๋ ๊ณณ์ ์ฒดํฌ๋ฅผ ํด์ ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ดํ์ ๊ณ์์ ๋๋ฌ์ ๋ฒํท์ ๋ง๋ค์ด์ฃผ์๋ฉด ๋ฉ๋๋ค.
์๋์ ๊ฐ์ด ์์ฑ๋ ๊ฒ์ด ํ์ธ์ด ๋ฉ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ค์ ์ข์ธก ์๋จ์ ๋ฉ๋ด์์ IAM ๋ฐ ๊ด๋ฆฌ์์ ์๋น์ค ๊ณ์ ์ ์ ํ.
์๋น์ค ๊ณ์ ์ ๋ค์ด์ค๋ฉด ํ์ฌ ํ๋ฉด์ด ๋จ๋๋ฐ, ์๋๋ ์์ ์ ์์ฑํด๋ ์๋น์ค ๊ณ์ ์ด๊ณ , ์๋ก ๋ง๋ค์ด์ค ๊ฒ์ด๋ค. ํ๋์์ผ๋ก ํ์๋ ์๋น์ค ๊ณ์ ์ถ๊ฐ ํด๋ฆญ.
์๋์ ์ธ ๊ฐ์ง ์ญํ ์ ์ถ๊ฐ.
์ด์ ์๋ฃํด์ ์์ฑํ๋ฉด ์๋ ์๋ก์ด ์๋น์ค ๊ณ์ ์ด ์์ฑ๋ ๊ฒ์ด ๋ณด์ผ ๊ฒ์ด๋ค. ํด๋ฆญํ์ฌ ๋ค์ด๊ฐ์ค๋๋ค.
ํด๋ฆญํ์ฌ ์๋น์ค ๊ณ์ ์ ๋ค์ด์์ ํค๋ฅผ ์ ํ ํ ์ ํค ๋ง๋ค๊ธฐ๋ฅผ ์ ํ. JSON์ผ๋ก ๋ง๋ค๋ฉด ๋ฉ๋๋ค.
๊ทธ๋ฌ๋ฉด ์ด์ ํค๊ฐ ์์ฑ์ด ๋๊ณ ์๋์ผ๋ก ๋ค์ด๋ก๋๊ฐ ๋๋ค.
์ด ํ์ผ์ ๋์ค์ resources ๋๋ ํ ๋ฆฌ์ ๋ณด๊ดํด์ผ ํ๋ ํ์ผ ๊ฒฝ๋ก๋ฅผ ๊ธฐ์ตํด๋๋ก ํฉ์๋ค.
ํ๊ฒฝ ์ค์ โ๏ธ
build.gradle ์์กด์ฑ ์ถ๊ฐ
implementation group: 'org.springframework.cloud', name: 'spring-cloud-gcp-starter', version: '1.2.5.RELEASE'
implementation group: 'org.springframework.cloud', name: 'spring-cloud-gcp-storage', version: '1.2.5.RELEASE'
๋ฒํท ์ด๋ฆ์ ์์ํ ๋ ์์ฑํ๋ ์ด๋ฆ, ํ๋ก์ ํธ ID๋ ํด๋ผ์ฐ๋ ์ฝ์์์ ํ์ธํ๊ฑฐ๋ ๋ค์ด๋ก๋ ๋ฐ์ ์ ์ด์จ ํค ํ์ผ ์์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
application.yml ์ค์
spring
cloud:
gcp:
storage:
credentials:
location: classpath:[ํค ์ด๋ฆ].json
project-id: [ํ๋ก์ ํธ ID]
bucket: [๋ฒํท ์ด๋ฆ]
ํ๋ก์ ํธ ๊ตฌ์กฐ ๐
src/main/java/com/office/
โโโ controller
โ โโโ GCSController.java
โโโ service
โ โโโ GCSService.java
โโโ dto
โ โโโ GCSRequest.java
โ โโโ GCSResponse.java
โโโ exception
โโโ GCSException.java
โโโ resources
โโโ [ํค ํ์ผ ๋ถ์ฌ๋ฃ๊ธฐ]
๊ตฌํํ๊ธฐ ๐ป
์์ธ ์ฒ๋ฆฌ
๋จผ์ GCS ๊ด๋ จ ์์ ์ค ๋ฐ์ํ ์ ์๋ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํ ์ปค์คํ ์์ธ ํด๋์ค๋ฅผ ๋ง๋ญ๋๋ค.
public class GCSException extends RuntimeException {
public GCSException(String message) {
super(message);
}
public GCSException(String message, Throwable cause) {
super(message, cause);
}
}
Controller ๊ตฌํ ๐ฎ
REST API ์๋ํฌ์ธํธ๋ฅผ ์ ๊ณตํ๋ ์ปจํธ๋กค๋ฌ๋ฅผ ๊ตฌํํฉ๋๋ค.
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/gcs")
public class GCSController {
private final GCSService gcsService;
@PostMapping("/upload")
public ResponseEntity<GCSResponse> uploadObject(@ModelAttribute GCSRequest gcsRequest) throws GCSException {
return ResponseEntity.ok(gcsService.uploadObject(gcsRequest));
}
@GetMapping("/download/{fileName}")
public ResponseEntity<GCSResponse> downloadObject(@PathVariable String fileName) throws GCSException {
return ResponseEntity.ok(gcsService.downloadObject(fileName));
}
@DeleteMapping("/{fileName}")
public ResponseEntity<Void> deleteObject(@PathVariable String fileName) throws GCSException {
gcsService.deleteObject(fileName);
return ResponseEntity.ok().build();
}
@GetMapping("/list")
public ResponseEntity<List<GCSResponse>> listObjects() throws GCSException {
return ResponseEntity.ok(gcsService.listObjects());
}
@ExceptionHandler(GCSException.class)
public ResponseEntity<String> handleGCSException(GCSException e) {
log.error("GCS operation failed", e);
return ResponseEntity.badRequest().body(e.getMessage());
}
}
Service ๊ตฌํ โก
GCS์์ ์ค์ ์ํธ์์ฉ์ ๋ด๋นํ๋ ์๋น์ค ํด๋์ค์ ๋๋ค.
@Slf4j
@Service
public class GCSService {
@Value("${spring.cloud.gcp.storage.bucket}")
private String bucketName;
@Value("${spring.cloud.gcp.storage.credentials.location}")
private String keyFileName;
private Storage storage;
@PostConstruct
public void init() throws GCSException {
try {
InputStream keyFile = ResourceUtils.getURL(keyFileName).openStream();
storage = StorageOptions.newBuilder()
.setCredentials(GoogleCredentials.fromStream(keyFile))
.build()
.getService();
log.info("GCS Service initialized successfully");
} catch (IOException e) {
log.error("Failed to initialize GCS Service", e);
throw new GCSException("Failed to initialize GCS service", e);
}
}
// ํ์ผ ์
๋ก๋ ๋ฉ์๋
public GCSResponse uploadObject(GCSRequest request) throws GCSException {
validateUploadRequest(request);
try {
MultipartFile file = request.getFile();
String fileName = generateUniqueFileName(file.getOriginalFilename());
BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, fileName)
.setContentType(file.getContentType())
.build();
Blob blob = storage.create(blobInfo, file.getInputStream());
return createGCSResponse(blob);
} catch (IOException e) {
throw new GCSException("Failed to upload file", e);
}
}
// ๊ธฐํ ๋ฉ์๋ ๊ตฌํ...
}
DTO ๊ตฌํ ๐ฆ
๋ฐ์ดํฐ ์ ์ก์ ์ํ DTO ํด๋์ค๋ค์ ๋๋ค.
@Data
public class GCSRequest {
private String name;
private MultipartFile file;
}
@Data
@Builder
public class GCSResponse {
private String fileName;
private String downloadUrl;
private Long fileSize;
private String contentType;
private String uploadTime;
}
์ฃผ์ ๊ธฐ๋ฅ ๐ฏ
- ํ์ผ ์
๋ก๋ ๐ค
- ์ ๋ํฌํ ํ์ผ๋ช ์์ฑ
- ํ์ผ ๋ฉํ๋ฐ์ดํฐ ์ค์
- ์ ๋ก๋ ๊ฒฐ๊ณผ ๋ฐํ
- ํ์ผ ๋ค์ด๋ก๋ ๐ฅ
- ํ์ผ๋ช ์ผ๋ก ๊ฐ์ฒด ์กฐํ
- ๋ค์ด๋ก๋ URL ์ ๊ณต
- ํ์ผ ์ญ์ ๐๏ธ
- ์ง์ ๋ ํ์ผ ์ญ์
- ์ญ์ ๊ฒฐ๊ณผ ํ์ธ
- ํ์ผ ๋ชฉ๋ก ์กฐํ ๐
- ๋ฒํท ๋ด ๋ชจ๋ ํ์ผ ๋ชฉ๋ก ์กฐํ
- ํ์ผ ๋ฉํ๋ฐ์ดํฐ ํฌํจ
API ํ ์คํธํ๊ธฐ ๐งช
Postman์ ์ฌ์ฉํ์ฌ ๊ตฌํ๋ API๋ฅผ ํ ์คํธํด๋ณด๊ฒ ์ต๋๋ค.
1. ํ์ผ ์ ๋ก๋ ํ ์คํธ ๐ค
Request:
- Method:
POST
- URL:
http://localhost:8080/api/gcs/upload
- Headers:
Content-Type
:multipart/form-data
- Body (form-data):
- Key:
file
(Type: File) - Key:
name
(Type: Text)
- Key:
POST /api/gcs/upload HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="test.pdf"
Content-Type: application/pdf
[ํ์ผ ๋ด์ฉ]
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="name"
test-document
------WebKitFormBoundary7MA4YWxkTrZu0gW--
Response Example:
{
"fileName": "6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
"downloadUrl": "https://storage.googleapis.com/[bucket-name]/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
"fileSize": 15234,
"contentType": "application/pdf",
"uploadTime": "2024-01-20T09:30:15.123Z"
}
2. ํ์ผ ๋ชฉ๋ก ์กฐํ ํ ์คํธ ๐
Request:
- Method:
GET
- URL:
http://localhost:8080/api/gcs/list
GET /api/gcs/list HTTP/1.1
Host: localhost:8080
Response Example:
[
{
"fileName": "6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
"downloadUrl": "https://storage.googleapis.com/[bucket-name]/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
"fileSize": 15234,
"contentType": "application/pdf",
"uploadTime": "2024-01-20T09:30:15.123Z"
},
{
"fileName": "7d2b5e9f-8g4d-5c3b-0d1e-8e0c0f234567.jpg",
"downloadUrl": "https://storage.googleapis.com/[bucket-name]/7d2b5e9f-8g4d-5c3b-0d1e-8e0c0f234567.jpg",
"fileSize": 45678,
"contentType": "image/jpeg",
"uploadTime": "2024-01-20T10:15:30.456Z"
}
]
3. ํ์ผ ๋ค์ด๋ก๋ ํ ์คํธ ๐ฅ
Request:
- Method:
GET
- URL:
http://localhost:8080/api/gcs/download/{fileName}
GET /api/gcs/download/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf HTTP/1.1
Host: localhost:8080
Response Example:
{
"fileName": "6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
"downloadUrl": "https://storage.googleapis.com/[bucket-name]/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf",
"fileSize": 15234,
"contentType": "application/pdf",
"uploadTime": "2024-01-20T09:30:15.123Z"
}
4. ํ์ผ ์ญ์ ํ ์คํธ ๐๏ธ
Request:
- Method:
DELETE
- URL:
http://localhost:8080/api/gcs/delete/{fileName}
DELETE /api/gcs/6c1a4d8e-7f3c-4b2a-9c0f-7d9b9e123456.pdf HTTP/1.1
Host: localhost:8080
Response:
- Status:
200 OK
- Body: Empty
์๋ฌ ์๋ต ์์ โ ๏ธ
ํ์ผ์ด ์กด์ฌํ์ง ์๋ ๊ฒฝ์ฐ:
{
"error": "File not found: non-existent-file.pdf",
"status": 400
}
ํ์ผ ์ ๋ก๋ ์คํจ ์:
{
"error": "Failed to upload file: Invalid file format",
"status": 400
}
Postman ํ๊ฒฝ ์ค์ ํ ๐ก
- ํ๊ฒฝ ๋ณ์ ์ค์
baseUrl: http://localhost:8080 bucketName: your-bucket-name
- Collection ๋ณ์ ์ค์
- API ๊ฒฝ๋ก:
/api/gcs
- ๊ณตํต ํค๋ ์ค์
- API ๊ฒฝ๋ก:
- ํ ์คํธ ์คํฌ๋ฆฝํธ ์์
// ํ์ผ ์ ๋ก๋ ํ ํ ์คํธ pm.test("Upload successful", function () { phttp://m.response.to.have.status(200); phttp://m.response.to.have.jsonSchema({ required: ['fileName', 'downloadUrl', 'fileSize'] }); });
์ฃผ์์ฌํญ ๐จ
- ํ์ผ ์ ๋ก๋ ์ ์ต๋ ํ์ผ ํฌ๊ธฐ ์ ํ์ ํ์ธํ์ธ์.
- multipart/form-data ํ์์ผ๋ก ์ ์กํด์ผ ํฉ๋๋ค.
- ํ์ผ๋ช ์ ํน์๋ฌธ์๊ฐ ํฌํจ๋ ๊ฒฝ์ฐ URL ์ธ์ฝ๋ฉ์ด ํ์ํ ์ ์์ต๋๋ค.
- ํฐ ํ์ผ ์ ๋ก๋ ์ ํ์์์ ์ค์ ์ ์ ์ ํ ์กฐ์ ํ์ธ์.
๋ง์น๋ฉฐ โจ
์ด๋ ๊ฒ Spring Boot์์ Google Cloud Storage๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์์ต๋๋ค. ์ ์ฝ๋๋ ๊ธฐ๋ณธ์ ์ธ ํ์ผ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ ๊ตฌํํ ๊ฒ์ผ๋ก, ์ค์ ํ๋ก์ ํธ์์๋ ๋ณด์, ์ฑ๋ฅ, ์๋ฌ ์ฒ๋ฆฌ ๋ฑ์ ๊ณ ๋ คํ์ฌ ์ถ๊ฐ์ ์ธ ๊ตฌํ์ด ํ์ํ ์ ์์ต๋๋ค.