반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 11장을 기반으로 작성된 글입니다.

 

 

 

 


좋은 단위 테스트의 특징을 정리하면 다음과 같다.

  • 코드의 문제를 정확하게 감지한다.
  • 구현 세부 정보에 구애받지 않는다.
  • 실패가 잘 설명된다.
  • 테스트 코드가 이해하기 쉽다.
  • 테스트를 쉽고 빠르게 실행할 수 있다.

이를 위한 몇 가지 방법을 살펴보자.

모든 동작을 시험하라

함수 당 하나의 테스트 케이스만 있으면, 그 함수의 여러 동작을 테스트할 수 없다.

예컨대, 고객이 주택담보대출을 받을 자격이 있는지를 판단하는 함수가 있다고 하자.

  • 아래에 해당하면 기각한다.
    • 신용 등급이 좋지 않으면 거절한다.
    • 이미 대출이 있으면 거절한다.
    • 대출이 금지된 고객이면 거절한다.
  • 신청이 받아들여지면, 로직에 따라 금액을 산정한다.

위의 동작 각각을 모두 테스트해야 한다.

또한, 오류가 발생하는 시나리오, 예컨대 신청 액수가 음수인 경우와 같은 것들도 테스트해야 한다.

테스트만을 위해 퍼블릭으로 만들지 마라

  • 프라이빗 함수 테스트는 우리가 신경 쓰는 행동을 테스트하는 것이 아니다.
  • 구현 세부 사항에 독립적이지 못하다.
    • 리팩터링 후 실패할 수 있다.
  • 다른 개발자가 다른 곳에서 사용할 수 있다. 퍼블릭 API를 변경한 것과 마찬가지이다.

하지만 퍼블릭 함수 하나의 역할이 너무 비대해 테스트를 작성하기 어렵다면,

코드를 더 작은 단위로 분할하는 방법이 있다.

한 번에 하나의 동작만 테스트하라

여러 동작을 동시에 테스트하면,

  • 테스트를 이해하기 어렵고,
  • 실패 메시지가 명확하지 않아진다.

각 동작을 자체 테스트 케이스에서 테스트하면,

테스트 함수명과 로직을 통해 테스트를 이해하기 쉬울 뿐더러,

실패 메시지를 통해 혹은 테스트 함수명을 통해 실패 원인을 쉽게 알 수 있다.

매개변수를 사용한 테스트로, 하나의 동작에 대한 여러 경우를 동시에 테스트할 수도 있다.

자바의 Junit은 @ParameterizedTest로 매개변수 테스트가 가능하다.

공유 설정을 적절하게 사용하라

BeforeAll, BeforeEach, AfterAll, AfterEach 등을 통해 공유 설정을 실행하거나 해체할 수 있다.

BeforeAll 블록에 설정을 추가하는 것을 상태 공유,

BeforeEach 블록에 설정을 추가하는 것을 설정 공유 라고 한다.

상태 공유의 문제

이전에 실행된 테스트가 다른 테스트 케이스에 영향을 줄 수 있다.

따라서, 가능하다면 상태를 공유하지 않는 것이 좋다.

반드시 공유해야 한다면, AfterEach 블록을 통해 계속해서 새롭게 초기화해주는 것도 방법이다.

설정 공유의 문제

공유된 설정이 바뀌면, 사용하는 모든 테스트 케이스에 영향을 준다는 문제가 있다.

따라서 중요한 설정은 각각의 테스트 케이스 내에서 설정하는 것이 좋다.

설정 공유가 적절한 경우

하지만 뚜렷하게 반복되거나 비용이 많이 들어가는 설정은,

공유를 통해 반복과 비용을 줄일 수 있다.

테스트가 효과적이지 못하거나 파악하기 어려울 수 있다는 점을 명심한 채로,

심사숙고 후 사용할 필요가 있다.

적절한 어서션 확인자를 사용하라

assertThat(result.contains("custom_class_1")).isTrue();
assertThat(result.contains("custom_class_2")).isTrue();

테스트 케이스 testGetClassNames_containsCustomClassNames 실패 :
예상한 값은 true이지만 결과는 false입니다.

테스트의 의미는 맞지만 가독성이 좋지않고,

무엇보다 실패 메시지가 명확하지 않다.

asserThat(result).containsAtLeast("custom_class_1", "custom_class_2");

자바에서 지원하는 위와 같은 Assertion 확인자를 사용하면,

테스트 케이스 testGetClassNames_containsCustomClassNames 실패 :
Not true that
	[text-widget, selectable, custom_class_2[는
contains at least
	[custom_class_1, custom_class_2]를 포함하지 않습니다.
-------
빠진 항목: custom_class_1

위처럼 실패 메시지를 통해 실패 이유를 명확하게 알 수 있다.

테스트 용이성을 위해 의존성 주입을 사용하라

의존성 주입을 사용한 코드에서든, 테스트 더블을 사용하기 쉽다.

// 의존성을 하드코딩한 경우
class InvoiceReminder {
	private final AddressBook addressBook;
	private EmainSender emainSender;

	InvoiceReminder() {
		this.addressBook = DataStore.getAddressBook();
		this.emailSender = new EmailSenderImpl();
	}
	...
}

// 의존성을 외부에서 주입받는 경우
class InvoiceReminder {
	private final AddressBook addressBook;
	private EmainSender emainSender;

	InvoiceReminder(AddressBook addressBook, EmailSender emailSender) {
		this.addressBook = addressBook;
		this.emailSender = emailSender;
	}
	
	static InvoiceReminder create() { // 정적 팩토리 함수로, 의존성 주입 없이 생성된 인스턴스 제공
		return new InvoiceReminder(DataStore.getAddressBook(), new EmailSenderImpl());
	}
	...
}

의존성을 외부에서 주입받도록 코드를 작성하면, 아래와 같이 테스트 더블(Fake)을 사용할 수 있다.

InvoiceReminder invoiceReminder = new InvoiceReminder(addressBook, new FakeEmailSender());

테스트에 대한 몇 가지 결론

단위 테스트가 아닌 테스트 종류

  • 통합 테스트(integration test) : 여러 구성 요소, 하위 시스템, 모듈을 서로 연결하는 프로세스를 통합이라고 한다. 통합이 제대로 작동하는지 확인하기 위한 테스트이다.
  • 종단 간 테스트(end-to-end test) : 시작부터 끝까지 전체 소프트웨어 시스템을 통과하는 여정을 테스트한다.
    • 온라인 쇼핑몰 소프트웨어의 예 : 웹 브라우저 자동 구동 → 사용자가 구매를 완료하는 과정 → 구매 확인
  • 회귀 테스트 : 동작, 기능의 잘못된 변경은 없는지 정기적으로 수행하는 테스트
  • 골튼 테스트(특성화 테스트) : 주어진 입력 집합에 대해 코드가 생성한 출력을 스냅샷으로 저장 → 그것을 기반으로 테스트 수행 후, 코드가 생성한 출력이 다르면 테스트 실패
    • 신뢰성이 낮다
  • 퍼즈 테스트 : 무작위 값이나 특수한 값으로 코드를 호출하여, 코드 동작이 멈추지 않는지 점검

단위 테스트 뿐만 아니라, 다양한 테스트 기술, 유형, 수준에 대해 알아보고, 새로운 툴과 기술에 대한 최신 정보를 유지하는 것이 좋다.

반응형
반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 10장을 기반으로 작성된 글입니다.

 

 

 

 


테스트 더블

테스트하는 코드가 다른 것들에 의존하고, 의존성을 실제로 사용하는 것이 불가능하거나 바람직하지 않은 경우가 있다.

이런 경우에, 의존성을 실제로 사용하지 않고 테스트 더블을 사용할 수 있다.

테스트 더블은 테스트에 적합한 형태로 의존성을 시뮬레이션하는 객체이다.

테스트 더블을 사용하는 이유

  • 테스트 단순화 : 의존성 설정, 하위 의존성 설정, 구현 세부 사항과의 결합을 없앤다.
  • 테스트로부터 외부 세계 보호 : 의존하는 것에 대한 부수 효과를 방지한다.
    • 실제 서버 요청, 실제 DB 변경 등을 막는다.
  • 외부로부터 테스트 보호
    • DB의 값을 읽어서 테스트한다 → DB의 값이 바뀔 수 있다!

목(Mock)

목은 클래스나 인터페이스를 시뮬레이션하여, 단순히 멤버 함수에 대한 호출만 기록한다.

특정 함수가 특정 인자를 가지고 호출되었는지만 판단하는 형태로 테스트를 진행할 수 있다.

void testSettleInvoice_accountDebited() {
	BankAccount mockAccount = createMock(BankAccount); // BankAccount의 목 객체 생성
	...
	paymentManager.settleInvoice(mockAccount, invoice);
	
	verifyThat(mockAccount.debit)
			.wasCalledOnce()
			.withArguments(invoiceBalance)

테스트로부터 외부 세계를 보호했다.

하지만, 테스트가 비현실적이고, 중요한 버그를 놓칠 수 있다.

또한, 구현 세부 사항에 의존한다.

리팩터링을 통해 settleInvoice() 함수가 BankAccount의 debit() 함수를 호출하지 않게 될 수도 있다.

스텁을 알아본 후, 그 문제점을 아래에서 더 살펴볼 것이다.

스텁(Stub)

스텁은 함수가 호출되면 미리 정해 높은 값을 반환하는 방식이다.

목과 스텁은 차이가 있지만, 개발자들이 일상적으로 목이라고 말할 때는 둘 다 지칭한다.

그리고 많은 테스트 도구에서 목을 만들어야 스텁할 수 있다.

PaymentManager.settleInvoice() 함수에, 잔액이 부족한지 확인하는 로직이 추가되었다고 가정하자.

그리고 이를 스텁으로 테스트해보자.

void testSettleInvoice_insufficientFundsCorrectResultReturned() {
	MonetaryAmount invoiceBalance = new MonetaryAmount(10.0, Currency.USD);
	Invoice invoice = new Invoice(invoiceBalance, "test-id");
	BankAccount mockAccount = createMock(BankAccount); // BankAccount의 목 객체 생성
	when(mockAccount.getBalance())
			.thenReturn(new MonetaryAmount(9.99, Currency.USD)); // 스텁을 설정하여, mockAccount.getBalance()가 항상 9.99달러를 반환하도록 한다.
	PaymentManager paymentManager = new PaymentManager();
	PaymentResult paymentResult =	paymentManager.settleInvoice(mockAccount, invoice);
	
	assertThat(result.getStatus()).isEqualTo(INSUFFICIENT_FUNDS);
}

스텁을 통해 외부로부터 테스트를 보호했다.

하지만 이 또한 단점이 있다.

아래에서 목과 스텁의 단점을 알아보자.

목과 스텁의 문제

  • 목이나 스텁이 실제 의존성과 다른 방식으로 동작하도록 설정되면 → 잘못된 테스트를 할 수 있다.
  • 구현 세부 사항과 테스트가 밀접하게 결합한다.
  • 잘못된 테스트
    • 목은 단순히 호출 여부만 테스트하여, 잘못되는 경우를 테스트하기 어렵다.
      • 음수 값의 Invoice가 입력되는 경우, BankAccount가 예외를 발생시킬 수 있지만, mockAccount에서는 문제가 되지 않는다.
    • 스텁을 통해 반환 받는 값 또한, 실제 의존성 코드가 반환하는 값인지 검증하지 않는다.
  • 구현 세부 정보와 결합한다.
    • 내부에서 호출되는 함수가 바뀔 수 있다. → 리팩터링이 잘 되었더라도, 테스트가 실패할 수 있다.

따라서,

실제 의존성을 사용하거나 → 페이크를 사용하거나(아래에서 알아보자) → 가능한 대안이 없다면 목이나 스텁을 사용

위와 같은 순서로 생각하는 것이 좋다.

페이크

페이크는 실제 의존성의 공개 API를 테스트 용으로 단순하게 구현한 객체이다.

외부 시스템과 통신하는 대신, 페이크 내의 멤버 변수에 상태를 저장한다.

실제 의존성에 대한 코드를 유지보수하는 팀이, 페이크 코드도 유지보수해야 한다.

예시 코드를 아래에서 알아보자.

class FakeBankAccount implements BankAccount {
	private MonetaryAmount balance;

	FakeBankAccount(MonetaryAmount startingBalance) {
		this.balance = startingBalance;
	}
	
	override void debit(MonetaryAmount amount) {
		if (amount.isNegative()) {
			throw new ArgumentException("액수는 0보다 적을 수 없다.");
		}
		balance = balance.subtract(amount);
	}
	
	...
	
}

위처럼 페이크 내부에 실제 의존성의 논리를 담을 수 있다.

또한, 구현 세부 정보로부터 테스트를 분리할 수 있다.

물론 이 또한, 아래와 같은 문제가 있을 수 있다 (개인 의견)

  • 페이크 객체를 유지보수해야 한다.
  • 페이크 객체 내부의 논리가, 테스트 하는 코드가 의존하는 코드의 논리와 일치해야 한다.

목과 스텁에 대한 추가 의견

  • 목 찬성론자 : ‘런던 학파’라고도 일컬어지며, 단위 테스트에서 의존성 실제 사용을 피해야 한다고 주장한다.
  • 고전주의자 : ‘디트로이트 학파’라고도 일컬어지며, 개발자는 테스트에서 의존성을 실제로 사용하는 것을 최우선으로 해야 한다고 주장한다. 그렇지 못할 때 페이크를 사용하고, 목과 스텁은 최후의 수단으로 사용되어야 한다고 주장한다.

목은 상호작용을 테스트한다. (How)

고전주의 방법은 결과를 테스트한다. (What)

목 사용을 지지하는 다음의 주장도 있다.

  • 단위 테스트가 더욱 격리된다.
    • 문제가 생긴 코드를 의존하는 코드는 실패하지 않는다.
  • 테스트 코드 작성이 쉬워진다.

고전주의적 접근법에 찬성하고, 목에 반대하는 다음의 주장도 있다.

  • 목은 호출을 확인할 뿐, 호출이 유효한지 검증하지 않는다. 코드에 문제가 있어도 테스트는 통과할 수 있다.
  • 고전적인 접근 방법은 구현 세부 사항에 독립적이다.

필자는, 목을 사용하는 주된 이유가 테스트 코드 작성이 쉬워지는 것 뿐이라고 말한다.

테스트 철학

  • 테스트 주도 개발(Test-Driven Development) : TDD는 실제 코드 작성 전 테스트 케이스를 먼저 작성하는 것을 지지한다. 실제 코드는 테스트만 통과하도록 최소한으로 작성하고, 이후 구조 개선 및 중복 제거 등의 리팩터링을 한다.
    • 테스트 케이스를 격리하고, 하나의 동작만 테스트하도록 집중하며, 구현 세부 사항을 테스트하지 않는 등 다른 원칙들을 지지한다.
  • 행동 주도 개발(Behavior-Driven Development) : BDD는 사용자, 고객, 비즈니스 관점에서 소프트웨어의 행동을 식별하는 데 집중한다.
  • 수용 테스트 주도 개발(Acceptance Test-Driven Development) : ATDD는 BDD와 비슷한데, 고객 관점에서 소프트웨어가 보여줘야 하는 동작을 식별하고, 소프트웨어가 전체적으로 필요에 따라 작동하는지 검증하기 위해 자동화된 수락 테스트를 만든다.
    • TDD와 마찬가지로, 실제 코드 구현 전 테스트를 생성해야 한다.
      • 이론적으로 Acceptance test가 모두 통과하면 소프트웨어는 고객이 수락하기에 완전한 것이다.

우리는 하나의 철학만 고수하고, 나머지를 배척할 것이 아니라,

많은 철학을 통해 옳다고 생각하는 바를 신중하게 선택하는 것이 좋다.

우리가 궁극적으로 달성하고자 하는 목표는 고품질의 테스트 코드를 철저하게 작성하고, 고품질의 소프트웨어를 생산하는 것이다.

반응형
반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 10장을 기반으로 작성된 글입니다.

 

 

 

 


단위 테스트의 정의가 명확하지는 않지만, 상대적으로 격리된 방식의 클래스, 함수, 코드 파일을 테스트하는 것을 의미한다.

중요한 것은, 코드를 잘 테스트하고 이 작업을 유지보수할 수 있는 방법으로 수행하는 것이다.

몇 가지 중요한 용어를 짚고 넘어가자.

  • 테스트 중인 코드 : 실제 코드를 의미한다. 테스트의 대상이 된다.
  • 테스트 코드 : 테스트 중인 코드를 테스트하는 코드를 나타낸다. 단위 테스트를 구성한다.
  • 테스트 케이스 : 테스트 코드의 각 파일에는 여러 테스트 케이스가 있을 수 있다. 하나의 동작, 시나리오를 테스트한다.
    • 준비, 실행, 단언의 단계로 나뉘며, Given, When, Then으로 일컬어지기도 한다.
  • 테스트 러너 : 테스트를 실제로 실행하는 도구다.

좋은 단위 테스트를 작성하는 법

  • 훼손의 정확한 감지 : 코드가 훼손되면 테스트가 실패해야 하고, 테스트는 코드가 실제로 훼손된 경우에만 실패해야 한다.
  • 세부 구현 사항에 독립적 : 리팩터링을 해도 테스트의 성공은 유지되어야 한다.
  • 잘 설명되는 실패 : 실패 원인, 문제점을 명확하게 설명해야 한다.
  • 이해할 수 있는 테스트 코드 : 다른 개발자들이 테스트 코드가 무엇을 테스트하고, 어떻게 수행되는지 쉽게 이해할 수 있어야 한다.
    • 테스트 코드는 종종 실제 코드를 이해할 수 있는 문서로서의 역할도 담당한다.
  • 쉽고 빠르게 실행 : 단위 테스트가 느리거나 실행이 어려우면 개발 시간이 낭비된다.

훼손의 정확한 감지

이를 통해 코드에 대한 초기 신뢰를 주고, 미래의 훼손을 막을 수 있다.

세부 구현 사항에 독립적

개발자가 코드베이스에 가할 수 있는 변경을 두 종류가 있다.

  • 기능적 변화 - 외부에서 보이는 동작 수정 O
  • 리팩터링 - 외부에서 보이는 동작 변경 X

기능적 변화가 일어난다면, 일반적으로 테스트도 수정해야 할 것으로 예상된다.

하지만, 리팩터링의 경우 테스트를 수정하지 않아도 되는 것이 바람직하다.

단위 테스트를 작성하는 두 가지 방식을 고려해보자.

A : 테스트는 코드의 모든 동작을 확인하고, 구현 세부사항을 확인한다. Private 멤버 변수 및 의존성을 직접 조작하여 상태를 시뮬레이션 한다.

B : 동작만 테스트할 뿐, 구현 세부 사항은 확인하지 않는다. 코드의 공개 API를 사용하여 상태를 설정하고, 가능한 곳에서 동작을 확인한다. Private 변수, 함수는 조작하거나 검증하지 않는다.

A 방식의 경우, 리팩터링을 올바르게 수행했더라도 테스트가 실패할 확률이 높다.

B 방식의 경우, 리팩터링을 올바르게 수행했다면 테스트가 여전히 성공할 것이다. 그렇지 않다면 리팩터링 과정이 잘못되지 않았는지 생각해볼 수 있다.

따라서 우리는, B 방식으로 동작만 테스트해야 한다.

세부 구현 사항을 테스트해서는 안 된다.

여기서 얻을 수 있는 또 하나의 인사이트는,

기능 변경과 리팩터링을 분리해야 한다는 것이다.

기능 변경을 동작을 변경하여 테스트 결과가 달라질 수 있기 때문이다.

잘 설명되는 실패

  • 테스트 케이스가 한 가지 사항만 검사한다.
  • 테스트 케이스에 대해 서술적인 이름을 사용한다.
  • 테스트 실패 시 무엇이 잘못되었는지 명확하게 설명한다.
// 이해하기 어려운 실패
Test case testGetEvents failed: // 테스트 메서드명이 이해하기 어렵다.
Expected: [Event@ea4a92b, Event@3c5a99da] // 참조 변수의 주소값 -> 실패 이유를 알기 어렵다.
But was actually: [Event@3c5a99da, Event@ea4a92b]

// 이해하기 쉬운 실패
Test case testGetEvents_inChronologicalOrder failed: // 테스트 메서드명이 이해하기 쉽다.
Expected: [<Spaceflight, April 12, 1961>, <Moon Landing, July 20, 1969>]
But was actually: [<Moon Landing, July 20, 1969>, <Spaceflight, April 12, 1961>] // 순서가 잘못되었음을 명확하게 알 수 있다.

이해 가능한 테스트 코드

한 번에 너무 많은 것을 테스트하거나, 너무 많은 공유 테스트 설정을 하면 이해하기 어렵다.

테스트가 어렵다는 것은, 수정이 어려어진다는 것이다.

수정의 결과가 안전한지 이해하기 어렵기 때문이다.

일부 개발자들은, 테스트를 코드에 대한 사용 설명서로 사용한다.

코드의 사용법이나, 코드가 어떤 기능을 제공하는지 단위 테스트로 알아보는 것도 좋은 방법이다.

그렇기 때문에, 테스트 코드는 더욱이 이해하기 쉽게 작성되어야 한다.

쉽고 빠른 실행

테스트가 쉽고 빠르게 실행되어야 개발자들이 실제로 테스트할 수 있는 기회를 극대화할 수 있다.

퍼블릭 API에 집중하되, 중요한 동작은 무시하지 말라

일반적으로는 퍼블릭 API만을 사용하여 테스트해야 하고,

구현 세부 사항에 의존하면 안 된다.

하지만 테스트 대상 코드가 다른 코드에 의존할 수 있다. (굉장히 많은 경우 그렇다.)

의존하는 코드로부터 외부 입력이 제공되거나, 테스트 대상 코드가 의존하는 코드에 부수효과를 일으킨다면, 테스트의 의미가 달라질 수 있다.

아래의 경우를 생각해보자.

  • 서버와 상호작용하는 코드
    • 서버로부터 필요한 값을 받을 수 있도록 서버를 설정하거나 시뮬레이션해야 한다.
    • 서버를 얼마나 자주 호출하는지, 요청이 유효한지 등 서버에 가하는 부수효과를 확인해야 할 수도 있다.
  • 데이터베이스에 값을 저장하거나 읽는 코드
    • DB에 저장된 여러 값으로 코드를 테스트하거나, 부수 효과로 DB에 어떤 값을 저장하는지 확인해야 할 수도 있다.

이런 경우, 퍼블릭 API만 테스트해서는 본질적인 것을 테스트할 수 없다.

퍼블릭 API만 사용하고, 구현 세부 사항을 의존하지 말라는 것은 훌륭한 조언이지만,

궁극적으로 중요한 것은 코드의 모든 중요한 동작을 제대로 테스트하는 것이다.

최대한 퍼블릭 API만으로 테스트하고, 대안이 없는 경우 퍼블릭 API를 벗어나 테스트해야 한다.

 

반응형
반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 9장을 기반으로 작성된 글입니다.

 

 

 

 


간결한 추상화 계층, 모듈화를 통해 재사용성과 일반화성이 높은 코드를 작성할 수 있다.

그리고 추가적으로 재사용성과 일반화성이 높은 코드를 작성하는 방법이 있다.

아래에서 그 방법을 알아본다.

가정을 주의하라

코드 작성 시, 어떤 가정을 통해 코드를 단순하고 효율적으로 작성할 수 있다.

하지만 이는 버그의 시작이 될 수 있다.

class Article {
	private List<Section> sections;
	...
	
	List<Image> getAllImages() {
		for (Section section : sections) {
			if (section.containsImages()) {
				// 기사 내에 이미지를 포함하는 섹션은 최대 하나만 있다.
				return section.getImages();
			}
		}
		return [];
	}
}

위의 코드처럼, 기사 내에 이미지를 포함하는 섹션이 최대 하나만 있다는 가정을 기반으로 코드를 작성한다면,

이미지 포함 섹션이 여러 개인 다른 상황에서 재사용이 불가능해진다.

섣부른 최적화

코드 최적화는 일반적으로 비용이 든다. 최적화된 해결책을 구현하는 데 시간이 필요하며, 그 결과로 코드의 가독성이 떨어지고 유지보수성이 떨어질 수 있다.

특정 가정을 기반으로 코드가 작성된다면 견고함이 떨어질 수도 있다.

게다가 최적화는 보통 수천에서 수백만 번 실행되는 코드에 대해 이루어질 때 상당한 이점ㅇ미 있다.

따라서, 대부분의 경우에는 큰 효과가 없는 코드 최적화를 하느라 애쓰기보다는

코드의 가독성, 유지보수성, 견고함에 집중하는 것이 좋다.

코드의 어떤 부분이 굉장히 많이 실행되고,

그 부분을 최적화하는 것이 성능 향상에 큰 효과를 볼 수 있다는 것이 명백할 때라야 최적화 작업을 해도 무방하다.

가정의 강제적 확인

특정 가정을 통해 코드가 엄청나게 간결해진다면,

해당 가정이 맞지 않을 경우 빠르게 실패하는 코드를 작성할 수 있다.

분기 또는 체크를 통해 예외를 발생시킬 수 있겠다.

전역 상태를 주의하라

전역 상태는 재사용성이 낮다.

static 필드의 사용을 지양하고, 인스턴스 간 의존성 주입을 통해 문제를 해결해야 한다.

기본 반환값을 적절하게 사용하라

기본값을 사용자 친화적 소프트웨어를 위한 좋은 방법이지만,

너무 낮은 층위에서 남발하는 경우 적절하게 사용하기 어렵다.

낮은 층위의 기본 반환값

낮은 층위의 코드가 기본 반환값을 가질 경우, 그것을 사용하는 많은 상위 코드들에서 영향을 받는다.

상위 코드의 모든 상황에서 같은 기본 반환값을 원할 확률은 낮다.

따라서, 상위 코드에서 원하는 기본 반환값을 직접 제공하는 것이 올바르다.

필요한 매개변수만 받아라

class TextOptions {
	private final Font font;
	private final Double fontSize;
	private final Double lineHeight;
	private final Color textColor;

	TextOptions(Font font, Double fontSize, Double lineHeight, Color textColor) {
		this.font = font;
		this.fontSize = fontSize;
		this.lineHeight = lineHeight;
		this.textColor = textColor;
	}
	
	Font getFont() { return font; }
	Double getFontSize() {	return fontSize; }
	Double getLineHeight() {	return lineHeight; }
	Color getTextColor() {	return textColor; }

위와 같이 정의된 TextOptions을 사용하는 클래스 TextBox가 있다. (아래 코드)

class TextBox {
	private final Element textContainer;
	...
	
	void setTextStyle(TextOptions options) {
		setFont(...);
		setFontSize(...);
		setLineHeight(...);
		setTextColor(options);
	}
	
	void setTextColor(TextOptions options) {
		textContainer.setStyleProperty(
				"color", options.getTextColor().asHexRgb());
	}
}

setTextColor 메서드는 TextOptions를 매개변수로 받지만, textColor 필드만 사용한다.

지금 같은 코드에서는 문제가 없지만,

setTextColor 메서드를 재사용하려고 할 때, TextOptions를 입력해야 함은 적잖이 당황스러울 것이다.

이럴 경우, 아래와 같이 필요한 것만 매개변수로 받도록 수정이 필요하다.

void setTextStyle(TextOptions options) {
	setFont(...);
	setFontSize(...);
	setLineHeight(...);
	setTextColor(options.getTextColor());
}

void setTextColor(Color color) {
	textContainer.setStyleProperty("color", color.asHexRgb());
}

이제 setTextColor를 재사용할 때, 필요한 Color만 넣어주면 되어 재사용성이 크게 증가했다.

제네릭의 사용을 고려하라

타입에 종속된 기능, 로직이 없는 일반적인 코드의 경우,

제네릭을 사용하면 일반화성을 크게 증가시킬 수 있다.

반응형
반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 8장을 기반으로 작성된 글입니다.

 

 

 


요구 사항이 어떻게 바뀔지는 예측할 수 없어도,

어떤 식으로든 바뀐다는 점은 확신할 수 있다. (대부분의 경우)

모듈화는 어떤 상황에서도 변경과 재구성이 용이한 코드를 작성하기 위해 중요하다.

요구사항이 변경되면, 관련 부분만 수정하면 되도록 말이다.

이는 간결한 추상화 계층을 기반으로 한다.

모듈화가 잘 이루어지면 적응성이 뛰어날 뿐만 아니라 시스템에 대한 추론도 쉬워진다.

재사용과 테스트에도 더 적합해진다.

의존성 주입

필드에 있는 인스턴스를 직접 생성하지 말고, 생성자 호출 인자 등을 통해 외부로부터 주입 받는다.

class RoutePlanner {
	private final RoadMap roadMap;

	RoutePlanner(RoadMap roadMap) {
		this.roadMap = roadMap;
	}
}

필요한 경우에는, 팩토리 함수를 통해 생성할 수도 있다.

static RoutePlanner createEuropeRoutePlanner() {
	return new RoutePlanner(newEuropeRoadMap());
}

의존성 주입을 사용할 수 있다는 가능성을 의식적으로 고려하는 것이 좋다.

특히, static 함수에 과도하게 의존하는 정적 매달림은 좋지 않다.

주요 이유 중 하나는 테스트 더블을 사용할 수 없다는 것이다.

인터페이스에 의존하라

구체적인 구현 클래스에 의존하면 적응성이 제한된다.

interface RoadMap {
	List<Road> getRoads
	List<Junction> getJunctions();
}

class NorthAmericanRoadMap implements RoadMap {
	...
}

class RoutePlanner {
	private final RoadMap roadMap; // 인터페이스에 의존
	// private final NorthAmericaRoadMap roadmap // 구체 클래스에 의존

	RoutePlanner(RoadMap roadMap) {
		this.roadMap = roadMap;
	}
}

이를 통해 보다 더 간결한 추상화 계층, 더 나은 모듈화를 달성할 수 있다.

구체적인 것 말고 추상적인 것에 의존하라는 것은 SOLID 원칙 중 D에 해당하는 의존성 역전 원리(Dependency Inversion Principle)의 핵심이다.

클래스 상속을 주의하라

상속은 반드시 is-a 관계일 때만 사용해야 한다.

하지만 is-a 관계라고 무조건 안전한 것은 아니므로, 될 수 있다면 다른 방법을 사용해야 한다.

아래와 같은 문제가 있기 때문이다.

  • 추상화 계층에 방해가 된다.
    • Car, Aircraft가 있을 때, FlyingCar가 개발되었다면 무엇을 상속해야 할지 정하기 어렵다.
      • 많은 언어가 다중 상속을 지원하지 않는다.
      • 다중 상속을 지원하면, 다이아몬드 문제가 발생한다.
  • 적응성 높은 코드 작성이 어려워진다.
  • 슈퍼클래스가 수정된 후, 서브클래스의 동작이 안전한지 판단하기 어렵다.

따라서 구성(조합) 즉, Composition을 사용해야 한다.

클래스는 자신의 기능에만 집중해야 한다

다른 클래스의 지식을 필요로 하거나, 그것을 활용해 수정을 가하면 코드 모듈화가 깨진다.

클래스는 서로에 대한 어느 정도의 지식을 필요로 할 때도 있지만, 가능한 한 이것을 최소화해야 한다.

관련 있는 데이터를 함께 캡슐화하라

관련 있는 데이터가 캡슐화되지 않고 흩어져 있다면, 그것을 한 데 모아 사용하기까지 클래스끼리 서로의 구현을 지나치게 많이 알아야 한다.

함께 움직여야 하는 데이터라면, 하나의 객체로 캡슐화하여 다루는 것이 좋다.

반환 타입, 예외에 구현 세부 정보가 유출되지 않도록 하라

반환 타입 또는 그것의 필드가, 상위에서는 몰라도 되는 많은 정보를 담고 있을 수 있다.

class ProfilePictureService {
	private final HttpFetcher httpFetcher;
	...
	
	ProfilePictureResult getProfilePicture(Int64 userId) { ... }
}

class ProfilePictureResult {
	...
	
	HttpResponse.Status getStatus() { ... }
	
	HttpResponse.Payload? getImageData() { ...}
}

ProfilePictureService의 getProfilePicture() 메서드는 ProfilePictureResult를 반환하는데,

ProfilePictureResult는 게터 함수를 통해 HTTP 관련 객체를 반환한다.

이는 내부 구현이 HTTP와 관련되어 있음을 불필요하게 밝히는 것이다.

HTTP가 아닌 Web socket을 이용한 구현이 추가로 이루어져, 의존성 주입을 통해 로직이 변경될 수도 있다.

이런 경우에는 추상화 계층에 적합한 새로운 타입을 생성할 수 있다.

enum Status {
	SUCCESS,
	USER_DOES_NOT_EXIST,
	OTHER_ERROR;
}

...

List<Byte>? getImageData() { ... }

예외의 경우도 마찬가지이다.

예외의 이름이 하위 객체와 관련되게 지어졌고, 비검사 예외라서 상위에서 그것이 해결되어야 한다면,

다른 구현을 사용할 때 예외 처리문이 문제가 될 수 있다.

추상화 계층에 적절한 예외를 만들어서 사용할 수도 있고,

표준 예외 타입을 사용할 수도 있다.

반응형
반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 7장을 기반으로 작성된 글입니다.

 

 

 

 


많은 사람들이 함께 작성하는 거대한 코드가 올바르게 작동하려면, 오용하기 어렵게 만들어야 한다.

불변 객체로 만드는 것을 고려하라

가변 객체는 추론이 어렵고, 멀티 스레드 환경에서 문제가 발생할 수 있다.

(불변 객체) 객체 생성 시에만 값을 할당

setter 함수를 없애고, 생성자에서만 값을 할당한다.

class TextOptions {
    private final Font font;
    private final Double fontSize;

    TextOptions(Font font, Double fontSize) {
        this.font = font;
        this.fontSize = fontSize;
    }
    ...
}

(불변 객체) 빌더 패턴 - 하나의 클래스를 아래와 같이 두 개의 클래스로 분리한다.

  • 값을 하나씩 설정할 수 있는 빌더 클래스
  • 빌더에 의해 작성된 불변 읽기 전용 클래스
// 읽기 전용 클래스
class TextOptions {
    private final Font font;
    private final Double? fontSize;

    TextOptions(Font font, Double? fontSize) {
        this.font = font;
        this.fontSize = fontSize;
    }

    Font getFont() {
        return font;
    }

    Double? getFontSize() {
        return fontSize;
    }
}

// 빌더 클래스
class TestOptionBuilder {
    private final Font font; // 필수 필드
    private Double? fontSize; // 선택 필드

    TextOptionBuilder(Font font) { // 필수 값만 받는다
        this.font = font;
    }

    TextOptionsBuilder setFontSize(Double fontSize) { // 세터로 필수적이지 않은 값을 받는다.
        this.fontSize = fontSize;
        return this; // 체이닝이 가능하도록 this(현재 인스턴스)를 반환한다.
    }

    TextOptions build() { // 모든 값이 정해지고 나면, build 메서드로 TextOption 객체를 반환한다.
        return new TextOptions(font, fontSize);
    }
}

호출하는 쪽의 사용법은 아래와 같다.

TextOptions getDefaultTextOptions() {
    return new TextOptionsBuilder(Font.ARIAL)
            .setFontSize(12.0)
            .build();
}

TextOptions getDefaultTextOptionsWithoutFontSize() {
    return new TextOptionsBuilder(Font.ARIAL)
            .build();
}

필드의 일부가 선택 사항일 때, 본 객체가 가변적일 위험이 있다.

빌더 패턴은 그럴 때 본 객체를 불변으로 유지하면서, 빌더 객체로 값을 설정할 수 있다.

(불변 객체) 복사 패턴

기존의 객체 값을 유지하면서, 일부 필드만 새로 설정하고 싶을 때가 있다.

이럴 때는, 필드를 새로 쓰기 할 때에 복사를 하는 패턴을 사용할 수 있다.

class TextOptions {
    private final Font font;
    private final Double? fontSize;

    TextOptions(Font font, Double? fontSize) {
        this.font = font;
        this.fontSize = fontSize;
    }

    Font getFont() {
        return font;
    }

    Double? getFontSize() {
        return fontSize;
    }

    TextOptions withFont(Font newFont) {
        return new TextOptions(newFont, this.fontSize);
    }

    TextOptions withFontSize(Double newFontSize) {
        return new TextOptions(this.font, newFontSize);
    }
}

깊은 수준까지 불변으로 만드는 것을 고려하라

객체의 필드가 참조 타입인 경우, 객체는 불변이어도, 참조 타입인 필드는 가변일 수 있다.

이 또한 문제가 될 수 있다는 것은 너무 자명하다.

TextOptions의 필드 font가 fontFamliy로 바뀐 아래의 코드를 가지고 알아보자.

class TextOptions {
    private final List<Font> fontFamliy;
    private final Double fontSize;

    TextOptions(List<Font> fontFamliy;, Double fontSize) {
        this.fontFamliy = fontFamliy;
        this.fontSize = fontSize;
    }

    List<Font> getFontFamliy() {
        return fontFamliy;
    }

    Double getFontSize() {
        return fontSize;
    }
}

해결책 : 방어적으로 복사한다

방어적으로 복사한다는 것은, 원본과의 연결을 끊는다는 것을 의미한다.

더 쉽게 말하면, 원본에 변경이 생겨도 복사본에 변경이 생기지 않도록 복사하는 것이다.

    List<Font> getFontFamliy() {
        return List.copyOf(fontFamliy);
    }

List.copyOf() 메서드는, 방어적으로 복사하면서 복사본의 변경이 불가능하게 만든다.

이는 좋은 방식이지만, 아래와 같은 문제가 있다.

  • 복사 비용이 든다
  • 컬렉션 내부에 참조형 객체가 있는 경우 → 또다시 가변 문제가 발생한다.
    • 이것까지 고려한 복사를 깊은 복사라고 한다.

해결책 : 불변적 자료구조를 사용한다

자바에서는 Guava 라이브러리의 ImmutableList 클래스를 사용할 수 있다.

하지만 자바 9 이상부터는 List.of() 메서드, 자바 10 이상부터는 List.copyOf() 메서드를 통해 불변 리스트를 생성할 수 있다.

Set, Map도 마찬가지이다.

이렇게 생성한 객체들은, 방어적으로 복사하지 않아도 수정이 불가능해서 안전하다.

지나치게 일반적인 데이터 유형을 피하라

위도, 경도를 나타내는 좌표를 List로 표현할 수 있다.

하지만 이는 타입만으로 의미를 추론할 수 없고, 인덱스에 의존한다.

문서를 통한 추가 설명에 지나치게 의존해야 한다.

이럴 때는, 좌표를 표현하는 전용 타입을 만들어줄 수 있다.

class LatLong {
    private final Double latibude;
    private final Double longitude;
    ...
}

시간 처리

시간을 정수로 나타내는 것은 문제가 될 수 있다.

시간은 순간(시각)을 의미할 때도, 시간의 양을 의미할 때도 있다.

이것을 모두 정수로만 표현하면 의미가 혼동되기 쉽다.

그리고 단위를 오고갈 때도 많은 문제가 생긴다.

해결책 : 적절한 자료구조를 사용하라

자바에서는 시간의 양을 Duration, 시각을 LocalDateTime 클래스로 표현할 수 있다.

Duration을 사용하면 단위 문제도 해결이 되는데, 단위를 변환하는 예시 코드는 아래와 같다.

Duration duration1 = Duration.ofSeconds(5);
print(duration1.toMillis()); // 출력 : 5000

Duration duration2 = Duration.ofMinutes(2);
print(duration1.toMillis()); // 출력 : 120000

단위가 Duration 타입 내부로 캡슐화되어, 외부에서 그것을 설정하거나 고려할 필요가 없다.

사용할 때만 원하는 단위를 사용하면 된다.

데이터에 대해 진실의 원천을 하나만 가져야 한다

다른 필드로 표현할 수 있는 값은, 새로운 필드로 가지고 있으면 안 된다.

대변, 차변을 필드로 갖는 계좌 객체가, 잔액을 새로운 필드로 가지면,

대변, 차변이 업데이트될 때 잔액이 반드시 업데이트되어야 한다.

그렇지 않은 경우 잘못된 값을 전달할 수 있다.

// 차액 데이터에 대한 원천이 2개인 경우
class UserAccount {
    private final Double credit;
    private final Double debit;
    private final Double balance;
    ...
}

// 차액 데이터에 대한 원천이 1개인 경우
class UserAccount {
    private final Double credit;
    private final Double debit;
    ...
    Double getBalance() {
        return credit - debit;
    }
}

데이터 계산에 비용이 많이 드는 경우?

지연 연산 및 캐싱으로 해결할 수 있다.

class UserAccount {
    private final List<Transaction> transactions;

    private Double? cachedCredit;
    private Double? cachedDebit;

    UserAccount(ImmutableList<Transaction> transactions) {
        this.transactions = transactions;
    }

    ...

    Double getCredit() {
        if (cachedCredit == null) { // 호출되었을 때(lazy), 값이 빈 경우에만 연산한다.
        cachedCredit = transactions
                .map(transaction -> transaction.getCredit())
                .sum();
        }
        return cachedCredit;
    }
}

지연(lazy) 연산은, 미리 연산하지 않고 필요한 경우에 연산을 수행한다는 뜻이다.

논리에 대한 진실의 원천을 하나만 가져야 한다.

정수 리스트를 파일로 저장하는 클래스,

파일에서 정수 리스트를 불러오는 클래스,

이렇게 2개의 클래스를 생각해보자.

파일로 저장하는 논리(로직)와, 파일에서 정수 리스트를 불러오는 로직은 일치해야 한다.

하지만 다음의 코드는 그렇지 못하다.

// 정수 리스트 저장 (직렬화)
class DataLogger {
    private final List<Int> loggedValues;
    ...

    saveValues(FileHandler file) {
        String serializedValues = loggedValues
                .map(value -> value.toString(Radix.BASE_10))
                .join(",");
        file.write(serializedValues);
    }
}

// 정수 리스트 불러오기 (역직렬화)
class DataLoader {
    ...

    List<Int> loadValues(FileHandler file) {
        return file.readAsString()
                .split(",")
                .map(str -> Int.parse(str, Radix.BASE_10));
    }
}

DataLogger, DataLoader는 모두 쉼표로 데이터를 구분한다는 것과, 문자열을 십진수 정수로 변환했다는 로직을 가지고 이싿.

하지만, 한 쪽의 논리만 바뀌는 경우 두 클래스는 호환되지 않는다.

해결책 : 로직을 하나로 통합한다

class IntListFormat {
    private const String DELIMITER = ",";
    private const Radix RADIX = Radix.Base_10;

    Strint serialize(List<Int> values) {
        return values
                .map(value -> value.toString(RADIX))
                .join(DELIMITER);
    }

    List<Int> deserialize(String serialized) {
        return serialized
                .split(DELIMITER)
                .map(str -> Int.parse(str, RADIX));
    }
}

위처럼, 로직을 하나의 클래스가 관리해서

하나가 변경되더라도 문제가 없도록 만들 수 있다.

반응형
반응형

 

* 본 아티클은, '좋은 코드 나쁜 코드' 6장을 기반으로 작성된 글입니다.

 

 

 

 


매직값을 반환하지 않는다

-1을 반환하는 등, 매직값을 반환하는 것은 함수 호출하는 위치에서 예측이 불가능하다.

null, Optional을 반환하거나, 오류를 발생시키는 것으로 해결할 수 있다.

Null object pattern을 적절히 사용하라

컬렉션을 반환할 때, 비어 있음을 나타내려고 빈 컬렉션을 반환할 수 있다.

이 또한 Null object pattern의 예시이다.

하지만 빈 문자열은 의미를 갖고 있다고 볼 수 있어 지양해야 한다.

Null의 의미를 갖는 새로운 객체를 구현할 수 있다.

하지만 예상을 벗어나는 동작을 할 수 있어 조심해야 한다.

Null 안정성을 지원하는 언어를 사용하거나, Optional을 사용할 수 있게 되면서

Null object pattern의 사용을 지지하는 많은 주장이 힘을 잃고 있다.

예상치 못한 부수 효과를 피하라

부수 효과는 함수의 호출이 외부에 초래한 상태 변화를 의미한다.

  • 사용자 출력
  • 파일, DB 저장
  • 다른 시스템 호출
  • 캐시 업데이트 등

함수의 이름 등을 통해, 동작을 충분히 설명해서 부수 효과를 방지해야 한다.

그 전에 부수 효과를 일으키는 동작을 제거할 수 있다면 가장 좋다.

입력 매개변수를 수정하는 것에 주의하라

인자로 받은 객체를 수정하는 것은 버그를 초래할 수 있다.

해당 객체가 다른 곳에서 재사용될 수 있고, 변화가 있었다고 예측하기 어렵기 때문이다.

이는 새 자료구조에 복사해서 변경함으로써 해결할 수 있다.

오해를 일으키는 함수를 작성하지 마라

중요한 입력을 필수 항목으로 만들어(null일 수 없게),

값을 사용할 수 없는 경우 함수를 호출할 수 없도록 할 수 있다.

미래를 대비한 열거형 처리

열거형 추가를 고려하지 않고, 암묵적으로 처리하는 것은 문제가 될 수 있다.

예컨대, A, B에 대해서는 true를, 나머지에 대해서는 false를 반환하는 식이다.

이 경우, true를 반환해야 하는 값이 열거형에 추가되는 경우,

true를 반환하는 코드를 추가하지 않아도 개발자가 알 수 없다.

코드에 추가되지 않은 열거형에 대해서는 예외를 발생시킬 수 있다.

또한 스위치 문을 사용할 수도 있다. (이때, 기본 케이스 사용은 지양해야 한다.)

Boolean isOutcomeSafe(PredictedOutcome prediction) {
    switch (prediction)
    case COMPANY_WILL_GO_BUST:
    case WORLD_WILL_END:
        return false;
    case COMPANY_WILL_MAKE_A_PROFIT:
        return true;
    }
    throw new UncheckedException("Unhandled prediction: " + prediction);
}

테스트로 해결할 수는 없나?

테스트는 별개로 잘 작성되어야 하지만,

테스트만으로 예측 가능한 코드를 작성하고, 반대의 경우 발생하는 문제를 모두 해결할 수는 없다.

테스트 또한 완벽할 수 없기 때문이다. (놓칠 수 있고, 완벽한 테스트 작성이 불가능할 수 있고(멀티 스레딩 등), mocking 등을 잘못 작성할 수 있다.)

반응형
반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 5장을 기반으로 작성된 글입니다.

 

 

 

 


 

코드의 가독성이 좋지 않으면,

다른 개발자들이 코드를 이해하는 데에 시간을 들여야 하고,

버그를 유발할 수 있는 오해를 만들고,

수정이 어려워진다. (수정 이후 제대로 동작하지 않을 수도 있다.)

 

 

서술형 명칭 사용

클래스명, 변수명, 함수명(메서드명) 등은 의미를 가지고 있어야 한다.

그리고 이것은 주석문으로 대체될 수 없다.

// 1. 서술적이지 않은 이름
class T {
    Set<String> pns = new Set();
    ...
}

// 2. 서술적이지 않지만, 주석문으로 설명한 코드
/** 팀을 나타낸다. */
class T {
    Set<String> pns = new Set(); // 팀에 속한 선수의 이름
    ...
}

// 3. 서술적인 이름
class Team {
    Set<String> playerNames = new Set();
    ...
}

주석문의 적절한 사용

  • 코드가 무엇을 하는지 설명
  • 코드가 왜 그 일을 하는지 설명

무엇을 하는지, 왜 그 일을 하는지 등을 요약하는 높은 수준에서의 주석문은 유용하다.

코드만으로 알 수 있는 내용을 설명하는 것은 부적절하다.

주석보다 가독성 높은 코드를 항상 우선시해야 한다.

코드 줄 수, 일관성

간결함은 중요하지만, 정확함이나 가독성이 항상 우선시되어야 한다.

코딩 스타일은 일관적이어야 한다.

중첩을 줄여라

중첩이 깊어지면 가독성이 낮아진다.

중첩된 블록에 반환문이 있다면 조기 반환으로 중첩을 쉽게 낮출 수 있다.

하지만 중첩된 블록에 반환문이 없다면, 그것은 대개 함수가 너무 많은 일을 하고 있다는 신호다.

함수를 분리함으로써 해결할 수 있다.

매개변수의 가독성

sendMessage("hello", 1, true); // 1, true가 무엇을 의미하는지 알기 어렵다.

void sendMessage(String message, int priority, boolean allowRetry) { ... } // 함수의 정의를 확인해야 알 수 있다.

위처럼 함수 정의를 확인해야만 인자의 의미를 알 수 있는 경우가 있다.

이는 명명된 매개변수를 사용함으로 해결할 수 있는데, 모든 언어가 그것을 지원하는 것은 아니다.

그렇다면 서술적 유형, 즉 설명된 이름을 가지고 있는 타입을 사용함으로 그것을 해결할 수 있다.

아래 코드와 같다.

class MessagePriority {
    ...
    MessagePriority(int priority) { ... }
    ...
}

enum RetryPolicy {
    ALLOW,
    DISALLOW;
}

void sendMessage(String message, MessagePriority priority, RetryPolicy retryPolicy) { ... }

IDE에서 인자의 타입을 보여주기도 하지만, IDE에 의존할 수 없는 경우도 고려해야 한다.

설명되지 않은 값을 사용하지 마라

매직 넘버 사용을 지양해야 한다.

  1. 값을 이름이 있는 상수로 바꿀 수도 있고,
  2. 값을 반환하는 공급자 함수를 사용할 수도 있고,
  3. 값을 변환하는 헬퍼 함수를 사용할 수도 있다.
Double getKineticEnergyJ() {
    return 0.5 *
        getMassUsTon() * 907.1847 // Problem : 의미를 알 수 없는 상수
        ...
}

private const Double KILOGRAMS_PER_US_TON = 907.1847 // 1. 상수 정의

private static Double kilogramsPerUsTon() { // 2. 공급자 함수 정의
    return 907.1847;
}

private static Double usTonsToKilograms(Double usTons) { // 3. 헬퍼 함수 정의
    return usTons * 907.1847;
}

익명 함수는 적절하게 사용해야 한다

익명 함수는 간단한 로직에 사용해야 한다.

익명 함수는 이름을 가지고 있지 않기 때문에, 가독성이 떨어질 염려가 있다.

길이가 너무 길어지거나, 로직이 복잡해지는 경우,

명명된 여러 개의 함수로 나눠야 한다.

새 기능을 적절히 사용하라

자바의 스트림을 사용하면, 복잡한 코드를 함수형 스타일로 간결하게 표현할 수 있다.

하지만 불필요한 사용은 지양해야 한다.

아래 코드는 맵 자료구조 에서 key 값으로 value를 검색한다.

String value = map.get(key)

그런데 괜히 스트림을 사용하겠다고 아래처럼 코드를 복잡하게 만들 수 있다.

String value = map.entrySet().stream()
        .filter(entry -> entry.getKey().equals(key))
        .map(Entry::getValue)
        .findFirst()
        .orElseGet(() -> "hi")

또, 새로운 기능은 협업하는 팀원들 전체에 공유된 내용이어야 한다.

개선사항보다 팀원들의 수고가 더 커지지는 않을지 고민해야 한다.

반응형

+ Recent posts