워밍업 클럽 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는 단일 상속만 가능 → 조합을 쓰면 다른 기능도 쉽게 조합 가능
불변성 강화
CellState
를final
로 구성하거나 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;
}
}
댓글을 작성해보세요.