diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md
index a8ace53e5..b81d438ed 100644
--- a/.codeguide/loopers-1-week.md
+++ b/.codeguide/loopers-1-week.md
@@ -6,40 +6,55 @@
 
 **🧱 단위 테스트**
 
-- [ ]  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 테스트**
 
-- [ ]  회원 가입이 성공할 경우, 생성된 유저 정보를 응답으로 반환한다.
-- [ ]  회원 가입 시에 성별이 없을 경우, `400 Bad Request` 응답을 반환한다.
+- [x]  회원 가입이 성공할 경우, 생성된 유저 정보를 응답으로 반환한다.
+- [x]  회원 가입 시에 성별이 없을 경우, `400 Bad Request` 응답을 반환한다.
 
 ### 내 정보 조회
 
 **🔗 통합 테스트**
 
-- [ ]  해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.
-- [ ]  해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.
+- [x]  해당 ID 의 회원이 존재할 경우, 회원 정보가 반환된다.
+- [x]  해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.
 
 **🌐 E2E 테스트**
 
-- [ ]  내 정보 조회에 성공할 경우, 해당하는 유저 정보를 응답으로 반환한다.
-- [ ]  존재하지 않는 ID 로 조회할 경우, `404 Not Found` 응답을 반환한다.
+- [x]  내 정보 조회에 성공할 경우, 해당하는 유저 정보를 응답으로 반환한다.
+- [x]  존재하지 않는 ID 로 조회할 경우, `404 Not Found` 응답을 반환한다.
 
 ### 포인트 조회
 
 **🔗 통합 테스트**
 
-- [ ]  해당 ID 의 회원이 존재할 경우, 보유 포인트가 반환된다.
-- [ ]  해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.
+- [x]  해당 ID 의 회원이 존재할 경우, 보유 포인트가 반환된다.
+- [x]  해당 ID 의 회원이 존재하지 않을 경우, null 이 반환된다.
 
 **🌐 E2E 테스트**
 
-- [ ]  포인트 조회에 성공할 경우, 보유 포인트를 응답으로 반환한다.
-- [ ]  `X-USER-ID` 헤더가 없을 경우, `400 Bad Request` 응답을 반환한다.
+- [x]  포인트 조회에 성공할 경우, 보유 포인트를 응답으로 반환한다.
+- [x]  `X-USER-ID` 헤더가 없을 경우, `400 Bad Request` 응답을 반환한다.
+
+### 포인트 충전
+
+**🧱 단위 테스트**
+
+- [x]  0 이하의 정수로 포인트를 충전 시 실패한다.
+
+**🔗 통합 테스트**
+
+- [x]  존재하지 않는 유저 ID 로 충전을 시도한 경우, 실패한다.
+
+**🌐 E2E 테스트**
+
+- [x]  존재하는 유저가 1000원을 충전할 경우, 충전된 보유 총량을 응답으로 반환한다.
+- [x]  존재하지 않는 유저로 요청할 경우, `404 Not Found` 응답을 반환한다.
diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts
index 03ce68f02..a5f94f5ad 100644
--- a/apps/commerce-api/build.gradle.kts
+++ b/apps/commerce-api/build.gradle.kts
@@ -1,22 +1,24 @@
 dependencies {
     // add-ons
-    implementation(project(":modules:jpa"))
+    implementation(project(":core:infra:database:mysql:mysql-config"))
     implementation(project(":modules:redis"))
     implementation(project(":supports:jackson"))
     implementation(project(":supports:logging"))
     implementation(project(":supports:monitoring"))
 
+    //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"]}")
 
-    // querydsl
-    annotationProcessor("com.querydsl:querydsl-apt::jakarta")
-    annotationProcessor("jakarta.persistence:jakarta.persistence-api")
-    annotationProcessor("jakarta.annotation:jakarta.annotation-api")
-
     // test-fixtures
-    testImplementation(testFixtures(project(":modules:jpa")))
-    testImplementation(testFixtures(project(":modules:redis")))
+    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/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 60%
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..9b6fe4638 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,13 +1,15 @@
-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 com.loopers.core.domain.error.NotFoundException;
 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.MissingRequestHeaderException;
 import org.springframework.web.bind.MissingServletRequestParameterException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -23,10 +25,11 @@
 @RestControllerAdvice
 @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 +38,22 @@ 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
+    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
@@ -43,7 +61,14 @@ 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
+    public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingRequestHeaderException e) {
+        String name = e.getHeaderName();
+        String message = String.format("필수 요청 헤더 '%s'가 누락되었습니다.", name);
+        return failureResponse(ApiErrorType.BAD_REQUEST, message);
     }
 
     @ExceptionHandler
@@ -53,15 +78,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 + "]";
             }
 
@@ -69,26 +94,26 @@ 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 메세지 규격을 확인해주세요.";
         }
 
-        return failureResponse(ErrorType.BAD_REQUEST, errorMessage);
+        return failureResponse(ApiErrorType.BAD_REQUEST, errorMessage);
     }
 
     @ExceptionHandler
@@ -96,21 +121,26 @@ 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);
+    }
+
+    @ExceptionHandler
+    public ResponseEntity<ApiResponse<?>> handleNotFound(NotFoundException e) {
+        return failureResponse(ApiErrorType.NOT_FOUND, e.getMessage());
     }
 
     private String extractMissingParameter(String message) {
@@ -119,8 +149,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/UserV1Api.java b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
new file mode 100644
index 000000000..512c48949
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Api.java
@@ -0,0 +1,73 @@
+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.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;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Objects;
+
+import static com.loopers.application.api.user.UserV1Dto.*;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/api/v1/users")
+public class UserV1Api implements UserV1ApiSpec {
+
+    private final JoinUserService joinUserService;
+    private final UserQueryService userQueryService;
+    private final UserPointQueryService userPointQueryService;
+    private final UserPointService userPointService;
+
+    @Override
+    @GetMapping("/{identifier}")
+    public ApiResponse<GetUserResponse> getUser(@PathVariable String identifier) {
+        User user = userQueryService.getUserByIdentifier(new GetUserQuery(identifier));
+        if (Objects.isNull(user)) {
+            throw NotFoundException.withName("사용자");
+        }
+
+        return ApiResponse.success(GetUserResponse.from(user));
+    }
+
+    @Override
+    @PostMapping("/join")
+    public ApiResponse<JoinUserResponse> joinUser(
+            @RequestBody @Valid JoinUserRequest request
+    ) {
+        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));
+    }
+
+    @Override
+    @PostMapping("/points/charge")
+    public ApiResponse<UserPointChargeResponse> userPointCharge(
+            @RequestBody @Valid 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
new file mode 100644
index 000000000..b1f6899fb
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1ApiSpec.java
@@ -0,0 +1,39 @@
+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;
+
+import static com.loopers.application.api.user.UserV1Dto.*;
+
+@Tag(name = "User V1 API", description = "User API 입니다.")
+public interface UserV1ApiSpec {
+
+    @Operation(
+            summary = "사용자 조회",
+            description = "ID로 사용자를 조회합니다."
+    )
+    ApiResponse<GetUserResponse> getUser(
+            @Schema(name = "사용자 ID", description = "조회할 사용자 ID")
+            String identifier
+    );
+
+    @Operation(
+            summary = "사용자 회원가입",
+            description = "사용자가 회원가입합니다."
+    )
+    ApiResponse<JoinUserResponse> joinUser(JoinUserRequest request);
+
+    @Operation(
+            summary = "사용자 포인트 조회",
+            description = "사용자의 포인트를 조회합니다."
+    )
+    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
new file mode 100644
index 000000000..40d6a2e04
--- /dev/null
+++ b/apps/commerce-api/src/main/java/com/loopers/application/api/user/UserV1Dto.java
@@ -0,0 +1,89 @@
+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 com.loopers.core.service.user.command.UserPointChargeCommand;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Positive;
+
+import java.time.format.DateTimeFormatter;
+
+public class UserV1Dto {
+
+    public record JoinUserRequest(
+            @NotBlank String identifier,
+            @NotBlank String email,
+            @NotBlank String birthday,
+            @NotBlank String gender
+    ) {
+        public JoinUserCommand toCommand() {
+            return new JoinUserCommand(identifier, email, birthday, gender);
+        }
+    }
+
+    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(
+            String userId,
+            String identifier,
+            String email,
+            String birthday,
+            String gender
+    ) {
+
+        public static JoinUserResponse from(User user) {
+            return new JoinUserResponse(
+                    user.getUserId().value(),
+                    user.getIdentifier().value(),
+                    user.getEmail().value(),
+                    user.getBirthDay().value().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
+                    user.getGender().name()
+            );
+        }
+    }
+
+    public record GetUserPointResponse(
+            int balance
+    ) {
+
+        public static GetUserPointResponse from(UserPoint userPoint) {
+            return new GetUserPointResponse(userPoint.getBalance().value());
+        }
+    }
+
+    public record UserPointChargeRequest(
+            @Positive 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/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/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..0fce31016
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/charge-user-point.http
@@ -0,0 +1,7 @@
+POST {{commerce-api}}/api/v1/users/points/charge
+Content-Type: application/json
+X-USER-ID: {{identifier}}
+
+{
+  "point": {{point}}
+}
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..fe8a6d139
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/get-user-by-identifier.http
@@ -0,0 +1,2 @@
+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
new file mode 100644
index 000000000..730ce9b07
--- /dev/null
+++ b/apps/commerce-api/src/main/resources/http/get-user-point-by-user-identifier.http
@@ -0,0 +1,2 @@
+GET {{commerce-api}}/api/v1/users/points
+X-USER-ID: {{identifier}}
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/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/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..7eda27c11
--- /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
+    protected TestRestTemplate testRestTemplate;
+
+    @Autowired
+    private DatabaseCleanUp databaseCleanUp;
+
+    @AfterEach
+    void databaseCleanUp() {
+        databaseCleanUp.truncateAllTables();
+    }
+}
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 64%
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..2f7e1964e 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,22 @@
-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;
 
-class CoreExceptionTest {
+class ApiExceptionTest {
+
     @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 +27,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/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..c4a49da18
--- /dev/null
+++ b/apps/commerce-api/src/test/java/com/loopers/application/api/user/UserV1ApiApiIntegrationTest.java
@@ -0,0 +1,314 @@
+package com.loopers.application.api.user;
+
+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 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;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+
+class UserV1ApiApiIntegrationTest extends ApiIntegrationTest {
+
+    @Autowired
+    private UserRepository userRepository;
+
+    @Autowired
+    private JoinUserService joinUserService;
+
+    @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);
+            }
+        }
+    }
+
+    @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);
+            }
+        }
+    }
+
+    @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);
+            }
+        }
+    }
+
+    @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/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/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/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/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/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/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..be83cce71
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/common/vo/DeletedAt.java
@@ -0,0 +1,14 @@
+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());
+    }
+
+    public static DeletedAt empty() {
+        return new DeletedAt(null);
+    }
+}
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/error/DomainErrorCode.java b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
new file mode 100644
index 000000000..380684311
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/error/DomainErrorCode.java
@@ -0,0 +1,35 @@
+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자 이하여야 합니다."),
+    INVALID_EMAIL_FORMAT(1003, "유효하지 않은 이메일 형식입니다."),
+    INVALID_BIRTHDATE_FORMAT(1004, "생년월일은 yyyy-MM-dd 형식이어야 합니다."),
+    GENDER_IS_MALE_OR_FEMALE(1005, "성별은 MALE 혹은 FEMALE이어야 합니다."),
+    PRESENT_USER_IDENTIFIER(1006, "이미 존재하는 사용자 ID입니다."),
+    NOT_FOUND(1007, "%s를 찾지 못했습니다."),
+    USER_POINT_BALANCE_NON_NEGATIVE(1008, "사용자 포인트의 잔액은 음수가 될 수 없습니다."),
+    CANNOT_CHARGE_POINTS_NEGATIVE(1009, "음수 포인트를 충전할 수 없습니다.");
+
+    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);
+    }
+
+    public static String notFoundMessage(String fieldName) {
+        return String.format(NOT_FOUND.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));
+    }
+}
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..a4d68019d
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/User.java
@@ -0,0 +1,83 @@
+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.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 lombok.Getter;
+
+@Getter
+public class User {
+
+    private final UserId userId;
+
+    private final UserIdentifier identifier;
+
+    private final UserEmail email;
+
+    private final UserBirthDay birthDay;
+
+    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;
+    }
+
+    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()
+        );
+    }
+
+    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/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..88d093dfa
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/UserPoint.java
@@ -0,0 +1,66 @@
+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.AccessLevel;
+import lombok.Builder;
+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;
+
+    @Builder(access = AccessLevel.PRIVATE, toBuilder = true)
+    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);
+    }
+
+    public UserPoint charge(int point) {
+        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/repository/UserPointRepository.java b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
new file mode 100644
index 000000000..5bedd7734
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserPointRepository.java
@@ -0,0 +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
new file mode 100644
index 000000000..68a2942bf
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/repository/UserRepository.java
@@ -0,0 +1,18 @@
+package com.loopers.core.domain.user.repository;
+
+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;
+
+public interface UserRepository {
+
+    User save(User user);
+
+    Optional<User> findById(UserId userId);
+
+    Optional<User> findByIdentifier(UserIdentifier identifier);
+
+    User getByIdentifier(UserIdentifier identifier);
+}
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..f36216e02
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/type/UserGender.java
@@ -0,0 +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;
+
+    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
new file mode 100644
index 000000000..665706a42
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserBirthDay.java
@@ -0,0 +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/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..a6199811d
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserEmail.java
@@ -0,0 +1,30 @@
+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);
+        validate(value);
+        this.value = value;
+    }
+
+    public static UserEmail create(String email) {
+        return new UserEmail(email);
+    }
+
+    private static void validateNotNull(String email) {
+        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/UserId.java b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java
new file mode 100644
index 000000000..3e8d67714
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserId.java
@@ -0,0 +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/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..84070ba29
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserIdentifier.java
@@ -0,0 +1,44 @@
+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) {
+        validateNotNull(value);
+
+        this.value = value;
+    }
+
+    public static UserIdentifier create(String value) {
+        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());
+        }
+
+        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/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..6fbab390d
--- /dev/null
+++ b/core/domain/src/main/java/com/loopers/core/domain/user/vo/UserPointBalance.java
@@ -0,0 +1,28 @@
+package com.loopers.core.domain.user.vo;
+
+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_NON_NEGATIVE.getMessage());
+        }
+
+        this.value = value;
+    }
+
+    public static UserPointBalance init() {
+        return new UserPointBalance(0);
+    }
+
+    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/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/UserPointTest.java b/core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java
new file mode 100644
index 000000000..43a177ad5
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/UserPointTest.java
@@ -0,0 +1,9 @@
+package com.loopers.core.domain.user;
+
+import org.junit.jupiter.api.DisplayName;
+
+@DisplayName("UserPoint")
+class UserPointTest {
+
+
+}
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/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/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/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..d04c689c4
--- /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/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..655def895
--- /dev/null
+++ b/core/domain/src/test/java/com/loopers/core/domain/user/vo/UserPointBalanceTest.java
@@ -0,0 +1,108 @@
+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 유효한_값인_경우 {
+
+            static Stream<Integer> validPoints() {
+                return Stream.of(0, 1, 100, 10000, Integer.MAX_VALUE);
+            }
+
+            @ParameterizedTest
+            @MethodSource("validPoints")
+            @DisplayName("UserPointBalance 객체를 생성한다")
+            void 객체_생성(int validPoint) {
+                // when
+                UserPointBalance pointBalance = new UserPointBalance(validPoint);
+
+                // then
+                assertThat(pointBalance.value()).isEqualTo(validPoint);
+            }
+        }
+
+        @Nested
+        @DisplayName("value가 음수인 경우")
+        class value가_음수인_경우 {
+
+            static Stream<Integer> invalidPoints() {
+                return Stream.of(-1, -100, -10000, Integer.MIN_VALUE);
+            }
+
+            @ParameterizedTest
+            @MethodSource("invalidPoints")
+            @DisplayName("예외를 발생시킨다")
+            void 예외_발생(int invalidPoint) {
+                // when & then
+                assertThatThrownBy(() -> new UserPointBalance(invalidPoint))
+                        .isInstanceOf(IllegalArgumentException.class)
+                        .hasMessage("사용자 포인트의 잔액은 0보다는 커야합니다.");
+            }
+        }
+    }
+
+    @Nested
+    @DisplayName("init() 메서드")
+    class InitMethod {
+
+        @Test
+        @DisplayName("포인트가 0인 UserPointBalance를 생성한다")
+        void 초기_포인트_생성() {
+            // when
+            UserPointBalance pointBalance = UserPointBalance.init();
+
+            // then
+            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/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/modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/DataSourceConfig.java
similarity index 93%
rename from modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java
rename to core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/DataSourceConfig.java
index 723ffa6a4..5686ed667 100644
--- a/modules/jpa/src/main/java/com/loopers/config/jpa/DataSourceConfig.java
+++ b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/DataSourceConfig.java
@@ -1,4 +1,4 @@
-package com.loopers.config.jpa;
+package com.loopers.core.infra.database.mysql.config;
 
 import com.zaxxer.hikari.HikariConfig;
 import com.zaxxer.hikari.HikariDataSource;
@@ -10,6 +10,7 @@
 
 @Configuration
 class DataSourceConfig {
+
     @Bean
     @ConfigurationProperties(prefix = "datasource.mysql-jpa.main")
     HikariConfig mySqlMainHikariConfig() {
diff --git a/modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/JpaConfig.java
similarity index 76%
rename from modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java
rename to core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/JpaConfig.java
index 7fad5872b..1a8ed0565 100644
--- a/modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java
+++ b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/JpaConfig.java
@@ -1,4 +1,4 @@
-package com.loopers.config.jpa;
+package com.loopers.core.infra.database.mysql.config;
 
 import org.springframework.boot.autoconfigure.domain.EntityScan;
 import org.springframework.context.annotation.Configuration;
@@ -8,6 +8,6 @@
 @Configuration
 @EnableTransactionManagement
 @EntityScan({"com.loopers"})
-@EnableJpaRepositories({"com.loopers.infrastructure"})
+@EnableJpaRepositories({"com.loopers.core.infra.database.mysql"})
 public class JpaConfig {
 }
diff --git a/modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/QueryDslConfig.java
similarity index 89%
rename from modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java
rename to core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/QueryDslConfig.java
index 2b95dd37a..5efec726b 100644
--- a/modules/jpa/src/main/java/com/loopers/config/jpa/QueryDslConfig.java
+++ b/core/infra/database/mysql/mysql-config/src/main/java/com/loopers/core/infra/database/mysql/config/QueryDslConfig.java
@@ -1,4 +1,4 @@
-package com.loopers.config.jpa;
+package com.loopers.core.infra.database.mysql.config;
 
 import com.querydsl.jpa.impl.JPAQueryFactory;
 import jakarta.persistence.EntityManager;
diff --git a/modules/jpa/src/main/resources/jpa.yml b/core/infra/database/mysql/mysql-config/src/main/resources/jpa.yml
similarity index 100%
rename from modules/jpa/src/main/resources/jpa.yml
rename to core/infra/database/mysql/mysql-config/src/main/resources/jpa.yml
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..356e58fd4
--- /dev/null
+++ b/core/infra/database/mysql/mysql-core/build.gradle.kts
@@ -0,0 +1,16 @@
+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: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")
+}
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/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/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/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..b84067f76
--- /dev/null
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserJpaRepository.java
@@ -0,0 +1,11 @@
+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;
+
+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/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..95952b3c6
--- /dev/null
+++ b/core/infra/database/mysql/user-mysql/src/main/java/com/loopers/core/infra/database/mysql/user/UserPointJpaRepository.java
@@ -0,0 +1,11 @@
+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;
+
+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/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..49ad931f5
--- /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,82 @@
+package com.loopers.core.infra.database.mysql.user.entity;
+
+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.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Optional;
+
+@Entity
+@Table(
+        name = "users",
+        indexes = {
+                @Index(name = "idx_user_identifier", columnList = "identifier"),
+                @Index(name = "idx_user_email", columnList = "email")
+        }
+)
+@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;
+
+    private String email;
+
+    private LocalDate birthDay;
+
+    private String gender;
+
+    private LocalDateTime createdAt;
+
+    private LocalDateTime updatedAt;
+
+    private LocalDateTime deletedAt;
+
+    public static UserEntity from(User user) {
+        return UserEntity.builder()
+                .id(
+                        Optional.ofNullable(user.getUserId().value())
+                                .map(Long::parseLong)
+                                .orElse(null)
+                )
+                .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/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..6826b7b40
--- /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,68 @@
+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.Objects;
+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),
+                Objects.requireNonNull(
+                        Optional.ofNullable(userPoint.getUserId().value())
+                                .map(Long::parseLong)
+                                .orElse(null)
+                ),
+                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..bcb90f1b1
--- /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,48 @@
+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.Objects;
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class UserPointRepositoryImpl implements UserPointRepository {
+
+    private final UserPointJpaRepository repository;
+
+    @Override
+    public UserPoint save(UserPoint userPoint) {
+        return repository.save(UserPointEntity.from(userPoint)).to();
+    }
+
+    @Override
+    public UserPoint getByUserId(UserId userId) {
+        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(
+                        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
new file mode 100644
index 000000000..8b0e6daba
--- /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,50 @@
+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;
+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;
+import org.springframework.stereotype.Repository;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@Repository
+@RequiredArgsConstructor
+public class UserRepositoryImpl implements UserRepository {
+
+    private final UserJpaRepository repository;
+
+    @Override
+    public User save(User user) {
+        return repository.save(UserEntity.from(user)).to();
+    }
+
+    @Override
+    public Optional<User> findById(UserId userId) {
+        return repository.findById(
+                        Objects.requireNonNull(Optional.ofNullable(userId.value())
+                                .map(Long::parseLong)
+                                .orElse(null)
+                        )
+                )
+                .map(UserEntity::to);
+    }
+
+    @Override
+    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/build.gradle.kts b/core/service/user-service/build.gradle.kts
new file mode 100644
index 000000000..9292dba55
--- /dev/null
+++ b/core/service/user-service/build.gradle.kts
@@ -0,0 +1,12 @@
+dependencies {
+    implementation(project(":core:domain"))
+
+    // 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
new file mode 100644
index 000000000..5bbb9052c
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/JoinUserService.java
@@ -0,0 +1,43 @@
+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.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.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)
+                .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());
+
+        User savedUser = userRepository.save(User.create(userIdentifier, userEmail, userBirthDay, userGender));
+        userPointRepository.save(UserPoint.create(savedUser.getUserId()));
+
+        return savedUser;
+    }
+}
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/UserPointService.java b/core/service/user-service/src/main/java/com/loopers/core/service/user/UserPointService.java
new file mode 100644
index 000000000..9f7ac4560
--- /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(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
new file mode 100644
index 000000000..f4ab00586
--- /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 getUserByIdentifier(GetUserQuery query) {
+        return userRepository.findByIdentifier(UserIdentifier.create(query.getIdentifier()))
+                .orElse(null);
+    }
+}
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
new file mode 100644
index 000000000..379770ac0
--- /dev/null
+++ b/core/service/user-service/src/main/java/com/loopers/core/service/user/command/JoinUserCommand.java
@@ -0,0 +1,22 @@
+package com.loopers.core.service.user.command;
+
+import lombok.Getter;
+
+@Getter
+public class JoinUserCommand {
+
+    private final String userIdentifier;
+
+    private final String email;
+
+    private final String birthDay;
+
+    private final String gender;
+
+    public JoinUserCommand(String userIdentifier, String email, String birthDay, String gender) {
+        this.userIdentifier = userIdentifier;
+        this.email = email;
+        this.birthDay = birthDay;
+        this.gender = gender;
+    }
+}
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/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/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/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
new file mode 100644
index 000000000..248e95e94
--- /dev/null
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/JoinUserServiceIntegrationTest.java
@@ -0,0 +1,83 @@
+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.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;
+import org.springframework.beans.factory.annotation.Autowired;
+
+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());
+                });
+            }
+        }
+
+        @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입니다.");
+            }
+        }
+    }
+}
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..7e47a906c
--- /dev/null
+++ b/core/service/user-service/src/test/java/com/loopers/core/service/user/UserPointQueryServiceIntegrationTest.java
@@ -0,0 +1,84 @@
+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의_회원이_존재할_경우 {
+
+            User savedUser;
+            UserPoint savedUserPoint;
+
+            @BeforeEach
+            void setUp() {
+                savedUser = userRepository.save(
+                        User.create(
+                                UserIdentifier.create("kilian"),
+                                UserEmail.create("kilian@gmail.com"),
+                                UserBirthDay.create("1997-10-08"),
+                                UserGender.create("MALE")
+                        )
+                );
+
+                savedUserPoint = userPointRepository.save(UserPoint.create(savedUser.getUserId()));
+            }
+
+            @Test
+            @DisplayName("보유 포인트가 반환된다.")
+            void 보유_포인트가_반환된다() {
+                UserPoint userPoint = service.getByUserIdentifier(new GetUserPointQuery("kilian"));
+
+                SoftAssertions.assertSoftly(softly -> {
+                    softly.assertThat(userPoint).isNotNull();
+                    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/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);
+            }
+        }
+    }
+
+}
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..099d6427f
--- /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.getUserByIdentifier(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.getUserByIdentifier(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);
+                });
+            }
+        }
+
+    }
+}
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..34f9eeeb0
--- /dev/null
+++ b/core/service/user-service/src/test/resources/application-test.yml
@@ -0,0 +1,4 @@
+spring:
+  jpa:
+    hibernate:
+      ddl-auto: create-drop
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/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/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/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 c99fb6360..c114398f8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,14 +1,19 @@
-rootProject.name = "loopers-java-spring-template"
+rootProject.name = "e-commerce"
 
 include(
     ":apps:commerce-api",
     ":apps:commerce-streamer",
-    ":modules:jpa",
     ":modules:redis",
     ":modules:kafka",
     ":supports:jackson",
     ":supports:logging",
     ":supports:monitoring",
+    ":core:domain",
+    ":core:infra:database:mysql:mysql-config",
+    ":core:infra:database:mysql:mysql-core",
+    ":core:infra:database:mysql:user-mysql",
+    ":core:service:user-service",
+    ":core:common"
 )
 
 // configurations
diff --git a/supports/jackson/build.gradle.kts b/supports/jackson/build.gradle.kts
index f73320711..c68413b78 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-autoconfigure")
+
     // jackson
     implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
     implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
