본문 바로가기

개발공부/원티드 챌린지 정리

9월 백엔드 챌린지 정리 : 클린 코드 (1)

728x90

1. 의미있는 이름

의도를 분명히 밝혀라

- 클래스로 정의하기

 

이름을 정할 때

1) 존재 이유

2) 수행 기능

3) 사용 방법

 

정보를 잘못 전달하지 말 것

1) 약속된 용어에 대한 사용

ex) String accountList = "송혜교, 전지현, 김태희, 고소영"; (X)

-> List를 쓰는 것은 List 타입일 때 사용해야 정보를 올바르게 전달할 수 있다

2) 유사한 이름을 사용하지 않는다

3) 개념 단위로 묶을 때 유사 표기법을 사용하기

4) 구분하기 어려운 대문자/ 소문자 사용 지양

 

의미있게 구분하기 : 각 메서드 뒤에 1, A, B 등으로만 구분하는 것은 좋지 못한 구분이다

 

발음하기 쉬운 이름짓기

 

검색하기 쉬운 이름 사용하기

 

인코딩 피하기

이름에 데이터 타입을 명시하는 것은 이후 형변환 등으로 타입이 변경되어도 변수명은 동일하기 때문에, 혼동을 줄 수 있다

~ 클래스 & 객체 : 명사, 명사구

~ 메서드 : 접근자 - get, 변경자 - set, 조건자 - is

 

한 개념에 한 단어 사용하기 : 클래스마다 유사한 메서드에 fetch, retrieve, get으로 각각 부르면 혼동을 줄 수 있다

 

의미있는 맥락 추가하기 : 고유 변수만으로는 의미가 충분하지 않을 때, 접두어를 사용해 의미를 명확히 한다

+ 불필요한 맥락 제거하기

2. 함수

1) 작게 만들어라

- 블록과 들여쓰기를 통해 클린한 코드 작성

- 함수의 기능은 한 가지여야 한다

- 섹션으로 나눌 수 있는 함수는 기능이 하나 이상이라는 뜻이므로, 나눌 수 없는 단계까지 나눠야 한다

- 하나의 함수 다음에는 추상화 수준이 한 단계씩 낮은 함수가 온다

~ 예를 들어, "DB Table의 데이터를 읽는다." → "DB의 Connection 맺고 객체를 반환한다." → "Connection 객체를 이용해서 SQL문을 실행시켜 데이터를 가져온다."

 

2) Switch

Switch문을 추상 팩토리에 숨겨 클린 코드 작성하기

public abstract class Employee {
	public abstract boolean isPayday();
	public abstract Money calculatePay();
	public abstract void deliverPay();
}

public interface EmployeeFactory {
	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}

@Configure
public class EmployeeFactoryImpl implements EmployeeFactory {
	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
		switch(r.type){
			case COMMISSION:
				return new ComissionedEmployee(r);
			case HOURLY:
				return new HourlyEmployee(r);
			case SALARIED:
				return new SalariedEmployee(r);
		}
	}
}

@Service
public class EmployService{
	private EmployeeFactoryImpl employeeFactory;

	public Money getTotalAmount(EmployeeRecord r){
		Employee e = employeeFactory.makeEmployee(r);
		if(e.isPayday()){
			return e.calculatePay();
		else
			return Money.ZERO;
	}

 

3) 서술적인 이름 사용하기

이름을 보며 유추한 기능이 그대로 동작할 수 있도록 이름 정하기

 

4) 함수, 인수 타입

~ Output(Or Result) 타입

appendFooter(s)

객체 지향적인 코드는 this를 사용하므로 appendFooter(s)보다는 report.appendFooter();가 더 명확한 코드이다

일반적으로 출력 인수는 피해야 한다. 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식이 더 좋다

 

5) 사이드 이펙트 주의하기

 

6) 명령과 조회 분리하기

* 혼용된 코드

@Test
public void getListDirectoryTest() {
	String directoryPath = "jinho/x";
	  try {
			boolean isExisted = Optional.ofNullable(webHdfsService.getListDirectory(directoryPath).execute().body())
				.isPresent();
				log.info("[RESULT] isExisted ::: {}", isExisted);
				if(!isExisted){
					Optional.ofNullable(webHdfsService.makeNewDirectory(directoryPath).execute().body())
						.orElse(new HashMap<String, Boolean>())
						.getOrDefault("boolean", false);
        }
				RespFileStatuses dirList = webHdfsService.getListDirectory(directoryPath).execute().body();
				log.info("[RESULT] GetListDirectory ::: {}", dirList);

    } catch (IOException e) {
			log.error(e.getMessage());
    }
}

* 분리한 코드

@SpringBootTest
@Slf4j
public class HdfsServiceTest {

    @Autowired
    private WebHdfsService webHdfsService;

    @Autowired
    private HdfsService hdfsService;

    @Test
    public void getListDirectoryTest() {
        String directoryPath = "jinho";
        try {
            if(!hdfsService.isExistedDirectory(directoryPath))
                hdfsService.makeDirectory(directoryPath);
            RespFileStatuses dirList = hdfsService.getListDirectory(directoryPath);
            log.info("[RESULT] GetListDirectory ::: {}", dirList);

        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}
@Service
@RequiredArgsConstructor
public class HdfsService {
    private final WebHdfsService webHdfsService;

    public boolean isExistedDirectory(String toPath) throws IOException {
        Optional<RespFileStatuses> response = Optional.ofNullable(webHdfsService.getListDirectory(toPath).execute().body());
        return response.isPresent();
    }

    public boolean makeDirectory(String toPath) throws IOException {
        return Optional.ofNullable(webHdfsService.makeNewDirectory(toPath).execute().body())
            .orElse(new HashMap<String, Boolean>())
            .getOrDefault("boolean", false);
    }

    public RespFileStatuses getListDirectory(String toPath) throws IOException {
        return webHdfsService.getListDirectory(toPath).execute().body();
    }
}

 

7) 오류 코드보다 예외를 사용하기

Enum 클래스 등으로 오류 코드를 정의해 사용하는 경우 비즈니스 로직과 예외처리 로직이 섞일 수 있다.

Try - Catch가 더 명확하다

3. 기존 코드와 스타일 맞추기

4 - 1. 객체와 자료구조

객체의 인스턴스 변수를 정의할 때 항상 private로 하는데, get/setter를 사용하는 경우 그 의미가 퇴색된다

OOP를 지키는 클래스는 get/setter가 아닌 추상 인터페이스를 제공해 사용자가 구현체를 모르는 상태로 기능을 수행할 수 있어야 한다

1) 객체 : 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다

2) 자료 구조 : 자료를 그대로 공개하며 함수를 제공하지 않는다. 자료 구조를 다루는 함수는 외부에 존재한다

* 절차 지향 코드(자료 구조)의 예시

public class Square {
	public Point topLeft;
	public double side;
}

public class Rectangle {
	public Point topLeft;
	public double height;
	public double width;
}

public class Circle {
	public Point center;
	public double radius;
}

public class Geometry {
	public final double PI = 3.141592653589793;
	
	public double area(Obeject shape) throw NoSuchShapeException
	{
		if(shape instanceof Square) {
			Square s = (Square)shape;
			return s.side * s.side;
		}
		else if(shape instanceof Rectangle) {
			Rectangle r = (Rectangle)shape;
			return r.height * r.width;
		}
		else if(shape instanceof Circle) {
			Circle c = (Circle)shape;
			return c.radius * c.radius;
		}
		throw NoSuchShapeException()'
	}
}

- 각 도형 클래스는 자료 구조

- 도형을 다루는 책임은 Geometry 클래스에게 있다

- 도형의 둘레를 구하는 함수를 추가하고 싶은 경우, G. 클래스에 함수를 추가하면 된다. 자료 구조에는 아무 영향이 없다

- 다만, 자료 구조(도형)를 추가하고 싶은 경우에는 G. 클래스 및 그 밖의 자료 구조에 의존하는 모든 클래스를 수정해야 한다

 

* 객체 지향 코드(객체)의 예시

public class Square implements Shape {
	private Point topLeft;
	private double side;

	public double area(){
		return side * side;
	};
}

public class Rectangle implements Shape {
	private Point topLeft;
	private double width;
	private double height;

	public double area(){
		return width * height;
	};
}

- 새 도형을 추가하는 경우 Shape 인터페이스를 상속하기만 하면 된다

- 그러나 새로운 기능을 추가하고 싶은 경우, 인터페이스를 수정하고 이를 상속하는 모든 도형 클래스를 수정해야 한다

~ 종류의 추가는 쉽지만, 기능을 추가하는 것에 보다 많은 자원이 필요하다

 

-> 때에 따라 적절한 방식을 사용해야 한다

4 - 2. 디미터 법칙

모듈은 자신이 조작하는 객체의 내부 구현을 몰라야 한다

* 자료 구조에는 이 법칙이 적용되지 않는다 ~ 자료 구조의 모든 상태 값은 public으로 외부에 공개되어야 하기 때문

~ 객체에게 적절한 책임을 할당하고 적절한 추상화 수준으로 정의한다면, 모듈에서 해당 함수는 불필요하게 여러 함수를 탐색할 필요가 없다

4 - 3. 자료 전달 객체 : DTO

자료 구조체의 전형적인 형태는 public 변수만 있고, 비즈니스 로직이 없는 형태로, DTO라고 한다

728x90