궤도

[Spring] [면적면적[14]] Refactoring 본문

💻 현생/📕 면적면적(스프링 실습)

[Spring] [면적면적[14]] Refactoring

영이오 2021. 8. 20. 16:07

북적북적의 백엔드를 클론하고 있다.

 

Github

 

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하는 것에 대한 오버헤드가 발생한다.

 

이제 관련 테스트를 돌려보자

before
after

시간이 더 걸린다. 후...

아무래도 현재 테스트 유저에는 책 데이터가 약 30여개 정도만 들어있어서 save를 통한 오버헤드가 좀 더 크게 작용했다보다... 다른 테스트도 돌려보자

 

before
after

이것도 시간이 더 걸린다.

책 데이터가 nnnn개씩 쌓이면 달라질거라고 애써 위로해보자...

Comments