Overview
키즈핑 프로젝트를 진행하면서 제가 맡았던 부분중에 하나는 자녀 성향 진단이였습니다.
이번 글에서는 자녀 성향 진단 로직을 구현하면서 겪었던 고민과, 이를 해결하기 위해 시도한 리팩터링 과정에 대해 이야기해보려 합니다.
로직 설계
자녀 성향 진단
- 프론트가 보낸 자녀의 id 값으로 자녀를 조회한다.
- 프론트로부터 받은 점수를 유저의 MBTI 설문 조사에 저장한다.
- 받은 점수를 통해 자녀의 MBTI(성향)를 계산하고 정한다.
- 자녀 성향에 MBTI 점수와 정해진 성향을 저장한다.
- 자녀 성향 히스토리에도 저장한다.
우선 위와 같이 자녀 성향 진단 로직을 정리 했습니다.
간단하게 정리하면 우선 서버에서 프론트로 자녀 성향을 진단하는 질문 목록을 보냅니다.
자녀는 본인에게 맞는 성향을 체크하면 프론트에서 한번에 자녀의 각 성향별로 점수를 매겨서 서버로 전송합니다.
(예시 I : 50, E : 60 ...)
서버는 프론트로부터 받는 MBTI 점수를 계산하여 자녀의 MBTI 성향을 정합니다.
(예시 I : 50, E : 60, S : 20, N : 10, T : 20, F : 5, P : 30, J : 10 -> ESTP)
계산하여 결정된 자녀의 MBTI 성향을 자녀 성향 테이블과 자녀 성향 히스토리 테이블에 저장합니다.
아래 코드는 초기에 작성했던 코드입니다.
위 로직에서 3번까지 처리한 로직이라고 보시면 됩니다.
@Transactional
@Override
public void diagnoseKidMBTI(KidMBTIDiagnosisRequest diagnosisRequest) {
Kid kid = getKid(diagnosisRequest);
MbtiResponse mbtiResponse = KidMBTIDiagnosisRequest.of(diagnosisRequest, kid);
mbtiResponseRepository.save(mbtiResponse);
StringBuilder sb = new StringBuilder();
if (diagnosisRequest.getEScore() >= diagnosisRequest.getIScore()) {
sb.append("E");
} else {
sb.append("I");
}
if (diagnosisRequest.getSScore() >= diagnosisRequest.getNScore()) {
sb.append("S");
} else {
sb.append("N");
}
if (diagnosisRequest.getFScore() >= diagnosisRequest.getTScore()) {
sb.append("F");
} else {
sb.append("T");
}
if (diagnosisRequest.getPScore() >= diagnosisRequest.getJScore()) {
sb.append("P");
} else {
sb.append("J");
}
String mbti = sb.toString();
}
코드를 보면 아시겠지만 굉장히 폭력적인(?) 코드라고 생각합니다.
그 이유는 if - else가 난발하는 코드라 코드의 복잡성이 증가한다고 생각합니다.
특히 여러 조건이 복합적으로 얽혀있는 경우, 코드의 흐름을 파악하기 어렵게 만들어 코드의 가독성이 저하되고 디버깅의 어려움이 생긴다고 생각합니다. 또한, 실수를 유발하기 쉽다고 생각합니다.
if - else 리팩터링
그래서 if - else문을 사용하지 않는 방식으로 아래와 같이 코드를 리팩터링 했습니다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class KidServiceImpl implements KidService {
...
@Transactional
@Override
public void diagnoseKidMBTI(KidMBTIDiagnosisRequest diagnosisRequest) {
Kid kid = getKid(diagnosisRequest);
MbtiResponse mbtiResponse = KidMBTIDiagnosisRequest.of(diagnosisRequest, kid);
mbtiResponseRepository.save(mbtiResponse);
String mbti = calculateMBTI(diagnosisRequest);
MbtiStatus mbtiStatus = MbtiStatus.toMbtiStatus(mbti);
...
}
// 각 MBTI 성향을 계산하는 메소드
private String calculateMBTI(KidMBTIDiagnosisRequest request) {
return compareScores(request.getExtraversionScore(), request.getIntroversionScore(), "E", "I")
+ compareScores(request.getSensingScore(), request.getIntuitionScore(), "S", "N")
+ compareScores(request.getFeelingScore(), request.getThinkingScore(), "F", "T")
+ compareScores(request.getPerceivingScore(), request.getJudgingScore(), "P", "J");
}
// 점수 비교 로직을 추출한 메소드
private String compareScores(int firstScore, int secondScore, String firstType, String secondType) {
return firstScore >= secondScore ? firstType : secondType;
}
}
enum으로 매직 리터럴 제거 리팩터링
또한 자녀의 MBTI(ISTP, INFJ 등)를 관리하면서, 코드의 가독성과 유지보수성을 높이기 위해 "ISTP"와 같은 매직 리터럴을 사용하는 대신, 아래와 같이 enum을 활용하여 관리했습니다.
public enum MbtiStatus {
ISTJ,
ISFJ,
INFJ,
INTJ,
ISTP,
ISFP,
INFP,
INTP,
ESTP,
ESFP,
ENFP,
ENTP,
ESTJ,
ESFJ,
ENFJ,
ENTJ;
public static MbtiStatus toMbtiStatus(String mbti) {
try {
return MbtiStatus.valueOf(mbti);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid MBTI type: " + mbti);
}
}
}
아래 코드는 지금까지 리팩터링한 코드입니다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class KidServiceImpl implements KidService {
private final KidRepository kidRepository;
private final MbtiResponseRepository mbtiResponseRepository;
private final KidMBTIRepository kidMBTIRepository;
private final KidMBTIHistoryRepository kidMBTIHistoryRepository;
@Transactional
@Override
public void diagnoseKidMBTI(KidMBTIDiagnosisRequest diagnosisRequest) {
Kid kid = getKid(diagnosisRequest);
MbtiResponse mbtiResponse = KidMBTIDiagnosisRequest.of(diagnosisRequest, kid);
mbtiResponseRepository.save(mbtiResponse);
String mbti = calculateMBTI(diagnosisRequest);
MbtiStatus mbtiStatus = MbtiStatus.toMbtiStatus(mbti);
KidMbti kidMbti = KidMbti.builder()
.isDeleted(false)
.mbtiStatus(mbtiStatus)
.mbtiScore(mbtiScore)
.build();
kidMBTIRepository.save(kidMbti);
kid.updateKidMbti(kidMbti);
KidMbtiHistory kidMbtiHistory = KidMbtiHistory.builder()
.kid(kid)
.mbtiStatus(mbtiStatus)
.isDeleted(false)
.build();
kidMBTIHistoryRepository.save(kidMbtiHistory);
}
private Kid getKid(KidMBTIDiagnosisRequest diagnosisRequest) {
return kidRepository.findById(diagnosisRequest.getUserId())
.orElseThrow(() -> new RuntimeException("no kid"));
}
// 각 MBTI 성향을 계산하는 메소드
private String calculateMBTI(KidMBTIDiagnosisRequest request) {
return compareScores(request.getExtraversionScore(), request.getIntroversionScore(), "E", "I")
+ compareScores(request.getSensingScore(), request.getIntuitionScore(), "S", "N")
+ compareScores(request.getFeelingScore(), request.getThinkingScore(), "F", "T")
+ compareScores(request.getPerceivingScore(), request.getJudgingScore(), "P", "J");
}
// 점수 비교 로직을 추출한 메소드
private String compareScores(int firstScore, int secondScore, String firstType, String secondType) {
return firstScore >= secondScore ? firstType : secondType;
}
}
함수 추출 리팩터링
diagnoseKidMBTI() 함수를 살펴보면서, 하나의 함수가 여러 역할을 수행하고 있다고 느껴졌습니다.
자녀의 성향을 계산하고, MBTI를 생성하며, 히스토리까지 생성하는 등 다양한 작업을 한곳에서 처리하고 있었습니다.
이를 개선하기 위해, 자녀 성향 계산과 히스토리 생성을 각각 독립적인 함수로 분리했습니다. 또한 코드의 가독성을 높이고 의도를 분명히 전달하기 위해 함수 이름을 통해 각 함수의 역할을 드러내고자 했습니다
private KidMbti saveKidMBTI(MbtiStatus mbtiStatus, MbtiScore mbtiScore) {
KidMbti kidMbti = KidMbti.builder()
.isDeleted(false)
.mbtiStatus(mbtiStatus)
.mbtiScore(mbtiScore)
.build();
return kidMBTIRepository.save(kidMbti);
}
private void saveKidMBTIHistory(Kid kid, MbtiStatus mbtiStatus) {
KidMbtiHistory kidMbtiHistory = KidMbtiHistory.builder()
.kid(kid)
.mbtiStatus(mbtiStatus)
.isDeleted(false)
.build();
kidMBTIHistoryRepository.save(kidMbtiHistory);
}
추가적으로 아직 매직 리터럴이 쓰였던 MBTI의 성향들도(I, E ...) enum으로 관리 했습니다.
@Getter
public enum PersonalityTrait {
INTROVERSION("I"),
EXTRAVERSION("E"),
SENSING("S"),
INTUITION("N"),
THINKING("T"),
FEELING("F"),
JUDGING("J"),
PERCEIVING("P");
private final String type;
PersonalityTrait(String type) {
this.type = type;
}
}
이후 개발 과정에서 다른 서비스 로직에서도 MBTI 성향을 계산해야 하는 상황이 생겼습니다. 코드 중복을 줄이고 재사용성을 높이기 위해, MBTI 성향 계산 로직을 유틸리티 클래스로 분리하고, 정적 메서드로 리팩터링했습니다.
public class MbtiCalculator {
public static MbtiStatus determineMbtiType(MbtiScore mbtiScore) {
String mbti = determinePersonalityTrait(mbtiScore.getEScore(), mbtiScore.getIScore(),
PersonalityTrait.EXTRAVERSION.getType(), PersonalityTrait.INTROVERSION.getType())
+ determinePersonalityTrait(mbtiScore.getSScore(), mbtiScore.getNScore(),
PersonalityTrait.SENSING.getType(), PersonalityTrait.INTUITION.getType())
+ determinePersonalityTrait(mbtiScore.getFScore(), mbtiScore.getTScore(),
PersonalityTrait.FEELING.getType(), PersonalityTrait.THINKING.getType())
+ determinePersonalityTrait(mbtiScore.getPScore(), mbtiScore.getJScore(),
PersonalityTrait.PERCEIVING.getType(), PersonalityTrait.JUDGING.getType());
return MbtiStatus.toMbtiStatus(mbti);
}
private static String determinePersonalityTrait(int firstScore, int secondScore, String firstType,
String secondType) {
return firstScore >= secondScore ? firstType : secondType;
}
}
아래 코드는 지금까지 리팩터링한 코드입니다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class KidServiceImpl implements KidService {
private final KidRepository kidRepository;
private final MbtiResponseRepository mbtiResponseRepository;
private final KidMBTIRepository kidMBTIRepository;
private final KidMBTIHistoryRepository kidMBTIHistoryRepository;
@Transactional
@Override
public void diagnoseKidMBTI(KidMBTIDiagnosisRequest diagnosisRequest) {
Kid kid = findKid(diagnosisRequest);
MbtiResponse mbtiResponse = KidMBTIDiagnosisRequest.getMBTIResponse(diagnosisRequest, kid);
mbtiResponseRepository.save(mbtiResponse);
MbtiScore mbtiScore = KidMBTIDiagnosisRequest.getMBTIScore(diagnosisRequest);
MbtiStatus mbtiStatus = calculateMbtiStatus(diagnosisRequest);
KidMbti kidMbti = saveKidMBTI(mbtiStatus, mbtiScore);
kid.updateKidMbti(kidMbti);
saveKidMBTIHistory(kid, mbtiStatus);
}
private Kid findKid(KidMBTIDiagnosisRequest diagnosisRequest) {
return kidRepository.findById(diagnosisRequest.getUserId())
.orElseThrow(() -> new RuntimeException("no kid"));
}
private MbtiStatus calculateMbtiStatus(KidMBTIDiagnosisRequest request) {
String mbti = compareScores(request.getExtraversionScore(), request.getIntroversionScore(),
PersonalityTrait.EXTRAVERSION.getType(), PersonalityTrait.INTROVERSION.getType())
+ compareScores(request.getSensingScore(), request.getIntuitionScore(),
PersonalityTrait.SENSING.getType(), PersonalityTrait.INTUITION.getType())
+ compareScores(request.getFeelingScore(), request.getThinkingScore(),
PersonalityTrait.FEELING.getType(), PersonalityTrait.THINKING.getType())
+ compareScores(request.getPerceivingScore(), request.getJudgingScore(),
PersonalityTrait.PERCEIVING.getType(), PersonalityTrait.JUDGING.getType());
return MbtiStatus.toMbtiStatus(mbti);
}
private String compareScores(int firstScore, int secondScore, String firstType, String secondType) {
return firstScore >= secondScore ? firstType : secondType;
}
private KidMbti saveKidMBTI(MbtiStatus mbtiStatus, MbtiScore mbtiScore) {
KidMbti kidMbti = KidMbti.builder()
.isDeleted(false)
.mbtiStatus(mbtiStatus)
.mbtiScore(mbtiScore)
.build();
return kidMBTIRepository.save(kidMbti);
}
private void saveKidMBTIHistory(Kid kid, MbtiStatus mbtiStatus) {
KidMbtiHistory kidMbtiHistory = KidMbtiHistory.builder()
.kid(kid)
.mbtiStatus(mbtiStatus)
.isDeleted(false)
.build();
kidMBTIHistoryRepository.save(kidMbtiHistory);
}
}
추가 로직 설계
- 프론트가 보낸 자녀의 id 값으로 자녀를 조회한다.
- 프론트로부터 받은 점수를 유저의 MBTI 설문 조사에 저장한다.
- 받은 점수를 통해 자녀의 MBTI(성향)를 계산하고 정한다.
- 자녀 성향에 MBTI 점수와 정해진 성향을 저장한다.
- 자녀 성향 히스토리에도 저장한다.
앞서 자녀 성향을 진단 로직을 위와 같이 정리를 했습니다.
개발을 진행하면서 진단을 할 경우 해당 진단이 처음하는 진단인지 아니면 재진단인지 구분하는 로직이 필요 했습니다.
그래서 아래와 같이 로직을 추가 했습니다.
재진단
처음 진단인지 재진단인지 구분
재진단인 경우 아래 로직을 추가
- 자녀 성향 초기화
- 장르 점수 초기화
- 자녀가 도서, 장르 좋아요 한것들 다 삭제
자녀의 MBTI 값을 가져와 존재하지 않으면 새로 생성하고, 이미 값이 있다면 업데이트하는 코드를 추가로 작성했습니다. 처음에는 생성과 수정의 역할을 확실히 분리하고 싶었지만, 멘토님의 피드백을 통해 이 로직의 통합이 불가피하다는 점을 알게 되었습니다. 이에 따라, 함수의 의도를 분명히 전달하고자 updateOrCreateKidMbti()와 같이 생성과 수정을 모두 포함하는 메서드명을 사용해 로직을 리팩터링했습니다.
private void updateOrCreateKidMbti(Kid kid, KidMbtiDiagnosisRequest diagnosisRequest,
MbtiStatus updatedMbtiStatus) {
KidMbti currentKidMbti = kid.getKidMbti();
if (currentKidMbti == null) {
currentKidMbti = createKidMbti(diagnosisRequest, updatedMbtiStatus);
} else {
MbtiScore mbtiScore = MbtiScore.from(diagnosisRequest);
currentKidMbti.updateMbti(mbtiScore, updatedMbtiStatus);
// 자녀 장르 점수, 도서 좋아요, 장르 좋아요 초기화
genreScoreService.resetGenreScoreForKid(kid.getId());
likeGenreService.resetGenreLikesForKid(kid.getId());
likeMbtiService.resetMbtiLikesForKid(kid.getId());
}
kid.updateKidMbti(currentKidMbti);
}
최종 코드
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class KidServiceImpl implements KidService {
...
/**
* 자녀의 MBTI를 진단하는 메서드
*
* @param diagnosisRequest 자녀의 MBTI 점수 데이터를 담은 KidMbtiDiagnosisRequest 객체
*/
@Transactional
@Override
public void diagnoseKidMbti(KidMbtiDiagnosisRequest diagnosisRequest) {
Kid kid = findKidById(diagnosisRequest.getKidId());
saveMbtiResponse(diagnosisRequest, kid);
MbtiScore diagnosedKidMbtiScore = MbtiScore.from(diagnosisRequest);
MbtiStatus updatedMbtiStatus = MbtiCalculator.determineMbtiType(diagnosedKidMbtiScore);
updateOrCreateKidMbti(kid, diagnosisRequest, updatedMbtiStatus);
saveKidMbtiHistory(kid, updatedMbtiStatus);
}
/**
* 자녀 엔티티를 조회하는 메서드.
*
* @param kidId 자녀 엔티티의 id 값
*/
private Kid findKidById(Long kidId) {
return kidRepository.findKidBy(kidId)
.orElseThrow(NotFoundKidException::new);
}
/**
* 자녀가 응답한 MBTI 설문 결과를 저장하는 메서드
*
* @param diagnosisRequest 자녀의 MBTI 점수 데이터를 담은 KidMbtiDiagnosisRequest 객체
* @param kid 자녀 엔티티
*/
private void saveMbtiResponse(KidMbtiDiagnosisRequest diagnosisRequest, Kid kid) {
MbtiAnswer mbtiAnswer = KidMbtiDiagnosisRequest.getMbtiAnswer(diagnosisRequest, kid);
mbtiAnswerRepository.save(mbtiAnswer);
}
/**
* 자녀의 현재 MBTI를 조회하여 null 인경우 새로 생성하고 null 이 아니면 기존 자녀의 MBTI를 변경하는 메서드 자녀의 현재 MBTI(currentKidMbti)가 null 이면 처음 진단
* 자녀의 현재 MBTI(currentKidMbti)가 null 이 아니면 재진단
*
* @param kid 자녀 엔티티
* @param diagnosisRequest 자녀의 MBTI 점수 데이터를 담은 KidMbtiDiagnosisRequest 객체
* @param updatedMbtiStatus 설문 결과를 바탕으로 업데이트 된 자녀 MBTI 상태 객체
*/
private void updateOrCreateKidMbti(Kid kid, KidMbtiDiagnosisRequest diagnosisRequest,
MbtiStatus updatedMbtiStatus) {
KidMbti currentKidMbti = kid.getKidMbti();
if (currentKidMbti == null) {
currentKidMbti = createKidMbti(diagnosisRequest, updatedMbtiStatus);
} else {
MbtiScore mbtiScore = MbtiScore.from(diagnosisRequest);
currentKidMbti.updateMbti(mbtiScore, updatedMbtiStatus);
// 자녀 장르 점수, 도서 좋아요, 장르 좋아요 초기화
genreScoreService.resetGenreScoreForKid(kid.getId());
likeGenreService.resetGenreLikesForKid(kid.getId());
likeMbtiService.resetMbtiLikesForKid(kid.getId());
}
kid.updateKidMbti(currentKidMbti);
}
/**
* 자녀의 MBTI를 생성하는 메서드
*
* @param diagnosisRequest 자녀의 MBTI 점수 데이터를 담은 KidMbtiDiagnosisRequest 객체
* @param updatedMbtiStatus 설문 결과를 바탕으로 업데이트 된 자녀 MBTI 상태 객체
*/
private KidMbti createKidMbti(KidMbtiDiagnosisRequest diagnosisRequest, MbtiStatus updatedMbtiStatus) {
KidMbti kidMbti = KidMbti.builder()
.eScore(diagnosisRequest.getExtraversionScore())
.iScore(diagnosisRequest.getIntroversionScore())
.sScore(diagnosisRequest.getSensingScore())
.nScore(diagnosisRequest.getIntuitionScore())
.fScore(diagnosisRequest.getFeelingScore())
.tScore(diagnosisRequest.getThinkingScore())
.jScore(diagnosisRequest.getJudgingScore())
.pScore(diagnosisRequest.getPerceivingScore())
.mbtiStatus(updatedMbtiStatus)
.build();
return kidMBTIRepository.save(kidMbti);
}
/**
* 자녀의 MBTI 히스토리를 생성하는 메서드
*
* @param kid 자녀 엔티티
* @param updatedMbtiStatus 설문 결과를 바탕으로 업데이트 된 자녀 MBTI 상태 객체
*/
private void saveKidMbtiHistory(Kid kid, MbtiStatus updatedMbtiStatus) {
KidMbtiHistory kidMbtiHistory = KidMbtiHistory.builder()
.kid(kid)
.mbtiStatus(updatedMbtiStatus)
.isDeleted(false)
.build();
kidMBTIHistoryRepository.save(kidMbtiHistory);
}
@Override
public GetKidMbtiResponse getKidMbti(Long kidId) {
KidMbti kidMbti = kidMBTIRepository.findKidMbtiBy(kidId)
.orElseThrow(() -> new NotFoundException(ExceptionCode.NOT_FOUND_KID_MBTI));
return GetKidMbtiResponse.from(kidMbti);
}
}
마무리
이번 글에서는 자녀 성향 진단 로직을 구현하면서 겪었던 고민과, 이를 해결하기 위해 시도한 리팩터링 과정에 대해 공유했습니다. 처음 코드를 작성할 때는 굉장히 폭력적(?)이였는데 리팩터링을 거치며 점차 더 나은 코드를 만들어갈 수 있었습니다. 이러한 과정을 통해 앞으로도 가독성과 유지보수성을 고려한 코드를 위해서는 리팩터링은 필수라고 생각 하게 되었습니다.
'프로젝트 > Kidsping' 카테고리의 다른 글
개발 기록 - 선착순 응모 시스템 이슈 및 해결 과정 (0) | 2024.10.30 |
---|---|
트러블 슈팅 - AWS 프리티어 EC2 인스턴스 메모리 부족 현상 해결하기 (0) | 2024.10.25 |
개발 기록 - N + 1 문제 fetch join, Batch Size로 해결 (0) | 2024.10.24 |
개발 기록 - Java에서 Enum 의 비교는 '==' 인가? 'equals' 인가? (0) | 2024.10.22 |
개발 기록 - @RequestBody DTO에 값이 안들어오는 이유 (Jackson, Lombok) (0) | 2024.10.22 |
Overview
키즈핑 프로젝트를 진행하면서 제가 맡았던 부분중에 하나는 자녀 성향 진단이였습니다.
이번 글에서는 자녀 성향 진단 로직을 구현하면서 겪었던 고민과, 이를 해결하기 위해 시도한 리팩터링 과정에 대해 이야기해보려 합니다.
로직 설계
자녀 성향 진단
- 프론트가 보낸 자녀의 id 값으로 자녀를 조회한다.
- 프론트로부터 받은 점수를 유저의 MBTI 설문 조사에 저장한다.
- 받은 점수를 통해 자녀의 MBTI(성향)를 계산하고 정한다.
- 자녀 성향에 MBTI 점수와 정해진 성향을 저장한다.
- 자녀 성향 히스토리에도 저장한다.
우선 위와 같이 자녀 성향 진단 로직을 정리 했습니다.
간단하게 정리하면 우선 서버에서 프론트로 자녀 성향을 진단하는 질문 목록을 보냅니다.
자녀는 본인에게 맞는 성향을 체크하면 프론트에서 한번에 자녀의 각 성향별로 점수를 매겨서 서버로 전송합니다.
(예시 I : 50, E : 60 ...)
서버는 프론트로부터 받는 MBTI 점수를 계산하여 자녀의 MBTI 성향을 정합니다.
(예시 I : 50, E : 60, S : 20, N : 10, T : 20, F : 5, P : 30, J : 10 -> ESTP)
계산하여 결정된 자녀의 MBTI 성향을 자녀 성향 테이블과 자녀 성향 히스토리 테이블에 저장합니다.
아래 코드는 초기에 작성했던 코드입니다.
위 로직에서 3번까지 처리한 로직이라고 보시면 됩니다.
@Transactional
@Override
public void diagnoseKidMBTI(KidMBTIDiagnosisRequest diagnosisRequest) {
Kid kid = getKid(diagnosisRequest);
MbtiResponse mbtiResponse = KidMBTIDiagnosisRequest.of(diagnosisRequest, kid);
mbtiResponseRepository.save(mbtiResponse);
StringBuilder sb = new StringBuilder();
if (diagnosisRequest.getEScore() >= diagnosisRequest.getIScore()) {
sb.append("E");
} else {
sb.append("I");
}
if (diagnosisRequest.getSScore() >= diagnosisRequest.getNScore()) {
sb.append("S");
} else {
sb.append("N");
}
if (diagnosisRequest.getFScore() >= diagnosisRequest.getTScore()) {
sb.append("F");
} else {
sb.append("T");
}
if (diagnosisRequest.getPScore() >= diagnosisRequest.getJScore()) {
sb.append("P");
} else {
sb.append("J");
}
String mbti = sb.toString();
}
코드를 보면 아시겠지만 굉장히 폭력적인(?) 코드라고 생각합니다.
그 이유는 if - else가 난발하는 코드라 코드의 복잡성이 증가한다고 생각합니다.
특히 여러 조건이 복합적으로 얽혀있는 경우, 코드의 흐름을 파악하기 어렵게 만들어 코드의 가독성이 저하되고 디버깅의 어려움이 생긴다고 생각합니다. 또한, 실수를 유발하기 쉽다고 생각합니다.
if - else 리팩터링
그래서 if - else문을 사용하지 않는 방식으로 아래와 같이 코드를 리팩터링 했습니다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class KidServiceImpl implements KidService {
...
@Transactional
@Override
public void diagnoseKidMBTI(KidMBTIDiagnosisRequest diagnosisRequest) {
Kid kid = getKid(diagnosisRequest);
MbtiResponse mbtiResponse = KidMBTIDiagnosisRequest.of(diagnosisRequest, kid);
mbtiResponseRepository.save(mbtiResponse);
String mbti = calculateMBTI(diagnosisRequest);
MbtiStatus mbtiStatus = MbtiStatus.toMbtiStatus(mbti);
...
}
// 각 MBTI 성향을 계산하는 메소드
private String calculateMBTI(KidMBTIDiagnosisRequest request) {
return compareScores(request.getExtraversionScore(), request.getIntroversionScore(), "E", "I")
+ compareScores(request.getSensingScore(), request.getIntuitionScore(), "S", "N")
+ compareScores(request.getFeelingScore(), request.getThinkingScore(), "F", "T")
+ compareScores(request.getPerceivingScore(), request.getJudgingScore(), "P", "J");
}
// 점수 비교 로직을 추출한 메소드
private String compareScores(int firstScore, int secondScore, String firstType, String secondType) {
return firstScore >= secondScore ? firstType : secondType;
}
}
enum으로 매직 리터럴 제거 리팩터링
또한 자녀의 MBTI(ISTP, INFJ 등)를 관리하면서, 코드의 가독성과 유지보수성을 높이기 위해 "ISTP"와 같은 매직 리터럴을 사용하는 대신, 아래와 같이 enum을 활용하여 관리했습니다.
public enum MbtiStatus {
ISTJ,
ISFJ,
INFJ,
INTJ,
ISTP,
ISFP,
INFP,
INTP,
ESTP,
ESFP,
ENFP,
ENTP,
ESTJ,
ESFJ,
ENFJ,
ENTJ;
public static MbtiStatus toMbtiStatus(String mbti) {
try {
return MbtiStatus.valueOf(mbti);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid MBTI type: " + mbti);
}
}
}
아래 코드는 지금까지 리팩터링한 코드입니다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class KidServiceImpl implements KidService {
private final KidRepository kidRepository;
private final MbtiResponseRepository mbtiResponseRepository;
private final KidMBTIRepository kidMBTIRepository;
private final KidMBTIHistoryRepository kidMBTIHistoryRepository;
@Transactional
@Override
public void diagnoseKidMBTI(KidMBTIDiagnosisRequest diagnosisRequest) {
Kid kid = getKid(diagnosisRequest);
MbtiResponse mbtiResponse = KidMBTIDiagnosisRequest.of(diagnosisRequest, kid);
mbtiResponseRepository.save(mbtiResponse);
String mbti = calculateMBTI(diagnosisRequest);
MbtiStatus mbtiStatus = MbtiStatus.toMbtiStatus(mbti);
KidMbti kidMbti = KidMbti.builder()
.isDeleted(false)
.mbtiStatus(mbtiStatus)
.mbtiScore(mbtiScore)
.build();
kidMBTIRepository.save(kidMbti);
kid.updateKidMbti(kidMbti);
KidMbtiHistory kidMbtiHistory = KidMbtiHistory.builder()
.kid(kid)
.mbtiStatus(mbtiStatus)
.isDeleted(false)
.build();
kidMBTIHistoryRepository.save(kidMbtiHistory);
}
private Kid getKid(KidMBTIDiagnosisRequest diagnosisRequest) {
return kidRepository.findById(diagnosisRequest.getUserId())
.orElseThrow(() -> new RuntimeException("no kid"));
}
// 각 MBTI 성향을 계산하는 메소드
private String calculateMBTI(KidMBTIDiagnosisRequest request) {
return compareScores(request.getExtraversionScore(), request.getIntroversionScore(), "E", "I")
+ compareScores(request.getSensingScore(), request.getIntuitionScore(), "S", "N")
+ compareScores(request.getFeelingScore(), request.getThinkingScore(), "F", "T")
+ compareScores(request.getPerceivingScore(), request.getJudgingScore(), "P", "J");
}
// 점수 비교 로직을 추출한 메소드
private String compareScores(int firstScore, int secondScore, String firstType, String secondType) {
return firstScore >= secondScore ? firstType : secondType;
}
}
함수 추출 리팩터링
diagnoseKidMBTI() 함수를 살펴보면서, 하나의 함수가 여러 역할을 수행하고 있다고 느껴졌습니다.
자녀의 성향을 계산하고, MBTI를 생성하며, 히스토리까지 생성하는 등 다양한 작업을 한곳에서 처리하고 있었습니다.
이를 개선하기 위해, 자녀 성향 계산과 히스토리 생성을 각각 독립적인 함수로 분리했습니다. 또한 코드의 가독성을 높이고 의도를 분명히 전달하기 위해 함수 이름을 통해 각 함수의 역할을 드러내고자 했습니다
private KidMbti saveKidMBTI(MbtiStatus mbtiStatus, MbtiScore mbtiScore) {
KidMbti kidMbti = KidMbti.builder()
.isDeleted(false)
.mbtiStatus(mbtiStatus)
.mbtiScore(mbtiScore)
.build();
return kidMBTIRepository.save(kidMbti);
}
private void saveKidMBTIHistory(Kid kid, MbtiStatus mbtiStatus) {
KidMbtiHistory kidMbtiHistory = KidMbtiHistory.builder()
.kid(kid)
.mbtiStatus(mbtiStatus)
.isDeleted(false)
.build();
kidMBTIHistoryRepository.save(kidMbtiHistory);
}
추가적으로 아직 매직 리터럴이 쓰였던 MBTI의 성향들도(I, E ...) enum으로 관리 했습니다.
@Getter
public enum PersonalityTrait {
INTROVERSION("I"),
EXTRAVERSION("E"),
SENSING("S"),
INTUITION("N"),
THINKING("T"),
FEELING("F"),
JUDGING("J"),
PERCEIVING("P");
private final String type;
PersonalityTrait(String type) {
this.type = type;
}
}
이후 개발 과정에서 다른 서비스 로직에서도 MBTI 성향을 계산해야 하는 상황이 생겼습니다. 코드 중복을 줄이고 재사용성을 높이기 위해, MBTI 성향 계산 로직을 유틸리티 클래스로 분리하고, 정적 메서드로 리팩터링했습니다.
public class MbtiCalculator {
public static MbtiStatus determineMbtiType(MbtiScore mbtiScore) {
String mbti = determinePersonalityTrait(mbtiScore.getEScore(), mbtiScore.getIScore(),
PersonalityTrait.EXTRAVERSION.getType(), PersonalityTrait.INTROVERSION.getType())
+ determinePersonalityTrait(mbtiScore.getSScore(), mbtiScore.getNScore(),
PersonalityTrait.SENSING.getType(), PersonalityTrait.INTUITION.getType())
+ determinePersonalityTrait(mbtiScore.getFScore(), mbtiScore.getTScore(),
PersonalityTrait.FEELING.getType(), PersonalityTrait.THINKING.getType())
+ determinePersonalityTrait(mbtiScore.getPScore(), mbtiScore.getJScore(),
PersonalityTrait.PERCEIVING.getType(), PersonalityTrait.JUDGING.getType());
return MbtiStatus.toMbtiStatus(mbti);
}
private static String determinePersonalityTrait(int firstScore, int secondScore, String firstType,
String secondType) {
return firstScore >= secondScore ? firstType : secondType;
}
}
아래 코드는 지금까지 리팩터링한 코드입니다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class KidServiceImpl implements KidService {
private final KidRepository kidRepository;
private final MbtiResponseRepository mbtiResponseRepository;
private final KidMBTIRepository kidMBTIRepository;
private final KidMBTIHistoryRepository kidMBTIHistoryRepository;
@Transactional
@Override
public void diagnoseKidMBTI(KidMBTIDiagnosisRequest diagnosisRequest) {
Kid kid = findKid(diagnosisRequest);
MbtiResponse mbtiResponse = KidMBTIDiagnosisRequest.getMBTIResponse(diagnosisRequest, kid);
mbtiResponseRepository.save(mbtiResponse);
MbtiScore mbtiScore = KidMBTIDiagnosisRequest.getMBTIScore(diagnosisRequest);
MbtiStatus mbtiStatus = calculateMbtiStatus(diagnosisRequest);
KidMbti kidMbti = saveKidMBTI(mbtiStatus, mbtiScore);
kid.updateKidMbti(kidMbti);
saveKidMBTIHistory(kid, mbtiStatus);
}
private Kid findKid(KidMBTIDiagnosisRequest diagnosisRequest) {
return kidRepository.findById(diagnosisRequest.getUserId())
.orElseThrow(() -> new RuntimeException("no kid"));
}
private MbtiStatus calculateMbtiStatus(KidMBTIDiagnosisRequest request) {
String mbti = compareScores(request.getExtraversionScore(), request.getIntroversionScore(),
PersonalityTrait.EXTRAVERSION.getType(), PersonalityTrait.INTROVERSION.getType())
+ compareScores(request.getSensingScore(), request.getIntuitionScore(),
PersonalityTrait.SENSING.getType(), PersonalityTrait.INTUITION.getType())
+ compareScores(request.getFeelingScore(), request.getThinkingScore(),
PersonalityTrait.FEELING.getType(), PersonalityTrait.THINKING.getType())
+ compareScores(request.getPerceivingScore(), request.getJudgingScore(),
PersonalityTrait.PERCEIVING.getType(), PersonalityTrait.JUDGING.getType());
return MbtiStatus.toMbtiStatus(mbti);
}
private String compareScores(int firstScore, int secondScore, String firstType, String secondType) {
return firstScore >= secondScore ? firstType : secondType;
}
private KidMbti saveKidMBTI(MbtiStatus mbtiStatus, MbtiScore mbtiScore) {
KidMbti kidMbti = KidMbti.builder()
.isDeleted(false)
.mbtiStatus(mbtiStatus)
.mbtiScore(mbtiScore)
.build();
return kidMBTIRepository.save(kidMbti);
}
private void saveKidMBTIHistory(Kid kid, MbtiStatus mbtiStatus) {
KidMbtiHistory kidMbtiHistory = KidMbtiHistory.builder()
.kid(kid)
.mbtiStatus(mbtiStatus)
.isDeleted(false)
.build();
kidMBTIHistoryRepository.save(kidMbtiHistory);
}
}
추가 로직 설계
- 프론트가 보낸 자녀의 id 값으로 자녀를 조회한다.
- 프론트로부터 받은 점수를 유저의 MBTI 설문 조사에 저장한다.
- 받은 점수를 통해 자녀의 MBTI(성향)를 계산하고 정한다.
- 자녀 성향에 MBTI 점수와 정해진 성향을 저장한다.
- 자녀 성향 히스토리에도 저장한다.
앞서 자녀 성향을 진단 로직을 위와 같이 정리를 했습니다.
개발을 진행하면서 진단을 할 경우 해당 진단이 처음하는 진단인지 아니면 재진단인지 구분하는 로직이 필요 했습니다.
그래서 아래와 같이 로직을 추가 했습니다.
재진단
처음 진단인지 재진단인지 구분
재진단인 경우 아래 로직을 추가
- 자녀 성향 초기화
- 장르 점수 초기화
- 자녀가 도서, 장르 좋아요 한것들 다 삭제
자녀의 MBTI 값을 가져와 존재하지 않으면 새로 생성하고, 이미 값이 있다면 업데이트하는 코드를 추가로 작성했습니다. 처음에는 생성과 수정의 역할을 확실히 분리하고 싶었지만, 멘토님의 피드백을 통해 이 로직의 통합이 불가피하다는 점을 알게 되었습니다. 이에 따라, 함수의 의도를 분명히 전달하고자 updateOrCreateKidMbti()와 같이 생성과 수정을 모두 포함하는 메서드명을 사용해 로직을 리팩터링했습니다.
private void updateOrCreateKidMbti(Kid kid, KidMbtiDiagnosisRequest diagnosisRequest,
MbtiStatus updatedMbtiStatus) {
KidMbti currentKidMbti = kid.getKidMbti();
if (currentKidMbti == null) {
currentKidMbti = createKidMbti(diagnosisRequest, updatedMbtiStatus);
} else {
MbtiScore mbtiScore = MbtiScore.from(diagnosisRequest);
currentKidMbti.updateMbti(mbtiScore, updatedMbtiStatus);
// 자녀 장르 점수, 도서 좋아요, 장르 좋아요 초기화
genreScoreService.resetGenreScoreForKid(kid.getId());
likeGenreService.resetGenreLikesForKid(kid.getId());
likeMbtiService.resetMbtiLikesForKid(kid.getId());
}
kid.updateKidMbti(currentKidMbti);
}
최종 코드
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class KidServiceImpl implements KidService {
...
/**
* 자녀의 MBTI를 진단하는 메서드
*
* @param diagnosisRequest 자녀의 MBTI 점수 데이터를 담은 KidMbtiDiagnosisRequest 객체
*/
@Transactional
@Override
public void diagnoseKidMbti(KidMbtiDiagnosisRequest diagnosisRequest) {
Kid kid = findKidById(diagnosisRequest.getKidId());
saveMbtiResponse(diagnosisRequest, kid);
MbtiScore diagnosedKidMbtiScore = MbtiScore.from(diagnosisRequest);
MbtiStatus updatedMbtiStatus = MbtiCalculator.determineMbtiType(diagnosedKidMbtiScore);
updateOrCreateKidMbti(kid, diagnosisRequest, updatedMbtiStatus);
saveKidMbtiHistory(kid, updatedMbtiStatus);
}
/**
* 자녀 엔티티를 조회하는 메서드.
*
* @param kidId 자녀 엔티티의 id 값
*/
private Kid findKidById(Long kidId) {
return kidRepository.findKidBy(kidId)
.orElseThrow(NotFoundKidException::new);
}
/**
* 자녀가 응답한 MBTI 설문 결과를 저장하는 메서드
*
* @param diagnosisRequest 자녀의 MBTI 점수 데이터를 담은 KidMbtiDiagnosisRequest 객체
* @param kid 자녀 엔티티
*/
private void saveMbtiResponse(KidMbtiDiagnosisRequest diagnosisRequest, Kid kid) {
MbtiAnswer mbtiAnswer = KidMbtiDiagnosisRequest.getMbtiAnswer(diagnosisRequest, kid);
mbtiAnswerRepository.save(mbtiAnswer);
}
/**
* 자녀의 현재 MBTI를 조회하여 null 인경우 새로 생성하고 null 이 아니면 기존 자녀의 MBTI를 변경하는 메서드 자녀의 현재 MBTI(currentKidMbti)가 null 이면 처음 진단
* 자녀의 현재 MBTI(currentKidMbti)가 null 이 아니면 재진단
*
* @param kid 자녀 엔티티
* @param diagnosisRequest 자녀의 MBTI 점수 데이터를 담은 KidMbtiDiagnosisRequest 객체
* @param updatedMbtiStatus 설문 결과를 바탕으로 업데이트 된 자녀 MBTI 상태 객체
*/
private void updateOrCreateKidMbti(Kid kid, KidMbtiDiagnosisRequest diagnosisRequest,
MbtiStatus updatedMbtiStatus) {
KidMbti currentKidMbti = kid.getKidMbti();
if (currentKidMbti == null) {
currentKidMbti = createKidMbti(diagnosisRequest, updatedMbtiStatus);
} else {
MbtiScore mbtiScore = MbtiScore.from(diagnosisRequest);
currentKidMbti.updateMbti(mbtiScore, updatedMbtiStatus);
// 자녀 장르 점수, 도서 좋아요, 장르 좋아요 초기화
genreScoreService.resetGenreScoreForKid(kid.getId());
likeGenreService.resetGenreLikesForKid(kid.getId());
likeMbtiService.resetMbtiLikesForKid(kid.getId());
}
kid.updateKidMbti(currentKidMbti);
}
/**
* 자녀의 MBTI를 생성하는 메서드
*
* @param diagnosisRequest 자녀의 MBTI 점수 데이터를 담은 KidMbtiDiagnosisRequest 객체
* @param updatedMbtiStatus 설문 결과를 바탕으로 업데이트 된 자녀 MBTI 상태 객체
*/
private KidMbti createKidMbti(KidMbtiDiagnosisRequest diagnosisRequest, MbtiStatus updatedMbtiStatus) {
KidMbti kidMbti = KidMbti.builder()
.eScore(diagnosisRequest.getExtraversionScore())
.iScore(diagnosisRequest.getIntroversionScore())
.sScore(diagnosisRequest.getSensingScore())
.nScore(diagnosisRequest.getIntuitionScore())
.fScore(diagnosisRequest.getFeelingScore())
.tScore(diagnosisRequest.getThinkingScore())
.jScore(diagnosisRequest.getJudgingScore())
.pScore(diagnosisRequest.getPerceivingScore())
.mbtiStatus(updatedMbtiStatus)
.build();
return kidMBTIRepository.save(kidMbti);
}
/**
* 자녀의 MBTI 히스토리를 생성하는 메서드
*
* @param kid 자녀 엔티티
* @param updatedMbtiStatus 설문 결과를 바탕으로 업데이트 된 자녀 MBTI 상태 객체
*/
private void saveKidMbtiHistory(Kid kid, MbtiStatus updatedMbtiStatus) {
KidMbtiHistory kidMbtiHistory = KidMbtiHistory.builder()
.kid(kid)
.mbtiStatus(updatedMbtiStatus)
.isDeleted(false)
.build();
kidMBTIHistoryRepository.save(kidMbtiHistory);
}
@Override
public GetKidMbtiResponse getKidMbti(Long kidId) {
KidMbti kidMbti = kidMBTIRepository.findKidMbtiBy(kidId)
.orElseThrow(() -> new NotFoundException(ExceptionCode.NOT_FOUND_KID_MBTI));
return GetKidMbtiResponse.from(kidMbti);
}
}
마무리
이번 글에서는 자녀 성향 진단 로직을 구현하면서 겪었던 고민과, 이를 해결하기 위해 시도한 리팩터링 과정에 대해 공유했습니다. 처음 코드를 작성할 때는 굉장히 폭력적(?)이였는데 리팩터링을 거치며 점차 더 나은 코드를 만들어갈 수 있었습니다. 이러한 과정을 통해 앞으로도 가독성과 유지보수성을 고려한 코드를 위해서는 리팩터링은 필수라고 생각 하게 되었습니다.
'프로젝트 > Kidsping' 카테고리의 다른 글
개발 기록 - 선착순 응모 시스템 이슈 및 해결 과정 (0) | 2024.10.30 |
---|---|
트러블 슈팅 - AWS 프리티어 EC2 인스턴스 메모리 부족 현상 해결하기 (0) | 2024.10.25 |
개발 기록 - N + 1 문제 fetch join, Batch Size로 해결 (0) | 2024.10.24 |
개발 기록 - Java에서 Enum 의 비교는 '==' 인가? 'equals' 인가? (0) | 2024.10.22 |
개발 기록 - @RequestBody DTO에 값이 안들어오는 이유 (Jackson, Lombok) (0) | 2024.10.22 |