์ค๋๋ง์ ๊ธ์ ์ฐ๋ ๊ฑฐ ๊ฐ๋ค์.. ๋ฆฌ๋ ์ค๋ ๋ญ ์ด๊ฒ์ ๊ฒ ๊ณต๋ถํ๋ค๊ณ ๋ฐ๋ป์...ใ ใ
์ค๋์ QueryDSL์ ์ฌ์ฉํด์ ๊ฒ์ํ ๊ฒ์(์กฐ๊ฑด์ ๋ฐ๋ฅธ ๋์ ๊ฒ์), ํ์ด์ง, ์กฐํ ๊ธฐ๋ฅ์ ๊ตฌํํ์ฌ ๊ธฐ๋ณธ์ ์ธ ๊ฒ์ํ์ ์์ฑ์์ผ ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค!
- ์ํ๋ฆฌํฐ๋ฅผ ์ด์ฉํ JSON ๋ฐ์ดํฐ๋ก ๋ก๊ทธ์ธ (์๋ฃ)
- JWT๋ฅผ ์ด์ฉํ ์ธ์ฆ (์๋ฃ)
- ๋๋ฉ์ธ, ํ ์ด๋ธ ์ค๊ณ, ์ํฐํฐ ์์ฑ (์๋ฃ)
- ๋๊ธ ์ญ์ ๋ก์ง ๊ตฌํ (์๋ฃ)
- ํ์๊ฐ์ + ์ ๋ณด์์ ๋ฑ ํ์ ์๋น์ค ๊ตฌํ (์๋ฃ)
- ๊ฒ์ํ ์๋น์ค ๊ตฌํ (์งํ ์ค)
- ๋๊ธ ์๋น์ค ๊ตฌํ (1๋๊ธ -> *(๋ฌดํ) ๋๋๊ธ ๊ตฌ์กฐ) (์๋ฃ)
- ์์ธ ์ฒ๋ฆฌ (์๋ฃ)
- ์์ธ ๋ฉ์ธ์ง ๊ตญ์ ํ
- ์นดํ ๊ณ ๋ฆฌ๋ณ ๊ฒ์ํ ๋ถ๋ฅ
- ๊ฒ์๊ธ ํ์ด์ง (์งํ ์ค)
- ๋์ ์ธ ๊ฒ์ ์กฐ๊ฑด์ ์ฌ์ฉํ ๊ฒ์ (์งํ ์ค)
- ์ฌ์ฉ์ ๊ฐ ์ชฝ์ง ๊ธฐ๋ฅ
- ๋ฌดํ ์ชฝ์ง ์คํฌ๋กค
- ๊ฒ์๋ฌผ & ๋๊ธ์ ๋ํ ์๋
- ์ชฝ์ง์ ๋ํ ์๋
- ์ ์ํ ์ฌ์ฉ์ ๊ฐ ์ค์๊ฐ ์ฑํ
- ํ์๊ฐ์ ์ ๊ฒ์ฆ(์: XX๋ํ๊ต XX๊ณผ๊ฐ ์๋๋ฉด ๊ฐ์ ํ ์ ์๊ฒ)
- Swagger๋ฅผ ์ฌ์ฉํ API ๋ฌธ์ ๋ง๋ค๊ธฐ
- ์ ๊ณ & ๋ธ๋๋ฆฌ์คํธ ๊ธฐ๋ฅ
- AOP๋ฅผ ํตํ ๋ก๊ทธ
- ์ด๋๋ฏผ ํ์ด์ง
- ์บ์
- ๋ฐฐํฌ (+ ๋ฌด์ค๋จ ๋ฐฐํฌ)
- ๋ฐฐํฌ ์๋ํ
- ํฌํธ์ ์ด๋ํฐ ์ค๊ณ๋ฅผ ๋ฐ๋ฅด๋ ํจํค์ง ๊ตฌ์กฐ ์ค๊ณํ๊ธฐ
- ...
QueryDSL ์์กด์ฑ ์ถ๊ฐ
QueryDSL์ ์ฌ์ฉํ์ฌ ๊ตฌํํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์์กด์ฑ์ ์ถ๊ฐํด ์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค.
(ํน์ ์๋ ์ค์ ์ด ์๋ํ์ง ์๋๋ค๋ฉด, ์๋ ์ฌ์ดํธ๋ ์ฐธ๊ณ ํด์ฃผ์ธ์.
https://ttl-blog.tistory.com/855?category=906290 )
build.gradle์ ์ถ๊ฐ
plugins {
...
//QueryDSL ์ถ๊ฐ
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
dependencies {
...
//QueryDSL ์ถ๊ฐ
implementation 'com.querydsl:querydsl-jpa:5.0.0'
annotationProcessor group: 'com.querydsl', name: 'querydsl-apt', version: '5.0.0'
}
(๋ค์์ ๋งจ ์๋์ ๋ฐ๋ก ์ถ๊ฐํด์ฃผ์๋ฉด ๋ฉ๋๋ค.)
//querydsl ์ถ๊ฐ ์์
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
//querydsl ์ถ๊ฐ ๋
์ ์ฒด build.gradle
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
group = 'boardexample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
//p6spy ์ ์ฉ
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.7.1'
//JWT
implementation 'com.auth0:java-jwt:3.18.2'
//QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0'
annotationProcessor group: 'com.querydsl', name: 'querydsl-apt', version: '5.0.0'
}
test {
useJUnitPlatform()
}
//querydsl ์ถ๊ฐ ์์
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
//querydsl ์ถ๊ฐ ๋
์ ๋๋ก ์ถ๊ฐ๋์๋์ง ํ์ธํ๊ธฐ ์ํด ์ธํ ๋ฆฌ์ ์ด ์ฐ์ธก์ Gradle -> Tasks -> other -> complieQuerydsl์ ํด๋ฆญํด์ค์๋ค.
(์ ํ ๋ง ๋ฐ๊ฟจ๋๋ฐ ์ด๋ป์ ๋ง์๋ญ๋๋ค ใ ใ .)
๊ทธ๋ฆฌ๊ณ ์ฑ๊ณตํ์ จ๋ค๋ฉด build ํ์ผ๊ณผ, ๊ทธ ์์ querydsl ํ์ผ ์์ Qํ์ ํ์ผ๋ค์ด ์๊ธฐ์ จ์๊ฒ๋๋ค
๊ทธ๋ ๋ค๋ฉด ์ด์ QueryDSL์ ์ฌ์ฉํ ์ค๋น๊ฐ ์ ๋ง ๋ค ๋๋๊ฒ๋๋ค
application.yml ๋ณ๊ฒฝ
์ถ๊ฐํ ์ค์ ๋ค์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
1. ํ์ด์ง ํ ๋ ๊ธฐ๋ณธ ํ์ด์ง ๊ฐ์
2. ํ์ผ ์ ๋ก๋ ์, ํ์ผ์ ๊ฐ๋น ํฌ๊ธฐ & ๋ชจ๋ ํ์ผ์ ์ด๋ ์ ํ (์ ๋ฒ์ ๊ตฌํํ ๋ ํ์ด์ผ ํ๋๋ฐ... ์ํ์์ต๋๋ค..ใ ใ )
3. OSIV ์ฌ์ฉ X
4. default_batch_fetch_size ์ง์ (Collection ์กฐํ ์ฟผ๋ฆฌ ์ต์ ํ๋ฅผ ์ํจ)
5. ํ ์คํธ ํ๊ฒฝ ๋ถ๋ฆฌํ๊ธฐ
(์ด์ ๋ถํฐ๋ ์ผ๋ฐ ์ ํ๋ฆฌ์ผ์ด์ ํ๊ฒฝ์์๋ ๋๋ฏธ ๋ฐ์ดํฐ๋ฅผ ์ ๋ ฅํด๋๊ณ ์ฌ์ฉํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์, ํ ์คํธ์ฝ๋์์ ํ๊ฒฝ์ ๋ถ๋ฆฌํ์ฌ
ํ ์คํธ์ฝ๋๋ ๋๋ฏธ ๋ฐ์ดํฐ ์์ด ๊ธฐ์กด์ ํ ์คํธ์ฝ๋๊ฐ ์ ์๋ํ๋๋ก ๋ง๋ค์ด์ฃผ๊ฒ ์ต๋๋ค.)
์ถ๊ฐํ ๋ถ๋ถ์ ์ฃผ์์ ๋ฌ์์ต๋๋ค.
(์ค์ ์ด ์กฐ๊ธ์ฉ ๋ฐ๋ ๋ถ๋ถ์ด ์์ต๋๋ค. ๋น๊ตํด๊ฐ๋ฉด์, ์์์ ์ค์ ํด์ฃผ์ธ์!)
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:tcp://localhost/~/jpa
username: sa
password: 1
data:
web:
pageable:
default-page-size: 20 #ํ์ด์ง ํ ๋ ๊ธฐ๋ณธ๊ฐ, 20๊ฐ์ฉ ์กฐํ
servlet:
multipart:
max-request-size: 5MB #์
๋ก๋ ํ์ผ ํฌ๊ธฐ ์ด๋ ์ ํ
max-file-size: 2MB #์
๋ก๋ ํ์ผ ํฌ๊ธฐ ์ ํ
jpa:
show-sql: true #P6spy์ ํจ๊ป, SQL ๋ก๊ทธ๋ ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค..! ์ํด์ฃผ์
๋ ๋ฌด๋ฐฉํฉ๋๋ค๋ง, P6spy๊ฐ ๋๋ฌด ์ ์ด์๊ฒ ๋ณด์ฌ์ค์...ใ
ใ
properties:
hibernate:
format_sql: true
user_sql_cooments: true
default_batch_fetch_size: 500 #๋ฐฐ์น ์ฌ์ด์ฆ (Collection ์กฐํ ์ต์ ํ)
hibernate:
ddl-auto: none ##์ด์ ๋ฐฐํฌํ๊ฒฝ์์๋ create๋ฅผ ์ฌ์ฉํ์ง ์๊ฒ ์ต๋๋ค.
open-in-view: false #OSIV ์ฌ์ฉํ์ง ์๊ธฐ
profiles:
include: jwt
logging:
level:
org:
apache:
coyote:
http11: OFF #์ ๋ HTTP๋ก๊ทธ๋ฅผ ์ฌ์ฉํ์ง ์๊ฒ ์ต๋๋ค. ์ฌ์ฉํ์
๋ ๋ฌด๋ฐฉํฉ๋๋ค.
hiberante:
SQL: debug
boardexample:
myboard: info
file:
dir: D:\files\
ํ ์คํธ ํ๊ฒฝ ๋ถ๋ฆฌํ๊ธฐ
๋ค์ ์์น์ application.yml ํ์ผ์ ์ถ๊ฐํฉ๋๋ค.
ํ ์คํธํ๊ฒฝ application.yml
๋ค์๊ณผ ๊ฐ์ต๋๋ค.
(์ด์ ๋ถํฐ ํ ์คํธ ํ๊ฒฝ์์๋ ์๋ฒ ๋๋ ๋ฐฉ์์ H2๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๋๋ก ํฉ๋๋ค.)
spring:
profiles:
include: jwt
datasource:
driver-class-name: org.h2.Driver
username: sa
data:
web:
pageable:
default-page-size: 20
servlet:
multipart:
max-request-size: 5MB
max-file-size: 2MB
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
user_sql_cooments: true
default_batch_fetch_size: 100
hibernate:
ddl-auto: create
open-in-view: false
h2:
console:
enabled: true ##์๋ฒ ๋๋ ๋ฐฉ์์ H2๋๋น ์ฌ์ฉ
logging:
level:
org:
apache:
coyote:
http11: OFF #debug
hiberante:
SQL: debug
boardexample:
myboard: info
file:
dir: D:\files\
Member ํด๋์ค ์์
Member์์ ๋ค์ ๋ ํ๋์ ๋ํด์ ์ถ๊ฐ ์์ ์ ํด์ฃผ์ด์ผ ํฉ๋๋ค.
//== ํ์ํํด -> ์์ฑํ ๊ฒ์๋ฌผ, ๋๊ธ ๋ชจ๋ ์ญ์ ==//
@OneToMany(mappedBy = "writer", cascade = ALL, orphanRemoval = true)
private List<Post> postList = new ArrayList<>();
@OneToMany(mappedBy = "writer", cascade = ALL, orphanRemoval = true)
private List<Comment> commentList = new ArrayList<>();
์์ ๋ ํ๋๊ฐ ์์ต๋๋ค.
์ ๋ ํ์ฌ ํด๋์ค์ @Builder๋ฅผ ๋ถ์๊ธฐ์, new ArrayList();๋ก ์ด๊ธฐํ๋ฅผ ์์ผ์คฌ์์๋ ๋ถ๊ตฌํ๊ณ ๋น๋๋ฅผ ํตํด Member๋ฅผ ์์ฑํ๋ฉด ์ ๋ ํ๋๋ null์ด ๋ค์ด์ค๊ฒ ๋ฉ๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ค์ ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ฃผ๊ฒ ์ต๋๋ค.
@Builder.Default
์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
import static javax.persistence.CascadeType.ALL;
import static javax.persistence.EnumType.*;
import static javax.persistence.GenerationType.*;
@Table(name = "MEMBER")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@AllArgsConstructor
@Builder
public class Member extends BaseTimeEntity {
@Id @GeneratedValue(strategy = IDENTITY)
@Column(name = "member_id")
private Long id; //primary Key
@Column(nullable = false, length = 30, unique = true)
private String username;//์์ด๋
private String password;//๋น๋ฐ๋ฒํธ
@Column(nullable = false, length = 30)
private String name;//์ด๋ฆ(์ค๋ช
)
@Column(nullable = false, length = 30)
private String nickName;//๋ณ๋ช
@Column(nullable = false, length = 30)
private Integer age;//๋์ด
@Enumerated(STRING)
@Column(nullable = false, length = 30)
private Role role;//๊ถํ -> USER, ADMIN
@Column(length = 1000)
private String refreshToken;//RefreshToken
//== ํ์ํํด -> ์์ฑํ ๊ฒ์๋ฌผ, ๋๊ธ ๋ชจ๋ ์ญ์ ==//
@Builder.Default
@OneToMany(mappedBy = "writer", cascade = ALL, orphanRemoval = true)
private List<Post> postList = new ArrayList<>();
@Builder.Default
@OneToMany(mappedBy = "writer", cascade = ALL, orphanRemoval = true)
private List<Comment> commentList = new ArrayList<>();
//== ์ฐ๊ด๊ด๊ณ ๋ฉ์๋ ==//
public void addPost(Post post){
//post์ writer ์ค์ ์ post์์ ํจ
postList.add(post);
}
public void addComment(Comment comment){
//comment์ writer ์ค์ ์ comment์์ ํจ
commentList.add(comment);
}
//== ์ ๋ณด ์์ ==//
public void updatePassword(PasswordEncoder passwordEncoder, String password){
this.password = passwordEncoder.encode(password);
}
public void updateName(String name){
this.name = name;
}
public void updateNickName(String nickName){
this.nickName = nickName;
}
public void updateAge(int age){
this.age = age;
}
public void updateRefreshToken(String refreshToken){
this.refreshToken = refreshToken;
}
public void destroyRefreshToken(){
this.refreshToken = null;
}
/**
* ํจ์ค์๋ ์ํธํ
*/
public void encodePassword(PasswordEncoder passwordEncoder){
this.password = passwordEncoder.encode(password);
}
/**
* ํจ์ค์๋ ์ผ์นํ๋์ง ํ์ธ
* @param passwordEncoder ํจ์ค์๋ ์ธ์ฝ๋
* @param checkPassword ๊ฒ์ฌํ ๋น๋ฐ๋ฒํธ
* @return
*/
public boolean matchPassword(PasswordEncoder passwordEncoder, String checkPassword){
return passwordEncoder.matches(checkPassword, getPassword());
}
//== ๊ถํ ๋ถ์ฌ ==//
public void addUserAuthority() {
this.role = Role.USER;
}
}
PostServiceImpl ์ถ๊ฐ ๊ตฌํ
ํ์ฌ ์ ํฌ๋ getPostInfo์ getPostList๋ฉ์๋๋ฅผ ๊ตฌํํ์ง ์์์ต๋๋ค.
์ฐ์ ๊ฒ์๊ธ ํ๋๋ฅผ ์กฐํํ๋ getPostInfo๋ถํฐ ๊ตฌํํด ๋ณด๊ฒ ์ต๋๋ค.
/**
* Post์ id๋ฅผ ํตํด Post ์กฐํ
*/
@Override
public PostInfoDto getPostInfo(Long id) {
/**
* Post + MEMBER ์กฐํ -> ์ฟผ๋ฆฌ 1๋ฒ ๋ฐ์
*
* ๋๊ธ&๋๋๊ธ ๋ฆฌ์คํธ ์กฐํ -> ์ฟผ๋ฆฌ 1๋ฒ ๋ฐ์(POST ID๋ก ์ฐพ๋ ๊ฒ์ด๋ฏ๋ก, IN์ฟผ๋ฆฌ๊ฐ ์๋ ์ผ๋ฐ where๋ฌธ ๋ฐ์)
* (๋๊ธ๊ณผ ๋๋๊ธ ๋ชจ๋ Comment ํด๋์ค์ด๋ฏ๋ก, JPA๋ ๊ตฌ๋ถํ ๋ฐฉ๋ฒ์ด ์์ด์, ๋น์ฐํ CommentList์ ๋ชจ๋ ๋์ค๋๊ฒ์ด ๋ง๋ค,
* ๊ฐ์ง๊ณ ์จ ๊ฒ์ ๊ฐ์ง๊ณ ์ฐ๋ฆฌ๊ฐ ๊ตฌ๋ถ์ง์ด์ฃผ์ด์ผ ํ๋ค.)
*
* ๋๊ธ ์์ฑ์ ์ ๋ณด ์กฐํ -> ๋ฐฐ์น์ฌ์ด์ฆ๋ฅผ ์ด์ฉํ๊ธฐ๋๋ฌธ์ ์ฟผ๋ฆฌ 1๋ฒ ํน์ N/๋ฐฐ์น์ฌ์ด์ฆ ๋งํผ ๋ฐ์
*
*
*/
return new PostInfoDto(postRepository.findWithWriterById(id)
.orElseThrow(() -> new PostException(POST_NOT_POUND)));
}
๋จผ์ findWithWriterById๋ฅผ ์ฌ์ฉํ๋๋ฐ, ์ด๋ ์ ๊ฐ ๋ง๋ค์ด์ค ํจ์์ ๋๋ค.
PostRepository์ ์ถ๊ฐ
public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(attributePaths = {"writer"})
Optional<Post> findWithWriterById(Long id);
}
@EntityGraph๋ฅผ ์ฌ์ฉํ์ฌ Post์ Member๋ฅผ ์ฟผ๋ฆฌ ํ๋ฒ์ ๊ฐ์ ธ์์ต๋๋ค.
(EntityGraph๋ ํ์น์กฐ์ธ์ ๊ฐํธํ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก ํด์ฃผ๋ ์ด๋ ธํ ์ด์ ์ ๋๋ค!)
์๋ฅผ JPQL๋ก ๋ฐ๊พธ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
"select p from Post p join fetch p.writer w where p.id = :id"
์ด์ PostInfoDto๋ฅผ ๊ตฌํํ๋ฌ ๊ฐ๊ฒ ์ต๋๋ค.
PostInfoDto ๊ตฌํ
@Data
public class PostInfoDto {
private Long postId; //POST์ ID
private String title;//์ ๋ชฉ
private String content;//๋ด์ฉ
private String filePath;//์
๋ก๋ ํ์ผ ๊ฒฝ๋ก
private MemberInfoDto writerDto;//์์ฑ์์ ๋ํ ์ ๋ณด
private List<CommentInfoDto> commentInfoDtoList;//๋๊ธ ์ ๋ณด๋ค
public PostInfoDto(Post post) {
this.postId = post.getId();
this.title = post.getTitle();
this.content = post.getContent();
this.filePath = post.getFilePath();
this.writerDto = new MemberInfoDto(post.getWriter());
/**
* ๋๊ธ๊ณผ ๋๋๊ธ์ ๊ทธ๋ฃน์ง๊ธฐ
* post.getCommentList()๋ ๋๊ธ๊ณผ ๋๋๊ธ์ด ๋ชจ๋ ์กฐํ๋๋ค.
*/
Map<Comment, List<Comment>> commentListMap = post.getCommentList().stream()
.filter(comment -> comment.getParent() != null)
.collect(Collectors.groupingBy(Comment::getParent));
/**
* ๋๊ธ๊ณผ ๋๋๊ธ์ ํตํด CommentInfoDto ์์ฑ
*/
commentInfoDtoList = commentListMap.keySet().stream()
.map(comment -> new CommentInfoDto(comment, commentListMap.get(comment)))
.toList();
}
}
๋๊ธ๊ณผ ๋๋๊ธ ๊ทธ๋ฃน์ง๊ธฐ
๋๊ธ๊ณผ ๋๋๊ธ์ ๊ทธ๋ฃน์ง๋ ๋ถ๋ถ์ด ์ด๋ ค์ฐ์ค ๊ฒ ๊ฐ์ ์กฐ๊ธ ์์ธํ ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
Map<Comment, List<Comment>> commentListMap = post.getCommentList().stream() //(1)
.filter(comment -> comment.getParent() != null) //(2)
.collect(Collectors.groupingBy(Comment::getParent)); //(3)
commentInfoDtoList = commentListMap.keySet().stream() //(4)
.map(comment -> new CommentInfoDto(comment, commentListMap.get(comment)))//(5)
.toList();
์ค๋ช
(1) post์ Comment ๋ฆฌ์คํธ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
์ด๋ CommentList๋ ๋๊ธ๊ณผ ๋๋๊ธ์ด ๋ชจ๋ ์์ฌ์๋ ์ํ์ ๋๋ค.
๊ทธ ์ด์ ๋ Comment์ Recomment๋ ๋จ์ง parent๊ฐ ์๋์ง ์๋์ง๋ก๋ง ๊ตฌ๋ถ์ง์ด์ง๋ฏ๋ก, JPA๋ ๋๊ธ๊ณผ ๋๋๊ธ์ ๊ตฌ๋ถํ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
๋ฐ๋ผ์ CommentList๋ฅผ ํตํด ๋๊ธ๊ณผ ๋๋๊ธ์ ๋ชจ๋ ๊ฐ์ ธ์ค๊ฒ ๋๋ ๊ฒ์ ๋๋ค.
(์ถ๊ฐ๋ก ์ด๋ ๋ฐฐ์น ์ฌ์ด์ฆ๋ฅผ 100์ผ๋ก ์ค์ ํด ์ฃผ์๊ธฐ ๋๋ฌธ์, ์ฟผ๋ฆฌ๋ 1๋ฒ ํน์ N/100๋งํผ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.)
(2) Comment์ parent๊ฐ null์ด ์๋, ์ฆ ๋๊ธ์ด ์๋ ๋๋๊ธ์ธ ๊ฒ๋ค๋ง ๊ฐ์ ธ์ต๋๋ค.
(3) ํํฐ๋ง ๋ ๊ฒ๋ค์ ๋ชจ๋ ๋๋๊ธ์ด๊ณ , ๋๋๊ธ์ Parent(๋๊ธ)๋ฅผ ํตํด ๊ทธ๋ฃนํํฉ๋๋ค. ์ด๋ ๊ฒ ๋๋ฉด Map์๋ <๋๊ธ, List<ํด๋น ๋๊ธ์ ๋ฌ๋ฆฐ ๋๋๊ธ>>์ ํ์์ผ๋ก ๊ทธ๋ฃนํ๋ฉ๋๋ค.
(4) ๊ทธ๋ฃน์ง์ ๊ฒ๋ค ์ค keySet , ์ฆ ๋๊ธ๋ค์ ๊ฐ์ง๊ณ ์ต๋๋ค.
(5) ๋๊ธ๋ค์ CommentInfoDto๋ก ๋ณํ์์ผ์ค๋๋ค. ์ด๋ CommentInfoDto์ ์์ฑ์๋ก ๋๊ธ๊ณผ ํด๋น ๋๊ธ์ ๋ฌ๋ฆฐ ๋๋๊ธ๋ค์ ์ธ์๋ก ๋ฃ์ด์ค๋๋ค.
์ด์ CommentInfoDto๋ฅผ ์์ฑํ๋๋ก ํ๊ฒ ์ต๋๋ค.
CommentInfoDto ์์ฑ
์์น๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
package boardexample.myboard.domain.commnet.dto;
import boardexample.myboard.domain.commnet.Comment;
import boardexample.myboard.domain.member.dto.MemberInfoDto;
import lombok.Data;
import java.util.List;
@Data
public class CommentInfoDto {
private final static String DEFAULT_DELETE_MESSAGE = "์ญ์ ๋ ๋๊ธ์
๋๋ค";
private Long postId;//๋๊ธ์ด ๋ฌ๋ฆฐ POST์ ID
private Long commentId;//ํด๋น ๋๊ธ์ ID
private String content;//๋ด์ฉ (์ญ์ ๋์๋ค๋ฉด "์ญ์ ๋ ๋๊ธ์
๋๋ค ์ถ๋ ฅ")
private boolean isRemoved;//์ญ์ ๋์๋์ง?
private MemberInfoDto writerDto;//๋๊ธ ์์ฑ์์ ๋ํ ์ ๋ณด
private List<ReCommentInfoDto> reCommentListDtoList;//๋๋๊ธ์ ๋ํ ์ ๋ณด๋ค
/**
* ์ญ์ ๋์์ ๊ฒฝ์ฐ ์ญ์ ๋ ๋๊ธ์
๋๋ค ์ถ๋ ฅ
*/
public CommentInfoDto(Comment comment, List<Comment> reCommentList) {
this.postId = comment.getPost().getId();
this.commentId = comment.getId();
this.content = comment.getContent();
if(comment.isRemoved()){
this.content = DEFAULT_DELETE_MESSAGE;
}
this.isRemoved = comment.isRemoved();
this.writerDto = new MemberInfoDto(comment.getWriter());
this.reCommentListDtoList = reCommentList.stream().map(ReCommentInfoDto::new).toList();
}
}
์ด๋ ค์ด ๊ฒ์ ์์ ๊ฒ์ด๋ผ ์๊ฐํ๊ณ ๋์ด๊ฐ๊ฒ ์ต๋๋ค.
ReCommentInfoDto ์์ฑ
package boardexample.myboard.domain.commnet.dto;
import boardexample.myboard.domain.commnet.Comment;
import boardexample.myboard.domain.member.dto.MemberInfoDto;
import lombok.Data;
@Data
public class ReCommentInfoDto {
private final static String DEFAULT_DELETE_MESSAGE = "์ญ์ ๋ ๋๊ธ์
๋๋ค";
private Long postId;
private Long parentId;
private Long reCommentId;
private String content;
private boolean isRemoved;
private MemberInfoDto writerDto;
public ReCommentInfoDto(Comment reComment) {
this.postId = reComment.getPost().getId();
this.parentId = reComment.getParent().getId();
this.reCommentId = reComment.getId();
this.content = reComment.getContent();
if(reComment.isRemoved()){
this.content = DEFAULT_DELETE_MESSAGE;
}
this.isRemoved = reComment.isRemoved();
this.writerDto = new MemberInfoDto(reComment.getWriter());
}
}
์์ฑํ์ต๋๋ค.
์ด์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ ์ ๋๋ฏธ ๋ฐ์ดํฐ๋ฅผ ์ ๋ ฅํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๋๋ฏธ๋ฐ์ดํฐ ์ ๋ ฅ
์์น๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
(์ด๋ฆ์.. ๋ญ๋ผ ์ง์ ์ง ๋ชจ๋ฅด๊ฒ ์ด์ ์ ๋ ๊ฒ ์ง์์ต๋๋ค)
@RequiredArgsConstructor
@Component
public class InitService {
private final Init init;
@PostConstruct
public void init(){
init.save();
}
@RequiredArgsConstructor
@Component
private static class Init{
private final MemberRepository memberRepository;
private final PostRepository postRepository;
private final CommentRepository commentRepository;
@Transactional
public void save() {
PasswordEncoder delegatingPasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
//== MEMBER ์ ์ฅ ==//
memberRepository.save(Member.builder().username("username1").password(delegatingPasswordEncoder.encode("1234567890")).name("USER1").nickName("๋ฐฅ ์๋จน๋ ๋ํ์ด1").role(Role.USER).age(22).build());
memberRepository.save(Member.builder().username("username2").password(delegatingPasswordEncoder.encode("1234567890")).name("USER2").nickName("๋ฐฅ ์๋จน๋ ๋ํ์ด2").role(Role.USER).age(22).build());
memberRepository.save(Member.builder().username("username3").password(delegatingPasswordEncoder.encode("1234567890")).name("USER3").nickName("๋ฐฅ ์๋จน๋ ๋ํ์ด3").role(Role.USER).age(22).build());
Member member = memberRepository.findById(1L).orElse(null);
for(int i = 0; i<=50; i++ ){
Post post = Post.builder().title(format("๊ฒ์๊ธ %s", i)).content(format("๋ด์ฉ %s", i)).build();
post.confirmWriter(memberRepository.findById((long) (i % 3 + 1)).orElse(null));
postRepository.save(post);
}
for(int i = 1; i<=150; i++ ){
Comment comment = Comment.builder().content("๋๊ธ" + i).build();
comment.confirmWriter(memberRepository.findById((long) (i % 3 + 1)).orElse(null));
comment.confirmPost(postRepository.findById(parseLong(valueOf(i%50 + 1))).orElse(null));
commentRepository.save(comment);
}
commentRepository.findAll().stream().forEach(comment -> {
for(int i = 1; i<=50; i++ ){
Comment recomment = Comment.builder().content("๋๋๊ธ" + i).build();
recomment.confirmWriter(memberRepository.findById((long) (i % 3 + 1)).orElse(null));
recomment.confirmPost(comment.getPost());
recomment.confirmParent(comment);
commentRepository.save(recomment);
}
});
}
}
}
์ฝ๋๋ฅผ ๋ณด์๋ฉด ์์ํ์ ๋ถ๋ถ์ด ํ๋ ์์ ๊ฒ์ ๋๋ค.
์ ๊ตณ์ด inner ํด๋์ค๋ฅผ ์์ฑํด์ ๋๋ฏธ ๋ฐ์ดํฐ๋ฅผ ์ ๋ ฅํด ์ฃผ์์๊น์?
@PostConstruct๋ ํด๋น ๋น ์์ฒด๋ง ์์ฑ๋์๋ค๊ณ ๊ฐ์ ํ๊ณ ํธ์ถ๋ฉ๋๋ค. ํด๋น ๋น์ ๊ด๋ จ๋ AOP๋ฑ์ ํฌํจํ, ์ ์ฒด ์คํ๋ง ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ๊ฐ ์ด๊ธฐํ ๋ ๊ฒ์ ์๋ฏธํ์ง๋ ์์ต๋๋ค.
ํธ๋์ญ์ ์ ์ฒ๋ฆฌํ๋ AOP๋ฑ์ ์คํ๋ง์ ํ ์ฒ๋ฆฌ๊ธฐ(post processer)๊ฐ ์์ ํ ๋์์ ๋๋ด์, ์คํ๋ง ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ์ ์ด๊ธฐํ๊ฐ ์๋ฃ๋์ด์ผ ์ ์ฉ๋ฉ๋๋ค.
์ ๋ฆฌํ๋ฉด @PostConstruct๋ ํด๋น๋น์ AOP ์ ์ฉ์ ๋ณด์ฅํ์ง ์์ต๋๋ค.
- ๊น์ํ ์ ์๋
๋ฐ๋ผ์ ์์ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ์ฐํํ์ฌ ์ฌ์ฉํ์ต๋๋ค!
์ด์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํ์์ผ๋ณด๋ฉด ๋ฐ์ดํฐ๊ฐ ์ ๋ค์ด๊ฐ ๊ฒ์ ๋๋ค!
(์ด๋ ํ ์คํธ์ฝ๋๋ฅผ ์๋ํด๋ ๊ณ์ ์๋ํ๊ธฐ ๋๋ฌธ์ ์ ํ๋ฆฌ์ผ์ด์ ์ 1ํ ์คํ์์ผ ๋ฐ์ดํฐ๊ฐ ์ ๋ ฅ๋์๋ค๋ฉด ์ ์ฒด๋ฅผ ์ฃผ์ ์ฒ๋ฆฌํด์ ํ ์คํธ์ฝ๋๊ฐ ์คํ๋ ๋ ๋ฐ์ดํฐ๊ฐ ์ ๋ ฅ๋๋ ๊ฒ์ ๋ฐฉ์งํด์ฃผ์ธ์)
Post ์กฐํ ๊ธฐ๋ฅ ํ ์คํธ์ฝ๋ ์์ฑ
@Test
public void ํฌ์คํธ_์กฐํ() throws Exception {
Member member1 = memberRepository.save(Member.builder().username("username1").password("1234567890").name("USER1").nickName("๋ฐฅ ์๋จน๋ ๋ํ์ด1").role(Role.USER).age(22).build());
Member member2 = memberRepository.save(Member.builder().username("username2").password("1234567890").name("USER1").nickName("๋ฐฅ ์๋จน๋ ๋ํ์ด2").role(Role.USER).age(22).build());
Member member3 = memberRepository.save(Member.builder().username("username3").password("1234567890").name("USER1").nickName("๋ฐฅ ์๋จน๋ ๋ํ์ด3").role(Role.USER).age(22).build());
Member member4 = memberRepository.save(Member.builder().username("username4").password("1234567890").name("USER1").nickName("๋ฐฅ ์๋จน๋ ๋ํ์ด4").role(Role.USER).age(22).build());
Member member5 = memberRepository.save(Member.builder().username("username5").password("1234567890").name("USER1").nickName("๋ฐฅ ์๋จน๋ ๋ํ์ด5").role(Role.USER).age(22).build());
Map<Integer, Long> memberIdMap = new HashMap<>();
memberIdMap.put(1,member1.getId());
memberIdMap.put(2,member2.getId());
memberIdMap.put(3,member3.getId());
memberIdMap.put(4,member4.getId());
memberIdMap.put(5,member5.getId());
/**
* Post ์์ฑ
*/
Post post = Post.builder().title("๊ฒ์๊ธ").content("๋ด์ฉ").build();
post.confirmWriter(member1);
postRepository.save(post);
em.flush();
/**
* Comment ์์ฑ(๋๊ธ)
*/
final int COMMENT_COUNT = 10;
for(int i = 1; i<=COMMENT_COUNT; i++ ){
Comment comment = Comment.builder().content("๋๊ธ" + i).build();
comment.confirmWriter(memberRepository.findById(memberIdMap.get(i % 3 + 1)).orElse(null));
comment.confirmPost(post);
commentRepository.save(comment);
}
/**
* ReComment ์์ฑ(๋๋๊ธ)
*/
final int COMMENT_PER_RECOMMENT_COUNT = 20;
commentRepository.findAll().stream().forEach(comment -> {
for(int i = 1; i<=20; i++ ){
Comment recomment = Comment.builder().content("๋๋๊ธ" + i).build();
recomment.confirmWriter(memberRepository.findById(memberIdMap.get(i % 3 + 1)).orElse(null));
recomment.confirmPost(comment.getPost());
recomment.confirmParent(comment);
commentRepository.save(recomment);
}
});
clear();
//when
PostInfoDto postInfo = postService.getPostInfo(post.getId());
//then
assertThat(postInfo.getPostId()).isEqualTo(post.getId());
assertThat(postInfo.getContent()).isEqualTo(post.getContent());
assertThat(postInfo.getWriterDto().getUsername()).isEqualTo(post.getWriter().getUsername());
int recommentCount = 0;
for (CommentInfoDto commentInfoDto : postInfo.getCommentInfoDtoList()) {
recommentCount += commentInfoDto.getReCommentListDtoList().size();
}
assertThat(postInfo.getCommentInfoDtoList().size()).isEqualTo(COMMENT_COUNT);
assertThat(recommentCount).isEqualTo(COMMENT_PER_RECOMMENT_COUNT * COMMENT_COUNT);
}
๋ด์ฉ์ด ๋๋ฌด ๊ธธ๊ธฐ ๋๋ฌธ์, ๋ค์ ํฌ์คํ ์ ์ด์ด์ ๊ฒ์ ์กฐ๊ฑด์ ๋ฐ๋ฅธ ๊ฒ์ํ ๋ฆฌ์คํธ๋ฅผ ์กฐํํ๋ ์ฝ๋๋ฅผ ์์ฑํ๋๋ก ํ๊ฒ ์ต๋๋ค.
์ ์ฒด ์ฝ๋๋ ๊นํ๋ธ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
https://github.com/ShinDongHun1/SpringBoot-Board-API
๐ Reference
๋กฌ๋ณต ๋น๋ ์ฌ์ฉ์ ์ปฌ๋ ์ ์ Null์ด ์ธํ ๋๋ ๊ฒฝ์ฐ
@PostConstruct์ @Transactional์ ๋ถ๋ฆฌ
https://www.inflearn.com/questions/26902