🎯 목표 설정
1주차 과제에서는 테스트 코드 작성에 집중했으나, 코드 리뷰를 통해 단순히 다양한 케이스를 테스트하는 것뿐만 아니라 단위 테스트를 통해 보다 효율적이고 체계적인 테스트를 작성하는 것이 중요하다는 피드백을 받았다. 이번 주차에는 해당 피드백을 반영하여 TDD 방식을 적용하고, 단위 테스트에 중점을 두어 더 나은 테스트 코드를 작성하는 것을 목표로 삼았다.
또한, 1주차 과제의 코드 리뷰와 공통 피드백을 통해 주석 사용, 객체지향적 설계, 그리고 매직 넘버 사용에 대한 개선점(프리코스 1주차 기록 고찰에 작성)을 피드백 받았는데, 이번 주차에서는 이러한 부분을 더욱 신경 써서 코드를 작성하려고 노력했다.
마지막으로, MVC 패턴을 도입해 보았다. 객체지향적인 설계와 책임 분리를 통해 코드의 가독성과 유지보수성을 높이기 위해 MVC 패턴을 학습하고 적용해 보았다.
🔽 진행 계획
- 기능 구현 목록 정리
- 통합 테스트 코드 작성
- 기능 구현
- 단위 테스트 코드 작성
- 단위 테스트 기반 기능 분리 리팩토링
- 클린 코드 원칙 점검 및 코드 개선
- 배운 점 정리
💻 진행 과정
▶ 기능 구현 목록 정리
공통 피드백에서 과제 요구 사항을 정확히 준수하라는 점을 강조하고 있어, 이를 프리코스에서 매우 중요하게 여기고 있음을 느꼈다. 이에 따라 과제 요구 사항을 세밀하게 정리하고, 이를 기반으로 기능 구현 목록을 작성하며 전체적인 코드 구조를 설계했다.
▶ 통합 테스트 코드 작성
기능을 Application에 구현하기 전에, 기능 구현 목록을 기반으로 다양한 케이스에 대한 통합 테스트 코드를 작성했다. 총 23개의 통합 테스트 코드를 작성했으며, 자동차 이름 입력과 시도 횟수 입력에서 발생할 수 있는 여러 예외 케이스를 꼼꼼히 고려하고자 했다.
class ApplicationTest extends NsTest {
private static final int MOVING_FORWARD = 4;
private static final int STOP = 3;
@Test
@DisplayName("기능_테스트_기본")
void 기능_테스트() {
assertRandomNumberInRangeTest(
() -> {
run("pobi,woni", "1");
assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi");
},
MOVING_FORWARD, STOP
);
}
...
@Test
void 예외_테스트_시도_횟수_숫자_문자_특수_문자_혼합() {
assertSimpleTest(() ->
assertThatThrownBy(() -> runException("pobi,woni", "3abc$"))
.isInstanceOf(IllegalArgumentException.class)
);
}
@Override
public void runMain() {
Application.main(new String[]{});
}
}
▶ 기능 구현
2주차에서도 1주차와 마찬가지로, 모든 로직을 Application 클래스에 몰아넣고 기능 구현에 우선적으로 집중했다. 통합 테스트를 기반으로 로직을 작성하며 기능을 구현했고, 기능 단위를 최대한 분리한 기능 구현 목록에 따라 로직을 정리하고 커밋을 진행했다.
public class Application {
public static void main(String[] args) {
System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)");
String carsNameInput = Console.readLine();
System.out.println("시도할 횟수는 몇 회인가요?");
String countInput = Console.readLine();
...
}
}
🔨 기술 고도화 및 리팩토링
▶ MVC 패턴을 통한 코드 분리 및 TDD 기반 리팩토링
Application 클래스에 모든 로직이 포함되어 있어, 1주차와 마찬가지로 Application의 주요 로직을 MVC 패턴에 따라 개별 클래스로 분리했다. 이번에는 1주차에서 한 걸음 더 나아가 TDD 방식을 적용하고자 노력했다. 먼저 각 클래스별로 기능 단위 테스트를 작성한 후, 테스트에 맞춰 로직을 순차적으로 구현해 나갔다.
- controller
- RaceController: 경주 게임의 핵심 제어 로직을 담당하는 컨트롤러로, 모델과 뷰 사이의 연결을 관리한다.
- model
- domain
- Car: 경주에 참여하는 자동차 객체를 정의한 도메인 클래스로, 자동차의 이름과 현재 이동거리를 포함한다.
- Parser: 입력 데이터를 필요한 형식으로 변환하는 클래스로, 문자열 파싱과 조인 작업 등을 수행한다.
- Validator: 입력 값에 대한 유효성을 검사하는 클래스로, 자동차 이름과 시도할 횟수의 사용자 입력이 적합한지 확인한다.
- RaceService: 경주의 핵심 로직을 수행하는 서비스 클래스로, 자동차 생성, 경주 진행, 우승자 결정 등의 기능을 제공한다.
- domain
- view
- InputView: 사용자로부터 입력을 받는 클래스로, 입력 메시지를 표시하고 콘솔에서 값을 읽어온다.
- OutputView: 사용자에게 출력 메시지를 보여주는 클래스로, 경주 결과나 우승자 발표 등을 화면에 출력한다.
▶확장성과 재사용성을 고려한 private 메서드 캡슐화
캡슐화는 내부 구현을 외부로부터 숨기고, 클래스가 제공하는 기능에 집중할 수 있게 도와준다. 예시로 Validator 클래스의 유효성 검사 메서드를 private 메서드로 캡슐화하여 재사용성과 확장성을 높일 수 있었다.
🔽 변경 전
public class Validator {
public void validateCarNameNotEmpty(List<String> CarNames) {
for (String carName : CarNames) {
if (carName == null || carName.isBlank()) {
throw new IllegalArgumentException();
}
}
}
public void validateCarNameNoSpaces(List<String> CarNames) {
for (String carName : CarNames) {
if (carName.contains(SPACE)) {
throw new IllegalArgumentException();
}
}
}
...
}
🔽 변경 후
public class Validator {
public void validateCarNames(List<String> CarNames) {
for (String carName : CarNames) {
validateNotEmpty(carName);
validateNoSpaces(carName);
...
}
}
private void validateNotEmpty(String input) {
if (input == null || input.isBlank()) {
throw new IllegalArgumentException();
}
}
private void validateNoSpaces(String input) {
if (input.contains(SPACE)) {
throw new IllegalArgumentException();
}
}
...
}
🔽 장점
- 가독성 향상: 주요 유효성 검사를 validateCarNames 메서드에서 한눈에 파악할 수 있어 코드가 더욱 명확해졌다.
- 유지보수성 증가: 세부 유효성 검사 메서드가 private으로 캡슐화되어 외부에서 직접 호출할 수 없으므로, 클래스 외부에서 해당 메서드의 로직 변경에 의존하지 않게 되었다.
- 확장성: 새로운 유효성 검사 항목이 추가될 경우, validatorCarNames 메서드에 새로운 private 메서드를 호출하는 방식으로 쉽게 확장할 수 있다.
- 코드의 일관성: private 메서드의 이름과 변수명을 일반화하여 코드를 직관적이고 일관성 있게 작성함으로써, 유지보수 시 로직을 파악하고 수정하는 데 드는 노력을 줄일 수 있으며, 다른 입력에 대한 유효성 검사에서도 재사용이 가능하도록 고려했다.
▶ 매직 넘버 추출 및 상수 관리
1주차 과제 코드 리뷰에서 매직 넘버가 코드의 가독성과 유지보수성을 저해할 수 있다는 피드백을 받았다. 따라서, 각 클래스에 하드코딩된 매직 넘버를 Constant 클래스에서 관리하도록 했다.
🔽 Constant 클래스
public class Constant {
// Validator
public static final int MAX_CAR_NAME_LENGTH = 5;
public static final int MIN_CAR_COUNT = 2;
public static final String SPACE = " ";
public static final int MIN_ATTEMPT_COUNT = 1;
// Parser
public static final String CAR_NAME_SEPARATOR = ",";
...
}
🔽 장점
- 가독성 향상: 매직 넘버를 상수로 관리함으로써 코드의 의미가 명확해졌다. 코드만 보더라도 각 값이 어떤 용도로 사용되는지 쉽게 이해할 수 있다.
- 유지보수성: 매직 넘버가 하나의 상수로 관리되기 때문에, 변경이 필요할 때 Constant 클래스만 수정하면 된다.
- 확장성: 새로운 매직 넘버가 추가되더라도 Constant 클래스에 추가하는 방식으로 관리가 가능하여, 프로젝트가 확장되더라도 관리가 용이하다.
▶ 오버플로우 방지를 위한 BigInteger 기반 입력 처리 개선
이전에 받은 피드백에서 문자열로 입력을 받을 때, 예상보다 큰 값이 입력될 수 있어 int 자료형만으로 처리하는 데 한계가 있다는 점을 지적받았다. 시도할 횟수와 같은 입력이 아주 큰 값일 경우, int는 오버플로우 위험이 있으므로 이를 고려해 BigInteger로 리팩토링했다.
- int
- 자료형 크기: 32비트 (4바이트)
- 범위: -2,147,483,648 ~ 2,147,483,647
- BigInteger
- 자료형 크기: 비트 수 제한 없음 (메모리 허용 범위 내 사용 가능)
- 범위: 메모리 용량에 따라 제한되며 사실상 무한대
🔽 변경 전
public int convertStringToInt(String input) {
return Integer.parseInt(input);
}
🔽 변경 후
public BigInteger convertStringToBigInteger(String input) {
try {
return new BigInteger(input);
} catch (NumberFormatException e) {
throw new IllegalArgumentException();
}
}
🔽 개선점
다만, 현재 코드 구조에서 개선이 필요한 부분은 Validator가 입력에 대한 유효성 검증을 담당하고, Parser가 입력을 필요한 타입으로 변환하는 역할을 맡고 있음에도 불구하고, 해당 메서드는 Parser에 위치하면서 숫자가 아닌 값에 대한 유효성 검증도 함께 처리하고 있다는 점이다. 이를 분리할 뚜렷한 대안이 없어, 현재 상태로 유지했다.
🔍 클린 코드 원칙 준수 점검 및 코드 개선
클린 코드 원칙 정리는 여기를 참고하면 좋을 것 같다.
▶ 코드 한 줄에 점(.)을 하나만 허용했는가?
carName.trim()처럼 한 줄에 점(.)이 두 번 사용된 부분이 있었다. 이는 코드의 가독성과 유지보수성을 저하할 수 있다. 점을 여러 번 사용하면 메서드 체이닝이 길어질 수 있고, 메서드의 역할을 분리하기 어렵기 때문이다.
🔽 변경 전
private List<String> trimCarNames(List<String> carNames) {
List<String> trimmedCarNames = new ArrayList<>();
for (String carName : carNames) {
trimmedCarNames.add(carName.trim()); // 한 줄에 점 2개
}
return trimmedCarNames;
}
🔽 변경 후
private List<String> trimCarNames(List<String> carNames) {
List<String> trimmedCarNames = new ArrayList<>();
for (String carName : carNames) {
trimmedCarNames.add(trimString(carName));
}
return trimmedCarNames;
}
private String trimString(String input) {
return input.trim();
}
▶ 3개 이상의 인스턴스 변수를 가진 클래스를 구현하지 않았는가?
클래스에 3개 이상의 인스턴스 변수가 포함되면, 해당 클래스가 과도한 책임을 가지고 있을 가능성이 크다. 이러한 구조는 단일 책임 원칙을 위반할 수 있으며, 변경 사항이 발생할 때 클래스의 안정성을 해칠 수 있다. 이를 해결하기 위해, RaceController 인스턴스 변수를 3개 이하로 줄이고 책임을 분리하여 InputController 클래스로 나누었다.
🔽 변경 전
public class RaceController {
private final InputView inputView;
private final OutputView outputView;
private final Parser parser;
private final Validator validator;
private final RaceService raceService;
public RaceController(InputView inputView, OutputView outputView, Parser parser, Validator validator,
RaceService raceService) {
this.inputView = inputView;
this.outputView = outputView;
this.parser = parser;
this.validator = validator;
this.raceService = raceService;
}
...
}
🔽 변경 후
public class RaceController {
private final OutputView outputView;
private final Parser parser;
private final RaceService raceService;
public RaceController(OutputView outputView, Parser parser, RaceService raceService) {
this.outputView = outputView;
this.parser = parser;
this.raceService = raceService;
}
...
}
public class InputController {
private final InputView inputView;
private final Parser parser;
private final Validator validator;
public InputController(InputView inputView, Parser parser, Validator validator) {
this.inputView = inputView;
this.parser = parser;
this.validator = validator;
}
...
}
▶ 컬렉션에 대해 일급 컬렉션을 적용했는가?
기존에는 Car 객체 리스트를 직접 관리하면서 Controller와 Model에 Car 리스트를 생성하고 처리하는 로직이 포함되어 있었다. 이를 개선하기 위해 Car 리스트를 관리하는 일급 컬렉션 클래스인 Cars를 도입하여 해당 로직을 Cars 클래스에서 일관되게 관리하도록 변경했다.
🔽 변경 전
public class RaceService {
public List<Car> createCars(List<String> carNames) {
List<Car> cars = new ArrayList<>();
for (String carName : carNames) {
cars.add(new Car(carName));
}
return cars;
}
public void raceOnce(List<Car> cars) {
for (Car car : cars) {
moveConditionally(car);
}
}
...
}
🔽 변경 후
public class Cars {
private final List<Car> cars;
public Cars(List<String> carNames) {
this.cars = createCars(carNames);
}
private static List<Car> createCars(List<String> carNames) {
List<Car> cars = new ArrayList<>();
for (String carName : carNames) {
cars.add(new Car(carName));
}
return cars;
}
public void raceOnce() {
for (Car car : cars) {
car.moveConditionally();
}
}
...
}
🔽 비고
컬렉션을 처리하는 로직을 Cars 일급 컬렉션으로 통합하면서 RaceService 클래스가 불필요해졌다. 이제 Cars 클래스 내에서 Car 객체 리스트 생성과 경주 진행을 모두 처리하므로, 코드의 응집도가 향상되어 유지보수가 용이해졌다.
✏️ 배운 점 정리
▶ MVC 패턴
MVC 패턴은 Model, View, Controller의 약자로, 애플리케이션을 세 가지 역할로 분리하여 구성하는 디자인 패턴이다.
🔽 구성 요소 및 규칙 설명
- Model: 애플리케이션의 핵심 데이터와 비즈니스 로직을 관리하며, 데이터의 상태를 저장하고 가공하는 로직을 포함한다.
- 사용자가 수정, 조회, 생성하고자 하는 모든 데이터가 포함된다.
- View나 Controller에 대한 정보를 가지지 않고, 자체적으로 데이터를 관리한다.
- 데이터가 변경될 경우 이를 알리는 메커니즘이 있어야 한다. 옵저버 패턴처럼, 변경이 일어나면 이를 감지하여 View에 알릴 수 있다.
- View: 사용자 인터페이스(UI)를 담당하며, Model의 데이터를 화면에 표시하고 사용자 입력을 Controller로 전달한다.
- Model의 데이터를 화면에 표시하지만, 자체적으로 데이터를 저장하지 않아야 한다.
- Model이나 Controller에 대한 직접적인 참조 없이 독립적으로 동작한다.
- Model의 변경이 발생하면 이를 반영할 수 있도록 알림을 받을 수 있어야 한다.
- Controller: 사용자의 입력을 해석하고, 이에 따라 Model을 수정하거나 View를 업데이트하여 데이터 흐름을 관리한다.
- Model과 View에 대한 정보를 알고 있어야 하며, 이들 간의 상호작용을 관리한다.
- Model이 View의 상태 변화를 모니터링하여, 필요한 경우 Model을 변경하거나 View를 업데이트한다.
이렇게 역할을 명확히 나누는 MVC 패턴은 각 구성 요소의 독립성을 유지하고, 유연하고 유지보수하기 쉬운 구조를 만든다.
▶ TDD (테스트 주도 개발)
TDD(Test-Driven Development, 테스트 주도 개발)는 코드를 작성하기 전에 테스트를 먼저 설계하고 구현하는 개발 방식이다. 이를 통해 코드의 유지보수성을 높이고, 기능 요구사항을 명확하게 반영할 수 있다. TDD의 주된 과정은 "테스트 작성 -> 코드 작성 -> 리팩토링"의 반복하는 것으로, 이 과정에서 코드가 자연스럽게 개선된다.
🔽 과정
- 테스트 작성: 구현하려는 기능에 대한 테스트 코드를 먼저 작성한다. 이를 통해 기능에 대한 요구사항을 명확히 정의한다. 작성된 테스트는 초기에 실패한다.
- 코드 작성: 테스트를 통과하기 위해 필요한 최소한의 코드를 작성한다. 이 단계에서는 효율적인 코드보다 테스트 통과가 우선이다.
- 리팩토링: 테스트가 통과한 후 코드를 개선하는 리팩토링을 진행한다. 중복 코드를 제거하고, 코드의 가독성과 유지보수성을 높이기 위한 수정이 이루어진다.
▶ 단언문을 활용한 테스트 검증
AssertJ와 JUnit을 사용해 테스트 코드를 작성할 때, 코드의 동작을 검증할 수 있는 다양한 단언문(Assertion)을 제공한다. JUnit은 테스트 실행과 예외 검증에 적합하며, AssertJ는 세부 검증과 가독성 높은 코드 작성을 위해 JUnit과 함께 사용해 테스트의 유연성을 높인다.
🔽 AssertJ
- assertThat(actual).isEqualTo(expected): 실제 값이 기대하는 값과 같은지 검증한다.
- assertThat(actual).isNotEqualTo(expected): 실제 값이 기대하는 값과 같지 않음을 검증한다.
- assertThat(actual).isTrue() / assertThat(actual).isFalse(): Boolean 값이 true 또는 false인지 검증한다.
- assertThat(collection).hasSize(expectedSize): 컬렉션의 크기가 예상한 값과 동일한지 검증한다.
- assertThat(collection).contains(value): 컬렉션이 특정 값을 포함하고 있는지 검증한다.
- assertThat(collection).containsExactly(value1, value2, ...): 컬렉션이 특정 값들을 정확히 해당 순서대로 포함하는지 검증한다.
- assertThat(collection).doesNotContain(value): 컬렉션이 특정 값을 포함하지 않는지 검증한다.
- assertThat(actual).isBetween(start, end): 실제 값이 지정된 범위 (start와 end 포함) 내에 있는지 검증한다.
- assertThat(actual).isNotEmpty(): 컬렉션이나 문자열이 비어 있지 않음을 검증한다.
- assertThat(actual).isGreaterThan(expected) / assertThat(actual).isLessThan(expected): 실제 값이 예상 값보다 크거나 작은지 검증한다.
- assertThat(actual).isInstanceOf(expectedClass): 객체가 특정 클래스의 인스턴스인지 검증한다.
- assertThatThrownBy(() -> { /* code */ }).isInstanceOf(expectedException.class): 특정 코드가 예상한 예외를 던지는지 검증한다.
- assertThatCode(() -> { /* code */ }).doesNotThrowAnyException(): 특정 코드 블록이 예외를 발생시키지 않음을 확인한다.
🔽 JUnit
- assertEquals(expected, actual): 실제 값이 기대하는 값과 같은지 검증한다.
- assertNotEquals(expected, actual): 실제 값이 기대하는 값과 같지 않음을 검증한다.
- assertTrue(condition) / assertFalse(condition): Boolean 값이 true 또는 false인지 검증한다.
- assertNull(object) / assertNotNull(object): 객체가 null인지 또는 null이 아닌지 검증한다.
- assertArrayEquals(expectedArray, actualArray): 두 배열의 값이 같은지 검증한다.
- assertThrows(expectedException.class, () -> { /* code */ }): 특정 코드가 예상한 예외를 던지는지 검증한다.
- assertSame(expected, actual) / assertNotSame(expected, actual): 두 객체가 동일한 참조를 가지고 있는지 (==) 검증한다.
- assertIterableEquals(expectedIterable, actualIterable): 두 Iterable 객체가 같은 순서와 값을 가지는지 검증한다.
- assertTimeout(Duration.ofMillis(1000), () -> { /* code */ }): 특정 코드가 주어진 시간 안에 완료되는지 검증한다.
- assertLinesMatch(expectedList, actualList): 두 리스트가 같은 순서와 문자열을 가지는지 검증한다.
▶ 콘솔 입력 테스트 시 주의사항
콘솔을 통해 입력값을 테스트할 때, 입력 스트림(System.in)을 재설정한 후 테스트를 진행하는 경우가 많다. 하지만 테스트가 완료된 후 콘솔 입력에 사용된 리소스를 적절하게 해제하지 않으면, 후속 테스트에 영향을 줄 수 있다. 이를 방지하기 위해 Console.close()를 호출하여 콘솔 리소스를 반드시 해제해야 한다.
🔽 예시: InputViewTest 클래스
class InputViewTest {
...
@AfterEach
void afterEach() {
Console.close(); // 리소스 해제
}
@Test
@DisplayName("자동차 이름 입력을 받아 반환하는지 확인하는 테스트")
void getCarNamesInput() {
// given
String input = "pobi,woni,jun";
System.setIn(new ByteArrayInputStream(input.getBytes()));
// when
String carNamesInput = inputView.getCarNamesInput();
// then
assertThat(carNamesInput).isEqualTo(input);
}
▶ 자바의 다양한 메서드
🔽 repeat()
문자열을 지정된 횟수만큼 반복하여 연결한 새로운 문자열을 반환한다.
- 형태:
- String.repeat(int count)
- 활용 예시: "-".repeat(5)
- 주의: count가 음수일 경우 IllegalArgumentException이 발생한다.
🔽 format()
지정된 형식의 문자열로 포맷팅하여 새로운 문자열을 반환한다. 다양한 형식 지정자(%s, %d 등)를 활용할 수 있다.
- 형태:
- String.format(String format, Object... args)
- 활용 예시: String.format("%s : %s", "pobi", "-----")
- 주의: 형식 지정자와 일치하지 않는 인수를 전달하면 IllegalFormatException이 발생할 수 있다.
🔽 Arrays.asList(array)
배열을 List로 변환하여 반환한다. 반환된 리스트는 고정 크기이며, 추가나 삭제 작업이 불가능하다.
- 형태:
- Arrays.asList(T... a)
- 활용 예시: Arrays.asList("pobi", "woni")
- 주의: 반환된 리스트는 UnsupportedOperationException을 발생시키므로, 크기를 변경하는 작업을 수행할 수 없다.
🔽 trim()
문자열 양 끝의 공백을 제거한 새로운 문자열을 반환한다.
- 형태:
- String.trim()
- 활용 예시: input.trim()
- 주의: 문자열 중간의 공백은 제거되지 않는다.
🔽 join()
지정된 구분자를 사용해 여러 문자열을 하나의 문자열로 결합한다.
- 형태:
- String.join(CharSequence delimiter, CharSequence... elements)
- 활용 예시: String.join(", ", "pobi", "woni", "jun")
- 주의: 요소가 null이면 NullPointerException이 발생할 수 있다.
▶ 일급 컬렉션을 통한 컬렉션 관리 최적화
일급 컬렉션은 컬렉션을 별도의 클래스로 감싸 관리하는 설계 기법이다. 단순한 List나 Set 같은 컬렉션을 그대로 사용하는 대신, 이를 감싼 클래스를 만들어 컬렉션의 관리와 관련된 로직을 캡슐화한다. 이는 코드를 보다 응집력 있게 만들어 유지보수와 재사용을 쉽게 하는 장점이 있다.
🔽 특징 및 장점
- 응집도 향상: 컬렉션을 직접 사용하지 않고 관련 메서드로만 접근하게 함으로써, 불필요한 외부 로직을 줄이고 컬렉션 관리 로직이 한곳에 집중된다.
- 불변성 유지: 컬렉션이 변경되지 않아야 할 때, Collections.unmodifiableList()나 Collections.unmodifiableSet() 등을 사용하여 read-only 형태로 반환할 수 있다.
- 중복 방지 및 검증 로직 추가: 컬렉션을 생성하는 동시에 데이터의 중복 여부나 특정 규칙을 검사하는 로직을 포함할 수 있다.
🔽 예시: Cars 클래스
public class Cars {
private final List<Car> cars;
public Cars(List<String> carNames) {
this.cars = createCars(carNames);
}
...
public List<Car> getCarsReadOnly() {
return Collections.unmodifiableList(cars);
}
...
}
▶ BigInteger로 큰 정수 안전하게 처리
BigInteger 클래스는 Java에서 int나 long보다 큰 정수를 다루기 위해 사용하는 클래스이다. int는 약 21억, long은 약 92경까지의 값을 표현할 수 있다. 이 한계를 넘어서는 큰 수를 계산하거나 저장해야 할 때 BigInteger를 사용한다.
🔽 특징
- 무제한 크기 지원: 정수의 크기 제한이 없어, 거의 무제한에 가까운 큰 정수를 표현할 수 있다.
- 불변 객체: BigInteger는 불변 객체로, 계산 후 새로운 객체를 반환하므로 기존 객체의 값이 변경되지 않는다.
- 다양한 연산 지원: 사칙연산(add, subtract, multiply, divide), 거듭제곱(pow), 최대공약수(gcd) 등 수학적인 연산을 지원한다.
- 정수 이외의 숫자 표현: BigDecimal과 같이 소수점 연산이 필요할 경우 BigInteger와 함께 사용할 수 있다.
🔽 비교 방법
- compareTo()
- 0을 반환하면 this와 other가 같다.
- 1을 반환하면 this가 other보다 크다.
- -1을 반환하면 this가 other보다 작다.
BigInteger bigInt1 = new BigInteger("12345");
BigInteger bigInt2 = new BigInteger("12345");
BigInteger bigInt3 = new BigInteger("54321");
int result1 = bigInt1.compareTo(bigInt2); // 0
int result2 = bigInt1.compareTo(bigInt3); // -1
int result3 = bigInt3.compareTo(bigInt1); // 1
- equals()
- 이 메서드는 값뿐 아니라 객체의 타입도 비교한다.
boolean isEqual = bigInt1.equals(bigInt2);
- max(), min()
- max(BigInteger other): 두 값 중 큰 값을 반환한다.
- min(BigInteger other): 두 값 중 작은 값을 반환한다.
BigInteger maxVal = bigInt1.max(bigInt3); // 큰 값 반환
BigInteger minVal = bigInt1.min(bigInt3); // 작은 값 반환
💡 고찰
▶ MVC 패턴을 적용하며 느낀 점
이번 과제에서 코드 구조에 대한 고민이 많았다. 이전 기수와 1주차의 다른 사람들 코드를 참고하면서, 많은 사람들이 MVC 패턴을 사용한다는 점이 궁금했다. 무작정 따라 하기보다는 그 이유와 장점을 이해하고자 했고, 이를 통해 MVC 패턴이 코드의 유지보수성과 책임 분리에 효과적이라는 점을 느끼게 되었다.
1주차에는 각 객체의 책임을 분리하는 것이 쉽지 않아 여러 시도를 했으나, 판단에 어려움이 있었다. 그러나 이번 주에는 Model, View, Controller를 역할에 맞게 구분하면서 자연스럽게 책임 분리가 이루어졌다. 각 클래스가 불필요하게 많은 인스턴스 변수를 가지지 않도록 조정하니 역할이 명확해지고 코드의 응집도도 높아졌다.
또한, MVC 패턴이 유지보수를 용이하게 만든다는 점을 이번 기회에 직접 경험할 수 있었다. 특정 기능에 변경이 필요할 때 해당 부분만 유지보수할 수 있는 코드 구조 덕분에, 코드가 깔끔해지고 확장성이 높아진 것을 체감했다.
▶ TDD 적용 효과 및 개선 방안
이번 주차에서 TDD를 도입해 기능 구현 전에 설계하고 테스트 코드를 작성하면서, 요구사항에 맞춘 코드를 작성할 수 있었다. 특히 기능이 확장될 때 추가 테스트 작성이 쉬웠으며, 입력과 출력을 고정하면서 전반적인 설계를 깊이 고민할 수 있었다. 또한, 단위 테스트를 작성하고 기능을 세분화해 리팩토링하는 과정을 통해 코드의 안정성을 높일 수 있었다.
이번 과제에서는 통합 테스트를 먼저 작성한 후 기능 구현을 진행했고, 이후 단위 테스트를 추가하며 기능을 리팩토링하는 방식을 반복했다. 그런데 TDD를 학습하면서 기능 구현 역시 TDD의 중요한 일부라는 점을 알게 되었다. 앞으로 기능 구현 단계에서도 단위 테스트를 작성하며 개발을 진행하면 더 효율적으로 설계된 코드를 작성할 수 있을 것 같아, 다음 주차부터 이를 적용해 볼 계획이다.
▶ 단위 테스트 코드의 효과
단위 테스트를 통해 여러 장점들을 체감할 수 있었다. 먼저, 코드가 단일 책임 원칙을 잘 준수하고 있는지를 검증할 수 있었고, 작은 단위로 기능을 테스트함으로써 불필요한 개발 공수를 줄이는 데에도 효과적이었다. 또한, 개별 기능에서 발생한 오류를 빠르게 찾아 수정할 수 있어 개발 속도를 높이는 데 큰 도움이 되었다. 이전에는 단위 테스트 코드의 필요성을 알지 못해 작성해 본 적이 없었지만, 프리코스를 통해 효율적인 개발이 무엇인지 새롭게 배우고 있다.
▶ 1주차 회고를 통한 성장
1주차에 작성한 회고를 바탕으로, 이번 주차에는 더 명확한 개선 목표를 설정하고 진행할 수 있었다. 주차별 회고를 통해 부족했던 점을 파악하고 개선 방향을 설정하면서 한층 성장할 수 있었다.
특히, 1주차 과제의 코드 리뷰와 공통 피드백을 통해 주석 사용, 객체지향적 설계, 그리고 매직 넘버 사용에 대한 개선점들을 받았다. 이번 주차에서는 이러한 피드백을 바탕으로 불필요한 주석을 줄이고, 코드의 가독성을 높이기 위해 메서드명과 변수명을 더욱 신경 써서 작성했다. 또한, 매직 넘버를 상수로 정의하여 코드의 유지보수성을 높이고, 객체의 역할을 명확히 분리하면서 객체지향적인 설계를 더욱 강화하려고 노력했다.
▶ 기타 느낀 점
- 메서드명과 변수명을 짓는 일이 여전히 어렵다는 점을 다시금 느꼈다. 명확한 의미 전달을 위해 신경을 쓰면서도, 적절한 이름을 찾기 위해 많은 고민이 필요하다는 것을 실감했다.
- 1주차에 비해 회고와 기록 작성이 점차 습관으로 자리 잡아가고 있다. 에러가 발생한 부분이나 개선할 점이 있으면 블로그에 바로 기록하려 하고 있으며, 기능 구현 후에는 기억에서 사라지지 않도록 PR 제출 직후에 회고를 기록하고 있다.
- 프리코스를 시작하기 전, 클린 코드 원칙을 정리하면서 이를 쉽게 실천할 수 있을 것이라 생각했지만, 실제 기능 구현 중에는 놓치는 부분이 많았다. 이후, 클린 코드 원칙을 바탕으로 리팩토링하는 과정에서 큰 보람을 느꼈다,
▶ PR 리뷰와 공통 피드백에서 얻은 추가 고찰 (11.3 추가)
- 인스턴스를 생성하지 않는 클래스는 Util 클래스로 변환하는 것이 좋겠다는 제안을 받았다. 개발을 시작한 지 오래되지 않아 Util 클래스의 존재에 익숙하지 않았으나, 다음 주차부터 상황에 맞게 이를 적용해 볼 예정이다.
- 예외를 던질 때 메시지를 함께 제공하는 방식에 대한 피드백을 받았다. 또한, 3주차 과제 요구사항을 반영하여 enum을 적극 활용해 볼 계획이다.
- 상수를 한 클래스에 모아 관리하는 방식은 장점이 있지만, 각 클래스마다 주석으로 구분하는 것은 유지보수에 좋지 않을 수 있다는 피드백을 받았다. 이에 따라, 각 클래스별로 상수를 만드는 방안도 고려해 볼 예정이다.
- 피드백을 통해 정적 팩토리 메서드에 대해 알게 되었다. 이를 계기로 향후 공부해 볼 주제를 발견할 수 있어 좋은 피드백이었다.
- 변수 이름에 자료형을 포함하지 말라는 공통 피드백을 받았다. 변수명은 자료형이 아닌, 그 역할을 직관적으로 나타낼 수 있도록 신중하게 네이밍하는 것이 중요하다는 점을 깨달았다.
- 2주차 공통 피드백에서 "한 메서드가 한 가지 기능만 담당하게 한다"라는 피드백이 있었다. 예시로 안내 문구 출력, 사용자 입력 처리, 유효값 검증 등의 작업을 하나의 함수에 포함하는 것보다, 각각의 기능을 다른 함수로 분리하는 것이 좋다는 내용이었다. 이 피드백을 보면서 InputView 클래스가 떠올랐다. 현재는 안내 메시지를 출력하며 값을 입력 받도록 구성했는데, 이 안내 메시지 출력은 OutputView의 책임에 더 적합하지 않을까 하는 생각이 들었다. 이후 과제에서 이 부분을 반영하여 InputView는 입력만 처리하고, 안내 메시지 출력은 OutputView가 담당하도록 역할을 분리하는 방향을 고려해야할 것 같다.
📍 참고 자료
- mission-utils
- [TDD] 단위 테스트(Unit Test) 작성의 필요성 (1/3)
- [Java] JUnit을 활용한 Java 단위 테스트 코드 작성법 (2/3)
- [Spring] JUnit과 Mockito 기반의 Spring 단위 테스트 코드 작성법 (3/3)
- [개발자 면접준비]#1. MVC패턴이란
- [Kotlin/Java] JUnit을 사용한 단위 테스트에서 System.in을 사용하는 콘솔 입력 로직 테스트하기
- TDD - JUnit assert(단언,assertThat,assertEqual,assertTrue,asserrtFalse,is..) 예제
'우아한테크코스' 카테고리의 다른 글
[우아한테크코스] Java Enum 활용기 (4) | 2024.11.05 |
---|---|
[우아한테크코스] 프리코스 3주차 기록 (8) | 2024.11.05 |
[우아한테크코스] 프리코스 1주차 기록 (4) | 2024.10.22 |
[우아한테크코스] 우테코 코드 스타일 포매터 적용 (0) | 2024.10.15 |
[우아한테크코스] 프리코스 시작 전, 목표 설정 및 클린 코드 원칙 정리 (0) | 2024.10.07 |