diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..3742c4db297857fdcb046f38032f0616bdbdddfd
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,23 @@
+FROM gradle:jdk21 AS build
+
+WORKDIR /tmp
+RUN wget -O lego.tar.gz "https://github.com/go-acme/lego/releases/download/v4.22.2/lego_v4.22.2_linux_amd64.tar.gz" && tar -xzf lego.tar.gz && rm -f lego.tar.gz
+
+WORKDIR /home/gradle/project
+
+COPY --chown=gradle:gradle build.gradle settings.gradle .
+RUN gradle dependencies --no-daemon
+
+COPY --chown=gradle:gradle src ./src
+RUN gradle clean bootJar --no-daemon
+
+FROM eclipse-temurin:21-jre-alpine
+
+COPY --from=build /tmp/lego /usr/local/bin/lego
+RUN chmod +x /usr/local/bin/lego
+
+WORKDIR /app
+COPY --from=build /home/gradle/project/build/libs/*.jar app.jar
+
+EXPOSE 8080
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/build.gradle b/build.gradle
index 9305e703fb238deb9b38371a98864482965a4253..1b29e4ade60f1ece03c8e7680004ea772cfc1ef3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -26,13 +26,23 @@ repositories {
 dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
     implementation 'org.springframework.boot:spring-boot-starter-web'
+    implementation 'org.springframework.boot:spring-boot-starter-validation'
     compileOnly 'org.projectlombok:lombok'
     runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
+    runtimeOnly 'com.mysql:mysql-connector-j'
     annotationProcessor 'org.projectlombok:lombok'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
+    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
+    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
+    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
+
 }
 
 tasks.named('test') {
     useJUnitPlatform()
 }
+clean {
+    delete file('src/main/generated')
+}
\ No newline at end of file
diff --git a/src/main/java/com/aolda/itda/ItdaApplication.java b/src/main/java/com/aolda/itda/ItdaApplication.java
index c7f4ef7eb3aae27b2c85e6297c9e74317f4ccdf6..674e65780f404587b1f8a528c8cb03087f919e60 100644
--- a/src/main/java/com/aolda/itda/ItdaApplication.java
+++ b/src/main/java/com/aolda/itda/ItdaApplication.java
@@ -2,8 +2,10 @@ package com.aolda.itda;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 
 @SpringBootApplication
+@EnableJpaAuditing
 public class ItdaApplication {
 
     public static void main(String[] args) {
diff --git a/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java
new file mode 100644
index 0000000000000000000000000000000000000000..30df250f9ffd08dbf07b458750c7c464409faf82
--- /dev/null
+++ b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java
@@ -0,0 +1,152 @@
+package com.aolda.itda.aspect;
+
+import com.aolda.itda.dto.certificate.CertificateDTO;
+import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.entity.log.ObjectType;
+import com.aolda.itda.entity.user.User;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.repository.log.LogRepository;
+import com.aolda.itda.repository.user.UserRepository;
+import jakarta.persistence.EntityManager;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import java.util.Objects;
+
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class CertificateLogAspect {
+
+    private final CertificateRepository certificateRepository;
+    private final UserRepository userRepository;
+    private final LogRepository logRepository;
+    private final EntityManager entityManager;
+
+    private static final DateTimeFormatter LOG_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    /* Create 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.certificate.*Service.*create*(..))"
+            , returning = "result")
+    public void createLogging(JoinPoint joinPoint, CertificateDTO result) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 생성된 엔티티 조회 */
+        Certificate certificate = certificateRepository.findByCertificateIdAndIsDeleted(result.getId(), false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "domain: " + certificate.getDomain() + "\n"
+                + "email: " + certificate.getEmail() + "\n"
+                + "challenge: " + certificate.getChallenge() + "\n"
+                + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null");
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.CERTIFICATE)
+                .objectId(certificate.getCertificateId())
+                .action(Action.CREATE)
+                .projectId(certificate.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Delete 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.certificate.*Service.*delete*(..))")
+    public void deleteLogging(JoinPoint joinPoint) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 삭제된 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Certificate certificate = certificateRepository.findByCertificateIdAndIsDeleted(id, true).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "domain: " + certificate.getDomain() + "\n"
+                + "email: " + certificate.getEmail() + "\n"
+                + "challenge: " + certificate.getChallenge() + "\n"
+                + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null");
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.CERTIFICATE)
+                .objectId(certificate.getCertificateId())
+                .action(Action.DELETE)
+                .projectId(certificate.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Update(edit) 로깅 */
+    @Around("execution(* com.aolda.itda.service.certificate.*Service.*edit*(..))")
+    public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 변경 전 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Certificate old = certificateRepository.findByCertificateIdAndIsDeleted(id, false).orElse(null);
+        if (old != null) {
+            entityManager.detach(old);
+        }
+
+        /* 메소드 진행 */
+        Object result = joinPoint.proceed();
+
+        /* 변경 후 엔티티 조회 */
+        Certificate newObj = certificateRepository.findByCertificateIdAndIsDeleted(id, false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : " -> " + newObj.getDomain()) + "\n"
+                + "email: " + old.getEmail() + (old.getEmail().equals(newObj.getEmail()) ? "" : " -> " + newObj.getEmail()) + "\n"
+                + "challenge: " + old.getChallenge() + (old.getChallenge() == newObj.getChallenge() ? "" : " -> " + newObj.getChallenge()) + "\n"
+                + "expiresAt: " + (old.getExpiresAt() != null ? old.getExpiresAt().format(LOG_DATE_FORMATTER) : "null")
+                + (old.getExpiresAt() != null && newObj.getExpiresAt() != null && !old.getExpiresAt().equals(newObj.getExpiresAt()) ? " -> " + newObj.getExpiresAt().format(LOG_DATE_FORMATTER) : "");
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.CERTIFICATE)
+                .objectId(newObj.getCertificateId())
+                .action(Action.UPDATE)
+                .projectId(newObj.getProjectId())
+                .description(description)
+                .build());
+        return result;
+    }
+} 
\ No newline at end of file
diff --git a/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java
new file mode 100644
index 0000000000000000000000000000000000000000..d50c2feaaedc63a3b5dbac4fa28666ee146ca2a0
--- /dev/null
+++ b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java
@@ -0,0 +1,155 @@
+package com.aolda.itda.aspect;
+
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.entity.forwarding.Forwarding;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.entity.log.ObjectType;
+import com.aolda.itda.entity.user.User;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.repository.log.LogRepository;
+import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.repository.user.UserRepository;
+import com.aolda.itda.service.AuthService;
+import jakarta.persistence.EntityManager;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Map;
+import java.util.Objects;
+
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class ForwardingLogAspect {
+
+    private final ForwardingRepository forwardingRepository;
+    private final LogRepository logRepository;
+    private final EntityManager entityManager;
+    private final UserRepository userRepository;
+
+    /* Create 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.forwarding.*Service.*create*(..))"
+    , returning = "result")
+    public void createLogging(JoinPoint joinPoint, ForwardingDTO result) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 생성된 엔티티 조회 */
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(result.getId(), false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + forwarding.getName() + "\n"
+                + "serverPort: " + forwarding.getServerPort() + "\n"
+                + "instanceIp: " + forwarding.getInstanceIp() + "\n"
+                + "instancePort: " + forwarding.getInstancePort();
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.FORWARDING)
+                .objectId(forwarding.getForwardingId())
+                .action(Action.CREATE)
+                .projectId(forwarding.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Delete 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.forwarding.*Service.*delete*(..))")
+    public void deleteLogging(JoinPoint joinPoint) {
+        
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 삭제된 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(id, true).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + forwarding.getName() + "\n"
+                + "serverPort: " + forwarding.getServerPort() + "\n"
+                + "instanceIp: " + forwarding.getInstanceIp() + "\n"
+                + "instancePort: " + forwarding.getInstancePort();
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.FORWARDING)
+                .objectId(forwarding.getForwardingId())
+                .action(Action.DELETE)
+                .projectId(forwarding.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Update(edit) 로깅 */
+    @Around("execution(* com.aolda.itda.service.forwarding.*Service.*edit*(..))")
+    public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable {
+        
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 변경 전 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Forwarding old = forwardingRepository.findByForwardingIdAndIsDeleted(id, false).orElse(null);
+        if (old != null) {
+            entityManager.detach(old);
+        }
+        
+        /* 메소드 진행 */
+        Object result = joinPoint.proceed();
+
+        /* 변경 후 엔티티 조회*/
+        Forwarding newObj = forwardingRepository.findByForwardingIdAndIsDeleted(id, false).orElse(null);
+        
+        /* 로그 메세지 작성 */
+        String description = "name: " + old.getName() + (old.getName().equals(newObj.getName()) ? "" : (" -> " + newObj.getName())) + "\n"
+                + "serverPort: " + old.getServerPort() + (old.getServerPort().equals(newObj.getServerPort()) ? "" : (" -> " + newObj.getServerPort())) + "\n"
+                + "instanceIp: " + old.getInstanceIp() + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : (" -> " + newObj.getInstanceIp())) + "\n"
+                + "instancePort: " + old.getInstancePort() + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : (" -> " + newObj.getInstancePort()));
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.FORWARDING)
+                .objectId(newObj.getForwardingId())
+                .action(Action.UPDATE)
+                .projectId(newObj.getProjectId())
+                .description(description)
+                .build());
+        return result;
+    }
+
+
+
+}
diff --git a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
new file mode 100644
index 0000000000000000000000000000000000000000..8b9d5b9fab5e51248cf51ca4fb4c8a93d8b09822
--- /dev/null
+++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java
@@ -0,0 +1,175 @@
+package com.aolda.itda.aspect;
+
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.dto.routing.RoutingDTO;
+import com.aolda.itda.entity.forwarding.Forwarding;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.entity.log.ObjectType;
+import com.aolda.itda.entity.routing.Routing;
+import com.aolda.itda.entity.user.User;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.repository.log.LogRepository;
+import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.repository.user.UserRepository;
+import jakarta.persistence.EntityManager;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Map;
+import java.util.Objects;
+
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class RoutingLogAspect {
+
+    private final RoutingRepository routingRepository;
+    private final UserRepository userRepository;
+    private final LogRepository logRepository;
+    private final EntityManager entityManager;
+
+    /* Create 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.routing.*Service.*create*(..))"
+            , returning = "result")
+    public void createLogging(JoinPoint joinPoint, RoutingDTO result) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 생성된 엔티티 조회 */
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(result.getId(), false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + routing.getName() + "\n"
+                + "domain: " + routing.getDomain() + "\n"
+                + "ip: " + routing.getInstanceIp() + "\n"
+                + "port: " + routing.getInstancePort() + "\n"
+                +  (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "")
+                + "caching: " + routing.getCaching();
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.ROUTING)
+                .objectId(routing.getRoutingId())
+                .action(Action.CREATE)
+                .projectId(routing.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Delete 로깅 */
+    @AfterReturning(pointcut = "execution(* com.aolda.itda.service.routing.*Service.*delete*(..))")
+    public void deleteLogging(JoinPoint joinPoint) {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 삭제된 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(id, true).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + routing.getName() + "\n"
+                + "domain: " + routing.getDomain() + "\n"
+                + "ip: " + routing.getInstanceIp() + "\n"
+                + "port: " + routing.getInstancePort() + "\n"
+                +  (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "")
+                + "caching: " + routing.getCaching();
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.ROUTING)
+                .objectId(routing.getRoutingId())
+                .action(Action.DELETE)
+                .projectId(routing.getProjectId())
+                .description(description)
+                .build());
+    }
+
+    /* Update(edit) 로깅 */
+    @Around("execution(* com.aolda.itda.service.routing.*Service.*edit*(..))")
+    public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable {
+
+        /* 사용자 조회 */
+        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
+        Map<String, String> tmp = (Map<String, String>) request.getAttribute("user");
+        User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow(
+                () -> new CustomException(ErrorCode.NOT_FOUND_USER)
+        );
+
+        /* 변경 전 엔티티 조회 */
+        Object[] args = joinPoint.getArgs();
+
+        Long id = (Long) args[0];
+        Routing old = routingRepository.findByRoutingIdAndIsDeleted(id, false).orElse(null);
+        if (old != null) {
+            entityManager.detach(old);
+        }
+
+        /* 메소드 진행 */
+        Object result = joinPoint.proceed();
+
+        /* 변경 후 엔티티 조회 */
+        Routing newObj = routingRepository.findByRoutingIdAndIsDeleted(id, false).orElse(null);
+
+        /* 로그 메세지 작성 */
+        String description = "name: " + old.getName() + (old.getName().equals(newObj.getName()) ? "" : " -> " + newObj.getName()) + "\n"
+                + "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : " -> " + newObj.getDomain()) + "\n"
+                + "ip: " + old.getInstanceIp() + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : " -> " + newObj.getInstanceIp()) + "\n"
+                + "port: " + old.getInstancePort() + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : " -> " + newObj.getInstancePort()) + "\n";
+
+        if (isSameCertificate(old, newObj)) {
+            description = description + "certificateId: " + (old.getCertificate() == null ? "null" : old.getCertificate().getCertificateId()) + "\n";
+        } else {
+            description = description + "certificateId: " + (old.getCertificate() == null ? "null" : old.getCertificate().getCertificateId()) +
+                    (newObj.getCertificate() == null ? "null" : " -> " + newObj.getCertificate().getCertificateId()) + "\n";
+        }
+
+        description = description + "caching: " + (old.getCaching() == newObj.getCaching() ? newObj.getCaching() : (" -> " + newObj.getCaching()));
+
+        /* 로그 엔티티 저장 */
+        logRepository.save(Log.builder()
+                .user(user)
+                .objectType(ObjectType.ROUTING)
+                .objectId(newObj.getRoutingId())
+                .action(Action.UPDATE)
+                .projectId(newObj.getProjectId())
+                .description(description)
+                .build());
+        return result;
+    }
+
+    private boolean isSameCertificate(Routing old, Routing newObj) {
+        if (old.getCertificate() == null && newObj.getCertificate() == null) {
+            return true;
+        } else if (old.getCertificate() == null || newObj.getCertificate() == null) {
+            return false;
+        } else {
+            return old.getCertificate().getCertificateId().equals(newObj.getCertificate().getCertificateId());
+        }
+    }
+}
diff --git a/src/main/java/com/aolda/itda/config/AuthFilter.java b/src/main/java/com/aolda/itda/config/AuthFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..f810c2c7452b862c85b64cd1a51dd6ff130189b8
--- /dev/null
+++ b/src/main/java/com/aolda/itda/config/AuthFilter.java
@@ -0,0 +1,76 @@
+package com.aolda.itda.config;
+
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.service.AuthService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+@RequiredArgsConstructor
+@Component
+@Slf4j
+public class AuthFilter extends OncePerRequestFilter {
+
+    private final AuthService authService;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+
+        if (request.getRequestURI().contains("/api/auth")) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        String token = request.getHeader("X-Subject-Token");
+
+        // 토큰 헤더 검증
+        if (token == null || token.isEmpty()) {
+            throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
+        }
+
+        // 유효 토큰 검증
+        String userId = authService.validateTokenAndGetUserId(token);
+        if (userId == null) {
+            log.error("Token validation failed for URI {}: {}", request.getRequestURI(), request.getRemoteAddr());
+            throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI());
+        }
+
+        // 프로젝트 권한 검증
+        String projectId = request.getParameter("projectId");
+        if (projectId != null) {
+            try {
+                authService.getBestRoleWithinProject(token, projectId).get("role");
+                if (!request.getMethod().equals("GET") && !authService.getBestRoleWithinProject(token, projectId).get("role").equals("admin")) {
+                    throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
+                }
+            } catch (Exception e) {
+                throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI());
+            }
+        }
+
+        // 프로젝트 리스트 조회
+        List<String> projects;
+        if (authService.isAdmin(Map.of("id", userId, "token", token))) {
+            projects = authService.getAllProjects(token).stream().map(IdAndNameDTO::getId).toList();
+        } else {
+            projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token)).stream().map(IdAndNameDTO::getId).toList();
+        }
+
+        request.setAttribute("projects", projects);
+        request.setAttribute("user", Map.of("id", userId, "token", token));
+
+        filterChain.doFilter(request, response);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/aolda/itda/config/LoggingFilter.java b/src/main/java/com/aolda/itda/config/LoggingFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c5c2f0e090df6f13d35f4c7df103849b9ea9c8e
--- /dev/null
+++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java
@@ -0,0 +1,57 @@
+package com.aolda.itda.config;
+
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.service.AuthService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.ContentCachingRequestWrapper;
+
+import java.io.IOException;
+import java.util.Map;
+
+@Component
+@RequiredArgsConstructor
+public class LoggingFilter extends OncePerRequestFilter {
+
+    private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
+    private final AuthService authService;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+
+        // Request Body를 읽을 수 있도록 래핑
+        ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
+        filterChain.doFilter(cachingRequest, response);
+
+        // 로그 기록
+        logRequest(cachingRequest);
+    }
+
+    private void logRequest(ContentCachingRequestWrapper request) throws JsonProcessingException {
+        String ip = request.getRemoteAddr();
+        String method = request.getMethod();
+        String uri = request.getRequestURI();
+        String queryString = request.getQueryString();
+        String body = getRequestBody(request);
+        Map<String, String> user = (Map<String, String>) request.getAttribute("user");
+
+        logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, User: {}, Request Body: {}",
+                ip, method, uri, (queryString != null ? queryString : "None"), (user != null ? user.get("id") : "None"),
+                (!body.isEmpty() ? body : "None"));
+    }
+
+    private String getRequestBody(ContentCachingRequestWrapper request) {
+        byte[] buf = request.getContentAsByteArray();
+        return (buf.length > 0) ? new String(buf) : "";
+    }
+}
diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..a3559c4628d3c5854fc8386cbb1d173943f9ea42
--- /dev/null
+++ b/src/main/java/com/aolda/itda/config/WebConfig.java
@@ -0,0 +1,53 @@
+package com.aolda.itda.config;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+@RequiredArgsConstructor
+public class WebConfig implements WebMvcConfigurer {
+
+    private final AuthFilter authFilter;
+    private final LoggingFilter loggingFilter;
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) { // 스프링단에서 cors 설정
+        registry.addMapping("/**")
+                .allowedOriginPatterns("*")
+                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "FETCH")
+                .allowedHeaders("*")
+                .allowCredentials(true)
+                .exposedHeaders("Authorization", "X-Refresh-Token", "Access-Control-Allow-Origin")
+        ;
+    }
+
+
+    @Bean
+    public FilterRegistrationBean<AuthFilter> authFilterRegistration() {
+        FilterRegistrationBean<AuthFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(authFilter);
+        registrationBean.setOrder(1); // AuthFilter의 순서를 1로 설정
+        registrationBean.addUrlPatterns("/*");
+        return registrationBean;
+    }
+
+    @Bean
+    public FilterRegistrationBean<LoggingFilter> loggingFilterRegistration() {
+        FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(loggingFilter);
+        registrationBean.setOrder(2); // LoggingFilter의 순서를 2로 설정
+        registrationBean.addUrlPatterns("/*");
+        return registrationBean;
+    }
+
+    @Bean
+    public JPAQueryFactory jpaQueryFactory(EntityManager em) {
+        return new JPAQueryFactory(em);
+    }
+}
diff --git a/src/main/java/com/aolda/itda/controller/AuthController.java b/src/main/java/com/aolda/itda/controller/AuthController.java
new file mode 100644
index 0000000000000000000000000000000000000000..79046d2d2459cde40b302cc0e190d3240280257e
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/AuthController.java
@@ -0,0 +1,31 @@
+package com.aolda.itda.controller;
+
+import com.aolda.itda.dto.auth.LoginRequestDTO;
+import com.aolda.itda.service.AuthService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/auth")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final AuthService authService;
+
+    @PostMapping("/login")
+    public ResponseEntity<Object> login(HttpServletResponse response,
+                                        @RequestBody LoginRequestDTO loginRequestDTO) throws JsonProcessingException {
+
+        return ResponseEntity.ok(authService.userLogin(response, loginRequestDTO));
+    }
+
+    @GetMapping("/role")
+    public ResponseEntity<Object> roleWithinProject(@RequestHeader("X-Subject-Token") String token,
+                                                    @RequestParam String projectId) throws JsonProcessingException {
+
+        return ResponseEntity.ok(authService.getBestRoleWithinProject(token, projectId));
+    }
+}
diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
new file mode 100644
index 0000000000000000000000000000000000000000..26a0322cc29fb6c642ccafbf9dd5cce005030a9e
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java
@@ -0,0 +1,112 @@
+package com.aolda.itda.controller.certificate;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.certificate.CertificateDTO;
+import com.aolda.itda.service.certificate.CertificateService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class CertificateController {
+
+    private final CertificateService certificateService;
+
+    /**
+     * 인증서 생성
+     * POST /api/certificate?projectId=xxx
+     */
+    @PostMapping("/certificate")
+    public ResponseEntity<Void> create(
+            @RequestParam String projectId,
+            @RequestBody CertificateDTO dto,
+            HttpServletRequest request
+    ) {
+        certificateService.createCertificate(
+                projectId,
+                dto,
+                (List<String>) request.getAttribute("projects")
+        );
+        return ResponseEntity.ok().build();
+    }
+
+    /**
+     * 인증서 단건 조회
+     * GET /api/certificate?certificateId=xxx
+     */
+    @GetMapping("/certificate")
+    public ResponseEntity<CertificateDTO> view(
+            @RequestParam Long certificateId,
+            HttpServletRequest request
+    ) {
+        return ResponseEntity.ok(
+                certificateService.getCertificate(
+                        certificateId,
+                        (List<String>) request.getAttribute("projects")
+                )
+        );
+    }
+
+    /**
+     * 인증서 목록 조회 (domain 필터링 optional)
+     * GET /api/certificates?projectId=xxx&domain=foo
+     */
+    @GetMapping("/certificates")
+    public ResponseEntity<PageResp<CertificateDTO>> lists(
+            @RequestParam String projectId,
+            @RequestParam(required = false) String domain,
+            @RequestParam(required = false) String query
+    ) {
+
+        if (query != null) {
+            return ResponseEntity.ok(
+                    certificateService.getCertificatesWithSearch(
+                            projectId,
+                            query
+                    )
+            );
+        }
+        return ResponseEntity.ok(
+                certificateService.getCertificates(projectId, domain)
+        );
+    }
+
+    /**
+     * 인증서 수정
+     * PATCH /api/certificate?certificateId=xxx
+     */
+    @PatchMapping("/certificate")
+    public ResponseEntity<Void> edit(
+            @RequestParam Long certificateId,
+            @RequestBody CertificateDTO dto,
+            HttpServletRequest request
+    ) {
+        certificateService.editCertificate(
+                certificateId,
+                dto,
+                (List<String>) request.getAttribute("projects")
+        );
+        return ResponseEntity.ok().build();
+    }
+
+    /**
+     * 인증서 삭제
+     * DELETE /api/certificate?certificateId=xxx
+     */
+    @DeleteMapping("/certificate")
+    public ResponseEntity<Void> delete(
+            @RequestParam Long certificateId,
+            HttpServletRequest request
+    ) {
+        certificateService.deleteCertificate(
+                certificateId,
+                (List<String>) request.getAttribute("projects")
+        );
+        return ResponseEntity.ok().build();
+    }
+}
diff --git a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
new file mode 100644
index 0000000000000000000000000000000000000000..999707aed33a9f559d2c396c6532df50aada5173
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java
@@ -0,0 +1,53 @@
+package com.aolda.itda.controller.forwarding;
+
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.service.forwarding.ForwardingService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class ForwardingController {
+
+    private final ForwardingService forwardingService;
+
+    @PostMapping("/forwarding")
+    public ResponseEntity<Object> create(@RequestParam String projectId,
+                                         @RequestBody ForwardingDTO dto) {
+        forwardingService.createForwarding(projectId, dto);
+        return ResponseEntity.ok().build();
+    }
+
+    @GetMapping("/forwarding")
+    public ResponseEntity<Object> view(@RequestParam Long forwardingId, HttpServletRequest request) {
+        return ResponseEntity.ok(forwardingService.getForwarding(forwardingId, (List<String>) request.getAttribute("projects")));
+    }
+
+    @GetMapping("/forwardings")
+    public ResponseEntity<Object> lists(@RequestParam String projectId,
+                                        @RequestParam(required = false) String query) {
+        return ResponseEntity.ok(forwardingService.getForwardingsWithSearch(projectId, query));
+    }
+
+    @PatchMapping("/forwarding")
+    public ResponseEntity<Object> edit(@RequestParam Long forwardingId,
+                                         @RequestBody ForwardingDTO dto,
+                                       HttpServletRequest request) {
+        forwardingService.editForwarding(forwardingId, dto, (List<String>) request.getAttribute("projects") );
+        return ResponseEntity.ok().build();
+    }
+
+    @DeleteMapping("/forwarding")
+    public ResponseEntity<Object> delete(@RequestParam Long forwardingId,
+                                         HttpServletRequest request) {
+        forwardingService.deleteForwarding(forwardingId, (List<String>) request.getAttribute("projects"));
+        return ResponseEntity.ok().build();
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/controller/log/LogController.java b/src/main/java/com/aolda/itda/controller/log/LogController.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d4bfcf7bedf91d5fc7d1fbca6f4d68e6a5e48e8
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/log/LogController.java
@@ -0,0 +1,41 @@
+package com.aolda.itda.controller.log;
+
+import com.aolda.itda.service.log.LogService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class LogController {
+
+    private final LogService logService;
+
+    @GetMapping("/log")
+    public ResponseEntity<Object> view(@RequestParam Long logId, HttpServletRequest request) {
+        return ResponseEntity.ok(logService.getLog(logId, (List<String>) request.getAttribute("projects")));
+    }
+
+    @GetMapping("/logs")
+    public ResponseEntity<Object> lists(@RequestParam(required = false) String projectId
+                                        ,@RequestParam(required = false) String type
+                                        ,@RequestParam(required = false) String username
+                                        ,@RequestParam(required = false) String action
+                                        ,@RequestParam(defaultValue = "false") boolean isASC
+                                        ,@PageableDefault(size = 10) Pageable pageable
+                                        ,HttpServletRequest request) {
+        return ResponseEntity.ok(logService.getLogs(projectId, type, username, action, isASC, pageable,
+                (Map<String, String>) request.getAttribute("user")));
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/controller/main/MainController.java b/src/main/java/com/aolda/itda/controller/main/MainController.java
new file mode 100644
index 0000000000000000000000000000000000000000..371cce5c41e82deb7dbe8bf10ac0c618d52e5a74
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/main/MainController.java
@@ -0,0 +1,33 @@
+package com.aolda.itda.controller.main;
+
+import com.aolda.itda.service.main.MainService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class MainController {
+
+    private final MainService mainService;
+
+    @GetMapping("/projects")
+    public ResponseEntity<Object> projects(HttpServletRequest request) throws JsonProcessingException {
+        return ResponseEntity.ok(mainService.getAllProjects((Map<String, String>) request.getSession().getAttribute("user")));
+    }
+
+    @GetMapping("/main")
+    public ResponseEntity<Object> mainInfo(@RequestParam String projectId, HttpServletRequest request) {
+        return ResponseEntity.ok(mainService.getMainInfo(projectId, (List<String>) request.getAttribute("projects")));
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ca1aa859c30e146f0552f2dc45cbefdc59ce677
--- /dev/null
+++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java
@@ -0,0 +1,55 @@
+package com.aolda.itda.controller.routing;
+
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.dto.routing.RoutingDTO;
+import com.aolda.itda.service.routing.RoutingService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class RoutingController {
+
+    private final RoutingService routingService;
+
+    @PostMapping("/routing")
+    public ResponseEntity<Object> create(@RequestParam String projectId,
+                                         @RequestBody RoutingDTO dto) throws IOException {
+        routingService.createRouting(projectId, dto);
+        return ResponseEntity.ok().build();
+    }
+
+    @GetMapping("/routing")
+    public ResponseEntity<Object> view(@RequestParam Long routingId,
+                                       HttpServletRequest request) {
+        return ResponseEntity.ok(routingService.getRouting(routingId, (List<String>) request.getAttribute("projects")));
+    }
+
+    @GetMapping("/routings")
+    public ResponseEntity<Object> lists(@RequestParam String projectId,
+                                        @RequestParam(required = false) String query) {
+        return ResponseEntity.ok(routingService.getRoutingsWithSearch(projectId, query));
+    }
+
+    @PatchMapping("/routing")
+    public ResponseEntity<Object> edit(@RequestParam Long routingId,
+                                       @RequestBody RoutingDTO dto,
+                                       HttpServletRequest request) throws IOException {
+        routingService.editRouting(routingId, dto, (List<String>) request.getAttribute("projects"));
+        return ResponseEntity.ok().build();
+    }
+
+    @DeleteMapping("/routing")
+    public ResponseEntity<Object> delete(@RequestParam Long routingId,
+                                         HttpServletRequest request) {
+        routingService.deleteRouting(routingId, (List<String>) request.getAttribute("projects"));
+        return ResponseEntity.ok().build();
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/dto/PageResp.java b/src/main/java/com/aolda/itda/dto/PageResp.java
new file mode 100644
index 0000000000000000000000000000000000000000..7079600bbabe8b47e3e4c57f5267e8b287c8a473
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/PageResp.java
@@ -0,0 +1,27 @@
+package com.aolda.itda.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Builder
+@Getter
+@AllArgsConstructor
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class PageResp<T> {
+    private Integer totalPages;
+    private Long totalElements;
+    private Integer size;
+    private List<T> contents;
+    private Boolean first;
+    private Boolean last;
+
+    public static <T> PageRespBuilder<T> builderFor(Class<T> clazz) {
+        return (PageRespBuilder<T>) new PageRespBuilder<>();
+    }
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java b/src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..5918105381265b66446aa2d221657508e177fb58
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java
@@ -0,0 +1,24 @@
+package com.aolda.itda.dto.auth;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.querydsl.core.annotations.QueryProjection;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@NoArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class IdAndNameDTO {
+
+    private String id;
+    private String name;
+
+    @QueryProjection
+    public IdAndNameDTO(String id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..b91b3af32112b005db11a228e682e42889db7505
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java
@@ -0,0 +1,11 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class LoginRequestDTO {
+    private String id;
+    private String password;
+}
diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..a647b05835b9614cf21b1b8340a31b256fade9bc
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java
@@ -0,0 +1,17 @@
+package com.aolda.itda.dto.auth;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class LoginResponseDTO {
+    private Boolean isAdmin;
+    private List<IdAndNameDTO> projects;
+}
diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..058fdd789d0f2c53bbda5ac10469bd9e72d6313b
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java
@@ -0,0 +1,35 @@
+package com.aolda.itda.dto.certificate;
+
+import com.aolda.itda.entity.certificate.Challenge;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import jakarta.persistence.Column;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class CertificateDTO {
+
+    private Long id;          // 인증서 고유 ID
+    private String projectId;            // 프로젝트 식별자
+    private String domain;               // SSL 인증받을 도메인 주소
+    private String email;
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 도메인 소유자의 이메일
+    private LocalDateTime expiresAt;
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 인증서 만료일
+    private LocalDateTime createdAt;
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 인증서 생성일
+    private LocalDateTime updatedAt;    // 인증서 업데이트일
+    private Challenge challenge;         // 챌린지 방식 (HTTP, DNS_CLOUDFLARE)
+    private Boolean isDeleted;           // 삭제 여부 (soft delete)
+    private String apiToken;
+}
+/* 이메일, 챌린지 방식, http인지 dns인지... "*/
+//도메인, 소유자 이메일, 챌린지 방식 확실하게 들어가야함!!
+/*erd 보고 만들기*/
+//Challenge 키는 따로 (private으로 api 키 받기)
\ No newline at end of file
diff --git a/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..be003b6ca78d1f4e317c391559fd41e2c4a304ad
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java
@@ -0,0 +1,50 @@
+package com.aolda.itda.dto.forwarding;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ForwardingDTO {
+
+    private Long id;
+
+    @Pattern(regexp = "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$",
+            message = "잘못된 IP 형식 (server)")
+    private String serverIp;
+
+    @NotBlank(message = "serverPort 값이 존재하지 않습니다")
+    @Pattern(regexp = "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$",
+            message = "잘못된 포트 형식 (server)")
+    private String serverPort;
+
+    @NotBlank(message = "instanceIp 값이 존재하지 않습니다")
+    @Pattern(regexp = "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$",
+            message = "잘못된 IP 형식 (instance)")
+    private String instanceIp;
+
+    @NotBlank(message = "instancePort 값이 존재하지 않습니다")
+    @Pattern(regexp = "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$",
+            message = "잘못된 포트 형식 (instance)")
+    private String instancePort;
+
+    @NotBlank(message = "name 값이 존재하지 않습니다")
+    private String name;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private LocalDateTime createdAt;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/aolda/itda/dto/log/LogDTO.java b/src/main/java/com/aolda/itda/dto/log/LogDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..9946028fa254c24ea5197f8002c610ad02a083b5
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/log/LogDTO.java
@@ -0,0 +1,43 @@
+package com.aolda.itda.dto.log;
+
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.ObjectType;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.querydsl.core.annotations.QueryProjection;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class LogDTO {
+    private Long id;
+    private IdAndNameDTO user;
+    private Action action;
+    private ObjectType type;
+    private Long objectId;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private String description;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private LocalDateTime createdAt;
+
+    @QueryProjection
+    public LogDTO(Long id, IdAndNameDTO user, Action action, ObjectType type, Long objectId, String description, LocalDateTime createdAt) {
+        this.id = id;
+        this.user = user;
+        this.action = action;
+        this.type = type;
+        this.objectId = objectId;
+        this.description = description;
+        this.createdAt = createdAt;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..cbcdcd583831431a778791d15c8551ac834d5fe5
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java
@@ -0,0 +1,20 @@
+package com.aolda.itda.dto.main;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class MainInfoDTO {
+
+    private Long routing;
+    private Long forwarding;
+    private Long certificate;
+
+}
diff --git a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..985056ea3771b0a0fc3b7afc2db890c039f4db84
--- /dev/null
+++ b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java
@@ -0,0 +1,40 @@
+package com.aolda.itda.dto.routing;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class RoutingDTO {
+    @NotBlank
+    private String ip;
+    @NotBlank
+    private String domain;
+    @NotBlank
+    private String name;
+    @NotBlank
+    private String port;
+    private Long id;
+    @NotNull
+    private Long certificateId;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private LocalDateTime createdAt;
+
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private LocalDateTime updatedAt;
+
+    @NotNull
+    private Boolean caching;
+}
diff --git a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
new file mode 100644
index 0000000000000000000000000000000000000000..b62af84f1afea4909e4af4e7cb72848ddd0feee1
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java
@@ -0,0 +1,29 @@
+package com.aolda.itda.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import jakarta.persistence.Column;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import lombok.Getter;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+@Getter
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class BaseTimeEntity {
+
+    @CreatedDate
+    @Column(updatable = false, columnDefinition = "DATETIME")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private LocalDateTime createdAt;
+
+    @LastModifiedDate
+    @Column(name = "updated_at", columnDefinition = "DATETIME")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private LocalDateTime updatedAt;
+
+}
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
new file mode 100644
index 0000000000000000000000000000000000000000..9161b45397475cd8555591d962df43f3c8684d78
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java
@@ -0,0 +1,58 @@
+package com.aolda.itda.entity.certificate;
+
+import com.aolda.itda.entity.BaseTimeEntity;
+import com.aolda.itda.entity.user.User;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "certificate")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Certificate extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long certificateId;
+
+    @Column(length = 64)
+    private String projectId;
+
+    @Setter
+    @Column(length = 64)
+    private String domain;
+
+    @Column(length = 64)
+    @Setter
+    private String email;
+
+    @Setter
+    @Column(columnDefinition = "DATETIME")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
+    private LocalDateTime expiresAt; //인증서 만료일
+
+    @Enumerated(EnumType.STRING)
+    private Challenge challenge;
+
+    private Boolean isDeleted;
+
+
+    public String formatDomain() {
+        return domain == null ? null : domain.replace("*", "_");
+    }
+
+    public void setIsDeleted(boolean b) {
+        this.isDeleted = b;
+    }
+
+    @Transient
+    private String apiToken;
+
+}
+
diff --git a/src/main/java/com/aolda/itda/entity/certificate/Challenge.java b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java
new file mode 100644
index 0000000000000000000000000000000000000000..41f607e0289f9533cf6bc18c106e9d6037f1abed
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java
@@ -0,0 +1,15 @@
+package com.aolda.itda.entity.certificate;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+@JsonFormat(shape = JsonFormat.Shape.STRING)
+public enum Challenge {
+    HTTP, DNS_CLOUDFLARE;
+
+    @JsonValue
+    @Override
+    public String toString() {
+        return name().toLowerCase();
+    }
+}
diff --git a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
new file mode 100644
index 0000000000000000000000000000000000000000..0ba96d651f09690952337da5a311e12d8f1f51e6
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java
@@ -0,0 +1,82 @@
+package com.aolda.itda.entity.forwarding;
+
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.entity.BaseTimeEntity;
+import com.aolda.itda.entity.user.User;
+import jakarta.persistence.*;
+import lombok.*;
+
+@Entity
+@Table(name = "forwarding")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+public class Forwarding extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long forwardingId;
+
+    @Column(length = 64)
+    private String projectId;
+
+    @Column(length = 32)
+    private String serverIp;
+
+    @Column(length = 8)
+    private String serverPort;
+
+    @Column(length = 32)
+    private String instanceIp;
+
+    @Column(length = 8)
+    private String instancePort;
+
+    private Boolean isDeleted;
+
+    @Column(length = 256)
+    private String name;
+
+    public Forwarding(Forwarding forwarding) {
+        this.forwardingId = forwarding.getForwardingId();
+        this.projectId = forwarding.getProjectId();
+        this.serverIp = forwarding.getServerIp();
+        this.serverPort = forwarding.getServerPort();
+        this.instanceIp = forwarding.getInstanceIp();
+        this.instancePort = forwarding.getInstancePort();
+        this.isDeleted = forwarding.getIsDeleted();
+        this.name = forwarding.getName();
+    }
+
+    public ForwardingDTO toForwardingDTO() {
+        return ForwardingDTO.builder()
+                .id(forwardingId)
+                .name(name)
+                .serverPort(serverPort)
+                .instanceIp(instanceIp)
+                .instancePort(instancePort)
+                .createdAt(getCreatedAt())
+                .updatedAt(getUpdatedAt())
+                .build();
+    }
+
+    public void edit(ForwardingDTO dto) {
+        this.name = dto.getName() != null ? dto.getName() : this.name;
+        this.serverPort = dto.getServerPort() != null &&
+                dto.getServerPort().matches("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$")
+                ? dto.getServerPort() : this.serverPort;
+        this.instanceIp = dto.getInstanceIp() != null &&
+                dto.getInstanceIp().matches("^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$")
+                ? dto.getInstanceIp() : this.instanceIp;
+        this.instancePort = dto.getInstancePort() != null &&
+                dto.getInstancePort().matches("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$")
+                ? dto.getInstancePort() : this.instancePort;
+    }
+
+    public void delete() {
+        this.isDeleted = true;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/entity/log/Action.java b/src/main/java/com/aolda/itda/entity/log/Action.java
new file mode 100644
index 0000000000000000000000000000000000000000..448a65e6e29e793da43011c1f40fd64b01659dae
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/log/Action.java
@@ -0,0 +1,15 @@
+package com.aolda.itda.entity.log;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+@JsonFormat(shape = JsonFormat.Shape.STRING)
+public enum Action {
+    CREATE, UPDATE, DELETE;
+
+    @JsonValue
+    @Override
+    public String toString() {
+        return name().toLowerCase();
+    }
+}
diff --git a/src/main/java/com/aolda/itda/entity/log/Log.java b/src/main/java/com/aolda/itda/entity/log/Log.java
new file mode 100644
index 0000000000000000000000000000000000000000..6bf607497bb95f9ba71ca677dbcb9af5d7b9a36d
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/log/Log.java
@@ -0,0 +1,58 @@
+package com.aolda.itda.entity.log;
+
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.dto.log.LogDTO;
+import com.aolda.itda.entity.BaseTimeEntity;
+import com.aolda.itda.entity.user.User;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "log")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Log extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long logId;
+
+    @ManyToOne
+    @JoinColumn(name = "user_id", nullable = false)
+    private User user;
+
+    @Column(length = 64)
+    private String projectId;
+
+    @Enumerated(EnumType.STRING)
+    private ObjectType objectType;
+
+    @Column(length = 64)
+    private Long objectId;
+
+    @Enumerated(EnumType.STRING)
+    private Action action;
+
+    @Lob
+    @Column(length = 1024)
+    private String description;
+
+    public LogDTO toLogDTO() {
+        return LogDTO.builder()
+                .id(logId)
+                .user(IdAndNameDTO.builder().id(user.getKeystoneId()).name(user.getKeystoneUsername()).build())
+                .action(action)
+                .type(objectType)
+                .objectId(objectId)
+                .description(description)
+                .createdAt(getCreatedAt())
+                .build();
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/entity/log/ObjectType.java b/src/main/java/com/aolda/itda/entity/log/ObjectType.java
new file mode 100644
index 0000000000000000000000000000000000000000..c91f27911dfc4c661be7ab343b03c2391ddb1a56
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/log/ObjectType.java
@@ -0,0 +1,15 @@
+package com.aolda.itda.entity.log;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+@JsonFormat(shape = JsonFormat.Shape.STRING)
+public enum ObjectType {
+    ROUTING, CERTIFICATE, FORWARDING;
+
+    @JsonValue
+    @Override
+    public String toString() {
+        return name().toLowerCase();
+    }
+}
diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java
new file mode 100644
index 0000000000000000000000000000000000000000..9412368edff4100d0d3db65d5a25faf97760cd97
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java
@@ -0,0 +1,75 @@
+package com.aolda.itda.entity.routing;
+
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.dto.routing.RoutingDTO;
+import com.aolda.itda.entity.BaseTimeEntity;
+import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.user.User;
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "routing")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Routing extends BaseTimeEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long routingId;
+
+    @ManyToOne
+    @JoinColumn(name = "certificate_id")
+    private Certificate certificate;
+
+    @Column(length = 64)
+    private String projectId;
+
+    @Column(length = 64)
+    private String domain;
+
+    @Column(length = 32)
+    private String instanceIp;
+
+    @Column(length = 8)
+    private String instancePort;
+
+    private Boolean isDeleted;
+
+    private Boolean caching;
+
+    @Column(length = 256)
+    private String name;
+
+    public RoutingDTO toRoutingDTO() {
+        return RoutingDTO.builder()
+                .id(routingId)
+                .name(name)
+                .port(instancePort)
+                .ip(instanceIp)
+                .certificateId(certificate == null ? null : certificate.getCertificateId())
+                .caching(caching)
+                .domain(domain)
+                .createdAt(getCreatedAt())
+                .updatedAt(getUpdatedAt())
+                .build();
+    }
+
+    public void edit(RoutingDTO dto, Certificate certificate) {
+        this.name = dto.getName() != null ? dto.getName() : this.name;
+        this.instanceIp = dto.getIp() != null ? dto.getIp() : this.instanceIp;
+        this.instancePort = dto.getPort() != null ? dto.getPort() : this.instancePort;
+        this.caching = dto.getCaching() != null ? dto.getCaching() : this.caching;
+        this.domain = dto.getDomain() != null ? dto.getDomain() : this.domain;
+        this.certificate = certificate;
+    }
+    public void delete() {
+        this.isDeleted = true;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/entity/user/User.java b/src/main/java/com/aolda/itda/entity/user/User.java
new file mode 100644
index 0000000000000000000000000000000000000000..682a50e9004d3849bc795b1cd98dc0b392eefb02
--- /dev/null
+++ b/src/main/java/com/aolda/itda/entity/user/User.java
@@ -0,0 +1,28 @@
+package com.aolda.itda.entity.user;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Entity
+@Table(name = "user")
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class User {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(nullable = false)
+    private Long userId;
+
+    private String keystoneUsername;
+    private String keystoneId;
+
+    public void changeUsername(String username) {
+        this.keystoneUsername = username;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..20ef9ec85a3b546782271043c8bebc1854dae3b2
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java
@@ -0,0 +1,28 @@
+package com.aolda.itda.exception;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@Slf4j
+@RestControllerAdvice
+public class ApiExceptionHandler {
+
+    @ExceptionHandler(value = CustomException.class)
+    public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
+        log.error("[handleCustomException] {} : {}, {}", e.getErrorCode().name(), e.getErrorCode().getMessage(), e.getStackTrace());
+        return ErrorResponse.fromException(e);
+    }
+
+    @ExceptionHandler(Exception.class)
+    public ResponseEntity<ErrorResponse> handleException(Exception e) {
+        log.error("Unexpected error occurred: {}", e.getMessage(), e);
+        ErrorResponse response = new ErrorResponse(
+                ErrorCode.INTERNAL_SERVER_ERROR.getStatus(),
+                ErrorCode.INTERNAL_SERVER_ERROR.getCode(),
+                ErrorCode.INTERNAL_SERVER_ERROR.getMessage()
+        );
+        return new ResponseEntity<>(response, ErrorCode.INTERNAL_SERVER_ERROR.getStatus());
+    }
+}
diff --git a/src/main/java/com/aolda/itda/exception/CustomException.java b/src/main/java/com/aolda/itda/exception/CustomException.java
new file mode 100644
index 0000000000000000000000000000000000000000..d110063b5baf441b8dfba80e64ef0eb21816cb09
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/CustomException.java
@@ -0,0 +1,19 @@
+package com.aolda.itda.exception;
+
+import lombok.Getter;
+
+@Getter
+public class CustomException extends RuntimeException{
+    private ErrorCode errorCode;
+
+    private String info;
+
+    public CustomException(ErrorCode errorCode) {
+        this.errorCode = errorCode;
+    }
+
+    public CustomException(ErrorCode errorCode, String info){
+        this.errorCode = errorCode;
+        this.info = info;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java
new file mode 100644
index 0000000000000000000000000000000000000000..5adaa1ff1fab812ee5ba185453902715aaa9c522
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java
@@ -0,0 +1,57 @@
+package com.aolda.itda.exception;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@RequiredArgsConstructor
+public enum ErrorCode {
+
+    // User
+    INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"),
+    NOT_FOUND_USER(HttpStatus.BAD_REQUEST, "존재하지 않는 사용자입니다"),
+    UNAUTHORIZED_USER(HttpStatus.BAD_REQUEST, "권한이 없는 사용자입니다"),
+
+    // Token
+    INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"),
+
+    // Forwarding
+    NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"),
+
+    // Routing
+    NOT_FOUND_ROUTING(HttpStatus.BAD_REQUEST, "라우팅 파일이 존재하지 않습니다"),
+    
+    // Routing
+    NOT_FOUND_LOG(HttpStatus.BAD_REQUEST, "로그가 존재하지 않습니다"),
+
+    // Certificate
+    NOT_FOUND_CERTIFICATE(HttpStatus.BAD_REQUEST, "SSL 인증서가 존재하지 않습니다"),
+
+    // CONF File
+    FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"),
+    FAIL_UPDATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 수정하지 못했습니다"),
+    INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"),
+    DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"),
+    DUPLICATED_SERVER_PORT(HttpStatus.BAD_REQUEST, "중복된 서버 포트입니다"),
+    DUPLICATED_DOMAIN_NAME(HttpStatus.BAD_REQUEST, "중복된 도메인 주소입니다"),
+
+    // Nginx
+    FAIL_NGINX_CONF_TEST(HttpStatus.BAD_REQUEST, "Conf 파일 테스트에 실패했습니다"),
+    FAIL_NGINX_CONF_RELOAD(HttpStatus.BAD_REQUEST, "Nginx 재시작에 실패했습니다"),
+
+    FAIL_DELETE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 삭제하지 못했습니다"),
+    FAIL_ROLL_BACK(HttpStatus.BAD_REQUEST, "롤백 실패"),
+
+    FAIL_CREATE_CERT(HttpStatus.BAD_REQUEST, "인증서 생성에 실패했습니다"),
+
+    // System
+    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다");
+
+    private final HttpStatus status;
+    private final String message;
+
+    public String getCode() {
+        return this.name();
+    }
+}
diff --git a/src/main/java/com/aolda/itda/exception/ErrorResponse.java b/src/main/java/com/aolda/itda/exception/ErrorResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..aa020023e72b23a971940f74b147a49950e0d74e
--- /dev/null
+++ b/src/main/java/com/aolda/itda/exception/ErrorResponse.java
@@ -0,0 +1,31 @@
+package com.aolda.itda.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+@Getter
+@Builder
+@AllArgsConstructor
+public class ErrorResponse {
+
+    private final HttpStatus status;  // HTTP 상태 코드
+    private final String code;        // 에러 코드
+    private final String message;     // 에러 메시지
+
+    public static ResponseEntity<ErrorResponse> fromException(CustomException e) {
+        String message = e.getErrorCode().getMessage();
+        if (e.getInfo() != null) {
+            message += " " + e.getInfo(); // 추가 정보가 있는 경우 결합
+        }
+        return ResponseEntity
+                .status(e.getErrorCode().getStatus())
+                .body(ErrorResponse.builder()
+                        .status(e.getErrorCode().getStatus())
+                        .code(e.getErrorCode().name())
+                        .message(message)
+                        .build());
+    }
+}
diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..0f3c137a9795352e199bf81f2df8e081812a82cc
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java
@@ -0,0 +1,33 @@
+package com.aolda.itda.repository.certificate;
+
+import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.routing.Routing;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+public interface CertificateRepository extends JpaRepository<Certificate, Long> {
+
+    // 단건 조회 (Soft Delete 고려)
+    Optional<Certificate> findByCertificateIdAndIsDeleted(Long certificateId, Boolean isDeleted);
+
+    // 프로젝트별 목록 조회 (Soft Delete 고려)
+    List<Certificate> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
+
+    // 만료일이 주어진 날짜 이전인(=만료 30일 이내) 인증서 조회
+    //List<Certificate> findByExpiresAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted);
+
+    // 1) domain 필터링용 메서드
+    List<Certificate> findByProjectIdAndDomainContainingAndIsDeleted(
+            String projectId, String domain, Boolean isDeleted);
+
+    // 3) 만료 30일 이내 대상 조회
+    List<Certificate> findByExpiresAtBeforeAndIsDeleted(
+            LocalDateTime date, Boolean isDeleted);
+
+    @Query("SELECT r FROM Routing r WHERE r.projectId = ?1 AND r.isDeleted = ?3 AND (r.domain LIKE %?2% OR r.instanceIp LIKE %?2% OR r.name LIKE %?2%)")
+    List<Certificate> findWithSearch(String projectId, String query, Boolean isDeleted);
+}
diff --git a/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..9a330f0dbc0fe111dd99aed57e29a960ff9f5031
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java
@@ -0,0 +1,18 @@
+package com.aolda.itda.repository.forwarding;
+
+import com.aolda.itda.entity.forwarding.Forwarding;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface ForwardingRepository extends JpaRepository<Forwarding, Long> {
+    List<Forwarding> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
+    Optional<Forwarding> findByForwardingIdAndIsDeleted(Long forwardingId, Boolean isDeleted);
+    Boolean existsByInstanceIpAndInstancePortAndIsDeleted(String instanceIp, String instancePort, Boolean isDeleted);
+    Boolean existsByServerPortAndIsDeleted(String serverPort, Boolean isDeleted);
+
+    @Query("SELECT f FROM Forwarding f WHERE f.projectId = ?1 AND f.isDeleted = ?3 AND (f.instanceIp LIKE %?2% OR f.serverPort LIKE %?2% OR f.name LIKE %?2%)")
+    List<Forwarding> findWithSearch(String projectId, String query, Boolean isDeleted);
+}
diff --git a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
new file mode 100644
index 0000000000000000000000000000000000000000..19feb191a3f4f9dfaab6bb8b8c2925756a69c244
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java
@@ -0,0 +1,102 @@
+package com.aolda.itda.repository.log;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.auth.QIdAndNameDTO;
+import com.aolda.itda.dto.log.LogDTO;
+import com.aolda.itda.dto.log.QLogDTO;
+import com.aolda.itda.entity.log.Action;
+import com.aolda.itda.entity.log.ObjectType;
+import com.querydsl.core.BooleanBuilder;
+import com.querydsl.jpa.impl.JPAQuery;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.support.PageableExecutionUtils;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+import static com.aolda.itda.entity.log.QLog.*;
+import static com.aolda.itda.entity.user.QUser.*;
+
+@Repository
+@RequiredArgsConstructor
+public class LogQueryDSL {
+
+    private final JPAQueryFactory jpaQueryFactory;
+
+    /* log 목록 반환 */
+    public PageResp<LogDTO> getLogs(String projectId, String type,
+                                    String username, String action, Boolean isASC, Pageable pageable) {
+
+        List<LogDTO> content = jpaQueryFactory
+                .select(new QLogDTO(
+                        log.logId,
+                        new QIdAndNameDTO(user.keystoneId, user.keystoneUsername),
+                        log.action,
+                        log.objectType,
+                        log.objectId,
+                        log.description,
+                        log.createdAt
+                )).from(log)
+                .join(log.user, user).on(log.user.eq(user))
+                .where(getFilter(projectId, type, username, action))
+                .orderBy(isASC ? log.logId.asc() : log.logId.desc())
+                .offset(pageable.getOffset())
+                .limit(pageable.getPageSize())
+                .fetch();
+
+        JPAQuery<Long> cnt = jpaQueryFactory
+                .select(log.count())
+                .from(log)
+                .where(getFilter(projectId, type, username, action));
+
+        Page<LogDTO> page = PageableExecutionUtils.getPage(content, pageable, cnt::fetchOne);
+
+        return PageResp.<LogDTO>builder()
+                .contents(page.getContent())
+                .first(page.isFirst())
+                .last(page.isLast())
+                .size(page.getSize())
+                .totalPages(page.getTotalPages())
+                .totalElements(page.getTotalElements())
+                .build();
+    }
+
+    /* Where 필터 */
+    private BooleanBuilder getFilter(String projectId, String type,
+                                     String username, String action) {
+        BooleanBuilder builder = new BooleanBuilder();
+
+        /* 프로젝트 조건 */
+        if (projectId != null) {
+            builder.and(log.projectId.eq(projectId));
+        }
+
+        /* 오브젝트 타입 조건 */
+        if (type != null) {
+            switch (type) {
+                case "certificate" -> builder.and(log.objectType.eq(ObjectType.CERTIFICATE));
+                case "forwarding" -> builder.and(log.objectType.eq(ObjectType.FORWARDING));
+                case "routing" -> builder.and(log.objectType.eq(ObjectType.ROUTING));
+            }
+        }
+
+
+        /* 사용자 ID 조건 */
+        if (username != null) {
+            builder.and(log.user.keystoneUsername.contains(username));
+        }
+
+        /* CUD 조건 */
+        if (action != null) {
+            switch (action) {
+                case "create" -> builder.and(log.action.eq(Action.CREATE));
+                case "update" -> builder.and(log.action.eq(Action.UPDATE));
+                case "delete" -> builder.and(log.action.eq(Action.DELETE));
+            }
+        }
+        return builder;
+    }
+}
diff --git a/src/main/java/com/aolda/itda/repository/log/LogRepository.java b/src/main/java/com/aolda/itda/repository/log/LogRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..de0a769f6b1b206f5d350aae620fa997f7763006
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/log/LogRepository.java
@@ -0,0 +1,10 @@
+package com.aolda.itda.repository.log;
+
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.entity.routing.Routing;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface LogRepository extends JpaRepository<Log, Long> {
+}
diff --git a/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..44b88aecdc26ff76583c01149a9862503a0c8d79
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java
@@ -0,0 +1,18 @@
+package com.aolda.itda.repository.routing;
+
+import com.aolda.itda.entity.forwarding.Forwarding;
+import com.aolda.itda.entity.routing.Routing;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
+import java.util.Optional;
+
+public interface RoutingRepository extends JpaRepository<Routing, Long> {
+    List<Routing> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted);
+    Optional<Routing> findByRoutingIdAndIsDeleted(Long routingId, Boolean isDeleted);
+    Boolean existsByDomainAndIsDeleted(String domain, Boolean isDeleted);
+
+    @Query("SELECT r FROM Routing r WHERE r.projectId = ?1 AND r.isDeleted = ?3 AND (r.domain LIKE %?2% OR r.instanceIp LIKE %?2% OR r.name LIKE %?2%)")
+    List<Routing> findWithSearch(String projectId, String query, Boolean isDeleted);
+}
diff --git a/src/main/java/com/aolda/itda/repository/user/UserRepository.java b/src/main/java/com/aolda/itda/repository/user/UserRepository.java
new file mode 100644
index 0000000000000000000000000000000000000000..44cae688e4032b6caec7482c6c6f7e3c038736e2
--- /dev/null
+++ b/src/main/java/com/aolda/itda/repository/user/UserRepository.java
@@ -0,0 +1,11 @@
+package com.aolda.itda.repository.user;
+
+import com.aolda.itda.entity.user.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface UserRepository extends JpaRepository<User, Long> {
+    Optional<User> findByKeystoneId(String keystoneId);
+    Optional<User> findByKeystoneUsername(String keystoneUsername);
+}
diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c1740ec38b088511b9e4cf68afc54ac71fb365f
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/AuthService.java
@@ -0,0 +1,351 @@
+package com.aolda.itda.service;
+
+import com.aolda.itda.dto.auth.LoginRequestDTO;
+import com.aolda.itda.dto.auth.LoginResponseDTO;
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.entity.user.User;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.user.UserRepository;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.*;
+
+@Service
+@RequiredArgsConstructor
+public class AuthService {
+
+    @Value("${spring.server.keystone}")
+    private String keystone;
+    @Value("${spring.server.admin-id}")
+    private String adminId;
+    @Value("${spring.server.admin-password}")
+    private String adminPassword;
+    private final RestTemplate restTemplate = new RestTemplate();
+    private final ObjectMapper objectMapper = new ObjectMapper();
+    private final UserRepository userRepository;
+
+    // 사용자 로그인 후 토큰 발행 및 Role 반환
+    public LoginResponseDTO userLogin(HttpServletResponse response, LoginRequestDTO loginRequestDTO) throws JsonProcessingException {
+        Map<String, String> user = getToken(loginRequestDTO.getId(), loginRequestDTO.getPassword());
+
+        String userId = user.get("id");
+        String token = user.get("token");
+        String systemToken = getSystemToken(userId, loginRequestDTO.getPassword());
+
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+
+        User entity = userRepository.findByKeystoneId(userId).orElse(null);
+        if (entity == null) {
+            userRepository.save(User.builder().keystoneId(userId).
+                    keystoneUsername(loginRequestDTO.getId()).build());
+        }
+        else if (!entity.getKeystoneUsername().equals(loginRequestDTO.getId())) {
+            entity.changeUsername(loginRequestDTO.getId());
+            userRepository.save(entity);
+        }
+
+        response.addHeader("X-Subject-Token", systemToken != null ? systemToken : token);
+        return LoginResponseDTO.builder()
+                .isAdmin(systemToken != null)
+                .projects(getProjectsWithUser(user))
+                .build();
+    }
+
+    // 특정 사용자의 토큰 발행
+    private Map<String, String> getToken(String id, String password) {
+
+        String url = keystone + "/auth/tokens";
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        String requestBody = "{\n" +
+                "    \"auth\": {\n" +
+                "        \"identity\": {\n" +
+                "            \"methods\": [\n" +
+                "                \"password\"\n" +
+                "            ],\n" +
+                "            \"password\": {\n" +
+                "                \"user\": {\n" +
+                "                    \"name\": \""+ id + "\",\n" +
+                "                    \"domain\": {\n" +
+                "                        \"name\": \"Default\"\n" +
+                "                    },\n" +
+                "                    \"password\": \"" + password + "\"\n" +
+                "                }\n" +
+                "            }\n" +
+                "        }\n" +
+                "    }\n" +
+                "}";
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
+        ResponseEntity<Map> res;
+        try {
+            res = restTemplate.postForEntity(url, requestEntity, Map.class);
+        } catch (Exception e) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+        Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
+        Map<String, Object> resUser = (Map<String, Object>) resToken.get("user");
+        String userId = (String) resUser.get("id");
+        String token = res.getHeaders().getFirst("X-Subject-Token");
+
+        return Map.of("id", userId,
+                    "token", token);
+    }
+
+    private String getSystemToken(String id, String password) {
+
+        String url = keystone + "/auth/tokens";
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+        String requestBody = "{\n" +
+                "    \"auth\": {\n" +
+                "        \"identity\": {\n" +
+                "            \"methods\": [\n" +
+                "                \"password\"\n" +
+                "            ],\n" +
+                "            \"password\": {\n" +
+                "                \"user\": {\n" +
+                "                    \"id\": \"" + id + "\",\n" +
+                "                    \"password\": \"" + password + "\"\n" +
+                "                }\n" +
+                "            }\n" +
+                "        },\n" +
+                "        \"scope\": {\n" +
+                "            \"system\": {\n" +
+                "                \"all\": true\n" +
+                "            }\n" +
+                "        }\n" +
+                "    }\n" +
+                "}";
+
+        HttpEntity<String> requestEntity;
+        ResponseEntity<Map> res;
+        try {
+            requestEntity = new HttpEntity<>(requestBody, headers);
+            res = restTemplate.postForEntity(url, requestEntity, Map.class);
+        } catch (Exception e) {
+            return null;
+        }
+
+        Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
+        Map<String, Object> resUser = (Map<String, Object>) resToken.get("user");
+        String userId = (String) resUser.get("id");
+        String token = res.getHeaders().getFirst("X-Subject-Token");
+
+        return token;
+    }
+
+    private String getProjectToken(String unscopedToken, String projectId) {
+
+        String url = keystone + "/auth/tokens";
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+
+
+        String requestBody = "{\n" +
+                "    \"auth\": {\n" +
+                "        \"identity\": {\n" +
+                "            \"methods\": [\n" +
+                "                \"token\"\n" +
+                "            ],\n" +
+                "            \"token\": {\n" +
+                "                \"id\": \"" + unscopedToken +"\"\n" +
+                "            }\n" +
+                "        },\n" +
+                "        \"scope\": {\n" +
+                "            \"project\": {\n" +
+                "                \"id\": \""+ projectId +"\"\n" +
+                "            }\n" +
+                "        }\n" +
+                "    }\n" +
+                "}";
+
+        HttpEntity<String> requestEntity;
+        ResponseEntity<Map> res;
+        try {
+            requestEntity = new HttpEntity<>(requestBody, headers);
+            res = restTemplate.postForEntity(url, requestEntity, Map.class);
+        } catch (HttpClientErrorException.Forbidden e) {
+            return unscopedToken;
+        }
+        catch (Exception e) {
+            throw new CustomException(ErrorCode.INVALID_TOKEN);
+        }
+
+        Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token");
+        Map<String, Object> resUser = (Map<String, Object>) resToken.get("user");
+        String userId = (String) resUser.get("id");
+        String token = res.getHeaders().getFirst("X-Subject-Token");
+
+        return token;
+    }
+
+
+    // 특정 사용자의 특정 프로젝트 내 최고 권한 반환
+    public Map<String, String> getBestRoleWithinProject(String token, String projectId) throws JsonProcessingException {
+
+        return getBestRoleWithinProject(Map.of(
+                "id", validateTokenAndGetUserId(token),
+                "token", getProjectToken(token, projectId)),
+                projectId);
+    }
+
+    private Map<String, String> getBestRoleWithinProject(Map<String, String> user, String projectId) throws JsonProcessingException {
+        String userId = user.get("id");
+        String token = user.get("token");
+
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+        String url = keystone + "/role_assignments?user.id=" + userId + "&effective&include_names=true&scope.project.id=" + projectId;
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", token);
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+
+        JsonNode node = objectMapper.readTree(res.getBody());
+        ArrayNode arrayNode = (ArrayNode) node.get("role_assignments");
+
+        String bestRole = "reader";
+
+        for (JsonNode assignment : arrayNode) {
+
+            String roleName = assignment.path("role").path("name").asText();
+
+            if (roleName.equals("admin")) { // admin인 경우
+                bestRole = roleName;
+            } else if (roleName.equals("manager") && !bestRole.equals("admin")) { // 최고 권한이 admin이 아닌 경우
+                bestRole = roleName;
+            } else if (roleName.equals("member") && bestRole.equals("reader")) { // 최고 권한이 reader인 경우
+                bestRole = roleName;
+            }
+
+        }
+
+        return Map.of("role", bestRole);
+    }
+
+    // 관리자용 토큰 발행
+    public String getAdminToken() {
+        Map<String, String> user = getToken(adminId, adminPassword);
+        return user.get("token");
+    }
+
+    // 특정 사용자의 참여 프로젝트 반환
+    public List<IdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException {
+        String userId = user.get("id");
+        String token = user.get("token");
+        if (userId == null || token == null) {
+            throw new CustomException(ErrorCode.INVALID_USER_INFO);
+        }
+
+        String url = keystone + "/users/" + userId + "/projects";
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", token);
+
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+
+        JsonNode node = objectMapper.readTree(res.getBody());
+        ArrayNode arrayNode = (ArrayNode) node.get("projects");
+
+        List<IdAndNameDTO> lists = new ArrayList<>();
+
+        for (JsonNode assignment : arrayNode) {
+            String projectId = assignment.path("id").asText();
+            String projectName = assignment.path("name").asText();
+            lists.add(new IdAndNameDTO(projectId, projectName));
+        }
+        return lists;
+    }
+
+    public String validateTokenAndGetUserId(String token) throws JsonProcessingException {
+        String url = keystone + "/auth/tokens";
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", token);
+        headers.set("X-Subject-Token", token);
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res;
+        try {
+            res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+        } catch (Exception e) {
+            throw new CustomException(ErrorCode.INVALID_TOKEN);
+        }
+        return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText();
+
+    }
+
+    public List<IdAndNameDTO> getAllProjects(String token) throws JsonProcessingException {
+        String url = keystone + "/projects";
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", token);
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res;
+        try {
+            res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+        } catch (Exception e) {
+            throw new CustomException(ErrorCode.INVALID_TOKEN);
+        }
+
+        JsonNode node = objectMapper.readTree(res.getBody());
+        ArrayNode arrayNode = (ArrayNode) node.get("projects");
+
+        List<IdAndNameDTO> lists = new ArrayList<>();
+
+        for (JsonNode assignment : arrayNode) {
+            String projectId = assignment.path("id").asText();
+            String projectName = assignment.path("name").asText();
+            lists.add(new IdAndNameDTO(projectId, projectName));
+        }
+
+        return lists;
+
+    }
+
+    public void validateProjectAuth(List<String> projects, String projectId) {
+        if (projects != null && !projects.contains(projectId)) {
+            throw new CustomException(ErrorCode.UNAUTHORIZED_USER);
+        }
+    }
+
+    public Boolean isAdmin(Map<String, String> user) throws JsonProcessingException {
+        String url = keystone + "/role_assignments?user.id=" + user.get("id") + "&scope.system&include_names";
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("X-Auth-Token", user.get("token"));
+        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
+        ResponseEntity<String> res;
+        try {
+            res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
+        } catch (Exception e) {
+            return false;
+        }
+        JsonNode node = objectMapper.readTree(res.getBody()).path("role_assignments");
+        String system_all = node.path("scope").path("system").path("all").asText();
+        String role = node.path("role").path("name").asText();
+        if (system_all.equals("true") && role.equals("admin")) {
+            return true;
+        }
+        return false;
+
+    }
+}
diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
new file mode 100644
index 0000000000000000000000000000000000000000..7f426709d5480a8ed5564376ec12616c3a8bb8e6
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java
@@ -0,0 +1,279 @@
+package com.aolda.itda.service.certificate;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.certificate.CertificateDTO;
+import com.aolda.itda.dto.routing.RoutingDTO;
+import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.certificate.Challenge;
+import com.aolda.itda.entity.routing.Routing;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.service.AuthService;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+@Slf4j
+public class CertificateService {
+
+    @Value("${spring.server.admin-project}")
+    private String adminProject;
+    private final CertificateRepository certificateRepository;
+    private final AuthService authService;
+
+    /** 1) 단건 조회 **/
+    public CertificateDTO getCertificate(Long certificateId, List<String> projects) {
+        Certificate cert = certificateRepository
+                .findByCertificateIdAndIsDeleted(certificateId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+        authService.validateProjectAuth(projects, cert.getProjectId());
+        return toDTO(cert);
+    }
+
+    /* Certificate 목록 조회 + 검색 */
+    public PageResp<CertificateDTO> getCertificatesWithSearch(String projectId, String query) {
+
+        /* 입력 검증 */
+        if (query == null || query.isBlank()) {
+            return PageResp.<CertificateDTO>builder()
+                    .contents(certificateRepository.findByProjectIdAndIsDeleted(projectId, false)
+                            .stream()
+                            .map(this::toDTO)
+                            .toList()).build();
+        }
+
+        /* 도메인 패턴 검증 */
+        String domainPattern = "^(\\*\\.)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$";
+        if (Pattern.matches(domainPattern, query) && query.startsWith("*.")) {
+            query = query.substring(2);
+        }
+
+        return PageResp.<CertificateDTO>builder()
+                .contents(certificateRepository.findWithSearch(projectId, query, false)
+                        .stream()
+                        .map(this::toDTO)
+                        .toList()).build();
+    }
+
+    /** 1) 목록 조회 (domain 필터 optional) **/
+    public PageResp<CertificateDTO> getCertificates(String projectId, String domain) {
+        Set<Certificate> set = new HashSet<>();
+        // 도메인이 입력된 경우 처리
+        if (domain != null && !domain.isBlank()) {
+            // 서브도메인이 있는 경우 
+            if (domain.indexOf('.') != domain.lastIndexOf('.')) {
+                String wildcardDomain = "*." + domain.substring(domain.indexOf('.') + 1);
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                projectId, wildcardDomain, false));
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                projectId, domain, false));
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                adminProject, wildcardDomain, false));
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                adminProject, domain, false));
+            } else {
+                // 서브도메인이 없는 경우 일반 검색
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                projectId, domain, false));
+                set.addAll(certificateRepository
+                        .findByProjectIdAndDomainContainingAndIsDeleted(
+                                adminProject, domain, false));
+            }
+        } else {
+            set.addAll(certificateRepository
+                    .findByProjectIdAndIsDeleted(projectId, false));
+            set.addAll(certificateRepository
+                    .findByProjectIdAndIsDeleted(adminProject, false));
+        }
+        List<CertificateDTO> dtos = set.stream()
+                .map(this::toDTO)
+                .toList();
+        return PageResp.<CertificateDTO>builder()
+                .contents(dtos)
+                .build();
+    }
+
+    /** 2) 생성: expiredAt 자동 90일 설정 + 로깅 **/
+    public CertificateDTO createCertificate(String projectId,
+                                            CertificateDTO dto,
+                                            List<String> projects) {
+        log.info("createCertificate start (project={})", projectId);
+        authService.validateProjectAuth(projects, projectId);
+        validateDTO(dto);
+
+        // 발급
+        executeLego(dto);
+        log.info("certificate issued for domain={}", dto.getDomain());
+
+        // 엔티티 저장 (expiredAt 기본 90일 뒤)
+        Certificate cert = Certificate.builder()
+                .projectId(projectId)
+                .domain(dto.getDomain())
+                .email(dto.getEmail())
+                .challenge(dto.getChallenge())
+                .expiresAt(LocalDateTime.now().plusDays(90))
+                .isDeleted(false)
+                .apiToken(dto.getApiToken())
+                .build();
+        certificateRepository.save(cert);
+        log.info("certificate saved (id={}, domain={})",
+                cert.getCertificateId(), cert.getDomain());
+
+        return toDTO(cert);
+    }
+
+    /** 4) 수정 **/
+    public CertificateDTO editCertificate(Long certificateId,
+                                          CertificateDTO dto,
+                                          List<String> projects) {
+        Certificate cert = certificateRepository
+                .findByCertificateIdAndIsDeleted(certificateId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+        authService.validateProjectAuth(projects, cert.getProjectId());
+
+        if (dto.getDomain() != null)      cert.setDomain(dto.getDomain());
+        if (dto.getEmail() != null)       cert.setEmail(dto.getEmail());
+
+        certificateRepository.save(cert);
+        log.info("certificate edited (id={}, domain={})",
+                cert.getCertificateId(), cert.getDomain());
+        return toDTO(cert);
+    }
+
+    /** 4) 삭제 + 로깅 **/
+    public void deleteCertificate(Long certificateId, List<String> projects) {
+        Certificate cert = certificateRepository
+                .findByCertificateIdAndIsDeleted(certificateId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+        authService.validateProjectAuth(projects, cert.getProjectId());
+
+        cert.setIsDeleted(true);
+        certificateRepository.save(cert);
+        log.info("certificate deleted (id={}, domain={})",
+                cert.getCertificateId(), cert.getDomain());
+    }
+
+    /** 3) 만료 30일 전 자동 갱신 배치 **/
+    @Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul")
+    public void renewExpiringCertificates() {
+        LocalDateTime threshold = LocalDateTime.now().plusDays(30);
+        List<Certificate> expiring = certificateRepository
+                .findByExpiresAtBeforeAndIsDeleted(threshold, false);
+
+        for (Certificate cert : expiring) {
+            try {
+                log.info("renewing (id={}, domain={})", cert.getCertificateId(), cert.getDomain());
+                CertificateDTO dto = CertificateDTO.builder()
+                        .domain(cert.getDomain())
+                        .email(cert.getEmail())
+                        .challenge(cert.getChallenge())
+                        .apiToken(cert.getApiToken())
+                        .build();
+                executeLego(dto);
+
+                cert.setExpiresAt(LocalDateTime.now().plusDays(90));
+                certificateRepository.save(cert);
+                log.info("renewed (id={}, newExpiry={})",
+                        cert.getCertificateId(), cert.getExpiresAt());
+            } catch (Exception e) {
+                log.error("failed to renew (id={}, domain={}): {}",
+                        cert.getCertificateId(), cert.getDomain(), e.getMessage());
+            }
+        }
+    }
+
+    /** DTO 유효성 검사 **/
+    private void validateDTO(CertificateDTO dto) {
+        for (ConstraintViolation<CertificateDTO> v :
+                Validation.buildDefaultValidatorFactory()
+                        .getValidator().validate(dto)) {
+            throw new CustomException(ErrorCode.INVALID_CONF_INPUT, v.getMessage());
+        }
+    }
+
+    /** Entity→DTO 변환 **/
+    private CertificateDTO toDTO(Certificate cert) {
+        return CertificateDTO.builder()
+                .id(cert.getCertificateId())
+
+                .domain(cert.getDomain())
+                .email(cert.getEmail())
+                .challenge(cert.getChallenge())
+                .expiresAt(cert.getExpiresAt())
+                .createdAt(cert.getCreatedAt())
+                .updatedAt(cert.getUpdatedAt())
+                .isDeleted(cert.getIsDeleted())
+                .apiToken(cert.getApiToken())
+                .projectId(cert.getProjectId())
+                .build();
+    }
+
+    /** 인증서 발급용 lego 실행 **/
+    private void executeLego(CertificateDTO dto) {
+        if (dto.getChallenge() == Challenge.DNS_CLOUDFLARE && dto.getApiToken() == null) {
+            throw new CustomException(ErrorCode.INVALID_CONF_INPUT,
+                    "DNS_CLOUDFLARE 챌린지는 apiToken 필요");
+        }
+
+        List<String> cmd = new ArrayList<>();
+        cmd.add("/usr/local/bin/lego");
+        cmd.add("--accept-tos");
+        cmd.add("--email");   cmd.add(dto.getEmail());
+        cmd.add("--path");    cmd.add("/data/lego");
+
+        if (dto.getChallenge() == Challenge.HTTP) {
+            cmd.add("--http");
+            cmd.add("--http.webroot"); cmd.add("/data/letsencrypt-acme-challenge");
+        } else {
+            cmd.add("--dns");  cmd.add("cloudflare");
+        }
+
+        cmd.add("--domains"); cmd.add(dto.getDomain());
+        cmd.add("run");
+
+        log.info("executing lego: {}", String.join(" ", cmd));
+        ProcessBuilder pb = new ProcessBuilder(cmd)
+                .redirectErrorStream(true);
+        pb.environment().put("CF_DNS_API_TOKEN", dto.getApiToken());
+
+        try {
+            Process p = pb.start();
+            try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
+                r.lines().forEach(line -> log.info("[lego] {}", line));
+            }
+            if (p.waitFor() != 0) {
+                throw new CustomException(ErrorCode.FAIL_CREATE_CERT,
+                        "lego exit code=" + p.exitValue());
+            }
+        } catch (IOException | InterruptedException e) {
+            log.error("lego error", e);
+            throw new CustomException(ErrorCode.FAIL_CREATE_CERT,
+                    "lego 실행 실패: " + e.getMessage());
+        }
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
new file mode 100644
index 0000000000000000000000000000000000000000..e6fc9d45ee66e4f74ac7898bd2c43de0544de86a
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java
@@ -0,0 +1,307 @@
+package com.aolda.itda.service.forwarding;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.forwarding.ForwardingDTO;
+import com.aolda.itda.entity.forwarding.Forwarding;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.service.AuthService;
+import com.aolda.itda.template.ForwardingTemplate;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.HttpServerErrorException;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.regex.Pattern;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+@Slf4j
+public class ForwardingService {
+
+    @Value("${spring.server.base-ip}")
+    private String serverBaseIp;
+    private final ForwardingTemplate forwardingTemplate;
+    private final ForwardingRepository forwardingRepository;
+    private final AuthService authService;
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    /* 포트포워딩 정보 조회 */
+    public ForwardingDTO getForwarding(Long forwardingId, List<String> projects) {
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, forwarding.getProjectId());
+
+        return forwarding.toForwardingDTO();
+    }
+
+    /* 포트포워딩 목록 조회 + 검색 */
+    public PageResp<ForwardingDTO> getForwardingsWithSearch(String projectId, String query) {
+
+        /* 입력 검증 */
+        if (query == null || query.isBlank()) {
+            return PageResp.<ForwardingDTO>builder()
+                    .contents(forwardingRepository.findByProjectIdAndIsDeleted(projectId, false)
+                            .stream()
+                            .map(Forwarding::toForwardingDTO)
+                            .toList()).build();
+        }
+
+        return PageResp.<ForwardingDTO>builder()
+                .contents(forwardingRepository.findWithSearch(projectId, query, false)
+                        .stream()
+                        .map(Forwarding::toForwardingDTO)
+                        .toList()).build();
+    }
+
+    /* 포트포워딩 생성 */
+    public ForwardingDTO createForwarding(String projectId, ForwardingDTO dto) {
+
+        /* 입력 DTO 검증 */
+        validateDTO(dto);
+
+        /* 중복 검증 */
+        if (forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(dto.getInstanceIp(), dto.getInstancePort(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO);
+        }
+
+        if (forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
+        }
+
+        /* 포트포워딩 엔티티 생성 */
+        Forwarding forwarding = Forwarding.builder()
+                .isDeleted(false)
+                .projectId(projectId)
+                .name(dto.getName())
+                .serverIp(dto.getServerIp() == null ? serverBaseIp : dto.getServerIp())
+                .serverPort(dto.getServerPort())
+                .instanceIp(dto.getInstanceIp())
+                .instancePort(dto.getInstancePort())
+                .build();
+
+        forwardingRepository.save(forwarding);
+
+        /* nginx conf 파일 생성 및 예외 처리 */
+        String content = forwardingTemplate.getPortForwardingWithTCP(dto.getServerPort(), dto.getInstanceIp(), dto.getInstancePort(), dto.getName());
+        String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
+
+        File file = new File(confPath);
+        try {
+            Path path = Paths.get(confPath);
+            Files.createDirectories(path.getParent());
+            if (!file.createNewFile()) {
+                throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 포트포워딩 Conf 파일이 존재합니다");
+            }
+        } catch (IOException e) {
+            throw new CustomException(ErrorCode.FAIL_CREATE_CONF);
+        }
+
+        /* conf 파일 작성 및 예외 처리 */
+        try {
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, false)); // 예외처리 필요
+            bw.write(content);
+            bw.flush();
+            bw.close();
+        } catch (Exception e) {
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
+            }
+            throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다");
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        } catch (Exception e) {
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpServerErrorException.InternalServerError e) {
+            log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage());
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        } catch (Exception e) {
+            if (file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+        return forwarding.toForwardingDTO();
+    }
+
+    /* 포트포워딩 정보 수정 */
+    public void editForwarding(Long forwardingId, ForwardingDTO dto, List<String> projects) {
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, forwarding.getProjectId());
+
+        /* 중복 검증 */
+        if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) {
+            forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false);
+            throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT);
+        }
+
+        if (!(dto.getInstanceIp() == null && dto.getInstancePort() == null) &&
+                forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(
+                        dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp()
+                , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort()
+                , false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO);
+        }
+
+        /* 파일 수정 */
+        forwarding.edit(dto);
+        String content = forwardingTemplate.getPortForwardingWithTCP(forwarding.getServerPort(),
+                forwarding.getInstanceIp(),
+                forwarding.getInstancePort(),
+                forwarding.getName());
+        String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
+        File file = new File(confPath);
+        if (!file.exists()) {
+            throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 수정할 수 없습니다");
+        }
+
+        Path backup;
+        try {
+            backup = Files.createTempFile("temp_", ".tmp");
+            Files.copy(Paths.get(confPath), backup, StandardCopyOption.REPLACE_EXISTING
+            , StandardCopyOption.COPY_ATTRIBUTES);
+
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, false));
+            bw.write(content);
+            bw.flush();
+            bw.close();
+        } catch (Exception e) {
+            throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "포트포워딩 Conf 파일을 수정하지 못했습니다");
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (RuntimeException e) {
+
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (RuntimeException e) {
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+
+        /* DB 정보 수정 */
+        forwardingRepository.save(forwarding);
+    }
+
+    /* 포트포워딩 삭제 (소프트) */
+    public void deleteForwarding(Long forwardingId, List<String> projects) {
+        Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, forwarding.getProjectId());
+
+        /* 파일 삭제 */
+        String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf";
+        String deletePath = confPath + ".deleted";
+        try {
+            Files.move(Paths.get(confPath), Paths.get(deletePath));
+        } catch (IOException e) {
+            throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (Exception e) {
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (Exception e) {
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+
+        /* DB */
+        forwarding.delete();
+        forwardingRepository.save(forwarding);
+
+    }
+
+    /* 입력 DTO 검증 */
+    private void validateDTO(ForwardingDTO dto) {
+
+        for (ConstraintViolation<ForwardingDTO> violation : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) {
+            throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage());
+        }
+
+    }
+}
diff --git a/src/main/java/com/aolda/itda/service/log/LogService.java b/src/main/java/com/aolda/itda/service/log/LogService.java
new file mode 100644
index 0000000000000000000000000000000000000000..b33cc2e4a1e923ed046bea7aaff6ddc18c345c53
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/log/LogService.java
@@ -0,0 +1,57 @@
+package com.aolda.itda.service.log;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.log.LogDTO;
+import com.aolda.itda.entity.log.Log;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.log.LogRepository;
+import com.aolda.itda.repository.log.LogQueryDSL;
+import com.aolda.itda.service.AuthService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+@Slf4j
+public class LogService {
+
+    private final LogRepository logRepository;
+    private final LogQueryDSL logQueryDSL;
+    private final AuthService authService;
+
+    /* CUD 로그 조회 */
+    public LogDTO getLog(Long logId, List<String> projects) {
+        Log log = logRepository.findById(logId)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_LOG));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, log.getProjectId());
+
+        return log.toLogDTO();
+    }
+
+    /* CUD 로그 목록 조회 */
+    public PageResp<LogDTO> getLogs(String projectId, String type, String username, String action, Boolean isASC,
+                                    Pageable pageable, Map<String, String> user) {
+
+        if (projectId == null) {
+            try {
+                if(!authService.isAdmin(user)) throw new CustomException(ErrorCode.UNAUTHORIZED_USER);
+            }
+            catch (Exception e) {
+                throw new CustomException(ErrorCode.UNAUTHORIZED_USER);
+            }
+        }
+
+        return logQueryDSL.getLogs(projectId, type, username, action, isASC, pageable);
+    }
+
+}
diff --git a/src/main/java/com/aolda/itda/service/main/MainService.java b/src/main/java/com/aolda/itda/service/main/MainService.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a4345eb4ab48685fbd7e80e5b6900bbd433bffa
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/main/MainService.java
@@ -0,0 +1,61 @@
+package com.aolda.itda.service.main;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.auth.IdAndNameDTO;
+import com.aolda.itda.dto.main.MainInfoDTO;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.repository.forwarding.ForwardingRepository;
+import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.service.AuthService;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class MainService {
+
+    private final AuthService authService;
+    private final RoutingRepository routingRepository;
+    private final ForwardingRepository forwardingRepository;
+    private final CertificateRepository certificateRepository;
+
+    /* 메인 페이지에 필요한 정보 반환 */
+    public MainInfoDTO getMainInfo(String projectId, List<String> projects) {
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, projectId);
+
+        /* 카운팅 */
+        Long routing = (long) routingRepository.findByProjectIdAndIsDeleted(projectId, false).size();
+        Long forwarding = (long) forwardingRepository.findByProjectIdAndIsDeleted(projectId, false).size();
+        Long certificate = 0L;
+
+        return MainInfoDTO.builder()
+                .routing(routing)
+                .forwarding(forwarding)
+                .certificate(certificate)
+                .build();
+    }
+
+    /* 접근 가능한 프로젝트 조회 */
+    public PageResp<IdAndNameDTO> getAllProjects(Map<String, String> user) throws JsonProcessingException {
+
+        List<IdAndNameDTO> projects;
+        if (authService.isAdmin(user)) {
+            projects = authService.getAllProjects(user.get("token"));
+        }
+
+        else {
+            projects = authService.getProjectsWithUser(user);
+        }
+
+        return PageResp.<IdAndNameDTO>builder()
+                .contents(projects).build();
+    }
+}
diff --git a/src/main/java/com/aolda/itda/service/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a50622ae8ec705c49df00623796dc07d2184eb0
--- /dev/null
+++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java
@@ -0,0 +1,325 @@
+package com.aolda.itda.service.routing;
+
+import com.aolda.itda.dto.PageResp;
+import com.aolda.itda.dto.routing.RoutingDTO;
+import com.aolda.itda.entity.certificate.Certificate;
+import com.aolda.itda.entity.routing.Routing;
+import com.aolda.itda.exception.CustomException;
+import com.aolda.itda.exception.ErrorCode;
+import com.aolda.itda.repository.certificate.CertificateRepository;
+import com.aolda.itda.repository.routing.RoutingRepository;
+import com.aolda.itda.service.AuthService;
+import com.aolda.itda.template.RoutingTemplate;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.HttpServerErrorException;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.regex.Pattern;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+@Slf4j
+public class RoutingService {
+
+    private final RoutingRepository routingRepository;
+    private final CertificateRepository certificateRepository;
+    private final AuthService authService;
+    private final RoutingTemplate routingTemplate;
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    /* Routing 조회 */
+    public RoutingDTO getRouting(Long routingId, List<String> projects) {
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, routing.getProjectId());
+
+        return routing.toRoutingDTO();
+    }
+
+    /* Routing 목록 조회 + 검색 */
+    public PageResp<RoutingDTO> getRoutingsWithSearch(String projectId, String query) {
+
+        /* 입력 검증 */
+        if (query == null || query.isBlank()) {
+            return PageResp.<RoutingDTO>builder()
+                    .contents(routingRepository.findByProjectIdAndIsDeleted(projectId, false)
+                            .stream()
+                            .map(Routing::toRoutingDTO)
+                            .toList()).build();
+        }
+
+        /* 도메인 패턴 검증 */
+        String domainPattern = "^(\\*\\.)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$";
+        if (Pattern.matches(domainPattern, query) && query.startsWith("*.")) {
+            query = query.substring(2);
+        }
+
+        return PageResp.<RoutingDTO>builder()
+                .contents(routingRepository.findWithSearch(projectId, query, false)
+                        .stream()
+                        .map(Routing::toRoutingDTO)
+                        .toList()).build();
+    }
+
+    /* Routing 생성 */
+    public RoutingDTO createRouting(String projectId, RoutingDTO dto) throws IOException {
+        /* 입력 DTO 검증 */
+        validateDTO(dto);
+
+        /* 중복 검증 */
+        if (routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME);
+        }
+
+        /* SSL 인증서 조회 */
+        Certificate certificate = dto.getCertificateId() == -1 ? null :
+                certificateRepository.findById(dto.getCertificateId())
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요
+
+        /* 라우팅 엔티티 생성 */
+        Routing routing = Routing.builder()
+                .isDeleted(false)
+                .projectId(projectId)
+                .name(dto.getName())
+                .instanceIp(dto.getIp())
+                .instancePort(dto.getPort())
+                .domain(dto.getDomain())
+                .certificate(certificate)
+                .caching(dto.getCaching())
+                .build();
+
+        routingRepository.save(routing);
+
+        /* nginx conf 파일 생성 및 예외 처리 */
+        String content = routingTemplate.getRouting(dto, certificate == null ? null : certificate.formatDomain());
+        String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
+
+        File file = new File(confPath);
+        try {
+            Path path = Paths.get(confPath);
+            Files.createDirectories(path.getParent());
+            if (!file.createNewFile()) {
+                throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 라우팅 Conf 파일이 존재합니다");
+            }
+        } catch (IOException e) {
+            throw new CustomException(ErrorCode.FAIL_CREATE_CONF);
+        }
+
+        /* conf 파일 작성 및 예외 처리 */
+        try {
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, false)); // 예외처리 필요
+            bw.write(content);
+            bw.flush();
+            bw.close();
+        } catch (Exception e) {
+            if (!file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
+            }
+            throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다");
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+
+        } catch (HttpClientErrorException | HttpServerErrorException e) {
+            String responseBody = e.getResponseBodyAsString();
+            System.err.println("Response Body: " + responseBody);
+
+            Path filePath = Paths.get(confPath);
+            List<String> lines = Files.readAllLines(filePath);
+
+            // 파일 내용 출력
+            for (String line : lines) {
+                System.out.println(line);
+            }
+
+            if (!file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (Exception e) {
+            if (!file.delete()) {
+                throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD, "(롤백 실패)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+
+        return routing.toRoutingDTO();
+    }
+
+    /* Routing 수정 */
+    public void editRouting(Long routingId, RoutingDTO dto, List<String> projects) throws IOException {
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, routing.getProjectId());
+
+        /* 중복 검증 */
+        if (dto.getDomain() != null && routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) {
+            throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME);
+        }
+
+        /* SSL 인증서 조회 */
+        Certificate certificate;
+        if (dto.getCertificateId() == null) {
+            certificate = routing.getCertificate();
+        }
+        else if (dto.getCertificateId() == -1) {
+            certificate = null;
+        } else {
+            certificate = certificateRepository.findById(dto.getCertificateId())
+                    .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요
+        }
+
+        /* 파일 수정 */
+        routing.edit(dto, certificate);
+        RoutingDTO tmp = routing.toRoutingDTO();
+        if (tmp.getCertificateId() == null) tmp.setCertificateId( (long) -1);
+        String content = routingTemplate.getRouting(tmp, certificate == null ? null : certificate.formatDomain());
+        String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
+        File file = new File(confPath);
+        if (!file.exists()) {
+            throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 수정할 수 없습니다");
+        }
+
+        Path backup;
+        try {
+            backup = Files.createTempFile("temp_", ".tmp");
+            Files.copy(Paths.get(confPath), backup, StandardCopyOption.REPLACE_EXISTING
+                    , StandardCopyOption.COPY_ATTRIBUTES);
+
+            BufferedWriter bw = new BufferedWriter(new FileWriter(file, false));
+            bw.write(content);
+            bw.flush();
+            bw.close();
+        } catch (Exception e) {
+            throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "라우팅 Conf 파일을 수정하지 못했습니다");
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (HttpClientErrorException | HttpServerErrorException e) {
+            String responseBody = e.getResponseBodyAsString();
+            System.err.println("Response Body: " + responseBody);
+
+            Path filePath = Paths.get(confPath);
+            List<String> lines = Files.readAllLines(filePath);
+
+            // 파일 내용 출력
+            for (String line : lines) {
+                System.out.println(line);
+            }
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (RuntimeException e) {
+            try {
+                Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING
+                        , StandardCopyOption.COPY_ATTRIBUTES);
+                Files.delete(backup);
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+
+        /* DB 정보 수정 */
+        routingRepository.save(routing);
+    }
+
+    /* Routing 삭제 */
+    public void deleteRouting(Long routingId, List<String> projects) {
+        Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false)
+                .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING));
+
+        /* 프로젝트 권한 검증 */
+        authService.validateProjectAuth(projects, routing.getProjectId());
+
+        /* 파일 삭제 */
+        String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf";
+        String deletePath = confPath + ".deleted";
+        try {
+            Files.move(Paths.get(confPath), Paths.get(deletePath));
+        } catch (IOException e) {
+            throw new CustomException(ErrorCode.FAIL_DELETE_CONF);
+        }
+
+        /* nginx test */
+        String url = "http://nginx:8081/nginx-api/test";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (Exception e) {
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST);
+        }
+
+        /* nginx reload */
+        url = "http://nginx:8081/nginx-api/reload";
+        try {
+            restTemplate.getForEntity(url, String.class);
+        } catch (Exception e) {
+            try {
+                Files.move(Paths.get(deletePath), Paths.get(confPath));
+            } catch (IOException e1) {
+                throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)");
+            }
+            throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD);
+        }
+
+        /* DB */
+        routing.delete();
+        routingRepository.save(routing);
+    }
+
+    private void validateDTO(RoutingDTO dto) {
+
+        for (ConstraintViolation<RoutingDTO> violation : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) {
+            throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage());
+        }
+
+    }
+}
diff --git a/src/main/java/com/aolda/itda/template/ForwardingTemplate.java b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java
new file mode 100644
index 0000000000000000000000000000000000000000..6d8bf2cedcba6f6122cb7b93b80b95078745554d
--- /dev/null
+++ b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java
@@ -0,0 +1,16 @@
+package com.aolda.itda.template;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class ForwardingTemplate {
+
+    public String getPortForwardingWithTCP(String serverPort, String instanceIp, String instancePort, String name) {
+        return  "# " + name + "\n" +
+                "server { \n" +
+                " listen " + serverPort + "; \n" +
+                " listen [::]:" + serverPort + "; \n" +
+                " proxy_pass " + instanceIp + ":" + instancePort + ";\n" +
+                "} \n";
+    }
+}
diff --git a/src/main/java/com/aolda/itda/template/OptionTemplate.java b/src/main/java/com/aolda/itda/template/OptionTemplate.java
new file mode 100644
index 0000000000000000000000000000000000000000..add4dfcceee3a419aad336c369ec434f47a8b8e7
--- /dev/null
+++ b/src/main/java/com/aolda/itda/template/OptionTemplate.java
@@ -0,0 +1,26 @@
+package com.aolda.itda.template;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class OptionTemplate {
+
+    public String getSSL(String certificateDomain) {
+        return "\ninclude conf.d/include/letsencrypt-acme-challenge.conf;\n" +
+                "include conf.d/include/ssl-ciphers.conf;\n" +
+                "ssl_certificate /data/lego/certificates/" + certificateDomain + ".crt;\n" +
+                "ssl_certificate_key /data/lego/certificates/" + certificateDomain + ".key;\n";
+    }
+
+    public String getAssetCaching() {
+        return "\ninclude conf.d/include/assets.conf;\n";
+    }
+
+    public String getBlockExploits() {
+        return "\ninclude conf.d/include/block-exploits.conf;\n";
+    }
+
+    public String getForceSSL() {
+        return "\ninclude conf.d/include/force-ssl.conf;\n";
+    }
+}
diff --git a/src/main/java/com/aolda/itda/template/RoutingTemplate.java b/src/main/java/com/aolda/itda/template/RoutingTemplate.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a53785b704298cf74767176635369806265bf73
--- /dev/null
+++ b/src/main/java/com/aolda/itda/template/RoutingTemplate.java
@@ -0,0 +1,44 @@
+package com.aolda.itda.template;
+
+import com.aolda.itda.dto.routing.RoutingDTO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class RoutingTemplate {
+
+    private final OptionTemplate optionTemplate;
+
+    public String getRouting(RoutingDTO dto, String certificateDomain) {
+        return "server { \n"
+                + "set $forward_scheme http;\n"
+                + "set $server \"" + dto.getIp() +"\";\n"
+                + "set $port "+ dto.getPort() +";\n"
+                + "\n"
+                + "listen 80;\n"
+                + "listen [::]:80;\n"
+                + (dto.getCertificateId() == -1 ? "" :
+                "listen 443 ssl;\nlisten [::]:443 ssl;\n")
+                + "server_name " + dto.getDomain() + ";\n"
+                + (dto.getCertificateId() == -1 ? "" :
+                optionTemplate.getSSL(certificateDomain))
+                + (!dto.getCaching() ? "" :
+                optionTemplate.getAssetCaching()
+        )
+                + "proxy_set_header Upgrade $http_upgrade;\n"
+                + "proxy_set_header Connection $http_connection;\n"
+                + "proxy_http_version 1.1;\n"
+                + "\n"
+                + "access_log /data/logs/proxy-host-" + dto.getId() + "_access.log proxy;\n"
+                + "error_log /data/logs/proxy-host-" + dto.getId() + "_error.log warn;\n"
+                + "location / { \n"
+                + "proxy_set_header Upgrade $http_upgrade;\n"
+                + "proxy_set_header Connection $http_connection;\n"
+                + "proxy_http_version 1.1;\n"
+                + "include conf.d/include/proxy.conf;\n"
+                + "}\n"
+                + "}\n";
+    }
+
+}
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
new file mode 100644
index 0000000000000000000000000000000000000000..39e9337fdac44b059f0a540fc6764747ed91d193
--- /dev/null
+++ b/src/main/resources/logback-spring.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+
+    <property name="MAX_FILE_SIZE" value="10MB" />
+    <property name="TOTAL_SIZE" value="1GB" />
+    <property name="MAX_HISTORY" value="30" />
+
+    <!-- 콘솔에 출력할 로그 형식 -->
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <!-- INFO 로그 파일 저장 (1개당 10MB, 5개까지 유지, 이후 압축) -->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/data/logs/info.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>/data/logs/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+        <!-- INFO 레벨만 허용 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>INFO</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- WARN 로그 파일 저장 -->
+    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/data/logs/warn.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>/data/logs/warn.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+        <!-- WARN 레벨만 허용 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>WARN</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- ERROR 로그 파일 저장 -->
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>/data/logs/error.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>/data/logs/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
+            <maxHistory>${MAX_HISTORY}</maxHistory>
+            <totalSizeCap>${TOTAL_SIZE}</totalSizeCap>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+        <!-- ERROR 레벨만 허용 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <logger name="com.aolda.itda" additivity="false">
+        <!-- 각 Appender 참조 (필터는 Appender 내부에 정의됨) -->
+        <appender-ref ref="INFO_FILE"/>
+        <appender-ref ref="WARN_FILE"/>
+        <appender-ref ref="ERROR_FILE"/>
+        <!-- 콘솔 출력 -->
+        <appender-ref ref="CONSOLE"/>
+    </logger>
+
+</configuration>
\ No newline at end of file