저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

스프링 부트 2에서 JUnit 5를 사용하는 방법을 정리한다. 먼저 pom.xml 파일을 다음과 같이 수정한다.


  • spring-boot-starter-test 의존 설정에서 junit:junit을 제외 처리
  • junit-jupiter-api 의존 추가
  • maven-surefire-plugin 플러그인 JUnit 5 기준 설정. 주의할 점은 maven-sufire-plugin의 버전을 2.19.1로 설정해야 한다는 점이다. 스프링 부트 2.0.2는 maven-surefire-plugin의 2.21.0 버전을 기본으로 사용하는데 이 버전은 JUnit 5를 제대로 처리하지 못한다.

다음은 설정 예이다.


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

<project ...>

    <modelVersion>4.0.0</modelVersion>


    <groupId>madvirus</groupId>

    <artifactId>boot2-junit5</artifactId>

    <version>0.0.1-SNAPSHOT</version>

    <packaging>jar</packaging>


    <name>boot2-junit5</name>


    <parent>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-parent</artifactId>

        <version>2.0.2.RELEASE</version>

        <relativePath/> <!-- lookup parent from repository -->

    </parent>


    <properties>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

        <java.version>1.9</java.version>

        <junit-jupiter.version>5.1.1</junit-jupiter.version>

        <junit-platform.version>1.1.1</junit-platform.version>

    </properties>


    <dependencies>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>


        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-devtools</artifactId>

            <scope>runtime</scope>

        </dependency>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-test</artifactId>

            <scope>test</scope>

            <exclusions>

                <exclusion>

                    <groupId>junit</groupId>

                    <artifactId>junit</artifactId>

                </exclusion>

            </exclusions>

        </dependency>


        <dependency>

            <groupId>org.junit.jupiter</groupId>

            <artifactId>junit-jupiter-api</artifactId>

        </dependency>

    </dependencies>


    <build>

        <plugins>

            <plugin>

                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-maven-plugin</artifactId>

            </plugin>


            <plugin>

                <groupId>org.apache.maven.plugins</groupId>

                <artifactId>maven-surefire-plugin</artifactId>

                <version>2.19.1</version>

                <dependencies>

                    <dependency>

                        <groupId>org.junit.platform</groupId>

                        <artifactId>junit-platform-surefire-provider</artifactId>

                        <version>${junit-platform.version}</version>

                    </dependency>

                    <dependency>

                        <groupId>org.junit.jupiter</groupId>

                        <artifactId>junit-jupiter-engine</artifactId>

                        <version>${junit-jupiter.version}</version>

                    </dependency>

                </dependencies>

            </plugin>

        </plugins>

    </build>


</project>


JUnit5를 이용해서 스프링 부트 테스트를 실행하는 예제 코드는 다음과 같다.


package boot2junit5;


import org.assertj.core.api.Assertions;

import org.junit.jupiter.api.Test;

import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit.jupiter.SpringExtension;


@ExtendWith(SpringExtension.class)

@SpringBootTest

public class ApplicationTest {


    @Autowired

    private HelloService helloService;


    @Test

    void hello() {

        Assertions.assertThat(helloService.hello("안녕")).isEqualTo("안녕");

    }


}


@ExtendWith 애노테이션은 JUnit5에서 확장 기능을 실행할 때 사용한다. SpringExtension은 JUnit5를 위한 스프링 확장 기능으로 스프링 연동 테스트를 실행할 수 있게 한다.


관련 링크


Posted by 최범균 madvirus

댓글을 달아 주세요

스프링 부트 2.0에서 엑셀 다운로드 기능을 구현하는 방법을 정리했다.


pom.xml 설정


https://start.spring.io/ 사이트에서 스프링 부트 2.0.x 버전을 선택해서 프로젝트를 생성한다. Dependencies로는 Web과 Thymeleaf를 선택한다. 생성한 프로젝트 pom.xml 파일에 엑셀 생성을 위해 poi 의존을 추가한다.


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

<project xmlns="http://maven.apache.org/POM/4.0.0"

         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 

              http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>


    <groupId>madvirus</groupId>

    <artifactId>excel-download</artifactId>

    <version>0.0.1-SNAPSHOT</version>

    <packaging>jar</packaging>


    <name>excel-download</name>

    <description>Demo project for Spring Boot</description>


    <parent>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-parent</artifactId>

        <version>2.0.1.RELEASE</version>

        <relativePath/> <!-- lookup parent from repository -->

    </parent>


    <properties>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

        <java.version>1.8</java.version>

    </properties>


    <dependencies>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-web</artifactId>

        </dependency>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-thymeleaf</artifactId>

        </dependency>


        <dependency>

            <groupId>org.apache.poi</groupId>

            <artifactId>poi</artifactId>

            <version>3.17</version>

        </dependency>


        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-devtools</artifactId>

            <scope>runtime</scope>

        </dependency>

        <dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-test</artifactId>

            <scope>test</scope>

        </dependency>

    </dependencies>


    <build>

        <plugins>

            <plugin>

                <groupId>org.springframework.boot</groupId>

                <artifactId>spring-boot-maven-plugin</artifactId>

            </plugin>

        </plugins>

    </build>



</project>


엑셀 다운로드 위한 application.properties 파일 설정


확장자나 파라미터를 이용해서 엑셀 다운로드를 처리할 수 있도록 application.propertie 파일에 다음 설정을 추가한다.


spring.mvc.contentnegotiation.favor-parameter=true

spring.mvc.contentnegotiation.favor-path-extension=true

spring.mvc.contentnegotiation.media-types.xls=application/vnd.ms-excel


스프링 부트는 기본적으로 ContentNegotiationViewResolver를 사용하는데 각 프로퍼티는 다음을 설정한다.

  • favor-parameter: 이 값이 true면 ContentNegotiationViewResolver가 format 파라미터로 지정한 미디어 타입을 사용하도록 설정
  • favor-path-extension: 이 값이 true면 ContentNegotiationViewResolver가 확장자로 지정한 미디어 타입을 사용하도록 설정
  • media-types.타입: 타입에 해당하는 컨텐츠 타입을 지정

예를 들어 위 설정을 사용하면 다음 요청을 엑셀 타입(application/vnd.ms-excel) 요청으로 인지하고, 엑셀 미디어 타입에 해당하는 응답을 처리할 수 있는 뷰를 사용해서 응답을 생성한다.

  • stat.xls (확장자가 xls)
  • stat?format=xls (format 파라미터가 xls)

예제 컨트롤러


다음 코드는 일반 뷰와 엑셀 다운로드를 처리하는 컨트롤러 코드이다.


@Controller

public class StatController {

    private void populateModel(Model model) {

        List<StatRow> rows = Arrays.asList(

                new StatRow("고객1", 1000, 1500),

                new StatRow("고객2", 2000, 2500),

                new StatRow("고객3", 3000, 3500)

        );

        model.addAttribute("rows", rows);

    }


    @GetMapping("/stat")

    public String get(Model model) {

        populateModel(model);

        return "stat";

    }


    @GetMapping("/stat.xls")

    public String getExcelByExt(Model model) {

        populateModel(model);

        return "statXls";

    }


    @GetMapping(path = "/stat", params = "format=xls")

    public String getExcelByParam(Model model) {

        populateModel(model);

        return "statXls";

    }

}


get() 메서드는 일반 뷰를 사용해서 응답을 생성한다. getExcelByExt() 메서드는 확장자가 xls인 요청 경로를 처리하므로 "statXls"에 대응하는 뷰 중에서 엑셀 타입을 응답으로 생성할 수 있는 뷰를 선택한다. 비슷하게 getExcelByParam() 역시 format 파라미터가 xls인 요청을 처리하므로 엑셀 타입을 생성할 수 있는 뷰를 선택한다.


엑셀 생성을 위한 뷰 클래스


엑셀 다운로드를 위한 뷰 클래스는 다음과 같이 구현한다. 빈 객체 이름으로 "statxls"를 사용했는데 이 이름은 앞서 컨트롤러에서 리턴한 뷰 이름과 같다.


package exceldownload;


import org.apache.poi.ss.usermodel.*;

import org.springframework.stereotype.Component;

import org.springframework.web.servlet.view.document.AbstractXlsView;


import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.util.List;

import java.util.Map;


@Component("statXls")

public class StatXlsView extends AbstractXlsView {

    @Override

    protected void buildExcelDocument(

            Map<String, Object> model, Workbook workbook,

            HttpServletRequest request, HttpServletResponse response) throws Exception {

        response.setHeader("Content-Disposition", "attachment; filename=\"stat.xls\"");


        List<StatRow> stats = (List<StatRow>) model.get("rows");


        CellStyle numberCellStyle = workbook.createCellStyle();

        DataFormat numberDataFormat = workbook.createDataFormat();

        numberCellStyle.setDataFormat(numberDataFormat.getFormat("#,##0"));


        Sheet sheet = workbook.createSheet("mobilestat");

        for (int i = 0 ; i < stats.size() ; i++) {

            StatRow stat = stats.get(i);

            Row row = sheet.createRow(i);


            Cell cell0 = row.createCell(0);

            cell0.setCellValue(stat.getName());


            Cell cell1 = row.createCell(1);

            cell1.setCellType(CellType.NUMERIC);

            cell1.setCellValue(stat.getValue1());

            cell1.setCellStyle(numberCellStyle);


            Cell cell2 = row.createCell(2);

            cell2.setCellType(CellType.NUMERIC);

            cell2.setCellValue(stat.getValue2());

            cell2.setCellStyle(numberCellStyle);

        }

    }

}



타임리프 뷰 구현


타임리프트를 이용한 뷰 구현 파일인 stat.html은 다음과 같아 간단하게 구현했다. 엑셀 다운로드를 위한 링크를 추가했다.


<!DOCTYPE HTML>

<html xmlns:th="http://www.thymeleaf.org">

<head>

    <meta charset="utf-8" />

    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

    <title>통계</title>

</head>

<body>


<a href="stat.xls">엑셀다운, 확장자(stat.xls)</a> |

<a href="stat?format=xls">엑셀다운, 파라미터(stat?format=xls)</a>

<table border="1">

    <thead>

    <tr>

        <th>이름</th>

        <th>값1</th>

        <th>값2</th>

    </tr>

    </thead>

    <tbody>

    <tr th:each="row : ${rows}">

        <td th:text="${row.name}"></td>

        <td th:text="${#numbers.formatInteger(row.value1, 1, 'COMMA')}"></td>

        <td th:text="${#numbers.formatInteger(row.value2, 1, 'COMMA')}"></td>

    </tr>

    </tbody>

</table>


</body>

</html>


예제 실행


완전한 예제 프로젝트는 https://github.com/madvirus/excel-download 리포지토리에서 구할 수 있다. 명령 프롬프트에서 "mvnw spring-boot:run" 명령어를 부트 어플리케이션을 실행한 뒤에 http://localhost:8080/stat 주소에 연결해보자. 다음 결과를 볼 수 있다.



엑셀 다운로드 링크를 클릭해보자. 두 링크 중 아무거나 클릭하면 엑셀 파일을 다운로드 한다.



실제 다운로드한 파일을 열어보자. 아래와 같이 엑셀 파일이 올바르게 생성된 것을 확인할 수 있다.



Posted by 최범균 madvirus

댓글을 달아 주세요

스프링 부트(spring boot)를 사용한다면 타임리프(thymeleaf)의 식 객체(expression object)를 쉽게 확장할 수 있다. 먼저 식 객체를 생성해주는 타임리프 IExpressionObjectDialect를 구현한다. 이 클래스를 스프링 빈으로 등록해야 한다. 아래 예는 @Component를 붙여 컴포넌트 스캔 대상으로 설정했다.


import java.util.Collections;

import java.util.Set;


import org.springframework.stereotype.Component;

import org.thymeleaf.context.IExpressionContext;

import org.thymeleaf.dialect.AbstractDialect;

import org.thymeleaf.dialect.IExpressionObjectDialect;

import org.thymeleaf.expression.IExpressionObjectFactory;


@Component

public class MyFormatDialect extends AbstractDialect implements IExpressionObjectDialect {


    protected ScgFormatDialect() {

        super("myFormat");

    }


    @Override

    public IExpressionObjectFactory getExpressionObjectFactory() {

        return new IExpressionObjectFactory() {


            @Override

            public Set<String> getAllExpressionObjectNames() {

                return Collections.singleton("scgFormat");

            }


            @Override

            public Object buildObject(IExpressionContext context, String expressionObjectName) {

                return new MyFormat();

            }


            @Override

            public boolean isCacheable(String expressionObjectName) {

                return true;

            }

        };

    }


}


생성자에서는 식 객체의 이름을 "myFormat"으로 지정한다.

getExpressionObjectFactory() 메서드는 IExpressionObjectFactory 객체를 리턴한다. 이 객체의 buildObject() 메서드가 생성하는 객체가 식 객체가 된다. 이 객체는 타임리프 식에서 사용할 메서드를 제공한다. 다음은 식 객체로 사용할 클래스의 구현 예이다.


public class MyFormat {


    public String date(String date) {

        if (!StringUtils.hasText(date))

            return null;

        if (date.length() == 8) {

            return date.substring(0, 4) + "-" + date.substring(4, 6) + "-" + date.substring(6, 8);

        } else {

            return date;

        }

    }


    public String contractNum(String contractNum) {

        if (!StringUtils.hasText(contractNum))

            return null;

        if (contractNum.length() > 5) {

            return contractNum.substring(0, 5) + "-" + contractNum.substring(5);

        } else {

            return contractNum;

        }

    }


    public String phone(String phone) {

        if (!StringUtils.hasText(phone))

            return null;

        if (phone.length() == 11) {

            return phone.substring(0, 3) + "-" + phone.substring(3, 7) + "-" + phone.substring(7);

        } else if (phone.length() == 10) {

            return phone.substring(0, 3) + "-" + phone.substring(3, 6) + "-" + phone.substring(6);

        } else {

            return phone;

        }

    }

}


이제 커스텀 식 객체를 타임리프 식에서 사용하면 된다.


<td th:text="${#myFormat.phone(item.handphone)}"></td>

<td th:text="${#myFormat.contractNum(item.useContractNum)}"></td>




Posted by 최범균 madvirus

댓글을 달아 주세요

스프링 부트 2.0은 기본 사용하는 커넥션풀을 HikariCP로 교체했다. HikariCP와 관련된 커넥션 풀 속성은 다음과 같다. 


 속성

 설명

기본 값

 connectionTimeout

풀에서 커넥션을 구할 때 대기 시간을 밀리초 단위로 지정한다. 대기 시간 안에 구하지 못하면 익셉션이 발생한다.


허용 가능한 최소 값은 250이다. 

30000

(30초)

 validationTimeout

커넥션이 유효한지 검사할 때 대기 시간을 지정한다. 이 값은 connectionTimeout보다 작아야 한다.


허용 가능한 최소 값은 250이다. 

5000
(5초)

 idleTimeout

커넥션이 풀에서 유휴 상태로 남을 수 있는 최대 시간을 밀리초 단위로 지정한다. 이 값은 minimumIdle이 maximumPoolSize보다 작을 때만 적용된다.


타임아웃이 지난 커넥션을 제거하기까지 최대 30초, 평균 15초 차이 날 수 있다.


이 타임아웃 전에는 유휴 커넥션을 제거하지 않는다. 풀의 커넥션 개수가 minimumIdle이 되면 유휴 여부에 상관없이 커넥션을 풀에서 제거하지 않는다.


이 값이 0이면 유휴 커넥션을 풀에서 제거하지 않는다.


허용 가능한 최소 값은 10000(10초)이다.

600000

(10분)

 maxLifetime

커넥션의 최대 유지 시간을 밀리초 단위로 설정한다. 이 시간이 지난 커넥션 중에서 사용중인 커넥션은 종료된 이후에 풀에서 제거한다.


갑자기 풀에서 많은 커넥션이 제거되는 것을 피하기 위해 negative attenuation(감쇠)를 적용해 점진적으로 제거한다.

 

이 값을 설정할 것을 권장한다. DB나 인프라스트럭처에서 제한한 커넥션 제한 시간 보다 최소한 30초는 짧아야 한다..


이 값이 0이면 풀에서 제거하지 않지만 idleTimeout은 적용된다.

1800000
(30분)
 maximumPoolSize

유휴 상태와 사용중인 커넥션을 포함해서 풀이 허용하는 최대 커넥션 개수를 설정한다. 이 값은 데이터베이스에 대한 실제 커넥션의 최대 개수를 결정한다.


풀이 이 크기에 도달하고 유휴 커넥션이 없을 때 connectionTimeout이 지날 때까지 getConnection() 호출은 블록킹된다.

10

 minimumIdle

풀에 유지할 유휴 커넥션의 최소 개수를 설정한다.

maximumPoolSize와 동일
 connectionTestQuery

커넥션이 유효한지 검사할 때 사용할 쿼리를 지정한다. 드라이버가 JDBC4를 지원하면 이 프로퍼티를 설정하지 말자. 이 프로퍼티를 설정하지 않으면 JDBC4의 Conneciton.isValid()를 사용해서 유효한지 검사를 수행한다.

없음

 leakDetectionThreshold

커넥션이 누수 가능성이 있다는 로그 메시지를 출력하기 전에 커넥션이 풀에서 벗어날 수 있는 시간을 밀리초로 설정한다. 0은 누수 발견을 하지 않는다. 허용하는 최소 값은 2000(2초)이다.


0


스프링 부트가 자동 제공하는 DataSource를 사용한다면 속성 앞에 "spring.datasource.hikari."를 접두어로 붙이면 된다. 다음은 속성의 사용 예이다. 


spring.datasource.hikari.connectionTimeout=5000

spring.datasource.hikari.validationTimeout=1000

spring.datasource.hikari.maxPoolSize=30


Posted by 최범균 madvirus

댓글을 달아 주세요

스프링 부트 날짜 타입을 JSON으로 응답할 때 별도 설정을 하지 않으면 부트 버전에 따라 응답 형식이 다르다. 먼저 간다한 테스트를 위해 다음과 같이 세 개의 날짜 형식을 갖는 Now 클래스를 사용하자.


import java.time.LocalDateTime;

import java.time.OffsetDateTime;

import java.util.Date;


public class Now {

    private LocalDateTime localTime;

    private OffsetDateTime offsetTime;

    private Date date;


    public Now() {

        localTime = LocalDateTime.now();

        offsetTime = OffsetDateTime.now();

        date = new Date();

    }


    ...getter 생략

}



Now를 생성해서 JSON으로 응답하는 컨트롤러를 다음과 같이 작성했다.


@RestController

public class SampleController {

    @GetMapping("/now")

    public Now time() {

        return new Now();

    }

}


이 글에서 사용한 코드는 https://github.com/madvirus/boot-jackson 리포지토리에서 참고할 수 있다.


부트 2.0에서 기본 JSON 시간 타입 포맷팅


부트 2.0으로 테스트하면 응답 결과가 다음과 같다. 각 타입을 ISO-8601 형식으로 출력하고 있다.


{

    "localTime": "2018-03-01T17:03:50.445428",

    "offsetTime": "2018-03-01T17:03:50.445428+09:00",

    "date": "2018-03-01T08:03:50.445+0000"

}


시간대 정보가 있는 OffsetDateTime 타입은 "+09:00"이 뒤에 붙어 있다. 반면에 Date 타입은 UTC 기준 시간을 사용했다. 또 다른 차이는 OffsetDateTime과 Date의 시간대 표시가 다르다는 것이다. OffsetDateTime 타입은 "+09:00"와 같이 콜론이 포함되어 있고 Date 타입은 "+0000"과 같이 콜론이 없다. LocalDateTime의 경우 오프셋 정보가 없으므로 시간대 부분이 없다.


부트 1.5에서 기본 JSON 시간 타입 포맷팅


부트 1.5에서 테스하면 응답 결과가 다음과 같다. 아주 난리다!


{

"localTime": {

"month": "MARCH",

"dayOfWeek": "THURSDAY",

"dayOfYear": 67,

"nano": 644321400,

"year": 2018,

"monthValue": 3,

"dayOfMonth": 1,

"hour": 17,

"minute": 28,

"second": 2,

"chronology": {

"id": "ISO",

"calendarType": "iso8601"

}

},

"offsetTime": {

"offset": {

"totalSeconds": 32400,

"id": "+09:00",

"rules": {

"fixedOffset": true,

"transitions": [],

"transitionRules": []

}

},

"month": "MARCH",

"dayOfWeek": "THURSDAY",

"dayOfYear": 67,

"nano": 644321400,

"year": 2018,

"monthValue": 3,

"dayOfMonth": 1,

"hour": 17,

"minute": 28,

"second": 2

},

"date": 1519894518644

}


부트 1.5에서 jackson-datatype-jsr310 모듈 추가


부트 1.5에 아래와 같이 jackson-datatype-jsr310 모듈을 추가해보자. 이 모듈은 LocalDateTime이나 OffsetDateTime과 같이 자바 8의 시간 타입을 지원하는 모듈이다.


<dependency>

    <groupId>com.fasterxml.jackson.datatype</groupId>

    <artifactId>jackson-datatype-jsr310</artifactId>

</dependency>


이 모듈을 추가한 뒤에 JSON 생성 결과를 보면 다음과 같다.


{

"localTime": [

2018,

3,

8,

21,

16,

30,

166225000

],

"offsetTime": 1520511390.166383,

"date": 1520511390166

}



부트 1.5 Date 포맷: WRITE_DATES_AS_TIMESTAMPS 비활성, StdFormat 사용


부트 1.5에서 아래 프로퍼티를 application.properties 파일에 추가하면 Date 타입을 ISO-8601 포맷을 사용해서 변환한다.


spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false


다음 프로퍼티를 설정해도 결과가 같다.


spring.jackson.date-format=com.fasterxml.jackson.databind.util.StdDateFormat


실제 출력 결과는 다음과 같다. UTC 기준으로 출력하고 있다.


"date": "2018-03-08T08:51:53.972+0000"



부트 1.5, 2.0: Jackson2ObjectMapperBuilderCustomizer로 포맷팅 설정


부트는 Jackson2ObjectMapperBuilderCustomizer 인터페이스를 제공하는데 이 인터페이스를 구현한 클래스를 빈으로 등록하면 변환 포맷을 설정할 수 있다. 다음 코드는 사용 예이다.


import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

import com.fasterxml.jackson.datatype.jsr310.ser.OffsetDateTimeSerializer;

import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;

import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;


import java.time.LocalDateTime;

import java.time.OffsetDateTime;

import java.time.format.DateTimeFormatter;


@SpringBootApplication

public class Boot15JacksonApplication implements Jackson2ObjectMapperBuilderCustomizer {


    public static void main(String[] args) {

        SpringApplication.run(Boot15JacksonApplication.class, args);

    }


    // Jackson2ObjectMapperBuilderCustomizer 인터페이스 메서드

    @Override

    public void customize(Jackson2ObjectMapperBuilder builder) {

        // LocalDateTime은 오프셋 정보가 없으므로 패턴에 시간대에 해당하는 Z가 없다.

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

        LocalDateTimeSerializer localSerializer = new LocalDateTimeSerializer(formatter);


        DateTimeFormatter formatter2 = 

                DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");

        CustomOffsetDateTimeSerializer offsetSerializer = 

                new CustomOffsetDateTimeSerializer(formatter2);


        builder

                .simpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")

                .serializerByType(LocalDateTime.class, localSerializer)

                .serializerByType(OffsetDateTime.class, offsetSerializer)

        ;

    }


    public class CustomOffsetDateTimeSerializer extends OffsetDateTimeSerializer {

        public CustomOffsetDateTimeSerializer(DateTimeFormatter formatter) {

            super(OffsetDateTimeSerializer.INSTANCE, false, formatter);

        }

    }


}


Jackson2ObjectMapperBuilder의 다음 메서드를 이용해서 시간 타입 변환 포맷을 설정했다. 
  • simpleDateFormat: Date 타입을 위한 변환 포맷 설정
  • serializerByType: 타입을 위한 Jackson의 Serializer 설정
serializerByType 메서드를 이용해서 LocalDateTime과 OffsetDateTime을 위한 Serializer를 설정했다. LocalDateTime을 위한 Serializer로는 Jackson이 제공하는 LocalDateTimeSerializer 클래스를 사용했다. 

OffsetDateTime은 약간 수고가 더 필요하다. Jackson이 제공하는 OffsetDateTimeSerializer를 상속해서 구현한 Serializer를 사용했다. 

위 설정을 추가한 뒤 결과는 다음과 같다.

{
"localTime": "2018-03-08T21:26:46",
"offsetTime": "2018-03-08T21:26:46+0900",
"date": "2018-03-08T21:26:46+0900"
}




Posted by 최범균 madvirus

댓글을 달아 주세요

스프링을 이용해서 회사 서비스를 개발하다보면 은근히 비슷하게 작성하는 설정 코드가 존재하기 마련이다. 예를 들어, 여러 서비스에서 LDAP 연동을 위해 같은 스프링 시큐리티 설정을 사용하거나 내부 API 연동을 위해 동일한 설정을 사용할 수 있다. 또한, 데이터 저장소에 따라 사내에서 제안하는 설정 가이드가 존재할 수도 있다.


이런 류의 설정은 여러 곳에서 반복되기 때문에 http://javacan.tistory.com/entry/spring-at-enable-config 글에서 설명한 @Enable을 이용해서 커스텀 설정 편의 기능을 제공하면 편리하다. 스프링부트를 사용한다면, 부트의 자동 설정 기능까지 함께 지원함으로써 설정 편의 기능을 완성할 수 있다.


스프링캠프2016에서 이수홍님이 발표하신 자료(http://www.slideshare.net/sbcoba/2016-deep-dive-into-spring-boot-autoconfiguration-61584342)를 보면 스프링 부트의 자동 설정 기능에 대해 많은 내용을 알 수 있다. 이 자료와 함께 이수홍님 깃헙 프로젝트(https://github.com/sbcoba/spring-camp-2016-spring-boot-autoconfiguration)에서 관련 코드도 참고할 수 있다.


이수홍님 자료를 바탕으로 자동 설정 기능을 추가하는 방법을 정리하면 다음과 같다.

  • 자동 설정 기능 제공 모듈: 설정을 제공하는 @Configuration 적용 클래스 구현
  • 자동 설정 기능 제공 모듈: spring.factories 파일 작성
  • 적용: 자동 설정이 필요한 프로젝트에서 모듈에 대한 의존 추가

1. 자동으로 설정할 내용을 담을 @Configuration 클래스 작성


자동 설정 기능을 위한 코드를 담고 있는 모듈(jar 파일)은 @Configuration 애노테이션을 사용해서 설정 클래스를 작성한다.


@Configuration

@ConditionalOnMissingBean(AuthClient.class)

public class CompAutoConf {


    @Bean

    public AuthClient authClient() {

        return new HttpAuthClient();

    }



이 설정 클래스는 일반적인 스프링 설정 클래스와 차이가 없다. 보통 자동 설정 기능에서 사용하는 @Configuration 클래스는 조건에 따라 설정 사용 여부를 결정하기 위해 @Conditional 애노테이션을 포함한다. 위 코드의 경우 AuthClient 타입의 빈이 없는 경우에만 CompAutoConf를 설정 클래스로 사용하도록 했다.



2. META-INF/spring.factories 파일에 설정 클래스 지정하기


설정 클래스를 구현했다면, 그 다음으로 할 일은 spring.factories 파일에 설정 클래스를 지정하는 것이다. 클래스패스 위치에(메이븐 같으면 src/main/resources 폴더에) META-INF/spring.factories 파일을 만들고, org.springframework.boot.autoconfigure.EnableAutoConfiguration 프로퍼티의 값으로 자동 설정으로 사용할 클래스를 값으로 준다.


# Auto Configure

org.springframework.boot.autoconfigure.EnableAutoConfiguration=mycompany.CompAutoConf



3. 자동 설정 모듈 사용


앞서 언급한 이수홍님 자료를 보면, 스프링 부트는 클래스패스에 위치한 모든 META-INF/spring.factories 파일의 org.springframework.boot.autoconfigure.EnableAutoConfiguration 프로퍼티 값을 읽어와 설정 클래스로 사용한다. 따라서, 스프링 부트 프로젝트에 자동 설정 모듈에 대한 의존을 추가해주기만 하면 된다.


@SpringBootApplication // META-INF/spring.factories에 지정한 설정 클래스 사용

public class Application {


    public static void main(String[] args) {

        ConfigurableApplicationContext ctx = SpringApplication.run(Application.class, args);

        // CompAutoConf에 정의한 @Bean 설정에 따라 HttpAuthClient 객체를 빈으로 등록

        AuthClient client = ctx.getBean(AuthClient.class);

        ...

    }

}



조건에 따라 설정 적용하기: @Conditional


앞서 CompAutoConf 예를 보면 @ConditiionalOnMissingBean(AuthClient.class)가 있는데, 이는 AuthClient 타입의 빈이 없는 경우에만 해당 설정을 사용한다는 것을 의미한다. 만약 다음과 같이 설정에 AuthClient 타입 빈이 포함되어 있으면 @ConditiionalOnMissingBean(AuthClient.class)에 따라 CompAutoConf 설정 클래스를 사용하지 않는다.


@SpringBootApplication

public class Application {


    @Bean

    public AuthClient authClient() {

        return new ProtobuffAuthClient();

    }


    ...


@Conditional은 스프링 4.0부터 지원하는데, 스프링은 클래스 존재 여부, 프로퍼티 존재 여부, 빈 존재 여부 등 다양한 @Conditional을 제공하고 있다.


설정 순서 지정하기: @AutoConfigureBefore/@AutoConfigureAfter/@AutoConfigureOrder


자동 설정 클래스를 적용하는 순서가 중요할 때가 있다. 예를 들어, 내가 만들 자동 설정이 DataSource를 설정한다고 해보자. 이 자동 설정은 스프링 부트가 제공하는 DataSource 자동 설정 클래스인 DataSourceAutoConfiguration보다 먼저 실행되어야 한다. 그렇지 않을 경우, DataSourceAutoConfiguration이 생성한 DataSource와 내가 만든 자동 설정 기능이 생성한 DataSource가 함께 만들어진다.


보통 이는 원하는 결과가 아니다. 스프링 부트의 DataSourceAutoConfiguration는 DataSource 타입 빈이 존재하지 않는 경우에만 DataSource를 만드므로, 스프링 부트의 DataSourceAutoConfiguration보다 내가 만든 자동 설정 클래스가 먼저 실행되면 여러 DataSource가 만들어지는 문제를 피할 수 있다. 이를 위한 간단한 방법은 내가 만든 자동 설정 클래스에 @AutoConfigureBefore를 붙이는 것이다.


@AutoConfigureBefore(DataSourceAutoConfiguration.class)

@ConditionalOnMissingBean(DataSource.class)

public class MyCompDataSourceAutoConfiguration {

    ...

}


@AutoConfigureAfter를 사용해서 다른 설정 뒤에 자동 설정을 적용하거나 @AutoConfigureOrder를 사용해서 설정의 우선순위를 지정할 수 있다.

Posted by 최범균 madvirus

댓글을 달아 주세요




Posted by 최범균 madvirus

댓글을 달아 주세요