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 구성을 할 수 있다.
    • 다음에 해보는 걸로...

2021년 5월 20일 목요일

AWS 자동배포 연습해보기

 

이전 포스팅(https://cotmulgyu.blogspot.com/2020/07/blog-post.html)에서 github의 webhooks 를 통해 push가 되면 젠킨스에서 빌드하고 특정 명령어를 실행해서 배포할 수 있음을 확인하였다.

이번에는 CI/CD 서버에서 빌드 후 실제 배포 서버(aws)에 빌드된 파일을 보내고 배포서버에서 바로 배포까지 할 수 있도록 해본다!

  • aws 인스턴스 생성과정은 생략

1_프로젝트 설정

REPOSITORY=/home/ec2-user/project/todolist
PROJECT_NAME=to-do-list

echo "> Build 파일복사"

cp $REPOSITORY/zip/*.jar $REPOSITORY/


echo "> 현재 구동중인 애플리케이션 pid 확인"

CURRENT_PID=$(pgrep -f ${PROJECT_NAME}.*.jar)

echo "현재 구동중인 애플리케이션 pid: $CURRENT_PID"


if [ -z "$CURRENT_PID" ]; then
        echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
        echo "> kill -15 $CURRENT_PID"
        kill -15 $CURRENT_PID
        sleep 5
fi

echo "> 새 애플리케이션 배포"

JAR_NAME=$(ls -tr $REPOSITORY/*.jar | tail -n 1)

echo "> JAR Name: $JAR_NAME"


echo "> $JAR_NAME 실행"

nohup java -jar \
        -Dspring.config.location=classpath:/application.yml,/home/ec2-user/project/properties/application-oauth.properties \
        -Dspring.profiles.active=real \
        $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &
  • AWS CodeDeploy에서 사용할 appspec.yml 파일을 생성한다.

version: 0.0
os: linux
files:
  - source:  /
    destination: /home/ec2-user/project/todolist/zip # S3에서 이동시킬 경로
    overwrite: yes

permissions: # 권한설정이 없으면 root로 설정되므로 다음의 설정 필요
  - object: /
    pattern: "**"
    owner: ec2-user
    group: ec2-user

hooks:
  ApplicationStart:
    - location: start.sh # 애플리케이션 실행
      runas: ec2-user


2_AWS 설정

  • http://m.yes24.com/Goods/Detail/83849117 해당 책을 참고하면 자세하게 설명되어있다...

  • IAM

    • Identity and Access Management(IAM)는 AWS 리소스에 대한 액세스를 안전하게 제어할 수 있는 웹 서비스. IAM을 사용하여 리소스를 사용하도록 인증(로그인) 및 권한 부여(권한 있음)된 대상을 제어함
    • IAM > 사용자 추가
      • 엑세스유형: 프로그래밍 방식 엑세스
      • 권한 설정: 기존정책 직접 연결 > AmazonS3FullAccess , AWSCodeDeployFullAccess 체크
      • 엑세스키와 비밀 엑세스 키는 잘 관리하도록한다..
  • S3 버킷

    • AWS의 S3 서비스는 일종의 파일 서버임 (파일들을 저장하고, 접근 권한을 관리, 검색을 지원)
    • S3 > 버킷 만들기
      • 다른 건 기본설정..
      • 퍼블릭 엑세스 차단 : 모든퍼블릭 엑세스 차단 체크
  • CodeDeploy

    • AWS CodeDeploy는 Amazon EC2 인스턴스 및 온프레미스에서 실행 중인 인스턴스를 비롯하여 모든 인스턴스에 대한 코드 배포를 자동화하는 서비스

    • IAM > 역할만들기

      • 역할은 EC2에서 사용할 것임
      • 신뢰할 수 있는 유형의 개체 선택 : AWS 서비스
      • 이 역할을 사용할 서비스 선택 : EC2
      • 정책 : AmazonEC2RoleforAWSCodeDeploy 체크
    • EC 인스턴스로 이동 > 인스턴스 우클릭 > 인스턴스 설정 > IAM 역할 연결/바꾸기 > 아까 역할 추가후 재부팅 (보안-IAM 연결 수정)

    • CodeDeploy 에이전트 설치

      • CodeDeploy의 요청을 받을 수 있도록 EC2에 에이전트를 설치한다.

      • EC2에서 다음 명령어 입력

        aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-  northeat-2
        
        
        chmod -x ./install
        
        
        sudo ./install auto
        
    • CodeDeploy 권한 생성

      • CodeDeploy가 EC2에 접근하려면 권한이 필요하다.
      • IAM > 역할 생성
        • 서비스 : AWS 서비스 > CodeDeploy
        • 사용사례 선택 : CodeDeploy
        • 권한 : AWSCodeDeployRole
    • CodeDeploy 생성

      • CodeDeploy > 애플리케이션 생성

      • 컴퓨팅 플랫폼 : EC2/온프레미스

      • 배포그룹 생성

        • 서비스역할: CodeDeploy IAM 역할 선택
        • 배포유형 : 현재 위치
        • 환경 구성 : Amazon EC2 인스턴스
        • 배포 구성 : CodeDeployDefault.AllAtOnce
        • 로드 밸런서 : 로드 밸런싱 활성화 해제


3_젠킨스 설정

  • 젠킨스 - Jenkins 관리 - 플러그인 관리 - 다음 플러그인 설치 (AWS CodeDeploy Plugin for Jenkins)

    • 해당 플러그인은 빌드 후 AWS CodeDeploy를 사용할 수 있는 플러그인으로 빌드 후 특정 경로를 압축하여 S3에 업로드한다. 그리고 설정한 배포를 시작한다



  • 이전에 설정한 Build-Execute Shell 부분에 자신의 프로젝트에 맞는 빌드명령어를 실행한다. (build, bootJar 등)




  • Build-Execute Shell 부분에 AWS CodeDeploy에서 사용할 파일(appspec.yml, start.sh)과 빌드된 jar를 포함하여 복사시킨다.

    • 여기있는 파일들을 aws S3 Bucket으로 이동시킴 (zip)

    • CodeDeploy 말고 다른플러그인으로 S3 업로드만도 가능한데 그러면 여기서 미리 압축이 필요하다.




  • 빌드 후 조치 추가 - Deploy an application to AWS CodeDeploy

    • AWS CodeDeploy Application Name : AWS CodeDeploy 에서 만든 어플리케이션 이름

    • AWS CodeDeploy Deployment Group : 설정한 그룹 이름

    • AWS CodeDeploy Deployment Config : 설정한 CodeDeploy 배포 구성

    • S3 Bucket : AWS S3 Bucket 이름

    • S3 Prefix : 버킷에 생성될 디렉토리

    • Subdirectory : 젠킨스 서버에서 포함될 파일이 저장될 디렉토리

    • Include Files : 포함될 파일

    • Use Access/Secret keys : AWS 에서 생성한 IAM 정보





  • 젠킨스 빌드 후 AWS S3 버킷을 확인해보면 다음과 같이 설정한 디렉토리 밑에 zip 파일이 생기는 것을 확인할 수 있음



  • AWS CodeDeploy 에서 아까 생성한 애플리케이션을 들어가보면 다음과 같이 배포 이력을 볼 수 있음



    • 배포 실패 시 /var/log/aws/codedeploy-agent 경로에서 로그확인 가능!!
    • appspec.yml 에 명시된 경로에 s3 zip파일이 풀려있는 것을 확인할 수 있다.



    • start.sh에 의해 jar가 복사되고 애플리케이션 로그를 볼 수 있는 nohup.out 파일이 생성된 것을 확인할 수 있음




2021년 4월 4일 일요일

Entity 생성 - 컬럼에 default 값 관련 메모

Entity - 컬럼에 default 값

토이프로젝트 중 컬럼에 기본 값을 넣고 싶어서 다음과 같은 어노테이션을 사용하였는데, 내가 생각하는 null을 insert 하였을 때 기본 값이 들어가지 않았다.

@Column(columnDefinition="varchar(10) default 'N'")
private Long flag;

테이블 스키마 생성에만 관여하고 insert할 때는 영향이 없는 것 같다. (이유는 더 찾아봐야겠따...)

김영한님은 다음과 같이 사용하신다고 한다. https://www.inflearn.com/questions/83662

...
제가 신규로 진행하는 프로젝트들은 테이블이 제공하는 default를 거의 사용하지 않습니다.

엔티티에 중심으로 개발하다 보니, 객체에 값을 넣는 방식으로 주로 진행합니다.

쉽게 이야기해서 생성자에서 기본값을 미리 설정하는 방식을 사용합니다.

다른 예를 들어드리면, 실무에서 @Index(인덱스 조건)도 DDL을 생성할 때만 사용하기 때문에 사실은 적을 필요가 없지만, 
그래도 엔티티에 이 애노테이션이 있으면, 개발자분들이 엔티티만 보고 인덱스를 생각하고 JPQL을 작성할 수 있기 때문에 사용합니다. 
말씀하신 default도 그런 관점에서 저는 적으면 좋겠다고 생각합니다.
...

위 내용과 같이 생성자에서 값을 그냥 넣어주던가 이렇게 사용하는 방법도 있다.

@Column
private Long flag = "N";

2021년 3월 15일 월요일

샘플데이터 생성하기

 로컬에서 조회 기능 확인할 때 자동으로 생성되는 샘플데이터가 있으면 편하다.

다음과 같이 생성하자

@Component
@RequiredArgsConstructor
public class InitContents {

    private final InitContentsService initContentsService;

    @PostConstruct
    public void init(){
        initContentsService.init();
    }

    @Component
    static class InitContentsService{

        @PersistenceContext
        private EntityManager em;

        @Transactional
        public void init(){

            Board board = new Board("board1");
            em.persist(board);

            Topic topic1 = new Topic("topic1", board);
            Topic topic2 = new Topic("topic2", board);
            em.persist(topic1);
            em.persist(topic2);


            Card card1 = new Card("card1", "des1", topic1);
            Card card2 = new Card("card2", "des2", topic1);
            Card card3 = new Card("card3", "des3", topic2);
            Card card4 = new Card("card4", "des4", topic2);
            Card card5 = new Card("card5", "des5", topic2);
            Card card6 = new Card("card6", "des6", topic2);
            em.persist(card1);
            em.persist(card2);
            em.persist(card3);
            em.persist(card4);
            em.persist(card5);
            em.persist(card6);

            CheckList checkList1 = new CheckList("checkList1", card1);
            CheckList checkList2 = new CheckList("checkList2", card1);
            em.persist(checkList1);
            em.persist(checkList2);

            CheckItem checkItem1 = new CheckItem("checkItem1",checkList1);
            CheckItem checkItem2 = new CheckItem("checkItem2",checkList1);
            CheckItem checkItem3 = new CheckItem("checkItem3",checkList1);
            CheckItem checkItem4 = new CheckItem("checkItem4",checkList2);
            CheckItem checkItem5 = new CheckItem("checkItem5",checkList2);

            checkItem1.update("checkItem1", "N", "Y");
            checkItem2.update("checkItem2", "N", "Y");

            em.persist(checkItem1);
            em.persist(checkItem2);
            em.persist(checkItem3);
            em.persist(checkItem4);
            em.persist(checkItem5);

            checkList1.addCheckItem(checkItem1);
            checkList1.addCheckItem(checkItem2);
            checkList1.addCheckItem(checkItem3);
            checkList2.addCheckItem(checkItem4);
            checkList2.addCheckItem(checkItem5);

            Label label1 = new Label("label1", "green");
            Label label2 = new Label("label2", "red");
            Label label3 = new Label("label3", "blue");

            em.persist(label1);
            em.persist(label2);
            em.persist(label3);

            CardLabel cardLabel1 = new CardLabel(card1, label1);
            CardLabel cardLabel2 = new CardLabel(card2, label2);
            CardLabel cardLabel3 = new CardLabel(card1, label3);
            CardLabel cardLabel4 = new CardLabel(card4, label3);

            card1.addCardLabel(cardLabel1);
            card2.addCardLabel(cardLabel2);
            card1.addCardLabel(cardLabel3);
            card4.addCardLabel(cardLabel4);

        }
    }
}
  • 왜 바로 @PostContruct 에 넣지 않고 따로 빈을 만들었는가?

    This is as defined, actually: init methods (such as @PostConstruct methods) are always called on the target instance itself.
    The proxy will only be generated once the target instance has been fully initialized...
    In other words, the @Transactional proxy isn't even created at the point of the @PostConstruct call yet.
    
    
    @PostConstructor 는 인스턴스 자체에서 호출됨
    프록시는 인스턴스가 완전히 초기화된 후에 생성됨
    그래서 @Transcational 프록시는 @PostConstruct 호출 시점에 생성되지 않음

2021년 3월 1일 월요일

@NoArgsConstructor(access = AccessLevel.PROTECTED)

@NoArgsConstructor(access = AccessLevel.PROTECTED) 쓰는 이유


  • JPA는 기본적으로 디폴트 생성자가 필요함(파라미터가 없는 생성자)

    • protected 까지만 허용됨
    • @NoArgsConstructor(access = AccessLevel.PROTECTED) 을 통해 가능
  • 그렇다고 public 으로 만들면 객체 생성이 일관하지 않게 막 생성될 수 있음!

    • 그래서 protected로 제한하자. (JPA에 필요하기 때문에 private X)

// title이 필수인데 실수로 누락될 수 있음
// 혹은 이상한 곳에서 setter로 인해 엔티티 값이 변경될 수 있음
Board board1 = new Board();
board1.setContent("content1");


// setter를 사용하지 말고 이렇게 사용하자
Board board2 = Board.builder()
  .title("title1")
  .content("content1")
  .build();

board2.updateInfo("updateTitle", "updateContent");