Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .cursor/rules/apiresponse-class.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
description: Structure of ApiResponse class.
globs: **/src/main/java/com/study/bitly/ApiResponse.java
---
6 changes: 6 additions & 0 deletions .cursor/rules/dto-conventions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
description: Sets standards for Data Transfer Objects (DTOs), typically records, including parameter validation in compact canonical constructors.
globs: **/src/main/java/com/study/bitly/dtos/*.java
---
- Must be of type record, unless specified in a prompt otherwise.
- Must specify a compact canonical constructor to validate input parameter data (not null, blank, etc., as appropriate).
9 changes: 9 additions & 0 deletions .cursor/rules/entity-class-conventions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
description: Sets the standards for entity class design including annotations, ID generation strategies, and relationship configurations for database interaction.
globs: **/src/main/java/com/example/entities/*.java
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix glob to match this project’s package.

The current glob points to com.example; align to com.study.bitly.

-globs: **/src/main/java/com/example/entities/*.java
+globs: **/src/main/java/com/study/bitly/entities/**/*.java
📝 Committable suggestion

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

Suggested change
globs: **/src/main/java/com/example/entities/*.java
globs: **/src/main/java/com/study/bitly/entities/**/*.java
🤖 Prompt for AI Agents
.cursor/rules/entity-class-conventions.mdc around line 3: the glob currently
targets com.example package (globs:
**/src/main/java/com/example/entities/*.java) but this project uses
com.study.bitly; update the glob to point to the correct package path (e.g.,
replace com/example with com/study/bitly) so the rule matches the project's
entity classes.

---
- Must annotate entity classes with @Entity.
- Must annotate entity classes with @Data (from Lombok), unless specified in a prompt otherwise.
- Must annotate entity ID with @Id and @GeneratedValue(strategy=GenerationType.IDENTITY).
- Must use FetchType.LAZY for relationships, unless specified in a prompt otherwise.
- Annotate entity properties properly according to best practices, e.g., @Size, @NotEmpty, @Email, etc.
Comment on lines +6 to +9
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid Lombok @DaTa on JPA entities; tighten guidance.

@DaTa can generate problematic equals/hashCode/toString for entities and relationships.

 - Must annotate entity classes with @Entity.
-- Must annotate entity classes with @Data (from Lombok), unless specified in a prompt otherwise.
+- Prefer Lombok @Getter/@Setter and explicitly manage @EqualsAndHashCode(onlyExplicitlyIncluded = true) and @ToString(exclude = {"relations"}).
 - Must annotate entity ID with @Id and @GeneratedValue(strategy=GenerationType.IDENTITY).
 - Must use FetchType.LAZY for relationships, unless specified in a prompt otherwise.
 - Annotate entity properties properly according to best practices, e.g., @Size, @NotEmpty, @Email, etc.
+- Provide a protected no-args constructor for JPA; consider auditing fields (@CreatedDate, @LastModifiedDate) with @EntityListeners.
📝 Committable suggestion

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

Suggested change
- Must annotate entity classes with @Data (from Lombok), unless specified in a prompt otherwise.
- Must annotate entity ID with @Id and @GeneratedValue(strategy=GenerationType.IDENTITY).
- Must use FetchType.LAZY for relationships, unless specified in a prompt otherwise.
- Annotate entity properties properly according to best practices, e.g., @Size, @NotEmpty, @Email, etc.
- Must annotate entity classes with @Entity.
- Prefer Lombok @Getter/@Setter and explicitly manage @EqualsAndHashCode(onlyExplicitlyIncluded = true) and @ToString(exclude = {"relations"}).
- Must annotate entity ID with @Id and @GeneratedValue(strategy=GenerationType.IDENTITY).
- Must use FetchType.LAZY for relationships, unless specified in a prompt otherwise.
- Annotate entity properties properly according to best practices, e.g., @Size, @NotEmpty, @Email, etc.
- Provide a protected no-args constructor for JPA; consider auditing fields (@CreatedDate, @LastModifiedDate) with @EntityListeners.
🤖 Prompt for AI Agents
In .cursor/rules/entity-class-conventions.mdc around lines 6-9, remove the
blanket requirement to annotate JPA entity classes with Lombok @Data and instead
state that @Data is discouraged for entities because it generates
equals/hashCode/toString that can break persistence/relationships; require
either explicit getters/setters or limited Lombok usage (e.g., @Getter/@Setter
only) and, if Lombok equals/hashCode/toString are used, require explicit
configuration such as @EqualsAndHashCode(onlyExplicitlyIncluded = true) and
@ToString(exclude = "relationships") or implementation based on identifier
logic; keep the other rules (use @Id with @GeneratedValue(strategy =
GenerationType.IDENTITY), default FetchType.LAZY for relationships, and apply
JSR-303 annotations like @Size/@NotEmpty/@Email) but add a short note to prefer
explicit/controlled implementations for equals/hashCode/toString on entities.

8 changes: 8 additions & 0 deletions .cursor/rules/general-java-development-practices.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
description: Applies general coding standards and best practices for Java development, focusing on SOLID, DRY, KISS, and YAGNI principles, along with OWASP security guidelines.
globs: **/*.java
---
- You are an experienced Senior Java Developer.
- You always adhere to SOLID principles, DRY principles, KISS principles and YAGNI principles.
- You always follow OWASP best practices.
- You always break tasks down to smallest units and approach solving any task in a step-by-step manner.
4 changes: 4 additions & 0 deletions .cursor/rules/globalexceptionhandler-class.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
description: Structure of GlobalExceptionHandler class.
globs: **/src/main/java/com/study/bitly/GlobalExceptionHandler.java
---
10 changes: 10 additions & 0 deletions .cursor/rules/repository-class-conventions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
description: Governs the structure and functionality of repository classes, emphasizing the use of JpaRepository, JPQL queries, and EntityGraphs to prevent N+1 problems.
globs: **/src/main/java/com/study/bitly/repositories/*.java
---
- Must annotate repository classes with @Repository.
- Repository classes must be of type interface.
- Must extend JpaRepository with the entity and entity ID as parameters, unless specified in a prompt otherwise.
- Must use JPQL for all @Query type methods, unless specified in a prompt otherwise.
- Must use @EntityGraph(attributePaths={"relatedEntity"}) in relationship queries to avoid the N+1 problem.
Comment on lines +8 to +9
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make query guidance practical (JPQL vs native; N+1 mitigation).

Hard “must”s here are too rigid; prefer guidance with options.

Apply:

- Must use JPQL for all @Query type methods, unless specified in a prompt otherwise.
- Must use @EntityGraph(attributePaths={"relatedEntity"}) in relationship queries to avoid the N+1 problem.
+ Prefer JPQL for @Query methods; use native queries only when required (e.g., DB-specific features or performance).
+ To mitigate N+1, use @EntityGraph, fetch joins, or batching (hibernate.default_batch_fetch_size) as appropriate and verified by query plans.
📝 Committable suggestion

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

Suggested change
- Must use JPQL for all @Query type methods, unless specified in a prompt otherwise.
- Must use @EntityGraph(attributePaths={"relatedEntity"}) in relationship queries to avoid the N+1 problem.
- Prefer JPQL for @Query methods; use native queries only when required (e.g., DB-specific features or performance).
- To mitigate N+1, use @EntityGraph, fetch joins, or batching (hibernate.default_batch_fetch_size) as appropriate and verified by query plans.
🤖 Prompt for AI Agents
.cursor/rules/repository-class-conventions.mdc around lines 8-9: the two rules
are too rigid; replace the hard "Must" statements with practical guidance that
(1) prefers JPQL for @Query methods but permits native SQL when justified
(performance, DB-specific features) and requires a brief justification comment
on why native SQL is used, and (2) recommends using
@EntityGraph(attributePaths={"relatedEntity"}) or explicit JOIN FETCH clauses to
mitigate N+1 problems and note that either approach is acceptable depending on
the use-case; update the wording to "Prefer" or "Recommended" and add a short
note about documenting exceptions.

- Must use a DTO as The data container for multi-join queries with @Query.
11 changes: 11 additions & 0 deletions .cursor/rules/restcontroller-conventions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
description: Specifies standards for RestController classes, including API route mappings, HTTP method annotations, dependency injection, and error handling with ApiResponse and GlobalExceptionHandler.
globs: **/src/main/java/com/study/controllers/*.java
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix glob path to match package (controllers under com.study.bitly)

Current glob misses the actual package and nested packages.

-globs: **/src/main/java/com/study/controllers/*.java
+globs: **/src/main/java/com/study/bitly/controllers/**/*.java
📝 Committable suggestion

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

Suggested change
globs: **/src/main/java/com/study/controllers/*.java
globs: **/src/main/java/com/study/bitly/controllers/**/*.java
🤖 Prompt for AI Agents
.cursor/rules/restcontroller-conventions.mdc around line 3: the glob currently
targets **/src/main/java/com/study/controllers/*.java which misses the actual
package path and nested packages under com.study.bitly; update the glob to
include the bitly segment and nested folders (for example use
**/src/main/java/com/study/bitly/controllers/**/*.java or a broader
**/src/main/java/com/study/**/controllers/**/*.java) so it matches controller
classes in the correct package and any subpackages.

---
- Must annotate controller classes with @RestController.
- Must specify class-level API routes with @RequestMapping, e.g. ("/api/user").
- Class methods must use best practice HTTP method annotations, e.g, create = @postMapping("/create"), etc.
- All dependencies in class methods must be @Autowired without a constructor, unless specified otherwise.
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Constructor injection over field/method injection

Do not instruct @Autowired on “class methods”; use constructor injection for dependencies.

-- All dependencies in class methods must be @Autowired without a constructor, unless specified otherwise.
+- Prefer constructor injection for controller dependencies; avoid field or method injection.
📝 Committable suggestion

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

Suggested change
- All dependencies in class methods must be @Autowired without a constructor, unless specified otherwise.
Prefer constructor injection for controller dependencies; avoid field or method injection.
🤖 Prompt for AI Agents
In .cursor/rules/restcontroller-conventions.mdc around line 8, the rule
currently tells readers to use @Autowired on class methods; change the guidance
to require constructor injection instead of field or method (setter) injection.
Update the sentence to explicitly forbid @Autowired on fields and methods and
instruct authors to declare dependencies via constructor parameters (or use
framework-supported constructor injection annotations only if necessary), and
add a brief example note recommending final fields assigned from the constructor
for immutability.

- Methods return objects must be of type Response Entity of type ApiResponse.
Comment on lines +7 to +9
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Correct annotations, paths, and return type

  • Use proper case for Spring annotations.
  • Prefer resource‑based paths over verb endpoints.
  • Clarify ResponseEntity<ApiResponse<?>>.
-- Class methods must use best practice HTTP method annotations, e.g, create = @postMapping("/create"), etc.
-+ Use @PostMapping, @GetMapping, @PutMapping, @DeleteMapping with resource-based paths (e.g., @PostMapping("/users")).
-
-- Methods return objects must be of type Response Entity of type ApiResponse.
-+ Methods should return ResponseEntity<ApiResponse<?>>.
📝 Committable suggestion

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

Suggested change
- Class methods must use best practice HTTP method annotations, e.g, create = @postMapping("/create"), etc.
- All dependencies in class methods must be @Autowired without a constructor, unless specified otherwise.
- Methods return objects must be of type Response Entity of type ApiResponse.
- Use @PostMapping, @GetMapping, @PutMapping, @DeleteMapping with resource-based paths (e.g., @PostMapping("/users")).
- All dependencies in class methods must be @Autowired without a constructor, unless specified otherwise.
- Methods should return ResponseEntity<ApiResponse<?>>.
🤖 Prompt for AI Agents
.cursor/rules/restcontroller-conventions.mdc around lines 7 to 9: the rules text
uses incorrect casing for Spring annotations, suggests verb-based endpoints, and
is ambiguous about the return type and dependency injection style; update the
guidance to use proper annotation casing (e.g., @PostMapping, @GetMapping,
@PutMapping, @DeleteMapping), prefer resource-based paths (e.g., POST /resources
to create, GET /resources/{id} to fetch) instead of verb endpoints, require
controller methods that return bodies to use ResponseEntity<ApiResponse<?>> (use
the wildcard generic) and clarify that dependencies should be injected with
@Autowired on fields (not via constructor) unless an exception is documented;
make these three concrete bullet points so implementers know to change
annotations, path conventions, and the return type signature.

- All class method logic must be implemented in a try..catch block(s).
- Caught errors in catch blocks must be handled by the Custom GlobalExceptionHandler class.
Comment on lines +10 to +11
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t require try/catch in every handler; use @RestControllerAdvice

Let controllers be thin; centralize error handling.

-- All class method logic must be implemented in a try..catch block(s).
-- Caught errors in catch blocks must be handled by the Custom GlobalExceptionHandler class.
+- Do not wrap controller methods in try/catch. Throw exceptions and handle them in GlobalExceptionHandler (@RestControllerAdvice).
📝 Committable suggestion

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

Suggested change
- All class method logic must be implemented in a try..catch block(s).
- Caught errors in catch blocks must be handled by the Custom GlobalExceptionHandler class.
- Do not wrap controller methods in try/catch. Throw exceptions and handle them in GlobalExceptionHandler (@RestControllerAdvice).
🤖 Prompt for AI Agents
In .cursor/rules/restcontroller-conventions.mdc around lines 10-11, the rule
currently mandates wrapping all controller method logic in try..catch blocks and
routing caught errors to a GlobalExceptionHandler; update this to remove the
blanket requirement for try/catch in every handler and instead instruct to use
centralized error handling via @RestControllerAdvice (controllers should remain
thin), only using local try/catch for recoverable or context-specific cases, and
direct all uncaught exceptions to the GlobalExceptionHandler for consistent
handling and logging.

11 changes: 11 additions & 0 deletions .cursor/rules/service-class-conventions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
description: Defines the structure and implementation of service classes, enforcing the use of interfaces, ServiceImpl classes, DTOs for data transfer, and transactional management.
globs: **/src/main/java/com/study/services/*.java
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix glob path to match current package structure

The project packages are under com.study.bitly. Update the glob so the rule actually applies.

-globs: **/src/main/java/com/study/services/*.java
+globs: **/src/main/java/com/study/bitly/services/**/*.java
📝 Committable suggestion

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

Suggested change
globs: **/src/main/java/com/study/services/*.java
globs: **/src/main/java/com/study/bitly/services/**/*.java
🤖 Prompt for AI Agents
In .cursor/rules/service-class-conventions.mdc around line 3 the glob currently
targets **/src/main/java/com/study/services/*.java which doesn't match the
project's package path; update the glob to
**/src/main/java/com/study/bitly/services/*.java so the rule applies to files
under com.study.bitly.services; ensure you save the file and verify the rule
picks up existing service classes.

---
- Service classes must be of type interface.
- All service class method implementations must be in ServiceImpl classes that implement the service class.
- All ServiceImpl classes must be annotated with @Service.
- All dependencies in ServiceImpl classes must be @Autowired without a constructor, unless specified otherwise.
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prefer constructor injection; avoid field injection mandate

Field injection (@Autowired on fields) harms testability and immutability. Spring Boot 3 favors constructor injection (no annotation needed for single ctor).

-- All dependencies in ServiceImpl classes must be @Autowired without a constructor, unless specified otherwise.
+- Prefer constructor injection for dependencies (no field injection). Use @Autowired on the constructor only when multiple constructors exist.
📝 Committable suggestion

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

Suggested change
- All dependencies in ServiceImpl classes must be @Autowired without a constructor, unless specified otherwise.
- Prefer constructor injection for dependencies (no field injection). Use @Autowired on the constructor only when multiple constructors exist.
🤖 Prompt for AI Agents
In .cursor/rules/service-class-conventions.mdc around line 8, update the rule
language that currently mandates field @Autowired dependencies in ServiceImpl
classes: replace the hard requirement with guidance to prefer constructor
injection (Spring will auto-wire a single constructor) and only allow field
injection in rare, documented exceptions; state that constructor injection
improves testability and immutability and suggest examples or a note that
constructor parameters require no @Autowired in Spring Boot 3.

- Return objects of ServiceImpl methods should be DTOs, not entity classes, unless absolutely necessary.
- For any logic requiring checking the existence of a record, use the corresponding repository method with an appropriate .orElseThrow lambda method.
- For any multiple sequential database executions, must use @Transactional or transactionTemplate, whichever is appropriate.
11 changes: 11 additions & 0 deletions .cursor/rules/spring-boot-configuration.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
description: Governs application logic design in Spring Boot projects, defining the roles and responsibilities of RestControllers, Services, Repositories, and DTOs.
globs: **/src/main/java/**/*
---
- Framework: Java Spring Boot 3 Maven with Java 17 Dependencies: Spring Web, Spring Data JPA, Thymeleaf, Lombok, PostgreSQL driver
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Rule set mismatches the project stack (Maven/PostgreSQL vs Gradle/MySQL).

Align the rule description with this repository (Gradle + MySQL).

Apply:

-- Framework: Java Spring Boot 3 Maven with Java 17 Dependencies: Spring Web, Spring Data JPA, Thymeleaf, Lombok, PostgreSQL driver
+- Framework: Spring Boot 3 with Gradle (Java 17). Dependencies: Spring Web, Spring Data JPA, Lombok, MySQL driver.
+  - Add Thymeleaf only if server‑side rendering is required.
📝 Committable suggestion

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

Suggested change
- Framework: Java Spring Boot 3 Maven with Java 17 Dependencies: Spring Web, Spring Data JPA, Thymeleaf, Lombok, PostgreSQL driver
- Framework: Spring Boot 3 with Gradle (Java 17). Dependencies: Spring Web, Spring Data JPA, Lombok, MySQL driver.
- Add Thymeleaf only if server-side rendering is required.
🤖 Prompt for AI Agents
In .cursor/rules/spring-boot-configuration.mdc around line 5, the rule metadata
lists "Java Spring Boot 3 Maven with Java 17 Dependencies: Spring Web, Spring
Data JPA, Thymeleaf, Lombok, PostgreSQL driver" which mismatches the repository
(Gradle + MySQL); update the rule description and dependency list to reflect the
project's stack: indicate Gradle build, MySQL driver instead of PostgreSQL, and
any matching Java version and dependencies used in the repo so the rule aligns
with the actual project configuration.

- All request and response handling must be done only in RestController.
- All database operation logic must be done in ServiceImpl classes, which must use methods provided by Repositories.
- RestControllers cannot autowire Repositories directly unless absolutely beneficial to do so.
- ServiceImpl classes cannot query the database directly and must use Repositories methods, unless absolutely necessary.
- Data carrying between RestControllers and ServiceImpl classes, and vice versa, must be done only using DTOs.
- Entity classes must be used only to carry data out of database query executions.
Comment on lines +6 to +11
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Tighten and correct architectural guidance (remove ambiguity, fix terminology).

Current wording introduces exceptions that undermine the rules and uses “ServiceImpl” rigidly.

Apply:

- All request and response handling must be done only in RestController.
- All database operation logic must be done in ServiceImpl classes, which must use methods provided by Repositories.
- RestControllers cannot autowire Repositories directly unless absolutely beneficial to do so.
- ServiceImpl classes cannot query the database directly and must use Repositories methods, unless absolutely necessary.
- Data carrying between RestControllers and ServiceImpl classes, and vice versa, must be done only using DTOs.
- Entity classes must be used only to carry data out of database query executions.
+ All HTTP request/response handling must reside in @RestController classes.
+ Business logic and DB operations belong to the Service layer (interfaces + implementations). Services must use Repository methods (no direct EntityManager access except in rare, justified cases).
+ RestControllers must not depend on Repository types; depend on Service interfaces only.
+ Use DTOs at the controller–service boundary (both directions). Do not expose entities in API contracts.
+ Entities represent persisted domain objects for both reads and writes. Avoid using entities as external API payloads.
📝 Committable suggestion

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

Suggested change
- All request and response handling must be done only in RestController.
- All database operation logic must be done in ServiceImpl classes, which must use methods provided by Repositories.
- RestControllers cannot autowire Repositories directly unless absolutely beneficial to do so.
- ServiceImpl classes cannot query the database directly and must use Repositories methods, unless absolutely necessary.
- Data carrying between RestControllers and ServiceImpl classes, and vice versa, must be done only using DTOs.
- Entity classes must be used only to carry data out of database query executions.
- All HTTP request/response handling must reside in @RestController classes.
- Business logic and DB operations belong to the Service layer (interfaces + implementations). Services must use Repository methods (no direct EntityManager access except in rare, justified cases).
- RestControllers must not depend on Repository types; depend on Service interfaces only.
- Use DTOs at the controller–service boundary (both directions). Do not expose entities in API contracts.
- Entities represent persisted domain objects for both reads and writes. Avoid using entities as external API payloads.
🤖 Prompt for AI Agents
In .cursor/rules/spring-boot-configuration.mdc around lines 6 to 11, the
guidance is ambiguous and uses brittle terminology like "ServiceImpl" and soft
exceptions; update the rules to be strict and terminology-accurate: replace
"RestController" with "controllers annotated with @RestController", require
controllers to handle request/response and delegate business logic to
service-layer interfaces (not concrete "ServiceImpl" names), state that
service-layer implementations must perform database operations via repository
interfaces, forbid controllers from directly autowiring repositories (remove
exception clause), require DTOs for all data transfer between controllers and
services, and specify that JPA entities are only used as persistence models
returned by repositories; make phrasing normative (must/shall) and remove
exception language.

87 changes: 87 additions & 0 deletions .cursorrules
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
## Instruction to developer: save this file as .cursorrules and place it on the root project directory

AI Persona:

You are an experienced Senior Java Developer, You always adhere to SOLID principles, DRY principles, KISS principles and YAGNI principles. You always follow OWASP best practices. You always break task down to smallest units and approach to solve any task in step by step manner.

Technology stack:

Framework: Java Spring Boot 3 Maven with Java 17 Dependencies: Spring Web, Spring Data JPA, Lombok, MySQL driver, Redis
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Tooling mismatch: project uses Gradle, not Maven

Align the rule text with the build system to avoid confusion.

-Framework: Java Spring Boot 3 Maven with Java 17 Dependencies: Spring Web, Spring Data JPA, Lombok, MySQL driver, Redis
+Framework: Java Spring Boot 3 with Java 17 (Gradle). Dependencies: Spring Web, Spring Data JPA, Lombok, MySQL driver, Redis
📝 Committable suggestion

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

Suggested change
Framework: Java Spring Boot 3 Maven with Java 17 Dependencies: Spring Web, Spring Data JPA, Lombok, MySQL driver, Redis
Framework: Java Spring Boot 3 with Java 17 (Gradle). Dependencies: Spring Web, Spring Data JPA, Lombok, MySQL driver, Redis
🤖 Prompt for AI Agents
.cursorrules around line 9: the rule text incorrectly states the project uses
Maven; update the line to reflect the actual build system (Gradle) and
optionally adjust any build-specific phrasing (e.g., “Gradle” instead of
“Maven”, change dependency declaration hints if present) so the tooling
description matches the repository setup.


Application Logic Design:

1. All request and response handling must be done only in RestController.
2. All database operation logic must be done in ServiceImpl classes, which must use methods provided by Repositories.
3. RestControllers cannot autowire Repositories directly unless absolutely beneficial to do so.
4. ServiceImpl classes cannot query the database directly and must use Repositories methods, unless absolutely necessary.
5. Data carrying between RestControllers and serviceImpl classes, and vice versa, must be done only using DTOs.
6. Entity classes must be used only to carry data out of database query executions.

Entities

1. Must annotate entity classes with @Entity.
2. Must annotate entity classes with @Data (from Lombok), unless specified in a prompt otherwise.
3. Must annotate entity ID with @Id and @GeneratedValue(strategy=GenerationType.IDENTITY).
4. Must use FetchType.LAZY for relationships, unless specified in a prompt otherwise.
5. Annotate entity properties properly according to best practices, e.g., @Size, @NotEmpty, @Email, etc.

Comment on lines +24 to +27
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid Lombok @DaTa on JPA entities

@DaTa generates equals/hashCode/toString that can traverse lazy relations and cause bugs. Prefer granular annotations.

-2. Must annotate entity classes with @Data (from Lombok), unless specified in a prompt otherwise.
+2. Prefer @Getter/@Setter and define @EqualsAndHashCode(onlyExplicitlyIncluded = true) on identifiers; avoid @Data on entities.
📝 Committable suggestion

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

Suggested change
3. Must annotate entity ID with @Id and @GeneratedValue(strategy=GenerationType.IDENTITY).
4. Must use FetchType.LAZY for relationships, unless specified in a prompt otherwise.
5. Annotate entity properties properly according to best practices, e.g., @Size, @NotEmpty, @Email, etc.
2. Prefer @Getter/@Setter and define @EqualsAndHashCode(onlyExplicitlyIncluded = true) on identifiers; avoid @Data on entities.
3. Must annotate entity ID with @Id and @GeneratedValue(strategy=GenerationType.IDENTITY).
4. Must use FetchType.LAZY for relationships, unless specified in a prompt otherwise.
5. Annotate entity properties properly according to best practices, e.g., @Size, @NotEmpty, @Email, etc.
🤖 Prompt for AI Agents
In .cursorrules around lines 24-27, replace guidance that allows Lombok's @Data
on JPA entities: remove/forbid @Data because it generates
equals/hashCode/toString that can traverse lazy relationships; instead require
explicit, granular Lombok annotations (@Getter, @Setter, @NoArgsConstructor,
@AllArgsConstructor as needed) and a safe equals/hashCode implementation that
uses only the entity identifier (or follow Hibernate's recommended pattern), and
ensure toString does not include relationship fields (use @ToString.Exclude or
omit toString entirely) to avoid triggering lazy loads.

Repository (DAO):

1. Must annotate repository classes with @Repository.
2. Repository classes must be of type interface.
3. Must extend JpaRepository with the entity and entity ID as parameters, unless specified in a prompt otherwise.
4. Must use JPQL for all @Query type methods, unless specified in a prompt otherwise.
5. Must use @EntityGraph(attributePaths={"relatedEntity"}) in relationship queries to avoid the N+1 problem.
6. Must use a DTO as The data container for multi-join queries with @Query.

Service:

1. Service classes must be of type interface.
2. All service class method implementations must be in ServiceImpl classes that implement the service class,
3. All ServiceImpl classes must be annotated with @Service.
4. All dependencies in ServiceImpl classes must be @Autowired without a constructor, unless specified otherwise.
5. Return objects of ServiceImpl methods should be DTOs, not entity classes, unless absolutely necessary.
6. For any logic requiring checking the existence of a record, use the corresponding repository method with an appropriate .orElseThrow lambda method.
7. For any multiple sequential database executions, must use @Transactional or transactionTemplate, whichever is appropriate.
Comment on lines +42 to +45
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Adopt constructor injection and drop blanket try/catch requirement

Promote testability and centralized error handling.

-4. All dependencies in ServiceImpl classes must be @Autowired without a constructor, unless specified otherwise.
+4. Use constructor injection in ServiceImpl classes (avoid field injection).

-4. All dependencies in class methods must be @Autowired without a constructor, unless specified otherwise.
-6. All class method logic must be implemented in a try..catch block(s).
+4. Use constructor injection in controllers (avoid field/method injection).
+6. Do not wrap handlers in try/catch; handle exceptions via GlobalExceptionHandler.

Also applies to: 57-60

🤖 Prompt for AI Agents
.cursorrules lines 42-45 and 57-60: the rule text currently mandates field
@Autowired and blanket try/catch which reduces testability and central error
handling; update the guidance to require constructor injection (declare
dependencies as final fields and provide a single constructor for ServiceImpls)
and remove any blanket try/catch requirement (advise throwing checked/unchecked
exceptions to be handled by a centralized exception handler), while keeping an
allowance where constructor injection is explicitly not required; also ensure
the rule references returning DTOs, using repository .orElseThrow for existence
checks, and using @Transactional/transactionTemplate for multi-step DB
operations as separate rules rather than coupling them to injection/try-catch
guidance.


Data Transfer object (DTo):

1. Must be of type record, unless specified in a prompt otherwise.
2. Must specify a compact canonical constructor to validate input parameter data (not null, blank, etc., as appropriate).

RestController:

1. Must annotate controller classes with @RestController.
2. Must specify class-level API routes with @RequestMapping, e.g. ("/api/user").
3. Use @GetMapping for fetching, @PostMapping for creating, @PutMapping for updating, and @DeleteMapping for deleting. Keep paths resource-based (e.g., '/users/{id}'), avoiding verbs like '/create', '/update', '/delete', '/get', or '/edit'
4. All dependencies in class methods must be @Autowired without a constructor, unless specified otherwise.
5. Methods return objects must be of type Response Entity of type ApiResponse.
6. All class method logic must be implemented in a try..catch block(s).
7. Caught errors in catch blocks must be handled by the Custom GlobalExceptionHandler class.

ApiResponse Class (/ApiResponse.java):

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private String result; // SUCCESS or ERROR
private String message; // success or error message
private T data; // return object from service class, if successful
}
Comment on lines +64 to +71
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

ApiResponse snippet lacks factory methods used elsewhere

GlobalExceptionHandler calls ApiResponse.error(...), which doesn’t exist. Add success/error factories.

 @Data
 @NoArgsConstructor
 @AllArgsConstructor
 public class ApiResponse<T> {
   private String result;    // SUCCESS or ERROR
   private String message;   // success or error message
   private T data;           // return object from service class, if successful
+
+  public static <T> ApiResponse<T> success(String message, T data) {
+    return new ApiResponse<>("SUCCESS", message, data);
+  }
+
+  public static <T> ApiResponse<T> error(String message) {
+    return new ApiResponse<>("ERROR", message, null);
+  }
 }
🤖 Prompt for AI Agents
In .cursorrules around lines 64-71, the ApiResponse class is missing the static
factory methods used elsewhere (e.g., GlobalExceptionHandler calls
ApiResponse.error(...)); add static generic factory methods such as success(T
data) and success(String message, T data), and error(String message) and
error(String message, T data) that construct and return ApiResponse instances
with result set to "SUCCESS" or "ERROR" respectively, populate message and data
appropriately, and make them generic (public static <T> ApiResponse<T> ...) so
callers can use them without needing to instantiate ApiResponse directly.


GlobalExceptionHandler Class (/GlobalExceptionHandler.java)

@RestControllerAdvice
public class GlobalExceptionHandler {

public static ResponseEntity<ApiResponse<?>> errorResponseEntity(String message, HttpStatus status) {
ApiResponse<?> response = new ApiResponse<>("error", message, null)
return new ResponseEntity<>(response, status);
}

@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<?>> handleIllegalArgumentException(IllegalArgumentException ex) {
return new ResponseEntity<>(ApiResponse.error(400, ex.getMessage()), HttpStatus.BAD_REQUEST);
}
Comment on lines +78 to +86
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

GlobalExceptionHandler snippet won’t compile (missing semicolon) and mismatched ApiResponse API

  • Missing semicolon after new ApiResponse<>(...).
  • Align handler with new ApiResponse factories; remove unused numeric code in error(...)
 @RestControllerAdvice
 public class GlobalExceptionHandler {
 
     public static ResponseEntity<ApiResponse<?>> errorResponseEntity(String message, HttpStatus status) {
-      ApiResponse<?> response = new ApiResponse<>("error", message, null)
+      ApiResponse<?> response = new ApiResponse<>("ERROR", message, null);
       return new ResponseEntity<>(response, status);
     }
 
     @ExceptionHandler(IllegalArgumentException.class)
     public ResponseEntity<ApiResponse<?>> handleIllegalArgumentException(IllegalArgumentException ex) {
-        return new ResponseEntity<>(ApiResponse.error(400, ex.getMessage()), HttpStatus.BAD_REQUEST);
+        return new ResponseEntity<>(ApiResponse.error(ex.getMessage()), HttpStatus.BAD_REQUEST);
     }
 }
📝 Committable suggestion

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

Suggested change
public static ResponseEntity<ApiResponse<?>> errorResponseEntity(String message, HttpStatus status) {
ApiResponse<?> response = new ApiResponse<>("error", message, null)
return new ResponseEntity<>(response, status);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<?>> handleIllegalArgumentException(IllegalArgumentException ex) {
return new ResponseEntity<>(ApiResponse.error(400, ex.getMessage()), HttpStatus.BAD_REQUEST);
}
@RestControllerAdvice
public class GlobalExceptionHandler {
public static ResponseEntity<ApiResponse<?>> errorResponseEntity(String message, HttpStatus status) {
ApiResponse<?> response = new ApiResponse<>("ERROR", message, null);
return new ResponseEntity<>(response, status);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<?>> handleIllegalArgumentException(IllegalArgumentException ex) {
return new ResponseEntity<>(ApiResponse.error(ex.getMessage()), HttpStatus.BAD_REQUEST);
}
}
🤖 Prompt for AI Agents
.cursorrules around lines 78-86: the code has a missing semicolon after the
manual new ApiResponse<>(...) and uses an outdated ApiResponse API; replace the
manual constructor with the new factory method and remove the numeric code
parameter—i.e. return ResponseEntity<>(ApiResponse.error(message), status) for
errorResponseEntity (add the missing semicolon where needed), and change the
exception handler to return new
ResponseEntity<>(ApiResponse.error(ex.getMessage()), HttpStatus.BAD_REQUEST).

}
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.gradle
build/
out/
*.log
.DS_Store
.idea/

Comment on lines +1 to +7
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add .env and editor artifacts to prevent secret leaks and noise.

Current rules miss common local secrets and editor files.

 .gradle
 build/
 out/
 *.log
 .DS_Store
 .idea/
+
+# env/secrets
+.env
+.env.*
+*.env
+
+# editors
+.vscode/
+*.iml
+
+# logs dir
+logs/
📝 Committable suggestion

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

Suggested change
.gradle
build/
out/
*.log
.DS_Store
.idea/
.gradle
build/
out/
*.log
.DS_Store
.idea/
# env/secrets
.env
.env.*
*.env
# editors
.vscode/
*.iml
# logs dir
logs/
🤖 Prompt for AI Agents
In .gitignore around lines 1-7, the file currently ignores build artifacts but
is missing entries for local environment files and editor artifacts which can
leak secrets and create noise; update .gitignore to include common local secret
files (e.g., .env, .env.* , .env.local) and popular editor/IDE artifacts (e.g.,
.vscode/, *.sublime-*, *.swp, .idea/ already present but ensure .idea/* and
workspace files are covered, and add OS temp files like Thumbs.db) so that local
env files and editor-specific files are ignored consistently across developers.

12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Build stage
FROM gradle:8.5.0-jdk17-jammy AS build
WORKDIR /home/gradle/src
COPY build.gradle settings.gradle ./
COPY src ./src
RUN gradle build --no-daemon

# Package stage
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=build /home/gradle/src/build/libs/*.jar app.jar
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

COPY may match multiple jars and fail the build.

Spring Boot often produces both app.jar and app-plain.jar; copying “*.jar” to a single filename is unsafe.

Apply (paired with build.gradle change to name the artifact app.jar):

-COPY --from=build /home/gradle/src/build/libs/*.jar app.jar
+COPY --from=build /home/gradle/src/build/libs/app.jar app.jar
📝 Committable suggestion

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

Suggested change
COPY --from=build /home/gradle/src/build/libs/*.jar app.jar
COPY --from=build /home/gradle/src/build/libs/app.jar app.jar
🤖 Prompt for AI Agents
In Dockerfile at line 11, replace the unsafe glob COPY with a specific filename:
instead of COPY --from=build /home/gradle/src/build/libs/*.jar app.jar, copy the
exact artifact name (e.g. /home/gradle/src/build/libs/app.jar) and ensure
build.gradle/gradle.properties config sets the produced jar filename to app.jar
(configure bootJar.archiveFileName or equivalent) so the Docker COPY matches a
single known file.

ENTRYPOINT ["java","-jar","app.jar"]
26 changes: 26 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
}

group = 'com.study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
useJUnitPlatform()
}
Comment on lines +24 to +26
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Ensure a single, predictable Boot jar for containerization.

Avoid producing both boot and plain jars; also fix the Docker COPY to a stable filename.

Apply:

 tasks.named('test') {
   useJUnitPlatform()
 }
+
+// Produce a single Boot executable jar with a stable name for Docker
+bootJar {
+    archiveFileName = 'app.jar'
+}
+tasks.named('jar') {
+    enabled = false
+}

Follow-up: adjust Dockerfile COPY to build/libs/app.jar (see comment there).

🤖 Prompt for AI Agents
In build.gradle around lines 24-26, ensure only a single predictable Spring Boot
jar is produced by disabling the plain jar and pinning the bootJar filename: add
tasks.named('jar') { enabled = false } and configure bootJar { archiveFileName =
'app.jar' } (or use archiveBaseName/version as needed) so the build produces
only build/libs/app.jar; then update the Dockerfile COPY to refer to
build/libs/app.jar as noted in the Dockerfile comment.

15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

version: '3.8'
services:
db:
image: mysql:8.0
container_name: mysql_boot
restart: always
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: 'bitly_db'
MYSQL_ROOT_PASSWORD: 'root'
Comment on lines +10 to +12
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid using root credentials; move secrets to .env and create an app user.

Using root/root in-compose is risky even for dev and couples secrets to VCS.

 services:
   db:
     image: mysql:8.0
     container_name: mysql_boot
     restart: always
-    ports:
-      - "3306:3306"
-    environment:
-      MYSQL_DATABASE: 'bitly_db'
-      MYSQL_ROOT_PASSWORD: 'root'
+    env_file:
+      - .env
+    environment:
+      MYSQL_DATABASE: ${MYSQL_DATABASE:-bitly_db}
+      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
+      MYSQL_USER: ${MYSQL_USER:-app_user}
+      MYSQL_PASSWORD: ${MYSQL_PASSWORD:-app_pass}
+    ports:
+      - "${HOST_MYSQL_PORT:-3306}:3306"
+    volumes:
+      - mysql_data:/var/lib/mysql
     command:
       - --character-set-server=utf8mb4
       - --collation-server=utf8mb4_unicode_ci
+    healthcheck:
+      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
+      interval: 5s
+      timeout: 3s
+      retries: 30
+
+volumes:
+  mysql_data:

Follow-up: add .env to .gitignore (see separate comment).

📝 Committable suggestion

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

Suggested change
environment:
MYSQL_DATABASE: 'bitly_db'
MYSQL_ROOT_PASSWORD: 'root'
services:
db:
image: mysql:8.0
container_name: mysql_boot
restart: always
env_file:
- .env
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE:-bitly_db}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
MYSQL_USER: ${MYSQL_USER:-app_user}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-app_pass}
ports:
- "${HOST_MYSQL_PORT:-3306}:3306"
volumes:
- mysql_data:/var/lib/mysql
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
interval: 5s
timeout: 3s
retries: 30
volumes:
mysql_data:
🤖 Prompt for AI Agents
In docker-compose.yml around lines 10 to 12, the service is using hard-coded
root credentials (MYSQL_ROOT_PASSWORD='root') and committing secrets to VCS;
replace these with non-root application credentials and load them from an
external .env (or env_file) instead: add MYSQL_DATABASE, MYSQL_USER and
MYSQL_PASSWORD entries (for an app user) and remove or avoid hard-coded
MYSQL_ROOT_PASSWORD in the compose file, then create a .env containing the
secret values and reference it from docker-compose; finally add .env to
.gitignore so secrets aren’t committed.

command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
108 changes: 108 additions & 0 deletions gemini.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# gemini.md: URL 단축기 만들기

## 1. 소개 (Introduction)

URL 단축기(URL Shortener)는 긴 URL을 짧고 기억하기 쉬운 URL로 변환해 주는 서비스입니다. 이렇게 생성된 짧은 URL을 클릭하면 원래의 긴 URL로 리디렉션됩니다. 소셜 미디어, 이메일, 문자 메시지 등 글자 수 제한이 있는 플랫폼에서 유용하게 사용되며, 링크 추적 및 분석에도 활용될 수 있습니다. 💻

본 문서는 URL 단축기의 핵심 기능, 시스템 아키텍처, 데이터베이스 설계, 그리고 구현 단계에 대한 개요를 제공합니다.

---

## 2. 핵심 기능 (Core Features)

기본적인 URL 단축기는 다음과 같은 핵심 기능을 포함해야 합니다.

* **URL 단축:** 사용자가 긴 URL을 입력하면, 고유하고 짧은 URL을 생성하여 반환합니다.
* **URL 리디렉션:** 짧은 URL에 접속하면, 원래의 긴 URL로 리디렉션합니다.
* **(선택) 사용자 정의 URL:** 사용자가 원하는 문자열로 짧은 URL을 직접 지정할 수 있는 기능을 제공할 수 있습니다.
* **(선택) 링크 만료:** 생성된 짧은 URL이 특정 시간이 지나면 만료되도록 설정할 수 있습니다.
* **(선택) 분석:** 짧은 URL의 클릭 수, 접속 지역 등 통계 정보를 제공할 수 있습니다.

---

## 3. 시스템 아키텍처 (System Architecture)

URL 단축기의 시스템 아키텍처는 일반적으로 다음과 같은 구성 요소를 포함합니다.



* **웹 서버 (Web Server):** 사용자의 요청을 받아 처리하는 역할을 합니다. (예: Nginx, Apache)
* **애플리케이션 서버 (Application Server):** URL 단축 및 리디렉션 로직을 수행합니다. (예: Node.js, Python/Django, Java/Spring)
* **데이터베이스 (Database):** 원본 URL과 단축된 URL의 매핑 정보를 저장합니다. 대규모 트래픽을 처리하기 위해 NoSQL 데이터베이스가 유리할 수 있습니다. (예: Redis, Cassandra, MongoDB)
* **캐시 (Cache):** 자주 요청되는 URL의 리디렉션 속도를 높이기 위해 사용됩니다. 사용자가 짧은 URL을 요청하면, 먼저 캐시에서 찾아보고 없는 경우에만 데이터베이스에 접근합니다. (예: Redis, Memcached)

### 작업 흐름

1. **URL 단축 요청:**
* 사용자가 긴 URL을 입력하고 단축을 요청합니다.
* 애플리케이션 서버는 이 URL에 대한 고유한 짧은 키를 생성합니다.
* 생성된 짧은 키와 원본 URL을 데이터베이스에 저장합니다.
* 사용자에게 단축된 전체 URL을 반환합니다.

2. **URL 리디렉션 요청:**
* 사용자가 단축된 URL을 클릭합니다.
* 웹 서버는 이 요청을 애플리케이션 서버로 전달합니다.
* 애플리케이션 서버는 먼저 캐시에서 해당 짧은 키에 매핑된 원본 URL을 찾습니다.
* 캐시에 없는 경우, 데이터베이스에서 조회합니다.
* 조회된 원본 URL로 사용자를 301 Moved Permanently 리디렉션합니다.
* 데이터베이스에서 조회한 경우, 해당 정보를 캐시에 저장하여 다음 요청에 빠르게 응답할 수 있도록 합니다.

---

## 4. 데이터베이스 설계 (Database Design)

URL 단축기를 위한 데이터베이스 스키마는 비교적 간단하게 설계할 수 있습니다. 관계형 데이터베이스(RDBMS)나 NoSQL 데이터베이스 모두 사용 가능하며, 대규모 서비스를 고려한다면 읽기 성능이 뛰어난 NoSQL이 더 적합할 수 있습니다.

**기본 테이블 스키마 예시 (`urls` 테이블):**

| 컬럼명 (Column Name) | 데이터 타입 (Data Type) | 설명 (Description) |
| -------------------- | ----------------------- | -------------------------------------------------- |
| `id` | BIGINT (Auto Increment) | 고유 식별자 (Primary Key) |
| `original_url` | TEXT | 사용자가 입력한 원본 URL |
| `short_key` | VARCHAR(10) | 생성된 고유한 짧은 키 (Index, Unique) |
| `created_at` | TIMESTAMP | 생성 일시 |
| `expires_at` | TIMESTAMP (Nullable) | 만료 일시 (선택 기능) |
| `click_count` | INT (Default: 0) | 클릭 수 (분석 기능을 위한 선택 컬럼) |

Comment on lines +58 to +66
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Tight short_key length; add constraints and indexes

Base62 of a 64‑bit id can exceed 10 chars. Define explicit constraints and uniqueness.

-| `short_key`          | VARCHAR(10)             | 생성된 고유한 짧은 키 (Index, Unique)              |
+| `short_key`          | VARCHAR(16)             | 생성된 고유한 짧은 키 (UNIQUE INDEX)               |

Example DDL:

CREATE TABLE urls (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  original_url TEXT NOT NULL,
  short_key VARCHAR(16) NOT NULL,
  created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  expires_at TIMESTAMP(3) NULL,
  click_count BIGINT NOT NULL DEFAULT 0,
  CONSTRAINT uq_urls_short_key UNIQUE (short_key)
);
🤖 Prompt for AI Agents
In gemini.md around lines 58 to 66, the schema uses VARCHAR(10) for short_key
and INT for click_count without explicit constraints or indexes; update the
table definition to use a larger short_key (e.g., VARCHAR(16)) and mark it NOT
NULL, change click_count to BIGINT with DEFAULT 0, add a UNIQUE constraint and
an index on short_key, and set TIMESTAMP precision (e.g., TIMESTAMP(3)) and NOT
NULL defaults for created_at so the DDL reflects these constraints and prevents
key collisions and overflow.

`short_key`는 고유해야 하며, 빠른 조회를 위해 인덱스를 생성하는 것이 중요합니다.

---

## 5. 구현 단계 (Implementation Steps)

### 1단계: 짧은 키 생성 전략 결정

고유하고 충돌 없는 짧은 키를 생성하는 것이 핵심입니다. 몇 가지 전략이 있습니다.

* **해싱 (Hashing):** 원본 URL을 MD5나 SHA-256과 같은 해시 함수로 해싱한 후, 결과값의 일부를 잘라내어 사용합니다. 해시 충돌(collision)이 발생할 수 있으므로, 충돌 시 다른 문자열을 덧붙여 다시 해싱하는 등의 처리 로직이 필요합니다.
* **Base62 인코딩:** 데이터베이스의 Auto Increment 되는 `id` 값을 62진법(0-9, a-z, A-Z)으로 변환하는 방법입니다. 이 방법은 생성된 키의 길이가 예측 가능하고 충돌이 발생하지 않는다는 장점이 있습니다.
* 예시: `id`가 `1000`이라면, Base62로 인코딩하면 `g8`이 됩니다.

### 2단계: API 엔드포인트 설계

* `POST /api/v1/shorten`: 긴 URL을 받아 짧은 URL을 생성하는 엔드포인트.
* **Request Body:** `{ "originalUrl": "https://www.example.com/very/long/url" }`
* **Response Body:** `{ "shortUrl": "https://your.domain/g8" }`
* `GET /{shortKey}`: 짧은 URL을 받아 원래 URL로 리디렉션하는 엔드포인트.

### 3단계: 리디렉션 처리

사용자가 `https://your.domain/{shortKey}`로 접속했을 때, 데이터베이스에서 `{shortKey}`에 해당하는 `original_url`을 찾아 301 리디렉션을 수행합니다. 301 리디렉션은 영구 이동을 의미하며, 검색 엔진 최적화(SEO)에도 도움이 됩니다.

---

## 6. 확장성 및 고려사항 (Scalability and Considerations)

* **데이터베이스 확장성:**
* **샤딩 (Sharding):** 데이터베이스를 여러 서버에 분산하여 저장함으로써 부하를 분산시킬 수 있습니다.
* **고가용성 (High Availability):**
* **로드 밸런서 (Load Balancer):** 여러 대의 애플리케이션 서버에 트래픽을 분산하여 하나의 서버에 장애가 발생하더라도 서비스가 중단되지 않도록 합니다.
* **데이터베이스 복제 (Replication):** 데이터베이스를 여러 개 복제하여 원본에 문제가 생겨도 복제본을 통해 서비스를 계속할 수 있습니다.
* **보안 (Security):**
* **악성 URL 차단:** Google Safe Browsing API 등을 이용하여 사용자가 악성 사이트로 리디렉션되는 것을 방지할 수 있습니다.
* **Rate Limiting:** 특정 IP 주소에서 비정상적으로 많은 요청이 오는 경우, 요청을 제한하여 서비스 공격(DoS)을 방지합니다.
* **분석 (Analytics):**
* 클릭 이벤트가 발생할 때마다 관련 정보를 비동기적으로 처리하여 데이터베이스에 저장하면, 리디렉션 성능에 영향을 주지 않으면서 통계 데이터를 수집할 수 있습니다. 메시지 큐(Message Queue)를 활용하는 것이 좋은 방법입니다. (예: RabbitMQ, Kafka)



Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading