youngseo's TECH blog

[Java] 클린코드1 본문

BackEnd

[Java] 클린코드1

jeonyoungseo 2023. 10. 4. 18:14

아래 글은 로버트 마틴의 클린코드 책 1장 ~ 13장 을 읽고 작성한 글입니다.

개요

이 책을 읽게 된 이유는 최근에 내가 너무 '어쨌든 굴러가는 코드'에만 집중해 코드를 작성하려고 해서였다. 구현이 빨리 되어 결과물이 바로 눈에 보이는 게 좋긴 하였으나 너무 성급하게 짠 코드는 정신이 하나도 없었다. 이 책을 읽어보면서 내가 작성한 코드를 리팩토링하기로 했다. 내가 개발한 코드와 서비스를 이어받아 운영할 분들에게 어느 정도 도움이 되고 싶다.


클린 코드란?

깨끗한 코드는

  • ‘보기에 즐거운’ 코드다.
    • 클래스, 함수 , 메서드 등을 최대한 줄인다.
    • 중복이 없다.
  • 세세한 사항까지 꼼꼼하게 처리하는 코드다.
    • 모든 테스트를 통과한다.
  • 한가지에 ‘집중’한다.

의미있는 이름

  • 함축성을 담자. 특히나 for문에 i,j,k 쓰지 말 것!

예를 들어 아래와 같이 시간에 대한 변수를 만들고자 한다.

int d; # no..

int elapsedTimeInDays; #경과된 시간
int daysSinceCreation; #만들어진 후 지난 시간
# 측정하려는 값과 단위를 표현하자!

이번에는 리스트를 만들고자 한다.

List = new ArrayList();
for (int x: theList) {
	if (x[0] == 4) List.add(x)
}

flaggedCells = new ArrayList();
for (int cell: gameBoard) {
	if (x[0] == 4) flaggedCells.add(cell)
}
# 각 개념이 어떤 것을 의미하고 있는지, 그리고 서로 어떤 관계에 있는지가 명확하다.
  • 너 혼자 만든 약어(ex. hypotenuse 빗변 → hp) 쓰지 말 것
  • 서로 흡사한 이름을 사용하지 말 것 (ex. ControllerForSafety 가 있는데 SafeController 만들기)
  • 불용어(문장 내에서 빈번하게 발생하여 의미 부여 어려운 단어) 사용하지 말 것(ex. a, the, a1 a2 aN, Info, Data)
  • 검색하기 쉬운 이름을 사용하라. 즉, 상수를 변수화하라. 7→ const int WORK_DAYS_PER_WEEK = 7;
  • 일관성 있게 사용하자. 한 프로젝트 안에 ‘무언가를 가져오는 메소드’의 시작을 fetch, retrive, get으로 제각각으로 부르면 혼란스럽다. 하나로 통일하자
  • 의미있는 맥락을 추가하라. 같은 이름이더라도 firstName, lastName, cityName, stateName 등 다른 이름들이 있을 땐 접두사를 붙이자.

함수

  • 작게 만들어라! ⭐
  • 더 작게 만들어라! ⭐
  • 함수는 한 가지를 해야 한다. 그 한가지를 잘해야 한다. 그 한가지만을 해야 한다. ⭐
  • 내려가기 규칙 → 위에서 아래로 코드가 술술 읽히도록 하자.
  • 인수를 적게 만들자. 테스트를 할 때에도 인수가 너무 많으면 복잡하다. 인수는 어렵다. 부득이하게 필요하다면 인수를 2개 사용하자(x축 y축). 그리고 3개 이상으로 늘리지 말자. 3개가 필요하면 인자 하나를 객체화하자! makeCircle(double x, double y, double radius) → makeCircle(Point center, double radius)
  • 함수명을 통해 인수의 순서와 의도를 표현하자. assertEquals(expected, actual) → assertExpectedEqualsActual(expected, actual)
  • this를 이용해 출력인수를 피하라! appendFooter(s) → s.appendFooter();
  • 명령과 조회를 분리하라! 즉 함수 하나는 뭔가를 수행하거나 / 답하거나 둘 중 하나만 하도록 하자!
if (set("username", "youngseo")) ... #만약 username 속성이 영서로 되어있다면 seet

if(attributeExists("username")) {
	setAttribute("username", "youngseo");
}
  • 오류코드보다는 Try - Catch문을 쓰자. 그리고 Try - Catch 문은 코드 구조에 혼란을 일으키니 함수로 뽑아내자

주석

좋은 주석이란?

  • 법적인 주석 (ex. Copyright ~~~)
  • 정보를 제공하는 주석 (ex. 복잡하게 쓰여있는 정규표현식이 어떤 형태인지~ → 이런 건 굳이 시간들여서 파악하기 전에 그냥 주석으로 알려준다.)
  • 의도를 설명하는 주석 (ex. 구현을 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명.)
  • 결과를 경고하는 주석 (ex. //여유 시간이 충분하지 않다면 아래 내용은 실행하지 마십시오.)
  • TODO 주석 → 앞으로 할 일. 프로그래머가 필요하다고 여기지만 당장 구현하기 어려운 업무

형식 맞추기

원할한 소통을 장려하는 코드 형식

  • 적절한 행 길이(120자 정도)를 유지하라. 마치 신문 기사처럼! 대다수 짧다!
  • 가로로 길게 X 세로로 길게 O
  • 메서드끼리는 한칸씩 칸을 준다.
  • 변수는 사용하는 위치에 최대한 가까이 선언한다. (수직거리 확보!) 단, 인스턴스 변수는 클래스 맨 처음에 선언
  • 한 함수가 다른 함수를 호출한다면 (A함수 안에서 B 함수 호출) B를 A보다 앞에 두고, 가까이 둔다. B를 읽으면서, 아 곧 선언되겠군! 예측할 수 있음

객체와 자료구조

  • 변수를 private으로 정의하는 이유는 남들이 변수에 의존하지 않게 만들기 위해서이다.
  • interface에서는 자료를 세세하게 공개하기 보다는 추상적인 개념으로 표현하는 것이 더 좋다.
  • 디미터 법칙 → 객체의 내부 구조를 외부로 드러내지 않는 것이 중요하며 이를 준수하기 위해 객체 간 .(dot)를 최대한 쓰지 않도록 하자.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath(); 
#이처럼 타고타고 들어가는 것 막기! 차라리 아래와 같이 구현한다.

Options opts = ctxt.getOptions(); 
File scratchDir = opts.getScratchDir(); 
final String outputDir = scratchDir.getAbsolutePath();
  • 객체에게는 무언가를 하라고 말해야지, 속을 드러내라고 말하면 안된다! 즉
BufferedOutputStream bos = ctxt.createScratchFileStream(classFileName); 
# 이렇게 객체에게 시켜, 내부 구조를 드러낼 필요가 없도록 한다.
  • 자료 전달을 위해서는 객체보다는 DTO를 사용하고, 객체에게는 보여달라하지말고 무언가를 하라고 시키자!

오류처리

  • try-catch-finally 문부터 작성하라
  • 확인된 예외는 유용하지만, OCP를 위반할 수 있음에 유의해 사용하자!
👉 예외처리

[ 확인된 예외(checked exception) ]

잘못된 코드가 아닌 잘못된 상황에서 발생하는 예외
파일 열기와 같이 정확한 코드로 구현했음에도, 외부 환경(파일이 없는 상황 등)에 따라 발생 가능
예외처리를 구현하지 않으면 컴파일 에러 발생 (컴파일 시 확인해서 확인된 예외)
RuntimeException 이외의 예외들
[ 미확인 예외(unchecked exception) ]

런타임 시 잘못 구현된 코드로 인해 발생하는 예외
컴파일 에러가 나지 않지만 적절한 예외처리가 없을 경우 프로그램이 강제 종료
컴파일 시 확인하지 않기 때문에 미확인 예외
RuntimeException에 포함된 예외들 </aside>
예외에 의미를 제공하라. 예외 메시지에 정보를 담는다.
오류를 형편없이 분류해 try문에 여러 개를 나열하지 말고 감싸기 기법을 사용해 한 개의 오류 잡아내기 class를 따로 만들자. (특정 라이브러리를 쓸 때 이런 클래스를 만들어두면 관리가 편함)
null을 반환하지 않도록 주의하라. NullPointerException을 반환할 것 같다면 차라리 Collections.emptyList()나 <Optional> 등을 사용하자. 추가로 인수로 null을 전달하지 않도록 주의하자.

경계

  • 경계 인터페이스는 숨긴다. 어떠한 메서드에서 Map, List 와 같은 자료구조(경계 인터페이스)를 반환하거나 공개 API 인수로 넘겨서 클라이언트에서 해당 인터페이스를 사용하는 경우를 줄이자!
Map<String, Sensor> sensors = new HashMap<Sensor>();
Sensor s = sensors.get(sensorId);

# 위보다는 아래 -> 경계 인터페이스인 Map을 Sensors 안으로 숨긴다.

public class Sensors {
	private Map sensors = new HashMap();

	public Sensor getById(String id){
		return (Sensor) sensors.get(id);
	}
}
  • 경계는 아는 코드와 모르는 코드를 분리할 수 있도록 하자.
  • 깨끗한 경계를 만들자. 경계 인터페이스를 만들 때에는 변경 비용이 커지지 않도록 주의하며 만들자!

단위테스트 TDD

테스트는 이후 변경이 두렵지 않도록 프로그래머를 도와주는 역할을 한다.

  • 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다. ⭐
  • 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다. ⭐
  • 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다. ⭐
  • 실제 코드와 맞먹는 정도 양의 테스트코드는 좋지 않다. 🤮
  • 테스트당 assert 문 개수를 줄여라
    • given-when-then 관계에서 @Before 에 given 코드를 넣고, @Test 부분에 then 코드를 넣어라.
  • 테스트당 개념 하나
    • F I R S T 를 따르라
      • F - 테스트는 빠르게 돌아야 한다. fast
      • I - 각 테스트는 서로 의존하면 안된다. independent
      • R - 테스트는 어떤 환경에서도 반복 가능해야 한다. Repeatable
      • S - 자가 검증하는 boolean 값으로 결과를 내야 한다. Self-Validating
      • T - 테스트는 적시에 작성해야 한다. TDD의 핵심은 실제 코드 구현 직전 테스트 코드 작성이다. Timely

클래스

  • 클래스도 함수처럼 최대한 작게!
  • if, and, or, but 등의 클래스명을 사용하지 않고서 클래스명은 25단어 내외로 한다.
  • 단일 책임 원칙을 생각하며 변경할 이유를 추적해보자! A 함수에 B,C 메소드가 있다. B는 소프트웨어 버전 정보를 추적한다. C 는 버전이 변경될 때마다 코드가 바뀐다. 이러면 Version이라는 클래스를 하나 더 독자적으로 만들어 책임을 전달하자! ’책임’을 분리해보려면, 일단 해당 클래스가 뭘 하는지 써보자! → 만약 Scoreboard가 점수를 작성하며, 점수 보드에 점수를 입력한다. 라고 한다면 이 ‘하며~’ 가 책임이 많아진 걸 의미한다.
  • 응집도를 유지하면 작은 클래스 여럿이 나온다. 즉 몇몇 함수가 몇몇 변수만 사용한다면 독자적인 클래스로 분리해도 된다.
  • 추상화를 이용해 구체적인 사실을 숨긴다.

시스템

시스템은 도시라는 말이 너무 좋다. 도시를 세운다고 하자. 이 도시는 여러 사람들의 모듈화와 추상화가 적절히 일어났기 때문에 잘 돌아간다. 도시를 보며 큰 그림을 그리는 사람과 작은 사항에 집중하는 사람이 있기에 도시가 잘 돌아간다.

  • 관심사의 분리 - 설계할 사람은 설계하고, 운영하는 사람은 운영하자.
  • 초기화 지연 - 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다. 코드로 표현하면 아래와 같다.
public Service getService() {
	if (service == null) 
		service = new MyServiceImpl(...);
	return service;
}

#위 코드의 문제 
# 1) DI 관계 Service-MyServiceImpl 
# 2) 테스트 시 적절한 테스트 전용 Impl 객체를 service 필드에 할당해야 하는 문제

이 문제를 해결하기 위해 DI 컨테이너를 사용한다.  (DI 개념 설명은 여기에서 확인할 수 있다.)

  • 자바 프록시 API, 자바 AOP 프레임워크, AspectJ를 통해 관심사를 분리한다. ( → 이쪽은 아직 잘 이해를 못 함)
  • 애플리케이션 도메인 논리를 (Java 이외의 다른 기술에 얽매이지 않으며, 특정 환경에 종속적이지 다른 않은) POJO 로 작성할 수 있다면, 즉 코드 수준에서 아키텍처 관심사를 분리할 수 있다면 진정한 테스트 주도 아키텍처 구축이 가능하다
👉 POJO 란?

Spring은 POJO 프로그래밍을 지향하는 프레임워크이다.
최대한 다른 환경이나 기술에 종속적이지 않고 객체 지향적인 원리에 충실하자는 것이다. 
Spring 프레임워크에서는 IoC/DI, AOP, PSA를 지원하고 있다.

창발성(떠오름 현상)

설계를 단순하게 하는 방법

  1. 모든 테스트를 실행한다.
  2. 중복을 없앤다. → 템플릿 메소드 패턴
  3. 프로그래머의 의도를 표현한다.
  4. 클래스와 메서드 수를 최소로 줄인다.

위의 것들을 실현하기 위한 가장 근본적인 방법은 ‘노력’이다. 표현력을 높이는 가장 중요한 방법은 노력 뿐이다.


동시성

객체는 처리의 추상화이며 스레드는 일정의 추상화이다. 동시성 개념은 OS에서 자세히 배울 수 있고 여러 개념을 여기에서 확인해볼 수 있다.
동시성은 Decoupling, 즉 결합을 없애는 전략이다. 즉 ‘무엇’과 ‘언제’를 분리하는 작업이다. 웹 요청과 응답 처리를 해주는 자바 API인 서블릿이 좋은 예이다.

  • 동시성 방어 원칙 - 동시성 코드가 일으키는 문제로부터 시스템을 방어하는 원칙이다.
    • SRP 단일 책임 원칙
    • 임계영역을 보호하라. 공유자료를 수정하는 위치를 파악하라.
    • 자료사본을 사용하여, 공유하도록 하지 말고 사본을 사용하도록 하라.
    • 스레드는 가능한 독립적으로 구현하라.
    • 라이브러리를 이해하라. 자바 클래스 중 ConcurrentHashMap이 HashMap보다 다중 스레드 환경에서 안전하다.
  • 동기화하는 부분을 작게 만든다. synchronized 키워드를 사용하면 java에서 락을 설정한다. 여기저기 synchronized를 남발하는 것은 바람직하지 않지만 임계영역은 반드시 보호하자.
  • 올바른 종료 코드는 구현하기 어렵다. 따라서 종료 코드를 개발 초기부터 고민하고 동작하도록 초기부터 구현하자. 생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라.
  • 스레드 코드 테스트를 작성할 때에는 문제를 노출하는 테스트 케이스를 작성하고, 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라. 테스트가 실패하면 원인을 추적하라. 다시 돌려서 됐는데? 이렇게 넘어가면 절대! 안 된다.
  • 보조 코드를 이용해 코드를 흔들(jiggle)어보자. 스레드를 매번 다른 순서로 실행해보며 오류를 드러내는 부분들을 계속 확인한다.