728x90

네이버 핵데이 컨벤션

많은 사람들이 네이버 핵데이 컨벤션을 사용하여 협업을 진행한다고 한다. 나 또한 복잡하게 컨벤션을 정하고 규칙을 자동화하기 위한 작업을 하기보다는 이미 짜여 있은 네이버 핵데이 컨벤션을 사용하는 것이 편리하다. 하지만 내가 멍청이라는 사실을 프로젝트 Build를 할 때야 되서나 알게 되었다.


핵데이 적용 했을 때 유의사항이다.

개발 시에는 아래 2가지 설정을 Gradle 또는 Maven에 추가하고 개발을 진행한다. 그리고 프로젝트 루트 디렉터리에 '네이버-체크스타일-룰스.xml'과 '네이버-체크스타일-서프레션.xml'을 추가한다.

plugins {
    id 'java'
}

...

compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'
plugins {
    id 'checkstyle'
}

checkstyle {
    maxWarnings = 0 // 규칙이 어긋나는 코드가 하나라도 있을 경우 빌드 fail을 내고 싶다면 이 선언을 추가한다.
    configFile = file("${rootDir}/naver-checkstyle-rules.xml")
    configProperties = ["suppressionFile" : "${rootDir}/naver-checkstyle-suppressions.xml"]
    toolVersion ="8.24"  // checkstyle 버전 8.24 이상 선언
}

 

 

개발을 진행할 때는 위의 설정이 별다른 문제가 되지 않는다. 여느 때와 같이 아무 생각 없이 프로젝트 빌드를 진행하여 배포를 하고자 했더니 에러를 마주하게 되었다...

1: Task failed with an exception.
 -----------
 * What went wrong:
 Execution failed for task ':checkstyleMain'.
 > A failure occurred while executing org.gradle.api.plugins.quality.internal.CheckstyleAction
    > An unexpected error occurred configuring and executing Checkstyle.
       > Unable to create Root Module: config {/app/naver-checkstyle-rules.xml}, classpath {null}.

이런 에러를 마주하게 되었는데... 확인해 보니 '네이버-체크스타일-룰스.xml'가 루트 디렉터리에 없다는 것이다. 빌드를 하기 위해 프로젝트 폴더를 통째로 복사하여 빌드를 진행하기보다는 빌드에 필요한 파일만 선정하여 작업폴더에 복사하고 가장 중요한 소스(src 디렉터리)를 작업폴더에 복사하여 빌드를 진행했다.(부자가 아니라서 조금이라도 용량을 줄이고 싶으니까...) 이 과정에서 체크스타일.xml을 누락하는 바람에 빌드시 에러를 만나게 된 것이다.


결론 - 빌드시 유의사항

  1. 핵데이 컨벤션 페이지의 지침에 따라서 적용하고 있는 사람이라면 빌드시 체크스타일.xml도 빠지지 않고 루트디렉터리에 마련하도록 하자.
  2. 그게 아니라면 src디렉터리 내부에 체크스타일.xml를 배치하고 Gradle설정을 파일경로에 맞춰서 수정을 하여 사용하도록 하자.

이번 경험으로 빌드라는 행위는 개발과정에서 굉장히 반복적인 작업이지만 자료, 소스, 설정 등 필요한 항목이 무엇인지 제대로 파악하고 사용해야 한다는 교훈을 얻었다. 만약 나는 개발만 하고 배포는 다른 사람이 진행하는데 내가 필요한 설정을 누락하여 준다면? 상상만 해도 기분이 매우 안 좋은 일이 발생할게 뻔하다. 개발을 하는 사람으로서 많은 부분이 자동화되고 간편해졌지만 그래도 알고 있어야 한다는 것이 이런 부분들 아닐까?

728x90
728x90

오늘 기록한 내용은 Spring에서 제공하는 Page Class이다. Page클래스는 우리가 많은 데이터를 손쉽게 페이지로 표현할 수 있도록 제공하는 클래스이고 이와 함께 JPA에서도 Page와 관련된 객체인 Pageable을 사용하여 원하는 데이터를 받을 수 있도록 해준다. 상당히 간편하게 사용할 수 있고 사용법도 까다롭지 않아서 어렵지 않은 클래스이지만 이런 클래스를 사용하면서 깨달은 부분을 적어보도록 하겠다.

계기

우선 이런 글을 쓰게 된 계기이다. Front로 Page<>데이터를 보내고 받은 데이터를 사용하기 위해 확인하는 과정에서 잘못된 사용으로 문제가 있었다. JPA에서 원하는 데이터 전체를 받아오고 PageImple을 만들었는데 원하는 페이지의 데이터만 있는것이 아니라 전체 데이터가 들어가 있는 문제가 발생했다.

public Page<Post> getPosts(Pageable pageable){
    // jpa에 Pageable을 바로 넣지 않은 이유는 원래 코드에서는 타 서비스에서 데이터를 가져왔기 때문.
    List<Post> list = postRepository.findAll();
    
    return new PageImple<>(list, pageable, list.getTotalElements());
}

위의 코드와 같이 데이터를 받아오고 그 데이터를  pageImple에 Page로 구현했으나 받은 데이터에는 페이지사이즈인 5개만 들어있는 것이 아니라 전체 Post데이터가 들어있었다. 잘못된 사용법에 따른 잘못된 결과였으나, 알지 못했으니 의도하지 않은 결과가 나온 것에 대해서 매우 열받는 것은 어쩔수가 없었다.

// JSON
{
 contents : {전체 Post 데이터},
 number : 0,
 pageSize : 5,
 first : true,
 last : false,
 ...
 }

 

문제해결을 위한 탐구

이런 문제를 해결하기 위해서 사용법을 다시 확인했고 PageImpl, Page, Chunk, Slice와 같이 Page와 관련된 클래스들을 살펴보았다. 전체 Post데이터가 content에 있는 문제의 원인을 찾게 되었고, 그럼 어떻게 원하는 데이터만 넣을 수 있느지 확인하면서 JPA에서 Pageable을 어떻게 사용하는지 알게 되었다.

먼저, 왜 전체 데이터를 반환했는가이다. PageImple 생성자의 content는 단순히 받은 파라미터를 필드의 content에 저장할 뿐 pageable의 값에 따라서 PageImple클래스가 알아서 분류 및 정렬을 해주지 않는다. 즉, 원하는 데이터와 전혀 관계없는 Post를 집어 넣어도 객체를 생성하는데는 문제가 되지 않는다는 것이다.

문제를 해결하기 위해(내가 원하는 페이지의 데이터를 받기 위해) 어떻게 해야하는가 찾아보았다. JPA를 이용해서 Pageable 파라미터를 집어 넣고 데이터베이스에서 원하는 데이터만 가져오면 된다는 것을 알게 되었다. 그러면서 어떻게 JPA는 데이터를 가져오는지 Query를 확인해보았다.

SELECT * FROM Posts
LIMIT 5 OFFSET 0;

JPA는 Pageable을 해석하여 위와 같이 SQL쿼리를 작성하고 원하는 페이지의 데이터를 찾아서 그 데이터를 Page 객체에 반환하는 방식이었다. 위의 쿼리는 시작이 0번째 행부터 5개의 행만 조회하라는 의미이며, pageSize를 limit으로 pageSize와 현재페이지의 값으로 offset을 설정하여 데이터를 반환해준다.

 

결론

내가 겪었던 문제인 전체 데이터를 포함하는 Page를 해결하기 위해서는 JPA에서 Pageable을 사용하여 쿼리를 작성 및 조회할 수 있도록 하고 그 데이터를 받아서 해결할 수 있었다. 일반적으로는 이런 문제가 발생하지는 않았겠지만(결론과 같은 사용법이 매우 일반적인 방법이므로) 이번에 발생한 특이한 케이스 덕에 JPA에서 어떤 파라미터를 받고 어떤 데이터를 반환하는지 어떻게 쿼리를 작성하는지 알게 되었으며, Page와 관련된 클래스들을 확인해보면서 상속을 이용한 클래스 구현(성격이나 특징과 같은 것들을 클래스로 정의하여  어떤 클래스가 같은 성격과 특징을 가진다면 상속을 이용해 구현)에 대해 좀 더 잘 알게 된 계기가 되었다.

 

추가로 생각하게된 부분

응답으로 받은 Page 데이터를 확인해보면 얻고 싶은 데이터인 content 외 number, totalElements, first, sorted 등 다양한 데이터가 존재한다. 처음 확인했을 때는 이런 불필요한 데이터들을 왜 같이 보내지 싶었다. 그러나, 페이지네이션을 구현하기 위해 Front에서 다양한 기능(반복문 작성, 현재/이전/첫/마지막 페이지로 이동, 페이지 표현 등)들을 구현해 보면서 불필요하다고 생각했던 데이터들이 매우 유용하게 사용된다는 것을 느꼈다. 향후 프론트엔드-백엔드 협업을 많이 하게 될 텐데, 어떤 데이터가 필요한지 충분히 고민하고 논의하면서 데이터를 활용도 있고 편리하게 사용할 수 있도록 데이터를 구성하는 것도 능력이 될 수 있을 것이라고 생각하게 되었다.

728x90
728x90

React에서 useState를 사용하는 과정에서 비동기방식으로 인해 상태값 중 어떤 상태값(1번)에 의존적인 어떤 상태값(2번)을 변경하는데 어려움을 겪었다. 1번이 변경되면 2번도 같이 변경되는 방식인데, 1번의 변경이 한번에 많이 발생할 경우 2번의 값이 1번이 변경된 만큼 변경되지 않고 일부만 적용되었다. 이는 useState가 가지는 비동기성 때문이였으며 그로인한 해결책을 찾아서 해결하였다.

 

Hook에 대해 처음 작성하므로 간단한 소개와 함께 글을 시작하겠다. React의 State Hook을 사용한 상태 관리방식은 상당히 편리하다. 아래의 코드는 React 공식문서의 코드의 일부이다.

기존의 방식(React 16.8이전)으로 상태관리를 할때는 아래의 코드와 같이 class를 선언하고 그것을 변경하는 함수를 사용했다.

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

하지만, Hook이 적용된 이후에는 보다 편하게 사용할 수 있으며 함수형 컴포넌트에서도 상태를 사용하고 관리할 수 있게 되었다.

import React, { useState } from 'react';

function Example() {
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

 

내가 발생한 문제는 아래의 코드와 같다.(가장 유사하게 만들었다.) sum을 setCount에서 한번에 변경하지 않은 이유는 원래 코드에서 숫자와 문자가 섞어서 받았기 때문이다.

import React, { useState } from 'react';

function Example() {
    let num1 = 0;
    let num2 = 0;

    const [count, setCount] = useState({
        a: 0,
        b: 0,
        sum: 0,
    });

    const changeCountA = (e) => {
        setCount(preData => ({
            ...preData,
            a : e.target.value,
        }))
        const total = parseInt(count.a) + parseInt(count.b);

        setCount(preData => ({
            ...preData,
            sum: total,
        }))
    }

    const changeCountB = (e) => {
        setCount(preData => ({
            ...preData,
            b: e.target.value
        }))

        const total = parseInt(count.a) + parseInt(count.b);

        setCount(preData => ({
            ...preData,
            sum: total,
        }))
    }

    return (
        <div>
            <p>a : {count.a}</p>
            <p>b : {count.b}</p>
            <p>sum : {count.sum}</p>
            <span>a :</span>
            <input type='number' id='a' value={count.a} onChange={changeCountA} />
            <span>b : </span>
            <input type='number' id='b' value={count.b} onChange={changeCountB} />
        </div>
    );
}

export default Example;

이렇게 코드를 구성했을 때 발생했던 문제점이 비동기 방식으로 인해서 sum의 결과가 이상하게 나온다는 것이다.

결과물...

결과물에서 볼 수 있듯이 a의 값을 입력했을 때 a가 변경되고 sum이 바뀌는것이 아니라 a가 바뀌는 도중에 sum도 바뀌면서 sum의 값이 이상하게 나오는 것을 확인할 수 있다. 다시말하면 변경이 이루어지고 있는 현재의 상태값을 참조해서 변경을 하기 때문에 이런 문제가 생겼던 것이다. 문제를 해결하기 위해서 sum을 계산할 때, 이전의 상태값 즉, a와 b의 값이 변하기 전의 상태값을 참조하여 변경하면 된다.

새로운 결과

아래 처럼 변경하면 이전의 값을 호출하여 합산을 하기 때문에 문제없이 합쳐지는 것을 확인할 수 있다.

import React, { useState } from 'react';

function Example() {
    let num1 = 0;
    let num2 = 0;

    const [count, setCount] = useState({
        a: 0,
        b: 0,
        sum: 0,
    });

    const changeCountA = (e) => {
        setCount(preData => ({
            ...preData,
            a : e.target.value,
        }))
        // 다른 코드

        setCount(preData => ({
            ...preData,
            sum: makesum(preData, e.target.value),
        }))
    }

    const changeCountB = (e) => {
        setCount(preData => ({
            ...preData,
            b: e.target.value
        }))

        // 다른 코드

        setCount(preData => ({
            ...preData,
            sum: makesum(preData, e.target.value),
        }))
    }

    function makesum(preData, newValue) {
        return parseInt(preData.a) + parseInt(preData.b);
    }

    return (
        <div>
            <p>a : {count.a}</p>
            <p>b : {count.b}</p>
            <p>sum : {count.sum}</p>
            <span>a :</span>
            <input type='number' id='a' value={count.a} onChange={changeCountA} />
            <span>b : </span>
            <input type='number' id='b' value={count.b} onChange={changeCountB} />
        </div>
    );
}

export default Example;
728x90
728x90

본 글은 본격적인 내용을 적기에 앞서서 orval를 사용하는데 성공하는 것을 기념하기 위해 작성해 보았다.

백엔드를 중심으로 공부를 하다보니, 프론트 구현의 ㄱ자도 모르는 상태였다. Orval에 대해 알게 되어 사용을 시도해보려고 했으나, React도 모르고, Typescrip도 모르고 axios도 몰라서 찾고 찾고 또 찾아서 Orval을 결국 사용하는데 성공했다.

하나의 엔드포인트에 대해서만 성공하여 이후에는 모든 엔드포인트 및 요청에 대해 작성해보고 사용해보고자 한다.

글을 작성하는 도중 충격적인 사실을 알아서 우선 작성해두려고 한다. orval.config에 보면 input항목이 있다. Orval 공식에는 이 항목에 yaml파일을 참조하도록 했다. 해당 예제를 참고하여 손으로 yaml을 작성했는데... Spring REST Docs를 이용하여 자동화 할 수 있다는 사실을 알게되었다... 충격적이다. 저거 작성을 위해 들인 시간도 있었것만...

하여튼, 다들 사용해 보시라. 편하다.

https://orval.dev/

728x90
728x90

 

프록시 서버

프록시 서버(Proxy Server)는 클라이언트와 서버 간의 중계 역할을 하는 서버이다. 클라이언트가 인터넷을 통해 특정 서버에 직접 연결하지 않고, 프록시 서버를 경유하여 통신하는 방식이다. 프록시 서버는 포워드 프록시 서버와 리버스 프록시 서버가 있다. 포워드 보다는 리버스에 집중하여 알아보고자 한다. 우선, 포워드 프록시이다.

 

포워드 프록시

우리가 인터넷을 사용할 때 보통 클라이언트 - 서버간 직접 통신을 한다. 프록시 서버는 클라이언트와 서버 사이에 위치한다.

출처 : cloudflare

프록시 서버는 클라이언트 - 서버 사이에서 요청을 중개해주는 역할을 한다. 클라이언트의 요청을 받아서 서버로 대신 보내고, 서버에서 받은 응답을 클라이언트에게 전달해준다. 그럼 굳이 왜 사용하는가?

  • 보안 및 개인 정보 보호: 프록시 서버를 통해 통신하면 클라이언트의 IP 주소가 숨겨지므로 익명성을 유지할 수 있다. 또한 프록시 서버는 악의적인 웹사이트나 해킹 시도로부터 보호할 수 있다.
  • 캐싱: 프록시 서버는 이전에 요청된 데이터를 캐시하여 동일한 요청에 대한 응답을 즉시 제공할 수 있다. 이를 통해 네트워크 대역폭을 절약하고 웹 페이지 로딩 시간을 단축할 수 있다.
  • 접근 제어: 기업이나 학교 등에서는 프록시 서버를 사용하여 특정 웹사이트나 콘텐츠에 대한 접근을 제어할 수 있다. 예를 들어, 학교에서 Youtube나 facebook 등 콘텐츠를 차단하거나, 부적절한 콘텐츠에 대한 접근을 차단하는 등 특정 웹사이트의 사용을 제한할 수 있다.

 

리버스 프록시

리버스 프록시는 포워드와는 다르게 서버의 앞에 위치한다. 포워드 프록시와의 중요한 차이점은 포워드 프록시 서버의 경우 클라이언트가 어떤 서버와도 직접 통신하지 못하도록 막는다면 반대로, 리버스 프록시는 서버가 어떤 클라이언트와도 직접 통신하는 것을 막아준다.

출처 : cloudflare

포워드 프록시는 보안, 캐시, 접근제한 등의 이유로 사용한다면 리버스 프록시는 왜 사용할까?

  • 로드 밸런싱 : 인기있는 서비스의 경우 엄청나게 많은 트래픽이 발생하고 이를 하나의 서버가 처리하기에는 어려움이 있다. 리버스 프록시는 서버로 들어오는 수 많은 클라이언트 요청을 분산시켜 서버의 부하를 줄일 수 있다.
  • 보안 강화 : 리버스 프록시는 클라이언트와 서버 간의 중계 역할을 하므로, 실제 서버의 IP 주소나 구성을 숨길 수 있다. 이를 통해 DDos공격과 같은 위험에서 보다 안전할 수 있다. 
  • 캐싱 : 리버스 프록시도 이전에 받은 응답을 캐시하여 동일한 요청에 대한 응답을 빠르게 제공할 수 있다. 캐싱으로 웹 애플리케이션의 성능을 향상시킬 수 있다.
  • 암호화 : 각 클라이언트에 대한 SSL 통신의 암호화 및 암호 해독은 원본 서버의 리소스가 많이 소요된다. 역방향 프록시는 들어오는 모든 요청을 해독하고 나가는 모든 응답을 암호화하여 원본 서버의 귀중한 리소스를 확보할 수 있다.

 

어떤 것을 쓸까?

그러면 리버스 프록시 서버로 사용할 수 있는 products들은 뭐가 있을까?

가장 유명한 것으로는 NGINX, apache http Server의 mod_proxy 모듈, HAProxy 등이 있다. 필요에 따라서 골라서 사용하도록하자.

열심히 비교해 주셨으니, 우리는 감사하며 보면되지 않을까? 리버스 프록시 서버 10개 비교

728x90
728x90

Java에서 조건문을 사용하기 위해 == 연산자를 많이 사용한다. ==은 두 변수가 동일한지 확인하는 연산자이고 사용이 편리하다. 단, 참조타입의 변수(String, Integer, Object 등)은 ==을 사용할 수 없다.

'==' 연산자는 참조타입의 변수의 경우 두 참조타입의 변수의 주소가 일치하는지 확인한다. 따라서, 같은 주소를 가리키고 있지않는다면 다르다는 결과가 나오고, 변수의 값이 동일하더라도 주소가 다른 경우 true를 반환하지 않는다.

참조타입의 변수의 동등성을 비교하기 위해서는 equals메소드와 hashcode를 사용하여 유일한 객체로 설정, 동등하다는 결과를 만들어 낼 수 있다.

728x90
728x90

동시성(Concurrency)은  여러 작업이 동시에 실행되는 것을 의미한다. 예를 들면 하나의 콘서트 티켓 예약이 대표적이지 않을까? 한번에 다수의 사람들이 데이터에 접근하고 수정하려고 한다. 

동시성은 여러가지 문제를 야기할 수 있다.

1. 경쟁 상태 (Race Conditions): 두 개 이상의 프로세스 또는 스레드가 공유된 자원에 동시에 접근할 때 발생할 수 있는 문제. 경쟁 상태는 예상하지 못한 결과를 초래할 수 있으며, 이를 방지하기 위해 동기화 메커니즘이 필요하다.

2. 데드락 (Deadlocks): 두 개 이상의 프로세스나 스레드가 서로의 작업을 기다리며 무한정 대기 상태에 빠지는 문제. 이는 각각의 프로세스나 스레드가 점유한 자원을 해제하지 않을 때 발생할 수 있다.

3. 교착 상태 (Livelocks): 두 개 이상의 프로세스나 스레드가 서로의 작업을 기다리면서 계속해서 상태를 변경하지만 전체적으로 진행이 없는 상태.

4. 자원 소진 (Resource Starvation): 한 프로세스나 스레드가 필요한 자원을 다른 프로세스나 스레드가 지속적으로 점유하는 상황. 이는 프로세스나 스레드가 실행을 완료하지 못하고 계속해서 대기하는 결과를 초래할 수 있다.

5. 컨텍스트 스위칭 오버헤드 (Context Switching Overhead): 여러 프로세스나 스레드가 동시에 실행될 때 발생하는 오버헤드. 컨텍스트 스위칭은 실행 중인 작업의 상태를 저장하고 다른 작업의 상태를 로드하는 과정을 의미하며, 이로 인해 성능 저하가 발생할 수 있다.


이러한 문제들을 해결하기 위해서 적절한 동시성 제어 메커니즘이 필요하고, 이를 위해 동기화 기법, 락(Lock), 세마포어(Semaphore) 등의 동시성 제어 도구를 사용할 수 있다. 다음에는 이러한 도구들을 다루어 보려고 한다.

728x90
728x90
Unique index or primary key violation: "PUBLIC.UK_OUW0LR92CBLYSTEKQKWHUMEFF_INDEX_C ON PUBLIC.CHECK_RESERVATION(RESERVATION_OPTION_ID NULLS FIRST) VALUES ( /* 9 */ CAST(3 AS BIGINT) )"; SQL statement:
/* insert for cohttp://m.ll.tourdemonde.payment.checkReservation.entity.CheckReservation */insert into check_r [23505-224]

위와 같은 에러를 만났을 때, 무엇이 문제인지 인지하는데 시간이 걸려서 적어두고자 한다.

JPA 사용할 때, OneToOne 어노테이션을 사용하여 데이터베이스를 저장하는 경우에 중복하여 데이터를 저장하려고 할 때, 위와 유사한 에러를 경험하게 될 것이다. 개발 중에 이것저것 테스트 해보려고 하다가 만나게 될 수 있다.

나의 경우에는 갓 만든 기능을 시험해보겠다고 예외처리를 하지 않은 상태에서 기능을 시험하다가 마주했다. 한참을 원인이 무엇인지 찾다가(유니크 조건 또는 Primary 키를 특별히 설정하지는 않았으므로 찾기가 어려웠다.) OneToOne 어노테이션 역시 유니크하다는 것을 인지하고는 바로 예외조건을 추가했다.

OneToOne을 사용하게 된다면 데이터를 저장하기 이전에 동일한 데이터가 있는지 조회, 호출하는 코드를 만들어두자. 시간을 아낄 수 있다.

728x90

+ Recent posts