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

[Refactor] #252 - S3 Presigned Url 서비스 리팩토링 #253

Merged
merged 6 commits into from
Mar 13, 2024
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.moonshot.user.controller;

import lombok.RequiredArgsConstructor;
import org.moonshot.model.Logging;
import org.moonshot.response.MoonshotResponse;
import org.moonshot.response.SuccessType;
import org.moonshot.s3.S3Service;
import org.moonshot.s3.dto.request.NotifyImageSaveSuccessRequestDto;
import org.moonshot.s3.dto.response.PresignedUrlVO;
import org.moonshot.user.model.LoginUser;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -15,27 +17,23 @@
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v1")
@RequestMapping("/v1/image")
@RequiredArgsConstructor
public class ImageController {

private final S3Service s3Service;

//TODO
// 추후 로그인 유저를 확인하여 해당 유저에 대한 데이터로 getUploadPreSignedUrl로 username을 넘기는 로직으로 변경해야 함.
@GetMapping("/image")
public ResponseEntity<MoonshotResponse<PresignedUrlVO>> getPresignedUrl() {
return ResponseEntity.status(HttpStatus.OK).body(
MoonshotResponse.success(
SuccessType.GET_PRESIGNED_URL_SUCCESS, s3Service.getUploadPreSignedUrl("test", "SMC")));
@GetMapping
@Logging(item = "Image", action = "Get")
public ResponseEntity<MoonshotResponse<PresignedUrlVO>> getPresignedUrl(@LoginUser Long userId) {
return ResponseEntity.status(HttpStatus.OK)
.body(MoonshotResponse.success(SuccessType.GET_PRESIGNED_URL_SUCCESS, s3Service.getUploadPreSignedUrl("test", userId)));
its-sky marked this conversation as resolved.
Show resolved Hide resolved
}

//TODO
// 해당 API도 username을 @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : username")
// 등을 이용하여 Annotation화 하여 바로 username을 넘길 수 있도록 변경해야 함.
@PostMapping("/image")
public ResponseEntity<MoonshotResponse<?>> notifyImageSaveSuccess(@RequestBody final NotifyImageSaveSuccessRequestDto request) {
s3Service.notifyImageSaveSuccess(request);
@PostMapping
@Logging(item = "Image", action = "Post")
public ResponseEntity<MoonshotResponse<?>> notifyImageSaveSuccess(@LoginUser Long userId, @RequestBody final NotifyImageSaveSuccessRequestDto request) {
s3Service.notifyImageSaveSuccess(userId, request);
return ResponseEntity.status(HttpStatus.CREATED).body(MoonshotResponse.success(SuccessType.POST_NOTIFY_IMAGE_SAVE_SUCCESS));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public record UserInfoResponse(
public static UserInfoResponse of(User user) {
return new UserInfoResponse(
user.getSocialPlatform().getValue(),
user.getProfileImage(),
user.getImageUrl(),
user.getNickname(),
user.getDescription());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.moonshot.user.service;

import lombok.RequiredArgsConstructor;
import org.moonshot.s3.ImageEvent;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
public class ImageEventListener {

private final UserService userService;

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handleImageEvent(ImageEvent imageEvent) {
userService.updateUserProfileImage(imageEvent.userId(), imageEvent.imageUrl());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public SocialLoginResponse googleLogin(final SocialLoginRequest request) {
.socialId(userResponse.sub())
.socialPlatform(request.socialPlatform())
.name(userResponse.name())
.profileImage(userResponse.picture())
.imageUrl(userResponse.imageUrl())
.email(userResponse.email())
.build());
user = newUser;
Expand Down Expand Up @@ -121,7 +121,7 @@ public SocialLoginResponse kakaoLogin(final SocialLoginRequest request) {
.socialId(userResponse.id())
.socialPlatform(request.socialPlatform())
.name(userResponse.kakaoAccount().profile().nickname())
.profileImage(userResponse.kakaoAccount().profile().profileImageUrl())
.imageUrl(userResponse.kakaoAccount().profile().imageUrl())
.email(null)
.build());
user = newUser;
Expand Down Expand Up @@ -176,14 +176,19 @@ public UserInfoResponse getMyProfile(final Long userId) {
return UserInfoResponse.of(user);
}

public void updateUserProfileImage(final Long userId, final String imageUrl) {
User user = userRepository.findById(userId).orElseThrow(() -> new NotFoundException(NOT_FOUND_USER));
user.modifyProfileImage(imageUrl);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void publishSignUpEvent(final User user) {
eventPublisher.publishEvent(SignUpEvent.of(
user.getName(),
user.getEmail() == null ? "" : user.getEmail(),
user.getSocialPlatform().toString(),
LocalDateTime.now(),
user.getProfileImage()
user.getImageUrl()
));
}

Expand Down
2 changes: 1 addition & 1 deletion moonshot-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ spring:

flyway:
baseline-on-migrate: false
baseline-version: 1
baseline-version: 2
enabled: false

google:
Expand Down
1 change: 1 addition & 0 deletions moonshot-api/src/main/resources/db/migration/V3__DDL.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE user CHANGE profile_image image_url varchar(255);
31 changes: 22 additions & 9 deletions moonshot-domain/src/main/java/org/moonshot/user/model/User.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package org.moonshot.user.model;

import jakarta.persistence.*;
import lombok.*;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.time.LocalDateTime;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
Expand Down Expand Up @@ -32,7 +40,7 @@ public class User {
private String name;

@Column(nullable = false)
private String profileImage;
private String imageUrl;

private String email;

Expand All @@ -43,24 +51,24 @@ public class User {
private LocalDateTime deleteAt;

@Builder
private User(String socialId, SocialPlatform socialPlatform, String name, String profileImage, String email,
private User(String socialId, SocialPlatform socialPlatform, String name, String imageUrl, String email,
String nickname, String description) {
this.socialId = socialId;
this.socialPlatform = socialPlatform;
this.name = name;
this.profileImage = profileImage;
this.imageUrl = imageUrl;
this.email = email;
this.nickname = nickname;
this.description = description;
}

@Builder(builderMethodName = "builderWithSignIn")
public static User of(String socialId, SocialPlatform socialPlatform, String name, String profileImage, String email) {
public static User of(String socialId, SocialPlatform socialPlatform, String name, String imageUrl, String email) {
return builder()
.socialId(socialId)
.socialPlatform(socialPlatform)
.name(name)
.profileImage(profileImage)
.imageUrl(imageUrl)
.email(email)
.build();
}
Expand All @@ -69,9 +77,14 @@ public static User of(String socialId, SocialPlatform socialPlatform, String nam

public void modifyDescription(String description) { this.description = description; }

public void modifyProfileImage(String imageUrl) {
this.imageUrl = imageUrl;
}

public void resetDeleteAt() {
this.deleteAt = null;
}

public void setDeleteAt(){
this.deleteAt = LocalDateTime.now().plusDays(USER_RETENTION_PERIOD);
}
Expand Down
7 changes: 6 additions & 1 deletion moonshot-external/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ repositories {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

// JWT
// Transaction Retry
implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework:spring-aspects'

// JWT
implementation group: "io.jsonwebtoken", name: "jjwt-api", version: "0.11.2"
implementation group: "io.jsonwebtoken", name: "jjwt-impl", version: "0.11.2"
implementation group: "io.jsonwebtoken", name: "jjwt-jackson", version: "0.11.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ public record GoogleInfoResponse(
String name,
String givenName,
String familyName,
String picture,
String imageUrl,
its-sky marked this conversation as resolved.
Show resolved Hide resolved
String email,
Boolean emailVerified,
String locale
) {
public static GoogleInfoResponse of(String sub, String name, String givenName, String familyName, String picture, String email, Boolean emailVerified, String locale) {
return new GoogleInfoResponse(sub, name, givenName, familyName, picture, email, emailVerified, locale);
public static GoogleInfoResponse of(String sub, String name, String givenName, String familyName, String imageUrl, String email, Boolean emailVerified, String locale) {
return new GoogleInfoResponse(sub, name, givenName, familyName, imageUrl, email, emailVerified, locale);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record KakaoUserProfile(
String nickname,
String profileImageUrl
String imageUrl
its-sky marked this conversation as resolved.
Show resolved Hide resolved
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.moonshot.s3;

public record ImageEvent(Long userId, String imageUrl, ImageType imageType) {

public static ImageEvent of(Long userId, String imageUrl, ImageType imageType) {
return new ImageEvent(userId, imageUrl, imageType);
}

}
15 changes: 15 additions & 0 deletions moonshot-external/src/main/java/org/moonshot/s3/ImageType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.moonshot.s3;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum ImageType {

PROFILE("프로필");

private final String value;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.moonshot.s3;

import org.moonshot.exception.BadRequestException;
import org.moonshot.response.ErrorType;
import org.springframework.core.convert.converter.Converter;

public class ImageTypeConverter implements Converter<String, ImageType> {

@Override
public ImageType convert(String source) {
for (ImageType imageType : ImageType.values()) {
if (imageType.getValue().equals(source)) {
return imageType;
}
}
throw new BadRequestException(ErrorType.INVALID_TYPE);
}

}
37 changes: 25 additions & 12 deletions moonshot-external/src/main/java/org/moonshot/s3/S3Service.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.moonshot.config.AWSConfig;
import org.moonshot.constants.AWSConstants;
import org.moonshot.s3.dto.request.NotifyImageSaveSuccessRequestDto;
import org.moonshot.s3.dto.response.PresignedUrlVO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
Expand All @@ -18,16 +22,18 @@ public class S3Service {

private final String bucketName;
private final AWSConfig awsConfig;
private final ApplicationEventPublisher eventPublisher;
private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSSS");

public S3Service(@Value("${aws.s3-bucket-name}") final String bucketName,
AWSConfig awsConfig) {
AWSConfig awsConfig, ApplicationEventPublisher eventPublisher) {
this.bucketName = bucketName;
this.awsConfig = awsConfig;
this.eventPublisher = eventPublisher;
}

public PresignedUrlVO getUploadPreSignedUrl(String prefix, String username) {
final String fileName = generateFileName(username);
public PresignedUrlVO getUploadPreSignedUrl(final String prefix, final Long userId) {
final String fileName = generateFileName(userId);
final String key = prefix + "/" + fileName;

S3Presigner preSigner = awsConfig.getS3Presigner();
Expand All @@ -47,16 +53,23 @@ public PresignedUrlVO getUploadPreSignedUrl(String prefix, String username) {
return PresignedUrlVO.of(key, url);
}

public void notifyImageSaveSuccess(final NotifyImageSaveSuccessRequestDto request) {
//TODO
// 추후 User 엔티티 개발 후
// username 아이디를 가진 User 정보에 profile image를 bucketName + key로 삽입하면 됨.
// 이는 UserService로 위임하여 데이터 처리하도록 하면 됨.
// 또한 기존의 S3에 저장되어 있던 이미지를 삭제해야 함.
@Transactional
@Retryable(maxAttempts = 3, backoff = @Backoff(2000))
public void notifyImageSaveSuccess(final Long userId, final NotifyImageSaveSuccessRequestDto request) {
String imageUrl = getImageUrl(request.fileName());
publishImageEvent(userId, imageUrl, request.imageType());
}

private String generateFileName(final Long userId) {
return userId + "-" + simpleDateFormat.format(new Date());
}

private String getImageUrl(final String fileName) {
return "https://" + bucketName + ".s3.ap-northeast-2.amazonaws.com/" + fileName;
}

private String generateFileName(String username) {
return username + "-" + simpleDateFormat.format(new Date());
private void publishImageEvent(final Long userId, final String imageUrl, final ImageType imageType) {
eventPublisher.publishEvent(ImageEvent.of(userId, imageUrl, imageType));
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.moonshot.s3.dto.request;

import org.moonshot.s3.ImageType;

public record NotifyImageSaveSuccessRequestDto(
String key,
String username
String fileName,
ImageType imageType
) {
}
Loading