HyunJun 기술 블로그

스프링 프레임워크 Logback Log -> File, DB 저장하기 본문

Spring Framework

스프링 프레임워크 Logback Log -> File, DB 저장하기

공부 좋아 2023. 5. 16. 20:55
728x90
반응형

1. 로그 저장의 필요성?


로그를 분석 및 관리하고 시간이 지나더라도 해당 시간대의 로그를 확인하고 싶다면 로그를 저장할 필요성이 있다.


2. 로그 저장 플랫폼

이 전까지 작성했던 로그 관련 글의 순서대로 기술해 보자면, Consol Log -> Slack, Discord, Telegram Log 전송의 순서대로 발전해 왔다. 하지만 직접 로그를 자동으로 Messenger로 보내는 로직을 구현해 본 사람이라면 아래와 같은 불편한 점을 느꼈을 것이라고 생각한다.

 

  • 슬랙, 디스코드, 텔레그램 등으로 로그를 전송하는 것도 좋지만 해당 기능은 기본적으로 특정 인원만 메시지를 받을 수 있고,
    아무래도 모바일로도 알람이 가는 만큼 무분별하게 전송하게 된다면.... 알람이 매우 울릴 것이므로 Warn, Error 등 정말 중요한 정보들 위주로만 받게 된다.
  • 또한, 해당 플랫폼에 전송된 메시지는 해당 플랫폼에 종속되어 있기에 해당 플랫폼에서 직접 삭제해야 한다.
  • 로그의 포맷이 일정하고, 해당 로그들을 프로그래밍으로 분석하려 할 때 슬랙, 디스코드, 텔레그램으로 전송하였다면 해당 채팅 기록들을 불러오는 기능이 있을지도 모를뿐더러, 3개의 플랫폼에 대한 설계 및 코딩을 해야 한다.

 

3. File과 Database

그렇다면 이러한 문제점을 해결하려면 어떻게 해야 할까? File 혹은 Database로 설계 및 구현하면 된다.

 

File로 저장하게 되면 자연스럽게 알람과 용량의 압박에서 벗어나 info 레벨같이 많은 로그를 저장할 수 있고, 일상적인 로그를 많이 저장하는 만큼 해당 로그들을 분석해 사용자 분석이 가능해지고 좋은 방향으로 프로그램을 업데이트할 수 있다. 또한 로그를 중요한 자산으로서 저장하고 공유해야 한다면 Database에 저장하면 된다. 또한 Spring batch, scheduler를 사용하여 일정 기간이 지난 로그는 자동으로 삭제하는 로직을 구현할 수 있다.

 

이러한 방식들의 장단점을 보면 알 수 있듯이, File은 서버에서 일어나는 모든 로그, Database는 모든 로그까지는 아니지만 공유할 필요성이 있는 어느 정도 중요한 레벨 이상의 로그, 모바일 메신저로서의 전송은 서버에서 일어난 매우 위험한 로그(비정상적 접근 등)로 사용하면 되지 않을까?

 

4. 구현하기

4-1. File

build.gradle

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

Logback을 직접 추가해 주어도 되지만 어차피 lombok은 반 필수이므로 lombok으로 사용했습니다. lombok 안에 Logback이 들어있습니다.

 

 

 

properties

logging.config=classpath:logback-spring.xml

 

 

resources/logback-spring.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>Info</level>
        </filter>
        <encoder>
            <pattern>%d %-5level %logger{35} - %msg%n</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>./log/local/%d{yyyy-MM-dd}/server.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
    </appender>


    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>%d %-5level %logger{35} - %msg%n</Pattern>
        </encoder>
    </appender>


    <root level="Info">
        <appender-ref ref="Console"/>
        <appender-ref ref="file"/>
    </root>

</configuration>

 

maxHistory: 최대 보관 주기

totalSizeCap: 파일의 크기

 

 

 

이 정도까지만 하고 프로그램을 실행시켜도 바로 로그가 저장됩니다.

 

 

 

server.log.txt
Consol Log

 

쉽죠? 파일 저장은 xml 파일을 이해하고, 파일 패턴 및 보관 주기 설정 등만 이해하면 Logback 자체가 구현이 잘 되어있어서 어렵지 않습니다. 사실상 로그 설정만 해주면 된다고 보시면 됩니다.

 

 

 

 

4-2. Database

AWS의 RDS로 MySQL 서버를 생성했고 DBeaver라는 데이터베이스 관리 도구 툴로 DB 서버에 접속하여 테스트용 데이터베이스를 만들어 볼게요.

 

 

 

 

 

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

jpa, mysql 관련 의존성을 추가했습니다.

 

 

 

 

properties: DB 관련 Datasource 정보를 넣어줬습니다.

spring.datasource.url=jdbc:mysql://mydbdbdbdbdbdb:3306/log-test
spring.datasource.username=admin
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=create

logging.config=classpath:logback-spring.xml

처음 실행 시 ddl-auto=create 옵션을 넣어주어야 테이블이 자동으로 생성됩니다.

 

 

 

resources/logback-spring.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>

    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>Info</level>
        </filter>
        <encoder>
            <pattern>%d %-5level %logger{35} - %msg%n</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>./log/local/%d{yyyy-MM-dd}/server.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <appender name="MYSQL_DB" class="com.example.logbackfiledatabase.MySqlAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>Error</level>
        </filter>
    </appender>


    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>%d %-5level %logger{35} - %msg%n</Pattern>
        </encoder>
    </appender>


    <root level="Info">
        <appender-ref ref="Console"/>
        <appender-ref ref="file"/>
        <appender-ref ref="MYSQL_DB" />
    </root>

</configuration>

 

MYSQL_DB가 추가되었으며, Error Log가 발생했을 때  MySqlAppender를 사용하여 DB에 저장하는 방식입니다.

아무래도 실제 서비스 중에 많은 로그가 데이터베이스로 전송된다면 부하를 많이 받겠죠?

그래서, Error 로그만 저장을 하고 Error 로그들만 의도적으로 생성해 보려고 합니다.

 

 

 

 

 

 

 

Log 발생 시 적용되는 Appender입니다.

package com.example.logbackfiledatabase;


import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Slf4j
@Getter
@Setter
@Component
public class MySqlAppender extends UnsynchronizedAppenderBase<ILoggingEvent> implements ApplicationContextAware {

    private static LogRepository logRepository;


    @Override
    protected void append(ILoggingEvent eventObject) {
        Log entity = new Log(eventObject.getFormattedMessage());

        logRepository.save(entity);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (applicationContext.getAutowireCapableBeanFactory().getBean(LogRepository.class) != null) {
            logRepository = (LogRepository) applicationContext.getAutowireCapableBeanFactory().getBean(LogRepository.class);
        }
    }
}

 

 

 

 

 

 

 

Repository입니다.

package com.example.logbackfiledatabase;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface LogRepository extends JpaRepository<Log, Long> {
}

 

 

 

 

Log 테이블을 표현해 줄 Entity입니다.

package com.example.logbackfiledatabase;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Getter
@NoArgsConstructor
public class Log extends Timestamped{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /*로그*/
    @Column
    private String log;


    public Log(String log) {
        this.log = log;
    }
}

 

 

 

 

 

 

Timestamped.java

package com.example.logbackfiledatabase;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column
    private LocalDateTime modifiedAt;

}

 

 

 

 

시간 관련 엔티티 자동 적용을 위해 @EnableJpaAuditing를 적용해 주세요.

 

 

마지막으로 테스트용 컨트롤러입니다.

package com.example.logbackfiledatabase;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class TestController {


    @RequestMapping("test")
    public void errLogTest() {
        log.error("에러 로그1");
        log.error("에러 로그2");
        log.error("에러 로그3");
        log.error("에러 로그4");
        log.error("에러 로그5");

    }
}

 

 

 

 

프로젝트 실행 후 테이블이 생성이 되었나 확인을 한 뒤에,

 

 

localhost:8080/test로 연결 시, 의도한 것과 같이 결과가 잘 나왔습니다.

 

console
file
database

728x90
반응형
Comments