먼저 아래 링크에서 루트 사용자로 로그인 하도록 하자
AWS 콘솔
공식 가이드를 보고 싶으면
Getting started with Amazon S3 해당 사이트 방문해 읽어보면 좋을 것 같다.
설명 및 장단점 등은 다른 글에도 많으니 생략하고 사용방법 및 설정을 다루어 보자.
S3를 사용하여 파일을 관리하기 위해선 먼저 버킷이라는 객체 관리 컨테이너를 만들어야한다.
사용할 버킷 이름을 작성하면 된다.
중복되지 않는 버킷 이름을 작성하고 언하는 리전을 선택(오른쪽 상단 닉네임 왼쪽에서 설정 가능)

이후 업로드한 사진을 확인하기 위해 퍼블릭엑세스 차단을 해제한다


나머지는 그대로 설정해줬다

IAM사용자를 생성해 S3접근 권한을 부여해야한다. 엑세스키와 시크릿키를 사용하기 때문에 꼭 저장하고 이 값은 절대 외부에 노출되어선 안되니 주의해야한다.
필자는 git으로 관리할 때 .ignore파일에 application-secret파일에 DB정보나 엑세스/시크릿 키 등 주요 정보를 포함시켰다.


직접 정책 연결을 선택하고
AmazonS3FullAccess를 포함시키고 사용자 생성

다시 IAM으로 들어가 사용자를 클릭하고 엑세스 키 만들기를 누른다.

보안 개선을 위한 사용사례 및 대안 고려인데 크게 차이가 없다

이후 설명 태그 설정은 선택사항이기에 넘겼고 엑세스키를 만들었다.

.csv파일로 저장하도록 하는걸 추천!
버킷 정책을 편집을 클릭하고
본인의 버킷 ARN을 복사한 뒤 정책생성기를 누른다.


이후 Generate Policy하여 만들어진 JSON Document 복사후 변경에 붙여넣고 저장
이제 퍼블릭 설정이 완료되었다.
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"DELETE"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"x-amz-server-side-encryption",
"x-amz-request-id",
"x-amz-id-2"
],
"MaxAgeSeconds": 3000
}
]
붙여넣고 저장하면 된다.
Spring Cloud AWS Starter
해당 mvn사이트로 가서 의존성을 가져와 원하는대로 추가해주면 된다
//AWS
// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-aws
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.6.RELEASE'
pom.xml도 비슷하다
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-aws</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-aws-context</artifactId>
<version>1.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-aws-autoconfigure</artifactId>
<version>1.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.11.1001</version>
</dependency>
application.properties
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
spring.config.import=application-secret.properties
# AWS credentials
cloud.aws.credentials.access-key=엑세스키
cloud.aws.credentials.secret-key=시크릿 엑세스 키
cloud.aws.stack.auto=false
# AWS region and S3 bucket
cloud.aws.region.static=리전
cloud.aws.s3.bucket=버킷이름
application.yml
# Multipart
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 10MB
# AWS
cloud:
aws:
credentials:
accessKey: 엑세스키
secretKey: 시크릿 엑세스 키
region:
static: 리전
stack:
auto: false
s3:
bucket: 버킷이름
package com.ssafy.picple.AwsS3;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
.build();
}
}
package com.ssafy.picple.AwsS3;
import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
import com.amazonaws.services.s3.model.GetObjectRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import com.ssafy.picple.config.baseResponse.BaseException;
import com.ssafy.picple.config.baseResponse.BaseResponseStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.UUID;
import java.util.logging.Logger;
@Service
@Slf4j
@RequiredArgsConstructor
public class S3FileUploadService {
private static final Logger logger = Logger.getLogger(S3FileUploadService.class.getName());
private final AmazonS3 s3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
private final String defaultUrl = "https://picple.s3.ap-northeast-2.amazonaws.com";
public String uploadFile(MultipartFile file) throws IOException, BaseException {
String originalFilename = file.getOriginalFilename();
try {
s3Client.putObject(bucketName, originalFilename, file.getInputStream(), getObjectMetadata(file));
return defaultUrl + "/" + originalFilename;
} catch (SdkClientException e) {
throw new BaseException(BaseResponseStatus.FILE_UPLOAD_ERROR);
}
}
// MultipartFile 사이즈랑 타입 명시용
private ObjectMetadata getObjectMetadata(MultipartFile file) {
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(file.getContentType());
objectMetadata.setContentLength(file.getSize());
return objectMetadata;
}
private String generateFileName(MultipartFile file) {
// return UUID.randomUUID().toString() + "-" + file.getOriginalFilename(); // 중복안되게 랜덤하게 넣으려면 이렇게 그때그때 UUID붙여서
return file.getOriginalFilename();
}
// 인코딩 필요하면 사용
// 파일 이름을 UTF-8로 인코딩
public static String encodeFileName(String fileName) {
try {
return URLEncoder.encode(fileName, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return fileName; // 인코딩 실패 시 원래 파일 이름 리턴
}
}
public S3Object downloadFile(String fileName) throws BaseException {
try {
return s3Client.getObject(new GetObjectRequest(bucketName, fileName));
} catch (SdkClientException e) {
throw new BaseException(BaseResponseStatus.FILE_DOWNLOAD_ERROR);
}
}
public void deleteFile(String file) throws BaseException {
try {
s3Client.deleteObject(new DeleteObjectRequest(bucketName, file));
} catch (SdkClientException e) {
throw new BaseException(BaseResponseStatus.FILE_DELETE_ERROR);
}
}
}
아래부턴 원하는대로 엔티티부터 코딩하면 된다. 다음은 내가 사용했던 컨트롤러 예시들
package com.fitdo.controller;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.fitdo.model.dto.Follow;
import com.fitdo.model.dto.Post;
import com.fitdo.model.dto.User;
import com.fitdo.model.service.CommentService;
import com.fitdo.model.service.FollowService;
import com.fitdo.model.service.PostService;
import com.fitdo.model.service.S3FileUploadServiceImpl;
import com.fitdo.model.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/post")
@Tag(name = "PostRestController", description = "게시물 관리")
@CrossOrigin("*")
public class PostRestController {
@Autowired
private PostService ps;
@Autowired
private UserService us;
@Autowired
private S3FileUploadServiceImpl ss;
@Autowired
private FollowService fs;
@Autowired
private CommentService cs;
// 게시물 전체 조회
@GetMapping("/")
@Operation(summary = "게시물 전체 조회")
public ResponseEntity<List<Post>> getAllPosts() {
List<Post> posts = ps.getPost();
if (posts == null || posts.size() == 0) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(posts, HttpStatus.OK);
}
// 게시물 전체 조회 + 프사 + 댓글수
@GetMapping("/withImg")
@Operation(summary = "게시물 전체 조회 + 프사 + 댓글수")
public ResponseEntity<List<Post>> getAllPostsWithProfileImg() {
List<Post> posts = ps.getPostWithUserProfileImg();
for (Post post : posts) {
User user = us.searchById(post.getPostUserId()).get(0);
int commentsNum = cs.getCommentNum(post.getPostId());
// 사용자 정보가 null이 아닌 경우에만 설정
if (user != null) {
post.setUser(user);
post.setCommentNum(commentsNum);
}
}
System.out.println(posts);
if (posts == null || posts.size() == 0) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(posts, HttpStatus.OK);
}
// 팔로우 하는 유저 게시물 전체 조회 + 본인포함
@GetMapping("/followee/{userId}")
@Operation(summary = "팔로우 하는 유저 게시물 전체 조회 + 본인포함")
public ResponseEntity<List<Post>> getFolloweePosts(@PathVariable("userId") String userId) {
List<Follow> followee = fs.selectFolloweeList(userId);
System.out.println("내가 팔로우 하는 사람들 객체 : " + followee);
List<String> followeeUserId = new LinkedList<>();
for (Follow follow : followee) {
followeeUserId.add(follow.getToUserId());
}
System.out.println("내가 팔로우 하는 사람들 아이디 목록 : " + followeeUserId);
// 나도추가
followeeUserId.add(userId);
// 내가 볼 팔로우하는 사람들의 게시물 리스트
List<Post> myFolloweePosts = new ArrayList<>();
for (String followeeId : followeeUserId) {
List<Post> userPosts = ps.selectPostsByUserId(followeeId);
myFolloweePosts.addAll(userPosts);
}
List<Post> posts = myFolloweePosts;
for (Post post : posts) {
User user = us.searchById(post.getPostUserId()).get(0);
// 사용자 정보가 null이 아닌 경우에만 설정
if (user != null) {
post.setUser(user);
}
}
if (posts == null || posts.size() == 0) {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(posts, HttpStatus.OK);
}
// 게시물 상세 조회
@GetMapping("/{postId}")
@Operation(summary = "게시물 상세 조회")
public ResponseEntity<Post> getOnePost(@PathVariable int postId) {
Post post = ps.selectOnePost(postId);
if (post != null) {
return new ResponseEntity<>(post, HttpStatus.OK);
} else {
return ResponseEntity.notFound().build();
}
}
// 게시물 상세 조회
@GetMapping("/getPostByUserId/{postUserId}")
@Operation(summary = "게시물 유저아이디로 상세 조회 + 유저 정보")
public ResponseEntity<Post> getOnePostByUserId(@PathVariable String postUserId) {
Post post = ps.selectPostsByUserId(postUserId).get(0);
User user = us.searchById(postUserId).get(0);
if (user != null) {
post.setUser(user);
}
if (post != null) {
return new ResponseEntity<>(post, HttpStatus.OK);
} else {
return ResponseEntity.notFound().build();
}
}
// 게시물들 유저아이디 일치하는 사람으로 조회
@GetMapping("/getPostsListByUserId/{postUserId}")
@Operation(summary = "게시물들 리스트 유저아이디로 상세 조회 - 게시물들 여러개 받는 거")
public ResponseEntity<?> getPostsListByUserId(@PathVariable String postUserId) {
List<Post> posts = ps.selectPostlistByUserId(postUserId);
if (posts != null) {
return new ResponseEntity<>(posts, HttpStatus.OK);
} else {
return ResponseEntity.notFound().build();
}
}
// 게시물 등록
@PostMapping("/addpost/{postUserId}")
@Operation(summary = "게시물 등록")
public ResponseEntity<?> createPost(@PathVariable("postUserId") String postUserId, @RequestBody Post post,
HttpSession session, HttpServletRequest request, HttpServletResponse response) {
// User user = (User) session.getAttribute("user");
// System.out.println("user : " + user);
// Cookie myCookie = new Cookie("myUserId : ", user.getUserid());
// System.out.println(myCookie.getName());
// System.out.println(myCookie.getValue());
// Cookie[] cookies = request.getCookies();
// if (cookies != null) {
// for (Cookie cookie : cookies) {
// System.out.println("cookie.getName() : " + cookie.getName());
// System.out.println("cookie.getValue() : " + cookie.getValue());
// }
// }
// Swagger통신시 세션저장이 안되는 문제점 해결 할 때 까지 테스트용 임시user "ssafy"생성
// if (user == null) {
// // 세션에 사용자 정보가 없을 때 임시로 사용자 객체 생성
// user = new User();
// user.setUserid("ssafy");
// // 임시로 생성한 사용자 객체를 세션에 저장
// session.setAttribute("user", user);
//// return new ResponseEntity<>("로그인이 필요합니다!", HttpStatus.UNAUTHORIZED);
// }
// post.setPostUserId(user.getUserid());
// User객체까지 등록할 때 프론트단으로 넘겨줄거임
User user = us.searchById(postUserId).get(0);
post.setPostUserId(user.getUserid());
post.setUser(user);
System.out.println("postUserId : " + post.getPostUserId());
System.out.println("user : " + user);
System.out.println("userId : " + user.getUserid());
System.out.println("userProfileImg : " + user.getProfileImg());
int result = 0;
try {
result = ps.createPost(post, request);
} catch (IllegalStateException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("게시물 등록 실패!");
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("게시물 등록 실패!");
}
if (result > 0) {
return new ResponseEntity<>("게시물 등록 성공!", HttpStatus.CREATED);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("게시물 등록 실패!");
}
}
// 게시물 등록 및 이미지 업로드
@PostMapping(value = "/addpostWithImg/{postUserId}", consumes = "multipart/form-data")
@Operation(summary = "게시물 등록 및 이미지 업로드")
public ResponseEntity<?> createPostWithImage(@PathVariable("postUserId") String postUserId,
@RequestPart("post") @Valid Post post, @RequestPart("file") MultipartFile file, HttpSession session,
HttpServletRequest request, HttpServletResponse response) throws IOException {
int result = 0;
try {
// System.out.println("https://fit-do.s3.ap-southeast-2.amazonaws.com/"+file.getOriginalFilename());
// post.setPostImg("https://fit-do.s3.ap-southeast-2.amazonaws.com/"+file.getOriginalFilename());
String fileUrl = ss.uploadFile(file);
System.out.println("fileUrl : " + fileUrl);
post.setPostImg(fileUrl);
// User객체까지 등록할 때 프론트단으로 넘겨줄거임
User user = us.searchById(postUserId).get(0);
post.setPostUserId(user.getUserid());
post.setUser(user);
System.out.println("postUserId : " + post.getPostUserId());
System.out.println("user : " + user);
System.out.println("userId : " + user.getUserid());
System.out.println("userProfileImg : " + user.getProfileImg());
result = ps.createPost(post, request);
if (result > 0) {
return new ResponseEntity<>("게시물 등록 성공!", HttpStatus.CREATED);
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("게시물 등록 실패!");
}
} catch (IllegalStateException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("게시물 등록 실패!");
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("게시물 등록 실패!");
}
}
// 게시물 수정
@PutMapping("/{postId}")
@Operation(summary = "게시물 수정")
public ResponseEntity<String> updatePost(@PathVariable int postId, @RequestBody Post post) {
post.setPostId(postId);
int result = ps.updatePost(post);
if (result > 0) {
return ResponseEntity.ok("게시물 수정 성공!");
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("게시물 수정 실패");
}
}
// 게시물 사진 수정
@PutMapping(value = "/{postid}/updateImage", consumes = "multipart/form-data")
@Operation(summary = "게시물 사진 수정")
public ResponseEntity<String> updatePostImg(@PathVariable int postid, @RequestPart MultipartFile file)
throws IOException {
try {
String url = ss.uploadFile(file);
System.out.println("게시물 사진 url : " + url);
Post post = ps.selectOnePost(postid);
if (post == null) {
return new ResponseEntity<>("해당 게시물이 존재하지 않음", HttpStatus.NOT_FOUND);
}
int updateImg = ps.updatePostImageUrl(postid, url);
System.out.println("updateImage : " + updateImg);
return new ResponseEntity<>("게시물 사진 수정 성공!", HttpStatus.OK);
} catch (IOException e) {
return new ResponseEntity<>("사진 업로드 실패", HttpStatus.BAD_REQUEST);
}
}
// 게시물 삭제
@DeleteMapping("/{postId}")
@Operation(summary = "게시물 삭제")
public ResponseEntity<String> deletePost(@PathVariable int postId) {
int result = ps.deletePost(postId);
if (result > 0) {
return ResponseEntity.ok("게시물 삭제!");
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("게시물 삭제 실패");
}
}
}
package com.ssafy.picple.domain.photo.controller;
import com.ssafy.picple.AwsS3.S3FileUploadService;
import com.ssafy.picple.config.baseResponse.BaseResponseStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ssafy.picple.config.baseResponse.BaseResponse;
import com.ssafy.picple.domain.photo.entity.Photo;
import com.ssafy.picple.domain.photo.service.PhotoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/photo")
@RequiredArgsConstructor
public class PhotoController {
private final PhotoService photoService;
private final S3FileUploadService s3FileUploadService;
@PostMapping("")
public BaseResponse<?> savePhoto(@RequestBody Photo photo, MultipartFile file) {
try {
String photoUrl = s3FileUploadService.uploadFile(file);
Photo newPhoto = Photo.builder()
.photoUrl(photoUrl)
.isShared(false)
.isDeleted(false)
.build();
return new BaseResponse<>(photoService.insertPhoto(newPhoto));
} catch (Exception e) {
return new BaseResponse<>(BaseResponseStatus.FILE_UPLOAD_ERROR);
}
}
}
정리가 잘 되어있어서 테스트에 유용하게 사용할 것 같아요! 감사합니다 :D