주요글: 도커 시작하기
반응형

리액티브 관련 자주 나오는 용어 정리


동기, 비동기


 용어

동기(synchronous)

비동기(asynchronous)

 설명

한 프로세스(쓰레드)가 작업을 순차 실행

다른 프로세스(쓰레드)로 작업을 실행

 비고

작업 완료 여부를 호출하는 곳에서 처리

작업 완료 여부를 호출된 곳에서 처리 

동시에 실행할 수도 있고 지금이 아닌 미래 시점에 실행할 수도 있음


블록, 논블록


 용어

블록(block)

논블록(non-block)

 설명

작업 실행이 끝날 때가지 쓰레드가 다른 작업을 하지 못하고 대기

작업 완료를 기다리지 않고 진행

 비고

호출된 곳에서 작업 완료 후 리턴

호출된 곳에서 바로 리턴


병행, 병렬


 용어

병행(concurrency)

병렬(parallelism)

설명

주어진 시점에 두 개 이상의 작업 진행

주어진 시점에 두 개 이상의 작업이 동시 발생

  • 병렬 처리를 위한 서로 다른 처리 장치(CPU) 필요 (병행처리는 필수 아님)
  • 병렬은 병행을 내포

비고

모듈화, 응답성, 유지보수성 중요

(프로그램 속성)


주요 관심

  • 언제 실행 시작
  • 정보 교환 방식
  • 공유 자원 관리

효율이 주요 관심

(머신 속성)


주요 관심

더 빨리 계산하기 위해

- 큰 문제를 작은 문제로 나누는 방법 고민

- 병렬 HW 사용 최적화

 




반응형

"5 Things to Know About Reactive Programming" 글 요약(원문은 여기 참고)


1. 리액티브 프로그래밍은 비동기 데이터 스트림을 이용한 프로그래밍

  • 리액티브 프로그래밍 사용시, 데이터 스트림이 어플리케이션의 뼈대가 됨
  • 이벤트, 메시지, 호출, 실패를 데이터 스트림으로 전달
  • 이 스트림을 Observe(Subscrive)하고, 값을 발생(emit)할 때 반응
  • 모든 것이 스트림: 클릭 이벤트, HTTP 요청, 유입 메시지, 변수 변경, 세선 값 등 바뀌거나 발생할 수 있는 모든 것 (이는 본질적으로 비동기와 관련)
2. 콜드(Cold) vs 핫(Hot)
  • 콜드 스트림 : 누군가 Observe를 시작할 때까지 아무것도 안 함. 스트림이 소비될 때 동작하기 시작. 여러 subscriber가 공유하지 않음. 예) 파일 다운로드
  • 핫 스트림 : 소비 전에 활성. 개별 subscriber에 독립적으로 데이터 생성. 구독하기 시작한 이후의 데이터 수신. 예) 실시간 주식 주가, 센서 데이터
3. 비동기 오용 주의
  • 비동기는 리액티브 프로그래밍에서 중요한 단어
  • 중요한 세 가지 : 부수 효과, 쓰레드, 블록
    • 부수 효과 : 불필요한 부수 효과를 피할 것. 부수 효과 없는 함수 -> 쓰레드 안정성
    • 쓰레드 : 너무 많은 쓰레드 사용을 피할 것. 다중 쓰레드는 동기화 문제 위험
    • 블록 : 쓰레드를 소유하지 않으므로 쓰레드를 절대 블록하지 말 것
4. 단순
  • 다른 개발자가 읽을 코드임을 잊지 말 것
5. 리액티브 프로그래밍 != 리액티브 시스템


반응형

스프링 부트 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


반응형

스프링 부트 날짜 타입을 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"
}




  1. 하짜증 2021.08.19 17:29

    저의 책 속의 선생님...

    이 글이 2018년도 글인데, 현 시점의 스프링부트 버전에서 이것보다 더욱 좋아진 방법이 있을까요?

    • madvirus 2021.09.02 09:45 신고

      요즘 저는 각 필드에 타입 포맷을 직접 정의하고 있어요.

반응형

JSTL 1.2의 <fmt:formatDate> 태그는 LocalDateTime과 같이 자바 8부터 제공하는 시간 타입에 대한 포맷팅 출력을 지원하지 않는다. 그래서 JSP에서 LocalDateTime 값을 지정한 형식에 맞춰 출력해주는 간단한 태그 파일을 만들었다.


아래의 태그 파일을 /WEB-INF/tags 폴더에 formatDateTime.tag 이름으로 만들었다.


<%@ tag body-content="empty" pageEncoding="utf-8" %>

<%@ tag import="java.time.format.DateTimeFormatter" %>

<%@ tag trimDirectiveWhitespaces="true" %>

<%@ attribute name="value" required="true" 

              type="java.time.temporal.TemporalAccessor" %>

<%@ attribute name="pattern" type="java.lang.String" %>

<%

if (pattern == null) pattern = "yyyy-MM-dd";

%>

<%= DateTimeFormatter.ofPattern(pattern).format(value) %>


JSP 코드에서는 다음과 같이 태그 파일을 사용해서 원하는 형식으로 값을 출력한다.


<%@ page contentType="text/html; charset=utf-8" %>

<%@ taglib prefix="tf" tagdir="/WEB-INF/tags" %>

<!DOCTYPE html>

<html>

<head>

    <title>회원 조회</title>

</head>

<body>

    ...

    <tf:formatDateTime

            value="${mem.registerDateTime}" 

            pattern="yyyy-MM-dd" />

    ...

</body>

</html>


반응형

Bean Validation 2.0(JSR-380)에는 검증과 관련해서 간지러운 점을 긁어주는 애노테이션이 몇 개 추가되었다. 예를 들어 2.0에 추가된 @Email 애노테이션을 사용하면 이메일 형식을 검사하기 위해 정규 표현식을 사용하지 않아도 되고, @Positive 애노테이션을 사용하면 값이 양수인지 검사할 수 있다.


스프링 5에서 Bean Validation 2.0 설정


스프링 5 버전에서 Bean Validation 2.0을 사용하려면 다음과 같이 2.0 API와 관련 프로바이더를 의존에 추가해주면 된다. hibernate-validator 6.0 의존을 추가하면 validationa-api 2.0 의존도 함께 추가되므로 validation-api 의존을 생략해도 된다.


<!-- validation-api는 생략 가능 -->

<dependency>

    <groupId>javax.validation</groupId>

    <artifactId>validation-api</artifactId>

    <version>2.0.1.Final</version>

</dependency>


<dependency>

    <groupId>org.hibernate.validator</groupId>

    <artifactId>hibernate-validator</artifactId>

    <version>6.0.7.Final</version>

</dependency>


스프링 관련 설정


@EnableWebMvc 애노테이션을 사용하면 된다. 이 애노테이션을 사용하면 Bean Validation 애노테이션에 대한 검증 기능을 제공하는 OptionalValidatorFactoryBean를 글로벌 Validator로 등록한다.


@Configuration

@EnableWebMvc

public class SpringConfig implements WebMvcConfigurer {

    ...

}


애노테이션 사용 예


다음은 Bean Validation 애노테이션을 사용한 예이다.


import org.springframework.format.annotation.DateTimeFormat;


import javax.validation.constraints.Email;

import javax.validation.constraints.NotBlank;

import javax.validation.constraints.NotEmpty;

import javax.validation.constraints.PastOrPresent;

import java.time.LocalDate;


public class FormData {

    @NotBlank

    @Email

    private String email;


    @NotBlank

    private String name;


    @NotEmpty

    private String password;


    @DateTimeFormat(pattern = "yyyyMMdd")

    @PastOrPresent

    private LocalDate birthday;


컨트롤러는 다음과 같이 @Valid를 이용해서 검증을 적용할 수 있다.


@PostMapping

public String submit(@ModelAttribute @Valid FormData formData, Errors errors) {

    if (errors.hasErrors()) return "form";

    return "submit";

}


실제 예제 코드는 https://github.com/madvirus/spring5-bv2 리포지토리에서 확인할 수 있다. 리포지토리에서 코드를 clone하고 아래 순서대로 실행한다.

  • git clone https://github.com/madvirus/spring5-bv2.git
  • cd spring5-bv2
  • cd sp5-bv2
  • mvnw jetty:run

이 예제는 JSP를 뷰 기술로 사용했다. 관련 폼을 보여주는 JSP 코드는 다음과 같다.


<%@ page contentType="text/html; charset=utf-8" %>

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<!DOCTYPE html>

<html>

<head>

    <title>가입</title>

</head>

<body>

    <h2>가입</h2>

    <form:form modelAttribute="formData">

    <p>

        <label>이메일:<br>

        <form:input path="email" />

        <form:errors path="email"/>

        </label>

    </p>

    <p>

        <label>이름:<br>

        <form:input path="name" />

        <form:errors path="name"/>

        </label>

    </p>

    <p>

        <label>암호:<br>

        <form:password path="password" />

        <form:errors path="password"/>

        </label>

    </p>

    <p>

        <label>생일:<br>

        <form:input path="birthday" />

        <form:errors path="birthday" />

        </label>

    </p>

    <input type="submit" value="가입">

    </form:form>

</body>

</html>


브라우저에서 http://localhost:8080/register로 연결한 뒤 폼에 아무 값도 입력하지 않고 [가입] 버튼을 누르면 검증 결과를 확인할 수 있다. 실제 결과는 다음과 같다.



에러 메시지를 위한 MessageSource를 설정하지 않았다. 위 결과에서 이메일, 이름, 암호 필드에 출력한 메시지는 hibernate-validator가 제공하는 기본 에러 메시지이다.


스프링 부트 2 사용


스프링 부트 2 버전은 기본적으로 Bean Validation 2.0을 사용하므로 추가 설정이 필요없다. 그냥 스프링 부트 2 프로젝트를 만들어 사용하면 된다. 관련 예제 코드는 같은 리포지토리의 boot2-bv2 폴더에 있다. 아래 순서대로 실행한 뒤 브라우저에서 http://localhost:8080/register 주소로 연결해서 확인할 수 있다.

  • git clone https://github.com/madvirus/spring5-bv2.git
  • cd spring5-bv2
  • cd boot2-bv2
  • mvnw spring-boot:run

부트 예제는 Thymeleaf를 뷰로 사용했다. 폼을 보여주는 Themeleaf 템플릿 코드는 다음과 같다.


<!DOCTYPE html>

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

<head>

    <meta charset="UTF-8" />

    <title>가입</title>

</head>

<body>

<h2>가입</h2>

<form th:object="${formData}" method="POST">

    <p>

        <label>이메일:<br>

            <input type="text" th:field="*{email}" />

            <span th:each="err : ${#fields.errors('email')}" th:text="${err}" />

        </label>

    </p>

    <p>

        <label>이름:<br>

            <input type="text" th:field="*{name}" />

            <span th:each="err : ${#fields.errors('name')}" th:text="${err}" />

        </label>

    </p>

    <p>

        <label>암호:<br>

            <input type="password" th:field="*{password}" />

            <span th:each="err : ${#fields.errors('password')}" th:text="${err}" />

        </label>

    </p>

    <p>

        <label>생일:<br>

            <input type="text" th:field="*{birthday}" />

            <span th:each="err : ${#fields.errors('birthday')}" th:text="${err}" />

        </label>

    </p>

    <input type="submit" value="가입">

</form>

</body>

</html>


실행 결과는 앞서 본 그림과 동일하다.


Bean Validation 2.0 제공 애노테이션


Bean Validation 2.0이 제공하는 애노테이션 목록은 다음과 같다. 2.0에 추가된 애노테이션에는 (2)를 표시했다. 아래 표의 지원 타입에서 CharSequence는 문자열 관련 인터페이스로 이 인터페이스를 구현한 대표적인 클래스가 String이다.


애노테이션 

주요 속성 

설명 

 지원 타입

@AssertTrue

@AssertFalse

 

값이 true인지 또는 false인지 검사한다. null은 유효하다고 판단한다. 

boolean
Boolean

@DecimalMax

@DecimalMin

String value

- 최댓값 또는 최솟값


boolean inclusive

- 지정값 포함 여부

- 기본 값 true

지정한 값보다 작거나 같은지 또는 크거나 같은지 검사한다.

inclusive가 false면 value로 지정한 값은 포함하지 않는다.

null은 유효하다고 판단한다.

BigDecimal

BigInteger

CharSequence

byte, short, int, long 및 각 래퍼 타입

@Max

@Min

long value

지정한 값보다 작거나 같은지 또는 크거나 같은지 검사한다.

null은 유효하다고 판단한다.

BigDecimal
BigInteger
byte, short, int, long 및 관련 래퍼 타입

@Digits

int integer

- 허용 가능한 정수 자릿수


int fraction

- 허용 가능한 소수점 이하 자릿수

자릿수가 지정한 크기를 넘지 않는지 검사한다.

null은 유효하다고 판단한다.

BigDecimal

BigInteger

CharSequence

byte, short, int, long 및 관련 래퍼 타입

@Size

int min

- 최소 크기

- 기본 값 0


int max

- 최대 크기

- 기본 값 

길이나 크기가 지정한 값 범위에 있는지 검사한다.

null은 유효하다고 판단한다.

CharSequence

Collection

Map

배열

@Null

@NotNull

 

값이 null인지 또는 null이 아닌지 검사한다. 

 

@Pattern

String regexp

- 정규표현식 

값이 정규표현식에 일치하는지 검사한다. 

null은 유효하다고 판단한다.

CharSequence

@NotEmpty (2)

 

문자열나 배열의 경우 null이 아니고 길이가 0이 아닌지 검사한다. 콜렉션의 경우 null이 아니고 크기가 0이 아닌지 검사한다.

CharSequence

Collection

Map

배열

@NotBlank (2)


null이 아니고 최소한 한 개 이상의 공백아닌 문자를 포함하는지 검사한다.

CharSequence

@Positive (2)

@PositiveOrZero (2)

 

양수인지 검사한다.

OrZero가 붙은 것은 0 또는 양수인지 검사한다.

null은 유효하다고 판단한다.

BigDecimal
BigInteger
byte, short, int, long 및 관련 래퍼 타입

@Negative (2)

@NegativeOrZero (2)

 

음수인지 검사한다.
OrZero가 붙은 것은 0 또는 음수인지 검사한다. 

null은 유효하다고 판단한다.

BigDecimal
BigInteger
byte, short, int, long 및 관련 래퍼 타입

@Email (2)

 

이메일 주소가 유효한지 검사한다. 
null은 유효하다고 판단한다.
CharSequence 

@Future (2)

@FutureOrPresent (2)

 

해당 시간이 미래 시간인지 검사한다.

OrPresent가 붙은 것은 현재 또는 미래 시간인지 검사한다.

null은 유효하다고 판단한다.

시간 관련 타입

@Past (2)

@PastOrPresent (2)

 

해당 시간이 과거 시간인지 검사한다.

OrPresent가 붙은 것은 현재 또는 과거 시간인지 검사한다.
null은 유효하다고 판단한다.

시간 관련 타입 

* 애노테이션이 속한 패키지: javax.validation.constraints

* 시간 관련 타입: Date, Calendar, Instant, LocalDate, LocalDateTime, MonthDay, OffsetDateTime, OffsetTime, Year, YearMonth, ZonedDateTime 등



관련 링크

  • 예제 코드: https://github.com/madvirus/spring5-bv2
  • Bean Validation: http://beanvalidation.org/


  1. 자바덕 2018.02.28 16:27

    안녕하세요. DDD 마스터 최범균님..
    https://github.com/madvirus/spring5-bv2 링크 들어가면 404 나옵니당..
    저만 그런가요! 확인 부탁드립니다.
    항상 좋은 글 감사합니다. (--)(__)

    • madvirus 2018.02.28 16:49 신고

      아,, private으로 놔두고 있었네요. public으로 바꿨습니다.

  2. 홍성민 2020.04.24 17:03

    너무 잘 정리해주셨네요: )
    감사합니다!

반응형

이클립스에서 톰캣 서버를 구동하다보면 아래 그림과 같이 "Server Tomcat Server at localhost was unable to start within 45 seconds" 메시지가 뜨면서 톰캣 서버 실행에 실패할 때가 있다.



이 경우 이클립스의 톰캣 서버 실행 제한 시간을 늘려주거나 실행 시간에 영향을 주는 원인을 제거하면 된다. 이 글에서는 실행 시간을 늘려주는 방법을 설명한다.


먼저 이클립스의 [Window] -> [Show View] -> [Other] 메뉴를 실행한 뒤에 Server/Servers 뷰를 선택하고 오픈한다. Servers 뷰에서 서버를 더블 클릭하면 아래와 같이 편집 영역에 서버 설정 정보가 보인다.



서버 설정 정보에서 Timeouts을 클릭하면 아래 그림과 같이 설정 시간을 변경할 수 있는데 여기서 Start 항목의 시간을 늘려준 뒤 저장(CTRL + S)하면 된다.



톰캣 구동 시간 자체를 줄이는 방법 중 하나로 jar 파일 스캔 제외처리가 있는데 이에 대한 내용은 "Jar 파일 스캔 제외로 톰캣 시작 시간 단축(http://javacan.tistory.com/475)" 글을 참고한다.



  1. sweet 2018.02.27 14:58

    저 같은경우엔 항상 999로 설정해 두는데 프로그램 마다 엄청 무거운것들이 있어서 구동되는데만 5분 이상 걸리니 여유롭게 쓸 수 있더라고요.
    근데 이 시간이 이렇게 999 길게 걸어두었을 때 문제가 되는 부분이 있을까요?
    단순히 시간제한에 대한 설정으로 timeout만 걸리지 않는걸까요?

    • madvirus 2018.02.28 10:34 신고

      999초면 15분 이상으로 잡은거네요. 다소 길게 느껴지긴 합니다. 근데 이 시간은 이클립스의 톰캣 구동 시간 타이아웃이니까 길게 잡는다고 문제가 될 건 없어 보입니다.
      실제 구동 시간이 5분 이상 걸리는 점이 문제가 될 순 있을 것 같아요.

반응형

톰캣 구동 속도에 영향을 주는 것은 jar 파일 탐색이다. 톰캣은 기본적으로 다음의 두 가지를 위해 jar 파일의 클래스나 자원을 스캔한다.

  • 특정 애노테이션을 가진 클래스 검색
  • TLD 파일

위 내용과 상관없는 jar 파일을 스캔 대상에서 제외하면 톰캣 구동 속도가 그만큼 빨라진다.


대상 jar 파일을 찾기 위한 로깅 설정


톰캣을 구동할 때 로그에 아래와 같은 메시지가 출력될 때가 있다.


org.apache.jasper.servlet.TldScanner.scanJars At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.


이 메시지는 웹 어플리케이션에 포함된 jar 파일을 탐색하는데 해당 jar 파일에 커스텀 태그와 관련된 TLD 파일이 없을 때 발생한다. TLD 파일이 없는 jar 파일을 탐색하는 건 시간 낭비이므로 TLD 파일 검색 대상에서 해당 jar 파일을 제외하면 톰캣 시작 시간을 줄일 수 있다.


추가로 웹 어플리케이션과 관련된 애노테이션을 붙인 클래스가 없는 jar 파일을 검색하는 것 역시 톰캣 구동 시간을 느리게 만드므로 이런 jar 파일도 검색 대상에서 제외하면 톰캣 시작 시간을 줄일 수 있다.


이 두 종류의 jar 파일을 찾아내기 위해 [톰캣]/conf/logging.properties 파일에 TLD 관련 로그 레벨을 FINE으로 바꾼다.


# logging.properties 파일 하단에 추가


# 탐색하는 jar 파일 목록 출력

org.apache.catalina.startup.ContextConfig.level = FINE

# TLD 관련 로그 출력

org.apache.jasper.servlet.TldScanner.level = FINE


톰캣을 재시작하면 아래와 같은 로그가 출력되는 것을 확인할 수 있다.


22-Feb-2018 13:25:52.781 미세 [localhost-startStop-1] org.apache.catalina.startup.ContextConfig.processAnnotationsJar Scanning jar file for class files with annotations [file:/C:/Java/jdk1.8.0_51/jre/lib/rt.jar]

...

...

22-Feb-2018 20:42:24.130 미세 [localhost-startStop-1] org.apache.jasper.servlet.TldScanner$TldScannerCallback.scan No TLD files were found in [file:/C

:/apache-tomcat-8.5.27/webapps/sp5-chap09/WEB-INF/lib/spring-expression-5.0.2.RELEASE.jar]. Consider adding the JAR to the tomcat.util.scan.StandardJarScanFilter.jarsToSkip property in CATALINA_BASE/conf/catalina.properties file.


애노테이션 스캔 대상 jar 파일과 TLD 파일이 없는 jar 파일을 확인하고 [톰캣]/conf/catalina.properties 파일에 탐색 생략 대상으로 추가한다. 아래 코드는 JDK의 rt.jar 파일과 spring 관련 jar 파일을 생략 대상에 추가한 예이다.


tomcat.util.scan.StandardJarScanFilter.jarsToSkip=\

bootstrap.jar,commons-daemon.jar,tomcat-juli.jar,\

...생략

xom-*.jar,\

rt.jar,\

spring-*.RELEASE.jar


tomcat.util.scan.StandardJarScanFilter.jarsToScan=\

log4j-web*.jar,log4j-taglib*.jar,log4javascript*.jar,slf4j-taglib*.jar,\

spring-webmvc-*.jar



위 코드에서 tomcat.util.scan.StandardJarScanFilter.jarsToScan 속성(검색 대상)에 spring-webmvc-*.jar 를 추가했는데 이는 spring-webmvc 모듈에 스프링이 제공하는 커스텀 태그와 관련된 TLD 파일이 포함되어 있기 때문이다. spring-webmvc 모듈을 검색 대상에 넣지 않으면 <spring:message>와 같이 스프링이 제공하는 커스텀 태그를 사용할 수 없게 된다.


이클립스에서는 다음과 같이 Servers 프로젝트의 catalina.properties 파일을 수정하면 된다.




catalina.properties 파일에 검색 대상 jar 파일을 설정했다면 다시 톰캣을 구동해보자. 설정 전보다 빠르게 구동되는 것을 확인할 수 있을 것이다.


  1. 윤하민 2019.04.24 14:46

    감사합니다 덕분에 훨씬 빨라졌어요

반응형
  1. 자바덕 2018.02.08 13:10

    DDD 너무 어렵습니다.
    유비쿼터스 언어가 무슨뜻인지 모르겠어요.

  2. 잘 보고 갑니다~~

반응형

기존 코드의 중복 코드: 흐름 제어 중복


최근에 정리한 코드는 다음과 같은 형태를 가졌다.


private ErrorLogger errorLogger;


public Response chooseMeeting(Request req) {

    String apiId = "E0004";

    String code = null;

    String jupno = null;

    String transt = null;

    try {

        jupno = request.getJupno();

        changeTranst(jupno, transt = "0", req.getId(), req.getIp()); // 전송전 상태

        Result result = tranService.chooseMeeting(req); // 전송 처리

        code = eocsResult.getCode();

        changeTranst(jupno, transt = "1", req.getId(), req.getIp()); // 전송완료 상태

    }catch (EocsException ex) {

        transt(jupno, transt = "2", req.getId(), request.getIp()); // 전송오류 상태

        code = ex.getEocsErrorCode();

        errorLogger.append(apiId, code, jupno, transt, req, req.getId(), req.getIp(),....);

    }catch (Exception e) {

        code = "C00003";

        errorLogger.append(apiId, code, jupno, transt, req, req.getId(), req.getIp()....);

    }

    return new Response(apiId , code);

}


private changeTranst(String jupno, String transt, String id, String ip) {

    ...

}


chooseMeeting() 메서드는 다음 흐름을 갖는다.

  1. 전송전 상태 변경: changeTranst()
  2. 실제 전송 업무 처리: tranService.chooseMeeting()
  3. 전송완료 상태 변경: changeTranst()
    1. 전송오류 발생시 전송오류 상태 변경 후 로그 기록
    2. 이 외 익셉션 로그 기록
  4. 결과 리턴

chooseMeeting() 메서드만 이런 코드를 가진 게 아니다. 이와 동일한 흐름을 갖는 메서드가 3개가 더 있었다. 차이점은 apiId와 전송 처리 코드뿐이었다. 예를 들어, 다른 코드는 아래와 같다.


public Response choosePip(PipRequest req) {

    String apiId = "E0003";

    String code = null;

    String jupno = null;

    String transt = null;

    try {

        jupno = request.getJupno();

        changeTranst(jupno, transt = "0", req.getId(), req.getIp()); // 전송전 상태

        Result result = tranService.choosePip(req); // 전송 처리

        code = eocsResult.getCode();

        changeTranst(jupno, transt = "1", req.getId(), req.getIp()); // 전송완료 상태

    }catch (EocsException ex) {

        transt(jupno, transt = "2", req.getId(), request.getIp()); // 전송오류 상태

        code = ex.getEocsErrorCode();

        errorLogger.append(apiId, code, jupno, transt, req, req.getId(), req.getIp(),....);

    }catch (Exception e) {

        code = "C00003";

        errorLogger.append(apiId, code, jupno, transt, req, req.getId(), req.getIp()....);

    }

    return new Response(apiId , code);

}



메서드 추출로 하려다가...


처음엔 대충 다음과 같은 모양을 상상하면서 메서도 추출로 가려했다.


public Response chooseMeeting(Request req) {

    String apiId = "E0004";

    return runTrans(apiId, req.getJupno(), req.getId(), req.getIp(), 

                         () -> tranService.chooseMeeting(req));

}


public Response choosePip(PipRequest req) {

    String apiId = "E0003";

    return runTrans(apiId, req.getJupno(), req.getId(), req.getIp(), 

                         () -> tranService.choosePip(req));

}


private Response runTrans(String apiId, 

                          String jupno, String id, String ip, // 상태처리나 로그 기록에 필요

                          Supplier<Result> transition) {

    String code = null;

    String transt = null;

    try {

        jupno = request.getJupno();

        changeTranst(jupno, transt = "0", id, ip); // 전송전 상태

        Result result = transition.get(); // 전송 처리

        code = eocsResult.getCode();

        changeTranst(jupno, transt = "1", id, ip); // 전송완료 상태

    }catch (EocsException ex) {

        transt(jupno, transt = "2", id, ip); // 전송오류 상태

        code = ex.getEocsErrorCode();

        errorLogger.append(apiId, code, jupno, transt, id, ip,....);

    }catch (Exception e) {

        code = "C00003";

        errorLogger.append(apiId, code, jupno, transt, id, ip,....);

    }

    return new Response(apiId , code);

}


private changeTranst(String jupno, String transt, String id, String ip) {

    ...

}


runTrans() 메서드는 로그를 남기거나 상태 변경을 처리하는 코드에서 필요한 값을 파라미터로 받는데, 파라미터가 다소 많다. 로그를 남길 때 사용할 jupno, id, ip를 구하는 코드가 달라서, 이 세 개 값을 파라미터로 전달해야 했다.


이렇게 되면 코드를 추가로 정리할 때 번잡함이 발생한다. 예를 들어, runTrans()에서 changeTranst()에 의미를 더 부여하기 위해 다음과 같이 변경한다고 하자.


private Response runTrans(String apiId, String jupno, 

                                   String id, String ip, Object req, Supplier<Result> transition) {

    String code = null;

    String transt = null;

    try {

        jupno = request.getJupno();

        transt = beforeTranst(jupno, id, ip); // 전송전 상태

        Result result = transition.get(); // 전송 처리

        code = eocsResult.getCode();

        transt = afterTranst(jupno, id, ip); // 전송완료 상태

    }catch (EocsException ex) {

        errorTranst(jupno, id, ip); // 전송오류 상태

        code = ex.getEocsErrorCode();

        errorLogger.append(apiId, code, jupno, transt, req, id, ip,....);

    }catch (Exception e) {

        code = "C00003";

        errorLogger.append(apiId, code, jupno, transt, req, id, ip,....);

    }

    return new Response(apiId , code);

}


private String beforeTranst(String jupno, String id, String ip) {

    changeTranst(jupno, "1", id, ip);

    return "1";

}

...afterTranst()와 errorTranst()도 비슷하게 구현


private changeTranst(String jupno, String transt, String id, String ip) {

    ...

}


runTrans() 메서드는 에러 로그를 기록하는데 transt 로컬 변수를 사용한다. 그래서 beforeTranst() 메서드는 상태 변경 실행후 transt 로컬 변수에 할당할 값을 리턴해야 한다. 게다가 beforeTranst() 메서드는 changeTranst() 메서드를 호출하기 위해 필요한 값을 파라미터 3개로 받고 있다.


흐름 처리를 수행하는 클래스 작성


메서드 추출로 중복을 없앨 수는 있지만, 그 결과로 만들어진 코드가 이쁘지 않다. 로컬 변수나 파라미터 개수가 많기에 이를 별도 객체로 뽑아서 중복을 없애는 것으로 방법을 바꿨다. 먼저 다음과 같이 프로세스를 처리하는 클래스를 만들었다.


public class TransitionProcess {

    private ErrorLogger errorLogger;


    private String apiId;

    private String jupno;

    private String id;

    private String ip;


    private String transt;

    private String code;


    @Builder

    public TransitionProcess(

            ErrorLogger errorLogger, String apiId,

            String jupno, String id, String ip) {

        this.errorLogger = errorLogger;

        this.apiId = apiId;

        this.jupno = jupno;

        this.id = id;

        this.ip = ip;

    }

    

    public Response runWith(Supplier<Result> transition) {

        try {

            beforeTranst();

            Result result = transition.get();

            code = result.getCode();

            afterTranst();

        } catch (EocsClientException ex) {

            transtError();

            code = ex.getEocsErrorCode();

            appendErrorLog(ex);

        } catch (Exception ex) {

            code = "C00003";

            appendErrorLog(ex);

        }

        return new Response(apiId , code);

    }


    private void beforeTranst() {

        transt = "0";

        changeTranst();

    }


    ...


    private void changeTranst() {

        ... // apiId, jupno, code, transt 등 필요한 값이 필드에 존재

    }


    private void appendError(Exception ex) {

        errorLogger.append(apiId, code, jupno, transt, id, ip, ex);

    }

}


TransitionProcess 클래스는 생성자를 통해서 로그 기록이나 상태 변경에 필요한 값을 받는다. runWith() 메서드는 전송 처리 기능을 함수형 인터페이스로 전달받는다.


상태 변경을 수행하는데 필요한 값(jupno, id, ip, transt)이 필드에 존재하므로 changeTranst() 메서드는 파라미터가 필요없다. 동일하게 beforeTranst() 메서드나 afterTranst() 메서드도 파라미터가 필요 없다.


에러 로그를 남기기 위한 appendError()도 동일하게 에러 로그를 기록하는데 필요한 값 중 Exception을 제외한 나머지는 필드에 존재한다. 그래서 파라미터로 Exception만 받으면 된다.


사실, TransitionProcess의 beforeTrans(), afterTrans(), errorTrans(), appendError() 메서드는 처음부터 존재한 것이 아니고 클래스로 추출한 뒤에 리팩토링하는 과정에서 생긴 것이다. 파라미터로 필요한 값을 전달할 필요가 없기 때문에 코드를 정리하기 더 쉽고, 메서드 호출에 파라미터가 없으므로 코드도 덜 복잡하다.


결과


다음은 흐름 처리 객체를 이용햇서 변경한 코드이다.


public Response chooseMeeting(Request req) {

    String apiId = "E0004";

    TransitionProcess process = TransitionProcess.builder()

        .errorLogger(errorLogger)

        .apiId(apiId)

        .jupno(req.getJupno())

        .id(req.getId())

        .ip(req.getIp())

        .build();

    return process.runWith(() -> tranService.chooseMeeting(req));

}


public Response choosePip(PipRequest req) {

    String apiId = "E0003";

    TransitionProcess process = TransitionProcess.builder()

        .errorLogger(errorLogger)

        .apiId(apiId)

        .jupno(req.getJupno())

        .id(req.getId())

        .ip(req.getIp())

        .build();

    return process.runWith(() -> tranService.choosePip(req));

}


// changeTranst 메서드 제거. TransitionProcess로 이동.


개인적으로는 처음 메서드 추출로 중복을 제거하려고 했던 것보다 더 깔끔하고 프로세스 처리가 별도 클래스로 빠져서 프로세스 분석이나 수정이 용이해진 것 같다.

반응형

개발 중인 서비스에서 통합 테스트를 수행하려면 외부 시스템과의 상호 작용을 통해서만 정상적으로 동작하는 기능이 존재한다. 다음은 그 예이다.(E는 외부 시스템과 연동이 필요한 기능을 의미)

  • 접수함(E) > [배관유무확인요청1상태] > 알림벨(E) > [배관유무확인요청2상태] > 배관있음(E) > [표시/회합 결정대기상태]
여기서 특정 데이터를 표시/회합결정대기상태로 만들려면 내부 시스템의 데이터만 바꿔서는 안 된다. 외부에 있는 시스템의 기능을 순차적으로 실행해야 해당 상태로 바꿀 수 있다. 접수함 행위를 외부 시스템에서 하지 않으면 [배관유무확인요청1상태]로 바뀌지 않는다.

외부 시스템과 연동없이 내부 데이터만 [배관유무확인요청1상태]로 바꿔 알림벨 기능을 통합 테스트하면 정상 동작하지 않는다. 외부 시스템의 데이터 상태가 유효하지 않아 외부 시스템이 에러를 발생하기 때문이다.


통합 테스트나 QA를 하려면 수시로 데이터를 특정 상태로 변경해야 했다. 특정 상태로 변경하려면 올바른 순서대로 기능을 실행해야했는데, 이를 위해 다음과 같이 상태를 변경할 수 있는 코드를 테스트 영역에 추가했다.


EocsTransitionRunner runner = ...;

EocsTransitions trans = EocsTransitionsBuilder.builder()

        .receive()

        .alimBellOfNormalJobgu()

        .assignBlock(...)

        .pipeYes()

        .needMeetJobpr20()

        .lipynA()

        .build();


runner.run(trans, jupno, empId);


상태를 변경하는 것을 Transition(전이)로 표현했고, EocsTransitions은 특정 상태로 가기 위한 Transition 목록을 담는다. 메서드 호출로 전이를 표현했고 상태에 따라 알맞은 전이 목록을 생성하도록 했다.


예를 들어, receive() 메서드는 접수를 의미하고, receive() 다음에는 alimBellOfNormalJob()이나 alimBellOfSmallOrEmergent()만 호출할 수 있게 했다. alimBellOfNormalJob() 메서드 호출 다음에는 assignBlock()만 호출 가능하고, 이어서 pipeYes()나 pipeNo()만 호출 가능하게 했다.


EocsTransitionsBuilder는 상태 전이를 어떻게 할지를 기술하는 EocsTransitions를 생성하고 상태 전이는 EocsTransitionRunner라는 별도의 실행기로 처리했다. 그래서 EocsTransitionRunner#run()을 호출하지 않으면 실제 상태 전이는 발생하지 않는다.


EocsTransitions과 EocsTransition


EocsTransitions과 EocsTransition은 간단한 전이 과정을 담기 위한 간단한 클래스이다.


EocsTransition은 다음과 같다.


public class Transition {


    private TranstionType type;

    private Map<String, String> props = new HashMap<>();

    

    public Transition(TranstionType type) {

        this.type = type;

    }

    

    public TranstionType type() {

        return type;

    }


    public Transition addProp(String prop, String value) {

        props.put(prop, value);

        return this;

    }


    public String getProp(String prop) {

        return props.get(prop);

    }


    public String getPropOrEmpty(String prop) {

        String value = getProp(prop);

        return value == null ? "" : value;

    }

}


상태 전이에 따라 추가적인 데이터가 필요한데 이를 props라는 Map에 담도록 했다.


TransitionType은 열거 타입으로 각 전이를 값으로 표현한다.


public enum TranstionType {


    RECEIVE, 

    ALIMBELL, ALIMBELL_OF_SMALL_OR_EMERGENT,

    ASSIGN_BLOCK,

    PIPE_Y, PIPE_N, PIPE_S, PIPE_T,

    JOBPR_20, JOBPR_40, JOBPR_50,

    LIPYN_A, LIPYN_B, LIPYN_C, LIPYN_D, LIPYN_Y, LIPYN_N, 

    

    EOCS_POS_DISP, EOCS_COMPLETE,

}


EocsTransitions는 Transition 목록을 담는 간단한 클래스이다.


public class EocsTransitions {


    private List<Transition> transitions;


    public EocsTransitions(List<Transition> transitions) {

        this.transitions = Collections.unmodifiableList(transitions);

    }


    public List<Transition> transitions() {

        return transitions;

    }

}


상태 전이 생성 위한 EocsTransitionsBuilder와 관련 클래스


EocsTransitionsBuilder와 관련 클래스 일부를 아래 표시했다.




최초 상태는 S20PipeConfirmBuilder인데, 이는 EocsTransitionBuilder의 정적 메서드인 receive()가 생성한다.


EocsTransitionsBuilder.receive() // S20PipeConfirmBuilder 리턴


AbstractTransitionBuilder를 상속받은 각 클래스는 특정 상태에서 가능한 전이만을 제공한다. 예를 들어, S30PipeConfirmBuilder는 상태 값이 "30"인 배관유무확인2 상태에서 선택할 수 있는 전이 네 개(pipeYes, pipeNo, pipeSelf, pipeTest)를 제공한다. 비슷하게 S30PipeConfirmBeforeAssignBlockBuilder는 한 개의 상태 전이인 assignBlock()만 제공한다.


전이를 위한 메서드는 다음 상태를 위한 Transition Builder를 리턴한다. 각 Transition Builder는 해당 상태에서 가능한 전이만 제공하므로, 잘못된 전이 경로를 설정할 수 없다. 예를 들어, S20PipeConfirmBuilder의 alimBellOfNormalJobgu() 메서드는 S30PipeConfirmBeforeAssignBlockBuilder를 리턴하므로 alimBellOfNormalJobggu() 메서드 호출 다음에는 assignBlock() 메서드만 호출할 수 있다. 다른 메서드는 호출할 수 없다.


EocsTransitions trans = EocsTransitionsBuilder

    .receive() // S20PipeConfirmBuilder 리턴

    .alimBellOfNormalJobgu() // S30PipeConfirmBeforeAssignBlockBuilder 리턴

    .assignBlock(...) // S30PipeConfirmBuilder 리턴

    .pipeYes() // S40DispMeetWaitBuilder 리턴

    .build();



EocsTransitionsBuilder 클래스


public class EocsTransitionsBuilder {


    public static EocsTransitionsBuilder builder() {

        return new EocsTransitionsBuilder();

    }


    private List<Transition> transitions = new ArrayList<>();


    public S20PipeConfirmBuilder receive() {

        addTransition(new Transition(TranstionType.RECEIVE));

        return new S20PipeConfirmBuilder(this);

    }


    public void addTransition(Transition transition) {

        transitions.add(transition);

    }


    public EocsTransitions build() {

        return new EocsTransitions(transitions);

    }

}


EocsTransitionsBuilder 클래스의 receive() 메서드는 TransitionType.RECEIVE을 값으로 갖는 최초 상태 전이를 추가하고, 첫 번째 상태를 위한 S20PipeConfirmBuilder 객체를 리턴한다.


AbstractTransitionBuilder 클래스


AbstractTransitionBuilder은 각 Transition Builder가 필요로 하는 기능을 제공한다.


public static abstract class AbstractTransitionBuilder {

    private EocsTransitionsBuilder builder;


    public AbstractTransitionBuilder(EocsTransitionsBuilder builder) {

        this.builder = builder;

    }


    public final EocsTransitions build() {

        return builder.build();

    }


    protected final void addTransition(Transition transition) {

        builder.addTransition(transition);

    }


    protected final EocsTransitionsBuilder getBuilder() {

        return builder;

    }

}


build() 메서드는 생성자로 전달받은 EocsTransitionsBuilder의 build()를 호출하는데, 이 메서드가 있어 전이 생성 과정에서 언제든지 EocsTransitions를 생성할 수 있다.


개별 Transition Builder 클래스


개별 Transition Builder 클래스는 AbstractTransitionBuilder 클래스를 상속받아 구현했고, 상태 전이를 위한 메서드를 제공했다. 상태 전이 메서드는 addTransition() 메서드를 이용해서 알맞은 상태 전이를 추가하고 다음 Transition Builder를 리턴한다. 다음은 예이다.


public static class S20PipeConfirmBuilder extends AbstractTransitionBuilder {


    public S20PipeConfirmBuilder(EocsTransitionsBuilder builder) {

        super(builder);

    }


    public S30PipeConfirmBeforeAssignBlockBuilder alimBellOfNormalJobgu() {

        addTransition(new Transition(TranstionType.ALIMBELL));

        return new S30PipeConfirmBeforeAssignBlockBuilder(getBuilder());

    }


    public S60EnterWaitBeforeAssignBlockBuilder alimBellOfSmallOrEmergent() {

        addTransition(new Transition(TranstionType.ALIMBELL_OF_SMALL_OR_EMERGENT));

        return new S60EnterWaitBeforeAssignBlockBuilder(getBuilder());

    }

}


최종 상태를 위한 Transition Builder는 더 이상 진행할 전이가 없으므로 다음과 같이 전이를 위한 메서드가 없다.


public static class S90DoneBuilder extends AbstractTransitionBuilder {

    public S90DoneBuilder(EocsTransitionsBuilder builder) {

        super(builder);

    }

}


실행기

실행기는 다음과 같다..


public class EocsTransitionRunner {

    public EocsTransitionRunner(...의존) {

        ...의존주입

    }


    public void run(EocsTransitions trans, String jupno, String empId) {

        trans.transitions().forEach(tran -> {

            runTransition(tran, jupno, empId);

        });

    }


    private void runTransition(Transition tran, String jupno, String employeeId) {

        switch (tran.type()) {

        case RECEIVE:

            initEocs(jupno, DigWorkPathFlag.EOCS_1.cd());

            break;

        case ALIMBELL:

        case ALIMBELL_OF_SMALL_OR_EMERGENT:

            alimBell(jupno, employeeId, "01012345678")

            break;

        case ASSIGN_BLOCK:

            assignBlock(tran, jupno, employeeId);

            break;

        case PIPE_Y:

        ...생략

        case PIPE_T:

            doPipeynProcess(jupno, employeeId, tran);

            break;

        case JOBPR_20:

        case JOBPR_40:

        case JOBPR_50:

            doJobprProcess(jupno, employeeId, tran);

            break;

        case LIPYN_A:

        ...생략

        case LIPYN_N:

            doLipynProcess(jupno, employeeId, tran);

            break;

        case EOCS_POS_DISP:

            unsupport(tran.type());

            break;

        case EOCS_COMPLETE:

            skip(tran.type());

            break;

        }

    }


run() 메서드는 EocsTransitions에 보관된 Transition 목록을 구해서 차례대로 전이를 실행한다. runTransition() 메서드는 각 전이 타입에 따라 알맞은 기능을 실행해서 상태를 알맞게 변경한다. 상태 전이를 수행하는데 필요한 의존은 생성자를 통해서 전달받았다.


이제 데이터를 특정 상태로 맞추고 싶으면 다음과 같은 코드를 사용해서 상태를 변경할 수 있다.


EocsTransitionRunner runner = new EocsTransitionRunner(...);


EocsTransitions data1Trans = EocsTransitionsBuilder.builder() // 전이 정의

        .receive()

        .alimBellOfNormalJobgu()

        .assignBlock(...)

        .pipeYes()

        .build();

runner.run(data1Trans, jupno1, empId); // 전이 실행


EocsTransitions data2Trans = EocsTransitionsBuilder.builder() // 전이 정의

        .receive()

        .build();


runner.run(data2Trans, jupno2, empId); // 전이 실행


각 상태별로 알맞게 전이를 지정할 수 있게 하려고, 상태마다 클래스를 추가했지만, 상태 초기화하는 코드를 보면 쉽게 상태 초기화가 어떤 과정으로 이루어지는지 알 수 있게 되었고, 특정 상태로 맞추는 코드 역시 코드 자동 완성 기능으로 쉽게 정의할 수 있게 되었다.


반응형


"OKKYCON 2017 소통, 개발에 숨을 불어넣다"에서 집중해서 들은 세션 내용 요약:


개인적 소감은 "가길 잘했다"!


XP 더 나은 소프트웨어 품질을 위한 일의 방법 (정윤진)


프로젝트를 통해 배운 것

  • 작은 규모 팀이 효율적
  • 다른 전문성이 모여 어려운 문제 해결
  • 문제를 즉각 공유해야 빠른 해결 가능
  • 팀 구성원의 성향이 다르면 감정적 문제가 커지는 경향
  • 스타트업은 제품보다는 보통 팀이 깨져서 무너짐
작은 팀은 소통에 도움이 됨
  • 소통 횟수가 줄어듬
  • 팀이 커지면 소통 비용 증가(허락을 구하기 위한 미팅 시간 증가)
  • 팀이 작으면 쉽게 공유
밸런스팀
  • 매니저, 사용자, 엔지니어, 데이터 사이언스 등으로 구성된 팀
  • 가치: 신뢰, 경험 공유(배움), (작은) 실패를 환영하는 문화, 다양한 목소리
  • 적은 스트레로 더 대단한 걸 더 빠르게 이룸
XP/TDD
  • 결국 모든 것은 빠르고 안전하게 제품을 내보내기 위함
  • 예: TDD - 여러 이유로 테스트 작성을 포기하지만, 지속적으로 빠르게 가려면 테스트 필수
  • 예: 페어 - 돈이 아까울 수 있으나, 2명이 같이 일하면 생산성은 20% 떨어지나 품질은 80% 향상
피보탈 업무 패턴
  • 8:30 아침 식사 (대화)
  • 9:00 스탠드업 (5분 진행)
  • 9:05 팀 스탠드업 (페어 결정), 해결할 스토리는 트래커에 존재, 늦어도 9:30분을 넘기지 않음
  • 9:20 페어 업무 시작
  • 11:30 점심 식사
  • 13:00 오후 시작
  • 18:00 아무도 없음

채용시 하루 동안 페어를 하고 결정한다는 것이 인상적



애자일은 애자일이란 단어를 버려야 한다. (신정호)


애자일의 형식적인 부분을 버려야 함. 형식으로 망하는 프로젝트 징후:

  • 애자일에 대한 맹신
  • 목표 없는 작은 주기
  • 의미 없는 회의
  • 기준 없는 추정
  • 자발적이지 않은 교육
  • 겉치레 시스템 평가
역할

직책보단 역할에 집중

  • 스크럼 마스터니 PO니 하는 직책보다는 "파악, 결정, 책임" 역할을 수행해야 함
파악
  • 파악을 하지 못하면 결과적으로 결정할 수 없음
  • 파악하기 위한 도구: 정책, 요구사항 기록, 통신 프로토콜, UI/UX, 진행사항, 테스트 케이스, 커밋기록, 현황판 중심 데일리 미팅, 리스트 목록 등
  • 팀내/팀외적으로 놓치는 일이 없는지 계속 확인 필요
결정
  • 결정에 필요한 배경, 결정할 케이스를 객관식으로 제공
책임
  • 파악 -> 좋은 결정 -> 책임은 작아짐(없어짐)
구성원(내부/외부 포함) 간 협업, 부딪칠 수 있는 경우를 정리
  • 기획자 vs 디자이너
    • 기획자는 와이엎프레임 없이 요구사항 위주 정리, 디자이너는 사용자에 맞는 디자인
  • 기획자 vs 개발자 vs 품질 책임자
    • 테스트케이스 중심 개발(상호 간 인식 차이 줄임), 테스트케이스 공유 시간
  • 기획자 vs 품질 책임자
    • 정책이 정해지지 않은 경우 데일리 정책 회의
  • 디자이너 vs 개발자
    • TODO 목록
  • 최고의사결정자 vs 팀/TFT
    • 일정 관련 정량적인 수치 기반 상황 공유
  • 팀 vs 팀

스케줄링


중요한 건 결국 정량적 파악. 다음을 고려:

  • 근무일수, 휴가, 테스트 일수
  • 개발 완료 기준
  • 늘 어디쯤인지 파악할 수 있어야 함

스트린트 회의

  • 정책과 요구사항 발표
  • 구현 이슈 논의
  • 모호한 상황을 명확하게 파악하고 결정
  • 회고를 했다면 이어서 진행
스트린트
  • 기획 중심 기간, 구현 중심 기간, 테스트 기간 중심에 따라 알맞게 진행
    • 예, 기획 중심 기간에는 부문 미완성이지만 흐름을 먼저 보임
    • 예, 구현 중심 기간에는 부문 완성으로 기능을 먼저 보임

회고


회고는 해결한 것과 해결할 것을 명확히 하는 시간

  • 진행 상황 공유
    • 목표로 했는데 한 것 못 한 것 공유
    • 결과 보기(가장 중요)
    • 이슈 현황판, 데일리 미팅 기록
    • 남은 일(테스트케이스로 관리)
  • 리스크 확인
  • 마지막 스트린트에 좋은 점, 나쁜 점(감정 고려)


애자일은 말이나 형식이 아닌 행동으로 보여주는 것



협업도구로 제대로 말하기 (김동수)


협업 도구를 제대로 쓰게 하는 것이 어려웠음

  • 구성원들의 변화 필요성이 있거나 확실히 지금 문제를 해결할 솔루션이 되는 경우, 정착할 가능성이 높아짐

협업 도구를 변경하고 싶다면

  • 협업을 더디게 만드는 것을 식별
  • 구성원의 요구사항 수집
  • 영향력있는 조력자(대표, staff 부서) 필요
  • 비용 발생시, 최종 결정권자를 설득

협업 도구 평가 기준

  • 사용이 쉬운가
  • 검색할 수 있는가
  • 공유가 쉬운가
  • 다른 도구와 연동
  • 사용료 대비 가치가 충분한가

마켓컬리 적용

  • 도구에 대한 교육 진행
  • 도구에 대한 계정 관리를 관리팀에 맡김
  • 프로필 사진은 소통에 도움
도구로서의 말
  • 상대방이 이해할 수 있는 단어
  • 단어가 정확해야 함
  • 날짜가 정확
  • 검색 가능하게
  • 에티켓


생산성 지향의 커뮤니케이션과 기업 문화(최영근)


커뮤니케이션

  • 소프트 스킬 > 하드 스킬 (소프트 스킬이 18% 더 높음)
  • 소통 == passive skill
    • 소통에서 90% 전달은(10% 손실) 매우 낮은 완성도임, 6회 쌓이면 53%
  • 소통은 정보 전달 목적 -> 청자 중심

소통법

  • 경영진에게
    • 신속한 의사 결정 목적 -> 단기적 효과, 숫자 사용
    • 집행 요약(executive summary) 사용
  • 세일즈
    • '고객이 원하잖아요'에 숨어있게 하지 말라
    • 요구를 끌어내는 대화 (목적, 대안 등을 끌어낼 필요)
  • 자기 자신
    • 철저한 자기 검열 필요 (나를 위한 결정은 아닌지)
    • 성과를 위한 결정인가?

관성, 조직

  • 규칙화, 문서화(성문화), 선언 --> 무언의 압박
  • 넥플릭스 사례: 스포츠팀처럼
    • 계약 관계, 전문가, 조직의 비즈니스 목표 달성
  • 유비쿼터스 언어
  • 회의록 작성
    • goal, discussion point, agreement, action item을 담음
  • 박터지게 싸우자
    • 팁1, 우리 잘되려고 하는 거지 --> 나 자신도 흥분을 가라 앉히는 효과
    • 팁2, 설득되면 확실하게 리액션
  • 메신저 : 정원사 필요
    • 정원사는 검열을 하지 않는다는 것을 느끼게 해야 함

협업의 미신 5가지 - 근거 기반 협업으로 가기 위해 (김창준)


미신1 : 프로그래밍은 잘 하는데 협업은 잘 못 해? 또는 반대 협업은 잘 하는데 프로그래밍은 딸려?


프로그램과 협업은 별개라는 사고!


뿌리:

  • 잘못된 연구 방식 (1960년대)
    • 뛰어난 프로그래밍을 측정한 기준이 잘못 됨(예, 문제를 잘 푼 사람이 뛰어난 프로그래머라 가정하는 식)
    • 한 명으로 실험실에서 혼자두고 측정

1990년대부터 여러 명을 두고 측정해 보니,

  • 뛰어난 프로그래머 그룹의 조언 : 사회적 조언 포함 (70%)
  • 평균적인 그룹의 조언 : 사회적 조언이 적음 (20%)
  • 실제로 성과를 잘 내는 프로그래머일수록 대화에 많은 시간 투입
  • 프로그래밍을 재정의해야 하는 순간이 옴
협력도 프로그래밍의 한 부분으로 인지하기 시작


협업은 프로그래밍을 잘 하는데 있어 필수적인 요소



미신2 : 팀의 퍼포먼스는 가장 뛰어난 사람이 결정


MIT의 연구:

  • CQ(집단 지성) - 어떤 팀은 어떤 일을 줘도 잘 하고, 어떤 팀은 어떨 일을 줘도 못함
    • 가장 잘 하는 사람, 평균 IQ도 팀의 성과에 영향을 주지 못함
  • CQ 성과에 영향을 주는 지표 
    • 공감력
    • 말을 골고루 함(여러 명이 골고루 할 때)
구글 연구: 탁월한 팀의 특정 찾기
  • 팀의 성과는 개기인의 기술적 전문성이 중요하지 않음,
  • 심리적 안정감이 높을수록 성과가 좋음

일이 복잡할수록 더 그러함


미신3 : 전문가를 모아두면 협력을 잘 할 것이다


  • 전문성에서 사회적인 요소를 빼고 봄
  • 오래 본다고 협력이 늘지 않음 (예, 양치질), 협력 훈련을 받은 적이 거의 없음
  • 협력할지 논의한 뒤에 일을 진행하면 성과가 좋아질 수 있음

의식적인 협력의 중요성 : 어떻게 협력할까를 먼저 고민



미신4 : 협력을 잘 하는 것은 서로 좋은 관계를 유지하는 것이다


  • 미신: 협력 잘 하는 것 = 인간 관계 좋은 것 = 술 자주 마시는 것
  • 회피적으로 좋은 관계인지 고민 필요 -> 갈등을 처리하는데 능숙하지 못함
  • (건설적으로) 부정적인 감정을 얘기할 수 있는 팀이 성과가 좋음, 솔직하게 불만을 얘기할 수 있어야 좋은 관계

미신5 : 분업을 잘 하는 것이 협업을 잘 하는 것이다

  • 분업 외에는 협업에 대한 아이디어가 없음
  • 리차드 행만: 
    • 팀과 워크 그룹 구분
      • 팀 : 구성원 사이에 네트워크로 연결
      • 워크 그룹 : 구성원 사이에 트리/스타 구조로 연결
    • 팀의 성과가 압도적으로 높음
    • 일이 불확실할수록 팀의 성과가 높음
  • 왜 잘하게 되느냐?
    • 기본적으로 피어 코칭 때문(동료 간 소통)
  • 공통점, 팀의 효과성을 예측하는 변수 : 상호 간 모니터랑하는 팀 -> 성과가 높음
    • 서로 의견 제시하는 팀

70 20 10

    • 성과의 70%는 팀원이 만나기 전에 이미 결정
    • 20%는 시작하는 시점에 결정
    • 10%는 일을 하는 과정에서 결정
  • 미리 사전에 어떻게 일할 지를 미리 정하고, 초반에 굉장히 신경 써야 함
분업을 너무 많이 해서 문제가 된 에피소드: "131번 서버 누구 담당입니까?"


상호 작용이 중요함


QA에 나온 이야기

  • 조직의 미션: 개인의 일을 한다는 느낌을 없애야 함!
    • 우리 팀의 최고 우선순위가 뭡니까?
  • 사람이 계속 바뀌는 환경에서는 더더욱 사회적인 요소에 신경을 많이 써야 함
  • 의견 내기가 무섭다 --> 일단 우리 안에서 해보는 것!
    • 예: 단계를 구분 - 아이디어를 내는 것과 누가/어떻게 할 지를 정하는 것을 구분
      • 누가 할지 고를 때 팀이 함: 독박이 아니고 팀원이 협력하게 유도
      • 누군가 해야 하는데 하기 싫은 일 -> 팀이 회의를 해서, 어떻게 전환을 하면 재미있어질 것인지 논의
  • 문화 -> 작은 상호작용이 쌓이면서 주변에 영향을 미침


반응형
반응형

HTTP 요청 헤더를 이용해서 접속한 사용자 정보를 받기로 했다. 받아야 할 정보가 두 개여서 스프링 컨트롤러에서 다음과 같은 코드를 사용하게 되었다.


@RestController

public class HandoverApi {


    private HandoverService handoverService;


    @PostMapping("/api/handover")

    public HandoverPk postHandover(

            @RequestHeader(name = "employeeId", required = false) String employeeIdHeader,

            @RequestParam(name = "employeeId", required = false) String employeeIdParam,

            @RequestHeader(name = "cellphone", required = false) String cellphoneHeader,

            @RequestParam(name = "cellphone", required = false) String cellphoneParam,

            @Valid HandoverRequest req) throws ServletRequestBindingException {

        if (StringUtils.isEmpty(employeeIdHeader) && StringUtils.isEmpty(employeeIdParam)) {

            throw new ServletRequestBindingException("Missing employeeId");

        }

        if (StringUtils.isEmpty(cellphoneHeader) && StringUtils.isEmpty(cellphoneParam)) {

            throw new ServletRequestBindingException("Missing cellphoneParam");

        }

        String employeeId = employeeIdParam != null ? employeeIdParam : employeeIdHeader;

        String cellphone = cellphoneParam != null ? cellphoneParam : cellphoneHeader;

        req.setEmployeeId(employeeId);

        req.setCellphone(cellphone);


        return handoverService.handoverDigWork(req);

    }


employeeId와 cellphone 값을 요청 헤더나 요청 파라미터로 받을 수 있도록 했다. 헤더와 파라미터를 함께 사용할 수 있게 해서 코드가 다소 장황해졌다. 문제는 이런 코드가 계속해서 중복해서 출현하게 되었다는 것이다. employeeId와 cellphone 값이 필요한 API에서 이런 장황하면서 보기 싫은 코드를 중복해서 사용하게 되었다.


이런 중복을 없애기 위해 스프링의 커스텀 HandlerMethodArgumentResolver를 사용했다. 원하는 코드 모양은 다음과 같다.


@RestController

public class HandoverApi {


    @PostMapping("/api/handover")

    public HandoverPk postHandover(

            @MobileUser(check = MobileUserCheck.ALL) MobileUserInfo mobUserInfo,

            @Valid DigWorkHandoverRequest req) {

        req.setEmployeeId(mobUserInfo.getEmployeeId());

        req.setCellphone(mobUserInfo.getCellphone());


        return handoverService.handoverDigWork(req);

    }


@MobileUser 애노테이션이 붙은 MobileUserInfo 타입 파라미터에 자동으로 employeeId와 cellphone 값이 담기도록 스프링을 확장하는 것을 목표로 했다.


파라미터에 사용할 MobileUserInfo와 @MobileUser


먼저 employeeId와 cellphone을 담을 데이터 클래스를 하나 만들었다.


@Getter

@AllArgsConstructor

@ToString

public class MobileUserInfo {

    private String employeeId;

    private String cellphone;

    

    public void checkEmployeeId() throws ServletRequestBindingException {

        if (StringUtils.isEmpty(employeeId))

            throw new ServletRequestBindingException("Missing employeeId");

    }

    

    public void checkCellphone() throws ServletRequestBindingException {

        if (StringUtils.isEmpty(cellphone))

            throw new ServletRequestBindingException("Missing cellphone");

    }


    public void checkAll() throws ServletRequestBindingException {

        checkEmployeeId();

        checkCellphone();

    }

    

}


@MobileUser 애노테이션은 다음과 같이 정의했다.


@Target(value = { ElementType.PARAMETER })

@Retention(value = RetentionPolicy.RUNTIME)

public @interface MobileUser {


    MobileUserCheck check() default MobileUserCheck.EMPLOYEE_ID;


}


check 속성은 값을 어디까지 검사할지 여부를 지정하기 위한 용도로 사용한다. 이 예제의 경우 employeeId만 필요한 경우가 있고, cellphone만 필요한 경우가 있고, 둘 다 필요한 경우도 있다. 이를 필요에 따라 검사 대상을 지정할 수 있도록 check 속성을 추가했다. MobileUserCheck 열거 타입은 검사할 대상을 포함한다.


public enum MobileUserCheck {

    ALL, EMPLOYEE_ID, CELLPHONE, NONE

}


커스텀 HandlerMethodArgumentResolver 구현


스프링이 컨트롤러 메서드의 인자로 MobileUserInfo 타입 객체를 받을 수 있으려면 HandlerMethodArgumentResolver의 구현체를 알맞게 제공해야 한다. 예제를 위한 구현체는 다음과 같다.


public class MobileUserInfoResolver implements HandlerMethodArgumentResolver {

    @Override

    public boolean supportsParameter(MethodParameter parameter) {

        MobileUser mobUserAnnot = parameter.getParameterAnnotation(MobileUser.class);

        return mobUserAnnot != null && 

                 MobileUserInfo.class.isAssignableFrom(parameter.getParameterType());

    }


    @Override

    public Object resolveArgument(MethodParameter parameter, 

            ModelAndViewContainer mavContainer,

            NativeWebRequest webRequest, WebDataBinderFactory binderFactory) 

            throws Exception {

        String employeeIdHeader = webRequest.getHeader("employeeId");

        String employeeIdParam = webRequest.getParameter("employeeId");

        String cellphoneHeader = webRequest.getHeader("cellphone");

        String cellphoneParam = webRequest.getParameter("cellphone");


        String employeeId = employeeIdParam != null ? employeeIdParam : employeeIdHeader;

        String cellphone = cellphoneParam != null ? cellphoneParam : cellphoneHeader;


        MobileUser mobUserAnnot = parameter.getParameterAnnotation(MobileUser.class);

        MobileUserInfo mobileUserInfo = new MobileUserInfo(employeeId, cellphone);

        switch (mobUserAnnot.check()) {

        case ALL:

            mobileUserInfo.checkAll();

            break;

        case EMPLOYEE_ID:

            mobileUserInfo.checkEmployeeId();

            break;

        case CELLPHONE:

            mobileUserInfo.checkCellphone();

            break;

        default:

            break;

        }

        return mobileUserInfo;

    }

}


supportsParameter() 메서드는 컨트롤러 메서드의 특정 파라미터를 지원하는지 여부를 리턴한다. 이 예제의 경우 파라미터에 MobileUser 애노테이션이 붙이 있고 파라미터 타입이 MobileUserInfo인 경우 true를 리턴하도록 구현했다.


resolveArgument() 메서드는 파라미터에 전달할 객체를 생성한다. 이 예에서는 employeeId와 cellphone 값을 요청 헤더나 요청 파라미터에서 읽어와 MobileUserInfo를 생성하고, @MobileUser 애노테이션의 check 값에 따라 값 검사를 수행한 뒤에, 검사에 통과하면 MobileUserInfo를 리턴한다.


WebMvcConfigurer로 설정하기


마지막으로 준비할 작업은 커스텀 HandlerMethodArgumentResolver를 사용하도록 스프링 MVC를 설정하는 것이다. @EnableWebMvc나 스프링 부트를 사용한다면 다음과 같이 WebMvcConfigurer 구현 클래스를 사용해서 커스텀 HandlerMethodArgumentResolver를 등록하면 된다.


@Configuration

public class WebMvcCustomConfiguration extends WebMvcConfigurerAdapter {


    @Override

    public void addArgumentResolvers(

            List<HandlerMethodArgumentResolver> argumentResolvers) {

        argumentResolvers.add(new MobileUserInfoResolver());

    }

    

}



커스텀 인자 사용


이제 컨트롤러 메서드의 파라미터로 커스텀 인자 타입을 사용하면 된다.


@PostMapping("/api/handover")

public HandoverPk postHandover(

        @MobileUser(check = MobileUserCheck.ALL) MobileUserInfo mobUserInfo,

        @Valid DigWorkHandoverRequest req) {

    req.setEmployeeId(mobUserInfo.getEmployeeId());

    req.setCellphone(mobUserInfo.getCellphone());


    return handoverService.handoverDigWork(req);

}


@GetMapping(value = "/api/checks")

public yCheckData getCheckData(@MobileUser MobileUserInfo userInfo) {

    CheckData checks = checkService.getChecks(userInfo.getEmployeeId());

    return checks;

}


반응형

Spring Data Jpa의 Speicfication을 애용하는 편인데, 이 Specification을 사용해서 조건을 조합하다보면 다음과 같은 코드를 종종 작성하게 된다. (관련 내용은 http://javacan.tistory.com/entry/SpringDataJPA-Specifcation-Usage 참고)


Specifications<Check> specs = Specifications.where(

    CheckSpecs.yearQuarter(searchRequest.getYear(), searchRequest.getQuarter()));


if (searchRequest.hasTeamCd())

    specs = specs.and(CheckSpecs.teamCd(searchRequest.getTeamCd()));

if (searchRequest.hasPlanDate())

    specs = specs.and(CheckSpecs.planDate(searchRequest.getPlanDate()));


List<Check> checks = checkRepository.findAll(specs);


if 절과 각 Spec을 and로 엮는 코드가 실수하기 좋게 되어 있다. 이를 보완하고자 SpecBuilder라는 보조 클래스를 하나 만들었다. 이 클래스를 사용하면 위 코드를 다음과 같이 변경할 수 있다.


Specification<Check> spec = SpecBuilder.builder(Check.class)

        .and(CheckSpecs.yearQuarter(searchRequest.getYear(), searchRequest.getQuarter()))

        .whenHasText(searchRequest.getTeamCd(), str -> CheckSpec.teamCd(str))

        .whenHasText(searchRequest.getPlanDate(), CheckSpec::planDate)

        .toSpec();


.List<Check> checks = checkRepository.findAll(specs);


단순히 and로 조합하는 경우, if를 사용할 때보다 코드를 보기가 더 좋아졌다.


SpecBuilder의 완전한 코드는 다음과 같다.


public class SpecBuilder {


    public static <T> Builder<T> builder(Class<T> type) {

        return new Builder<T>();

    }


    public static class Builder<T> {

        private List<Specification<T>> specs = new ArrayList<>();


        public Builder<T> and(Specification<T> spec) {

            specs.add(spec);

            return this;

        }


        public Builder<T> whenHasText(String str, 

                 Function<String, Specification<T>> specSupplier) {

            if (StringUtils.hasText(str)) {

                specs.add(specSupplier.apply(str));

            }

            return this;

        }


        public Builder<T> when(String str, 

                 Function<String, Specification<T>> specSupplier) {

            specs.add(specSupplier.apply(str));

            return this;

        }

        

        public Builder<T> whenHasTextThenBetween(String from, String to,

                BiFunction<String, String, Specification<T>> specSupplier) {

            if (StringUtils.hasText(from) && StringUtils.hasText(to)) {

                specs.add(specSupplier.apply(from, to));

            }

            return this;

        }


        public Builder<T> whenIsTrue(Boolean cond,

                Supplier<Specification<T>> specSupplier) {

            if (cond != null && cond.booleanValue()) {

                specs.add(specSupplier.get());

            }

            return this;

        }


        public Specification<T> toSpec() {

            if (specs.isEmpty())

                return Specifications.where(null);

            else if (specs.size() == 1)

                return specs.get(0);

            else {

                return specs.stream().reduce(

                        Specifications.where(null),

                        (specs, spec) -> specs.and(spec),

                        (specs1, specs2) -> specs1.and(specs2));

            }

        }

    }

}


+ Recent posts