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)
  }
}

BDD


1. BDD(Behavior-Driven Development)란?

BDDTDD에서 파생된 개발 방법론으로 테스트에 대한 집중보다는 명세와 행위에 대해 비중을 두고 있다.

1.1. TDD

tdd
Figure 1. TDD

TDDTest Case(이하 TC)를 작성하고 실패를 확인 후 실제 비즈니스 코드를 작성, TC 성공확인을 하나의 반복주기로 잡고, 이를 반복해서 진행하는 것을 요구한다. 하지만 TC를 작성하는데 의문점이 있다. 코드가 없는데 무엇을 테스트할 것인가? TDD를 보면 무엇을 테스하는지에 대한 명시는 되어있지 않다. 또한, 보통 테스트라 하면 제품이 나오면 그 제품을 사용해보면서 제대로 작동하는지 확인을 해보는 과정인데, 제품이 나오지도 않았는데 테스트를 한다라는 것부터 모순이 생긴다. 이를 개선한 것이 BDD라고 생각을 한다.

1.2. BDD(Behavior-Driven Development)

BDD는 위에서 언급한바와 같이 TDD에서 파생된 개발방법론이고, 코드의 구현과 테스트 보다는 행위(동작, 명세)에 집중하고 있다. BDD에서는 기능의 TC를 작성하는 것이 아닌 명세를 작성하는 것이고, 요구사항 분석 후 기능 설계 및 명세작성, 코드구현의 순서로 진행을 하게된다.

BDD에서 테스트(Test) 라는 단어를 사용하지 않고 명세(Specification)를 사용함으로써 많은 것이 바뀌게 된다. TDD에서는 테스트라는 단어를 사용하게 되어 혼란과 모순을 가지게 되었는데, BDD에서는 이를 명세라는 단어로 사용함으로써, 제품이 생산되기 전에 제품에 대한 명세를 작성하게 되는 것이므로 이에 대한 모순이 사라지게 된다.

2. BDD Template

Title: 스토리에대한 제목을 간략하고 명확하게 작성
User Story
  Who 누가
  Why 왜
  What 무엇을 하는지
Scenario
  Given 어떤 값이 주어졌을 때
  When 어떤 행위를 하게 되면
  Then 어떤 결과를 도출한다

3. BDD Example

할일을 관리할 수 있는 프로그램을 만든다고 가정을 한다. 그렇다면 기획자 혹은 개발자는 고객과 커뮤니케이션을 하며 프로그램에 대한 요구사항을 수집하게 된다.

할일을 관리할 수 있는 시스템을 개발한다.
Todo Item을 등록/수정/삭제 할 수 있다.
Todo/Doing/Done 할 수 있고 Archive할 수 있어야 한다.
Todo Item을 등록할 때 Todo 상태로 시작한다.
상태변경을 할 수 있고 상태 변경은
Todo > Doing, Doing > Done, Done > Doing, Doing > Todo로만 할 수 있다.
Archive는 Todo/Doing/Done 모든 상태에서 가능하다.
목록보기/상세보기 기능을 포함한다.
페이징 기능은 스펙에서 제외하고 현재 스펙에서는 전체 목록을 한번에 조회한다.

수집한 요구항이 위와 같다고 하면 아래 그림과 같은 Use Case DiagramClass Diagram을 그릴 수 있다.

use case diagram
Figure 2. Use Case Diagram
class diagram
Figure 3. Class Diagram

위 예시에서 상태변경을 통해 명세작성 예시를 진행하도록 하겠다.

Title: Todo Item의 상태를 변경한다.

User Story
Who: Todo Management System을 사용하는 사용자가
Why: Todo Item의 상태관리를 위해서
What: 각각의 Todo Item의 상태를 변경할 수 있다.
      상태 변경은 Todo > Doing, Doing > Done,
      Done > Doing, Doing > Todo로만 할 수 있다.

Scenario 1: Todo상태를 Doing상태로 변경하면 상태가 변경된다.
Scenario 2: Doing상태를 Done상태로 변경하면 상태가 변경된다.
Scenario 3: Done상태를 Doing상태로 변경하면 상태가 변경된다.
Scenario 4: Doing상태를 Todo상태로 변경하면 상태가 변경된다.
Scenario 5: Todo상태를 Done상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다.
Scenario 6: Done상태를 Todo상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다.

일반 글로 위와 같은 명세를 작성할 수 있고 아래와 같이 코드로 명세를 작성할 수 있다.

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

import kr.pe.nuti.home.api.core.application.Application;
import kr.pe.nuti.home.api.core.application.JpaConfiguration;
import kr.pe.nuti.home.api.core.application.WebConfiguration;
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 org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.Optional;

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

/**
 * Title: Todo Item의 상태를 변경한다.
 * User Story:
 * Todo Management System을 사용하는 사용자가
 * Todo Item의 상태관리를 위해서
 * 각각의 Todo Item의 상태를 변경할 수 있다.
 * 상태 변경은 Todo > Doing, Doing > Done,
 * Done > Doing, Doing > Todo로만 할 수 있다.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {JpaConfiguration.class, WebConfiguration.class, Application.class})
public class TodoServiceStateChangeTest {

  @Mock
  private TodoItemRepository todoItemRepository;

  @Autowired
  @Spy
  @InjectMocks
  private TodoService service;

  @Before
  public void setup() {
    MockitoAnnotations.initMocks(this);
  }

  /**
   * Todo상태를 Doing상태로 변경하면 상태가 변경된다.
   * @throws Exception
   */
  @Test
  public void testStateChangeFromTodoToDoing() throws Exception {
    // given Todo 상태의 Todo Item
    TodoItem savedItem = new TodoItem();
    savedItem.setIdx(1L);
    savedItem.setState(TodoState.TODO);
    TodoItem changedItem = new TodoItem();
    changedItem.setIdx(1L);
    changedItem.setState(TodoState.DOING);
    when(todoItemRepository.save(any(TodoItem.class))).thenReturn(changedItem);
    when(todoItemRepository.findById(1L)).thenReturn(Optional.of(savedItem));

    TodoItem item = new TodoItem();
    item.setIdx(1L);

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

    // then Todo Item의 상태가 Doing으로 변경된다.
    Assert.assertThat(result.getState(), is(TodoState.DOING));
    verifyNoMoreInteractions(service);
  }

  /**
   * Todo상태를 Done상태로 변경하면 상태가 변경되지 않고 예외사항이 발생한다.
   * @throws Exception
   */
  @Test(expected = IllegalStateChangeException.class)
  public void testStateChangeFromTodoToDoneThrownException() throws Exception {
    try {
      // given Todo 상태의 Todo Item
      TodoItem savedItem = new TodoItem();
      savedItem.setIdx(1L);
      savedItem.setState(TodoState.TODO);
      TodoItem changedItem = new TodoItem();
      changedItem.setIdx(1L);
      changedItem.setState(TodoState.DOING);
      when(todoItemRepository.findById(any(Long.class))).thenReturn(Optional.of(savedItem));
      when(todoItemRepository.save(any(TodoItem.class))).thenReturn(changedItem);

      TodoItem item = new TodoItem();
      item.setIdx(1L);

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

      // then Todo Item의 상태가 변경되지 않고 예외사항이 발생한다.
    } catch (Exception e) {
      verifyNoMoreInteractions(service);
      throw e;
    }
  }
}

Spring AOP(Aspect Oriented Programming)


1. 개요

AOP는 스프링의 기반 기술 중 하나로 이해하기 힘든 용어와 개념을 가졌다. AOP는 자바의 Reflection API를 활용해서 구현을 하게되고, 주로 비즈니스 요구사항이 아닌 부분들을 처리하기 위해 사용한다.

2. Reflection API

jvm architecture
Figure 1. JVM Architecture

자바의 Reflection API는 컴파일 레벨에서 실행될 클래스를 정하는 것이 아닌, 런타임에 실행할 클래스 파일을 정하게 할 수 있다. 또한, 런타임에서 클래스의 공개되지 않은 필드에 대한 정보를 볼 수 있고 조작할 수 있다.

주로 JDBC나 MyBatis에서 많이 사용한다. 예시는 아래와 같다.

2.1. Reflection API Example

package kr.pe.nuti.home.api.core.annotation;

import java.lang.annotation.*;

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
    ElementType.METHOD
})
public @interface LogDetail {
}
package kr.pe.nuti.home.api.core.annotation;

import java.lang.annotation.*;

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
    ElementType.TYPE
})
public @interface TrackLog {
}
package kr.pe.nuti.home.api.core.util;

import kr.pe.nuti.home.api.core.annotation.TrackLog;

import java.lang.reflect.Field;

import static kr.pe.nuti.home.api.core.util.BooleanUtil.not;

public final class LogUtil {

  private LogUtil() {
    throw new IllegalAccessError();
  }

  public static String argValues(Object arg, int depth) throws IllegalAccessException {
    if (arg == null) {
      return "null";
    }
    final Class<?> cls = arg.getClass();

    if (cls.isPrimitive() || cls.isAssignableFrom(String.class) || not(cls.isAnnotationPresent(TrackLog.class))) {
      return arg.toString();
    }

    StringBuilder builder = new StringBuilder();
    builder.append(whiteSpace(depth)).append(cls.getName()).append("{\n");

    for (Field field : cls.getDeclaredFields()) {
      field.setAccessible(true);
      Object fieldObj = field.get(arg);
      builder.append(whiteSpace(depth + 1))
          .append(field.getName())
          .append(" : ")
          .append(argValues(fieldObj, depth + 1))
          .append("\n");
    }

    builder.append("}");

    return builder.toString();
  }

  public static String whiteSpace(int depth) {
    final String appender = "  ";
    StringBuilder builder = new StringBuilder();

    for (int i = 0; i < depth; i++) {
      builder.append(appender);
    }

    return builder.toString();
  }
}
package kr.pe.nuti.home.api.core.handler;

import kr.pe.nuti.home.api.core.annotation.LogDetail;
import kr.pe.nuti.home.api.core.util.LogUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

import static kr.pe.nuti.home.api.core.util.BooleanUtil.not;

public class LogDetailMethodInvocationHandler implements InvocationHandler {

  private static final Logger logger = LoggerFactory.getLogger(LogDetailMethodInvocationHandler.class);

  private Object target;

  public LogDetailMethodInvocationHandler(Object target) {
    this.target = target;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (not(method.isAnnotationPresent(LogDetail.class))) {
      return method.invoke(target, args);
    }
    String className = method.getDeclaringClass().getName();
    String methodName = method.getName();
    StringBuilder argBuilder = new StringBuilder();

    for (Object arg : args) {
      argBuilder.append(LogUtil.argValues(arg, 0))
          .append("\n");
    }
    String argString = argBuilder.toString();

    logger.info("invoke method {}${}", className, methodName);
    logger.info("method arguments: {}", argString);

    Object result  = method.invoke(target, args);

    logger.info("finish the method {}${}", className, methodName);

    return result;
  }
}

위 예시는 LogDetail이라는 어노테이션을 가진 메소드에 대해서 해당 메소드의 파라미터 정보를 상세하게 로깅하는 것이다. 런타임에서 메소드의 정보를 분석해서 어노테이션 표기 여부에 따라 로그를 남기고 메소드를 실행시키게 된다. 또한, LogUtil.argValues는 Object의 정보를 상세하게 분석해서 Object 내부의 필드정보를 보여줄 수 있도록 되어있다.

Reflection API는 이런식으로 컴파일 타임에 어떤 클래스의 인스턴스가 실행될 지 알 수 없는 경우에 런타임에서 클래스정보를 분석하고 실행할 수 있도록 할 때 사용한다.

3. Proxy Pattern

proxy pattern
Figure 2. Proxy Pattern
  • 클라이언트가 실제 사용하려 하는 기능에 부가적인 기능을 더해서 자신이 핵심 기능인 척 위장하는 것

  • 타겟은 프록시가 있는지 알아서는 안된다.

  • 타겟의 기능을 확장 및 접근 방법을 제어할 수 있는 유용한 방법

  • 특정 Object에 대한 접근을 제어

  • 대상이 되는 Object의 생성에 관여를 하기도 함

    • 생성이 복잡한 경우

    • 당장 생성이 필요하지 않은 경우에 바로 생성하지 않고, 필요한 시기에 생성

  • 원격 Object를 이용하는 경우에 사용

    • RMI

    • EJB

  • 대상이 되는 Object에 대한 접근권한을 제어하기 위해 사용

4. Decorator Pattern

decorator pattern
Figure 3. Decorator Pattern
  • 대상이 되는 Object에 부가적인 기능을 부여하기 위해 사용

  • 컴파일 시점에 어떤 방법과 순서로 연결되어 사용하는지 정해지지 않음

  • InputStream, OutputStream

4.1. 프록시 패턴과의 차이

  • 프록시는 어떤 오브젝트를 사용하기 위해 대리인 역할을 맡은 오브젝트를 사용하는 방법을 총칭

  • 프록시패턴 프록시를 사용하는 방법 중 타겟에 대한 접근 방법을 제어하려는 목적

  • 타겟을 생성하기 복잡하거나 당장 필요하지 않은 경우에 타겟을 바로 생성하지 않고 프록시를 사용

  • 실제 타겟을 사용할 때 타겟을 생성(Lazy)

  • 기능에 대한 접근 권한을 제어하는 목적으로도 사용(읽기/쓰기 권한)

  • 자신이 만들거나 접근할 타겟을 알고있는 경우가 많음

5. Proxy

  • Client와 사용 대상 Object 사이에서 대리 역할을 하는 Object

  • 대상 Object의 핵심 기능에 부가적인 기능을 추가

  • 대상 Object는 Proxy Object의 존재 여부를 모름

  • 대상 Object를 Target 또는 Real Object라고 부름

6. Dynamic Proxy

dynamic proxy
Figure 4. Dynamic Proxy
  • 프록시는 매 Class, Method마다 Proxy를 정의해주어야 한다는 단점이 존재

  • JAVA의 Reflection API를 통해 Runtime에 동적으로 Proxy하도록 함

7. AOP

aop
Figure 5. AOP
  • Advice

    • 타겟이 필요 없는 순수한 부가 기능

    • 스프링에서는 부가기능을 제공하는 Object를 Advice라고 부름

  • Pointcut

    • 부가기능 적용 대상 선정 방법

    • 스프링에서는 메소드 선정 알고리즘을 담은 Object를 Pointcut이라고 부름

  • Advisor

    • Pointcut + Advice

  • Join Point

    • Advice가 적용될 수 있는 위치

  • Aspect

    • 독립적인 모듈화가 불가능한 모듈

    • 그 자체로 핵심 기능을 담고 있지는 않지만, 어플리케이션을 구성하는 중요한 한 가지 요소이고, 핵심 기능에 부가되어 의미를 갖는 특별한 모듈

  • 핵심적인 기능에서 부가적인 기능을 분리해서 Aspect라는 독특한 모듈로 만들어 설계하고 개발하는 방법

  • 객체지향을 좀 더 편하고 객체지향답게 사용할 수 있도록 하는 개념

8. AOP Example

8.1. Expression

execution([접근제한자 패턴] 타입패턴 [타입패턴.]이름패턴 (타입패턴 | “..}, …) [throws 예외 패턴])

ex) public int springbook.learningtest.spring.pointcut.Target.mins(int, int) throws java.lang.RuntimeException

  • public

    • 접근 제한자, 생략 가능

  • int

    • 리턴 값의 타입을 나타내는 패턴

  • springbook.learningtest.spring.pointcut.Target

    • 패키지 및 클래스 이름 패턴

  • minus

    • 메소드 이름 패턴

  • (int, int)

    • 메소드 파리미터 패턴

  • throws java.lang.RuntimeException

    • 예외 이름 패턴

8.2. Example Code

package kr.pe.nuti.home.api.core.annotation;

import java.lang.annotation.*;

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
    ElementType.METHOD
})
public @interface LogDetail {

}
package kr.pe.nuti.home.api.core.aspect;

import kr.pe.nuti.home.api.core.annotation.LogDetail;
import kr.pe.nuti.home.api.core.util.LogUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LogDetailAspect {

  private static final Logger logger = LoggerFactory.getLogger(LogDetailAspect.class);

  @Around("execution(* kr.pe.nuti.home.api..*.*(..)) && @annotation(logDetail)")
  public Object aroundTargetObject(ProceedingJoinPoint joinPoint, LogDetail logDetail) throws Throwable {
    Object target = joinPoint.getTarget();
    Object[] args = joinPoint.getArgs();

    String className = target.getClass().getName();
    String methodName = joinPoint.getSignature().getName();
    StringBuilder argBuilder = new StringBuilder();

    for (Object arg : args) {
      argBuilder.append(LogUtil.argValues(arg, 0))
          .append("\n");
    }
    String argString = argBuilder.toString();

    logger.debug("invoke method {}${}", className, methodName);
    logger.debug("method arguments: {}", argString);

    Object result  = joinPoint.proceed(args);

    logger.debug("finish the method {}${}", className, methodName);

    return result;
  }
}
@Transactional
@Override
public TodoItem changeState(@NonNull TodoItem todo, @NonNull TodoState state) throws IllegalStateChangeException {
  TodoItem savedItem = this.getItem(todo.getIdx());

  final boolean possibleToChangeState = TodoState.isPossibleToChangeState(savedItem.getState(), state);
  if (not(possibleToChangeState)) {
    throw new IllegalStateChangeException();
  }

  savedItem.setState(state);

  return todoItemRepository.save(savedItem);
}

Pagination