Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,26 @@
# SPRING PLUS
# ec2 생성<img width="1484" height="985" alt="ec2 생성완료" src="https://github.com/user-attachments/assets/e35358f5-3464-479f-ace2-2a8a6a31e0c5" />
# db생성<img width="1832" height="494" alt="db 생성완료" src="https://github.com/user-attachments/assets/1e3d3ac9-0cd3-417a-9ce7-b8e61f0a55cd" />
# ec2 & db 연결<img width="721" height="461" alt="서버 연결" src="https://github.com/user-attachments/assets/47f81fd6-7fe8-4f57-9c51-da3f86272846" />
# ec2 구동<img width="902" height="1010" alt="구동완료" src="https://github.com/user-attachments/assets/845d2264-6e9f-4e6b-a95d-6b4c22272d6d" />

# s3 버킷 생성<img width="1865" height="960" alt="s3 버킷 생성" src="https://github.com/user-attachments/assets/b9cc8a77-18dd-4fff-a000-5a9a3d6e364e" />


### 1. 대용량 데이터 삽입 (Bulk Insert)
- **사용 기술**: `JdbcTemplate` (Batch Update)
- **최적화 설정**: JDBC URL 옵션에 `rewriteBatchedStatements=true` 적용
- **데이터 양**: 5,000,000건
- **결과**: 총 **88초** 소요 (약 1분 28초)
- **분석**: JPA의 `saveAll` 방식(단건 호출) 대신 JDBC Batch를 활용하여 네트워크 오버헤드를 최소화함으로써 대량의 데이터를 매우 빠르게 삽입할 수 있었습니다.

---

### 2. 조회 성능 테스트 및 인덱스(Index) 최적화
유저 닉네임을 조건으로 하는 검색 기능에 대해 인덱스 적용 전/후 성능을 비교 측정했습니다.
- **검색 대상**: `User_4999999` (데이터의 최하단부 검색)

| 구분 | 검색 방식 | 소요 시간 | 비고 |
| :--- | :--- | :--- | :--- |
| **인덱스 적용 전** | **Full Table Scan** | **4,281ms** | 전체 행(500만건)을 하나씩 대조 |
| **인덱스 적용 후** | **Index Range Scan** | **210ms** | B-Tree 구조를 통해 즉시 접근 (약 20배 향상) |
34 changes: 34 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ repositories {
}

dependencies {


// springSecurity 추가
implementation 'org.springframework.boot:spring-boot-starter-security'


implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand All @@ -41,6 +47,34 @@ dependencies {
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

//QueryDSL
// 1. QueryDSL 라이브러리
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'

// 2. QueryDSL용 어노테이션 프로세서 (컴파일 시 Q클래스 생성)
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"

implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
}

def querydslDir = "src/main/generated"

sourceSets {
main.java.srcDirs += [ querydslDir ]
}

tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}

clean {
delete file(querydslDir)
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.example.expert.domain.comment.entity;

import static com.querydsl.core.types.PathMetadataFactory.*;

import com.querydsl.core.types.dsl.*;

import com.querydsl.core.types.PathMetadata;
import javax.annotation.processing.Generated;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.PathInits;


/**
* QComment is a Querydsl query type for Comment
*/
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QComment extends EntityPathBase<Comment> {

private static final long serialVersionUID = 1329458967L;

private static final PathInits INITS = PathInits.DIRECT2;

public static final QComment comment = new QComment("comment");

public final org.example.expert.domain.common.entity.QTimestamped _super = new org.example.expert.domain.common.entity.QTimestamped(this);

public final StringPath contents = createString("contents");

//inherited
public final DateTimePath<java.time.LocalDateTime> createdAt = _super.createdAt;

public final NumberPath<Long> id = createNumber("id", Long.class);

//inherited
public final DateTimePath<java.time.LocalDateTime> modifiedAt = _super.modifiedAt;

public final org.example.expert.domain.todo.entity.QTodo todo;

public final org.example.expert.domain.user.entity.QUser user;

public QComment(String variable) {
this(Comment.class, forVariable(variable), INITS);
}

public QComment(Path<? extends Comment> path) {
this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
}

public QComment(PathMetadata metadata) {
this(metadata, PathInits.getFor(metadata, INITS));
}

public QComment(PathMetadata metadata, PathInits inits) {
this(Comment.class, metadata, inits);
}

public QComment(Class<? extends Comment> type, PathMetadata metadata, PathInits inits) {
super(type, metadata, inits);
this.todo = inits.isInitialized("todo") ? new org.example.expert.domain.todo.entity.QTodo(forProperty("todo"), inits.get("todo")) : null;
this.user = inits.isInitialized("user") ? new org.example.expert.domain.user.entity.QUser(forProperty("user")) : null;
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.example.expert.domain.common.entity;

import static com.querydsl.core.types.PathMetadataFactory.*;

import com.querydsl.core.types.dsl.*;

import com.querydsl.core.types.PathMetadata;
import javax.annotation.processing.Generated;
import com.querydsl.core.types.Path;


/**
* QTimestamped is a Querydsl query type for Timestamped
*/
@Generated("com.querydsl.codegen.DefaultSupertypeSerializer")
public class QTimestamped extends EntityPathBase<Timestamped> {

private static final long serialVersionUID = -1617243527L;

public static final QTimestamped timestamped = new QTimestamped("timestamped");

public final DateTimePath<java.time.LocalDateTime> createdAt = createDateTime("createdAt", java.time.LocalDateTime.class);

public final DateTimePath<java.time.LocalDateTime> modifiedAt = createDateTime("modifiedAt", java.time.LocalDateTime.class);

public QTimestamped(String variable) {
super(Timestamped.class, forVariable(variable));
}

public QTimestamped(Path<? extends Timestamped> path) {
super(path.getType(), path.getMetadata());
}

public QTimestamped(PathMetadata metadata) {
super(Timestamped.class, metadata);
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.example.expert.domain.manager.entity;

import static com.querydsl.core.types.PathMetadataFactory.*;

import com.querydsl.core.types.dsl.*;

import com.querydsl.core.types.PathMetadata;
import javax.annotation.processing.Generated;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.PathInits;


/**
* QManager is a Querydsl query type for Manager
*/
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QManager extends EntityPathBase<Manager> {

private static final long serialVersionUID = 216623447L;

private static final PathInits INITS = PathInits.DIRECT2;

public static final QManager manager = new QManager("manager");

public final NumberPath<Long> id = createNumber("id", Long.class);

public final org.example.expert.domain.todo.entity.QTodo todo;

public final org.example.expert.domain.user.entity.QUser user;

public QManager(String variable) {
this(Manager.class, forVariable(variable), INITS);
}

public QManager(Path<? extends Manager> path) {
this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
}

public QManager(PathMetadata metadata) {
this(metadata, PathInits.getFor(metadata, INITS));
}

public QManager(PathMetadata metadata, PathInits inits) {
this(Manager.class, metadata, inits);
}

public QManager(Class<? extends Manager> type, PathMetadata metadata, PathInits inits) {
super(type, metadata, inits);
this.todo = inits.isInitialized("todo") ? new org.example.expert.domain.todo.entity.QTodo(forProperty("todo"), inits.get("todo")) : null;
this.user = inits.isInitialized("user") ? new org.example.expert.domain.user.entity.QUser(forProperty("user")) : null;
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.example.expert.domain.todo.dto.response;

import com.querydsl.core.types.dsl.*;

import com.querydsl.core.types.ConstructorExpression;
import javax.annotation.processing.Generated;

/**
* org.example.expert.domain.todo.dto.response.QTodoSearchResponse is a Querydsl Projection type for TodoSearchResponse
*/
@Generated("com.querydsl.codegen.DefaultProjectionSerializer")
public class QTodoSearchResponse extends ConstructorExpression<TodoSearchResponse> {

private static final long serialVersionUID = -1940571367L;

public QTodoSearchResponse(com.querydsl.core.types.Expression<String> title, com.querydsl.core.types.Expression<Long> managerCount, com.querydsl.core.types.Expression<Long> commentCount) {
super(TodoSearchResponse.class, new Class<?>[]{String.class, long.class, long.class}, title, managerCount, commentCount);
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.example.expert.domain.todo.entity;

import static com.querydsl.core.types.PathMetadataFactory.*;

import com.querydsl.core.types.dsl.*;

import com.querydsl.core.types.PathMetadata;
import javax.annotation.processing.Generated;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.PathInits;


/**
* QTodo is a Querydsl query type for Todo
*/
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QTodo extends EntityPathBase<Todo> {

private static final long serialVersionUID = -1664369315L;

private static final PathInits INITS = PathInits.DIRECT2;

public static final QTodo todo = new QTodo("todo");

public final org.example.expert.domain.common.entity.QTimestamped _super = new org.example.expert.domain.common.entity.QTimestamped(this);

public final ListPath<org.example.expert.domain.comment.entity.Comment, org.example.expert.domain.comment.entity.QComment> comments = this.<org.example.expert.domain.comment.entity.Comment, org.example.expert.domain.comment.entity.QComment>createList("comments", org.example.expert.domain.comment.entity.Comment.class, org.example.expert.domain.comment.entity.QComment.class, PathInits.DIRECT2);

public final StringPath contents = createString("contents");

//inherited
public final DateTimePath<java.time.LocalDateTime> createdAt = _super.createdAt;

public final NumberPath<Long> id = createNumber("id", Long.class);

public final ListPath<org.example.expert.domain.manager.entity.Manager, org.example.expert.domain.manager.entity.QManager> managers = this.<org.example.expert.domain.manager.entity.Manager, org.example.expert.domain.manager.entity.QManager>createList("managers", org.example.expert.domain.manager.entity.Manager.class, org.example.expert.domain.manager.entity.QManager.class, PathInits.DIRECT2);

//inherited
public final DateTimePath<java.time.LocalDateTime> modifiedAt = _super.modifiedAt;

public final StringPath title = createString("title");

public final org.example.expert.domain.user.entity.QUser user;

public final StringPath weather = createString("weather");

public QTodo(String variable) {
this(Todo.class, forVariable(variable), INITS);
}

public QTodo(Path<? extends Todo> path) {
this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
}

public QTodo(PathMetadata metadata) {
this(metadata, PathInits.getFor(metadata, INITS));
}

public QTodo(PathMetadata metadata, PathInits inits) {
this(Todo.class, metadata, inits);
}

public QTodo(Class<? extends Todo> type, PathMetadata metadata, PathInits inits) {
super(type, metadata, inits);
this.user = inits.isInitialized("user") ? new org.example.expert.domain.user.entity.QUser(forProperty("user")) : null;
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.example.expert.domain.user.entity;

import static com.querydsl.core.types.PathMetadataFactory.*;

import com.querydsl.core.types.dsl.*;

import com.querydsl.core.types.PathMetadata;
import javax.annotation.processing.Generated;
import com.querydsl.core.types.Path;


/**
* QUser is a Querydsl query type for User
*/
@Generated("com.querydsl.codegen.DefaultEntitySerializer")
public class QUser extends EntityPathBase<User> {

private static final long serialVersionUID = -1825397529L;

public static final QUser user = new QUser("user");

public final org.example.expert.domain.common.entity.QTimestamped _super = new org.example.expert.domain.common.entity.QTimestamped(this);

//inherited
public final DateTimePath<java.time.LocalDateTime> createdAt = _super.createdAt;

public final StringPath email = createString("email");

public final NumberPath<Long> id = createNumber("id", Long.class);

//inherited
public final DateTimePath<java.time.LocalDateTime> modifiedAt = _super.modifiedAt;

public final StringPath nickname = createString("nickname");

public final StringPath password = createString("password");

public final EnumPath<org.example.expert.domain.user.enums.UserRole> userRole = createEnum("userRole", org.example.expert.domain.user.enums.UserRole.class);

public QUser(String variable) {
super(User.class, forVariable(variable));
}

public QUser(Path<? extends User> path) {
super(path.getType(), path.getMetadata());
}

public QUser(PathMetadata metadata) {
super(User.class, metadata);
}

}

Loading