워밍업 클럽 4기 BE - 1주차 발자국

1주차 미션 회고

강의에서 배운 내용을 내 머릿속에 있는 단어들로 재해석하니 개념들이 확실히 잡히는 느낌이었다.

AS-IS 코드에 챕터 별 내용을 하나씩 적용해가면서 리팩토링을 하니 어느새 클린 코드가 되어 있어서 재밌는 경험이었다.

 

1주차 강의 회고

생각보다 긴 강의 시간에 조금 지치기도 했지만 실습 위주의 커리큘럼으로 집중해서 진행할 수 있었다.

요즘 업무가 바쁘다는 핑계로 코드를 작성할 때 이런 개념들을 잠시 등한시 했던 나를 반성하게 된다.

클린 코드 핸드북을 목표로 핵심만 정리해서 추후 코드 작성 시 매번 참고하는 자료가 되도록 만들어야겠다.

 

1주차 학습 내용 요약

추상화


추상(抽象) : 중요한 정보는 가려내어 남기고, 덜 중요한 정보는 생략하여 버린다.

  • 추상화는 복잡한 데이터와 복잡한 로직을 단순화하여 이해가 쉽도록 돕는다.

  • 잘못된 추상화가 야기하는 사이드 이펙트는 생각보다 크다.

  • 적절한 추상화는 해당 도메인의 문맥 안에서, 정말 중요한 핵심 개념만 남겨서 표현하는 것이다.

이름 짓기


  • 단수와 복수를 구분하기

  • 이름 줄이지 않기 (관용어는 예외)

  • 은어/방언 사용하지 않기

  • 비슷한 상황에서 자주 사용하는 단어, 개념 습득하기

메서드 추상화


void 서점에서_책을_샀다() {
		우빈이는 산책을 하다 은행에 가서 얼마 인출했다.
		서점 가는 길에 아이스크림을 하나 사먹었다.
		남은 돈으로 서점에 가서 보고싶은 책을 고르고, 책을 구매했다.
}
void 산책하면서_돈쓰기() {
		Money 은행에서_현금_인출();
		Balance 아이스크림_사먹기(Money);
		Book 서점에서_책_구입하기(Balance);
}

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

추상화 레벨


public static void main(String[] args) {
		showGameStartComments(); **// 10 (추상화 레벨)**
		initializeGame(); **// 10**
		showBoard(); **// 10**
		
		if(gameStatus == 1) { **// 5 (갑자기 너무 구체적인 내용)**
				...
		}
}
  • 하나의 세계 안에서는, 추상화 레벨이 동등해야 한다.

  • 코드 주변과 동등한 추상화 레벨을 갖는게 중요하다.

매직 넘버, 매직 스트링


private static String[][] board = new String[8][10];
public static final int BOARD_ROW_SIZE = 8;
public static final int BOARD_COL_SIZE = 10;

private static String[][] board = new String[BOARD_ROW_SIZE][BOARD_COL_SIZE];
  • 의미를 갖고 있으나, 상수로 추출되지 않은 숫자, 문자열 등을 의미한다.

  • 상수 추출로 이름을 짓고 의미를 부여함으로써 가독성, 유지보수성이 높아진다.

메서드 선언부


// bad
String createDailyShopKey(String shopId, String localDateString);

// good
String createDailyShopKey(String shopId, LocalDate sellingDate);
  • 추상화된 구체를 유추할 수 있는, 적절한 의미가 담긴 이름으로 짓는다.

  • 파라미터의 타입, 개수, 순서를 통해 의미를 전달한다.

  • 적절한 타입의 반환값으로 리턴한다. (void 대신 반환할 만한 값이 있는지 고민해보기)

Early return


if(a > 3) {
		doSomething1();
} else if(a <= 3 && b > 1) {
		doSomething2();
} else {
		doSomething3();
}
void extracted() {
		if(a > 3) {
				doSomething1();
				return;
		}
		if(a <= 3 && b > 1) {
				doSomething2();
				return;
		}
		doSomething3();
}

중첩문 줄이기


for(int i=0; i<20; i++) {
		for(int j=20; j<30; j++) {
				if(i>=10 && j<25) {
						doSomething();
				}
		}
}
  • 추상화를 통한 사고 과정의 depth를 줄이는 것이 중요하다.

  • 중첩 구조로 표현하는 것이 사고하는 데 더 도움이 된다고 판단한다면, 그대로 놔두는 것이 더 나은 선택일 수 있다.

사용할 변수는 최대한 가깝게 선언하기


int i = 10;
// 코드 20줄
...

int j = i + 30; // 해당 코드에서 i의 값을 다시 기억해내야 한다.
int i = 10;
int j = i + 30;

부정어를 대하는 자세


  • 부정 연산자(!)는 가독성이 떨어진다.

  • 부정어구를 쓰지 않아도 되는 상황인지 체크한다.

  • 부정의 의미를 담은 다른 단어가 존재하는지 고민해본다.

Null을 대하는 자세


  • 항상 NullPointException을 방지하는 방향으로 경각심을 가진다.

  • 메서드 설계 시 return null 사용을 지양한다. (Optional 사용 고려)

Optional

public T orElse(T other) {
		return value != null ? value : other;
}

public T orElseGet(Supplier<? extends T> supplier) {
		return value != null ? value : supplier.get();
}

// ================================================================================ //
Optional<String> value = Optional.of("Hello");

// ⚠️ expensiveOperation() 항상 호출됨
String result = value.orElse(expensiveOperation()); 

// ✅ 값이 비어있을 때만 expensiveOperation() 호출됨
String result = value.orElseGet(() -> expensiveOperation()); 
  • 꼭 필요한 상황에서 반환 타입에 사용한다.

  • Optional을 파라미터로 받지 않도록 한다.

  • Optional을 반환받았다면 최대한 빠르게 해소한다.

상속 대신 조합 사용하기


public abstract class Cell {
		protected boolean isOpened;
		protected boolean isFlagged;
		
		public abstract String getSign();
		...
}

public class EmptyCell extends Cell {
		...
    @Override
    public String getSign() {
        if (isOpened) {
            return EMPTY_SIGN;
        }
        if (isFlagged) {
            return FLAG_SIGN;
        }
        return UNCHECKED_SIGN;
    }
}
public class CellState {
    private boolean isFlagged;
    private boolean isOpened;
    
    public static CellState initialize() {
        return new CellState(false, false);
    }
    
    public boolean isOpened() {
        return isOpened;
    }

    public boolean isFlagged() {
        return isFlagged;
    }
}

public class EmptyCell {
		private final CellState cellState = CellState.initialize();
}
  • SRP 분리

    • CellState는 "상태 관리"만 담당하고, EmptyCell은 "표현 방식"만 담당함

  • 재사용성 증가

    • 동일한 CellState 클래스를 다른 셀 유형(MineCell, NumberCell)에서도 재사용 가능

  • 테스트 용이성

    • 상태 로직(isOpened, isFlagged)을 독립적으로 테스트할 수 있음

  • 유연성 증가

    • EmptyCell에 새로운 동작을 추가할 때 상속보다 덜 제한적 (확장 쉬움)

  • 상속 제한 회피

    • Java는 단일 상속만 가능 → 조합을 쓰면 다른 기능도 쉽게 조합 가능

  • 불변성 강화

    • CellStatefinal로 구성하거나 setter 없이 만들어 불변 객체로 설계 가능

  • 의존성 주입 가능

    • 테스트나 상태 공유 목적일 때 CellState를 외부에서 주입 받을 수 있음 (mock 주입 등)

Value Object


class Address {
		private String 시도;
		private String 시군구;
		private String 도로명;
		
		// 생성자 외에 상태 변경 메서드 없음 (불변성)
		public Address(String 시도, String 시군구, String 도로명) {
		
				// 유효성 검증
			  if (시도== null || 시도.trim().isEmpty()) {
            throw new IllegalArgumentException(fieldName + "는 비어 있을 수 없습니다.");
        }
        if(...)
        
        this.시도 = 시도;
        this.시군구 = 시군구;
        this.도로명 = 도로명;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        
        // 동등성 검증
        Address that = (Address) o;
        return Objects.equals(시도, that.시도) &&
               Objects.equals(시군구, that.시군구) &&
               Objects.equals(도로명, that.도로명);
    }

    @Override
    public int hashCode() {
        return Objects.hash(시도, 시군구, 도로명);
    }
}
  • 도메인의 어떤 개념을 추상화하여 표현한 값 객체

  • 값으로 취급하기 위해서, 불변성, 동등성, 유효성 검증 등을 보장해야 한다.

vs Entity

class UserAccount {
		private String userId; // 식별자
		private String 이름;
		private String 생년월일;
}
  • 식별자가 같으면 동등한 객체로 취급한다.

  • VO는 식별자 없이, 내부의 모든 값이 다 같아야 동등한 객체로 취급한다.

일급 컬렉션


class CreditCards {
		private final List<CreditCard> cards;
		
		public List<CreditCard> findValidCards() {
				return this.cards.stream()
						.filter(CreditCard::isValid)
						.toList();
		}
}
  • 컬렉션을 포장하면서, 컬렉션만을 유일하게 필드로 가지는 객체이다.

  • 컬렉션을 추상화여 의미를 담을 수 있고, 가공 로직의 보금자리가 생긴다.

  • 컬렉션 반환 시 외부 조작을 피하기 위해 새로운 컬렉션으로 만들어서 반환해야 한다.

Enum의 특성과 활용


public enum UserAction {
		OPEN("셀 열기"),
		FLAG("깃발 꽂기"),
		UNKNOWN("알 수 없음");
		
		private final String description;
		
		UserAction(String description) {
				this.description = description;
		}
}
  • Enum은 상수의 집합이며, 상수와 관련된 로직을 담을 수 있는 공간이다.

  • 특정 도메인 개념에 대해 그 종류와 기능을 명시적으로 표현해줄 수 있다.

  • 변경이 잦은 개념은 Enum 보다 DB로 관리하는 것이 나을 수 있다.

다형성 활용하기


private String decideCellSignFrom(CellSnapshot snapshot) {
    CellSnapshotStatus status = snapshot.getStatus();
    if (status == CellSnapshotStatus.EMPTY) {
        return EMPTY_SIGN;
    }
    if (status == CellSnapshotStatus.FLAG) {
        return FLAG_SIGN;
    }
    if (status == CellSnapshotStatus.LAND_MINE) {
        return LAND_MINE_SIGN;
    }
    if (status == CellSnapshotStatus.NUMBER) {
        return String.valueOf(snapshot.getNearbyLandMineCount());
    }
    if (status == CellSnapshotStatus.UNCHECKED) {
        return UNCHECKED_SIGN;
    }
    throw new IllegalArgumentException("확인할 수 없는 셀입니다.");
}
private String decideCellSignFrom(CellSnapshot snapshot) {
		List<CellSignProvidable> cellSignProviders = List.of(
				new EmptyCellSignProvicer(),
				new FlagCellSignProvider(),
				...
		);
		
		return cellSignProviders.stream()
				.filter(provider -> provider.supports(snapshot))
				.findFirst()
				.map(provider -> provider.provide(snapshot))
				.orElseThrow(() -> new IllegalArgumentException("확인할 수 없는 셀입니다."));
}
public interface CellSignProvidable {
		boolean supports(CellSnapshot cellSnapshot);
		String provide(CellSnapshot cellSnapshot);
}
public class EmptyCellSignProvicer implements CellSignProvidable {
		private static final String EMPTY_SIGN = "■";
		
		@Override 
		public boolean supports(CellSnapshot cellSnapshot) {
				return cellSnapshot.getStatus() == CellSnapshotStatus.EMPTY;
		}

		@Override
		public String provice(CellSnapshot cellSnapshot) {
				return EMPTY_SIGN;		
		}
} 

댓글을 작성해보세요.

채널톡 아이콘