Skip to content

Conversation

@psychology50
Copy link
Member

μž‘μ—… 이유

  • μ±„νŒ…λ°© 생성 μ‹œ, μ±„νŒ…λ°© 이미지 선택을 μœ„ν•œ Pending 절차λ₯Ό μ œκ±°ν•˜κ³ , λΉ„μ§€λ‹ˆμŠ€ 둜직 ν”Œλ‘œμš°λ₯Ό λ‹¨μˆœν™”ν•˜κΈ° μœ„ν•œ μˆ˜μ •
  • μš”μ•½
    • 문제 원인: presigend url을 λ°œκΈ‰λ°›μ„ λ•Œ ν•„μš”ν•œ chatroom_idλŠ” μ±„νŒ…λ°© 생성 μ „κΉŒμ§€ 비결정적인 μš”μ†Œ
    • As-is: μ±„νŒ…λ°© μž„μ‹œ 생성 ν›„ λ°œκΈ‰λ˜λŠ” chatroom_idλ₯Ό 톡해 이미지λ₯Ό μ €μž₯ν•˜κ³ , S3 μ €μž₯ 이후 μ±„νŒ…λ°© 생성 ν™•μ • μš”μ²­μ„ λ³΄λ‚΄λŠ” 2λ‹¨κ³„λ‘œ ꡬ성
    • To-be: μž„μ‹œ μ €μž₯을 μœ„ν•œ chatroom_idλŠ” UUID μž„μ‹œ ν‚€λ‘œ μƒμ„±ν•˜κ³ , μ‹€μ œ μ±„νŒ…λ°© 생성 μ‹œ origin으둜 λ³΅μ›ν•˜λŠ” κ³Όμ •μ—μ„œ μ‹€μ œ μ±„νŒ…λ°© 아이디λ₯Ό μ‚½μž…ν•˜λ„λ‘ μž¬κ΅¬μ„±

μž‘μ—… 사항

1️⃣ Presigned Url λ°œκΈ‰ 둜직 μˆ˜μ •

@Tag(name = "[S3 이미지 μ €μž₯을 μœ„ν•œ Presigned URL λ°œκΈ‰ API]")
public interface StorageApi {
    @Operation(summary = "S3 이미지 μ €μž₯을 μœ„ν•œ Presigned URL λ°œκΈ‰", description = "S3에 이미지λ₯Ό μ €μž₯ν•˜κΈ° μœ„ν•œ Presigned URL을 λ°œκΈ‰ν•©λ‹ˆλ‹€.")
    @Parameters({
            @Parameter(name = "type", description = "이미지 μ’…λ₯˜", required = true, in = ParameterIn.QUERY, examples = {
                    @ExampleObject(value = "PROFILE", name = "μ‚¬μš©μž ν”„λ‘œν•„"),
                    @ExampleObject(value = "FEED", name = "ν”Όλ“œ"),
                    @ExampleObject(value = "CHATROOM_PROFILE", name = "μ±„νŒ…λ°© ν”„λ‘œν•„"),
                    @ExampleObject(value = "CHAT", name = "μ±„νŒ…"),
                    @ExampleObject(value = "CHAT_PROFILE", name = "μ±„νŒ… ν”„λ‘œν•„")
            }),
            @Parameter(name = "ext", description = "파일 ν™•μž₯자", required = true, in = ParameterIn.QUERY, examples = {
                    @ExampleObject(value = "jpg", name = "jpg"),
                    @ExampleObject(value = "png", name = "png"),
                    @ExampleObject(value = "jpeg", name = "jpeg")
            }),
            @Parameter(name = "chatroomId", description = "μ±„νŒ…λ°© ID", in = ParameterIn.QUERY, example = "123456789"),
            @Parameter(name = "request", hidden = true)
    })
    @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = PresignedUrlDto.Res.class)))
    @ApiResponseExplanations(errors = {
            @ApiExceptionExplanation(value = StorageErrorCode.class, constant = "NOT_FOUND", name = "μš”μ²­ν•œ λ¦¬μ†ŒμŠ€λ₯Ό 찾을 수 μ—†μŒ")
    })
    ResponseEntity<?> getPresignedUrl(@Validated PresignedUrlDto.Req req, BindingResult bindingResult, @AuthenticationPrincipal SecurityUserDetails user);
}
  • Ignore: πŸ›πŸ”§ Modification and refactoring of the presigned URL creation and conversion logicΒ #179 μ—μ„œ μΆ”κ°€ν–ˆλ˜ feed_id와 chat_id 쿼리 μ‚­μ œ
    • μ‚¬μš©μž ν”„λ‘œν•„: url 생성을 μœ„ν•œ user_idλŠ” 값이 이미 μ •ν•΄μ Έ μžˆμœΌλ―€λ‘œ, μ‚¬μš©μž 아이디λ₯Ό μ‚¬μš©
    • ν”Όλ“œ: url 생성을 μœ„ν•œ feed_idλŠ” 비결정적 μš”μ†Œμ΄λ―€λ‘œ, UUID둜 μž„μ‹œ ν‚€ λ°œκΈ‰
    • μ±„νŒ…λ°©: url 생성을 μœ„ν•œ chatroom_idλŠ” 비결정적 μš”μ†Œμ΄λ―€λ‘œ, UUID둜 μž„μ‹œ ν‚€ λ°œκΈ‰
    • μ±„νŒ…λ°© ν”„λ‘œν•„: url 생성을 μœ„ν•œ user_id, chatroom_id λͺ¨λ‘ μ •ν•΄μ Έ μžˆμœΌλ―€λ‘œ, ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ λ°›μ•„μ•Ό 함.
    • μ±„νŒ… ν”„λ‘œν•„: url 생성을 μœ„ν•œ chatroom_idλŠ” ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ λ°›κ³ , chat_idλŠ” UUID둜 μž„μ‹œ ν‚€ λ°œκΈ‰

public class PresignedUrlPropertyFactory {
    private final PresignedUrlProperty property;

    private PresignedUrlPropertyFactory(Long userId, String ext, ObjectKeyType type, Long chatRoomId) {
        this.property = switch (type) {
            case PROFILE -> new ProfileUrlProperty(userId, ext);
            case CHATROOM_PROFILE -> new ChatRoomProfileUrlProperty(ext);
            case CHAT_PROFILE -> new ChatProfileUrlProperty(userId, chatRoomId, ext);
            case CHAT -> new ChatUrlProperty(chatRoomId, ext);
            case FEED -> new FeedUrlProperty(ext);
        };
    }

    public static PresignedUrlPropertyFactory createInstance(String ext, ObjectKeyType type, Long userId, Long chatRoomId) {
        return new PresignedUrlPropertyFactory(userId, ext, type, chatRoomId);
    }

    public PresignedUrlProperty getProperty() {
        return property;
    }
}
public abstract class BaseUrlProperty implements PresignedUrlProperty {
    private static final Set<String> extensionSet = Set.of("jpg", "png", "jpeg");

    protected final String imageId;
    protected final String timestamp;
    protected final String ext;
    protected final ObjectKeyType type;

    protected BaseUrlProperty(String ext, ObjectKeyType type) {
        if (!extensionSet.contains(ext)) {
            throw new IllegalArgumentException("μ§€μ›ν•˜μ§€ μ•ŠλŠ” ν™•μž₯μžμž…λ‹ˆλ‹€.");
        }

        this.imageId = UUIDUtil.generateUUID();
        this.timestamp = String.valueOf(System.currentTimeMillis());
        this.ext = ext;
        this.type = type;
    }

    ...
}
public class ChatRoomProfileUrlProperty extends BaseUrlProperty {
    private final String chatroomId;

    public ChatRoomProfileUrlProperty(String ext) {
        super(ext, ObjectKeyType.CHATROOM_PROFILE);
        this.chatroomId = UUIDUtil.generateUUID();
    }

    @Override
    public Map<String, String> variables() {
        ...
    }
}
  • 기쑴의 μΈν„°νŽ˜μ΄μŠ€λ‘œλ§Œ μ •μ˜ν–ˆλ˜ Property 정보λ₯Ό 좔상 클래슀λ₯Ό μ‚¬μš©ν•˜μ—¬, 쀑볡 μš”μ†Œμ— λŒ€ν•œ μœ νš¨μ„± 처리λ₯Ό ν•œ 곳으둜 톡합.
  • Factory의 κ³Όλ„ν•œ Builder νŒ¨ν„΄μ„ μ œκ±°ν•˜μ—¬, 보닀 μ½”λ“œλ₯Ό λͺ…μ‹œμ μœΌλ‘œ 이해할 수 μžˆλ„λ‘ μˆ˜μ •ν•¨.

2️⃣ μ±„νŒ…λ°© 생성 컨트둀러 μˆ˜μ •

  • Pend, Create 두 개의 컨트둀러둜 κ΅¬μ„±λ˜μ–΄ 있던 μž‘μ—…μ„ ν•˜λ‚˜λ‘œ ν†΅ν•©ν•˜μ—¬ 처리.
  • deleteUrl을 originUrl둜 μΉ˜ν™˜ν•˜κΈ° μœ„ν•΄μ„œ, Service λ‘œμ§μ—μ„œ TSID 기반 IDλ₯Ό 생성 ν›„ S3Adapter둜 전달. -> λ³€κ²½λ˜μ–΄μ•Ό ν•  μš”μ†Œκ°€ 무엇인지 μ•Œλ €μ£Όμ–΄μ•Ό 함.
    • κ·ΈλŸ¬λ‚˜, 이 κ³Όμ •μ—μ„œ κ°œλ°œμžκ°€ S3 μ •μ±… μ„ΈλΆ€ 사항에 κ³Όν•˜κ²Œ 많이 μ•Œμ•„μ•Ό ν•œλ‹€λŠ” λ¬Έμ œκ°€ μ‘΄μž¬ν•˜μ—¬, λ‹€μŒκ³Ό 같이 μΆ”μƒν™”ν–ˆμŠ΅λ‹ˆλ‹€.
/**
 * μž„μ‹œ μ €μž₯ URLμ—μ„œ μž„μ˜λ‘œ μ„€μ •λœ IDλ₯Ό μ‹€μ œ ID둜 λ³€κ²½ν•˜κΈ° μœ„ν•œ 정보λ₯Ό μ œκ³΅ν•˜λŠ” 클래슀
 */
public final class ActualIdProvider {
    private final ObjectKeyType type;
    private final Map<String, String> actualIds;

    private ActualIdProvider(ObjectKeyType type, Map<String, String> actualIds) {
        this.type = type;
        this.actualIds = actualIds;
    }

    ...

    /**
     * μ±„νŒ…λ°© ν”„λ‘œν•„ 이미지 URL을 μƒμ„±ν•˜κΈ° μœ„ν•œ ActualIdProvider μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
     *
     * @param chatroomId μ‹€μ œ μ±„νŒ…λ°© ID
     */
    public static ActualIdProvider createInstanceOfChatroomProfile(Long chatroomId) {
        Map<String, String> ids = new HashMap<>();
        ids.put("chatroom_id", chatroomId.toString());
        return new ActualIdProvider(ObjectKeyType.CHATROOM_PROFILE, ids);
    }

    ...
}
@Slf4j
@Adapter
@RequiredArgsConstructor
public class AwsS3Adapter {
    private final AwsS3Provider awsS3Provider;

    /**
     * μž„μ‹œ μ €μž₯ κ²½λ‘œμ—μ„œ 원본 μ €μž₯ 경둜둜 사진을 λ³΅μ‚¬ν•˜κ³ , 원본이 μ €μž₯된 ν‚€λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
     *
     * @param deleteImageUrl String : μž„μ‹œ μ €μž₯ 이미지 URL
     * @param type           {@link ActualIdProvider} : μ‹€μ œ IDλ₯Ό μ œκ³΅ν•˜λŠ” 클래슀
     * @return ν”„λ‘œν•„ 이미지 원본이 μ €μž₯된 key
     * @throws StorageException ν”„λ‘œν•„ 이미지 URL이 μœ νš¨ν•˜μ§€ μ•Šμ„ λ•Œ
     */
    public String saveImage(String deleteImageUrl, ActualIdProvider type) {
        if (!awsS3Provider.isObjectExist(deleteImageUrl)) {
            log.info("ν”„λ‘œν•„ 이미지 URL이 μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.");
            throw new StorageException(StorageErrorCode.NOT_FOUND);
        }

        return awsS3Provider.copyObject(type, deleteImageUrl);
    }

    ...
}
  • deleteUrl을 originUrl둜 λ³€κ²½ν•˜κΈ° μœ„ν•΄ ν•„μš”ν•œ 정보λ₯Ό μš”κ΅¬ν•˜λŠ” ActualIdProviderλ₯Ό μ‚¬μš©ν•˜μ—¬, ν΄λΌμ΄μ–ΈνŠΈκ°€ λ¬Έμžμ—΄ 의쑴적인 킀와 S3 μ •μ±… 세뢀사항에 λŒ€ν•œ 관심을 쀄일 수 있게 μˆ˜μ •.

리뷰어가 μ€‘μ μ μœΌλ‘œ 확인해야 ν•˜λŠ” λΆ€λΆ„

# Request
{
  "title": "νŽ˜λ‹ˆμ›¨μ΄",
  "description": "νŽ˜λ‹ˆμ›¨μ΄ μ±„νŒ…λ°©μž…λ‹ˆλ‹€.",
  "password": "123456",
  "backgroundImageUrl": "delete/chatroom/49791a78-7b2b-4d2f-8f41-cb48febfc3bc/eb51ff2e-2b79-4683-b503-262346f88c8a_1729338581781.png"
}
# Response
{
  "code": "2000",
  "data": {
    "chatRoom": {
      "id": 635445399499637100,
      "title": "νŽ˜λ‹ˆμ›¨μ΄",
      "description": "νŽ˜λ‹ˆμ›¨μ΄ μ±„νŒ…λ°©μž…λ‹ˆλ‹€.",
      "backgroundImageUrl": "chatroom/635445399499637070/origin/eb51ff2e-2b79-4683-b503-262346f88c8a_1729338581781.png",
      "isPrivate": true,
      "participantCount": 1,
      "createdAt": "2024-10-19 20:53:09"
    }
  }
}

λ³€κ²½λœ μ±„νŒ…λ°© 생성 ν”Œλ‘œμš°λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  1. ν΄λΌμ΄μ–ΈνŠΈκ°€ μ±„νŒ…λ°© 사진을 선택
  2. GET /v1/storage/presigend-url μš”μ²­μœΌλ‘œ, μž„μ‹œ μ €μž₯ 경둜 μˆ˜μ‹ 
  3. PUT {S3 presigned url} μš”μ²­μœΌλ‘œ 사진 μ €μž₯.
  4. μ±„νŒ…λ°© 정보λ₯Ό POST /v2/chat-rooms둜 전달. (url은 /delete/~.{ext} λ²”μœ„λ‘œ νŒŒμ‹±ν•˜μ—¬ 전달)
  5. μ„œλ²„μ—μ„œ μ±„νŒ…λ°© IDλ₯Ό 생성 ν›„, μž„μ‹œ μ €μž₯된 사진을 원본 μ €μž₯μ†Œλ‘œ copyν•œ ν›„ μ±„νŒ…λ°© 정보 생성
  6. μ„œλ²„μ—μ„œ ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ μƒμ„±λœ μ±„νŒ…λ°© 정보 λ°˜ν™˜

λ°œκ²¬ν•œ 이슈

  • presigned url 생성 μ‹œ, νŠΉμ • μ±„νŒ…λ°©μ˜ ID둜 URL을 μƒμ„±ν•˜λ―€λ‘œ, ν•΄λ‹Ή μ±„νŒ…λ°©μ— λŒ€ν•œ μžμ› μ ‘κ·Ό 검사가 λˆ„λ½λ˜μ–΄ 있음.
  • μ΄λŠ” μ‚¬μš©μžκ°€ μž„μ˜μ˜ IDλ₯Ό μ£Όμž…ν•΄λ„ 성곡함을 μ˜λ―Έν•˜λ―€λ‘œ, λ³΄μ•ˆ μœ„ν˜‘μ΄ 됨.

@psychology50 psychology50 added bug κΈ΄κΈ‰ν•˜κ³ , μ€‘μš”λ„κ°€ 높은 이슈 fix κΈ°λŠ₯ μˆ˜μ • labels Oct 19, 2024
@psychology50 psychology50 self-assigned this Oct 19, 2024
@psychology50 psychology50 merged commit f051fe7 into dev Oct 19, 2024
1 check passed
@psychology50 psychology50 deleted the fix/create-chat-room branch October 19, 2024 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug κΈ΄κΈ‰ν•˜κ³ , μ€‘μš”λ„κ°€ 높은 이슈 fix κΈ°λŠ₯ μˆ˜μ •

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants