코드를 짜며 클린코드에 대해서 많이 접하게 됩니다.
근데, 실질적으로 클린 코드에 어떤 요소들이 있는지 정확히는 알지 못합니다.
오늘은 망나니개발자님의 블로그를 참고하여 포스트를 정리해보려고 합니다!
각각 클린 코드의 대목들을 하나씩 말씀드리고 그에 대한 설명을 하는 식으로 이어나가겠습니다.
// 두 번째 인자가 무엇인지 파악이 어렵다.
Product product = new Product("사과", 10000);
// 이름을 부여하여 두 번째 인자를 명확하게 파악할 수 있다.
Product product = Product.withPrice("사과", 10000);
객체의 생성자 같은 경우는 두 번째 인자가 무엇을 의미하는지 읽는자로 하여금 파악하기 어려운 경우가 많습니다.
그럴 때에는 위와 같이 정적 팩토리 메서드를 활용하여, 생성자에도 의미를 부여해보는 것도 좋은 선택이라고 생각합니다.
함수는 하나의 역할만을 수행해야 합니다.
그렇지 않음으로써 수반되는 문제점은 함수가 너무 길어지고, 한 가지 작업만을 수행하지 않습니다.
그렇기 때문에, 정책의 수정이 필요하다면, 혹은 추가가 필요하다면, 이는 유지보수의 어려움으로 다가올 것입니다.
이를 해결하기 위해서 망나니개발자님은 아래와 같은 예시를 들었습니다.
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch(e.type) {
case COMMISSIONED:
return calculateCommisionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
위 코드는 타입 확인과 임금을 계산하는 로직을 내포하고 있습니다.
이를 명확하게 구분해내기 위해서 Employee 라는 클래스와 그에서 활용될 수 있는 메서드를 정의할 수 있습니다.
public abstract class Employee {
public abstract int calculatePay();
public abstract void deliverPay();
}
public class EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return HourlyEmployee(r);
case SALARIED:
return SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
위와 같은 형식으로 객체가 타입을 확인하여 그에 맞는 객체를 반환하는 하나의 명확한 역할만을 수행할 수 있도록하여 유지보수의 어려움을 줄여줄 것입니다.
아무래도 명령과 조회를 분리하지 않으면, 사이드 이펙트가 발생할 수도 있다.
사용자가 예측하지 못하는 동작이 일어날 수도 있기 때문이다.
만일, 조회 연산에 삽입 연산까지 일어난다고 생각해보자.
그러면 해당 메서드의 특성을 알고 있지 못하는 사용자가 사용하다가, 사이드 이펙트가 발생하기 쉽고, 또한 알고 있다고 하더라도 굉장히 골치가 아플 것이다.
이 부분은 분기가 많이 발생하지 않게 하기 위함이다.
예외처리를 if 문으로 검사하는 경우 끊임없는 depth 를 경험하게 될 수도 있다.
하지만, 예외는 발생하는 순간, 잡아주지 않으면 애플리케이션이 종료가 되기 때문에 분기 처리를 하지 않아도 되고, 또한 발생하는 예외를 한곳에서 집중적으로 잡아줄 수 있기 때문에 상당히 괜찮은 방법이다.
여러 예외가 발생하게 되면 아래와 같은 코드가 발생하게 될 수도 있다.
ACMEPort port = new ACMEPort(12);
try {
port.open();
} catch (DeviceResponseException e) {
log.error(e.getMessage());
} catch (ATM1212UnlockedException e) {
log.error(e.getMessage());
} catch (GMXError e) {
log.error(e.getMessage());
} finally {
...
}
알아보기 힘들다.
분기도 너무 많이 일어나고
하지만, 아래와 같이 Wrapping 하면 어떻게 될까?
LocalPort port = new LocalPort(12);
try {
port.open();
} catch (PortDeviceFailure e) {
log.error(e.getMessage());
} finally {
...
}
public class LocalPort {
private ACMEPort innerPort;
public LocalPort(int portNumber) {
this.innerPort = new ACMEPort(portNumber);
}
public void open() {
try {
innerPort.open();
} catch (DeviceResponseException e) {
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e) {
throw new PortDeviceFailure(e);
} catch (GMXError e) {
throw new PortDeviceFailure(e);
}
}
}
물론 구현측은 동일하지만, 사용측은 코드가 굉장히 깔끔해지고, 또한 내부 예외가 살짝 변경되더라도 클라이언트에서는 신경쓸 필요가 없어지게 될 것이다.
테스트 코드를 작성하게 되면, 유연성, 유지보수성, 재사용성을 얻을 수 있다.
이는, 하나의 특성으로부터 오는 것이지 않을까 싶다.
바로 안정성
안정성이 증가하게 되기 때문에 코드를 변경하기 쉽고, 틀린 코드를 빠르게 찾을 수 있게 되며, 확실한 코드이니, 다시 사용할 때 거리낌이 없다.
클래스 역시 함수와 마찬가지로 간결하게 작성하는 것이 중요하다고 한다.
함수는 물리적 크기를 측정했다면, 클래스는 몇개의 역할 또는 책임을 갖는지를 척도로 활용한다.
높은 응집도와 낮은 결합도를 갖는게 좋다는 말이 객체지향에서는 많이 나온다.
그 중 응집도는 클래스의 메소드와 변수가 얼마나 의존하여 사용되는지를 나타낸다.
Stack 클래스는 Size() 를 제외한 모든 함수에서 두 인스턴스 변수를 사용하므로 응집도가 아주 높은 경우라고 할 수 있을 것 같다.
public class Stack {
private int topOfStack=0;
private List<Integer> elements = new LinkedList<>();
public int size() {
return topOfStack;
}
public void push(int element) {
topOfStack++;
elements.add(element);
}
public int pop() throws PoppedWhenEmpty{
if(topOfStack == 0) {
throw new PoppedWhenEmpty();
}
int element = elements.get(--topOfStack);
elements.remove(topOfStack);
return element;
}
}
당연하다, 단일 책임 원칙을 지키고, 구현체 보다는 추상체에 의존하여 다형성을 활용하여야지 변경하기 쉬운 클래스를 만들어 낼 수 있다.
디미터의 법칙은 어떤 모듈이 호출하는 객체의 속사정을 몰라, 캡슐화를 지키자는 근간을 가진 법칙이다.
구현 방법은 내부 인스턴스 노출을 최소화하고, 함수를 통해서 원하는 바를 반환하는 것이다.
final String outputDir = FileManager.getInstance().getOptions().getModule().getAbsolutePath();
내부 속사정을 너무 많이 알고있다.
Options options = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
위와 같이 getter 를 건너뛰면서 원하는 인스턴스에 빠르게 접근할 수 있다.
BufferedOutputStream bos = ctxt.createScartchFileStream(classFileName);
위에서 조금 더 캡슐화를 진행해주면 위와 같이 한번 더 감싸줄 수 있다.