2024. 7. 13. 16:22ㆍ프로젝트/[EATceed] 몸무게 증량 어플
프로젝트를 진행하며 아래의 도메인 클래스가 너무 많은 기능을 가지고 있어 리팩토링할 필요가 있다고 생각했습니다.
public class Nutritionist {
private DailyMeal dailyMeal;
private Member member;
public boolean evaluateProteinAchieve() {
return member.measureTargetProtein() - getCurrentProtein() <= 0;
}
public boolean evaluateFatAchieve() {
return member.measureTargetFat() - getCurrentFat() <= 0;
}
public boolean evaluateCarbohydrateAchieve() {
return member.measureTargetCarbohydrate() - getCurrentCarbohydrate() <= 0;
}
public boolean evaluateCalorieAchieve() {
return member.measureTargetCalorie() - getCurrentCalorie() <= 0;
}
private Double getCurrentProtein() {
return dailyMeal.calculateCurrentProtein();
}
private Double getCurrentFat() {
return dailyMeal.calculateCurrentFat();
}
private Double getCurrentCarbohydrate() {
return dailyMeal.calculateCurrentCarbohydrate();
}
private Double getCurrentCalorie() {
return dailyMeal.calculateCurrentCalorie();
}
public static Nutritionist createNutritionist(Member memberModel, DailyMeal dailyMeal) {
return Nutritionist.builder().member(memberModel).dailyMeal(dailyMeal).build();
}
}
위의 Nutritionist 즉 영양사 도메인은 단백질, 탄수화물, 지방, 칼로리 달성률을 계산하는 책임과 영양사 도메인을 생성하는 책임을 가지고 있습니다.
하나의 도메인이 너무 많은 책임을 가지고 있다고 생각하여 책임을 분리하기로 하였습니다.
- 영양소를 분석하는 책임
- "영양소를 분석하는 객체"를 생성하는 책임
현재는 단백질, 탄수화물, 지방 칼로리 라는 영양소를 분석하고 있지만 미래에 다른 영양소도 분석할 수 있다고 생각하였습니다.
따라서, 영양소를 분석하는 추상 클래스를 만들어 추후 다른 영양소를 분석하는 클래스도 만들 수 있도록 하였습니다.
public interface Analyzable {
boolean analyze();
}
public abstract class Analyzer implements Analyzable {
protected DailyMeal dailyMeal;
protected Member member;
public Analyzer(DailyMeal dailyMeal, Member member) {
this.dailyMeal = dailyMeal;
this.member = member;
}
}
추상클래스인 Analyzer는 Member 도메인과 DailyMeal을 가지고 있습니다.
왜냐하면, Member 도메인에게는 "해당 회원이 목표 칼로리는 얼마야?" 라고 물어봐야하고, DailyMeal 도메인에게는 "오늘 먹은 식사의 칼로리를 계산해줘"라고 요청해야하기 때문입니다.
다음으로, 추상 클래스인 Analyzer를 구현한 CalorieAnalyzer와 같은 영양소 분석 도메인들을 만들었습니다.
public class CalorieAnalyzer extends Analyzer {
public CalorieAnalyzer(DailyMeal dailyMeal, Member member) {
super(dailyMeal, member);
}
@Override
public boolean analyze() {
return member.measureTargetCalorie() - dailyMeal.calculateCurrentCalorie() <= 0;
}
}
마찬가지로, 각각의 영양소 분석 도메인을 생성하는 추상 팩토리 클래스를 정의하였습니다.
public abstract class AbstractAnalyzerFactory {
public abstract Analyzer createAnalyzer(DailyMeal dailyMeal, Member member);
}
그리고, AbstractAnalyzerFactory를 구현한 CalorieAnalyzerFactory와 같은 영양소 분석 팩토리를 만들었습니다.
public class CalorieAnalyzerFactory extends AbstractAnalyzerFactory {
private CalorieAnalyzerFactory() {}
private static class SingletonHolder {
private static final CalorieAnalyzerFactory INSTANCE = new CalorieAnalyzerFactory();
}
public static CalorieAnalyzerFactory getInstance() {
return SingletonHolder.INSTANCE;
}
@Override
public CalorieAnalyzer createAnalyzer(DailyMeal dailyMeal, Member member) {
return new CalorieAnalyzer(dailyMeal, member);
}
}
CalorieAnalyzerFactory와 같이 ConcreteFactory들은 메모리 최적화를 위해 싱글톤을 사용하였습니다.
싱글톤을 사용하지 않더라도 일회용으로 사용된 인스턴스는 JVM의 GC로 인해 지워지지만 GC가 일어나는 동안 Stop The World가 일어나기 때문에 가능한 Stop The World가 발생하는 빈도를 줄이면 좋다고 생각하였습니다.
만들고 보니, 팩토리 메서드 패턴과 거의 흡사하였습니다.
리팩토링을 하며, 전의 코드보다 나아진 점은 아래와 같습니다.
- 객체 생성 코드와 실제 객체 코드를 분리하여 단일 책임 원칙을 준수하여 유지보수 하기 용이해졌습니다.
- 요구사항이 추가되어 새로운 영양소를 분석할 때에도 기존 코드를 수정하지 않고 새로운 유형의 영양소를 추가할 수 있습니다.
단점은 요구되는 영양소 종류가 많아질수록, 구현해야하는 클래스 수가 많아진다는 점입니다.
전체 코드는 아래 PR에서 확인할 수 있습니다.
https://github.com/JNU-econovation/EATceed/pull/379
감사합니다!!
'프로젝트 > [EATceed] 몸무게 증량 어플' 카테고리의 다른 글
헥사고날 아키텍처 회고 (2) (0) | 2024.07.28 |
---|---|
ApplicationContext Caching을 활용한 테스트 환경 개선 (2) | 2024.07.23 |
CI시 JPA Entity와 DB Schema 검증하기 (0) | 2024.06.28 |
통합 테스트에 테스트컨테이너 도입 (0) | 2024.06.25 |
Spring Event와 @Aysnc를 사용해 회원가입 개선 (0) | 2024.06.09 |