From ac48d07859c8896d03d3073b729bf34dd33de83c Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 21:03:41 +0900
Subject: [PATCH 01/29] =?UTF-8?q?setting=20:=20#1=20=EB=8F=84=EB=A9=94?=
 =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=93=88=20=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 core/domain/build.gradle.kts | 3 +++
 settings.gradle.kts          | 1 +
 2 files changed, 4 insertions(+)
 create mode 100644 core/domain/build.gradle.kts

diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts
new file mode 100644
index 000000000..2e3ca6ab4
--- /dev/null
+++ b/core/domain/build.gradle.kts
@@ -0,0 +1,3 @@
+dependencies {
+
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index c99fb6360..945f0010e 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -9,6 +9,7 @@ include(
     ":supports:jackson",
     ":supports:logging",
     ":supports:monitoring",
+    ":core:domain"
 )
 
 // configurations

From 8595b8460be2082518d12c65e67ab081e608a88f Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 21:27:54 +0900
Subject: [PATCH 02/29] =?UTF-8?q?setting=20:=20#1=20API=20=EC=98=88?=
 =?UTF-8?q?=EC=8B=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C=20=ED=9B=84?=
 =?UTF-8?q?=20User=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20?=
 =?UTF-8?q?=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/api/ApiControllerAdvice.java  | 127 ++++++++++++++++++
 .../api/common/dto}/ApiResponse.java          |   2 +-
 .../api/error}/ApiControllerAdvice.java       |  29 ++--
 .../api/error/ApiErrorType.java}              |   4 +-
 .../application/api/error/ApiException.java   |  19 +++
 .../application/api/user/UserV1ApiSpec.java   |  19 +++
 .../api/user/UserV1Controller.java            |  22 +++
 .../application/api/user/UserV1Dto.java       |   8 ++
 .../application/example/ExampleFacade.java    |  17 ---
 .../application/example/ExampleInfo.java      |  13 --
 .../loopers/domain/example/ExampleModel.java  |  44 ------
 .../domain/example/ExampleRepository.java     |   7 -
 .../domain/example/ExampleService.java        |  20 ---
 .../example/ExampleJpaRepository.java         |   6 -
 .../example/ExampleRepositoryImpl.java        |  19 ---
 .../api/example/ExampleV1ApiSpec.java         |  19 ---
 .../api/example/ExampleV1Controller.java      |  28 ----
 .../interfaces/api/example/ExampleV1Dto.java  |  15 ---
 .../loopers/support/error/CoreException.java  |  19 ---
 .../api/error/ApiExceptionTest.java}          |  16 ++-
 .../domain/example/ExampleModelTest.java      |  65 ---------
 .../ExampleServiceIntegrationTest.java        |  72 ----------
 .../interfaces/api/ExampleV1ApiE2ETest.java   | 114 ----------------
 .../com/loopers/core/domain/user/User.java    |  15 +++
 24 files changed, 236 insertions(+), 483 deletions(-)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/ApiControllerAdvice.java
 rename apps/commerce-api/src/main/java/com/loopers/{interfaces/api => application/api/common/dto}/ApiResponse.java (95%)
 rename apps/commerce-api/src/main/java/com/loopers/{interfaces/api => application/api/error}/ApiControllerAdvice.java (83%)
 rename apps/commerce-api/src/main/java/com/loopers/{support/error/ErrorType.java => application/api/error/ApiErrorType.java} (91%)
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiException.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
 create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/support/error/CoreException.java
 rename apps/commerce-api/src/test/java/com/loopers/{support/error/CoreExceptionTest.java => application/api/error/ApiExceptionTest.java} (62%)
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/User.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/application/api/ApiControllerAdvice.java
new file mode 100644
index 000000000..593635941
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/ApiControllerAdvice.java
@@ -0,0 +1,127 @@
+package com.loopers.application.api;
+
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.exc.InvalidFormatException;
+import com.fasterxml.jackson.databind.exc.MismatchedInputException;
+import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.application.api.error.ApiException;
+import com.loopers.application.api.error.ApiErrorType;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.server.ServerWebInputException;
+import org.springframework.web.servlet.resource.NoResourceFoundException;
+
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+@RestControllerAdvice
+@Slf4j
+public class ApiControllerAdvice {
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handle(ApiException e) {
+        log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage(), e);
+        return failureResponse(e.getApiErrorType(), e.getCustomMessage());
+    }
+
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentTypeMismatchException e) {
+        String name = e.getName();
+        String type = e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "unknown";
+        String value = e.getValue() != null ? e.getValue().toString() : "null";
+        String message = String.format("요청 파라미터 '%s' (타입: %s)의 값 '%s'이(가) 잘못되었습니다.", name, type, value);
+        return failureResponse(ApiErrorType.BAD_REQUEST, message);
+    }
+
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingServletRequestParameterException e) {
+        String name = e.getParameterName();
+        String type = e.getParameterType();
+        String message = String.format("필수 요청 파라미터 '%s' (타입: %s)가 누락되었습니다.", name, type);
+        return failureResponse(ApiErrorType.BAD_REQUEST, message);
+    }
+
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableException e) {
+        String errorMessage;
+        Throwable rootCause = e.getRootCause();
+
+        if (rootCause instanceof InvalidFormatException invalidFormat) {
+            String fieldName = invalidFormat.getPath().stream()
+                .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
+                .collect(Collectors.joining("."));
+
+            String valueIndicationMessage = "";
+            if (invalidFormat.getTargetType().isEnum()) {
+                Class<?> enumClass = invalidFormat.getTargetType();
+                String enumValues = Arrays.stream(enumClass.getEnumConstants())
+                    .map(Object::toString)
+                    .collect(Collectors.joining(", "));
+                valueIndicationMessage = "사용 가능한 값 : [" + enumValues + "]";
+            }
+
+            String expectedType = invalidFormat.getTargetType().getSimpleName();
+            Object value = invalidFormat.getValue();
+
+            errorMessage = String.format("필드 '%s'의 값 '%s'이(가) 예상 타입(%s)과 일치하지 않습니다. %s",
+                fieldName, value, expectedType, valueIndicationMessage);
+
+        } else if (rootCause instanceof MismatchedInputException mismatchedInput) {
+            String fieldPath = mismatchedInput.getPath().stream()
+                .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
+                .collect(Collectors.joining("."));
+            errorMessage = String.format("필수 필드 '%s'이(가) 누락되었습니다.", fieldPath);
+
+        } else if (rootCause instanceof JsonMappingException jsonMapping) {
+            String fieldPath = jsonMapping.getPath().stream()
+                .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
+                .collect(Collectors.joining("."));
+            errorMessage = String.format("필드 '%s'에서 JSON 매핑 오류가 발생했습니다: %s",
+                fieldPath, jsonMapping.getOriginalMessage());
+
+        } else {
+            errorMessage = "요청 본문을 처리하는 중 오류가 발생했습니다. JSON 메세지 규격을 확인해주세요.";
+        }
+
+        return failureResponse(ApiErrorType.BAD_REQUEST, errorMessage);
+    }
+
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleBadRequest(ServerWebInputException e) {
+        String missingParams = extractMissingParameter(e.getReason() != null ? e.getReason() : "");
+        if (!missingParams.isEmpty()) {
+            String message = String.format("필수 요청 값 '%s'가 누락되었습니다.", missingParams);
+            return failureResponse(ApiErrorType.BAD_REQUEST, message);
+        } else {
+            return failureResponse(ApiErrorType.BAD_REQUEST, null);
+        }
+    }
+
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleNotFound(NoResourceFoundException e) {
+        return failureResponse(ApiErrorType.NOT_FOUND, null);
+    }
+
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handle(Throwable e) {
+        log.error("Exception : {}", e.getMessage(), e);
+        return failureResponse(ApiErrorType.INTERNAL_ERROR, null);
+    }
+
+    private String extractMissingParameter(String message) {
+        Pattern pattern = Pattern.compile("'(.+?)'");
+        Matcher matcher = pattern.matcher(message);
+        return matcher.find() ? matcher.group(1) : "";
+    }
+
+    private ResponseEntity<ApiResponse<?>> failureResponse(ApiErrorType apiErrorType, String errorMessage) {
+        return ResponseEntity.status(apiErrorType.getStatus())
+            .body(ApiResponse.fail(apiErrorType.getCode(), errorMessage != null ? errorMessage : apiErrorType.getMessage()));
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiResponse.java b/apps/commerce-api/src/main/java/com/loopers/application/api/common/dto/ApiResponse.java
similarity index 95%
rename from apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiResponse.java
rename to apps/commerce-api/src/main/java/com/loopers/application/api/common/dto/ApiResponse.java
index 33b77b529..9aaad2452 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiResponse.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/common/dto/ApiResponse.java
@@ -1,4 +1,4 @@
-package com.loopers.interfaces.api;
+package com.loopers.application.api.common.dto;
 
 public record ApiResponse<T>(Metadata meta, T data) {
     public record Metadata(Result result, String errorCode, String message) {
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
similarity index 83%
rename from apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
rename to apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
index 20b2809c8..d569effed 100644
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
@@ -1,10 +1,9 @@
-package com.loopers.interfaces.api;
+package com.loopers.application.api.error;
 
 import com.fasterxml.jackson.databind.JsonMappingException;
 import com.fasterxml.jackson.databind.exc.InvalidFormatException;
 import com.fasterxml.jackson.databind.exc.MismatchedInputException;
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
+import com.loopers.application.api.common.dto.ApiResponse;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -24,9 +23,9 @@
 @Slf4j
 public class ApiControllerAdvice {
     @ExceptionHandler
-    public ResponseEntity<ApiResponse<?>> handle(CoreException e) {
+    public ResponseEntity<ApiResponse<?>> handle(ApiException e) {
         log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage(), e);
-        return failureResponse(e.getErrorType(), e.getCustomMessage());
+        return failureResponse(e.getApiErrorType(), e.getCustomMessage());
     }
 
     @ExceptionHandler
@@ -35,7 +34,7 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentTypeMismatc
         String type = e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "unknown";
         String value = e.getValue() != null ? e.getValue().toString() : "null";
         String message = String.format("요청 파라미터 '%s' (타입: %s)의 값 '%s'이(가) 잘못되었습니다.", name, type, value);
-        return failureResponse(ErrorType.BAD_REQUEST, message);
+        return failureResponse(ApiErrorType.BAD_REQUEST, message);
     }
 
     @ExceptionHandler
@@ -43,7 +42,7 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingServletRequestPara
         String name = e.getParameterName();
         String type = e.getParameterType();
         String message = String.format("필수 요청 파라미터 '%s' (타입: %s)가 누락되었습니다.", name, type);
-        return failureResponse(ErrorType.BAD_REQUEST, message);
+        return failureResponse(ApiErrorType.BAD_REQUEST, message);
     }
 
     @ExceptionHandler
@@ -88,7 +87,7 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableExc
             errorMessage = "요청 본문을 처리하는 중 오류가 발생했습니다. JSON 메세지 규격을 확인해주세요.";
         }
 
-        return failureResponse(ErrorType.BAD_REQUEST, errorMessage);
+        return failureResponse(ApiErrorType.BAD_REQUEST, errorMessage);
     }
 
     @ExceptionHandler
@@ -96,21 +95,21 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(ServerWebInputException e
         String missingParams = extractMissingParameter(e.getReason() != null ? e.getReason() : "");
         if (!missingParams.isEmpty()) {
             String message = String.format("필수 요청 값 '%s'가 누락되었습니다.", missingParams);
-            return failureResponse(ErrorType.BAD_REQUEST, message);
+            return failureResponse(ApiErrorType.BAD_REQUEST, message);
         } else {
-            return failureResponse(ErrorType.BAD_REQUEST, null);
+            return failureResponse(ApiErrorType.BAD_REQUEST, null);
         }
     }
 
     @ExceptionHandler
     public ResponseEntity<ApiResponse<?>> handleNotFound(NoResourceFoundException e) {
-        return failureResponse(ErrorType.NOT_FOUND, null);
+        return failureResponse(ApiErrorType.NOT_FOUND, null);
     }
 
     @ExceptionHandler
     public ResponseEntity<ApiResponse<?>> handle(Throwable e) {
         log.error("Exception : {}", e.getMessage(), e);
-        return failureResponse(ErrorType.INTERNAL_ERROR, null);
+        return failureResponse(ApiErrorType.INTERNAL_ERROR, null);
     }
 
     private String extractMissingParameter(String message) {
@@ -119,8 +118,8 @@ private String extractMissingParameter(String message) {
         return matcher.find() ? matcher.group(1) : "";
     }
 
-    private ResponseEntity<ApiResponse<?>> failureResponse(ErrorType errorType, String errorMessage) {
-        return ResponseEntity.status(errorType.getStatus())
-            .body(ApiResponse.fail(errorType.getCode(), errorMessage != null ? errorMessage : errorType.getMessage()));
+    private ResponseEntity<ApiResponse<?>> failureResponse(ApiErrorType apiErrorType, String errorMessage) {
+        return ResponseEntity.status(apiErrorType.getStatus())
+            .body(ApiResponse.fail(apiErrorType.getCode(), errorMessage != null ? errorMessage : apiErrorType.getMessage()));
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiErrorType.java
similarity index 91%
rename from apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java
rename to apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiErrorType.java
index 5d142efbf..fa50edbd5 100644
--- a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiErrorType.java
@@ -1,4 +1,4 @@
-package com.loopers.support.error;
+package com.loopers.application.api.error;
 
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
@@ -6,7 +6,7 @@
 
 @Getter
 @RequiredArgsConstructor
-public enum ErrorType {
+public enum ApiErrorType {
     /** 범용 에러 */
     INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "일시적인 오류가 발생했습니다."),
     BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "잘못된 요청입니다."),
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiException.java b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiException.java
new file mode 100644
index 000000000..d26d10079
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiException.java
@@ -0,0 +1,19 @@
+package com.loopers.application.api.error;
+
+import lombok.Getter;
+
+@Getter
+public class ApiException extends RuntimeException {
+    private final ApiErrorType apiErrorType;
+    private final String customMessage;
+
+    public ApiException(ApiErrorType apiErrorType) {
+        this(apiErrorType, null);
+    }
+
+    public ApiException(ApiErrorType apiErrorType, String customMessage) {
+        super(customMessage != null ? customMessage : apiErrorType.getMessage());
+        this.apiErrorType = apiErrorType;
+        this.customMessage = customMessage;
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
new file mode 100644
index 000000000..dea54db24
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
@@ -0,0 +1,19 @@
+package com.loopers.application.api.user;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@Tag(name = "User V1 API", description = "User API 입니다.")
+public interface UserV1ApiSpec {
+
+    @Operation(
+        summary = "사용자 조회",
+        description = "ID로 사용자를 조회합니다."
+    )
+    ApiResponse<UserV1Dto.GetUserResponse> getUser(
+        @Schema(name = "사용자 ID", description = "조회할 사용자 ID")
+        Long userId
+    );
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
new file mode 100644
index 000000000..8fa067a52
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
@@ -0,0 +1,22 @@
+package com.loopers.application.api.user;
+
+import com.loopers.application.api.common.dto.ApiResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/v1/users")
+public class UserV1Controller implements UserV1ApiSpec {
+
+    @GetMapping("/{userId}")
+    @Override
+    public ApiResponse<UserV1Dto.GetUserResponse> getUser(
+            @PathVariable(value = "userId") Long userId
+    ) {
+        return ApiResponse.success(null);
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
new file mode 100644
index 000000000..65dc4f80c
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -0,0 +1,8 @@
+package com.loopers.application.api.user;
+
+public class UserV1Dto {
+
+    public record GetUserResponse(String userId) {
+
+    }
+}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
deleted file mode 100644
index 552a9ad62..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleFacade.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.loopers.application.example;
-
-import com.loopers.domain.example.ExampleModel;
-import com.loopers.domain.example.ExampleService;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-@RequiredArgsConstructor
-@Component
-public class ExampleFacade {
-    private final ExampleService exampleService;
-
-    public ExampleInfo getExample(Long id) {
-        ExampleModel example = exampleService.getExample(id);
-        return ExampleInfo.from(example);
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
deleted file mode 100644
index 877aba96c..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/application/example/ExampleInfo.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.loopers.application.example;
-
-import com.loopers.domain.example.ExampleModel;
-
-public record ExampleInfo(Long id, String name, String description) {
-    public static ExampleInfo from(ExampleModel model) {
-        return new ExampleInfo(
-            model.getId(),
-            model.getName(),
-            model.getDescription()
-        );
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
deleted file mode 100644
index c588c4a8a..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleModel.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.loopers.domain.example;
-
-import com.loopers.domain.BaseEntity;
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Table;
-
-@Entity
-@Table(name = "example")
-public class ExampleModel extends BaseEntity {
-
-    private String name;
-    private String description;
-
-    protected ExampleModel() {}
-
-    public ExampleModel(String name, String description) {
-        if (name == null || name.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "이름은 비어있을 수 없습니다.");
-        }
-        if (description == null || description.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "설명은 비어있을 수 없습니다.");
-        }
-
-        this.name = name;
-        this.description = description;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public void update(String newDescription) {
-        if (newDescription == null || newDescription.isBlank()) {
-            throw new CoreException(ErrorType.BAD_REQUEST, "설명은 비어있을 수 없습니다.");
-        }
-        this.description = newDescription;
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
deleted file mode 100644
index 3625e5662..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleRepository.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.loopers.domain.example;
-
-import java.util.Optional;
-
-public interface ExampleRepository {
-    Optional<ExampleModel> find(Long id);
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java b/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
deleted file mode 100644
index c0e8431e8..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/domain/example/ExampleService.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.loopers.domain.example;
-
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-@RequiredArgsConstructor
-@Component
-public class ExampleService {
-
-    private final ExampleRepository exampleRepository;
-
-    @Transactional(readOnly = true)
-    public ExampleModel getExample(Long id) {
-        return exampleRepository.find(id)
-            .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] 예시를 찾을 수 없습니다."));
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java
deleted file mode 100644
index ce6d3ead0..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleJpaRepository.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.loopers.infrastructure.example;
-
-import com.loopers.domain.example.ExampleModel;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-public interface ExampleJpaRepository extends JpaRepository<ExampleModel, Long> {}
diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
deleted file mode 100644
index 37f2272f0..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.loopers.infrastructure.example;
-
-import com.loopers.domain.example.ExampleModel;
-import com.loopers.domain.example.ExampleRepository;
-import lombok.RequiredArgsConstructor;
-import org.springframework.stereotype.Component;
-
-import java.util.Optional;
-
-@RequiredArgsConstructor
-@Component
-public class ExampleRepositoryImpl implements ExampleRepository {
-    private final ExampleJpaRepository exampleJpaRepository;
-
-    @Override
-    public Optional<ExampleModel> find(Long id) {
-        return exampleJpaRepository.findById(id);
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java
deleted file mode 100644
index 219e3101e..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1ApiSpec.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.loopers.interfaces.api.example;
-
-import com.loopers.interfaces.api.ApiResponse;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.tags.Tag;
-
-@Tag(name = "Example V1 API", description = "Loopers 예시 API 입니다.")
-public interface ExampleV1ApiSpec {
-
-    @Operation(
-        summary = "예시 조회",
-        description = "ID로 예시를 조회합니다."
-    )
-    ApiResponse<ExampleV1Dto.ExampleResponse> getExample(
-        @Schema(name = "예시 ID", description = "조회할 예시의 ID")
-        Long exampleId
-    );
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java
deleted file mode 100644
index 917376016..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Controller.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.loopers.interfaces.api.example;
-
-import com.loopers.application.example.ExampleFacade;
-import com.loopers.application.example.ExampleInfo;
-import com.loopers.interfaces.api.ApiResponse;
-import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RequiredArgsConstructor
-@RestController
-@RequestMapping("/api/v1/examples")
-public class ExampleV1Controller implements ExampleV1ApiSpec {
-
-    private final ExampleFacade exampleFacade;
-
-    @GetMapping("/{exampleId}")
-    @Override
-    public ApiResponse<ExampleV1Dto.ExampleResponse> getExample(
-        @PathVariable(value = "exampleId") Long exampleId
-    ) {
-        ExampleInfo info = exampleFacade.getExample(exampleId);
-        ExampleV1Dto.ExampleResponse response = ExampleV1Dto.ExampleResponse.from(info);
-        return ApiResponse.success(response);
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java
deleted file mode 100644
index 4ecf0eea5..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.loopers.interfaces.api.example;
-
-import com.loopers.application.example.ExampleInfo;
-
-public class ExampleV1Dto {
-    public record ExampleResponse(Long id, String name, String description) {
-        public static ExampleResponse from(ExampleInfo info) {
-            return new ExampleResponse(
-                info.id(),
-                info.name(),
-                info.description()
-            );
-        }
-    }
-}
diff --git a/apps/commerce-api/src/main/java/com/loopers/support/error/CoreException.java b/apps/commerce-api/src/main/java/com/loopers/support/error/CoreException.java
deleted file mode 100644
index 0cc190b6b..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/support/error/CoreException.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.loopers.support.error;
-
-import lombok.Getter;
-
-@Getter
-public class CoreException extends RuntimeException {
-    private final ErrorType errorType;
-    private final String customMessage;
-
-    public CoreException(ErrorType errorType) {
-        this(errorType, null);
-    }
-
-    public CoreException(ErrorType errorType, String customMessage) {
-        super(customMessage != null ? customMessage : errorType.getMessage());
-        this.errorType = errorType;
-        this.customMessage = customMessage;
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/error/ApiExceptionTest.java
similarity index 62%
rename from apps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.java
rename to apps/commerce-api/src/test/java/com/loopers/application/api/error/ApiExceptionTest.java
index 44db8c5e6..d695507ee 100644
--- a/apps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/error/ApiExceptionTest.java
@@ -1,21 +1,23 @@
-package com.loopers.support.error;
+package com.loopers.application.api.error;
 
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ApiExceptionTest {
 
-class CoreExceptionTest {
     @DisplayName("ErrorType 기반의 예외 생성 시, 별도의 메시지가 주어지지 않으면 ErrorType의 메시지를 사용한다.")
     @Test
     void messageShouldBeErrorTypeMessage_whenCustomMessageIsNull() {
         // arrange
-        ErrorType[] errorTypes = ErrorType.values();
+        ApiErrorType[] apiErrorTypes = ApiErrorType.values();
 
         // act & assert
-        for (ErrorType errorType : errorTypes) {
-            CoreException exception = new CoreException(errorType);
-            assertThat(exception.getMessage()).isEqualTo(errorType.getMessage());
+        for (ApiErrorType apiErrorType : apiErrorTypes) {
+            ApiException exception = new ApiException(apiErrorType);
+            assertThat(exception.getMessage()).isEqualTo(apiErrorType.getMessage());
         }
     }
 
@@ -26,7 +28,7 @@ void messageShouldBeCustomMessage_whenCustomMessageIsNotNull() {
         String customMessage = "custom message";
 
         // act
-        CoreException exception = new CoreException(ErrorType.INTERNAL_ERROR, customMessage);
+        ApiException exception = new ApiException(ApiErrorType.INTERNAL_ERROR, customMessage);
 
         // assert
         assertThat(exception.getMessage()).isEqualTo(customMessage);
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
deleted file mode 100644
index 44ca7576e..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.loopers.domain.example;
-
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-class ExampleModelTest {
-    @DisplayName("예시 모델을 생성할 때, ")
-    @Nested
-    class Create {
-        @DisplayName("제목과 설명이 모두 주어지면, 정상적으로 생성된다.")
-        @Test
-        void createsExampleModel_whenNameAndDescriptionAreProvided() {
-            // arrange
-            String name = "제목";
-            String description = "설명";
-
-            // act
-            ExampleModel exampleModel = new ExampleModel(name, description);
-
-            // assert
-            assertAll(
-                () -> assertThat(exampleModel.getId()).isNotNull(),
-                () -> assertThat(exampleModel.getName()).isEqualTo(name),
-                () -> assertThat(exampleModel.getDescription()).isEqualTo(description)
-            );
-        }
-
-        @DisplayName("제목이 빈칸으로만 이루어져 있으면, BAD_REQUEST 예외가 발생한다.")
-        @Test
-        void throwsBadRequestException_whenTitleIsBlank() {
-            // arrange
-            String name = "   ";
-
-            // act
-            CoreException result = assertThrows(CoreException.class, () -> {
-                new ExampleModel(name, "설명");
-            });
-
-            // assert
-            assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-        }
-
-        @DisplayName("설명이 비어있으면, BAD_REQUEST 예외가 발생한다.")
-        @Test
-        void throwsBadRequestException_whenDescriptionIsEmpty() {
-            // arrange
-            String description = "";
-
-            // act
-            CoreException result = assertThrows(CoreException.class, () -> {
-                new ExampleModel("제목", description);
-            });
-
-            // assert
-            assertThat(result.getErrorType()).isEqualTo(ErrorType.BAD_REQUEST);
-        }
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
deleted file mode 100644
index bbd5fdbe1..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.loopers.domain.example;
-
-import com.loopers.infrastructure.example.ExampleJpaRepository;
-import com.loopers.support.error.CoreException;
-import com.loopers.support.error.ErrorType;
-import com.loopers.utils.DatabaseCleanUp;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-@SpringBootTest
-class ExampleServiceIntegrationTest {
-    @Autowired
-    private ExampleService exampleService;
-
-    @Autowired
-    private ExampleJpaRepository exampleJpaRepository;
-
-    @Autowired
-    private DatabaseCleanUp databaseCleanUp;
-
-    @AfterEach
-    void tearDown() {
-        databaseCleanUp.truncateAllTables();
-    }
-
-    @DisplayName("예시를 조회할 때,")
-    @Nested
-    class Get {
-        @DisplayName("존재하는 예시 ID를 주면, 해당 예시 정보를 반환한다.")
-        @Test
-        void returnsExampleInfo_whenValidIdIsProvided() {
-            // arrange
-            ExampleModel exampleModel = exampleJpaRepository.save(
-                new ExampleModel("예시 제목", "예시 설명")
-            );
-
-            // act
-            ExampleModel result = exampleService.getExample(exampleModel.getId());
-
-            // assert
-            assertAll(
-                () -> assertThat(result).isNotNull(),
-                () -> assertThat(result.getId()).isEqualTo(exampleModel.getId()),
-                () -> assertThat(result.getName()).isEqualTo(exampleModel.getName()),
-                () -> assertThat(result.getDescription()).isEqualTo(exampleModel.getDescription())
-            );
-        }
-
-        @DisplayName("존재하지 않는 예시 ID를 주면, NOT_FOUND 예외가 발생한다.")
-        @Test
-        void throwsException_whenInvalidIdIsProvided() {
-            // arrange
-            Long invalidId = 999L; // Assuming this ID does not exist
-
-            // act
-            CoreException exception = assertThrows(CoreException.class, () -> {
-                exampleService.getExample(invalidId);
-            });
-
-            // assert
-            assertThat(exception.getErrorType()).isEqualTo(ErrorType.NOT_FOUND);
-        }
-    }
-}
diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java
deleted file mode 100644
index 1bb3dba65..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.loopers.interfaces.api;
-
-import com.loopers.domain.example.ExampleModel;
-import com.loopers.infrastructure.example.ExampleJpaRepository;
-import com.loopers.interfaces.api.example.ExampleV1Dto;
-import com.loopers.utils.DatabaseCleanUp;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.web.client.TestRestTemplate;
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-
-import java.util.function.Function;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
-class ExampleV1ApiE2ETest {
-
-    private static final Function<Long, String> ENDPOINT_GET = id -> "/api/v1/examples/" + id;
-
-    private final TestRestTemplate testRestTemplate;
-    private final ExampleJpaRepository exampleJpaRepository;
-    private final DatabaseCleanUp databaseCleanUp;
-
-    @Autowired
-    public ExampleV1ApiE2ETest(
-        TestRestTemplate testRestTemplate,
-        ExampleJpaRepository exampleJpaRepository,
-        DatabaseCleanUp databaseCleanUp
-    ) {
-        this.testRestTemplate = testRestTemplate;
-        this.exampleJpaRepository = exampleJpaRepository;
-        this.databaseCleanUp = databaseCleanUp;
-    }
-
-    @AfterEach
-    void tearDown() {
-        databaseCleanUp.truncateAllTables();
-    }
-
-    @DisplayName("GET /api/v1/examples/{id}")
-    @Nested
-    class Get {
-        @DisplayName("존재하는 예시 ID를 주면, 해당 예시 정보를 반환한다.")
-        @Test
-        void returnsExampleInfo_whenValidIdIsProvided() {
-            // arrange
-            ExampleModel exampleModel = exampleJpaRepository.save(
-                new ExampleModel("예시 제목", "예시 설명")
-            );
-            String requestUrl = ENDPOINT_GET.apply(exampleModel.getId());
-
-            // act
-            ParameterizedTypeReference<ApiResponse<ExampleV1Dto.ExampleResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<ExampleV1Dto.ExampleResponse>> response =
-                testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType);
-
-            // assert
-            assertAll(
-                () -> assertTrue(response.getStatusCode().is2xxSuccessful()),
-                () -> assertThat(response.getBody().data().id()).isEqualTo(exampleModel.getId()),
-                () -> assertThat(response.getBody().data().name()).isEqualTo(exampleModel.getName()),
-                () -> assertThat(response.getBody().data().description()).isEqualTo(exampleModel.getDescription())
-            );
-        }
-
-        @DisplayName("숫자가 아닌 ID 로 요청하면, 400 BAD_REQUEST 응답을 받는다.")
-        @Test
-        void throwsBadRequest_whenIdIsNotProvided() {
-            // arrange
-            String requestUrl = "/api/v1/examples/나나";
-
-            // act
-            ParameterizedTypeReference<ApiResponse<ExampleV1Dto.ExampleResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<ExampleV1Dto.ExampleResponse>> response =
-                testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType);
-
-            // assert
-            assertAll(
-                () -> assertTrue(response.getStatusCode().is4xxClientError()),
-                () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST)
-            );
-        }
-
-        @DisplayName("존재하지 않는 예시 ID를 주면, 404 NOT_FOUND 응답을 받는다.")
-        @Test
-        void throwsException_whenInvalidIdIsProvided() {
-            // arrange
-            Long invalidId = -1L;
-            String requestUrl = ENDPOINT_GET.apply(invalidId);
-
-            // act
-            ParameterizedTypeReference<ApiResponse<ExampleV1Dto.ExampleResponse>> responseType = new ParameterizedTypeReference<>() {};
-            ResponseEntity<ApiResponse<ExampleV1Dto.ExampleResponse>> response =
-                testRestTemplate.exchange(requestUrl, HttpMethod.GET, new HttpEntity<>(null), responseType);
-
-            // assert
-            assertAll(
-                () -> assertTrue(response.getStatusCode().is4xxClientError()),
-                () -> assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND)
-            );
-        }
-    }
-}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/User.java b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
new file mode 100644
index 000000000..991f7379e
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
@@ -0,0 +1,15 @@
+package com.loopers.core.domain.user;
+
+public class User {
+
+    private final UserId userId;
+
+    private final UserIdentifier identifier;
+
+    private final UserEmail email;
+
+    private final UserBirthDay birthDay;
+
+    private final UserGender gender;
+
+}

From 31f50616079dbe295566dfff7eeb3c072806c039 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 21:37:38 +0900
Subject: [PATCH 03/29] =?UTF-8?q?feature=20:=20#1=20User=20=EB=8F=84?=
 =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../core/domain/common/vo/CreatedAt.java      | 10 ++++++
 .../core/domain/common/vo/DeletedAt.java      | 10 ++++++
 .../core/domain/common/vo/UpdatedAt.java      | 10 ++++++
 .../com/loopers/core/domain/user/User.java    | 36 +++++++++++++++++++
 .../domain/user/enumeration/UserGender.kt     |  6 ++++
 .../core/domain/user/vo/UserBirthDay.java     |  6 ++++
 .../core/domain/user/vo/UserEmail.java        |  4 +++
 .../loopers/core/domain/user/vo/UserId.java   |  4 +++
 .../core/domain/user/vo/UserIdentifier.java   |  4 +++
 9 files changed, 90 insertions(+)
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/common/vo/CreatedAt.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/common/vo/DeletedAt.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/common/vo/UpdatedAt.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/enumeration/UserGender.kt
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java

diff --git a/core/domain/src/main/java/com/loopers/core/domain/common/vo/CreatedAt.java b/core/domain/src/main/java/com/loopers/core/domain/common/vo/CreatedAt.java
new file mode 100644
index 000000000..2b71f249b
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/common/vo/CreatedAt.java
@@ -0,0 +1,10 @@
+package com.loopers.core.domain.common.vo;
+
+import java.time.LocalDateTime;
+
+public record CreatedAt(LocalDateTime value) {
+
+    public static CreatedAt now() {
+        return new CreatedAt(LocalDateTime.now());
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/common/vo/DeletedAt.java b/core/domain/src/main/java/com/loopers/core/domain/common/vo/DeletedAt.java
new file mode 100644
index 000000000..b5efff97c
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/common/vo/DeletedAt.java
@@ -0,0 +1,10 @@
+package com.loopers.core.domain.common.vo;
+
+import java.time.LocalDateTime;
+
+public record DeletedAt(LocalDateTime value) {
+
+    public static DeletedAt now() {
+        return new DeletedAt(LocalDateTime.now());
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/common/vo/UpdatedAt.java b/core/domain/src/main/java/com/loopers/core/domain/common/vo/UpdatedAt.java
new file mode 100644
index 000000000..fe56b2640
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/common/vo/UpdatedAt.java
@@ -0,0 +1,10 @@
+package com.loopers.core.domain.common.vo;
+
+import java.time.LocalDateTime;
+
+public record UpdatedAt(LocalDateTime value) {
+
+    public static UpdatedAt now() {
+        return new UpdatedAt(LocalDateTime.now());
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/User.java b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
index 991f7379e..78a53ebb7 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/User.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
@@ -1,5 +1,16 @@
 package com.loopers.core.domain.user;
 
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.DeletedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.user.enumeration.UserGender;
+import com.loopers.core.domain.user.vo.UserBirthDay;
+import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserId;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import lombok.Getter;
+
+@Getter
 public class User {
 
     private final UserId userId;
@@ -12,4 +23,29 @@ public class User {
 
     private final UserGender gender;
 
+    private final CreatedAt createdAt;
+
+    private final UpdatedAt updatedAt;
+
+    private final DeletedAt deletedAt;
+
+    private User(
+            UserId userId,
+            UserIdentifier identifier,
+            UserEmail email,
+            UserBirthDay birthDay,
+            UserGender gender,
+            CreatedAt createdAt,
+            UpdatedAt updatedAt,
+            DeletedAt deletedAt
+    ) {
+        this.userId = userId;
+        this.identifier = identifier;
+        this.email = email;
+        this.birthDay = birthDay;
+        this.gender = gender;
+        this.createdAt = createdAt;
+        this.updatedAt = updatedAt;
+        this.deletedAt = deletedAt;
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/enumeration/UserGender.kt b/core/domain/src/main/java/com/loopers/core/domain/user/enumeration/UserGender.kt
new file mode 100644
index 000000000..309932dde
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/enumeration/UserGender.kt
@@ -0,0 +1,6 @@
+package com.loopers.core.domain.user.enumeration
+
+enum class UserGender {
+
+    MALE, FEMALE
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
new file mode 100644
index 000000000..194505251
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
@@ -0,0 +1,6 @@
+package com.loopers.core.domain.user.vo;
+
+import java.time.LocalDateTime;
+
+public record UserBirthDay(LocalDateTime value) {
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
new file mode 100644
index 000000000..65a6fdbd1
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
@@ -0,0 +1,4 @@
+package com.loopers.core.domain.user.vo;
+
+public record UserEmail(String value) {
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java
new file mode 100644
index 000000000..c01ae14a8
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java
@@ -0,0 +1,4 @@
+package com.loopers.core.domain.user.vo;
+
+public record UserId(String value) {
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java
new file mode 100644
index 000000000..8318c4b9f
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java
@@ -0,0 +1,4 @@
+package com.loopers.core.domain.user.vo;
+
+public record UserIdentifier(String value) {
+}

From a6aab4608f4ffdbc7effc17f063ba48702143b02 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 21:53:44 +0900
Subject: [PATCH 04/29] =?UTF-8?q?setting=20:=20#1=20mysql=20config=20?=
 =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/commerce-api/build.gradle.kts            |  1 +
 .../mysql/mysql-config/build.gradle.kts       | 15 +++++++++++
 .../mysql/config/DataSourceConfig.java        | 25 +++++++++++++++++++
 .../database/mysql/config/JpaConfig.java      | 13 ++++++++++
 .../database/mysql/config/QueryDslConfig.java | 17 +++++++++++++
 settings.gradle.kts                           |  3 ++-
 6 files changed, 73 insertions(+), 1 deletion(-)
 create mode 100644 core/infra/database/mysql/mysql-config/build.gradle.kts
 create mode 100644 core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/DataSourceConfig.java
 create mode 100644 core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/JpaConfig.java
 create mode 100644 core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/QueryDslConfig.java

diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index 03ce68f02..f24a6969b 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -1,5 +1,6 @@
 dependencies {
     // add-ons
+    implementation(project(":core:infra:database:mysql:mysql-config"))
     implementation(project(":modules:jpa"))
     implementation(project(":modules:redis"))
     implementation(project(":supports:jackson"))
diff --git a/core/infra/database/mysql/mysql-config/build.gradle.kts b/core/infra/database/mysql/mysql-config/build.gradle.kts
new file mode 100644
index 000000000..c39b300ed
--- /dev/null
+++ b/core/infra/database/mysql/mysql-config/build.gradle.kts
@@ -0,0 +1,15 @@
+plugins {
+    `java-library`
+}
+
+
+dependencies {
+    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
+    implementation("com.querydsl:querydsl-jpa::jakarta")
+
+    annotationProcessor("com.querydsl:querydsl-apt::jakarta")
+    annotationProcessor("jakarta.persistence:jakarta.persistence-api")
+    annotationProcessor("jakarta.annotation:jakarta.annotation-api")
+
+    runtimeOnly("com.mysql:mysql-connector-j")
+}
diff --git a/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/DataSourceConfig.java b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/DataSourceConfig.java
new file mode 100644
index 000000000..5686ed667
--- /dev/null
+++ b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/DataSourceConfig.java
@@ -0,0 +1,25 @@
+package com.loopers.core.infra.database.mysql.config;
+
+import com.zaxxer.hikari.HikariConfig;
+import com.zaxxer.hikari.HikariDataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+@Configuration
+class DataSourceConfig {
+
+    @Bean
+    @ConfigurationProperties(prefix = "datasource.mysql-jpa.main")
+    HikariConfig mySqlMainHikariConfig() {
+        return new HikariConfig();
+    }
+
+    @Primary
+    @Bean
+    HikariDataSource mySqlMainDataSource(@Qualifier("mySqlMainHikariConfig") HikariConfig hikariConfig) {
+        return new HikariDataSource(hikariConfig);
+    }
+}
diff --git a/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/JpaConfig.java b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/JpaConfig.java
new file mode 100644
index 000000000..1a8ed0565
--- /dev/null
+++ b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/JpaConfig.java
@@ -0,0 +1,13 @@
+package com.loopers.core.infra.database.mysql.config;
+
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+@Configuration
+@EnableTransactionManagement
+@EntityScan({"com.loopers"})
+@EnableJpaRepositories({"com.loopers.core.infra.database.mysql"})
+public class JpaConfig {
+}
diff --git a/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/QueryDslConfig.java b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/QueryDslConfig.java
new file mode 100644
index 000000000..5efec726b
--- /dev/null
+++ b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/QueryDslConfig.java
@@ -0,0 +1,17 @@
+package com.loopers.core.infra.database.mysql.config;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+@Configuration
+class QueryDslConfig {
+
+    @Primary
+    @Bean
+    JPAQueryFactory mySqlJpaQueryFactory(EntityManager entityManager) {
+        return new JPAQueryFactory(entityManager);
+    }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 945f0010e..f338b7b6a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -9,7 +9,8 @@ include(
     ":supports:jackson",
     ":supports:logging",
     ":supports:monitoring",
-    ":core:domain"
+    ":core:domain",
+    ":core:infra:database:mysql:mysql-config"
 )
 
 // configurations

From f6a0f1a7fb8f568a15e9e9ad2e74ece7d8de2c91 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 22:21:33 +0900
Subject: [PATCH 05/29] =?UTF-8?q?setting=20:=20#1=20mysql=20core=20?=
 =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- mysql, JPA의 공통 기능을 포함합니다.
---
 apps/commerce-api/build.gradle.kts            |   3 +-
 .../application/api/ApiControllerAdvice.java  | 127 ------------------
 .../mysql-config/src/main/resources/jpa.yml   |  96 +++++++++++++
 .../mysql/mysql-core/build.gradle.kts         |  15 +++
 .../infra/mysql/util}/DatabaseCleanUp.java    |  11 +-
 .../loopers/utils/MySqlDatabaseCleanUp.java   |  40 ++++++
 settings.gradle.kts                           |   5 +-
 7 files changed, 160 insertions(+), 137 deletions(-)
 delete mode 100644 apps/commerce-api/src/main/java/com/loopers/application/api/ApiControllerAdvice.java
 create mode 100644 core/infra/database/mysql/mysql-config/src/main/resources/jpa.yml
 create mode 100644 core/infra/database/mysql/mysql-core/build.gradle.kts
 rename {modules/jpa/src/testFixtures/java/com/loopers/utils => core/infra/database/mysql/mysql-core/src/testFixtures/java/com/loopers/core/infra/mysql/util}/DatabaseCleanUp.java (80%)
 create mode 100644 modules/jpa/src/testFixtures/java/com/loopers/utils/MySqlDatabaseCleanUp.java

diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index f24a6969b..78d7c702b 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -1,7 +1,6 @@
 dependencies {
     // add-ons
     implementation(project(":core:infra:database:mysql:mysql-config"))
-    implementation(project(":modules:jpa"))
     implementation(project(":modules:redis"))
     implementation(project(":supports:jackson"))
     implementation(project(":supports:logging"))
@@ -18,6 +17,6 @@ dependencies {
     annotationProcessor("jakarta.annotation:jakarta.annotation-api")
 
     // test-fixtures
-    testImplementation(testFixtures(project(":modules:jpa")))
+    testImplementation(testFixtures(project(":core:infra:database:mysql:mysql-core")))
     testImplementation(testFixtures(project(":modules:redis")))
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/application/api/ApiControllerAdvice.java
deleted file mode 100644
index 593635941..000000000
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/ApiControllerAdvice.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package com.loopers.application.api;
-
-import com.fasterxml.jackson.databind.JsonMappingException;
-import com.fasterxml.jackson.databind.exc.InvalidFormatException;
-import com.fasterxml.jackson.databind.exc.MismatchedInputException;
-import com.loopers.application.api.common.dto.ApiResponse;
-import com.loopers.application.api.error.ApiException;
-import com.loopers.application.api.error.ApiErrorType;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.http.ResponseEntity;
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.web.bind.MissingServletRequestParameterException;
-import org.springframework.web.bind.annotation.ExceptionHandler;
-import org.springframework.web.bind.annotation.RestControllerAdvice;
-import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
-import org.springframework.web.server.ServerWebInputException;
-import org.springframework.web.servlet.resource.NoResourceFoundException;
-
-import java.util.Arrays;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-@RestControllerAdvice
-@Slf4j
-public class ApiControllerAdvice {
-    @ExceptionHandler
-    public ResponseEntity<ApiResponse<?>> handle(ApiException e) {
-        log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage(), e);
-        return failureResponse(e.getApiErrorType(), e.getCustomMessage());
-    }
-
-    @ExceptionHandler
-    public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentTypeMismatchException e) {
-        String name = e.getName();
-        String type = e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "unknown";
-        String value = e.getValue() != null ? e.getValue().toString() : "null";
-        String message = String.format("요청 파라미터 '%s' (타입: %s)의 값 '%s'이(가) 잘못되었습니다.", name, type, value);
-        return failureResponse(ApiErrorType.BAD_REQUEST, message);
-    }
-
-    @ExceptionHandler
-    public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingServletRequestParameterException e) {
-        String name = e.getParameterName();
-        String type = e.getParameterType();
-        String message = String.format("필수 요청 파라미터 '%s' (타입: %s)가 누락되었습니다.", name, type);
-        return failureResponse(ApiErrorType.BAD_REQUEST, message);
-    }
-
-    @ExceptionHandler
-    public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableException e) {
-        String errorMessage;
-        Throwable rootCause = e.getRootCause();
-
-        if (rootCause instanceof InvalidFormatException invalidFormat) {
-            String fieldName = invalidFormat.getPath().stream()
-                .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
-                .collect(Collectors.joining("."));
-
-            String valueIndicationMessage = "";
-            if (invalidFormat.getTargetType().isEnum()) {
-                Class<?> enumClass = invalidFormat.getTargetType();
-                String enumValues = Arrays.stream(enumClass.getEnumConstants())
-                    .map(Object::toString)
-                    .collect(Collectors.joining(", "));
-                valueIndicationMessage = "사용 가능한 값 : [" + enumValues + "]";
-            }
-
-            String expectedType = invalidFormat.getTargetType().getSimpleName();
-            Object value = invalidFormat.getValue();
-
-            errorMessage = String.format("필드 '%s'의 값 '%s'이(가) 예상 타입(%s)과 일치하지 않습니다. %s",
-                fieldName, value, expectedType, valueIndicationMessage);
-
-        } else if (rootCause instanceof MismatchedInputException mismatchedInput) {
-            String fieldPath = mismatchedInput.getPath().stream()
-                .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
-                .collect(Collectors.joining("."));
-            errorMessage = String.format("필수 필드 '%s'이(가) 누락되었습니다.", fieldPath);
-
-        } else if (rootCause instanceof JsonMappingException jsonMapping) {
-            String fieldPath = jsonMapping.getPath().stream()
-                .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
-                .collect(Collectors.joining("."));
-            errorMessage = String.format("필드 '%s'에서 JSON 매핑 오류가 발생했습니다: %s",
-                fieldPath, jsonMapping.getOriginalMessage());
-
-        } else {
-            errorMessage = "요청 본문을 처리하는 중 오류가 발생했습니다. JSON 메세지 규격을 확인해주세요.";
-        }
-
-        return failureResponse(ApiErrorType.BAD_REQUEST, errorMessage);
-    }
-
-    @ExceptionHandler
-    public ResponseEntity<ApiResponse<?>> handleBadRequest(ServerWebInputException e) {
-        String missingParams = extractMissingParameter(e.getReason() != null ? e.getReason() : "");
-        if (!missingParams.isEmpty()) {
-            String message = String.format("필수 요청 값 '%s'가 누락되었습니다.", missingParams);
-            return failureResponse(ApiErrorType.BAD_REQUEST, message);
-        } else {
-            return failureResponse(ApiErrorType.BAD_REQUEST, null);
-        }
-    }
-
-    @ExceptionHandler
-    public ResponseEntity<ApiResponse<?>> handleNotFound(NoResourceFoundException e) {
-        return failureResponse(ApiErrorType.NOT_FOUND, null);
-    }
-
-    @ExceptionHandler
-    public ResponseEntity<ApiResponse<?>> handle(Throwable e) {
-        log.error("Exception : {}", e.getMessage(), e);
-        return failureResponse(ApiErrorType.INTERNAL_ERROR, null);
-    }
-
-    private String extractMissingParameter(String message) {
-        Pattern pattern = Pattern.compile("'(.+?)'");
-        Matcher matcher = pattern.matcher(message);
-        return matcher.find() ? matcher.group(1) : "";
-    }
-
-    private ResponseEntity<ApiResponse<?>> failureResponse(ApiErrorType apiErrorType, String errorMessage) {
-        return ResponseEntity.status(apiErrorType.getStatus())
-            .body(ApiResponse.fail(apiErrorType.getCode(), errorMessage != null ? errorMessage : apiErrorType.getMessage()));
-    }
-}
diff --git a/core/infra/database/mysql/mysql-config/src/main/resources/jpa.yml b/core/infra/database/mysql/mysql-config/src/main/resources/jpa.yml
new file mode 100644
index 000000000..37f4fb1b0
--- /dev/null
+++ b/core/infra/database/mysql/mysql-config/src/main/resources/jpa.yml
@@ -0,0 +1,96 @@
+spring:
+  jpa:
+    open-in-view: false
+    generate-ddl: false
+    show-sql: false
+    hibernate:
+      ddl-auto: none
+    properties:
+      hibernate:
+        default_batch_fetch_size: 100
+        timezone.default_storage: NORMALIZE_UTC
+        jdbc.time_zone: UTC
+
+datasource:
+  mysql-jpa:
+    main:
+      driver-class-name: com.mysql.cj.jdbc.Driver
+      jdbc-url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}
+      username: ${MYSQL_USER}
+      password: "${MYSQL_PWD}"
+      pool-name: mysql-main-pool
+      maximum-pool-size: 40
+      minimum-idle: 30
+      connection-timeout: 3000 # 커넥션 획득 대기시간(ms) ( default: 3000 = 3sec )
+      validation-timeout: 5000 # 커넥션 유효성 검사시간(ms) ( default: 5000 = 5sec )
+      keepalive-time: 0 # 커넥션 최대 생존시간(ms) ( default: 0 )
+      max-lifetime: 1800000 # 커넥션 최대 생존시간(ms) ( default: 1800000 = 30min )
+      leak-detection-threshold: 0  # 커넥션 누수 감지 (주어진 ms 내에 반환 안 하면 로그 경고) ( default: 0 = 비활성화 )
+      initialization-fail-timeout: 1  # DB 연결 실패 시 즉시 예외 발생 ( default: -1 = 무한대기 )
+      data-source-properties:
+        rewriteBatchedStatements: true
+
+---
+spring.config.activate.on-profile: local
+
+spring:
+  jpa:
+    show-sql: true
+    hibernate:
+      ddl-auto: create
+
+datasource:
+  mysql-jpa:
+    main:
+      jdbc-url: jdbc:mysql://localhost:3306/loopers
+      username: application
+      password: application
+
+---
+spring.config.activate.on-profile: test
+
+spring:
+  jpa:
+    show-sql: true
+    hibernate:
+      ddl-auto: create
+
+datasource:
+  mysql-jpa:
+    main:
+      maximum-pool-size: 10
+      minimum-idle: 5
+
+---
+spring.config.activate.on-profile: dev
+
+spring:
+  jpa:
+    show-sql: true
+
+datasource:
+  mysql-jpa:
+    main:
+      jdbc-url: jdbc:mysql://localhost:3306/loopers
+      username: application
+      password: application
+
+---
+spring.config.activate.on-profile: qa
+
+datasource:
+  mysql-jpa:
+    main:
+      jdbc-url: jdbc:mysql://localhost:3306/loopers
+      username: application
+      password: application
+
+---
+spring.config.activate.on-profile: prd
+
+datasource:
+  mysql-jpa:
+    main:
+      jdbc-url: jdbc:mysql://localhost:3306/loopers
+      username: application
+      password: application
diff --git a/core/infra/database/mysql/mysql-core/build.gradle.kts b/core/infra/database/mysql/mysql-core/build.gradle.kts
new file mode 100644
index 000000000..403433ffd
--- /dev/null
+++ b/core/infra/database/mysql/mysql-core/build.gradle.kts
@@ -0,0 +1,15 @@
+plugins {
+    `java-library`
+    `java-test-fixtures`
+}
+
+
+dependencies {
+    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
+    runtimeOnly("com.mysql:mysql-connector-j")
+
+    testFixturesImplementation("org.testcontainers:testcontainers")
+    testFixturesImplementation("org.springframework.boot:spring-boot-starter-data-jpa")
+    testFixturesImplementation("org.testcontainers:junit-jupiter")
+    testFixturesImplementation("org.testcontainers:mariadb")
+}
diff --git a/modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java b/core/infra/database/mysql/mysql-core/src/testFixtures/java/com/loopers/core/infra/mysql/util/DatabaseCleanUp.java
similarity index 80%
rename from modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
rename to core/infra/database/mysql/mysql-core/src/testFixtures/java/com/loopers/core/infra/mysql/util/DatabaseCleanUp.java
index 85c43ace9..8cb3904c9 100644
--- a/modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java
+++ b/core/infra/database/mysql/mysql-core/src/testFixtures/java/com/loopers/core/infra/mysql/util/DatabaseCleanUp.java
@@ -1,4 +1,4 @@
-package com.loopers.utils;
+package com.loopers.core.infra.mysql.util;
 
 import jakarta.persistence.Entity;
 import jakarta.persistence.EntityManager;
@@ -14,17 +14,16 @@
 @Component
 public class DatabaseCleanUp implements InitializingBean {
 
+    private final List<String> tableNames = new ArrayList<>();
     @PersistenceContext
     private EntityManager entityManager;
 
-    private final List<String> tableNames = new ArrayList<>();
-
     @Override
     public void afterPropertiesSet() {
         entityManager.getMetamodel().getEntities().stream()
-            .filter(entity -> entity.getJavaType().getAnnotation(Entity.class) != null)
-            .map(entity -> entity.getJavaType().getAnnotation(Table.class).name())
-            .forEach(tableNames::add);
+                .filter(entity -> entity.getJavaType().getAnnotation(Entity.class) != null)
+                .map(entity -> entity.getJavaType().getAnnotation(Table.class).name())
+                .forEach(tableNames::add);
     }
 
     @Transactional
diff --git a/modules/jpa/src/testFixtures/java/com/loopers/utils/MySqlDatabaseCleanUp.java b/modules/jpa/src/testFixtures/java/com/loopers/utils/MySqlDatabaseCleanUp.java
new file mode 100644
index 000000000..2a464dcea
--- /dev/null
+++ b/modules/jpa/src/testFixtures/java/com/loopers/utils/MySqlDatabaseCleanUp.java
@@ -0,0 +1,40 @@
+package com.loopers.utils;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Table;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+public class MySqlDatabaseCleanUp implements InitializingBean {
+
+    private final List<String> tableNames = new ArrayList<>();
+    @PersistenceContext
+    private EntityManager entityManager;
+
+    @Override
+    public void afterPropertiesSet() {
+        entityManager.getMetamodel().getEntities().stream()
+                .filter(entity -> entity.getJavaType().getAnnotation(Entity.class) != null)
+                .map(entity -> entity.getJavaType().getAnnotation(Table.class).name())
+                .forEach(tableNames::add);
+    }
+
+    @Transactional
+    public void truncateAllTables() {
+        entityManager.flush();
+        entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate();
+
+        for (String table : tableNames) {
+            entityManager.createNativeQuery("TRUNCATE TABLE `" + table + "`").executeUpdate();
+        }
+
+        entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate();
+    }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f338b7b6a..b7630195c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,4 +1,4 @@
-rootProject.name = "loopers-java-spring-template"
+rootProject.name = "e-commerce"
 
 include(
     ":apps:commerce-api",
@@ -10,7 +10,8 @@ include(
     ":supports:logging",
     ":supports:monitoring",
     ":core:domain",
-    ":core:infra:database:mysql:mysql-config"
+    ":core:infra:database:mysql:mysql-config",
+    ":core:infra:database:mysql:mysql-core"
 )
 
 // configurations

From 5d884f90ec3046f2f1b16156868bc79b660d05b5 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 22:24:14 +0900
Subject: [PATCH 06/29] =?UTF-8?q?setting=20:=20#1=20mysql=20core=EC=97=90?=
 =?UTF-8?q?=20BaseEntity.java=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../core/infra/mysql/entity/BaseEntity.java   | 28 +++++++++++++++++++
 1 file changed, 28 insertions(+)
 create mode 100644 core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/mysql/entity/BaseEntity.java

diff --git a/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/mysql/entity/BaseEntity.java b/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/mysql/entity/BaseEntity.java
new file mode 100644
index 000000000..123160f52
--- /dev/null
+++ b/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/mysql/entity/BaseEntity.java
@@ -0,0 +1,28 @@
+package com.loopers.core.infra.mysql.entity;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+
+import java.time.ZonedDateTime;
+
+/**
+ * 생성/수정/삭제 정보를 자동으로 관리해준다.
+ * 재사용성을 위해 이 외의 컬럼이나 동작은 추가하지 않는다.
+ */
+@MappedSuperclass
+@Getter
+public abstract class BaseEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private final Long id = 0L;
+
+    @Column(name = "created_at", nullable = false, updatable = false)
+    private ZonedDateTime createdAt;
+
+    @Column(name = "updated_at", nullable = false)
+    private ZonedDateTime updatedAt;
+
+    @Column(name = "deleted_at")
+    private ZonedDateTime deletedAt;
+}

From fa8e3fec46e37ac125727cd13eed26cf6b5a1b39 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 22:28:14 +0900
Subject: [PATCH 07/29] =?UTF-8?q?setting=20:=20#1=20modules=20=ED=95=98?=
 =?UTF-8?q?=EC=9C=84=20jpa=20=EB=AA=A8=EB=93=88=20=EC=82=AD=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/commerce-streamer/build.gradle.kts       |  2 -
 modules/jpa/build.gradle.kts                  | 21 ----
 .../loopers/config/jpa/DataSourceConfig.java  | 24 -----
 .../com/loopers/config/jpa/JpaConfig.java     | 13 ---
 .../loopers/config/jpa/QueryDslConfig.java    | 17 ----
 .../java/com/loopers/domain/BaseEntity.java   | 73 --------------
 modules/jpa/src/main/resources/jpa.yml        | 96 -------------------
 .../MySqlTestContainersConfig.java            | 36 -------
 .../loopers/utils/MySqlDatabaseCleanUp.java   | 40 --------
 settings.gradle.kts                           |  1 -
 10 files changed, 323 deletions(-)
 delete mode 100644 modules/jpa/build.gradle.kts
 delete mode 100644 modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java
 delete mode 100644 modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java
 delete mode 100644 modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java
 delete mode 100644 modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
 delete mode 100644 modules/jpa/src/main/resources/jpa.yml
 delete mode 100644 modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java
 delete mode 100644 modules/jpa/src/testFixtures/java/com/loopers/utils/MySqlDatabaseCleanUp.java

diff --git a/apps/commerce-streamer/build.gradle.kts b/apps/commerce-streamer/build.gradle.kts
index ba710e6eb..4b45916ba 100644
--- a/apps/commerce-streamer/build.gradle.kts
+++ b/apps/commerce-streamer/build.gradle.kts
@@ -1,6 +1,5 @@
 dependencies {
     // add-ons
-    implementation(project(":modules:jpa"))
     implementation(project(":modules:redis"))
     implementation(project(":modules:kafka"))
     implementation(project(":supports:jackson"))
@@ -17,7 +16,6 @@ dependencies {
     annotationProcessor("jakarta.annotation:jakarta.annotation-api")
 
     // test-fixtures
-    testImplementation(testFixtures(project(":modules:jpa")))
     testImplementation(testFixtures(project(":modules:redis")))
     testImplementation(testFixtures(project(":modules:kafka")))
 }
diff --git a/modules/jpa/build.gradle.kts b/modules/jpa/build.gradle.kts
deleted file mode 100644
index e62a6a7ed..000000000
--- a/modules/jpa/build.gradle.kts
+++ /dev/null
@@ -1,21 +0,0 @@
-plugins {
-    `java-library`
-    `java-test-fixtures`
-}
-
-dependencies {
-    // jpa
-    api("org.springframework.boot:spring-boot-starter-data-jpa")
-    // querydsl
-    api("com.querydsl:querydsl-jpa::jakarta")
-    annotationProcessor("com.querydsl:querydsl-apt::jakarta")
-    annotationProcessor("jakarta.persistence:jakarta.persistence-api")
-    annotationProcessor("jakarta.annotation:jakarta.annotation-api")
-    // jdbc-mysql
-    runtimeOnly("com.mysql:mysql-connector-j")
-
-    testImplementation("org.testcontainers:mysql")
-
-    testFixturesImplementation("org.springframework.boot:spring-boot-starter-data-jpa")
-    testFixturesImplementation("org.testcontainers:mysql")
-}
diff --git a/modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java b/modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java
deleted file mode 100644
index 723ffa6a4..000000000
--- a/modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.loopers.config.jpa;
-
-import com.zaxxer.hikari.HikariConfig;
-import com.zaxxer.hikari.HikariDataSource;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
-
-@Configuration
-class DataSourceConfig {
-    @Bean
-    @ConfigurationProperties(prefix = "datasource.mysql-jpa.main")
-    HikariConfig mySqlMainHikariConfig() {
-        return new HikariConfig();
-    }
-
-    @Primary
-    @Bean
-    HikariDataSource mySqlMainDataSource(@Qualifier("mySqlMainHikariConfig") HikariConfig hikariConfig) {
-        return new HikariDataSource(hikariConfig);
-    }
-}
diff --git a/modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java b/modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java
deleted file mode 100644
index 7fad5872b..000000000
--- a/modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.loopers.config.jpa;
-
-import org.springframework.boot.autoconfigure.domain.EntityScan;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
-import org.springframework.transaction.annotation.EnableTransactionManagement;
-
-@Configuration
-@EnableTransactionManagement
-@EntityScan({"com.loopers"})
-@EnableJpaRepositories({"com.loopers.infrastructure"})
-public class JpaConfig {
-}
diff --git a/modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java b/modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java
deleted file mode 100644
index 2b95dd37a..000000000
--- a/modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.loopers.config.jpa;
-
-import com.querydsl.jpa.impl.JPAQueryFactory;
-import jakarta.persistence.EntityManager;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
-
-@Configuration
-class QueryDslConfig {
-
-    @Primary
-    @Bean
-    JPAQueryFactory mySqlJpaQueryFactory(EntityManager entityManager) {
-        return new JPAQueryFactory(entityManager);
-    }
-}
diff --git a/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java b/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
deleted file mode 100644
index d15a9c764..000000000
--- a/modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.loopers.domain;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.MappedSuperclass;
-import jakarta.persistence.PrePersist;
-import jakarta.persistence.PreUpdate;
-import lombok.Getter;
-import java.time.ZonedDateTime;
-
-/**
- * 생성/수정/삭제 정보를 자동으로 관리해준다.
- * 재사용성을 위해 이 외의 컬럼이나 동작은 추가하지 않는다.
- */
-@MappedSuperclass
-@Getter
-public abstract class BaseEntity {
-
-    @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private final Long id = 0L;
-
-    @Column(name = "created_at", nullable = false, updatable = false)
-    private ZonedDateTime createdAt;
-
-    @Column(name = "updated_at", nullable = false)
-    private ZonedDateTime updatedAt;
-
-    @Column(name = "deleted_at")
-    private ZonedDateTime deletedAt;
-
-    /**
-     * 엔티티의 유효성을 검증한다.
-     * 이 메소드는 PrePersist 및 PreUpdate 시점에 호출된다.
-     */
-    protected void guard() {}
-
-    @PrePersist
-    private void prePersist() {
-        guard();
-
-        ZonedDateTime now = ZonedDateTime.now();
-        this.createdAt = now;
-        this.updatedAt = now;
-    }
-
-    @PreUpdate
-    private void preUpdate() {
-        guard();
-
-        this.updatedAt = ZonedDateTime.now();
-    }
-
-    /**
-     * delete 연산은 멱등하게 동작할 수 있도록 한다. (삭제된 엔티티를 다시 삭제해도 동일한 결과가 나오도록)
-     */
-    public void delete() {
-        if (this.deletedAt == null) {
-            this.deletedAt = ZonedDateTime.now();
-        }
-    }
-
-    /**
-     * restore 연산은 멱등하게 동작할 수 있도록 한다. (삭제되지 않은 엔티티를 복원해도 동일한 결과가 나오도록)
-     */
-    public void restore() {
-        if (this.deletedAt != null) {
-            this.deletedAt = null;
-        }
-    }
-}
diff --git a/modules/jpa/src/main/resources/jpa.yml b/modules/jpa/src/main/resources/jpa.yml
deleted file mode 100644
index 37f4fb1b0..000000000
--- a/modules/jpa/src/main/resources/jpa.yml
+++ /dev/null
@@ -1,96 +0,0 @@
-spring:
-  jpa:
-    open-in-view: false
-    generate-ddl: false
-    show-sql: false
-    hibernate:
-      ddl-auto: none
-    properties:
-      hibernate:
-        default_batch_fetch_size: 100
-        timezone.default_storage: NORMALIZE_UTC
-        jdbc.time_zone: UTC
-
-datasource:
-  mysql-jpa:
-    main:
-      driver-class-name: com.mysql.cj.jdbc.Driver
-      jdbc-url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}
-      username: ${MYSQL_USER}
-      password: "${MYSQL_PWD}"
-      pool-name: mysql-main-pool
-      maximum-pool-size: 40
-      minimum-idle: 30
-      connection-timeout: 3000 # 커넥션 획득 대기시간(ms) ( default: 3000 = 3sec )
-      validation-timeout: 5000 # 커넥션 유효성 검사시간(ms) ( default: 5000 = 5sec )
-      keepalive-time: 0 # 커넥션 최대 생존시간(ms) ( default: 0 )
-      max-lifetime: 1800000 # 커넥션 최대 생존시간(ms) ( default: 1800000 = 30min )
-      leak-detection-threshold: 0  # 커넥션 누수 감지 (주어진 ms 내에 반환 안 하면 로그 경고) ( default: 0 = 비활성화 )
-      initialization-fail-timeout: 1  # DB 연결 실패 시 즉시 예외 발생 ( default: -1 = 무한대기 )
-      data-source-properties:
-        rewriteBatchedStatements: true
-
----
-spring.config.activate.on-profile: local
-
-spring:
-  jpa:
-    show-sql: true
-    hibernate:
-      ddl-auto: create
-
-datasource:
-  mysql-jpa:
-    main:
-      jdbc-url: jdbc:mysql://localhost:3306/loopers
-      username: application
-      password: application
-
----
-spring.config.activate.on-profile: test
-
-spring:
-  jpa:
-    show-sql: true
-    hibernate:
-      ddl-auto: create
-
-datasource:
-  mysql-jpa:
-    main:
-      maximum-pool-size: 10
-      minimum-idle: 5
-
----
-spring.config.activate.on-profile: dev
-
-spring:
-  jpa:
-    show-sql: true
-
-datasource:
-  mysql-jpa:
-    main:
-      jdbc-url: jdbc:mysql://localhost:3306/loopers
-      username: application
-      password: application
-
----
-spring.config.activate.on-profile: qa
-
-datasource:
-  mysql-jpa:
-    main:
-      jdbc-url: jdbc:mysql://localhost:3306/loopers
-      username: application
-      password: application
-
----
-spring.config.activate.on-profile: prd
-
-datasource:
-  mysql-jpa:
-    main:
-      jdbc-url: jdbc:mysql://localhost:3306/loopers
-      username: application
-      password: application
diff --git a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java b/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java
deleted file mode 100644
index 9c41edacc..000000000
--- a/modules/jpa/src/testFixtures/java/com/loopers/testcontainers/MySqlTestContainersConfig.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.loopers.testcontainers;
-
-import org.springframework.context.annotation.Configuration;
-import org.testcontainers.containers.MySQLContainer;
-import org.testcontainers.utility.DockerImageName;
-
-@Configuration
-public class MySqlTestContainersConfig {
-
-    private static final MySQLContainer<?> mySqlContainer;
-
-    static {
-        mySqlContainer = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
-            .withDatabaseName("loopers")
-            .withUsername("test")
-            .withPassword("test")
-            .withExposedPorts(3306)
-            .withCommand(
-                "--character-set-server=utf8mb4",
-                "--collation-server=utf8mb4_general_ci",
-                "--skip-character-set-client-handshake"
-            );
-        mySqlContainer.start();
-
-        String mySqlJdbcUrl = String.format(
-            "jdbc:mysql://%s:%d/%s",
-            mySqlContainer.getHost(),
-            mySqlContainer.getFirstMappedPort(),
-            mySqlContainer.getDatabaseName()
-        );
-
-        System.setProperty("datasource.mysql-jpa.main.jdbc-url", mySqlJdbcUrl);
-        System.setProperty("datasource.mysql-jpa.main.username", mySqlContainer.getUsername());
-        System.setProperty("datasource.mysql-jpa.main.password", mySqlContainer.getPassword());
-    }
-}
diff --git a/modules/jpa/src/testFixtures/java/com/loopers/utils/MySqlDatabaseCleanUp.java b/modules/jpa/src/testFixtures/java/com/loopers/utils/MySqlDatabaseCleanUp.java
deleted file mode 100644
index 2a464dcea..000000000
--- a/modules/jpa/src/testFixtures/java/com/loopers/utils/MySqlDatabaseCleanUp.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.loopers.utils;
-
-import jakarta.persistence.Entity;
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.PersistenceContext;
-import jakarta.persistence.Table;
-import org.springframework.beans.factory.InitializingBean;
-import org.springframework.stereotype.Component;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Component
-public class MySqlDatabaseCleanUp implements InitializingBean {
-
-    private final List<String> tableNames = new ArrayList<>();
-    @PersistenceContext
-    private EntityManager entityManager;
-
-    @Override
-    public void afterPropertiesSet() {
-        entityManager.getMetamodel().getEntities().stream()
-                .filter(entity -> entity.getJavaType().getAnnotation(Entity.class) != null)
-                .map(entity -> entity.getJavaType().getAnnotation(Table.class).name())
-                .forEach(tableNames::add);
-    }
-
-    @Transactional
-    public void truncateAllTables() {
-        entityManager.flush();
-        entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate();
-
-        for (String table : tableNames) {
-            entityManager.createNativeQuery("TRUNCATE TABLE `" + table + "`").executeUpdate();
-        }
-
-        entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate();
-    }
-}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b7630195c..f840c03be 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -3,7 +3,6 @@ rootProject.name = "e-commerce"
 include(
     ":apps:commerce-api",
     ":apps:commerce-streamer",
-    ":modules:jpa",
     ":modules:redis",
     ":modules:kafka",
     ":supports:jackson",

From ce89af3a1905787c7825020c0d68edbfd64c8187 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 22:34:52 +0900
Subject: [PATCH 08/29] =?UTF-8?q?setting=20:=20#1=20=EB=B6=88=ED=95=84?=
 =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=82=AD?=
 =?UTF-8?q?=EC=A0=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/commerce-api/build.gradle.kts         | 6 ------
 build.gradle.kts                           | 2 --
 core/service/user-service/build.gradle.kts | 4 ++++
 modules/kafka/build.gradle.kts             | 2 ++
 settings.gradle.kts                        | 4 ++++
 supports/jackson/build.gradle.kts          | 2 ++
 6 files changed, 12 insertions(+), 8 deletions(-)
 create mode 100644 core/service/user-service/build.gradle.kts

diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index 78d7c702b..3df87b41d 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -11,12 +11,6 @@ dependencies {
     implementation("org.springframework.boot:spring-boot-starter-actuator")
     implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")
 
-    // querydsl
-    annotationProcessor("com.querydsl:querydsl-apt::jakarta")
-    annotationProcessor("jakarta.persistence:jakarta.persistence-api")
-    annotationProcessor("jakarta.annotation:jakarta.annotation-api")
-
     // test-fixtures
     testImplementation(testFixtures(project(":core:infra:database:mysql:mysql-core")))
-    testImplementation(testFixtures(project(":modules:redis")))
 }
diff --git a/build.gradle.kts b/build.gradle.kts
index 9c8490b8a..c17a325c6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -48,8 +48,6 @@ subprojects {
     dependencies {
         // Web
         runtimeOnly("org.springframework.boot:spring-boot-starter-validation")
-        // Spring
-        implementation("org.springframework.boot:spring-boot-starter")
         // Serialize
         implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
         // Lombok
diff --git a/core/service/user-service/build.gradle.kts b/core/service/user-service/build.gradle.kts
new file mode 100644
index 000000000..eae275973
--- /dev/null
+++ b/core/service/user-service/build.gradle.kts
@@ -0,0 +1,4 @@
+dependencies {
+
+
+}
diff --git a/modules/kafka/build.gradle.kts b/modules/kafka/build.gradle.kts
index a6747ded8..fefa813fa 100644
--- a/modules/kafka/build.gradle.kts
+++ b/modules/kafka/build.gradle.kts
@@ -5,6 +5,8 @@ plugins {
 
 dependencies {
     api("org.springframework.kafka:spring-kafka")
+    implementation("org.springframework.boot:spring-boot-starter")
+
 
     testImplementation("org.springframework.kafka:spring-kafka-test")
     testImplementation("org.testcontainers:kafka")
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f840c03be..a9acc0ea8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -33,3 +33,7 @@ pluginManagement {
         }
     }
 }
+
+include("core:service")
+
+include("core:service:user-service")
\ No newline at end of file
diff --git a/supports/jackson/build.gradle.kts b/supports/jackson/build.gradle.kts
index f73320711..0f56f268a 100644
--- a/supports/jackson/build.gradle.kts
+++ b/supports/jackson/build.gradle.kts
@@ -5,6 +5,8 @@ plugins {
 dependencies {
     // spring
     implementation("org.springframework:spring-web")
+    implementation("org.springframework.boot:spring-boot-starter")
+
     // jackson
     implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
     implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")

From 401050d3319400b68703d32fbad433dbf8d46b66 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 22:43:33 +0900
Subject: [PATCH 09/29] =?UTF-8?q?setting=20:=20#1=20user=20=EC=84=9C?=
 =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=AA=A8=EB=93=88=20=EB=B6=84=EB=A6=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/commerce-api/build.gradle.kts                 |  3 +++
 .../java/com/loopers/core/domain/user/User.java    |  2 +-
 .../core/domain/user/enumeration/UserGender.kt     |  6 ------
 .../loopers/core/domain/user/type/UserGender.java  |  5 +++++
 core/service/user-service/build.gradle.kts         |  4 ++--
 .../core/service/user/CreateUserService.java       | 14 ++++++++++++++
 6 files changed, 25 insertions(+), 9 deletions(-)
 delete mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/enumeration/UserGender.kt
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/type/UserGender.java
 create mode 100644 core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java

diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index 3df87b41d..4382891a5 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -6,6 +6,9 @@ dependencies {
     implementation(project(":supports:logging"))
     implementation(project(":supports:monitoring"))
 
+    //service
+    implementation(project(":core:service:user-service"))
+
     // web
     implementation("org.springframework.boot:spring-boot-starter-web")
     implementation("org.springframework.boot:spring-boot-starter-actuator")
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/User.java b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
index 78a53ebb7..c02fba1d7 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/User.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
@@ -3,7 +3,7 @@
 import com.loopers.core.domain.common.vo.CreatedAt;
 import com.loopers.core.domain.common.vo.DeletedAt;
 import com.loopers.core.domain.common.vo.UpdatedAt;
-import com.loopers.core.domain.user.enumeration.UserGender;
+import com.loopers.core.domain.user.type.UserGender;
 import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
 import com.loopers.core.domain.user.vo.UserId;
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/enumeration/UserGender.kt b/core/domain/src/main/java/com/loopers/core/domain/user/enumeration/UserGender.kt
deleted file mode 100644
index 309932dde..000000000
--- a/core/domain/src/main/java/com/loopers/core/domain/user/enumeration/UserGender.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.loopers.core.domain.user.enumeration
-
-enum class UserGender {
-
-    MALE, FEMALE
-}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/type/UserGender.java b/core/domain/src/main/java/com/loopers/core/domain/user/type/UserGender.java
new file mode 100644
index 000000000..826463372
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/type/UserGender.java
@@ -0,0 +1,5 @@
+package com.loopers.core.domain.user.type;
+
+public enum UserGender {
+    MALE, FEMALE
+}
diff --git a/core/service/user-service/build.gradle.kts b/core/service/user-service/build.gradle.kts
index eae275973..b19e7e652 100644
--- a/core/service/user-service/build.gradle.kts
+++ b/core/service/user-service/build.gradle.kts
@@ -1,4 +1,4 @@
 dependencies {
-
-
+    implementation("org.springframework.boot:spring-boot-starter")
+    implementation(project(":core:domain"))
 }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java
new file mode 100644
index 000000000..c6caa059f
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java
@@ -0,0 +1,14 @@
+package com.loopers.core.service.user;
+
+import com.loopers.core.domain.user.User;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CreateUserService {
+
+    public User createUser(User user) {
+        return user;
+    }
+}

From f39b06ad42786ccafb5c4a7c62e804a0a1b2a4ff Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 22:52:00 +0900
Subject: [PATCH 10/29] =?UTF-8?q?feature=20:=20#1=20user=20=EC=83=9D?=
 =?UTF-8?q?=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../core/domain/common/vo/DeletedAt.java      |  4 ++++
 .../com/loopers/core/domain/user/User.java    | 18 ++++++++++++++
 .../user/repository/UserRepository.java       |  8 +++++++
 .../loopers/core/domain/user/vo/UserId.java   |  4 ++++
 .../core/service/user/CreateUserService.java  | 17 +++++++++++--
 .../user/command/CreateUserCommand.java       | 24 +++++++++++++++++++
 6 files changed, 73 insertions(+), 2 deletions(-)
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
 create mode 100644 core/service/user-service/src/main/java/com/loopers/core/service/user/command/CreateUserCommand.java

diff --git a/core/domain/src/main/java/com/loopers/core/domain/common/vo/DeletedAt.java b/core/domain/src/main/java/com/loopers/core/domain/common/vo/DeletedAt.java
index b5efff97c..be83cce71 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/common/vo/DeletedAt.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/common/vo/DeletedAt.java
@@ -7,4 +7,8 @@ public record DeletedAt(LocalDateTime value) {
     public static DeletedAt now() {
         return new DeletedAt(LocalDateTime.now());
     }
+
+    public static DeletedAt empty() {
+        return new DeletedAt(null);
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/User.java b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
index c02fba1d7..eb396cb81 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/User.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
@@ -48,4 +48,22 @@ private User(
         this.updatedAt = updatedAt;
         this.deletedAt = deletedAt;
     }
+
+    public static User create(
+            UserIdentifier identifier,
+            UserEmail email,
+            UserBirthDay birthDay,
+            UserGender gender
+    ) {
+        return new User(
+                UserId.empty(),
+                identifier,
+                email,
+                birthDay,
+                gender,
+                CreatedAt.now(),
+                UpdatedAt.now(),
+                DeletedAt.empty()
+        );
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
new file mode 100644
index 000000000..1ef63b91c
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
@@ -0,0 +1,8 @@
+package com.loopers.core.domain.user.repository;
+
+import com.loopers.core.domain.user.User;
+
+public interface UserRepository {
+    
+    User save(User user);
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java
index c01ae14a8..3e8d67714 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java
@@ -1,4 +1,8 @@
 package com.loopers.core.domain.user.vo;
 
 public record UserId(String value) {
+
+    public static UserId empty() {
+        return new UserId(null);
+    }
 }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java
index c6caa059f..74c28cc25 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java
@@ -1,6 +1,12 @@
 package com.loopers.core.service.user;
 
 import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.type.UserGender;
+import com.loopers.core.domain.user.vo.UserBirthDay;
+import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.user.command.CreateUserCommand;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
@@ -8,7 +14,14 @@
 @RequiredArgsConstructor
 public class CreateUserService {
 
-    public User createUser(User user) {
-        return user;
+    private final UserRepository userRepository;
+
+    public User createUser(CreateUserCommand command) {
+        UserIdentifier userIdentifier = new UserIdentifier(command.getUserIdentifier());
+        UserEmail userEmail = new UserEmail(command.getEmail());
+        UserBirthDay userBirthDay = new UserBirthDay(command.getBirthDay());
+        UserGender userGender = UserGender.valueOf(command.getGender());
+
+        return userRepository.save(User.create(userIdentifier, userEmail, userBirthDay, userGender));
     }
 }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/command/CreateUserCommand.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/command/CreateUserCommand.java
new file mode 100644
index 000000000..ae0fb95f3
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/command/CreateUserCommand.java
@@ -0,0 +1,24 @@
+package com.loopers.core.service.user.command;
+
+import lombok.Getter;
+
+import java.time.LocalDateTime;
+
+@Getter
+public class CreateUserCommand {
+
+    private final String userIdentifier;
+
+    private final String email;
+
+    private final LocalDateTime birthDay;
+
+    private final String gender;
+
+    public CreateUserCommand(String userIdentifier, String email, LocalDateTime birthDay, String gender) {
+        this.userIdentifier = userIdentifier;
+        this.email = email;
+        this.birthDay = birthDay;
+        this.gender = gender;
+    }
+}

From d9aa79fc8a47f63b3e45f2fb8535d2adf6a6a6cc Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 23:03:45 +0900
Subject: [PATCH 11/29] =?UTF-8?q?feature=20:=20#1=20user-mysql=20=EB=AA=A8?=
 =?UTF-8?q?=EB=93=88=20=EC=83=9D=EC=84=B1=20=ED=9B=84=20entity=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../mysql/entity/BaseEntity.java              |  2 +-
 .../mysql/user-mysql/build.gradle.kts         |  5 ++++
 .../mysql/user/entity/UserEntity.java         | 27 +++++++++++++++++++
 settings.gradle.kts                           |  3 ++-
 4 files changed, 35 insertions(+), 2 deletions(-)
 rename core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/{ => database}/mysql/entity/BaseEntity.java (92%)
 create mode 100644 core/infra/database/mysql/user-mysql/build.gradle.kts
 create mode 100644 core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java

diff --git a/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/mysql/entity/BaseEntity.java b/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/database/mysql/entity/BaseEntity.java
similarity index 92%
rename from core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/mysql/entity/BaseEntity.java
rename to core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/database/mysql/entity/BaseEntity.java
index 123160f52..b3c680b90 100644
--- a/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/mysql/entity/BaseEntity.java
+++ b/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/database/mysql/entity/BaseEntity.java
@@ -1,4 +1,4 @@
-package com.loopers.core.infra.mysql.entity;
+package com.loopers.core.infra.database.mysql.entity;
 
 import jakarta.persistence.*;
 import lombok.Getter;
diff --git a/core/infra/database/mysql/user-mysql/build.gradle.kts b/core/infra/database/mysql/user-mysql/build.gradle.kts
new file mode 100644
index 000000000..1533786bb
--- /dev/null
+++ b/core/infra/database/mysql/user-mysql/build.gradle.kts
@@ -0,0 +1,5 @@
+dependencies {
+    implementation(project(":core:domain"))
+    implementation(project(":core:infra:database:mysql:mysql-core"))
+    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
+}
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
new file mode 100644
index 000000000..089cd49ef
--- /dev/null
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
@@ -0,0 +1,27 @@
+package com.loopers.core.infra.database.mysql.user.entity;
+
+import com.loopers.core.infra.database.mysql.entity.BaseEntity;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Index;
+import jakarta.persistence.Table;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(
+        name = "users",
+        indexes = {
+                @Index(name = "idx_user_identifier", columnList = "identifier"),
+                @Index(name = "idx_user_email", columnList = "email")
+        }
+)
+public class UserEntity extends BaseEntity {
+
+    private String identifier;
+
+    private String email;
+
+    private LocalDateTime birthDay;
+
+    private String gender;
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a9acc0ea8..b02833bb7 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -36,4 +36,5 @@ pluginManagement {
 
 include("core:service")
 
-include("core:service:user-service")
\ No newline at end of file
+include("core:service:user-service")
+include("core:infra:database:mysql:user-mysql")
\ No newline at end of file

From fd6afd61b3952dc54005334f30cd005eea6256a7 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 23:24:37 +0900
Subject: [PATCH 12/29] =?UTF-8?q?feature=20:=20#1=20User=20mysql=20?=
 =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/loopers/core/domain/user/User.java    | 13 ++++
 .../database/mysql/entity/BaseEntity.java     | 28 ---------
 .../mysql/user/UserJpaRepository.java         |  7 +++
 .../mysql/user/entity/UserEntity.java         | 59 +++++++++++++++++--
 .../mysql/user/impl/UserRepositoryImpl.java   | 20 +++++++
 core/service/user-service/build.gradle.kts    |  7 ++-
 ...eUserService.java => JoinUserService.java} |  2 +-
 7 files changed, 101 insertions(+), 35 deletions(-)
 delete mode 100644 core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/database/mysql/entity/BaseEntity.java
 create mode 100644 core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
 create mode 100644 core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
 rename core/service/user-service/src/main/java/com/loopers/core/service/user/{CreateUserService.java => JoinUserService.java} (96%)

diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/User.java b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
index eb396cb81..f7141eb16 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/User.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
@@ -66,4 +66,17 @@ public static User create(
                 DeletedAt.empty()
         );
     }
+
+    public static User mappedBy(
+            UserId userId,
+            UserIdentifier identifier,
+            UserEmail email,
+            UserBirthDay birthDay,
+            UserGender gender,
+            CreatedAt createdAt,
+            UpdatedAt updatedAt,
+            DeletedAt deletedAt
+    ) {
+        return new User(userId, identifier, email, birthDay, gender, createdAt, updatedAt, deletedAt);
+    }
 }
diff --git a/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/database/mysql/entity/BaseEntity.java b/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/database/mysql/entity/BaseEntity.java
deleted file mode 100644
index b3c680b90..000000000
--- a/core/infra/database/mysql/mysql-core/src/main/java/com/loopers/core/infra/database/mysql/entity/BaseEntity.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.loopers.core.infra.database.mysql.entity;
-
-import jakarta.persistence.*;
-import lombok.Getter;
-
-import java.time.ZonedDateTime;
-
-/**
- * 생성/수정/삭제 정보를 자동으로 관리해준다.
- * 재사용성을 위해 이 외의 컬럼이나 동작은 추가하지 않는다.
- */
-@MappedSuperclass
-@Getter
-public abstract class BaseEntity {
-
-    @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private final Long id = 0L;
-
-    @Column(name = "created_at", nullable = false, updatable = false)
-    private ZonedDateTime createdAt;
-
-    @Column(name = "updated_at", nullable = false)
-    private ZonedDateTime updatedAt;
-
-    @Column(name = "deleted_at")
-    private ZonedDateTime deletedAt;
-}
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
new file mode 100644
index 000000000..101df6ba4
--- /dev/null
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
@@ -0,0 +1,7 @@
+package com.loopers.core.infra.database.mysql.user;
+
+import com.loopers.core.infra.database.mysql.user.entity.UserEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
+}
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
index 089cd49ef..849034797 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
@@ -1,9 +1,19 @@
 package com.loopers.core.infra.database.mysql.user.entity;
 
-import com.loopers.core.infra.database.mysql.entity.BaseEntity;
-import jakarta.persistence.Entity;
-import jakarta.persistence.Index;
-import jakarta.persistence.Table;
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.DeletedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.type.UserGender;
+import com.loopers.core.domain.user.vo.UserBirthDay;
+import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserId;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.NoArgsConstructor;
 
 import java.time.LocalDateTime;
 
@@ -15,7 +25,14 @@
                 @Index(name = "idx_user_email", columnList = "email")
         }
 )
-public class UserEntity extends BaseEntity {
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Builder(access = AccessLevel.PRIVATE)
+public class UserEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
 
     private String identifier;
 
@@ -24,4 +41,36 @@ public class UserEntity extends BaseEntity {
     private LocalDateTime birthDay;
 
     private String gender;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime updatedAt;
+
+    private LocalDateTime deletedAt;
+
+    public static UserEntity from(User user) {
+        return UserEntity.builder()
+                .id(Long.parseLong(user.getUserId().value()))
+                .identifier(user.getIdentifier().value())
+                .email(user.getEmail().value())
+                .birthDay(user.getBirthDay().value())
+                .gender(user.getGender().name())
+                .createdAt(user.getCreatedAt().value())
+                .updatedAt(user.getUpdatedAt().value())
+                .deletedAt(user.getDeletedAt().value())
+                .build();
+    }
+
+    public User to() {
+        return User.mappedBy(
+                new UserId(this.id.toString()),
+                new UserIdentifier(this.identifier),
+                new UserEmail(this.email),
+                new UserBirthDay(this.birthDay),
+                UserGender.valueOf(this.gender),
+                new CreatedAt(this.createdAt),
+                new UpdatedAt(this.updatedAt),
+                new DeletedAt(this.deletedAt)
+        );
+    }
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
new file mode 100644
index 000000000..bec17d1b4
--- /dev/null
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
@@ -0,0 +1,20 @@
+package com.loopers.core.infra.database.mysql.user.impl;
+
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.infra.database.mysql.user.UserJpaRepository;
+import com.loopers.core.infra.database.mysql.user.entity.UserEntity;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class UserRepositoryImpl implements UserRepository {
+
+    private final UserJpaRepository repository;
+
+    @Override
+    public User save(User user) {
+        return repository.save(UserEntity.from(user)).to();
+    }
+}
diff --git a/core/service/user-service/build.gradle.kts b/core/service/user-service/build.gradle.kts
index b19e7e652..7b81f3693 100644
--- a/core/service/user-service/build.gradle.kts
+++ b/core/service/user-service/build.gradle.kts
@@ -1,4 +1,9 @@
 dependencies {
-    implementation("org.springframework.boot:spring-boot-starter")
     implementation(project(":core:domain"))
+
+    // spring
+    implementation("org.springframework.boot:spring-boot-starter")
+
+    // mysql
+    runtimeOnly(project(":core:infra:database:mysql:user-mysql"))
 }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
similarity index 96%
rename from core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java
rename to core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
index 74c28cc25..faee266ea 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/CreateUserService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
@@ -12,7 +12,7 @@
 
 @Service
 @RequiredArgsConstructor
-public class CreateUserService {
+public class JoinUserService {
 
     private final UserRepository userRepository;
 

From f01fb58867d52a7764fae16e321eeb4338cdd40b Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Sun, 26 Oct 2025 23:51:03 +0900
Subject: [PATCH 13/29] =?UTF-8?q?feature=20:=20#1=20User=20=ED=9A=8C?=
 =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 apps/commerce-api/build.gradle.kts            |  4 +++
 .../application/api/user/UserV1ApiSpec.java   | 22 ++++++++++------
 .../api/user/UserV1Controller.java            | 23 +++++++++++------
 .../application/api/user/UserV1Dto.java       | 25 +++++++++++++++++++
 .../src/main/resources/http/join-user.http    |  9 +++++++
 .../core/domain/user/vo/UserBirthDay.java     |  4 +--
 .../mysql/user/entity/UserEntity.java         | 10 ++++++--
 .../core/service/user/JoinUserService.java    |  4 +--
 ...eUserCommand.java => JoinUserCommand.java} |  8 +++---
 9 files changed, 85 insertions(+), 24 deletions(-)
 create mode 100644 apps/commerce-api/src/main/resources/http/join-user.http
 rename core/service/user-service/src/main/java/com/loopers/core/service/user/command/{CreateUserCommand.java => JoinUserCommand.java} (61%)

diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index 4382891a5..c7946a04a 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -9,8 +9,12 @@ dependencies {
     //service
     implementation(project(":core:service:user-service"))
 
+    //domain
+    implementation(project(":core:domain"))
+    
     // web
     implementation("org.springframework.boot:spring-boot-starter-web")
+    implementation("org.springframework.boot:spring-boot-starter-validation")
     implementation("org.springframework.boot:spring-boot-starter-actuator")
     implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
index dea54db24..bed1841cc 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
@@ -2,18 +2,26 @@
 
 import com.loopers.application.api.common.dto.ApiResponse;
 import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.tags.Tag;
 
+import static com.loopers.application.api.user.UserV1Dto.JoinUserRequest;
+import static com.loopers.application.api.user.UserV1Dto.JoinUserResponse;
+
 @Tag(name = "User V1 API", description = "User API 입니다.")
 public interface UserV1ApiSpec {
 
+//    @Operation(
+//            summary = "사용자 조회",
+//            description = "ID로 사용자를 조회합니다."
+//    )
+//    ApiResponse<UserV1Dto.GetUserResponse> getUser(
+//            @Schema(name = "사용자 ID", description = "조회할 사용자 ID")
+//            Long userId
+//    );
+
     @Operation(
-        summary = "사용자 조회",
-        description = "ID로 사용자를 조회합니다."
+            summary = "사용자 회원가입",
+            description = "사용자가 회원가입합니다."
     )
-    ApiResponse<UserV1Dto.GetUserResponse> getUser(
-        @Schema(name = "사용자 ID", description = "조회할 사용자 ID")
-        Long userId
-    );
+    ApiResponse<JoinUserResponse> joinUser(JoinUserRequest request);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
index 8fa067a52..e7a2dc3ec 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
@@ -1,22 +1,31 @@
 package com.loopers.application.api.user;
 
 import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.service.user.JoinUserService;
 import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-@RequiredArgsConstructor
+import static com.loopers.application.api.user.UserV1Dto.JoinUserRequest;
+import static com.loopers.application.api.user.UserV1Dto.JoinUserResponse;
+
 @RestController
+@RequiredArgsConstructor
 @RequestMapping("/api/v1/users")
 public class UserV1Controller implements UserV1ApiSpec {
 
-    @GetMapping("/{userId}")
+    private final JoinUserService joinUserService;
+
     @Override
-    public ApiResponse<UserV1Dto.GetUserResponse> getUser(
-            @PathVariable(value = "userId") Long userId
+    @PostMapping("/join")
+    public ApiResponse<JoinUserResponse> joinUser(
+            @RequestBody JoinUserRequest request
     ) {
-        return ApiResponse.success(null);
+        User user = joinUserService.joinUser(request.toCommand());
+
+        return ApiResponse.success(JoinUserResponse.from(user));
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
index 65dc4f80c..fee18e277 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -1,8 +1,33 @@
 package com.loopers.application.api.user;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.service.user.command.JoinUserCommand;
+import jakarta.validation.constraints.NotBlank;
+
+import java.time.LocalDate;
+
 public class UserV1Dto {
 
+    public record JoinUserRequest(
+            @NotBlank String identifier,
+            @NotBlank String email,
+            @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") LocalDate birthday,
+            @NotBlank String gender
+    ) {
+        public JoinUserCommand toCommand() {
+            return new JoinUserCommand(identifier, email, birthday, gender);
+        }
+    }
+
     public record GetUserResponse(String userId) {
 
     }
+
+    public record JoinUserResponse(String userId) {
+
+        public static JoinUserResponse from(User user) {
+            return new JoinUserResponse(user.getUserId().value());
+        }
+    }
 }
diff --git a/apps/commerce-api/src/main/resources/http/join-user.http b/apps/commerce-api/src/main/resources/http/join-user.http
new file mode 100644
index 000000000..0eda1a35c
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/join-user.http
@@ -0,0 +1,9 @@
+POST {{commerce-api}}/api/v1/users/join
+Content-Type: application/json
+
+{
+  "identifier": "{{identifier}}",
+  "email": "{{email}}",
+  "birthday": "{{birthday}}",
+  "gender":"{{gender}}"
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
index 194505251..785d1040c 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
@@ -1,6 +1,6 @@
 package com.loopers.core.domain.user.vo;
 
-import java.time.LocalDateTime;
+import java.time.LocalDate;
 
-public record UserBirthDay(LocalDateTime value) {
+public record UserBirthDay(LocalDate value) {
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
index 849034797..49ad931f5 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserEntity.java
@@ -15,7 +15,9 @@
 import lombok.Builder;
 import lombok.NoArgsConstructor;
 
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.util.Optional;
 
 @Entity
 @Table(
@@ -38,7 +40,7 @@ public class UserEntity {
 
     private String email;
 
-    private LocalDateTime birthDay;
+    private LocalDate birthDay;
 
     private String gender;
 
@@ -50,7 +52,11 @@ public class UserEntity {
 
     public static UserEntity from(User user) {
         return UserEntity.builder()
-                .id(Long.parseLong(user.getUserId().value()))
+                .id(
+                        Optional.ofNullable(user.getUserId().value())
+                                .map(Long::parseLong)
+                                .orElse(null)
+                )
                 .identifier(user.getIdentifier().value())
                 .email(user.getEmail().value())
                 .birthDay(user.getBirthDay().value())
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
index faee266ea..2006e0760 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
@@ -6,7 +6,7 @@
 import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
 import com.loopers.core.domain.user.vo.UserIdentifier;
-import com.loopers.core.service.user.command.CreateUserCommand;
+import com.loopers.core.service.user.command.JoinUserCommand;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 
@@ -16,7 +16,7 @@ public class JoinUserService {
 
     private final UserRepository userRepository;
 
-    public User createUser(CreateUserCommand command) {
+    public User joinUser(JoinUserCommand command) {
         UserIdentifier userIdentifier = new UserIdentifier(command.getUserIdentifier());
         UserEmail userEmail = new UserEmail(command.getEmail());
         UserBirthDay userBirthDay = new UserBirthDay(command.getBirthDay());
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/command/CreateUserCommand.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/command/JoinUserCommand.java
similarity index 61%
rename from core/service/user-service/src/main/java/com/loopers/core/service/user/command/CreateUserCommand.java
rename to core/service/user-service/src/main/java/com/loopers/core/service/user/command/JoinUserCommand.java
index ae0fb95f3..91e124c1d 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/command/CreateUserCommand.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/command/JoinUserCommand.java
@@ -2,20 +2,20 @@
 
 import lombok.Getter;
 
-import java.time.LocalDateTime;
+import java.time.LocalDate;
 
 @Getter
-public class CreateUserCommand {
+public class JoinUserCommand {
 
     private final String userIdentifier;
 
     private final String email;
 
-    private final LocalDateTime birthDay;
+    private final LocalDate birthDay;
 
     private final String gender;
 
-    public CreateUserCommand(String userIdentifier, String email, LocalDateTime birthDay, String gender) {
+    public JoinUserCommand(String userIdentifier, String email, LocalDate birthDay, String gender) {
         this.userIdentifier = userIdentifier;
         this.email = email;
         this.birthDay = birthDay;

From f8b1a1155dc84e06551369b0306a8da011fdc757 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Mon, 27 Oct 2025 23:42:24 +0900
Subject: [PATCH 14/29] =?UTF-8?q?feature=20:=20#1=20User=20ID=20=EA=B2=80?=
 =?UTF-8?q?=EC=A6=9D=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../api/user/UserV1Controller.java            |   3 +-
 .../core/domain/error/DomainErrorCode.java    |  24 ++++
 .../com/loopers/core/domain/user/User.java    |   3 +-
 .../core/domain/user/vo/UserIdentifier.java   |  35 ++++++
 .../loopers/core/domain/user/UserTest.java    |  24 ++++
 .../domain/user/vo/UserIdentifierTest.java    | 119 ++++++++++++++++++
 .../core/service/user/JoinUserService.java    |   2 +-
 7 files changed, 207 insertions(+), 3 deletions(-)
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/user/vo/UserIdentifierTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
index e7a2dc3ec..a30d5dc5b 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
@@ -3,6 +3,7 @@
 import com.loopers.application.api.common.dto.ApiResponse;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.service.user.JoinUserService;
+import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -22,7 +23,7 @@ public class UserV1Controller implements UserV1ApiSpec {
     @Override
     @PostMapping("/join")
     public ApiResponse<JoinUserResponse> joinUser(
-            @RequestBody JoinUserRequest request
+            @RequestBody @Valid JoinUserRequest request
     ) {
         User user = joinUserService.joinUser(request.toCommand());
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
new file mode 100644
index 000000000..2dbdb5c8a
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -0,0 +1,24 @@
+package com.loopers.core.domain.error;
+
+import lombok.Getter;
+
+@Getter
+public enum DomainErrorCode {
+
+    MUST_BE_NOT_NULL(1000, "%s는 Null이 될 수 없습니다."),
+    USER_ID_MUST_BE_ENG_OR_NUMBER(1001, "사용자의 ID는 영문 또는 숫자로만 이루어져야합니다."),
+    USER_ID_LENGTH_VALIDATE(1002, "사용자의 ID의 길이는 %d자 이상 %d자 이하여야 합니다.");
+
+    private final int code;
+
+    private final String message;
+
+    DomainErrorCode(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    public static String notNullMessage(String fieldName) {
+        return String.format(MUST_BE_NOT_NULL.message, fieldName);
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/User.java b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
index f7141eb16..a4d68019d 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/User.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
@@ -77,6 +77,7 @@ public static User mappedBy(
             UpdatedAt updatedAt,
             DeletedAt deletedAt
     ) {
-        return new User(userId, identifier, email, birthDay, gender, createdAt, updatedAt, deletedAt);
+        return new User(userId, identifier, email, birthDay, gender, createdAt, updatedAt, deletedAt
+        );
     }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java
index 8318c4b9f..12e77e4b6 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java
@@ -1,4 +1,39 @@
 package com.loopers.core.domain.user.vo;
 
+import com.loopers.core.domain.error.DomainErrorCode;
+
+import java.util.Objects;
+
+import static com.loopers.core.domain.error.DomainErrorCode.USER_ID_LENGTH_VALIDATE;
+import static com.loopers.core.domain.error.DomainErrorCode.USER_ID_MUST_BE_ENG_OR_NUMBER;
+
 public record UserIdentifier(String value) {
+
+    private static final String IDENTIFIER_REGEX = "^[a-zA-Z0-9]+$";
+    private static final int IDENTIFIER_MAX_LENGTH = 10;
+    private static final int IDENTIFIER_MIN_LENGTH = 1;
+
+    public UserIdentifier(String value) {
+        Objects.requireNonNull(value, DomainErrorCode.notNullMessage("사용자의 ID"));
+        this.value = value;
+    }
+
+    public static UserIdentifier create(String value) {
+        Objects.requireNonNull(value, DomainErrorCode.notNullMessage("사용자의 ID"));
+        validate(value);
+
+        return new UserIdentifier(value);
+    }
+
+    private static void validate(String identifier) {
+        if (!identifier.matches(IDENTIFIER_REGEX)) {
+            throw new IllegalArgumentException(USER_ID_MUST_BE_ENG_OR_NUMBER.getMessage());
+        }
+
+        if (identifier.length() < IDENTIFIER_MIN_LENGTH || identifier.length() > IDENTIFIER_MAX_LENGTH) {
+            throw new IllegalArgumentException(
+                    String.format(USER_ID_LENGTH_VALIDATE.getMessage(), IDENTIFIER_MIN_LENGTH, IDENTIFIER_MAX_LENGTH)
+            );
+        }
+    }
 }
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java
new file mode 100644
index 000000000..41eb9c23c
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java
@@ -0,0 +1,24 @@
+package com.loopers.core.domain.user;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+class UserTest {
+
+    @Nested
+    @DisplayName("create()")
+    class CreateTest {
+
+        @Nested
+        @DisplayName("id값이 null이라면")
+        class IdIsNull {
+
+            @Test
+            @DisplayName("IllegalArgumentException이 발생한다.")
+            void shouldThrowIllegalArgumentException_whenIdIsNull() {
+
+            }
+        }
+    }
+}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserIdentifierTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserIdentifierTest.java
new file mode 100644
index 000000000..d947c7996
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserIdentifierTest.java
@@ -0,0 +1,119 @@
+package com.loopers.core.domain.user.vo;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("UserIdentifier")
+class UserIdentifierTest {
+
+    @Nested
+    @DisplayName("create() 메서드")
+    class CreateMethod {
+
+        @Nested
+        @DisplayName("유효한 값인 경우")
+        class 유효한_값인_경우 {
+
+            @Test
+            @DisplayName("UserIdentifier 객체를 생성한다")
+            void 객체_생성() {
+                // given
+                String validId = "user123";
+
+                // when
+                UserIdentifier identifier = UserIdentifier.create(validId);
+
+                // then
+                assertThat(identifier.value()).isEqualTo(validId);
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 null인 경우")
+        class value가_null인_경우 {
+
+            @Test
+            @DisplayName("NullPointerException을 발생시킨다")
+            void 예외_발생() {
+                // given
+                String id = null;
+
+                // when & then
+                assertThatThrownBy(() -> UserIdentifier.create(id))
+                        .isInstanceOf(NullPointerException.class)
+                        .hasMessage("사용자의 ID는 Null이 될 수 없습니다.");
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 영문과 숫자가 아닌 경우")
+        class value가_영문_숫자가_아닌_경우 {
+
+            @Test
+            @DisplayName("특수문자를 포함하면 예외를 발생시킨다")
+            void 특수문자_예외() {
+                // given
+                String id = "user@123";
+
+                // when & then
+                assertThatThrownBy(() -> UserIdentifier.create(id))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("사용자의 ID는 영문 또는 숫자로만 이루어져야합니다.");
+            }
+
+            @Test
+            @DisplayName("공백을 포함하면 예외를 발생시킨다")
+            void 공백_예외() {
+                // given
+                String id = "user 123";
+
+                // when & then
+                assertThatThrownBy(() -> UserIdentifier.create(id))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("사용자의 ID는 영문 또는 숫자로만 이루어져야합니다.");
+            }
+
+            @Test
+            @DisplayName("한글을 포함하면 예외를 발생시킨다")
+            void 한글_예외() {
+                // given
+                String id = "사용자123";
+
+                // when & then
+                assertThatThrownBy(() -> UserIdentifier.create(id))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("사용자의 ID는 영문 또는 숫자로만 이루어져야합니다.");
+            }
+        }
+
+        @Nested
+        @DisplayName("value의 길이가 범위를 벗어나는 경우")
+        class value의_길이가_범위를_벗어나는_경우 {
+
+            static Stream<String> invalidLengthIds() {
+                return Stream.of(
+                        "abcde123456"       // 길이 11 (최대 초과)
+                );
+            }
+
+            @ParameterizedTest
+            @MethodSource("invalidLengthIds")
+            @DisplayName("예외를 발생시킨다")
+            void 예외_발생(String id) {
+
+                assertThatThrownBy(() -> UserIdentifier.create(id))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessageContaining("1자 이상")
+                        .hasMessageContaining("10자 이하");
+            }
+        }
+    }
+}
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
index 2006e0760..43a561f3f 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
@@ -17,7 +17,7 @@ public class JoinUserService {
     private final UserRepository userRepository;
 
     public User joinUser(JoinUserCommand command) {
-        UserIdentifier userIdentifier = new UserIdentifier(command.getUserIdentifier());
+        UserIdentifier userIdentifier = UserIdentifier.create(command.getUserIdentifier());
         UserEmail userEmail = new UserEmail(command.getEmail());
         UserBirthDay userBirthDay = new UserBirthDay(command.getBirthDay());
         UserGender userGender = UserGender.valueOf(command.getGender());

From 9a6e5897c0261af35e1ad2d9efdb5cd49fe0da66 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 28 Oct 2025 21:06:06 +0900
Subject: [PATCH 15/29] =?UTF-8?q?test=20:=20#1=20User=20=EC=9D=B4=EB=A9=94?=
 =?UTF-8?q?=EC=9D=BC=20=EA=B2=80=EC=A6=9D=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../core/domain/error/DomainErrorCode.java    |  5 +-
 .../core/domain/user/vo/UserEmail.java        | 28 +++++++
 .../core/domain/user/vo/UserIdentifier.java   |  9 +-
 .../core/domain/user/vo/UserEmailTest.java    | 84 +++++++++++++++++++
 settings.gradle.kts                           | 10 +--
 5 files changed, 126 insertions(+), 10 deletions(-)
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/user/vo/UserEmailTest.java

diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index 2dbdb5c8a..138d4ef5a 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -5,9 +5,10 @@
 @Getter
 public enum DomainErrorCode {
 
-    MUST_BE_NOT_NULL(1000, "%s는 Null이 될 수 없습니다."),
+    MUST_BE_NOT_NULL(1000, "%s는(은) Null이 될 수 없습니다."),
     USER_ID_MUST_BE_ENG_OR_NUMBER(1001, "사용자의 ID는 영문 또는 숫자로만 이루어져야합니다."),
-    USER_ID_LENGTH_VALIDATE(1002, "사용자의 ID의 길이는 %d자 이상 %d자 이하여야 합니다.");
+    USER_ID_LENGTH_VALIDATE(1002, "사용자의 ID의 길이는 %d자 이상 %d자 이하여야 합니다."),
+    INVALID_EMAIL_FORMAT(1003, "유효하지 않은 이메일 형식입니다.");
 
     private final int code;
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
index 65a6fdbd1..12c6efbe3 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
@@ -1,4 +1,32 @@
 package com.loopers.core.domain.user.vo;
 
+import com.loopers.core.domain.error.DomainErrorCode;
+
+import java.util.Objects;
+
 public record UserEmail(String value) {
+
+    private static final String EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
+
+    public UserEmail(String value) {
+        validateNotNull(value);
+        this.value = value;
+    }
+
+    public static UserEmail create(String email) {
+        validateNotNull(email);
+        validate(email);
+
+        return new UserEmail(email);
+    }
+
+    private static String validateNotNull(String email) {
+        return Objects.requireNonNull(email, DomainErrorCode.notNullMessage("사용자의 이메일"));
+    }
+
+    private static void validate(String email) {
+        if (!email.matches(EMAIL_REGEX)) {
+            throw new IllegalArgumentException(DomainErrorCode.INVALID_EMAIL_FORMAT.getMessage());
+        }
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java
index 12e77e4b6..84070ba29 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java
@@ -14,17 +14,22 @@ public record UserIdentifier(String value) {
     private static final int IDENTIFIER_MIN_LENGTH = 1;
 
     public UserIdentifier(String value) {
-        Objects.requireNonNull(value, DomainErrorCode.notNullMessage("사용자의 ID"));
+        validateNotNull(value);
+
         this.value = value;
     }
 
     public static UserIdentifier create(String value) {
-        Objects.requireNonNull(value, DomainErrorCode.notNullMessage("사용자의 ID"));
+        validateNotNull(value);
         validate(value);
 
         return new UserIdentifier(value);
     }
 
+    private static void validateNotNull(String value) {
+        Objects.requireNonNull(value, DomainErrorCode.notNullMessage("사용자의 ID"));
+    }
+
     private static void validate(String identifier) {
         if (!identifier.matches(IDENTIFIER_REGEX)) {
             throw new IllegalArgumentException(USER_ID_MUST_BE_ENG_OR_NUMBER.getMessage());
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserEmailTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserEmailTest.java
new file mode 100644
index 000000000..c0ca504ff
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserEmailTest.java
@@ -0,0 +1,84 @@
+package com.loopers.core.domain.user.vo;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("UserEmail")
+class UserEmailTest {
+
+    @Nested
+    @DisplayName("create() 메서드")
+    class CreateMethod {
+
+        @Nested
+        @DisplayName("유효한 값인 경우")
+        class 유효한_값인_경우 {
+
+            @Test
+            @DisplayName("UserEmail 객체를 생성한다")
+            void 객체_생성() {
+                // given
+                String validEmail = "user@example.com";
+
+                // when
+                UserEmail email = UserEmail.create(validEmail);
+
+                // then
+                assertThat(email.value()).isEqualTo(validEmail);
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 null인 경우")
+        class value가_null인_경우 {
+
+            @Test
+            @DisplayName("NullPointerException을 발생시킨다")
+            void 예외_발생() {
+                // given
+                String email = null;
+
+                // when & then
+                assertThatThrownBy(() -> UserEmail.create(email))
+                        .isInstanceOf(NullPointerException.class)
+                        .hasMessage("사용자의 이메일는(은) Null이 될 수 없습니다.");
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 유효한 이메일 형식이 아닌 경우")
+        class value가_유효한_이메일_형식이_아닌_경우 {
+
+            static Stream<String> invalidEmails() {
+                return Stream.of(
+                        "invalid.email",              // @ 없음
+                        "user@",                      // 도메인 없음
+                        "@example.com",               // 로컬 부분 없음
+                        "user name@example.com",      // 공백 포함
+                        "user@example",               // TLD 없음
+                        "user@.com",                  // 도메인명 없음
+                        "user@@example.com",          // @ 중복
+                        "user@exam ple.com"           // 도메인에 공백
+                );
+            }
+
+            @ParameterizedTest
+            @MethodSource("invalidEmails")
+            @DisplayName("예외를 발생시킨다")
+            void 예외_발생(String email) {
+                // when & then
+                assertThatThrownBy(() -> UserEmail.create(email))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("유효하지 않은 이메일 형식입니다.");
+            }
+        }
+    }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b02833bb7..c114398f8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -10,7 +10,10 @@ include(
     ":supports:monitoring",
     ":core:domain",
     ":core:infra:database:mysql:mysql-config",
-    ":core:infra:database:mysql:mysql-core"
+    ":core:infra:database:mysql:mysql-core",
+    ":core:infra:database:mysql:user-mysql",
+    ":core:service:user-service",
+    ":core:common"
 )
 
 // configurations
@@ -33,8 +36,3 @@ pluginManagement {
         }
     }
 }
-
-include("core:service")
-
-include("core:service:user-service")
-include("core:infra:database:mysql:user-mysql")
\ No newline at end of file

From 7bc22b79660d70d8e790ecf855636badafe924e8 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 28 Oct 2025 21:35:17 +0900
Subject: [PATCH 16/29] =?UTF-8?q?test=20:=20#1=20User=20=EC=84=B1=EB=B3=84?=
 =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../application/api/user/UserV1Dto.java       |  5 +-
 .../core/domain/error/DomainErrorCode.java    |  4 +-
 .../core/domain/user/type/UserGender.java     | 15 ++++-
 .../core/domain/user/vo/UserBirthDay.java     | 28 +++++++++
 .../core/domain/user/type/UserGenderTest.java | 62 +++++++++++++++++++
 .../core/service/user/JoinUserService.java    |  6 +-
 .../service/user/command/JoinUserCommand.java |  6 +-
 .../user/JoinUserServiceIntegrationTest.java  |  8 +++
 8 files changed, 121 insertions(+), 13 deletions(-)
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/user/type/UserGenderTest.java
 create mode 100644 core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
index fee18e277..24fabb722 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -1,18 +1,15 @@
 package com.loopers.application.api.user;
 
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.service.user.command.JoinUserCommand;
 import jakarta.validation.constraints.NotBlank;
 
-import java.time.LocalDate;
-
 public class UserV1Dto {
 
     public record JoinUserRequest(
             @NotBlank String identifier,
             @NotBlank String email,
-            @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") LocalDate birthday,
+            @NotBlank String birthday,
             @NotBlank String gender
     ) {
         public JoinUserCommand toCommand() {
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index 138d4ef5a..b87dfdba9 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -8,7 +8,9 @@ public enum DomainErrorCode {
     MUST_BE_NOT_NULL(1000, "%s는(은) Null이 될 수 없습니다."),
     USER_ID_MUST_BE_ENG_OR_NUMBER(1001, "사용자의 ID는 영문 또는 숫자로만 이루어져야합니다."),
     USER_ID_LENGTH_VALIDATE(1002, "사용자의 ID의 길이는 %d자 이상 %d자 이하여야 합니다."),
-    INVALID_EMAIL_FORMAT(1003, "유효하지 않은 이메일 형식입니다.");
+    INVALID_EMAIL_FORMAT(1003, "유효하지 않은 이메일 형식입니다."),
+    INVALID_BIRTHDATE_FORMAT(1004, "생년월일은 yyyy-MM-dd 형식이어야 합니다."),
+    GENDER_IS_MALE_OR_FEMALE(1004, "성별은 MALE 혹은 FEMALE이어야 합니다.");
 
     private final int code;
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/type/UserGender.java b/core/domain/src/main/java/com/loopers/core/domain/user/type/UserGender.java
index 826463372..f36216e02 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/type/UserGender.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/type/UserGender.java
@@ -1,5 +1,18 @@
 package com.loopers.core.domain.user.type;
 
+import com.loopers.core.domain.error.DomainErrorCode;
+
+import java.util.Arrays;
+import java.util.Objects;
+
 public enum UserGender {
-    MALE, FEMALE
+    MALE, FEMALE;
+
+    public static UserGender create(String value) {
+        Objects.requireNonNull(value, DomainErrorCode.notNullMessage("사용자의 성별"));
+        return Arrays.stream(values())
+                .filter(gender -> gender.name().equalsIgnoreCase(value))
+                .findAny()
+                .orElseThrow(() -> new IllegalArgumentException(DomainErrorCode.GENDER_IS_MALE_OR_FEMALE.getMessage()));
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
index 785d1040c..665706a42 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
@@ -1,6 +1,34 @@
 package com.loopers.core.domain.user.vo;
 
+import com.loopers.core.domain.error.DomainErrorCode;
+
 import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Objects;
 
 public record UserBirthDay(LocalDate value) {
+
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    public UserBirthDay(LocalDate value) {
+        Objects.requireNonNull(value, DomainErrorCode.notNullMessage("생년월일"));
+        this.value = value;
+    }
+
+    public static UserBirthDay create(String dateString) {
+        Objects.requireNonNull(dateString, DomainErrorCode.notNullMessage("생년월일"));
+        validate(dateString);
+
+        LocalDate parsedDate = LocalDate.parse(dateString, DATE_FORMATTER);
+        return new UserBirthDay(parsedDate);
+    }
+
+    private static void validate(String dateString) {
+        try {
+            DateTimeFormatter.ofPattern("yyyy-MM-dd").parse(dateString);
+        } catch (DateTimeParseException e) {
+            throw new IllegalArgumentException(DomainErrorCode.INVALID_BIRTHDATE_FORMAT.getMessage());
+        }
+    }
 }
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/type/UserGenderTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/type/UserGenderTest.java
new file mode 100644
index 000000000..18581121c
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/type/UserGenderTest.java
@@ -0,0 +1,62 @@
+package com.loopers.core.domain.user.type;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class UserGenderTest {
+
+    @Nested
+    @DisplayName("create() 메서드")
+    class CreateMethod {
+
+        @Nested
+        @DisplayName("유효한 값인 경우")
+        class 유효한_값인_경우 {
+
+            @Test
+            @DisplayName("UserGender 객체를 생성한다")
+            void 객체_생성() {
+                String validGender = "MALE";
+                UserGender gender = UserGender.create(validGender);
+
+                assertThat(gender).isEqualTo(UserGender.MALE);
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 null인 경우")
+        class value가_null인_경우 {
+
+            @Test
+            @DisplayName("NullPointerException을 발생시킨다")
+            void 예외_발생() {
+                // given
+                String gender = null;
+
+                // when & then
+                assertThatThrownBy(() -> UserGender.create(gender))
+                        .isInstanceOf(NullPointerException.class)
+                        .hasMessage("사용자의 성별는(은) Null이 될 수 없습니다.");
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 유효하지 않은 경우")
+        class value가_유효하지_않은_경우 {
+
+            @Test
+            @DisplayName("IllegalArgumentException을 발생시킨다.")
+            void 예외_발생() {
+                String gender = "invalid-gender";
+
+                assertThatThrownBy(() -> UserGender.create(gender))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("성별은 MALE 혹은 FEMALE이어야 합니다.");
+            }
+        }
+    }
+}
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
index 43a561f3f..d3027db68 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
@@ -18,9 +18,9 @@ public class JoinUserService {
 
     public User joinUser(JoinUserCommand command) {
         UserIdentifier userIdentifier = UserIdentifier.create(command.getUserIdentifier());
-        UserEmail userEmail = new UserEmail(command.getEmail());
-        UserBirthDay userBirthDay = new UserBirthDay(command.getBirthDay());
-        UserGender userGender = UserGender.valueOf(command.getGender());
+        UserEmail userEmail = UserEmail.create(command.getEmail());
+        UserBirthDay userBirthDay = UserBirthDay.create(command.getBirthDay());
+        UserGender userGender = UserGender.create(command.getGender());
 
         return userRepository.save(User.create(userIdentifier, userEmail, userBirthDay, userGender));
     }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/command/JoinUserCommand.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/command/JoinUserCommand.java
index 91e124c1d..379770ac0 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/command/JoinUserCommand.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/command/JoinUserCommand.java
@@ -2,8 +2,6 @@
 
 import lombok.Getter;
 
-import java.time.LocalDate;
-
 @Getter
 public class JoinUserCommand {
 
@@ -11,11 +9,11 @@ public class JoinUserCommand {
 
     private final String email;
 
-    private final LocalDate birthDay;
+    private final String birthDay;
 
     private final String gender;
 
-    public JoinUserCommand(String userIdentifier, String email, LocalDate birthDay, String gender) {
+    public JoinUserCommand(String userIdentifier, String email, String birthDay, String gender) {
         this.userIdentifier = userIdentifier;
         this.email = email;
         this.birthDay = birthDay;
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
new file mode 100644
index 000000000..f3ffa72c1
--- /dev/null
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
@@ -0,0 +1,8 @@
+package com.loopers.core.service.user;
+
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class JoinUserServiceIntegrationTest {
+
+}

From 6de75cce5ee96743480aa5d5e9e42fb62d6b1d38 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 28 Oct 2025 22:24:50 +0900
Subject: [PATCH 17/29] =?UTF-8?q?test=20:=20#1=20JoinUserService=20?=
 =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../user/repository/UserRepository.java       |  7 ++-
 .../mysql/mysql-core/build.gradle.kts         |  3 +-
 .../MySqlTestContainersExtension.java         | 31 ++++++++++++
 .../mysql/user/impl/UserRepositoryImpl.java   |  9 ++++
 core/service/user-service/build.gradle.kts    |  2 +
 .../loopers/UserServiceTestApplication.java   | 11 ++++
 .../loopers/core/service/IntegrationTest.java | 21 ++++++++
 .../user/JoinUserServiceIntegrationTest.java  | 50 +++++++++++++++++--
 .../src/test/resources/application-test.yml   |  4 ++
 9 files changed, 133 insertions(+), 5 deletions(-)
 create mode 100644 core/infra/database/mysql/mysql-core/src/testFixtures/java/com/loopers/core/infra/mysql/testcontainers/MySqlTestContainersExtension.java
 create mode 100644 core/service/user-service/src/test/java/com/loopers/UserServiceTestApplication.java
 create mode 100644 core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java
 create mode 100644 core/service/user-service/src/test/resources/application-test.yml

diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
index 1ef63b91c..71eb2d3a6 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
@@ -1,8 +1,13 @@
 package com.loopers.core.domain.user.repository;
 
 import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.vo.UserId;
+
+import java.util.Optional;
 
 public interface UserRepository {
-    
+
     User save(User user);
+
+    Optional<User> findById(UserId userId);
 }
diff --git a/core/infra/database/mysql/mysql-core/build.gradle.kts b/core/infra/database/mysql/mysql-core/build.gradle.kts
index 403433ffd..356e58fd4 100644
--- a/core/infra/database/mysql/mysql-core/build.gradle.kts
+++ b/core/infra/database/mysql/mysql-core/build.gradle.kts
@@ -8,8 +8,9 @@ dependencies {
     implementation("org.springframework.boot:spring-boot-starter-data-jpa")
     runtimeOnly("com.mysql:mysql-connector-j")
 
+    testFixturesImplementation("org.testcontainers:mysql")
+    testFixturesImplementation("org.springframework.boot:spring-boot-starter-test")
     testFixturesImplementation("org.testcontainers:testcontainers")
     testFixturesImplementation("org.springframework.boot:spring-boot-starter-data-jpa")
     testFixturesImplementation("org.testcontainers:junit-jupiter")
-    testFixturesImplementation("org.testcontainers:mariadb")
 }
diff --git a/core/infra/database/mysql/mysql-core/src/testFixtures/java/com/loopers/core/infra/mysql/testcontainers/MySqlTestContainersExtension.java b/core/infra/database/mysql/mysql-core/src/testFixtures/java/com/loopers/core/infra/mysql/testcontainers/MySqlTestContainersExtension.java
new file mode 100644
index 000000000..3432d6279
--- /dev/null
+++ b/core/infra/database/mysql/mysql-core/src/testFixtures/java/com/loopers/core/infra/mysql/testcontainers/MySqlTestContainersExtension.java
@@ -0,0 +1,31 @@
+package com.loopers.core.infra.mysql.testcontainers;
+
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.utility.DockerImageName;
+
+public class MySqlTestContainersExtension implements BeforeAllCallback {
+    private static final MySQLContainer<?> mySqlContainer;
+
+    static {
+        mySqlContainer = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
+                .withDatabaseName("loopers")
+                .withUsername("test")
+                .withPassword("test")
+                .withExposedPorts(3306)
+                .withCommand(
+                        "--character-set-server=utf8mb4",
+                        "--collation-server=utf8mb4_general_ci",
+                        "--skip-character-set-client-handshake"
+                );
+        mySqlContainer.start();
+    }
+
+    @Override
+    public void beforeAll(ExtensionContext context) {
+        System.setProperty("datasource.mysql-jpa.main.jdbc-url", mySqlContainer.getJdbcUrl());
+        System.setProperty("datasource.mysql-jpa.main.username", mySqlContainer.getUsername());
+        System.setProperty("datasource.mysql-jpa.main.password", mySqlContainer.getPassword());
+    }
+}
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
index bec17d1b4..9b933c044 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
@@ -2,11 +2,14 @@
 
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.infra.database.mysql.user.UserJpaRepository;
 import com.loopers.core.infra.database.mysql.user.entity.UserEntity;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Repository;
 
+import java.util.Optional;
+
 @Repository
 @RequiredArgsConstructor
 public class UserRepositoryImpl implements UserRepository {
@@ -17,4 +20,10 @@ public class UserRepositoryImpl implements UserRepository {
     public User save(User user) {
         return repository.save(UserEntity.from(user)).to();
     }
+
+    @Override
+    public Optional<User> findById(UserId userId) {
+        return repository.findById(Long.parseLong(userId.value()))
+                .map(UserEntity::to);
+    }
 }
diff --git a/core/service/user-service/build.gradle.kts b/core/service/user-service/build.gradle.kts
index 7b81f3693..411681ae0 100644
--- a/core/service/user-service/build.gradle.kts
+++ b/core/service/user-service/build.gradle.kts
@@ -6,4 +6,6 @@ dependencies {
 
     // mysql
     runtimeOnly(project(":core:infra:database:mysql:user-mysql"))
+    testImplementation(testFixtures(project(":core:infra:database:mysql:mysql-core")))
+    testImplementation(project(":core:infra:database:mysql:mysql-config"))
 }
diff --git a/core/service/user-service/src/test/java/com/loopers/UserServiceTestApplication.java b/core/service/user-service/src/test/java/com/loopers/UserServiceTestApplication.java
new file mode 100644
index 000000000..c092c3611
--- /dev/null
+++ b/core/service/user-service/src/test/java/com/loopers/UserServiceTestApplication.java
@@ -0,0 +1,11 @@
+package com.loopers;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class UserServiceTestApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(UserServiceTestApplication.class, args);
+    }
+}
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java
new file mode 100644
index 000000000..56ee3dd13
--- /dev/null
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java
@@ -0,0 +1,21 @@
+package com.loopers.core.service;
+
+import com.loopers.core.infra.mysql.testcontainers.MySqlTestContainersExtension;
+import com.loopers.core.infra.mysql.util.DatabaseCleanUp;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+@ExtendWith(MySqlTestContainersExtension.class)
+public class IntegrationTest {
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    @AfterEach
+    void databaseCleanUp() {
+        databaseCleanUp.truncateAllTables();
+    }
+}
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
index f3ffa72c1..02e88083b 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
@@ -1,8 +1,52 @@
 package com.loopers.core.service.user;
 
-import org.springframework.boot.test.context.SpringBootTest;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.service.IntegrationTest;
+import com.loopers.core.service.user.command.JoinUserCommand;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
 
-@SpringBootTest
-class JoinUserServiceIntegrationTest {
+import java.util.Optional;
+
+class JoinUserServiceIntegrationTest extends IntegrationTest {
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Autowired
+    private JoinUserService joinUserService;
+
+    @Nested
+    @DisplayName("회원가입 시")
+    class 회원_가입 {
+
+        @Nested
+        @DisplayName("유효한 경우")
+        class 유효한_경우 {
+
+            @Test
+            @DisplayName("회원이 저장된다.")
+            void 회원_저장() {
+                JoinUserCommand joinUserCommand = new JoinUserCommand(
+                        "user123",
+                        "kilian@gmail.com",
+                        "2000-01-15",
+                        "MALE"
+                );
+
+                User user = joinUserService.joinUser(joinUserCommand);
+                Optional<User> findUser = userRepository.findById(user.getUserId());
+
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(findUser.isPresent()).isTrue();
+                    softly.assertThat(findUser.get().getIdentifier().value()).isEqualTo(joinUserCommand.getUserIdentifier());
+                });
+            }
+        }
+    }
 
 }
diff --git a/core/service/user-service/src/test/resources/application-test.yml b/core/service/user-service/src/test/resources/application-test.yml
new file mode 100644
index 000000000..d0a6e1fcd
--- /dev/null
+++ b/core/service/user-service/src/test/resources/application-test.yml
@@ -0,0 +1,4 @@
+spring:
+  jpa:
+    hibernate:
+      ddl-auto: update

From f778cb6b548724e5901c79ab92f67f68f1e7432f Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 28 Oct 2025 22:41:55 +0900
Subject: [PATCH 18/29] =?UTF-8?q?feature=20:=20#1=20=EC=A1=B4=EC=9E=AC?=
 =?UTF-8?q?=ED=95=98=EB=8A=94=20ID=20=EA=B2=80=EC=A6=9D=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .codeguide/loopers-1-week.md                  | 10 +++---
 .../core/domain/error/DomainErrorCode.java    |  3 +-
 .../user/repository/UserRepository.java       |  3 ++
 .../mysql/user/UserJpaRepository.java         |  4 +++
 .../mysql/user/impl/UserRepositoryImpl.java   |  7 ++++
 .../core/service/user/JoinUserService.java    |  8 +++++
 .../user/JoinUserServiceIntegrationTest.java  | 32 +++++++++++++++++++
 7 files changed, 61 insertions(+), 6 deletions(-)

diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md
index a8ace53e5..5419d5c6d 100644
--- a/.codeguide/loopers-1-week.md
+++ b/.codeguide/loopers-1-week.md
@@ -6,14 +6,14 @@
 
 **🧱 단위 테스트**
 
-- [ ]  ID 가 `영문 및 숫자 10자 이내` 형식에 맞지 않으면, User 객체 생성에 실패한다.
-- [ ]  이메일이 `xx@yy.zz` 형식에 맞지 않으면, User 객체 생성에 실패한다.
-- [ ]  생년월일이 `yyyy-MM-dd` 형식에 맞지 않으면, User 객체 생성에 실패한다.
+- [x]  ID 가 `영문 및 숫자 10자 이내` 형식에 맞지 않으면, User 객체 생성에 실패한다.
+- [x]  이메일이 `xx@yy.zz` 형식에 맞지 않으면, User 객체 생성에 실패한다.
+- [x]  생년월일이 `yyyy-MM-dd` 형식에 맞지 않으면, User 객체 생성에 실패한다.
 
 **🔗 통합 테스트**
 
-- [ ]  회원 가입시 User 저장이 수행된다. ( spy 검증 )
-- [ ]  이미 가입된 ID 로 회원가입 시도 시, 실패한다.
+- [x]  회원 가입시 User 저장이 수행된다. ( spy 검증 )
+- [x]  이미 가입된 ID 로 회원가입 시도 시, 실패한다.
 
 **🌐 E2E 테스트**
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index b87dfdba9..aeddefd20 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -10,7 +10,8 @@ public enum DomainErrorCode {
     USER_ID_LENGTH_VALIDATE(1002, "사용자의 ID의 길이는 %d자 이상 %d자 이하여야 합니다."),
     INVALID_EMAIL_FORMAT(1003, "유효하지 않은 이메일 형식입니다."),
     INVALID_BIRTHDATE_FORMAT(1004, "생년월일은 yyyy-MM-dd 형식이어야 합니다."),
-    GENDER_IS_MALE_OR_FEMALE(1004, "성별은 MALE 혹은 FEMALE이어야 합니다.");
+    GENDER_IS_MALE_OR_FEMALE(1004, "성별은 MALE 혹은 FEMALE이어야 합니다."),
+    PRESENT_USER_IDENTIFIER(1005, "이미 존재하는 사용자 ID입니다.");
 
     private final int code;
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
index 71eb2d3a6..fb3c0ba2a 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
@@ -2,6 +2,7 @@
 
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.vo.UserId;
+import com.loopers.core.domain.user.vo.UserIdentifier;
 
 import java.util.Optional;
 
@@ -10,4 +11,6 @@ public interface UserRepository {
     User save(User user);
 
     Optional<User> findById(UserId userId);
+
+    Optional<User> findByIdentifier(UserIdentifier identifier);
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
index 101df6ba4..b84067f76 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
@@ -3,5 +3,9 @@
 import com.loopers.core.infra.database.mysql.user.entity.UserEntity;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.util.Optional;
+
 public interface UserJpaRepository extends JpaRepository<UserEntity, Long> {
+
+    Optional<UserEntity> findByIdentifier(String identifier);
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
index 9b933c044..ccd7950ce 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
@@ -3,6 +3,7 @@
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
 import com.loopers.core.domain.user.vo.UserId;
+import com.loopers.core.domain.user.vo.UserIdentifier;
 import com.loopers.core.infra.database.mysql.user.UserJpaRepository;
 import com.loopers.core.infra.database.mysql.user.entity.UserEntity;
 import lombok.RequiredArgsConstructor;
@@ -26,4 +27,10 @@ public Optional<User> findById(UserId userId) {
         return repository.findById(Long.parseLong(userId.value()))
                 .map(UserEntity::to);
     }
+
+    @Override
+    public Optional<User> findByIdentifier(UserIdentifier identifier) {
+        return repository.findByIdentifier(identifier.value())
+                .map(UserEntity::to);
+    }
 }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
index d3027db68..573b97c86 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
@@ -1,5 +1,6 @@
 package com.loopers.core.service.user;
 
+import com.loopers.core.domain.error.DomainErrorCode;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
 import com.loopers.core.domain.user.type.UserGender;
@@ -18,6 +19,13 @@ public class JoinUserService {
 
     public User joinUser(JoinUserCommand command) {
         UserIdentifier userIdentifier = UserIdentifier.create(command.getUserIdentifier());
+        boolean presentIdentifier = userRepository.findByIdentifier(userIdentifier)
+                .isPresent();
+
+        if (presentIdentifier) {
+            throw new IllegalArgumentException(DomainErrorCode.PRESENT_USER_IDENTIFIER.getMessage());
+        }
+
         UserEmail userEmail = UserEmail.create(command.getEmail());
         UserBirthDay userBirthDay = UserBirthDay.create(command.getBirthDay());
         UserGender userGender = UserGender.create(command.getGender());
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
index 02e88083b..41012abd2 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
@@ -2,9 +2,15 @@
 
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.type.UserGender;
+import com.loopers.core.domain.user.vo.UserBirthDay;
+import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserIdentifier;
 import com.loopers.core.service.IntegrationTest;
 import com.loopers.core.service.user.command.JoinUserCommand;
+import org.assertj.core.api.Assertions;
 import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -47,6 +53,32 @@ class 유효한_경우 {
                 });
             }
         }
+
+        @Nested
+        @DisplayName("이미 존재하는 회원의 ID인 경우")
+        class 이미_존재하는_회원의_ID인_경우 {
+
+            @BeforeEach
+            void setUp() {
+                userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+            }
+
+            @Test
+            @DisplayName("IllegalArgumentException이 발생한다.")
+            void 예외_발생() {
+                JoinUserCommand command = new JoinUserCommand("kilian", "ddd@gmail.com", "2000-01-15", "FEMALE");
+                Assertions.assertThatThrownBy(() -> joinUserService.joinUser(command))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("이미 존재하는 사용자 ID입니다.");
+            }
+        }
     }
 
 }

From 12bb883ed38bcb8830df74d41f1b7e48bfa61517 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 28 Oct 2025 23:12:40 +0900
Subject: [PATCH 19/29] =?UTF-8?q?test=20:=20#1=20End=20To=20End=20?=
 =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .codeguide/loopers-1-week.md                  |  4 +-
 apps/commerce-api/build.gradle.kts            |  3 +-
 .../api/error/ApiControllerAdvice.java        | 38 +++++---
 .../{UserV1Controller.java => UserV1Api.java} |  2 +-
 .../application/api/user/UserV1Dto.java       | 18 +++-
 .../application/api/ApiIntegrationTest.java   | 25 ++++++
 .../api/user/UserV1ApiApiIntegrationTest.java | 89 +++++++++++++++++++
 .../loopers/core/service/IntegrationTest.java |  2 +-
 8 files changed, 163 insertions(+), 18 deletions(-)
 rename apps/commerce-api/src/main/java/com/loopers/application/api/user/{UserV1Controller.java => UserV1Api.java} (95%)
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/api/ApiIntegrationTest.java
 create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java

diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md
index 5419d5c6d..07ac100c9 100644
--- a/.codeguide/loopers-1-week.md
+++ b/.codeguide/loopers-1-week.md
@@ -17,8 +17,8 @@
 
 **🌐 E2E 테스트**
 
-- [ ]  회원 가입이 성공할 경우, 생성된 유저 정보를 응답으로 반환한다.
-- [ ]  회원 가입 시에 성별이 없을 경우, `400 Bad Request` 응답을 반환한다.
+- [x]  회원 가입이 성공할 경우, 생성된 유저 정보를 응답으로 반환한다.
+- [x]  회원 가입 시에 성별이 없을 경우, `400 Bad Request` 응답을 반환한다.
 
 ### 내 정보 조회
 
diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index c7946a04a..a5f94f5ad 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -11,7 +11,7 @@ dependencies {
 
     //domain
     implementation(project(":core:domain"))
-    
+
     // web
     implementation("org.springframework.boot:spring-boot-starter-web")
     implementation("org.springframework.boot:spring-boot-starter-validation")
@@ -19,5 +19,6 @@ dependencies {
     implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")
 
     // test-fixtures
+    testImplementation(project(":core:infra:database:mysql:mysql-config"))
     testImplementation(testFixtures(project(":core:infra:database:mysql:mysql-core")))
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
index d569effed..c5a3015a9 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
@@ -7,6 +7,7 @@
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -37,6 +38,21 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentTypeMismatc
         return failureResponse(ApiErrorType.BAD_REQUEST, message);
     }
 
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentNotValidException e) {
+        String errorMessage = e.getBindingResult()
+                .getFieldErrors()
+                .stream()
+                .map(error -> String.format("필드 '%s': %s", error.getField(), error.getDefaultMessage()))
+                .collect(Collectors.joining(", "));
+
+        if (errorMessage.isEmpty()) {
+            errorMessage = "요청 데이터 검증에 실패했습니다.";
+        }
+
+        return failureResponse(ApiErrorType.BAD_REQUEST, errorMessage);
+    }
+
     @ExceptionHandler
     public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingServletRequestParameterException e) {
         String name = e.getParameterName();
@@ -52,15 +68,15 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableExc
 
         if (rootCause instanceof InvalidFormatException invalidFormat) {
             String fieldName = invalidFormat.getPath().stream()
-                .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
-                .collect(Collectors.joining("."));
+                    .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
+                    .collect(Collectors.joining("."));
 
             String valueIndicationMessage = "";
             if (invalidFormat.getTargetType().isEnum()) {
                 Class<?> enumClass = invalidFormat.getTargetType();
                 String enumValues = Arrays.stream(enumClass.getEnumConstants())
-                    .map(Object::toString)
-                    .collect(Collectors.joining(", "));
+                        .map(Object::toString)
+                        .collect(Collectors.joining(", "));
                 valueIndicationMessage = "사용 가능한 값 : [" + enumValues + "]";
             }
 
@@ -68,20 +84,20 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableExc
             Object value = invalidFormat.getValue();
 
             errorMessage = String.format("필드 '%s'의 값 '%s'이(가) 예상 타입(%s)과 일치하지 않습니다. %s",
-                fieldName, value, expectedType, valueIndicationMessage);
+                    fieldName, value, expectedType, valueIndicationMessage);
 
         } else if (rootCause instanceof MismatchedInputException mismatchedInput) {
             String fieldPath = mismatchedInput.getPath().stream()
-                .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
-                .collect(Collectors.joining("."));
+                    .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
+                    .collect(Collectors.joining("."));
             errorMessage = String.format("필수 필드 '%s'이(가) 누락되었습니다.", fieldPath);
 
         } else if (rootCause instanceof JsonMappingException jsonMapping) {
             String fieldPath = jsonMapping.getPath().stream()
-                .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
-                .collect(Collectors.joining("."));
+                    .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?")
+                    .collect(Collectors.joining("."));
             errorMessage = String.format("필드 '%s'에서 JSON 매핑 오류가 발생했습니다: %s",
-                fieldPath, jsonMapping.getOriginalMessage());
+                    fieldPath, jsonMapping.getOriginalMessage());
 
         } else {
             errorMessage = "요청 본문을 처리하는 중 오류가 발생했습니다. JSON 메세지 규격을 확인해주세요.";
@@ -120,6 +136,6 @@ private String extractMissingParameter(String message) {
 
     private ResponseEntity<ApiResponse<?>> failureResponse(ApiErrorType apiErrorType, String errorMessage) {
         return ResponseEntity.status(apiErrorType.getStatus())
-            .body(ApiResponse.fail(apiErrorType.getCode(), errorMessage != null ? errorMessage : apiErrorType.getMessage()));
+                .body(ApiResponse.fail(apiErrorType.getCode(), errorMessage != null ? errorMessage : apiErrorType.getMessage()));
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
similarity index 95%
rename from apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
rename to apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
index a30d5dc5b..e709fc6aa 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Controller.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
@@ -16,7 +16,7 @@
 @RestController
 @RequiredArgsConstructor
 @RequestMapping("/api/v1/users")
-public class UserV1Controller implements UserV1ApiSpec {
+public class UserV1Api implements UserV1ApiSpec {
 
     private final JoinUserService joinUserService;
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
index 24fabb722..90db4c5cb 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -4,6 +4,8 @@
 import com.loopers.core.service.user.command.JoinUserCommand;
 import jakarta.validation.constraints.NotBlank;
 
+import java.time.format.DateTimeFormatter;
+
 public class UserV1Dto {
 
     public record JoinUserRequest(
@@ -21,10 +23,22 @@ public record GetUserResponse(String userId) {
 
     }
 
-    public record JoinUserResponse(String userId) {
+    public record JoinUserResponse(
+            String userId,
+            String identifier,
+            String email,
+            String birthday,
+            String gender
+    ) {
 
         public static JoinUserResponse from(User user) {
-            return new JoinUserResponse(user.getUserId().value());
+            return new JoinUserResponse(
+                    user.getUserId().value(),
+                    user.getIdentifier().value(),
+                    user.getEmail().value(),
+                    user.getBirthDay().value().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
+                    user.getGender().name()
+            );
         }
     }
 }
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/ApiIntegrationTest.java
new file mode 100644
index 000000000..1021784c5
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/ApiIntegrationTest.java
@@ -0,0 +1,25 @@
+package com.loopers.application.api;
+
+import com.loopers.core.infra.mysql.testcontainers.MySqlTestContainersExtension;
+import com.loopers.core.infra.mysql.util.DatabaseCleanUp;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ExtendWith(MySqlTestContainersExtension.class)
+public class ApiIntegrationTest {
+
+    @Autowired
+    private TestRestTemplate testRestTemplate;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    @AfterEach
+    void databaseCleanUp() {
+        databaseCleanUp.truncateAllTables();
+    }
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
new file mode 100644
index 000000000..844a230b2
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
@@ -0,0 +1,89 @@
+package com.loopers.application.api.user;
+
+import com.loopers.application.api.ApiIntegrationTest;
+import com.loopers.application.api.common.dto.ApiResponse;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class UserV1ApiApiIntegrationTest extends ApiIntegrationTest {
+
+    @Autowired
+    private TestRestTemplate testRestTemplate;
+
+    @Nested
+    @DisplayName("회원가입")
+    class 회원가입 {
+
+        @Nested
+        @DisplayName("정상 요청인 경우")
+        class 정상_요청인_경우 {
+
+            @Test
+            @DisplayName("생성된 유저의 정보를 반환한다.")
+            void 생성된_유저의_정보를_반환한다() {
+                // given
+                UserV1Dto.JoinUserRequest request = new UserV1Dto.JoinUserRequest(
+                        "user123",
+                        "user@example.com",
+                        "2000-01-15",
+                        "MALE"
+                );
+
+                String endPoint = "/api/v1/users/join";
+                ParameterizedTypeReference<ApiResponse<UserV1Dto.JoinUserResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                // when
+                ResponseEntity<ApiResponse<UserV1Dto.JoinUserResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, new HttpEntity<>(request), responseType);
+
+                // then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                assertThat(response.getBody()).isNotNull();
+                assertThat(response.getBody().data()).isNotNull();
+                assertThat(response.getBody().data().identifier()).isEqualTo("user123");
+                assertThat(response.getBody().data().email()).isEqualTo("user@example.com");
+                assertThat(response.getBody().data().birthday()).isEqualTo("2000-01-15");
+                assertThat(response.getBody().data().gender()).isEqualTo("MALE");
+            }
+        }
+
+        @Nested
+        @DisplayName("성별이 없는 경우")
+        class 성별이_없는_경우 {
+
+            @Test
+            @DisplayName("400 Bad Request 응답을 반환한다.")
+            void badRequest응답을_반환한다() {
+                // given
+                UserV1Dto.JoinUserRequest request = new UserV1Dto.JoinUserRequest(
+                        "user123",
+                        "user@example.com",
+                        "2000-01-15",
+                        null
+                );
+
+                String endPoint = "/api/v1/users/join";
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, new HttpEntity<>(request), responseType);
+
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+            }
+        }
+    }
+}
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java
index 56ee3dd13..56e0ec323 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java
@@ -7,7 +7,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
 @ExtendWith(MySqlTestContainersExtension.class)
 public class IntegrationTest {
 

From 6d9a80b550e2040d1a674f1453d2f358b3bfb68b Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 28 Oct 2025 23:31:28 +0900
Subject: [PATCH 20/29] =?UTF-8?q?feature=20:=20#1=20=ED=9A=8C=EC=9B=90=20?=
 =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .codeguide/loopers-1-week.md                  |  4 +-
 .../api/user/UserV1ApiApiIntegrationTest.java |  2 +
 .../core/service/user/UserQueryService.java   | 20 +++++
 .../core/service/user/query/GetUserQuery.java | 13 ++++
 .../loopers/core/service/IntegrationTest.java |  2 +-
 .../service/user/UserQueryServiceTest.java    | 74 +++++++++++++++++++
 6 files changed, 112 insertions(+), 3 deletions(-)
 create mode 100644 core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java
 create mode 100644 core/service/user-service/src/main/java/com/loopers/core/service/user/query/GetUserQuery.java
 create mode 100644 core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java

diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md
index 07ac100c9..34fa53b3b 100644
--- a/.codeguide/loopers-1-week.md
+++ b/.codeguide/loopers-1-week.md
@@ -24,8 +24,8 @@
 
 **🔗 통합 테스트**
 
-- [ ]  해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.
-- [ ]  해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.
+- [x]  해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.
+- [x]  해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.
 
 **🌐 E2E 테스트**
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
index 844a230b2..259bedff3 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
@@ -9,8 +9,10 @@
 import org.springframework.boot.test.web.client.TestRestTemplate;
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 
 import static org.assertj.core.api.Assertions.assertThat;
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java
new file mode 100644
index 000000000..812005e24
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java
@@ -0,0 +1,20 @@
+package com.loopers.core.service.user;
+
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.user.query.GetUserQuery;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class UserQueryService {
+
+    private final UserRepository userRepository;
+
+    public User getUserBy(GetUserQuery query) {
+        return userRepository.findByIdentifier(new UserIdentifier(query.getIdentifier()))
+                .orElse(null);
+    }
+}
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/query/GetUserQuery.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/query/GetUserQuery.java
new file mode 100644
index 000000000..a1024aa72
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/query/GetUserQuery.java
@@ -0,0 +1,13 @@
+package com.loopers.core.service.user.query;
+
+import lombok.Getter;
+
+@Getter
+public class GetUserQuery {
+
+    private final String identifier;
+
+    public GetUserQuery(String identifier) {
+        this.identifier = identifier;
+    }
+}
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java
index 56e0ec323..56ee3dd13 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/IntegrationTest.java
@@ -7,7 +7,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@SpringBootTest
 @ExtendWith(MySqlTestContainersExtension.class)
 public class IntegrationTest {
 
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java
new file mode 100644
index 000000000..da626d556
--- /dev/null
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java
@@ -0,0 +1,74 @@
+package com.loopers.core.service.user;
+
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.type.UserGender;
+import com.loopers.core.domain.user.vo.UserBirthDay;
+import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.IntegrationTest;
+import com.loopers.core.service.user.query.GetUserQuery;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+class UserQueryServiceTest extends IntegrationTest {
+
+    @Autowired
+    private UserQueryService userQueryService;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Nested
+    @DisplayName("getUserBy()")
+    class GetUserBy {
+
+        @Nested
+        @DisplayName("ID에 해당하는 사용자가 없는 경우")
+        class ID에_해당하는_사용자가_없는_경우 {
+
+            @Test
+            @DisplayName("null이 반환된다.")
+            void null이_반환된다() {
+                User user = userQueryService.getUserBy(new GetUserQuery("identifier"));
+                Assertions.assertThat(user).isNull();
+            }
+        }
+
+        @Nested
+        @DisplayName("ID에 해당하는 사용자가 존재할 경우")
+        class ID에_해당하는_사용자가_존재할_경우 {
+
+            @BeforeEach
+            void setUp() {
+                userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+            }
+
+            @Test
+            @DisplayName("회원 정보가 반환된다.")
+            void 회원_정보가_반환된다() {
+                User user = userQueryService.getUserBy(new GetUserQuery("kilian"));
+
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(user).isNotNull();
+                    softly.assertThat(user.getIdentifier().value()).isEqualTo("kilian");
+                    softly.assertThat(user.getEmail().value()).isEqualTo("kilian@gmail.com");
+                    softly.assertThat(user.getGender()).isEqualTo(UserGender.MALE);
+                });
+            }
+        }
+
+    }
+}

From 5958f848bea3a7f9448787b9dbee0a9e55391272 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Tue, 28 Oct 2025 23:51:05 +0900
Subject: [PATCH 21/29] =?UTF-8?q?feature=20:=20#1=20=ED=9A=8C=EC=9B=90=20?=
 =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20Eto?=
 =?UTF-8?q?E=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../api/error/ApiControllerAdvice.java        |  6 ++
 .../application/api/user/UserV1Api.java       | 26 ++++--
 .../application/api/user/UserV1ApiSpec.java   | 17 ++--
 .../application/api/user/UserV1Dto.java       | 17 +++-
 .../api/user/UserV1ApiApiIntegrationTest.java | 84 ++++++++++++++++++-
 .../core/domain/error/DomainErrorCode.java    |  7 +-
 .../core/domain/error/NotFoundException.java  | 12 +++
 7 files changed, 150 insertions(+), 19 deletions(-)
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/error/NotFoundException.java

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
index c5a3015a9..87b11aa1a 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
@@ -4,6 +4,7 @@
 import com.fasterxml.jackson.databind.exc.InvalidFormatException;
 import com.fasterxml.jackson.databind.exc.MismatchedInputException;
 import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.error.NotFoundException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
@@ -128,6 +129,11 @@ public ResponseEntity<ApiResponse<?>> handle(Throwable e) {
         return failureResponse(ApiErrorType.INTERNAL_ERROR, null);
     }
 
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleNotFound(NotFoundException e) {
+        return failureResponse(ApiErrorType.NOT_FOUND, e.getMessage());
+    }
+
     private String extractMissingParameter(String message) {
         Pattern pattern = Pattern.compile("'(.+?)'");
         Matcher matcher = pattern.matcher(message);
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
index e709fc6aa..a2a355f79 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
@@ -1,17 +1,18 @@
 package com.loopers.application.api.user;
 
 import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.error.NotFoundException;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.service.user.JoinUserService;
+import com.loopers.core.service.user.UserQueryService;
+import com.loopers.core.service.user.query.GetUserQuery;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
-import static com.loopers.application.api.user.UserV1Dto.JoinUserRequest;
-import static com.loopers.application.api.user.UserV1Dto.JoinUserResponse;
+import java.util.Objects;
+
+import static com.loopers.application.api.user.UserV1Dto.*;
 
 @RestController
 @RequiredArgsConstructor
@@ -19,6 +20,18 @@
 public class UserV1Api implements UserV1ApiSpec {
 
     private final JoinUserService joinUserService;
+    private final UserQueryService userQueryService;
+
+    @Override
+    @GetMapping("/{identifier}")
+    public ApiResponse<GetUserResponse> getUser(@PathVariable String identifier) {
+        User user = userQueryService.getUserBy(new GetUserQuery(identifier));
+        if (Objects.isNull(user)) {
+            throw NotFoundException.withName("사용자");
+        }
+
+        return ApiResponse.success(GetUserResponse.from(user));
+    }
 
     @Override
     @PostMapping("/join")
@@ -26,7 +39,6 @@ public ApiResponse<JoinUserResponse> joinUser(
             @RequestBody @Valid JoinUserRequest request
     ) {
         User user = joinUserService.joinUser(request.toCommand());
-
         return ApiResponse.success(JoinUserResponse.from(user));
     }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
index bed1841cc..6359ce7b9 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
@@ -2,6 +2,7 @@
 
 import com.loopers.application.api.common.dto.ApiResponse;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.tags.Tag;
 
 import static com.loopers.application.api.user.UserV1Dto.JoinUserRequest;
@@ -10,14 +11,14 @@
 @Tag(name = "User V1 API", description = "User API 입니다.")
 public interface UserV1ApiSpec {
 
-//    @Operation(
-//            summary = "사용자 조회",
-//            description = "ID로 사용자를 조회합니다."
-//    )
-//    ApiResponse<UserV1Dto.GetUserResponse> getUser(
-//            @Schema(name = "사용자 ID", description = "조회할 사용자 ID")
-//            Long userId
-//    );
+    @Operation(
+            summary = "사용자 조회",
+            description = "ID로 사용자를 조회합니다."
+    )
+    ApiResponse<UserV1Dto.GetUserResponse> getUser(
+            @Schema(name = "사용자 ID", description = "조회할 사용자 ID")
+            String identifier
+    );
 
     @Operation(
             summary = "사용자 회원가입",
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
index 90db4c5cb..9e4bc3870 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -19,8 +19,23 @@ public JoinUserCommand toCommand() {
         }
     }
 
-    public record GetUserResponse(String userId) {
+    public record GetUserResponse(
+            String userId,
+            String identifier,
+            String email,
+            String birthday,
+            String gender
+    ) {
 
+        public static GetUserResponse from(User user) {
+            return new GetUserResponse(
+                    user.getUserId().value(),
+                    user.getIdentifier().value(),
+                    user.getEmail().value(),
+                    user.getBirthDay().value().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
+                    user.getGender().name()
+            );
+        }
     }
 
     public record JoinUserResponse(
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
index 259bedff3..889c1b778 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
@@ -2,6 +2,13 @@
 
 import com.loopers.application.api.ApiIntegrationTest;
 import com.loopers.application.api.common.dto.ApiResponse;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.type.UserGender;
+import com.loopers.core.domain.user.vo.UserBirthDay;
+import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -9,19 +16,21 @@
 import org.springframework.boot.test.web.client.TestRestTemplate;
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
 
 class UserV1ApiApiIntegrationTest extends ApiIntegrationTest {
 
     @Autowired
     private TestRestTemplate testRestTemplate;
 
+    @Autowired
+    private UserRepository userRepository;
+
     @Nested
     @DisplayName("회원가입")
     class 회원가입 {
@@ -88,4 +97,75 @@ class 성별이_없는_경우 {
             }
         }
     }
+
+    @Nested
+    @DisplayName("내 정보 조회")
+    class 내_정보_조회 {
+
+        @Nested
+        @DisplayName("성공할 경우")
+        class 성공할_경우 {
+
+            @BeforeEach
+            void setUp() {
+                userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+            }
+
+            @Test
+            @DisplayName("해당하는 유저 정보를 응답으로 반환한다.")
+            void 해당하는_유저_정보를_응답으로_반환한다() {
+                // given
+                String identifier = "kilian";
+                String endPoint = "/api/v1/users/" + identifier;
+                ParameterizedTypeReference<ApiResponse<UserV1Dto.GetUserResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                // when
+                ResponseEntity<ApiResponse<UserV1Dto.GetUserResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                // then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                assertSoftly(softly -> {
+                    softly.assertThat(response.getBody()).isNotNull();
+                    softly.assertThat(response.getBody().data()).isNotNull();
+                    softly.assertThat(response.getBody().data().identifier()).isEqualTo("kilian");
+                    softly.assertThat(response.getBody().data().email()).isEqualTo("kilian@gmail.com");
+                    softly.assertThat(response.getBody().data().birthday()).isEqualTo("1997-10-08");
+                    softly.assertThat(response.getBody().data().gender()).isEqualTo("MALE");
+                });
+            }
+        }
+
+        @Nested
+        @DisplayName("존재하지 않는 ID로 조회할 경우")
+        class 존재하지_않는_ID로_조회할_경우 {
+
+            @Test
+            @DisplayName("404 Not Found 응답을 반환한다.")
+            void notFound응답을_반환한다() {
+                // given
+                String identifier = "nonexistent";
+                String endPoint = "/api/v1/users/" + identifier;
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                // when
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                // then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index aeddefd20..e7b0605a8 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -11,7 +11,8 @@ public enum DomainErrorCode {
     INVALID_EMAIL_FORMAT(1003, "유효하지 않은 이메일 형식입니다."),
     INVALID_BIRTHDATE_FORMAT(1004, "생년월일은 yyyy-MM-dd 형식이어야 합니다."),
     GENDER_IS_MALE_OR_FEMALE(1004, "성별은 MALE 혹은 FEMALE이어야 합니다."),
-    PRESENT_USER_IDENTIFIER(1005, "이미 존재하는 사용자 ID입니다.");
+    PRESENT_USER_IDENTIFIER(1005, "이미 존재하는 사용자 ID입니다."),
+    NOT_FOUND(1006, "%s를 찾지 못했습니다.");
 
     private final int code;
 
@@ -25,4 +26,8 @@ public enum DomainErrorCode {
     public static String notNullMessage(String fieldName) {
         return String.format(MUST_BE_NOT_NULL.message, fieldName);
     }
+
+    public static String notFoundMessage(String fieldName) {
+        return String.format(MUST_BE_NOT_NULL.message, fieldName);
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/NotFoundException.java b/core/domain/src/main/java/com/loopers/core/domain/error/NotFoundException.java
new file mode 100644
index 000000000..516d18e86
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/NotFoundException.java
@@ -0,0 +1,12 @@
+package com.loopers.core.domain.error;
+
+public class NotFoundException extends RuntimeException {
+
+    public NotFoundException(String message) {
+        super(message);
+    }
+
+    public static NotFoundException withName(String name) {
+        return new NotFoundException(DomainErrorCode.notFoundMessage(name));
+    }
+}

From 657bbe9754e697e9b3ee401b50f7b2b9f997433d Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Wed, 29 Oct 2025 21:31:40 +0900
Subject: [PATCH 22/29] =?UTF-8?q?feature=20:=20#1=20=EC=9C=A0=EC=A0=80=20?=
 =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=A0=80=EC=9E=A5=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .codeguide/loopers-1-week.md                  |   4 +-
 .../com/loopers/CommerceApiContextTest.java   |  14 ---
 core/common/build.gradle.kts                  |   2 +
 .../core/domain/error/DomainErrorCode.java    |   3 +-
 .../loopers/core/domain/user/UserPoint.java   |  56 +++++++++
 .../user/repository/UserPointRepository.java  |   8 ++
 .../core/domain/user/vo/UserPointBalance.java |  18 +++
 .../core/domain/user/vo/UserPointId.java      |   8 ++
 .../core/domain/user/vo/UserBirthDayTest.java | 107 ++++++++++++++++++
 .../domain/user/vo/UserPointBalanceTest.java  |  75 ++++++++++++
 .../mysql/user/UserPointJpaRepository.java    |   7 ++
 .../mysql/user/entity/UserPointEntity.java    |  63 +++++++++++
 .../user/impl/UserPointRepositoryImpl.java    |  20 ++++
 core/service/user-service/build.gradle.kts    |   3 +-
 .../core/service/user/JoinUserService.java    |  10 +-
 15 files changed, 379 insertions(+), 19 deletions(-)
 delete mode 100644 apps/commerce-api/src/test/java/com/loopers/CommerceApiContextTest.java
 create mode 100644 core/common/build.gradle.kts
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
 create mode 100644 core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointId.java
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/user/vo/UserBirthDayTest.java
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/user/vo/UserPointBalanceTest.java
 create mode 100644 core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
 create mode 100644 core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java
 create mode 100644 core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java

diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md
index 34fa53b3b..c28ee618f 100644
--- a/.codeguide/loopers-1-week.md
+++ b/.codeguide/loopers-1-week.md
@@ -29,8 +29,8 @@
 
 **🌐 E2E 테스트**
 
-- [ ]  내 정보 조회에 성공할 경우, 해당하는 유저 정보를 응답으로 반환한다.
-- [ ]  존재하지 않는 ID 로 조회할 경우, `404 Not Found` 응답을 반환한다.
+- [x]  내 정보 조회에 성공할 경우, 해당하는 유저 정보를 응답으로 반환한다.
+- [x]  존재하지 않는 ID 로 조회할 경우, `404 Not Found` 응답을 반환한다.
 
 ### 포인트 조회
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/CommerceApiContextTest.java b/apps/commerce-api/src/test/java/com/loopers/CommerceApiContextTest.java
deleted file mode 100644
index a485bafe8..000000000
--- a/apps/commerce-api/src/test/java/com/loopers/CommerceApiContextTest.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.loopers;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class CommerceApiContextTest {
-
-    @Test
-    void contextLoads() {
-        // 이 테스트는 Spring Boot 애플리케이션 컨텍스트가 로드되는지 확인합니다.
-        // 모든 빈이 올바르게 로드되었는지 확인하는 데 사용됩니다.
-    }
-}
diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts
new file mode 100644
index 000000000..7d82dc72f
--- /dev/null
+++ b/core/common/build.gradle.kts
@@ -0,0 +1,2 @@
+dependencies {
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index e7b0605a8..13248f6a0 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -12,7 +12,8 @@ public enum DomainErrorCode {
     INVALID_BIRTHDATE_FORMAT(1004, "생년월일은 yyyy-MM-dd 형식이어야 합니다."),
     GENDER_IS_MALE_OR_FEMALE(1004, "성별은 MALE 혹은 FEMALE이어야 합니다."),
     PRESENT_USER_IDENTIFIER(1005, "이미 존재하는 사용자 ID입니다."),
-    NOT_FOUND(1006, "%s를 찾지 못했습니다.");
+    NOT_FOUND(1006, "%s를 찾지 못했습니다."),
+    USER_POINT_BALANCE_GRATER_THEN_ZERO(1007, "사용자 포인트의 잔액은 0보다는 커야합니다.");
 
     private final int code;
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java b/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
new file mode 100644
index 000000000..fda580676
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
@@ -0,0 +1,56 @@
+package com.loopers.core.domain.user;
+
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.user.vo.UserId;
+import com.loopers.core.domain.user.vo.UserPointBalance;
+import com.loopers.core.domain.user.vo.UserPointId;
+import lombok.Getter;
+
+@Getter
+public class UserPoint {
+
+    private final UserPointId id;
+
+    private final UserId userId;
+
+    private final UserPointBalance balance;
+
+    private final CreatedAt createdAt;
+
+    private final UpdatedAt updatedAt;
+
+    private UserPoint(
+            UserPointId id,
+            UserId userId,
+            UserPointBalance balance,
+            CreatedAt createdAt,
+            UpdatedAt updatedAt
+    ) {
+        this.id = id;
+        this.userId = userId;
+        this.balance = balance;
+        this.createdAt = createdAt;
+        this.updatedAt = updatedAt;
+    }
+
+    public static UserPoint create(UserId userId) {
+        return new UserPoint(
+                UserPointId.empty(),
+                userId,
+                UserPointBalance.init(),
+                CreatedAt.now(),
+                UpdatedAt.now()
+        );
+    }
+
+    public static UserPoint mappedBy(
+            UserPointId id,
+            UserId userId,
+            UserPointBalance balance,
+            CreatedAt createdAt,
+            UpdatedAt updatedAt
+    ) {
+        return new UserPoint(id, userId, balance, createdAt, updatedAt);
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
new file mode 100644
index 000000000..ec0a0adeb
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
@@ -0,0 +1,8 @@
+package com.loopers.core.domain.user.repository;
+
+import com.loopers.core.domain.user.UserPoint;
+
+public interface UserPointRepository {
+
+    UserPoint save(UserPoint userPoint);
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
new file mode 100644
index 000000000..8be25d7d2
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
@@ -0,0 +1,18 @@
+package com.loopers.core.domain.user.vo;
+
+import static com.loopers.core.domain.error.DomainErrorCode.USER_POINT_BALANCE_GRATER_THEN_ZERO;
+
+public record UserPointBalance(int value) {
+
+    public UserPointBalance(int value) {
+        if (value < 0) {
+            throw new IllegalArgumentException(USER_POINT_BALANCE_GRATER_THEN_ZERO.getMessage());
+        }
+
+        this.value = value;
+    }
+
+    public static UserPointBalance init() {
+        return new UserPointBalance(0);
+    }
+}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointId.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointId.java
new file mode 100644
index 000000000..c7a0d3841
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointId.java
@@ -0,0 +1,8 @@
+package com.loopers.core.domain.user.vo;
+
+public record UserPointId(String value) {
+
+    public static UserPointId empty() {
+        return new UserPointId(null);
+    }
+}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserBirthDayTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserBirthDayTest.java
new file mode 100644
index 000000000..b3b0f59db
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserBirthDayTest.java
@@ -0,0 +1,107 @@
+package com.loopers.core.domain.user.vo;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.time.LocalDate;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("UserBirthDay")
+class UserBirthDayTest {
+
+    @Nested
+    @DisplayName("create() 메서드")
+    class CreateMethod {
+
+        @Nested
+        @DisplayName("유효한 값인 경우")
+        class 유효한_값인_경우 {
+
+            @Test
+            @DisplayName("UserBirthDay 객체를 생성한다")
+            void 객체_생성() {
+                // given
+                String validBirthDay = "2000-01-15";
+
+                // when
+                UserBirthDay birthDay = UserBirthDay.create(validBirthDay);
+
+                // then
+                assertThat(birthDay.value()).isEqualTo(LocalDate.parse(validBirthDay));
+            }
+
+            @ParameterizedTest
+            @MethodSource("validBirthDays")
+            @DisplayName("유효한 yyyy-MM-dd 형식의 생년월일로 생성한다")
+            void 유효한_생년월일(String dateString) {
+                // when
+                UserBirthDay birthDay = UserBirthDay.create(dateString);
+
+                // then
+                assertThat(birthDay.value()).isEqualTo(LocalDate.parse(dateString));
+            }
+
+            static Stream<String> validBirthDays() {
+                return Stream.of(
+                        "1990-01-01",
+                        "2000-12-31",
+                        "2023-06-15",
+                        "1980-02-29"  // 윤년
+                );
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 null인 경우")
+        class value가_null인_경우 {
+
+            @Test
+            @DisplayName("NullPointerException을 발생시킨다")
+            void 예외_발생() {
+                // given
+                String dateString = null;
+
+                // when & then
+                assertThatThrownBy(() -> UserBirthDay.create(dateString))
+                        .isInstanceOf(NullPointerException.class)
+                        .hasMessage("생년월일는(은) Null이 될 수 없습니다.");
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 yyyy-MM-dd 형식이 아닌 경우")
+        class value가_형식이_아닌_경우 {
+
+            @ParameterizedTest
+            @MethodSource("invalidBirthDays")
+            @DisplayName("예외를 발생시킨다")
+            void 예외_발생(String dateString) {
+                // when & then
+                assertThatThrownBy(() -> UserBirthDay.create(dateString))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("생년월일은 yyyy-MM-dd 형식이어야 합니다.");
+            }
+
+            static Stream<String> invalidBirthDays() {
+                return Stream.of(
+                        "2000-1-15",                // MM이 1자
+                        "2000-01-5",                // dd가 1자
+                        "00-01-15",                 // yyyy가 2자
+                        "2000/01/15",               // / 사용
+                        "2000.01.15",               // . 사용
+                        "20000115",                 // 구분자 없음
+                        "01-15-2000",               // MM-dd-yyyy 순서
+                        "invalid-date",             // 숫자 아님
+                        "2000-13-01",               // 월 범위 초과
+                        "2000-01-32"                // 일 범위 초과
+                );
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserPointBalanceTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserPointBalanceTest.java
new file mode 100644
index 000000000..9e1387383
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserPointBalanceTest.java
@@ -0,0 +1,75 @@
+package com.loopers.core.domain.user.vo;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("UserPointBalance")
+class UserPointBalanceTest {
+
+    @Nested
+    @DisplayName("생성자")
+    class Constructor {
+
+        @Nested
+        @DisplayName("유효한 값인 경우")
+        class 유효한_값인_경우 {
+
+            @ParameterizedTest
+            @MethodSource("validPoints")
+            @DisplayName("UserPointBalance 객체를 생성한다")
+            void 객체_생성(int validPoint) {
+                // when
+                UserPointBalance pointBalance = new UserPointBalance(validPoint);
+
+                // then
+                assertThat(pointBalance.value()).isEqualTo(validPoint);
+            }
+
+            static Stream<Integer> validPoints() {
+                return Stream.of(0, 1, 100, 10000, Integer.MAX_VALUE);
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 음수인 경우")
+        class value가_음수인_경우 {
+
+            @ParameterizedTest
+            @MethodSource("invalidPoints")
+            @DisplayName("예외를 발생시킨다")
+            void 예외_발생(int invalidPoint) {
+                // when & then
+                assertThatThrownBy(() -> new UserPointBalance(invalidPoint))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("사용자 포인트의 잔액은 0보다는 커야합니다.");
+            }
+
+            static Stream<Integer> invalidPoints() {
+                return Stream.of(-1, -100, -10000, Integer.MIN_VALUE);
+            }
+        }
+    }
+
+    @Nested
+    @DisplayName("init() 메서드")
+    class InitMethod {
+
+        @Test
+        @DisplayName("포인트가 0인 UserPointBalance를 생성한다")
+        void 초기_포인트_생성() {
+            // when
+            UserPointBalance pointBalance = UserPointBalance.init();
+
+            // then
+            assertThat(pointBalance.value()).isZero();
+        }
+    }
+}
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
new file mode 100644
index 000000000..a8a99875d
--- /dev/null
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
@@ -0,0 +1,7 @@
+package com.loopers.core.infra.database.mysql.user;
+
+import com.loopers.core.infra.database.mysql.user.entity.UserPointEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface UserPointJpaRepository extends JpaRepository<UserPointEntity, Long> {
+}
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java
new file mode 100644
index 000000000..a5692f836
--- /dev/null
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java
@@ -0,0 +1,63 @@
+package com.loopers.core.infra.database.mysql.user.entity;
+
+import com.loopers.core.domain.common.vo.CreatedAt;
+import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.user.UserPoint;
+import com.loopers.core.domain.user.vo.UserId;
+import com.loopers.core.domain.user.vo.UserPointBalance;
+import com.loopers.core.domain.user.vo.UserPointId;
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+@Entity
+@Table(
+        name = "user_point",
+        indexes = {
+                @Index(name = "idx_user_point_user_id", columnList = "user_id")
+        }
+)
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+@Builder(access = AccessLevel.PRIVATE)
+public class UserPointEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    private Long userId;
+
+    private int balance;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime updatedAt;
+
+    public static UserPointEntity from(UserPoint userPoint) {
+        return new UserPointEntity(
+                Optional.ofNullable(userPoint.getId().value())
+                        .map(Long::parseLong)
+                        .orElse(null),
+                Long.parseLong(userPoint.getUserId().value()),
+                userPoint.getBalance().value(),
+                userPoint.getCreatedAt().value(),
+                userPoint.getUpdatedAt().value()
+        );
+    }
+
+    public UserPoint to() {
+        return UserPoint.mappedBy(
+                new UserPointId(this.id.toString()),
+                new UserId(this.userId.toString()),
+                new UserPointBalance(this.balance),
+                new CreatedAt(this.createdAt),
+                new UpdatedAt(this.updatedAt)
+        );
+    }
+}
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
new file mode 100644
index 000000000..e99e039ad
--- /dev/null
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
@@ -0,0 +1,20 @@
+package com.loopers.core.infra.database.mysql.user.impl;
+
+import com.loopers.core.domain.user.UserPoint;
+import com.loopers.core.domain.user.repository.UserPointRepository;
+import com.loopers.core.infra.database.mysql.user.UserPointJpaRepository;
+import com.loopers.core.infra.database.mysql.user.entity.UserPointEntity;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class UserPointRepositoryImpl implements UserPointRepository {
+
+    private final UserPointJpaRepository repository;
+
+    @Override
+    public UserPoint save(UserPoint userPoint) {
+        return repository.save(UserPointEntity.from(userPoint)).to();
+    }
+}
diff --git a/core/service/user-service/build.gradle.kts b/core/service/user-service/build.gradle.kts
index 411681ae0..9292dba55 100644
--- a/core/service/user-service/build.gradle.kts
+++ b/core/service/user-service/build.gradle.kts
@@ -3,9 +3,10 @@ dependencies {
 
     // spring
     implementation("org.springframework.boot:spring-boot-starter")
-
+    implementation("org.springframework:spring-tx")
     // mysql
     runtimeOnly(project(":core:infra:database:mysql:user-mysql"))
+
     testImplementation(testFixtures(project(":core:infra:database:mysql:mysql-core")))
     testImplementation(project(":core:infra:database:mysql:mysql-config"))
 }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
index 573b97c86..5bbb9052c 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
@@ -2,6 +2,8 @@
 
 import com.loopers.core.domain.error.DomainErrorCode;
 import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.UserPoint;
+import com.loopers.core.domain.user.repository.UserPointRepository;
 import com.loopers.core.domain.user.repository.UserRepository;
 import com.loopers.core.domain.user.type.UserGender;
 import com.loopers.core.domain.user.vo.UserBirthDay;
@@ -10,13 +12,16 @@
 import com.loopers.core.service.user.command.JoinUserCommand;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 @Service
 @RequiredArgsConstructor
 public class JoinUserService {
 
     private final UserRepository userRepository;
+    private final UserPointRepository userPointRepository;
 
+    @Transactional
     public User joinUser(JoinUserCommand command) {
         UserIdentifier userIdentifier = UserIdentifier.create(command.getUserIdentifier());
         boolean presentIdentifier = userRepository.findByIdentifier(userIdentifier)
@@ -30,6 +35,9 @@ public User joinUser(JoinUserCommand command) {
         UserBirthDay userBirthDay = UserBirthDay.create(command.getBirthDay());
         UserGender userGender = UserGender.create(command.getGender());
 
-        return userRepository.save(User.create(userIdentifier, userEmail, userBirthDay, userGender));
+        User savedUser = userRepository.save(User.create(userIdentifier, userEmail, userBirthDay, userGender));
+        userPointRepository.save(UserPoint.create(savedUser.getUserId()));
+
+        return savedUser;
     }
 }

From 95d655db5986b0fafe03c4948d8414002bc038c3 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Wed, 29 Oct 2025 22:06:20 +0900
Subject: [PATCH 23/29] =?UTF-8?q?feature=20:=20#1=20=EC=9C=A0=EC=A0=80=20?=
 =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .codeguide/loopers-1-week.md                  |  4 +-
 .../api/error/ApiControllerAdvice.java        |  8 ++
 .../application/api/user/UserV1Api.java       | 17 +++-
 .../application/api/user/UserV1ApiSpec.java   |  6 ++
 .../application/api/user/UserV1Dto.java       | 10 +++
 .../http/get-user-by-identifier.http          |  2 +
 .../get-user-point-by-user-identifier.http    |  2 +
 .../api/user/UserV1ApiApiIntegrationTest.java | 73 +++++++++++++++++
 .../core/domain/error/DomainErrorCode.java    |  2 +-
 .../user/repository/UserPointRepository.java  |  7 ++
 .../user/repository/UserRepository.java       |  2 +
 .../mysql/user/UserPointJpaRepository.java    |  4 +
 .../user/impl/UserPointRepositoryImpl.java    | 17 ++++
 .../mysql/user/impl/UserRepositoryImpl.java   |  8 ++
 .../service/user/UserPointQueryService.java   | 29 +++++++
 .../core/service/user/UserQueryService.java   |  2 +-
 .../service/user/query/GetUserPointQuery.java | 13 +++
 .../UserPointQueryServiceIntegrationTest.java | 81 +++++++++++++++++++
 .../service/user/UserQueryServiceTest.java    |  4 +-
 19 files changed, 284 insertions(+), 7 deletions(-)
 create mode 100644 apps/commerce-api/src/main/resources/http/get-user-by-identifier.http
 create mode 100644 apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http
 create mode 100644 core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointQueryService.java
 create mode 100644 core/service/user-service/src/main/java/com/loopers/core/service/user/query/GetUserPointQuery.java
 create mode 100644 core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java

diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md
index c28ee618f..495b371c7 100644
--- a/.codeguide/loopers-1-week.md
+++ b/.codeguide/loopers-1-week.md
@@ -36,8 +36,8 @@
 
 **🔗 통합 테스트**
 
-- [ ]  해당 ID 의 회원이 존재할 경우, 보유 포인트가 반환된다.
-- [ ]  해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.
+- [x]  해당 ID 의 회원이 존재할 경우, 보유 포인트가 반환된다.
+- [x]  해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.
 
 **🌐 E2E 테스트**
 
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
index 87b11aa1a..3eb01d2a3 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
@@ -9,6 +9,7 @@
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
 import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingRequestHeaderException;
 import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -62,6 +63,13 @@ public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingServletRequestPara
         return failureResponse(ApiErrorType.BAD_REQUEST, message);
     }
 
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingRequestHeaderException e) {
+        String name = e.getHeaderName();
+        String message = String.format("필수 요청 헤더 '%s'가 누락되었습니다.", name);
+        return failureResponse(ApiErrorType.BAD_REQUEST, message);
+    }
+
     @ExceptionHandler
     public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableException e) {
         String errorMessage;
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
index a2a355f79..7f971fc11 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
@@ -3,8 +3,11 @@
 import com.loopers.application.api.common.dto.ApiResponse;
 import com.loopers.core.domain.error.NotFoundException;
 import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.service.user.JoinUserService;
+import com.loopers.core.service.user.UserPointQueryService;
 import com.loopers.core.service.user.UserQueryService;
+import com.loopers.core.service.user.query.GetUserPointQuery;
 import com.loopers.core.service.user.query.GetUserQuery;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
@@ -21,11 +24,12 @@ public class UserV1Api implements UserV1ApiSpec {
 
     private final JoinUserService joinUserService;
     private final UserQueryService userQueryService;
+    private final UserPointQueryService userPointQueryService;
 
     @Override
     @GetMapping("/{identifier}")
     public ApiResponse<GetUserResponse> getUser(@PathVariable String identifier) {
-        User user = userQueryService.getUserBy(new GetUserQuery(identifier));
+        User user = userQueryService.getUserByIdentifier(new GetUserQuery(identifier));
         if (Objects.isNull(user)) {
             throw NotFoundException.withName("사용자");
         }
@@ -41,4 +45,15 @@ public ApiResponse<JoinUserResponse> joinUser(
         User user = joinUserService.joinUser(request.toCommand());
         return ApiResponse.success(JoinUserResponse.from(user));
     }
+
+    @Override
+    @GetMapping("/points")
+    public ApiResponse<GetUserPointResponse> getUserPoint(@RequestHeader(name = "X-USER-ID") String userIdentifier) {
+        UserPoint userPoint = userPointQueryService.getByUserIdentifier(new GetUserPointQuery(userIdentifier));
+        if (Objects.isNull(userPoint)) {
+            throw NotFoundException.withName("사용자 포인트");
+        }
+
+        return ApiResponse.success(GetUserPointResponse.from(userPoint));
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
index 6359ce7b9..efe1fef84 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
@@ -25,4 +25,10 @@ ApiResponse<UserV1Dto.GetUserResponse> getUser(
             description = "사용자가 회원가입합니다."
     )
     ApiResponse<JoinUserResponse> joinUser(JoinUserRequest request);
+
+    @Operation(
+            summary = "사용자 포인트 조회",
+            description = "사용자의 포인트를 조회합니다."
+    )
+    ApiResponse<UserV1Dto.GetUserPointResponse> getUserPoint(String userIdentifier);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
index 9e4bc3870..fa3146750 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -1,6 +1,7 @@
 package com.loopers.application.api.user;
 
 import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.service.user.command.JoinUserCommand;
 import jakarta.validation.constraints.NotBlank;
 
@@ -56,4 +57,13 @@ public static JoinUserResponse from(User user) {
             );
         }
     }
+
+    public record GetUserPointResponse(
+            int balance
+    ) {
+
+        public static GetUserPointResponse from(UserPoint userPoint) {
+            return new GetUserPointResponse(userPoint.getBalance().value());
+        }
+    }
 }
diff --git a/apps/commerce-api/src/main/resources/http/get-user-by-identifier.http b/apps/commerce-api/src/main/resources/http/get-user-by-identifier.http
new file mode 100644
index 000000000..fc2990518
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/get-user-by-identifier.http
@@ -0,0 +1,2 @@
+GET http://localhost:8080/api/v1/users/{{identifier}}
+Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http b/apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http
new file mode 100644
index 000000000..3201de441
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http
@@ -0,0 +1,2 @@
+GET http://localhost:8080/api/v1/users/points
+X-USER-ID: {{identifier}}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
index 889c1b778..0b54f817b 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
@@ -8,6 +8,8 @@
 import com.loopers.core.domain.user.vo.UserBirthDay;
 import com.loopers.core.domain.user.vo.UserEmail;
 import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.user.JoinUserService;
+import com.loopers.core.service.user.command.JoinUserCommand;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -31,6 +33,9 @@ class UserV1ApiApiIntegrationTest extends ApiIntegrationTest {
     @Autowired
     private UserRepository userRepository;
 
+    @Autowired
+    private JoinUserService joinUserService;
+
     @Nested
     @DisplayName("회원가입")
     class 회원가입 {
@@ -168,4 +173,72 @@ class 존재하지_않는_ID로_조회할_경우 {
             }
         }
     }
+
+    @Nested
+    @DisplayName("포인트 조회")
+    class 포인트_조회 {
+
+        @Nested
+        @DisplayName("성공할 경우")
+        class 성공할_경우 {
+
+            @BeforeEach
+            void setUp() {
+                joinUserService.joinUser(new JoinUserCommand(
+                        "kilian",
+                        "kilian@gmail.com",
+                        "1997-10-08",
+                        "MALE"
+                ));
+            }
+
+            @Test
+            @DisplayName("보유 포인트를 응답으로 반환한다.")
+            void 보유_포인트를_응답으로_반환한다() {
+                // given
+                String userIdentifier = "kilian";
+                String endPoint = "/api/v1/users/points";
+                org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
+                headers.set("X-USER-ID", userIdentifier);
+                HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
+                ParameterizedTypeReference<ApiResponse<UserV1Dto.GetUserPointResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                // when
+                ResponseEntity<ApiResponse<UserV1Dto.GetUserPointResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, httpEntity, responseType);
+
+                // then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                assertSoftly(softly -> {
+                    softly.assertThat(response.getBody()).isNotNull();
+                    softly.assertThat(response.getBody().data()).isNotNull();
+                    softly.assertThat(response.getBody().data().balance()).isEqualTo(0);
+                });
+            }
+        }
+
+        @Nested
+        @DisplayName("X-USER-ID 헤더가 없을 경우")
+        class X_USER_ID_헤더가_없을_경우 {
+
+            @Test
+            @DisplayName("400 Bad Request 응답을 반환한다.")
+            void badRequest응답을_반환한다() {
+                // given
+                String endPoint = "/api/v1/users/points";
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                // when
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.GET, HttpEntity.EMPTY, responseType);
+
+                // then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+            }
+        }
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index 13248f6a0..59b9b2949 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -29,6 +29,6 @@ public static String notNullMessage(String fieldName) {
     }
 
     public static String notFoundMessage(String fieldName) {
-        return String.format(MUST_BE_NOT_NULL.message, fieldName);
+        return String.format(NOT_FOUND.message, fieldName);
     }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
index ec0a0adeb..5bedd7734 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
@@ -1,8 +1,15 @@
 package com.loopers.core.domain.user.repository;
 
 import com.loopers.core.domain.user.UserPoint;
+import com.loopers.core.domain.user.vo.UserId;
+
+import java.util.Optional;
 
 public interface UserPointRepository {
 
     UserPoint save(UserPoint userPoint);
+
+    UserPoint getByUserId(UserId userId);
+
+    Optional<UserPoint> findByUserId(UserId userId);
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
index fb3c0ba2a..68a2942bf 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
@@ -13,4 +13,6 @@ public interface UserRepository {
     Optional<User> findById(UserId userId);
 
     Optional<User> findByIdentifier(UserIdentifier identifier);
+
+    User getByIdentifier(UserIdentifier identifier);
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
index a8a99875d..95952b3c6 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
@@ -3,5 +3,9 @@
 import com.loopers.core.infra.database.mysql.user.entity.UserPointEntity;
 import org.springframework.data.jpa.repository.JpaRepository;
 
+import java.util.Optional;
+
 public interface UserPointJpaRepository extends JpaRepository<UserPointEntity, Long> {
+
+    Optional<UserPointEntity> findByUserId(Long userId);
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
index e99e039ad..ca6d0174b 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
@@ -1,12 +1,16 @@
 package com.loopers.core.infra.database.mysql.user.impl;
 
+import com.loopers.core.domain.error.NotFoundException;
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.domain.user.repository.UserPointRepository;
+import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.infra.database.mysql.user.UserPointJpaRepository;
 import com.loopers.core.infra.database.mysql.user.entity.UserPointEntity;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Repository;
 
+import java.util.Optional;
+
 @Repository
 @RequiredArgsConstructor
 public class UserPointRepositoryImpl implements UserPointRepository {
@@ -17,4 +21,17 @@ public class UserPointRepositoryImpl implements UserPointRepository {
     public UserPoint save(UserPoint userPoint) {
         return repository.save(UserPointEntity.from(userPoint)).to();
     }
+
+    @Override
+    public UserPoint getByUserId(UserId userId) {
+        return repository.findByUserId(Long.parseLong(userId.value()))
+                .orElseThrow(() -> NotFoundException.withName("사용자 포인트"))
+                .to();
+    }
+
+    @Override
+    public Optional<UserPoint> findByUserId(UserId userId) {
+        return repository.findByUserId(Long.parseLong(userId.value()))
+                .map(UserPointEntity::to);
+    }
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
index ccd7950ce..42fc94650 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
@@ -1,5 +1,6 @@
 package com.loopers.core.infra.database.mysql.user.impl;
 
+import com.loopers.core.domain.error.NotFoundException;
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.repository.UserRepository;
 import com.loopers.core.domain.user.vo.UserId;
@@ -33,4 +34,11 @@ public Optional<User> findByIdentifier(UserIdentifier identifier) {
         return repository.findByIdentifier(identifier.value())
                 .map(UserEntity::to);
     }
+
+    @Override
+    public User getByIdentifier(UserIdentifier identifier) {
+        return repository.findByIdentifier(identifier.value())
+                .orElseThrow(() -> NotFoundException.withName("사용자"))
+                .to();
+    }
 }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointQueryService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointQueryService.java
new file mode 100644
index 000000000..da9b419b8
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointQueryService.java
@@ -0,0 +1,29 @@
+package com.loopers.core.service.user;
+
+import com.loopers.core.domain.error.NotFoundException;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.UserPoint;
+import com.loopers.core.domain.user.repository.UserPointRepository;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.user.query.GetUserPointQuery;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class UserPointQueryService {
+
+    private final UserRepository userRepository;
+    private final UserPointRepository userPointRepository;
+
+    public UserPoint getByUserIdentifier(GetUserPointQuery query) {
+        try {
+            User user = userRepository.getByIdentifier(new UserIdentifier(query.getUserIdentifier()));
+            
+            return userPointRepository.getByUserId(user.getUserId());
+        } catch (NotFoundException exception) {
+            return null;
+        }
+    }
+}
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java
index 812005e24..3416ca2dd 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java
@@ -13,7 +13,7 @@ public class UserQueryService {
 
     private final UserRepository userRepository;
 
-    public User getUserBy(GetUserQuery query) {
+    public User getUserByIdentifier(GetUserQuery query) {
         return userRepository.findByIdentifier(new UserIdentifier(query.getIdentifier()))
                 .orElse(null);
     }
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/query/GetUserPointQuery.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/query/GetUserPointQuery.java
new file mode 100644
index 000000000..b20bab1ae
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/query/GetUserPointQuery.java
@@ -0,0 +1,13 @@
+package com.loopers.core.service.user.query;
+
+import lombok.Getter;
+
+@Getter
+public class GetUserPointQuery {
+
+    private final String userIdentifier;
+
+    public GetUserPointQuery(String userIdentifier) {
+        this.userIdentifier = userIdentifier;
+    }
+}
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
new file mode 100644
index 000000000..a840911c7
--- /dev/null
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
@@ -0,0 +1,81 @@
+package com.loopers.core.service.user;
+
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.UserPoint;
+import com.loopers.core.domain.user.repository.UserPointRepository;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.type.UserGender;
+import com.loopers.core.domain.user.vo.UserBirthDay;
+import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.IntegrationTest;
+import com.loopers.core.service.user.query.GetUserPointQuery;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+class UserPointQueryServiceIntegrationTest extends IntegrationTest {
+
+    @Autowired
+    private UserPointQueryService service;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Autowired
+    private UserPointRepository userPointRepository;
+
+    @Nested
+    @DisplayName("getByUserIdentifier()")
+    class GetByUserIdentifier {
+
+        @Nested
+        @DisplayName("해당 ID의 회원이 존재하지 않을 경우")
+        class 해당_ID의_회원이_존재하지_않을_경우 {
+
+            @Test
+            @DisplayName("null이 반환된다.")
+            void null이_반환된다() {
+                UserPoint userPoint = service.getByUserIdentifier(new GetUserPointQuery("non-existent"));
+
+                Assertions.assertThat(userPoint).isNull();
+            }
+        }
+
+        @Nested
+        @DisplayName("해당 ID의 회원이 존재할 경우")
+        class 해당_ID의_회원이_존재할_경우 {
+
+            @BeforeEach
+            void setUp() {
+                User user = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+
+                userPointRepository.save(UserPoint.create(user.getUserId()));
+            }
+
+            @Test
+            @DisplayName("보유 포인트가 반환된다.")
+            void 보유_포인트가_반환된다() {
+                UserPoint userPoint = service.getByUserIdentifier(new GetUserPointQuery("kilian"));
+
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(userPoint).isNotNull();
+                    softly.assertThat(userPoint.getUserId().value()).isEqualTo("1");
+                    softly.assertThat(userPoint.getBalance().value()).isEqualTo(0);
+                });
+            }
+        }
+
+    }
+}
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java
index da626d556..099d6427f 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserQueryServiceTest.java
@@ -35,7 +35,7 @@ class ID에_해당하는_사용자가_없는_경우 {
             @Test
             @DisplayName("null이 반환된다.")
             void null이_반환된다() {
-                User user = userQueryService.getUserBy(new GetUserQuery("identifier"));
+                User user = userQueryService.getUserByIdentifier(new GetUserQuery("identifier"));
                 Assertions.assertThat(user).isNull();
             }
         }
@@ -59,7 +59,7 @@ void setUp() {
             @Test
             @DisplayName("회원 정보가 반환된다.")
             void 회원_정보가_반환된다() {
-                User user = userQueryService.getUserBy(new GetUserQuery("kilian"));
+                User user = userQueryService.getUserByIdentifier(new GetUserQuery("kilian"));
 
                 SoftAssertions.assertSoftly(softly -> {
                     softly.assertThat(user).isNotNull();

From ec17913da7ca7fdad031b2ab06ac4e6f61cd4966 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Wed, 29 Oct 2025 22:40:55 +0900
Subject: [PATCH 24/29] =?UTF-8?q?feature=20:=20#1=20=EC=9C=A0=EC=A0=80=20?=
 =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=A9=EC=A0=84=20=EA=B5=AC?=
 =?UTF-8?q?=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .codeguide/loopers-1-week.md                  | 19 +++-
 .../application/api/user/UserV1Api.java       | 14 +++
 .../application/api/user/UserV1ApiSpec.java   | 13 ++-
 .../application/api/user/UserV1Dto.java       | 19 ++++
 .../resources/http/charge-user-point.http     |  7 ++
 .../api/user/UserV1ApiApiIntegrationTest.java | 74 +++++++++++++++
 .../core/domain/error/DomainErrorCode.java    |  3 +-
 .../loopers/core/domain/user/UserPoint.java   | 15 +++
 .../core/domain/user/vo/UserPointBalance.java |  8 +-
 .../core/domain/user/UserPointTest.java       | 42 +++++++++
 .../domain/user/vo/UserIdentifierTest.java    |  2 +-
 .../core/service/user/UserPointService.java   | 25 +++++
 .../user/command/UserPointChargeCommand.java  | 15 +++
 .../user/JoinUserServiceIntegrationTest.java  |  1 -
 .../user/UserPointServiceIntegrationTest.java | 91 +++++++++++++++++++
 15 files changed, 337 insertions(+), 11 deletions(-)
 create mode 100644 apps/commerce-api/src/main/resources/http/charge-user-point.http
 create mode 100644 core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java
 create mode 100644 core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
 create mode 100644 core/service/user-service/src/main/java/com/loopers/core/service/user/command/UserPointChargeCommand.java
 create mode 100644 core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java

diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md
index 495b371c7..0c2e19ec1 100644
--- a/.codeguide/loopers-1-week.md
+++ b/.codeguide/loopers-1-week.md
@@ -41,5 +41,20 @@
 
 **🌐 E2E 테스트**
 
-- [ ]  포인트 조회에 성공할 경우, 보유 포인트를 응답으로 반환한다.
-- [ ]  `X-USER-ID` 헤더가 없을 경우, `400 Bad Request` 응답을 반환한다.
+- [x]  포인트 조회에 성공할 경우, 보유 포인트를 응답으로 반환한다.
+- [x]  `X-USER-ID` 헤더가 없을 경우, `400 Bad Request` 응답을 반환한다.
+
+### 포인트 충전
+
+**🧱 단위 테스트**
+
+- [x]  0 이하의 정수로 포인트를 충전 시 실패한다.
+
+**🔗 통합 테스트**
+
+- [x]  존재하지 않는 유저 ID 로 충전을 시도한 경우, 실패한다.
+
+**🌐 E2E 테스트**
+
+- [ ]  존재하는 유저가 1000원을 충전할 경우, 충전된 보유 총량을 응답으로 반환한다.
+- [ ]  존재하지 않는 유저로 요청할 경우, `404 Not Found` 응답을 반환한다.
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
index 7f971fc11..d4cefd012 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
@@ -6,6 +6,7 @@
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.service.user.JoinUserService;
 import com.loopers.core.service.user.UserPointQueryService;
+import com.loopers.core.service.user.UserPointService;
 import com.loopers.core.service.user.UserQueryService;
 import com.loopers.core.service.user.query.GetUserPointQuery;
 import com.loopers.core.service.user.query.GetUserQuery;
@@ -25,6 +26,7 @@ public class UserV1Api implements UserV1ApiSpec {
     private final JoinUserService joinUserService;
     private final UserQueryService userQueryService;
     private final UserPointQueryService userPointQueryService;
+    private final UserPointService userPointService;
 
     @Override
     @GetMapping("/{identifier}")
@@ -56,4 +58,16 @@ public ApiResponse<GetUserPointResponse> getUserPoint(@RequestHeader(name = "X-U
 
         return ApiResponse.success(GetUserPointResponse.from(userPoint));
     }
+
+    @Override
+    @PostMapping("/points/charge")
+    public ApiResponse<UserPointChargeResponse> userPointCharge(
+            @RequestBody UserPointChargeRequest request,
+            @RequestHeader(name = "X-USER-ID") String userIdentifier
+    ) {
+        UserPoint userPoint = userPointService.charge(request.toCommand(userIdentifier));
+
+        return ApiResponse.success(UserPointChargeResponse.from(userPoint));
+
+    }
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
index efe1fef84..b1f6899fb 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
@@ -5,8 +5,7 @@
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.tags.Tag;
 
-import static com.loopers.application.api.user.UserV1Dto.JoinUserRequest;
-import static com.loopers.application.api.user.UserV1Dto.JoinUserResponse;
+import static com.loopers.application.api.user.UserV1Dto.*;
 
 @Tag(name = "User V1 API", description = "User API 입니다.")
 public interface UserV1ApiSpec {
@@ -15,7 +14,7 @@ public interface UserV1ApiSpec {
             summary = "사용자 조회",
             description = "ID로 사용자를 조회합니다."
     )
-    ApiResponse<UserV1Dto.GetUserResponse> getUser(
+    ApiResponse<GetUserResponse> getUser(
             @Schema(name = "사용자 ID", description = "조회할 사용자 ID")
             String identifier
     );
@@ -30,5 +29,11 @@ ApiResponse<UserV1Dto.GetUserResponse> getUser(
             summary = "사용자 포인트 조회",
             description = "사용자의 포인트를 조회합니다."
     )
-    ApiResponse<UserV1Dto.GetUserPointResponse> getUserPoint(String userIdentifier);
+    ApiResponse<GetUserPointResponse> getUserPoint(String userIdentifier);
+
+    @Operation(
+            summary = "사용자 포인트 충전",
+            description = "사용자의 포인트를 충전합니다."
+    )
+    ApiResponse<UserPointChargeResponse> userPointCharge(UserPointChargeRequest request, String userIdentifier);
 }
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
index fa3146750..b9808e83f 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -3,6 +3,7 @@
 import com.loopers.core.domain.user.User;
 import com.loopers.core.domain.user.UserPoint;
 import com.loopers.core.service.user.command.JoinUserCommand;
+import com.loopers.core.service.user.command.UserPointChargeCommand;
 import jakarta.validation.constraints.NotBlank;
 
 import java.time.format.DateTimeFormatter;
@@ -66,4 +67,22 @@ public static GetUserPointResponse from(UserPoint userPoint) {
             return new GetUserPointResponse(userPoint.getBalance().value());
         }
     }
+
+    public record UserPointChargeRequest(
+            int point
+    ) {
+
+        public UserPointChargeCommand toCommand(String userIdentifier) {
+            return new UserPointChargeCommand(userIdentifier, point);
+        }
+    }
+
+    public record UserPointChargeResponse(
+            int balance
+    ) {
+
+        public static UserPointChargeResponse from(UserPoint userPoint) {
+            return new UserPointChargeResponse(userPoint.getBalance().value());
+        }
+    }
 }
diff --git a/apps/commerce-api/src/main/resources/http/charge-user-point.http b/apps/commerce-api/src/main/resources/http/charge-user-point.http
new file mode 100644
index 000000000..30827d931
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/charge-user-point.http
@@ -0,0 +1,7 @@
+POST http://localhost:8080/api/v1/users/points/charge
+Content-Type: application/json
+X-USER-ID: {{identifier}}
+
+{
+  "point": {{point}}
+}
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
index 0b54f817b..8c94f9e99 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
@@ -241,4 +241,78 @@ class X_USER_ID_헤더가_없을_경우 {
             }
         }
     }
+
+    @Nested
+    @DisplayName("포인트 충전")
+    class 포인트_충전 {
+
+        @Nested
+        @DisplayName("존재하는 유저가 1000원을 충전할 경우")
+        class 존재하는_유저가_충전 {
+
+            @BeforeEach
+            void setUp() {
+                joinUserService.joinUser(new JoinUserCommand(
+                        "kilian",
+                        "kilian@gmail.com",
+                        "1997-10-08",
+                        "MALE"
+                ));
+            }
+
+            @Test
+            @DisplayName("충전된 보유 총량을 응답으로 반환한다.")
+            void 포인트_충전_성공() {
+                // given
+                String userIdentifier = "kilian";
+                String endPoint = "/api/v1/users/points/charge";
+                org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
+                headers.set("X-USER-ID", userIdentifier);
+                UserV1Dto.UserPointChargeRequest request = new UserV1Dto.UserPointChargeRequest(1000);
+                HttpEntity<UserV1Dto.UserPointChargeRequest> httpEntity = new HttpEntity<>(request, headers);
+                ParameterizedTypeReference<ApiResponse<UserV1Dto.UserPointChargeResponse>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                // when
+                ResponseEntity<ApiResponse<UserV1Dto.UserPointChargeResponse>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+
+                // then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
+                assertSoftly(softly -> {
+                    softly.assertThat(response.getBody()).isNotNull();
+                    softly.assertThat(response.getBody().data()).isNotNull();
+                    softly.assertThat(response.getBody().data().balance()).isEqualTo(1000);
+                });
+            }
+        }
+
+        @Nested
+        @DisplayName("존재하지 않는 유저로 요청할 경우")
+        class 존재하지_않는_유저로_요청 {
+
+            @Test
+            @DisplayName("404 Not Found 응답을 반환한다.")
+            void notFound응답을_반환한다() {
+                // given
+                String userIdentifier = "non-existent";
+                String endPoint = "/api/v1/users/points/charge";
+                org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
+                headers.set("X-USER-ID", userIdentifier);
+                UserV1Dto.UserPointChargeRequest request = new UserV1Dto.UserPointChargeRequest(1000);
+                HttpEntity<UserV1Dto.UserPointChargeRequest> httpEntity = new HttpEntity<>(request, headers);
+                ParameterizedTypeReference<ApiResponse<Void>> responseType =
+                        new ParameterizedTypeReference<>() {
+                        };
+
+                // when
+                ResponseEntity<ApiResponse<Void>> response =
+                        testRestTemplate.exchange(endPoint, HttpMethod.POST, httpEntity, responseType);
+
+                // then
+                assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+            }
+        }
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index 59b9b2949..ab0e555b4 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -13,7 +13,8 @@ public enum DomainErrorCode {
     GENDER_IS_MALE_OR_FEMALE(1004, "성별은 MALE 혹은 FEMALE이어야 합니다."),
     PRESENT_USER_IDENTIFIER(1005, "이미 존재하는 사용자 ID입니다."),
     NOT_FOUND(1006, "%s를 찾지 못했습니다."),
-    USER_POINT_BALANCE_GRATER_THEN_ZERO(1007, "사용자 포인트의 잔액은 0보다는 커야합니다.");
+    USER_POINT_BALANCE_GRATER_THAN_ZERO(1007, "사용자 포인트의 잔액은 0보다는 커야합니다."),
+    CANNOT_CHARGE_POINTS_LESS_THAN_ZERO(1008, "0보다 작거나 같은 포인트를 충전할 수 없습니다.");
 
     private final int code;
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java b/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
index fda580676..2d8f8bed1 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
@@ -2,9 +2,12 @@
 
 import com.loopers.core.domain.common.vo.CreatedAt;
 import com.loopers.core.domain.common.vo.UpdatedAt;
+import com.loopers.core.domain.error.DomainErrorCode;
 import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.domain.user.vo.UserPointBalance;
 import com.loopers.core.domain.user.vo.UserPointId;
+import lombok.AccessLevel;
+import lombok.Builder;
 import lombok.Getter;
 
 @Getter
@@ -20,6 +23,7 @@ public class UserPoint {
 
     private final UpdatedAt updatedAt;
 
+    @Builder(access = AccessLevel.PRIVATE, toBuilder = true)
     private UserPoint(
             UserPointId id,
             UserId userId,
@@ -53,4 +57,15 @@ public static UserPoint mappedBy(
     ) {
         return new UserPoint(id, userId, balance, createdAt, updatedAt);
     }
+
+    public UserPoint charge(int point) {
+        if (point <= 0) {
+            throw new IllegalArgumentException(DomainErrorCode.CANNOT_CHARGE_POINTS_LESS_THAN_ZERO.getMessage());
+        }
+
+        return this.toBuilder()
+                .balance(this.balance.add(point))
+                .updatedAt(UpdatedAt.now())
+                .build();
+    }
 }
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
index 8be25d7d2..c8d11c23c 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
@@ -1,12 +1,12 @@
 package com.loopers.core.domain.user.vo;
 
-import static com.loopers.core.domain.error.DomainErrorCode.USER_POINT_BALANCE_GRATER_THEN_ZERO;
+import static com.loopers.core.domain.error.DomainErrorCode.USER_POINT_BALANCE_GRATER_THAN_ZERO;
 
 public record UserPointBalance(int value) {
 
     public UserPointBalance(int value) {
         if (value < 0) {
-            throw new IllegalArgumentException(USER_POINT_BALANCE_GRATER_THEN_ZERO.getMessage());
+            throw new IllegalArgumentException(USER_POINT_BALANCE_GRATER_THAN_ZERO.getMessage());
         }
 
         this.value = value;
@@ -15,4 +15,8 @@ public UserPointBalance(int value) {
     public static UserPointBalance init() {
         return new UserPointBalance(0);
     }
+
+    public UserPointBalance add(int point) {
+        return new UserPointBalance(this.value + point);
+    }
 }
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java
new file mode 100644
index 000000000..422ca280d
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java
@@ -0,0 +1,42 @@
+package com.loopers.core.domain.user;
+
+import com.loopers.core.domain.user.vo.UserId;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@DisplayName("UserPoint")
+class UserPointTest {
+
+    @Nested
+    @DisplayName("recharge() 메서드")
+    class RechargeMethod {
+
+        @Nested
+        @DisplayName("0 이하의 정수로 충전할 경우")
+        class 영_이하의_정수로_충전할_경우 {
+
+            static Stream<Integer> invalidPoints() {
+                return Stream.of(0, -1, -100, -10000, Integer.MIN_VALUE);
+            }
+
+            @ParameterizedTest
+            @MethodSource("invalidPoints")
+            @DisplayName("예외를 발생시킨다")
+            void 예외_발생(int invalidPoint) {
+                // given
+                UserPoint userPoint = UserPoint.create(new UserId("1"));
+
+                // when & then
+                assertThatThrownBy(() -> userPoint.charge(invalidPoint))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("0보다 작거나 같은 포인트를 충전할 수 없습니다.");
+            }
+        }
+    }
+}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserIdentifierTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserIdentifierTest.java
index d947c7996..d04c689c4 100644
--- a/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserIdentifierTest.java
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserIdentifierTest.java
@@ -49,7 +49,7 @@ class value가_null인_경우 {
                 // when & then
                 assertThatThrownBy(() -> UserIdentifier.create(id))
                         .isInstanceOf(NullPointerException.class)
-                        .hasMessage("사용자의 ID는 Null이 될 수 없습니다.");
+                        .hasMessage("사용자의 ID는(은) Null이 될 수 없습니다.");
             }
         }
 
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
new file mode 100644
index 000000000..744a45f90
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
@@ -0,0 +1,25 @@
+package com.loopers.core.service.user;
+
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.UserPoint;
+import com.loopers.core.domain.user.repository.UserPointRepository;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.user.command.UserPointChargeCommand;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class UserPointService {
+
+    private final UserRepository userRepository;
+    private final UserPointRepository userPointRepository;
+
+    public UserPoint charge(UserPointChargeCommand command) {
+        User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
+        UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
+
+        return userPointRepository.save(userPoint.charge(command.getPoint()));
+    }
+}
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/command/UserPointChargeCommand.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/command/UserPointChargeCommand.java
new file mode 100644
index 000000000..39b4ea4a7
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/command/UserPointChargeCommand.java
@@ -0,0 +1,15 @@
+package com.loopers.core.service.user.command;
+
+import lombok.Getter;
+
+@Getter
+public class UserPointChargeCommand {
+
+    private final String userIdentifier;
+    private final int point;
+
+    public UserPointChargeCommand(String userIdentifier, int point) {
+        this.userIdentifier = userIdentifier;
+        this.point = point;
+    }
+}
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
index 41012abd2..248e95e94 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
@@ -80,5 +80,4 @@ void setUp() {
             }
         }
     }
-
 }
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
new file mode 100644
index 000000000..ccd7e41bd
--- /dev/null
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointServiceIntegrationTest.java
@@ -0,0 +1,91 @@
+package com.loopers.core.service.user;
+
+import com.loopers.core.domain.error.NotFoundException;
+import com.loopers.core.domain.user.User;
+import com.loopers.core.domain.user.UserPoint;
+import com.loopers.core.domain.user.repository.UserPointRepository;
+import com.loopers.core.domain.user.repository.UserRepository;
+import com.loopers.core.domain.user.type.UserGender;
+import com.loopers.core.domain.user.vo.UserBirthDay;
+import com.loopers.core.domain.user.vo.UserEmail;
+import com.loopers.core.domain.user.vo.UserIdentifier;
+import com.loopers.core.service.IntegrationTest;
+import com.loopers.core.service.user.command.UserPointChargeCommand;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+class UserPointServiceIntegrationTest extends IntegrationTest {
+
+    @Autowired
+    private UserPointService service;
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Autowired
+    private UserPointRepository userPointRepository;
+
+    @Nested
+    @DisplayName("포인트 충전")
+    class 포인트_충전 {
+
+        @Nested
+        @DisplayName("정상적으로 충전할 경우")
+        class 정상_충전 {
+
+            User user;
+
+            @BeforeEach
+            void setUp() {
+                user = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+                userPointRepository.save(UserPoint.create(user.getUserId()));
+            }
+
+            @Test
+            @DisplayName("포인트가 충전된다.")
+            void 포인트_충전_성공() {
+                // given
+                UserPointChargeCommand command = new UserPointChargeCommand("kilian", 1000);
+
+                // when
+                service.charge(command);
+                UserPoint chargedPoint = userPointRepository.getByUserId(user.getUserId());
+
+                // then
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(chargedPoint).isNotNull();
+                    softly.assertThat(chargedPoint.getBalance().value()).isEqualTo(1000);
+                });
+            }
+        }
+
+        @Nested
+        @DisplayName("존재하지 않는 유저 ID로 충전을 시도한 경우")
+        class 존재하지_않는_유저_ID로_충전 {
+
+            @Test
+            @DisplayName("실패한다.")
+            void 충전_실패() {
+                // given
+                UserPointChargeCommand command = new UserPointChargeCommand("non-existent", 1000);
+
+                // when & then
+                Assertions.assertThatThrownBy(() -> service.charge(command))
+                        .isInstanceOf(NotFoundException.class);
+            }
+        }
+    }
+
+}

From 0eda7f8ab0a0027a878ebefb1ba3bcb89fd0aadd Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Wed, 29 Oct 2025 22:42:17 +0900
Subject: [PATCH 25/29] =?UTF-8?q?feature=20:=20#1=20create()=20=ED=85=8C?=
 =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20displayName=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/test/java/com/loopers/core/domain/user/UserTest.java    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java
index 41eb9c23c..d8b7ada81 100644
--- a/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java
@@ -7,7 +7,7 @@
 class UserTest {
 
     @Nested
-    @DisplayName("create()")
+    @DisplayName("create() 메서드")
     class CreateTest {
 
         @Nested

From e839d63e517d4db6af936bf18a0c8006ab31dd2e Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Wed, 29 Oct 2025 22:42:44 +0900
Subject: [PATCH 26/29] =?UTF-8?q?feature=20:=20#1=201=EC=A3=BC=EC=B0=A8=20?=
 =?UTF-8?q?TODO=20=EB=A7=88=ED=81=AC=EB=8B=A4=EC=9A=B4=20=EC=99=84?=
 =?UTF-8?q?=EB=A3=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .codeguide/loopers-1-week.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md
index 0c2e19ec1..b81d438ed 100644
--- a/.codeguide/loopers-1-week.md
+++ b/.codeguide/loopers-1-week.md
@@ -56,5 +56,5 @@
 
 **🌐 E2E 테스트**
 
-- [ ]  존재하는 유저가 1000원을 충전할 경우, 충전된 보유 총량을 응답으로 반환한다.
-- [ ]  존재하지 않는 유저로 요청할 경우, `404 Not Found` 응답을 반환한다.
+- [x]  존재하는 유저가 1000원을 충전할 경우, 충전된 보유 총량을 응답으로 반환한다.
+- [x]  존재하지 않는 유저로 요청할 경우, `404 Not Found` 응답을 반환한다.

From 8aa28599f19ddf9f5af82b39afecb76a83f01cae Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Thu, 30 Oct 2025 00:52:05 +0900
Subject: [PATCH 27/29] =?UTF-8?q?refactor=20:=20#1=20testRestTemplate=20Ap?=
 =?UTF-8?q?iIntegrationTest=EB=A1=9C=20=EC=82=AC=EC=9A=A9=ED=95=98?=
 =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../loopers/application/api/error/ApiControllerAdvice.java    | 1 +
 .../java/com/loopers/application/api/ApiIntegrationTest.java  | 2 +-
 .../com/loopers/application/api/error/ApiExceptionTest.java   | 1 -
 .../application/api/user/UserV1ApiApiIntegrationTest.java     | 4 ----
 4 files changed, 2 insertions(+), 6 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
index 3eb01d2a3..9b6fe4638 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/error/ApiControllerAdvice.java
@@ -25,6 +25,7 @@
 @RestControllerAdvice
 @Slf4j
 public class ApiControllerAdvice {
+
     @ExceptionHandler
     public ResponseEntity<ApiResponse<?>> handle(ApiException e) {
         log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage(), e);
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/ApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/ApiIntegrationTest.java
index 1021784c5..7eda27c11 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/ApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/ApiIntegrationTest.java
@@ -13,7 +13,7 @@
 public class ApiIntegrationTest {
 
     @Autowired
-    private TestRestTemplate testRestTemplate;
+    protected TestRestTemplate testRestTemplate;
 
     @Autowired
     private DatabaseCleanUp databaseCleanUp;
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/error/ApiExceptionTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/error/ApiExceptionTest.java
index d695507ee..2f7e1964e 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/error/ApiExceptionTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/error/ApiExceptionTest.java
@@ -4,7 +4,6 @@
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
 
 class ApiExceptionTest {
 
diff --git a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
index 8c94f9e99..c4a49da18 100644
--- a/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
@@ -15,7 +15,6 @@
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.web.client.TestRestTemplate;
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpMethod;
@@ -27,9 +26,6 @@
 
 class UserV1ApiApiIntegrationTest extends ApiIntegrationTest {
 
-    @Autowired
-    private TestRestTemplate testRestTemplate;
-
     @Autowired
     private UserRepository userRepository;
 

From a66be49485d6f5c2cde6db0d15a883a9ff4084e0 Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Fri, 31 Oct 2025 10:44:40 +0900
Subject: [PATCH 28/29] =?UTF-8?q?fix=20:=20#1=20code=20rabbit=201=EC=B0=A8?=
 =?UTF-8?q?=20PR=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- UserIdentifier 검증 수정
- 사용자 포인트 충전 API 검증 추가
- jackson Spring autoconfigure 의존성 추가
- ErrorCode code 중복 수정
- http파일 URL 변경
- 회원 존재 확인 테스트 수정
---
 .../com/loopers/application/api/user/UserV1Api.java    |  2 +-
 .../com/loopers/application/api/user/UserV1Dto.java    |  3 ++-
 .../src/main/resources/http/charge-user-point.http     |  2 +-
 .../main/resources/http/get-user-by-identifier.http    |  2 +-
 .../http/get-user-point-by-user-identifier.http        |  2 +-
 .../com/loopers/core/domain/error/DomainErrorCode.java | 10 +++++-----
 .../loopers/core/service/user/UserPointService.java    |  2 +-
 .../loopers/core/service/user/UserQueryService.java    |  2 +-
 .../user/UserPointQueryServiceIntegrationTest.java     |  9 ++++++---
 .../src/test/resources/application-test.yml            |  2 +-
 supports/jackson/build.gradle.kts                      |  2 +-
 11 files changed, 21 insertions(+), 17 deletions(-)

diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
index d4cefd012..512c48949 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
@@ -62,7 +62,7 @@ public ApiResponse<GetUserPointResponse> getUserPoint(@RequestHeader(name = "X-U
     @Override
     @PostMapping("/points/charge")
     public ApiResponse<UserPointChargeResponse> userPointCharge(
-            @RequestBody UserPointChargeRequest request,
+            @RequestBody @Valid UserPointChargeRequest request,
             @RequestHeader(name = "X-USER-ID") String userIdentifier
     ) {
         UserPoint userPoint = userPointService.charge(request.toCommand(userIdentifier));
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
index b9808e83f..40d6a2e04 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -5,6 +5,7 @@
 import com.loopers.core.service.user.command.JoinUserCommand;
 import com.loopers.core.service.user.command.UserPointChargeCommand;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Positive;
 
 import java.time.format.DateTimeFormatter;
 
@@ -69,7 +70,7 @@ public static GetUserPointResponse from(UserPoint userPoint) {
     }
 
     public record UserPointChargeRequest(
-            int point
+            @Positive int point
     ) {
 
         public UserPointChargeCommand toCommand(String userIdentifier) {
diff --git a/apps/commerce-api/src/main/resources/http/charge-user-point.http b/apps/commerce-api/src/main/resources/http/charge-user-point.http
index 30827d931..0fce31016 100644
--- a/apps/commerce-api/src/main/resources/http/charge-user-point.http
+++ b/apps/commerce-api/src/main/resources/http/charge-user-point.http
@@ -1,4 +1,4 @@
-POST http://localhost:8080/api/v1/users/points/charge
+POST {{commerce-api}}/api/v1/users/points/charge
 Content-Type: application/json
 X-USER-ID: {{identifier}}
 
diff --git a/apps/commerce-api/src/main/resources/http/get-user-by-identifier.http b/apps/commerce-api/src/main/resources/http/get-user-by-identifier.http
index fc2990518..fe8a6d139 100644
--- a/apps/commerce-api/src/main/resources/http/get-user-by-identifier.http
+++ b/apps/commerce-api/src/main/resources/http/get-user-by-identifier.http
@@ -1,2 +1,2 @@
-GET http://localhost:8080/api/v1/users/{{identifier}}
+GET {{commerce-api}}/api/v1/users/{{identifier}}
 Content-Type: application/json
diff --git a/apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http b/apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http
index 3201de441..730ce9b07 100644
--- a/apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http
+++ b/apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http
@@ -1,2 +1,2 @@
-GET http://localhost:8080/api/v1/users/points
+GET {{commerce-api}}/api/v1/users/points
 X-USER-ID: {{identifier}}
diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index ab0e555b4..022fe5084 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -10,11 +10,11 @@ public enum DomainErrorCode {
     USER_ID_LENGTH_VALIDATE(1002, "사용자의 ID의 길이는 %d자 이상 %d자 이하여야 합니다."),
     INVALID_EMAIL_FORMAT(1003, "유효하지 않은 이메일 형식입니다."),
     INVALID_BIRTHDATE_FORMAT(1004, "생년월일은 yyyy-MM-dd 형식이어야 합니다."),
-    GENDER_IS_MALE_OR_FEMALE(1004, "성별은 MALE 혹은 FEMALE이어야 합니다."),
-    PRESENT_USER_IDENTIFIER(1005, "이미 존재하는 사용자 ID입니다."),
-    NOT_FOUND(1006, "%s를 찾지 못했습니다."),
-    USER_POINT_BALANCE_GRATER_THAN_ZERO(1007, "사용자 포인트의 잔액은 0보다는 커야합니다."),
-    CANNOT_CHARGE_POINTS_LESS_THAN_ZERO(1008, "0보다 작거나 같은 포인트를 충전할 수 없습니다.");
+    GENDER_IS_MALE_OR_FEMALE(1005, "성별은 MALE 혹은 FEMALE이어야 합니다."),
+    PRESENT_USER_IDENTIFIER(1006, "이미 존재하는 사용자 ID입니다."),
+    NOT_FOUND(1007, "%s를 찾지 못했습니다."),
+    USER_POINT_BALANCE_GRATER_THAN_ZERO(1008, "사용자 포인트의 잔액은 0보다는 커야합니다."),
+    CANNOT_CHARGE_POINTS_LESS_THAN_ZERO(1009, "0보다 작거나 같은 포인트를 충전할 수 없습니다.");
 
     private final int code;
 
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
index 744a45f90..9f7ac4560 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
@@ -17,7 +17,7 @@ public class UserPointService {
     private final UserPointRepository userPointRepository;
 
     public UserPoint charge(UserPointChargeCommand command) {
-        User user = userRepository.getByIdentifier(new UserIdentifier(command.getUserIdentifier()));
+        User user = userRepository.getByIdentifier(UserIdentifier.create(command.getUserIdentifier()));
         UserPoint userPoint = userPointRepository.getByUserId(user.getUserId());
 
         return userPointRepository.save(userPoint.charge(command.getPoint()));
diff --git a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java
index 3416ca2dd..f4ab00586 100644
--- a/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserQueryService.java
@@ -14,7 +14,7 @@ public class UserQueryService {
     private final UserRepository userRepository;
 
     public User getUserByIdentifier(GetUserQuery query) {
-        return userRepository.findByIdentifier(new UserIdentifier(query.getIdentifier()))
+        return userRepository.findByIdentifier(UserIdentifier.create(query.getIdentifier()))
                 .orElse(null);
     }
 }
diff --git a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
index a840911c7..7e47a906c 100644
--- a/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
@@ -50,9 +50,12 @@ class 해당_ID의_회원이_존재하지_않을_경우 {
         @DisplayName("해당 ID의 회원이 존재할 경우")
         class 해당_ID의_회원이_존재할_경우 {
 
+            User savedUser;
+            UserPoint savedUserPoint;
+
             @BeforeEach
             void setUp() {
-                User user = userRepository.save(
+                savedUser = userRepository.save(
                         User.create(
                                 UserIdentifier.create("kilian"),
                                 UserEmail.create("kilian@gmail.com"),
@@ -61,7 +64,7 @@ void setUp() {
                         )
                 );
 
-                userPointRepository.save(UserPoint.create(user.getUserId()));
+                savedUserPoint = userPointRepository.save(UserPoint.create(savedUser.getUserId()));
             }
 
             @Test
@@ -71,7 +74,7 @@ void setUp() {
 
                 SoftAssertions.assertSoftly(softly -> {
                     softly.assertThat(userPoint).isNotNull();
-                    softly.assertThat(userPoint.getUserId().value()).isEqualTo("1");
+                    softly.assertThat(userPoint.getUserId().value()).isEqualTo(savedUser.getUserId().value());
                     softly.assertThat(userPoint.getBalance().value()).isEqualTo(0);
                 });
             }
diff --git a/core/service/user-service/src/test/resources/application-test.yml b/core/service/user-service/src/test/resources/application-test.yml
index d0a6e1fcd..34f9eeeb0 100644
--- a/core/service/user-service/src/test/resources/application-test.yml
+++ b/core/service/user-service/src/test/resources/application-test.yml
@@ -1,4 +1,4 @@
 spring:
   jpa:
     hibernate:
-      ddl-auto: update
+      ddl-auto: create-drop
diff --git a/supports/jackson/build.gradle.kts b/supports/jackson/build.gradle.kts
index 0f56f268a..c68413b78 100644
--- a/supports/jackson/build.gradle.kts
+++ b/supports/jackson/build.gradle.kts
@@ -5,7 +5,7 @@ plugins {
 dependencies {
     // spring
     implementation("org.springframework:spring-web")
-    implementation("org.springframework.boot:spring-boot-starter")
+    implementation("org.springframework.boot:spring-boot-autoconfigure")
 
     // jackson
     implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

From 1f98e536c5e4b5e1361ef8503824b0b0e5e2dc0a Mon Sep 17 00:00:00 2001
From: kilian <audrb96@gmail.com>
Date: Fri, 31 Oct 2025 11:28:36 +0900
Subject: [PATCH 29/29] =?UTF-8?q?fix=20:=20#1=20code=20rabbit=202=EC=B0=A8?=
 =?UTF-8?q?=20PR=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 도메인 에러코드 메시지 수정
- 이메일 검증 수정
- 사용자 추가 검증 balance로 위임
- repository로 조회 시 null safe
-
---
 .../core/domain/error/DomainErrorCode.java    |  4 +-
 .../loopers/core/domain/user/UserPoint.java   |  5 --
 .../core/domain/user/vo/UserEmail.java        |  8 ++-
 .../core/domain/user/vo/UserPointBalance.java | 10 +++-
 .../core/domain/user/UserPointTest.java       | 33 -------------
 .../loopers/core/domain/user/UserTest.java    | 24 ---------
 .../domain/user/vo/UserPointBalanceTest.java  | 49 ++++++++++++++++---
 .../mysql/user/entity/UserPointEntity.java    |  7 ++-
 .../user/impl/UserPointRepositoryImpl.java    | 15 +++++-
 .../mysql/user/impl/UserRepositoryImpl.java   |  8 ++-
 10 files changed, 80 insertions(+), 83 deletions(-)
 delete mode 100644 core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java

diff --git a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
index 022fe5084..380684311 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -13,8 +13,8 @@ public enum DomainErrorCode {
     GENDER_IS_MALE_OR_FEMALE(1005, "성별은 MALE 혹은 FEMALE이어야 합니다."),
     PRESENT_USER_IDENTIFIER(1006, "이미 존재하는 사용자 ID입니다."),
     NOT_FOUND(1007, "%s를 찾지 못했습니다."),
-    USER_POINT_BALANCE_GRATER_THAN_ZERO(1008, "사용자 포인트의 잔액은 0보다는 커야합니다."),
-    CANNOT_CHARGE_POINTS_LESS_THAN_ZERO(1009, "0보다 작거나 같은 포인트를 충전할 수 없습니다.");
+    USER_POINT_BALANCE_NON_NEGATIVE(1008, "사용자 포인트의 잔액은 음수가 될 수 없습니다."),
+    CANNOT_CHARGE_POINTS_NEGATIVE(1009, "음수 포인트를 충전할 수 없습니다.");
 
     private final int code;
 
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java b/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
index 2d8f8bed1..88d093dfa 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
@@ -2,7 +2,6 @@
 
 import com.loopers.core.domain.common.vo.CreatedAt;
 import com.loopers.core.domain.common.vo.UpdatedAt;
-import com.loopers.core.domain.error.DomainErrorCode;
 import com.loopers.core.domain.user.vo.UserId;
 import com.loopers.core.domain.user.vo.UserPointBalance;
 import com.loopers.core.domain.user.vo.UserPointId;
@@ -59,10 +58,6 @@ public static UserPoint mappedBy(
     }
 
     public UserPoint charge(int point) {
-        if (point <= 0) {
-            throw new IllegalArgumentException(DomainErrorCode.CANNOT_CHARGE_POINTS_LESS_THAN_ZERO.getMessage());
-        }
-
         return this.toBuilder()
                 .balance(this.balance.add(point))
                 .updatedAt(UpdatedAt.now())
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
index 12c6efbe3..a6199811d 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
@@ -10,18 +10,16 @@ public record UserEmail(String value) {
 
     public UserEmail(String value) {
         validateNotNull(value);
+        validate(value);
         this.value = value;
     }
 
     public static UserEmail create(String email) {
-        validateNotNull(email);
-        validate(email);
-
         return new UserEmail(email);
     }
 
-    private static String validateNotNull(String email) {
-        return Objects.requireNonNull(email, DomainErrorCode.notNullMessage("사용자의 이메일"));
+    private static void validateNotNull(String email) {
+        Objects.requireNonNull(email, DomainErrorCode.notNullMessage("사용자의 이메일"));
     }
 
     private static void validate(String email) {
diff --git a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
index c8d11c23c..6fbab390d 100644
--- a/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
@@ -1,12 +1,14 @@
 package com.loopers.core.domain.user.vo;
 
-import static com.loopers.core.domain.error.DomainErrorCode.USER_POINT_BALANCE_GRATER_THAN_ZERO;
+import com.loopers.core.domain.error.DomainErrorCode;
+
+import static com.loopers.core.domain.error.DomainErrorCode.USER_POINT_BALANCE_NON_NEGATIVE;
 
 public record UserPointBalance(int value) {
 
     public UserPointBalance(int value) {
         if (value < 0) {
-            throw new IllegalArgumentException(USER_POINT_BALANCE_GRATER_THAN_ZERO.getMessage());
+            throw new IllegalArgumentException(USER_POINT_BALANCE_NON_NEGATIVE.getMessage());
         }
 
         this.value = value;
@@ -17,6 +19,10 @@ public static UserPointBalance init() {
     }
 
     public UserPointBalance add(int point) {
+        if (point <= 0) {
+            throw new IllegalArgumentException(DomainErrorCode.CANNOT_CHARGE_POINTS_NEGATIVE.getMessage());
+        }
+
         return new UserPointBalance(this.value + point);
     }
 }
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java
index 422ca280d..43a177ad5 100644
--- a/core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java
@@ -1,42 +1,9 @@
 package com.loopers.core.domain.user;
 
-import com.loopers.core.domain.user.vo.UserId;
 import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import java.util.stream.Stream;
-
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 @DisplayName("UserPoint")
 class UserPointTest {
 
-    @Nested
-    @DisplayName("recharge() 메서드")
-    class RechargeMethod {
-
-        @Nested
-        @DisplayName("0 이하의 정수로 충전할 경우")
-        class 영_이하의_정수로_충전할_경우 {
-
-            static Stream<Integer> invalidPoints() {
-                return Stream.of(0, -1, -100, -10000, Integer.MIN_VALUE);
-            }
-
-            @ParameterizedTest
-            @MethodSource("invalidPoints")
-            @DisplayName("예외를 발생시킨다")
-            void 예외_발생(int invalidPoint) {
-                // given
-                UserPoint userPoint = UserPoint.create(new UserId("1"));
 
-                // when & then
-                assertThatThrownBy(() -> userPoint.charge(invalidPoint))
-                        .isInstanceOf(IllegalArgumentException.class)
-                        .hasMessage("0보다 작거나 같은 포인트를 충전할 수 없습니다.");
-            }
-        }
-    }
 }
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java
deleted file mode 100644
index d8b7ada81..000000000
--- a/core/domain/src/test/java/com/loopers/core/domain/user/UserTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.loopers.core.domain.user;
-
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-
-class UserTest {
-
-    @Nested
-    @DisplayName("create() 메서드")
-    class CreateTest {
-
-        @Nested
-        @DisplayName("id값이 null이라면")
-        class IdIsNull {
-
-            @Test
-            @DisplayName("IllegalArgumentException이 발생한다.")
-            void shouldThrowIllegalArgumentException_whenIdIsNull() {
-
-            }
-        }
-    }
-}
diff --git a/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserPointBalanceTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserPointBalanceTest.java
index 9e1387383..655def895 100644
--- a/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserPointBalanceTest.java
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserPointBalanceTest.java
@@ -22,6 +22,10 @@ class Constructor {
         @DisplayName("유효한 값인 경우")
         class 유효한_값인_경우 {
 
+            static Stream<Integer> validPoints() {
+                return Stream.of(0, 1, 100, 10000, Integer.MAX_VALUE);
+            }
+
             @ParameterizedTest
             @MethodSource("validPoints")
             @DisplayName("UserPointBalance 객체를 생성한다")
@@ -32,16 +36,16 @@ class 유효한_값인_경우 {
                 // then
                 assertThat(pointBalance.value()).isEqualTo(validPoint);
             }
-
-            static Stream<Integer> validPoints() {
-                return Stream.of(0, 1, 100, 10000, Integer.MAX_VALUE);
-            }
         }
 
         @Nested
         @DisplayName("value가 음수인 경우")
         class value가_음수인_경우 {
 
+            static Stream<Integer> invalidPoints() {
+                return Stream.of(-1, -100, -10000, Integer.MIN_VALUE);
+            }
+
             @ParameterizedTest
             @MethodSource("invalidPoints")
             @DisplayName("예외를 발생시킨다")
@@ -51,10 +55,6 @@ class value가_음수인_경우 {
                         .isInstanceOf(IllegalArgumentException.class)
                         .hasMessage("사용자 포인트의 잔액은 0보다는 커야합니다.");
             }
-
-            static Stream<Integer> invalidPoints() {
-                return Stream.of(-1, -100, -10000, Integer.MIN_VALUE);
-            }
         }
     }
 
@@ -72,4 +72,37 @@ class InitMethod {
             assertThat(pointBalance.value()).isZero();
         }
     }
+
+    @Nested
+    @DisplayName("add() 메서드")
+    class AddMethod {
+
+        @Nested
+        @DisplayName("양수 포인트를 추가하는 경우")
+        class positiveAdd {
+
+            @Test
+            @DisplayName("포인트가 추가된다.")
+            void addPoint() {
+                UserPointBalance userPointBalance = new UserPointBalance(100);
+                UserPointBalance addBalance = userPointBalance.add(10);
+                assertThat(addBalance.value()).isEqualTo(110);
+            }
+        }
+
+        @Nested
+        @DisplayName("음수 포인트를 추가하는 경우")
+        class negativeAdd {
+
+            @Test
+            @DisplayName("예외가 발생한다.")
+            void throwException() {
+                UserPointBalance userPointBalance = new UserPointBalance(100);
+                assertThatThrownBy(() -> userPointBalance.add(-1))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("음수 포인트를 충전할 수 없습니다.");
+            }
+        }
+
+    }
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java
index a5692f836..6826b7b40 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/entity/UserPointEntity.java
@@ -13,6 +13,7 @@
 import lombok.NoArgsConstructor;
 
 import java.time.LocalDateTime;
+import java.util.Objects;
 import java.util.Optional;
 
 @Entity
@@ -44,7 +45,11 @@ public static UserPointEntity from(UserPoint userPoint) {
                 Optional.ofNullable(userPoint.getId().value())
                         .map(Long::parseLong)
                         .orElse(null),
-                Long.parseLong(userPoint.getUserId().value()),
+                Objects.requireNonNull(
+                        Optional.ofNullable(userPoint.getUserId().value())
+                                .map(Long::parseLong)
+                                .orElse(null)
+                ),
                 userPoint.getBalance().value(),
                 userPoint.getCreatedAt().value(),
                 userPoint.getUpdatedAt().value()
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
index ca6d0174b..bcb90f1b1 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserPointRepositoryImpl.java
@@ -9,6 +9,7 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Repository;
 
+import java.util.Objects;
 import java.util.Optional;
 
 @Repository
@@ -24,14 +25,24 @@ public UserPoint save(UserPoint userPoint) {
 
     @Override
     public UserPoint getByUserId(UserId userId) {
-        return repository.findByUserId(Long.parseLong(userId.value()))
+        return repository.findByUserId(
+                        Objects.requireNonNull(Optional.ofNullable(userId.value())
+                                .map(Long::parseLong)
+                                .orElse(null)
+                        )
+                )
                 .orElseThrow(() -> NotFoundException.withName("사용자 포인트"))
                 .to();
     }
 
     @Override
     public Optional<UserPoint> findByUserId(UserId userId) {
-        return repository.findByUserId(Long.parseLong(userId.value()))
+        return repository.findByUserId(
+                        Objects.requireNonNull(Optional.ofNullable(userId.value())
+                                .map(Long::parseLong)
+                                .orElse(null)
+                        )
+                )
                 .map(UserPointEntity::to);
     }
 }
diff --git a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
index 42fc94650..8b0e6daba 100644
--- a/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/impl/UserRepositoryImpl.java
@@ -10,6 +10,7 @@
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Repository;
 
+import java.util.Objects;
 import java.util.Optional;
 
 @Repository
@@ -25,7 +26,12 @@ public User save(User user) {
 
     @Override
     public Optional<User> findById(UserId userId) {
-        return repository.findById(Long.parseLong(userId.value()))
+        return repository.findById(
+                        Objects.requireNonNull(Optional.ofNullable(userId.value())
+                                .map(Long::parseLong)
+                                .orElse(null)
+                        )
+                )
                 .map(UserEntity::to);
     }
 
