흔히들 말하는 객체지향의 5대원칙 SOLID.
- SRP (Single Responsibility Principle) : 클래스는 단 하나의 책임을 가져야 하며, 한 가지 변경 사항에 대해 한 가지 이유로만 변경되어야 합니다.
- OCP (Open-Closed Principle) : 클래스는 확장에는 열려 있으나, 수정에는 닫혀 있어야 합니다. 즉, 새로운 기능을 추가할 때 기존 코드를 수정하지 않아도 되도록 설계되어야 합니다.
- LSP (Liskov Substitution Principle) : 하위 클래스는 상위 클래스의 역할을 대체할 수 있어야 합니다. 즉, 상위 클래스의 인스턴스는 언제나 하위 클래스의 인스턴스로 대체될 수 있어야 합니다.
- ISP (Interface Segregation Principle) : 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다. 따라서, 인터페이스는 클라이언트가 필요로 하는 메서드만 포함해야 합니다.
- DIP (Dependency Inversion Principle) : 추상화된 것은 구체적인 것에 의존해서는 안 되며, 구체적인 것이 추상화된 것에 의존해야 합니다. 즉, 의존성은 추상화된 것에 의존해야 하며, 구체적인 것에 의존하면 안 됩니다.
사실 스프링 프레임워크를 사용하게 되면 누구나 객체지향 원칙에 맞는 코드를 짜는 것이 수월해진다.
그럼에도 불구하고 코드의 양이 많아지거나 복잡한 로직이 생기면 그 원칙을 준수하면서 코드를 짜는것이 힘들어진다.
오늘은 JPQL -> QueryDsl로 리팩토링 하면서 내가 느낀 SOLID의 필요성에 대해서 작성해보겠다.
package com.codelap.api.service.study;
import com.codelap.api.service.study.dto.GetStudiesDto.GetStudiesStudyDto;
import com.codelap.common.study.domain.StudyRepository;
import com.codelap.common.study.dto.GetOpenedStudiesDto;
import com.codelap.common.study.dto.GetStudiesCardDto;
import com.codelap.common.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class DefaultStudyQueryAppService implements StudyQueryAppService {
private final StudyRepository studyRepository;
@Override
public List<GetStudiesCardDto.GetStudyInfo> getAttendedStudiesByUser(User user) {
return studyRepository.getAttendedStudiesByUser(user);
}
@Override
public List<GetStudiesCardDto.GetTechStackInfo> getTechStacks(List<Long> studyIds) {
return studyRepository.getTechStacks(studyIds);
}
}
기존에 짜여진 DefaultStduyQueryAppService 라는 클래스는 StudyQueryAppService를 상속받아서, StudyRepository에 짜여져 있는 JPQL 쿼리를 불러오는 역할을 맡고 있는 클래스이다.
그런데 오늘, 동적쿼리를 쉽게 짜기 위해서 QueryDsl을 도입하게 되어서 지금 위의 코드는 더이상 필요하지 않게 되었다.
코드에서 유지보수성이나 확장성을 생각하지 않았더라면 기존의 코드를 다 지워버리고 새로운 코드를 짰을것이다.
하지만 나는 SOLID 원칙을 최대한 준수하면서 코드를 작성하고 싶었기 때문에,
StudyQueryAppService를 상속받는 클래스를 하나 더 생성하고 빈으로 등록하게 되었다.
DIP를 준수하게 끔 코드를 작성하면 내가 새로운 클래스를 만들어서, 해당 클래스를 빈으로 등록해도 기존의 코드 수정 없이, 즉 OCP를 지키면서 코드를 작성할 수 있다.
package com.codelap.api.service.study;
import com.codelap.api.service.study.dto.GetStudiesDto;
import com.codelap.common.bookmark.domain.QBookmark;
import com.codelap.common.study.domain.QStudy;
import com.codelap.common.study.domain.StudyRepository;
import com.codelap.common.study.dto.GetOpenedStudiesDto;
import com.codelap.common.studyComment.domain.QStudyComment;
import com.codelap.common.studyView.domain.QStudyView;
import com.codelap.common.user.domain.User;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Repository;
import java.util.List;
import static com.codelap.common.study.domain.StudyStatus.DELETED;
import static com.codelap.common.study.dto.GetStudiesCardDto.GetStudyInfo;
import static com.codelap.common.study.dto.GetStudiesCardDto.GetTechStackInfo;
import static com.querydsl.core.types.ExpressionUtils.count;
import static com.querydsl.core.types.Projections.constructor;
@Repository
@Primary
@RequiredArgsConstructor
public class DefaultStudyQueryDslAppService implements StudyQueryAppService {
private final JPAQueryFactory queryFactory;
private final StudyRepository studyRepository;
@Override
public List<GetStudyInfo> getAttendedStudiesByUser(User user) {
return queryFactory
.select(
constructor(
GetStudyInfo.class,
QStudy.study.id,
QStudy.study.name,
QStudy.study.period,
QStudy.study.leader.name,
ExpressionUtils.as(JPAExpressions
.select(count(QStudyComment.studyComment.id))
.from(QStudyComment.studyComment)
.where(QStudyComment.studyComment.study.id.eq(QStudy.study.id)),
"commentCount"
),
ExpressionUtils.as(JPAExpressions
.select(count(QStudyView.studyView.id))
.from(QStudyView.studyView)
.where(QStudyView.studyView.study.id.eq(QStudy.study.id)),
"viewCount"
),
ExpressionUtils.as(JPAExpressions
.select(count(QBookmark.bookmark.id))
.from(QBookmark.bookmark)
.where(QBookmark.bookmark.study.id.eq(QStudy.study.id)),
"bookmarkCount"
),
QStudy.study.maxMembersSize))
.from(QStudy.study)
.where(QStudy.study.status.ne(DELETED))
.where(QStudy.study.members.contains(user))
.fetch();
}
@Override
public List<GetTechStackInfo> getTechStacks(List<Long> studyIds) {
return studyRepository.getTechStacks(studyIds);
}
}
StudyQueryAppService 라는 빈을 주입받는 다른 클래스 입장에서는 StudyQueryAppService 클래스에서 어떤 코드가 짜여져있든 전혀 상관하지 않는다.
또한, 해당 클래스에서는 JPQL 로직 또한 사용하는데(getTechStacks 라는 메소드)
만약 더 이상 쓸 일이 없다고 판단해서 JPQL 로직을 다 지워버렸으면 어떻게 됐을까?
다시 쌩 고생 하면서 코드를 직접 짜야한다..
결국 짜놓은 코드는 재사용성이 있기 때문에 함부로 손대지 않고, 확장하는것이 올바르다는 것이다.
다른 핵심 로직에 전혀 관여하지 않기 때문에, 유지보수성과 확장성이 굉장히 높아지게 된다.
package com.codelap.api.service.study;
import com.codelap.api.service.study.dto.GetStudiesDto.GetStudiesStudyDto;
import com.codelap.common.study.dto.GetOpenedStudiesDto;
import com.codelap.common.study.dto.GetStudiesCardDto.GetStudyInfo;
import com.codelap.common.study.dto.GetStudiesCardDto.GetTechStackInfo;
import com.codelap.common.user.domain.User;
import com.codelap.common.user.domain.UserRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@Transactional
@RequiredArgsConstructor
public class DefaultStudyAppService implements StudyAppService {
private final StudyQueryAppService studyQueryAppService;
private final UserRepository userRepository;
@Override
public List<GetStudiesStudyDto> getAttendedStudiesByUser(Long userId) {
User user = userRepository.findById(userId).orElseThrow();
List<GetStudyInfo> allStudies = studyQueryAppService.getAttendedStudiesByUser(user);
Map<Long, List<GetTechStackInfo>> techStacksMap = studyQueryAppService.getTechStacks(toStudyIds(allStudies))
.stream()
.collect(Collectors
.groupingBy(GetTechStackInfo::getStudyId));
allStudies.forEach(study -> study.setTechStackList(techStacksMap.get(study.getStudyId())));
return getGetStudiesStudyDto(allStudies);
}
private static List<GetStudiesStudyDto> getGetStudiesStudyDto(List<GetStudyInfo> allStudies) {
return allStudies
.stream()
.map(GetStudiesStudyDto::new)
.collect(Collectors.toList());
}
private static List<Long> toStudyIds(List<GetStudyInfo> allStudies) {
return allStudies
.stream()
.map(GetStudyInfo::getStudyId)
.collect(Collectors.toList());
}
}
바로 예시를 들어보자면, DefaultAppService 라는 클래스는 StudyQueryAppService를 주입받는다.
StudyQueryAppService 내부에서는 QueryDsl 로직을 추가한 클래스가 하나 더 생기고, 빈으로 등록되는 등 복잡한 일이 일어났는데 DefaultAppService 라는 녀석은 거기에 있어서 아무런 영향을 받지 않는 것이다.
같은 이름이 빈으로 등록된 것만 @Primary를 사용해서 우선권을 적용해주면 전혀 바뀐 것 없이 정상적으로 코드가 작동하는 것을 확인할 수 있다.
'CodeLap 프로젝트' 카테고리의 다른 글
CodeLap - S3로 파일 업로드(자바, 스프링부트) (0) | 2023.05.12 |
---|---|
[CodeLap] Github Action, AWS(EC2, S3, RDS, CodeDeploy)를 활용한 자바 + 스프링부트 백앤드 서버 배포 - 2편 (0) | 2023.04.28 |
[CodeLap] Github Action, AWS(EC2, S3, RDS, CodeDeploy)를 활용한 자바 + 스프링부트 백앤드 서버 배포 - 1편 (0) | 2023.04.27 |
[CodeLap] 조회 쿼리 성능 최적화 및 DTO 이너클래스 리팩토링 (0) | 2023.04.22 |
트러블 슈팅 - Querydsl 조회 로직 (0) | 2023.04.04 |