java.validation의 @Valid 어노테이션 사용법 정리 글입니다.

Spring Boot 라이브러리에서 기본적으로 탑재된 기능이며 따로 dependency해 줄 필요가 없습니다.

Spring Boot Version은 2.2.2.RELEASE 입니다.


1. java.validation 어노테이션 설명

Anotation 제약조건
@NotNull Null 불가
@Null Null만 입력 가능
@NotEmpty Null, 빈 문자열 불가
@NotBlank Null, 빈 문자열, 스페이스만 있는 문자열 불가
@Size(min=,max=) 문자열, 배열등의 크기가 만족하는가?
@Pattern(regex=) 정규식을 만족하는가?
@Max(숫자) 지정 값 이하인가?
@Min(숫자) 지정 값 이상인가
@Future 현재 보다 미래인가?
@Past 현재 보다 과거인가?
@Positive 양수만 가능
@PositiveOrZero 양수와 0만 가능
@Negative 음수만 가능
@NegativeOrZero 음수와 0만 가능
@Email 이메일 형식만 가능
@Digits(integer=, fraction = ) 대상 수가 지정된 정수와 소수 자리 수 보다 작은가?
@DecimalMax(value=)  지정된 값(실수) 이하인가?
@DecimalMin(value=) 지정된 값(실수) 이상인가?
@AssertFalse false 인가?
@AssertTrue true 인가?

2. Member.java

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

public class Member {

    @NotNull(message = "id는 필수 값입니다.")
    @Size(min = 5, max = 10)
    private String id;

    @Max(value = 25, message = "25세 이하만 가능합니다.")
    @Min(value = 18, message = "18살 이상만 가능합니다.")
    private int age;

    @Pattern(regexp = "[a-zA-z0-9]+@[a-zA-z]+[.]+[a-zA-z.]+")
    private String email;
    
    ..setter/getter 생략..
}

3. TestController.java

import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping(value = "/member")
public class TestController {
    
    @PostMapping
    @ResponseBody
    public ResponseEntity saveMember(@Valid Member member, BindingResult bindingResult) {
        
        if (bindingResult.hasErrors()) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(bindingResult.getAllErrors());
        }
        
        /*
        	save Memeber
        */
        
        return ResponseEntity.ok(member);
    }
}
  • [post] /member요청 시, member를 저장하는 코드입니다.
  • @Valid 어노테이션을 통해 Member가 유효한 객체인지 검사합니다.
  • Member 객체가 유효하지 않으면 bindingResult.hasErrors() 메소드에서 true 값이 반환됩니다.
  • Member가 유효하지 않다면, ResponseEntity에 BAD_REQUEST와 bindingResult.getAllErrors() 값을 넣고 생성하여 반환합니다.

4. 테스트

POST http://localhost:8080/{contextPath}/member?id=ki&age=17&email=bamdule@navercom

Reponse 400 Bad Request

thymeleaf template layout은 thymeleaf를 이용하여 공통 page를 fragment, layout형식으로 조립할 수 있는 template engine입니다.

thymeleaf template layout을 사용하는 간단한 예제 프로젝트를 진행해보도록 하겠습니다.


1. Maven dependency 및 프로젝트 구조

   1) pom.xml

        ...
   <version>2.2.2.RELEASE</version>
   <!-- Spring Boot Version -->
        ...
            
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
        <dependency>
            <groupId>nz.net.ultraq.thymeleaf</groupId>
            <artifactId>thymeleaf-layout-dialect</artifactId>
        </dependency>
    </dependencies>
    
        ...
        

 

   2) 프로젝트 구조


2. header.html (공통 fragment)

<html lagn="ko" 
      xmlns:th="http://www.thymeleaf.org">
       
    <!--headerFragment 선언-->
    <div th:fragment="headerFragment">
        <h1>HEADER</h1>
    </div>
    
</html>

3. footer.html (공통 fragment)

<html lagn="ko" 
      xmlns:th="http://www.thymeleaf.org">
      
    <!--footerFragment 선언-->
    <div th:fragment="footerFragment">
        <h1>FOOTER</h1>
    </div>
    
</html>

4. config.html (공통 fragment)

<html lagn="ko" 
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

    <!--configFragment 선언-->
    <th:block th:fragment="configFragment">

          
        <!-- 이 영역에 공통으로 사용할 css, js library를 선언한다. -->
        <link rel="stylesheet" th:href="@{/css/common/common.css}" >
        
        <script th:src="@{/js/common/common.js}"></script>

        <!-- Content Page의 CSS fragment 삽입 -->
        <th:block layout:fragment="css"></th:block>

        <!-- Content Page의 script fragment 삽입 -->
        <th:block layout:fragment="script"></th:block>
        
    </th:block>
</html>
  • config fragment에 공통으로 사용할 css, js를 선언하고 Content Page의 css, js를 선언합니다.
  • @{/js/common/common.js} 는 src/main/resources/static이 생략되어있습니다.

5. default_layout.html (Layout)

<!DOCTYPE html>

<html lagn="ko" 
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">

    <head>
        <meta charset="UTF-8" />
        <title>Bamdule</title>
        
        <!-- config fragment 사용 -->
        <th:block th:replace="fragment/config :: configFragment" ></th:block>
    </head>
    
    <body>
        <!-- header fragment 사용 -->
        <th:block th:replace="fragment/header :: headerFragment"></th:block>
        
        <!-- 
            content fragment 사용 
            현재 layout을 사용하는 content fragment의 내용을 삽입한다.
        -->
        <th:block layout:fragment="content"></th:block>
        
        <!-- footer fragment 사용 -->
        <th:block th:replace="fragment/footer :: footerFragment"> </th:block>
    </body>

</html>
  • Layout은 fragment들이 조합된 html입니다.
  • th:replace="frament경로 :: fragment이름" 속성은 해당 영역을 fragment로 치환하겠다는 의미입니다.
  • layout:fragment="content"는 해당 layout을 사용하는 content의 내용을 불러오겠다는 의미입니다.

6. home.html (Content Page - fragment )

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/my-layout}">

    <!-- index.html 고유 CSS 추가 -->
    <th:block layout:fragment="css">
        <link rel="stylesheet" th:href="@{/css/page/home.css}" >
    </th:block>
    
    <!-- index.html 고유 스크립트 추가 -->
    <th:block layout:fragment="script">
        <script th:src="@{/js/page/home.js}"></script>
    </th:block>

    <div layout:fragment="content">
        <h1>content</h1>
    </div>
</html>
  • 여기서 유심히 볼 코드는 layout:decorate="~{layouts/my-layout}" 입니다. 
  • layout:decorate="layout path" 속성을 입력해 layout을 사용할 수 있습니다.
  • layout:fragment="content" 속성을 통해 content fragment를 선언합니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class BamduleApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(BamduleApplication.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(BamduleApplication.class, args);
    }

}

Spring Boot 실행 클래스에서 SpringBootServletInitializer 클래스를 상속 받는 것을 볼 수 있습니다.

SpringBootServletInitializer 클래스를 왜 상속 받아야 할까요?

Spring개발 시  web.xml에 DispatcherServlet을 등록하는 작업이 필요했습니다.

하지만 Servlet 3.0으로 업데이트 되면서 web.xml이 없어도 배포가 가능해졌습니다.

그 이유는 web.xml 설정을 WebApplicationInitializer 인터페이스를 구현하여 대신 할 수 있고,
프로그래밍적으로 Spring IoC 컨테이너를 생성하여 ServletContext에 추가할 수 있도록 변경 되었기 때문입니다.

SpringBootServletInitializer 클래스는 WebApplicationInitializer 인터페이스의 구현체입니다. 

결과적으로 SpringBootServletInitializer 상속 한다는 것은 Spring Boot 애플리케이션 동작이 가능 하도록 웹 애플리케이션 컨텍스트(IoC 방식으로 Bean을 관리하는 컨테이너) 구성 한다는 의미입니다.

1. XXXApplication.java

스프링부트는 main 메소드가 선언된 클래스를 기준으로 실행됩니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BamduleApplication {

	public static void main(String[] args) {
		SpringApplication.run(BamduleApplication.class, args);
	}

}
@SpringBootApplication 어노테이션은 스프링 부트의 가장 기본적인 설정을 선언해 줍니다.
해당 어노테이션을 보면 아래와 같은 어노테이션이 다시 선언되어 있습니다.

2. @SpringBootApplication

@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
    @ComponentScan.Filter(type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}),
    @ComponentScan.Filter(type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class})})
public @interface SpringBootApplication {

    @AliasFor(annotation = EnableAutoConfiguration.class)
    public Class<?>[] exclude() default {};

    @AliasFor(annotation = EnableAutoConfiguration.class)
    public String[] excludeName() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    public String[] scanBasePackages() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
    public Class<?>[] scanBasePackageClasses() default {};

    @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
    public Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    @AliasFor(annotation = Configuration.class)
    public boolean proxyBeanMethods() default true;
}
여기서 눈여겨 볼 설정은 @ComponentScan@EnableAutoConfiguration 입니다. 

3. @ComponentScan

@ComponentScan@component 어노테이션 및 @Service, @Repository, @Controller 등의 어노테이션을 스캔하여 Bean으로 등록해주는 어노테이션입니다.


4. @EnableAutoConfiguration

@EnableAutoConfiguration은 사전에 정의한 라이브러리들을  Bean으로 등록해 주는 어노테이션입니다. 
사전에 정의한 라이브러리들 모두가 등록되는 것은 아니고 특정 Condition(조건)이 만족될 경우에 Bean으로 등록합니다.

사전 정의 파일 위치
Dependencies > spring-boot-autoconfigure > META-INF > spring.factories
# spring.factories 파일 내용

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
...
"org.springframework.boot.autoconfigure.EnableAutoConfiguration=" 에 등록된 클래스들이 자동으로 등록되는 Bean 입니다.  각 Bean은 OnBeanCondition, OnClassCondition, OnWebApplicationCondition 어노테이션의 조건에 의해 등록 여부가 결정됩니다.
@OnBeanCondition : 특정 Bean이 사전에 생성되어있지 않을 경우에 조건이 만족됩니다.
@ConditionalOnBean : 특정 Bean이 이미 생성되어있을 경우에 조건이 만족됩니다.
@ConditionalOnClass : Classpath에 특정 class가 존재할 경우에 조건이 만족됩니다.

1. Spring Boot란

Spring Boot는 Spring 프레임워크를 간편하게 설정하고, 별도에 어려운 설정 없이 바로 개발에 들어갈 수 있도록 만든 프레임 워크입니다.

강력한 기능을 가진 라이브러리들을 Spring Boot 라이브러리에 내장하였고, 내장형 톰캣 등을 탑재하여 단독 실행이 가능해졌습니다.

그리고 프로젝트 생성 시 기존 Spring은 복잡한 설정 파일을 요구했는데, Spring boot는 aplication.yml 설정 파일로 간단하게 설정이 가능하게 되었습니다,

2. 사용하는 라이브러리

...
<!--Spring Boot Version-->	
  <version>2.2.2.RELEASE</version>
...
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
  <scope>provided</scope>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

3. 프로젝트 생성

아래 주소에서 Spring Boot 프로젝트를 생성합니다.

https://start.spring.io/

src 폴더와 pom.xml을 제외한 나머지 파일은 사용하지 않으니 삭제해주세요.


4. application.yml 설정

application.yml 파일은 스프링의 설정 정보를 저장하는 파일입니다.

기본적으로 application.profiles파일이 저장되어 있습니다. yml, profiles 두가지 타입의 파일로 설정이 가능합니다.

이번에는 조금 더 보기좋고 설정하기 편한 yml 파일로 설정해보겠습니다. 

파일 경로는 src/main/resources/ 입니다.

spring:
    thymeleaf:
        prefix : classpath:templates/
        suffix : .html
        cache : false

우선 기본적인 thymeleaf 설정만 하였습니다.

html 파일의 위치와 확장자 명, 캐시 유무를 설정하였습니다.


5. HomeController 생성

@SpringBootApplication를 선언한 클래스 패키지의 하위 패키지에 HomeController를 생성합니다.

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping(value = "/")
public class HomeController {

    @GetMapping(value = "/")
    public ModelAndView home() {
        ModelAndView modelAndView = new ModelAndView();
        
        modelAndView.setViewName("home");
        
        Map<String, Object> map = new HashMap<>();
        map.put("name", "Bamdule");
        map.put("date", LocalDateTime.now());
        
        modelAndView.addObject("data", map);
        
        return modelAndView;
    }
}

modelAndView.setViewName("home"); 은 application.yml에서 설정한 "src/main/resources/" 값이 생략되어있고, 
확장자 .html 또한 생략되어있습니다. 이 값을 통해 ViewResolver는 view를 생성합니다.

그리고 modelAndView.addObject("data",map); 를 통해 화면에 데이터를 렌더링합니다.


6. home.html 생성

src/main/resources/templates/ 경로에 home.html을 생성해줍니다.

<!DOCTYPE html>
<html>
    <head>
        <title>title</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <div th:text="${data.name}"></div>
        <th:block th:text="${data.date}"/>
    </body>
</html>

"th:" 가 포함된 tag attribute는 thymeleaf 문법입니다.

data.name과 data.date 값을 화면에 뿌려줍니다.


7. 결과

위와 같이 하셨나면 톰캣을 실행해 줍니다. 

build하면 target 폴더에 war 파일이 생성되고, war파일을 설치한 톰캣 또는 IDE툴을 통해서 실행하면 다음과 같은 화면이 보여집니다.


도움이 되셨다면 공감 한번씩 눌러주시면 감사하겠습니다.

Logback이란 SLF4J 인터페이스를 구현한 클래스이며, 특정 로그를 콘솔, 파일, DB 등에 남길 수 있습니다.

 

...
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
...

spring-boot-starter-web 라이브러리에 logback-classic, logback-core가 탑재되어있어서 따로 dependency 하지 않아도 됩니다.

이번 테스트에서는 Maven을 사용하고 라이브러리 버전은 다음과 같습니다.

spring-boot-2.2.2.RELEASE
logback-classic-1.2.3
logback-core-1.2.3

1. application.yml

bamdule.logging.dir : D://logs/spring-boot-log/bamdule
bamdule.logging.level : DEBUG

로그 파일 경로와 로그 레벨을 설정해 줍니다.


2. logback-spring.xml

  • src/main/resources/logback-spring.xml
  • spring boot 구동 시 자동으로 위 경로에 logback-spring.xml 파일이 있는지 탐색하고 logback 설정을 적용 합니다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

	<!-- aplication.yml에 정의한 log level, dir 데이터를 정의합니다 -->
    <springProperty scope="context" name="LOG_LEVEL" source="bamdule.logging.level"/>
    <springProperty scope="context" name="LOG_DIR" source="bamdule.logging.dir"/>

    <!--콘솔에 로그를 남깁니다.-->
    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>
                %d{yyyy-MM-dd HH:mm:ss} [%thread] [%-5level] %logger{36} - %msg%n
            </Pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--파일에 로그를 남깁니다.-->
    <appender name="File" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_DIR}/bamdule.log</file>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>
                %d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} - %msg%n
            </Pattern>
            <charset>UTF-8</charset>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_DIR}/bamdule/bamdule.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>
   
<!--파일에 로그를 남깁니다. (Roling)-->
    <appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_DIR}/archived/bamdule_roling.log</file>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} - %msg%n</Pattern>
            <charset>UTF-8</charset>
        </encoder>
        
        <!-- 10MB가 넘어가면 새로운 압축파일이 생성 -->
        <triggeringPolicy
            class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>

        <!-- 파일을 덮어쓰는 정책 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <FileNamePattern>${LOG_DIR}/archived/bamdule_roling.%i.log.zip</FileNamePattern>
            <!--
                10MB단위로 로그 압축 파일이 생성된다. (1 ~ 10)
                10개가 넘어가면 오래된순으로 덮어쓰기 됩니다.
            -->
            <MinIndex>1</MinIndex>
            <MaxIndex>10</MaxIndex>
        </rollingPolicy>
    </appender>   
   
    <!--에러 일 경우 파일에 로그를 남깁니다--> 
    <appender name="Error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>error</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <file>${LOG_DIR}/bamdule-error.log</file>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>
                %d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} - %msg%n
            </Pattern>
            <charset>UTF-8</charset>
        </encoder>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_DIR}/bamdule/bamdule-error.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
    </appender>
    
    <!--
        org.springframework.web 패키지 아래에서 발생하는 INFO 레벨 로그를 Console, File에 남깁니다.
        root의 속성을 상속받지 않습니다.
    -->
    <logger name="org.springframework.web" level="INFO" additivity="false">
        <appender-ref ref="Console"/>
        <appender-ref ref="File"/>
        <appender-ref ref="Error"/>
    </logger>
    
    <logger name="org.apache.ibatis" level="DEBUG" additivity="false">
        <appender-ref ref="Console"/>
        <appender-ref ref="RollingFile"/>
        <appender-ref ref="Error"/>
    </logger>

    <!--
        DEBUG 레벨 로그를 Console, File, RollingFile에 남깁니다.
        ERROR 인 경우 bamdule-error.log에 남깁니다.
    -->
    <root level="${LOG_LEVEL}">
        <appender-ref ref="Console"/>
        <appender-ref ref="File"/>
        <appender-ref ref="Error"/>
    </root>

</configuration>

위와 같이 하면 설정이 끝나게 됩니다.


3. Logback Test

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@SpringBootTest
@RunWith(SpringRunner.class)
class LogbackTest {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Test
    void 로그_테스트() {
        logger.debug("[DEBUG]");
        logger.info("[INFO]");
        logger.warn("[WARN]");
        logger.error("[ERROR]");
    }
}

 

  • org.slf4j.Logger와 org.slf4j.LoggerFactory를 import 해야 하며, LoggerFactory를 통해 logger를 주입받으면 됩니다.

 

4. Log Level

  • TRACE, DEBUG, INFO, WARN, ERROR Level이 존재하며 TRACE가 제일 낮고, ERROR가 가장 높습니다.
  • Log Level은 출력 범위를 나타내며, Level이 INFO 인 경우 TRACE, DEBUG Level Log는 보이지 않고
    INFO Level 이상 Log만 화면에 출력됩니다. 즉 현재 레벨 이상의 로그만 화면에 출력됩니다. 

5. Appender

  • 로그를 출력 위치와 출력 패턴 등을 설정합니다.. 
  • ConsoleAppender
    • 로그를 OutputStream에 write하여 콘솔에 출력되도록 합니다.
  • FileAppender
    • 로그의 내용을 File에 write합니다.
    • 최대 보관 일 수를 지정할 수 있습니다.
  • RolingFileAppender
    • FileAppender를 상속 받습니다.
    • 지정한 용량이 넘어간 Log File을 넘버링을 통해 나누어 저장할 수 있습니다.
    • 최소, 최대 개수를 지정 할 수 있고, 최대 개수가 넘어갔을 경우 오래된 순으로 덮어쓰기 됩니다.

6. <logger>와 <root>

logger는 지역적인 설정을 의미하고, root는 전역적인 설정을 의미합니다.

   ...
    <logger name="org.springframework.web" level="INFO" additivity="false">
        <appender-ref ref="Console"/>
        <appender-ref ref="File"/>
        <appender-ref ref="Error"/>
    </logger>
    
    <logger name="org.apache.ibatis" level="DEBUG" additivity="false">
        <appender-ref ref="Console"/>
        <appender-ref ref="RollingFile"/>
        <appender-ref ref="Error"/>
    </logger>

    <root level="${LOG_LEVEL}">
        <appender-ref ref="Console"/>
        <appender-ref ref="File"/>
        <appender-ref ref="Error"/>
    </root>
  • logger
    • org.springframework.web 하위에 동작하는 INFO 로그를 Console, File, Error에 출력하라는 의미입니다.
      그리고 레벨이 INFO이기 떄문에 DEBUG 로그는 찍히지 않습니다.
    • additivity 값은 root 설정 상속 유무인데, 상속을 받게 되면 logger에서 설정하지 않은 나머지 설정이 적용됩니다.
  • root
    • 지정된 레벨 이상의 로그를 출력 했을 경우 Console, File, Error에 출력하라는 의미입니다. 지역적으로 선언된 logger 설정이 있다면 해당 logger 설정으로 적용됩니다.

도움이 되셨다면 공감 한번씩 눌러주시면 감사하겠습니다.

안녕하세요. 밤둘레입니다.

MariaDB 연동 및 Mybatis 사용법 정리 글입니다.

Spring Boot Project를 생성하려면 아래 URL로 이동해서 프로젝트를 생성해주세요.

https://start.spring.io/

위와 같이 Spring Boot 프로젝트를 생성 하면 됩니다.


1. pom.xml

...
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.1</version>
        </dependency>

        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
</dependencies>
...

2. application.properties

spring.datasource.hikari.maximum-pool-size = 4
spring.datasource.url = URL
spring.datasource.username = username
spring.datasource.password = password

mybatis.type-aliases-package = com.example.bamdule.model
mybatis.mapper-locations = mapper/xml/*.xml  
  • mybatis.type-aliases-package : Model Class Package 경로를 적어주시면 됩니다.
    사용하지 않으면 생략 해도 됩니다. 대신 mapper.xml 작성 시, Model의 모든 경로를 적어주셔야 합니다.
  • mybatis.mapper-locations : mapper.xml 경로를 적어주시면 됩니다.

3. Model Class 생성

import java.time.LocalDateTime;

public class Student {

    private Integer id;
    private String name;
    private String code;
    private LocalDateTime saveDate = LocalDateTime.now();
	
    //setter, getter, toString 생략
	
}

4. Mapper interface 생성

import com.example.bamdule.model.Student;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface StudentMapper {

    public List<Student> findList();

    public Student findOneByCode(String codes);

    public void save(Student student);

    public void update(Student student);

    public void deleteById(Integer id);
}

5. mapper-student.xml

<!DOCTYPE mapper
	PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
	"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.bamdule.mapper.StudentMapper">
    <select id="findList" resultType="student">
        SELECT 
            id,
            name,
            code,
            save_date as 'saveDate'
        FROM student
    </select>
    
    <select id="findOneByCode" parameterType="String" resultType="student">
        SELECT
            id,
            name,
            code,
            save_date as 'saveDate'
        FROM student
        WHERE code = #{code}
    </select>    
    
    <insert id="save" parameterType="student">
        INSERT INTO student (
            name,
            code,
            save_date
        ) VALUES (
            #{name},
            #{code},
            #{saveDate}
        )
    </insert>
    
    <update id="update" parameterType="student">
        UPDATE student
        SET 
            name = #{name},
            code = #{code},
            save_date = #{saveDate}
        WHERE id = #{id}
    </update>
    
    <delete id = "deleteById" parameterType="int">
        DELETE FROM student WHERE id = #{id}
    </delete>
</mapper>
  • src/main/resources/mapper/xml/mapper-student.xml 위치에 생성해주시면 됩니다.
  • DB Query를 정의해 놓은 xml입니다.
  • namaspace는 Mapper Interface Package를 적어주시면 됩니다.

6. Table Create Query

CREATE TABLE `student` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(20) NULL DEFAULT NULL,
	`save_date` DATETIME NULL DEFAULT NULL,
	`code` VARCHAR(30) NOT NULL,
	PRIMARY KEY (`id`),
	UNIQUE INDEX `CODE_UK` (`code`)
)

Test 하기전 Student Table을 생성해줍니다.


7. Test

import com.example.bamdule.mapper.StudentMapper;
import com.example.bamdule.model.Student;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
@RunWith(SpringRunner.class)
class BamduleApplicationTests {

    @Autowired
    private StudentMapper studentMapper;

    @Test
    void 학생_생성() {
        String code = "S20200102154530";
        String name = "Son";
        Student student = studentMapper.findOneByCode(code);

        if (student == null) {
            student = new Student(name, code);
            studentMapper.save(student);
        } else {
            student.setSaveDate(LocalDateTime.now());
            studentMapper.update(student);
        }
    }

    @Test
    void 학생_리스트_출력() {
        studentMapper.findList().forEach(data -> System.out.println(data));
    }

}

8. 결과

위와 같이 출력되었다면 성공입니다.


도움이 되셨다면 공감 한번씩 눌러주시면 감사하겠습니다.

git file 상태 흐름도

git source file Status는 tracked와 untracked로 구분됩니다. 

tracked는 git에 의해 추적되고 있는 상태를 뜻하고, untracked는 추적되고 있지 않은 상태를 의미합니다.


  • Untracked
    • 원격 저장소에 올릴 필요가 없는 파일
    • 새롭게 생성된 파일

  • Tracked
  • git에 의해 추적이 되고있는 상태를 의미하고 총 3가지로 구성됩니다.
    • modified
      • 원격 저장소에 업로드 되어있고, 파일이 수정된 상태
    • unmodified
      • 커밋 된 상태
      • 원격 저장소에 업로드 되어있고, 파일이 수정되지 않은 상태
      • git status 명령어를 실행해도 화면에 노출되지 않는다.
    • staged
      • git add 명령어를 통해 index(stage) 저장소에 올라온 상태
      • modified 또는 untracked 일 경우 staged로 넘어 올 수 있다.

workspace안에 readme.txt파일 과 new_file.txt파일이 있다. readme.txt 파일은 원격저장소에 업로드 되었고
수정이 되지 않아서 unmodified 상태이다. 

new_file.txt 파일은 새롭게 추가되었고, untracked 상태이다.

readme.txt 파일을 수정하면 modified 상태로 변하게 된다.

위 두 파일을 git add 명령어를 통해 index(stage) 저장소로 이동해 staged 상태로 변경해보자.

git add .
git status

위 두 파일이 staged 상태로 변경된 것을 볼 수 있다. 이때 readme.txt 파일을 수정하면 다음과 같은 상태로 변경된다.

readme.txt 파일은 staged 상태와 modified 상태이다. 이 상태에서 commit을 하게 되면 현재 index(stage) 저장소에 있는 소스 들만 로컬 저장소로 이동하며, unmodified 상태로 변경된다.

이 경우 다시 git add 명령어를 입력하여 수정된 소스도 index(stage) 저장소로 이동시켜주자.

git add .
git status

이제 두 파일을 git commit 명령어를 통해 로컬 저장소로 이동 시켜 unmodified 상태로 변경해보자.

git commit --message "커밋메시지"
git status

unmodified 상태인 경우 git status 명령어를 입력해도 화면에 노출되지 않는다.

new_file.txt와 readme.txt 파일은 로컬저장소에 저장되어 있으며,
원격 저장소로 올리기 위해서는 push명령어를 사용해야한다.

git push 원격지명 브랜치명  

 

참조 : https://git-scm.com/book/ko/v2/Git%EC%9D%98-%EA%B8%B0%EC%B4%88-%EC%88%98%EC%A0%95%ED%95%98%EA%B3%A0-%EC%A0%80%EC%9E%A5%EC%86%8C%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0

+ Recent posts