지난 과제는 아래 글을 참고해주세요.
2024.10.29 - [프리코스] - [프리코스 7기 백엔드] 1주차 과제 회고 (+ MetricsReloaded 플러그인으로 리팩토링 효과성 평가)
[프리코스 7기 백엔드] 1주차 과제 회고 (+ MetricsReloaded 플러그인으로 리팩토링 효과성 평가)
프리코스 1주차에 참여하며1주차 과제인 만큼 최고의 능력을 끌어내려는 것보다 기본적인 내용을 구현하는 목적으로 이런 과제를 내주신 것 같다. 특히 기능적 요구 사항은 익숙하더라도 과제
tt2-blogging.tistory.com
2024.10.29 - [프리코스] - [프리코스 7기 백엔드] 2주차 과제 회고 (+ JUnit, AssertJ, java.util.Comparator)
[프리코스 7기 백엔드] 2주차 과제 회고 (+ JUnit, AssertJ, java.util.Comparator)
🏃🏻♀️ 프리코스 2주차에 참여하며지난 1주차에는 리팩토링 전후를 코드라인으로 직접 비교하여 그 효과를 수치로 확인하는 것이 목표였다면, 이번 2주차에는 리팩토링이 아예 필요하지
tt2-blogging.tistory.com
프리코스 3주차에 참여하며
처음 3주차 프로젝트 레포를 클론해와서 열어봤을 때 기본적으로 제공되는 Lotto
클래스에 validate메소드가 들어있었다. 그동안 validator 클래스를 따로 만들어서 진행했으나 왜 이 기능이 여기 안에 있는지 생각해봤을 때 도메인 중심 구조이지 않을까 하는 생각이 들었다.
그래서 이번 3주차 과제를 계기로 항상 사용하던 MVC 패턴의 설계가 아니라, 도메인 중심 구조로 해봐야겠다는 생각이 들었다.
또 그동안 1,2주차에서는 service 레이어를 설계하지 않았지만 3주차에서는 설계에 포함하여 그동안의 과제와의 몇 가지 차별점을 두고 학습하고자 한다.
그렇다면 어떤 면에서 두 구조가 다른 것인지 먼저 정리해보아야 한다.
🔍 MVC 구조 vs DDD 구조
MVC (모델-뷰-컨트롤러)
구조 개요:
- 모델 (Model): 데이터와 비즈니스 로직을 관리하는 부분. 데이터베이스와의 상호작용을 포함.
- 뷰 (View): 사용자에게 보여지는 UI를 담당. 사용자 인터페이스의 모든 요소를 포함.
- 컨트롤러 (Controller): 사용자의 입력을 받아서 모델과 뷰를 연결해주는 역할. 모델의 데이터를 업데이트하거나 뷰를 변경하는 방식으로 상호작용을 처리.
예시 구조:
com.lotto
├── model
│ ├── Lotto.java
│ ├── LottoMachine.java
│ └── Purchase.java
├── service
│ └── LottoService.java
└── controller
└── LottoController.java
장점:
- 각 구성 요소가 명확히 나눠져 있어 책임이 분리됨.
- UI 변경 시 모델이나 로직에 영향을 주지 않음.
- 테스트가 용이함.
단점:
- 애플리케이션 규모가 커질수록 컨트롤러가 복잡해질 수 있음.
- 모델, 뷰, 컨트롤러 간의 의존성이 높아질 수 있음.
도메인 중심(Domain-Driven Design) 구조
구조 개요:
- 도메인 (Domain): 특정 비즈니스 또는 문제 영역을 중심으로 패키지 구성. 관련된 데이터, 비즈니스 로직, 서비스, 컨트롤러를 함께 묶어 놓음.
장점:
- 관련된 기능들이 같은 패키지 내에 있어 응집력이 높음.
- 도메인 중심으로 구성되어 있어 비즈니스 로직이 명확함.
- 새로운 도메인 또는 기능 추가가 용이함.
단점:
- 도메인 간의 의존성이 생길 수 있어 관리가 필요함.
- 명확한 분리가 필요할 경우 MVC처럼 세부적으로 나누기 어려울 수 있음.
비교 정리
구분 | 전통적인 MVC 구조 | 도메인 중심 구조 |
---|---|---|
비즈니스 로직 위치 | Service 계층에 비즈니스 로직이 집중됨 | 도메인 객체가 비즈니스 로직을 소유하고 관리함 |
Service 역할 | 비즈니스 로직을 직접 처리하는 계층 | 도메인 객체 간의 로직 호출과 협력을 조직하고 조정함 |
Controller 역할 | 입력과 출력을 처리하며, 서비스와 협력하여 로직을 실행 | 입력과 출력을 처리하며, 주요 로직 호출을 서비스나 도메인 객체에 위임 |
Model(도메인) 역할 | 주로 데이터 전송 및 보관 역할을 수행 | 비즈니스 규칙과 행위를 포함한 도메인 로직을 직접 포함하며, 자신의 데이터와 규칙을 스스로 관리함 |
패키지 구성 방식 | Layer 중심으로 controller , service , repository 계층을 패키지로 구분 |
기능이나 의미 중심으로 order , product 등 도메인 단위로 패키지 구성 |
확장성과 유지보수 | 코드가 많아지면 각 계층에 비즈니스 로직이 집중되어 코드 복잡성이 증가될 가능성이 있음 | 도메인 객체를 기준으로 로직이 나뉘어 코드 증가에도 유지보수 및 확장이 용이 |
복잡한 로직 처리 | 서비스에 비즈니스 로직이 쌓여 복잡성이 증가될 수 있음 | 도메인 객체 내부에 분산되어 각 객체가 자율적인 책임을 가짐 |
응집도 | 낮은 응집도 (데이터 처리와 비즈니스 로직이 분리됨) | 높은 응집도 (도메인 객체가 자신의 데이터와 로직을 함께 관리) |
사용 예시 | 주로 간단한 CRUD 애플리케이션 | 규모가 크고 복잡한 도메인을 다루는 애플리케이션 (e.g., 금융, ERP 시스템 등) |
MVC 구조에서는 Service가 중심이 되어서 로직이 처리된다. 이때 도메인 객체는 데이터를 잠시 담는 역할 뿐이라고 볼 수 있다.
DDD에서는 Service가 도메인 객체를 활용해서 비즈니스 로직을 처리하고, 도메인 객체에서는 생성과 검증 로직을 담당한다.
로또 발매기 로직에 대한 흐름 비교
과제 내에서 5. 저장 기능은 수행되지 않음
기능 | MVC 구조에서의 수행 주체 및 흐름 | 도메인 중심 구조에서의 수행 주체 및 흐름 |
---|---|---|
1. 금액 입력 | View가 입력받아 Controller로 전달 | View가 입력받아 Controller로 전달 |
2. 비즈니스 로직 호출 | Controller가 입력받은 금액을 Service에 전달 | Controller가 입력받은 금액을 Service에 전달 |
3. 로또 발행 로직 수행 | Service가 로또 개수 산정 후, Lotto 객체 생성 |
Service가 로또 개수를 계산하고 Lotto 도메인 객체 생성 |
4. 유효성 검증 | Validator 또는 Service 내부에서 개수, 금액 유효성 검증 | Lotto 객체가 자체적으로 유효성 검증 수행 |
5. 로또 저장 | Repository가 발행된 Lotto 객체를 저장 |
Repository가 발행된 Lotto 객체를 저장 |
6. 결과 전달 | Service에서 생성된 로또를 Controller로 반환, View로 전달 | Controller가 로또 결과를 받아 출력 View에 전달 |
⇒ 1~3까지는 흐름에서 별 차이가 없지만 4, 5에서 차이가 난다. 로또 객체가 유효성이 통과해야만 생성되기 때문에 다른 파트에서는 로또의 유효성을 신경쓰지 않고 안전하게 사용할 수 있게 된다.
최종 설계 방향 결정
위 내용을 종합하여 보았을 때 도메인 중심 구조와 단일 책임 원칙에 신경쓰며 설계를 해보았다.
DDD 구조의 로또 발매기
com.lotto
├── domain
│ ├── Lotto.java // 로또 번호를 관리하는 도메인 객체
│ ├── LottoMachine.java // 로또를 발행하고 관리하는 도메인 객체
│ ├── Purchase.java // 구매 정보를 관리하는 도메인 객체
│ └── WinningNumbers.java // 당첨 번호를 관리하는 도메인 객체
├── service
│ ├── LottoService.java // 비즈니스 로직을 처리하는 서비스 클래스
│ └── PurchaseService.java // 구매 관련 비즈니스 로직을 처리하는 서비스 클래스
├── controller
│ └── LottoController.java // 사용자 입력을 처리하고 결과를 반환
└── View
├── InputView.java // 사용자 입력 관련 처리
└── OutputView.java // 콘솔 출력 관련 처리
👩🏻💻 새로 배운 내용
컬렉션 중 해당 카테고리가 몇개 인지 갯수 세기 :: Collectors.groupingBy
Collectors.groupingBy
는 리스트 안의 요소들을 특정 기준에 따라 그룹화해주는 메서드야. 여기서는 results
리스트에 있는 WinningResult
값을 기준으로 몇 개씩 있는지 세기 위해 사용한 거야.
이 메서드가 하는 일은 두 단계로 나눌 수 있어:
- 그룹화 기준 설정:
Collectors.groupingBy(r -> r)
는 리스트의 요소r
을 기준으로 그룹화해줘. 여기서는WinningResult
값 자체를 기준으로 그룹을 만든 거지. 예를 들어,results
리스트에WinningResult.FIRST
가 세 번,WinningResult.SECOND
가 두 번 있다면,FIRST
와SECOND
를 키로 해서 각각을 그룹으로 만들어줄 거야. - 그룹별 개수 세기:
Collectors.counting()
은 각 그룹에 속한 요소가 몇 개인지 세어주는 기능이야. 이를 통해 그룹당 개수를 세고, 최종 결과를Map<WinningResult, Long>
타입으로 반환하게 돼. 예를 들어, 결과가 다음과 같을 수 있어: java 코드 복사 {WinningResult.FIRST=3, WinningResult.SECOND=2}
이렇게 리스트의 WinningResult
값을 그룹화하면서 각 값의 개수를 세는 게 groupingBy
와 counting
의 조합이 하는 일이야.
출력 형식 정하기
- 구매 금액을 천원 단위로 쉼표 출력을 하기 위해서 사용
java.text
패키지에 소속되어 있다.
DecimalFormat decimalFormat = new DecimalFormat("#,##0.0");
0
은 반드시 표시해야하는 자리#
은 숫자가 없다면 생략가능한 자리를 의미한다.
🐜 리팩토링 예시
BEFORE
보너스 볼 일치 구문을 추가하기 위해 조건에 따라 변경될 전체 내용을 작성하였으나 중복 코드가 많은 상황
public String toStringResult(List<WinningResult> results) {
DecimalFormat formatter = new DecimalFormat("#,###");
StringBuilder stringBuilder = new StringBuilder();
for(WinningResult rank : WinningResult.values()){
if(rank.isMatchBonusNumber()) {
stringBuilder.append(rank.getMatchNumberCount()).append("개 일치, 보너스 볼 일치 ")
.append("(").append(formatter.format(rank.getPrize())).append("원)").append(" - ")
.append(countWinningResult(results, rank)).append("개")
.append("\n");
continue;
}
stringBuilder.append(rank.getMatchNumberCount()).append("개 일치").append("(")
.append(formatter.format(rank.getPrize())).append("원)").append(" - ")
.append(countWinningResult(results, rank)).append("개")
.append("\n");
}
return stringBuilder.toString();
}
AFTER
- 메소드 분리
- 반복문을 돌면서 append를 시작하되 하위 메소드를 호출하며 변수를 넘김
- 하위 메소드에서 상세한 내용 작성
- 조건문에 따라 append 한줄을 더 추가할지 말지로 단순화
public String toStringResult(List<WinningResult> results) {
DecimalFormat formatter = new DecimalFormat("#,###");
StringBuilder stringBuilder = new StringBuilder();
for (WinningResult rank : WinningResult.values()) {
stringBuilder.append(formatRankInfo(rank, results, formatter));
}
return stringBuilder.toString();
}
private String formatRankInfo(WinningResult rank, List<WinningResult> results, DecimalFormat formatter) {
StringBuilder sb = new StringBuilder();
sb.append(rank.getMatchNumberCount()).append("개 일치");
// 보너스 볼 일치 여부에 따른 처리
if (rank.isMatchBonusNumber()) {
sb.append(", 보너스 볼 일치 ");
}
sb.append("(").append(formatter.format(rank.getPrize())).append("원) - ")
.append(countWinningResult(results, rank)).append("개\n");
return sb.toString();
}
📍 3주차 피드백 내용 정리
지난 주차에 이어서 누적하여 정리를 해보았다. 1+2+3주차
주제 | 내용 |
---|---|
기능 목록 작성 | 정상 경우 뿐만 아니라 예외 상황도 함께 작성한다. |
Git | 기본적인 Git 명령어를 숙지한다. |
Git으로 관리할 자원을 고려한다. | |
해당 커밋에서 수행된 작업을 이해할 수 있도록 커밋 메시지를 의미 있게 작성한다. | |
풀 리퀘스트를 만든 후에는 닫지 말고 추가 커밋을 한다. 이미 풀 리퀘스트를 생성하면 변경을 위해 새 풀 리퀘스트를 만들 필요가 없다. 변경이 필요한 경우 추가 커밋을 하면 자동으로 반영된다. |
|
과제 제출 | 과제를 제출하기 전에 과제 진행 요구 사항, 기능 요구 사항, 프로그래밍 요구 사항을 모두 충족하였는지 다시 한번 확인한다. 이러한 요구 사항들은 미션마다 다르므로 항상 주의 깊게 읽어 본다. |
코드 포맷 | if, for, while문 사이의 공백도 코딩 컨벤션이다. |
코드 포매팅을 사용한다. | |
구현 순서도 코딩 컨벤션이므로 클래스는 상수, 멤버 변수, 생성자, 메서드 순으로 작성한다. | |
메소드 내 공백 라인도 한 라인으로 간주한다. 15라인이 넘으면 역활을 명확히 나눠 함수 분리 또는 클래스 분리를 고려해야한다는 신호이다. | |
주석 | 변수 이름, 함수(메서드) 이름을 통해 어떤 의도인지가 드러난다면 굳이 주석을 달지 않는다. |
의도를 드러내기 힘든 경우에만 주석을 다는 연습을 한다. | |
변수 활용 | 배열 대신 컬렉션을 사용한다. |
이름을 통해 의도를 드러내며 축약하지 않는다. | |
변수명에 자료형을 사용하지 않는다. 코드 작성 시점에 자연스럽게 이해될 수 있도록 한다. | |
객체 활용 | 객체는 객체답게 데이터를 get 으로 꺼내지 않고 데이터를 가지는 객체가 필요에 따라 스스로 처리하도록 구현한다. 즉, 상태 데이터를 꺼내 로직을 처리하도록 구현하지 말고 객체에 메시지를 보내 일을 하도록 리팩토링한다. |
필드(인스턴스 변수)의 수를 줄이기 위해 노력한다. 필드 수가 많아지면 객체의 복잡도가 증가하고 관리가 어려워진다. | |
테스트 | 문제를 작게 나누어 핵심 기능부터 작게 테스트를 만들어가는 것이 효과적이다. |
경계값이나 잘못된 입력에 대한 테스트를 꼼꼼히 작성하여 예기치 않은 오류를 방지해야 한다. | |
단위 테스트가 가능하도록 클래스 내부가 아닌 외부로 분리하면 좋다. 테스트하기 어려운 의존성을 외부에서 주입하거나 분리하여 테스트가 가능하도록 해야한다. | |
private 함수가 중요한 역할을 수행하는 경우, 테스트가 필요할 수 있다. 그럴 때는 단일 책임 원칙을 따르는지 체크하여 클래스 분리를 고려한다. |
|
예외 처리 | 예상되는 예외를 미리 고려하여 프로그램이 비정상적으로 종료되거나 잘못된 결과를 내지 않도록 프로그래밍 한다. |
💬 getter에 대해서 더 생각해볼까
공통 피드백에서 남겨주신 글을 참고하여 정리했다.
객체는 객체답게 데이터를 get
으로 꺼내지 않고 데이터를 가지는 객체가 필요에 따라 스스로 처리하도록 구현하기 위해서 getter
를 아예 사용하지 말아야할까? 자바 bean 설계 규약에 따르면 모든 멤버 변수에 대해 get
, set
메소드가 존재해야 한다. 출력을 위한 값 등 순수 값 프로퍼티를 가져오기 위해서 사용되는 getter
는 허용된다.
❌ 하지만 모든 멤버 변수의 getter
를 통해 상태값을 꺼내 외부에서 로직을 수행한다면 객체가 로직을 갖는 것이 아니며, 메세지를 주고 받는 형태도 아니게 되어 객체로서 제대로 활용되는 것이 아닌 셈이다.
예를 들어, 자동차 경주 프로그램에서 경주 종료 후 우승 차를 선별하기 위해 Car
클래스가 갖는 private
거리 변수를 활용해야한다. 최대 값을 찾아내어 일치하는지 작은지 크기 비교를 해야하기 때문이다. 이때, 이 private
거리 변수는 해당 객체 Car
가 직접 활용해아하는 것이 맞는 방향인 것이다.
즉, 우승 차를 선별하는 로직을 구현하기 위해 외부에서 거리 변수를 getter
로 불러와 비교하는 것이 아니라, Car
객체에게 스스로 거리를 비교할 수 있도록 메세지를 보내야한다.
내 코드에도..? 😭
왠지 내 얘기인 것 같은데 싶어서 다시 프로젝트를 열어보니 내가 제출한 코드에서도 Car
클래스의 getDistance
를 통해 값을 직접 가져와 최대 거리값을 찾고 있었다.
public Cars sortWinnerCars() {
int maxDistance = WinnerCalculator.findMaxDistance(this.getCarList());
Cars winnerCars = new Cars();
carList.forEach(car -> {
if (car.getDistance().getDistanceValue() == maxDistance) {
winnerCars.getCarList().add(car);
}
});
return winnerCars;
}
이 내용을 외부가 아니라 Car 객체가 직접 처리하도록 하려면 어떻게 할까? 거리 변수 값을 가져오는 getter
가 없다면 어떻게 했을지 생각해보면 된다.
1. 직접 Car 객체 안에서 거리값을 비교하도록 compareTo()
를 구현한다.
public Cars sortWinnerCars() {
int maxDistance = WinnerCalculator.findMaxDistance(this.getCarList());
Cars winnerCars = new Cars();
carList.forEach(car -> {
if (car.getDistance().getDistanceValue() == maxDistance) {
winnerCars.getCarList().add(car);
}
});
return winnerCars;
}
2. 그리고 Cars
에서 해당 compareTo
메소드로 호출을 보내 각 차량의 거리를 비교하는 로직을 Car
가 수행하도록 연결하면 된다.
public class Cars {
...
public List<String> findWinners() {
final Car maxPositionCar = findMaxPositionCar(); //가장 멀리 있는 Car 반환
return findSamePositionCars(maxPositionCar); //먼 거리 차와 같은 거리에 있는 차량 이름 리스트 반환
}
private Car findMaxPositionCar() {
Car maxPositionCar = cars.stream()
.max(Car::compareTo) //거리값 비교는 Car객체에서 직접 수행
.orElseThrow(() -> new IllegalArgumentException("차량 리스트가 비었습니다."));
}
private List<String> findSamePositionCar(Car maxPositionCar) {
return cars.stream()
.filter(maxPositionCar::isSamePosition) //최대 거리값과 일치하는 차량 찾기도 Car에서 수행
.map(Car::getName)
.collect(Collectors.toList());
}
외부에서는 상태값을 변경하지 못하도록 설정
어쨋든 순수 필드 변수값을 활용해야할 때도 있으니 getter가 없을 순 없다.
1️⃣ 이럴 때는 final
명령어를 통해 상수로 선언을 하거나
2️⃣ Wrapper
클래스로 감싸줘도 된다.
기본 타입을 객체로 다뤄야 할때 많이 사용
int
→Integer
double
→Double
boolean
→Boolean
int number = 10; // 기본 타입
Integer wrappedNumber = Integer.valueOf(number); // 래퍼 클래스인 Integer로 감싸기
3️⃣ 컬렉션을 이용해야한다면 Collections.unmodifiableList/Map/Set()
와 같은 메소드를 통해 외부에서 값을 가져와도 변경하지 못하도록 설정하면 된다.
- 읽기 전용 리스트로 반환해주는 메소드로 원본이 변경되면 같이 업데이트 된다.
- 안전하게 데이터를 전달할 수 있다.
public List<Car> getCars() {
return Collections.unmodifiableList(cars); //해당 리스트는 변경할 수 없음
}
✚ 규칙 4: 한 줄에 점을 하나만 찍는다. (디미터의 법칙)
이 부분도 이번에 과제를 작성하면서 소홀했던 내용 중 하나인 것 같아서 함께 정리해보려고 한다.
디미터의 법칙은 낯선 객체에 메세지를 보내는 설계를 피하라는 것이다.
🔍 여기서 자료구조는 디미터 법칙이 적용되지 않으며 내부 구조가 외부로 노출되는 경우에만 적용된다.
즉, 다른 객체를 탐색해 뭔가를 일어나게 해서는 안된다는 것인데 하나의 객체가 알아야하는 다른 객체를 최소한으로 유지하라는 의미로 최소 지식 원칙(Principle of least knowledge)라고 불린다.
이 원칙을 지키지 않은 코드 예시를 통해 중요성을 살펴보도록 한다.
아래 printCarEngineType
메소드는 Car
객체를 통해 Engine
객체의 내부 메서드인 getType
을 직접 호출하고 있다.
class Driver {
public void printCarEngineType(Car car) {
System.out.println(car.getEngine().getType());
}
}
이렇게 되면 Driver
는 Car
와 Engine
를 모두 알아야한다. 만약 둘 중 하나의 객체의 구조가 수정된다면 Driver도 같이 수정을 해야할 수 있을 정도로 두 객체의 변화에 대한 영향을 모두 받게 된다. 즉, 객체간 결합도가 높아져 유지보수에 어려움이 생길 수 있다.
이를 해결하려면 어떤 방법이 있을까? 여기서 System.out.println(car.getEngineType());
로 내용을 바꾸고 car.getEngineType()
에서는 Engine.getType
를 바로 반환해주어 Driver
가 Engine
을 몰라도 되도록 연결다리를 마련해주면 된다.
✚ 규칙 7: 2개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
클래스가 하나의 책임을 가지도록 하기 위해서 필요한 규칙이라는 것이 잘 와닿지 않았어서 이 내용도 이번 기회에 다시 정리해보려고 한다. 간단한 예로 Address
클래스에 city
, state
, postalCode
과 같은 인스턴스 변수가 있다면 Address
클래스는 자동차의 3가지 특성을 표현하고 관리해야하는 다양한 책임을 갖게 된다.
해당 내용은 분리가 가능하다.
class Address {
String city; // 도시
String state; // 주
String postalCode; // 우편번호
}
class Street {
String name; // 거리 이름
String number; // 거리 번호
}
class City {
String name; // 도시 이름
}
class PostalCode {
String code; // 우편번호
}
위와 같이 Address
객체를 속성의 집합보다는 협력 객체의 계층 구조로 분해를 한다면 더 효율적인 모델이 될 수 있다.
📌 코드라인 지표 평가
패키지 단위
- domain > service > view > controller 순으로 NCLOC가 구성되어 있다.
클래스 단위
이번에는 테스트 코드도 작성한 클래스가 좀 있어서 분석에 포함하였다.
Class | CLOC | JLOC | LOC |
---|---|---|---|
lotto.Application | 0 | 0 | 6 |
lotto.ApplicationTest | 0 | 0 | 47 |
lotto.LottoControllerTest | 0 | 0 | 9 |
lotto.LottoMachineTest | 0 | 0 | 10 |
lotto.LottoTest | 1 | 0 | 14 |
lotto.PurchaseServiceTest | 0 | 0 | 11 |
lotto.PurchaseTest | 0 | 0 | 12 |
lotto.WinningNumbersTest | 0 | 0 | 20 |
lotto.controller.LottoController | 0 | 0 | 34 |
lotto.domain.Lotto | 0 | 0 | 42 |
lotto.domain.LottoMachine | 0 | 0 | 43 |
lotto.domain.Purchase | 0 | 0 | 15 |
lotto.domain.WinningNumbers | 1 | 0 | 34 |
lotto.domain.WinningResult | 0 | 0 | 25 |
lotto.service.LottoService | 1 | 0 | 43 |
lotto.service.PurchaseService | 0 | 0 | 20 |
lotto.view.InputView | 0 | 0 | 42 |
lotto.view.OutputView | 0 | 0 | 14 |
Total | 3 | 0 | 441 |
Average | 0.17 | 0.00 | 24.50 |
- 총 18개의 클래스를 종합하여 전체 코드 라인 수가 441, 평균 한 클래스당 24.50 코드라인을 갖고 있다.
메소드 단위
Method | CLOC | JLOC | LOC | NCLOC | RLOC |
---|---|---|---|---|---|
lotto.Application.main(String[]) | 0 | 0 | 4 | 4 | 66.67% |
lotto.ApplicationTest.runMain() | 0 | 0 | 4 | 4 | 8.51% |
lotto.ApplicationTest.기능_테스트() | 0 | 0 | 33 | 33 | 70.21% |
lotto.ApplicationTest.예외_테스트() | 0 | 0 | 7 | 7 | 14.89% |
lotto.LottoControllerTest.로또구매_테스트() | 0 | 0 | 7 | 7 | 77.78% |
lotto.LottoMachineTest.갯수만큼_로또생성_출력() | 0 | 0 | 7 | 7 | 70.00% |
lotto.LottoTest.로또_번호에_중복된_숫자가_있으면_예외가_발생한다() | 0 | 0 | 6 | 6 | 42.86% |
lotto.LottoTest.로또_번호의_개수가_6개가_넘어가면_예외가_발생한다() | 0 | 0 | 5 | 5 | 35.71% |
lotto.PurchaseServiceTest.수익률계산() | 0 | 0 | 8 | 8 | 72.73% |
lotto.PurchaseTest.로또구매개수_테스트() | 0 | 0 | 5 | 5 | 41.67% |
lotto.PurchaseTest.천워이하단위_에러테스트() | 0 | 0 | 5 | 5 | 41.67% |
lotto.WinningNumbersTest.당첨_2등_테스트() | 0 | 0 | 6 | 6 | 30.00% |
lotto.WinningNumbersTest.당첨_3등_테스트() | 0 | 0 | 6 | 6 | 30.00% |
lotto.WinningNumbersTest.당첨_계산_테스트() | 0 | 0 | 6 | 6 | 30.00% |
lotto.controller.LottoController.calculateLotto(List, int) | 0 | 0 | 7 | 7 | 20.59% |
lotto.controller.LottoController.makeLotto(int) | 0 | 0 | 5 | 5 | 14.71% |
lotto.controller.LottoController.makeWinningNumbers() | 0 | 0 | 6 | 6 | 17.65% |
lotto.controller.LottoController.purchaseLotto() | 0 | 0 | 10 | 10 | 29.41% |
lotto.domain.Lotto.Lotto(List) | 0 | 0 | 4 | 4 | 9.52% |
lotto.domain.Lotto.checkWinningResult(int, boolean) | 0 | 0 | 18 | 18 | 42.86% |
lotto.domain.Lotto.getNumbers() | 0 | 0 | 3 | 3 | 7.14% |
lotto.domain.Lotto.hasDuplicates(List) | 0 | 0 | 3 | 3 | 7.14% |
lotto.domain.Lotto.toString() | 0 | 0 | 3 | 3 | 7.14% |
lotto.domain.Lotto.validate(List) | 0 | 0 | 8 | 8 | 19.05% |
lotto.domain.LottoMachine.LottoMachine() | 0 | 0 | 3 | 3 | 6.98% |
lotto.domain.LottoMachine.createLottos(int) | 0 | 0 | 8 | 8 | 18.60% |
lotto.domain.LottoMachine.createWinningNumbers(List, int) | 0 | 0 | 3 | 3 | 6.98% |
lotto.domain.LottoMachine.getInstance() | 0 | 0 | 6 | 6 | 13.95% |
lotto.domain.LottoMachine.getLottos() | 0 | 0 | 3 | 3 | 6.98% |
lotto.domain.LottoMachine.makeNumbers() | 0 | 0 | 8 | 8 | 18.60% |
lotto.domain.LottoMachine.printAllLottoNumbers() | 0 | 0 | 4 | 4 | 9.30% |
lotto.domain.LottoMachine.sortNumbers(List) | 0 | 0 | 4 | 4 | 9.30% |
lotto.domain.Purchase.Purchase(int) | 0 | 0 | 4 | 4 | 26.67% |
lotto.domain.Purchase.calculatePurchasableLotto() | 0 | 0 | 3 | 3 | 20.00% |
lotto.domain.Purchase.validate(int) | 0 | 0 | 5 | 5 | 33.33% |
lotto.domain.WinningNumbers.WinningNumbers(List, int) | 0 | 0 | 7 | 7 | 20.59% |
lotto.domain.WinningNumbers.getInstance(List, int) | 0 | 0 | 6 | 6 | 17.65% |
lotto.domain.WinningNumbers.isBonusNumberMatch(Lotto) | 0 | 0 | 3 | 3 | 8.82% |
lotto.domain.WinningNumbers.matchNumberCount(Lotto) | 1 | 0 | 5 | 5 | 14.71% |
lotto.domain.WinningNumbers.matchWinningNumbers() | 0 | 0 | 8 | 8 | 23.53% |
lotto.domain.WinningResult.WinningResult(int, boolean, int) | 0 | 0 | 5 | 5 | 20.00% |
lotto.domain.WinningResult.getMatchNumberCount() | 0 | 0 | 3 | 3 | 12.00% |
lotto.domain.WinningResult.getPrize() | 0 | 0 | 3 | 3 | 12.00% |
lotto.domain.WinningResult.isMatchBonusNumber() | 0 | 0 | 3 | 3 | 12.00% |
lotto.service.LottoService.countLottoMatchNumbers(List, int) | 0 | 0 | 4 | 4 | 9.30% |
lotto.service.LottoService.countWinningResult(List, WinningResult) | 0 | 0 | 6 | 6 | 13.95% |
lotto.service.LottoService.formatRankInfo(WinningResult, List) | 1 | 0 | 12 | 11 | 27.91% |
lotto.service.LottoService.makeLotto(int) | 0 | 0 | 4 | 4 | 9.30% |
lotto.service.LottoService.makeWinningLotto(List, int) | 0 | 0 | 4 | 4 | 9.30% |
lotto.service.LottoService.printLottoNumbers() | 0 | 0 | 4 | 4 | 9.30% |
lotto.service.LottoService.toStringResult(List) | 0 | 0 | 7 | 7 | 16.28% |
lotto.service.PurchaseService.buyLotto(int) | 0 | 0 | 5 | 5 | 25.00% |
lotto.service.PurchaseService.calculateTotalPrize(List) | 0 | 0 | 7 | 7 | 35.00% |
lotto.service.PurchaseService.calculateWinningPercent(List) | 0 | 0 | 5 | 5 | 25.00% |
lotto.view.InputView.getBonusNumber() | 0 | 0 | 7 | 7 | 16.67% |
lotto.view.InputView.getPrice() | 0 | 0 | 7 | 7 | 16.67% |
lotto.view.InputView.getWinningNumbers() | 0 | 0 | 9 | 9 | 21.43% |
lotto.view.InputView.isNumber(String) | 0 | 0 | 3 | 3 | 7.14% |
lotto.view.InputView.isNumbers(String) | 0 | 0 | 3 | 3 | 7.14% |
lotto.view.InputView.validateNumberInput(String) | 0 | 0 | 5 | 5 | 11.90% |
lotto.view.InputView.validateNumbersInput(String) | 0 | 0 | 6 | 6 | 14.29% |
lotto.view.OutputView.printLottoResult(String) | 0 | 0 | 5 | 5 | 35.71% |
lotto.view.OutputView.printPurchaseResult(int) | 0 | 0 | 3 | 3 | 21.43% |
lotto.view.OutputView.printWinningPercent(double) | 0 | 0 | 4 | 4 | 28.57% |
Total | 2 | 0 | 380 | 379 | |
Average | 0.03 | 0.00 | 5.94 | 5.92 | 18.69% |
- 리팩토링을 더 진행한다면 내가 작성한 코드 중에서 RLOC가 높은 순으로 lotto.controller.LottoController.purchaseLotto() - 10라인 29.41% RLOC-, lotto.domain.Lotto.checkWinningResult(int, boolean) -18라인 42.86% RLOC -를 우선적으로 고려해야겠다.
⭐️ 3주차 회고
1,2주차에는 넣지 않았던 서비스 레이어를 설계에 추가하고 나니 컨트롤러와 비즈니스 로직 처리가 훨씬 단정해지는 것 같다는 느낌을 받았다. 특히 로또 구매 서비스와 로또 생성 및 관리 서비스를 따로 나누어서 작성하였는데, 수익률을 계산하는 로직을 구현하면서 초기에 로또 구매 로직을 실행하기 위해 필드에 저장해두었던 값을 재사용할 수 있게 되면서 나누길 잘했다는 생각을 했다. 두 서비스를 모두 컨트롤러에서 필요에 따라 활용하면서 전체 로직을 연결할 수 있게 되니 흐름이 덜 복잡하게 정리된 것 같아서 이전 과제에서보다는 나은 구조가 된 것 같았다.
프리코스 1~3주차 과제 RLOC 현황
1,2,3주차의 총 라인수와 평균 메소드 라인수, 평균 RLOC(Real Lines of Code)의 동태는 아래와 같다.
주차 | 총 라인 수 | 평균 메소드 LOC | 평균 메소드 RLOC |
---|---|---|---|
1주차 | 75 | 6.22 | 23.14% |
2주차 | 318 | 5.39 | 19.19% |
3주차 | 380 | 5.94 | 18.69% |
최근으로 올수록 총 라인수가 증가하고 있으며 반대로 평균 메소드 RLOC(Real Lines of Code)는 감소하고 있다. 즉, 프로젝트가 커지지만 실제 실행되는 메소드들의 효율성은 증가하고 있다는 것을 보여주고 있다. 이 내용을 토대로 보았을 때 이번 프리코스에 참가한 목표인 리팩토링과 효율적인 코드 작성법에 대한 학습이 유의미했다는 것을 알 수 있다!
테스트코드는 1주차 분석에서 제외되었으며 1주차 과제에서는 기본적으로 제공된 테스트코드 1개 클래스외에 내가 추가한 입력값 유효성 검사 클래스(메소드 4개, 각 3줄)만 있었으므로, 포함 여부가 결과값에 미치는 영향은 미미할 것으로 보인다.
다만 평균 메소드 LOC는 2주차에서 감소되었다가 이번 3주차에서 증가되었는데, 아마 구현할 기능이 점점 늘어나면서 기능의 완성에 치중하여 리팩토링에 대한 시간을 덜 쏟았기 때문인 것 같다.
다음 4주차 과제에서는 메소드 길이가 길어지지 않도록 유의하면서도 기능적 구현이 일찍 끝난다면 리팩토링에도 조금 신경을 더 쏟을 수 있도록 시간을 마련하는 것을 목표로 삼고 싶다.
객체 지향 코드 작성 점검
객체지향적인 코드를 작성하기 위한 목록들을 다시 한번 읽어보며 기존 작성들을 점검해야겠다.
- 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
- 규칙 2: else 예약어를 쓰지 않는다.
- 규칙 3: 모든 원시값과 문자열을 포장한다.
- 규칙 4: 한 줄에 점을 하나만 찍는다. (디미터의 법칙)
- 규칙 5: 줄여쓰지 않는다(축약 금지).
- 규칙 6: 모든 엔티티를 작게 유지한다.
- 규칙 7: 2개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
- 규칙 8: 일급 콜렉션을 쓴다.
- 규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.
'프리코스' 카테고리의 다른 글
[프리코스 7기 백엔드] 4주차 과제 회고 :: 프리코스 참여를 마무리하며 (1) | 2025.01.03 |
---|---|
[프리코스 7기 백엔드] 2주차 과제 회고 (+ JUnit, AssertJ, java.util.Comparator) (0) | 2024.10.29 |
[프리코스 7기 백엔드] 1주차 과제 회고 (+ MetricsReloaded 플러그인으로 리팩토링 효과성 평가) (0) | 2024.10.29 |