[우아한테크코스] 8월 20일 TIL

6 minute read

Sprint 1 학습로그

[Git] 브랜치 전략

  • Main Branch

    최종 릴리즈 버전이 들어가는 브랜치로 이 브랜치가 배포 판에서 사용된다.

  • Dev Branch

    릴리즈 버전까지 구현되는 기능들이 병합되는 브랜치로 dev가 다 병합되면 main으로 병합한다.

  • Feature Branch

    각 이슈별 기능을 구현해 dev에 병합하는 브랜치

  • Hotfix Branch

    베포된 main 브랜치에서 오류가 발생하면 사용하는 브랜치

[협업] 컨벤션 정하기

  • 백엔드
    • 이슈 추정
    • Tab
    • Exception
    • DTO
    • Response Message
    • final
    • test
    • transactional
  • 전체
    • 일정 산출
    • 팀문화
    • 프로젝트 용어
    • PR
    • commit message
    • api 설계

[JPA] entity 설계

자율적인 객체들의 협력 공동체를 만들자

  • 객체를 테이블에 맞추어 모델링 JDBC를 이용해서는 객체의 협력 관계를 만들 수 없다. JDBC 테이블은 외래키 조인을 통해 연관된 테이블을 찾고, 객체는 객체대로 참조를 이용해야 한다는 단점이 존재한다.

  • 객체 지향 모델링 객체의 참조와 테이블의 외래키를 매핑한다. 참조로 연관관계 조회와 객체 그래프 탐색을 할 수 있다.

  • 데이터베이스 스키마 생성

    hibernate.hbm2ddl.auto.~~

    • create: drop + create
    • create-drop: 종료 시점에 테이블 drop
    • update: 변경된 것만 반영(운영DB에서 사용하면 안됨) -> 수만건일 때 문제 발생
    • validate: 엔티티와 테이블 정상 매핑 확인
    • none: 사용하지 않음

[JPA] 연관관계 매핑

  • 연관관계 매핑 어노테이션: @ManyToOne, @OneToMany, @OneToOne, @ManyToMany / @JoinColumn, @JoinTable
  • 상속관계 매핑 어노테이션: @Inheritance, @DiscriminatorColumn, @DiscriminatorValue, @MappedSuperclass
  • 복합키 어노테이션: @IdClass, @EmbeddedId, @Embeddable, @MapsId => 여러 키 묶어서 pk로 쓰고 싶은 경우

[JPA] 내부구조

entityManagerFactory에서 entityManager를 생성하고, entityManager가 커넥션풀에서 커넥션을 꺼내 db에 연결해 사용

  • 영속성 컨텍스트

    엔티티를 영구 저장하는 환경으로 엔티티 매니저와 영속성 컨텍스트가 N:1로 매핑됨

    • 1차 캐시
    • 동일성 보장
    • 트랜잭션을 지원하는 쓰기 지연
    • 변경 감지(dirty checking)
    • 지연 로딩(Lazy Loading)
  • 엔티티의 생명주기

    • 비영속(new/transient)

    • 영속(managed): 관리된 상태

    • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태(관리를 포기한 상태)
    • 삭제(removed)
  • 플러시

    영속성 컨텍스트의 변경 내용을 commit 직전에만 db에 동기화

    • 플러시 모드 옵션

      FlushModeType.AUTO: 커밋이나 쿼리 실행할 때(default)

      FlushModeType.COMMIT: 커밋할 때만 플러시

  • 프록시와 즉시로딩, 지연로딩

[JPA] Spring Data JPA와 QueryDSL 이해

  • Spring Data JPA

    개발자는 인터페이스만 작성하고 구현 객체는 스프링이 동적 생성해 주입

    JpaRepository는 공통 crud 제공

public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByUsername(String username);
}
  • 쿼리 메서드 기능: 인터페이스만 만들면 메서드 이름만으로 JPQL 쿼리 생성

  • 이름으로 검색 + 정렬 + 페이징

    전체 페이지수, 다음 페이지 및 페이징을 위한 모든 content, page관련 내용 나옴

public Page<Member> member() {
    PageRequest request = PageRequest.of(1, 10); //(page, size)
    return repository.findByName("name", request);
}
  • @Query, @JPQL 정의

    nameQuery와 같이 어노테이션으로 정의 가능

  • QueryDSL

    SQL, JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API

    sql, jpql은 문자라 실행되기 전까지 오류가 있는지 확인해볼 수 없는데 이걸 도와줌

    컴파일 시점에 문법 오류 발견. 조인, 쿼리, 동적 쿼리 다 됨

    무엇보다 자바 코드가 아닌 쿼리에서도 메서드 분리와 같이 자바 코드처럼 사용할 수 있다. 제약조건을 조립할 수 있어 가독성과 재사용성이 뛰어나다.

    JPaQueryFactory, QMember 사용

public void hello() {
    JPAQueryFactory query = new JPAQueryFactory(em);
    QMember m = QMember.member;

    List<Member> members = query.selectFrom(m)
        .where(m.age.gt(18).and(m.name.contains("hello")))
        .orderBy(m.age.desc())
        .limit(10)
        .offset(10)
        .fetch();
}

[JPA] 값 매핑

  1. @Lob: clob, blob과 매핑

    • CLOB: 문자 대형 객체, character
    • BLOB: 이진 대형 객체, Binary

    최고 4gb까지 저장, lob 값, lob 위치에 대한 포인터인 위치자 존재

    lob에 저장하고자 하는 값이 너무 크므로 메모리에 저장해두고 포인터로 들고 있다가 필요할 때 가서 쓴다.

    • mysql의 large object -> BLOB, TEXT, LONG TEXT

    • oracle의 large objecct -> BLOB, CLOB

  2. @Temporal: 날짜 타입(Date, Calendar) 매핑

    @Temporal(TemporalType.DATE) //날짜  
    @Temporal(TemporalType.TIME) //시간  
    @Temporal(TemporalType.TIMESTAMP) //날짜와 시간  
    

    temporal 사용하지 않으면 timestamp로 매핑

  3. @Column: 컬럼 매핑

    name, nullable과 같은 속성을 전달하고 생략하면 데이터타입에 맞춰서 전달한다.

    하지만 int 같은 경우 null이 허용되지 않는데 허용하고 싶으면 @Column 사용해서 전달해야 함

    • updatable, insertable

      읽기 전용으로 false면 쓰기 안됨

    • length

      String인 경우에만 길이 제한 가능

    • unique

      컬럼에 유니크 제약조건, 두 컬럼 이상 주고 싶으면 클래스 레벨에서 @Table(uniqueConstraints = ~~) 사용

  4. @Enumerated: 이넘 타입 매핑

    ORDINAL: enum을 숫자로 0, 1, 2로 저장

    STRING: enum의 이름을 String으로 저장

  5. @CreatedDate, @LastModifiedDate

    • @CreatedDate: 생성일자 LocalDateTime으로 매핑
    • @LastModifiedDate: 마지막 수정일자 LocalDateTime으로 매핑

참고

[JPA] 설정

  • mysql
spring.datasource.url=jdbc:h2:~/test;MODE=MySQL
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
  • h2
spring.datasource.url=jdbc:h2:~/test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.username=sa
  • 로그 설정
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

[JPA] Auditing

DB 데이터를 보고 있다가 DB에서 생성이나 수정이 일어나면 값을 넣어주도록 한다.

  • application이나 config 클래스 상단에 @EnableJpaAuditing 어노테이션 추가

    클래스 상단에 콜백 리스너 @EntityListeners(AuditingEntityListener.class) 어노테이션 추가

    • 생성 시점: @Created 어노테이션 추가
    • 마지막 변동 시점: @LastModified 어노테이션 추가

[Infra] Jenkins를 사용한 자동 배포환경 구축

Jenkins를 이용해 CI/CD 환경을 구축한다.

Jenkins에서 github pr이 build Success 하는지 확인하고, 확인되면 머지한다.

머지된 후에 jenkins가 한 번 더 빌드를 하고 성공하면 쉘 스크립트에 따른 배포를 진행한다.

  1. 프로젝트 등록

  2. jenkins 설치

  3. enter an item name 입력하고 Freestyle project 클릭하고 시작
  4. github url 따와서 소스 코드 관리 repository url에 연결
  5. username에 내 이름이랑 password 입력
  6. add 하면 credentials에 입력한거 뜨고 branches to build에 브랜치 연결
  7. 빌드 유발 GitHub hook trigger for GITScm polling 선택
  8. build execute shell에 ./gradlew clean print 설정
  9. 빌드 후 조치에 이메일 추가 가능
  10. 이후 github repository랑 Jenkins hook url 연결 -> webhooks에 payload에 http://jenkins url(local이면 안됨, ngrok 사용, Content type: application/json)/github-webhook/, just the push event 클릭,
  11. build now 클릭하면 빌드 수행
  12. console output에서 빌드 출력문 확인 가능

참고

[Spring] RestDocs with RestAssured

  1. build.gradle 설정
plugins {
    id 'org.springframework.boot' version '2.5.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id "org.asciidoctor.convert" version "1.5.10"
    id 'java'
}

group = 'com.woowacourse'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

ext {
    snippetsDir = file('build/generated-snippets')
}

dependencies {
    //restdocs
    asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
}

test {
    outputs.dir snippetsDir
    useJUnitPlatform()
}

asciidoctor {
    inputs.dir snippetsDir
    dependsOn test
}

task createDocument(type: Copy) {
    dependsOn asciidoctor
    from file("build/asciidoc/html5/index.html")
    into file("src/main/resources/static/docs")
}

bootJar {
    dependsOn createDocument
    from ("${asciidoctor.outputDir}/html5") {
        into 'static/docs'
    }
}
  1. src/docs/asciidoc/index.adoc
= ZZIMKKONG Application API Document
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3
:sectlinks:

include::member.adoc[]
  1. Test 코드 상단에 어노테이션 추가
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@AutoConfigureRestDocs
  1. Test 코드 setUp에 메서드 추가
@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
  RequestSpecification spec = new RequestSpecBuilder()
    .addFilter(documentationConfiguration(restDocumentation))
    .build();
  DocumentUtils.setRequestSpecification(spec);
}
  1. Test 코드에 메서드 추가
RestAssured
    .given(DocumentUtils.getRequestSpecification()).log().all()
    .accept("application/json")
    .filter(document("member", DocumentUtils.getRequestPreprocessor(), DocumentUtils.getResponsePreprocessor()))
    ...
  1. build.gradle에 asciidoctor 재생
    build/generated-snippets/기능명/메서드명 에 http-request.adoc, http-response.adoc 기타 등등 추가되었는지 확인

  2. src/docs/asciidoc/member.adoc 수정

== Member(멤버)
=== 멤버 회원가입
==== Request
include::{snippets}/member/http-request.adoc[]
==== Response
include::{snippets}/member/http-response.adoc[]
  1. Test 모두 통과하면 ./gradlew build bootJar 적용

  2. src/main/resources/static/docs에 index.html에 자신의 api가 추가되었는지 확인

  3. application 돌리고 http://localhost:8080/docs/index.html에 접속해서 반영되었는지 확인

[Java] Builder 패턴

빌더패턴이란, 객체 생성을 깔끔하고 유연하게 하기 위한 기법이다.
필수 인자를 받는 생성자를 만들고, 선택 인자들을 점진적으로 체이닝하여 받을 수 있도록 한다.

객체 내에 Builder 클래스를 만들고 선택 인자들을 부생성자로 추가해둔다.

최종 객체 생성은 다음과 같이 할 수 있다.

Reservation reservation = new Reservation
    .Builder(123, 345)
    .date(678)
    .description(890)
    .build();

이와 같이 구현함으로써 각 인자가 어떤 의미인지 알기 쉽고 immutable한 객체로 만들 수 있다.

Lombok을 사용할 경우 더욱 간단히 빌더패턴을 적용할 수 있다.
@Builder 어노테이션을 적용하면 아래와 같이 선언만 해도 빌더를 사용할 수 있다.

선언부

@Builder
public class Reservation {
    private final int name;
    private final int date;
    private final int description;
}

구현부

Reservation.ReservationBuilder builder = Reservation.builder();
builder.name(345);
builder.date(678);
builder.description(123);
Reservation reservation = builder.build();

Reservation reservation = Reservation.builder()
    .name(123)
    .date(456)
    .description(789)
    .build();

[Spring] custom validator 만들기

ConsistentDate 어노테이션

@Constraint(validatedBy = ConsistentDateValidator.class)
@Target(ElementType.METHOD)
@Retention(RUNTIME)
@Documented
public @interface ConsistentDate {
    String message() default
            "종료 시간을 확인해주세요.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

ConsistentDateValidator 구현부

public class ConsistentDateValidator
        implements ConstraintValidator<ConsistentDate, Object[]> {
    @Override
    public boolean isValid(
            Object[] value,
            ConstraintValidatorContext context) {

        if (value[0] == null || value[1] == null) {
            return true;
        }

        if (((LocalDateTime) value[0]).isBefore(LocalDateTime.now())) {
            throw new ImpossibleStartTimeException();
        }

        return ((LocalDateTime) value[0]).isAfter(LocalDateTime.now())
                && ((LocalDateTime) value[0]).isBefore((LocalDateTime) value[1]);
    }
}

등록한 타입에 맞춰서 어노테이션 달아서 적용

String 타입으로 어노테이션에 넣은 경우는 @ConsistentDate(startTime = "startDateTime", endTime = "endDateTime"), 그냥 메서드 위에 단 경우에는 @ConsistentDate로 적용

[JPA] AND/OR combination

필요한 것은 spaceId와 매칭되면서 startTime과 endTime 사이에도 매칭되는 것이었다.

기호로는 (spaceId && StartTimeBetween) || (spaceId &&EndTimeBetween) 이다.

이를 위해서 existBySpaceIdAndStartTimeBetweenOrEndTimeBetween을 하였다.

하지만 이렇게 되면 (spaceId && StartTimeBetween)||EndTimeBetween 을 보는 것이었다.

검색을 해봤지만 QueryDsl을 이용하는 방안밖에 찾지 못했고 결국에 jpa 메서드를 두번 날리고 그 둘의 결과를 OR 연산 하는 방안으로 수정했다.

existsBySpaceIdAndStartTimeBetween || existsBySpaceIdAndEndTimeBetween

그리고 나는 처음에는 내가 가진 start와 end 사이에 DB에 존재하는 start나 end가 있으면 될 것이라고 생각했는데 JPA의 Between 쿼리문은 내가 가진 start와 end 사이에 DB에 존재하는 start가 있는지 보고, end가 있는지 보는 일이 따로 일어나야 했다. 그래서 하나의 between ? 쿼리문마다 두개의 인자를 전달해주어야 한다.

[SpringBootTest] mockito 사용시

  • SpringBootTest에서 MockBean 생성해서 사용하는 방법
@SpringBootTest
@ActiveProfiles("test")
public class ServiceTest {
    @MockBean
    protected ReservationRepository reservationRepository;
}
  • Service 내에서 given when then 으로 사용하는 방법

jpaRepository에서는 save가 given - willReturn 시 이미 id가 있거나 비어있는 상태의 클래스가 들어오면 구현되지 않는다. 값을 any로 넣어야 한다.

given(reservationRepository.save(any(Reservation.class)))
    .willReturn(savedReservation);

BDD mockito vs mockito BDD의 given() <-> Mockito의 when() BDD의 then().should() <-> Mockito의 verify() “시나리오에 맞게 테스트 코드가 읽힐 수 있도록”

[Java] DateTime 초 날리기

나는 분까지만 필요한데 LocalDateTime.now()에서 나노초까지 나온다면 .truncatedTo(ChronoUnit.SECONDS) 사용 혹은 .withSecond(0).withNano(0) 사용

[Java] dto 값 비교하기

동일성이 아닌 동등성 비교 시 usingRecursiveComparison()을 적용한다. 필드값 비교를 통해 중첩된 객체까지 들어가서 값을 비교한다. ignoringFields()하면 특정 필드를 무시할 수 있고,ignoringActualNullFields(), ignoringExpectedNullFields()로 null field도 분류할 수 있다.

assertThat(actualResponse).usingRecursiveComparison()
        .ignoringCollectionOrder()
        .ignoringExpectedNullFields()
        .isEqualTo(expectedResponse);