[인프런 워밍업 클럽 3기] BE 클린코드&테스트 - 1주차 발자국
강의 수강 내용 핵심 정리1. 클린 코드와 추상화클린 코드의 핵심은 가독성 → 코드가 잘 읽히면 이해하기 쉽고 유지보수가 용이함.추상화(Abstract): 구체적인 정보에서 핵심 개념만 뽑아 단순화하는 과정.적절한 추상화는 복잡한 로직과 데이터를 단순화하여 읽기 좋은 코드를 만든다.잘못된 추상화는 문맥이 맞지 않는 용어 사용이나 과도한 단순화로 혼란을 유발할 수 있음.추상화 레벨을 맞추는 것이 중요 → 동일한 코드 블록 내에서는 비슷한 추상화 수준 유지.2. 이름 짓기(Naming)의도를 명확히 전달하는 이름을 사용 (e.g. input, input2 → cellInput, userActionInput).줄임말은 가독성을 해칠 수 있음 → 되도록 풀네임 사용 (e.g. cnt 대신 count).일관된 도메인 용어 사용 → 내부 팀에서 정한 용어를 지키기.단수/복수 명확히 구분 (e.g. row vs rows).getter/setter 사용 최소화 → 객체에 메시지를 보내도록 유도 (isAgeGreaterThan(19)).3. 메서드 설계와 선언부메서드는 하나의 책임(기능)만 가지도록 설계 → 한 가지 이상의 일을 하면 분리.메서드명은 추상적 의미를 담아야 함 (예: processPayment 대신 deductBalance).파라미터와 반환 타입을 신중하게 결정 → void 대신 반환값을 활용하여 테스트 가능하도록 함.메서드의 Depth(중첩 수준) 최소화 → 지나치게 깊은 if문, for문은 리팩토링 필요.4. 가독성을 위한 코드 스타일Early Return 사용 → 불필요한 else 제거로 가독성 향상.공백 라인 활용 → 의미 있는 단위로 코드 분리.부정문 지양 → !isLeft() 대신 isRight() 또는 isNotLeft().매직 넘버 & 매직 스트링 제거 → 10 대신 BOARD_SIZE 사용.변수는 사용되는 곳 가까이 선언 → 코드 이해도를 높이고 유지보수 용이.5. 객체지향 설계 원칙 (SOLID)(1) 단일 책임 원칙 (SRP)한 클래스는 하나의 책임(변경 이유)만 가져야 함.예) 게임 로직과 사용자 입력 처리를 분리 (e.g. GameLogic, UserInputHandler).(2) 개방-폐쇄 원칙 (OCP)기존 코드 수정 없이 기능 확장이 가능해야 함.추상화(인터페이스) 사용으로 변경 사항이 최소화되도록 설계.(3) 리스코프 치환 원칙 (LSP)부모 클래스를 자식 클래스로 대체해도 문제가 없어야 함.자식 클래스에서 부모 클래스의 기능을 임의로 변경하거나 지원하지 않는 기능을 추가하면 LSP 위반.(4) 인터페이스 분리 원칙 (ISP)하나의 인터페이스가 너무 많은 기능을 포함하면 안 됨 → 기능 단위로 분리.예) GameInitializable, GameRunnable로 인터페이스 분리.(5) 의존성 역전 원칙 (DIP)상위 모듈(비즈니스 로직)은 하위 모듈(구현)과 직접적으로 의존하지 않아야 함.인터페이스를 통해 의존성을 주입(DI) → 런타임에 유연한 변경 가능.스프링의 IoC 컨테이너를 활용하면 DIP 자동 적용 가능.6. 예외 처리와 NULL 다루기예외를 의도적으로 구분 → 사용자 예외(AppException) vs 시스템 예외.NULL 사용 최소화 → Optional<T> 활용, orElseGet()으로 성능 최적화.예외 발생 가능성이 높은 구간은 외부 세계와의 접점 (e.g. 입력값, API 응답).7. 코드 리팩토링 접근법처음부터 SOLID 원칙을 완벽하게 적용하려 하지 말고, 도메인 이해를 우선.코드를 작성하면서 점진적으로 리팩토링 → 적절한 추상화와 역할 분리를 고민하며 개선.성능, 유지보수성을 고려하여 때로는 원칙을 트레이드오프할 수도 있음.8. 상속보다는 조합을 사용하자상속은 부모-자식 간 결합도가 높아 수정이 어렵고 유연성이 떨어짐.부모 클래스 변경 시 모든 자식 클래스에 영향을 미침.조합(Composition)과 인터페이스를 활용하면 유연한 구조를 만들 수 있음.코드 중복 제거보다 유연한 설계가 더 중요하다.9. Value Object(VO)와 EntityVO(Value Object): 도메인의 개념을 표현하는 값 객체로, 불변성을 가지며 식별자가 없음.불변성: final 필드 사용, setter 금지.동등성: 값이 같으면 동일한 객체로 취급 → equals() & hashCode() 재정의 필요.유효성 검증: 객체 생성 시점에서 검증 수행.Entity: 식별자가 존재하며, 같은 ID를 가지면 같은 객체로 취급.VO와 Entity의 차이:Entity: 시간이 지나면서 값이 변할 수 있음.VO: 생성된 이후 값이 변하지 않으며, 모든 값이 같아야 같은 객체로 취급됨.→ equals() & hashCode()를 재정의해야 하는 이유와, hash 자료형을 구현하는 방법을 학습할 필요가 있음.10. 일급 컬렉션(First-Class Collection)컬렉션을 단순히 사용하지 않고 객체로 포장하여 의미를 부여함.컬렉션을 감싸면서 로직을 함께 관리 → 가공 로직을 포함하여 유지보수성을 높임.새로운 컬렉션을 반환해야 하는 경우기존 컬렉션을 변경할 여지를 없애기 위해 새로운 리스트(ArrayList 등)를 반환해야 할 때가 있음.예: new ArrayList<>(originalList)는 리스트 객체를 새로 만들지만 내부 요소는 기존 객체를 참조하므로, 원본 데이터를 안전하게 유지하려면 내부 객체도 깊은 복사(Deep Copy)해야 함.11. Enum의 특성과 활용Enum은 단순한 상수가 아닌, 상태와 관련된 로직을 포함할 수 있는 객체.특정 도메인의 개념을 명확하게 표현할 수 있음.변경이 잦은 개념이라면 Enum 대신 DB에서 관리하는 것이 유리함.12. 다형성(Polymorphism) 활용하기반복적인 if-else 문을 줄이기 위해 다형성을 적극 활용.변하는 것은 조건과 행위 → enum + 인터페이스 조합으로 해결 가능.Enum value별로 인터페이스를 구현할 수 있음.한 번 배운 개념도 반복해서 복습하는 것이 중요하다.13. 숨겨진 도메인 개념 도출하기객체 지향은 현실을 100% 반영하는 것이 아니라, 현실을 흉내 내는 것.완벽한 설계는 불가능하며, 그 당시의 최선의 선택이 중요함.시간이 지나면서 틀렸음을 인지할 수도 있기 때문에, 미래 변경 가능성을 고려한 코드 작성이 필요함.미션 해결 과정 정리Day 2 미션우리는 매일 숨을 쉬고 음식을 소화하지만, 이를 매번 세밀하게 설명한다면 너무 복잡하고 불편할 것입니다.이 미션을 수행하면서, 최근 읽고 있는 《데이터 중심 애플리케이션 설계》(a.k.a DDIA)에서 본 문장이 떠올랐습니다."우발적 복잡도(accidental complexity)를 제거하기 위한 최상의 도구는 추상화다."여기서도 추상화라는 개념이 강조됩니다. 또한, 책의 예시에서도 “기계어, CPU 레지스터, 시스템 호출을 추상화한 것이 고수준 프로그래밍 언어이다”라는 설명이 나옵니다. 이는 강의에서 들었던 내용과도 동일합니다.컴퓨터가 실제로 실행하는 것은 어셈블리어와 같은 저수준 코드이지만, 개발자인 우리가 이를 직접 다루는 것은 어렵고 비효율적입니다. 따라서, 우리는 Java 같은 고수준 프로그래밍 언어를 사용하며 자연스럽게 추상화의 이점을 누리고 있는 것입니다.또 다른 추상화의 예시로 떠오른 것은 Spring에서 Database(ex: MySQL)를 사용하기 위한 ORM인 JPA, Spring Data JPA입니다.우리는 데이터베이스를 사용하기 위해 보통 다음과 같은 작업을 수행해야 합니다.Connection Pool에서 Connection을 가져옴데이터베이스와 연결을 맺음SQL 쿼리를 작성하고 실행결과를 받아서 처리이 과정은 데이터베이스를 사용할 때마다 반복되며, 이는 우발적 복잡도를 증가시킵니다. 이를 해결하기 위해 JPA와 Spring Data JPA 같은 ORM이 등장했습니다.즉, 선배 개발자들이 데이터베이스 작업의 복잡성을 추상화하여 보다 간결하고 일관된 방식으로 데이터를 다룰 수 있도록 한 것입니다.또한, Java의 interface 역시 추상화의 대표적인 사례라고 생각합니다.interface는 구현 세부 사항을 숨기고, 다양한 구현체를 유연하게 사용할 수 있도록 하며, 다형성을 활용하여 코드의 재사용성과 유지보수성을 높이는 역할을 합니다.이렇듯, 이번 미션에서는 다시 한번 올바른 추상화의 중요성과 그것이 Readable Code 를 만드는데 얼마나 큰 요소를 차지하는지 잘 알 수 있었습니다.애플리케이션 설계, 코드 구현 등 여러 곳에서 추상이라는 개념이 얼마나 중요하게 여기는지 다시 한번 알 수 있었고, 적재적소에 잘 써먹어야겠다는 생각을 많이 했습니다.Day 4 미션AS-ISpublic boolean validateOrder(Order order) { if (order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; } else { if (order.getTotalPrice() > 0) { if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } else { return true; } } else if (!(order.getTotalPrice() > 0)) { log.info("올바르지 않은 총 가격입니다."); return false; } } return true; } Readable 하지 못한 점 파악if-else 가 중첩되어 있다. → 사고의 depth 를 줄이자!early return 을 하자!부정 연산자(!)의 가독성이 떨어진다.getter 의 연속 사용 → 메서드화 하자!TO-BE@Service @RequiredArgsConstructor public class OrderService { ... public boolean validateOrder(Order order) { if (order.hasNoItems()) { throw new OrderException("주문 항목이 없습니다."); } if (order.hasNotCustomerInfo()) { throw new OrderException("사용자 정보가 없습니다."); } if (order.isInValidTotalPrice())) { throw new OrderException("올바르지 않은 총 가격입니다."); } return true; } } @Entity @Table(name = "orders") @Where(clause = "deleted_at IS NULL") @SQLDelete(sql = "UPDATE orders SET deleted_at = NOW() WHERE id = ?") public class Order { ... private boolean hasNoItems() { return this.items.isEmpty(); } private boolean hasNotCustomerInfo() { return this.customInfo.isEmpty(); } private boolean isTotalPriceLessThanZero() { return this.items.stream().mapToInt(Item::getPrice).sum() <= 0; } } public class OrderException extends RuntimeException { public OrderException(String message) { super(message); } } if-else 중첩문을 if 문 3개로 변환했다.order.getItems().size() == 0 getter 의 연속적 사용을 메서드화하여 Order 객체 안으로 숨겨서 캡슐화했다.!order.hasCustomerInfo() 과 같은 부정 연산자를 없애고, hasNotCustomerInfo 처럼 부정어도 메서드화 하여 가독성을 높였다.추가적으로 if 문 조건에 들어가는 메서드들을 Order class 안으로 옮기고,단순히 log 처리만 하는 것을 넘어서 RuntimeException 인 OrderException 으로 예외처리를 하였다.SOLID에 대하여 자기만의 언어로 정리해 봅시다.SRP : 단일 책임 원칙(single responsibility principle)OCP : 개방-폐쇄 원칙 (Open/closed principle)LSP : 리스코프 치환 원칙 (Liskov substitution principle)ISP : 인터페이스 분리 원칙 (Interface segregation principle)DIP : 의존관계 역전 원칙 (Dependency inversion principle)SRP(단일 책임 원칙)한 클래스는 하나의 책임만 가져야 한다.중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것OCP(개방-폐쇄 원칙)소프트웨어 요소는 확장에 열려 있으나 변경에는 닫혀 있어야 한다.다형성을 활용해보자인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현하면 코드에 변경이 없다.LSP(리스코프 치환 원칙)프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것ISP(인터페이스 분리 원칙)특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다덩어리가 크면 그걸 다 구현하기가 힘들다. 덩어리가 작으면 작은 기능만 구현하면 되니까 훨씬 쉬워진다.인터페이스가 명확해지고, 대체 가능성이 높아진다.DIP(의존관계 역전 원칙)프로그래머는 추상화(역할)에 의존해야지, 구체화(구현)에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다.쉽게 이야기해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻앞에서 이야기한 역할(Role)에 의존하게 해야 한다는 것과 같다.객체 세상도 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존하게 되면 변경이 아주 어려워진다.개인적으로 Spring Bible 로 생각하는 토비의 스프링, 김영한님의 Spring 강의를 듣고 채득한 내용을 바탕으로 SOLID 개념을 정리해봤습니다.일주일 회고다른 워밍업 클럽과는 달리 한 달 안에 2개의 강의, 그것도 강의당 대략 13시간 정도의 강의를 수강해야 해서 강의를 빨리 들어야 하는 압박감이 들어서 개인적으로는 쉽지 않았던 것 같습니다.그래도, 진도에 늦지 않게 강의를 수강한 것 같아서 무척 뿌듯하네요.그리고, 이번주에는 미션이 2개 있었는데 해당 미션을 수행하면서 강사님이 중요하게 생각하는 건 무엇인지, 강의를 들을 때 어떤 걸 중점적으로 들어야 하는지에 대한 일종의 가이드라인을 주시는 것 같아서 강의를 수강하기에 좀 더 수월 했던 것 같습니다.현재, 2년 2개월을 회사 생활을 마치고, 이직 준비를 한지 벌써 1년이 지나면서 바쁘게 취직 준비를 하고 있지만 그럼에도 박우빈님의 강의를 들으면서 클린코드/테스트코드를 공부하기 정말 잘했다는 소위 말하는 돈값을 한다는 강의를 느꼈습니다.남은 3주도 열심히 하여 완강은 물론 우수러너도 되어보도록 하겠습니다.