안녕하세요 코이킹입니다.
이번 포스트는 웹 어플리케이션에 필수적인 기능 설정 몇 가지에 대한 설명과 스프링 부트에서 Log4j를 사용할 수 있게 설정하는 법에 대한 내용입니다.
※ 거창하게 필수라고 쓰고는 있지만, 이전 번외 편에서도 말씀드렸듯이 어디까지나 저의 생각을 적고 있으므로 참고만 해주시면 될 것 같습니다.
1. 목표
- 웹 어플리케이션의 CRUD기능 이외의 기본적인 필수 설정에 대해서 알기.
- 스프링 부트에서 Log4j를 사용할 수 있게 설정할 수 있다.
2. 내가 생각하는 CRUD기능이외의 기본적인 필수 설정이란 무엇인가?
일단 결론부터 말쓰드리자면, 아래의 3개 설정은 웹 애플리케이션을 구현할 때 필수로 해야 한다고 생각합니다.
1. 로그설정
2. 유효성 검사
3. 예외처리
글 목록에서부터 글 삭제까지 브라우저로부터의 요청을 컨트롤러에서 받아서 서비스에서 비즈니스 로직을 처리하고, 리포지토리를 통해 DB의 데이터를 조작하여 유저에 데 응답을 보내기까지의 흐름을 도식화해보면 아래와 같습니다.
간단한 CRUD기능을 구현하는 연습에서는 위와 같은 흐름이어도 문제 될 건 없다고 생각합니다.
연습할 때는 '메인 주제를 정하여 정상 동작하는 패턴 하나를 구현'하는 데 집중하게 되니까요.
하지만 실무에서는 정상 동작하는 패턴만을 구현할 수 없습니다.
왜냐하면 '제약사항(사양)'과 예기치 못한 '사고의 위험'이 존재하기 때문입니다.
예를 들자면 ① 유저가 원래 예상했던 파라미터의 길이보다 더 긴 데이터를 입력하여 서버 애플리케이션에 요청을 보낸다 던 지, ② 게시글 데이터를 갱신하던 중 DB와의 연결 시간을 초과하여 시스템 에러가 발생하는 상황이 있을 수 있습니다.
예시 ①의 경우 예상한 사양을 벗어난 동작, ②의 경우 예기치 못한 사고에 해당하며,
실무에서는 위의 예시와 같은 다양한 이슈가 언제든 발생할 수 있습니다.
운영 업무를 담당하게 되면 애플리케이션의 이 언제든 발생할 수 있는 여러 이슈를 조사하고 해결을 해야 하는데, 조사의 시작점이 바로 로그입니다.
로그란애플리케이션이 동작할 때 어떤 값을 사용했고, 언제 동작했고, 어느 순서로 동작했는지, 일련의 처리 중 어느 부분에서 실패했는지 등 다양한 정보를 기록한 것을 말합니다.
로그가 없다면 이슈의 원인을 특정하기가 매우 어려우므로, 애플리케이션을 구현한다면 반드시 로그를 출력할 수 있게 설정해 주어야 합니다.
예상한 사양을 벗어난 동작은 주로 잘못된 값의 입력에서 시작되곤 합니다.
예상외의 잘못된 값이 입력되었을 때, 처리가 실패하여 에러 로그가 출력될 때까지 로직 전부를 실행하는 것을 예방해 주는 것이 바로 유효성 검사입니다.
유효성 검사는 화면에서 자바 스크립트 코드에 의해 1차적으로 실시되는 경우가 대부분이지만, 서버에서도 반드시 실시해주어야 합니다.
저의 경우 실무에서는 컨트롤러에서 요청 바디와 헤더, 그리고 파라미터에 대해 유효성 검사를 실시하며, DB의 데이터를 컨트롤할 때 사용하는 값에도 유효성 검사를 반드시 실시하고 있습니다.
스프링 부트로 구현된 애플리케이션에서 예기치 못한 에러가 발생할 경우 위와 같은 에러 페이지를 보실 수 있습니다.
개발할 때에는 이렇게 에러 메시지가 잔뜩 나와주는 게 감사한 일인데, 상용 환경의 어플리케이션에서 예기치 못한 에러가 발생하여 위와 같은 화면이 출력되면 유저에게 불쾌감을 줄 뿐만 아니라 보안적으로 문제 발생할 수 있습니다.
위와 같은 에러가 발생했을 때 유저에게 필요한 정보만을 전해줄 수 있도록 에러를 컨트롤하는 것이 바로 예외처리입니다.
앞서 설명한 3개의 필수 설정에 대해서 필요성을 느끼셨다면 단순한 CRUD흐름에서 더 나아가 3개 필수 설정을 더한 흐름대로 어플리케이션을 구현하는 것을 추천드립니다.
아래의 캡처는 3개 필수 설정을 더한 흐름을 도식화한 것입니다.
3. Log4j설정
1) 의존성 라이브러리 설정 : /template-springboot/build.gradle
plugins {
id 'org.springframework.boot' version '2.6.8'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.sb.template'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
all*.exclude module : 'spring-boot-starter-logging'
}
repositories {
mavenCentral()
}
dependencies {
// For develop
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// Web
implementation 'org.springframework.boot:spring-boot-starter-web'
// thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// DB
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java'
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Log4j
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
}
tasks.named('test') {
useJUnitPlatform()
}
- 11~17행 : 스프링 부트의 기본 로그 설정을 사용하지 않게 하는 설정입니다.
- 45행 : Log4j를 사용하기 위해 라이브러리를 추가하는 설정입니다.
2) Log4j설정 Xml파일 : /template-springboot/src/main/resources/log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO" monitorInterval="30">
<Properties>
<Property name="LOG_FORMAT">%d{yyyy-MM-dd HH:mm:ss} [%t] %-5level %logger{36} - %msg%n</Property>
<Property name="BASE_DIR">./logs</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${LOG_FORMAT}"/>
</Console>
<RollingFile name="File"
fileName="${BASE_DIR}/template-springboot.log"
filePattern="${BASE_DIR}/template-springboot_%d{yyyyMMdd}.log">
<PatternLayout pattern="${LOG_FORMAT}"/>
<Policies>
<TimeBasedTriggeringPolicy />
</Policies>
<DefaultRolloverStrategy>
<Delete basePath="${BASE_DIR}">
<IfFileName glob="*.log" />
<IfLastModified age="30d" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File" />
</Root>
<!--
<Root level="debug">
<AppenderRef ref="Console"/>
<AppenderRef ref="File" />
</Root>
-->
</Loggers>
</Configuration>
- 4행 : 로그의 출력 포맷 설정
- 5행 : 로그 출력 디렉터리 설정
- 12~14행 : 로그 파일명 설정
- 19행 : 로그파일 삭제 기한 설정
- 28행 : 로그래벨설정
3) 컨트롤러 : /template-springboot/src/main/java/com/sb/template/controller/BoardController.java
package com.sb.template.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.sb.template.entity.Board;
import com.sb.template.enums.BoardType;
import com.sb.template.forms.BoardForm;
import com.sb.template.service.BoardService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Controller
@RequestMapping(path = "/board")
public class BoardController {
@Autowired
private BoardService boardService;
@RequestMapping(method = RequestMethod.GET, path = "list")
public String viewBoardList(Model model) {
log.info("Start API");
List<Board> boardList = boardService.getAllBoard();
model.addAttribute("boardList", boardList);
log.info("Response Data : {} ", boardList);
log.info("End API");
return "board/list";
}
@RequestMapping(method = RequestMethod.GET, path = "write")
public String writeBoard(Model model) {
model.addAttribute("boardTypes", BoardType.getBoardTypes());
return "board/write";
}
@RequestMapping(method = RequestMethod.POST, path = "write")
public String writeCompleteBoard(BoardForm form, Model model) {
Integer boardNo = null;
boardNo = boardService.createBoard(form.toEntity()).getBoardNo();
return "redirect:/board/list";
}
@RequestMapping(method = RequestMethod.GET, path = "read/{boardNo}")
public String viewBoardOne(@PathVariable int boardNo, Model model) {
model.addAttribute("board", boardService.getBoardOne(boardNo));
return "board/read";
}
@RequestMapping(method = RequestMethod.GET, path = "update/{boardNo}")
public String updateBoard(@PathVariable int boardNo, Model model) {
boardService.updateBoardForm(boardNo, model);
return "board/update";
}
@RequestMapping(method = RequestMethod.POST, path = "update/{boardNo}")
public String updateCompleteBoard(@PathVariable int boardNo, BoardForm form, Model model) {
boardService.updateBoard(boardNo, form.toEntity());
model.addAttribute("message", "Update Success");
return "redirect:/board/read/"+boardNo;
}
@RequestMapping(method = RequestMethod.GET, path = "delete/{boardNo}")
public String deleteBoard(@PathVariable int boardNo, Model model) {
model.addAttribute("board", boardService.getBoardOne(boardNo));
return "board/delete";
}
@RequestMapping(method = RequestMethod.POST, path = "delete")
public String deleteCompleteBoard(
@RequestParam(name = "boardNo", required = true) int boardNo,
Model model) {
boardService.deleteBoard(boardNo);
model.addAttribute("message", "Delete Success");
return "redirect:/board/list";
}
}
- 18행 : @Slf4j를 사용하기 위한 import
- 20행 : @Slf4j어노테이션을 사용, 이 어노테이션을 사용하면, 손쉽게 로그 출력이 가능하다.
- 30, 34, 36행 : 로그 출력. 로그 메시지 안에 {}를 넣고, 데이터를 매개변수로 넣어주면 데이터가 로그로 출력된다.
4. 전체 소스코드
https://github.com/leeyoungseung/template-springboot/tree/feature/setting_for_log4j2
웹 어플리케이션에 필수적인 기능 설정과 스프링 부트에서 Log4j 설정하는 법에 대한 포스트는 이것으로 마치겠습니다.
다음 포스트는 AOP를 적용하여 로그 출력에 대한 내용이 되겠습니다.
'프로그래밍 > Springboot-토이프로젝트' 카테고리의 다른 글
【게시판-번외05】유효성 검사 (0) | 2022.09.14 |
---|---|
【게시판-번외04】AOP를 적용한 로그출력 (0) | 2022.09.14 |
【게시판-06】글 삭제 (0) | 2022.09.13 |
【게시판-05】글 수정 (0) | 2022.09.11 |
【게시판-04】글상세보기 (0) | 2022.09.09 |
댓글