Skip to content

Conversation

@wlgns12370
Copy link
Contributor

@wlgns12370 wlgns12370 commented Sep 8, 2025

✨ 구현한 기능

📢 DB 제약 조건의 결정

프로필 생성 시의 동시성 문제를 해결하기 위해 여러 방법을 고려했습니다. 가장 먼저 떠올릴 수 있는 방법은 JPA의 @Lock을 활용한 비관적 락(Pessimistic Lock)입니다. 하지만 저는 데이터베이스의 Unique 제약 조건을 활용해 예외를 처리하는 방식을 최종적으로 선택했습니다.

그 이유는 다음과 같습니다.

비관적 락은 '자원 소모가 큰' 접근 방식입니다.

비관적 락은 데이터 충돌이 발생할 것이라 가정하고, 트랜잭션이 시작될 때부터 특정 데이터에 락을 겁니다.

  • 성능 저하: 락이 걸린 동안 다른 트랜잭션은 대기해야하기 때문에, 동시 요청이 많은 상황에 대기 시간이 시스템 전체의 처리량(Throughput)을 저하시킬 수 있습니다.
  • 불필요한 제어: User 테이블에 락을 건다면 프로필 생성과 무관한 다른 로직까지 막아버리는 과도한 제어가 될 수 있습니다.

DB 제약 조건 활용하기

데이터베이스의 Unique 제약 조건을 활용하는 방식은 다음과 같은 장점을 가집니다.

  • 최고의 효율성: 락을 위한 추가적인 자원 소모가 전혀 없습니다. 시스템은 평소처럼 최대 속도로 동작하다가, 정말로 중복된 데이터 생성이 시도되는 '드문' 경우에만 데이터베이스가 예외를 발생시킵니다.
  • 안정성과 간결함: 동시성 제어 책임을 개발자가 아닌, 데이터 정합성을 위해 만들어진 데이터베이스에 위임하는 방식입니다. 코드는 try-catch로 예외 상황을 처리하기만 하면 되므로 매우 간결하고, 데이터베이스의 동작은 신뢰할 수 있습니다.

결론적으로, 불필요한 비관적 락으로 인한 자원 낭비와 성능 저하를 피하고, 데이터베이스 제약조건을 활용하여 효율적이고 안정적으로 동시성 문제를 해결할 수 있다고 판단하였습니다.

@coderabbitai
Copy link

coderabbitai bot commented Sep 8, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds handling in ProfileService.registerProfile to catch DataIntegrityViolationException and throw CustomRuntimeException(ErrorCode.PROFILE_ALREADY_EXISTS, userId). Adds a concurrency integration test class that attempts simultaneous registrations, and updates build.gradle to exclude that test from test-source compilation.

Changes

Cohort / File(s) Summary
Profile service error handling
src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java
Imports DataIntegrityViolationException and adds a dedicated catch (DataIntegrityViolationException ex) to map DB unique-constraint violations to CustomRuntimeException(ErrorCode.PROFILE_ALREADY_EXISTS, userId).
Concurrency test (added, excluded from test sources)
src/test/java/life/mosu/mosuserver/application/profile/ProfileServiceConcurrencyTest.java
New Spring Boot integration test that launches two concurrent registerProfile calls, asserts a single persisted profile and that one thread throws CustomRuntimeException.
Test-source exclusion
build.gradle
Adds sourceSets { test { java { exclude '**/ProfileServiceConcurrencyTest.java' } } } to exclude the new concurrency test from test compilation/run.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant PS as ProfileService
  participant PR as ProfileRepository
  participant DB as Database

  C->>PS: registerProfile(userId, request)
  PS->>PR: save(profile)
  PR->>DB: INSERT profile (unique user_id)
  alt Insert succeeds
    DB-->>PR: OK
    PR-->>PS: Profile
    PS-->>C: 201 Created (profile)
  else Unique constraint violation
    DB-->>PR: Error: duplicate key
    PR-->>PS: DataIntegrityViolationException
    PS->>C: throws CustomRuntimeException(PROFILE_ALREADY_EXISTS)
    note right of PS: Mapped for 4xx handling by global exception layer
  end
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Assessment against linked issues

Objective Addressed Explanation
Handle concurrent profile registration so the second request results in 4xx instead of 500; ensure no duplicate profiles; approach may use DataIntegrityViolationException handling or locking (#373) ✅ Partially Service now maps DB unique-constraint to PROFILE_ALREADY_EXISTS. A concurrency test was added to verify behavior, but the test file is excluded from test sources in build.gradle, so the test won't run unless exclusion is removed.

Poem

A rabbit hops on keys with glee,
Two signups race beneath an old oak tree.
One wins the patch, the other gently stops—
A tidy error catches duplicate hops.
Hooray for order in this coding lea! 🐇✨

✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/mosu-373

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning The build.gradle edit that excludes ProfileServiceConcurrencyTest.java from test sources is not necessary to implement the issue's runtime behavior change and effectively prevents the new concurrency test from being executed, which is out-of-scope relative to the linked issue's goal of improving reliability and verifying the fix. The ProfileService change and the new test class are in scope, but the test exclusion weakens verification and appears unrelated to resolving the duplicate-key handling. Remove the test exclusion from build.gradle or provide a clear justification in the PR and instead mark the test @disabled with an explanation or move it to an integration test suite so CI can still validate the concurrency behavior.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title "MOSU-373 refactor: profile 등록 동시 요청 처리" concisely describes the primary change — handling concurrent profile registration — and references the issue number, so it communicates intent clearly for reviewers and history. It is specific and readable, though it mixes Korean and English which is acceptable if it follows project convention.
Linked Issues Check ✅ Passed The change in ProfileService.java adds a dedicated catch for DataIntegrityViolationException and throws CustomRuntimeException(ErrorCode.PROFILE_ALREADY_EXISTS, userId), which directly addresses the linked issue #373 by converting DB unique-constraint failures into an application-level error instead of letting a DB-originated 500 propagate. The added concurrency test targets the same objective, so the code changes align with the issue's primary coding goal, although the test is currently excluded from test sources and should be enabled to verify behavior in CI.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Description Check ✅ Passed The PR description clearly includes the required "✨ 구현한 기능" section with a direct link to issue #373 and a concise summary of the change, and it provides a detailed rationale for choosing DB unique-constraint handling over pessimistic locking which covers the template's expected discussion points; the content therefore conveys what was changed and why. The description uses a custom heading "DB 제약 조건의 결정" instead of the template's exact "📢 논의하고 싶은 내용" and does not include the optional "🎸 기타" section, but those are stylistic differences and do not remove essential information. Overall the description is substantive and actionable for reviewers.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @wlgns12370, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 프로필 등록 과정에서 발생할 수 있는 동시성 문제를 효율적으로 관리하기 위한 개선사항을 포함합니다. 데이터베이스의 고유 제약 조건을 활용하여 중복 등록 시 발생하는 오류를 명확하게 처리하고, 이를 검증하는 자동화된 테스트를 도입하여 시스템의 견고성을 높였습니다.

Highlights

  • 프로필 등록 동시 요청 처리: 프로필 등록 시 동일한 사용자에 대한 동시 요청으로 인해 발생할 수 있는 데이터베이스의 고유 제약 조건 위반 오류를 DataIntegrityViolationException으로 감지하고, 이를 PROFILE_ALREADY_EXISTS라는 커스텀 예외로 변환하여 처리하도록 ProfileService에 로직을 추가했습니다.
  • 동시성 테스트 추가: 프로필 등록 기능의 동시성 문제를 검증하기 위해 ProfileServiceConcurrencyTest를 새로 추가했습니다. 이 테스트는 여러 스레드에서 동시에 프로필 등록을 시도할 때, 하나의 요청만 성공하고 나머지 요청은 예상대로 CustomRuntimeException을 발생시키는지 확인합니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

프로필 등록 시 발생할 수 있는 동시성 문제를 데이터베이스의 Unique 제약 조건을 활용하여 해결하신 점이 인상적입니다. 이 방식은 별도의 Lock을 사용하지 않아 리소스 측면에서 효율적이며, 코드도 간결하게 유지할 수 있는 좋은 접근법입니다. 또한, 동시성 문제를 검증하기 위한 테스트 케이스를 추가하여 변경 사항의 안정성을 높여주신 점도 훌륭합니다. 전반적으로 좋은 변경이라고 생각합니다. 다만, 추가된 테스트 코드의 검증 로직을 조금 더 명확하게 개선할 수 있는 부분이 있어 리뷰 코멘트를 남겼습니다.

assertThat(exceptions).hasSize(1);

Exception thrownException = exceptions.getFirst();
assertThat(thrownException).isInstanceOf(CustomRuntimeException.class);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

좋은 테스트입니다. 테스트의 정확성을 더 높이기 위해, 발생하는 예외가 CustomRuntimeException이라는 것만 검증하는 것보다 어떤 ErrorCode를 가지고 있는지까지 확인하는 것이 좋습니다. 이렇게 하면 의도치 않은 다른 CustomRuntimeException이 발생했을 때 테스트가 실패하여 문제를 더 빨리 발견할 수 있습니다.

        assertThat(thrownException).isInstanceOfSatisfying(CustomRuntimeException.class, e ->
                assertThat(e.getCode()).isEqualTo("PROFILE_ALREADY_EXISTS"));

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java (1)

39-53: Catch duplicate-key inside the method: flush right after save.

Without an explicit flush, the unique-constraint violation may surface at transaction commit (outside this try/catch), bypassing your intended mapping to CustomRuntimeException. Force the insert to execute here.

-            ProfileJpaEntity profile = request.toEntity(userId);
-            profileJpaRepository.save(profile);
+            ProfileJpaEntity profile = request.toEntity(userId);
+            profileJpaRepository.saveAndFlush(profile); // ensure constraint violation is thrown here
🧹 Nitpick comments (5)
src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java (2)

48-50: Narrow the mapping to unique-constraint only (don’t collapse all DataIntegrityViolationException).

DataIntegrityViolationException also covers NOT NULL, FK, length, etc. Map to PROFILE_ALREADY_EXISTS only when it’s a unique/duplicate-key error; otherwise rethrow.

-        } catch (DataIntegrityViolationException ex) {
-            throw new CustomRuntimeException(ErrorCode.PROFILE_ALREADY_EXISTS, userId);
+        } catch (DataIntegrityViolationException ex) {
+            if (isUniqueConstraintViolation(ex)) {
+                throw new CustomRuntimeException(ErrorCode.PROFILE_ALREADY_EXISTS, userId);
+            }
+            throw ex;

Add helper:

// Place inside ProfileService
private boolean isUniqueConstraintViolation(Throwable t) {
    for (Throwable c = t; c != null; c = c.getCause()) {
        if (c instanceof org.hibernate.exception.ConstraintViolationException) {
            String sqlState = ((org.hibernate.exception.ConstraintViolationException) c).getSQLState();
            if ("23505".equals(sqlState) || "23000".equals(sqlState)) return true; // Postgres/H2, MySQL/MariaDB
        }
        if (c instanceof java.sql.SQLIntegrityConstraintViolationException) return true;
    }
    return false;
}

74-77: Method name misleads; it checks role, not profile existence.

checkIfProfileExistsForUser gates on ROLE_PENDING rather than actual profile presence. Consider renaming for clarity (e.g., assertUserIsPendingForProfileRegistration) or checking repository if you want semantic alignment.

src/test/java/life/mosu/mosuserver/application/profile/ProfileServiceConcurrencyTest.java (3)

105-109: Assert the exact error code for stronger guarantees.

Validate that the mapped exception is PROFILE_ALREADY_EXISTS, not just the type.

-        Exception thrownException = exceptions.get(0);
-        assertThat(thrownException).isInstanceOf(CustomRuntimeException.class);
+        Exception thrownException = exceptions.get(0);
+        assertThat(thrownException).isInstanceOf(CustomRuntimeException.class);
+        assertThat(((CustomRuntimeException) thrownException).getErrorCode())
+            .isEqualTo(life.mosu.mosuserver.global.exception.ErrorCode.PROFILE_ALREADY_EXISTS);

82-99: Make the start barrier explicit and time-bounded to reduce flakiness.

CountDownLatch-as-barrier works but can hang if a task errors before await. Use a CyclicBarrier or add a timeout to await.

-                    latch.countDown();
-                    latch.await();
+                    latch.countDown();
+                    if (!latch.await(3, TimeUnit.SECONDS)) {
+                        throw new IllegalStateException("Barrier timeout");
+                    }

72-109: Alternative: capture task outcomes via Future#get for deterministic completion.

Submitting Callables and collecting results/exceptions via get() simplifies shutdown/await logic.

If you want, I can provide a Future-based variant that asserts exactly one CustomRuntimeException and one success deterministically.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33eeee6 and fb2b5f8.

📒 Files selected for processing (2)
  • src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java (2 hunks)
  • src/test/java/life/mosu/mosuserver/application/profile/ProfileServiceConcurrencyTest.java (1 hunks)
🔇 Additional comments (2)
src/main/java/life/mosu/mosuserver/application/profile/ProfileService.java (2)

48-50: 4xx mapping confirmedErrorCode.PROFILE_ALREADY_EXISTS carries HttpStatus.CONFLICT and is returned as a 409 via GlobalExceptionHandler.handleCustomRuntimeException ([src/main/java/life/mosu/mosuserver/global/exception/GlobalExceptionHandler.java#179-185]).


46-46: Event listener runs AFTER_COMMIT; no change needed
ProfileTxEventListener is annotated with @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT), ensuring publishSuccessEvent handlers execute only after the transaction commits.

Comment on lines +107 to +109
Exception thrownException = exceptions.getFirst();
assertThat(thrownException).isInstanceOf(CustomRuntimeException.class);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid Java 21-only List.getFirst(); use get(0) for wider JDK compatibility.

This fails on JDK 17 (common LTS). Replace with get(0).

-        Exception thrownException = exceptions.getFirst();
+        Exception thrownException = exceptions.get(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Exception thrownException = exceptions.getFirst();
assertThat(thrownException).isInstanceOf(CustomRuntimeException.class);
}
Exception thrownException = exceptions.get(0);
assertThat(thrownException).isInstanceOf(CustomRuntimeException.class);
}
🤖 Prompt for AI Agents
In
src/test/java/life/mosu/mosuserver/application/profile/ProfileServiceConcurrencyTest.java
around lines 107 to 109, the test uses List.getFirst() which is Java 21-only;
replace the call with exceptions.get(0) to restore JDK 17 compatibility and keep
the same behavior, i.e., retrieve the first element from the list before
asserting its type.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
build.gradle (3)

75-78: Align Testcontainers versions to avoid classpath conflicts.

You’re mixing 1.19.3 with 1.20.0; this is a common source of NoSuchMethod/Linkage errors at runtime.

Update the core and JUnit Jupiter modules to match MySQL 1.20.0:

-    testImplementation 'org.testcontainers:testcontainers:1.19.3'
-    testImplementation 'org.testcontainers:junit-jupiter:1.19.3'
-    testImplementation 'org.testcontainers:mysql:1.20.0'
+    testImplementation 'org.testcontainers:testcontainers:1.20.0'
+    testImplementation 'org.testcontainers:junit-jupiter:1.20.0'
+    testImplementation 'org.testcontainers:mysql:1.20.0'

86-91: AWS SDK: inconsistent versions (BOM 2.20.0 vs core 2.30.20).

Mixing BOM-managed and explicitly pinned versions breaks dependency alignment.

Unify on a single version via the BOM and drop the explicit core:

-    implementation platform('software.amazon.awssdk:bom:2.20.0')
+    implementation platform('software.amazon.awssdk:bom:2.30.20')
     implementation 'software.amazon.awssdk:s3'
     implementation 'software.amazon.awssdk:sts'
-    implementation 'software.amazon.awssdk:core:2.30.20'
     implementation 'software.amazon.awssdk:ec2'

52-54: Replace javax JSTL with the Jakarta (GlassFish) implementation

File: build.gradle (lines 52–54) — change dependency:

-    implementation 'javax.servlet:jstl:1.2'
+    implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb2b5f8 and 0627278.

📒 Files selected for processing (1)
  • build.gradle (1 hunks)

Comment on lines +17 to +23
sourceSets {
test {
java {
exclude '**/ProfileServiceConcurrencyTest.java'
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t hide the new concurrency test; make it opt-in/opt-out instead of permanently excluded.

Excluding ProfileServiceConcurrencyTest.java from compilation defeats the PR’s objective and risks regressions. Prefer a property-gated exclude so CI can still run it when desired.

Apply this diff to make the exclusion conditional (run with -PincludeConcurrencyTests=true to include the test):

-sourceSets {
-    test {
-        java {
-            exclude '**/ProfileServiceConcurrencyTest.java'
-        }
-    }
-}
+sourceSets {
+    test {
+        java {
+            if (!project.hasProperty('includeConcurrencyTests')) {
+                exclude '**/ProfileServiceConcurrencyTest.java'
+            }
+        }
+    }
+}

If you want a cleaner separation, move it to an integrationTest source set and wire a dedicated task; I can provide a minimal snippet.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sourceSets {
test {
java {
exclude '**/ProfileServiceConcurrencyTest.java'
}
}
}
sourceSets {
test {
java {
if (!project.hasProperty('includeConcurrencyTests')) {
exclude '**/ProfileServiceConcurrencyTest.java'
}
}
}
}
🤖 Prompt for AI Agents
In build.gradle around lines 17–23 the ProfileServiceConcurrencyTest.java is
permanently excluded which hides the new concurrency test; change the exclusion
to be property-gated so the test is excluded by default but included when the
build is invoked with -PincludeConcurrencyTests=true. Implement a conditional
check on project.hasProperty('includeConcurrencyTests') (or
project.property('includeConcurrencyTests') == 'true') and only add the exclude
when that property is not present/false; alternatively, move the test into an
integrationTest sourceSet and wire an integrationTest task if you prefer
separation.

@wlgns12370 wlgns12370 added 우선순위 상 bug Something isn't working labels Sep 13, 2025
@wlgns12370 wlgns12370 added this to the 모수 MVP API 구현 milestone Sep 13, 2025
@wlgns12370 wlgns12370 self-assigned this Sep 13, 2025
@wlgns12370 wlgns12370 merged commit 20e381b into develop Oct 15, 2025
3 checks passed
@wlgns12370 wlgns12370 deleted the refactor/mosu-373 branch October 15, 2025 04:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working 우선순위 상

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[🛠 코드 안정성] profile 등록 동시 요청 처리

2 participants