기록은 기억의 연장선

더 많은 것을 기억하기 위해 기록합니다


  • Home

  • Tags

  • Categories

  • Archives

  • Search

elasticsearch

Posted on 2019-01-25 | Edited on 2020-11-02 | In etc | Comments:

참고자료
https://sanghaklee.gitbooks.io/elk/content/

elk docker로 띄우기
https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html

ELk를 마스터하면 어떤 빅데이터를 만나든 쉽게 다룰 수 있다?
로그 스테이시가 어떤 데이터든(csv든 상관없이) 수집해서 엘라스틱 서치에 수집해주고,
키바나는 데이터 비주얼라이션 툴로써 엘라스틱 서치의 데이터를 화면에 보기좋게 보여줌

엘리스틱 서치 : 키워드가 어떤 document에 있다고 저장하는 식이다
인덱스(가장 큰 개념)안에 타입, 타입안에 도큐먼트

REST API로 데이터 CRUD를 수행한다

인덱스 생성은 PUT으로 하고, document 생성은 POST로 한다
json 파일을 직접 넣을 수 있다

값 업데이트 : _update
_update시에 script에 객체탐색형식으로 접근해서 값을 수정할 수 있다

bulk insert : _bulk
bulk는 2개의 라인으로 구성되어있다
1번 라인은 metadata, 2번 라인은 데이터
데이터 라인에서 기본적인 데이터 타입은 자동으로 잡아서 mappings로 등록해주는 듯 하다

{ “index” : { “_index” : “basketball”, “_type” : “record”, “_id” : “1” } }
{“team” : “Chicago Bulls”,“name” : “Michael Jordan”, “points” : 30,“rebounds” : 3,“assists” : 4, “submit_date” : “1996-10-11”}
{ “index” : { “_index” : “basketball”, “_type” : “record”, “_id” : “2” } }
{“team” : “Chicago Bulls”,“name” : “Michael Jordan”,“points” : 20,“rebounds” : 5,“assists” : 8, “submit_date” : “1996-10-11”}

이런 데이터를 등록하면 자동으로 mappings에.
“mappings” : {
“record” : {
“properties” : {
“assists” : {
“type” : “long”
},
“name” : {
“type” : “text”,
“fields” : {
“keyword” : {
“type” : “keyword”,
“ignore_above” : 256
}
}
},
“points” : {
“type” : “long”
},
“rebounds” : {
“type” : “long”
},
“submit_date” : {
“type” : “date”
},
“team” : {
“type” : “text”,
“fields” : {
“keyword” : {
“type” : “keyword”,
“ignore_above” : 256
}
}
}
}
}
식으로 등록이 되어있었다.

엘라스틱서치에 데이터매핑을 안할수 있긴 하지만 위험하다. (키바나 시각화 등)
curl -XGET http://localhost:9200/classes?pretty 하면 mappings가 나오는데, 각 필드별 타입등을 지정해놓은 구간이다
classes/class/_mapping -d @json파일 형태로 지정 가능
이후 bulk insert하면 잘 들어간것을 확인할 수 있다는데, mapping이 있고 없고 차이가 뭔지?

curl -XGET http://localhost:9200/{_index}/{_type}/_search?q=points:30
requestBody로도 검색 가능

Read more »

[jpa] entity callback method

Posted on 2019-01-25 | Edited on 2020-11-02 | In jpa | Comments:

http://www.thejavageek.com/2014/05/23/jpa-lifecycle-callback-methods/
lifecycle call method

http://www.thejavageek.com/2014/05/24/jpa-entitylisteners/
클래스에 적용하는 법

class 단위 범용적인 life cycle을 적용하고 싶다면
EntityListener class 사용

  • entity에 접근할 수 있어야 하므로 callback method에서 단일파라미터로 엔티티를 받을 수 있다
  • 엔티티리스너 클래스는 public no args 생성자가 있어야 한다

엔티티리스너를 엔티티에 붙이려면 아래와 같이 해야함

  • @EntityListeners 어노테이션을 엔티티에 선언해줘야 함. 여러개 붙일 수 있음
  • 라이프사이클 이벤트가 발생하면 @EntityListeners에 선언된 애들 순서대로 생성되고 실행된다
  • 이벤트리스너 내의 callback 메서드를 실행하면서 entity를 리스너에 전달한다(콜백 메서드가 있다면)
Read more »

[jpa] Spring Data JPA

Posted on 2019-01-24 | Edited on 2020-11-02 | In jpa | Comments:

Spring Data JPA는 스프링에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.
데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결할 수 있게 해준다.

  • CRUD 처리를 위한 공통 인터페이스 제공
  • 인터페이스만 작성하면 동적으로 구현체를 생성해서 주입해줌
  • 따라서 인터페이스만 작성해도 개발을 완료할 수 있음

설정

maven repository에서 Spring Data Jpa 검색해서 gradle에 추가하면 된다.

1
compile group: 'org.springframework.data', name: 'spring-data-jpa', version: '2.1.2.RELEASE'

spring boot를 사용할 경우 Spring Boot Data JPA Starter를 사용해주는 것이 좋다.

1
compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.1.1.RELEASE'

이후 spring config에 아래와 같이 작성한다.

  • xml config

    1
    <jpa:repositories base-package="com.joon.repository">
  • java config

    1
    2
    3
    @Configuration
    @EnableJpaRepositories(basePackages = "com.joont.repository")
    public class AppConfig(){}

이렇게 설정해두면 Spring Data Jpa는 base package에 있는 레파지토리 인터페이스들을 찾아서 해당 인터페이스를 구현한 클래스들을 동적으로 생성한 다음, 빈으로 등록한다.

JpaRepository(공통 인터페이스)

JpaRepository는 앞서 언급했던 CRUD 처리를 위한 공통 인터페이스이다.
이 인터페이스를 상속받은 인터페이스만 생성하면 해당 엔티티에 대한 CRUD를 공짜로 사용할 수 있게된다.

1
2
3
public interface MemberRepository extends JpaRepository<Member, Long>{

}

제네릭에는 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입을 넣어주면 된다.

JpaRepository의 계층구조는 아래와 같다.
JpaRepository 계층구조

보다시피 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스를 사용하고
JpaRepository에서 JPA에 특화된 기능을 추가로 제공한다.

실제 구현체

공통 인터페이스인 JpaRespository는 org.springframework.data.jpa.repository.support.SimpleJpaRepository 클래스가 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Repository // 1
@Transactional(readOnly = true) // 2
public class SimpleJpaRepository<T, ID> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
@Transactional // 3
public <S extends T> S save(S entity) { // 4
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}

// ....
}
  1. @Repository 적용

    JPA 예외를 스프링이 추상화 한 예외로 변환한다.

  2. Transactional(readOnly = true) 적용
    • 전체적으로 트랜잭션이 적용되어 있다. 이로인해 서비스에서 트랜잭션을 적용하지 않으면 레파지토리에서 트랜잭션을 시작하게 된다.
    • 공통 인터페이스에는 조회하는 메서드가 많으므로 전체적으로 readOnly=true를 적용해서 약간의 성능향상을 얻는다.
  3. Transactional
    • 조회 메서드가 아니라서 readOnly=true가 빠지게 한다.
  4. save
    • 보다시피 저장할 엔티티가 새로운 엔티티면 저장하고 이미 있는 엔티티면 병합한다.
    • 여기서 사용되는 entityInformation.isNew는 식별자가 객체일때는 null, primitive 타입일때는 0이면 새로운 객체라고 판단한다.
    • Persistable 인터페이스를 구현한 객체를 빈으로 등록하면 위의 조건을 직접 정의할 수 있다.

사용

메서드 이름으로 쿼리 생성

인터페이스에 선언한 메서드의 이름으로 적절한 JPQL 쿼리를 생성해주는 마법같은(ㅋㅋ) 기능이다.

1
2
3
public interface MemberRepository extends JpaRepository<Member, Long>{
List<Member> findByEmailAndName(String email, String name);
}

이렇게만 작성하면 Spring Data JPA가 메서드 이름을 분석해서 JPQL을 생성한다.
위의 메서드를 통해 생성되는 JPQL은 아래와 같다.

1
SELECT m FROM Member m WHERE m.email = ?1 AND m.name = ?2

물론 정해진 규칙은 있다.
현재 보니까 생각보다 굉장히 많은 기능을 지원한다.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.details

  • findByNameAndEmail 의 형태로 작성. By 뒤부터 파싱 (조건은 여기에)
  • findDistinctBy 로 distinct 가능
  • 엔티티 탐색 가능. camel case로 해도 되지만 애매하니 findByAddress_ZipCode 처럼 _로 이어주는게 좋을 듯
  • findFirst3By, findTop10By, findFirstBy(1건) 로 limit 기능을 사용할 수 있다. findLast3By 같은건 없음
  • findByAgeOrderByNameDesc 처럼 order by 가능
  • findBy 말고 countBy, deleteBy도 있음

반환 타입

Spring Data JPA는 유연한 반환 타입을 지원한다.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repository-query-return-types
기본적으로 결과가 한건 이상이면 컬렉션 인터페이스를 사용하고, 단건이면 반환 타입을 지정한다.

1
2
List<Member> findByMember(String name); // 컬렉션  
Member findByEmail(String email); // 단건

단건의 경우 T 형태와 Optional<T> 형태 2개로 받을 수 있다.
결과가 2건이상 나오면 javax.persistence.NonUniqueResultException 예외가 발생하고,
결과가 0건일 경우 T는 null, Optional<T>는 Optional.empty() 를 리턴한다.

참고로 단건의 경우 내부적으로 query.getSingleResult()를 사용해서 결과가 0건일 경우 javax.persistence.NoResultException이 발생해야하지만, 이는 다루기가 까다로우므로 exception을 발생시키지 않는 방향으로 기능을 제공한다.

Named Query

엔티티나 xml에 작성한 Named Query도 찾아갈 수 있다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
@NamedQuery(
name = "Member.findByName",
query = "select m from Member m where m.name = :name"
)
public class Member{
}

public interface MemberRepository extends JpaRepository<Member, Long>{
List<Member> findByName(@Param("name") String name);
}

Spring Data JPA는 메서드 이름으로 쿼리를 생성하기 전에 해당 이름의 Named Query를 먼저 찾는다(전략 변경 가능)
도메인 클래스 + .(점) + 메서드 이름으로 먼저 찾고, 없으면 JPQL을 생성한다.
위의 상황에서는 Member.findByName Named Query를 찾게 된다.

JPQL 직접 정의

org.springframework.data.jpa.repository.Query 어노테이션을 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
// 위치기반 파라미터
public interface MemberRepository extends JpaRepository<Member, Long>{
@Query("SELECT m FROM Member m WHERE m.name = ?1")
Member findByName(String name);
}

// 이름기반 파라미터
public interface MemberRepository extends JpaRepository<Member, Long>{
@Query("SELECT m FROM Member m WHERE m.name = :name")
Member findByName(@Param("name") String name);
}

아무리봐도 위치기반 파라미터는 개극혐이다.
내 주변사람에는 위치기반 파라미터를 쓰는 사람이 없으면 좋겠다.

네티티브 쿼리도 사용할 수 있다. nativeQuery = true 옵션만 주면 된다.

1
2
3
4
public interface MemberRepository extends JpaRepository<Member, Long>{
@Query(value = "SELECT * FROM Member WHERE name = ?0", nativeQuery = true)
Member findByName(String name);
}

네이티브 쿼리는 위치기반 파라미터가 0부터 시작한다.

벌크 연산

1
2
3
4
5
public interface MemberRepository extends JpaRepository<Member, Long>{
@Modifying
@Query("UPDATE Product p SET p.price = p.price * 1.1 WHERE p.stockAmount < :stockAmount")
int updatePrice(@Param("stockAmount") String stockAmount);
}

@Modifying을 명시해줘야 한다.
기존 벌크 연산처럼 영향받은 엔티티의 개수를 반환한다.

알다시피 벌크 연산은 영속성 컨텍스트를 무시한다.
벌크 연산후에 영속성 컨텍스트를 초기화하고 싶으면 clearAutomatically 옵션을 true로 주면 된다. 기본값은 false이다.

1
2
3
@Modifying(clearAutomatically = true)
@Query("~~")
// ~~~

페이징과 정렬

아래의 두 파라미터를 사용하면 쿼리 메서드에 페이징과 정렬 기능을 추가할 수 있다.

  • org.springframework.data.domain.Sort : 정렬기능
  • org.springframework.data.domain.Pageable : 페이징기능(Sort 포함)

사용법은 간단하다. 위의 두 클래스를 파라미터에 사용하기만 하면 된다.

Pageable을 사용하면 반환타입으로 Page를 받을 수 있다.
해당 클래스를 사용하면 페이징과 관련된 다양항 정보들을 얻을 수 있다. 참고로 Page를 반환타입으로 받으면 전체 데이터 건수를 조회하는 count 쿼리가 추가로 날라간다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public interface Page<T> extends Slice<T> {
static <T> Page<T> empty() {
return empty(Pageable.unpaged());
}

static <T> Page<T> empty(Pageable pageable) {
return new PageImpl(Collections.emptyList(), pageable, 0L);
}

int getTotalPages();

long getTotalElements();

<U> Page<U> map(Function<? super T, ? extends U> var1);
}

public interface Slice<T> extends Streamable<T> {
int getNumber();

int getSize();

int getNumberOfElements();

List<T> getContent();

boolean hasContent();

Sort getSort();

boolean isFirst();

boolean isLast();

boolean hasNext();

boolean hasPrevious();

default Pageable getPageable() {
return PageRequest.of(this.getNumber(), this.getSize(), this.getSort());
}

Pageable nextPageable();

Pageable previousPageable();

<U> Slice<U> map(Function<? super T, ? extends U> var1);
}

아래는 간단한 사용 예제이다.

1
2
3
4
5
6
7
8
9
// Pageable은 interface 이므로 구현체인 PageRequest 를 사용해야 한다.  
// 페이지, limit수, Sort 객체를 주면 된다
PageRequest pageRequest = PageRequest.of(0, 10, new Sort(Direction.DESC, "name"));

Page<Member> result = memberRepository.findByNameStartingWith("김", pageRequest);

result.getContet(); // 조회된 데이터
result.getTotalPages(); // 전체 페이지 수
result.hasNextPage(); // 다음 페이지 존재 여부

컨트롤러에서 사용

  • Pageable 객체를 Controller parameter로 직접 받을수도 있다

    1
    2
    3
    4
    @GetMapping("/members")
    public String list(Pageable pageable){
    // ...
    }
    1
    /members?page=0&limit=10&sort=name,asc&sort=age,desc

    pageable 기본값은 page=0, size=20이다.
    정렬을 추가하고 싶으면 sort 파라미터를 계속 붙여주면 된다.

  • 페이징 정보가 둘 이상이면 접두사를 사용해서 구분할 수 있다

    1
    2
    3
    4
    5
    6
    @GetMapping("/members")
    public String list(
    @Qualifier("member") Pageable memberPageable,
    @Qualifier("order")Pageable orderPageable){
    // ...
    }
    1
    /members?member_page=0&order_page=1

사용자 정의 레파지토리 구현

Spring Data JPA로 개발하면 인터페이스만 정의하고 구현체는 만들지 않는데, 다양한 이유로 메서드를 직접 구현해야 할 때도 있다.
그렇다고 레파지토리 자체를 직접 구현하면 공통 인터페이스가 제공하는 모든 기능까지 다 구현해야 한다.
Spring Data JPA는 이런 문제를 우회해서 필요한 메서드만 구현할 수 있는 방법을 제공한다.

  1. 먼저 사용자가 직접 구현할 메서드를 위한 정의 인터페이스를 작성한다.

    1
    2
    3
    public interface MemberRepositoryCustom{
    public List<Member> search();
    }
  2. 위의 사용자정의 인터페이스를 구현한 클래스를 작성한다.
    이떄 클래스 이름은 레파지토리 인터페이스 이름 + Impl로 지어야한다.
    이렇게 해야 Spring Data JPA가 사용자 정의 구현 클래스로 인식한다.

    1
    2
    3
    4
    5
    6
    public class MemberRepositoryImpl implements MemberRepositoryCustom{
    @Override
    public List<Member> search(){
    // ....
    }
    }

    이 클래스 이름 규칙은 변경할 수 있다.
    config에 설정했던 spring data jpa 설정의 속성값인 repository-impl-postfix 값을 지정해주면 된다. 기본값은 Impl 이다.

    1
    @EnableJpaRepositories(basePackages = "com.joont.repository", repositoryImplementationPostfix = "impl")
  3. 마지막으로 레파지토리 인터페이스에서 사용자정의 인터페이스를 상속받으면 된다.

    1
    2
    3
    public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom{

    }

spring data jpa + QueryDSL

spring은 2가지 방법으로 QueryDSL을 지원하지만,
하나는 기능에 조금 한계가 있어서 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나 Spring Data JPA가 제공하는 QueryDslRepositorySupport를 사용하면 된다.

아래는 사용 예제이다.
코드를 직접 써야하므로 사용자 정의 레파지토리를 구현해야한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class OrderRepositoryImpl extends QueryDslRepositorySupport implements OrderRepositoryCustom {

public OrderRepositoryImpl() {
super(Order.class);
}

@Override
public List<Order> search(OrderSearch orderSearch, Pageable pa) {

QOrder order = QOrder.order;
QMember member = QMember.member;

JPQLQuery query = from(order);

if (StringUtils.hasText(orderSearch.getMemberName())) {
query.leftJoin(order.member, member)
.where(member.name.contains(orderSearch.getMemberName()));
}

if (orderSearch.getOrderStatus() != null) {
query.where(order.status.eq(orderSearch.getOrderStatus()));
}

return query.list(order);
}
}

QueryDslRepositorySupport 클래스를 상속받아서 사용하고 있다.
참고로 생성자에서 QueryDslRepositorySupport에 클래스 정보를 넘겨줘야 한다.

기존에 생성하기 나름 번거로웠던 JPAQuery, JPAUpdateClause, JPADeleteClause 등을 간단하게 생성할 수 있다.
참고로 반환은 전부 인터페이스 타입인 JPQLQuery, UpdateClause, DeleteClause 등을 반환한다.
이 외에도 EntityManager, QueryDSL헬퍼 등을 반환하는 메서드도 구현되어 있다.

QueryDSL에 Pageable 적용하기

QueryDslRepositorySupport를 사용하면 Spring Data JPA의 Pageable을 간단하게 적용할 수 있다.

1
2
3
4
5
6
7
8
9
QItem item = QItem.item;
JPQLQuery<Item> query = from(item);
// making condition
query.where(condition).distinct();

long totalCount = query.fetchCount();
List<Item> results = getQuerydsl().applyPagination(pageable, query).fetch();

return new PageImpl<>(results, pageable, totalCount);

Page를 리턴함으로써 완벽하게 Pageable을 사용할 수 있다.

Read more »

mac virtualbox 설치 실패

Posted on 2019-01-23 | Edited on 2020-11-02 | In etc | Comments:

https://hongku.tistory.com/64
환경설정 - 보안 및 개인정보 보호 - 일반 - 아래쪽 허용버튼 클릭 - Oracle America 허용

Read more »

[java] stream example

Posted on 2019-01-22 | Edited on 2020-11-02 | In java | Comments:

stream 도는 오브젝트에 추가적인 행위를 하기

1
2
3
4
list.stream()
.map(mapper::toEntity)
.peek(e -> e.setParent(parent))
.forEach(childRepository::save);

peek을 이용하여 중간중간 메서드들을 호출 가능하다.

Stream을 이용하여 노출순서 정렬하기

1
2
3
4
5
6
7
8
9
10
11
12
AtomicInteger i = new AtomicInteger(0);
SomeDTOList.stream()
.sorted(Comparator.comparingInt(SomeDTO::getDisplayOrder))
.peek(dto -> dto.setDisplayOrder(i.incrementAndGet()))
.peek(dto -> {
AtomicInteger j = new AtomicInteger(0);
dto.getChildren().stream()
.sorted(Comparator.comparingInt(SomeChildDTO::getDisplayOrder))
.forEach(c -> c.setDisplayOrder(j.incrementAndGet()));
})
.map(itemOptionMapper::toEntity)
.forEach(repository::save);

자식들의 속성들까지 들고와 하나의 리스트로 합치기

1
2
3
4
Stream.of(category)
.flatMap(c -> c.getChildCategories().stream())
.map(Category::getId)
.collect(Collectors.toList())
Read more »

[db] 락(lock)

Posted on 2019-01-18 | Edited on 2020-11-02 | In db | Comments:

MySQL에서 사용하는 잠금은 크게 MySQL 엔진 레벨과 스토리지 엔진 레벨으로 나눌 수 있다.
MySQL 엔진 레벨의 잠금은 모든 스토리제 엔진에 영향을 미치지만,
스트리지 엔진 레벨의 잠금은 스토리지 엔진 간 상호 영향을 미치지 않는다.

MySQL 엔진 잠금

글로벌 락

MySQL 서버에 존재하는 모든 테이블에 잠금을 걸게되며, MySQL에서 제공하는 락의 범위중에 가장 크다.

1
FLUSH TABLES WITH READ LOCK

명령으로 락을 획득할 수 있고(기존에 실행중인 락이 있으면 기다린다),
모든 테이블 모든 레코드에 변경이 불가능하게 된다.
서버의 미치는 영향이 크기 떄문에 웹 서비스용으로 사용되는 MySQL에서는 사용하지 않는것이 좋다.
mysqldump 같은것이 우리가 모르는 사이에 내부적으로 이 명령을 실행하고 백업할 때도 있다.

테이블 락

개별 테이블 단위로 잠금을 거는 방식이며, 명시적 또는 묵시적으로 특정 테이블의 락을 획득할 수 있다.

  • 묵시적 방법
    MyISAM이나 Memory DB에서 데이터를 변경하는 쿼리를 실행하면 자동으로 테이블 락이 획득된다.
    (쿼리가 실행되는 동안 자동으로 획득됬다가 쿼리가 완료되면 자동으로 해제된다.)
    MyISAM이나 Memory의 경우 이 묵시적 LOCK이 해당된다.
    InnoDB의 경우 레코드 기반 잠금을 사용하기 때문에 변경 쿼리를 실행해도 테이블 잠금이 발생하진 않지만, 스키마를 변경하는 DDL 쿼리를 수행할 경우 테이블 락을 묵시적으로 사용한다.

  • 명시적 방법
    InnoDB도 아래와 같이 명시적으로 선언하여 Table LOCK을 획득할 수 있다.

    1
    LOCK TABLES table_name [READ | WRITE]

    READ든 WRITE든 걸게 되면 다른 트랜잭션에서 해당 테이블에 변경 작업을 할 수 없게 된다.
    READ는 일관된 읽기를 위해 락을 거는 것이고, WRITE는 데이터 변경을 위해 락을 거는 것이다.

    • Table READ 락의 경우 다른 트랜잭션에서 READ가 가능하지만 WRITE가 불가능하고,
      READ 락을 건 트랜젹션이 해당 테이블의 데이터 변경을 원한다면 다시 WRITE 락을 획득해야 한다.
      아니면 Table was locked with a READ lock and can't be updated 와 같은 에러가 발생한다.

    • Table WRITE 락의 경우 락을 건 트랜잭션만이 해당 테이블에 접근 가능하고, 다른 트랜잭션은 접근 불가능하다.
      여기서 접근이란 read, write를 모두 포함하므로 Table WRITE 락이 걸린 테이블은 다른 트랜잭션에서 조회도 불가능하다.

    1
    UNLOCK TABLES

    위의 명령을 통해 트랜잭션에서 획득한 테이블 락을 해제할 수 있다.

유저 락

GET_LOCK 함수를 통해 잠금을 획득할 수 있으며, 단순히 사용자가 지정한 문자열에 대해 락을 획득하고 반납한다.
문자열에 대해 잠금을 획득한다는게 정확히 이해가 안간다…
문자열은 어쩌피 immutable 할텐데 락을 걸어야 할 이유가 있을까?

네임 락

db 객체(테이블 등)의 이름을 변경하는 경우 획득하는 잠금이다.
명시적으로 획득하거나 해제할 수 있는것은 아니고,
RENAME TABLE a TO b 처럼 테이블의 이름을 변경하는 경우 자동으로 획득하는 잠금이다.

스토리지 엔진 잠금

MyISAM, MEMOERY 스토리지 엔진 잠금

자체적인 잠금을 가지고 있지 않고 MySQL 엔진에서 제공하는 테이블 락을 그대로 사용한다.

InnoDB 스토리지 엔진 잠금

InnoDB의 경우 레코드 기반 잠금 방식을 사용한다.
이로 인해 훨씬 뛰어난 동시성 처리를 제공할 수 있게된다.

잠금 방식

  • 비관적 잠금
    • 변경하고자 하는 레코드에 대해 잠금을 먼저 획득하고 변경 작업을 처리하는 방식
    • 현재 변경하고자 하는 레코드를 다른 트랜잭션에서도 변경할 수 있다는 비관적 가정을 하기 때문에, 먼저 잠금을 획득
    • 높은 동시성 처리에 유리하며, InnoDB가 기본으로 채택하고 있는 방식임
  • 낙관적 잠금
    • 각 트랜잭션이 같은 레코드를 변경할 가능성은 희박할 것이라고 낙관적으로 가정
    • 변경 작업을 먼저 수행하고, 마지막에 잠금 충돌이 있는지 확인
    • 문제가 있었다면 ROLLBACK 처리

잠금 종류

  1. 레코드 락(Record Lock)
    레코드 자체만을 잠그는 행위를 말한다.
    다른 상용 DBMS의 레코드 락과 달리 InnoDB의 경우 인덱스를 참조하여 레코드를 잠근다는 큰 특징이 있다.
  2. 갭 락(Gap Lock)
    레코드 자체가 아니라 레코드와 바로 인접한 레코드 사이의 간격만을 잠그는 것을 말한다.
    레코드와 레코드 사이 간격에 새로운 레코드가 생성되는 것을 제어하기 위함이다.
    개념일 뿐 자체적으로 사용되지는 않고, 넥스트락의 일부로 사용된다.
  3. 넥스트 키 락(Next Key Lock)
    레코드락과 갭 락을 합쳐놓은 형태의 잠금을 말한다.
  4. 자동 증가 락(Auto Increment Lock)

인덱스와 잠금

위에서 언급했듯이 InnoDB의 잠금은 레코드를 바로 잠그는 것이 아니라, 인덱스를 사용하여 레코드를 잠근다.
아래와 같은 상황이 있다고 하자.

1
2
3
4
5
6
7
-- index : ix_firstname(firstname에 대한 index)  

-- 250건
SELECT COUNT(*) FROM employees WHERE first_name = 'Georgi';

-- 1건
SELECT COUNT(*) FROM employees WHERE first_name = 'Georgi' AND last_name = 'Klasen';

이 상황에서 아래와 같은 쿼리를 실행하게 되면,

1
UPDATE employees SET hire_date = NOW() WHERE first_name = 'Georgi' AND last_name = 'Klasen';

업데이트 될 레코드는 1건이지만, 인덱스로 필터할 수 있는 레코드의 개수는 250개가 한계이다.
last_name에 대한 인덱스는 없고, first_name에 대한 인덱스만 있기 때문이다.
즉, 최종적으로 first_name = 'Georgi' 에 해당하는 250건의 레코드가 모두 잠기는 현상이 발생한다.

이러한 특징 떄문에 UPDATE나 DELETE 문장을 위한 적절한 인덱스가 준비되어 있어야 한다. 그렇지 않으면 동시성이 상당히 떨어져서 한 세션에서 변경작업을 하는 중에는 다른 세션에서는 그 테이블을 변경하지 못하고 기다려야 하는 상황이 발생할 것이다.

인덱스가 없는 컬럼을 조건으로 변경 작업을 하게 될 경우, 테이블의 모든 레코드에 대해 내부 클러스터드 인덱스를 이용해 락을 걸게된다.
즉 last_name = 'Klasen'과 같은 조건으로 update 문을 실행하게 되면 외부에서 다른 아무 데이터도 수정할 수 없는 상황이 발생하게 되는 것이다.

이러한 특징 때문에 MySQL Client Tool(workbench 등)에서 기본적으로 index가 없는 컬럼으로 변경쿼리를 못 날리게 되어있는 것 같다(safe update 거리면서…)

해결법?(정확하지 않음)

이런 불필요한 레코드 잠금 현상은 InnoDB의 넥스트 키 락 때문에 발생하는 것이다.
넥스트 키 락의 경우 MySQL의 기본 isolation level인 REPETABLE READ에서 디폴트로 사용하는 잠금 방식이다.
여기서 isolaton level을 READ COMMITTED로 바꿔주면 불필요한 잠금 대신 실제 변경하는 레코드만 락을 거는 방식을 사용할 수 있게 된다.

그런데 MySQL 5.1 이상에서는 바이너리 로그가 활성화되면 최소 REPETABLE READ 이상의 격리 수준을 사용하도록 강제되고 있다.
그러므로 바이너리 로그를 사용하지 않아도 되는 상황이면, 바이너리 로그를 사용하지 않도록 설정하고 isolation level을 READ COMMITTED로 바꾸는 방법도 고려해 볼만하다.

참고로 READ COMMITTED로 불필요한 잠금이 없어졌다고 해서 엄청난 성능향상이 있는것은 아니다.
인덱스로 조회된 레코드에 모두 락을 거는 방식은 똑같은데, 이후에 바로 불필요한 부분에 대해서 락을 해제하는 식으로 동작하기 때문이다.
그러므로 최대한 인덱스를 사용헐 수 있게 튜닝해주는 것이 좋다.

참고 : 이성욱, 『Real MySQL』, 위키북스(2012)

Read more »

[jpa] QueryDSL

Posted on 2019-01-10 | Edited on 2020-11-02 | In jpa | Comments:

JPQL을 편하게, 동적으로 작성할 수 있도록 JPA에서 공식 지원하는 Creteria 라는것이 있다.
하지만 큰 단점이 있는데, 너무 불편하다는 것이다.

그에 반해 JPA에서 공식 지원하지는 않지만
쿼리를 문자가 아닌 코드로 작성해도 쉽고 간결하며, 모양도 쿼리와 비슷하게 개발할 수 있는 QueryDSL 이라는 것이 있다.
QueryDSL은 오픈소스 프로젝트이며, 이름 그대로 데이터를 조회하는데 기능이 특화되어 있다.

최범균님이 번역한 공식 한국어 문서를 제공한다.
http://www.querydsl.com/static/querydsl/4.0.1/reference/ko-KR/html_single/

설정

  • 필요 라이브러리

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-apt</artifactId>
    <version>${querydsl.version}</version>
    <scope>provided</scope>
    </dependency>

    <dependency>
    <groupId>com.querydsl</groupId>
    <artifactId>querydsl-jpa</artifactId>
    <version>${querydsl.version}</version>
    </dependency>
    • querydsl-jpa : QueryDSL JPA 라이브러리
    • querydsl-apt : 쿼리 타입(Q)를 생성할 때 사용하는 라이브러리
  • 쿼리 타입
    엔티티를 기반으로 생성된 쿼리용 클래스를 말한다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    buildscript {
    repositories {
    mavenCentral()
    maven {
    url "https://plugins.gradle.org/m2/"
    }
    }
    dependencies {
    classpath('net.ltgt.gradle:gradle-apt-plugin:0.18')
    }
    }
    repositories {
    mavenCentral()
    }
    apply plugin: "net.ltgt.apt"
    apply plugin: "net.ltgt.apt-idea”

    compile "org.projectlombok:lombok:${lombok_version}"
    annotationProcessor "org.projectlombok:lombok:${lombok_version}"

    compile "com.querydsl:querydsl-jpa:${querydsl_version}"
    compile "com.querydsl:querydsl-core:${querydsl_version}"
    compile "com.querydsl:querydsl-apt:${querydsl_version}"
    annotationProcessor "com.querydsl:querydsl-apt:${querydsl_version}:jpa"
    annotationProcessor "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:${hibernate_jpa_api_version}"

    빌드하면 지정한 outputDirectory에 지정한 target/generated-sources 위치에 QMember.java 처럼 Q로 시작하는 쿼리 타입들이 생성된다.

사용(4.1.3 버전 기준)

기본 사용법

동적으로 생성할 쿼리는 JPAQuery를 사용하여 만들 수 있는데, 이것보단 JPAQueryFactory를 사용하는게 권장된다고 한다.

JPQLQuery 인터페이스가 queryDSL 동적 쿼리 생성의 기준이 되는 인터페이스이고,
JPAQuery는 JPQLQuery를 구현한 클래스이다. 근데 왜 이름이 JPAQuery일까?

1
2
3
4
5
6
7
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember member = QMember.member;

Member foundMember =
queryFactory.selectFrom(member) // select + from
.where(customer.username.eq("joont"))
.fetchOne();

대충 위의 형태로 사용할 수 있다.

결과반환

  • fetch : 조회 대상이 여러건일 경우. 컬렉션 반환
  • fetchOne : 조회 대상이 1건일 경우(1건 이상일 경우 에러). generic에 지정한 타입으로 반환
  • fetchFirst : 조회 대상이 1건이든 1건 이상이든 무조건 1건만 반환. 내부에 보면 return limit(1).fetchOne() 으로 되어있음
  • fetchCount : 개수 조회. long 타입 반환
  • fetchResults : 조회한 리스트 + 전체 개수를 포함한 QueryResults 반환. count 쿼리가 추가로 실행된다.

프로젝션

프로젝션을 지정한다.

1
2
3
4
List<Member> foundMembers = 
queryFactory.select(member)
.from(member, order)
.fetch();

(아직 나오진 않았지만 from 절에 위처럼 쿼리 타입을 연속으로 줄 경우, 두 엔티티가 조인된다.)

member와 order가 조인된 상태에서 member 엔티티의 속성만 가져온다.
(select를 생략하면 기본적으로 from의 첫번째 엔티티를 프로젝션 대상으로 쓴다)

from

쿼리할 대상을 지정한다.

1
2
3
List<Member> foundMembers = 
queryFactory.from(member)
.fetch();

member 테이블을 전체 조회하게 된다. 프로젝션 지정(select)가 빠졌지만 위와 동일하게 from의 첫번째 엔티티를 사용한다.

from과 select를 나누기 보단 selectFrom 절을 쓰는것이 더 낫다.

조인

join, innerJoin, leftJoin, rightJoin 을 지원한다.
개인적으로 from절에 multiple arguments를 주는것보다 이게 더 좋다.(SQL에서도…)

1
2
3
4
5
6
QTeam team = QTeam.team;

List<Member> foundMembers =
queryFactory.selectFrom(member)
.innerJoin(member.team, team)
.fetch();

join의 첫번쨰 인자로는 join할 대상, 두번쨰 인자로는 join할 대상의 쿼리 타입을 주면 된다. on 절은 자동으로 붙는다.

  • 추가적인 on 절도 사용할 수 있다.
    1
    2
    3
    4
    5
    List<Member> foundMembers = 
    queryFactory.selectFrom(member)
    .innerJoin(member.team, team)
    .on(member.username.eq("joont"))
    .fetch();

조건

1
2
3
4
5
6
7
List<Member> foundMembers = 
queryFactory.selectFrom(member)
.where(member.username.eq("joont")) // 1. 단일 조건
.where(member.username.eq("joont"), member.homeAddress.city.eq("seoul")) // 2. 복수 조건. and로 묶임
.where(member.username.eq("joont").or(member.homeAddress.city.eq("seoul"))) // 3. 복수 조건. and나 or를 직접 명시할 수 있음
.where((member.username.eq("joont").or(member.homeAddress.city.eq("seoul"))).and(member.username.eq("joont").or(member.homeAddress.city.eq("busan"))))
.fetch();

(E1 and E2) or (E3 and E4) 같은 형태도 가능하다. 그냥 괄호로 묶어주면 된다.

1
2
3
4
List<Member> foundMembers = 
queryFactory.selectFrom(member)
.where((member.username.eq("joont").or(member.homeAddress.city.eq("seoul"))).and(member.username.eq("joont").or(member.homeAddress.city.eq("busan"))))
.fetch();

두가지 조건이 괄호로 묶이게 되었을때, or 이면 합집합이고 and 이면 교집합이다.
참고로 (E1 and E2) or (E3 and E4) 는 괄호가 생략되고 (E1 or E2) and (E3 or E4) 는 잘 동작한다.

그룹핑

group by도 가능하다.

1
2
3
4
5
List<String> foundCities = 
queryFactory.from(member)
.select(member.homeAddress.city)
.groupBy(member.homeAddress.city)
.fetch();

city로 group by 한 뒤 city만 출력하게 된다.

  • having도 가능하다. 집계함수도 쓸 수 있다.
    1
    2
    3
    4
    5
    6
    List<String> foundItems = 
    queryFactory.select(item.category) // category가 그냥 String이라고 가정
    .from(item)
    .groupBy(item.category)
    .having(item.price.avg().gt(1000)) // 집계함수 사용
    .fetch();

정렬

1
2
3
4
List<Member> foundMembers = 
queryFactory.selectFrom(member)
.orderBy(member.id.asc(), member.username.desc())
.fetch();

페이징

시작 인덱스를 지정하는 offset,
조회할 개수를 지정하는 limit,
두개를 인수로 받는 QueryModifiers를 사용하는 restrict를 지원한다.

근데 실제로 페이징 처리를 하려면 전체 데이터 개수를 알고 있어야하므로, fetchResults()를 사용해야 한다.

1
2
3
4
5
6
7
8
9
10
QueryResults<Member> result = 
queryFactory.selectFrom(member)
.offset(10)
.limit(10)
.fetchResults();

List<Member> foundMembers = result.getResults(); // 조회된 member
long total = result.getTotal(); // 전체 개수
long offset = result.getOffset(); // offset
long limit = result.getLimit(); // limit

다중 결과 반환

다중 프로젝션 할 경우 Tuple 클래스로 받을 수 있다.

1
2
3
4
5
6
7
List<Tuple> foundMembers = 
queryFactory.select(member.username, member.homeAddress.city)
.from(member)
.fetch();

System.out.println(founeMembers.get(0));
System.out.println(founeMembers.get(1));

리턴되는 클래스가 class com.querydsl.core.types.QTuple$TupleImpl 인데, 이것보단 아래 빈 생성(bean population)을 쓰는게 더 나아보인다.

빈 생성

자바빈을 말한다(스프링 빈 아님).
출력되는 다중 결과를 빈으로 변경해서 리턴할 수 있다.

1
2
3
4
List<MemberDTO> foundMembers = 
queryFactory.select(Projections.bean(UserDTO.class, member.username, member.homeAddress.city))
.from(member)
.fetch();

위의 bean 메서드를 호출하면 전달받은 인자와 동일하게 UserDTO의 setter를 호출한다.
field 메서드를 사용하면 필드에 직접 접근하고(private도 가능),
constructor 메서드를 사용하면 생성자를 사용한다. 지정한 프로젝션과 파라미터 순서가 같은 생성자가 필요하다.

엔티티의 필드명과 빈의 필드명이 다를 경우 아래와 같이 사용할 수 있다.

1
2
queryFactory.select(Projections.bean(UserDTO.class, member.username.as("name"), member.homeAddress.city))
....

Member 엔티티 필드 username을 MemberDTO의 name에 전달하게 된다.

서브쿼리

JPAExpression 을 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
QMember member = QMember.member;
QMember subQueryMember = new QMember("subQueryMember"); // 추가로 생성해줘야 함

List<Tuple> foundMembers =
queryFactory.select(member.name, member.homeAddress.city)
.from(member)
.where(member.name.in(
JPAExpressions.select(memberForSubquery.name)
.from(memberForSubquery)
))
.fetch();

동적 조건

com.querydsl.core.BooleanBuilder 를 사용하면 동적 조건을 생성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
BooleanBuilder builder = new BooleanBuilder();
if(param.getId() != null){
builder.and(member.id.eq(param.getId()));
}
if(param.getName() != null){
builder.and(member.name.contains(param.getName()));
}

List<Member> list =
queryFactory.selectFrom(member)
.where(booleanBuilder)
.fetch();

수정, 삭제, 배치 쿼리

  • update
    JPAUpdateClause 클래스를 통해 실행할 수 있다.(인터페이스는 UpdateClause이다)
    JPAQueryFactory의 update 메서드를 통해 생성할 수 있다.

    1
    2
    3
    4
    5
    QCustomer customer = QCustomer.customer;
    // rename customers named Bob to Bobby
    queryFactory.update(customer).where(customer.name.eq("Bob"))
    .set(customer.name, "Bobby")
    .execute();
  • delete
    JPADeleteClause 클래스를 통해 실행할 수 있다.(인터페이스는 DeleteClause이다)
    JPAQueryFactory의 delete 메서드를 통해 생성할 수 있다.

    1
    2
    3
    4
    5
    QCustomer customer = QCustomer.customer;
    // delete all customers
    queryFactory.delete(customer).execute();
    // delete all customers with a level less than 3
    queryFactory.delete(customer).where(customer.level.lt(3)).execute();
Read more »

Swagger와 OAS의 관계

Posted on 2019-01-08 | Edited on 2020-11-02 | In openAPI | Comments:

누가 이 글에 잘못된 것좀 알려주세요

원래는 기업별로 각각 OpenAPI를 제공하는 명세가 있었다.
(swagger를 만든 smart bear도 자신들의 OpenAPI specification이 있었다)

2015년에 이 모든 기업들이 OpenAPI initiative 라는 그룹으로 통합(가입?)하었고, 여기서 공통된 spec인 Open API specification을 제공한다.
https://github.com/OAI/OpenAPI-Specification

그러므로 swagger도 문서에 보면
2.0일때는 상단에 swagger: "2.0" 이었는데, 3.0부터 openapi: "3.0"으로 변경되었다.
(근데 통합인데 왜 3.0부터 가는건지… swagger가 주도하나…)

Read more »

[git] git-crypt

Posted on 2019-01-08 | Edited on 2020-11-02 | In git | Comments:

git-crypt란?

git-crypt를 사용하면 특정 파일 또는 폴더를 원격 repository에 올릴때는 encrypt하고, 로컬로 내려받을때는 decrypt 하는 식으로 관리할 수 있다.

git-crypt repostiroy : https://github.com/AGWA/git-crypt
git-crypt 적용법 : https://s-opensource.org/2017/07/18/introduction-gpg-encryption-git-crypt/

적용법

1
2
3
4
5
brew install git-crypt

cd /path/to/repository

git-crypt init

하면 .git/git-crypt가 추가되고, git-crypt 할 수 있게 됨

그리고 repository root 경로애 .gitattributes 파일을 만들고 내부에 git-crypt가 적용될 파일을 적어주면 된다.

1
2
config/production/** filter=git-crypt diff=git-crypt
# more...

원하는 패턴이 더 있을 경우 밑에 나열해주면 된다.(.gitignore과 비슷하게 작성한다)

기본적으로 초기에 git-crypt를 설정한 사람은 올리고 내릴때 자동으로 encrypt decrypt가 되지만,
다른 collaborator들은 적용되지 않으므로 추가적인 설정이 필요하다.

  1. gpg 적용

    상단의 git-crypt 적용법 보고 따라하면 될 듯

    1. 추가하고 싶은 collaborator의 pc에서 gpg 생성
    2. project 생성자가 git-crypt add-gpg-user 로 collaborator의 gpg를 추가
    3. pull 받은 collaborator는 git-crypt unlock 으로 간단하게 unlocking 가능
    4. 테스트 안해봄
  2. key export import

    encrypt 할 때 사용한 key를 export하고, collaborator는 해당 key로 unlock 하는 방법

    1. repository의 git-crypt key값을 export 하여 collaborator에게 전달하고,

      1
      git-crypt export-key /path/to/key
    2. 전달받은 collaborator는 해당 key 파일을 사용하여

      1
      git-crypt unlock /path/to/key

      의 형태로 decrypt 한다.

기타

  1. 만약 레파지토리끼리 키를 공유하고 싶으면?
    한쪽 레파지토리에서 생성된 .git/git-crypt/keys/default를 다른 레파지토리에서 경로 그대로 복사해서 사용하면 됨
    근데 이 방식으로 git-crypt unlock이 바로 되지는 않는다. 번거롭게 key 파일 위치를 지정해줘야 한다.
Read more »

[java] 객체 클로닝

Posted on 2019-01-06 | Edited on 2020-11-02 | In java | Comments:

http://javacan.tistory.com/entry/31

Cloneable 인터페이스를 implements하고(안하면 CloneNotSupportedException 발생),
clone 메서드를 오버라이드 하고 Object의 clone(super.clone)을 실행한다.

clone 메서드가 수행되면 원본과 같은 객체를 새로 생성하고, 모든 필드들을 원본의 필드들과 같은 값으로 초기화한다.
(생성자는 실행되지 않음)
단순하게 대입에 의해 복사되는 형태이기 때문에, 배열이나 객체의 경우 참조값만 복사되게 된다.
즉, 원본 객체에 대해 deep clone 하지 않고 shallow clone 한다는 뜻이다.

따라서 객체가 가지고 있는 배열이나 객체에 Cloneable 인터페이스를 구현하고 clone을 오버라이딩 해줘야하고,
(배열은 기본적으로 clone이 구현되어 있음)
클로닝의 대상이 되는 객체에서 해당 필드까지 전부 클로닝을 실행해줘야 한다.
클로닝 대상의 범위는 개발자가 필요로 하는 곳 까지 구현하면 된다.

Read more »
1…91011…19

JunYoung Park

182 posts
18 categories
344 tags
RSS
© 2020 JunYoung Park
Powered by Hexo v3.6.0
|
Theme – NexT.Muse v7.1.0