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

11 minute read

Sprint 3 (8/3 ~ 8/13)

[JPA] 양방향 매핑

기존 단방향 매핑을 적용하면 @ManyToOneReservation -> Space 매핑한 경우에는 reservation에서 space만 조회할 수 있다.

하지만 우리는 space에서 reservations도 조회하고 싶다. 왜? findById로 space 얻어오고, space를 바탕으로 reservationRepository로부터 reservations 가져오는게 싫다! 한번만 db에 요청 보내고 처리하고 싶다.

  • 단방향 매핑
    @ManyToOne
    private Team team;
  • 양방향 매핑 mappedBy: 객체와 테이블 간의 연관관계를 맺는 방식의 차이로 인해 등장 객체는 단방향으로 연관관계를 맺는다. 객체를 양방향으로 참조하려면 단방향 연관관계를 서로 갖고 있는 것이다. 테이블은 양방향으로 연관관계를 맺는다. 테이블은 외래키로 양방향 참조가 가능하다.

연관관계의 주인

둘 중 하나, 주인만 외래키를 관리할 수 있다. 주인이 아닌 쪽은 읽기만 가능하다. 주인이 아니면 mappedBy 속성으로 주인을 지정한다. 외래 키가 있는 곳을 주인으로 정하자.

    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();

연관관계의 주인에 값을 넣지 않고(Member.setTeam을 하지 않고) 주인이 아닌 객체에만 값을 넣으면(team에만 member를 add하면) 안된다. 이를 방지하기 위해 양쪽 다 값을 입력하도록 구현해야 한다. 단방향 매핑으로도 연관관계 매핑은 되었지만 반대 방향으로 조회 기능을 추가한 것(JPQL에서 역방향 탐색할 일이 많음)이므로 양방향은 필요할 때 추가해도 된다.

[JPA] 지연로딩, 즉시로딩

하지만 space만의 기능을 사용하고 reservation은 필요없는 경우가 있다. 100개의 reservation을 쓸데 없이 들고다니는 것은 비효율적이다. 이 때 지연로딩을 사용할 수 있다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "space_id", foreignKey = @ForeignKey(name = "fk_reservation_space"), nullable = false)
private Space space;

위와 같이 설정함으로써 지연로딩을 사용할 수 있다. 기본설정은 EAGER인 즉시로딩이다.

지연로딩이란 reservations에 프록시 객체를 넣어두었다가 호출되는 순간에 쿼리문을 날려 데려오는 것이다.

하지만 지연로딩을 이용해 이 때 reservations을 조회하면 이미 space 조회는 마쳐서 join 문 사용이 안되고 그 때마다 자신의 space를 조회하는 where 절을 가진 쿼리가 날라간다. 같은 space인데도 10개의 reservation이 있다면 그 쿼리문이 10번 날라가야 한다. (N+1)

[JPA] n+1 문제

메서드마다 @Transactional을 붙여줌으로써 하나의 영속성 컨텍스트를 생성한다. 보통 트랜잭션은 서비스 레이어까지 관리되므로 컨트롤러, 뷰 레이어에서 엔티티는 준영속 계층이 된다. 준영속 상태에서는 지연 로딩과 변경 감지가 이뤄지지 않는다. 이 때, 글로벌 페치 전략 수정, JPQL 페치 조인, 강제 초기화하거나, OSIV를 사용해 항상 영속 상태로 만들어 해결할 수 있다.

  • 즉시로딩과 n+1

    JPQL로 실행하면 JPA는 그냥 sql를 생성해서 실행한다.

    하지만 즉시로딩이 걸려있어 연관 되어 있는 sql을 그대로 또 쓸데없이 수행해야한다.

    실행한 sql 결과만큼 추가로 sql 실행한다.

  • 지연로딩과 n+1

    지연로딩을 사용하면 db에서 회원만 조회될 수 있다.

    하지만 모든 회원에 대한 주문을 조회하고자 할 때 n+1 문제가 발생한다.

    주로 지연로딩을 사용하도록 하자. OneToMany, ManyToMany의 기본은 지연로딩이다.

  • 페치조인을 사용한 n+1 해결

    @Query("select s from Space s join fetch s.reservations")
    List<Space> findAll();
    

    위와 같이 적용할 수 있다. 이는 repository의 메서드 상단에 사용할 수도 있고, entityManager를 사용하는 service 메서드 내부에 적용될 수도 있다.

    fetch join을 하게 되면 해당 쿼리문을 한번만 날리게 되어 효율적이다. 여기서는 Inner Join으로 sql 문이 날라간다.

  • 하이버네이트의 @BatchSize 사용

    연관된 엔티티 조회 시 지정한 size만큼만 조회한다. @BatchSize(size = 5)

    페이징 사용 시 유용하게 사용할 수 있다.

[JPA] Cascade 옵션

  • PERSIST

    엔티티 영속화할 때 해당 엔티티도 함께 영속화, 해당 엔티티도 같은 옵션 있어야 함

  • MERGE

    트랜잭션 종료 후 detach됐을 때 연관 엔티티 추가하거나 변경 후에 부모가 merge() 하면 변경사항 적용, 이미 영속화된 엔티티에 추가 수정 시 사용

  • REMOVE

    삭제 시 같이 삭제

  • DETACH

    Detach 시 같이 detach

  • ALL

    모든 옵션 적용, 일반적 orphanRemoval = true 있어야 같이 삭제

참조 무결성

데이터베이스 상의 참조가 모두 유효하다. 이는 데이터베이스에서 하나의 속성이 다른 테이블의 속성을 참조할 때 참조한 그 속성이 다른 테이블에 모두 존재한다는 것을 의미한다.

추가적으로 orphanremoval = true 설정을 하면 자식 객체가 함께 삭제된다는 점에서 cascade.remove나 마찬가지가 아닌가 생각했다.

하지만 orphanremoval = true 의 설정은 부모와의 관계가 바뀌거나, 끊어지는 경우에도 삭제해주는 것 같다. 기본 cascade 설정만으로는 참조가 변경되거나 삭제되어도 삭제되지 않는데 orphanremoval = true 설정이 되어 있으면 삭제되도록 하는 것 같다. 예를 들어 cascadeType.Remove 가 설정되어 있으면 관계가 끊어졌을 때 참조를 변경시켜 무결성 오류를 안나게 해줄 뿐 데이터는 남겨두게 되는 것이다.

또한 cascade 설정은 기존에 부모 객체 내부의 인스턴스로 포함된 자녀객체의 orm 동작에 대한 설정이다.

The difference between the two settings is in the response to disconnecting a relationship. For example, such as when setting the address field to null or to another Address object.

  • If orphanRemoval=true is specified the disconnected Address instance is automatically removed. This is useful for cleaning up dependent objects (e.g. Address) that should not exist without a reference from an owner object (e.g. Employee).
  • If only cascade=CascadeType.REMOVE is specified no automatic action is taken since disconnecting a relationship is not a remove operation.

참고

[JPA] EntityManager 사용하기

직접 트랜잭션 설정이 아니라 영속성 컨텍스트를 다루고 싶다면? 아래와 같은 어노테이션 사용해 주입

@PersistenceUnit
EntityManagerFactory emf;

@PersistenceContext
EnitityManger em;

사용할 수 있는 메서드는 아래와 같다.

em.find();    // 엔티티 조회
em.persist(); // 엔티티 저장
em.remove();  // 엔티티 삭제
em.flush();   // 영속성 컨텍스트 내용을 데이터베이스에 반영
em.detach();  // 엔티티를 준영속 상태로 전환
em.merge();   // 준영속 상태의 엔티티를 영속상태로 변경
em.clear();   // 영속성 컨텍스트 초기화
em.close();   // 영속성 컨텍스트 종료

보통 flush는 직접 호출, 트랜잭션 커밋, JPQL 쿼리 실행 시 자동 호출된다.

[JPA] JPQL

select * from space s left outer join (select * from reservation res where r.startTime >='2021-08-07' and res.end_date >= '2021-08-07') as r on r.space_id = s.id

위와 같은 쿼리를 전송하고 싶어서 객체지향쿼리는 jpql을 사용하게 되었다. 결과적으로은 native 쿼리에서 join 절 내에 서브쿼리를 넣는 방식을 찾지 못해 실패했다.

객체지향쿼리 방법: JPQL, JPA Criteria, QueryDSL, native SQL, JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.

  • 엔티티와 속성은 대소문자 구분

    • jpql 키워드는 구분 안함

    • 엔티티 이름 사용, 테이블 이름 아님

    • 별칭 필수

    • query.getResultList(): 결과 하나 이상, 리스트 반한

    • query.getSingleResult(): 결과 정확히 하나, 단일 객체 반환

    • 파라미터 바인딩

    • JPQL 프로젝션

      조회할 대상을 지정하는 것

      select m from Member m select m.team from Member m select username, age from Member m (단순 값) select new jpabook.jpql.UserDto(m.username, m.age) from Member m (단순 값을 DTO로 바로 조회)

  • 조인 내부 조인: select m from Member m join m.team t 외부 조인: select m from Member m left join m.team t 세타 조인: select count(m) from Member m, Team t where m.username = t.name 페치 조인: 엔티티 그래프 한번에 조회해서 지연 로딩 발생하지 않음 select m from Member m join fetch m.team
  • Named 쿼리 - 어노테이션 미리 쿼리 이름과 쿼리를 작성해둔다. 이러면 컴파일 시점에 다 파싱해서 어플리케이션 로딩 시점에 문제 있는걸 발견할 수 있다. spring @Query도 namedQuery로 사용 가능
@Query("select s from Space s join fetch s.reservations")
List<Space> findAll();

@Query(value = "select distinct s from Space s join fetch s.reservations r where s.id = :spaceId and r.startTime >=:now and r.endTime <= :endTime", nativeQuery = false)
Optional<Space> findByIdWithAfterTodayReservations(@Param("spaceId") final Long spaceId, @Param("now") final LocalDateTime now, @Param("endTime") final LocalDateTime endTime);

[JPA] QueryDsl

SQL, JPQL을 코드로 작성할 수 있도록 도와주는 빌더 API로 @Query 만으로는 조회 기능에 한계가 있어 정적 타입을 지원하는 조회 프레임워크이다.

sql, jpql은 문자라 실행되기 전까지 오류가 있는지 확인해볼 수 없는데 이걸 도와주어 컴파일 시점에 문법 오류를 발견하기 쉽다.

조인, 쿼리, 동적 쿼리 다 만들어낼 수 있다.

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

JPaQueryFactory, QMember 를 사용한다.

기본 설정

  1. gradle 설정

  2. configuration 추가

구현 방법

  1. QuerydslRepositorySupport 상속을 통한 구현

  2. Custom Repository를 이용한 구현
  3. 상속, 구현 없이 구현

참고

[Infra] Jacoco

jacoco는 java의 코드 커버리지를 체크하고 결과를 html로 만들어주는 라이브러리

설정한 minimum, maximum 등이 통과하지 않으면 오류가 발생

  1. jacoco 플러그인 추가

  2. gradle task 설정

    • jacocoTestReport

      바이너리 커버리지 결과를 사람이 읽기 좋은 형태의 리포트로 저장

      html 파일로 생성해 사람이 쉽게 눈으로 확인할 수도 있고, SonarQube 등으로 연동하기 위해 xml, csv 같은 형태로도 리포트 생성 가능

    • jacocoTestCoverageVerification

      내가 원하는 커버리지 기준을 만족하는지 확인해 주는 task

  3. Element 설정
    • BUNDLE (default): 패키지 번들
    • PACKAGE: 패키지
    • CLASS: 클래스
    • SOURCEFILE: 소스파일
    • METHOD: 메소드
  4. counter 설정
    • LINE: 빈 줄을 제외한 실제 코드의 라인 수
    • BRANCH: 조건문 등의 분기 수
    • CLASS: 클래스 수
    • METHOD: 메소드 수
    • INSTRUCTION (default): Java 바이트코드 명령 수
    • COMPLEXITY: 복잡도
  5. value 설정
    • TOTALCOUNT: 전체 개수
    • MISSEDCOUNT: 커버되지 않은 개수
    • COVEREDCOUNT: 커버된 개수
    • MISSEDRATIO: 커버되지 않은 비율. 0부터 1 사이의 숫자로, 1이 100%입니다.
    • COVEREDRATIO (default): 커버된 비율. 0부터 1 사이의 숫자로, 1이 100%입니다.
  6. 제외 클래스 지정
excludes = []
  1. 테스트 리포트 확인

[Spring] jackson 라이브러리

spring의 json message converter는 jackson 라이브러리를 사용한다.

ObjectMapper API(MappingJacksonHttpMessageConverter)를 이용해 java의 객체를 json 구조로 매핑해준다.

getter, setter의 명명 규칙으로 정해진다. getter없이 멤버변수로 데이터 매핑하고 싶다면 @JsonProperty("name")을 이용할 수 있다.

JsonAutoDetect

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)는 멤버변수 뿐 아니라 getter, setter의 데이터 매핑 정책도 정할 수 있다.

JsonInclude

@JsonInclude(Include.NON_NULL) 어노테이션으로 줄 수 있고, this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_ABSENT);로도 줄 수 있다.

값이 null일 때 데이터를 출력하지 않도록 하는 설정이다.

  • ALWAYS 기본값으로 모든 값을 출력한다.

  • NON_NULL null 제외

  • NON_ABSENT null 제외하고 Optional 값도 제외

  • NON_EMPTY null 제외, absent 제외, Collection이 empty면 제외, 배열이나 string 길이가 0이면 제외

  • NON_DEFAULT null 제외, absent 제외, Collection이 empty면 제외, 배열이나 string 길이가 0이면 제외, primitive default면 제외, timestamp 0이면 제외

해당 필드를 출력하고 싶지 않으면 @JsonIgnore로 필드에 어노테이션 부여할 수 있다.

@JsonIgnoreProperties({"id"})은 속성이나 속성 목록 무시

@JsonIgnoreType은 주석이 달린 형식의 모든 속성을 무시

  • 연달아 대문자가 오는 경우

jackson은 getter를 기반으로 json key를 만든다.

java beans에서는 앞에 두개가 대문자라면 그대로 리턴하고 아니라면 하나만 소문자로 바꿔서 리턴하는 규약이 있다.

lombok에서도 필드명 맨 앞을 항상 대문자 그대로 리턴한다.

하지만 jackson에서는 맨 앞 두 글자가 대문자인 경우 이어진 대문자를 모두 소문자로 변경한다.

따라서 앞에 두 글자에 연달아 대문자가 오는 경우 dto가 null로 들어가는 경우가 있으므로 주의해서 사용해야 한다.

꼭 있어야겠다면 롬복을 사용하지 않고 직접 getter를 만들거나, @jsonProperty를 사용해야 한다.

[Infra] SonarQube

정적 프로그램 분석이란? 실제 실행 없이 프로그램을 분석하는 것으로 소스 코드나 컴파일된 코드를 이용해 프로그램을 분석한다. 소스코드는 확인할 수 있지만 실행 환경에서의 상태는 정확히 알 수 없다.

정적 코드 분석 도구에는 pmd, findBugs, checkstyle, sonarqube 등이 있다.

정적 프로그램 분석을 통해 코드 점검, 품질 중앙화, devops 통합, 품질 요구사항 설정 등의 이점을 얻을 수 있다.

docker에서 설정할 수 있고, jenkins에서도 설정하여 빌드 시 돌아가도록 할 수 있다.

분석에 대해서

  • Code Smell : 심각한 이슈는 아니지만 베스트 프렉티스에서 사소한 이슈들로 모듈성(modularity), 이해 가능성(understandability), 변경 가능성(changeability), 테스트 용의성(testability), 재사용성(reusability) 등이 포함
  • Bugs : 일반적으로 잠재적인 버그 혹은 실행시간에 예상되는 동작을 하지 않는 코드
  • Vulnerabilities : 해커들에게 잠재적인 약점이 될 수 있는 보안상의 이슈로 SQL 인젝션, 크로스 사이트 스크립팅과 같은 보안 취약성 발견
  • Duplications : 코드 중복
  • Unit Test : 단위테스트 커버리지를 통해 단위 테스트의 수행 정도와 수행한 테스트의 성공/실패 정보
  • Complexity : 코드의 순환 복잡도, 인지 복잡도
  • Size : 소스코드 사이즈

참고

[DB] 더미데이터 넣기

프로시져를 이용해 db에 더미데이터를 넣고 테스트할 수 있다.

DELIMITER $$
DROP procedure IF EXISTS loopInsert$$

create procedure loopInsert()
begin
	declare i int default 1;
    while i <= 10000 do
		INSERT INTO `Database`.`table` (`column`) VALUES ('asdf');
		set i = i + 1;
	end while;
end$$
DELIMITER $$
 
call loopInsert;
  • Delimiter : 구문자로 세미콜론이랑 같다. 문법의 시작과 끝 알려줌
  • Procedure 이름이 loopInsert가 아니면 오류가 난다.

이외 더미데이터 만들어주는 사이트

[Spring] AOP를 이용한 커스텀 어노테이션

Proxy Pattern

디자인 패턴 중 하나로 실제 객체에 대한 interface 역할을 수행하는 class의 객체

spring aop에서는 spring bean이 만들어질 때 spring aop가 proxy를 자동으로 만들고 기존 클래스 대신 proxy를 bean으로 등록하고 기존 클래스가 사용될 때 proxy를 사용한다.

예를 들어 MemberService bean에 @Transactional 이 붙어있다면 MemberService 타입의 proxy가 만들어지고, 실행 시점에 @Transactionl 어노테이션이 지시하는 코드가 들어간다.

proxy는 직접 책임을 수행하지 않고 실제 객체를 감싸서 client와의 중계역할을 하는 wrapper로 사용된다. 실제 객체가 대신 처리하도록 위임한다.

AOP

관점 지향 프로그래밍으로 핵심 기능과 공통 기능을 나누고 핵심 기능에서 공통 기능을 불러와서 사용

  • 생각하게 된 이유?

성능 측정을 하는데 코드에 들어가야하는 것이 불편하다!

중복된 코드가 늘어나고 빼먹기 쉽상이다!

AOP 용어

  • advice: target 클래스의 jointpoint에 들어가 동작할 코드

  • Jointpoint: advice를 적용할 시점(메서드 호출, exception 발생 등)

  • pointcut: jointpoint의 집합으로 advice가 적용되는 jointpoint

  • weaving: advice를 핵심 로직에 적용하는 것

  • aspect: 여러 객체에 공통으로 적용되는 관심 사항(aop에 좋은 것)

  • Target: 핵심 로직 구현 클래스, advice 받는 대상

  • advisor: advice + point cut

  • Proxy: advice가 적용된 후 생성된 객체

구현 방법

  1. 메인 메서드에 어노테이션 추가

  2. 설정할 클래스 만들기

  3. 커스텀 어노테이션 만들기

[Infra] docker image 만들기

docker image

이미지를 Pull 받으면 여러 개로 나눠져서 내려받는데 이렇게 하나씩 나뉘는 것을 레이어라고 한다.

레이어는 도커 이미지가 빌드될 때 Dockerfile에 정의된 명령문을 순서대로 실행하면서 만들어진다.

이 레이어는 독립적으로 저장되며 읽기 전용이기 때문에 임의로 수정할 수 없다.

도커 컨테이너가 실행되면 읽기 전용을 순서대로 쌓고 마지막에 쓰기 가능한 레이어를 추가한다. 이후 컨테이너에서 만들어지는 결과물들이 쓰기 가능 레이어에 기록된다.

레이어별로 이미 만들어진 레이어는 캐시되어 재사용되므로 빌드 시간이 단축된다.

프로젝트를 도커 이미지로 만들어두면 어느 상황에서든 손쉽게 프로젝트를 실행할 수 있다.

1. 현재 돌아가고 있는 컨테이너를 image화 하기

$ docker commit -a "만들이미지이름" 컨테이너id 현재이미지이름

2. 우리의 프로젝트 기본 세팅을 image화 하기

Dockerfile 명령어

  • WORKDIR: mkdir + cd
  • ENTRYPOINT java -jar ~~: shell
  • ENTRYPOINT [“java”, “-jar ~~”]: exec
  • RUN: 캐시할 수 있는 단위
  • ADD, COPY: 로컬 파일을 컨테이너로 복사

image화 방법

  1. Dockerfile 생성

  2. Dockerfile build
  3. 만들어진 image run
  4. 만들어진 image hub에 올리기]

참고

[Infra] 무중단 배포

  • Rolling Deployment 두 대 이상의 서버를 돌리면서 사용자에게 중단이 없게 하는 것

    1. 서버 1을 로드 밸런서에서 뺀다.  
    2. 서버 1에 배포  
    3. 서버 1을 다시 로드 밸런서에 넣는다.  
    4. 서버 2 빼고, 배포하고, 다시 넣고
    

    전체적인 속도가 느리다.

  • Blue/Green Deployment 실제 서비스 중인 환경(Blue), 새롭게 배포할 환경(Green)을 세트로 준비해서 배포하는 것 서버를 그대로 본떠 하나의 서버를 만들고 새로운 서버 전체를 업데이트한다. 기존 서버에 연결된 연결을 새로운 서버의 연결로 변경한다.

    • nginx의 구성 엔진엑스 80(http), 443(https) 할당 spring boot1: 8081, spring boot2:8082
  • Canary Deployment

    위험을 빠르게 감지하도록 하는 배포 전략으로 특정 user에게만 배포했다가 위험이 없으면 전체에 배포하고 오류가 있으면 수정을 거진다.

    성능 모니터링에 유용하다.

참고

[Infra] Cloudwatch logs

실시간으로 실행 중인 애플리케이션을 모니터링하는 것으로 리소스 및 애플리케이션에 대해 측정할 수 있는 변수인 지표를 수집하고 추적할 수 있다.

지표를 감시하다가 임계값을 위반하면 경고를 보낼 수도 있다.

원하는 정보를 직접 대시보드에 등록해 모니터링할 수 있다.

[DB] flyway

오픈소스 마이그레이션 툴로 소스코드를 형상관리하는 Git과 같이 Flyway는 버전 관리 목적인 SCHEMA_VERSION 테이블을 통해 SQL 스크립트의 변화를 추적하면서 자동적으로 DB를 관리한다.

일일히 db를 바꾸지 않아도 sql문만 작성하면 flyway가 알아서 db를 바꿔준다.

준비

  1. 의존성 추가

    dependencies {
        implementation 'org.flywaydb:flyway-core:6.4.2'
    }
    
  2. Properties 추가

    # flyway
    spring.flyway.baseline-on-migrate=true
    spring.flyway.baseline-version=0
    spring.flyway.useMysqlMetadata=true
    

sql문 작성법

  1. Sql 파일 생성: V${버전 숫자}__원하는이름.sql
  2. 파일에 db에 변경하고자 하는 내용의 sql문 작성
  3. 빌드 시 파일 내용 변경사항 반영

[Spring] Lombok

어노테이션을 기반으로 코드를 자동완성해주는 라이브러리

게터, 빌더를 제거할 수 있다.

적용

  1. 의존성 추가

    // lombok
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
    
  2. 적용

    클래스 상단에 필요한 어노테이션 추가해 적용