조건식에 괄호 ( ) 사용하지 않음

// ❌ 잘못된 예
if (x > 10) {
fmt.Println("크다")
}

// ✅ 올바른 예
if x > 10 {
fmt.Println("크다")
}

중괄호 { }는 반드시 같은 줄에 위치해야 함

// ❌ 잘못된 예
if x > 10 
{
fmt.Println("크다")
}

// ✅ 올바른 예
if x > 10 {
fmt.Println("크다")
}

 

if 안에서 변수를 선언할 수 있음

if n := len(name); n > 3 {
fmt.Println("이름이 너무 깁니다:", n)
} else {
fmt.Println("이름 길이 OK:", n)
}

 

else는 반드시 같은 줄에 위치해야 함

// ❌ 잘못된 예
if x > 10 {
fmt.Println("크다")
}
else {
fmt.Println("작다")
}

// ✅ 올바른 예
if x > 10 {
fmt.Println("크다")
} else {
fmt.Println("작다")
}

 

if문에서 여러 조건을 쓸 때는 &&, || 사용

if age > 18 && country == "KR" {
fmt.Println("성인입니다.")
}

if score < 60 || attend < 0.8 {
fmt.Println("불합격")
}

 

728x90

'Golang > 기본 문법' 카테고리의 다른 글

기본 타입은 모두 소문자  (0) 2025.10.08
블로그 이미지

Link2Me

,

Go의 기본 타입들은 모두 소문자로 시작

 

package main

import "fmt"

func main() {
var a int
var b string // ✅ 소문자 string으로 수정

a = 7
b = "hello"

fmt.Println("a:", a)
fmt.Println("b:", b)
}

 

 

package main

import "fmt"

var globalVar = "전역 변수" // 패키지 전역 변수 ←  main 함수 밖에서 선언, 모든 함수에서 접근 가능

func main() {
var a int = 10             // 명시적 선언 : var 키워드와 type을 명시적으로 선언

var y = 10                 // 타입을 생략하면 Go가 자동 추론
b := "hello"               // 짧은 선언 : 함수 내에서만 사용 가능 (var 없이 자동 타입 추론)
const PI = 3.14159         // 상수
nums := []int{1, 2, 3, 4}  // 슬라이스
person := map[string]int{"age": 30, "height": 175} // 맵

fmt.Println(globalVar)
fmt.Println(a, b, PI)
fmt.Println(nums)
fmt.Println(person)
}

728x90

'Golang > 기본 문법' 카테고리의 다른 글

if 문 사용 주의사항  (0) 2025.10.08
블로그 이미지

Link2Me

,

말을 예쁘게 하자.

결혼은 외부와 내부의 연합

- 외부인의 환경과 조건을 안 볼 수는 없다.

- 부모로서 가장 중요한 것은 안정감

- 사위를 향한 표현은 삶을 돌아보며 느낀 '불안감'이었을 것

 

 

여성들의 결혼 대상 1순위 덕목

- 장모님과 아내가 원하는 건 안정감

 1. 사회적 안정감 : 직장

 2. 경제적 안정감 : 돈 얼마나 벌어?

 3. 정서적 안정감 : 나한테 꾸준히 잘 하느냐?

 

영혼의 언어는 반복된 행동

 

관계에서 중요한 것은 예뻐 보이는 것

 

 

나 조금 힘드네....

자기 만나서 힘든 난 안 힘들까?

 

그럼에도 불구하고

 

 

 

728x90

'세상사' 카테고리의 다른 글

내 군복과 다이어리  (0) 2021.10.22
자매가 한 남자와 결혼?  (0) 2014.02.19
블로그 이미지

Link2Me

,

AI의 등장과 LLM의 발전으로 자연어로 대화하듯 코딩할 수 있는 세상이 도래되었다.

개발자들은 이제 보일러플레이트 고드 작성, 단순한 함수 구현, 문서화 작업 등을 LLM에 맡기고, 시스템 아키텍처 설계, 비즈니스 로직 구현, 사용자 경험 개선 등 창의적이고 전략적인 업무에 시간을 투자해야 하는 시대가 되었다.

 

chatGPT를 주로 사용하다가 코딩경력 40년된 분으로부터 claude code 가 좋다고 해서 claude 를 사용해보기 시작했다.

 

chatGPT 와 다르게 claude 는 코드를 제대로 구현해주는 느낌을 받았다.

그러나 문제가 좀 있는 것은.... 

함수가 없어서 새로 구현해야 한다고 의견 제시를 하면 좋은데, 구현되어 있는 함수로 최대한 활용하여 코드를 구현하려는 못된 습성이 있다.

Web 에서 질문했을 때 무조건 새로운 대화창에 다시 문의하는 것이 대안이다. chatGPT에게 문의하면 오히려 문제가 해결될 수도 있다. 여러번 당해봐서 이제는 어떻게 해야 할지 알게 되었다.

claude code 에서는 ESC 키를 눌러서 중단하고, 명확하게 요구사항을 다시 작성해서 요구하는 것이 좋다.

 

claude 는 Web 상에 코드를 붙여넣고 원하는 걸 구현 요청하다보니 토큰 소모가 claude code 보다 더 많은 거 같다.

장점도 당연히 있다. 구현해준 코드를 확인하면서 다시 수정 요청하기가 편한 경우가 있다.

 

Back-End 코드를 구현하기 위해서는 먼저 테이블을 설계하고 테이블 구조를 claude에 알려줘야 한다.

테이블 설계는 chatGPT 와 claude 둘 다 활용하면서 진행해 봤다.

테이블 설계를 한 후, 또는 설계된 테이블을 수정하면서 Spring Boot 에 필요한 RestController, Service, Mybatis mapper 등을 claude 가 알아서 구현해 준다.

JPA + QueryDSL 코드인 경우에도 잘 구현된 샘플이 매우 중요하다.

 

맨땅에서 헤딩하면서 잘 된 코드를 기대해서는 안된다. 잘 구현된 샘플코드가 매우 중요하다.

샘플코드는 Layout 등 템플릿 구조가 잘 설계된 코드를 말한다.

잘 만들어진 템플릿을 구현하거나, 얻어서 테스트하고 내것으로 만드는 것이 중요한 거 같다.

설계를 잘 하기 위해서는 발주처의 요구사항을 분석하고 로직을 잘 그릴 줄 알아야 한다.

 

claude code는 기존 구조를 스스로 파악해서 요청하는 신규 코드를 잘 구현한다.

Web 처럼 샘플코드를 찾아서 구조 설명하면서 이런 구조로 구현해 달라고 하지 않는 점이 장점이다. 경로만 잘 인식하도록 지정해주면 알아서 그 코드를 분석하고 신규 코드를 구현한다.

그런데 구체적으로 요청하지 않으면 추론으로 쓸데없는 함수 구현을 많이 해주는 경향이 있다.

처음에는 명확하게 CRUD 만 구현할 것을 요청하는 것이 중요한 거 같다.

그런 다음에 Front-End 코드를 구현할 사항을 정의하여 claude code 에 제공하고 구현 요청한다.

당연히 관련된 백엔드 Controller 가 뭔지, 어떤 폴더에 있는지 알려줘야 한다.

Vue3 로 구현 시 잘 구현된 다른 폴더의 샘플을 제시하면 분석해서 순식간에 코드를 생성한다.

하지만, 코드가 완성도 있게 잘 구현되어 있기를 기대해서는 안되더라.

Spring Boot 를 구동시키고, Vue3 를 구동시켜 테스트 하면서 하나씩 기능을 점검하고 추가하는 작업을 해야 원하는 결과를 빠르게 구현할 수 있다.

막연히 알아서 구현해주겠지 하고 기대했다간 큰 코 다칠 수 있다는 걸 명심하자.

 

claude code 를 사용하고 exit 하기 전에 반드시 "claude.md 에 저장해줘. 그리고 다음에 이어서 할 수 있게 정리해줘" 라고 하고 나서 exit 하자.

"Vue3, Spring Boot, Android git commit 해줘" 라고 하자. 그러면 잘못 개발 진행되면 되돌릴 수 있다.

Front-End 로 React 를 사용하면 React git commit 해줘 라고 하면 된다.

코드를 대폭 수정해야 할 사항으로 진행하려고 한다면 반드시 git commit 을 먼저하고 나서 진행하는 것이 필수다.

그렇지 않으면 중도에 되돌리는데 애로사항이 있을 수 있다.

 

DB 연결 정보를 mcp mariadb 라고 claude code 에서 요청하면 해당 DB 접속정보를 만들어 낸다.

그걸 반드시 테스트 해서 처리해야 한다. DB 테이블 저장 데이터, 테스트하는 user_id 등을 알려주고 테이블명 확인 요청 등을 하면서 데이터 잘못 저장하는지 확인 등 하면 좋다.

그런데 신규 프로젝트를 생성하고 기존에 사용하던 .mcp.json 파일을 복사해서 DB 내용을 수정하고 claude Code 를 실행하면 접속이 자동으로 될 것으로 기대를 했으나 그렇지 않다.

몇번의 시행착오를 거치면서 알아낸 사실은 MCP 서버 자체의 env 파일은 다른 DB로 설정되어 있을 수 있으니 반드시 claude 에게 확인해서 정확한 접속이 되도록 설정을 하는 것이 중요하다.

항목
.env 파일 c:\DEV\mcp-servers\mariadb-mcp\nindv.env
DB_NAME  nindv
.mcp.json 위치 c:\AndroidStudioProjects\Logis\nindv\vue3\nindv-ui\.mcp.json
MCP 서버 이름 mariadb-nindv
Claude Code 시작 위치 c:\AndroidStudioProjects\Logis\nindv\vue3\nindv-ui
테스트 쿼리 SELECT * FROM hr_members WHERE user_id = 'admin01';

 

 

 

Web 브라우저 콘솔 창에서 발생하는 로그를 일일히 붙여하면서 요청하는 것이 너무 귀찮아서 찾아보니 playwright mcp 를 설정해서 사용하면 편하다고 되어 있다.

"playwright를 사용해서 https://www.naver.com 에 접속하고 스크린샷을 찍어줘"

"playwright로 구글에서 'MCP'를 검색하고 결과를 가져와줘"

"playwright를 이용해서 로그인 폼을 테스트하는 스크립트 만들어줘"

 

mcp playwright 로 스스로 Web 브라우저를 띄우고 알려준 user_id, pw 정보를 토대로 로그인 처리까지 하고 직접 콘솔 에러로그를 확인하면서 잘못된 것을 수정하고, 백엔드코드를 수정하는 걸 경험했다. 신세계를 경험하는 느낌이었다.

일일이 설명하기 정말 귀찮은데 알아서 분석하는 것을 보니까 놀랍더라.

개발서버에서 개발한 코드를 CI/CD 로 자동으로 서버에 업로드하고 배포하는 걸 스크립트화 했더니 그 파일을 가지고 스스로 서버에 배포하면서 확인까지 하더라.

 

 

장점

1. Vue3 프로젝트 폴더에서 백엔드 폴더 경로 알려주면 백엔드 파일도 같이 수정한다.

    한번에 Front-End 와 백엔드 코드를 개발하면서 진행하기 때문에 매우 편리하다.

2. 남들이 개발한 코드 분석하라고 하면 엄청 잘 한다. 세부적으로 잘하는 지 여부는 모르겠으나 전체 관점 로직 분석 짱.

3. Github 등 공개된 오픈 소스 분석해서 내가 구현하고 싶은 걸 요청하는 거 정말 잘한다.

4. 언어 상관없이 소스 분석 잘 한다. 그러니 내가 아는 Open 소스를 구하려고 하지 않아도 된다.

5. 템플릿이 잘 구성된 코드 기반으로 살을 붙여가는 방식으로 코드 구현은 매우 좋은 거 같다.

6. 이미지 분석을 정말 잘한다. 설명하기 모호하면 캡쳐해서 이미지 경로 알려주고 설명 적어주는게 빠르다.

   이해를 잘못하고 엉뚱 한 걸 수정하면 이미지 캡쳐해서 저장하고 경로 알려주면 바로 이해하더라.

7. "Android 코틀린 앱의 채팅과 동일한 구조로 Vue3 채팅 메뉴를 구현해줘" 라고 하면 Android kotlin 코드를 분석해서 Vue3 채팅 기능을 구현해준다.

 

단점

1. 분석을 100% 했을 거라고 기대하지만 막상 구현된 코드를 테스트하면 대략적인 흐름만 구현한다.

    즉 디테일한 요구사항까지 분석해서 코드를 구현하지 않더라.

2. 하나의 프로젝트를 거의 완벽하게 구현한 코드를 95% 활용하고 싶어서 분석을 요청하면 대충 분석을 한다.

    기존 코드와 약간 다른 점, DB가 달라지는 점 등을 제대로 설명하기 위한 방법을 찾으려고 한다.

    mWeb 기능 @c:\DEV\Vue3\pchat-ui\ 프로젝트 참조해서 구현해줘. 라고 했더니 엉뚱하게 처리하더라.

    제대로 분석 안했다고 하니까 그때서야 다시 분석하고 100% 그대로 복사하려는 경향을 보인다.

    결국 어떻게 알려줘야 제대로 할 것인가의 문제인 거 같다. 

3. Spring Boot 는 백엔드 제어권을 내가 가지고 에러 메시지를 확인하면서 피드백해야 빠르다.

    제어권 넘기고 알아서 로그 분석하면서 할 것으로 기대하면 안된다.

4. 중간에 변수명 변경한 것이 있으면 꼭 알려줘야 한다. 그리고 그와 관련된 모든 코드를 점검하라고 해야 한다.

    알아서 변경했겠거니 했다가 완전 맨붕상태 빠질 수 있다.

    chatGPT는 마지막 파일을 Web 에 붙여넣으면 그 코드 기준으로 변수명을 업데이트하는데

    claude 는 초기 읽어들인 변수명을 고집하는 경향이 있다. 그러므로 변수명 변경사항은 꼭 알려줘야 한다.

5. 전체 공통으로 영향을 주는 코드 파일이 있다. 하지만 무슨 문제가 생기면 이 파일도 수정해버리는 경향이 있다.

    반드시 이 파일은 수정 불가라고 알려줘야 한다.

6. 문제가 발생해서 에러 로그를 제공하면 문제 분석을 특정 부분으로 유추하지 못하고 엉뚱한 것부터 수정하려는 경향이 있다. 그러므로 이상한 분석을 하는 거 같으면 ESC 키를 눌러서 바로 중단하고 요청사항을 다시 적어 요청 수정해야 한다.

7. claude code 에서 이미지 경로를 알려주면 해당 이미지를 분석하고 그대로 구현하라고 하는 줄 알고 무조건 변경하는 경향이 있다. 이미지 경로를 알려주고, 여기서 원하는 것이 무엇인지 확인한 다음에 원하는 것이 정확하면 그때 수정하도록 하는 것이 중요하더라. Front-End 한 화면을 구현하기 위해서 이미지를 캡쳐하여 이미지 경로를 알려줄 때도 필요한 구체적인 요청을 먼저 확인한 다음에 개발 요청해야 한다.

 

아직 claude code 사용 초보라서 모르는 사항이 많아 잘못 알고 있을 수도 있다.

claude code 를 사용하면서 chatGPT 에게 문의하는 비율은 10%도 안되는 거 같다.

claude code 발전 속도가 너무 대단한 거 같다.

 

바이브 코딩에 대한 유투브 소개 자료를 찾아보니, 설계를 잘하는 기술사는 바이브코딩 수혜자라고 한다.

점점 단순한 코딩만 하는 Junior 개발자의 역할은 AI 가 대신하고 기획하고, 설계할 줄 아는 인력에게 호재라고 할 수 있는 셈인가 보다.

 

설계를 잘 한다는 것은 VOC 가 발생하지 않도록 신경써서 기획을 해야 하고, 지속 검토하면서 기획서를 완성시켜 가야 한다고 말할 수 있다. 이렇게 하면 무슨 문제가 발생할까, 저렇게 하면 무슨 문제가 발생할까 계속 검토하고 고민하면서 테스트하다보면 문제점 ZERO에 가까운 설계/기획서가 작성될 것이다. 출시후에도 민원처리부서의 요청사항, 고객의 소리 등을 분석하고 문제점을 찾아서 신속하게 대응하는 것 또한 매우 중요하다.

코딩을 할 때에도 마찬가지이다. 디테일한 로직을 그리고 User의 관점에서 보는 눈을 길러야 한다.

바이브코딩으로 해결하기 쉽지 않는 것이 보안 코딩이다. 질문자가 초보수준이면 답변도 초보 답변을 받을 수 밖에 없다.

그럼 초보탈출은 어떻게 할 것인가? 인프런 강의 등을 듣고 직접 코딩 연습을 하듯이 타이핑을 했다면, 이제는 인프런 강의를 듣고 전체 로직을 이해하고 코딩은 AI에게 요청해서 제대로 구현했는지 확인하는 것이다.

중요한 정보라고 판단되는 것은 notion 에 기록하던, 블로그에 기록하든 정리를 해두는 것이 매우 중요하다.

사람은 망각의 동물이라 배운 것을 금방 잊어버리게 된다. 기록해서 자주 반복하여 보거나, 코드를 직접 구현하면서 부딪쳐가면서 배우는 것이 기억에 오래도록 남는다.

전문가가 되려면 몸으로 부딪쳐 가면서 배우는 수밖에 없는 거 같다. 아직 많이 부족하여 틈틈히 인프런 강의, fastcampus 강의를 수강하고 실습하고 있다. 

배움에는 끝이 없다. 나이는 중요하지 않다. 노력하는 자만이 성장한다.

 

728x90
블로그 이미지

Link2Me

,

IntelliJ/IDEA 내 JDK 설정 확인

  • File > Project Structure > Project > Project SDK
    File > Project Structure > Modules > Module SDK
    File > Settings > Build, Execution, Deployment > Build Tools > Gradle > Gradle JVM
    모두 Java 17로 설정되어야 합니다.

 

$env:JAVA_HOME="C:\Program Files\OpenLogic\jdk-17.0.14.7-hotspot"
$env:PATH="$env:JAVA_HOME\bin;" + $env:PATH
java -version

728x90

'Spring Boot > IDEA' 카테고리의 다른 글

--add-opens java.base/java.lang=ALL-UNNAMED  (0) 2025.04.11
Spring Boot 테스트 Class 자동 생성 단축키  (0) 2025.04.10
Spring Boot Build 하기  (0) 2025.03.08
IntelliJ IDEA Auto Import 설정  (0) 2025.03.01
Build Gradle Update  (0) 2025.02.27
블로그 이미지

Link2Me

,

mariaDB 에서 PostgreSQL 로 변경하고 나서 데이터를 추가하니 이런 에러가 발생한다.

PK 시퀀스가 자동으로 증가된 값을 인지하지 못해서 생기는 현상이라고 한다.

 

1
2
3
4
5
6
2025-06-04T13:32:47.652+09:00  WARN 22624 --- [mall] [nio-8080-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 0, SQLState: 23505
2025-06-04T13:32:47.652+09:00 ERROR 22624 --- [mall] [nio-8080-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper   : 오류: 중복된 키 값이 "menu_orgchart_pkey" 고유 제약 조건을 위반함
  Detail: (id)=(7) 키가 이미 있습니다.
2025-06-04T13:32:47.658+09:00 ERROR 22624 --- [mall] [nio-8080-exec-6] c.m.s.a.m.a.s.filter.JWTCheckFilter      : JWT Check Failed: Request processing failed: org.springframework.dao.DataIntegrityViolationException: could not execute statement [오류: 중복된 키 값이 "menu_orgchart_pkey" 고유 제약 조건을 위반함
  Detail: (id)=(7) 키가 이미 있습니다.] [insert into menu_orgchart (depth,name,parent_id,textvalues (?,?,?,?)]; SQL [insert into menu_orgchart (depth,name,parent_id,textvalues (?,?,?,?)]; constraint [null]
 

 

해결 방법은 아래와 같다.

PostgreSQL 에 접속한 다음에 해당 DB에 접속한다.

아래 명령어를 한번만 해주면 해결된다.

 

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
DO $$
DECLARE
    rec RECORD;
BEGIN
    -- SERIAL, BIGSERIAL 컬럼의 시퀀스와 테이블/컬럼 정보를 얻어서
    FOR rec IN
        SELECT
            s.relname AS sequence_name,
            t.relname AS table_name,
            a.attname AS column_name
        FROM
            pg_class s
        JOIN pg_depend d ON d.objid = s.oid
        JOIN pg_class t ON d.refobjid = t.oid
        JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid
        WHERE
            s.relkind = 'S'
            AND d.deptype = 'a'
    LOOP
        -- setval을 수행
        EXECUTE format(
            'SELECT setval(''%I'', COALESCE((SELECT MAX(%I) FROM %I), 1))',
            rec.sequence_name,
            rec.column_name,
            rec.table_name
        );
    END LOOP;
END$$;
 

 

 

 

 

 

728x90
블로그 이미지

Link2Me

,

Spring Boot 에서 MariaDB를 연동하여 테스트하고 있는데, PostgreSQL 로 변동하려면 칼럼을 전부 소문자로 변경해야 하더라. JPA 에서의 변수를 userID 이렇게 사용하던 걸 userId 로 변경해야 DB 칼럼에는 user_id 로 연동되어 처리된다.

 

 

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
#############################################
######### RockeyOS 9.5 ##########
#############################################
# ripgrep 설치
sudo dnf install ripgrep
 
# Spring Boot 코드 일괄 변경
# DB 테이블을 PostgreSQL 변경을 고려하여 칼럼명을 snake_case 로 변경하면서 
# members 테이블 칼럼을 일괄 변경하고 나서 관련된 Entity를 수정한다.
# 이후에 아래 코드로 관련된 함수 등을 일괄 변경 시도한다.
 
find . -type f -name "*.java" -exec sed -i \
    -'s/\buserID\b/userId/g' \
    -'s/\buserNM\b/userNm/g' \
    -'s/\btelNO\b/telNo/g' \
    -'s/\bmobileNO\b/mobileNo/g' \
    -'s/\bphoneSE\b/phoneSe/g' \
    -'s/\bcodeID\b/codeId/g' \
    -'s/\bregNO\b/regNo/g' \
    {} +
 
 
# 적용후 확인 명령어
rg 'userID|userNM|telNO|mobileNO|phoneSE|codeID|regNO' --glob '*.java'
 

 

 

728x90
블로그 이미지

Link2Me

,

DB 칼럼에 중요한 정보는 암호화 저장을 해야 한다.

처음에 평문으로 구성했던 칼럼을 암호화 업데이트 하기 위해서 아래와 같이 임시 적용한 코드를 적어둔다.

 

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
public interface MemberRepository extends JpaRepository<Member, Long> {
 
    // 모든 회원 평문 mobile_no만 조회 (필요에 따라 select 절 수정)
    @Query("SELECT m FROM Member m WHERE m.mobileNo IS NOT NULL AND m.mobileNo <> ''")
    List<Member> findAllWithMobileNo();
}
 
 
@Service
@RequiredArgsConstructor
public class MemberMobileEncryptService {
    private final MemberRepository memberRepository;
    private final EncryptService encryptService;
 
    @Transactional
    public int encryptAllMobileNo() {
        String userName = SafeDBKeyConfig.userName;
        String tableName = SafeDBKeyConfig.tableName;
        String columnName = SafeDBKeyConfig.columnName_mobileNo;
 
        // 1. 평문 mobile_no가 있는 회원 모두 조회
        List<Member> memberList = memberRepository.findAllWithMobileNo();
 
        int updated = 0;
        for (Member member : memberList) {
            String plainMobile = member.getMobileNo();
            if (plainMobile == null || plainMobile.isBlank()) continue;
 
            // 2. 암호화
            String encrypted = encryptService.encrypt(userName, tableName, columnName, plainMobile);
 
            // 3. update (변경 감지, JPA 자동 저장)
            member.setMobileNo(encrypted);
            updated++;
        }
        // 트랜잭션 끝나면 JPA flush/commit
        return updated;
    }
}
 
@SpringBootTest
public class EncryptMobileNoTest {
 
    @Autowired
    private MemberMobileEncryptService mobileEncryptService;
 
    @Test
    public void testEncryptAllMobileNo() {
        int count = mobileEncryptService.encryptAllMobileNo();
        System.out.println("암호화 적용 회원 수: " + count);
    }
}
 

 

 

 

728x90
블로그 이미지

Link2Me

,

스프링 부트에서 이미지 처리시 주의사항이 되어버린 셈이다.

 

확장자 없는 요청을 아래와 같이 수정하고 정상동작함
// 확장자 없는 요청 처리 (jpg, png, gif 등 순회 탐색)
    @GetMapping("/{memberId:[0-9]+}")
    public ResponseEntity<Resource> getPhoto(@PathVariable String memberId)

// 확장자 포함 요청 처리 (예: /api/member/photo/10.png)
    @GetMapping("/{memberId}.{ext}")
    public ResponseEntity<Resource> getPhotoWithExt

 

728x90
블로그 이미지

Link2Me

,

fetch join은 Spring Boot + JPA (Hibernate) 환경에서 지연 로딩(LAZY) 관계의 데이터를 한 번의 쿼리로 함께 조회할 때 사용하는 방법

JPA에는 엔티티에 관계를 맵핑할 때 지연 로딩과 즉시 로딩 설정할 수 있다.
일반 Join : 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는 오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화한다.
Fetch Join : 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화

Member Entity와 Team Entity가 @ManyToOne 일 때 Entity 클래스에 서로의 관계를 표시해 준다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    @ToString.Exclude
    private Team team;
}


Fetch Join 을 사용해야 하는 상황
1. @ManyToOne, @OneToOne 연관 관계의 객체가 반복적으로 로딩될 때
   @Query("SELECT m FROM Member m JOIN FETCH m.team")
   List<Member> findAll();  // N+1 문제 해결

2. JPQL 또는 QueryDSL로 다대일 관계를 한 번에 조회하고자 할 때
   @ManyToOne, @OneToOne 관계는 fetch join으로 쉽게 한 쿼리에 묶을 수 있음

Fetch Join을 사용하면 안 되는 경우
1. 컬렉션(@OneToMany, @ManyToMany)에 페치 조인을 사용하면서 페이징이 필요한 경우
   JPA는 fetch join이 있는 경우 Pageable 정보를 무시하거나, 메모리에서 잘라내기 때문에 성능 이슈 발생
   @BatchSize 또는 별도 쿼리

2. 복수 개의 컬렉션을 동시에 Fetch Join 할 때
   @Query("SELECT d FROM Department d JOIN FETCH d.employees JOIN FETCH d.projects") 
   여러 컬렉션이 곱해져서 결과가 기하급수적으로 커질 수 있음
   해결 방법: 컬렉션 하나만 fetch join, 나머지는 batch fetch 사용

728x90
블로그 이미지

Link2Me

,

성능 최적화 요소까지 완벽한 고려는 아니지만 코드는 많이 정리된 거 같아서 적어둔다.

React 에서는 pk 값이 직접 노출되지 않도록 암호화처리하고, 대신에 no 를 넣어서 처리했다.

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
81
82
83
84
85
86
87
88
89
90
91
92
@Repository
@Transactional
@RequiredArgsConstructor
@Log4j2
public class ProductSearchImpl implements ProductSearch {
 
    private final JPAQueryFactory queryFactory;
    private final CryptoUtil cryptoUtil;
 
    @Override
    public Page<ProductDTO> searchList(PageRequestDTO pageRequestDTO) {
        QProduct product = QProduct.product;
        QProductImage productImage = QProductImage.productImage;
 
        SearchConditionBuilder<Product> scb = new SearchConditionBuilder<>(Product.class"product");
 
        String where = pageRequestDTO.getFilter().getWhere();
        String keyword = pageRequestDTO.getFilter().getKeyword();
 
        OrderSpecifier<?> orderSpecifier = product.pno.desc(); // 기본값: pno 내림차순
 
        if (keyword != null && !keyword.isBlank()) {
            switch (where) {
                case "pname" -> scb.addLike("pname", keyword);
                case "desc" -> scb.addLike("pdesc", keyword);
                case "price" -> {
                    try {
                        int minPrice = Integer.parseInt(keyword);
                        scb.addGreaterThanEqual("price", minPrice);
                        orderSpecifier = product.price.asc(); // 가격 오름차순 정렬
                    } catch (NumberFormatException e) {
                        log.warn("가격 필터 숫자 변환 실패: {}", keyword);
                    }
                }
            }
        }
 
        BooleanBuilder builder = scb.build();
 
        Pageable pageable = PageRequest.of(
            pageRequestDTO.getPage() - 1,
            pageRequestDTO.getSize()
        );
 
        List<Product> productList = queryFactory
            .selectFrom(product)
            .leftJoin(product.imageList, productImage).fetchJoin()
            .where(product.delFlag.eq(false)
                .and(productImage.ord.eq(0))
                .and(builder))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(orderSpecifier)
            .fetch();
 
        long totalCount = queryFactory
            .select(product.count())
            .from(product)
            .leftJoin(product.imageList, productImage)
            .where(product.delFlag.eq(false)
                .and(productImage.ord.eq(0))
                .and(builder))
            .fetchOne();
 
        List<ProductDTO> dtoList = IntStream.range(0, productList.size())
            .mapToObj(i -> {
                Product entity = productList.get(i);
                int no = (int) (totalCount - (pageable.getPageNumber() * pageable.getPageSize()) - i);
                return toDTO(entity, no);
            }).toList();
 
        return new PageImpl<>(dtoList, pageable, totalCount);
    }
 
    private ProductDTO toDTO(Product product, int no) {
        String encryptedPno = cryptoUtil.encryptAES(String.valueOf(product.getPno()));
 
        List<String> fileNames = product.getImageList().stream()
            .map(ProductImage::getFileName)
            .toList();
 
        return ProductDTO.builder()
            .no(no)
            .pno(encryptedPno)
            .pname(product.getPname())
            .price(product.getPrice())
            .pdesc(product.getPdesc())
            .uploadFileNames(fileNames)
            .build();
    }
}
 
728x90

'Spring Boot > Basic' 카테고리의 다른 글

Spring Boot 이미지 로딩 문제  (0) 2025.05.24
Spring Boot fetch join  (0) 2025.05.19
Spring Boot 3.4.5 QueryDSL 예제 개선  (0) 2025.05.04
Spring Boot 3.4.5 QueryDSL 예제  (0) 2025.05.04
Spring Boot 3.4.5 JOOQ TodoSearch 예제  (0) 2025.05.03
블로그 이미지

Link2Me

,

QueryDSL 이전 게시글의 최적화 버전을 chatGPT 도움으로 변경한 것이다.

 

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
 
@Service
@Transactional
@Log4j2
@RequiredArgsConstructor // 생성자 자동 주입
public class AccessLogSearchImpl implements AccessLogSearch {
    private final JPAQueryFactory queryFactory;
    private final ErrorCodeRepository errorCodeRepository;
 
    @Override
    public Page<AccessLogDTO> search1(PageRequestDTO pageRequestDTO) {
        QAccessLog accessLog = QAccessLog.accessLog;
        BooleanBuilder builder = new BooleanBuilder();
 
        String where = pageRequestDTO.getFilter().getWhere();
        String keyword = pageRequestDTO.getFilter().getKeyword();
 
        log.info("검색 필터 where = {}, keyword = {}", where, keyword);
 
        if (keyword != null && !keyword.trim().isEmpty()) {
            buildSearchCondition(builder, accessLog, where.trim(), keyword.trim());
        }
 
        Pageable pageable = PageRequest.of(
                pageRequestDTO.getPage() - 1,
                pageRequestDTO.getSize(),
                Sort.by("uid").descending()
        );
 
        JPAQuery<AccessLog> query = queryFactory
                .selectFrom(accessLog)
                .where(builder)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(accessLog.uid.desc());
 
        List<AccessLog> resultList = query.fetch();
 
        long totalCount = queryFactory
                .select(accessLog.count())
                .from(accessLog)
                .where(builder)
                .fetchOne();
 
        // errorCode 메시지 전체 Map으로 미리 조회 (N+1 제거)
        Map<Integer, String> errorMap = errorCodeRepository.findAll().stream()
                .collect(Collectors.toMap(ErrorCode::getCodeId, ErrorCode::getCodeNm));
 
        List<AccessLogDTO> dtoList = IntStream.range(0, resultList.size())
                .mapToObj(i -> {
                    AccessLog entity = resultList.get(i);
                    int no = (int) (totalCount - (pageable.getPageNumber() * pageable.getPageSize()) - i);
                    return toDTO(entity, no, errorMap);
                })
                .collect(Collectors.toList());
 
        return new PageImpl<>(dtoList, pageable, totalCount);
    }
 
    private void buildSearchCondition(BooleanBuilder builder, QAccessLog accessLog, String where, String keyword) {
        switch (where) {
            case "userID" -> builder.and(accessLog.userid.containsIgnoreCase(keyword));
            case "userNM" -> builder.and(accessLog.userNM.containsIgnoreCase(keyword));
            case "ipaddr" -> builder.and(accessLog.ipaddr.containsIgnoreCase(keyword));
            case "route" -> builder.and(accessLog.route.stringValue().containsIgnoreCase(keyword));
            case "errorCode" -> {
                List<Integer> codeIds = errorCodeRepository.findCodeIdsByCodeNmLike(keyword);
                if (!codeIds.isEmpty()) {
                    builder.and(accessLog.errCode.in(codeIds));
                } else {
                    builder.and(accessLog.errCode.eq(-9999)); // fallback
                }
            }
            case "accessDate" -> {
                String[] parts = keyword.split("/");
                if (parts.length == 2) {
                    String from = parts[0].trim();
                    String to = parts[1].trim();
                    if (from.length() == 8 && to.length() == 8) {
                        if (from.compareTo(to) > 0) {
                            String temp = from;
                            from = to;
                            to = temp;
                        }
                        builder.and(accessLog.date.between(from, to));
                    }
                } else {
                    builder.and(accessLog.date.startsWith(keyword));
                }
            }
            default -> {
                Set<String> allowedFields = Set.of("userid""userNM""ipaddr""browser""os""date");
                if (allowedFields.contains(where)) {
                    PathBuilder<AccessLog> pathBuilder = new PathBuilder<>(AccessLog.class"accessLog");
                    builder.and(pathBuilder.getString(where).containsIgnoreCase(keyword));
                } else {
                    log.warn(" 잘못된 where 필드명: '{}'. 검색 조건 무시", where);
                }
            }
        }
    }
 
    private AccessLogDTO toDTO(AccessLog entity, int no, Map<Integer, String> errorMap) {
        String errorMessage = errorCodeRepository
                .findMessageByCode(entity.getErrCode())
                .orElse(String.valueOf(entity.getErrCode()));
 
        return AccessLogDTO.builder()
                .no(no)  //  추가
                .uid(entity.getUid())
                .ipaddr(MaskingUtil.ipAddressMasking(entity.getIpaddr()))
                .date(InputSanitizer.displayDate(entity.getDate()))
                .time(entity.getTime())
                .OS(entity.getOs())
                .browser(entity.getBrowser())
                .userid(MaskingUtil.idMasking(entity.getUserid()))
                .userNM(MaskingUtil.letterMasking(entity.getUserNM()))
                .success(entity.getSuccess())
                .route(entity.getRoute())
                .errCode(entity.getErrCode())
                .errorMessage(errorMap.getOrDefault(entity.getErrCode(), String.valueOf(entity.getErrCode())))
                .build();
    }
 
}
 
 

 

 

728x90
블로그 이미지

Link2Me

,

접속로그를 동적 쿼리로 검색하는 걸 구현하는 예제를 적어둔다.

 

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@Service
@Transactional
@Log4j2
@RequiredArgsConstructor // 생성자 자동 주입
public class AccessLogSearchImpl implements AccessLogSearch {
    private final JPAQueryFactory queryFactory;
    private final ErrorCodeRepository errorCodeRepository;
 
    @Override
    public Page<AccessLogDTO> search1(PageRequestDTO pageRequestDTO) {
        QAccessLog accessLog = QAccessLog.accessLog;
 
        BooleanBuilder builder = new BooleanBuilder();
 
        String where = pageRequestDTO.getFilter().getWhere();
        String keyword = pageRequestDTO.getFilter().getKeyword();
 
        log.info("검색 필터 where = {}, keyword = {}", where, keyword);
 
        if (keyword != null && !keyword.trim().isEmpty()) {
            switch (where) {
                case "userID":
                    builder.and(accessLog.userid.containsIgnoreCase(keyword));
                    break;
                case "userNM":
                    builder.and(accessLog.userNM.containsIgnoreCase(keyword));
                    break;
                case "ipaddr":
                    builder.and(accessLog.ipaddr.containsIgnoreCase(keyword));
                    break;
                case "route":
                    builder.and(accessLog.route.stringValue().containsIgnoreCase(keyword));
                    break;
                case "errorCode":
                    builder.and(accessLog.errCode.stringValue().containsIgnoreCase(keyword));
                    break;
                case "accessDate":
                    if (keyword.contains("/")) {
                        String[] parts = keyword.split("/");
                        if (parts.length == 2) {
                            String from = parts[0].trim();
                            String to = parts[1].trim();
 
                            log.info("accessDate 조건: from = {}, to = {}", from, to);
 
                            if (from.length() == 8 && to.length() == 8) {
                                // 날짜 순서 보정
                                if (from.compareTo(to) > 0) {
                                    String temp = from;
                                    from = to;
                                    to = temp;
                                }
                                log.info("accessDate 조건: from = {}, to = {}", from, to);
                                builder.and(accessLog.date.between(from, to));
                            }
                        }
                    } else {
                        builder.and(accessLog.date.startsWith(keyword));
                    }
                    break;
                default:
                    // where가 명시되지 않은 컬럼이라면, 문자열 컬럼으로 간주하고 LIKE 처리
                    // 존재하지 않는 필드면 builder에 아무 조건도 추가하지 않음 → 결과 없음
                    try {
                        PathBuilder<AccessLog> entityPath = new PathBuilder<>(AccessLog.class"accessLog");
                        // 유효한 필드인지 체크
                        Field field = AccessLog.class.getDeclaredField(where);
                        if (field.getType().equals(String.class)) {
                            builder.and(entityPath.getString(where).containsIgnoreCase(keyword));
                        } else {
                            log.warn(" '{}' 필드는 문자열(String)이 아닙니다. 검색 제외됨", where);
                        }
                    } catch (NoSuchFieldException e) {
                        log.warn(" 존재하지 않는 where 필드명: '{}'. 조건 제외 → 결과 없음 처리", where);
                        // builder에 조건 추가 안 함 → 결과 없음 유도
                    }
 
            }
        }
 
        Pageable pageable = PageRequest.of(
                pageRequestDTO.getPage() - 1,
                pageRequestDTO.getSize(),
                Sort.by("uid").descending()
        );
 
        JPAQuery<AccessLog> query = queryFactory
                .selectFrom(accessLog)
                .where(builder)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(accessLog.uid.desc());
 
        List<AccessLog> resultList = query.fetch();
 
        long totalCount = queryFactory
                .select(accessLog.count())
                .from(accessLog)
                .where(builder)
                .fetchOne();
 
        List<AccessLogDTO> dtoList = IntStream.range(0, resultList.size())
                .mapToObj(i -> {
                    AccessLog entity = resultList.get(i);
                    int no = (int) (totalCount - ((pageable.getPageNumber()) * pageable.getPageSize()) - i);
                    return toDTO(entity, no);
                })
                .collect(Collectors.toList());
 
        return new PageImpl<>(dtoList, pageable, totalCount);
    }
 
    private AccessLogDTO toDTO(AccessLog entity, int no) {
        String errorMessage = errorCodeRepository
                .findMessageByCode(entity.getErrCode())
                .orElse(String.valueOf(entity.getErrCode()));
 
        return AccessLogDTO.builder()
                .no(no)  // 추가
                .uid(entity.getUid())
                .ipaddr(MaskingUtil.ipAddressMasking(entity.getIpaddr()))
                .date(InputSanitizer.displayDate(entity.getDate()))
                .time(entity.getTime())
                .OS(entity.getOs())
                .browser(entity.getBrowser())
                .userid(MaskingUtil.idMasking(entity.getUserid()))
                .userNM(MaskingUtil.letterMasking(entity.getUserNM()))
                .success(entity.getSuccess())
                .route(entity.getRoute())
                .errCode(entity.getErrCode())
                .errorMessage(errorMessage)
                .build();
    }
 
}
 

 

거의 동일한 코드를 최적화한 코드는 이 다음 게시글에 적어둘 것이다.

 

728x90
블로그 이미지

Link2Me

,

QueryDSL 방식의 코드를 JOOQ 코드로 변경하는 방법으로 테스트하고 적어둔다.

 

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
@Service
@Transactional
@Log4j2
@RequiredArgsConstructor // 생성자 자동 주입
public class TodoSearchImpl implements TodoSearch {
 
    private final DSLContext dsl;
 
    @Override
    public Page<Todo> search1(PageRequestDTO pageRequestDTO) {
 
        String keyword = pageRequestDTO.getFilter().getKeyword();
        String where = pageRequestDTO.getFilter().getWhere();
 
        Condition condition = DSL.trueCondition();
 
        // 검색 조건 적용
        if (keyword != null && !keyword.trim().isEmpty()) {
            switch (where) {
                case "title":
                    condition = condition.and(TLB_TODO.TITLE.likeIgnoreCase("%" + keyword + "%"));
                    break;
                case "writer":
                    condition = condition.and(TLB_TODO.WRITER.likeIgnoreCase("%" + keyword + "%"));
                    break;
                default:
                    condition = condition.and(
                            TLB_TODO.TITLE.likeIgnoreCase("%" + keyword + "%")
                                    .or(TLB_TODO.WRITER.likeIgnoreCase("%" + keyword + "%"))
                    );
            }
        }
 
        Pageable pageable = PageRequest.of(
                pageRequestDTO.getPage() - 1,
                pageRequestDTO.getSize(),
                Sort.by("tno").descending()
        );
 
        // 실제 데이터 조회
        List<Todo> content = dsl.selectFrom(TLB_TODO)
                .where(condition)
                .orderBy(TLB_TODO.TNO.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch()
                .map(record -> Todo.builder()
                        .tno(record.getTno())
                        .title(record.getTitle())
                        .writer(record.getWriter())
                        .complete(record.getComplete())
                        .dueDate(record.getDueDate())
                        .build());
 
        long totalCount = dsl.selectCount()
                .from(TLB_TODO)
                .where(condition)
                .fetchOne(0, Long.class);
 
        return new PageImpl<>(content, pageable, totalCount);
    }
}
 

 

 

JOOQ Config

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import javax.sql.DataSource;
 
@Configuration
public class JooqConfig {
 
    @Bean
    public DSLContext dslContext(DataSource dataSource) {
        return DSL.using(dataSource, SQLDialect.MARIADB); // DB에 맞게
    }
}
 

 

 

build.gradle

QueryDSL 과 JOOQ 혼용 테스트를 위해서 2가지 모두 처리할 수 있도록 구성

 

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.4'
    id 'io.spring.dependency-management' version '1.1.7'
    id 'nu.studer.jooq' version '8.2' // JOOQ용 플러그인
}
 
group = 'com.mansa.smartx.api'
version = '0.0.1-SNAPSHOT'
 
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
 
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
 
repositories {
    mavenCentral()
}
 
ext {
    querydslDir = "$buildDir/generated/querydsl"
    jooqDir = "$buildDir/generated-src/jooq"
}
 
sourceSets {
    main {
        java {
            srcDir querydslDir
            srcDir jooqDir
        }
    }
}
 
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    annotationProcessor 'org.projectlombok:lombok'
 
    implementation 'org.modelmapper:modelmapper:3.2.2'
 
    //QueryDSL 추가
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
 
    // JOOQ
    implementation "org.jooq:jooq:3.19.7"
    jooqGenerator "org.jooq:jooq-codegen:3.19.7"
    jooqGenerator "org.mariadb.jdbc:mariadb-java-client" 
 
    implementation 'net.coobird:thumbnailator:0.4.20'
 
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'com.google.code.gson:gson:2.12.1'
 
    implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
    implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'
 
    implementation 'org.springframework.boot:spring-boot-starter-validation'
 
    // P6Spy
//    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
    implementation 'org.hibernate.orm:hibernate-core:6.6.12.Final' // 최신 버전
 
    // Redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
 
    //test 롬복 사용
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    testImplementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
 
}
 
tasks.named('compileJava', JavaCompile).configure {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}
 
tasks.named('test') {
    useJUnitPlatform()
}
 
tasks.named('clean') {
    delete querydslDir
    delete jooqDir
}
 
jooq {
    version = '3.19.7'
    configurations {
        main {
            generationTool {
                jdbc {
                    driver = 'org.mariadb.jdbc.Driver' // 또는 PostgreSQL
                    url = 'jdbc:mariadb://localhost:3306/malldb'
                    user = 'testfox'
                    password = 'TestCodefox!!'
                }
                generator {
                    database {
                        name = 'org.jooq.meta.mariadb.MariaDBDatabase' // 또는 postgres
                        inputSchema = 'malldb' // 보통 'yourdb'
                        includes = '.*'
                    }
                    generate {
                        deprecated = false
                        records = true
                        immutablePojos = true
                        fluentSetters = true
                    }
                    target {
                        packageName = 'com.jpashop.api.jooq.generated'
                        directory = jooqDir
                    }
                }
            }
        }
    }
}
 
 

 

 

 

 

 

728x90
블로그 이미지

Link2Me

,

QueryDSL TodoSearch 에 대한 예제다.

 

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
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoSearch {
}
 
// TodoSearch는 JpaRepository에 붙이면 안 되는 인터페이스 => TodoSearch는 커스텀 구현용 인터페이스
public interface TodoSearch {
    Page<Todo> search1(PageRequestDTO pageRequestDTO);
}
 
@Service
@Transactional
@Log4j2
@RequiredArgsConstructor // 생성자 자동 주입
public class TodoSearchImpl implements TodoSearch {
    // TodoSearchImpl의 이름이 정확히 TodoSearch + Impl이면,
    // Spring Data JPA는 자동으로 이를 Repository 구현으로 인식한다.
 
    private final JPAQueryFactory queryFactory;
 
    @Override
    public Page<Todo> search1(PageRequestDTO pageRequestDTO) {
        QTodo qTodo = QTodo.todo;
 
        BooleanBuilder builder = new BooleanBuilder();
 
        String keyword = pageRequestDTO.getKeyword();
        String where = pageRequestDTO.getWhere();
 
        // 검색 조건 적용
        if (keyword != null && !keyword.trim().isEmpty()) {
            switch (where) {
                case "title":
                    builder.and(qTodo.title.containsIgnoreCase(keyword));
                    break;
                case "writer":
                    builder.and(qTodo.writer.containsIgnoreCase(keyword));
                    break;
                default:
                    builder.and(
                            qTodo.title.containsIgnoreCase(keyword)
                                    .or(qTodo.writer.containsIgnoreCase(keyword))
                    );
            }
        }
 
        Pageable pageable = PageRequest.of(
                pageRequestDTO.getPage() - 1,
                pageRequestDTO.getSize(),
                Sort.by("tno").descending()
        );
 
        JPAQuery<Todo> query = queryFactory
                .selectFrom(qTodo)
                .where(builder)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(qTodo.tno.desc());
 
        List<Todo> resultList = query.fetch();
 
        long totalCount = queryFactory
                .select(qTodo.count())
                .from(qTodo)
                .where(builder)
                .fetchOne();
 
        return new PageImpl<>(resultList, pageable, totalCount);
    }
}
 

 

QueryDSL로 직접 selectFrom(qTodo)를 사용하는 경우, 기본 정렬이 적용되지 않으며, 
Sort.by("tno").descending()과 같은 JPA Pageable의 정렬 정보도 자동 적용되지 않는다.

 

JPAQuery<Todo> query = queryFactory
    .selectFrom(qTodo)
    .where(builder)
    .offset(pageable.getOffset())
    .limit(pageable.getPageSize())
    .orderBy(qTodo.tno.desc()); 

 

 

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
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
 
@Data
@SuperBuilder  // - **상속 관계가 있는 클래스**에서 부모 클래스의 빌더 패턴을 지원하기 위해 사용된다.
@AllArgsConstructor  // 모든 필드에 대해 값을 받아 전체 매개변수 생성자를 자동으로 생성한다.
@NoArgsConstructor
public class PageRequestDTO {
 
    @Builder.Default  // 값을 명시적으로 설정하지 않을 때만 기본값을 부여
    private int page = 1;
 
    @Builder.Default
    private int size = 10;
 
    private Integer blockSize;  // Integer로 변경 (null 체크 가능하게)
 
    private String where; // 검색 KEY 추가
    private String keyword; // 검색어
 
    public int getBlockSize() {
        // blockSize가 null이면 size와 동일하게 처리
        return (blockSize != null) ? blockSize : size;
    }
}
 

 

 

728x90
블로그 이미지

Link2Me

,

온라인 강의 시점은 Spring Boot 버전이 2.X 이고 현재 내가 사용하는 Spring Boot 버전은 3.4.5 이다보니 오류가 발생해서 개고생을 해서 적어둔다.

chatGPT 도 엉터리로 답변을 해주는 통에 몇번의 시행착오를 거쳤다.

 

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
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.5'
    id 'io.spring.dependency-management' version '1.1.7'
}
 
group = 'jpabook'
version = '0.0.1-SNAPSHOT'
 
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
 
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
 
repositories {
    mavenCentral()
}
 
ext {
    querydslDir = "$buildDir/generated/querydsl"
}
 
sourceSets.main.java.srcDir(querydslDir)
 
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
    annotationProcessor 'org.projectlombok:lombok'
 
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
 
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
 
tasks.named('compileJava', JavaCompile).configure {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}
 
tasks.named('test') {
    useJUnitPlatform()
}
 
tasks.named('clean') {
    delete querydslDir
}

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class QueryDslConfig {
 
    @PersistenceContext
    private EntityManager entityManager;
 
    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}
 

 

QueryDSL을 사용할 때 반드시 JPAQueryFactory를 수동으로 Bean 등록해야 한다.
이 설정은 Spring이 애플리케이션 시작 시 JPAQueryFactory를 Bean으로 등록해준다.

728x90
블로그 이미지

Link2Me

,

Spring Boot 기능을 익히면서 회원 테이블을 생성하고 칼럼을 추가하면서 기능을 테스트하고 있다.

 

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
CREATE TABLE members (
  member_id bigint(20NOT NULL,
  userID varchar(60NOT NULL,
  userNM varchar(30NOT NULL,
  access int(2NOT NULL DEFAULT 1 COMMENT '접속상태',
  access_failed_count int(2NOT NULL DEFAULT 0 COMMENT '로그인실패횟수',
  access_date datetime DEFAULT NULL COMMENT '접속일자 및 시간',
  date date DEFAULT NULL COMMENT '최근접속일자',
  regNO int(5NOT NULL DEFAULT 0 COMMENT '팀서열',
  admin int(2NOT NULL DEFAULT 0 COMMENT '관리자유무',
  passwd varchar(120DEFAULT NULL,
  salt varchar(20DEFAULT NULL,
  email varchar(60DEFAULT NULL,
  org_id int(5NOT NULL DEFAULT 0 COMMENT '조직도ID',
  parent_id int(5NOT NULL DEFAULT 0 COMMENT '조직도 parent_id',
  codeID int(4NOT NULL DEFAULT 0 COMMENT '직위',
  telNO varchar(16DEFAULT NULL COMMENT '유선전화',
  mobileNO varchar(30DEFAULT NULL COMMENT '휴대폰번호',
  workrole varchar(200DEFAULT NULL COMMENT '담당업무',
  chosung varchar(10DEFAULT NULL COMMENT '초성',
  reg_date timestamp NULL DEFAULT current_timestamp() COMMENT '등록일자',
  phoneSE varchar(80DEFAULT NULL COMMENT 'deviceID',
  is_temp_password int(2DEFAULT 0 COMMENT '임시비번할당(1)',
  passwd_change_date datetime DEFAULT NULL COMMENT '비밀번호 변경일자',
  last_login_date datetime DEFAULT NULL COMMENT '마지막성공로그인시간',
  display int(2NOT NULL DEFAULT 1
ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
 
ALTER TABLE members
  ADD PRIMARY KEY (member_id),
  ADD UNIQUE KEY userID (userID) USING BTREE;
 
ALTER TABLE members
  MODIFY member_id bigint(20NOT NULL AUTO_INCREMENT;
COMMIT;
 

 

 

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import jakarta.persistence.*;
import lombok.*;
 
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
 
@Entity
@Table(name = "members")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString(exclude = "roleList")
public class Member {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long memberId;
 
    @Column(name = "userID", nullable = false, unique = truelength = 60)
    private String userID;
 
    @Column(name = "userNM", nullable = falselength = 30)
    private String userNM;
 
    @Column(name = "regNO")
    private int regNO;
 
    @Column(name = "admin")
    private int admin;
 
    @Column(name = "passwd"length = 120)
    private String passwd;
 
    @Column(name = "salt"length = 20)
    private String salt;
 
    @Column(name = "email"length = 60)
    private String email;
 
    @Column(name = "org_id")
    private Integer orgId;
 
    @Column(name = "parent_id")
    private Integer parentId;
 
    @Column(name = "codeID")
    private Integer codeID;
 
    @Column(name = "telNO"length = 16)
    private String telNO;
 
    @Column(name = "mobileNO"length = 30)
    private String mobileNO;
 
    @Column(name = "workrole"length = 200)
    private String workrole;
 
    @Column(name = "access")
    private Integer access;
 
    @Column(name = "chosung"length = 10)
    private String chosung;
 
    @Column(name = "access_failed_count")
    private Integer accessFailedCount;
 
    @Column(name = "access_date")
    private LocalDateTime accessDate;
 
    @Column(name = "reg_date", columnDefinition = "timestamp default current_timestamp")
    private LocalDateTime regDate;
 
    @Column(name = "date")
    private LocalDate date;
 
    @Column(name = "phoneSE"length = 80)
    private String phoneSE;
 
    @Column(name = "is_temp_password")
    private Integer isTempPassword; // 관리자 임시 비밀번호 할당 여부
 
    @Column(name = "passwd_change_date")
    private LocalDateTime passwdChangeDate; // 비밀번호 변경일자
 
    @Column(name = "last_login_date")
    private LocalDateTime lastLoginDate;
 
    @Column(name = "display")
    private Integer display;
 
    @Builder.Default
    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<MemberRoleList> roleList = new ArrayList<>();
 
    public void addRole(MemberRoleList role) {
        this.roleList.add(role);
    }
 
    public void clearRoles() {
        this.roleList.clear();
    }
}
 

 

 

728x90
블로그 이미지

Link2Me

,

mariaDB 를 주로 사용하는데 PostgreSQL 를 설치 및 사용이 필요해서 설치하는 과정을 적은 스크립트를 적어둔다.

 

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
################################
##### PostgreSQL 15 버전 설치 #####
################################
# Rocky Linux 9.5에서 MariaDB와 PostgreSQL을 동시에 설치하고 운용하는 것은 완전히 가능
 
# 현재 설치된 PostgreSQL 버전 확인
dnf list installed | grep postgres
 
rpm -qa | grep postgres
 
# PostgreSQL 13 삭제
# sudo dnf remove 명령어로 하나씩 찾아서 삭제한다.
 
# PostgreSQL 13 데이터 디렉터리 제거 (선택 사항)
sudo rm -rf /var/lib/pgsql/13
 
# postgreSQL 15 저장소 추가
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
 
#기존 내장된 PostgreSQL 저장소 비활성화
sudo dnf -qy module disable postgresql
 
# PostgreSQL 15 설치
sudo dnf -update
 
sudo dnf install -y postgresql15-server
 
# PostgreSQL 15 데이터베이스 구성 초기화
sudo postgresql-15-setup initdb
 
# PostgreSQL 15 실행 및 서비스 등록
sudo systemctl enable postgresql-15
sudo systemctl start postgresql-15
 
# PostgreSQL 상태 확인
sudo systemctl status postgresql-15
 
# PostgreSQL 15 확인 및 유지
/usr/pgsql-15/bin/psql --version
 
# PATH에 PostgreSQL 15 바이너리 추가 (권장)
echo 'export PATH=/usr/pgsql-15/bin:$PATH' >> ~/.bash_profile
source ~/.bash_profile
 
psql --version
 
#########################################################
# PostgreSQL 15 암호화 설정
1. postgres 사용자로 전환
sudo --u postgres
 
2. psql 실행
psql
 
3. 비밀번호 설정
\password
 
"postgres" 사용자의 새 암호:
 
# 특정 비밀번호로 직접 지정:
ALTER USER postgres WITH PASSWORD '새비밀번호';
ALTER USER postgres WITH PASSWORD 'Wonderfull!!';
 
4. 종료
\q
 
# 변경 후 PostgreSQL 재시작: ==> 반드시 root 권한으로 접속된 상태에서 실행 가능
sudo systemctl restart postgresql-15
 
#######################################################################
# pgAdmin 4 Web 모드 설치 (Rocky Linux 9.5)
#######################################################################
1. 필수 패키지 설치
sudo dnf install -y yum-utils
 
2. pgAdmin 4 저장소 추가
sudo rpm -i https://ftp.postgresql.org/pub/pgadmin/pgadmin4/yum/pgadmin4-redhat-repo-2-1.noarch.rpm
 
3. 시스템 패키지 목록 업데이트
sudo dnf update -y
 
4. pgAdmin 4 설치
sudo dnf install -y pgadmin4
 
5. Apache 웹서버 시작 및 활성화
sudo systemctl enable httpd
sudo systemctl start httpd
 
6. pgAdmin 4 웹 모드 설정
sudo /usr/pgadmin4/bin/setup-web.sh
 
→ 웹 접속용 이메일/비밀번호 입력
이 명령을 실행하면 관리자 이메일과 비밀번호를 설정하라는 메시지가 나타납니다. 
입력 후 Apache 웹서버가 자동으로 재시작됩니다.
 
7. 방화벽 설정 (HTTP 포트 80 허용)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload
 
8. pgAdmin 4 접속
# 웹 브라우저에서 다음 주소로 접속하세요:
http://<서버 IP 또는 도메인>/
 
#######################################################################
####### 실제 적용 예제 ######
#######################################################################
sudo --u postgres
psql
 
-- DB 생성
create database malldb ENCODING 'UTF8';
\c malldb
 
-- 테이블 생성
CREATE TABLE members (
    member_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    userID VARCHAR(60NOT NULL UNIQUE,
    userNM VARCHAR(30NOT NULL,
    access INTEGER NOT NULL DEFAULT 1,
    access_failed_count INTEGER NOT NULL DEFAULT 0,
    access_date TIMESTAMP NULL,
    date DATE DEFAULT NULL,
    regNO INTEGER NOT NULL DEFAULT 0,
    admin INTEGER NOT NULL DEFAULT 0,
    passwd VARCHAR(120DEFAULT NULL,
    email VARCHAR(60DEFAULT NULL,
    orgId INTEGER NOT NULL DEFAULT 0,
    parent_id INTEGER NOT NULL DEFAULT 0,
    codeID INTEGER NOT NULL DEFAULT 0,
    telNO VARCHAR(16DEFAULT NULL,
    mobileNO VARCHAR(30DEFAULT NULL,
    workrole VARCHAR(200DEFAULT NULL,
    chosung VARCHAR(10DEFAULT NULL,
    reg_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    phoneSE VARCHAR(80DEFAULT NULL,
    display INTEGER NOT NULL DEFAULT 1,
    org_id INTEGER DEFAULT NULL
);
 
-- 주석 설정
COMMENT ON COLUMN members.access IS '접속상태';
COMMENT ON COLUMN members.access_failed_count IS '로그인실패횟수';
COMMENT ON COLUMN members.date IS '최근접속일자';
COMMENT ON COLUMN members.regNO IS '팀서열';
COMMENT ON COLUMN members.admin IS '관리자유무';
COMMENT ON COLUMN members.orgId IS '조직도ID';
COMMENT ON COLUMN members.parent_id IS '조직도 parent_id';
COMMENT ON COLUMN members.codeID IS '직위';
COMMENT ON COLUMN members.telNO IS '유선전화';
COMMENT ON COLUMN members.mobileNO IS '휴대폰번호';
COMMENT ON COLUMN members.workrole IS '담당업무';
COMMENT ON COLUMN members.chosung IS '초성';
COMMENT ON COLUMN members.reg_date IS '등록일자';
COMMENT ON COLUMN members.phoneSE IS 'deviceID';
 
-- 테이블 생성 확인
\dt
 
-- 특정 테이블 구조 보기
\d members
 
-- 해당 DB 접속
\c malldb
 
-- 인덱스 확인
\di members*
 
-- 인덱스 추가 (CREATE INDEX)
CREATE INDEX idx_userid ON members(userID);
 
-- 여러 칼럼 복합 인덱스 
CREATE INDEX idx_userid_orgid ON members(userID, orgId);
 
-- 유니크 인덱스
CREATE UNIQUE INDEX idx_userid_unique ON members(userID);
 
-- 인덱스 삭제 (DROP INDEX)
-- 기본 문법
-- DROP INDEX index_name;
DROP INDEX idx_userid;
 
############################################################################
# 사용자 권한 부여
sudo --u postgres
psql
 
-- 1. 사용자 생성
CREATE USER codefox WITH PASSWORD 'Wonderfull!!';
 
-- 2. 특정 데이터베이스에 대한 모든 권한 부여 (예: malldb)
GRANT ALL PRIVILEGES ON DATABASE malldb TO codefox;
 
-- 3. (선택) 해당 데이터베이스의 모든 테이블, 시퀀스, 함수에 대한 권한 부여
-- DB를 처음 생성한 경우에는 필요 없지만, 이후 객체 생성 시엔 아래도 고려해야 합니다
\c malldb  -- 해당 DB에 접속한 후 실행
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO codefox;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO codefox;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO codefox;
 
-- 4. (선택) 앞으로 생성되는 객체에도 권한 자동 부여
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO codefox;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO codefox;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO codefox;
 
-- 5. 빠져나오기
\q
exit
#######################################################################
# PATH 추가 (권장)
echo 'export PATH=/usr/pgsql-15/bin:$PATH' >> ~/.bash_profile
source ~/.bash_profile
 
# DB 백업
sudo --u postgres
pg_dump -U postgres -d malldb -F p -f malldb.sql
 
# 다운로드 받은 파일의 경로
cd /var/lib/pgsql/
 
#######################################################################

 

 

 

 

 

728x90
블로그 이미지

Link2Me

,

명령어를 찾을 수 없어서 개삽질을 한참했다.

build.gralde 에 아래와 같이 추가를 하고 컴파일을 하면 에러가 계속 발생한다.

// P6Spy
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
implementation 'org.hibernate.orm:hibernate-core:6.2.12.Final'

 

그런데 이걸 Disable 시켜도 동일해서 아래와 같이 해결했다.

 

 

'Modify options' 버튼을 눌러 'Add VM options'를 선택

 

새벽에 잠도 못자고 뭐하는 개짓거리인지 ㅠㅠㅠ

해결 되었는데 잠이 오려나 모르겠네.

728x90

'Spring Boot > IDEA' 카테고리의 다른 글

IntelliJ/IDEA 내 JDK 설정 확인  (1) 2025.06.12
Spring Boot 테스트 Class 자동 생성 단축키  (0) 2025.04.10
Spring Boot Build 하기  (0) 2025.03.08
IntelliJ IDEA Auto Import 설정  (0) 2025.03.01
Build Gradle Update  (0) 2025.02.27
블로그 이미지

Link2Me

,

스프링부트에서 테스트 코드 Class 를 자동으로 생성하는 단축키

 

728x90

'Spring Boot > IDEA' 카테고리의 다른 글

IntelliJ/IDEA 내 JDK 설정 확인  (1) 2025.06.12
--add-opens java.base/java.lang=ALL-UNNAMED  (0) 2025.04.11
Spring Boot Build 하기  (0) 2025.03.08
IntelliJ IDEA Auto Import 설정  (0) 2025.03.01
Build Gradle Update  (0) 2025.02.27
블로그 이미지

Link2Me

,