Skip to content

Conversation

@jungeunyooon
Copy link
Member

@jungeunyooon jungeunyooon commented Sep 13, 2025

모니터링 설정, K6 스크립트 작성
디비 직접 조회, 인덱스 조회, 캐싱 3가지 경우 API 분리 작업 후 테스트 진행

Summary by CodeRabbit

  • New Features

    • Redis-backed cached redirects, cache stats endpoint, and phase-specific redirect endpoints (baseline/indexed/cached).
    • Actuator health + Prometheus metrics enabled; added Prometheus-friendly metrics export.
  • Monitoring

    • Full monitoring stack (Prometheus, Grafana, exporters, cAdvisor) and k6 phased performance tests included.
  • Documentation

    • README overhauled to a phase-based performance experiment with run and monitoring guides.
  • DevOps/Chores

    • Multi-stage Dockerfile tuned (healthcheck, memory opts), expanded docker-compose with resource limits and services, added .dockerignore and extended .gitignore, added actuator/prometheus deps.

@jungeunyooon jungeunyooon self-assigned this Sep 13, 2025
@jungeunyooon jungeunyooon linked an issue Sep 13, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Sep 13, 2025

Walkthrough

Refactors persistence into baseline/indexed tables, adds Redis cache and endpoints, integrates Actuator/Prometheus metrics, introduces a monitoring stack and k6 phased tests, updates Docker/Docker Compose and Dockerfile, adds Redis config and CacheService, updates README and ignore rules.

Changes

Cohort / File(s) Summary
Containerization & Compose
Dockerfile, docker-compose.yml, .dockerignore
Reworked multi-stage Dockerfile (builder → runtime, healthcheck, JAVA_OPTS), added .dockerignore. Compose adds api service, resource limits, healthchecks, networks/volumes, and a monitoring stack (Prometheus, Grafana, exporters, InfluxDB, k6, cAdvisor).
Observability & Dependencies
build.gradle, src/main/resources/application.yml, monitoring/prometheus/prometheus.yml, monitoring/grafana/provisioning/datasources/*
Adds Actuator and Micrometer Prometheus dependency; application.yml uses env vars and docker profile, enables actuator/prometheus endpoints; Prometheus and Grafana datasource provisioning added.
Performance Testing
monitoring/k6/performance-test-with-phases.js
New k6 script implementing phased (baseline/indexed/cached) load test with data seeding, health checks, phase selection and thresholds.
Persistence Model
Added:
src/main/java/com/example/bitly/entity/UrlBaseline.java, .../UrlIndexed.java, .../repository/UrlBaselineRepository.java, .../repository/UrlIndexedRepository.java
Removed:
src/main/java/com/example/bitly/entity/Url.java, src/main/java/com/example/bitly/repository/UrlRepository.java
Replaces single Url entity/repo with two entities and repositories (baseline/indexed). UrlIndexed declares an index on short_url. Original entity and repository removed.
Caching & Redis
src/main/java/com/example/bitly/config/RedisConfig.java, src/main/java/com/example/bitly/service/CacheService.java
Adds RedisTemplate bean and a Redis-backed CacheService (get/put/evict, basic stats, 15m TTL).
API & Service Flow
src/main/java/com/example/bitly/service/UrlService.java, src/main/java/com/example/bitly/controller/UrlController.java
UrlService reworked for baseline/indexed tables, counter-based shortId generation, cache-first retrieval; adds phase preparation methods. Controller adds phase-specific endpoints (/baseline, /indexed, /cached), /stats, and prepare endpoints; default redirect routes to cached lookup.
Monitoring Provisioning
monitoring/grafana/provisioning/datasources/prometheus.yml, monitoring/grafana/provisioning/datasources/influxdb.yml, monitoring/prometheus/prometheus.yml
Grafana datasource provisioning for Prometheus and InfluxDB; Prometheus scrape configs for app and exporters, including relabel for the Spring Boot app.
Docs & Git ignores
README.md, .gitignore
README rewritten into a phased performance-experiment doc; .gitignore extended for bin/, nested .bak patterns and backups.
CI Workflow Removal
.github/workflows/gemini-review.yml
Removes automated Gemini AI review workflow.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Controller as UrlController
  participant Service as UrlService
  participant Cache as Redis (CacheService)
  participant DB as JPA (Baseline/Indexed)

  rect rgba(230,245,255,0.6)
  note over Client,Controller: Cached redirect (default or /cached/{shortUrl})
  Client->>Controller: GET /api/v1/urls/{shortUrl}
  Controller->>Service: getOriginalUrlCached(shortUrl)
  Service->>Cache: get(shortUrl)
  alt cache HIT
    Cache-->>Service: originalUrl
    Service-->>Controller: originalUrl
    Controller-->>Client: 302 Redirect
  else cache MISS
    Service->>DB: findByShortUrl (Baseline -> Indexed fallback)
    DB-->>Service: originalUrl or null
    alt Found
      Service->>Cache: put(shortUrl, originalUrl)
      Service-->>Controller: originalUrl
      Controller-->>Client: 302 Redirect
    else Not Found
      Controller-->>Client: 404 / Not found
    end
  end
  end
Loading
sequenceDiagram
  autonumber
  actor Operator
  participant Compose as docker-compose
  participant API as api (Spring Boot)
  participant Pg as postgres
  participant Rds as redis
  participant Prom as prometheus
  participant Graf as grafana
  participant K6 as k6

  Operator->>Compose: docker-compose up -d
  Compose->>Pg: start (healthcheck)
  Compose->>Rds: start (healthcheck)
  Compose->>API: build & start (depends_on healthy)
  API-->>Compose: HEALTHCHECK /actuator/health
  Compose->>Prom: start (scrape targets)
  Compose->>Graf: start (provision datasources)
  Compose->>K6: start (run phased script)
  K6->>API: seed data & run phased GETs
  Prom->>API: scrape /actuator/prometheus
  Graf->>Prom: query metrics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • set: 도커, 스프링부트 세팅 #7 — Prior PR modifying Dockerfile, docker-compose, build.gradle and app config; closely related to this overhaul.
  • feat: 초기 구현 #9 — Prior changes to UrlController/UrlService and persistence; this PR refactors that model into phased baseline/indexed + cache.
  • 초기 Design Doc 작성 #5 — Earlier documentation/design updates; README rewrite here appears to replace that design with a phased experiment.

Poem

In burrows of bytes I hop and play,
I cache short links to speed the day.
Two tables, a Redis nest so snug —
Prometheus hums, k6 gives a tug.
302s leap, the logs go rap—rap! 🐇🚀

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "모니터링 및 구현 고도화" concisely and accurately reflects the primary changes in this PR—significant monitoring additions (Prometheus, Grafana, exporters, k6) and implementation enhancements (indexed/cached retrieval, Redis, service/controller and Docker compose/Dockerfile updates)—so it is short, relevant, and clear to reviewers.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch set/#10

Tip

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

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

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

Please see the documentation for more information.

Example:

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

Please share your feedback with us on this Discord post.


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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

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

⚠️ Outside diff range comments (1)
README.md (1)

44-52: Update data model: README shows single-table schema, code uses two tables (baseline/indexed).

Align docs with UrlBaseline (url_mapping_baseline) and UrlIndexed (url_mapping_indexed) entities to avoid confusion during experiments.

Apply:

-### 5. Data Storage
-- 테이블: `url_mapping`
-  - `id` (PK, 고정 길이 문자열) — 단축 코드, 기본 키 및 인덱스
-  - `original_url` (text)
-  - `shortUrl`
-  - `created_at`, `expires_at`(선택)
+### 5. Data Storage
+- 테이블:
+  - `url_mapping_baseline` (인덱스 없음; Full Scan 실험용)
+    - `id` (PK, bigint, identity)
+    - `short_url` (varchar, unique 제약 없음)
+    - `original_url` (varchar(2000))
+  - `url_mapping_indexed` (`short_url` 유니크 인덱스 보유)
+    - `id` (PK, bigint, identity)
+    - `short_url` (varchar, unique)
+    - `original_url` (varchar(2000))
🧹 Nitpick comments (26)
.gitignore (1)

55-65: Remove redundant ignores under bin/

bin/ already ignores everything beneath; the specific file paths are noise.

Apply:

 bin/
-bin/main/
-bin/main/application.yml
-bin/test/
-bin/test/resources/
-bin/test/resources/application.yml
-bin/test/resources/application.yml.bak
-bin/test/resources/application.yml.bak.bak
-bin/test/resources/application.yml.bak.bak.bak
-bin/test/resources/application.yml.bak.bak.bak.bak
-bin/test/resources/application.yml.bak.bak.bak.bak.bak
.dockerignore (2)

22-25: If you intend to keep README in context, invert the rule.

Currently README.md is excluded. If you want to keep only README while ignoring other markdown, use a negation.

Apply:

-README.md
-*.md
+*.md
+!README.md

26-29: Don’t ignore Dockerfile by default.

Ignoring Dockerfile* can confuse remote/buildkit workflows. It’s tiny and safe to include.

Apply:

-# Docker
-Dockerfile*
-docker-compose*.yml
+# Docker
+docker-compose*.yml
Dockerfile (1)

3-5: Improve build cache usage to speed up rebuilds.

Copy Gradle metadata first, resolve deps, then copy sources.

Apply:

 WORKDIR /home/gradle/project
-COPY . .
-RUN gradle build --no-daemon -x test
+COPY build.gradle settings.gradle gradle.properties* ./ 
+COPY gradle/ ./gradle/
+RUN gradle --no-daemon build -x test || true
+COPY src ./src
+RUN gradle bootJar --no-daemon -x test
README.md (3)

95-101: Fix markdown table spacing (MD058).

Tables should be surrounded by blank lines.

Apply:

-#### 7.2 Performance Comparison Matrix
-| Phase | Architecture | Expected p95 | Expected RPS | Cache Hit Rate |
+#### 7.2 Performance Comparison Matrix
+
+| Phase | Architecture | Expected p95 | Expected RPS | Cache Hit Rate |
 |-------|-------------|--------------|--------------|----------------|
 | 1 | DB Only | 100-200ms | ~10-20 | N/A |
 | 2 | DB + Index | 20-50ms | ~50-100 | N/A |
 | 3 | Redis + DB | 5-20ms | ~200-500 | 90%+ |
+

279-285: Wrap bare URLs to satisfy markdownlint (MD034).

Convert bare URLs to links.

Apply (pattern example):

-- **Grafana**: http://localhost:3000 (admin/admin)
-**Prometheus**: http://localhost:9090
+– **Grafana**: [http://localhost:3000](http://localhost:3000) (admin/admin)
+– **Prometheus**: [http://localhost:9090](http://localhost:9090)

Repeat similarly for the other bare URLs in this section.


176-176: k6 script naming: verify files actually exist.

README references k6-baseline.js, k6-indexed.js, k6-cached.js, while the repo adds monitoring/k6/performance-test-with-phases.js. Please align names/paths or add the missing files.

monitoring/grafana/provisioning/datasources/influxdb.yml (1)

15-15: Remove tlsSkipVerify: true unless you actually use HTTPS with self-signed certs.

Currently the URL is http; this flag is unnecessary and weakens defaults if switched to https later.

-      tlsSkipVerify: true
+      # tlsSkipVerify: false
src/main/java/com/example/bitly/config/RedisConfig.java (1)

12-25: Prefer StringRedisTemplate; simpler and already string-optimized.

Functionally equivalent for your usage and trims serializer boilerplate.

-    @Bean
-    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, String> template = new RedisTemplate<>();
-        template.setConnectionFactory(connectionFactory);
-
-        // String 직렬화 설정
-        template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new StringRedisSerializer());
-        template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new StringRedisSerializer());
-
-        template.afterPropertiesSet();
-        return template;
-    }
+    @Bean
+    public org.springframework.data.redis.core.StringRedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
+        return new org.springframework.data.redis.core.StringRedisTemplate(connectionFactory);
+    }
monitoring/prometheus/prometheus.yml (2)

19-22: Duplicate cAdvisor scrape; remove one to avoid double ingestion.

Both jobs hit cadvisor:8080. Keep a single job.

-  # Docker 컨테이너 메트릭
-  - job_name: 'docker-containers'
-    static_configs:
-      - targets: ['cadvisor:8080']
-    metrics_path: '/metrics'
+  # (removed duplicate 'docker-containers' job scraping cadvisor)

Also applies to: 38-43


27-36: Use the Docker service name (bitly) instead of host.docker.internal

docker-compose defines service "bitly" (and already maps host.docker.internal→host-gateway); replace the Prometheus target with the service name for portability or make the host configurable. Align the relabel instance label with the service name.

-      - targets: ['host.docker.internal:8080']
+      - targets: ['bitly:8080'] # use docker-compose service name

-        replacement: 'bitly-api'
+        replacement: 'bitly'    # keep instance label consistent
src/main/resources/application.yml (2)

8-8: Wrong datasource driver property key.

Use driver-class-name or drop it (Boot infers from URL).

-    driver: org.postgresql.Driver
+    # driver-class-name: org.postgresql.Driver

15-18: Disable Open Session In View (recommended).

Prevents lazy-loading surprises in controllers and aligns with service-layer boundaries.

   jpa:
     hibernate:
       ddl-auto: update
     show-sql: true
+    open-in-view: false
src/main/java/com/example/bitly/service/CacheService.java (2)

29-35: Consider sliding TTL on cache hit.

Refreshing expiry improves hot-key retention.

-            if (originalUrl != null) {
+            if (originalUrl != null) {
                 log.debug("Cache HIT for key: {}", key);
-                return Optional.of(originalUrl);
+                redisTemplate.expire(key, defaultTtl);
+                return Optional.of(originalUrl);

49-51: Use configurable TTL field.

Align with the property-based TTL.

-            redisTemplate.opsForValue().set(key, originalUrl, DEFAULT_TTL);
-            log.debug("Cached URL: {} -> {} (TTL: {})", shortUrl, originalUrl, DEFAULT_TTL);
+            redisTemplate.opsForValue().set(key, originalUrl, defaultTtl);
+            log.debug("Cached URL: {} -> {} (TTL: {})", shortUrl, originalUrl, defaultTtl);
src/main/java/com/example/bitly/controller/UrlController.java (3)

26-26: Avoid trailing slash in base @RequestMapping (double-slash paths).

Prevents // in resolved endpoints.

-@RequestMapping("/api/v1/urls/")
+@RequestMapping("/api/v1/urls")

46-56: Optional: build redirect via ResponseEntity to avoid servlet API.

Removes checked IOException and is test-friendly.

-    public void redirect(
+    public ResponseEntity<Void> redirect(
             @Parameter(description = "단축 URL", required = true) @PathVariable String shortUrl,
-            HttpServletResponse response) {
+            HttpServletResponse response) {
         // 기본 API는 Phase 3 (캐시 우선) 사용
         String originalUrl = urlService.getOriginalUrlCached(shortUrl);
-        try {
-            response.sendRedirect(originalUrl);
-        } catch (IOException e) {
-            throw new RuntimeException(e); // Checked → Unchecked 변환
-        }
+        return ResponseEntity.status(HttpStatus.FOUND)
+                .location(java.net.URI.create(originalUrl))
+                .build();
     }

(Apply similarly to baseline/indexed/cached handlers.)


104-109: /stats currently returns zeros; consider hiding or annotating as experimental.

Either implement real stats (see CacheService comment) or mark clearly as experimental to avoid confusion during k6 runs.

src/main/java/com/example/bitly/service/UrlService.java (3)

61-72: Make phase “prepare” methods read-only.

They only read counts; use readOnly = true to avoid unnecessary write transactions.

-@Transactional
+@Transactional(readOnly = true)
 public void prepareBaselinePhase() {
@@
-@Transactional
+@Transactional(readOnly = true)
 public void prepareIndexedPhase() {

90-111: Cache miss path should use the indexed repository for faster fallback.

If Phase 3 aims to demonstrate “cache + fast DB,” consider querying urlIndexedRepository on cache miss to avoid full scans on Baseline.

-String originalUrl = urlBaselineRepository.findByShortUrl(shortUrl)
+String originalUrl = urlIndexedRepository.findByShortUrl(shortUrl)
         .map(UrlBaseline::getOriginalUrl)
         .orElseThrow(() -> new EntityNotFoundException("URL not found for short URL: " + shortUrl));

If Phase 3 intentionally compares “cache vs. no index,” keep as-is but clarify in code comments.


113-121: Base62 encode: add a fast-path for zero and guard against negative IDs.

Defensive checks avoid accidental negatives from future generator changes.

 private String encode(long id) {
+    if (id < 0) throw new IllegalArgumentException("id must be >= 0");
     long targetId = id + ID_OFFSET;
+    if (targetId == 0) return String.valueOf(BASE62_CHARS.charAt(0));
     StringBuilder sb = new StringBuilder();
monitoring/k6/performance-test-with-phases.js (3)

213-216: Also assert Location header on 302 to catch broken redirects.

-const success = check(response, {
-    // 이제 302 상태 코드만 성공으로 간주합니다.
-    'status is 302': (r) => r.status === 302,
-});
+const success = check(response, {
+    'status is 302': (r) => r.status === 302,
+    'has Location header': (r) => !!r.headers.Location && r.headers.Location.length > 0,
+});

Also applies to: 205-211


218-221: Reduce noisy per-request logging under load.

Console I/O is expensive and can skew results. Log only early failures per VU.

-if (!success) {
-    console.log(`❌ 요청 실패: ${url} - Status: ${response.status}, Body: ${response.body}`);
-}
+if (!success && __ITER < 3) {
+    console.log(`❌ 요청 실패: ${url} - Status: ${response.status}`);
+}

69-84: Thresholds are very loose; consider adding per-phase SLIs via tags.

Add separate thresholds per phase (phase: baseline/indexed/cached) to make regressions visible.

Example:

thresholds: {
  'http_req_duration{phase:baseline}': ['p(95)<2000'],
  'http_req_duration{phase:indexed}':  ['p(95)<500'],
  'http_req_duration{phase:cached}':   ['p(95)<100'],
  errors: ['rate<0.05'],
}
docker-compose.yml (2)

90-201: Avoid floating “latest” tags to improve reproducibility.

Pin images (prometheus, grafana, exporters, cadvisor, k6, influxdb) to known versions to prevent surprise breakages on fresh pulls.

If you want, I can propose pinned tags aligned with your current stack versions.


200-216: cadvisor + privileged + host mounts may not work on macOS/Windows.

If teammates run on Docker Desktop, consider documenting platform caveats or gating cadvisor behind a profile.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 961f656 and 4056038.

📒 Files selected for processing (21)
  • .dockerignore (1 hunks)
  • .gitignore (1 hunks)
  • Dockerfile (1 hunks)
  • README.md (4 hunks)
  • build.gradle (1 hunks)
  • docker-compose.yml (2 hunks)
  • monitoring/grafana/provisioning/datasources/influxdb.yml (1 hunks)
  • monitoring/grafana/provisioning/datasources/prometheus.yml (1 hunks)
  • monitoring/k6/performance-test-with-phases.js (1 hunks)
  • monitoring/prometheus/prometheus.yml (1 hunks)
  • src/main/java/com/example/bitly/config/RedisConfig.java (1 hunks)
  • src/main/java/com/example/bitly/controller/UrlController.java (2 hunks)
  • src/main/java/com/example/bitly/entity/Url.java (0 hunks)
  • src/main/java/com/example/bitly/entity/UrlBaseline.java (1 hunks)
  • src/main/java/com/example/bitly/entity/UrlIndexed.java (1 hunks)
  • src/main/java/com/example/bitly/repository/UrlBaselineRepository.java (1 hunks)
  • src/main/java/com/example/bitly/repository/UrlIndexedRepository.java (1 hunks)
  • src/main/java/com/example/bitly/repository/UrlRepository.java (0 hunks)
  • src/main/java/com/example/bitly/service/CacheService.java (1 hunks)
  • src/main/java/com/example/bitly/service/UrlService.java (1 hunks)
  • src/main/resources/application.yml (1 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/com/example/bitly/entity/Url.java
  • src/main/java/com/example/bitly/repository/UrlRepository.java
🧰 Additional context used
🧬 Code graph analysis (8)
src/main/java/com/example/bitly/repository/UrlIndexedRepository.java (2)
src/main/java/com/example/bitly/repository/UrlRepository.java (3)
  • UrlRepository (7-12)
  • findByShortUrl (11-11)
  • findByOriginalUrl (9-9)
src/main/java/com/example/bitly/entity/Url.java (1)
  • Entity (12-33)
src/main/java/com/example/bitly/repository/UrlBaselineRepository.java (2)
src/main/java/com/example/bitly/repository/UrlRepository.java (3)
  • UrlRepository (7-12)
  • findByShortUrl (11-11)
  • findByOriginalUrl (9-9)
src/main/java/com/example/bitly/entity/Url.java (1)
  • Entity (12-33)
src/main/java/com/example/bitly/config/RedisConfig.java (1)
src/main/java/com/example/bitly/BitlyApplication.java (1)
  • SpringBootApplication (6-13)
src/main/java/com/example/bitly/service/CacheService.java (1)
src/main/java/com/example/bitly/service/UrlService.java (1)
  • Service (15-122)
src/main/java/com/example/bitly/controller/UrlController.java (1)
src/main/java/com/example/bitly/repository/UrlRepository.java (1)
  • UrlRepository (7-12)
src/main/java/com/example/bitly/entity/UrlBaseline.java (4)
src/main/java/com/example/bitly/controller/dto/UrlCreateRequest.java (1)
  • Getter (6-10)
src/main/java/com/example/bitly/entity/UrlIndexed.java (1)
  • Entity (9-26)
src/main/java/com/example/bitly/entity/Url.java (2)
  • Entity (12-33)
  • Url (26-28)
src/main/java/com/example/bitly/repository/UrlRepository.java (2)
  • UrlRepository (7-12)
  • findByShortUrl (11-11)
src/main/java/com/example/bitly/service/UrlService.java (3)
src/main/java/com/example/bitly/service/CacheService.java (1)
  • Service (11-92)
src/main/java/com/example/bitly/repository/UrlRepository.java (1)
  • UrlRepository (7-12)
src/main/java/com/example/bitly/entity/Url.java (1)
  • Entity (12-33)
src/main/java/com/example/bitly/entity/UrlIndexed.java (2)
src/main/java/com/example/bitly/entity/UrlBaseline.java (1)
  • Entity (9-27)
src/main/java/com/example/bitly/entity/Url.java (2)
  • Entity (12-33)
  • Url (26-28)
🪛 markdownlint-cli2 (0.17.2)
README.md

96-96: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


246-246: Bare URL used

(MD034, no-bare-urls)


247-247: Bare URL used

(MD034, no-bare-urls)


248-248: Bare URL used

(MD034, no-bare-urls)


279-279: Bare URL used

(MD034, no-bare-urls)


280-280: Bare URL used

(MD034, no-bare-urls)


281-281: Bare URL used

(MD034, no-bare-urls)


282-282: Bare URL used

(MD034, no-bare-urls)


283-283: Bare URL used

(MD034, no-bare-urls)


284-284: Bare URL used

(MD034, no-bare-urls)


308-308: Bare URL used

(MD034, no-bare-urls)

🪛 Checkov (3.2.334)
docker-compose.yml

[medium] 152-153: Basic Auth Credentials

(CKV_SECRET_4)

🔇 Additional comments (6)
build.gradle (1)

23-24: Actuator + Prometheus deps: good; ensure the Prometheus endpoint is exposed.

With Boot 3.x, /actuator/prometheus is not exposed by default. Please verify application.yml includes exposure for prometheus and enable health probes.

Add (outside this file):

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      probes:
        enabled: true
monitoring/grafana/provisioning/datasources/prometheus.yml (1)

3-9: Prometheus datasource provisioning looks good.

Confirm your docker-compose service name is prometheus on port 9090. Optionally set a scrape-aligned timeInterval.

Optional addition:

jsonData:
  timeInterval: 5s
src/main/java/com/example/bitly/repository/UrlBaselineRepository.java (1)

8-13: LGTM.

Repository methods and types look correct.

src/main/java/com/example/bitly/repository/UrlIndexedRepository.java (1)

8-11: LGTM.

Finder signature matches entity usage.

src/main/resources/application.yml (1)

9-14: Redis properties under spring.data.redis won’t configure the client. Use spring.redis.*.

Current settings will be ignored; may cause unexpected localhost defaults.

-  data:
-    redis:
-      host: ${REDIS_HOST:localhost}
-      port: ${REDIS_PORT:6379}
-      timeout: 2000ms
-      connect-timeout: 2000ms
+  redis:
+    host: ${REDIS_HOST:localhost}
+    port: ${REDIS_PORT:6379}
+    timeout: 2000ms
+    connect-timeout: 2000ms

Likely an incorrect or invalid review comment.

docker-compose.yml (1)

80-84: API healthcheck uses curl; ensure it’s installed in the API image.

If the base image doesn’t include curl, the healthcheck will always fail. Either add curl in the Dockerfile or switch to wget/busybox-httpd or CMD-SHELL with sh + /dev/tcp.

Comment on lines +36 to +45
# Temporary files
*.tmp
*.log
*.pid
*.seed
*.pid.lock

# Node modules (if any)
node_modules/

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Exclude secrets from the Docker build context.

Prevent accidental leakage of env files/keys into image layers.

Apply:

 # Temporary files
 *.tmp
 *.log
 *.pid
 *.seed
 *.pid.lock
+
+# Secrets
+.env
+.env.*
+**/*.pem
+**/*.key
+**/*_rsa
+**/*_ed25519
+**/*secret*
📝 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
# Temporary files
*.tmp
*.log
*.pid
*.seed
*.pid.lock
# Node modules (if any)
node_modules/
# Temporary files
*.tmp
*.log
*.pid
*.seed
*.pid.lock
# Secrets
.env
.env.*
**/*.pem
**/*.key
**/*_rsa
**/*_ed25519
**/*secret*
# Node modules (if any)
node_modules/
🤖 Prompt for AI Agents
In .dockerignore around lines 36–45, the file currently ignores temp files and
node_modules but does not exclude environment files or secret keys; update the
Docker ignore entries to explicitly exclude secrets from the build context by
adding patterns such as .env, .env.*, *.key, *.pem, id_rsa*, .aws, .aws/*,
.git-credentials, .gpg, .secrets, secrets/, and any project-specific secret
filenames or directories so environment variables, private keys, and credential
files are not sent to Docker during build.

Comment on lines +122 to +123
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Grafana admin password is set to a weak default. Externalize it.

-      - GF_SECURITY_ADMIN_PASSWORD=admin
+      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}

Set GRAFANA_ADMIN_PASSWORD via .env for local use.

📝 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
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}
- GF_USERS_ALLOW_SIGN_UP=false
🤖 Prompt for AI Agents
In docker-compose.yml around lines 122-123, the Grafana admin password is
hardcoded to a weak default; replace the literal
GF_SECURITY_ADMIN_PASSWORD=admin with
GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} and instruct developers to
add GRAFANA_ADMIN_PASSWORD=your_secure_password to the project's .env (and
ensure .env is in .gitignore), so the secret is externalized for local use; keep
GF_USERS_ALLOW_SIGN_UP as-is.

Comment on lines +152 to +153
- DATA_SOURCE_NAME=postgresql://bitly:bitly@postgres:5432/bitly?sslmode=disable
depends_on:
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Hard-coded Postgres credentials in DATA_SOURCE_NAME. Move to env or Docker secrets.

Use .env or secrets to avoid committing credentials, even for dev.

-      - DATA_SOURCE_NAME=postgresql://bitly:bitly@postgres:5432/bitly?sslmode=disable
+      - DATA_SOURCE_NAME=${POSTGRES_EXPORTER_DSN}

And define POSTGRES_EXPORTER_DSN in a local .env (not committed) or use Docker secrets.

📝 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
- DATA_SOURCE_NAME=postgresql://bitly:bitly@postgres:5432/bitly?sslmode=disable
depends_on:
- DATA_SOURCE_NAME=${POSTGRES_EXPORTER_DSN}
depends_on:
🧰 Tools
🪛 Checkov (3.2.334)

[medium] 152-153: Basic Auth Credentials

(CKV_SECRET_4)

🤖 Prompt for AI Agents
docker-compose.yml around lines 152-153: the Postgres DSN is hard-coded with
credentials; remove the embedded credentials and instead reference an
environment variable or Docker secret (e.g. DATA_SOURCE_NAME or POSTGRES_DSN)
and update the service to use either env_file/.env (ignored in VCS) or Docker
secrets for the username/password/connection string; additionally add a
POSTGRES_EXPORTER_DSN variable referenced from .env or secrets (do not commit
.env) so both the app and postgres_exporter consume credentials via env or
secrets rather than inline literals.

FROM gradle:8.3-jdk17 AS builder
WORKDIR /home/gradle/project
COPY . .
RUN gradle build --no-daemon -x test
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Wildcard COPY may fail: two jars (boot + plain) commonly exist.

build/libs/*.jar -> app.jar is ambiguous if both boot and plain jars are present. Prefer building a single bootJar with a fixed name and disable the plain jar.

Apply:

-RUN gradle build --no-daemon -x test
+RUN gradle bootJar --no-daemon -x test
@@
-COPY --from=builder /home/gradle/project/build/libs/*.jar app.jar
+COPY --from=builder /home/gradle/project/build/libs/app.jar app.jar

And in build.gradle (outside this file):

+tasks.named('jar') { enabled = false }
+tasks.named('bootJar') {
+  archiveFileName = 'app.jar'
+}

Also applies to: 12-13

🤖 Prompt for AI Agents
In Dockerfile around lines 5 and also affecting lines 12-13, the current build
and COPY use a wildcard jar (build/libs/*.jar -> app.jar) which is ambiguous
when both boot and plain jars are produced; change the Gradle build to produce a
single bootJar with a fixed name and disable the plain (jar) artifact in
build.gradle (e.g., enable bootJar, disable plain jar, and set
archiveBaseName/archiveFileName), run gradle bootJar (or gradle build ensuring
only the bootJar is created), and update the Dockerfile to COPY the fixed boot
jar filename (e.g., build/libs/my-app.jar) instead of using a wildcard.

Comment on lines +8 to +9
FROM openjdk:17-slim
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden runtime image: drop root and simplify JVM flags.

  • Run as a non-root user.
  • -XX:+UseContainerSupport is on by default in JDK 17; you can omit it.

Apply:

 FROM openjdk:17-slim
-RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
+RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists* \
+  && useradd -m -u 10001 appuser
@@
-ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
+ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0"
-CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
+USER appuser
+CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Also applies to: 18-19

🤖 Prompt for AI Agents
In Dockerfile around lines 8-9, the image installs packages and runs as root and
the JVM flags include -XX:+UseContainerSupport which is redundant in JDK 17;
modify the Dockerfile to create a dedicated non-root user (e.g.,
addgroup/adduser or useradd), set ownership of the application directories to
that user, switch to that user with the USER instruction before runtime, and
remove -XX:+UseContainerSupport from any JVM options; ensure any files or
directories the app needs are chowned appropriately and that the image still
exposes the required ports and environment variables for the non-root user.

Comment on lines +18 to +20
private static final String CACHE_PREFIX = "url:";
private static final Duration DEFAULT_TTL = Duration.ofMinutes(15); // 15분

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 TTL configurable via properties.

Externalize TTL for tuning per environment.

-    private static final String CACHE_PREFIX = "url:";
-    private static final Duration DEFAULT_TTL = Duration.ofMinutes(15); // 15분
+    private static final String CACHE_PREFIX = "url:";
+    @org.springframework.beans.factory.annotation.Value("${app.cache.url.ttl:15m}")
+    private Duration defaultTtl; // e.g., "15m"

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/example/bitly/service/CacheService.java around lines 18 to
20, the DEFAULT_TTL is hard-coded; change it to be configurable via application
properties by adding a property (e.g. bitly.cache.ttl) and wiring it into the
service (use @Value or a @ConfigurationProperties bean) so the TTL can be set
per environment; parse the property into a java.time.Duration with a sensible
default of 15 minutes when the property is absent or invalid, remove the static
final constant, and use the injected Duration for cache operations.

Comment on lines +72 to +81
public CacheStats getStats() {
try {
// Redis INFO 명령으로 통계 조회 (간단한 구현)
// 실제 운영에서는 Redis INFO 명령을 직접 사용하거나 RedisTemplate의 info() 메서드 사용
return new CacheStats(0, 0, 0.0); // 임시 구현
} catch (Exception e) {
log.error("Error getting cache stats", e);
return new CacheStats(0, 0, 0.0);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Implement real cache stats via Redis INFO (hits/misses).

Expose meaningful numbers for k6/Grafana.

-    public CacheStats getStats() {
-        try {
-            // Redis INFO 명령으로 통계 조회 (간단한 구현)
-            // 실제 운영에서는 Redis INFO 명령을 직접 사용하거나 RedisTemplate의 info() 메서드 사용
-            return new CacheStats(0, 0, 0.0); // 임시 구현
-        } catch (Exception e) {
-            log.error("Error getting cache stats", e);
-            return new CacheStats(0, 0, 0.0);
-        }
-    }
+    public CacheStats getStats() {
+        try {
+            java.util.Properties info = redisTemplate.execute(conn -> conn.info("stats"));
+            long hits = Long.parseLong(info.getProperty("keyspace_hits", "0"));
+            long misses = Long.parseLong(info.getProperty("keyspace_misses", "0"));
+            long total = hits + misses;
+            double hitRate = total == 0 ? 0.0 : (hits * 100.0) / total;
+            return new CacheStats(hits, misses, hitRate);
+        } catch (Exception e) {
+            log.error("Error getting cache stats", e);
+            return new CacheStats(0, 0, 0.0);
+        }
+    }

I can wire this into a /metrics gauge if you want.

📝 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 CacheStats getStats() {
try {
// Redis INFO 명령으로 통계 조회 (간단한 구현)
// 실제 운영에서는 Redis INFO 명령을 직접 사용하거나 RedisTemplate의 info() 메서드 사용
return new CacheStats(0, 0, 0.0); // 임시 구현
} catch (Exception e) {
log.error("Error getting cache stats", e);
return new CacheStats(0, 0, 0.0);
}
}
public CacheStats getStats() {
try {
java.util.Properties info = redisTemplate.execute(conn -> conn.info("stats"));
long hits = Long.parseLong(info.getProperty("keyspace_hits", "0"));
long misses = Long.parseLong(info.getProperty("keyspace_misses", "0"));
long total = hits + misses;
double hitRate = total == 0 ? 0.0 : (hits * 100.0) / total;
return new CacheStats(hits, misses, hitRate);
} catch (Exception e) {
log.error("Error getting cache stats", e);
return new CacheStats(0, 0, 0.0);
}
}
🤖 Prompt for AI Agents
In src/main/java/com/example/bitly/service/CacheService.java around lines 72-81,
replace the temporary stub with a real Redis INFO-based implementation: use your
RedisTemplate/connection factory to call the Redis INFO (stats) command, parse
the returned map/string for keyspace_hits and keyspace_misses, convert to long
values, compute hit rate (hits / (hits + misses) with zero-division guard), and
populate and return a CacheStats(hits, misses, hitRate). Preserve the existing
try/catch: on exception log the error and return a zeroed CacheStats; ensure
parsing is robust to missing keys by treating absent values as 0.

private final CacheService cacheService;
private static final String BASE62_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final long ID_OFFSET = 1000000000L;
private static final AtomicLong COUNTER = new AtomicLong(System.currentTimeMillis());
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Short URL generator is unsafe across restarts/scale-out; make ID generation distributed and collision-resistant.

An in-memory AtomicLong seeded with currentTimeMillis() risks collisions after restarts and with multiple API replicas. There’s also no DB uniqueness guard on shortUrl to fail fast.

  • Prefer a distributed monotonic counter (e.g., RedisAtomicLong) or a DB sequence.
  • Regardless of generator, enforce a unique index on shortUrl and add a retry-on-conflict loop.

Example (retry on unique-constraint violations around the persist path):

@@ public String createShortUrl(String originalUrl) {
-                // 새로운 shortUrl 생성
-                String shortUrl = generateUniqueShortUrl();
-
-                // 1. Baseline ...
-                UrlBaseline baselineUrl = UrlBaseline.builder()
-                        .shortUrl(shortUrl)
-                        .originalUrl(originalUrl)
-                        .build();
-                urlBaselineRepository.save(baselineUrl);
-
-                // 2. Indexed ...
-                UrlIndexed indexedUrl = UrlIndexed.builder()
-                        .shortUrl(shortUrl)
-                        .originalUrl(originalUrl)
-                        .build();
-                urlIndexedRepository.save(indexedUrl);
-
-                log.info("URL 생성 완료: {} -> {} (2개 테이블에 저장)", shortUrl, originalUrl);
-                return shortUrl;
+                for (int attempt = 1; attempt <= 5; attempt++) {
+                    String shortUrl = generateUniqueShortUrl();
+                    try {
+                        UrlBaseline baselineUrl = UrlBaseline.builder()
+                                .shortUrl(shortUrl)
+                                .originalUrl(originalUrl)
+                                .build();
+                        urlBaselineRepository.save(baselineUrl);
+
+                        UrlIndexed indexedUrl = UrlIndexed.builder()
+                                .shortUrl(shortUrl)
+                                .originalUrl(originalUrl)
+                                .build();
+                        urlIndexedRepository.save(indexedUrl);
+
+                        log.info("URL 생성 완료: {} -> {} (2개 테이블에 저장)", shortUrl, originalUrl);
+                        return shortUrl;
+                    } catch (org.springframework.dao.DataIntegrityViolationException e) {
+                        log.warn("shortUrl 충돌 발생, 재시도 중 (attempt {}/5): {}", attempt, shortUrl);
+                    }
+                }
+                throw new IllegalStateException("고유 shortUrl 생성 실패(5회 재시도 후).");

Follow-up:

  • Add a unique index on shortUrl in both UrlBaseline and UrlIndexed.
  • Optionally switch COUNTER to a RedisAtomicLong-backed sequence for multi-instance safety.

Also applies to: 56-59

🤖 Prompt for AI Agents
In src/main/java/com/example/bitly/service/UrlService.java around lines 25 (and
also apply same guidance to lines 56-59), the current in-memory AtomicLong
seeded with System.currentTimeMillis() is unsafe across restarts and multiple
replicas and can cause shortUrl collisions; replace it with a distributed
monotonic generator (for example a RedisAtomicLong or a DB sequence) or use the
database's sequence/auto-increment to allocate IDs, add a unique
index/constraint on the shortUrl column in both UrlBaseline and UrlIndexed
entity mappings/migrations, and wrap the persist/create logic in a
retry-on-conflict loop that catches unique-constraint violations and retries ID
generation/persist a few times before failing so collisions are resolved safely
in multi-instance deployments.

Comment on lines +29 to +33
// 이미 등록된 URL인지 확인 (Baseline 테이블에서 체크)
return urlBaselineRepository.findByOriginalUrl(originalUrl)
.map(UrlBaseline::getShortUrl)
.orElseGet(() -> {
// 새로운 shortUrl 생성
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

TOCTOU on findByOriginalUrl; enforce idempotency at the DB layer.

Concurrent POSTs with the same originalUrl can pass the pre-check and create divergent mappings unless originalUrl is uniquely constrained. Add a unique index on originalUrl (if you intend create to be idempotent) and handle conflicts by returning the existing mapping. Otherwise, remove this pre-check to allow multiple shortUrls per originalUrl by design.

Would you like a migration snippet (Flyway/Liquibase) to add unique indexes on (short_url) and optionally (original_url) for both tables?


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== show UrlService.java (if present) ==="
if [ -f src/main/java/com/example/bitly/service/UrlService.java ]; then
  sed -n '1,240p' src/main/java/com/example/bitly/service/UrlService.java || true
else
  echo "MISSING: src/main/java/com/example/bitly/service/UrlService.java"
fi

echo
echo "=== search for repository methods and entity classes mentioning Original/short URL ==="
rg -n --hidden --follow --line-number "findByOriginalUrl|findByOriginalurl|findByOriginal" || true
rg -n --hidden --follow --line-number "UrlBaseline" || true
rg -n --hidden --follow --line-number "UrlBaselineRepository|BaselineRepository" || true

echo
echo "=== search for fields/columns named originalUrl / original_url / shortUrl / short_url ==="
rg -n --hidden --follow --line-number "originalUrl|original_url|shortUrl|short_url" || true

echo
echo "=== search for JPA unique constraints / table DDL / migration files ==="
rg -n --hidden --follow --line-number "@Column\\([^)]*unique\\s*=\\s*true" || true
rg -n --hidden --follow --line-number "@Table\\([^)]*uniqueConstraints" || true
rg -n --hidden --follow --line-number "uniqueConstraints|@UniqueConstraint" || true
rg -n --hidden --follow --line-number "CREATE UNIQUE|CREATE UNIQUE INDEX|UNIQUE KEY|ALTER TABLE .*ADD CONSTRAINT .*UNIQUE" src/main/resources || true

echo
echo "=== dump candidate entity & repository files (first match of UrlBaseline / repository) ==="
entity_file=$(rg -n --hidden --follow --line-number --glob '!**/target/**' "class .*UrlBaseline|interface .*UrlBaseline" -S || true | sed -n '1p' | cut -d: -f1 || true)
if [ -n "$entity_file" ] && [ -f "$entity_file" ]; then
  echo "Found entity file: $entity_file"
  sed -n '1,240p' "$entity_file" || true
else
  echo "No UrlBaseline class file found by pattern search."
fi

repo_file=$(rg -n --hidden --follow --line-number --glob '!**/target/**' "interface .*UrlBaselineRepository|class .*UrlBaselineRepository" -S || true | sed -n '1p' | cut -d: -f1 || true)
if [ -n "$repo_file" ] && [ -f "$repo_file" ]; then
  echo "Found repository file: $repo_file"
  sed -n '1,240p' "$repo_file" || true
else
  echo "No UrlBaselineRepository file found by pattern search."
fi

echo
echo "=== list likely DB migration directories under src/main/resources ==="
rg -n --hidden --follow --line-number "db/migration|migrations|liquibase|changelog|V[0-9]+__" src/main/resources || true

echo
echo "=== done ==="

Length of output: 20814


Enforce idempotent create: add UNIQUE constraint on original_url and handle conflicts

createShortUrl does a pre-check via urlBaselineRepository.findByOriginalUrl(...) but UrlBaseline.original_url has no UNIQUE constraint (only short_url is unique), so concurrent POSTs can create divergent shortUrls for the same originalUrl.

  • Add a UNIQUE index/constraint on original_url (entity + DB migration) and handle insert conflicts by returning the existing mapping (upsert/ON CONFLICT ... RETURNING, or catch unique-constraint exceptions such as DataIntegrityViolationException/ConstraintViolationException and SELECT the existing row).
  • If multiple shortUrls per originalUrl is intended, remove the pre-check instead.

Locations: src/main/java/com/example/bitly/service/UrlService.java (createShortUrl), src/main/java/com/example/bitly/entity/UrlBaseline.java (original_url column).

🤖 Prompt for AI Agents
In src/main/java/com/example/bitly/service/UrlService.java around the
createShortUrl logic (lines ~29-33) and
src/main/java/com/example/bitly/entity/UrlBaseline.java (original_url column),
the service currently does a pre-check findByOriginalUrl(...) but original_url
lacks a UNIQUE constraint, so concurrent requests can create duplicate mappings;
add a UNIQUE constraint/index on original_url at the entity level (e.g.,
unique=true or @Table(indexes=...)) and create a DB migration to enforce it,
then change createShortUrl to perform an atomic upsert (ON CONFLICT ...
RETURNING) or attempt the insert and on unique-constraint failure catch the
appropriate exception
(DataIntegrityViolationException/ConstraintViolationException), query the
existing UrlBaseline for the originalUrl and return that mapping; alternatively,
if multiple shortUrls are intended, remove the pre-check and document that
behavior.

Comment on lines +31 to +35
redis:
host: redis
port: 6379
timeout: 2000ms
connect-timeout: 2000ms
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix docker profile Redis properties too.

Same issue in the docker profile.

-  data:
-    redis:
-      host: redis
-      port: 6379
-      timeout: 2000ms
-      connect-timeout: 2000ms
+  redis:
+    host: redis
+    port: 6379
+    timeout: 2000ms
+    connect-timeout: 2000ms

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/resources/application.yml around lines 31 to 35, the Redis
properties (host, port, timeout, connect-timeout) are set for the default
profile but the docker profile also needs the same keys; add or update the redis
block inside the docker profile section to include host: redis, port: 6379,
timeout: 2000ms and connect-timeout: 2000ms so the docker profile uses the same
Redis configuration.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
README.md (1)

128-130: README text vs Compose: api 서비스는 이미 포함되어 있습니다.

문구가 현재 상태와 불일치합니다. 혼선을 줄이기 위해 갱신하세요.

-- PostgreSQL 16, Redis 7(alpine). Spring Boot 앱은 추후 `api` 서비스로 추가 예정.
+- PostgreSQL 16, Redis 7(alpine), Spring Boot 앱(`api`) 포함.
🧹 Nitpick comments (5)
README.md (5)

102-107: Add blank lines around the table to satisfy markdownlint (MD058).

-#### 7.2 Performance Comparison Matrix
-| Phase | Architecture | Expected p95 | Expected RPS | Cache Hit Rate |
+#### 7.2 Performance Comparison Matrix
+
+| Phase | Architecture | Expected p95 | Expected RPS | Cache Hit Rate |
 ...
-| 3 | Redis + DB | 5-20ms | ~200-500 | 90%+ |
+| 3 | Redis + DB | 5-20ms | ~200-500 | 90%+ |
+

168-168: Wrap bare URL to satisfy markdownlint (MD034).

-1. Grafana 접속: http://localhost:3000
+1. Grafana 접속: <http://localhost:3000>

55-58: Ensure atomic writes to two tables (transactional requirement).

두 테이블 동시 저장은 부분 실패 시 데이터 불일치 위험이 큽니다. 서비스 레이어에서 단일 트랜잭션으로 보장되는지, 재시도/보상 전략이 있는지 명시하세요.

원하시면 트랜잭션 경계와 예외 처리 예시를 README에 추가하는 패치를 제안할 수 있습니다.


77-78: Use consistent terminology for the key: shortUrl vs shortCode.

문서 전반에서 shortUrl을 사용 중입니다. 키 포맷도 일관되게 맞추세요.

-  - 키 포맷: `url:{shortCode}`
+  - 키 포맷: `url:{shortUrl}`

34-38: Document the additional phase-support endpoints (stats/prepare) for completeness.

코드에 /stats, /prepare/baseline, /prepare/indexed가 있다면 여기에도 기재해 주세요.

 #### 4.2 Phase별 테스트 API들
 - **GET** `/api/v1/urls/baseline/{shortUrl}`: **Phase 1** - DB 직접 조회 (인덱스 없음)
 - **GET** `/api/v1/urls/indexed/{shortUrl}`: **Phase 2** - 인덱스 활용 조회
 - **GET** `/api/v1/urls/cached/{shortUrl}`: **Phase 3** - Redis 캐시 우선 조회
+
+#### 4.3 유틸리티/준비 API
+- **GET** `/api/v1/urls/stats`: 캐시 통계 조회 (hit/miss 등)
+- **POST** `/api/v1/urls/prepare/baseline`: Phase 1 데이터 준비
+- **POST** `/api/v1/urls/prepare/indexed`: Phase 2 데이터 준비
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4056038 and 2fe396c.

📒 Files selected for processing (2)
  • .github/workflows/gemini-review.yml (0 hunks)
  • README.md (2 hunks)
💤 Files with no reviewable changes (1)
  • .github/workflows/gemini-review.yml
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
README.md

102-102: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


168-168: Bare URL used

(MD034, no-bare-urls)

Comment on lines +50 to +53
- `short_url` (varchar, unique) — B-tree 인덱스 있음 (Index Scan)
- `original_url` (varchar, 2000자)
- 인덱스: `idx_short_url_indexed` on `short_url`

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 duplicate indexing: unique constraint already creates an index.

지금 설명은 unique + 별도 인덱스를 둘 다 암시합니다. 중복 인덱스는 쓰기 성능과 공간에 불리합니다. 하나로 통일하세요(권장: 명시적 unique index 한 개).

-  - `short_url` (varchar, unique) — B-tree 인덱스 있음 (Index Scan)
-  - `original_url` (varchar, 2000자)
-  - 인덱스: `idx_short_url_indexed` on `short_url`
+  - `short_url` (varchar)
+  - `original_url` (varchar, 2000자)
+  - 고유 인덱스: `idx_short_url_indexed` on (`short_url`)  — (unique index 1개로 일원화)
📝 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
- `short_url` (varchar, unique) — B-tree 인덱스 있음 (Index Scan)
- `original_url` (varchar, 2000자)
- 인덱스: `idx_short_url_indexed` on `short_url`
- `short_url` (varchar)
- `original_url` (varchar, 2000자)
- 고유 인덱스: `idx_short_url_indexed` on (`short_url`) — (unique index 1개로 일원화)
🤖 Prompt for AI Agents
In README.md around lines 50-53, the text currently implies both a unique
constraint and a separate index on short_url which creates a duplicate index;
update the wording to describe a single index only — remove the separate
idx_short_url_indexed mention (or state that the short_url column has a single
explicit unique index and give its name), and clarify that the unique constraint
provides the index so there is no additional index to avoid duplicate indexes
and wasted space/write overhead.

Comment on lines +79 to +80
- 에비션: LRU (Redis 기본)
- **특징**: O(1) 캐시 조회, DB 폴백
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix Redis eviction policy: LRU is not Redis’ default.

Default is noeviction. If you want LRU in tests, set maxmemory and maxmemory-policy=allkeys-lru. Adjust the doc accordingly.

-  - TTL: 15분 (단일 정책으로 단순화)
-  - 에비션: LRU (Redis 기본)
+  - TTL: 15분 (단일 정책으로 단순화)
+  - 에비션: allkeys-lru (실험용) — Redis 기본값은 noeviction 이므로 반드시 maxmemory 및 maxmemory-policy를 설정해야 함
📝 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
- 에비션: LRU (Redis 기본)
- **특징**: O(1) 캐시 조회, DB 폴백
- TTL: 15분 (단일 정책으로 단순화)
- 에비션: allkeys-lru (실험용) — Redis 기본값은 noeviction 이므로 반드시 maxmemory 및 maxmemory-policy를 설정해야 함
- **특징**: O(1) 캐시 조회, DB 폴백
🤖 Prompt for AI Agents
In README.md around lines 79-80, update the incorrect statement that Redis’
default eviction is LRU: change the text to state that Redis default is
noeviction and, if you need LRU for tests or environments, set maxmemory plus
maxmemory-policy=allkeys-lru (or a specific LRU variant) and document that those
settings are required for LRU behavior; ensure the feature bullet reflects O(1)
cache lookup and DB fallback but clarifies eviction must be configured via
maxmemory and maxmemory-policy.

Comment on lines +85 to +87
- **에비션 정책**: Redis LRU (최근 사용 우선)
- **TTL 전략**: 15분 고정 (단순화된 정책)
- **일관성**: Read-only 워크로드로 캐시 무효화 최소화
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct repeated eviction-policy statement and add operational preconditions.

Reiterate that LRU requires explicit config; otherwise your cache will not evict and hit rate metrics will be misleading.

-**에비션 정책**: Redis LRU (최근 사용 우선)
-**TTL 전략**: 15분 고정 (단순화된 정책)
+**에비션 정책**: allkeys-lru (실험용 설정)
+  - 전제: `maxmemory`와 `maxmemory-policy=allkeys-lru`가 설정되어 있어야 함
+**TTL 전략**: 15분 고정 (단순화된 정책)

예시(참고, docker-compose의 redis 서비스에 적용):

command: ["redis-server", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
🤖 Prompt for AI Agents
In README.md around lines 85-87, fix the duplicated/misspelled "에비션 정책" to "에비션
정책 (eviction policy)" and clarify that LRU is not enabled by default: state that
Redis must be started with both a maxmemory limit and an explicit
maxmemory-policy of an LRU variant (e.g., allkeys-lru) otherwise keys will not
be evicted and hit-rate metrics will be misleading; add an "Operational
preconditions" bullet listing required startup flags (set maxmemory and set
maxmemory-policy to an LRU mode) and include a short note pointing readers to a
docker-compose example for how to pass these redis-server flags.

@jungeunyooon jungeunyooon merged commit 3a8b918 into main Sep 14, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

모니터링 세팅 및 고도화

2 participants