728x90

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

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
블로그 이미지

Link2Me

,
728x90

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

,
728x90

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

 

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

,
728x90

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

,
728x90

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

,
728x90

온라인 강의 시점은 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

,
728x90

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

,
728x90

implementation 'org.springframework.boot:spring-boot-starter-web' 라이브러리는 Back-end 단에서 필요한 의존성입니다. React는 Front-end 라이브러리로 JavaScript 기반에서 동작하며, 서버와의 통신은 보통 HTTP 요청(REST API) 또는 WebSocket을 사용합니다. 이 의존성은 Spring Boot에서 HTTP 요청을 처리하거나 RESTful API를 제공하기 위해 필요한 설정과 구성 요소(Web 관련 모듈 포함)를 포함하고 있습니다.

의존성의 역할

  • spring-boot-starter-web 의존성을 추가하면 다음과 같은 기능을 사용할 수 있습니다:
    1. Spring MVC: 컨트롤러(endpoint)를 정의하기 위한 기능.
    2. 내장 톰캣 서버: 애플리케이션을 실행하기 위한 웹 서버.
    3. Jackson 라이브러리 포함: JSON 형식의 요청/응답 바인딩.
    4. 기타 Spring Web 관련 설정 및 모듈.

따라서 React가 Front-end로 쓰이고, Back-end를 Spring Boot로 구현한다면 API 서버를 구축하기 위해 반드시 필요합니다. React와 Spring Boot는 별개이지만, React에서 만들어진 요청을 Spring Boot가 처리하려면 spring-boot-starter-web 의존성은 필수입니다.

728x90
블로그 이미지

Link2Me

,
728x90

Rocky 9.5 리눅스 환경에서 스프링부트 실행을 위해 준비한 스크립트이다.

 

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
##############################################
######### RockeyOS 9.5 Spring Boot 설치 ######
##############################################
# 스크립트는 반드시 관리자 권한에서 실행해야 한다.
# openssl 버전 확인 => TLS 1.3 지원
openssl version
 
dnf -y install expat-devel
 
# 시스템 업데이트
sudo dnf -y update
 
sudo dnf -y install wget unzip mc git nmap curl
 
# SSL 관련 패키지 설치 확인
sudo dnf -y install openssl openssl-devel
 
####################################################################################
# 방화벽 데몬 시작
systemctl start firewalld
 
# 서버 부팅 시 firewalld 데몬 자동 시작 설정
systemctl enable firewalld
 
firewall-cmd --permanent --add-service=http 
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=mysql
firewall-cmd --permanent --zone=public --add-port=3306/tcp
 
firewall-cmd --permanent --zone=public --add-port=8080/tcp
firewall-cmd --permanent --zone=public --add-port=3000/tcp
firewall-cmd --reload
firewall-cmd --list-all
 
####################################################################################
# tomcat 배포 사전 조건
# 1. JAVA가 설치되어 있어야 한다. (톰캣 9는 자바 11 이상이 필요)
# jar 파일은 내부 톰캣이 있기에 따로 톰캣을 설치하지 않아도 된다. 
####################################################################################
# 1. 기본 패키지 업데이트
sudo dnf update -y
 
# 2. Java 17 버전 설치
sudo dnf install -y java-17-openjdk java-17-openjdk-devel
java -version
 
# 3. Java 환경 변수 설정
echo "export JAVA_HOME=/usr/lib/jvm/java-17-openjdk" | sudo tee -/etc/profile.d/java.sh
echo "export PATH=\$JAVA_HOME/bin:\$PATH" | sudo tee -/etc/profile.d/java.sh
source /etc/profile.d/java.sh
 
# 여러개 설치되어 있는 자바 버전 선택하기
sudo alternatives --config java
 
 
# 4. Spring Boot 실행
# 아래 명령어를 수행하여 실행되는 메시지를 직접 육안으로 확인한다.
java -jar <jar파일명>
java -Xshare:off -jar /home/spring/SpringJWT/SpringJWT-0.0.1-SNAPSHOT.jar
 
# 5. Spring Boot 애플리케이션 실행
# 애플리케이션 실행 (백그라운드)
# 노트북에서 빌드(build)한 파일을 리눅스 서버로 업로드하고 아래와 같이 실행한다.
 
nohup java -Xshare:off -jar /home/spring/SpringJWT/SpringJWT-0.0.1-SNAPSHOT.jar --server.port=8080 > /var/log/springjwt.log 2>&1 &
 
 
# 필요한 Apache 모듈 활성화
sudo systemctl restart httpd
 
####################################################################################
# 프로세스 확인
ps aux | grep SpringJWT
 
# 프로세스 종료 (필요 시)
kill $(ps aux | grep 'SpringJWT' | awk '{print $2}')
 
# 종료 프로세스 작성
vi /usr/local/bin/kill_8080.sh
 
#!/bin/bash
 
# 8080 포트 사용 중인 프로세스 확인
PID=$(lsof -i :8080 -t)
 
if [ -"$PID" ]; then
    echo "8080 포트에서 실행 중인 프로세스(PID: $PID) 종료 중..."
    kill $PID
    sleep 2
 
    # 프로세스가 종료되지 않았으면 강제 종료
    if ps -p $PID > /dev/null; then
        echo "⚠️ 정상 종료되지 않아 강제 종료 진행..."
        kill -9 $PID
    fi
 
    echo "8080 포트 프로세스 종료 완료."
else
    echo "ℹ️ 8080 포트에서 실행 중인 프로세스가 없습니다."
fi
 
# 저장(wq)하고 빠져나온다.
 
# 실행권한 부여
sudo chmod +/usr/local/bin/kill_8080.sh
 
# 스크립트 실행
sudo /usr/local/bin/kill_8080.sh
 
####################################################################################
# systemd 서비스 등록 (서버 부팅 후 자동 실행)
vi /etc/systemd/system/springjwt.service
 
[Unit]
Description=Spring Boot Application - SpringJWT
After=network.target
 
[Service]
User=apache
Group=apache
ExecStart=/usr/bin/java -Xshare:off -jar /home/spring/SpringJWT/SpringJWT-0.0.1-SNAPSHOT.jar
SuccessExitStatus=143
Restart=always
RestartSec=5
StandardOutput=file:/var/log/springjwt.log
StandardError=file:/var/log/springjwt.log
 
[Install]
WantedBy=multi-user.target
 
#저장(wq)하고 빠져나온다.
 
# systemd 데몬 리로드
sudo systemctl daemon-reload
 
# 서비스 등록 및 실행
sudo systemctl enable springjwt
sudo systemctl start springjwt
 
# 실행 상태 확인
sudo systemctl status springjwt
 
# 서비스 중지 (필요 시)
sudo systemctl stop springjwt
 
# 실행 로그 확인
cat /var/log/springjwt.log
 
####################################################################################
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

위와 같이 설정하고 Generate 하면 파일이 zip 으로 만들어진다.

이 파일을 C 드라이브 폴더에 압축을 풀고 Intellij IDEA 툴로 접속한 다음 MariaDB 연결을 위한 설정을 해야 한다.

처음부터 MariaDB Driver를 선택해야 하는데 실수한 것이라는 걸 뒤늦게 알았다.

 

Spring Boot 에서 MariaDB 연결

안정적인 버전은 3.3.3 이라고 권고하고 있다.

 

MariaDB Driver 를 선택하면 설치된다.

 

application.yml 파일을 새로 생성하여 아래와 같이 추가해준다.

ddl-auto : create 는 처음에는 이렇게 설정하지만, 나중에는 none 으로 변경해야 한다.

 

MariaDB를 설치하고 MariaDB 연결이 제대로 된 것인지 확인한다.

 

MariaDB 를 선택하고 아래와 같이 설정해준다.

 

 

마지막으로 Run 을 했을 때 에러가 발생하지 않고 제대로 동작되는 걸 확인해야 한다.

 

728x90
블로그 이미지

Link2Me

,
728x90

내 PC에는 PHP 를 연습하기 위해서 Autoset10 (Apache + PHP + MariaDB)가 설치되어 있다.

이미 설치된 MariaDB 와 연동하여 데이터 추가되는 걸 테스트했다.

앞에서 설명한 MySQL 버전과 다른 부분만 추가로 적어둔다.

 

1. 의존성 추가(build.gradle)

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    //implementation 'mysql:mysql-connector-java'
    implementation 'org.mariadb.jdbc:mariadb-java-client'
 
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
 
    runtimeOnly ('org.springframework.boot:spring-boot-devtools')
    runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
 
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

2. application.properties 에 DB 정보 추가하기

# MariaDB Driver
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
 
# DB Source URL
#spring.datasource.url=jdbc:mariadb://192.168.1.20:3306/studydb?useSSL=false
spring.datasource.url=jdbc:mariadb://localhost:3306/studydb?serverTimezone=Asia/Seoul
<IP>:<Port/<DB> ==> localhost:3306/study_db
 
# DB username
spring.datasource.username=codefox
 
# DB password
spring.datasource.password=Autoset12#$
 
spring.jpa.show-sql=true 
 
# 테이블 스키마를 생성하지 않았을 경우 자동으로 테이블 생성
#spring.jpa.hibernate.ddl-auto=update
 
 

 

3. 테이블 스키마

CREATE TABLE User (
  `uid` int(11) NOT NULL,
  `account` varchar(45) NOT NULL,
  `email` varchar(45) DEFAULT NULL,
  `phone_number` varchar(15) DEFAULT NULL COMMENT '전화번호',
  `created_at` datetime NOT NULL,
  `created_by` varchar(45) NOT NULL,
  `updated_at` datetime DEFAULT NULL,
  `updated_by` varchar(45) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
ALTER TABLE `user`
  ADD PRIMARY KEY (`uid`),
  ADD UNIQUE KEY `account` (`account`);
 
ALTER TABLE `user`
  MODIFY `uid` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;
 
 
use mysql;
create user codefox@localhost;
grant all privileges on studydb.* to codefox@localhost identified by 'Autoset12#$';
flush privileges;
 

 

 

MariaDB 연결이 제대로 된 것인지 1차 확인하는 방법이다.

 

 

728x90

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

Spring Boot 배포(Deploy) 스크립트  (1) 2025.03.14
Spring Boot MariaDB 연결  (0) 2025.02.22
Spring Boot JPA(Java Persistence API ) - MySQL  (0) 2021.10.23
[SpringBoot] Lombok  (0) 2021.10.22
MariaDB 10.6 설치  (0) 2021.10.21
블로그 이미지

Link2Me

,
728x90

JPA는 자바 객체와 DB 테이블 간의 매핑을 처리하는 ORM(Object Relational Mapping) 기술의 표준이다.

JPA를 이용하기 위해서는 maven(메이븐)을 이용하거나 gradle(그래들)을 이용하여 패키지를 관리한다.

 

1. 프로젝트 의존성 추가(build.gradle)

dependencies {
    implementation 'mysql:mysql-connector-java'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}

 

2. application.properties 에 DB 정보 추가하기

# MySQL 설정 
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 
 
# DB Source URL 
spring.datasource.url=jdbc:mysql://<IP>:<Port/<DB>?useSSL=false
<IP>:<Port/<DB> ==> localhost:3306/study_db
 
 
# DB username 
spring.datasource.username=<username> 
 
# DB password 
spring.datasource.password=<password> 
 
true 설정시 JPA 쿼리문을 볼 수 있다 
spring.jpa.show-sql=true 
 
# DDL(create, alter, drop) 정의시 DB의 고유 기능을 사용할 수 있다. 
spring.jpa.hibernate.ddl-auto=update 
 
# JPA의 구현체인 Hibernate가 동작하면서 발생한 SQL의 가독성을 높여준다. 
spring.jpa.properties.hibernate.format_sql=true
 

 

DB와 통신이 정상적으로 잘 되는지 먼저 확인을 해야 한다.

DB와 통신이 잘 되는데도 불구하고 에러가 발생해서 엄청 삽질을 했는데 결국 사소한 문제였다.

spring.datasource.password=<password> 패스워드 뒤에 공백이 추가되어 있는 줄 모르고 있었다.

 

3. 테이블 설계

   ㅇ DB 생성 : CREATE DATABASE studydb default CHARACTER SET UTF8;

 

   ㅇ 테이블 생성

       - 테이블을 직접 생성해주었는데, 테이블 생성하지 않고 자동으로 추가되는 것도 확인했다.

       - 테이블명을 User 로 첫글자를 대문자로 해서 생성후 테스트해보니, Spring Boot 에서 소문자 user 생성하더라.

CREATE TABLE `user` (
  `uid` int(11) NOT NULL,
  `account` varchar(45) NOT NULL,
  `email` varchar(45) DEFAULT NULL,
  `phone_number` varchar(15) DEFAULT NULL COMMENT '전화번호',
  `created_at` datetime NOT NULL,
  `created_by` varchar(45) NOT NULL,
  `updated_at` datetime DEFAULT NULL,
  `updated_by` varchar(45) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
ALTER TABLE `user`  ADD PRIMARY KEY (`uid`);
ALTER TABLE `user`  MODIFY `uid` int(11) NOT NULL AUTO_INCREMENT;
 

   ㅇ 사용자 권한 추가

MySQL 5.7 버전을 설치하고 테스트를 했더니, 패스워드 지정시 기본으로 영문 소문자, 대문자, 숫자, 특수문자를 포함해서 8자리 이상 입력해야 되더라.

// 비밀번호 정책 확인
show variables like 'validate_password%';

// DB 생성
create database studydb default character set utf8;

 

// 사용자 생성 및 권한 추가

use mysql;
create user codefox@localhost identified by 'Autoset12#$';
grant all privileges on studydb.* to codefox@localhost;
flush privileges;

use mysql;
create user codefox@'192.168.1.25' identified by 'Autoset12#$';
grant all privileges on studydb.* to codefox@'192.168.1.25';
flush privileges;

 

※ Virtual Box를 설치하여 CentOS 7 + MySQL 5.7 버전을 설치하고 192.168.1.X 동일 사설 네트워크 환경에서 테스트하기 위한 설정이다. 192.168.1.25 는 Windows 10 이 설치된 PC 의 IP주소이고 CentOS 7 서버의 IP주소는 192.168.1.20 이다.

 

4. Entity 클래스

package com.example.study.model.entity;
 
import lombok.*;
 
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.time.LocalDateTime;
import java.time.LocalTime;
 
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class User {
    // DB의 테이블의 이름과 동일한 User 클래스 생성
 
    @Id  // @Id : Primary Key를 의미
    @GeneratedValue(strategy = GenerationType.IDENTITY) //MySQL의 AUTO_INCREMENT를 사용
    private Long idx;
    private String account;
    private String email;
    private String phoneNumber;
    private LocalDateTime createdAt;
    private String createdBy;
    private LocalDateTime updatedAt;
    private String updatedBy;
}
 

 

5. JPA Repository 생성

@Repository
- 따로 쿼리문 작성없이 생성, 조회, 업데이트, 삭제(CRUD)를 할 수 있게 기능을 제공해줌
- 제너릭 타입으로는 첫번째부터, <Entity, PrimaryKey의 타입>을 넣는다

 

package com.example.study.repository;
 
import com.example.study.model.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
}
 

 

 

6. 테스트

테이블에 User 데이터가 잘 생성되는지 테스트하는 과정이다.

프로젝트 구조 확인

 

package com.example.study.repository;
 
import com.example.study.StudyApplicationTests;
import com.example.study.model.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
 
import java.time.LocalDateTime;
 
@SpringBootTest
public class UserReposityTest extends StudyApplicationTests {
 
    // Dependency Injection(DI)
    @Autowired
    private UserRepository userRepository;
 
    @Test
    public void create(){
        User user = new User();
        user.setAccount("test01");
        user.setEmail("jsk005@naver.com");
        user.setPhoneNumber("010-0001-0000");
        user.setCreatedAt(LocalDateTime.now());
        user.setCreatedBy("admin01");
 
        User newUser = userRepository.save(user);
        System.out.println("newUser :" + newUser);
    }
 
    public void read(){
 
    }
 
    public void update(){
 
    }
 
    public void delete(){
 
    }
}
 

 

3번을 눌러서 테스트하면 DB에 데이터가 저장되는지 여부를 확인할 수 있다.

 

 

 

MySQL 과 MariaDB 모두 테스트를 했고, User 데이터가 잘 생성되는걸 확인할 수 있다.

Run 정보를 확인해보면....

 

728x90

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

Spring Boot 배포(Deploy) 스크립트  (1) 2025.03.14
Spring Boot MariaDB 연결  (0) 2025.02.22
Spring Boot JPA(Java Persistence API ) - MariaDB  (0) 2021.10.23
[SpringBoot] Lombok  (0) 2021.10.22
MariaDB 10.6 설치  (0) 2021.10.21
블로그 이미지

Link2Me

,
728x90

Lombok이란 어노테이션 기반으로 코드를 자동완성 해주는 라이브러리이다. Lombok을 이용하면 Getter, Setter, Equlas, ToString 등과 다양한 방면의 코드를 자동완성 시킬 수 있다.

 

 

package com.example.study.model;
 
import lombok.AllArgsConstructor;
import lombok.Data;
 
// getter, setter 를 annotation 으로 생성시켜주는 라이브러리 : lombok
@Data
@AllArgsConstructor
public class SearchParam {
    private String account;
    private String email;
    private int page;
}
 

 

@Data 어노테이션을 활용하면 @ToString, @EqualsAndHashCode, @Getter, @Setter, @RequiredArgsConstructor를 자동완성 시켜준다.

 

@AllArgsConstructor는 모든 변수를 사용하는 생성자를 자동완성 시켜주는 어노테이션이다.

 

@NoArgsConstructor는 어떠한 변수도 사용하지 않는 기본 생성자를 자동완성 시켜주는 어노테이션이다.

 

@RequiredArgsConstructor는 특정 변수만을 활용하는 생성자를 자동완성 시켜주는 어노테이션이다. 생성자의 인자로 추가할 변수에 @NonNull 어노테이션을 붙여서 해당 변수를 생성자의 인자로 추가할 수 있다. 아니면 해당 변수를 final로 선언해도 의존성을 주입받을 수 있다.

 

@EqualsAndHashCode 어노테이션을 활용하면 클래스에 대한 equals 함수와 hashCode 함수를 자동으로 생성해준다.

https://mangkyu.tistory.com/78 에 어노테이션과 예제가 상세히 설명되어 있다.

 

Lombok 은 코틀린 data class 와 유사한 거 같다.

Android Studio 개발 툴과 IntelliJ IDEA 툴은 Jetbrains 에서 만든 툴이라 메뉴 사용법이 거의 동일하다.

 

IntelliJ IDEA 툴에서 아래 그림과 같이 확인해 보면 getter, setter, constructor 가 생성되어 있는 걸 확인할 수 있다.

 

 

@Getter @Setter 만 적용할 경우

 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

익숙하게 사용하기 위해서 Autoset10 을 설치하고 MariaDB를 구동하였으나 구동 자체가 안된다.

그래서 별도로 MariaDB 10.6을 다운로드 받아서 설치했다.

윈도우 기반에서 Spring boot 을 배우기 위해서 MariaDB 를 설치하는 것이다.

https://mariadb.org/ 사이트에 접속하여 파일을 다운로드한다.

 

 

 

 

 

 

여기까지 설정하고 나면 자동으로 MairaDB 데몬이 떠있다.

DB 생성 및 테이블 추가하는 것은 phpMyAdmin 을 이용하여 할 것이다.

윈도우10 기반으로 서버를 운용은 하지 않을 것이고, 단지 개발 연습용으로 활용할 것이기 때문에 익숙한 툴을 이용하여 DB 구조 설계 및 연습을 하면 된다.

 

728x90
블로그 이미지

Link2Me

,