Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] 김경규 오늘의 짝꿍은? #10

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
testImplementation platform('org.assertj:assertj-bom:3.25.1')
testImplementation('org.junit.jupiter:junit-jupiter')
testImplementation('org.assertj:assertj-core')
testImplementation('org.mockito:mockito-core')
}

tasks.named('test') {
Expand Down
188 changes: 173 additions & 15 deletions src/main/java/leets/leets_mate/LeetsMateApplication.java
Original file line number Diff line number Diff line change
@@ -1,42 +1,200 @@
package leets.leets_mate;

import leets.leets_mate.validation.exception.InvalidInputException;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;


public class LeetsMateApplication {

private static final int MAX_ERROR_COUNT = 5; // 최대 입력 횟수
private static final int INITIAL_ERROR_COUNT = 0; // 입력 횟수 세기 시작
private static final Pattern IS_KOREAN = Pattern.compile("^[ㄱ-ㅎㅏ-ㅣ가-힣]*$"); // regex 미리 컴파일
private static final Pattern IS_INTEGER = Pattern.compile("\\d+");
private BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

// 테스트 코드 실행을 위한 BufferedReader Setter
public void setBufferedReader(BufferedReader br) {
this.br = br;
}

public static void main(String[] args) {
LeetsMateApplication app = new LeetsMateApplication();
app.run();
}

// 동작 함수입니다.
public void run() {
/** 설계
* 1. 이름 입력 받기
* a. 입력 받기 (read)
* b. String -> List (parseMember)
* b. regex (checkHasNoEnglish)
* 2. 최대 짝(그룹) 인원 수 입력 받기
* a. 총 멤버 수 계산 (memberNumber)
* b. 입력 받기 (read)
* c. int형으로 안전하게 변환 (convertToInteger)
* d. '멤버 수 > 팀당 인원 수' 검사 (checkDataValidity)
* e. 최대 짝(그룹) 인원 수 입력 받기 (getSize)
* 3. 짝 랜덤 추첨 -> 출력
* a. 짝 추첨 실행 (executeDraw)
* b, 짝 생성 (generateRandomGroups)
* c. 결과 출력 (printResult)
* d. 추첨을 멈출 것인지 결정 (isBreak)
*
* 예외) 사용자가 5번 이상 올바르지 않은 값을 입력할 시 프로그램 종료
*/

System.out.println("""
[Leets 오늘의 짝에게]를 시작합니다.

멤버의 이름을 입력해주세요. (, 로 구분)""");

// 1. 이름 입력 받기
List<String> memberList = parseMembers(INITIAL_ERROR_COUNT);

// 2. 인원 수 입력 받기
System.out.println("최대 짝 수를 입력해주세요.");
int size = getSize(memberNumber(memberList), INITIAL_ERROR_COUNT);

// 3. 짝 추첨 -> 출력
executeDraw(memberList, size);
}

// 문자열로된 멤버들을 리스트로 분리하는 함수입니다.
public List<String> parseMembers(String members) {
return new ArrayList<>();
public List<String> parseMembers(int errorCnt) {
try {
return Arrays.stream(read(errorCnt).split(","))
.peek(this::checkHasNoEnglish) // validation
.collect(Collectors.toList());
} catch (InvalidInputException e) {
System.out.println(e.getMessage());
return parseMembers(++errorCnt);
}

}

// 총 멤버수를 반환합니다.
public int memberNumber(List<String> members) {
return 0;
}

// 멤버 문자열에 영어가 있는지 검사합니다. 영어가 있다면 예외 출력
public void checkHasNoEnglish(String members) {
}

// 멤버수와 최대 짝수 데이터가 유효한지 검사하는 함수입니다. 유효하지 않다면 예외 출력
public void checkDataValidity(int memberCount, int maximumGroupSize) {
return members.size();
}

// 랜덤 짝꿍 추첨하는 함수 입니다.
public List<List<String>> generateRandomGroups(List<String> memberList, int maximumGroupSize) {
return new ArrayList<>();
Collections.shuffle(memberList); // 리스트 셔플

int groupCnt = (int) Math.ceil((double) memberList.size() / maximumGroupSize);
return IntStream.range(0, groupCnt) // 0 이상 그룹 개수 미만 범위 루프
.mapToObj(i -> memberList.subList(i * maximumGroupSize,
Math.min((i + 1) * maximumGroupSize, memberList.size()) // 마지막 팀에 인원이 가득 차지 않았을 때 범위를 넘어가지 않기 위해 min 연산
))
.collect(Collectors.toList());
}

// 결과를 프린트 하는 함수입니다.
public void printResult(List<List<String>> result) {
StringJoiner groupJoiner = new StringJoiner("\n");
for (List<String> group : result) {
StringJoiner memberJoiner = new StringJoiner(" | ", "[ ", " ]");
for (String member : group) {
memberJoiner.add(member);
}
groupJoiner.add(memberJoiner.toString());
}
System.out.println(groupJoiner);
}

public static void main(String[] args) {
LeetsMateApplication app = new LeetsMateApplication();
app.run();
// 원하는 결과가 나올 때까지 추첨을 진행하는 함수입니다.
public void executeDraw(List<String> memberList, int size) throws InvalidInputException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 코드도 좋지만, do~while문이 조건이 뒤에 있기때문에 가독성 면에서 안좋다는 의견도 있습니다.
때문에 아래와 같은 코드도 한번 보시면 좋을거 같습니다. while 없이 재귀로 해결하여 가독성은 좀 더 좋아질 수 있다 생각합니다.
`public void executeDraw(List memberList, int size) throws InvalidInputException {
System.out.println("\n오늘의 짝 추천 결과입니다.");

    List<List<String>> result = generateRandomGroups(memberList, size); // 짝 추첨
    printResult(result);    // 출력
    System.out.println("추천을 완료했습니다.");

    if (isBreak()) {
        break;
    }
    executeDraw(memberList, size);
}`

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

초기엔 재귀로 코드를 짰었는데, 만약 사용자가 악의적으로 프로그램에 입력을 잘못 입력할 시엔 메모리가 계속 할당되어 스택 오버플로우가 발생할 수 있다는 생각도 했어서 넓은 의미로 루프문으로 고치게 되었습니다..!
근데 현재 상황은 그 정도까지 고려하는건 투머치라고 생각되네요.. 다음엔 문제 상황에 좀 더 알맞게 구성해보겠습니당 감사합니다!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 재귀로 사용했는데요! 스택오버플로를 고려하기도 했지만 현재는 오버엔지니어링이라 판단해 제한을 두지 않았습니다. 재귀로 사용한다면 tryCount를 두어 입력 횟수에 제한을 두는 것도 방법일 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 역시나 투머치였군요..! 입력 횟수 제한 좋은 방법인 것 같아요 추가해보도록 하겠슴당 감사합니다!

System.out.println("\n오늘의 짝 추천 결과입니다.");

List<List<String>> result = generateRandomGroups(memberList, size); // 짝 추첨
printResult(result); // 출력
System.out.println("추천을 완료했습니다.");

if (!isBreak(INITIAL_ERROR_COUNT))
executeDraw(memberList, size);
}

// 추첨을 멈출 것인지 결정하는 함수입니다.
public boolean isBreak(int errorCnt) {
try {
System.out.print("다시 구성하시겠습니까? (y or n): ");
String input = read(errorCnt);

if (input.equals("y")) { // 재구성
System.out.println("--------------------------------");
return false;
}

if (input.equals("n")) { // 고정
System.out.println("자리를 이동해 서로에게 인사해주세요.");
return true;
}

// 그 외의 잘못된 응답 처리
throw new InvalidInputException("[ERROR] 응답은 y 혹은 n으로 입력해야 합니다.");
} catch (InvalidInputException e) {
System.out.println(e.getMessage());
return isBreak(++errorCnt);
}
}

// 사용자의 모든 입력을 처리하는 함수입니다.
public String read(int errorCnt) {
try {
if (errorCnt >= MAX_ERROR_COUNT)
throw new InvalidInputException("[ERROR] 5회 이상 잘못 입력하셨습니다.\n프로그램을 종료합니다.");


return Optional.of(br.readLine())
.filter(input -> !input.trim().isEmpty())
.orElseThrow(() -> new InvalidInputException("[ERROR] 값을 입력해주세요.")); // validation
} catch (InvalidInputException e) {
System.out.println(e.getMessage());

if (errorCnt >= MAX_ERROR_COUNT)
System.exit(0);

return read(++errorCnt);
} catch (IOException e) {
throw new RuntimeException("[ERROR] 입력을 읽는 중 오류가 발생했습니다.", e);
}
}

// 그룹의 크기를 가져오는 함수입니다.
public int getSize(int memberCnt, int errorCnt) {
try {
return checkDataValidity(memberCnt, convertToInteger(read(errorCnt)));
} catch (InvalidInputException e) {
System.out.println(e.getMessage());
return getSize(memberCnt, ++errorCnt);
}
}

// validation
public void checkHasNoEnglish(String members) throws InvalidInputException {
if (!IS_KOREAN.matcher(members).matches())
throw new InvalidInputException("[ERROR] 이름은 공백없이 한글로 입력해야 합니다. 다시 입력해주세요.");
}

public int checkDataValidity(int memberCount, int maximumGroupSize) throws InvalidInputException {
if (memberCount < maximumGroupSize)
throw new InvalidInputException("[ERROR] 최대 짝 수는 이름의 갯수보다 클 수 없습니다. 다시 입력해주세요.");
if (maximumGroupSize < 1)
throw new InvalidInputException("[ERROR] 최대 짝 수는 1보다 작을 수 없습니다. 다시 입력해주세요.");
return maximumGroupSize;
}

public int convertToInteger(String input) throws InvalidInputException {
if (!IS_INTEGER.matcher(input).matches())
throw new InvalidInputException("[ERROR] 알맞은 짝의 크기를 입력해주세요.");
return Integer.parseInt(input);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package leets.leets_mate.validation.exception;

public class InvalidInputException extends RuntimeException {
public InvalidInputException(String message) {
super(message);
}
}
15 changes: 12 additions & 3 deletions src/test/java/leets/leets_mate/LeetsMateApplicationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;

class LeetsMateApplicationTests {
LeetsMateApplication app;
Expand All @@ -18,9 +22,14 @@ void setUp() {
}

@Test
void 입력받은_문자열을_파싱하여_리스트로_만든다() {
void 입력받은_문자열을_파싱하여_리스트로_만든다() throws IOException {
String members = "리츠에,오신,걸,환영합니다";
List<String> actual = app.parseMembers(members);

BufferedReader bufferedReaderMock = Mockito.mock(BufferedReader.class);
when(bufferedReaderMock.readLine()).thenReturn(members);
app.setBufferedReader(bufferedReaderMock);

List<String> actual = app.parseMembers(0);
assertThat(actual).containsExactly("리츠에", "오신", "걸", "환영합니다");
}

Expand Down