diff --git a/src/main/java/com/aolda/itda/config/LoggingFilter.java b/src/main/java/com/aolda/itda/config/LoggingFilter.java index 49264ec5197d646154309cb91c33ce7cf618267e..f2330bf6f3a4e77dabcc9c2e8bc54ef8a4b42e90 100644 --- a/src/main/java/com/aolda/itda/config/LoggingFilter.java +++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java @@ -1,48 +1,48 @@ -package com.aolda.itda.config; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -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; - -@Component -public class LoggingFilter extends OncePerRequestFilter { - - private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); - - @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) { - String ip = request.getRemoteAddr(); - String method = request.getMethod(); - String uri = request.getRequestURI(); - String queryString = request.getQueryString(); - String body = getRequestBody(request); - - logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, Request Body: {}", - ip, method, uri, (queryString != null ? queryString : "None"), - (!body.isEmpty() ? body : "None")); - } - - private String getRequestBody(ContentCachingRequestWrapper request) { - byte[] buf = request.getContentAsByteArray(); - return (buf.length > 0) ? new String(buf) : ""; - } -} +package com.aolda.itda.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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; + +@Component +public class LoggingFilter extends OncePerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); + + @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) { + String ip = request.getRemoteAddr(); + String method = request.getMethod(); + String uri = request.getRequestURI(); + String queryString = request.getQueryString(); + String body = getRequestBody(request); + + logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, Request Body: {}", + ip, method, uri, (queryString != null ? queryString : "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/controller/main/MainController.java b/src/main/java/com/aolda/itda/controller/main/MainController.java index 7b22923994abcc4d5d43e77b21f7bf30f466bb0d..371cce5c41e82deb7dbe8bf10ac0c618d52e5a74 100644 --- a/src/main/java/com/aolda/itda/controller/main/MainController.java +++ b/src/main/java/com/aolda/itda/controller/main/MainController.java @@ -1,33 +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"))); - } - -} +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/dto/main/MainInfoDTO.java b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java index 6de5fdfbdcf5f79a2e68a02fe39e2a4481327a70..cbcdcd583831431a778791d15c8551ac834d5fe5 100644 --- a/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java +++ b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java @@ -1,20 +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; - -} +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/entity/certificate/Challenge.java b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java index 14713bffb86a0eb1a8d67162420bad39d6837b96..41f607e0289f9533cf6bc18c106e9d6037f1abed 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Challenge.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java @@ -1,5 +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 + HTTP, DNS_CLOUDFLARE; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } } diff --git a/src/main/java/com/aolda/itda/entity/log/Action.java b/src/main/java/com/aolda/itda/entity/log/Action.java index a620f87a62fbb79faaf169690041b21c94c404f1..448a65e6e29e793da43011c1f40fd64b01659dae 100644 --- a/src/main/java/com/aolda/itda/entity/log/Action.java +++ b/src/main/java/com/aolda/itda/entity/log/Action.java @@ -1,5 +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 + CREATE, UPDATE, DELETE; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } } diff --git a/src/main/java/com/aolda/itda/entity/log/ObjectType.java b/src/main/java/com/aolda/itda/entity/log/ObjectType.java index 531031551afbfc5af9770dfc66cec886210715f4..c91f27911dfc4c661be7ab343b03c2391ddb1a56 100644 --- a/src/main/java/com/aolda/itda/entity/log/ObjectType.java +++ b/src/main/java/com/aolda/itda/entity/log/ObjectType.java @@ -1,5 +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 + ROUTING, CERTIFICATE, FORWARDING; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } } diff --git a/src/main/java/com/aolda/itda/service/main/MainService.java b/src/main/java/com/aolda/itda/service/main/MainService.java index 2c36ac84ff2e02f7812037198abf4719110c6cae..8a4345eb4ab48685fbd7e80e5b6900bbd433bffa 100644 --- a/src/main/java/com/aolda/itda/service/main/MainService.java +++ b/src/main/java/com/aolda/itda/service/main/MainService.java @@ -1,61 +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(); - } -} +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/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index a8acbeebcfc82e73b691923bbf927ec007d9648e..39e9337fdac44b059f0a540fc6764747ed91d193 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -1,90 +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> - +<?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