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

Api: πŸ› Modify the business logic of chat room creation #180

Merged
merged 19 commits into from
Oct 19, 2024

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.

1 participant