Specification


1. Intro

개발 경력이 많지는 않지만 백엔드 개발을 하며 느낀 분석, 설계, 문서화에 대해 기술한 포스트입니다. 의식의 흐름대로 작성한 내용이라 부족한 부분이 많을 수 있는데 잘 못된 내용이나 수정되어야하거나 추가되어야 할 내용이 있다면 글을 읽고 지적 부탁드리고, 문의를 남겨주시면 최대한 답변드리도록 하겠습니다.

인간은 망각의 동물이다. 본인이 개발을 하더라도 시간이 지난다면 왜 그런식으로 개발을 했는지 잊어버리는 경우가 많다. 본인의 개발을 한 내용에 대해서도 그런데 다른사람이 개발한 내용은 어떠할까? 그 사람이 개발한 내용을 알기 위해 개발한 당사자를 찾아가서 왜 그런식으로 개발했는지, 어떤 이유가 있어서 이런 행위를 하는지 히스토리 확인을 해야된다. 이것도 그 사람과 알고있다는 가정하에 아주 좋은 케이스다. 다른사람에게 물어봤더니 그 사람도 왜 그렇게 했는지 기억해는데 한참이 걸리고, 개발한 당사자에게 물어볼 수 없는 상황이 많다. 이러한 이유때문에 기능개발을 할 때는 그 기능이 어떤 행위를 하는지에 대해 문서로 남겨놔야하고, 그 문서를 통해 본인 혹은 다른 사람들이 그 기능이 어떤 행위를 하는지 알 수 있게 해야한다.

또한, 기능에 대한 문서를 작성하다 보면 그 기능이 해야하는 행위가 더욱 명확해지는 경우가 많다. 기능의 행위가 명확해지면 개발범위에 대해서 파악하는 것이 좀 더 수월해지고, 어떤식으로 개발을 할 지에 대한 설계가 가능해진다. 개발의 설계가 된다는 것은 기능 분리 및 관심사 분리가 가능해지는 것이고, 이는 이러한 과정을 거치지 않은 코드보다 뛰어난 품질의 코드를 작성하는데 도움이 된다.

물론 본인의 기억력에 대해서 엄청난 자신감을 가지고 있고, 다른 사람에게 그 기능이 무슨 행위를 하는지 전달할 필요나 생각이 없는 경우, 문서를 작성하지 않아도 머리속에서 설계가 가능하다면 문서를 작성할 필요는 없다.

이번 포스트에서는 웹 프로그램의 백엔드 기능을 개발하는 과정에서 기능을 분석하고 이를 문서로 남기는 방법에 대해 작성을 한다.

2. 기능 분석 및 설계

개발을 진행하면서 개발하는 기능에 대해서 분석행위를 하지 않는 사람은 없을 것이다. 분석행위를 하지 않는다는 것은 본인이 무엇을 개발하는지도 모르고 기계처럼 다른 사람의 지시에 의해서 타이핑을 하는것이지 개발을 하는것이 아니다.

기획자가 따로 있어서 화면설계서를 보면서 기능개발을 한다고 가정을 해보겠다. 화면에서 어떤 버튼을 클릭하면 어떤 조건에 대해서 검사를 하고, 조건이 만족되면 어떤 결과가 도출된다는 것이 정의되어 있을 것이다. 이때 백엔드를 개발할때는 화면에 대한 생각을 없애야 한다. 화면에서 발생하는 하나의 트리거에 의해서 백엔드에서는 두가지, 세가지 이상의 기능이 실행될 수 있다. 그렇기 때문에 화면을 참고해서 우선 기능분리를 해야된다. 앞의 예를 조금 더 자세하게 해서 사용자가 회원가입을 하기 위해 정보를 입력하고 가입 버튼을 눌렀을 때 아이디 중복검사, 비밀번호 정합성 검사를 거친 뒤 이를 만족하면 회원정보를 저장하고, 저장된 정보를 반환하는 기능을 개발해야된다고 가정을 하겠다. 기능 분석을 하지 않고 그저 화면설계서에 정의된 내용대로 가입 버튼을 눌렀으니 하나의 메소드에서 화면설계서에서 정의된 내용을 모두 하도록 개발을 한다고 가정을 해보겠다. 화면에서 하나의 트리거에 의해 발생했으니 하나로 묶어서 개발을 해서 결과값을 반환하게 된다면 처음 개발은 편할 것이다. 또한 코드가 한곳에 있으니 코드를 추적하기 쉬워보이기도 한다. 하지만 기능이 확장되거나 재사용되는 경우를 생각해보면 분리해서 개발하는 것이 좋다. 비밀번호 수정하는 기능이 추가되어 비밀번호 정합성 로직이 다른데도 필요하다고 생각을 해보자. 그렇다면 이때서야 비밀번호 정합성을 검사하는 로직을 분리해서 구성을 하거나, 최악의 경우 Copy&Paste로 코드를 복사해서 사용할 것이다. 비밀번호 정합성을 검사하는 로직이 그래도 메소드 내부에서 잘 구분되어 있어서 분리하기 쉽다면 좋지만, 다른 부분과 엮여서 분리하기 어려운 경우도 많다. 이 때문에, 최초에 개발을 진행하기 전에 기능에 대한 분석이 필요한 것이다.

기능 분석을 할 때 우선 어떤 기능들이 있는지 기능 리스트업을 해야된다. 앞의 예시에서는 아이디 중복검사, 비밀번호 정합성 검사, 회원정보 저장 이렇게 3가지 기능이 있다. 이 외에도 비기능 요구사항에 의해 실행 로그를 저장한다고 한다면 로그를 저장하는 기능까지 4가지 기능이 필요하게 된다. 기능 리스트업을 하면 상세 기능에 대한 분석이 필요하다. 비밀번호 정합성 검사를 예로들면 어떠한 규칙으로 정합성 검사를 하는지에 대해 분석을 하는 것이다.

기능에 대한 분석이 완료되면 설계를 하게 될 것이다. 회원정보를 저장하는데는 아이디 중복검사와 비밀번호 정합성 검사는 기능 요구사항이므로 기능 실행에 대한 필수 필요조건이 되고 UseCase로 표현하면 include 관계가 된다. 따라서 회원정보를 저장하는 로직에서 아이디 중복검사와 비밀번호 정합성 검사를 호출해서 사용하도록 개발을 할 수 있다. 실행 로그를 저장하는 행위는 비기능 요구사항으로 회원정보를 저장하는 로직에서는 실행 로그가 저장되는지 알 필요가 없다. 따라서 실행로그를 저장하는 행위는 회원정보를 저장하는 로직에 포함되지 않고, 회원정보를 저장하는 로직이 실행는 전 후로 실행되도록 개발을 할 수 있을 것이다. 이런 식으로 기능에 대한 기본 분석 이후에 기능의 호출 관계 및 실행 순서에 대해서 설계를 할 수 있다. 또한 이런식으로 기능을 분석해서 분리해서 개발을 하게 된다면 이후에 동일한 기능을 사용하는 기능이 추가될 때 추가개발범위가 줄어들게되는 이점이 있다.

기능을 분리하는 것은 어려운 일이다. 추가확장의 가능성이 없어서 분리할 필요가 없는데 분리하여 오버스펙이 될 수도 있고, 추가확장의 가능성이 없어서 분리하지 않았지만 뜬금없이 추가확장이 되는 경우도 있다. 이 때문에 기능을 분리할 지 하나로 합칠지에 대해서는 많은 고민이 필요하고 많은 경험이 필요하다.

3. 기능 명세 작성

기능 명세를 작성하는데는 특별한 룰은 없는 것 같다. 분석된 기능의 내용을 상세하게 기록하면 된다. 필자의 경우에는 우선 기능의 기본 기능에 대해 작성을 한다. 기본 기능에 대해서 작성한 이후에는 특이케이스 및 예외사항에 대해 작성을 한다. 그 다음에는 어떤 값을 가지고 이 기능을 실행하면 어떤 결과가 도출되는지에 대해 작성을 한다. 기능 명세는 상세하게 작성하면 할 수록 좋다. 그만큼 기능에 대해서 더욱 명확해지기 때문이다. 이렇게 작성한 명세서는 시나리오별로 잘 분리해서 Test Case로도 사용할 수 있다. 기능분리를 했고, Input/Output에 대한 내용을 정리했고, 특이케이스 및 예외사항에 대해 정의를 했기 때문에 가능한 일이다. 또한, 분석과정에서 기능의 호출관계에 대해서도 분석을 했기 때문에 Mock 대상에 대해서도 명확해지게 된다.

기능 명세를 작성하면서 주의할 점은 백엔드 기능에 대해 정의하는 것이므로 화면에 대한 내용이 들어가면 안된다는 것이다. 백엔드 개발자는 기능분석/설계를 하는 과정에서는 어떠한 경우라도 화면을 배제해야 된다.

4. 결론

기능을 분석하고 명세를 작성하는 것은 매우 어려운 일이라고 생각한다. 초기 개발과정에서는 쓸데없는 일로 오히려 리소스만 더 소모하는 것으로 보여질 수도 있다. 하지만 지금 당장이 아닌 미래를 본다면 충분히 가치있는 행동이고, 꼭 필요한 행동이다. 문서를 작성하다보면 더욱 많은 고민을 하게 될 것이고, 코드 뿐만 아니라 개인에게도 고민을 통해 얻는 이점이 충분히 있다고 생각을 한다. 또한, 기능을 분석/설계하는 행위와 명세를 작성하는 행위는 Test Case를 작성하는것이나 클린코드를 작성하는 것과도 밀접한 연관관계를 가지고 있기 때문에 이에 숙달되면 개발을 하는데 많은 도움이 될 것이라 생각을 한다.

문서를 작성하지 않는 것보다 더욱 위험한 행동은 문서를 업데이트 하지 않는 것이다. 없는 정보는 처음부터 찾아보면 되지만 잘못된 정보는 처음부터 찾아보지 않을 수 있고 이로인해 잘못된 판단을 할 수 있기 때문이다. 문서를 작성하지 않았다면 지금부터라도 문서를 작성하는 것에 습관을 들이고, 문서를 업데이트하는 습관을 들이는 것을 추천한다.


JVM Memory


1. JVM?

  • 정의된 스펙을 구현한 하나의 독자적인 프로세스 형태로 구동되는 Runtime Instance

  • Java 프로그램의 모든 것들을 실행시키는 기본 데몬

  • Class 파일을 로딩하고 `ByteCode`를 해석하는 과정을 거쳐 메모리 리소스를 할당하고 관리하며 정보를 처리

  • Thread 관리 및 `Garbage Collection`과 같은 메모리 정리

1.1. 용어

용어설명

Java Source

사용자가 작성한 Java 코드

Java Compiler

Java Source 파일을 JVM이 해석할 수 있는 Java Byte Code로 변경하는 Compiler

Java Byte Code

Java Compiler에 의해 수행될 결과물(.class 파일)

Class Loader

JVM 내로 .class파일들을 Load 하여 Loading된클래스들을 Runtime Data Area에 배치

Execution Engine

Loading된 클래스의 Byte Code를 해석

Runtime Data Area

JVM이 프로그램을 수행하기 위해 OS에서 할당 받은 메모리 공간

Method Area

클래스, 변수, Method, static변수,상수 정보 등이 저장되는 영역으로 모든 Thread가 공유

Heap Area

new 명령어로 생성된 인스턴스와 객체가 저장되는 영역으로 Garbage Collection 대상이고 모든 Thread가 공유

Stack Area

Method 내에서 사용되는 값들(Argument, Local Variable, Return Value)이 저장되는 영역으로 메소드가 호출될 때 LIFO로 하나씩 생성되고, 메소드 실행이 완료되면 LIFO로 하나씩 삭제(Thread별로 하나씩 생성)

PC Register

CPU의 Register와 비슷한 역할로 현재 수행중인 JVM 명령의 주소값이 저장(Thread별로 하나씩 생성)

Native Method Stack

다른 언어(C/C++)의 메소드 호출을 위해 할당되는 구역으로 언어에 맞게 Stack이 형성

2. Java Heap

용어설명

Eden(Young)

Object가 생성될 때 Heap에 최초로 할당되는 영역

Survivor (Young)

Eden 영역이 꽉 차서 Minor GC가 발생할 때 살아남은 Live Object들이 이동되는 영역

Tenured(Old)

Young Generation에서 Live Object로 오래 살아남은 Object들이 이동되는 영역(특정 회수 이상 참조되어 기준 Age를 초과한 Object) 비교적 오랫동안 참조되어 이용되고 있고 앞으로도 계속 사용될 확률이 높은 Object들이 저장

Permanent

Class와 Method의 Meta 정보나 Static 변수와 같은 상수 정보들이 저장되는 영역(Java 8 이후로 사라짐)

Metaspace(Native)

Java 8부터 생긴 영역으로 Permanent 영역에서 저장하던 정보 중 Static Object를 제외한 정보들을 저장하는 영역

  • Heap 영역은 JVM에 의해 관리되는 영역이고, Native 메모리는 OS레벨에서 관리하는 영역

Permanent 초기치와 최대치 확인

java -XX:+PrintFlagsFinal -version -server | grep "PermSize"

Metaspace 초기치와 최대치 확인

java -XX:+PrintFlagsFinal -version -server | grep "MetaspaceSize"

SpringOne Tour 2018 Memo


2. Full Stack Reactive Kotlin! with Spring Framework 5, Spring Boot2, & Project Reactor

Reactive Spring with Spring Boot 2.0

Mark Heckler, Developer Advocate, Pivotal

site: www.thehecklers.com

twitter: @mkheck

  • 트위터 홍보중..

  • 그 다음 이메일 홍보중…​

  • 일하는거 증명해야된다고 자꾸 사진찍음…​

  • 이 아저씨 뭐하는 아저씨야…​

  • Non-blocking, event-driven application

  • scale with a small number of threads

  • backpresure as a key ingredient

  • thread가 대기하는 비효율적인 상황을 없애고 효율적으로 쓰도록 한다

  • 적은 수의 thread로 많은 요청을 처리할 수 있도록한다

  • Callback이 많을수록 보기 어려워짐

2.1. Reactive Streams: 4 interfaces

  • Publisher<T>

    • 값을만들어냄

  • Subscriber<T>

    • 소비자

  • Subscription

    • ???

  • Processor<T>

    • Subscriber와 Subscription을 사용함

2.2. Project Reactor: a quick overview

  • 피보탈에서 진행중

  • 자바 8 사용

  • Spring In Busy????? 뭐야이게

  • Flux를 반환

2.3. Let’s Code

  • start.spring.io

  • 코틀린 설명중…​

  • 라이브 코딩

  • 코틀린 신기하당

2.4. Helpful resources

  • www.reactive-streams.org

  • projectreactor.io

  • github.com/mkheck/FSR

3. Cloud-Native Spring

Josh Long, Spring Developer Advocate, Pivotal

twitter: @starbuxman

  • 이 아저씨도 사진찍음…​

  • 책 커버 동물때문에 책내는데 2년 걸렸다고함

  • ReactiveSpringBook.io

  • 시작부터 라이브코딩

  • Reactive Web, Lombok, actuator, gateway

  • r2jdbc

  • 이아저씨 이상해…​

  • 라우터 게이트웨이용으로만 쓰면 좋을거같다

  • 초당 리퀘스트 갯수 개한할 수 있음

  • 뭔가 빠르게 코딩 잘하기는 한다..

  • 대규모에서 Http가 아니라 RSocket을 사용해서도 할 수 있음(byte stream)

  • 분산환경 및 더 빠르게 할 수 있음

4. Spring Cloud Gateway

Younjin Jeong, Principal Technologist, Pivotal

  • 다양한 클라이언트에서 각 서비스로 직접 접근을 했었음

  • 이를 l7이나 proxy를 통해서 구현하기도 했지만 점점 처리해야되는 요구사항이 많아짐

  • 게이트웨이를 두고 헤더나 다른 값들을 통해 추가적이 요구사항을 충족할 수 있도록 하게 함

  • Mesh: 사이드캅(?) 패턴과 프록시 패턴의 혼용?

  • Zuul - netflix.github.com?

    • 요청을 어디로보낼지 결정

  • Zuul2 - 비동기를 지원하기 위해 개발

  • Latency, Thread Count, Concurrent Connections가 중요함

  • Zuul2를 넣지 않음

  • Spring Boot 2.0 + Project Reactor + Netty

  • 유레카: Service Discovery

  • Ribbon: Latency에 따라 요청을 핸들링

  • curl은 사람이 쓸게 아님..HTTPie

  • fallbackUri > Service API에 문제 발생 시 미리지정한 Fallback Message를 줄 수 있음

  • Zipkin

5. Cloud Event Driven Architectures with Spring Cloud Stream 2.0

Jakub Pilimon, Principal Technologist Pivotal

  • pillopl.github.io

  • github.com/ddd-by-examples

  • 도메인을 어떻게 Discover 하는지

  • 이벤트를 발견하고 이벤트의 트리거를 찾는다

  • 이벤트간의 연관관계(?)를 파악한다

  • effect of events on invariants

  • 용어를 정의한다

  • 메세지가 중요하다

  • Event와 행위(Behavior)가 비슷한거같은데…​뭐가다른거지…​

  • 동일한 처리를 하는것 같아도 행위가 다르면 별도로 메소드 처리

  • given에 값이 들어가는 것 뿐만 아니라 전처리(선행 행위)를 쓸수있음

  • io.vavr.io.collection.List.ofAll(…​).foldLeft

  • include, exclude가 아닌 broker를 두고 broker가 호출하는 식으로 사용하는것이 흐름을 파악하기 쉽다

  • Consumer, Producer는 서로가 누구인지 알 필요가 없음

  • APPLICATION_STREAM_JSON_VALUE

  • 인텔리제이에 되게 신기한기능있다…​. 블록지정해서 메소드를 만들어냄

  • 굳이 하나로 다 쓸 필요 없이 and 라는 좋은것이 있음

given:
and:
and:
when:
and:
then:

6. Spring, Functions, Serverless and You

Nate Schutta, Solution Architect, Pivotal

  • IaaS(Infrastructure as a Service)

  • Docker Container

  • Kubernates

  • cloudfoundary

  • 집중이 안댐 ㅠㅠ

7. Spring Boot & Spring Cloud on Pivotal Application Service

Younjin Jeong, Principal Technologist, Pivotal

  • Full Cycle Developers

  • Pivotal Application Service

  • Cred Hub

  • Pivotal 제품 소개 위주

8. Using Spinnaker to Create a Development Workflow on Kubernates

Paul Czarkowski, Principal Technologist, Pivotal

  • 맥주(kloud)가 기술적이라함

  • 피보탈에서 돈받아서 피보탈얘기를 해야한다함

  • kubernates master가 여러 worker를 관리함

  • Pod

  • StatefulSet

  • Helm

  • Spinnaker

  • Halyard

@_@


Spring MVC


1. 기본 용어

1.1. @RequestMapping

  • DefaultAnnotationHandlerMapping에서 매핑

  • url과 컨트롤러 메소드 매핑정보 생성

  • 클래스, 메소드 레벨에 사용 가능

  • url, method, parameter, header 정보를 통해 구분 가능

  • url에 ANT스타일의 와일드카드 사용 가능

  • 상속 가능

@RequestMapping("/home")
@RequestMapping("/home*")
@RequestMapping("/home/**/action")
@RequestMapping("/user/{userId}")
@RequestMapping({"/", "/index"})
@RequestMapping(value="/user/{userId}", method=RequestMethod.GET)
@RequestMapping(value="/login", method=RequestMethod.POST, params="env=mobile")
@RequestMapping(value="/login", method=RequestMethod.POST, params="env=!mobile")
@RequestMapping(value="/login", method=RequestMethod.POST, headers="content-type=application/json")

1.2. @Controller

  • Controller를 정의

  • 여러 값들을 매핑할 수 있도록 함

@Controller
public class SampleController {
  ...
}

1.3. HttpServletRequest, HttpServletResponse

  • 서블릿의 HttpServletRequest와 HttpServletResponse

public void method(HttpServletRequest request, HttpServletResponse response) {
  ...
}

1.4. HttpSession

  • 세션관리 객체

public void method(HttpSession session) {
  ...
}

1.5. WebRequest

  • HttpServletRequest의 정보를 대부분 그대로 갖고 있는 서블릿 API에 종속적이지 않은 오브젝트 타입

  • 서블릿과 프틀릿 환경 양쪽에 모두 적용 가능한 범용적인 핸들러 인터셉터를 만들 때 활용하기 위함

1.6. NativeWebRequest

  • WebRequest 내부에 감춰진 HttpServeltRequest와 같은 환경종속적인 오브젝트를 가져올 수 있는 메소드가 추가되어있음

1.7. @PathVariable

  • url로 들어가는 패스 변수를 받는다

// curl https://sample.nuti.pe.kr/home/1
@RequestMappint("/home/{id}")
public void method(@PathVariable("id") Long id) {
  ...
}

1.8. @RequestParam

  • application/x-www-form-urlencoded요청에서 쿼리스트링을 매핑

// curl https://sample.nuti.pe.kr/home?id=1
public void method(@RequestParam("id") Long id) {
  ...
}
  • 쿠키값을 받음

  • 필수여부 지정 가능

// curl -b 'auth=abc' https://sample.nuti.pe.kr
public void method(@CookieValue(value="auth", required=false, defaultValue="") String auth) {
  ...
}

1.10. @RequestHeader

  • 헤더 정보를 받음

  • 필수여부 지정 가능

// curl -H 'Content-Type: application/json' https://sample.nuti.pe.kr
public void method(@RequestHeader(value="Content-Type", required=true) String contentType) {
  ...
}

1.11. Map, Model, ModelMap

  • 모델 정보를 담는데 사용

public void method(Map model) {
  ...
}
public void method(Model model) {
  ...
}
public void method(ModelMap model) {
  ...
}

1.12. @ModelAttribute

  • 생략가능

  • 모델 정보를 객체에 매핑

  • 클라이언트에서 받은 정보를 매핑할 수도 있고, 서버에서 등록한 정보도 매핑할 수 있음

public void method(User user) {
  ...
}
public void method(@ModelAttribute User user) {
  ...
}
public void method(@ModelAttribute("currentUser") User user) {
  ...
}

@ModelAttribute("userId")
public String getUserId(HttpServletRequest request) {
  Object userId = request.getAttribute("userId");
  return (String) userId;
}
public void method(@ModelAttribute("userId") String userId) {
  ...
}

1.13. @RequestBody

  • payload를 mapping

// curl -X POST -H 'Content-Type: application/json' -d '{ "idx": 1, "id": "user" }'
public void method(@RequestBody User user) {
  ..
}

1.14. @Value

  • 프로퍼티값

  • 상수 값

  • 특정 메소드를 호출한 결과 값

@Value("#{systemProperties['user.home']}")
private String userHome;

1.15. ModelAndView

  • 반환하는 객체와 뷰를 지정

@RequestMapping(value = "")
public ModelAndView index(Device device) {
  ModelAndView mav = new ModelAndView();

  mav.addObject("isMobileDevice", device.isMobile());
  mav.setViewName("index");

  return mav;
}

1.16. 뷰 지정

  • String으로 뷰 이름을 지정 가능

  • void로 사용할 시 url을 통해 view 지정

  • 이외 모델 사용시 url을 통해 지정

public String method(Model model) {
  ...
  return "index";
}

@RequestMapping("/index")
public void method(Model model) {
  ...
}

@RequestMapping("/index")
public User method() {
  ...
}

public String method(RedirectAttributes redirectAttributes) {
  return "redirect:/index";
}

1.17. @ResponseBody

  • 반환값을 응답 본문으로 사용

@ResponseBody
public String method() {
  return "<html><head></head><body>Hello</body></html>"
}

@SessionAttributes

  • 모델 객체를 세션에 저장해서 사용할 수 있도록 함

  • 세션의 값을 사용

@Controller
@SessionAttributes("user")
public class SampleController {
  ...

  @RequestMapping("/")
  public String method(@ModelAttribute User user) {
    ...
  }
}

1.18. SessionStatus

  • 세션을 관리

  • 사용이 완료된 세션을 제거하지 않으면 메모리 누수가 발생할 수 있으므로 사용 후 제거해야함

public void method(SessionStatus sessionStatus) {
  sessionStatus.setComplete();
}

1.19. @InitBinder, WebDataBinder

  • 메소드 파라미터를 바인딩 및 검증

  • allowedFields, disallowedFields

private AgentValidator agentValidator;

@InitBinder("agent")
public void initAgentBinder(WebDataBinder dataBinder) {
  dataBinder.setValidator(agentValidator);
}

1.20. Validator

  • @ModelAttribute로 바인딩되는 모델의 데이터 검

@Component
public class AgentValidator implements Validator {
  @Override
  public boolean supports(Class<?> clazz) {
    return (String.class.isAssignableFrom(clazz));
  }

  @Override
  public void validate(Object target, Errors errors) {
    List<String> agentList = Arrays.asList(OS_ANDROID.getName(), OS_IOS.getName());
    ErrorCode error = null;

    String agent = (String) target;

    if (! agentList.contains(agent)) {
      error = ErrorCode.UnsupportedAgent;
    }

    if (error != null) {
      errors.reject(error.getCode(), error.getMessage());
    }
  }
}

1.21. @Valid

  • @ModelAttribute의 값을 검증

@RequestMapping(value = "/{agent}", method = RequestMethod.GET)
public JigjakVersion getVersion(@ModelAttribute("agent") @Valid String agent, BindingResult bindingResult) {
  ...
}

1.22. Errors, BindingResult

  • @ModelAttribute의 Validation 결과를 담음

public void method(@ModelAttribute User, BindingResult result) {
  if (bindingResult.hasErrors()) {
    ObjectError error = bindingResult.getAllErrors().get(0);

    throw new ResourceNotFoundException(UnsupportedAgent);
  }
}

1.23. Converter

  • 데이터 바인딩 시 데이터를 변환

  • ex) String → 클래스

  • 클래스 → String

public interface Converters<S, T> {
  T convert(S source);
}

1.24. ConversionService

  • 컨트롤러 값 바인딩 시 데이터 타입 변환에 사용

  • InitBinder 혹은 ConfigurableWebBindingInitializer를 통해 등록

<bean class="org.springframework.context.support.ConversionServiceFactoryBean">
  <property name="converters">
    <set>
      <bean class="kr.pe.nuti.converter.CustomConverter" />
    </set>
  </property>
</bean>
@Autowired
private ConversionService conversionService;

@InitBinder
public void initBinder(WEbDataBinder dataBinder) {
  dataBinder.setConversionService(this.conversionService);
}

1.25. Formatter, FormattingConversionService

  • 오브젝트 > 문자열

  • 문자열 > 오브젝트

  • locale이 포함

  • html

1.26. @NumberFormat

  • 숫자, 문자 포맷

@NumberFormat("$###,##0.00")
BigDecimal price;

1.27. @DateTimeFormat

  • 날짜, 문자 포맷

@DateTimeFormat(pattern="yyyy/MM/dd")
Date date;

1.28. Message Converter

  • 요청본문과 응답 본문을 다룸

1.28.1. ByteArrayHttpMessageConverter

  • byte[] 지원

  • application/octet-stream

1.28.2. StringHttpConverter

  • xml, json같이 문서 포맷이 있다면 적절한 파서를 붙여서 활용할 수 있음

1.28.3. FormHttpMessageConverter

  • application/x-www-form-urlencoded

1.28.4. SourceHttpMessageConverter

  • application/json

  • application/*+xml

  • text/xml

1.28.5. Jaxb2RootElementHttpMessageConverter

  • JAXB2의 @XmlRootElement, @XmlType이 붙은 클래스를 이용해 XML과 오브젝트를 변환할 때 사용

1.28.6. MarshallingHttpMessageConverter

  • Marshaller와 UnMarshaller

  • XML문서와 자바 오브젝트 사이의 변환을 지원

1.28.7. MappingJacksonHttpMessageConverter

  • Jackson ObjectMapper를 통해서 JSON 문서와 자바오브젝트 변환을 지원

1.29. mvc:annotation-driven

  • MVC에서 지원하는 빈을 자동으로 등록

  • 라이브러리의 존재 여부를 파악해서 자동으로 관련 빈을 등록

1.29.1. DefaultAnnotationHandlerMapping

  • @RequestMapping을 이용한 핸들러 매핑 전략을 등록

1.29.2. AnnotationMethodHandlerAdapter

  • DispatcherServlet이 자동으로 등록해주는 디폴트 핸들러 어댑터

1.29.3. ConfigurableWebBindingInitializer

  • 모든 컨트롤러 메소드에 자동으로 적용되는 WebDataBinder 초기화용 빈을 등록

1.29.4. 메세지 컨버터

  • 기본 컨버터와 라이브러리 유무에 따라 Jaxb2RootElementHttpMessageConverter, MappingJacksonHttpMessageConverter 등록

1.29.5. validator

  • 모든 컨테이너에 일괄 적용하는 validator 등록

1.29.6. conversion-service

  • Default: FormattingConversionServiceFactoryBean

<mvc:annotation-driven conversion-service="myConversionService" />

<bean id="myConversionService" class="FormattingConversionServiceFactoryBean">
  <property name="converters">
    ...
  </property>
</bean>

1.30. Interceptors

  • 컨트롤러 전후로 작업할 내용 지정

<mvc:interceptors>
  <bean class="kr.pe.nuti.GlobalInterceptor" />
</mvc:intercptors>
<mvc:interceptors>
  <mvc:interceptor>
    <mvc:mapping path="/home/*" />
    <bean class="kr.pe.nuti.HomeInterceptor" />
  </mvc:interceptor>
</mvc:interceptors>

1.31. view-controller

  • 뷰로 매핑만 할 경우

<mvc:view-controller path="/" view-name="/index" />

2. MVC 확장 포인트

2.1. SessionAttributeStore

  • @SessionAttribute 에 의해 지정된 모델은 세션에 저장된

  • 세션은 메모리에 저장됨

  • 메모리 문제를 해결하기 위해 별도의 저장소를 사용해서 사용할 수 있음

2.2. WebArgumentResolver

  • 어플리케이션에 특화된 컨트롤러 파라미터 타입을 추가할 수 있음

  • 암호화된 정보를 복호화해서 파라미터로 넘김

2.3. ModelAndViewResolver

  • 컨트롤러 메소드의 리턴 타입과 메소드 정보, 어노테이션 정보등을 통해 ModelAndView를 생성

  • 활용도는 낮음

2.4. HandlerMethodReturnValueHandler

  • 리턴 값을 처리

  • RequestMaiingHalderAdapter의 customReturnValueHandlers 프로퍼티에 주입

3. url과 리소스 관리

3.1. <mvc:default-servlet-handler />

  • servlet path가 /일 경우 모든 요청이 DispatcherServlet으로 전달됨

  • 이때 함께 사용해야됨

  • @RequestMapping 정보에 의존

  • 매핑된 정보를 찾을 수 없을 시 가장 우선순위가 낮은 디폴트 서블릿 매핑 전략을 통해 컨테이너가 제공하는 디폴트 서블릿으로 요청이 포워딩

  • 동작원리는 이해할 필요는 없고 같이 사용해야된다는 것만 기억

3.2. <url:resource />

  • 요청에 따라 리소스 경로를 지정

<mvc:resources mapping="/resources/**" location="/resources/" />

Spock Framework


1. Spock Framework?

Spock FrameworkGroovy 언어에서 동작하는 명세 프레임워크로 BDD를 편하게할 수 있도록 도와준다. SpockJavaGroovy 어플리케이션을 위한 명세 프레임워크로 Groovy(DSL)로 작성하므로 간결하고 직관적인 장점이 있다. 또한, 기존의 JavaJUnit, Hamcrest, Mockito를 전부 다 학습하는 것보다 손쉽게 학습할 수 있고, Mock, Stub, Spy등 사용이 편리하고 명세를 작성하기 편리하다. JavaGroovy 어플리케이션을 위한 프레임워크이므로 Java환경에서도 사용할 수 있다.

2. Lifecycle

  • setup: 메소드 실행 전에 실행(given)

  • when: 행위에 대한 명세를 작성

  • then: 행위에 대한 예측을 작성

  • expect: 행위에 대한 명세와 예측을 작성(when + then)

  • cleanup: 메소드 실행 후에 실행

  • where: 여러 값에 대해 반복행위를 할 때 작성

spock lifecycle
Figure 1. Spock Lifecycle

3. Example

앞서 BDD 포스트에서 작성한 예시Spock으로 변환하게 되면 아래와 같다.

package kr.pe.nuti.home.api.service.todo

import kr.pe.nuti.home.api.domain.todo.TodoItem
import kr.pe.nuti.home.api.enumeration.todo.TodoState
import kr.pe.nuti.home.api.exception.todo.IllegalStateChangeException
import kr.pe.nuti.home.api.repository.todo.TodoItemRepository
import spock.lang.Issue
import spock.lang.Narrative
import spock.lang.See
import spock.lang.Specification
import spock.lang.Title

@Title("Todo Item의 상태를 변경한다.")
@Narrative("""
Todo Management System을 사용하는 사용자가
Todo Item의 상태관리를 위해서
각각의 Todo Item의 상태를 변경할 수 있다.
상태 변경은 Todo > Doing, Doing > Done,
Done > Doing, Doing > Todo로만 할 수 있다.
""")
class TodoServiceStateChangeSpec extends Specification {

  TodoService service
  def todoItemRepository

  def setup() {
    todoItemRepository = Mock(TodoItemRepository)
    service = Spy(TodoService)
    service.todoItemRepository = todoItemRepository
  }

  @See(["https://github.com/hyeonil/smart-home-api/issues/6"])
  @Issue("#6")
  def "Todo상태를 Doing상태로 변경하면 상태가 변경된다."() {
    given: "Todo 상태의 Todo Item"
    TodoItem savedItem = new TodoItem([idx: 1L, state: TodoState.TODO])
    TodoItem changedItem = new TodoItem([idx: 1L, state: TodoState.DOING])
    todoItemRepository.findById(_) >> Optional.of(savedItem)
    todoItemRepository.save(_) >>  changedItem

    TodoItem item = new TodoItem([idx: 1L])

    when: "Todo Item의 상태를 Doing으로 변경한다."
    def result = service.changeState(item, TodoState.DOING)

    then: "Todo Item의 상태가 Doing으로 변경된다."
    1 * service.getItem(_)
    result.state == TodoState.DOING
  }

  @See(["https://github.com/hyeonil/smart-home-api/issues/6"])
  @Issue("#6")
  def "Todo상태를 Done상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다."() {
    given: "Todo 상태의 Todo Item"
    TodoItem savedItem = new TodoItem([idx: 1L, state: TodoState.TODO])
    todoItemRepository.findById(_) >> Optional.of(savedItem)

    TodoItem item = new TodoItem([idx: 1L])

    when: "Todo Item의 상태를 Done으로 변경한다."
    service.changeState(item, TodoState.DONE)

    then: "Todo Item의 상태가 변경되지 않고 예외사항이 발생한다."
    1 * service.getItem(_)
    thrown(IllegalStateChangeException)
  }
}

Pagination