diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..c2065bc2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,37 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..c35b6693
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,7 @@
+FROM openjdk:17-jdk
+LABEL maintainer="Yosongsong"
+
+ARG JAR_FILE=build/libs/springboot-url-shortener-0.0.1-SNAPSHOT.jar
+ADD ${JAR_FILE} url-shortener-springboot.jar
+
+ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/url-shortener-springboot.jar"]
diff --git a/README.md b/README.md
index a557279f..df00d01c 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,79 @@
-# springboot-url-shortener
-SprintBoot URL Shortener 구현 미션 Repository 입니다.
-
-## 요구사항
-각 요구사항을 모두 충족할 수 있도록 노력해봅시다.
-- [ ] URL 입력폼 제공 및 결과 출력
-- [ ] URL Shortening Key는 8 Character 이내로 생성
-- [ ] 단축된 URL 요청시 원래 URL로 리다이렉트
-- [ ] 단축된 URL에 대한 요청 수 정보저장 (optional)
-- [ ] Shortening Key를 생성하는 알고리즘 2개 이상 제공하며 애플리케이션 실행중 동적으로 변경 가능 (optional) 
-
-
-## Short URL Service
-### 읽으면 좋은 레퍼런스
-- [Naver 단축 URL API](https://developers.naver.com/docs/utils/shortenurl/)
-- [짧게 줄인 URL의 실제 URL 확인 원리 및 방법](https://metalkin.tistory.com/50)
-- [짧게 줄인 URL 알고리즘 고찰](https://metalkin.tistory.com/53)
-- [단축 URL 원리 및 개발](https://blog.siyeol.com/26)
-
-### Short URL의 동작 과정
-예시로 bitly를 봅시다
-
-
-1. 원본 URL을 입력하고 Shorten 버튼을 클릭합니다.
-2. Unique Key를 7문자 생성합니다.
-3. Unique Key와 원본 URL을 DB에 저장합니다.
-4. bitly.com/{Unique Key} 로 접근하면, DB를 조회하여 원본 URL로 redirect합니다.
-
-### Short URL의 특징
-단축 URL서비스는 간편하지만, 단점(위험성)이 있습니다. 
-링크를 클릭하는 사용자는 단축된 URL만 보고 클릭하기 때문에 어떤 곳으로 이동할지 알 수 없습니다.
-
-- Short URL 서비스는 주로 요청을 Redirect 시킵니다. (Redirect와 Forward의 차이점에 대해 검색해보세요.)
-- 긴 URL을 짧은 URL로 압축할 수 있다.
-- short url만으로는 어디에 연결되어있는 지 알 수 없다. 때문에 피싱 사이트 등의 보안에 취약하다.
-- 광고를 본 뒤에 원본url로 넘겨주기도 한다. 이 과정에서 악성 광고가 나올 수 있다.
-- 당연하지만 이미 존재하는 키를 입력하여 들어오는 사람이 존재할 수 있다.
-- 기존의 원본 URL 변경되었더라도 단축 URL을 유지하여, 혼란을 방지할 수 있다.
-
-### 예시 사이트
-[https://url.kr/](https://url.kr/)
+## 📌 설명
+>시연 동영상입니다. https://vimeo.com/manage/videos/873085666
+
+- URL Shortner 서비스를 구현했습니다.
+- 프론트엔드와 백엔드 모두 구현했습니다.
+- Shortening key 알고리즘은 Base62, Short UUID, Adler Hashing을 사용했습니다.
+- 추가적인 팀 미션으로 배포를 진행했습니다.
+- 개인 미션으로 Redis를 활용한 캐싱을 구현했습니다.
+- 개인 미션으로 Docker를 이용한 배포를 진행했습니다.(도커 네트워크 활용)
+
+~~[Url Shortener 서비스 링크](http://ec2-3-35-240-254.ap-northeast-2.compute.amazonaws.com:3000)~~
+
+## 👩💻 요구 사항과 구현 내용 
+
+- [x] URL 입력폼 제공 및 결과 출력 
+   > 리액트로 구현했습니다. [(링크)](https://github.com/Dev-Yesung/react-url-shortener)
+- [x] URL Shortening Key는 8 Character 이내로 생성
+   > base62 방식으로 인코딩 완료
+- [x] 단축된 URL 요청시 원래 URL로 리다이렉트 
+   > 상태코드 301(MOVE_PERMANENTLY)
+- [x] 단축된 URL에 대한 요청 수 정보저장
+   > MySQL 이외에 Redis를 활용하여 캐싱했습니다.
+- [x] Shortening Key를 생성하는 알고리즘 2개 이상 제공하며 애플리케이션 실행중 동적으로 변경 가능
+   > 추가적으로 8글자 이내의 UUID, Adler 알고리즘을 사용하였습니다.
+
+## 📝 Redis를 활용한 캐싱과 MySQL과의 데이터 일치전략
+
+Redis는 서버가 다운될 것에 대비해 어느 정도 데이터를 백업해두는 기능을 갖고 있습니다.
+하지만 완벽한 백업이 아니기 때문에 Redis에서 사용하는 캐싱 데이터는
+다음의 조건을 만족하는 데이터에 사용하면 좋다고 생각합니다.
+
+1) 캐싱 했을 때의 성능(속도) 향상
+2) 손실되어도 괜찮은 데이터
+
+URL Shortener서버는 Redis를 두 가지 용도로 사용 중 입니다.
+1) 리다이렉션으로 보낼 원본 url을 빠르게 찾기 위해
+2) 인코딩된 url의 총 click 수를 빠르게 저장하고 조회하기 위해
+
+-----
+1️⃣
+인코딩된 shortening key에 매핑되는 원본 URL은 자주 변경되지 않습니다.
+그래서 RDB 저장소까지 가서 읽기를 수행할 필요가 없고 캐시 저장소를 통해
+빠르게 요청을 처리하면 좋을 거 같다고 생각했습니다.
+
+-----
+2️⃣
+클릭 수(API요청 횟수)에 관한 업데이트는 1차적으로 Redis에만 진행되도록 했습니다.
+그 이유는 클릭 수는 손실되어도 타격이 큰 데이터가 아니라는 생각을 했습니다.
+물론 선착순 당첨 이벤트와 같이 특수한 상황에서 클릭수의 경우 정확도가 중요하겠고
+마케팅 데이터로 활용할 클릭수는 어느 정도 의미가 있겠지만, 
+현재는 그런 특수한 상황이 아니라 배제했습니다.
+
+-----
+3️⃣
+속도나 동시성을 어느 정도 고려해주는게 좋다는 생각을 해서 Redis를 활용했습니다.
+Redis는 싱글 스레드 방식으로 작동하기 때문에 동시에 여러 스레드가 접근할 경우
+순차적으로 요청을 처리하게 되어 데이터 정합성을 보장하고 
+인메모리 데이터베이스라 속도 또한 보장하기 때문입니다.
+
+-----
+4️⃣
+1차적으로 Redis에서 업데이트된 클릭수는 
+매일 새벽 3시(트래픽이 가장 적게 몰릴것 같읕 시간)에
+MySQL로 데이터를 업데이트 합니다. 이때 처리하는 방법은
+@Scheduled(스프링 스케줄러)를 사용하였습니다. 
+
+-----
+5️⃣
+클릭수에 관한 데이터를 다루는 방법으로, 
+Redis 서버가 다운 될 것을 고려해 MySQL에도 클릭수를 저장할까 생각했지만, 
+클릭수가 크게 중요한 데이터가 아니고 서비스의 본질은 
+긴 URL을 줄이는 것과 빠르게 원본 URL을 찾아주는 거라 생각해 배제했습니다.
+
+-----
+6️⃣
+Redis에 계속해서 캐시 데이터를 두게 되면 메모리 낭비가 심할거라 생각해
+최근에 클릭한 데이터들에는 만료시간을 연장하는 알고리즘을 적용할까 생각했지만,
+구현할 시간이 없어 패스했습니다!
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..9e67466e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,35 @@
+plugins {
+    id 'java'
+    id 'org.springframework.boot' version '3.1.4'
+    id 'io.spring.dependency-management' version '1.1.3'
+}
+
+group = 'kr.co.programmers'
+version = '0.0.1-SNAPSHOT'
+
+java {
+    sourceCompatibility = '17'
+}
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+    implementation 'org.springframework.boot:spring-boot-starter-web'
+
+    compileOnly 'org.projectlombok:lombok:1.18.30'
+
+    runtimeOnly 'com.h2database:h2'
+    runtimeOnly 'com.mysql:mysql-connector-j'
+
+    testImplementation 'org.springframework.boot:spring-boot-starter-test'
+
+    annotationProcessor 'org.projectlombok:lombok:1.18.30'
+}
+
+tasks.named('test') {
+    useJUnitPlatform()
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..033e24c4
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..9f4197d5
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..e5ddb406
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+  APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+  [ -h "$app_path" ]
+do
+  ls=$(ls -ld "$app_path")
+  link=${ls#*' -> '}
+  case $link in         #(
+  /*) app_path=$link ;; #(
+  *) app_path=$APP_HOME$link ;;
+  esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$(cd "${APP_HOME:-./}" && pwd -P) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn() {
+  echo "$*"
+} >&2
+
+die() {
+  echo
+  echo "$*"
+  echo
+  exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$(uname)" in           #(
+CYGWIN*) cygwin=true ;;      #(
+Darwin*) darwin=true ;;      #(
+MSYS* | MINGW*) msys=true ;; #(
+NONSTOP*) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ]; then
+  if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+    # IBM's JDK on AIX uses strange locations for the executables
+    JAVACMD=$JAVA_HOME/jre/sh/java
+  else
+    JAVACMD=$JAVA_HOME/bin/java
+  fi
+  if [ ! -x "$JAVACMD" ]; then
+    die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+  fi
+else
+  JAVACMD=java
+  if ! command -v java >/dev/null 2>&1; then
+    die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+  fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop"; then
+  case $MAX_FD in #(
+  max*)
+    # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+    # shellcheck disable=SC3045
+    MAX_FD=$(ulimit -H -n) ||
+      warn "Could not query maximum file descriptor limit"
+    ;;
+  esac
+  case $MAX_FD in #(
+  '' | soft) : ;; #(
+  *)
+    # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+    # shellcheck disable=SC3045
+    ulimit -n "$MAX_FD" ||
+      warn "Could not set maximum file descriptor limit to $MAX_FD"
+    ;;
+  esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys"; then
+  APP_HOME=$(cygpath --path --mixed "$APP_HOME")
+  CLASSPATH=$(cygpath --path --mixed "$CLASSPATH")
+
+  JAVACMD=$(cygpath --unix "$JAVACMD")
+
+  # Now convert the arguments - kludge to limit ourselves to /bin/sh
+  for arg; do
+    if
+      case $arg in #(
+      -*) false ;; # don't mess with options #(
+      /?*)
+        t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+        [ -e "$t" ]
+        ;; #(
+      *) false ;;
+      esac
+    then
+      arg=$(cygpath --path --ignore --mixed "$arg")
+    fi
+    # Roll the args list around exactly as many times as the number of
+    # args, so each arg winds up back in the position where it started, but
+    # possibly modified.
+    #
+    # NB: a `for` loop captures its iteration list before it begins, so
+    # changing the positional parameters here affects neither the number of
+    # iterations, nor the values presented in `arg`.
+    shift              # remove old arg
+    set -- "$@" "$arg" # push replacement arg
+  done
+fi
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+  "-Dorg.gradle.appname=$APP_BASE_NAME" \
+  -classpath "$CLASSPATH" \
+  org.gradle.wrapper.GradleWrapperMain \
+  "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1; then
+  die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+  printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+    xargs -n1 |
+    sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+    tr '\n' ' '
+)" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..93e3f59f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..6be9ab47
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'springboot-url-shortener'
diff --git a/src/main/java/shortener/SpringbootUrlShortenerApplication.java b/src/main/java/shortener/SpringbootUrlShortenerApplication.java
new file mode 100644
index 00000000..8c21f3d2
--- /dev/null
+++ b/src/main/java/shortener/SpringbootUrlShortenerApplication.java
@@ -0,0 +1,13 @@
+package shortener;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SpringbootUrlShortenerApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(SpringbootUrlShortenerApplication.class, args);
+	}
+
+}
diff --git a/src/main/java/shortener/application/CacheMigrationScheduler.java b/src/main/java/shortener/application/CacheMigrationScheduler.java
new file mode 100644
index 00000000..9bc0ceca
--- /dev/null
+++ b/src/main/java/shortener/application/CacheMigrationScheduler.java
@@ -0,0 +1,44 @@
+package shortener.application;
+
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import lombok.extern.slf4j.Slf4j;
+import shortener.domain.ClicksCacheRepository;
+import shortener.domain.ShortUrl;
+import shortener.infrastructure.ShortUrlJpaRepository;
+
+@Slf4j
+@Transactional
+@Service
+public class CacheMigrationScheduler {
+
+	private final ShortUrlJpaRepository shortUrlRepository;
+	private final ClicksCacheRepository clicksCacheRepository;
+
+	public CacheMigrationScheduler(
+		ShortUrlJpaRepository shortUrlRepository,
+		ClicksCacheRepository clicksCacheRepository
+	) {
+		this.shortUrlRepository = shortUrlRepository;
+		this.clicksCacheRepository = clicksCacheRepository;
+	}
+
+	@Scheduled(cron = "0 0 3 * * *")
+	public void migrateClicksCacheDataToMasterDatabase() {
+		log.info("Run scheduler to migrate clicks from cache to master database...");
+		log.info("Find all shortUrl from master database...");
+		List savedShortUrls = shortUrlRepository.findAll();
+		log.info("Success to find all shortUrl.");
+		Map clicksForMigration = clicksCacheRepository.findAll(savedShortUrls);
+		clicksForMigration.forEach((id, clicks) -> {
+			log.info("Trying to update id({}) clicks({})...", id, clicks);
+			shortUrlRepository.updateClicks(id, clicks);
+			log.info("Success to update");
+		});
+	}
+}
diff --git a/src/main/java/shortener/application/ShortenerService.java b/src/main/java/shortener/application/ShortenerService.java
new file mode 100644
index 00000000..c0a86fcb
--- /dev/null
+++ b/src/main/java/shortener/application/ShortenerService.java
@@ -0,0 +1,133 @@
+package shortener.application;
+
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import lombok.extern.slf4j.Slf4j;
+import shortener.application.dto.response.ClicksResponse;
+import shortener.application.dto.response.ShortUrlCreateResponse;
+import shortener.domain.ClicksCacheRepository;
+import shortener.domain.OriginalUrlCacheRepository;
+import shortener.domain.ShortUrl;
+import shortener.global.error.ErrorCode;
+import shortener.global.error.exception.EntityNotFoundException;
+import shortener.infrastructure.ShortUrlJpaRepository;
+import shortener.domain.urlencoder.UrlEncoder;
+
+@Slf4j
+@Transactional
+@Service
+public class ShortenerService {
+
+	private final ShortUrlJpaRepository shortUrlRepository;
+	private final OriginalUrlCacheRepository originalUrlCacheRepository;
+	private final ClicksCacheRepository clicksCacheRepository;
+
+	public ShortenerService(
+		ShortUrlJpaRepository shortUrlRepository,
+		@Qualifier("originalUrlRedisCacheRepository")
+		OriginalUrlCacheRepository originalUrlCacheRepository,
+		@Qualifier("clicksRedisCacheRepository")
+		ClicksCacheRepository clicksCacheRepository
+	) {
+		this.shortUrlRepository = shortUrlRepository;
+		this.originalUrlCacheRepository = originalUrlCacheRepository;
+		this.clicksCacheRepository = clicksCacheRepository;
+	}
+
+	public ShortUrlCreateResponse saveNewShortUrl(String originalUrl, int algorithmId) {
+		ShortUrl newShortUrl = saveShortUrlInMaster(originalUrl, algorithmId);
+		saveOriginalUrlAndClicksInCache(newShortUrl);
+
+		return ShortUrlCreateResponse.of(newShortUrl);
+	}
+
+	public String findOriginalUrl(String encodedUrl) {
+		log.info("Trying to find originalUrl from cache...");
+		Optional cachedOriginalUrl = originalUrlCacheRepository.findOriginalUrlByEncodedUrl(encodedUrl);
+		if (cachedOriginalUrl.isPresent()) {
+			String originalUrl = cachedOriginalUrl.get();
+			log.info("Success to find originalUrl({}) in cache.", originalUrl);
+			updateClicksInCache(encodedUrl);
+
+			return originalUrl;
+		}
+		log.warn("Fail to find originalUrl from cache!!!");
+
+		log.info("Switch to master database system");
+		String originalUrl = findOriginalUrlByNoCache(encodedUrl);
+
+		log.info("Trying to save shortUrl(key({}), value({})) in cache...", encodedUrl, originalUrl);
+		ShortUrl shortUrl = shortUrlRepository.findShortUrlByEncodedUrl(encodedUrl)
+			.orElseThrow(() -> new EntityNotFoundException(ErrorCode.NOT_FOUND_MAPPED_URL));
+		saveOriginalUrlAndClicksInCache(shortUrl);
+		updateClicksInCache(encodedUrl);
+
+		return originalUrl;
+	}
+
+	public ClicksResponse findClicks(String encodedUrl) {
+		log.info("Trying to find clicks for encodedUrl({}) from cache...", encodedUrl);
+		Optional clicksFromCache = clicksCacheRepository.findClicksByEncodedUrl(encodedUrl);
+		if (clicksFromCache.isPresent()) {
+			Long clicks = clicksFromCache.get();
+			log.info("Success to find clicks(encodedUrl({}), clicks({})) from cache.", encodedUrl, clicks);
+
+			return ClicksResponse.of(encodedUrl, clicks);
+		}
+		log.warn("Fail to find clicks from cache!!!");
+
+		log.info("Switch to master database system");
+		log.info("Trying to find clicks for encodedUrl({}) from master database...", encodedUrl);
+		ShortUrl shortUrl = shortUrlRepository.findShortUrlByEncodedUrl(encodedUrl)
+			.orElseThrow(() -> new EntityNotFoundException(ErrorCode.NOT_FOUND_MAPPED_URL));
+		long clicks = shortUrl.getClicks();
+		log.info("Success to find clicks(encodedUrl({}), clicks({})) from master database.", encodedUrl, clicks);
+
+		saveOriginalUrlAndClicksInCache(shortUrl);
+
+		return ClicksResponse.of(encodedUrl, clicks);
+	}
+
+	private ShortUrl saveShortUrlInMaster(String originalUrl, int algorithmId) {
+		log.info("Start creating new shortUrl Entity...");
+		ShortUrl newShortUrl = new ShortUrl(originalUrl);
+		ShortUrl savedShortUrl = shortUrlRepository.save(newShortUrl);
+		log.info("Success to create new Entity(id({}))", savedShortUrl.getId());
+
+		log.info("Start encoding new shortUrl...");
+		String encodedUrl = UrlEncoder.getShortUrl(savedShortUrl, algorithmId);
+
+		log.info("Start saving new shortUrl({}) in master database...", encodedUrl);
+		savedShortUrl.updateEncodedUrl(encodedUrl);
+		log.info("Success to save new shortUrl");
+
+		return savedShortUrl;
+	}
+
+	private void saveOriginalUrlAndClicksInCache(ShortUrl shortUrl) {
+		log.info("Trying to save shortUrl in cache...");
+		originalUrlCacheRepository.save(shortUrl);
+		clicksCacheRepository.save(shortUrl);
+		log.info("Success to save shortUrl in cache.");
+	}
+
+	private String findOriginalUrlByNoCache(String encodedUrl) {
+		log.info("Trying to find originalUrl by encodedUrl({})...", encodedUrl);
+		ShortUrl foundShortUrl = shortUrlRepository.findShortUrlByEncodedUrl(encodedUrl)
+			.orElseThrow(() -> new EntityNotFoundException(ErrorCode.NOT_FOUND_MAPPED_URL));
+		String originalUrl = foundShortUrl.getOriginalUrl();
+		log.info("Success to find originalUrl({})", originalUrl);
+
+		return originalUrl;
+	}
+
+	private void updateClicksInCache(String encodedUrl) {
+		log.info("Update clicks in cache...");
+		clicksCacheRepository.updateClicks(encodedUrl);
+		log.info("Success to update clicks in cache.");
+	}
+}
diff --git a/src/main/java/shortener/application/dto/response/ClicksResponse.java b/src/main/java/shortener/application/dto/response/ClicksResponse.java
new file mode 100644
index 00000000..8e0ce980
--- /dev/null
+++ b/src/main/java/shortener/application/dto/response/ClicksResponse.java
@@ -0,0 +1,11 @@
+package shortener.application.dto.response;
+
+public record ClicksResponse(
+	String encodedUrl,
+	Long clicks
+) {
+
+	public static ClicksResponse of(String encodedUrl, Long clicks) {
+		return new ClicksResponse(encodedUrl, clicks);
+	}
+}
diff --git a/src/main/java/shortener/application/dto/response/ShortUrlCreateResponse.java b/src/main/java/shortener/application/dto/response/ShortUrlCreateResponse.java
new file mode 100644
index 00000000..039738a7
--- /dev/null
+++ b/src/main/java/shortener/application/dto/response/ShortUrlCreateResponse.java
@@ -0,0 +1,18 @@
+package shortener.application.dto.response;
+
+import shortener.domain.ShortUrl;
+
+public record ShortUrlCreateResponse(
+	Long id,
+	String shortUrl,
+	String originalUrl
+) {
+
+	public static ShortUrlCreateResponse of(ShortUrl newShortUrl) {
+		Long shortUrlId = newShortUrl.getId();
+		String shortUrl = newShortUrl.getEncodedUrl();
+		String originalUrl = newShortUrl.getOriginalUrl();
+
+		return new ShortUrlCreateResponse(shortUrlId, shortUrl, originalUrl);
+	}
+}
diff --git a/src/main/java/shortener/config/ClicksCacheRedisConfig.java b/src/main/java/shortener/config/ClicksCacheRedisConfig.java
new file mode 100644
index 00000000..c2da708d
--- /dev/null
+++ b/src/main/java/shortener/config/ClicksCacheRedisConfig.java
@@ -0,0 +1,40 @@
+package shortener.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+@EnableRedisRepositories(redisTemplateRef = "redisTemplateForClicks")
+public class ClicksCacheRedisConfig {
+
+	@Value("${spring.data.redis.host}")
+	private String redisHost;
+
+	@Value("${spring.data.redis.port}")
+	private int redisPort;
+
+	@Bean
+	public RedisConnectionFactory clicksCacheRedisConnectionFactory() {
+		LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisHost, redisPort);
+		lettuceConnectionFactory.setDatabase(1);
+
+		return lettuceConnectionFactory;
+	}
+
+	@Bean
+	public RedisTemplate, ?> redisTemplateForClicks() {
+		RedisTemplate redisTemplate = new RedisTemplate<>();
+		redisTemplate.setConnectionFactory(clicksCacheRedisConnectionFactory());
+		redisTemplate.setKeySerializer(new StringRedisSerializer());
+		redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class));
+
+		return redisTemplate;
+	}
+}
diff --git a/src/main/java/shortener/config/OriginalUrlCacheRedisConfig.java b/src/main/java/shortener/config/OriginalUrlCacheRedisConfig.java
new file mode 100644
index 00000000..092562b1
--- /dev/null
+++ b/src/main/java/shortener/config/OriginalUrlCacheRedisConfig.java
@@ -0,0 +1,39 @@
+package shortener.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+@EnableRedisRepositories(redisTemplateRef = "redisTemplateForOriginalUrl")
+public class OriginalUrlCacheRedisConfig {
+
+	@Value("${spring.data.redis.host}")
+	private String redisHost;
+
+	@Value("${spring.data.redis.port}")
+	private int redisPort;
+
+	@Bean
+	public RedisConnectionFactory originalUrlCacheRedisConnectionFactory() {
+		LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisHost, redisPort);
+		lettuceConnectionFactory.setDatabase(0);
+
+		return lettuceConnectionFactory;
+	}
+
+	@Bean
+	public RedisTemplate, ?> redisTemplateForOriginalUrl() {
+		RedisTemplate redisTemplate = new RedisTemplate<>();
+		redisTemplate.setConnectionFactory(originalUrlCacheRedisConnectionFactory());
+		redisTemplate.setKeySerializer(new StringRedisSerializer());
+		redisTemplate.setValueSerializer(new StringRedisSerializer());
+
+		return redisTemplate;
+	}
+}
diff --git a/src/main/java/shortener/config/SchedulerConfig.java b/src/main/java/shortener/config/SchedulerConfig.java
new file mode 100644
index 00000000..feae8001
--- /dev/null
+++ b/src/main/java/shortener/config/SchedulerConfig.java
@@ -0,0 +1,9 @@
+package shortener.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@Configuration
+@EnableScheduling
+public class SchedulerConfig {
+}
diff --git a/src/main/java/shortener/config/WebConfig.java b/src/main/java/shortener/config/WebConfig.java
new file mode 100644
index 00000000..3b0628b2
--- /dev/null
+++ b/src/main/java/shortener/config/WebConfig.java
@@ -0,0 +1,20 @@
+package shortener.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@EnableWebMvc
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+	@Override
+	public void addCorsMappings(CorsRegistry registry) {
+		registry.addMapping("/**")
+			.allowedOrigins("http://url-shortener-react:3000", "http://localhost:3000",
+				"http://ec2-3-35-240-254.ap-northeast-2.compute.amazonaws.com:3000")
+			.allowedMethods("GET", "POST", "PUT", "DELETE")
+			.allowedHeaders("*");
+	}
+}
diff --git a/src/main/java/shortener/domain/ClicksCacheRepository.java b/src/main/java/shortener/domain/ClicksCacheRepository.java
new file mode 100644
index 00000000..66008301
--- /dev/null
+++ b/src/main/java/shortener/domain/ClicksCacheRepository.java
@@ -0,0 +1,15 @@
+package shortener.domain;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public interface ClicksCacheRepository {
+	ShortUrl save(ShortUrl shortUrl);
+
+	void updateClicks(String encodedUrl);
+
+	Optional findClicksByEncodedUrl(String encodedUrl);
+
+	Map findAll(List shortUrls);
+}
diff --git a/src/main/java/shortener/domain/OriginalUrlCacheRepository.java b/src/main/java/shortener/domain/OriginalUrlCacheRepository.java
new file mode 100644
index 00000000..e9a16011
--- /dev/null
+++ b/src/main/java/shortener/domain/OriginalUrlCacheRepository.java
@@ -0,0 +1,9 @@
+package shortener.domain;
+
+import java.util.Optional;
+
+public interface OriginalUrlCacheRepository {
+	ShortUrl save(ShortUrl shortUrl);
+
+	Optional findOriginalUrlByEncodedUrl(String encodedUrl);
+}
diff --git a/src/main/java/shortener/domain/ShortUrl.java b/src/main/java/shortener/domain/ShortUrl.java
new file mode 100644
index 00000000..e3709bb7
--- /dev/null
+++ b/src/main/java/shortener/domain/ShortUrl.java
@@ -0,0 +1,59 @@
+package shortener.domain;
+
+import org.springframework.data.redis.core.index.Indexed;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.extern.slf4j.Slf4j;
+import shortener.global.error.ErrorCode;
+import shortener.global.error.exception.BusinessException;
+
+@Slf4j
+@Entity
+@Table(name = "short_urls")
+public class ShortUrl {
+
+	@Id
+	@GeneratedValue(strategy = GenerationType.IDENTITY)
+	Long id;
+	@Indexed
+	@Column(name = "encoded_url", unique = true)
+	String encodedUrl;
+	@Column(name = "original_url", nullable = false, length = 2000)
+	String originalUrl;
+	@Column(name = "clicks", nullable = false)
+	long clicks = 0L;
+
+	protected ShortUrl() {
+	}
+
+	public ShortUrl(String originalUrl) {
+		log.info("Create ShortUrl Entity...");
+		this.originalUrl = originalUrl;
+	}
+
+	public void updateEncodedUrl(String encodedUrl) {
+		log.info("Update shortUrl({}) to Entity(id({}))", encodedUrl, this.id);
+		this.encodedUrl = encodedUrl;
+	}
+
+	public Long getId() {
+		return id;
+	}
+
+	public String getEncodedUrl() {
+		return encodedUrl;
+	}
+
+	public String getOriginalUrl() {
+		return originalUrl;
+	}
+
+	public long getClicks() {
+		return clicks;
+	}
+}
diff --git a/src/main/java/shortener/domain/urlencoder/UrlEncoder.java b/src/main/java/shortener/domain/urlencoder/UrlEncoder.java
new file mode 100644
index 00000000..e2744378
--- /dev/null
+++ b/src/main/java/shortener/domain/urlencoder/UrlEncoder.java
@@ -0,0 +1,32 @@
+package shortener.domain.urlencoder;
+
+import shortener.domain.ShortUrl;
+import shortener.domain.urlencoder.algorithm.AdlerHash;
+import shortener.global.error.ErrorCode;
+import shortener.global.error.exception.BusinessException;
+import shortener.domain.urlencoder.algorithm.AlgorithmType;
+import shortener.domain.urlencoder.algorithm.Base62Hash;
+import shortener.domain.urlencoder.algorithm.ShortUuidHash;
+
+public class UrlEncoder {
+
+	private static final Base62Hash base62Hash = new Base62Hash();
+	private static final ShortUuidHash shortUuidHash = new ShortUuidHash();
+	private static final AdlerHash adlerHash = new AdlerHash();
+
+	public static String getShortUrl(ShortUrl shortUrl, int algorithmId) {
+		if (AlgorithmType.BASE62.getId() == algorithmId) {
+			Long id = shortUrl.getId();
+
+			return base62Hash.encode(id);
+		} else if (AlgorithmType.SHORT_UUID.getId() == algorithmId) {
+			return shortUuidHash.encode();
+		} else if (AlgorithmType.ADLER.getId() == algorithmId) {
+			String originalUrl = shortUrl.getOriginalUrl();
+
+			return adlerHash.encode(originalUrl);
+		} else {
+			throw new BusinessException(ErrorCode.INVALID_ENCODING_ALGORITHM);
+		}
+	}
+}
diff --git a/src/main/java/shortener/domain/urlencoder/algorithm/AdlerHash.java b/src/main/java/shortener/domain/urlencoder/algorithm/AdlerHash.java
new file mode 100644
index 00000000..fe7e76c9
--- /dev/null
+++ b/src/main/java/shortener/domain/urlencoder/algorithm/AdlerHash.java
@@ -0,0 +1,18 @@
+package shortener.domain.urlencoder.algorithm;
+
+public class AdlerHash {
+
+	private static final int MOD_ADLER = 65521;
+	private static int dataSum = 1;
+	private static int rollingSum = 0;
+
+	public String encode(String originalUrl) {
+		for (char urlElement : originalUrl.toCharArray()) {
+			dataSum = (dataSum + urlElement) % MOD_ADLER;
+			rollingSum = (rollingSum + dataSum) % MOD_ADLER;
+		}
+		int key = (rollingSum << 16) | dataSum;
+
+		return Integer.toHexString(key);
+	}
+}
diff --git a/src/main/java/shortener/domain/urlencoder/algorithm/AlgorithmType.java b/src/main/java/shortener/domain/urlencoder/algorithm/AlgorithmType.java
new file mode 100644
index 00000000..0b17a28d
--- /dev/null
+++ b/src/main/java/shortener/domain/urlencoder/algorithm/AlgorithmType.java
@@ -0,0 +1,23 @@
+package shortener.domain.urlencoder.algorithm;
+
+public enum AlgorithmType {
+	BASE62(1, "Base62"),
+	SHORT_UUID(2, "ShortUuid"),
+	ADLER(3, "Adler");
+
+	private final int id;
+	private final String name;
+
+	AlgorithmType(int id, String name) {
+		this.id = id;
+		this.name = name;
+	}
+
+	public int getId() {
+		return id;
+	}
+
+	public String getName() {
+		return name;
+	}
+}
diff --git a/src/main/java/shortener/domain/urlencoder/algorithm/Base62Hash.java b/src/main/java/shortener/domain/urlencoder/algorithm/Base62Hash.java
new file mode 100644
index 00000000..8b627362
--- /dev/null
+++ b/src/main/java/shortener/domain/urlencoder/algorithm/Base62Hash.java
@@ -0,0 +1,30 @@
+package shortener.domain.urlencoder.algorithm;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class Base62Hash {
+
+	private static final char[] base62CryptoWords = {
+		'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+		'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+		'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+		'U', 'V', 'W', 'X', 'Y', 'Z',
+		'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+		'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+		'u', 'v', 'w', 'x', 'y', 'z'
+	};
+
+	public String encode(long index) {
+		StringBuilder encodedUrlBuilder = new StringBuilder();
+		log.info("Start encoding shortUrl by id({})", index);
+		do {
+			int cryptoIndex = (int)(index % 62);
+			encodedUrlBuilder.append(base62CryptoWords[cryptoIndex]);
+			index /= 62;
+		} while (index % 62 > 0);
+		log.info("Success to encode to shortUrl({})", encodedUrlBuilder);
+
+		return encodedUrlBuilder.toString();
+	}
+}
diff --git a/src/main/java/shortener/domain/urlencoder/algorithm/ShortUuidHash.java b/src/main/java/shortener/domain/urlencoder/algorithm/ShortUuidHash.java
new file mode 100644
index 00000000..6b18a940
--- /dev/null
+++ b/src/main/java/shortener/domain/urlencoder/algorithm/ShortUuidHash.java
@@ -0,0 +1,12 @@
+package shortener.domain.urlencoder.algorithm;
+
+import java.util.UUID;
+
+public class ShortUuidHash {
+
+	public String encode() {
+		return UUID.randomUUID()
+			.toString()
+			.substring(0, 7);
+	}
+}
diff --git a/src/main/java/shortener/global/error/ErrorCode.java b/src/main/java/shortener/global/error/ErrorCode.java
new file mode 100644
index 00000000..107e3306
--- /dev/null
+++ b/src/main/java/shortener/global/error/ErrorCode.java
@@ -0,0 +1,24 @@
+package shortener.global.error;
+
+public enum ErrorCode {
+	INVALID_REQUEST_NUMBERS("U001", "API의 총 요청 횟수가 잘 못 되었습니다."),
+	NOT_FOUND_MAPPED_URL("U002", "요청받은 URL을 찾을 수 없습니다."),
+	INVALID_ENCODING_ALGORITHM("U003", "올바르지 않은 인코딩 요청입니다."),
+	INVALID_INPUT_VALUE("U004", "올바르지 않은 입력입니다.");
+
+	private final String code;
+	private final String message;
+
+	ErrorCode(String code, String message) {
+		this.code = code;
+		this.message = message;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+}
diff --git a/src/main/java/shortener/global/error/ErrorResponse.java b/src/main/java/shortener/global/error/ErrorResponse.java
new file mode 100644
index 00000000..6a35ab92
--- /dev/null
+++ b/src/main/java/shortener/global/error/ErrorResponse.java
@@ -0,0 +1,80 @@
+package shortener.global.error;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.validation.BindingResult;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+public record ErrorResponse(
+	@JsonSerialize(using = LocalDateTimeSerializer.class)
+	@JsonDeserialize(using = LocalDateTimeDeserializer.class)
+	@JsonFormat(pattern = "yyyy-MM-dd kk:mm:ss")
+	LocalDateTime timestamp,
+	String code,
+	List errors,
+	String message
+) {
+
+	private ErrorResponse(ErrorCode code, List errors) {
+		this(LocalDateTime.now(), code.getCode(), errors, code.getMessage());
+	}
+
+	private ErrorResponse(ErrorCode code) {
+		this(LocalDateTime.now(), code.getCode(), new ArrayList<>(), code.getMessage());
+	}
+
+	public static ErrorResponse of(ErrorCode errorCode, BindingResult bindingResult) {
+		return new ErrorResponse(errorCode, FieldError.of(bindingResult));
+	}
+
+	public static ErrorResponse of(ErrorCode errorCode) {
+		return new ErrorResponse(errorCode);
+	}
+
+	public static ErrorResponse of(final ErrorCode code, final List errors) {
+		return new ErrorResponse(code, errors);
+	}
+
+	@Getter
+	@NoArgsConstructor(access = AccessLevel.PROTECTED)
+	public static class FieldError {
+
+		private String field;
+		private String value;
+		private String reason;
+
+		private FieldError(final String field, final String value, final String reason) {
+			this.field = field;
+			this.value = value;
+			this.reason = reason;
+		}
+
+		public static List of(final String field, final String value, final String reason) {
+			List fieldErrors = new ArrayList<>();
+			fieldErrors.add(new FieldError(field, value, reason));
+			return fieldErrors;
+		}
+
+		private static List of(final BindingResult bindingResult) {
+			final List fieldErrors = bindingResult.getFieldErrors();
+			return fieldErrors.stream()
+				.map(error -> new FieldError(
+					error.getField(),
+					error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
+					error.getDefaultMessage()))
+				.collect(Collectors.toList());
+		}
+	}
+}
diff --git a/src/main/java/shortener/global/error/GlobalExceptionHandler.java b/src/main/java/shortener/global/error/GlobalExceptionHandler.java
new file mode 100644
index 00000000..7a165198
--- /dev/null
+++ b/src/main/java/shortener/global/error/GlobalExceptionHandler.java
@@ -0,0 +1,35 @@
+package shortener.global.error;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+import shortener.global.error.exception.BusinessException;
+import shortener.global.error.exception.EntityNotFoundException;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+
+	@ExceptionHandler(EntityNotFoundException.class)
+	public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e) {
+		ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode());
+
+		return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
+	}
+
+	@ExceptionHandler(BusinessException.class)
+	public ResponseEntity handleBusinessException(BusinessException e) {
+		ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode());
+
+		return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+	}
+
+	@ExceptionHandler(MethodArgumentNotValidException.class)
+	public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+		ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
+
+		return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+	}
+}
diff --git a/src/main/java/shortener/global/error/exception/BusinessException.java b/src/main/java/shortener/global/error/exception/BusinessException.java
new file mode 100644
index 00000000..cfce3f53
--- /dev/null
+++ b/src/main/java/shortener/global/error/exception/BusinessException.java
@@ -0,0 +1,15 @@
+package shortener.global.error.exception;
+
+import shortener.global.error.ErrorCode;
+
+public class BusinessException extends RuntimeException {
+	ErrorCode errorCode;
+
+	public BusinessException(ErrorCode errorCode) {
+		this.errorCode = errorCode;
+	}
+
+	public ErrorCode getErrorCode() {
+		return errorCode;
+	}
+}
diff --git a/src/main/java/shortener/global/error/exception/CacheNotFoundException.java b/src/main/java/shortener/global/error/exception/CacheNotFoundException.java
new file mode 100644
index 00000000..ef54ad09
--- /dev/null
+++ b/src/main/java/shortener/global/error/exception/CacheNotFoundException.java
@@ -0,0 +1,16 @@
+package shortener.global.error.exception;
+
+import shortener.global.error.ErrorCode;
+
+public class CacheNotFoundException extends RuntimeException {
+
+	ErrorCode errorCode;
+
+	public CacheNotFoundException(ErrorCode errorCode) {
+		this.errorCode = errorCode;
+	}
+
+	public ErrorCode getErrorCode() {
+		return errorCode;
+	}
+}
diff --git a/src/main/java/shortener/global/error/exception/EntityNotFoundException.java b/src/main/java/shortener/global/error/exception/EntityNotFoundException.java
new file mode 100644
index 00000000..311bac45
--- /dev/null
+++ b/src/main/java/shortener/global/error/exception/EntityNotFoundException.java
@@ -0,0 +1,15 @@
+package shortener.global.error.exception;
+
+import shortener.global.error.ErrorCode;
+
+public class EntityNotFoundException extends RuntimeException {
+	ErrorCode errorCode;
+
+	public EntityNotFoundException(ErrorCode errorCode) {
+		this.errorCode = errorCode;
+	}
+
+	public ErrorCode getErrorCode() {
+		return errorCode;
+	}
+}
diff --git a/src/main/java/shortener/infrastructure/ClicksRedisCacheRepository.java b/src/main/java/shortener/infrastructure/ClicksRedisCacheRepository.java
new file mode 100644
index 00000000..76838fc3
--- /dev/null
+++ b/src/main/java/shortener/infrastructure/ClicksRedisCacheRepository.java
@@ -0,0 +1,81 @@
+package shortener.infrastructure;
+
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Repository;
+
+import lombok.extern.slf4j.Slf4j;
+import shortener.domain.ClicksCacheRepository;
+import shortener.domain.ShortUrl;
+import shortener.global.error.ErrorCode;
+import shortener.global.error.exception.CacheNotFoundException;
+
+@Slf4j
+@Repository
+public class ClicksRedisCacheRepository implements ClicksCacheRepository {
+
+	private final ValueOperations valueOperations;
+
+	public ClicksRedisCacheRepository(RedisTemplate redisTemplateForClicks) {
+		this.valueOperations = redisTemplateForClicks.opsForValue();
+	}
+
+	@Override
+	public ShortUrl save(ShortUrl shortUrl) {
+		String encodedUrl = shortUrl.getEncodedUrl();
+		long clicks = shortUrl.getClicks();
+
+		log.info("Trying to save clicks(key({}), value({}) into cache repository...", encodedUrl, clicks);
+		valueOperations.set(encodedUrl, clicks);
+		log.info("Success to save into cache.");
+
+		return shortUrl;
+	}
+
+	@Override
+	public void updateClicks(String encodedUrl) {
+		log.info("Trying to find clicks in cache...");
+		Long clicks = Optional.ofNullable(valueOperations.get(encodedUrl))
+			.orElseThrow(() -> new CacheNotFoundException(ErrorCode.NOT_FOUND_MAPPED_URL));
+		log.info("Success to find clicks(value({})) from cache.", clicks);
+
+		log.info("Trying to update clicks({}) in cache...", clicks);
+		valueOperations.set(encodedUrl, clicks + 1);
+		log.info("Success to update clicks(value({})) in cache.", clicks + 1);
+	}
+
+	@Override
+	public Optional findClicksByEncodedUrl(String encodedUrl) {
+		log.info("Get clicks from cache...");
+		Long clicks = valueOperations.get(encodedUrl);
+
+		return Optional.ofNullable(clicks);
+	}
+
+	@Override
+	public Map findAll(List shortUrls) {
+		log.info("Trying to map shortUrl id to clicks from cache...");
+		return shortUrls.stream()
+			.collect(Collectors.toConcurrentMap(shortUrl -> {
+				Long id = shortUrl.getId();
+				log.info("key(id): {}", id);
+
+				return id;
+			}, shortUrl -> {
+				String encodedUrl = shortUrl.getEncodedUrl();
+				Long clicks = Optional.ofNullable(valueOperations.get(encodedUrl))
+					.orElseThrow(() -> new RuntimeException(
+						MessageFormat.format("Can not find clicks for key({0})", encodedUrl)
+					));
+				log.info("value(clicks): {}", clicks);
+
+				return clicks;
+			}));
+	}
+}
diff --git a/src/main/java/shortener/infrastructure/OriginalUrlRedisCacheRepository.java b/src/main/java/shortener/infrastructure/OriginalUrlRedisCacheRepository.java
new file mode 100644
index 00000000..b965daab
--- /dev/null
+++ b/src/main/java/shortener/infrastructure/OriginalUrlRedisCacheRepository.java
@@ -0,0 +1,42 @@
+package shortener.infrastructure;
+
+import java.util.Optional;
+
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Repository;
+
+import lombok.extern.slf4j.Slf4j;
+import shortener.domain.OriginalUrlCacheRepository;
+import shortener.domain.ShortUrl;
+
+@Slf4j
+@Repository
+public class OriginalUrlRedisCacheRepository implements OriginalUrlCacheRepository {
+
+	private final ValueOperations valueOperations;
+
+	public OriginalUrlRedisCacheRepository(RedisTemplate redisTemplateForOriginalUrl) {
+		this.valueOperations = redisTemplateForOriginalUrl.opsForValue();
+	}
+
+	@Override
+	public ShortUrl save(ShortUrl shortUrl) {
+		String encodedUrl = shortUrl.getEncodedUrl();
+		String originalUrl = shortUrl.getOriginalUrl();
+
+		log.info("Trying to save originalUrl(key({}), value({}) into cache...", encodedUrl, originalUrl);
+		valueOperations.set(encodedUrl, originalUrl);
+		log.info("Success to save into cache.");
+
+		return shortUrl;
+	}
+
+	@Override
+	public Optional findOriginalUrlByEncodedUrl(String encodedUrl) {
+		log.info("Get original url from cache...");
+		String originalUrl = valueOperations.get(encodedUrl);
+
+		return Optional.ofNullable(originalUrl);
+	}
+}
diff --git a/src/main/java/shortener/infrastructure/ShortUrlJpaRepository.java b/src/main/java/shortener/infrastructure/ShortUrlJpaRepository.java
new file mode 100644
index 00000000..85063436
--- /dev/null
+++ b/src/main/java/shortener/infrastructure/ShortUrlJpaRepository.java
@@ -0,0 +1,19 @@
+package shortener.infrastructure;
+
+import java.util.Optional;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import shortener.domain.ShortUrl;
+
+public interface ShortUrlJpaRepository extends JpaRepository {
+
+	Optional findShortUrlByEncodedUrl(String encodedUrl);
+
+	@Modifying
+	@Query("UPDATE ShortUrl su SET su.clicks = :clicks WHERE su.id = :id")
+	void updateClicks(@Param("id") Long id, @Param("clicks") Long clicks);
+}
diff --git a/src/main/java/shortener/presentation/ShortenerController.java b/src/main/java/shortener/presentation/ShortenerController.java
new file mode 100644
index 00000000..46cae68d
--- /dev/null
+++ b/src/main/java/shortener/presentation/ShortenerController.java
@@ -0,0 +1,91 @@
+package shortener.presentation;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import lombok.extern.slf4j.Slf4j;
+import shortener.application.ShortenerService;
+import shortener.application.dto.response.ClicksResponse;
+import shortener.application.dto.response.ShortUrlCreateResponse;
+
+@Slf4j
+@RestController
+public class ShortenerController {
+
+	private final ShortenerService shortenerService;
+
+	public ShortenerController(ShortenerService shortenerService) {
+		this.shortenerService = shortenerService;
+	}
+
+	@PostMapping("/v1/util/short-url/{algorithmId}")
+	public ResponseEntity createShortUrl(
+		String originalUrl,
+		@PathVariable int algorithmId
+	) {
+		log.info("Receive request to create originalUrl({}) to shortURL...", originalUrl);
+		ShortUrlCreateResponse response = shortenerService.saveNewShortUrl(originalUrl, algorithmId);
+		log.info("Success to create shortURL : {}", response.shortUrl());
+
+		return ResponseEntity
+			.status(HttpStatus.CREATED)
+			.body(response);
+	}
+
+	@GetMapping("/{encodedUrl}")
+	public ResponseEntity getOriginalUrl(@PathVariable String encodedUrl) {
+		String originalUrl = shortenerService.findOriginalUrl(encodedUrl);
+		HttpHeaders headers = createRedirectionHeader(originalUrl);
+
+		return ResponseEntity
+			.status(HttpStatus.MOVED_PERMANENTLY)
+			.headers(headers)
+			.build();
+	}
+
+	@GetMapping("/{encodedUrl}/clicks")
+	public ResponseEntity getClicks(@PathVariable String encodedUrl) {
+		log.info("Receive request to get clicks for encodedUrl({})...", encodedUrl);
+		ClicksResponse response = shortenerService.findClicks(encodedUrl);
+		log.info("Success to get clicks({}) for encodedUrl({})",
+			response.clicks(), response.encodedUrl());
+
+		return ResponseEntity.ok(response);
+	}
+
+	private HttpHeaders createRedirectionHeader(String originalUrl) {
+		String httpAppendedOriginalUrl = appendHttpToUrlIfAbsent(originalUrl);
+
+		log.info("Create redirection Http headers...");
+		HttpHeaders headers = new HttpHeaders();
+		headers.add(HttpHeaders.HOST, "http://yosongsong.shortener.co.kr");
+		headers.add(HttpHeaders.SERVER, "Tomcat");
+		headers.add(HttpHeaders.LOCATION, httpAppendedOriginalUrl);
+		headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, max-age=0");
+		log.info("Success to create headers");
+
+		return headers;
+	}
+
+	private String appendHttpToUrlIfAbsent(String originalUrl) {
+		log.info("Trying to append \"http://\" to url if absent...");
+		boolean hasHttp = originalUrl.startsWith("http://");
+		boolean hasHttps = originalUrl.startsWith("https://");
+
+		if (hasHttp || hasHttps) {
+			log.info("Already exist \"http://\" or \"https://\" => {}", originalUrl);
+
+			return originalUrl;
+		}
+
+		String httpAppended = "http://" + originalUrl;
+		log.info("append \"http://\" => {}", httpAppended);
+
+		return httpAppended;
+	}
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 00000000..3296a2ec
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,31 @@
+server:
+  port: 8081
+
+spring:
+
+  datasource:
+    url: jdbc:mysql://url-shortener-mysql:3306/url_shortener
+    username: root
+    password: 12345
+    driver-class-name: com.mysql.cj.jdbc.Driver
+
+  h2:
+    console:
+      enabled: true
+
+  jpa:
+    open-in-view: true
+    hibernate:
+      ddl-auto: create
+      naming:
+        implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
+    show-sql: false
+    properties:
+      hibernate:
+        format_sql: true
+        dialect: org.hibernate.dialect.MySQL8Dialect
+
+  data:
+    redis:
+      host: url-shortener-redis
+      port: 6379