기능
을 사용하였는지쉽게 말하자면, 스토리지 즉 구글 드라이브 처럼 파일 저장 서비스이며, 데이터를 온라인으로 오브젝트 형태로 저장하는 서비스라고 보면 된다.
Simple Storage Service
앞에 온라인이라는 글자가 붙는 이유는 데이터 조작에 HTTP/HTTPS를 통한 API가 사용되기 때문이다.
또한 편리한 UI 인터페이스를 통해 어디서나 쉽게 데이터를 저장하고 불러올 수 있어 개발자가 쉽게 웹 규모 컴퓨팅 작업을 수행할 수 있도록 한다.
user -> server -> S3 이미지 업로드
user -> client(pre-signed URL 발급 요청 ) -> server(S3에 pre-signed URL 발급요청)->client(pre-signed URL 권한을 이용 하여 S3에 이미지 파일 업로드)-> server(파일이 아닌 url로 글 등록)
이미지 업로드는 매우 부하가 큰 작업으로 JSON을 주고 받는 일반 API요청에 비하면 훨씬 부하가 큽니다. 이미지 파일 용량 자체가 매우 크니깐요. 따라서 이미지 업로드가 백엔드 서버를 거치게 되면 백엔드 서버가 금방 죽을 수 있다.
프론트에서 바로 S3와 같은 파일저장소나 데이터베이스로 데이터를 보내지 않고 백엔드를 거치는 이유를 생각해봐야 되요. 보안문제 때문이죠. 아무나 업로드하고 삭제하면 안되니깐요. 저희가 정해둔 규칙 안에서 데이터가 관리되어야겠죠.
PresignedUrl을 이용하면 이미지를 업로드할 때 백엔드 서버를 거치지 않고 클라이언트에서 바로 S3로 업로드가 가능해져요. 원래는 이미지를 백엔드가 이미지를 S3로 전달함과 동시에 보안절차(aws sdk secret key활용해서 aws s3 접속)도 같이 한번에 진행이 되죠. 이 과정을 분리시키는거에요. 백엔드는 presignedUrl 생성으로 보안절차 작업만 해주는겁니다. Client가 AWS S3로 바로 업로드할 수 있도록요.
코드
는 어떠한 로직
을 가지고 있는지vs code
//--------------------[글등록버튼 누를시 실행] --------------------------------
function contactWrite() {
var presignedUrlg = "이미지.jpa"
console.log("이미지라고 나와야함 = " + presignedUrlg)
//--------------------[프리사인url발급요청---]-------------------------
var presigned = {
"url": "http://localhost:8080/api/managers/notices/presigned",
"method": "POST",
"timeout": 0,
"headers": {
"Authorization": localStorage.getItem('accessToken'),
"Content-Type": "application/json"
},
"data": JSON.stringify({
"imageName": document.getElementById("contact-File").files[0].name
}),
};
console.log("이미지 이름이 나와야함 = " + document.getElementById("contact-File").files[0].name)
$.ajax(presigned).done(function (response) {
presignedUrlg = response.toString()
console.log("함수안에서" + presignedUrlg)
//--------------------[프리사인url발급성공시 클라이언트가 S3에 이미지 upload요청--]-------------------------
var upload = {
"url": presignedUrlg,
"method": "PUT",
"timeout": 0,
"processData": false,
"Content-Type": "binary/octet-stream",
"data": document.getElementById("contact-File").files[0]
};
$.ajax(upload).done(function (response) {
console.log("s3업로드성공시"+response);
});
//--------------------[프리사인url발급성공시 서버에 게시글 upload요청--]-------------------------
var settings = {
"url": "http://localhost:8080/api/managers/notices",
"method": "POST",
"timeout": 0,
"headers": {
"Authorization": localStorage.getItem('accessToken'),
"Content-Type": "application/json"
},
"data": JSON.stringify({
"title": $('#contactTitle').val(),
"content": $('#contactContent').val(),
}),
};
$.ajax(settings).done(function (response) {
console.log(response);
alert(response);
//window.location = './contactPageIndex-notice.html'
});
});
//----------------[프리사인 발급및 게시글 등록 끝]-------------------------
//-----------------------------------------------------------------
const dtoTitle = $('#contactTitle').val();
if (!dtoTitle) {
return alert("제목을 입력해주세요")
}
const dtoContent = $('#contactContent').val();
if (!dtoContent) {
return alert("내용을 입력해주세요")
}
console.log("함수밖에서" + presignedUrlg)
}
document.getElementById("mypage").style.display = "none";
document.getElementById("MainLogout").style.display = "none";
document.getElementById("adminpage").style.display = "none";
if (localStorage.getItem('accessToken') === null) {
$('#findbyIdandPw').show()
} else {
$('#findbyIdandPw').hide()
}
</script>
application.yml
cloud:
aws:
credentials:
secret-key: [시크릿키]
access-key: [에세스키]
s3:
bucket: [버킷이름]
region:
static: ap-northeast-2
stack:
auto: 'false'
백엔드부분
noticeController
public class NoticeController {
private final NoticeService noticeService;
private final PresignedUrlService presignedUrlService;
private String path; //이미지파일 경로
//관리자 공지사항 등록
@PostMapping("")
public ResponseEntity saveNotice(@RequestBody @Valid NoticeRequest noticeRequest,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
String imageUrl = presignedUrlService.findByName(path);
noticeService.saveNotice(noticeRequest, userDetails.getBase().getId(),imageUrl);
return ResponseEntity.ok("등록 완료");
//new ResponseEntity<>("등록완료",HttpStatus.CREATED);
}
/**
* S3에게 pre-signed URL (권한) 요청
*/
@PostMapping("/presigned")
public String createPresigned(@RequestBody ImageNameDTO imageNameDTO
) {
path ="contact"; //원하는 경로 지정
String imageName = imageNameDTO.getImageName();
return presignedUrlService.getPreSignedUrl(path,imageName);
}
....
noticeService
@Transactional
@Override
public void saveNotice(@Valid NoticeRequest noticeRequest, Long managerId,String imageUrl) {
Notice notice = noticeRequest.toEntity(managerId,imageUrl);
noticeRepository.save(notice);
}
noticeRequestDto
@Getter
@NoArgsConstructor(force = true, access = AccessLevel.PROTECTED)
public class NoticeRequest {
@NotBlank
private final String title;
@NotBlank
private final String content;
@Builder
public NoticeRequest(String title, String content) {
this.title = title;
this.content = content;
}
public Notice toEntity(Long managerId, String imageUrl) {
return Notice.builder()
.managerId(managerId)
.title(title)
.content(content)
.imageUrl(imageUrl)
.build();
}
}
noticeResponseDto
@Getter
@NoArgsConstructor(force = true,access = AccessLevel.PROTECTED)
public class NoticeResponse {
private final Long id;
private final Long managerId;
private final String title;
private final String content;
private final String image;
@JsonFormat(shape= JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm")
private final LocalDateTime createdDate;
//private final LocalDateTime modifiedDate;
public NoticeResponse(Notice notice) {
this.id = notice.getId();
this.managerId = notice.getManagerId();
this.title = notice.getTitle();
this.content = notice.getContent();
this.createdDate = notice.getCreatedDate();
this.image = notice.getImageUrl(); // 이미지 url
// this.modifiedDate = notice.getModifiedDate();
}
}
ImageNameDto
@Getter
@NoArgsConstructor(force = true)
public class ImageNameDTO {
private final String imageName;
public ImageNameDTO(String imageName) {
this.imageName = imageName;
}
}
PresignedUrlService
@Slf4j
@Component
@RequiredArgsConstructor
public class PresignedUrlService {
private final AmazonS3 amazonS3;
private String useOnlyOneFileName;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Value("${cloud.aws.region.static}")
private String location;
public String getPreSignedUrl(String prefix, String fileName) {
String onlyOneFileName = onlyOneFileName(fileName);
useOnlyOneFileName = onlyOneFileName;
if (!prefix.equals("")) {
onlyOneFileName = prefix + "/" + onlyOneFileName;
}
GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest(bucket, onlyOneFileName);
return amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString();
}
private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String fileName) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(getPreSignedUrlExpiration());
generatePresignedUrlRequest.addRequestParameter(
Headers.S3_CANNED_ACL,
CannedAccessControlList.PublicRead.toString());
return generatePresignedUrlRequest;
}
private Date getPreSignedUrlExpiration() {
Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 2;
expiration.setTime(expTimeMillis);
log.info(expiration.toString());
return expiration;
}
private String onlyOneFileName(String filename){
return UUID.randomUUID().toString()+filename;
}
public String findByName(String path) {
// if (!amazonS3.doesObjectExist(bucket,editPath+ useOnlyOneFileName))
// return "File does not exist";
log.info("Generating signed URL for file name {}", useOnlyOneFileName);
// return amazonS3.getUrl(bucket,editPath+useOnlyOneFileName).toString();
return "https://"+bucket+".s3."+location+".amazonaws.com/"+path+"/"+useOnlyOneFileName;
}
}
S3Config
@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
@Primary
public BasicAWSCredentials awsCredentialsProvider(){
return new BasicAWSCredentials(accessKey, secretKey);
}
@Bean
public AmazonS3 amazonS3Client() {
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider()))
.build();
}
}
코드
를 작성하며 발견된 버그
나 오류
는 어떠한게 있었는지 그리고 어떻게 해결하였는지.맥인 나의 컴퓨터로 로직 작성후 공지사항에 이미지파일 함께 올렸을때403 에러가 발생했다.
그래서 다른 팀원의 페이지 이미지 업로드 해도 마찬가지였다.
하지만 다른 팀원은 에러 없이 잘 되었다.
팀원 모두 윈도우 사용중이라 혹시 맥북문제인가?? 혹시 모르니 집에 있던 윈도우컴으로 시도 했다.
윈도우컴으로 하니 팀원페이지 , 내 페이지도 에러없이 잘되었다.
추후 AWS 배포를 맡기로 하여 새로 내가 만든 S3버킷 사용시 문제 해결
아마도 설정에 문제가 있었던 것 같다.
클라이언트에서 S3에 있는 사진에 접근하려면 어떻게 해야되나요?