2022년 1월 17일 월요일

2018~2021 간단 회고

 

2018~2021 간단 회고

  • 잘한 점

    • 백기선님&김영한님 강의, 이동욱님 블로그를 알게 된 것
    • 인터넷 강의 구매에 돈을 아끼지 않은 점
    • 토이프로젝트를 통해 관심 있는 것들은 직접 경험해본 것
    • 새로운 업무환경으로 도전한 것
  • 아쉬웠던 점

    • 분명 공부했던 내용인데 까먹은 것이 많았음
    • 주로 혼자 공부한 점
    • 블로그 활동이 적음 (새로운 것이 없다면 예전에 잘못 작성한 글에 대한 수정하는 것도 좋아 보임)
    • 회사업무 중 효율적인 프로세스 도입에 대한 고민과 적극성이 부족했음
  • TODO

    • 영양가있는 블로그 글 쓰기
    • 스터디를 통한 개발 자극 얻기
    • 부족하다고 생각되는 기본 개념은 꼭 복습하기

2018년

  • 신입사원 회사 적응..

2019년

  • 2019년 읽은 책 : 7권

  • 완료한 강의 : 3개

    • 스프링 웹 MVC_백기선
    • 스프링 프레임워크 핵심기술_백기선
    • JPA 프로그래밍 기본다지기_김영한
  • git : 48 contributions in 2019

  • 블로그 : 12개 업로드

  • 2019년 학습 만족도

    • 베이스: 6점. 미루고 미뤘던 자바의 정석 복습 끝냄. 이펙티브 자바 읽기(가볍게 읽음). 자바8 겉핥기.
    • 프레임 워크: 6점. 백기선님 강의를 통해 스프링 프레임워크에 대해 많이 배움(정리는 해놨지만, 직접 써봐야 내것이 될듯). NoN_Ver2(토이프로젝트) 진행 중
    • 알고리즘: 2점. 기본서는 읽었지만 읽기만 해서 남는게 없음. 코딩테스트는 커녕 기본 자료구조 학습도 거의 안함(4챕터 진행중)
    • 데이터베이스: 2점. 기본SQL 아직 부족함. JPA 기본강의 들음(너무 신기하다. 잘 써보고싶다)
    • 회사 업무: 6점. 쿼리 튜닝, 기본 쿼리 작성에 부족합을 느꼈지만 그 외는 꼼꼼하게 잘 마무리했다고 생각함. 재미있는 개발은 많이 없어서 아쉬움. (잡일이 좀 많았음)

참 많은 일이 있었던 해였음.
병원에서 자는 일이 너무 많았음. (물론 좋은 일도 있었음)
공부한다고 항상 무언가를 하긴 하였으나, 개발자로 성장을 하였거나 코딩을 엄청하거나 그런 느낌이 없다.
강의들은 것은 좋으나 직접 활용해보지 않아 의미없다. (새롭게 알게된게 엄청 많긴한데.... 토이프로젝트에서 적용을 해봐야 함 아니면 까먹을듯...)
메모 습관 중요성 느낌!!(2020년에는 트렐로를 활용해보기로)
토이프로젝트 진행이 너무 더뎠음.
회사에서는 스트레스를 너무 많이 받았음. (업무과부하, 답답함) 맥북 램 16기가로 살껄.... 후회된다...

2020년

  • 2020년 읽은 책 : 9권

  • 완료한 강의 : 4개

    • 스프링 기반 REST API 개발_백기선
    • 더 자바-코드를 조작하는 다양한 방법_백기선
    • 자바 ORM 표준 JPA 프로그래밍-기본편_김영한
    • 실전! 스프링 데이터 JPA_김영한
  • git : 280 contributions in 2020

  • 블로그 : 16개 업로드

  • 2020년 학습 만족도

    • 베이스: 7점. 모던자바, 이펙티브 자바 책 끝냄. 아직 익숙하진 않지만 토이프로젝트에서 사용하려고 노력 중임
    • 프레임 워크: 7점. NoN_Ver2 완성, 스프링 강의, JPA 강의들이 아주 유익했음
    • 알고리즘: 2점. 20년 초반에 자료구조 끝내고 기본 문제를 풀다가 멈춤.
    • 데이터베이스: 1점. 기본SQL 공부 안했음
    • 회사 업무: 5점. 고민할 일이 너무 없다. 주어진 것에 대해서는 깔끔하게 처리하였으나 잘 했다고는 볼 수 없어보인다.

토이 프로젝트를 많은 것을 해볼 수 있었다.
인강에서 학습한 REST API, JPA 를 실습해볼 수 있었고, 멀티모듈 실습 등 재미있는거 많이 해볼 수 있었음!
github 에 커밋이력 채운다고 책 메모도 커밋하고 쌩쇼를하였지만, 결국 코딩을 해야한다.
21년엔 책 메모는 되도록 줄여서 하자.
최근 토이프로젝트를 통해 람다, 스트림을 활용하려고 하고 있다. 당연히 잘 써야한다.
들었던 강의 만족도는 아주 높다. 특히 김영한님 강의는 아주 좋다.
올해는 산거 다 들어보자. 학습 시간을 줄일 수 있다. 이번주 계획 툴로는 트렐로 만족한다. 계속 쓰자.

2021년

  • 2021년 읽은 책 : 4권

  • 완료한 강의 : 3개

    • 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화_김영한
    • 실전! Querydsl_김영한
    • 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술_김영한
  • git : 349 contributions in 2021

  • 블로그 : 8개 업로드

  • 2021년 학습 만족도

    • 베이스: 6점. 자바 성능 튜닝이야기 책 읽음. 이펙티브 자바 내용을 다 까먹은 듯하다. 다시 복습이 필요할 때임
    • 프레임 워크: 7점 : todolist(토이프로젝트) 완성, 스프링 배치가 무엇인지 정도 공부함..
    • 알고리즘: 7점 : 기본문제, 백준, 프로그래머스 단계별로 풀었음(계획대로 하였고, 제일 진도를 많이 나간 해였음)
    • 데이터베이스: 6점 : 기본SQL 책 사서 복습함
    • 회사 업무: 7점 : 나름 뭔가 많이 경험함. 서버설정, 배치프로그램 생성 등 나쁘진않았음

이번 년도 역시 토이프로젝트로 많은 것을 해볼 수 있었다.
연습이나 학습했던 것들을 직접 프로젝트에 적용해볼 수 있었다. 배포 자동화를 해보면서 재미있었고, 블로그에 쓸 내용도 있어서 좋았고 테스트코드를 짜려고 노력하였음!
또한 출근 전 1시간 공부가 많은 것을 할 수 있게 해줬음!(올해는 많이 안빼먹고 잘했음!!)

올해는 이직준비를 하여서 개념공부, 알고리즘 공부 등에 집중하였는데 개념공부는 '애매하게 알면 안된다' 알고리즘은 '꾸준하게 하자' 를 느꼈다.
역시 김영한님의 인터넷 강의 만족도는 높았다.

22년엔 재밌는 공부 많이하고, 좋은 책들 많이 읽자! (일단 오브젝트 책부터 ...!)

2021년 8월 17일 화요일

토이프로젝트 API 개선하기 (REST 하게..)

 

  • 토이프로젝트에 생성한 api는 REST 하지 못한 것 같다. (단순 json 응답임)

    • REST하게 개선해보자 (백기선님의 REST API 강의 참고)

      // 기존 소스
      @RequiredArgsConstructor
      @RestController
      @Slf4j
      @RequestMapping(value = "/api/board")
      public class BoardApiController {
          private final BoardService boardService;
      
      
              @PostMapping
              public ResponseEntity addBoard(@RequestBody BoardRequestDto boardRequestDto, @LoginUser SessionUser user) {
                  Map<String, Object> resultMap = new HashMap<>();
      
      
                  Long result = boardService.save(boardRequestDto, user.getEmail());
      
      
                  resultMap.put("result", result);
                  resultMap.put("resultMessage", "success");
      
      
                  return new ResponseEntity<>(resultMap, HttpStatus.OK);
              }
              ...
      }
      

REST API

  • REpresentational State Transfer(REST) + Application Programming Interface(API)

  • 서로 다른 시스템간의 독립적인 진화를 위함

  • REST 아키텍처 스타일을 따르는 API

    • Client-Server, Cache, Stateless, Layered System, ...

    • Uniform Interface : URL로 지정된 리소스에 대한 조작을 통일하고 한정된 인터페이스로 수행하는 아키텍쳐 스타일

      • Indentification of resources, ...

      • Hypermedia as the Engine of Application State(HATEOAS)

        • 하이퍼미디어(링크)를 통해 애플리케이션 상태 변화가 가능해야한다.

        • 링크 정보를 동적으로 바꿀 수 있다.(Versioning 할 필요 없이!)

        • 응답을 받은 다음에 다음 애플리케이션 상태로 전이를 하려면 서버가 보내준 응답에 들어있는 링크정보를 사용해서 이동을 해야한다.

        • HATEOAS 로 만들려면?

          • HAL을 통해 링크를 정의 (다른 방법도 있음)
      • self-descriptive message

        • 메시지 스스로 메시지에 대한 설명이 가능해야함

        • 서버가 변해도 클라이언트는 그 메시지를 보고 해석이 가능하다

        • 확장 가능한 커뮤니케이션

        • self-descriptive message로 만들려면?

          • HAL의 링크 데이터에 profile 링크 추가 (다른 방법도 있음)

HAL ?

  • Hypertext Application Language

  • API의 리소스 간에 일관되고 쉬운 하이퍼링크 방법을 제공하는 간단한 형식

  • REST API에 HAL을 포함하면 기본적으로 자체 문서화될 뿐만 아니라 사용자가 훨씬 더 쉽게 탐색할 수 있음

Spring HATEOAS ?

  • 스프링 프로젝트 중 하나로 주요 목적은 API를 만들 때 리소스를 REST 하게 쉽게 제공해주기 위한 툴을 편리하게 사용할 수 있게 해주는 라이브러리

  • 하이퍼미디어를 사용해서 클라이언트가 애플리케이션 서바와 동적으로 정보를 주고 받을 수 있는 방법

  • 여러 기능을 제공하는데 가장 중요한 기능은 링크를 만드는 기능, 리소스(응답+링크)를 만드는 기능임

    • 링크
      • HREF
      • REL (현재 릴레이션과의 관계)
        • self
        • profile
        • ... (조회, 업데이트 등 링크)

Spring Rest Docs ?

  • Spring MVC Test를 사용해서 REST API 문서의 조각들을 생성하는데 유용한 툴

    • 테스트에 사용된 정보가 문서로 생성됨
  • 이 문서 조각을 모아서 REST API 가이드 문서를 생성할 수 있음 (html)


실습

Spring HATEOAS

  • 백기선님의 REST API 강의에서는 Resource 사용하였지만 Spring HATEOAS 버전 변경에 따라 이름이 변경됨

  • Resource를 사용하여 구성한 body를 EntityModel 으로 바꿔보자

    • 강의 예제 소스

      public class EventResource extends Resource<Event> {
          public EventResource(Event event, Link... links){
              super(event, links);
              add(linkTo(EventController.class).slash(event.getId()).withSelfRel());
          }
      }
      
      
      ...
      
      
      @PostMapping
      public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto,
                                        Errors errors, @CurrentUser Account currentUser){
      
      
          // @Valid를 통해 검증한 에러들을 errors에 담고 검증하여 badRequest
          if(errors.hasErrors()){
              return badRequest(errors);
          }
          // validate를 통해 검증한 에러들을 errors에 담고 검증하여 badRequest
          eventValidator.validate(eventDto, errors);
          if(errors.hasErrors()){
              return badRequest(errors);
          }
      
      
          Event event = modelMapper.map(eventDto, Event.class);
          event.update();
          event.setManager(currentUser);
      
      
          Event newEvent = this.eventRepository.save(event);
          ControllerLinkBuilder selfLinkBuilder = linkTo(EventController.class).slash(newEvent.getId());
          URI createUri = selfLinkBuilder.toUri();
          EventResource eventResource  = new EventResource(event);
          eventResoruce.add(linkTo(EventController.class).withRel("query-events"));
          eventResoruce.add(selfLinkBuilder.withRel("update-event"));
          eventResoruce.add(new Link("/docs/index.html#resources-events-create").withRel("profile"));
      
      
          return ResponseEntity.created(createUri).body(eventResource);
      }
      
    • 예제를 참고하여 개선한 토이프로젝트 소스

      @PostMapping
      public ResponseEntity addBoard(@RequestBody BoardRequestDto boardRequestDto, @LoginUser SessionUser user, Errors errors) {
      
      
          boardValidator.validate(boardRequestDto, errors);
          if(errors.hasErrors()){
              log.debug("잘못된 요청입니다. error: " + errors.getFieldError());
              return ResponseEntity.badRequest().body(CollectionModel.of(errors.getAllErrors()));
          }
      
      
          Long result = boardService.save(boardRequestDto, user.getEmail());
          boardRequestDto.setId(result);
      
      
          WebMvcLinkBuilder webMvcLinkBuilder = linkTo(BoardApiController.class).slash(result);
      
      
          // 링크 제공
          EntityModel<BoardRequestDto> entityModel = EntityModel.of(boardRequestDto);
          // profile
          entityModel.add(linkTo(IndexController.class).slash("/docs/index.html#resources-add-board").withRel("profile"));
          // self
          entityModel.add(webMvcLinkBuilder.withSelfRel());
          // update
          entityModel.add(webMvcLinkBuilder.withRel("update-board"));
      
      
          return ResponseEntity.created(webMvcLinkBuilder.toUri()).body(entityModel);
      }
      
    • response body

      • _links 를 통해 다양한 상태의 링크를 서버가 제공한다. (HATEOAS)
      {
          "id":30,
          "boardName":"boardName1",
          "delFlag":"N",
          "_links":{
              "profile":{
                  "href":"http://localhost:8080/docs/index.html#resources-add-board"
              },
              "self":{
                  "href":"http://localhost:8080/api/board/30"
              },
              "update-board":{
                  "href":"http://localhost:8080/api/board/30"
              }
          }
      }
      

Spring Rest Docs

  • 문서조각 만들기

    • build.gradle 에 다음을 추가하고 test 코드에 document 관련 코드를 추가하면 문서 조각이 생성된다.

      dependencies {
              ...
              testImplementation('org.springframework.restdocs:spring-restdocs-mockmvc')
              ...
      }
      
      @SpringBootTest
      @Transactional
      @AutoConfigureMockMvc
      @AutoConfigureRestDocs
      @ActiveProfiles("test")
      class BoardApiControllerTest {
      
      
      @Test
      @DisplayName("보드 등록 api 테스트")
      public void addBoardApiTest() throws Exception{
          //given
          BoardRequestDto boardRequestDto = new BoardRequestDto("boardName1");
      
      
          User user = new User("testUser1", "email", "picture", Role.ADMIN);
          userRepository.save(user);
      
      
          // 세션 정보
          SessionUser sessionUser = new SessionUser(user);
          mockHttpSession = new MockHttpSession();
          mockHttpSession.setAttribute("user", sessionUser);
      
      
          //when then
          mockMvc.perform(
                  post("/api/board")
                          .session(mockHttpSession)
                          .contentType(MediaType.APPLICATION_JSON_UTF8)
                          .accept(MediaTypes.HAL_JSON)
                          .content(objectMapper.writeValueAsString(boardRequestDto))
          )
                  .andDo(print())
                  .andExpect(status().is2xxSuccessful())
                  .andExpect(jsonPath("_links.update-board").exists())
                  .andDo(document("create-board", 
                          links(
                                  linkWithRel("profile").description("link to profile"),
                                  linkWithRel("self").description("link to self"),
                                  linkWithRel("update-board").description("link to update an existing board")
                              ),
                          relaxedResponseFields(
                                  fieldWithPath("id").description("identifier of new Board"),
                                  fieldWithPath("boardName").description("name of new Board"),
                                  fieldWithPath("delFlag").description("delFlag of new Board")
                                  )
                          ));
           }
      }
      
    • 테스트 코드 실행 후 생성된 adoc 파일들 (문서조각)



  • 문서 만들기

    • gradle에 맞는 위치에 종합할 문서 파일을 직접 생성한다.(src/docs/asciidoc) https://spring.io/guides/gs/testing-restdocs/

      The default location for Asciidoctor sources in Gradle is src/doc/asciidoc
      
      • 아까 생성한 조각을 사용할 수 있다!

        include::{snippets}/create-board/curl-request.adoc[]
        


    • 생성한 index.adoc 파일을 html로 변환시켜야한다!! 여러 사이트를 참고해서 build.gradle 을 수정해보자

      • https://github.com/spring-projects/spring-restdocs/blob/main/samples/rest-notes-spring-hateoas/build.gradle

      • https://spring.io/guides/gs/testing-restdocs/

      • https://shinsunyoung.tistory.com/85

        plugins {
            id 'org.springframework.boot' version '2.3.8.RELEASE'
            id 'io.spring.dependency-management' version '1.0.11.RELEASE'
            id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
            id 'org.asciidoctor.convert' version '1.5.3' // 추가
            id 'java'
        }
        
        
        group = 'com.toy'
        version = '0.0.1-SNAPSHOT'
        sourceCompatibility = '11'
        
        
        repositories {
            mavenCentral()
            maven { url "https://plugins.gradle.org/m2/" }
        }
        
        
        subprojects {
            group = 'com.toy'
            version = '0.0.1-SNAPSHOT'
        
        
            apply plugin: 'java'
            apply plugin: 'org.springframework.boot'
            apply plugin: 'io.spring.dependency-management'
            apply plugin: 'com.ewerk.gradle.plugins.querydsl'
            apply plugin: 'org.asciidoctor.convert' // 추가
        
        
            sourceCompatibility = 11
        
        
            repositories {
                mavenCentral()
            }
        
        
            dependencies {
                asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor' // 추가
                compileOnly 'org.projectlombok:lombok'
                annotationProcessor 'org.projectlombok:lombok'
                compile("org.mariadb.jdbc:mariadb-java-client")
        
        
                testImplementation('org.springframework.boot:spring-boot-starter-test') {
                    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
                }
                testImplementation('org.springframework.restdocs:spring-restdocs-mockmvc')
                testImplementation 'org.springframework.security:spring-security-test'
                developmentOnly 'org.springframework.boot:spring-boot-devtools'
            }
        
        
                // 추가
            ext {
                snippetsDir = "${buildDir}/generated-snippets"
            }
        
        
            configurations {
                compileOnly {
                    extendsFrom annotationProcessor
                }
            }
        
        
            test {
                outputs.dir snippetsDir // 추가
                useJUnitPlatform()
            }
        
        
            // 추가
            asciidoctor {
                inputs.dir snippetsDir
                dependsOn test
            }
        
        
            def querydslDir = "$buildDir/generated/querydsl"
        
        
            querydsl {
                library = "com.querydsl:querydsl-apt"
                jpa = true
                querydslSourcesDir = querydslDir
            }
            sourceSets {
                main {
                    java {
                        srcDirs = ['src/main/java', querydslDir]
                    }
                }
            }
            compileQuerydsl{
                options.annotationProcessorPath = configurations.querydsl
            }
            configurations {
                querydsl.extendsFrom compileClasspath
            }
        
        
        }
        
        
        project(':module-web') {
            dependencies {
                implementation project(path: ':module-domain', configuration: 'default')
            }
        
        
            // 추가
            task copyDocument(type: Copy) {
                dependsOn asciidoctor
        
        
                from file("build/asciidoc/html5/")
                into file("src/main/resources/static/docs")
            }
        
        
            // 추가
            build {
                dependsOn copyDocument
            }
        }
        
        
        project(':module-batch') {
            dependencies {
                implementation project(path: ':module-domain', configuration: 'default')
            }
        }
        
        
        bootJar { enabled = false }
        
      • gradle build 후 다음 경로에 html 파일이 생성된 것을 볼 수 있으며 profile에 해당 문서를 링크하면 API에 대한 정보, 명세 등을 볼 수 있고, 스스로 설명할 수 있다! (self-descriptive message)


                    

            
[간단히 생성한 html 파일]
                  


2021년 6월 19일 토요일

gradle task - bootJar, build 비교 메모

젠킨스에서 토이프로젝트 jar 생성을 위해 ./gradlew bootJar로 사용하다 ./gradlew build 로 변경했는데 테스트 실패 오류가 빵빵 터졌다.

그렇다면 bootJar는 test를 안해서 그동안 성공을 했던건가??

동일한 모듈을 실행해보고 로그를 비교해보자!!


  • bootJar : Assembles an executable jar archive containing the main classes and their dependencies.

8:11:17 오전: Executing task 'bootJar'...

> Task :module-domain:initQuerydslSourcesDir
> Task :module-domain:compileQuerydsl UP-TO-DATE
> Task :module-domain:compileJava UP-TO-DATE
> Task :module-domain:processResources UP-TO-DATE
> Task :module-domain:classes UP-TO-DATE
> Task :module-domain:jar UP-TO-DATE
> Task :module-web:initQuerydslSourcesDir
> Task :module-web:compileQuerydsl UP-TO-DATE
> Task :module-web:compileJava UP-TO-DATE
> Task :module-web:processResources UP-TO-DATE
> Task :module-web:classes UP-TO-DATE
> Task :module-web:bootJar UP-TO-DATE
...
BUILD SUCCESSFUL in 1s
10 actionable tasks: 2 executed, 8 up-to-date
  • build : Assembles and tests this project.

8:13:01 오전: Executing task 'build'...

> Task :module-domain:initQuerydslSourcesDir
> Task :module-domain:compileQuerydsl UP-TO-DATE
> Task :module-domain:compileJava UP-TO-DATE
> Task :module-domain:processResources UP-TO-DATE
> Task :module-domain:classes UP-TO-DATE
> Task :module-domain:jar UP-TO-DATE
> Task :module-web:initQuerydslSourcesDir
> Task :module-web:compileQuerydsl UP-TO-DATE
> Task :module-web:compileJava UP-TO-DATE
> Task :module-web:processResources UP-TO-DATE
> Task :module-web:classes UP-TO-DATE
> Task :module-web:bootJar UP-TO-DATE

> Task :module-web:jar SKIPPED
> Task :module-web:assemble UP-TO-DATE
> Task :module-web:compileTestJava
> Task :module-web:processTestResources NO-SOURCE
> Task :module-web:testClasses
> Task :module-web:test
> Task :module-web:check
> Task :module-web:build
...
BUILD SUCCESSFUL in 35s
12 actionable tasks: 4 executed, 8 up-to-date
  • bootJar와 비교했을 때 빨간 부분이 추가되었다. 일단 설명만 봐도 test를 한다고 써있다!!

    • bootJar는 실행 가능한 jar만 생성하는 것으로 보여진다.

    상황에 맞게 task 를 잘 선택하자!!

  • 다음과 같이 그룹 및 task를 쉽게 추가할 수 있으며 기존 task 활용, 순서 등 다양하게 활용할 수 있다.

    • 설정하지 않아도 보여지는 task 그룹들은 build.gradle 에 추가한 플러그인들을 통해 기본으로 보여지는 것 같음
    • 프로젝트 환경에 따라 build 구성을 할 수 있다.
    • 다음에 해보는 걸로...