[4기] 1주 차 발자국 제출_백엔드

SOLID

객체지향 설계를 이해하기 쉽고 유지보수하기 유연하게 형태로 할 수 있도록 도움을 주는 5가지 원칙

  • SRP : Single Responsibility Principle

  • OCP : Open-Closed Principle

  • LSP : Liskov Substitution Principle

  • ISP : Interface Segregation Principle

  • DIP : Dependenty Inversion Principle

 

단일책임원칙 SRP : Single Responsibility Principle

하나의 클래스가 하나의 책임만 갖도록 설계를 하라는 원칙

  - 하나의 클래스가 변경 이유가 두가지라면, 해당 클래스의 책임은 2개 이상인 것.

  - 객체가 가진 공개 메서드, 필드, 상수 등은 해당 객체의 단일 책임에 의해서만 변경되어야 함

 

이유 : SRP 를 지키게 되면 높은 응집도를 가지게 되고 낮은 결합도를 가지고 된다.

  - 응집도 : 클래스나 모듈 내에 요소들이 긴밀하게 연관되어 있는 정도

  - 결합도 : 두개 이상의 객체가 협력했을 때 한 객체가 변경되었을때 다른 객체들에 영향을 미치는 정도

 

개방-폐쇄 원칙 OCP : Open-Closed Principle

확장에는 열려있고, 수정에는 닫혀 있어야 한다.

  - 새로운 요구사항이 추가되었을 때, 기존 코드의 변경 없이, 시스템의 기능을 확장할 수 있어야 한다.

어떻게 OCP 를 지킬 수 있나 ? >> 추상화와 다형성을 활용해서 OCP 를 지킬 수 있다. 

 

리스코프 치환 원칙 LSP : Liskov Substitution Principle

리스코프 : 바바라 리스코프(사람 이름)

상속 구조에서 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.

  - 자식 클래스는 부모 클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야 한다.

 

이유 : LSP 를 위반하면, 상속 클래스를 사용할 때 예외가 발생하거나 이를 방지하기 위한 불필요한 타입 체크가 동반될 수 있다. 따라서 상속 구조를 사용하려면 LSP 를 잘 지키는 형태로 구현해야한다.

 

인터페이스 분리 원칙 ISP : Interface Segregation Principle

클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안된다. 

  - 인터페이스를 잘게 쪼개는 것이 권장됨(=기능단위로 인터페이스를 잘게 나누어 사용해라)

 

이유 : 인터페이스 하나가 너무 무겁고 여러가지 기능이 합쳐져있게 되면 구현체에서 불필요한 기능까지 구현하게 되어버린다. 불필요한 의존성으로 인해 결합도가 높아지고, 특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.

 

의존성 역전 원칙 : DIP : Dependenty Inversion Principle

상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안되고, 둘 모두 추상화에 의존해야 한다는 원칙 (= 추상화 레벨이 높은 쪽의 모듈이 구체 쪽에 가까운 모듈에 직접적으로 의존해서는 안된다. 모두 추상화에 의존하자)

  - 의존성의 순방향 : 고수준 모듈이 저수준 모듈을 참조하는 것

  - 의존성의 역방향 : 고수준, 저수준 모듈이 모두 추상화에 의존하는 것

    - 의존성 : 하나의 모듈이 다른 하나의 모듈을 참조하거나, 직접적으로 생성하거나, 사용하는 것

  - 저수준 모듈이 변경되어도, 고수준 모듈에는 영향이 가지 않는다. 

 

이유 : 저수준 모듈은 자주 변경되는데, DIP 원칙을 지키면 저수준 모듈이 자유롭게 변경되어도 고수준 모듈에는 영향이 가지 않는다.


 

코드를 읽을 때 뇌 메모리 적게 쓰도록 도와주는(=가독성 좋은) 코드 작성

 

1. Early return

- Early return 의 사용으로 else 의 사용을 줄이자

- switch 문 지양

 

2. 중첩 분기문과 중첩 반복문의 리팩토링

중첩 반복문은 내가 기억해야하는 정보가 너무 많다.

for (let i = 0; i < 5; i++) {
    for (let j = 0; j < 3; j++) {
        if (i >= 2 && j < 1) {
            doSomething(i, j);
        }
    }
}

 

doSomething 을 읽을때쯤에는 기억해야하는 정보가 너무 많다.

메서드로 다 쪼개고 동작의 의미를 부여해서 추출한다.

function shouldProcess(i, j) {
    return i >= 2 && j < 1;
}

function processInnerLoop(i) {
    for (let j = 0; j < 3; j++) {
        if (shouldProcess(i, j)) {
            doSomething(i, j);
        }
    }
}

for (let i = 0; i < 5; i++) {
    processInnerLoop(i);
}

 

 

3. 공백라인을 대하는 자세

공백라인도 의미를 가진다.

- 복잡한 로직의 의미 단위를 나누어 보여줌으로써 읽는 사람에게 추가적인 정보를 전달할 수 있음

 

4. 부정어를 대하는 자세

복잡한 부정 조건 (읽기 비효율적이다)

  • 관리자거나

  • 밴 당했거나

  • 활성화되지 않은 사용자이면
    이메일을 보내지 않는다

for (let user of users) {
    if (!(user.isAdmin || user.isBanned || !user.isActive)) {
        sendEmail(user);
    }
}
function isEligibleForEmail(user) {
    return !user.isAdmin && !user.isBanned && user.isActive;
}

for (let user of users) {
    if (isEligibleForEmail(user)) {
        sendEmail(user);
    }
}
  • 이메일을 보내도 되는 사람이면 > 이메일을 보낸다

  • 코드를 읽었을 때 인식하기 쉬워졌다. 

또한, 부정 조건이 꼭 필요하다면 부정 연산자를 사용하기 보다 메서드 자체에 부정한 의미(NOT)를 담는 것이 권장된다.

if (!(order.status !== 'CANCELLED' && !order.isLocked)) {
    logWarning(order);
    return;
}
function isNotProcessable(order) {
    return order.status === 'CANCELLED' || order.isLocked;
}

if (isNotProcessable(order)) {
    logWarning(order);
    return;
}

 

  • isNotProcessable라는 이름으로 의미가 명확

  • 조건 해석이 자연어처럼 읽힘: “처리 불가능하면 경고 로그를 남긴다”

 

 

4. 해피 케이스와 예외 처리

해피케이스 : 프로그램의 진행방향이 개발자가 원하는 방향으로 잘 흘러가는 것

  - 예외 처리를 꼼꼼하는 개발자가 역량이 좋은 개발자로 평가됨

 

예외 처리를 잘하기

- 예외가 발생할 가능성 낮추기

- 어떤 값의 검증이 필요한 부분은 주로 외부 세계와의 접점

  - 사용자 입력, 객체 생성자, 외부 서버의 요청 등

- 의도한 예외와 예상하지 못한 예외를 구분하기

  - 사용자에게 보여줄 예외와, 개발자가 보고 처리해야 할 예외 구분

 


 

위 내용을 참고하여 읽기 좋은 코드로 리팩토링 하기

 사용자가 생성한 '주문'이 유효한지를 검증하는 메서드. 
 Order는 주문 객체이고, 필요하다면 Order에 추가적인 메서드를 만들어도 된다. (Order 내부의 구현을 구체적으로 할 필요는 없다.) 
필요하다면 메서드를 추출할 수 있다.

 

🚨 before

public 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;
}

 

After

public boolean validateOrder(Order order) {
    if (isEmptyItems(order)) {
        log.info("주문 항목이 없습니다.");
        return false;
    }

    if (isInvalidTotalPrice(order)) {
        log.info("올바르지 않은 총 가격입니다.");
        return false;
    }

    if (isMissingCustomerInfo(order)) {
        log.info("사용자 정보가 없습니다.");
        return false;
    }

    return true;
}

private boolean isEmptyItems(Order order) {
    return order.getItems().isEmpty();
}

private boolean isInvalidTotalPrice(Order order) {
    return order.getTotalPrice() <= 0;
}

private boolean isMissingCustomerInfo(Order order) {
    return !order.hasCustomerInfo();
}

댓글을 작성해보세요.

채널톡 아이콘