[우아한테크코스] 8월 20일 TIL
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] 값 매핑
-
@Lob: clob, blob과 매핑
- CLOB: 문자 대형 객체, character
- BLOB: 이진 대형 객체, Binary
최고 4gb까지 저장, lob 값, lob 위치에 대한 포인터인 위치자 존재
lob에 저장하고자 하는 값이 너무 크므로 메모리에 저장해두고 포인터로 들고 있다가 필요할 때 가서 쓴다.
-
mysql의 large object -> BLOB, TEXT, LONG TEXT
-
oracle의 large objecct -> BLOB, CLOB
-
@Temporal: 날짜 타입(Date, Calendar) 매핑
@Temporal(TemporalType.DATE) //날짜 @Temporal(TemporalType.TIME) //시간 @Temporal(TemporalType.TIMESTAMP) //날짜와 시간
temporal 사용하지 않으면 timestamp로 매핑
-
@Column: 컬럼 매핑
name, nullable과 같은 속성을 전달하고 생략하면 데이터타입에 맞춰서 전달한다.
하지만 int 같은 경우 null이 허용되지 않는데 허용하고 싶으면 @Column 사용해서 전달해야 함
-
updatable, insertable
읽기 전용으로 false면 쓰기 안됨
-
length
String인 경우에만 길이 제한 가능
-
unique
컬럼에 유니크 제약조건, 두 컬럼 이상 주고 싶으면 클래스 레벨에서
@Table(uniqueConstraints = ~~)
사용
-
-
@Enumerated: 이넘 타입 매핑
ORDINAL: enum을 숫자로 0, 1, 2로 저장
STRING: enum의 이름을 String으로 저장
-
@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가 한 번 더 빌드를 하고 성공하면 쉘 스크립트에 따른 배포를 진행한다.
-
프로젝트 등록
-
jenkins 설치
- enter an item name 입력하고 Freestyle project 클릭하고 시작
- github url 따와서 소스 코드 관리 repository url에 연결
- username에 내 이름이랑 password 입력
- add 하면 credentials에 입력한거 뜨고 branches to build에 브랜치 연결
- 빌드 유발 GitHub hook trigger for GITScm polling 선택
- build execute shell에
./gradlew clean print
설정 - 빌드 후 조치에 이메일 추가 가능
- 이후 github repository랑 Jenkins hook url 연결 -> webhooks에 payload에 http://jenkins url(local이면 안됨, ngrok 사용, Content type: application/json)/github-webhook/, just the push event 클릭,
- build now 클릭하면 빌드 수행
- console output에서 빌드 출력문 확인 가능
[Spring] RestDocs with RestAssured
- 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'
}
}
- src/docs/asciidoc/index.adoc
= ZZIMKKONG Application API Document
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3
:sectlinks:
include::member.adoc[]
- Test 코드 상단에 어노테이션 추가
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@AutoConfigureRestDocs
- Test 코드 setUp에 메서드 추가
@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
RequestSpecification spec = new RequestSpecBuilder()
.addFilter(documentationConfiguration(restDocumentation))
.build();
DocumentUtils.setRequestSpecification(spec);
}
- Test 코드에 메서드 추가
RestAssured
.given(DocumentUtils.getRequestSpecification()).log().all()
.accept("application/json")
.filter(document("member", DocumentUtils.getRequestPreprocessor(), DocumentUtils.getResponsePreprocessor()))
...
-
build.gradle에 asciidoctor 재생
build/generated-snippets/기능명/메서드명 에http-request.adoc
,http-response.adoc
기타 등등 추가되었는지 확인 -
src/docs/asciidoc/member.adoc 수정
== Member(멤버)
=== 멤버 회원가입
==== Request
include::{snippets}/member/http-request.adoc[]
==== Response
include::{snippets}/member/http-response.adoc[]
-
Test 모두 통과하면 ./gradlew build bootJar 적용
-
src/main/resources/static/docs에 index.html에 자신의 api가 추가되었는지 확인
-
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);