이번에 할 것은 정적이었던 쿼리를 동적을 바꿔 보려고 합니다.

 

일단 쿼리부터 보면

 

전부 동일한 쿼리이지만 마지막 where 절만 달랐고 JPQL로는 동적 쿼리 작성이 어렵기 때문에

쿼리를 3번을 작성하여야 했습니다.

 

그래서 쿼리를 동적으로 리팩토링을 하기로 했고 Querydsl을 사용해 봤습니다.

 

시작하기 앞서 강의에서 봤던 스프링 부트의 버젼은 2.x 버전이었고 3.x 이상부터는 설정이 좀 다르기 때문에 아래 블로그를 보고 참고했습니다.

 

https://ururuwave.tistory.com/148

 

프로젝트 환경설정 (SpringBoot 3.x 버전), Querydsl 설정

강의는 스프링부트 2.x 버전이라 3.x버전으로 세팅하는 법을 정리해보려 한다.강의 자료에 3.x 버전 설명이 있지만 중간 중간 설명이 달라서 환경설정하는데 예상보다 오래걸렸다;; 1. 프로젝트

ururuwave.tistory.com

 

 

 

Querydsl 설정을 마치고 Q클래서 까지 만들어 졌다면

 

spring data JPA와 Querydsl을 함께 사용하기 위해 

 

 

인터페이스를 하나 만들어 주고

 

 

 

BoardRepositoryImpl 클래스에서 구현을 해줬습니다.

 

 

 

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
public class BoardRepositoryImpl implements BoardRepositoryCustom {
 
    private final JPAQueryFactory qf;
    private BoardRepositoryImpl(EntityManager em) {
        qf = new JPAQueryFactory(em);
    }
 
 
 
    @Override
    public PagedModel<CmnBoardListResponseDTO> search(String keyword, Pageable pageable, String searchType) {
        QOeBoardImg oeBoardImgSub = new QOeBoardImg("OeBoardImgSub");
        List<CmnBoardListResponseDTO> content = qf.select(Projections.constructor(
                        CmnBoardListResponseDTO.class,
                        oeBoard.boardPk,
                        oeBoard.title,
                        oeBoard.viewCnt,
                        oeBoard.likeCnt,
                        oeBoard.member.nickname,
                        JPAExpressions
                                .select(oeBoardImg.s3ImgAddress)
                                .from(oeBoardImg)
                                .where(oeBoardImg.boardImgPk.eq(
                                        JPAExpressions
                                                .select(oeBoardImgSub.boardImgPk.min())
                                                .from(oeBoardImgSub)
                                                .where(oeBoardImgSub.board.boardPk.eq(oeBoard.boardPk))))
                ))
                .from(oeBoard)
                .leftJoin(oeBoard.member, member)
                .where(
                        nicknameEq(searchType,keyword),
                        titleOrContentEq(searchType,keyword)
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        Long total = qf.select(oeBoard.count())
                .from(oeBoard)
                .where(nicknameEq(searchType, keyword),
                        titleOrContentEq(searchType, keyword)).fetchOne();
 
        return new PagedModel<>(new PageImpl<>(content, pageable, total == null ? 0L : total));
 
    }
 
    private Predicate titleOrContentEq(String searchType, String keyword) {
        if(searchType.equals("title")){
            return oeBoard.title.contains(keyword);
        } else if (searchType.equals("titleAndContent")) {
            return oeBoard.title.contains(keyword).or(oeBoard.content.contains(keyword));
        }
        return null;
    }
 
    private BooleanExpression nicknameEq(String searchType, String keyword) {
        return searchType.equals("nickname") ? oeBoard.member.nickname.contains(keyword) : null;
    }
 
 
}
 
cs

 

위 코드는 앞서 봤던 3개의 정적 쿼리를 하나의 동적 쿼리로 리팩토링 된 모습입니다.

 

설명을 해보자면 쿼리는 실제 값을 가져오는 쿼리와 페이징을 위해 게시물 수를 나타내는 count쿼리 총 2번이 나갑니다.

count를 따로 뺀 이유는 실제 필요한 값을 가져오기 위해 조인하고 서브쿼리를 사용하는 쿼리랑 count쿼리가 같을 필요가 없기 때문에 따로 작성해 주었습니다.

 

또한 처음에 serch 메서드는 Page를 봔환 했으나

WARN 4209 --- [p-nio-80-exec-1] ration$PageModule$WarningLoggingModifier : Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
	For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
	or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.

 

위와 같은 경고 메시지가 나왔다 캡쳐를 따로 하지못하여서 다음 블로그에서 메시지를 가져왔습니다.

 

https://velog.io/@solst_ice/3.3-%EC%9D%B4%EC%83%81-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B6%80%ED%8A%B8%EC%9D%98-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC%EC%97%90%EC%84%9C-%EC%97%90%EC%84%9C-Page-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EA%B7%B8%EB%8C%80%EB%A1%9C-%EC%9D%91%EB%8B%B5%ED%95%A0-%EB%95%8C-%EB%B0%9C%EC%83%9D%ED%95%9C-%EA%B2%BD%EA%B3%A0

 

3.3 이상 스프링 부트의 컨트롤러에서 에서 Page 객체를 그대로 응답할 때 발생한 경고

경고 메시지 발견 3.3 이상의 스프링 부트를 사용하는 프로젝트에서 Page 객체를 컨트롤러에서 Json으로 변환하여 응답할 때 이전에는 볼 수 없었던 경고메시지가 보였습니다. > PageImpl 인스턴스를

velog.io

 

대충 요약하자면 Page 객체를 반환하는 건 엔티티를 그대로 반환하는 것과 다르지 Json 구조의 안정성을 보장 못 하니까 Page 대신 Spring Data에서 제공하는 PagedModel을 사용해야 한다.

정도로 이해했습니다.

그리고 혹시 Page 정보 외에 더 들어가야 할 것이 있다면 따로 DTO를 만드는 것도 방법이 될 수 있겠다 생각만 하고 넘어갔습니다.

 


마지막으로

 

일단 자바 코드로 쿼리를 작성하기 때문에 대부분의 오류는 컴파일 단계에서 잡아주고 where 절의 조건을 메서드 형식으로 빼서 보기 좋게 작성할 수 있었습니다.(쿼리를? 코드를?)

 

추가로 titleOrContentEq는 Predicate를 반환 하는데 Predicate는 and나 or같은 조건 조합 기능을

제공하지 않기 때문에 ( oeBoard.title.contains(keyword).or(oeBoard.content.contains(keyword))구문은 작동 하지만

titleOrContentEq.and()는 지원하지 않는다는 뜻 )

재사용성을 고려하여 BooleanExpression을 주로 쓰면 좋을 거 같다 라고 생각 했습니다.

 

이번 최적화는 성능 최적화 보다는 유지보수적 측면에서 최적화 하는 것에 집중 하였습니다.

 

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

 

인프런에서 강의를 듣고 몇달전 프로젝트 하던 것을 최적화 해보기로 했다.

오늘 최적화 해볼 쿼리다!

 

 

 

 

 

 

 

 

음.. 그러니까..

내가 했음에도 보기 좀 지져분했다.

 

이 JPQL을 작성할 당시에는 내가 가져오고자 하는 데이터들이 4개(Member, 게시판, 게시판 댓글, 게시판 이미지) 엔티티에 나눠서 담겨있기에

필요한 만큼만 DTO로 받아와서 성능적인 면으로 최적화하고자 했었다.

 

그 당시엔 최선이라 생각했었지만 강의를 듣다가 사실상 DTO로 필요한 정보만 빼오는 것이 가시성도 별로고

엔티티들을 통으로 가져와도 성능 차이가 크지 않고 유지보수적으로 매우 유리하고 나중에 성능이슈 생기면 그때 바꿔도 늦지 않는다고 하기도 하고 다 맞는 말이라 판단이 들어서 다시 쿼리를 짜봤다.

 

그래서 두번째 쿼리다.

보기에는 가시성도 매우 좋아졌고 유지보수도 매우 간편해보인다.

이 쿼리의 의도는 OeBoard에 필드로 있는 Entity들이 지연 로딩으로 설정되어 있어서 join fetch로 즉시 로딩을 하기 위한 의도였고

실행해 본 결과 MultipleBagFetchException이 발생했다 이유는 게시판 이미지와 댓글 엔티티가 OeBoard와 OneToMany관계이기 때문이었다.

더보기

Hibernate에서는 여러 개의 'bag'(순서 없는 List 형태) fetch join을 허용하지 않고, 이를 시도하면 MultipleBagFetchException을 일으킨다고 한다 혹시 Set을 사용하면 가능할까 했지만 그럼 Page클래스를 사용함에 제한이 생기기 때문에(쿼리를 실행하고 반환받은 값은 Page의 contens필드로 들어오게 되는데 contens는 List이다) 

좀 더 알아보니 MultipleBagFetchException가 아니더라도 join fetch를 사용하면 실제 쿼리를 실행할 때

원래는 Spring Data JPA가 Pageable 객체에 설정된 limit과 offset 정보를 바탕으로 자동으로 Limit절을 추가해 주지만 패치조인을 쓴다면 Limit절이 빠지고 where절을 만족하는 모든 값들이 넘어와서 메모리에 담고 페이지네이션이 된 것처럼 보이게 한다.

이건 나중에 데이터가 커지면 수정하고 그럴 것이 아니라 처음부터 이렇게 개발하면 안 될 거 같다라고 판단했고 패치조인 전략은 접기로 했다...

 

그럼 어떻게 해야 유지보수도 잡고 가시성도 잡을까 고민하던중 엔티티를 필드로 가지고 있는 DTO로 받으면 괜찮지 않을까? 생각했고 실행해봤다.

 

 

 

최종 쿼리이다. 많이 보기 편해졌다 계속 보다보니 착시인가?

 

보면 게시판이랑 멤버 엔티티만 통으로 받고 댓글 수나 썸네일로 보여줄 이미지 하나는 단일로 받고 있다.

일단 이런 식으로 혹시 수정이 생긴다면 이쪽 엔티티에서 생기겠다 싶은 엔티티만 직접적으로 받고 나머지는 컬럼을 지정해서 가져오는 것으로 타협을 봤다.

 

실제 처음 복잡해 보이는 쿼리와 최종 쿼리의 성능을 비교해 보면 하나의 페이지를 요청할 때 로컬에서10ms ~ 15ms초 정도 차이가 있었고 이 차이가 유의미한 수준인지는 아직 파악이 안된다.

 

마지막으로 일단 확실히 보기는 편해진 거 같아서 기분은 좋았다.

하지만 엔티티를 그대로 반환할 수는 없기 때문에 반환용 DTO를 하나 만들어야 하는 것이 단점이긴 하지만 막상 작성하다 보니 그리 어려운 로직도 아니고 그래봐야 service에서는 한 줄 추가되는 정도라 적어도 지금 상황에선 큰 단점은 아니라 생각했다.

아쉬운 점은 성능 테스트하는 법을 잘 몰라서 이 정도는 허용되는 범위인지 치명적인지 구분이 안 가는 것이었고 그래서 다음 공부할 내용은 성능 테스트로 결정했다! 끝!

이번에 개발한 기능은 오이를 좋아하는지, 싫어하는지 투표를 하고 그 공간에서 오이를 좋아하는 사람과 싫어하는 사람들이 대화를 할 수 있는 공간을 제공하는 기능이다.

 

결과

 


 

 

 

 

개발


개요

투표와 채팅은 STOMP를 사용하였는데 그 이유는 웹소켓을 쓰기 편해서도 있지만 확장성을 고려해서가 주 이유이다.

추후 여러 채팅방을 만들거나 다른 투표들도 생길 가능성을 염두에 두어 관리의 편의성을 챙기기 위하여 STOMP를 사용했다.

또한 지금 진행 중인 프로젝트는 swagger로 api명세를 대체하고 있는데 swagger는 HTTP 기반의 RESTful API만을 문서화하고 테스트하는 것에 초점이 맞춰져있어 WebSocket은 따로 테스트를 해야 하고, 프론트와의 소통이 매우 중요했다.

API 명세를 이상하게 적어서 프론트에게 혼란을 준 사람이 나야 나..

그리고 CORS설정도 하나의 벽이었다.

 

마지막으로 웹소켓을 통한 개념을 더 잘 잡은 거 같아서 성취에서 만족도가 좀 높은 기능이었다.

다른 블로그 등 레퍼런스를 보고는 이해가 쉽지 않았는데 프론트와 백엔드 양쪽에 controller가 있다고 생각하니 이해가 확 되었다.


과정

 

먼저

registry.enableSimpleBroker("/topic");

구독자에게 메시지를 전달할 경로를 설정 하였다.

클라이언트(프론트)는 이제 /topic/* 의 요청들을 받을 수 있다. (ex : /topic/vote, /topic/message)

 

 setApplicationDestinationPrefixes("/app")

클라이언트가 서버로 메시지를 보낼 때 사용할 경로 프리픽스를 설정한다.

이건 평소 HTTP프로토콜과 비슷하게 @RequestMapping("/api") 이런식으로 이해하면 편하다.

 

 

 

addEndpoint("/ws") 

WebSocket 연결을 시작할 STOMP 엔드포인트를 설정한다.

HTTP를 사용한다면 ws지만 HTTPS면 wss로 써야하는 거 아닌가? 했지만 따로 설정을 해주지 않아도 WebSockt 프로토콜이 사용하는 정송 계층이 HTTP(S) 프로토콜을 그대로 따르기 때문에 따로 설정을 해주지 않아도 된다. 더 자세히는 공부를 해봐야겠다.

 

setAllowedOriginPatterns("*")

CORS 설정이고 모든 곳에서 연결을 허용한다는 의미인데 개발이 끝나고 우리가 사용하는 도메인만 허용하게 바꿀 예정이다.

 

withSockJS()

WebSockt을 지원하지 않는 클라이언트에서도 사용 가능한 SockJS 폴백 옵션을 활성화 한다.

이 옵션은 WebSocket을 지원하지 않는 브라우저라면 SockJS를 통해 위의 대체 기술로 자동 전환하여 WebSocket을 지원하지 않는 브라우저에서도 STOMP를 사용할 수 있게금 해주는 것이다.

 

 

 

다음은 컨트롤러이다.

 

클래서 위에 @TimeTrace는 원래는 service 클래스 에서만 사용하지만 웹소켓 통신 테스트를 위해 잠깐 넣어뒀다.

(@TimeTrace : 내가 직접 만든 AOP 어노테이션이다. 메서드 시작과 끝을 알려주며 얼마나 시간이 걸렸는지도 알려준다.

예외 발생 시 발생지 파악을 위해서, 성능 체크를 위해 만들었다.)

 

위에 설명했듯 @MessageMapping은 일반 컨트롤러의 GetMapping으로 생각하면 편하다 클래스 위에 리퀘스트매핑 "/app"은 config에서 설정을 해놨기 때문에 따로 명시할 필요는 없다.

 

그리고 아래 @SendTo도 스톰프config에서 설정했듯이 /topic/message를 구독하고 있는 모든 클라이언트에게 프론트에 요청이 없이 전송을 하는 것이다.

이거 하려고 브로커 설정하고 CORS설정하고 한 것이다.

 

마치며


요청이 와야만 답을 할 수 있는 백엔드 입장에서 WebSocket 이란 흥미롭다 생각했지만 개발하다 보니 1 : 1 요청에서 1 : N으로 바뀐 것일 뿐이란 생각이 들었다.

 

개발 난이도는 코드가 복잡하다기보단 STOMP를 이해하여야 하고 CORS설정을 해줘야 하는 것 때문에 어렵게 느껴졌었다.

또한 service클래스는 웹소켓이라 특별한 무언가는 없기에 딱히 작성의 필요성을 느끼지 못했다.

 

하지만 보고 싶다면 깃허브를 통해서 보면 될 것 같다.

https://github.com/qkrwlstn1/-BE-Oeasy

 

GitHub - qkrwlstn1/-BE-Oeasy

Contribute to qkrwlstn1/-BE-Oeasy development by creating an account on GitHub.

github.com

 

 

오이지수란 매시간마다 날씨정보를 가지고 와 그에 맞는 오이 이미지를 보여주는 기능이다.

이게 왜 오이지수냐 한다면 초기 계획은 오이가 잘 자라는 환경에 맞춰 우리만의 지수를 정해보자 였으나 고려해야 할 것들이 너무 많기에 간소화하는 대신 오이들의 이미지를 보여주는 것으로 변경되었다

 

결과


 

온도 아래 '!'에 호버하면 OE지수의 대하 설명이 나오고, 오른쪽엔 날씨에 맞는 이미지가 출력된다.

 

개발


개요

API는 공공데이터포털에 https://www.data.go.kr/iim/api/selectAPIAcountView.do

 

공공데이터포털 통합 로그인

공공데이터포털 로그인 국민과 함께 하는 공공데이터포털에 오신 것을 환영합니다

auth.data.go.kr

를 이용하였다.

이 api는 1시간 단위로 정보를 제공하기 때문에 spring에 스케줄러를 이용함이 좋다고 판단하였고 1시간 단위라고 매시간 00분에 업데이트되는 것이 아니므로 매시간 30분마다 api를 요청하여 필요한 데이터만 잘 정제하여 db에 저장하고 사용자에게는 가장 최근의 정보만 제공하는 방향으로 개발하였다.

 


 

과정

 

먼저 사이트에 접속하여

application.yml

개인 api인증키를 발

급 받고 발급 받은 인증키를 서브모듈로 운용하고있는 yml에서 관리하고

 

WeatherScheduler.java

@Value 태그를 이용하여 변수로 할당받아 사용한다.

 

api는 위 사진과 같이 요청할 것인데 하나씩 끊어서 설명해 보자면 다음과 같다.

 

serviceKey : 할당받은 api키

pageNo : 볼 페이지

numOfRows : 몇 줄까지 볼 건지

dataType : 받을 데이터 타입 (XML과 JSON 두 개가 있는 것으로 안다. 육안으로 볼 때는 XML이 편함으로 데이터 체크를 직접 하게 된다면 XML로 보는 것을 추천)

base_date : 조회할 날짜 선택(24년 1월 1일은 24240101로 요청해야 하기 때문에 포맷을 맞춰 줘야한다.)

base_time : 조회할 시간 선택(날짜와 같이 포맷을 맞춰줘야 한다.)

nx=?&ny=? : 조회할 동네의 좌표이다. (기상청 어디 잘 찾아보면 표로 잘 정리되어있다.)

 

 

 

 

 

JSON으로 받은 데이터 핸들링은 주석으로 대체하겠다.

 

imgNum에 들어가는 숫자는 db에 저장된 이미지의 정보가 담겨있는 컬럼의 pk이다.

weatherNum에 들어간 정보는 날씨 관련으로 1, 2, 5, 6은 순서대로 비, 비/눈, 빗방울, 빗방울/눈 날림 이것들을 전부 비가 오는 상태로 정의하였고 3(눈), 7(눈 날림) 일 때만 눈으로 정의했다. 그럼 소거법으로 0과 4는 뭐냐 할 텐데 0은 눈이나 비가 오지 않는 상태이고 4는 소나기라 1시간을 보는 데이터에서 금방 그칠 비를 1시간 동안 보여주는 것은 어색하다고 판단하여 제외하였다.

 

 

마치며


이상으로 기상청 api와 스케줄러를 활용하여 날씨 데이터를 핸들링 해보았고 컨트롤러와 서비스 클래스는 여기서 핸들링해서 db에 넣어둔 데이터를 조회하는 것이 끝이기 때문에 따로 작성하지는 않았다.

 

더 자세한 코드를 보고싶다면 https://github.com/qkrwlstn1/-BE-Oeasy/blob/main/src/main/java/com/OEzoa/OEasy/util/scheduler/WeatherScheduler.java

 

-BE-Oeasy/src/main/java/com/OEzoa/OEasy/util/scheduler/WeatherScheduler.java at main · qkrwlstn1/-BE-Oeasy

Contribute to qkrwlstn1/-BE-Oeasy development by creating an account on GitHub.

github.com

여기서 보길바란다.

배포..... 정말 으릅다.... 

 

오늘은 나뿐만이 아닌 다른 사람도 내가 만든 ec2 인스턴스에 접근할 수 있는 2가지 방법을 정리해 보려고 한다.

정리하는 이유는 내가 블로그를 찾아보며 spring boot 프로젝트를 ec2에 배포하는 블로그들 중에 putty나 wsl로 접근하는 방법을

친절하게 정리한 곳을 내가 못 찾아서 기록용과 팀원에게 공유 목적이다.

 

목차

 

1. 키 준비하기

2-1. putty로 접근하기

2-2. wsl로 접근해보

 

1. 키 준비하기

ec2 인스턴스를 생성하며 만들었던 RSA유형의 .pem형식의 키를 준비한다.

 

2-1 Putty로 접근하기

1. puttygen(파일 형식 변환 목적)과 putty를 받아준다.

https://www.puttygen.com/download-putty

putty
puttygen

 

 

 

다운을 완료했다면 puttygen을 실행시킨 후 받은 키를 불러온다.

Load 

 

Load  클릭

 

 

All Files(체크) -> 변환할 .pem키 선택

 

 

이런식으로 나온다면 성공!

 

마지막으로 사진대로 클릭 후 key의 이름만 정해주면 준비는 끝난다.

 

이제 진짜 마지막으로

ec2 인스턴스의  퍼블릭 IPv4 주소를 복사 후

 

 

아까 변환했던 키를 3번을 눌러서 경로를 입력해 주고 Open을 누른다.

나는 ubuntu로 했기 때문에 초기에 따로 설정을 바꾸지 않았다면 이대로 접속하면 끝이다.(Linux라면 ec2-user로 입력하고 들어가면 된다.)

 

2-2 wsl로 접속해보기

wsl은 키를 변환할 필요 없이 그대로 쓴다 방법

wsl의 설치는 다른 블로그를 알아보도록 하자

 

 

우분투를 실행해주고

 

 sudo cp mnt/(.pem이 있는 경로)/AWS_KEY/OEasy-Key.pem /aws/OEasy-key.pem

키를 본인이 쓰기 편한 곳으로 복사해주고

 

권한을 준다

sudo ssh -i 본인키.pem ubuntu@ec2퍼블릭IPv4 순서로 작성하면 접근이 완료된다.

 

 

적용 방법


의존성


implementation 'io.hypersistence:hypersistence-utils-hibernate-60:3.5.1'

build.gradle에 한 줄 추가해준다.

Entity


import
필드

Id로 쓸 필드에 @Tsid를 추가해 준다

끝이다...

혹시 몰라 조금 더 설명해 보자면 TSID가 들어가는 Id 필드는 직접 값을 넣지 않고 시퀀스로 자동 할당을 하던 것처럼 그냥 두면 알아서 자동으로 들어간다 혹시 @GeneratedValue(strategy = GenerationType.IDENTITY) 이 비슷하게 생긴 어노테이션이 있다면 지워주자

이거 왜 써?


솔직히 그냥 트위터에서 쓰는 트위터에서 만든 snowflake와 ULID랑 장점 합쳐서 좋아 보여서 가져왔다.

공간 효율성과 대규모 분산 시스템에서 병렬 처리와 고유성을 보장 또 타임스탬프 값이 앞에 배치되어 정렬을 위한 컬럼이 따로 필요가 없어서이다.

비교


다른 할당 방식은 어떻길래 굳이 TSID를 쓰기로 생각했는지 아래 정리해 보겠다.

자동생성

단일 시스템에서는 자동으로 1부터 하나씩 증가 하도록 해도 큰 문제는 없다.

하지만, 대규모 분산 처리 시스템 기본키(PK)를 생성한다면 기본키가 중복으로 생성될 수 있다. (운이 없다면 소규모 시스템 에서도...)  

 

 

Random()을 이용한 할당

random의 가장 큰 문제는 자원의 낭비가 심하다.

아무리 범위를 크게 잡더라도 중복이 발생할 가능성이 있기 때문에 난수가 중복이 있는지 체크를 해주어야 하고 사용자가 많아지고 서비스 기간이 늘어날수록 중복이 연속으로 3번, 4번 ...N번 발생하지 않으리라는 보장도 없고 이는 서버의 자원을 낭비하게 되는 결과가 발생할 가능성이 높다. 그렇다고 중복을 최소화하겠다고 범위를 매우 크게 잡으면 그거 나름대로 낭비가 심해진다.

 

또한 id를 난수로 할당하기 때문에 정렬을 위한 컬럼을 하나 추가해야 하기 때문에 이것 또한 낭비라고 생각했다.

 

비슷한 이유로 16바이트를 차지하고 랜덤인 UUID도 패스했다

 

ULID

이름만 봐도 UUID와 관련이 있어보인다

UUID은 Universally Unique Identifiers여서 UUID인데 ULID는 Universally Unique Lexicographically Sortable Identifier면서 왜 UULSID가 아니지?

 

ULID는 48비트의 timestamp와 80비트의 랜덤 한 값으로 이루어져있다. 

timestamp가 앞쪽에 배치되어 있어 정렬을 위한 컬럼이 필요 없다는 게 장점이지만 여전히 문자열은 숫자로만 구성되었을 때보다 인덱스에서 삽입 위치를 찾는 것이 더 복잡하고 비효율적일 수 있고, 128비트로 여전히 많은 공간을 차지한다.

 

TSID

 

TSID는 snowflake와 ULID의 아이디어를 결합한 것이 TSID이다.

 

https://github.com/f4b6a3/tsid-creator/ 

 

GitHub - f4b6a3/tsid-creator: A Java library for generating Time-Sorted Unique Identifiers (TSID).

A Java library for generating Time-Sorted Unique Identifiers (TSID). - f4b6a3/tsid-creator

github.com

 

 

TSID의 장점은 

64비트로 UUID, ULID보다 가벼움

정수 사용으로 사용이 편하고 효율적

ULID처럼 timestamp를 사용하여 정렬에 용이

사용이 매우 간단함 

등이 있어서 사용하게 되었다

 

마지막으로 


프로젝트 정도는 자동할당으로 쓰는게 편하고 문제도 크게 없을 것 같긴하지만 DB가 2대 이상만 되어도 문제가 생길 가능성이 많이 올라가기 때문에 이왕 하는김에 TSID를 쓰기로 결정 하였고 무엇보다 쓰는 방법이 쉽고 보안적으로나 성능적으로나 신뢰성으로나 훨신 좋아보이기 때문에 쓰기로 하였고 지금까지 auto increment만 쓰던 나한테는 새로운 경험이었다.

데이터 베이스와 연결을 성공하고

테이블을 create하는 과정에서 문제가 생겼고 jpt한테 물어 봤는데 

 

이런식으로 db에 접근 하라는 것이다...

우리 db이름은

사진처럼 als_ide_db인데 web_ide_project_be_db_1 라는 이름으로 접근 하라길래 어디서 나온 이름이지? 라는 의문이 생겼고 몇 가지 생각이 들었다.

1. 어딘가에서 데이터 베이스 이름을 이렇게 적어놨나? 싶어 application.yml과 compose 파일을 살펴본 결과 어디에도 저런 이름은 없었다.

 

2. web_ide는 우리 프로젝트 이름이니까 뒤에 de_db_1만 구글링을 해봤다

결과는 역시나 실패였다.

 

3. jpt한테 물어보기로 했다

 

역시나 gpt였다

최근 4o로 업데이트 하더니 체감이 굿이다

 

이름의 출처는 확인됐고 다음은 왜 create 가 안됐나 봤더니 권한이 없어서 였고

 

 

 

 

 

 

 

 

 

 

 

 

root와 user에 권한을 주고 다시 배포를 시도했더니

배포시도
성공로그

위와 같이 테이블도 잘 만들고 마지막으로 배포가 잘 됐는지까지 확인할 수 있었다.

 

 

 

느낀점 : 크램폴린에서 1주일 고통받고 아마존으로 하루 만에 끝내니까 글로벌 대기업은 역시 대단하다 라는 생각이 들었다

 

--------------------------------------------------------------추가-----------------------------------------------------------------

 

다음날 쉬고 있다가 갑자기 인코딩은 잘 됐나?? 확인을 안 해봤는데.... 하고 확인했더니 역시나 ????로 나오고 있었다.

그래서 show databases 로 db이름을 확인하고

 

show tables 로 테이블 이름도 확인한 다음 인코딩을 확인할 수 있는 테이블을 조회해 준다.

 

select * from chatting_message를 조회한 결과 인코딩이 안돼서 ???로 떠서

set names 'urf8mb4'로 인코딩을 바꿔더니 27번째 튜플에 일부가 아직 덜 바뀐 걸 확인해서

 

db, db 테이블, db 컬럼 등 확인했으나 별문제는 없어 보였다.

 

 

그래서 혹시나 set names로 다시 인코딩을 설정했더니 ???? 왠진 모르겠지만 됐다... 왜 된 거야

'spring' 카테고리의 다른 글

ec2인스턴스에 접근하기 (putty, wsl)  (0) 2024.10.30
[DB][SpringBoot]PK - TSID 할당 방법  (0) 2024.06.05

+ Recent posts