반응형
들어가며
- 이번 글에서는 자동차 경주 과제를 진행하면서 배웠던점과 느낀 점들에 대해서 정리하고자 합니다.
배운 점
먼저 도메인을 설계하자
- Inside-out 방식으로 구현하는 것이 중요합니다.
- 즉, 1:N 관계일 때 N인 객체를 먼저 테스트하는 것이 TDD 하기에 적합합니다.
테스트가 어려운 메서드는 구조 설계에서 제일 하단으로 옮기자
public class RandomNumberGenerator {
private Random random;
public RandomNumberGenerator() {
this.random = new Random();
}
public int generateRandomNumber(int min, int max) {
return random.nextInt(max - min + 1) + min;
}
}
- 위 클래스는 주어진 범위에서 무작위로 숫자를 생성하는 기능을 가지고 있습니다. 하지만 이 클래스의 generateRandomNumber 메소드는 테스트하기가 어려울 수 있습니다. 왜냐하면 실행될 때마다 다른 결과를 반환하기 때문입니다.
- 따라서 이 메서드를 구조 설계에서 하단으로 옮겨서 테스트하기 쉽게 만들 수 있습니다. 예를 들어, 메서드의 로직을 추출하고 이를 인터페이스를 통해 주입하는 방식을 사용할 수 있습니다.
public interface RandomNumberProvider {
int getRandomNumber(int min, int max);
}
public class RandomNumberGenerator implements RandomNumberProvider {
private Random random;
public RandomNumberGenerator() {
this.random = new Random();
}
@Override
public int getRandomNumber(int min, int max) {
return random.nextInt(max - min + 1) + min;
}
}
- 이렇게 하면, RandomNumberProvider 인터페이스를 구현한 클래스를 만들어서 테스트할 때 해당 클래스의 Mock 또는 Stub을 사용할 수 있습니다.
- 이를 통해 예측 가능한 결과를 얻을 수 있게 됩니다. 따라서 random 함수와 같이 실행 결과가 무작위로 변하는 상황에서도 테스트가 쉽고 유지보수가 용이한 코드를 작성할 수 있습니다.
구현할 기능과 테스트 목록을 작성해라.
- 평소에 문서화는 좋은 개발 방향성이 아니라고 생각했었습니다. 코드의 가독성을 올려서 코드 자체가 문서화된 결과라고 생각했었습니다.
- 하지만 TDD를 진행하면서 문서화한 결과물을 코드에 바로 적용할 수 있다면 이는 의미 있는 작업이란 걸 알게 되었습니다.
- 위와 같이 테스트에 대한 명세서를 먼저 설계하고 테스트 내용은 @DisplayName에 작성한 내용들을 옮겨놓고 작업을 하니 구현해야 할 사항들이 명확해지면서 놓친 테스트들이 없게 되었습니다.
- 앞으로 신규 개발건 또는 레거시 개발건들에 대해서 테스테에 대한 내용들은 문서화해 놓고 작업을 진행하려고 합니다.
레거시 코드를 빠르게 테스트하고 싶다면?
- 개인적으로 이번 단원에서 미션을 구현하면서 배운 가장 큰 점이라고 생각합니다
// 기존의 어려운 테스트를 해야하는 메소드
class OriginalClass {
public int complexMethod(int a, int b) {
// 복잡한 로직이 포함된 메소드
return a + b;
}
}
- 위와 같이 테스트하기 복잡한 어려운 메서드가 있다고 가정해 보겠습니다.
- 이럴 경우 인터페이스를 사용하면 테스트하기가 굉장히 쉬워집니다.
// 인터페이스로 리팩토링
interface SimpleInterface {
int complexMethod(int a, int b); //기존 메소드와 동일하게 이름 설정
}
- 위와 같이 기존의 복잡한 메서드와 일치한 인터페이스를 생성합니다.
// 테스트 코드
class TestClass {
public void testMethod() {
// 인터페이스를 지역 메소드로 사용
SimpleInterface simpleInterface = new SimpleInterface() {
@Override
public int complexMethod(int a, int b) {
return a + b;
}
};
// 테스트
int result = simpleInterface.complexMethod(3, 4);
System.out.println("Result: " + result);
}
public static void main(String[] args) {
TestClass test = new TestClass();
test.testMethod();
}
}
- 이후 위와 같이 인터페이스를 지역메서드를 사용해서 테스트 코드를 작성하면 복잡한 메서드도 테스트 코드 작성이 가능합니다.
정규표현식은 Static을 사용해서 사용해라
public class Calculator {
private static final Pattern numberPattern = Pattern.compile(":|,");
...
}
- 자바 String의 split 메서드는 정규표현식 Pattern 자원을 생성해서 사용하는데 이때 생성하는 리소스가 크기 때문에 위와 같이 힙 영역에 한 번 생성한 뒤 재활용해서 사용하는 게 좋은 방법입니다.
- 그런데 항상 Pattern을 힙 영역에 생성해서 사용하는게 좋은 방법은 아닙니다. 아래 사진을 보면 split 메서드인데 주석에 보면 fastpath라고 적혀있습니다.
- 이게 무슨 의미냐면 정규 표현식을 한 문자로 사용하면서 아래 케이스(fastPath가 안 되는 경우 참조)가 아닌 경우에는 Pattern을 힙 영역에 생성해서 사용하는 것보다 더 빠르다는 것을 의미합니다.
- 따라서 한 문자면서 아래와 같은 경우가 아니라면 Pattern을 힙 영역에 선언해서 사용하지 않고 그냥 split을 사용하는 게 좋은 방법이고 그게 아니라면 Pattern을 힙 영역에 생성해서 사용하는 것이 좋은 방법입니다.
[fastpath가 안 되는 경우]
- "."
- "$"
- "|"
- "("
- ")"
- "["
- "{"
- "^"
- "?"
- "*"
- "+"
- ""
- """
- 아스키 문자가 아니여야 함.
- 정규식이 두 글자인데 첫 번째 문자는 백슬레시이면서 아스키숫자나 알파벳이 아닌 경우 (ex: "$")
일급 컬렉션이란?
- 일급 컬렉션"이란 프로그래밍 언어에서 일급 시민(First-class citizen)으로 취급되는 컬렉션(자료구조)을 가리킵니다.
- 즉, 해당 언어에서 컬렉션을 변수나 함수의 매개변수, 반환값으로 사용할 수 있는 등의 특성을 말합니다.
- 일급 컬렉션을 사용하게 될 경우 상위 객체에 책임과 역할을 분담하여 가독성 높은 코드를 작성할 수 있는 설계의 기반이 됩니다.
예외가 발생할 수 있는 Exception들을 정확히 명시해라
public static String inputCarGroupName() {
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
try {
String carNames = scanner.nextLine();
checkCarNamesMoreThan5Letters(carNames.split(","));
return carNames;
} catch (Exception e) {
throw new RuntimeException(UNEXPECTED_ERROR.getMessage(), e);
}
}
- 위와 같이 자동차의 이름을 입력받는 경우 클라이언트가 입력값을 잘못 입력하여 Excepetion을 명시해줘야 하는 경우가 있습니다.
- 하지만 위와 같은 방식으로 Exception을 명시할 경우 클라이언트가 어떤 잘못된 방식으로 입력해서 예외가 발생한 지 모릅니다.
public static String inputCarGroupName() {
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).");
try {
String carNames = scanner.nextLine();
checkCarNamesMoreThan5Letters(carNames.split(","));
return carNames;
} catch (CarNameExceedException e) {
throw new CarNameExceedException(e.getMessage());
} catch (InputMismatchException e) {
throw new RuntimeException(INVALID_INPUT_TYPE_INT.getMessage(), e);
} catch (NoSuchElementException e) {
throw new RuntimeException(INVALID_INPUT_EMPTY.getMessage(), e);
} catch (IllegalStateException e) {
throw new RuntimeException(ILLEGAL_STATE.getMessage(), e);
} catch (Exception e) {
throw new RuntimeException(UNEXPECTED_ERROR.getMessage(), e);
}
}
- 따라서 위와 같이 try문 안에서 발생할 수 있는 예외들을 정확히 명시하여 상황별 예외를 catch 하여 어떤 예외가 발생하였는지 알 수 있도록 구현해야 합니다.
디미터 법칙을 준수하자
- 디미터 법칙은 객체 지향 프로그래밍에서 객체 간의 결합도를 줄이기 위한 설계 원칙 중 하나입니다.
- 디미터 법칙은 객체가 다른 객체와 상호작용해야 할 때 객체 안의 메서드를 직접적으로 사용하는 것이 아닌 해당 객체에 속한 메서드를 사용해 기능을 수행해야 합니다.
- 즉, 객체는 자신이 직접 접근할 수 있는 범위 내에서만 메서드를 호출해야 합니다. 따라서. 를 여러 번 붙여서 사용하는 코드는 디미터 법칙을 위반하는 대표적인 예시입니다.
public class Person {
private String name;
private Department department;
public Person(String name, Department department) {
this.name = name;
this.department = department;
}
public Department getDepartment() {
return department;
}
}
public class Department {
private String name;
private Manager manager;
public Department(String name, Manager manager) {
this.name = name;
this.manager = manager;
}
public Manager getManager() {
return manager;
}
}
public class Manager {
private String name;
public Manager(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Manager manager = new Manager("John");
Department department = new Department("HR", manager);
Person person = new Person("Alice", department);
// 디미터 법칙 위반
String managerName = person.getDepartment().getManager().getName();
System.out.println("Manager: " + managerName);
}
}
- 위 예시는 **Person** 객체 안의 **Department**, **Manager**, **Name** 정보를 getter로 조회해서 사용하기 위해 도트(.)를 통해서 메서드 체이닝을 사용하여 디미터 법칙을 위반하는 예시입니다.
public class Person {
private String name;
private Department department;
public Person(String name, Department department) {
this.name = name;
this.department = department;
}
public Department getDepartment() {
return department;
}
// 새로운 메서드 추가
public String getManagerName() {
return department.getManagerName();
}
}
public class Department {
private String name;
private Manager manager;
public Department(String name, Manager manager) {
this.name = name;
this.manager = manager;
}
public String getManagerName() {
return manager.getName();
}
}
public class Manager {
private String name;
public Manager(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
Manager manager = new Manager("John");
Department department = new Department("HR", manager);
Person person = new Person("Alice", department);
// 디미터 법칙 준수
String managerName = person.getManagerName();
System.out.println("Manager: " + managerName);
}
}
- 위의 수정된 코드에서는 Person 클래스에 getManagerName() 메서드를 추가하여 디미터 법칙을 준수하였습니다.
- Person 클래스에서는 Department 클래스의 내부 구조에 직접 접근하지 않고도 **Manager**의 이름을 가져올 수 있습니다. 이러한 변경으로 인해 객체 간의 결합도가 낮아지고, 코드의 유지 보수성과 확장성이 향상되게 됩니다.
주생성자를 잘 이용해서 클라이언트의 편의성을 도모하자!
public class Person {
private String name;
private int age;
// 주 생성자 (Primary Constructor)
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 부 생성자 (Secondary Constructor) - 나이를 기본값으로 설정
public Person(String name) {
this(name, 0); // 부 생성자 내부에서 주 생성자 호출
}
- 구현한 객체는 클라이언트가 어떤 방식으로든 객체를 생성하기 쉽도록 만들어야 합니다.
- 이때 주성생 자를 먼저 선언한 뒤 부생성자를 추가하는 방식으로 구현하게 되면 클라이언트가 객체 선언 시 선택지를 다양하게 가져갈 수 있는 구현 방식이 됩니다.
- 위 코드 예시에서 주생성자만 있었다면 두 가지 필드 정보를 전부 입력해야만 객체를 생성할 수 있었지만 부생성자를 선언하여 문자열만 입력해도 Person 객체를 생성할 수 있게 되었으므로 클라이언트는 객체를 선언할 때 선택지가 늘어나 편의성을 얻게 됩니다.
이외의 좋은 말들
구현하기 어렵다면?
- 단위테스트, 요구사항 무시하고 일단 구현해라.
- 구현하다 보면 도메인 지식이 머리에 쌓인다.
- 이후 구현한 모든 코드를 버려라.
- 다시 구현할 기능 목록을 작성하고 간단한 도메인을 설계해라.
레거시 코드가 있는 상태에서 리팩터링 하는 것이 더 어렵다.
생성자가 여러 개일 경우 정적메서드를 고려해라.
클래스가 너무 클 경우 별도 클래스로 분리해라
정리
- 필드 값은 원시 클래스로 분리해서 단일 책임 원칙을 유지하도록 하자
- 컬렉션은 일급 컬렉션 클래스로 분리하여 단일 책임 원칙을 유지하도록 하자
- 실무에서 레거시 코드 중 메서드가 테스트코드하기 힘든 코드라면 메서드를 추출한 뒤 테스트 코드에서 지역 메소드를 인터페이스를 재정의해서 테스트할 수 있다.
반응형
'대외활동' 카테고리의 다른 글
[NEXTSTEP] 세 번째, 네 번째 과제를 마무리 하면서 + 후기 (1) | 2024.06.07 |
---|---|
[NEXTSTEP] 두 번째 미션을 마무리 하면서 (0) | 2024.04.02 |
[NEXTSTEP] TDD, 클린코드 with Java 18기 시작 (첫 번째 후기) (3) | 2024.03.29 |