Readable Code 적용

2024. 10. 24. 22:18·Language/Java

인프런 워밍업 스터디에서 진행하는 readable code 강의를 수강 중이다.

 

출석 체크 이벤트 기능 개발 업무를 한 적이 있는데 

단순 구현만 되어 있는 코드로, 읽기 좋은 코드는 절대 아니라 생각되어 배운 내용을 적용해보려 한다.

  • 구체적으로 표현되어 있는 코드 추상화 (저수준 -> 고수준)
  • 읽는 사람의 사고 depth 줄이기

강의에서는 여러 가지 방법을 소개하지만 위 내용을 중점적으로 적용해보려 한다.


아래는 이벤트 처리 프로세스이다.

 

출석 체크 이벤트 처리 프로세스

  •  금일 출석 체크 여부 확인
  • 이벤트 참여 등록 처리
  • 이벤트 쿠폰 조회 및 쿠폰 타입별 분리
  • 정책에 따른 쿠폰 지급
쿠폰은 연속 쿠폰과 일일 쿠폰이 있다.
일일 쿠폰은 사용자가 출석하면 즉시 지급되는 쿠폰이고,
연속 쿠폰은 관리자 페이지에서 지정한 연속 일수를 달성하면 지급되는 쿠폰이다.

 

처리 단계가 모두 하나의 메서드 안에서 동작하고 있으며, 단계마다 추상화 레벨이 달라 한눈에 들어오지 않았다.

@Transactional
public Map<String, Object> eventProcess(Event event, User user) {
	
    // 금일 출석 체크 여부 확인
    
    // 이벤트 참여 등록 처리
    
    // 출석 체크 이벤트 쿠폰 조회
    	// 쿠폰 타입별 분리(일일 쿠폰, 연속 쿠폰)
        
    // 조건에 따른 쿠폰 지급
    
    return 지급된 쿠폰정보, 사용자에게 보여질 메시지 등등;
}

단계별로 적용해 보자. 

 

금일 출석 체크 여부 확인 & 이벤트 참여 등록 처리 

boolean isAttendCheckedInToday = 출석 여부 DB 조회;
if (isAttendCheckedInToday) {
    //로그인 참여 1일 1회 제한
    return ...;
}

이벤트_참여_처리_메서드(user);

이 부분은 크게 고칠 것이 없다고 생각한다.

간단한 코드로 구성되어 있으며 지금으로도 충분히 읽기 편하다 생각하기 때문이다.

오히려 Early Return을 사용해서 사고 흐름이 직관적이다.

 

가장 문제가 되는 부분은 쿠폰 조회와 지급 부분이다.

앞서 연속쿠폰에 대해 설명했는데
관리자 페이지에서 연속쿠폰 지급에 대한 기능을 on/off 할 수 있다.
또한, 연속 쿠폰 지급일에 일일쿠폰 포함 여부를 설정할 수 있다. 

출석 체크 이벤트 쿠폰 조회 & 쿠폰 지급

List<AttendCoupon> attendCoupons = 출석 이벤트에 등록된 쿠폰 DB 조회(event);

// 쿠폰 타입별 분리
List<AttendCoupon> dailyCoupons = new ArrayList<>(); // 일일 쿠폰
List<AttendCoupon> conCoupons = new ArrayList<>(); // 연속 쿠폰

// 일일쿠폰:D , 연속쿠폰:C 
for (AttendCoupon coupon : attendCoupon) {
    if ("D".equals(coupon.getCouponType())) {
        dailyCoupons.add(coupon);
    } else {
        conCoupons.add(coupon);
    }
}
// 연속 출석 사용 여부에 따라 해당 쿠폰 지급
boolean isConAttendUse = 연속 출석 기능 사용 여부;
boolean isConAttendDay = 오늘이 연속 출석일인지 체크;
boolean isDailyWithConCouponDisburse = 일일쿠폰과 연속쿠폰 모두 지급하는지;

// 사용자에게 지급할 쿠폰 리스트
List<AttendCoupon> disburseCoupons = new ArrayList<>();

if (isConAttendUse) {
    if (isConAttendDay) {
        if (isDailyWithConCouponDisburse) {
            // 일일, 연속 쿠폰 모두 지급
            쿠폰_지급_메서드(disburseCoupons, attendCoupons, event, user);
        } else {
            // 연속 쿠폰만 지급
            쿠폰_지급_메서드(disburseCoupons, conCoupons, event, user);
        }
    } else {
        // 일일 쿠폰만 지급
        쿠폰_지급_메서드(disburseCoupons, dailyCoupons, event, user);
    }
} else {
    // 일일 쿠폰만 지급
    쿠폰_지급_메서드(disburseCoupons, dailyCoupons, event, user);
}

쿠폰 조회, 쿠폰 타입별 분리, 조건에 맞는 쿠폰 지급.

3개의 주제가 하나의 메서드 안에서 구체적으로 표현되어 있다.

 

잘 쓰인 코드라면, 한 메서드의 주제는 반드시 하나다.

만약 메서드 안에 여러 주제가 있어야 한다면, 더 큰 맥락 안에서 포괄적인 의미를 담는 메서드로 작성해야 한다.

 

3개의 주제는 결국, 쿠폰 지급을 위해 사용된다.

'사용자에게 지급할 쿠폰'이라는 내용으로 추상화해 보면 좋을 것 같다.

@Transactional
public Map<String, Object> eventProcess(Event event, User user) {
	
    boolean isAttendCheckedInToday = 출석 여부 DB 조회;
    if (isAttendCheckedInToday) {
        //로그인 참여 1일 1회 제한
        return ...;
    }
    
    이벤트_참여_처리_메서드(user);
    
    List<AttendCoupon> disburseCoupons = couponDisburseToUser(event, user);
    
    // ...
}

이벤트 처리 프로세스도 다시 작성해 보자.

  • 금일 출석 체크 여부 확인
  • 이벤트 참여 처리
  • 이벤트 쿠폰 지급

쿠폰 지급에 대한 구체적 표현을 메서드로 추상화함으로써, 각 처리 단계들과 추상화 레벨을 맞췄다.


 새로 분리된 couponDisburseToUser를 살펴보면 메서드만 분리됐을 뿐, 내용은 그대로다.

public List<AttendCoupon> couponDisburseToUser(Event event, User user) {
    List<AttendCoupon> attendCoupons = 출석 이벤트에 등록된 쿠폰 DB 조회(event);

    // 쿠폰 타입별 분리
    List<AttendCoupon> dailyCoupons = new ArrayList<>(); // 일일 쿠폰
    List<AttendCoupon> conCoupons = new ArrayList<>(); // 연속 쿠폰

    // 일일쿠폰:D , 연속쿠폰:C 
    for (AttendCoupon coupon : attendCoupon) {
        if ("D".equals(coupon.getCouponType())) {
            dailyCoupons.add(coupon);
        } else {
            conCoupons.add(coupon);
        }
    }

    // 연속 출석 사용 여부에 따라 해당 쿠폰 지급
    boolean isConAttendUse = 연속 출석 기능 사용 여부;
    boolean isConAttendDay = 오늘이 연속 출석일인지 체크;
    boolean isDailyWithConCouponDisburse = 일일쿠폰과 연속쿠폰 모두 지급하는지;

    // 사용자에게 지급할 쿠폰 리스트
    List<AttendCoupon> disburseCoupons = new ArrayList<>();

    if (isConAttendUse) {
        if (isConAttendDay) {
            if (isDailyWithConCouponDisburse) {
                // 일일, 연속 쿠폰 모두 지급
                쿠폰_지급_메서드(disburseCoupons, attendCoupons, event, user);
            } else {
                // 연속 쿠폰만 지급
                쿠폰_지급_메서드(disburseCoupons, conCoupons, event, user);
            }
        } else {
            // 일일 쿠폰만 지급
            쿠폰_지급_메서드(disburseCoupons, dailyCoupons, event, user);
        }
    } else {
        // 일일 쿠폰만 지급
        쿠폰_지급_메서드(disburseCoupons, dailyCoupons, event, user);
    }
    
    return disburseCoupons;
}

가장 보기 불편한 것은 분기문과 반복되는 '쿠폰_지급_메서드' 호출이다.

분기문이 깊어질수록 코드를 읽을 때마다 이전단계의 조건을 고려하면서 읽어야 한다.

그리고 무분별한 else 사용으로 처리 의도가 명확하지 않아 혼란을 준다.

 

여기서 사고 depth 줄이기를 적용해 볼 수 있을 것 같다.

먼저 '쿠폰_지급_메서드'의 반복 호출을 줄여보자.

 

반복 호출하는 이유는 타입별로 분리된 컬렉션 객체를 넣으려 하기 때문이라 생각된다.

파라미터를 살펴보면 조건에 따라 이벤트 쿠폰 파라미터만 변경되고 나머지는 변하지 않는다.

 

쿠폰 타입별 분리를 할 때 C, D와 같은 하나의 문자로 구분하여 컬렉션 객체를 생성한다.

컬렉션 객체 대신 String을 파라미터로 전달하여 '쿠폰_지급_메서드' 안에서 분리하도록 하면 좋을 것 같다.

그리고 빈 컬렉션인 disburseCoupons을 전달할 필요 없이 메서드에서 반환하도록 작성해 보자. 

public List<AttendCoupon> couponDisburseToUser(Event event, User user) {

    // 변수화 되어 있는 조건들 모두 메서드로 분리
    String couponType = "D";
    if (isUsedConAttendFeature(event)) {
        if (isConCouponDisburseDay(event, user)) {
            couponType = "C";
            if (isDuplicateCouponDisburse(event)) {
                couponType = "A";
            }
        }
    }
    
    return 쿠폰_지급_메서드(couponType, event, user);
}

두 번째로 조건문을 고쳐보려 한다.

 

조건을 글로 풀어서 설명해 보자.

  • 연속 출석 기능을 사용하고 있고,
  • 오늘이 연속 출석일이면 "C"를 반환해.
  • 아! 만약 일일쿠폰과 같이 지급해야 하면 "A"를 반환해.
  • 조건에 맞지 않으면 "D"를 반환해. 

음... 코드 스멜이 나지만 정확하게 어떤 부분을 고쳐야 할지 잘 모르겠다..

그렇다면 더 직관적으로 작성해 볼 수는 없을까?

 

조건에 따라 couponType이 변경되니, 메서드로 분리해서 Early return을 적용해 보자.

public List<AttendCoupon> couponDisburseToUser(Event event, User user) {
    String couponType = getCouponType(event, user);
    return 쿠폰_지급_메서드(couponType, event, user);
}

private String getCouponType(Event event, User user) {
    if (isNotUsedConAttendFeature(event)) {
        return "D";
    }
    if (isConCouponDisburseDay(event, user)) {
        if (isDuplicateCouponDisburse(event)) {
            return "A";
        }
        return "C";
    }
    return "D";
}

똑같은 것 같은데

 

비슷하지만 2번째 코드를 선택했다.

  • 구체적 표현보다 하나의 행위로 추상화.
  • 중첩되는 분기문을 최소화하여 Early return 적용.

위와 같은 이유로 깊어지는 사고에서 빠르게 벗어날 수 있을 거라 판단했기 때문이다.

기존의 isUsedConAttendFeature에서 부정어를 추가했다.
부정 연산자 대신 메서드명을 변경한 이유는 부정 연산자의 경우 한 번 더 비틀어서 사고해야 하기 때문이다.
그리고 is~ 로 시작하는 메서드명 앞에 부정 연산자를 붙이면 잘 안보이더라..

마지막으로 쿠폰_지급_메서드를 살펴보자.

private List<AttendCoupon> 쿠폰_지급_메서드(String couponType, Event event, User user) {
    // 지급할 쿠폰 조회
    List<AttendCoupon> disburseCoupons = findCandidateCoupon(couponType, event);
    
    // 쿠폰 지급 및 이력 DB insert

    return disburseCoupons;
}

private List<AttendCoupon> findCandidateCoupon(String couponType, Event event) {
    List<AttendCoupon> attendCoupons = 출석 이벤트에 등록된 쿠폰 DB 조회(event);

    if ("C".equals(couponType) || "D".equals(couponType)) {
        List<AttendCoupon> candidateCoupons = new ArrayList<>();
        for (AttendCoupon coupon : attendCoupon) {
            if (couponType.equals(coupon.getCouponType())) {
                candidateCoupons.add(coupon);
            }
        }
        return candidateCoupons;
    }

    return attendCoupons;
}

최종적으로 쿠폰 조회 및 타입 분리는 findCandidateCoupon 메서드에서 동작된다.

스트림 API를 사용해서 구현할 수도 있지만 프로젝트 버전 호환성 문제로 반복문으로밖에 구현하지 못하는 상황이다.  


메서드 추상화, Early return, 메서드 네이밍 변경 등을 활용해서 리팩터링 했지만 누군가에겐 읽기 불편한 코드가 될 수 있다. 

 

프로젝트의 모든 메서드 구조가 하나의 메서드 안에서만 동작한다면 메서드 분리가 오히려 가독성을 해칠 수도 있고,

스트림 API로 구현하는 것이 더 좋은 코드라 해도 그것이 프로젝트 코드의 일관성을 깨뜨릴 수 있다.

 

결국 읽기 좋은 코드라는 것은 프로젝트 환경에 따라 달라지는 것 같다.

저작자표시 (새창열림)

'Language > Java' 카테고리의 다른 글

AspectJ & Java 호환성 이슈  (0) 2024.10.02
'Language/Java' 카테고리의 다른 글
  • AspectJ & Java 호환성 이슈
tjdgus
tjdgus
  • tjdgus
    Do It...
    tjdgus
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Language
        • Java
      • CS
        • Data Structure
        • OS
        • Algorithm
        • Network
      • 오류 모음집
      • ETC
      • 함수형 프로그래밍
      • JPA
      • Toy
      • 데이터베이스
      • Spring
      • 코딩테스트
        • 99클럽 4기
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    개발자취업
    오블완
    99클럽
    TiL
    코딩테스트준비
    항해99
    티스토리챌린지
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
tjdgus
Readable Code 적용
상단으로

티스토리툴바