분류 전체보기 (356)

💻 Programming

springboot 2.4 업그레이드 시 gradle 버전 오류

An exception occurred applying plugin request [id: 'org.springframework.boot', version: '2.4.0']
> Failed to apply plugin [id 'org.springframework.boot']
   > Spring Boot plugin requires Gradle 5 (5.6.x only) or Gradle 6 (6.3 or later). The current version is Gradle 6.1.1

 

인텔리J에서 신규 그래들 프로젝트를 생성하고 스프링부트 최신버전인 2.4를 플러그인으로 추가했더니 위와같이 오류가 발생했다.

현재 gradle 6.1.1 버전을 사용중이고 스프링부트 플러그인을 사용하려면 6.3 이상의 버전이 필요하다는 거였다.

 

그래들은 최신버전이 현재 6.7.1 (그래들 공식사이트)이며 업그레이드는 아래 명령어를 실행하면 된다.

(주의: 우선 빌드시 에러가 발생하니 추가했던 내용을 주석처리 한뒤 실행한다. 또한 신규 프로젝트가 아닌 기존 프로젝트에서 그래들 버전을 업그레이드 할 시에는 공식문서를 충분히 읽어보고 진행할 것을 추천한다.)

> gradle wrapper --gradle-version 6.7.1

 

07:40:20: Executing tasks 'wrapper --gradle-version 6.7.1'...

> Task :wrapper

BUILD SUCCESSFUL in 344ms
1 actionable task: 1 executed
07:40:20: Tasks execution finished 'wrapper --gradle-version 6.7.1'.

이제 다시 build.gradle 파일에서 스프링부트 플러그인 주석을 해제하고 그래들 SYNC를 하면 아래처럼 6.7.1 버전을 다운로드하여 빌드에 성공한다.

Download https://services.gradle.org/distributions/gradle-6.7.1-bin.zip (102.84 MB)
Download https://services.gradle.org/distributions/gradle-6.7.1-bin.zip finished succeeded, took 12 s 837 ms
Starting Gradle Daemon...
Gradle Daemon started in 1 s 447 ms
> Task :prepareKotlinBuildScriptModel UP-TO-DATE

BUILD SUCCESSFUL in 1m 14s

 

 

 

 

 

참고문서: docs.gradle.org/current/userguide/upgrading_version_6.html

 

Upgrading your build from Gradle 6.x to the latest

This chapter provides the information you need to migrate your Gradle 6.x builds to the latest Gradle release. For migrating from Gradle 4.x or 5.x, see the older migration guide first. We recommend the following steps for all users: Try running gradle hel

docs.gradle.org

 

우선 화면 기획부터 시작했다.

퇴근 길 지하철에서 한시간 동안 갤노트에다가 끄적여봤다.

워낙 꼼꼼한 스타일이라 이것저것 디테일한 기능들을 다 적어넣고 싶었지만 그렇게 시작하면 힘들어서 중도포기하게 될 것 같아 계속 드는 생각들을 뿌리치고 간단하게만 끄적였다.

갤럭시노트의 S펜을 이용한 메모가 처음인지라 서투르게 작성했다 ㅎ

메인화면은 크게 상단부(헤더부분과 최상위메뉴)와 하단부(사이드메뉴 + 컨텐트)로 구성하였다.

컨텐트 영역은 좌측에 사이드메뉴가 있을수도 있고 없을수도 있으며 카드형 목록보가와 리스트형을 지원할 수 있도록 할 계획이다.

최상위 메뉴는 주인장 또는 블로그의 소개(Intro), 블로그(Blog), 그리고 분석/통계(Stats) 로 구성했다.
블로그메뉴의 하위에는 개발일지, 다이어리, 제품리뷰, 여행정보 메뉴가있고, 그리고 마지막으로 분석통계 메뉴에는 각종 통계자료를 공유할 생각이다.

그렇게 만들어본 화면은 아래와 같다.

 

이제 각 메뉴별 화면 기획 및 백엔드 개발을 시작하면 될 것 같다.

우선 데이터베이스는 AWS document DB에서 지원하는 mongo DB를 써볼 예정이다.

한 row에 대해서 총 16MB 까지 데이터 저장이 가능하니 이미지를 등록하는 게시글에 대해서도 하나의 row에 저장을 할 수 있을 것 같다. 

무료로 mongoDB 클라우드 서비스를 사용할 수도 있지만 이런저런 예기치 못한 제약사항이 생길 수도 있을 것 같아 커뮤니티 버전을 다운로드 받아서 사용할 예정이다.

백엔드는.....역시 제일 빨리 할 수 있는 스프링부트기반의 자바로 가야겠다.. 시간단축을 위해서...

 

💻 Programming/웹프로그래밍

[HTML] <fieldset> 태그

웹 상에서 설문지 페이지에 이용하기 좋은 fieldset 태그에 대해서 알아보겠습니다.

fieldset 태그는 form내에서 연관된 엘리먼트들을 그룹화할 때 사용합니다.

그리고 그렇게 그룹화된 엘리먼트들을 둘러싼 선을 그려줍니다.

아래처럼 말이죠

<fieldset> 태그를 활용한 설문지 예제

위 처럼 화면에 출력하려면 아래 코드를 이용하면 됩니다.

<form action="#">
  <fieldset>
      <legend>1. 좋아하는 색깔은?</legend>
      <input type="radio" id="blue" name="favorite-color"><label for="blue">파란색</label>
      <input type="radio" id="green" name="favorite-color"><label for="green">초록색</label>
      <input type="radio" id="red" name="favorite-color"><label for="red">빨간색</label>
    </fieldset>
    
    <p></p>
    
    <fieldset>
      <legend>2. 좋아하는 음식 종류는?</legend>
      <input type="radio" id="korean" name="food-type"><label for="korean">한식</label>
      <input type="radio" id="american" name="food-type"><label for="american">양식</label>
      <input type="radio" id="japanese" name="food-type"><label for="japanese">일식</label>
      <input type="radio" id="chinese" name="food-type"><label for="chinese">중식</label>
    </fieldset>
</form>

 

즉, legend 태그를 이용하여 타이틀을 넣어주고 input 이나 textarea와 같은 태그들을 이용해서 사용자의 입력을 받을 수 있도록 합니다.

위 코드에 하나의 필드셋을 더 추가하여 주관식 문항을 넣어보았습니다.

 

<fieldset> 태그를 활용한 설문지 예제

<form action="#">
    <fieldset>
      <legend>1. 좋아하는 색깔은?</legend>
      <input type="radio" id="blue" name="favorite-color"><label for="blue">파란색</label>
      <input type="radio" id="green" name="favorite-color"><label for="green">초록색</label>
      <input type="radio" id="red" name="favorite-color"><label for="red">빨간색</label>
    </fieldset>
    <p></p>
    <fieldset>
      <legend>2. 좋아하는 음식 종류는?</legend>
      <input type="radio" id="korean" name="food-type"><label for="korean">한식</label>
      <input type="radio" id="american" name="food-type"><label for="american">양식</label>
      <input type="radio" id="japanese" name="food-type"><label for="japanese">일식</label>
      <input type="radio" id="chinese" name="food-type"><label for="chinese">중식</label>
    </fieldset>
    <p></p>
    <fieldset>
      <legend>3. 탕수육은 어떻게 먹어야 맛있나요?</legend>
      <textarea placeholder="생각을 적어주세요."></textarea>
    </fieldset>
  </form>

 

fieldset 태그는 크롬, 파폭, 엣지, 오페라 등 대부분의 브라우저에서 지원하고 있으며 fieldset의 속성으로 disabled를 명시해주면 해당 필드셋 내의 엘리먼트들이 모두 비활성화 처리됩니다.

 

개인블로그 만들기 프로젝트하다가 알게된 새로운 태그라 기록용으로 포스팅해보았습니다.

 

 

참고문서: https://developer.mozilla.org/ko/docs/Web/HTML/Element/fieldset

블로그 웹앱 만들기 개인프로젝트

티스토리 블로그를 사용한지도 몇 년이 된 것 같다.

처음 시작은 구글 광고를 붙여서 광고수익을 얻을 생각으로 시작했었는데

이직을 하고 대규모 앱을 거의 혼자서 유지보수 및 신규개발을 하면서 너무 바빠서 글을 쓸 시간이 없었다.

그렇게 2년이 넘는 시간이 훌쩍 흘렀다.

이직한 회사에서 관리자 웹앱을 혼자 관리하게 되면서 평소 풀스택이 되고싶었던 꿈이 다시 살아나기 시작했고

몇 일 전부터 리액트 공부를 시작했다.

생활코딩의 리액트 동영상도 보고, 리덕스 강좌도 보고...

하지만 역시 보기만 하는건 큰 도움이 되지 않기에 직접 코딩을 해가면서 공부해야겠다는 생각이 들었고

어떤 프로젝트를 해볼까 하다가 개인 블로그를 직접 만들어야겠다는 생각이 들었다.

티스토리를 사용하면 다른 블로그들보다 커스터마이징할 수 있는 부분이 많아 좋긴하지만 그래도 제약사항이 있을 수 밖에 없다.

그리고 티스토리가 서비스 중지 선언을 해버리면? ㅠㅠ

그럼 작성한 글들도 모두 사라지게 될테니 말이다. 

설령 작성한 글들을 다운로드 받을 수 있게 해준다고 해도 포맷을 어떻게 제공하느냐에 따라 다른 블로그에 옮겨심기도 불편할 수 밖에 없다.

그래서 이번 기회에 개인 블로그를 직접 만들어서 도메인 연동도 하고 꾸준히 잘 가꾸어 나가야 겠다는 생각을 했다.

그리고 블로그를 완성하기 까지의 과정을 이곳에 남겨두기로 했다.

 

그렇게 백엔드 개발자의 리액트로 개인블로그 만들기는 시작이 되었다...

 

해야할 일은 많다. 서버는 어떻게 구성할 것이며 어떤 클라우드 서비스를 이용할 것인지 프로젝트 구성은 어떻게 할 것인지 등등...

하지만 이런것들을 상세하게 다 따져가면서 하기에는 시간이 오래걸리니 리액트로 화면구성하는 것에 일단 집중하기로 했다.

화면을 구성하는 것은 여러 사이트를 돌아다니면서 벤치마킹하면서 해야겠다

백엔드는 늘 해오던 자바와 스프링 부트를 이용할지...예전에 해봤던 노드를 다시 써볼지...파이썬을 써볼지 고민이 좀 되는데...

자바+스프링부트는 업으로 하고있는 스펙이다보니 빠른 시간내에 만들 수 있는 반면 리액트와 함께 사용하려면 프로젝트를 분리해서 관리해야할 것 같고 노드서버와 자바서버를 띄워야 완전체가 된다. 

노드를 쓰면 프론트와 백엔드 프로젝트를 굳이 분리할 필요가 없고 서버도 노드서버만 띄우면 되기 때문에 개인용으로 관리하기에는 더 편하긴 하지만, 익숙하지 않은 노드를 쓰자니 좀 부담이 되긴한다.

 

일단은 리액트를 이용해서 껍데기(프론트)부터 만들어놓고 고민해야겠다.

 

 

 

특별히 내가 뭐 잘못한거 없는 것 같은데 웹사이트를 서핑하다보면 꼭 나만 이상한 현상이 나타나기도 한다.

이번에도 처음보는 오류가 발생해서 짧게 기록으로 남겨본다.

크롬을 메인 브라우저로 자주 사용하고있는데 얼마 전부터 특정 사이트에만 접속하려고 할때 아래와 같은 오류가 떴다.

HTTP Bad Request - Header Field Too Long

와~ 이건 뭐지? 헤더 필드 길이가 ㄴ ~~~~ㅓ 무 길다고 ?? 난 뭐 한게 없는데? 난 그냥 사이트에 접속을 시도한것 뿐인데?

사파리를 이용해서 접속을 시도하니 정상적으로 접속이 된다.

뭐지?

역시 브라우저 문제였어.

크롬에서 쿠키와 임시인터넷 파일들을 삭제하고 재접속을 시도했다.

정.상.접.속.

브라우저에 캐싱된 뭔가가 header field와 연관되어있는건지 오류를 발생시킨 케이스였다.

💻 Programming

[MySQL] json 컬럼 업데이트 안되는 현상

mysql(AWS aurora aurora_version,2.07.1, innodb_version,5.7.12)에서 json 타입 컬럼을 지원하고 있고 json string을 저장해야할 일이 생겨 오랜만에 해당 타입으로 컬럼을 정의하여 사용했는데 컬럼의 내용이 업데이트가 안되는 현상을 확인했다. 문제가 생긴 데이터는 double 값을 포함하는 좌표데이터였다.

 

{"latitude": 24.4436779, "longitude": 116.35241670000043}

 

이런 데이터였는데 문제가 생긴 부분은 longitude 였다.

 

유닛테스트(java correto 11)에서 확인 시에 업데이트된 값이 제일 끝에 한자리가 바뀌어서 116.35241670000045 로 콘솔에 출력되었는데 jpa 업데이트 시 업데이트가 발생하지 않았다.

 

update일시 컬럼(timestamp)에 on update current_timestamp 설정이 되어있음에도 업데이트 일시가 변경되지 않았다.

 

쿼리를 아래와 같이 직접 날려도 마찬가지였다 -_-; 여기서 좀 멘붕;;

update location set coordinate = '{"latitude": 24.4436779, "longitude": 116.35241670000045}' where id=1;

jpa가 어떻게 쿼리를 날렸는지 확인해보니 직접 쿼리를 날린 것과 동일했다.

 

애초에 해당 데이터가 업데이트가 제대로 안되는 것을 확인했을 때 어차피 string으로 전달하기 때문에 double precision이 문제가 될거라는 생각은 없었으나, 구글링을 하면 할 수록 문제가 될만한건 double precision 밖에 없어보였다.

 

검색어를 좀 바꿔서 double precision 관련해서 찾아보니 double precision의 경우 16자리까지 ( . 제외하고 숫자만 셋을 때) 정확히 표한할 수 있다고 한다. 문제가 되었던 케이스는 총 17자리의 소수점이었고 따라서 마지막 자리수에 대한 정확도가 떨어지게 된 것이다.

 

해당 문제를 해결하려면 value의 타입을 double이 아닌 string으로 변경하여 저장할 필요가 있어보였으나, 좌표에 대한 자리수가 저렇게 긴게 맞는 것인지 확인해보니 좌표 데이터 저장시 8자리 이상은 노이즈값이라고 생각하고 저장할 필요가 없을것 같았다. -> 위키 참고

 

따라서 해당 이슈는 저장하는 쪽은 이슈가 없고 가져다 쓰는 쪽에서 잘 가져다 쓰면 될 문제였다.

 

오랜만에 야근했으나 또하나 알게된 것이 있어 기쁜 하루였다~

 

 

[참고내용]

stackoverflow.com/questions/49119871/mysql-json-stores-different-floating-point-value

 

 

💻 Programming

Apache Kafka 운영 팁

Kafka 운영시 유용한 명령어

토픽 목록 조회

bin/kafka-topics.sh --zookeeper localhost:2181 --list

 

토픽 상세 조회

bin/kafka-topics.sh --zookeeper localhost:2181 --describe --topic mytopic

 

토픽 삭제

bin/kafka-topics.sh --zookeeper localhost:2181 --delete --topic mytopic  (JRE 7 사용시 confluent-3.0.0 사용)

 

토픽 내 메시지 개수 조회 ???

bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic mytopic --time -1 --offsets 1 | awk -F ":" '{sum += $3} END {print sum}'

 

토픽 내 earliest 오프셋 조회

bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic mytopic --time -2

 

토픽 내 최신 오프셋 조회

bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic mytopic --time -1

 

콘솔 명령어로 메시지 컨숨하기

bin/kafka-console-consumer.sh --new-consumer --bootstrap-server localhost:9092 --topic mytopic --from-beginning

 

 

아파치 카프카 컨수머 멀티쓰레드로 돌리기 위한 팁

 

 

 

참고문서 : gist.github.com/ursuad/e5b8542024a15e4db601f34906b30bb5

1차원 배열 생성에 대한 내용을 아직 안보셨다면 보고 오세요~ >> 1차원 배열 생성(선언 및 초기화)

1차원 배열이 1열로 된 저장공간이었다면 2차원 배열은 matrix(행렬)을 생각하시면 됩니다.

 

2차원 배열 선언

자바에서 2차원 배열은 아래와 같이 선언할 수 있습니다.

    public static void main(String[] args) {
        int[][] array;  // O
        int [][]array;  // X
        int array[][];  // X
    }

지난 1차원 배열 생성 시와는 다르게 제일 위의 방법으로만 문법적 오류 없이 정상적으로 2차원 배열을 선언할 수 있습니다.

 

 

2차원 배열의 초기화

2차원 배열을 초기화 할 때는 아래와 같이 합니다.

    public static void main(String[] args) {

        int[][] array;  // 2차원 배열의 선언

        array = new int[1][5];	// 2차원 배열의 초기화
    }

 

2차원 배열의 선언과 초기화를 동시에 하는 방법

2차원 배열도 1차원 배열처럼 선언과 초기화를 동시에 할 수 있습니다.

    public static void main(String[] args) {
        
        int[][] array = new int[1][5];  // 2차원 배열의 선언과 초기화를 한 번에

    }

 

이렇게 하면 배열 내에는 0으로 기본값이 들어가게 됩니다.

 

1차원 배열과 마찬가지로 아래처럼 초기화도 가능합니다.

    public static void main(String[] args) {

        int[][] array = {{1,2,3}, {4,5,6}};

    }

이렇게 초기화를 한다는 것은 기본값을 하드코딩하여 정해주겠다는 것이겠죠.

 

자, 배열을 선언하고 공간을 만들어 주는 작업을 완료했습니다.

이제 배열에 저장된 내용을 출력하는 것을 한 번 보겠습니다.

 

2차원 배열 출력하기

1차원 배열을 출력할 때는 Arrays.toString() 메서드를 사용했었습니다.

하지만 2차원 배열을 출력할 때는 그 메서드를 사용할 수가 없습니다.

왜냐면 2차원 배열은 배열 안에 배열이 있는 형태이기 때문입니다.

그리고 배열은 기본적으로 Object 이고 Object의 toString이 호출되어 원하는 결과를 출력해주지 않습니다.

따라서 별도로 출력문을 구현해야합니다.

쉽게하려면 아래처럼 loop를 한번 돌리면서 출력을 할 수 있습니다.

    public static void main(String[] args) {

        int[][] array = new int[8][9];  // 구구단 결과값 저장을 위한 2차원 배열의 선언 및 초기화

        for (int i = 0; i < array.length; i++) {
            System.out.println(Arrays.toString(array[i]));
        }
    }

 

그럼 연습삼아 2차원 배열안에 구구단의 결과를 저장해보도록 해볼게요.

 

    public static void main(String[] args) {

        int[][] array = new int[8][9];  // 구구단 결과값 저장을 위한 2차원 배열의 선언 및 초기화

	// 배열에 구구단의 결과값을 저장
        for (int i = 2; i < 10; i++) {
            for (int j = 1; j < 10; j++) {
                array[i-2][j-1] = i * j;
            }
        }

	// 배열에 저장된 내용을 출력
        for (int i = 0; i < array.length; i++) {
            System.out.print((i + 2) + "단: ");
            System.out.print(Arrays.toString(array[i]));
            System.out.println();
        }
    }
    
    
    // 출력 결과
    2단: [2, 4, 6, 8, 10, 12, 14, 16, 18]
    3단: [3, 6, 9, 12, 15, 18, 21, 24, 27]
    4단: [4, 8, 12, 16, 20, 24, 28, 32, 36]
    5단: [5, 10, 15, 20, 25, 30, 35, 40, 45]
    6단: [6, 12, 18, 24, 30, 36, 42, 48, 54]
    7단: [7, 14, 21, 28, 35, 42, 49, 56, 63]
    8단: [8, 16, 24, 32, 40, 48, 56, 64, 72]
    9단: [9, 18, 27, 36, 45, 54, 63, 72, 81]

 

로직을 한번 보면 크게 구구단을 저장할 때와 저장된 내용을 출력하는 부분으로 구분했습니다. 그리고 for-loop문의 조건걸에 하드코딩한 숫자를 사용하기도 하고 array.length 를 사용하기도 했습니다. 가급적 array.length 의 사용을 권장하며 2차원 배열에서 array.length는 행이 몇 개가 존재하는지를 반환하고, array[x].length는 열의 개수를 반환합니다. 구구단은 보통 2단부터 9단까지이기 때문에 배열을 초기화할 때 사이즈를 8행, 9열로 정의를 하였고, array.length 는 8, array[x].length 는 9 가 됩니다.

 

 

이상으로 자바에서 2차원 배열을 생성하고 초기화하고 실제로 데이터를 저장한 뒤 출력하는 것 까지 살펴보았습니다.

질문있으시면 댓글 달아주세요~

 

도움이 되셨다면 공감~ 꾸~~~욱 눌러주세요 ^-^

감사합니다~

💻 Programming

정규 표현식 (Regular Expression)

안녕하세요, 케이치입니다.

오늘은 정규표현식이 무엇인지 그리고 문법은 어떻게 되며 어떻게 사용하는지에 대해서 알아보겠습니다.

 

정규표현식이란?

- 정규표현식(regular expression)이란 검색 패턴을 정의한 문자열이라고 정의할 수 있습니다. regex 또는 regexp로 줄여서 말하기도 하며 pattern이라고 하기도 합니다. 정규표현식은 보통 임의의 string 내에서 특정 패턴에 일치하는 문자나 문자열을 찾아내거나(find) 찾아서 변경(find and replace)할 때 매우 유용하게 사용됩니다. 참고로 정규표현식의 개념은 1950년대에 미국의 수학자 Stephen Cole Kleene에 의해서 시작되었다고 합니다. 

 

정규표현식의 기본문법

 

Boolean "or"

 | swim|swam 는 "swim" 또는 "swam" 글자와 매칭됩니다

 

Grouping

소괄호를 이용하여 그룹을 지정할 수 있습니다.

예를들어 sw(i|a)m 패턴은 바로 위 예제와 동일하게 "swim"과 "swam" 단어와 매칭됩니다.

 

수량(개수) 패턴

 ?  : 바로 앞에 있는 글자 또는 그룹이 0~1개 존재

 *  : 바로 앞에 있는 글자 또는 그룹이 0개 이상 존재

 +  : 바로 앞에 있는 글자 또는 그룹이 1개 이상 존재

{n} : 바로 앞에 있는 글자 또는 그룹이 정확히 n번 존재

{min,} : 바로 앞에 있는 글자 또는 그룹이 최소 min 개 존재

{min,max} : 바로 앞에 있는 글자 또는 그룹이 최소 min 이상 최대 max 이하 존재

 

와일드카드 (Wildcard)

 .  : 와일드카드 문자는 아무 캐릭터(any character)를 의미합니다. 그냥 어떤 글자이던 특수기호인지 알파벳인지 숫자인지에 관계없이 1개의 character를 의미합니다. 즉, a.b 패턴은 a와b 사이에 어떤 문자가 와도 매칭됩니다. "acb", "a3b", "aAb" 등등이 모두 매칭되죠. 이 와일드카드 문자와 수량을 나타내는 *를 함께 사용하여 a.*b 패턴으로 매칭을 시도하면 "a123b", "ab", "aTTb" 등의 문자열이 모두 매칭이 가능합니다. 즉, a와 b 사이에 0개 이상의 문자가 들어있는 문자열이 매칭이 됩니다.

 

이외 기본 패턴

 ^  : 문자열의 시작

 $  : 문자열의 끝

[ ] : 대괄호 내의 문자들 중 하나의 문자와 매칭

[^ ] : 대괄호 내의 문자들을 포함하지 않는 문자와 매칭

 

Expression Flags

g : global

i : case insensitive

m : multiline

s : single line

u : unicode

y : sticky

 

정규표현식 패턴 예제

정규표현식 패턴설명일치 문자열
^x- 소문자 x로 시작하는 문자열"xyz song"
a$
- 문자열 끝에 공백이나 줄바꿈 문자가 있을경우 매칭 X
"blah bla"
a.c- 소문자 a와 c 사이에 하나의 문자가 있는 문자열
"Javascript is easy"
a+- 소문자 a가 1번 이상 반복됨

"I am a boy"
a*- 소문자 a가 0번 이상 반복됨ba* -> "b", "ba", "baa"
a?- 소문자 a가 1번 존재하거나 존재하지 않는 케이스 
a|b- 소문자 a 또는 소문자 b 
(a)- 소문자 a를 그룹화 
(a)(b)- 그룹1에 소문자 a, 그룹 2에 소문자 b 매칭 
a{n}- 소문자 a가 n번 반복되는 문자열 
a{min,}- 소문자 a가 최소 min번 반복되는 문자열 
a{min,max}- 소문자 a가 최소 min번, 최대 max번 반복되는 문자열 
[ab]- 소문자 a 또는 b"I am a boy"
[^ab]- 소문자 a와 b를 제외한 다른 문자"cab"
[a-z]- 소문자 a부터 z사이의 문자중 하나 
[^a-y]- 소문자 a부터 y가 아닌 다른 문자"abcz"
\^특수문자 ^를 패턴내에 포함시킬 때 사용 
\ddigit (숫자) 
\D숫자가 아닌 문자 
\s공백문자 
\S공백문자가 아닌 문자 
\ttab 문자 
\vvertical tab 문자 
\w알파벳, 숫자, _ 문자 
\W(알파벳, 숫자, _ 문자)가 아닌 문자 

 

 

실제로 테스트를 해보기 위해서는 여러 온라인 사이트들이 존재하는데요, 저는 아래 사이트를 애용합니다.

테스트 문자열도 마음대로 입력해볼 수 있고 패턴을 입력하면 자동으로 매칭되는 문자들을 컬러링해줍니다.

기본적인 텍스트가 입력되어있어서 원하는대로 패턴을 입력해보고 기대하던 매칭이 이루어 지는지 바로바로 확인이 가능합니다

https://regexr.com/

RegExr: Learn, Build, & Test RegEx

RegExr is an online tool to learn, build, & test Regular Expressions (RegEx / RegExp).

regexr.com

 

또한, 정규식이 어떤 의미를 갖는지 도식화 해주는 사이트도 있는데요, 여기도 이용해볼만 합니다.

작성한 정규식이 정확히 어떤 의미를 갖는지 실제 패턴 매칭 테스트 만으로는 애매할 때 이용하면 좋습니다.

https://regexper.com/

 

Regexper

 

regexper.com

 

이상으로 정규표현식에 대해서 간략히 내용을 정리해보았습니다.

💻 Programming

Eclipse에 lombok 설치하기

How to install lombok plugin on Eclipse IDE

 

안녕하세요, 케이치입니다.

오늘은 이클립스에 롬복(lombok)을 설치하는 방법을 알려드리러 왔습니다.

lombok은 자바개발자들의 불필요한 메서드 생성을 대폭 줄여주는 매우 유용한 라이브러리 입니다.

getter와 setter를 지저분하게 수십 개 필드를 위해서 만들어줄 필요가 없게 해주기도 하고

logger 세팅도 알아서 척척 해주는 친절한 롬복씨죠. ㅎㅎ

 

그럼 이클립스에서 lombok을 사용하려면 어떻게 해야하는지 한번 알아보겠습니다.

⚠️Note: 이클립스는 최신버전인 2020-06 R 기준으로 설명하므로

구버전의 이클립스에서는 아래 설명이 정상적으로 동작하지 않을 수 있습니다.

 

  1. Download lombok
  2. 다운로드받은 lombok.jar 파일을 실행합니다.
  3. 아래처럼 창이 뜨면 우측 하단의 Install / Update 버튼을 클릭합니다.
  4. 이클립스를 재기동합니다.

이클립스 lombok 플러그인 설치 화면

자, 여기까지 완료하셨으면 이제 여러분의 이클립스에서 lombok 어노테이션을 사용할 준비가 완료된 것입니다.

 

이제 실제로 프로젝트에서 사용하시려면 dependency에 lombok을 추가한 다음 바로 사용하실 수 있습니다. 💯

 

실제로 lombok 플러그인을 어떻게 사용하는지 보고싶으시면 [Java] 30분완성 연락처 관리 프로그램 만들기 글을 참고하시면 됩니다.

 

그럼 오늘도 즐프하세요~

안녕하세요, 케이치입니다.

오늘은 연락처 관리 프로그램을 만드는 웹앱 프로젝트를 진행해보려고 합니다.

사용할 도구는 Eclipse 2020-06 R 이고요, 스프링부트부트스트랩, 그리고 데이터베이스 연동은 mampmybatis를 사용합니다.

우선 기본적인 도구들과 라이브러리들 그리고 플러그인들을 설치해야합니다.

개발환경은 다음과 같습니다.

 

  1. MacOS Catalina (10.15.6)
  2. Eclipse 2020-06 R (Download)
  3. Eclipse Spring Tool Suite 4 (설치하기 가이드)
  4. MAMP (Download) -> community 버전 다운로드 받으세요, Window사용자의 경우 WAMP를 설치하셔야 합니다.
  5. DBeaver (데이터베이스 무료 툴 Download) -> community 버전 다운로드 받으세요

 

자, 위 도구들을 모두 설치완료했다면 이제부터 시작해보겠습니다.

 

우선 결과물은 아래 동영상처럼 나올겁니다.

 

연락처 관리 웹앱

자, 그럼 어떻게 만드는지 본격적으로 알아보도록 하겠습니다.

진행은 아래 순서대로 합니다.

  1. 테이블 설계
  2. 백엔드 구현
  3. 프론트 구현

 

테이블 설계

우선 테이블 설계부터 하겠습니다. (MAMP설치 및 테이블 소유자 권한 등의 설정은 여기서 다루지 않습니다.)

테이블은 contact 라고 명명지어주고 컬럼은 아래처럼 두 개만 추가를 해줄 겁니다.

CREATE TABLE `contact` (
  `name` varchar(100) NOT NULL,
  `phone` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

 

 

DBeaver에서 조회한 contact 테이블 명세

정말 간단한 연락처 관리 프로그램이므로 이름과 연락처 정보만 담을 예정이고 개인정보 암호화 같은건 다루지 않습니다.

입력받은 정보 그대로 string으로 저장을 하도록 하겠습니다.

이렇게 테이블 설계가 완료되면 이제 백엔드에서 쿼리해올 수 있도록 구현을 들어가보겠습니다.

 

백엔드 개발

우선 새로운 프로젝트를 만들어야겠죠.

아래 순서대로 새로운 프로젝트를 만들어 줍니다.

Project Explorer > New > Project 선택

 

Wizards에서 spring으로 검색한 뒤 Spring Starter Prject 선택 (이게 안나온다면 STS설치하기부터 하고 오시면 됩니다.)

 

 

Name 항목은 프로젝트명을 입력하는 곳입니다.

여기에 contact-manager 라고 넣어주시고 Java Version은 8 이상으로 아무거나 쓰셔도 무방할겁니다.

여기서는 Java 11 버전을 사용하였습니다.

 

Next를 눌러 다음으로 넘어간 뒤 Available 검색창에서 spring web, lombok, mybatis, mysql를 검색하여 우측의 selected 항목에 세 개 항목이 들어가도록 체크하고 Finish 버튼을 클릭합니다. 저는 이미 사용한 적이 있어서 Frequently Used에 항목이 나타나서 체크만 해주었습니다. 처음 하시는 분들은 Available에서 검색하시면 됩니다.

 

우측 하단에 progress bar가 진행되는 것을 확인할 수 있습니다.

 

또는 Progress View에서 확인할 수도 있죠. Progress View가 안보인다면 이클립스 상단 메뉴에서 Window > Show View > Other > Progress로 검색하면 추가할 수 있습니다.

 

 

진행이 완료되었으면 Project Explorer에서 아래처럼 파일트리가 보일겁니다.

pom.xml 파일에는 아래와 같은 내용이 들어있죠.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>contact-manager</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>contact-manager</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>11</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.3</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

 

이제 자바로 코딩할 준비가 되었습니다.

 

우선 위에서 만든 contact 테이블의 내용을 조회해오는 API를 만들어 보도록 하겠습니다.

 

com.example.demo 패키지 하위에 domain 라는 이름의 패키지를 만들고 해당 패키지에 ContactInfo.java 파일을 생성해줍니다.

그리고 아래와 같은 내용을 넣어줍니다.

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ContactInfo {
    private String name;
    private String phone;
}

 

ContactInfo 클래스는 contact 테이블과 연동할 dto입니다.

 

com.example.demo 패키지 하위에 mapper 라는 이름의 패키지를 만들고 해당 패키지에 ContactInfoMapper.java 파일을 생성해줍니다. ContactInfoMapper는 클래스가 아닌 인터페이스로 생성해주고 아래와 같이 쿼리문을 작성해줍니다.

import com.example.demo.domain.ContactInfo;
import org.apache.ibatis.annotations.*;

import java.util.List;

@Mapper
public interface ContactInfoMapper {

    @Select("select * from contact")
    List<ContactInfo> selectAll();

    @Insert("insert into contact (name, phone) values (#{name}, #{phone})")
    int insert(ContactInfo contactInfo);

    @Delete("delete from contact where name = #{name}")
    int delete(String name);

    @Select("<script>" +
            "select * from contact " +
            "<where>" +
            "<if test=\"name != null and name.length > 0\">and name = #{name}</if>" +
            "<if test=\"phone != null and phone.length > 0\">and phone = #{phone}</if>" +
            "</where>" +
            "</script>")
    List<ContactInfo> selectBy(@Param("name") String name, @Param("phone") String phone);

    @Update("update contact " +
            "set phone = #{phone} " +
            "where name = #{name}")
    int update(String name, String phone);
}

 

이 ContactInfoMapper 인터페이스는 마이바티스와 연동이 되어 쿼리를 database로 전달하고 결과를 받아오는 역할을 합니다.

 

 

com.example.demo 패키지 하위에 service 라는 이름의 패키지를 만들고 해당 패키지에 ContactInfoService.java 파일을 생성해줍니다.

이 파일에는 아래와 같이 내용을 넣어주시면 됩니다.

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.domain.ContactInfo;
import com.example.demo.mapper.ContactInfoMapper;

@Service
public class ContactInfoService {

	@Autowired
	private ContactInfoMapper contactInfoMapper;

	public List<ContactInfo> getEveryContactInfo() {
		return contactInfoMapper.selectAll();
	}

	public int addContactInfo(ContactInfo contactInfo) {
		return contactInfoMapper.insert(contactInfo);
	}

	public int delContactInfo(String name) {
		return contactInfoMapper.delete(name);
	}

	public List<ContactInfo> getContactInfos(String name, String phone) {
		return contactInfoMapper.selectBy(name, phone);
	}

	public int updateContactInfo(String name, String phone) {
		return contactInfoMapper.update(name, phone);
	}
}

 

ContactInfoMapper의 메서드를 호출하는 역할만 하고 있지만 프로젝트가 복잡해지면 각종 비즈니스 로직이 들어가야할 클래스입니다.

조회한 뒤에 정렬을 한다든지, 필터링을 한다든지 하는 로직은 이 서비스 클래스에 넣어주시면 됩니다.

 

이제 controller쪽에서 사용할 응답 도메인 클래스를 하나 더 만들어 보겠습니다.

com.example.demo.domain 패키지에 Response.java 파일을 생성하고 아래와 같이 작성해줍니다.

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@JsonInclude(Include.NON_NULL)
public class Response<T> {

    private int code;
    private String message;
    private T data;

    public Response(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public Response(int code, String message, T data) {
        this(code, message);
        this.data = data;
    }
}

 

마지막으로 com.example.demo 패키지 하위에 controller 라는 이름의 패키지를 만들고 해당 패키지에 ContactInfoController.java 파일을 생성해서 아래 내용을 넣어주세요.

import com.example.demo.domain.ContactInfo;
import com.example.demo.domain.Response;
import com.example.demo.service.ContactInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
@RestController
@RequestMapping("/contact-info")
public class ContactInfoController {

	@Autowired
	private ContactInfoService contactInfoService;

	@GetMapping("/list") // --> localhost:8080/contact-info/list
	public List<ContactInfo> getAllContactInfo() {
		return contactInfoService.getEveryContactInfo();
	}

	@GetMapping("/get")
	public ResponseEntity<Response> getContactInfos(
			@RequestParam(required = false) String name,
			@RequestParam(required = false) String phone) {
		List<ContactInfo> result = contactInfoService.getContactInfos(name, phone);
		if (result.size() == 0) {
			return ResponseEntity.status(HttpStatus.OK).body(new Response(204, "데이터 없음"));
		}
		return ResponseEntity.ok(new Response(200, "조회 성공", result));
	}

	@PostMapping("/add")
	public ResponseEntity<Response> addNewContactInfo(@RequestBody ContactInfo contactInfo) {
		contactInfoService.addContactInfo(contactInfo);
		return ResponseEntity.ok(new Response(200, "등록 성공"));
	}

	@DeleteMapping("/del")
	public ResponseEntity<Response> delContactInfo(@RequestParam String name) {
		int result = contactInfoService.delContactInfo(name);
		if (result == 0) {
			return ResponseEntity.status(HttpStatus.OK).body(new Response(204, "데이터 없음"));
		}
		return ResponseEntity.ok(new Response(200, "삭제 성공"));
	}

	@PutMapping("/update")
	public ResponseEntity<Response> updateContactInfo(@RequestParam String name, @RequestParam String phone) {
		int result = contactInfoService.updateContactInfo(name, phone);
		if (result == 0) {
			return ResponseEntity.status(HttpStatus.OK).body(new Response(204, "데이터 없음"));
		}
		return ResponseEntity.ok(new Response(200, "수정 성공"));
	}

	@ExceptionHandler
	public ResponseEntity<Response> contactInfoErrorHandler(Exception e) {

		log.error("error!!, {}", e.getClass().getName(), e);

		if (e instanceof DuplicateKeyException) {
			return ResponseEntity.status(HttpStatus.CONFLICT).body(new Response(409, "이미 중복된 이름이 등록되어있습니다."));
		} else {
			return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new Response(500, "서버오류가 발생하였습니다."));
		}
	}
}

이 클래스가 프론트와 백엔드를 연결해주는 부분으로 API url을 정의하고 어떤 파라미터들을 전달받아야 하는지, 응답은 어떻게 주고 예외처리는 어떻게 하는지에 대한 내용이 담겨있죠. 

 

이제 백엔드 구현은 완료되었습니다. 아 참!! 하나 빼먹은게 있네요. database 접속 정보를 빼먹었습니다. 자, src/main/resources 하위의 application.properties 파일을 열어서 직접 생성하셨던 database 접속 정보를 아래처럼 넣어주세요.

spring.datasource.url=jdbc:mysql://localhost:8889/javastudy?useSSL=false&serverTimezone=UTC
spring.datasource.username=javastudy
spring.datasource.password=javastudy
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

접속 포트(8889)와 database명(javastudy) 그리고 username, password 정보를 생성하신 데이터베이스에 맞게 변경해주셔야 합니다.

 

여기까지 완료되었다면 Project Explorer에 아래와 같이 파일들이 존재해야 합니다.

이제 ContactManagerApplication을 Sppring Boot App으로 기동해서 API가 잘 동작하는지 확인을 해보도록 하겠습니다.

앱 기동에 성공하면 Console View에서 아래 로그를 확인 할 수 있습니다.

INFO 69998 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
INFO 69998 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
INFO 69998 --- [main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.37]
INFO 69998 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
INFO 69998 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1185 ms
INFO 69998 --- [main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
INFO 69998 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
INFO 69998 --- [main] c.e.demo.ContactManagerApplication       : Started ContactManagerApplication in 2.457 seconds (JVM running for 3.521)

 

이제 브라우저 창을 열고 http://localhost:8080/contact-info/get 을 주소창에 입력하고 호출해보겠습니다.

아래처럼 출력이 되면 정상적으로 잘 동작한 겁니다.

{"code":204,"message":"데이터 없음"}

 

여기서 혹시 에러가 난다면 위로 올라가서 다시 순서대로 따라해보세요.

그래도 안되신다면....댓글로 에러메시지를 남겨주시면 함께 고민해보도록 하겠습니다.

 

자, 이제 프론트 개발로 넘어갑니다.

 

프론트 개발

프론트 영역 개발을 위해서 pom.xml에 두 개의 dependency를 추가해주겠습니다.

<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>4.5.0</version>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

thymeleaf와 부트스트랩입니다.

 

그리고 controller 패키지 내에 ViewController.java 파일을 만들어서 아래와 같이 짧은 코드를 작성해주겠습니다.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ViewController {

    @GetMapping("/contact-info")
    public String contactInfo() {
        return "contact-info";	// contact-info.html을 화면에 보여주는 역할
    }

}

 

그리고 src/main/resources 하위의 templates 디렉토리에 contact-info.html 파일을 하나 생성하고 아래 내용을 넣어주세요.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">

    <!-- JS link -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="/webjars/bootstrap/4.5.0/js/bootstrap.min.js"></script>
    <script src="script/contact-info.js"></script>

    <!-- CSS link -->
    <link rel="stylesheet"
          href="/webjars/bootstrap/4.5.0/css/bootstrap.min.css"/>
    <link rel="stylesheet" href="style/contact-info.css"/>

</head>
<body>

<div id="content">
    <h1>연락처 관리</h1>
    <p>
        <label>이름:</label>
        <input id="name" type="text" placeholder="이름" pattern=""/>
        
        <label>연락처:</label>
        <input id="phone" type="text" placeholder="010-1234-5678"/>
        <button type="button" class="btn btn-primary btn-lg" onclick="addContact()">등록</button>
        <button type="button" class="btn btn-danger btn-lg" onclick="delContact()">삭제</button>
    </p>

    <div>
        <button type="button" class="btn btn-info btn-lg" onclick="getContacts()">연락처 목록 조회</button>
        <table>
            <thead>
	            <tr>
	                <th class="name">Name</th>
	                <th class="phone">Phone</th>
	            </tr>
            </thead>
            <tbody id="contact-info-table"></tbody>
        </table>
    </div>
</div>


<!-- Update Modal -->
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel"
     aria-hidden="true">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="updateModalLabel">연락처 수정</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>
            <div class="modal-body">

            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                <button type="button" class="btn btn-primary" onclick="updateContactInfo()">Save changes</button>
            </div>
        </div>
    </div>
</div>

</body>
</html>

 

그리고 src/main/resources 하위의 static 디렉토리 하위에 style, script 두 개의 디렉토리를 생성해줍니다.

style 디렉토리에는 contact-info.css 파일을 넣을거고, script 디렉토리에는 contact-info.js 파일을 넣을 겁니다.

각 파일의 내용은 다음과 같습니다.

 

src/main/resources/static/style/contact-info.css

body {background-color: beige;}
th {text-align:center}
.name {width:100px;}
.phone {width:200px;}
#content {margin:100px;}
#contact-info-table tr:hover {background-color: cornflowerblue;}

 

src/main/resources/static/script/contact-info.js

$( document ).ready(function() {
    getContacts();
});

function addContact() {
    let name = $('#name').val();
    if (!name) {
        alert('"name" is required!');
        return;
    }
    if (!validateName(name)) {
        alert('"name" is invalid! 한글만 입력가능합니다.');
        return;
    }
    let phone = $('#phone').val();
    if (!validatePhone(phone)) {
        alert('"phone number" is invalid!\nvalid format: 000-0000-0000');
        return;
    }

    let contactInfo = new Object();
    contactInfo.name = name;
    contactInfo.phone = phone;

    $.ajax({
        url: 'contact-info/add',
        dataType: 'json',
        type: 'post',
        contentType: 'application/json',
        data: JSON.stringify(contactInfo)
    }).done(function(response) {
        alert(response.message);
        console.log(response);
        $('#name').val('');
        $('#phone').val('');
        getContacts();
    }).fail(function(jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
        console.log(jqXHR.responseText);
    });
}

function validateName(name) {
    return /^[가-힣]+$/.test(name);
// /^[A-Za-z]+$/
}
function validatePhone(phone){
    return /^\d{2,3}-\d{3,4}-\d{4}$/.test(phone);
}

function delContact() {

    let name = $('#name').val();
    if (!name) {
        alert('"name" is required!');
        return;
    }
    $.ajax({
        url: 'contact-info/del?name=' + name,
        dataType: 'json',
        type: 'delete',
        contentType: 'application/json'
    }).done(function(response) {
        alert(response.message);
        console.log(response);
        $('#name').val('');
        $('#phone').val('');
        getContacts();
    }).fail(function(jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
        console.log(jqXHR.responseText);
    });
}

function getContacts() {
    let name = $('#name').val();
    let phone = $('#phone').val();

    $.ajax({
        url: 'contact-info/get' + generateQueryParams(name,phone),
        dataType: 'json',
        type: 'get',
        contentType: 'application/json'
    }).done(function(response) {
        printContactInfos(response.data);
    }).fail(function(jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
        console.log(jqXHR.responseText);
    });
}

function generateQueryParams(name, phone) {
    let params = '';
    if (name || phone) {
        params = '?';
        if (name) {
            params += 'name=' + name;
        }
        if (phone) {
           if (params.length > 1) { params += '&';}
            params += 'phone=' + phone;
        }
    }
    return params;
}

function printContactInfos(contactInfos) {
    let rows = '';
    if (contactInfos) {
        contactInfos.forEach(function (item, index) {
            rows += '<tr onclick="popsUpUpdateModal(this)" ><td class="name">' + item.name
            + '</td><td class="phone">' + item.phone + '</td></tr>';
        });
    } else {
        alert('조회된 데이터가 없습니다.');
    }
    $('#contact-info-table').html(rows);
}
function popsUpUpdateModal(row) {
    let name = $(row).find('.name').text();
    let phone = $(row).find('.phone').text();
    let modalBody = '<input type="text" class="name" value="' + name + '" disabled>'
        + '<input type="text" class="phone" value="' + phone + '">';
    $('#updateModal .modal-body').html(modalBody);
    $('#updateModal').modal('show');
}
function updateContactInfo() {
    let name = $('#updateModal .modal-body .name').val();
    let phone = $('#updateModal .modal-body .phone').val();

    if (!validateName(name)) {
        alert('"name" is invalid! 한글만 입력가능합니다.');
        return;
    }
    if (!validatePhone(phone)) {
        alert('"phone number" is invalid!\nvalid format: 000-0000-0000');
        return;
    }

    $.ajax({
        url: 'contact-info/update' + generateQueryParams(name,phone),
        dataType: 'json',
        type: 'put',
        contentType: 'application/json'
    }).done(function(response) {
        alert(response.message);
        console.log(response);
        getContacts();
        $('#updateModal').modal('hide');
    }).fail(function(jqXHR, textStatus, errorThrown) {
        alert(jqXHR.responseText);
        console.log(jqXHR.responseText);
    });
}

 

각 파일들 내에서 하는 역할이 뭔지 잘 모르겠다는 분들은 댓글 달아주시면 아는 한도 내에서 답변 달아드릴게요. 

포스팅이 너무 길어져서 일일이 설명을 다 추가할 수가 없음을 양해바랍니다.

 

자, 이렇게 총 4 개의 파일을 추가하는 것만으로 프론트 개발이 완료되었습니다.

이제 다시 애플리케이션을 재기동한 뒤 화면으로 접속해서 기능들을 사용해 보겠습니다.

이번에는 localhost:8080/contact-info 주소로 접속을 하시면 됩니다. 앞에서 사용했던 /get 주소는 API를 직접 호출하는 것이니 빼고 접속해주시면 아래처럼 화면이 뜰겁니다.

연락처 관리 웹앱 완성

이 웹앱에서는 등록시에 이름을 정상적인 한글을 입력해줘야 하고 연락처의 경우 -를 포함하여 자리수를 검증하도록 되어있습니다.

검증은 contact-info.js에서 validateName(), validatePhone() 함수에서 처리하고 있으니 확인해보시기 바랍니다.

 

오류발생시에는 화면에 오류발생했다고 경고팝업창이 뜨도록 되어있으며, 서버로그를 통해 확인이 가능하도록 되어있습니다.

또한 연락처 수정은 제일 처음에 공유해드린 동영상을 따라해주시면 됩니다.

 

여기까지 30분만에 연락처 관리 웹앱 만들기 포스팅을 마치도록 하겠습니다.

 

궁금하신 사항이나 잘 안되시는 분들은 댓글 달아주시면 확인해서 답변드리도록 하겠습니다.

 

오늘도 즐프하세요~ 

 

 

💻 Programming

Eclipse Version M1 M2 M3 R RC SR Difference

안녕하세요, 케이치입니다.

오늘은 이클립스의 버전에 대한 정보를 들고왔습니다.

이클리스는 자바 개발자들이 무료툴로 가장많이 사용하는 툴인데요

지속적인 업데이트가 이루어지면서

패키지 버전도 여러개로 구분을 해서 개발자가 사용해 볼 수 있도록 바뀌었습니다.

 

저도 너무 오랜만에 이클립스를 최신버전으로 설치하려고 다운로드하러 들어갔다가

다양한 버전들을 보고 이게 뭐지?? 이랬거든요.

그래서 각 패키지 버전이 어떤 의미를 가지고 있는건지 확인을 해보았습니다.

 

우선 이클립스 다운로드 사이트로 가보시면 아래처럼 패키지 종류가 여러가지가 있습니다.

Eclipse Release Versions for Packaging Project Releases

Eclipse의 패키징 프로젝트 배포버전의 목록을 보여주는 건데요

목록의 아래쪽을 보시면 아시겠지만 예전에는 이클립스의 버전을 이름으로 구분을 해서 사용해왔었습니다. 

그러다가 년도와 월에 따라 여러 버전의 패키지를 다운받을 수 있도록 제공을 하기 시작했습니다.

년도와 월로 되어있는 링크를 클릭하면 아래처럼 또 여러 개의 버전이 있는 것을 확인할 수 있습니다.

Eclipse 2020-06 package versions

2020-06 링크를 따라 들어가보면 패키지 종류가 다섯 가지가 있는 것을 확인할 수 있는데요

각각 이 패키지들이 어떤 패키지인지를 알려주는 이니셜이 붙어있습니다.

R, RC1, M3, M2, M1 이라는 이니셜은 각각 다음의 의미를 가지고 있습니다.

 

R : release 버전 (안정화 버전)

RC1 : release candidate 1 (안정화 버전 후보 1) -> 공식적인 안정화가 되기 전 버전

M1, M2, M3 : milestone 1, 2, 3 -> 개발이 진행중인 마일스톤 버전으로 아직 테스트 중인 버전입니다.

 

이외에도 오래된 이클립스 버전의 경우 SR 버전도 존재하는데요

이건 service release 버전이라고 해서 일종의 서비스팩같은 개념이라고 합니다.

 

 

Eclipse + SpringBoot + JSP 개발환경 세팅하기 2탄입니다.

오늘은 backend 보다는 frontend 작업을 위한 Bootstrap 추가를 webjar를 이용할 수 있도록 설정해보도록 할 예정입니다.

저는 backend 개발자다보니 front쪽은 자세히 알려드리기는 힘들지만 빠르게 서치해서 사용해본 경험상 webjar는 maven 의존성 관리를 통해 라이브러리를 다운받아 사용할 수 있어 매우 쉽게 라이브러리 추가가 가능합니다. org.webjars에는 bootstrap, jquery, font-awesome, swagger-ui 뿐만 아니라 angularJS, momentJS, d3js 등 매우 많은 프론트 개발을 위한 라이브러리들을 제공하고 있습니다. 매번 다운로드받아서 프로젝트에 추가할 필요 없이 백엔드 라이브러리들을 관리하듯이 메이븐 의존성으로 쉽게 관리할 수 있도록 도와주는 역할을 한다고 보시면 됩니다.

우선 지난시간에 했던 Eclipse + SpringBoot + JSP 개발환경 세팅하기 1탄 을 완성하셨던 분들을 기준으로 설명을 할 예정이니 해당 포스팅을 안보신 분들은 한번 쭈욱~ 훑어보시고 다시 오시기 바랍니다.

webjar 설정을 위해서 추가할 파일은 딱 하나입니다. org.springframework.web.servlet.config.annotation.WebMvcConfigurer를 구현하는 WebMVCConfig 클래스를 하나 생성하고 다음과 같이 addResourceHandlers 메서드를 오버라이드하여 작성해줍니다.

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
	    if (!registry.hasMappingForPattern("/webjars/**")) {
	        registry.addResourceHandler("/webjars/**").addResourceLocations(
	                "classpath:/META-INF/resources/webjars/");
	    }
	    if (!registry.hasMappingForPattern("/**")) {
	        registry.addResourceHandler("/**").addResourceLocations(
	        		new String[] {"classpath:/static/script", "classpath:/static/style"});
	    }
    }

 

우선 webjars 경로를 등록해주는 것 때문에 이 클래스를 만들어 사용하는데 이때 @EnableWebMvc 어노테이션을 사용하게 됩니다. 그런데 여기서 주의할 점이 이 어노테이션을 사용하게되면 WebMvcAutoConfiguration 이 disable되면서 기존 설정들이 정상적으로 동작하지 않게 됩니다. 따라서 기존에 잘 읽어오던 static하위의 javascript파일들과 css파일들을 못찾아 404 오류가 발생하게 되죠. 그래서 addResourceHandlers에 이 두 파일들이 존재하는 디렉토리를 classpath경로로 추가해줘야 합니다.

또한, application.properties 파일에 작성하였던 아래 두 라인은 더이상 사용하지 않고 방금 추가한 WebMVCConfig 클래스에 별도로 ViewResolver를 등록하여 사용해야합니다.

	@Bean
	public ViewResolver getViewResolver(){
	    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
	    resolver.setPrefix("/WEB-INF/views/");
	    resolver.setSuffix(".jsp");
	    resolver.setViewClass(JstlView.class);
	    return resolver;
	}

 

아래는 완성된 WebMVCConfig 클래스의 코드입니다.

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@Configuration
@EnableWebMvc
public class WebMVCConfig implements WebMvcConfigurer {

	@Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
		if (!registry.hasMappingForPattern("/webjars/**")) {
	        registry.addResourceHandler("/webjars/**").addResourceLocations(
	                "classpath:/META-INF/resources/webjars/");
	    }
	    if (!registry.hasMappingForPattern("/**")) {
	        registry.addResourceHandler("/**").addResourceLocations(
	        		new String[] {"classpath:/static/script", "classpath:/static/style"});
	    }
    }
	
	@Bean
	public ViewResolver getViewResolver(){
	    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
	    resolver.setPrefix("/WEB-INF/views/");
	    resolver.setSuffix(".jsp");
	    resolver.setViewClass(JstlView.class);
	    return resolver;
	}
}

 

자, 모든 준비는 완료가 되었습니다. 이제 pom.xml에 다음과 같이 부트스트랩 의존성을 추가해줍니다.

		<!-- Bootstrap CSS -->
		<dependency>
			<groupId>org.webjars</groupId>
			<artifactId>bootstrap</artifactId>
			<version>4.5.0</version>
		</dependency>

 

그리고 test.jsp 파일을 열어 아래와 같이 부트스트랩을 링크걸어줍니다.

<link rel="stylesheet" href="/webjars/bootstrap/4.5.0/css/bootstrap.min.css" />

 

만약 부트스트랩이 아닌 jQuery와 같은 javascript 라이브러리를 사용하고자 할 경우 의존성 추가는 동일하게, jsp파일에 링크를 걸어줄 때에는 아래와 같은 형태로 해주시면 됩니다.

<script src="/webjars/jqeury/3.5.1/js/jqeury.min.js"></script>

 

자, 여기까지 완료되면 모든 준비는 끝난것입니다. 이제 부트스트랩에서 제공하는 기능을 마음껏 사용하시면 됩니다.

저는 지난 시간에 최종결과물에서 버튼을 하나 추가해보는 정도로 이번 포스팅을 마치겠습니다.

test.jsp의 body에 다음 한 줄을 추가했습니다.

<button type="button" class="btn btn-danger btn-lg">버튼</button>

 

그리고 결과를 확인해보면 아래와 같이 빨간색 버튼이 추가된 것을 확인할 수 있습니다.

 

이상으로 Eclipse + SpringBoot + JSP 개발환경 세팅하기 2탄 포스팅을 마칩니다.

오늘도 즐겁고 행복한 코딩을 하는 케이치였습니다~

MacOS Catalina 업데이트 후 발생한 Invalid active developer path 오류 해결방법

 

MacOS를 Catalina로 업데이트 하고나니 아래와 같은 오류가 발생하였다.

 

xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), 
missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun

 

그리고 IntelliJ에서 아래와 같이 경고팝업을 띄워주었다.

 

 

뭐지? 이런 오류는 첨인데?? 일단 기존에 잘 되던것이라 설정만 바꾸면 되겠지 하고 Configure에 들어가보니 

 

 

path 관련해서 설정하는 것은 제일 상단에 있는 Git 실행파일 위치를 선택해주는 곳인데..이미 자동으로 찾아서 설정이 되어있다. 하지만 Test버튼을 누르는 순간 

 

이렇게 똑같은 오류메시지를 출력해준다.

콘솔창에서 직접 git 명령어를 입력해보니 동일한 오류메시지가 출력된다.

이럴땐 구글신에게 물어보면 모든 해결책이 다 있다.

검색해본 결과 xcode-select --install 명령어를 통해 cli 툴을 설치해주니 더이상 오류가 발생하지 않았다.

 

💻 Programming/Java

이클립스에 스프링툴즈(STS) 설치하기

이클립스 스프링툴즈 설치방법

이번에는 이클립스에서 스프링부트를 사용하기위한 스프링툴즈 설치방법을 알아볼게요.

이클립스가 설치완료되었다는 가정하에 아래 순서대로 진행하시면됩니다.

 

1. 이클립스 상단 메뉴에서 Help > Eclipse Marketplace... 선택합니다.

 

 

2. 검색창에 spring 으로 검색한 뒤 제일 상단에 Spring Tools (aka Spring Tools Suite) 가 보이면 install 버튼 클릭 (2020년 7월 현재 최신 버전은 4.7.0.RELEASE)

만약 4.7.0 버전이 보이지 않는다면 이클립스 버전이 오래된 건 아닌지 확인해보시고 최신 버전으로 이클립스를 다시 다운받아 설치하신 뒤 진행하시기 바랍니다.

 

3. required 항목만 남기고 나머지는 체크 해제합니다.

 

4. 라이센스 동의를 선택하고 Finish 버튼을 클릭합니다.

이상으로 이클립스에서 스프링부트를 쉽게 사용할 수 있도록 해주는 유용한 플러그인 스프링 툴즈 설치방법에 대한 포스팅을 마칩니다.

이클립스에서 스프링부트 기반으로 JSP개발하기

안녕하세요, 오늘은 스프링부트 기반으로 JSP개발을 할 수 있는 환경을 세팅하는 과정을 알려드리겠습니다.

최근에 대규모의 웹 개발은 백엔드와 프론트엔드를 구분해서 별도의 프로젝트로 구성하거나 모듈로 나누어 개발을 진행하고 있습니다.

어떻게 개발을 하던지 결국 백엔드와 프론트엔드를 완전히 분리하여 개발하게되죠. 하지만 그렇게까지 할 필요가 없는 작은 프로젝트들은 굳이 그렇게 나누어 개발을 할 필요가 없습니다. 그렇게 구분을 하는 것이 오히려 유지보수를 힘들게 하는 원인이 되기도 하죠.

오늘은 하나의 스프링부트 프로젝트를 만들고 JSP를 사용할 수 있도록 세팅하는 부분까지 알려드립니다.

개발에 필요한 준비물은 아래와 같습니다.

 

1. Eclipse (2020년 7월 기준 최신버전 다운로드)

Version: 2020-09 M1 (4.17.0 M1)

 

2. Spring Tools plugin (설치방법)

 

이렇게만 있으면 일단 준비는 완료입니다.

 

이제 새로운 프로젝트를 생성합니다. 현재 이클립스를 처음 설치하신 분이라면 패키지 탐색기(Project Explorer)에 아래와 같이 나오는데 여기서 밑에서 두 번째에 있는 Create a project...를 선택합니다. 만약 이미 프로젝트를 만들어 놓은게 있는 분들이라면 그냥 탐색기 창에서 우클릭해서 New > Project 를 선택하시면 됩니다.

아래와 같이 새 프로젝트 생성 마법사가 뜨면 spring 으로 검색을 해서 Spring Starter Project를 선택합니다.

 

이제 만들 프로젝트의 이름을 Name 항목에 적어줍니다. 그리고 Java Version 은 11로 선택해줍니다. (8로 해도 무방합니다)

 

Spring Boot의 버전을 선택할 수 있는데 이 부분은 그대로 놔두고 Available 검색창에서 web이라고 검색하여 Spring Web을 선택해줍니다. (이외에도 lombok이나 데이터 베이스 드라이버, MyBatis 등 유용한 기능들을 선택하여 사전설치가 가능합니다만 여기서는 선택하지 않습니다.)

 

Finish 버튼을 누르면 아래와 같은 구조를 갖는 프로젝트를 생성해줍니다.

이제 MyDemoApplication.java 파일을 열어 아래와 같이 수정해줍니다.

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class MyDemoApplication extends SpringBootServletInitializer {

	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
		return application.sources(MyDemoApplication.class);
	}

	public static void main(String[] args) {
		SpringApplication.run(MyDemoApplication.class, args);
	}

}

SpringBootServletInitializer를 상속하고 configure 메서드를 오버라이드하였습니다.

 

그리고 pom.xml 파일에 아래 의존성을 추가해줍니다. jasper는 JSP 파일을 컴파일 해주는 기능을 합니다.

<!-- Need this to compile JSP -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>

 

이제 프로젝트명에서 우클릭하고 "src/main/webapp" 새 폴더를 하나 만듭니다. 이렇게 폴더를 만들면 아래와 같이 생성됩니다.

 

이제 다시 우클릭하여 build path 설정(Configure Buildpath...)화면으로 들어갑니다.

 

Add Folder... 를 클릭하고 webapp 디렉토리를 찾아 체크해줍니다.

OK 버튼을 누르고 Apply하면 프로젝트 구조가 아래와 같이 바뀐것을 확인할 수 있습니다.

이제 webapp 아래에 WEB-INF디렉토리를 생성하고 하위에 또 views 라는 디렉토리를 생성합니다. 그리고 test.jsp 파일을 만들어 아래와 같이 작성해줍니다.

<%@ page import="java.util.*" %>

<!DOCTYPE html>
<html>
<body>
	<h1>Test Page</h1>
	Today's date: <%= new Date() %>
</body>
</html>

 

application.properties 파일에는 다음과 같이 두 라인을 추가해줍니다.

spring.mvc.view.prefix = /WEB-INF/views/
spring.mvc.view.suffix = .jsp

 

자, 이제 마지막으로 해당 페이지와 연결할 API를 작성합니다. root package에서 controller 라는 패키지를 하나 만들고 그 아래에 DemoController.java를 아래와 같이 작성합니다.

package com.example.demo.controller;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;


@Controller
public class DemoController {
	
	@GetMapping("/test")
	public String login() {
		return "/test";
	}
}

 

여기까지 완료되었으면 이제 Run As > Java Application 또는 Run As > Spring Boot App 으로 기동시켜줍니다.

그리고 localhost:8080/test 에 접속하면 아래와 같이 jsp 페이지가 뜨는 것을 확인할 수 있습니다.

 

JSP 페이지에 javascript 및 css 파일 연동

이제 JSP 페이지에 javascript 파일 및 css 파일을 연결시켜보겠습니다.

javascript와 css 파일은 src/main/resources 하위의 static 폴더 안쪽에 몰아넣어주면 됩니다.

우선 static 폴더 하위에 script 폴더를 만들어 test.js파일을 생성하고 아래 내용을 작성합니다.

$(document).ready(function() { printCurrentDatetime(); });

function printCurrentDatetime() {
	let date = new Date();
	$('#currentTime').html(date);
	setTimeout(printCurrentDatetime, 1000);
}

 

그리고 static 폴더 하위에 style 폴더를 만들어 test.css 파일을 생성하고 아래와 같이 작성합니다.

@charset "UTF-8";

body {background-color:cornflowerblue;}

 

자, 더이상 파일을 만들 필요는 없습니다. 마지막으로 JSP 파일에 위에서 작성한 두 파일을 연결시켜주겠습니다.

test.jsp 파일을 열어 아래와 같이 수정해줍니다.

<%@ page import="java.util.*"%>

<!DOCTYPE html>
<html>
<head>

<!-- JS link -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="script/test.js"></script>

<!-- CSS link -->
<link href="style/test.css" rel="stylesheet">

</head>
<body>
	<h1>Test Page</h1>
	Today's date: <span id='currentTime'></span>
</body>
</html>

 

변경한 내용은 다음과 같습니다.

1. html의 body 안에 있던 스크립트 코드를 test.js로 옮기면서 refresh 기능을 추가

2. test.js파일에서 jquery를 사용하기 때문에 html페이지(jsp파일)에 jQuery 라이브러리 링크 추가

3. css 링크를 추가하고 body의 백그라운드 색상을 cornflowerblue 로 설정

 

여기까지 작업이 완료되면 최종적으로 아래와 같은 패키지 구조를 갖게됩니다.

SpringBoot + JSP 연동 최종 프로젝트 구조

 

여기까지 잘 따라오셨다면 실행시켰을 때 아래와 같이 현재시간이 계속 업데이트되는 파란 화면을 볼 수 있습니다. 😊

 

최종 완료 화면

 

이상으로 Eclipse에서 SpringBoot와 JSP를 연동하여 웹프로젝트를 구성하는 방법을 알아보았습니다.

 

자동으로 시작하는 mysql 데몬프로세스 강제종료

MySQL 8 버전(mysql-8.0.20-macos10.15)을 MacOS 10.14 (Mojave) 에 설치했다. 설치는 CE버전 dmg 파일을 다운받아서 했는데, 설치할 때 마지막에 인스톨 프로세스 종료 후 자동으로 MySQL을 시작할건지에 대한 체크를 해제하지 않고 finish했고, MAMP가 실행되지 않길래(팝업창이 뜨지 않길래) 애플리케이션에서 수동으로 실행을 시켰는데 nginx 서버만 뜨고 mysql 서버가 뜨지 않았다.

오랜만에 MAMP를 쓰려다보니 왜 안뜨는지 원인을 찾기가 힘들어서 정리해본다.

일단 mysql 서버의 로그를 확인해보려 했다. 로그 위치가 어디인지를 몰라서 그냥 find 명령어로 mysql*.log 파일을 찾으려했는데 안나온다 -_-; 그래서 구글링을 해봤더니 .err 확장자로 끝나는 파일이 MAMP 안에 log 디렉토리 안에 있었고(파일명은 mysql_error_log.err 이다) 해당 파일을 열어서 에러메시지를 확인했더니 해당 포트가 이미 사용중이란다. 오잉? 그럼 설치후에 실행이 됐다는 얘긴가??? 그래서 프로세스를 확인해보았다.

ps -ef|grep mysql

그랬더니 mysql 데몬이 이미 떠있다. 실행시킨 유저는 _mysql 이라고 되어있었고 이 프로세스를 끄고 다시 MAMP를 실행시켜서 mysql 서버를 띄우려고 kill -9 PID를 실행시켰는데 해당 프로세스는 꺼졌으나 다른 PID를 갖는 mysql데몬이 자동으로 실행이 되어있었다. ㅡㅡ;

뭐지?? 얘 좀비네? 다시 열심히 구글링을 해서 동일한 문제에 대해 설명을 잘 해놓은 미디엄 포스팅을 하나 찾았다. 바로 여기이다. 해당 사이트에서는 mysql-8.0.12-macos10.13 버전에 대한 설명이 있었고 내가 설치한 버전은 mysql-8.0.20-macos10.15 버전이었다. 아마 macOS 버전도 다르지 않을까 싶은데 아무튼 저 사이트의 설명대로 해도 해당 프로세스는 죽었다 살아나고 죽었다 살아나고를 반복했다.


그래서 좀 더 구글링을 하여 MySQL 공식 문서 중 MySQL launch daemon에 관한 문서를 보게 되었다. 해당 문서에는 아래와 같은 내용이 있었다.

2.4.3 Installing and Using the MySQL Launch Daemon
macOS uses launch daemons to automatically start, stop, and manage processes 
and applications such as MySQL.

By default, the installation package (DMG) on macOS installs a launchd file named 
/Library/LaunchDaemons/com.oracle.oss.mysql.mysqld.plist that contains a plist 
definition similar to:


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" 
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>             <string>com.oracle.oss.mysql.mysqld</string>
    <key>ProcessType</key>       <string>Interactive</string>
    <key>Disabled</key>          <false/>
    <key>RunAtLoad</key>         <true/>
    <key>KeepAlive</key>         <true/>
    <key>SessionCreate</key>     <true/>
    <key>LaunchOnlyOnce</key>    <false/>
    <key>UserName</key>          <string>_mysql</string>
    <key>GroupName</key>         <string>_mysql</string>
    <key>ExitTimeOut</key>       <integer>600</integer>
    <key>Program</key>           <string>/usr/local/mysql/bin/mysqld</string>
    <key>ProgramArguments</key>
        <array>
            <string>/usr/local/mysql/bin/mysqld</string>
            <string>--user=_mysql</string>
            <string>--basedir=/usr/local/mysql</string>
            <string>--datadir=/usr/local/mysql/data</string>
            <string>--plugin-dir=/usr/local/mysql/lib/plugin</string>
            <string>--log-error=/usr/local/mysql/data/mysqld.local.err</string>
            <string>--pid-file=/usr/local/mysql/data/mysqld.local.pid</string>
            <string>--keyring-file-data=/usr/local/mysql/keyring/keyring</string>
            <string>--early-plugin-load=keyring_file=keyring_file.so</string>
        </array>
    <key>WorkingDirectory</key>  <string>/usr/local/mysql</string>
</dict>
</plist>

즉, mysql을 설치하면 /Library/LaunchDaemons/com.oracle.oss.mysql.mysqld.plist 파일에 위와 같은 설정 내용이 들어있다는 내용이었고 설정 항목들 중에 자동실행과 관련된 항목이 있을까 싶어 쭈욱 훑어보니 RunAtLoadLaunchOnlyOnce 항목이 눈에 띄었다.
일단 둘다 기본값과 반대로 설정하여 RunAtLoad 값은 false로, LaunchOnlyOnce의 값은 true로 수정해서 저장하고 다시 kill을 해보았다. (이 과정에서 재부팅을 했었는지 정확히 기억이 나지는 않는다;; 안했던것 같은데..^^; ) 그랬더니 더이상 자동으로 실행되지 않았고, MAMP를 실행시켜서 mysql 서버를 start하니 이제 잘 동작하는 것을 확인할 수 있었다.
 

도움이 되었다면 공감 꾸~~욱~~ 

MySQL의 auto_increment 값이 증가하는 시점

사내에서 테이블 데이터 수집을 위해서 테이블 만들 때 auto increment 컬럼을 pk로 추가해달라는 요청이 있어서 테이블 생성시 꼭 추가하고 있다. 그리고 그 auto increment 컬럼의 값은 실제로 데이터가 저장(insert)될 때만 올라갈 거라고 지레짐작만 하고 있었는데 이번에 어떤 서비스를 빨리 개발해줘야해서 새로운 테이블을 만들어 테스트하다가 auto increment값이 순서대로 올라가지 않는 현상을 보게되었다.

테스트코드는 insert -> select -> update -> select 순으로 동작하도록 구성했고 동일한 테스트를 최소 두 번 이상 돌렸다. 이렇게 돌리니까 처음에는 당연히 동일한 데이터가 없어서 no값은 default로 1로 생성이 되었다. 하지만 똑같은 테스트케이스를 다시 돌리면 duplicate key exception이 발생하면서 내부적으로 auto increment값이 증가하지 않을 줄 알았으나, 예외가 발생하여 데이터를 저장하지 못한다해도 insert가 시도될 때마다 no값이 증가하는 것을 확인할 수 있었다. auto increment값이 증가하는 케이스를 정리하자면 다음과 같다.

1. insert가 시도되면 no가 증가하게 된다.

2. duplication key 예외가 발생한다해도 증가하게 된다.

3. update시에는 증가하지 않았다.

일부 테이블은 많은 양의 insert on duplicate key update 문을 실행하고있는데 auto increment값이 overflow 되지 않을까 염려되어 현재 max no값을 조회해보니 아직 수 년은 버틸 수는 있을 정도였다.

만약 overflow 될정도로 많이 올라간다면 어떻게 해야할까 ??? no 값을 다시 1로 세팅하여 처음부터 시작하도록 할 수 있기는 하다. 아래 쿼리를 실행시키면 다시 1부터 세팅을 해준다.

ALTER TABLE YOUR_TABLE_NAME AUTO_INCREMENT=1;
SET @COUNT = 0;
UPDATE YOUR_TABLE_NAME SET AUTO_INCREMENT_COLUMN_NAME = @COUNT:=@COUNT+1;

하지만 해당 쿼리를 실행하는 동안 lock이 잡힐 것이라서 점검때에나 실행 할 수 있을 것이다.

이 방법 말고 직접 no를 세팅하는 방법도 있을 것 같다. 데이터 저장 시에 no값을 직접 할당해줄 수 있으니 말이다.

MyBatis TypeHandler를 이용한 객체리스트 핸들링

이번에 특정 서비스를 개발하다가 JSON 형태의 스트링을 그대로 데이터베이스에 저장했다가 꺼내써야하는 상황이 생겼다.

그것도 RBD인 mysql 데이터베이스에 말이다. mysql이 json 컬럼을 지원하기는 하지만 json 함수를 쿼리에 쓸 필요까지는 없는 상황이다보니 그냥 varchar타입으로 컬럼을 정의했고 여기에 json포맷의 스트링을 그대로 저장했다가 꺼내쓸 수 있도록 해야했다.

일단 타입핸들러를 이용하여 이를 처리하기로 했고 어떻게 사용하는 건지 검색을 좀 해봤다. 구글에서 "Type handler for ArrayList in myBatis" 라고 검색을 하니 많은 포스팅이 검색이 되었고 내 프로젝트에 맞게 가져다가 쓸 수 있었다.

작업 내용을 정리하자면 다음과 같다. 

 

우선 dto의 구조를 보면 MyJsonDataWrapperClass 가 myJsonData 리스트를 들고 있는데 이때 MyJsonData 클래스가 바로 json 형태로 데이터베이스에 저장될 정보이다.

@Getter
@Setter
@JsonInclude(Include.NON_NULL)
public class MyJsonDataWrapperClass {

    private Integer someIntData;
    private List<MyJsonData> myJsonData = new ArrayList<>();
    
    @Override
    public String toString() {
        return new Gson().toJson(this);
    }
}
@Getter
@Setter
@EqualsAndHashCode
public class MyJsonData {
    private String name;
    private int age;

    @Override
    public String toString() {
        return new Gson().toJson(this);
    }
}

 

데이터베이스에는 myJsonData 라는 이름으로 text타입의 컬럼을 만들었고 mybatis의 insert문에 사용할 타입핸들러를 아래와 같이 명시해주었다.

, myJsonData = #{myJsonData, typeHandler=MyJsonDataTypeHandler}

 

그리고 타입핸들러는 아래와 같이 정의하였다.

@Slf4j
public class MyJsonDataTypeHandler extends BaseTypeHandler<List<MyJsonData>> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, List<MyJsonData> parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, new Gson().toJson(parameter));
    }

    @Override
    public List<MyJsonData> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return convertToList(rs.getString(columnName));
    }

    @Override
    public List<MyJsonData> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return convertToList(rs.getString(columnIndex));
    }

    @Override
    public List<MyJsonData> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return convertToList(cs.getString(columnIndex));
    }

    private List<MyJsonData> convertToList(String myJsonDataListAsString) {
        try {
            return new ObjectMapper().readValue(myJsonDataListAsString, new TypeReference<List<MyJsonData>>() {
            });
        } catch (IOException e) {
            log.error("MyJsonDataTypeHandler failed to convert text to list, myJsonDataListAsString:{}", myJsonDataListAsString, e);
        }
        return Collections.emptyList();
    }
}

 

BaseTypeHandler를 상속하면서 List<MyJsonData> 타입에 대한 핸들러임을 명시해주었고 setter에는 리스트 형태를 json 스트링으로 만들어주도록 Gson.toJson() 메서드를 이용하였고, getter에는 조회한 json 스트링을 다시 리스트 형태로 변환해주도록 하였는데 이때는 ObjectMapper.readValue()를 이용하였다.

 

조회시에는 mybatis 매핑파일에 아래와 같이 resultMap을 정의하여 MyJsonDataTypeHandler를 이용해서 myJsonData필드의 값을 ArrayList로 변환하도록 하였으며, 이렇게 정의한 resultMap을 select 문의 resultMap으로 선언해주었다.

<resultMap id="myJsonDataClassMap" type="MyJsonDataWrapperClass">
<result property="myJsonData" column="myJsonData" javaType="java.util.ArrayList"
jdbcType="VARCHAR" typeHandler="MyJsonDataTypeHandler" />
</resultMap>

....중략....

<select id="findMyJsonDataType"
resultMap="myJsonDataClassMap">
SELECT * FROM findMyJsonDataWrapperClass
</select>

 

이렇게 해주면 소스레벨에서는 특정 객체의 리스트 형태로 핸들링을 하면서 데이터베이스에는 json 스트링으로 저장하여 사용할 수 있다.

 

컬럼단위로 데이터를 쪼개서 저장하기 애매한 상황에서 json 스트링을 varchar(text) 타입으로 통으로 저장하고 소스레벨에서는 객체타입으로 핸들링할 수 있도록 해주는 건 정말 멋진 기능인것 같다.

 

 

엘라스틱서치 shards failed 로인한 조회오류 해결방법

최근에 엘라스틱서치 로그백 어펜더를 직접 구현해서 사용하고 있는데 성능 이슈가 있어 원복을 했다.

 

근데 그때부터 키바나에서 로그 조회 시 5 of 240 shards failed 와 같은 오류 메시지가 뜨면서 조회에 실패하였다.

 

처음에는 type이 바뀌면서 문제가 생긴건가 싶었고, 어떻게 해야할지 몰라 특정 인덱스를 삭제해보기로 했다.

 

개발환경에서는 그렇게 했더니 조회가 잘 되기 시작했으나 상용환경에서는 여전히 마찬가지였다.

 

또한 원복한 일자의 인덱스 뿐만 아니라 이전 날짜에 대한 인덱스들도 조회에 실패하였다. 그것도 특정 인덱스 패턴에서만 말이다.

 

ElasticSearch Head 플러그인으로 개발환경과 상용환경의 인덱스 상태에 어떤 차이가 있는지 확인을 해보았는데, 

개발환경에는 키바나 관련 인덱스 파일이 1개 있었고 (.kibana_1) 상태가 푸른색이었다. 

상용환경에는 키바나 관련 파일이 2개가 있었고 (.kibana_2, .kibana_1) .kibana_2 파일이 주황색으로 표시가 되었다.

 

구글링을 좀 해보니 인덱스 패턴을 삭제했다가 다시 생성해보라는 얘기가 있어서 그렇게 진행했더니 데이터가 잘 조회되기 시작했다 ^-^