[STUDY] Class 2

joy0987·2023년 7월 23일

스터디

목록 보기
1/4

문제

🚀 기능 요구 사항

우아한테크코스에서는 교육생(이하 크루) 간 소통 시 닉네임을 사용한다. 간혹 비슷한 닉네임을 정하는 경우가 있는데, 이러할 경우 소통할 때 혼란을 불러일으킬 수 있다.

혼란을 막기 위해 크루들의 닉네임 중 같은 글자가 연속적으로 포함 될 경우 해당 닉네임 사용을 제한하려 한다. 이를 위해 같은 글자가 연속적으로 포함되는 닉네임을 신청한 크루들에게 알려주는 시스템을 만들려고
한다.

신청받은 닉네임 중 같은 글자가 연속적으로 포함 되는 닉네임을 작성한 지원자의 이메일 목록을 return 하도록 solution 메서드를 완성하라.

제한사항

  • 두 글자 이상의 문자가 연속적으로 순서에 맞추어 포함되어 있는 경우 중복으로 간주한다.
  • 크루는 1명 이상 10,000명 이하이다.
  • 이메일은 이메일 형식에 부합하며, 전체 길이는 11자 이상 20자 미만이다.
  • 신청할 수 있는 이메일은 email.com 도메인으로만 제한한다.
  • 닉네임은 한글만 가능하고 전체 길이는 1자 이상 20자 미만이다.
  • result는 이메일에 해당하는 부분의 문자열을 오름차순으로 정렬하고 중복은 제거한다.

실행 결과 예시

formsresult
[ ["jm@email.com", "제이엠"], ["jason@email.com", "제이슨"], ["woniee@email.com", "워니"], ["mj@email.com", "엠제이"], ["nowm@email.com", "이제엠"] ]["jason@email.com", "jm@email.com", "mj@email.com"]


구현 전 고려한 내용

  • JAVA 17 사용 (처음 써보았다!)
  • MVC2 패턴을 최대한 적용해보기
  • 사용자에게 가입 정보를 입력받는 서비스가 아니라, 기업 내부에서 사용하는 시스템으로서 처음에 회원 리스트 전체를 받는 것이라고 생각하고 구현 시작
  • 한 메서드가 하나의 역할만 하도록 하기
  • 문자열을 어떻게 해야 효율적으로 다룰 수 있을까?


구현 과정 및 피드백 내용

1. 입출력 클래스 구현

  • 입출력을 실행하는 클래스이기에 InputHandler, OutputHandler 라는 이름으로 입출력 클래스를 만들었다.

InputHandler.java

public InputHandler() {
        this.br = new BufferedReader(new InputStreamReader(System.in));
    }

    public String input() {
        try {
            return br.readLine();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

OutputHandler.java

public class OutputHandler {

    public void message(String message) {
        System.out.print(message);
    }
}

Interface 를 사용했다면 추상화와 재사용성이 높은 코드가 될 수 있었을 것이다. BufferedReader 가 아니고 Scanner 를 사용해도 실행될 수 있는 코드로 리팩토링이 필요하다는 지적을 받았다.



2. 크루, 크루 리스트 엔티티 생성

  • 이메일과 닉네임을 필드로 가지고 있는 크루 엔티티 클래스를 만들었다.

Crew.java

public class Crew {

    private final String email;
    private final String nickname;

    public Crew(String email, String nickname) {
        this.email = email;
        this.nickname = nickname;
    }

    public String getEmail() {
        return email;
    }

    public String getNickname() {
        return nickname;
    }

    @Override
    public String toString() {
        return "Crew{" +
                "email='" + email + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }
}
  • 크루를 담고 있는 리스트를 매번 새로 생성하고 싶지 않아서 크루 리스트를 관리하는 CrewList 클래스를 추가로 만들었다. (더 좋은 방법이 있지 않았을까... 고민이 필요할 것 같다.)
  • 여러 클래스에서 크루 리스트를 사용할 것이고, 하나의 인스턴스로만 관리하고 싶었기 때문에 싱글톤 패턴으로 구현했다.

❗ 필드가 모두 private final 이기 때문에 JAVA 14 부터 지원된 Record 로 만들기도 가능했지만 지식 부족(...)으로 사용하지 못했다. 다음에는 레코드를 꼭 사용해봐야겠다!

CrewList.java

public class CrewList {

    private List<Crew> crewList;
    private List<String> emailList;
    private List<String> nicknameList;

    private static final CrewList instance = new CrewList();

    private CrewList() {
        crewList = new LinkedList<>();
        this.emailList = getEmailList(crewList);
        this.nicknameList = getNicknameList(crewList);
    }

    public static CrewList getInstance() {
        return instance;
    }

    public List<Crew> getCrewList() {
        return crewList;
    }

    public List<String> getEmailList() {
        return emailList;
    }

    public List<String> getNicknameList() {
        return nicknameList;
    }

    public void setCrewList(Crew crew) {
        crewList.add(crew);
    }

    public List<String> getEmailList(List<Crew> crewList) {
        return this.crewList.stream()
                .map(Crew::getEmail)
                .collect(Collectors.toList());
    }

    public List<String> getNicknameList(List<Crew> crewList) {
        return this.crewList.stream()
                .map(Crew::getNickname)
                .collect(Collectors.toList());
    }
}

❗ getEmailList, getNicknameList 를 통해 여러번 참조하지 않고도 한번에 이메일과 닉네임 리스트에 접근하는 식으로 구현하고 싶었지만 그러지 못했고, 사용하지 않은 메서드가 되었다. 여러번의 참조와 반복문은 덤...💦

❗ 싱글톤 패턴에서 getter 를 사용할 때, 원본을 가져다주지 않도록 해야한다!



3. 이메일, 닉네임 검증 클래스 구현

  • 최대, 최소 길이를 그냥 숫자로 표현해두면 보는 사람으로 하여금 '이게 뭐임????' 싶을 수 있다. 그래서 상수를 적극적으로 사용해보았다.
  • 메서드 이름만 봐도 무슨 역할을 하는 지 알 수 있도록 네이밍에 신경썼다.

EmailValidator.java

public class EmailValidator {
    private final String EMAIL_FORMAT = "@email.com";
    // 11자 이상, 20자 미만
    private final int MIN_LENGTH = 11;
    private final int MAX_LENGTH = 20;

    public boolean valid(String email) {
        return formatValid(email) && sizeValid(email);
    }

    private boolean formatValid(String email) {
        return replace(email).endsWith(EMAIL_FORMAT);
    }

    private boolean sizeValid(String email) {
        return replace(email).length() >= MIN_LENGTH
                && replace(email).length() < MAX_LENGTH;
    }

}

NicknameValidator.java 도 같은 방식으로 작성했다.



4. 최종 검증 클래스 구현

  • 이번 문제의 가장 핵심인 '닉네임이 2글자 이상 연속으로 겹치는 경우 찾아내기' 로직을 구현한 부분이다.

  • 실행 과정
    리스트 길이 검증 ➡ 닉네임과 이메일 형식, 길이 검증 ➡ 중복된 경우를 찾아내서 리스트에 담기 ➡ 컨트롤러에서 리스트를 반환받기 ➡ solution() 메서드에서 size 가 0 이상일 경우 result 를 출력 OR size 가 0 일 경우 '중복없음' 출력

isDuplicate() : 닉네임이 2글자 이상 중복된 경우를 찾고, 해당 크루의 이메일을 리스트에 담는다.

private void isDuplicate(Crew currentCrew, Crew nextCrew) {
        String currentNickname = currentCrew.getNickname();
        String nextNickname = nextCrew.getNickname();
        for (int i = 0; i < currentNickname.length() - SUB_LENGTH; i++) {
            String subPrevNickname = currentNickname.substring(i, i + SUB_LENGTH);
            if (nextNickname.contains(subPrevNickname)) {
                duplicateEmailList.add(currentCrew.getEmail());
                duplicateEmailList.add(nextCrew.getEmail());
            }
        }
    }

findDuplicateNickname() : 리스트를 반복문으로 돌면서, 현재 크루와 다음 크루들의 닉네임을 비교한다.

 public List<String> findDuplicateNickname() {
        List<Crew> crewList = CrewList.getInstance().getCrewList();
        for (int i = 0; i < crewList.size() - 1; i++) {
            Crew currentCrew = crewList.get(i);
            for (int j = i + 1; j < crewList.size(); j++) {
                Crew nextCrew = crewList.get(j);
                isDuplicate(currentCrew, nextCrew);
            }
        }
        return duplicateEmailList;
    }

❗ 이 메서드는 검증의 역할이기 보다는 로직이기 때문에 Validator.java 클래스가 아니라 Service.java 를 만들어서 구현했어야하지 않냐는 지적을 받았다. 메서드의 큰 역할을 잘 생각해야겠다.



5. 문자열을 다루는 메서드

  • 처음 크루 리스트를 입력받을 때, [["jm@email.com", "제이엠"], ["jason@email.com", "제이슨"], ["woniee@email.com", "워니"], ["mj@email.com", "엠제이"], ["nowm@email.com", "이제엠"]] 식으로 입력을 받는다.
  • 나는 저기서 안 쪽의 대괄호를 기준으로 Crew 인스턴스를 생성하고, CrewList 의 crewList 변수에 담아주는 방식으로 구현했다.
  • 때문에 대괄호와 따옴표, 쉼표 등을 제거하는 메서드를 만들 수 밖에 없었다.
  • 바깥의 대괄호를 제거하는 메서드 / ,(공백)를 기준으로 분리해서 Crew 를 생성하는 메서드 / "" 를 지우는 메서드를 각각 만들었다.

replace() : 따옴표를 제거

public static String replace(String str) {
        return str.replace("\"", "");
    }

parseArray() : 바깥쪽의 대괄호를 제거하고, 안쪽의 대괄호를 기준으로 나눠, 배열에 담아 리턴하는 메서드

private String[] parseArray(String inputCrewData) {
        String substrCrewData = inputCrewData.substring(2, inputCrewData.length() - 2);
        return substrCrewData.split("], ?\\[");
    }

serCrewList() : "aaa@aaa.com", "닉네임" 에서 ,(공백)을 기준으로 나눠서 이메일과 닉네임을 분리한 뒤, 새로운 Crew 를 생성하여 CrewList의 crewList 필드에 담는 메서드

private void setCrewList(String inputCrewData) {
        for (String crewList : parseArray(inputCrewData)) {
            String[] crew = crewList.split(", | ");
            validator.crewValid(crew);
            CrewList.getInstance().setCrewList(new Crew(crew[0], crew[1])); // [0]="이메일", [1]="닉네임"
        }
    }

  • on() 에서 위를 활용한 일련의 과정을 거치고, 마지막에 solution() 을 실행시켰다.

on() 과 Controller 기본 생성자

public Controller() {
        validator = new Validator();
        input = new InputHandler();
        output = new OutputHandler();
        on();
    }

    private void on() {
        output.message("크루 정보 입력 => ");
        String inputCrewData = input.input();
        validator.crewListValid(inputCrewData);
        setCrewList(inputCrewData);
        validator.crewListLengthValid();
        solution(); // 여기서 실행!
    }

solution()

private void solution() {
        duplicateList = validator.findDuplicateNickname();
        if (duplicateList.size() > 0) {
            String result = duplicateList.stream().distinct().sorted().toList().toString();
            output.message("result : " + result);
            return;
        }
        output.message("👍 중복없음!");
    }

❗ stream() 을 사용하는 경우, 'stream().어쩌구.저쩌구...' 이런식으로 나열하는 게 아니라, 한 메서드마다 엔터를 쳐줘야한다. 이 부분을 고려하지 못했다. 코드의 가독성을 항상 고려해야겠다!



실행결과

회원을 더 추가해도 잘 실행된다😎



알게된 점

  • Record 의 개념
  • 내가 쓴 코드를 내가 설명 못하는 건 정말 별로였다... 내가 생각한 로직의 방향을 리드미에 잘 정리해두는 습관을 들여야겠다.
  • Set 자료구조를 통해 중복을 제거하는 방법도 있다.
  • 싱글톤 패턴에서 get필드를 할 때, 원본을 갖다주지 않도록 주의하기!
// 수정 전
public List<Crew> getCrewList() {
        return crewList;
    }
    
// 수정 후
public List<Crew> getCrewList() {
        return new LinkedList<>(crewList);
    }
profile
아자아자

1개의 댓글

comment-user-thumbnail
2023년 7월 23일

이런 유용한 정보를 나눠주셔서 감사합니다.

답글 달기