궤도
[Spring] [면적면적[14]] Refactoring 본문
북적북적의 백엔드를 클론하고 있다.
build.gradle
plugins {
id 'org.springframework.boot' version '2.5.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
id 'java'
}
group = 'jpa'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.6.RELEASE'
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
implementation 'com.querydsl:querydsl-jpa'
implementation 'org.projectlombok:lombok:1.18.18'
runtimeOnly 'mysql:mysql-connector-java'
implementation 'com.googlecode.json-simple:json-simple:1.1.1'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
configurations {
querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
options.annotationProcessorPath = configurations.querydsl
}
test {
useJUnitPlatform()
}
src/main/resources/application.yml
aladin:
url: http://www.aladin.co.kr/ttb/api/ItemLookUp.aspx?ttbkey=비밀키&itemIdType=ISBN13&output=js&Version=20131101&ItemId={isbn}
naver:
id: 아이디
secret: 비번
secret:
key: 비번
cloud:
aws:
credentials:
access-key: 엑세스 키
secret-key: 시크릿 키
s3:
bucket: 버킷 이름
region:
static: 지역
stack:
auto: false
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3305/데베?serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: 비번
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
open-in-view: false
hibernate:
ddl-auto: none
properties:
hibernate:
format_sql: true
show-sql: true
#logging:
# level:
# org:
# hibernate:
# type:
# descriptor:
# sql: trace
server:
servlet:
encoding:
force-response: true
logging:
level:
com:
amazonaws:
util:
EC2MetadataUtils: error
구현할건 다 구현했다. 이제 리팩토링을 하자...
일단 제일먼저 할 것은
public double bookHeight() {
return books.stream()
.filter(o -> o.getTotPage() != null)
.filter(o -> o.getBookStatus() == BookStatus.DONE)
.mapToInt(Book::getTotPage).sum() * 0.005;
}
이 친구다.
이 때 만들었던 비즈니스 로직인데
이런거였다.
이 로직은
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CharactersService {
private final UserCharacterRepository userCharacterRepository;
private final CharactersRepository charactersRepository;
private final CharactersMapper charactersMapper;
/**
* addNewCharacters
*
* @param user
* @return Characters
*/
@Transactional
public Characters addNewCharacters(User user) {
List<UserCharacter> list = charactersRepository.findByHeightLessThanEqualAndIdNotInOrderByHeightDesc(user.bookHeight(), getCharactersFromUser(user)).stream()
.map(o -> UserCharacter.builder()
.user(user)
.characters(o)
.achieve(LocalDate.now())
.representation(false)
.build())
.collect(Collectors.toList());
if (!list.isEmpty()) {
userCharacterRepository.saveAll(list);
return list.get(0).getCharacters();
}
return null;
}
/**
* removeCharacters
*
* @param user
*/
@Transactional
public void removeCharacters(User user) {
double height = user.bookHeight();
List<UserCharacter> list = user.getUserCharacters().stream()
.filter(o -> o.getCharacters().getHeight() > height)
.collect(Collectors.toList());
if (list.isEmpty())
return;
user.getUserCharacters().removeAll(list);
userCharacterRepository.deleteAll(list);
}
}
직접적으로는 여기서 쓰고
간접적으로는 여기저기서 쓴다. 책 추가/삭제/수정 등등...
근데 이걸 왜 수정하려 하는가?
public double bookHeight() {
return books.stream()
.filter(o -> o.getTotPage() != null)
.filter(o -> o.getBookStatus() == BookStatus.DONE)
.mapToInt(Book::getTotPage).sum() * 0.005;
}
여기선 사용자의 책 높이를 구하기 위해 사용자가 지닌 책을 모두 가져와서 거기서 높이만 빼와서 더하는 기타등등의 일을 한다. 한번에 n권씩 추가/삭제/수정하는거면 모를까 한번에 하나씩만 조작되는 현 상태에서 굳이 매번 이렇게 계산하는건 비효율적이다.
@ColumnDefault("0.0")
private double bookHeight;
그래서 User 도메인에 bookHeight라는 칼럼을 추가했다.
처음 설계할 때에는 이렇게 변동이 많고 다른 칼럼에 의존적인 칼럼을 만드는게 옳은 일인가? 싶었지만 일단 해보자
public void stackBook(Integer height) {
if (height != null)
this.bookHeight += (height * 0.005);
}
public void removeBook(Integer height) {
if (height != null)
this.bookHeight -= (height * 0.005);
}
그리고 이런 로직을 만들었다. 책을 쌓거나, 삭제할 때 그에 맞춰 bookHeight 값을 갱신하는 로직이다.
물론 removeBook 때는 -height를 넘긴다는 식으로 둘을 합칠 수 있는데, 가시성을 위해 분리했다.
/**
* addNewCharacters
*
* @param user
* @return Characters
*/
@Transactional
public Characters addNewCharacters(User user, Integer bookPage) {
user.stackBook(bookPage);
userRepository.save(user);
List<UserCharacter> list = charactersRepository.findByHeightLessThanEqualAndIdNotInOrderByHeightDesc(user.getBookHeight(), getCharactersFromUser(user)).stream()
.map(o -> UserCharacter.builder()
.user(user)
.characters(o)
.achieve(LocalDate.now())
.representation(false)
.build())
.collect(Collectors.toList());
if (!list.isEmpty()) {
userCharacterRepository.saveAll(list);
return list.get(0).getCharacters();
}
return null;
}
/**
* removeCharacters
*
* @param user
*/
@Transactional
public void removeCharacters(User user, Integer bookPage) {
user.removeBook(bookPage);
userRepository.save(user);
double height = user.getBookHeight();
List<UserCharacter> list = user.getUserCharacters().stream()
.filter(o -> o.getCharacters().getHeight() > height)
.collect(Collectors.toList());
if (list.isEmpty())
return;
user.getUserCharacters().removeAll(list);
userCharacterRepository.deleteAll(list);
}
캐릭터 서비스의 코드도 이에 맞춰서 수정했다.
원래 인자로 user만 받았는데 추가/삭제할 책의 총페이지 수도 받는 것이다.
물론 기존과 달리 변경사항을 save하는 것에 대한 오버헤드가 발생한다.
이제 관련 테스트를 돌려보자
시간이 더 걸린다. 후...
아무래도 현재 테스트 유저에는 책 데이터가 약 30여개 정도만 들어있어서 save를 통한 오버헤드가 좀 더 크게 작용했다보다... 다른 테스트도 돌려보자
이것도 시간이 더 걸린다.
책 데이터가 nnnn개씩 쌓이면 달라질거라고 애써 위로해보자...