[토이프로젝트] TimeLine 2.0 개선 오픈 (임시 휴업)

IT 지식/기타지식|2020. 1. 27. 21:41

설계, 운영 모든면에서 실패

토이프로젝트 TimeLine을 만들어서 오픈했었다. 

개발의 목표는 단순하게 개발 블로그, 채용등에 대해 한번에 볼 수 있는 개발자를 위한 통합 페이지를 만드는 것이었다.

https://wedul.site/626

 

[토이 프로젝트 소개] 개발자 채용, 기술 블로그 정보를 모아주는 TimeLine

혼자 공부를 집에서 어떻게 하면 효율적일까 고민을 많이했다. 집에서 주구장창 책을 읽고 해보면 스킬이 늘까? 그렇게 해봤지만 그게 정답은 아니었다. 남들에게는 모르겠으나 나에게는 아니었다. 회사에서 하는..

wedul.site

하지만 사용하는데 생각보다 불편한 부분이 많았고 설계부분에도 현재 시스템을 고려하지 않고 만들었던게 운영에 걸림돌이 되어버렸다. 그래서 서비스를 잠정적 중단하고 2.0을 만들어서 오픈하기로 했다.

 

 

설계 실패 요인

실패 요인 1.

- 젠킨스를 실행시키면 별도의 관리포인트가 생길것으로 우려해 batch application을 계속 실행시켰던 부분이 큰 문제였다. 내 서버는 배치, db, api모두 하나의 서버에서 돌아가는데 4기가라는 부족한 메모리 상황에서 불필요한 batch application을 실행시켜서 다른 애플리케이션에서 메모리가 많이 부족하게 만들었다. 그래서 젠킨스를 작은 메모리를 할당하여 실행시키고 batch job을 세분화하여 batch application이 계속 실행되지 않게 하였다.

 

 

각 모듈을 build하는 jenkins job
rss, 스크래핑 등을 진행하는 indexing job
현재 api를 교체하는 job

 

 

실패요인 2.

실패요인1과 같이 시스템의 리소스가 많이 부족한 상황에서 api에 호출이 늘어나면 부하가 걸렸다. 그래서 이를 조금이라도 줄여보기 위해서 api에 비동기를 붙이기로 했다. 하지만 사용하고 있는 rdb에서는 비동기가 지원을 하고 있지 않은 상황에서 어떻게 해야하지 고민했는데 마침 r2dbc mysql이 정식 release했다. 굿굿 그래서 이를 사용해서 적용했다. 실제로 드라마틱하게 api 성능이 오른건 아니지만 그래도 확실히 속도가 더 빨라졌다.

 

실패요인 3.

욕심이 생겨 batch에서 긁은 데이터를 writer를 통해 업데이트 하는게 아니라 별도 kafka에 메시지를 보내서 별도 모듈에서 업데이트를 했다. ㅋㅋ 결국 그 욕심이 적은 리소스에 또 문제를 일으켰고 애플케이션이 메모리부족으로 많이 죽었다 에휴 작은 환경에서 무리한 오버스펙은 오히려 문제를 야기한다는걸 더욱 느끼게 되었다. (심지어 필요없는 레디스도 실행시켰다.)

 

실패요인 4.

UI구성을 router, component 구분없이 하나의 컴포넌트에 구분없이 추가하여 확장성 고려도 path구분도 되지 않는 상황이었다. 이를 해결하지 못해서 결국 기능추가도 공유하기도 어려웠다. 그래서 router를 추가하고 페이지를 만들고 각 page에서 사용할 공통 component를 만들어서 편하게 가져다 쓸 수 있도록 구성했다. 그 결과 이제 페이지 하나 늘려서 컴포넌트를 추가하고 빼는건 일도 아니게 되었다. 그리고 하단의 footer를 없애고 vue-sidebar-menu 라이브러리를 사용하여 보는 페이지 뷰를 늘려주었다. 

 

 

변경된 UI

기존의 UI에는 몇 가지 문제가 있었다. 

 

UI 문제 1.

데이터를 보여주는 영역은 가지고 온 제목, 로고, 데이터등을 다 보여주는데 데이터 유형이 모두 달라서 같은 유형으로 보여주기 어려웠다. 채용의 경우 큰 문제가 되지 않았지만 개발 블로그 경우에는 컨텐츠 표시 형식이 모두 달라서 보여주는 영역마다 다 깨졌다. 그래서 채용은 기존틀을 유지하고 테크는 타이틀과 컨텐츠 대표 이미지를 사용해서 리스트를 모여줬다.

좌, 테크 (기술블로그) 영역 리스팅

 

UI 문제 2.

결국 모든 데이터를 아웃링크로 보내다보니 트래픽이 많이 빠졌다. 이를 해결하려면 내부에서 화면을 보여주는게 좋다고 생각했다. 그래서 iframe을 사용해서 화면을 보여주도록 변경했다. 하지만 iframe을 막은 사이트에 경우에는 아웃링크로 가서 볼 수 있도록 아웃링크로 가는 링크를 만들어주었다.

 

 

 

UI 문제 3.

보고 싶은 업체만 보고 싶은 경우에 볼 수 있는 방법이 없었다. 그래서 이를 해결하기 위해서 상단의 리스트 바를 만들어서 자유롭게 골라서 볼 수 있도록 구성했다.

 

업체 리스팅

 

 

 

회고

2.0을 만들겠다고 생각했을 때 나 혼자서 만들기는 어렵다고 판단해서 같이 전직장(pnpsecure)에서 일했던 친구에게 부탁했다. 친구(https://lelecoder.com/)가 마침 이직을 하기 전이라 놀고 있어서 업무를 많이 진행해줬고 그래서 더 많은 컨텐츠를 가져올 수 있었고 수집된 컨텐츠 정보와 에러 정보를 slack으로 받는 기능등을 추가 받을 수 있었다. 함께하니 더 재밌고 좋았다. 나중에 같이 일할 수 있는 기회가 또 왔으면 좋겠다.

 

수집한 컨텐츠 정보를 알려주는 slack과 에러 발생시 알려주는 slack

 

 

추가된 정보와 변경된 구조 그리고 ui가 모두 맘에 든다 많은 분들이 구경와서 더 좋은 컨텐츠를 모아서 보여줄 수 있는 서비스가 되었으면 좋겠다.

해당 프로젝트는 비영리적 목적으로 해당 코드를 가지고 수정하거나 상업적 용도로 사용하는 것을 금지한다.

http://wedul.space

 

새로워진 TimeLine 2.0

 

 

아쉽게도 임시적으로 서버 운영을 멈춥니다.

댓글()
  1. jayden 2020.01.27 23:23 댓글주소  수정/삭제  댓글쓰기

    좋은 정보네요. 타임라인 사이트 자주 찾아갈게요.

  2. byrage 2020.01.28 23:36 댓글주소  수정/삭제  댓글쓰기

    좋은서비스에 쿨한회고 조합 멋지네요. 고생하셨습니다~

  3. Favicon of https://lelecoder.com BlogIcon jayden-lee 2020.01.29 16:43 신고 댓글주소  수정/삭제  댓글쓰기

    wedul은 무슨 뜻인가요?

  4. 감자아빠 2020.01.30 14:54 댓글주소  수정/삭제  댓글쓰기

    해당 채용 정보를 다시 RSS로 제공해도 좋을것 같습니다.

RestHighLevelClient를 사용하여 search after 기능 구현하기

web/Spring|2019. 11. 14. 17:55

https://wedul.site/541에서 search after 기능을 사용해서 검색을 하는 이유를 알아봤었다.

그럼 spring boot에서 RestHighLevelClient를 이용해서 search after를 구현을 해보자.

 

1. Mapping

우선 index가 필요한데 간단하게 상품명과 지역 가격정보들을 가지고 있는 wedul_product 인덱스를 만들어 사용한다.

{
    "settings": {
        "index": {
            "analysis": {
                "tokenizer": {
                    "nori_user_dict": {
                        "type": "nori_tokenizer",
                        "decompound_mode": "mixed",
                        "user_dictionary": "analysis/userdict_ko.txt"
                    }
                },
                "analyzer": {
                    "wedul_analyzer": {
                        "tokenizer": "nori_user_dict",
                        "filter": [
                            "synonym"
                        ]
                    }
                },
                "filter": {
                    "synonym": {
                        "type": "synonym",
                        "synonyms_path": "analysis/synonyms.txt"
                    }
                }
            }
        }
    },
    "mappings": {
        "dynamic": "false",
        "properties": {
            "productId": {
                "type": "keyword"
            },
            "place": {
                "type": "text",
                "fields": {
                    "keyword": {
                        "type": "keyword"
                    }
                }
            },
            "message": {
                "type": "text"
            },
            "query": {
                "type": "percolator"
            },
            "name": {
                "type": "text",
                "analyzer": "wedul_analyzer",
                "fields": {
                    "keyword": {
                        "type": "keyword"
                    }
                }
            },
            "price": {
                "type": "integer"
            },
            "updateAt": {
                "type": "date",
                "format": "epoch_second"
            },
            "createAt": {
                "type": "date",
                "format": "epoch_second"
            }
        }
    }
}

값은 적당하게 3개정도 삽입하였다.

저장되어 있는 초기값.

 

2. 라이브러리 

사용에 필요한 라이브러리들을 gradle을 사용해서 추가한다. 

plugins {
    id 'org.springframework.boot' version '2.2.0.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'java'
}

ext {
    set('elasticsearch.version', '7.4.2')
}

group = 'com.wedul'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
    maven { url "https://plugins.gradle.org/m2/" }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.0'
    annotationProcessor 'org.projectlombok:lombok'
    testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // gson
    compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6'

    // elasticsearch
    compile 'org.elasticsearch.client:elasticsearch-rest-high-level-client:7.4.2'
    compile group: 'org.elasticsearch', name: 'elasticsearch', version: '7.4.2'
}

 

 

3.RestHighLevelClient configuration

restHighLevelClient 사용을 위한 Configuration 파일을 만들어주는데 id와 pw는 AppConfig라는 별도 properties를 관리하는 bean에서 받아서 사용하는데 base64로 인코딩되어있어서 이를 decoding후 사용한다. (부족한 코드는 글 맨 아래있는 github 링크 참조)

package com.wedul.study.common.config;

import com.wedul.study.common.util.EncodingUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

/**
 * study
 *
 * @author wedul
 * @since 2019-11-07
 **/
@Configuration
@Slf4j
public class ElasticsearchClientConfig implements FactoryBean<RestHighLevelClient>, InitializingBean, DisposableBean {

    @Autowired
    AppConfig appConfig;

    private RestHighLevelClient restHighLevelClient;

    @Override
    public RestHighLevelClient getObject() {
        return restHighLevelClient;
    }

    @Override
    public Class<?> getObjectType() {
        return RestHighLevelClient.class;
    }

    @Override
    public void destroy() {
        try {
            if (null != restHighLevelClient) {
                restHighLevelClient.close();
            }
        } catch (Exception e) {
            log.error("Error closing ElasticSearch client: ", e);
        }
    }

    @Override
    public boolean isSingleton() {
        return false;
    }

    @Override
    public void afterPropertiesSet() {
        restHighLevelClient = buildClient();
    }

    private RestHighLevelClient buildClient() {
        try {
            String id = EncodingUtil.decodingBase64(appConfig.getElasticsearchConfig().getId());
            String pw = EncodingUtil.decodingBase64(appConfig.getElasticsearchConfig().getPw());

            // 계정 설정
            final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
            credentialsProvider.setCredentials(AuthScope.ANY,
                new UsernamePasswordCredentials(id, pw));

            // client 설정
            RestClientBuilder builder = RestClient.builder(
                new HttpHost(appConfig.getElasticsearchConfig().getIp(),
                    appConfig.getElasticsearchConfig().getPort(), "http"))
                .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider));

            restHighLevelClient = new RestHighLevelClient(builder);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return restHighLevelClient;
    }

}

 

 

4. Handler 추가

자주 사용되는 Elasticsearch 문법을 처리하기 위해서 만들어 놓은 ElasticsearchHandler에 search after에 사용 될 메소드를 추가한다. search after는 sort 필드가 없으면 사용이 불가능 하기 때문에 sort 필드가 없는 경우 에러를 전달한다.

public static SearchSourceBuilder searchAfter(Map<String, SortOrder> sortFields, QueryBuilder query, Object[] searchAfter, int size) {
    return searchAfterBuilder(sortFields, query, searchAfter,  size);
}

public static SearchSourceBuilder searchAfter(Map<String, SortOrder> sortFields, QueryBuilder query, Object[] searchAfter) {
    return searchAfterBuilder(sortFields, query, searchAfter, 20);
}

private static SearchSourceBuilder searchAfterBuilder(Map<String, SortOrder> sortFields, QueryBuilder query, Object[] searchAfter, int size) {
    SearchSourceBuilder builder = new SearchSourceBuilder();

    if (CollectionUtils.isEmpty(sortFields)) {
        throw new InternalServerException("잘못된 필드 요청입니다.");
    }

    sortFields.forEach((field, sort) -> {
        builder.sort(field, sort);
    });
    builder.size(size);
    builder.query(query);

    if (ArrayUtils.isNotEmpty(searchAfter)) {
        builder.searchAfter(searchAfter);
    }

    return builder;
}

 

 

5. 기능 구현

위의 기능들을 이용해서 실제로 구현해보자. productService와 productRepository 클래스를 통해서 구현하였다. 자세한 설명없이 간단하기 때문에 소스를 보면 알 수 있다. 

 

우선 최종 결과물로 사용될 클래스는 ElasticResult인데 다음과 같이 현재 요청이 마지막인지 표시하는 isLast와 다음 요청을 위해 보내줘야 하는 cursor값과 결과값 전체 total과 결과 리스트 list 필드가 존재한다.

@Builder
@Data
public class ElasticResult<T extends ElasticsearchDto> {

    private boolean isLast;
    private long total;
    private List<T> list;
    private Object[] cursor;

}

 

그 다음 service로직을 통해 결과를 얻어서 위 ElasticResult에 결과를 담아보자. products 메서드는 요청을 받아서 elasticsearch에 실제 조작요청을 하는 productRepository에 동작을 요청하고 값을 받아서 처리하는 메서드이다. 그리고 extractProductList는 결과값에서 ProductDto 값을 뽑아내는 메서드이다.

public ElasticResult<ProductDto> products(String index, Object[] searchAfter, int size) throws IOException {
    SearchResponse searchResponse = productRepository.products(index, searchAfter, size);
    SearchHits searchHits = searchResponse.getHits();
    int hitCnt = searchHits.getHits().length;
    boolean isLast = 0 == hitCnt || size > hitCnt;

    return ElasticResult.<ProductDto>builder()
        .cursor(isLast ? null : searchHits.getHits()[hitCnt - 1].getSortValues())
        .isLast(isLast)
        .list(extractProductList(searchHits))
        .total(searchHits.getTotalHits().value)
        .build();
}

private List<ProductDto> extractProductList(SearchHits searchHits) {
    List<ProductDto> productList = new ArrayList<>();

    searchHits.forEach(hit -> {
        Map<String, Object> result = hit.getSourceAsMap();

        productList.add(ProductDto.builder()
            .name(String.valueOf(result.get("name")))
            .productId(String.valueOf(result.get("productId")))
            .place(String.valueOf(result.get("place")))
            .price(Integer.valueOf(result.get("price").toString()))
            .updateAt(Long.valueOf(result.get("updateAt").toString()))
            .createAt(Long.valueOf(result.get("createAt").toString())).build());
    });

    return productList;
}

 

그리고 마지막으로 es에 직접적으로 콜을 하는 productRepository 이다. 여기서 정렬 키워드는 name과 place를 사용한다.

public SearchResponse products(String index, Object[] searchAfter, int size) throws IOException {
    SearchRequest searchRequest = new SearchRequest(index);
    Map<String, SortOrder> sorts = new HashMap<String, SortOrder>() {
        {
            put("name.keyword", SortOrder.DESC);
            put("place.keyword", SortOrder.DESC);
        }
    };

    SearchSourceBuilder searchSourceBuilder = ElasticsearchHandler.searchAfter(sorts, QueryBuilders.matchAllQuery(), searchAfter, size);
    searchRequest.source(searchSourceBuilder);
    return restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
}

 

 

6. 테스트

그럼 위에 내용이 잘 구현되었는지 테스트를 해보자. 총 3개의 데이터가 있는데 이 데이터를 1개씩 search after를 통해서 값을 받아서 저장하고 한번에 출력하도록 해보자.

@Test
@DisplayName("search after")
public void searchAfter() throws IOException {
    ElasticResult<ProductDto> result = productService.products(PRODUCT_INDEX, new Object[]{}, 1);
    List<ProductDto> productDtos = new ArrayList<>();

    while(result != null && !result.isLast()) {
        productDtos.addAll(result.getList());
        result = productService.products(PRODUCT_INDEX, result.getCursor(), 1);
    }
    productDtos.addAll(result.getList());

    productDtos.forEach(productDto -> {
        System.out.println("이름 : " + productDto.getName());
        System.out.println("장소 : " + productDto.getPlace());
    });
}

결과는 정상적으로 3가지 모두 잘 출력되는 걸 알 수있다.

 

우선 기능 구현을 해보기 위해서 진행하였는데 더 다듬어야 할 것같다.

자세한 소스는 github참조

댓글()

Java 메모리 구조 및 GC 알고리즘 정리

JAVA/고급 자바|2019. 9. 23. 22:07

자바 메모리 구조는 1.8 이후로 일부분 바뀌었다.

이 부분에 대한 정리를 다시 하고 싶었고 GC 알고리즘에 대한 종류와 상세 내용을 정리하고 싶었다. 그럼 이 두 가지 사항에 대해 가볍게 정리해보자.

Java 메모리 구조

Method Area

프로그램이 실행되는 도중에 아직 사용되지 않은 클래스들의 코드는 new를 통해 클래스의 인스턴스가 생성되면 JVM Method Area에 인스턴스 변수, 메스드 코드, 클래스 변수등을 저장한다. 해당영역은 모든 쓰레드 사이에서 공유되고 static 키워드로 생성된 변수 또한 저장을 Runtime Constant Pool 영역에 저장한다. 실 데이터를 저장하는 것이 아니라 레퍼런스만 저장하며 실제 데이터는 Heap 영역에 저장한다.

 

JVM Language stack

각 스레드들은 생성과 동시에 각자의 stack을 생성하게 되는데 이 영역에 메소드가 실행될 때 사용된 메스드의 데이터들을 저장한다. 그리고 메서드 실행이 끝나면 해당 stack영역은 사라진다.

 

PC Registers

각 스레드별로 PC Registers가 존재하며 JVM 머신이 가장 최근에 실행한 명령어의 주소를 저장한다.

 

Native Method Area

Native Library에 의존하는 native 코드들을 저장하는 곳이다. (JNI)

 

Permenent (Java8 metaspace 대체)

permenent 영역에 로드된 클래스의 메타 정보와 static한 변수들 정보들이 담겨져 있는데 그 중 static 영역과 상수 영역은 heap으로 옮겨졌다.

-XX:MetaspaceSize : JVM이 사용하는 네이티브 메모리 
-XX:MaxMetaspaceSize : metaspace의 최대 메모리 

 

Heap 영역

GC가 발생되는 대표적인 영역이며 new를 통해 인스턴스가 동적으로 생성된 데이터와 배열정보를 저장하는 공간으로 xms, xmx등의 옵션으로 기본 힙사이즈를 설정할 수 있다. 해당 힙사이즈도 모든 쓰레드 사이에서 공유된다.

-Xms : JVM 시작 시 힙 영역 크기
-Xmx : 최대 힙 영역 크기

힙 영역은 GC가 발생되는 방법에 따라 Young, Old영역으로 나뉘게 된다.

1. Young 영역 

Eden

새롭게 할당된 데이터가 쌓이는 곳으로 일정주기 동안 참조가 유지되면 Survivor로 옮겨진다. Survivor로 옮겨지지 못한 데이터는 GC에 의해 청소된다.

Survivor

Eden영역에서 넘어온 데이터가 1 또는 2영역으로 나눠서 저장된다. 참조가 살아있는 경우 주기에 맞춰서 다른 Survivor영역으로 이동하고 그렇지 못한 데이터들은 GC에 의해서 처리된다. 위와 같은 경우를 Minor GC라고 한다. 

-XX: NewRatio      : New영역과 Old 영역의 비율
-XX: NewSize       : New 영역의 크기
-XX: SurvivorRatio : Eden 영역과 Survivor 영역의 비율

 

2. Old 영역

Survivor 영역에서 오래 살아남은 데이터의 경우 Old 영역으로 넘어가게 된다. Old 영역은 이렇게 넘어온 데이터가 많기 때문에 Young크기 보다 더 크게 설계되며 이부분에서 발생된 GC를 Major GC라고 한다.

 

GC 알고리즘

YOUNG, OLD GC가 발생되는 알고리즘 종류에 대해 정리해보자. 우선 GC의 경우 mark > sweep > compaction 작업이 순서대로 동작한다. 우선 GC 대상을 고르는 mark 작업이 선행되고 실제 제거를 수행하는 sweep가 동작한다. 그리고 메모리의 파편화가 된 부분을 채워 나가는 Compaction 작업으로 마무리한다.

Serial GC

위에서 언급한 3가지 작업이 진행되는 간단한 GC 알고리즘이다. 이 GC의 경우 처리하는 쓰레드가 단 하나이기 때문에 처리하는 과정 동안에 발생하는 STW (Stop the world) pause 시간이 길다.

 

Parallel GC

Serial GC에서 동작하는 스레드가 하나였기 때문에 문제가 자주 발생하였는데, 여기에 작업을 진행하는 스레드를 추가하여 병렬로 작업을 진행한 알고리즘 이다.

왼쪽부터 Serial GC, Parallel SerialGC (https://www.oracle.com/technetwork/java/index.html)

CMS GC

기존에 사용되던 GC 알고리즘 보다 STW pause 시간을 줄이기 위해 고안된 방법으로 없애야 하는 데이터를 정확하게 선별하는 작업이 추가로 진행된다. 그만큼 연산작업이 추가되어 CPU같은 리소스 자원 사용이 증가하였다. 최초 GC 판단하는 initial Mark, initial mark때 선정된 객체를 참조 하는 객체의 GC 대상인지 판단하는  Concurrent Mark, 마지막 검증 작업을 하는 Remark작업을 통해 대상을 선정한 후 Concurrent Sweep 작업을 통해 데이터를 지운다. 그리고 기존 Serial GC와의 차이점은 데이터를 sweep한 후 compaction 작업을 자주 진행하지 않는다. 파편화된 메모리를 자주 매꾸면 그만큼 오버헤드가 많이 발생할 수 있기 때문에 심각한 파편화가 발생했을 때만 매꾼다.

 

G1GC

기존에 GC 알고리즘과는 다른 알고리즘이 나온게 G1GC이다. 기존에는 Eden, Survivor, Old, Permanent영역으로 정확하게 나누어져 있었다. 하지만 G1GC 사용할 경우 heap 영역을 2048개 region 영역으로 쪼개고 이 지역의 크기는 G1HeapRegionSize를 통해 32mb 까지 지정이 가능하다.

또한 새로운 형태의 상태값이 생겼는데 Humongous와 Available/Unused이다. Humongous는 region크기의 50%를 초과하는 큰 데이터를 저장하기 위한 곳이고 Available/Unused는 아직 사용하지 않는 region을 뜻 한다.

각 Region은 Eden, Survivor, Old, Permanent, Humongous, Available/Unused 상태로 지정이 가능하고 데이터가 가장 많이 찬 Region에서 GC가 발생된다. 

G1GC 사용 시 heap 영역 (https://c-guntur.github.io/java-gc/#/6)

 

 

출처

https://www.guru99.com/java-virtual-machine-jvm.html
https://d2.naver.com/helloworld/1329

댓글()

백준 4673번 셀프 넘버

JAVA/알고리즘|2019. 6. 14. 23:45

1 ~ 10000까지의 숫자중에 셀프 넘버가 아닌 데이터를 noSelfNumber에 집어넣고 loop를 순회하면서 selfNumber 여부를 체크하면 된다. 간단한 문제이다.

https://www.acmicpc.net/problem/4673

 

4673번: 셀프 넘버

문제 셀프 넘버는 1949년 인도 수학자 D.R. Kaprekar가 이름 붙였다. 양의 정수 n에 대해서 d(n)을 n과 n의 각 자리수를 더하는 함수라고 정의하자. 예를 들어, d(75) = 75+7+5 = 87이다. 양의 정수 n이 주어졌을 때, 이 수를 시작해서 n, d(n), d(d(n)), d(d(d(n))), ...과 같은 무한 수열을 만들 수 있다.  예를 들어, 33으로 시작한다면 다음 수는 33 + 3 + 3 = 39이고, 그 다음 수는

www.acmicpc.net

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        getNonSelfNumber();
    }

    public static void getNonSelfNumber() {
        List<Integer> nonSelfNumber = new ArrayList<>();

        for (int i = 1; i < 10000; i++) {
            if (!nonSelfNumber.contains(i)) {
                System.out.println(i);
            }

            nonSelfNumber.add(func(i));
        }

    }

    public static int func(int num) {
        int result = num;

        while (num != 0) {
            result += num % 10;
            num /= 10;
        }

        return result;
    }

}

댓글()

DDD. 애거리루트 정리

DDD|2019. 5. 16. 22:53

애그리거트는 관련된 객체를 하나의 군으로 묶어주는 것 으로 상위수준에서 모델을 조망하는 방법 중 하나이다.

애그리거트는 비슷한 속성을 가진 객체를 묶어놓은 것을 의미한다.
예를 들어 주문 시스템에 주문 관련 애그리거트는 Order, Receiver, OrderLine.. 등이 있고 회원정보에는 Member, MemberInfo등으로 나눌 수 있다. 각 애그리거트에 연관된 객체를 담고 있으며 유사하고 동일한 라이프 사이클을 보유하고 있다.

애거리거트 루트


애그리거트에서 가장 핵심이 되는 주체 즉, 애그리거트 전체를 관리하고 책임지는 주체를 애그리거트의 루트 엔티티라고 한다. 애그리거트내에 존재하는 모든 엔티티는 루트 엔티티와 직간접적으로 연결되어있다.

애거리거트 루트의 핵심 역할은 애거리거트의 일관성을 유지하는 것이다. 그렇기 때문에 모든 애거리거트의 주요 기능은 애거리거트 루트 엔티티에 구현되어야 한다. 다시말하면 애거리거트 루트가 아닌 다른 객체가 애그리거트에 속한 객체를 직접 변경하면 안된다. 

예를 들어 상품에 대한 애그리거트가 있고 루트 엔티티로 Product가 있을 때 상품에 대한 정보를 보유한 ProductInfo의 price를 할인율 반영없이 단순히 변경하면 모델에 일관성을 깨트릴 수 있다.

ProductInfo productInfo = product.getProductInfo();
productInfo.setPrice(price);

또한 이렇게 변경하면서 중간에 할인율을 검사하도록 할 수 있지만 매번 중복된 코드가 만들어 질 수 있다.

ProductInfo productInfo = product.getProductInfo();
price = priceCalculator(price);
productInfo.setPrice(price);

 

트랜잭션의 범위


트랜잭션의 크기는 작을 수록 좋다. 하나의 트랜잭션에서 두 개 이상의 애거리거트(주문, 사용자정보등등)를 수정하게 되면 충돌이 발생할 가능성이 크다.

예를 들어 주문을 하면서 입력한 배송지를 사용자의 기본 배송지로 설정하는 경우에는 주문정보와 사용자 정보를 동시에 수정하는 경우가 발생 할 수 있다.

이런 경우에는 Order라는 애거리거트 루트에서 값을 두개 모두 수정하지 말고 OrderService라는 곳에서 값을 수정하는것이 옳다. 간단하게 보면 다음과 같이 OrderService에서 수정하는 것이 옳다.

public class OrderService {
  public void order(ShipInfo ship, isNewShippingAddr) {
    if(isnewShippingAddr) custermer.setAddr(ship.getAddr());
  }
}

 

애거리거트 필드 참조


애거리거트에서 다른 애거리거트를 필드로써 참조할 수 있다. 아래 예처럼 학교라는 School 클래스에 Student라는 애거리거트를 필드로써 참조 할 수있다.

public class School {
    private Student student;
}

이럴경우 ORM을 통해서 데이터를 함께 가져올 수 있는데 이럴 경우 단점이 있다.

1. 편한 탐색 오용
- 편하게 필드로써 애그리거트에서 다른 애그리거트를 접근할 수 있으므로 다른 애그리거트내에서 다른 애그리거트의 값을 수정할 수 있는 문제를 야기할 수 있다.
2. 성능에대한 문제
- 이는 사용에 따라서 lazy와 eager 둘 중의 하나로 선택해서 진행할 수 있다.
3. 확장이 어려움
- 만약 확장해서 student를 다른 도메인으로 빼고 싶을경우에 연관성이 깊어지다보면 분리하기 어려워서 확장이 여려워진다.

이에 대한 해결방법으로 School 내부에 Student라는 애그리거트를 포함하지 말고 student의 id인 studentId만을 포함하고 있는 것이다. 이럴경우 School에서 Student를 수정할 문제도 방지할 수 있고 lazy, eager를 고민할 필요도 dbms 확장도 자유롭게 할 수 있다.

 

애거리거트에 연관을 ID로 했을 시 상황


만약 School 내부에 Student가 List형태로 있다고 가정했을 때, 모든 학생정보를 가지고 오기 위해서 학생들 정보를 하나씩 다 가져와야한다.

이는 결국 School과 내부에 있는 학생들 수인 N개를 합쳐서 N + 1번 조회하게 된다고 해서 N+1문제라고 한다. 이를 속도가 엄청 느려지기 때문에 해결하기 위해서 조인을 사용해야 한다. 이런문제는 JPA에서 쿼리를 직접적으로 실행할 수 있는 기능등을 사용해서 해결하거나 이부분만 mybatis를 사용하는 등의 방법으로 해결할 수 있다.

 

출처 : DDD Start 도메인 주도 설계 구현과 핵심 개념 익히기 (출판 지앤선, 저자 최범균)

댓글()

Custom Validation 만들어서 추가하기

web/Spring|2018. 12. 24. 00:11

Spring에서 @NotBlank, @Email등 여러 템플릿에 맞게 Validation을 넣을 수 있다.

하지만 추가적으로 패스워드 규칙과 같이 별도 체크할 validator가 필요할 때 만들어서 사용해야 하는데 만들어서 지정해보는 작업을 해보자.


1. Controller

요청을 받을 DTO앞에 @Valid 어노테이션을 추가해야한다.

1
2
3
4
5
6
7
8
9
10
11
/**
 * 회원가입
 *
 * @param reqDto
 * @return
 * @throws Exception
 */
@RequestMapping("/join")
public ResponseEntity<?> join(@Valid UserDto reqDto) throws Exception {
    return ResponseEntity.ok(userService.insertUser(reqDto));
}
cs


2. Annotation 추가

Validation 사용을 위해서 필드에 @NotNull, @NotBlank와 같이 어노테이션을 붙혀줘야한다. 그래서 Custom Validation을 만들고 필드에 붙히기 위해서 어노테이션을 만들어줘야한다. 기본적으로 표현되는 메시지는 설정을 진행한 messageSource에서 가져오는데 가져오지 못하면 default로 설정한 메시지를 출력하게 할 수있다. 그리고 기존에 어노테이션 만들던 방법과 조금 다른 부분이 있는데 바로 @Constraint 필드이다. 여기서 지정하는 설정은 어떤 검증 클래스를 사용해서 필드의 값을 검증할 건지 지정해준다. 이 방법을 위해서는 ConstraintValidator 인터페이스를 구현한 클래스를 지정해줘야한다. 아래에서 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.wedul.common.annotation;
 
import com.wedul.common.validation.PasswordValidator;
 
import javax.validation.Constraint;
import java.lang.annotation.*;
 
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {PasswordValidator.class})
public @interface PasswordCheck {
 
  String message() default "";
Class<?>[] groups() default {};
 Class<? extends Payload>[] payload() default {};
cs
}
 
cs


3. Validator 추가

ConstraintValidator 인터페이스를 구현해주면서 지정해주는 제네릭 값 첫 번째에는 2번에서 만든 애노테이션 객체가 들어가고 두 번째 값에는 이 어노테이션 값이 붙어서 Constraint 작업을 진행할 필드의 데이터 유형을 넣는다. (패스워드라면 String) 만약 특정하기 어려운 어노테이션인경우 Object를 붙여서 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.wedul.common.validation;
 
import com.wedul.common.annotation.PasswordCheck;
import org.apache.commons.lang.StringUtils;
 
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
 
/**
 * 패스워드 validation
 *
 * @author wedul
 * @since 2018-12-23
 **/
public class PasswordValidator implements ConstraintValidator<PasswordCheck, String> {
 
  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    // 6자리이상 대문자 포함
    return StringUtils.isNotBlank(value) && value.length() >= 6 && value.chars().boxed().filter(data -> Character.isUpperCase(data)).findAny().isPresent();
  }
}
 
cs


4. DTO 적용

적용하고자하는 곳에 추가한 필드에 넣어보자!

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package com.wedul.wedulpos.user.dto;
 
import com.wedul.common.annotation.PasswordCheck;
import com.wedul.common.dto.CommonDto;
import com.wedul.common.util.HashUtil;
import lombok.*;
import org.apache.ibatis.type.Alias;
 
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
 
/**
 * User정보 Dto 
 * 
 * @author wedul
 * @date 2017. 11. 4.
 * @name UserDto
 */
@Alias("UserDto")
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper=false)
@Entity
@Table(name = "user")
public class UserDto extends CommonDto implements Serializable {
 
    @Id
    @GeneratedValue
    private int userId;
 
    @Column(nullable = false)
    private String nickname;
 
    @Column(nullable = false)
    @NotBlank(message = "user.login.message.mustemail")
    @Email(message = "user.login.message.validation_email")
    private String email;
 
    @Column(nullable = true)
    @PasswordCheck(message = "user.join.message.password")
    private String password = "";
 
    @Column(nullable = true)
    private String snsId;
 
    @Column(nullable = false)
    private boolean isAdmin = false;
    
    public UserDto(String email) {
        this.email = email;
    }
    
    public UserDto(String email, String password) {
        this.email = email;
        this.password = password;
    }
    
    public UserDto(String email, String password, boolean isAdmin) {
        this.email = email;
        this.password = password;
        this.isAdmin = isAdmin;
    }
    
    public UserDto(String email, String password, String nickname, boolean isAdmin) {
        this.email = email;
        this.password = password;
        this.nickname = nickname;
        this.isAdmin = isAdmin;
    }
    
    public String getEcPassword() {
        return HashUtil.sha256(this.password);
    }
 
}
 
cs


validation 오류 발생시 상황에 맞는 문구가 나올 수 있도록 별도의 설정을 해줘야 하는데 그 부분은 다음시간에 정리해서 올려보자.


우선 Custom Validation을 만든부분만 정리하자.


댓글()

연관관계 매핑 (다대일 - 양방향)

web/JPA|2018. 10. 26. 00:18

바로 앞에서 다대일 관계에서 단반향으로써 학생이 반을 접근하는 방식으로 진행했으나 이번에는 반에서 학생들을 접근하는 방식을 사용해보자.


그렇게 되면 학생 -> 반에서 반 -> 학생이 추가되어 결국 반 <-> 학생 이런 양방향 연관관계가 형성된다.

 

하나의 반에는 여러 학생이 포함되어 있다. 그렇기 때문에 반 클래스에 List<Student> 객체를 추가한다.


1

2

  @OneToMany(mappedBy = "classes")

  private List<Student> students;

cs

 


@OneToMany(mappedBy = "classes")

- 일대다 매핑을 정보를 추가하고 학생쪽에서 사용되는 반 필드명을 mappedBy에 값으로 추가해준다.


조회

반에 포함되어 있는 학생들을 조회한다.

1

2

3

4

5

6

7

8

9

10

@Override

@Transactional

public void selectClasses() {

  Classes classes1_1 = entityManager.find(Classes.class"1-1");

  List<Student> students = classes1_1.getStudents();

 

  for (Student student : students) {

    print(student);

  }

}

Colored by Color Scripter

cs

 


연관관계 주인 지정


테이블은 외래키 하나로 테이블의 연관관계를 관리 있다. 예를 들면 이름이 외래키라고 했을 학생 테이블에서 외래키 이름을 추가할 수도 있고, 테이블에서 반 이름을 관리할 있다. 하지만 엔티티에서는 외래키를 관리(추가, 수정, 삭제) 있는 것은 개의 엔티티의 연관관계의 주인이 되는 엔티티만이 가능하다. 나머지 다른 엔티티는 조회만 가능하다.

 

예를 들어 저번 시간에 공부 했었던 Student 엔티티 클래스는 Classes 외래키의 주인으로써 외래키를 추가, 수정, 삭제 있다. Classes 엔티티 클래스는 외래키의 주인이 아니므로 조회만 가능하다.

 

@ManyToOne 설정이 있는 곳이 무조건 주인이다. 그리고 양방향 설정된 엔티티에서 조회가 가능하도록 하기 위해서 다른 엔티티에 @OneToMany(mappedby ="classes") 지정해 주면 주인 설정이 끝난다.

 

그럼 진짜 주인이 아닌 엔티티 Classes에서는 외래키 관리가 안되는지 확인해보자.

 

1

2

3

4

5

6

7

@Override

@Transactional

public void saveClasses() {

  Student wedul = entityManager.find(Student.class"1-1-01");

  Classes classes2_1 = new Classes("2-2""2학년2", Arrays.asList(wedul));

  entityManager.persist(classes2_1);

}

Colored by Color Scripter

cs

-> 처음 생각대로라면 id : "2-2", name : "2학년2" 반이 classes테이블에 추가되고 학생 테이블에 wedul 학생의 반이 2-2 같지만 그렇지 않다. 왜냐하면 Classes 엔티티는 주인이 아니기 때문이다. 그래서 테이블에 값만 추가된다.



주의사항

 만약 단방향 그러니까 Student 엔티티에만 @ManyToOne 해줄 경우 Classes 엔티티를 통해 반에 등록된 학생을 조회 하려 값을 받게 된다. 왜냐하면 연관관계가 맺어지지 않았기 때문이다. 그래서 무조건 이럴 경우 양방향 연관관계를 맺어 주는것이 좋다.(@OneToMany)

 

아래 코드를 보면 wedul학생에 classes2_1 반을 추가해줬지만 classes2_1에서 학생을 조회하면 wedul 학생이 없다.

1

2

3

4

5

6

Student wedul = entityManager.find(Student.class, "1-1-01");

Classes classes2_1 = new Classes("2-2", "2학년2", Collections.emptyList());

wedul.setClasses(classes2_1);

    

// 반영 되어 있지 않아서 wedul 출력되지 않음

classes2_1.getStudents();

Colored by Color Scripter


그래서 이런 문제로 버그가 발생할 있기 때문에 좋은 방법으로 setClasses() 메소드를 다음과 같이 변경해주면 좋다.

1

2

3

4

public void setClasses(Classes classes) {

  this.classes = classes;

  classes.getStudents().add(this);

}

Colored by Color Scripter

그렇지만 이렇게만 해주고 나면 또다른 버그가 발생할 있다. 다음과 같은 상황을 가정해보자.

1

2

3

4

5

6

7

8

9

Student wedul = entityManager.find(Student.class, "1-1-01");

Classes classes2_1 = new Classes("2-2", "2학년2", Collections.emptyList());

wedul.setClasses(classes2_1);

 

classes2_1.getStudents();

 

// 학생의 반을 다른 반으로 변경할 경우 기존의 반에 들어있는 getStudents List안에서 학생을 지워줘야한다.

Classes classes3_1 = new Classes("3-1", "3학년1", Collections.emptyList());

wedul.setClasses(classes3_1);

Colored by Color Scripter

상황에서는 위에 변경해주었던 방식대로 진행하면 기존에 반이었던 classes2_1에도 wedul이 있고 classes3_1에도 wedul 있는 문제가 발생한다. 그래서 다음과 같이 바꿔주면 해결된다. 

1

2

3

4

5

6

7

8

  public void setClasses(Classes classes) {

    // 먼저 지워준다.

    classes.getStudents().remove(this);

    

    // 그리고 반을 바꾸고 학생추가

    this.classes = classes;

    classes.getStudents().add(this);

  }

Colored by Color Scripter


단방향 매핑만으로도 테이블과 객체의 연관관계 매핑이 되었지만 양방향 매핑을 통해서 더욱 편리하게 객체의 탐색이 가능하게 있다. 하지만 위에 보았듯이 양방향 매핑에서는 주의해서 관리 해줘야 포인트가 많다.


댓글()

JPA 매핑 어노테이션 - DDL

web/JPA|2018. 10. 12. 00:29

JPA 매핑에 사용되는 어노테이션은 크게 유형에 따라 4가지로 나누어진다.

유형
어노테이션
객체와 테이블 매핑
@Entity, @Table
기본 키 매핑
@Id
필드와 컬럼 매핑
@Column
연관관계 매핑
@ManyToOne, @JoinColumn


@Entity

JPA를 사용해서 테이블과 매핑할 클래스에는 무조건 붙혀야하는 어노테이션이다. 속성으로 name을 지정할 수 있다. 안할 시 기본 클래스 이름으로 한다.

1
@Entity(name = "Member")
cs


- 기본 생성자가 필수로 있어야 한다. 
- final 클래스와 private 생성자는 할 수 없다.


@Table

엔티티와 매핑할 테이블을 지정한다. 생략하면 매핑하는 엔티티이름으로 대신한다.
-name, catalog, schema, uniqueConstraints(DDL 선언시 유니크 제약조건) 등을 같이 설정하여 만들 수 있다.

이렇게 설정된 클래스를 가지고 자동으로 테이블을 생성할 수 있다.

1
spring.jpa.hibernate.ddl-auto=update
cs

설정하는 값에 따라서 테이블 생성이 달라지는데 create는 기존 테이블을 삭제하고 새로 생성하고, create-drop는 애플리케이션이 종료될때 제거되고 update는 변경된 사항만 수정하고 validate는 매핑정보가 달라졌을때 경고를 주면서 애플리케이션 실행을 하지 않고 none는 아무런 동작을 하지 않는다.


DDL 생성 시 설정

@Colummn 매핑에는 nullable 속성과 length등의 속성을 지정할 수 있다

1
2
3
4
@Column(name = "NAME", length = 10, nullable = false)
private String userName;
 
// NAME varchar(10) not null 과 매핑
cs


@Table 어노테이션은 UniqueConstraint를 지정할 수 있다.

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
/**
 * springboottest
 *
 * @author wedul
 * @since 03/10/2018
 **/
@Entity(name = "Member")
@Data
@Table(name = "MEMBER", uniqueConstraints = {@UniqueConstraint(
        name = "NAME_AGE_UNIQUE", columnNames = {"NAME""AGE"}
)})
public class Member {
 
  @Id
  @Column(name = "ID")
  private String id;
 
  @Column(name = "NAME", length = 10, nullable = true)
  private String userName;
 
  // 매핑 정보가 없는 필드
  private int age;
 
}
 
// ALTER TABLE MEMBER ADD CONSTRAINT NAME_AGE_UNIQUE UNIQUE(NAME, AGE); 로 매핑
cs


기본키는 @Id를 사용하여 지정할 수 있고 AUTO_INCREMENT와 같은 기능을 @GeneratedValue를 사용하여 지정할 수 있다.

1
2
3
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long orderId;
cs


추가적인 속성으로 IDENTITY(기본키 생성을 데이터베이스가 함), SEQUENCE(데이터베이스 시퀀스를 사용해서 기본 키 할당), TABLE (키 생성 테이블 생성)이 있다. 각 속성이 모든 데이터베이스 업체에 적용되는건 아니고 적용되는 벤더만 사용된다. 만약 이 모든 설정을 하는게 번거롭다면 그냥 @GeneratedValue 어노테이션만 사용하면 된다. 왜냐하면 기본 설정이 GenerationType.AUTO인데 이는 벤더에 맞게 전략이 선택되어 사용된다.


레퍼런스 : 자바 ORM 표준 JPA 프로그래밍

댓글()