본문 바로가기
Back-end/멋쟁이사자처럼

[멋쟁이사자처럼] level1~4 구현

by 잔디🌿 2024. 3. 12.

    10주동안 todo list를 클론코딩하는 과제를 한다고 하셨다!

    내가 알던 클론코딩은 거의 그대로 코드를 쓰는 것이었는데 여기선 level마다 구현해야 할 기능이 있고, 이들을 구현하여 test에 성공해야한다. 재밌을 것 같으면서도 두렵다......

     

    level 1

     

    Member.java

     

    <비밀번호 일치하지 않으면 예외>

     public void login(String password) {
            // TODO [1단계] 입력받은 password가 이 객체의 password와 같은지 확인하세요. 같지 않다면 "비밀번호가 일치하지 않습니다" 메시지와 함께 UnAuthorizedException을 발생시키세요.
            //해결
            if(!password.equals(this.password)){
                throw new UnAuthorizedException("비밀번호가 일치하지 않습니다");
            }
        }

    방학 과제때 좀 해봤어서 수월하게 해결했다. 에러메세지 정확하게 요구사항과 같아야한다는 걸 잘 기억해야겠다.

     

    MemberRepository.java

    멤버들을 HashMap으로 관리하는 클래스이다.

     

    <회원 객체에 id를 세팅하고 저장>

    public Member save(Member member) {
            // TODO [1단계] member의 id를 설정하세요.
            // TODO [1단계] members 맵에 member를 추가하세요.
            // TODO [1단계] member를 반환하세요.
            member.setId(id);
            members.put(id,member);
            id++;
            return member;
        }

    파라미터로 받은 멤버의 id를 setId로 설정해주고, hashmap에 해당 객체를 put한 후 id값을 갱신하고 멤버를 리턴하였다.

     

    <id로 객체 찾아 반환하기>

    조건에 Optional로 감싸서 반환하라는 이야기가 있었다. 

     

    Optional이란?

    값이 없는 경우를 위한 자바 클래스이다.

     

    Optional 선언하기

    Optional.of() :  값이 없는 경우에만 생성

    Optional.ofNullable() : 값이 없는 경우에도 생성, 값이 없으면 Optional.empty();리턴

     

    우리는 값이 없을 경우도 리턴해야하므로 후자를 사용했다.

    public Optional<Member> findById(Long id) {
            // TODO [1단계] id를 이용하여 members 맵에서 멤버를 찾으세요.
            // TODO [1단계] 찾은 멤버를 Optional로 감싸서 반환하세요.
            Member member = members.get(id);
            Optional<Member> memberOptional = Optional.ofNullable(member);
            return memberOptional;
        }

    위와 같이 구현하였다.

     

    <회원명으로 멤버 찾기>

    members에서 username과 이름이 일치하는 멤버를 찾아야한다.

    map의 value는 멤버이므로 멤버 내에 있는 변수인 username을 얻기 위해서는 우선 getter을 만들어줘야했다.

    public String getUserName(){
            return this.username;
        }

    위와 같이 간단하게 만들었다.

     

    그 다음 스트림을 이용해서 검색을 해아한다. 나는 filter을 사용해서 username과 받은 파라미터 값이 일치하는 member을 거르도록 하였다. 또한 한개만 거르기 위해서 findFirst도 사용했다.

    public Optional<Member> findByUsername(String username) {
            // TODO [1단계] members 맵에서 username이 일치하는 멤버를 스트림을 사용해 찾으세요.
            // TODO [1단계] 찾은 멤버를 Optional로 감싸서 반환하세요.
            Optional<Member> findMember =
            members.values()
                    .stream()
                    .filter(member -> member.getUserName().equals(username))
                    .findFirst();
            return findMember;
        }

    stream을 이용하면 자동으로 Optional에 넣어준다. 따라서 그대로 리턴하도록 하였다.

     

    level 1 클리어!!

     

    level 2

    Goal.java

     

    <주어진 멤버가 현재 멤버와 동일한지 확인하기>

        public void validateMember(Member member) {
            if(this.member != member){
                throw new ForbiddenException("해당 목표에 대한 권한이 없습니다.");
            }
        }

    앞과 동일한 방식으로 구현하였다.

     

    <객체의 이름과 색상 업데이트하기>

        public void update(String name, String color) {
            // TODO [2단계] 이 객체의 name을 새로운 name으로 설정하세요.
            // TODO [2단계] 이 객체의 color를 새로운 color로 설정하세요.
            this.name = name;
            this.color = color;
        }

    setter과 같은 방식으로 구현하였다.

     

    GoalRepository.java

     

    <goal의 id 생성하고 맵에 추가>

     public Goal save(Goal goal) {
            // TODO [2단계] goal의 id를 설정하고, goals 맵에 추가하세요. 그리고 goal을 반환하세요.
            goal.setId(id);
            goals.put(id,goal);
            id++;
            return goal;
        }

     

    level 1과 같은 방식으로 구현하였다.

     

    <id로 목표 찾아 반환하기>

    public Optional<Goal> findById(Long id) {
            // TODO [2단계] id를 사용하여 goals 맵에서 목표를 찾고, 찾은 목표를 Optional로 감싸서 반환하세요.
            Goal goal = goals.get(id);
            Optional<Goal> memberOptional = Optional.ofNullable(goal);
            return memberOptional;
        }

    level 1과 같은 방식으로 구현하였다.

     

    <goals 맵 초기화>

        public void clear() {
            // TODO [2단계] goals 맵의 모든 내용을 제거하세요.
            goals.clear();
        }

    level 1과 같은 방식으로 구현하였다.

     

    <goal 삭제하기>

        public void delete(Goal goal) {
            // TODO [2단계] goals 맵에서 주어진 goal의 id를 사용하여 해당 목표를 삭제하세요.
            goals.remove(goal.getId());
        }

    삭제 기능을 hashMap의 remove 기능을 사용하였다.

     

    <goals에서 특정 멤버의 목표를 리스트로 반환하기>

        public List<Goal> findAllByMemberId(Long memberId) {
            // TODO [2단계] goals 맵에서 memberId와 일치하는 모든 목표를 스트림을 사용해 찾아, 리스트로 반환하세요.
            return goals.values().stream()
                    .filter(goal -> Objects.equals(goal.getMember().getId(), memberId))
                    .collect(Collectors.toList());
        }

    앞과 마찬가지로 구현하였다. 처음에는 받은 id와 현재의 id를 비교할 때 ==를 사용했었는데 objects.eqauls를 사용해야한다고 한다. 

    이번에는 리스트에 filter로 거른 값을 넣어야하므로 collect를 사용하였다.

     

    GoalService

     

    <목표를 저장하기>

    public Long save(String name, String color, Long memberId) {
            // TODO [2단계] memberId를 사용하여 회원 정보를 조회하고, 없으면 "회원 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
            // TODO [2단계] Goal 인스턴스를 생성하세요.
            // TODO [2단계] 생성된 Goal 인스턴스를 goalRepository에 저장하고, 저장된 목표의 ID를 반환하세요.
            if(memberRepository.findById(memberId).isEmpty()){
                throw new NotFoundException("회원 정보가 없습니다.");
            }
            Goal newGoal = new Goal(name,color,memberRepository.findById(memberId).get());
            return goalRepository.save(newGoal).getId();//goal
        }

    우선 멤버레파지토리에 memberid를 검색하고 없으면 에러를 발생시킨다.

    그 다음 goal 인스턴스를 생성했다. 이 때

    memberRepository.findById(memberId)

    의 자료형은 Optional이므로 .get()를 이용해서 객체를 꺼냈다.

    저장된 목표의 id를 리턴해야하므로,

    goalRepository.save(newGoal)

    에서 리턴받은 goal 객체에 getId()로 값을 가져왔다.

     

    <목표 수정하기>

    public void update(Long goalId, String name, String color, Long memberId) {
            // TODO [2단계] memberId를 사용하여 회원 정보를 조회하고, 없으면 "회원 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
            // TODO [2단계] goalId를 사용하여 목표 정보를 조회하고, 없으면 "목표 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
            // TODO [2단계] 조회한 목표가 주어진 회원의 것인지 검증하세요.
            // TODO [2단계] 목표의 name과 color를 업데이트하세요.
            if(memberRepository.findById(memberId).isEmpty()){
                throw new NotFoundException("회원 정보가 없습니다.");
            }
            if(goalRepository.findById(goalId).isEmpty()){
                throw new NotFoundException("목표 정보가 없습니다.");
            }
            if(!Objects.equals((goalRepository.findById(goalId).get()).getMember().getId(), memberId)){
                throw new ForbiddenException("해당 목표에 대한 권한이 없습니다.");
            }
            goalRepository.findById(goalId).get().setColor(color);
            goalRepository.findById(goalId).get().setName(name);
        }

    위와 같은 방식으로 해당 아이디가 존재하는지 검색하였고, member의 id와 목표의 멤버아이디가 겹치는지 알기 위해 goal 객체를 가져와서 그 객체 안에 있는 멤벙에 접근하여 해당 아이디를 꺼냈다. 

    그 다음 해당 id에 맞는 목표를 setter을 이용하여 색깔과 이름을 수정할 수 있도록 하였다.

     

    <목표 삭제하기>

       public void delete(Long goalId, Long memberId) {
            // TODO [2단계] memberId를 사용하여 회원 정보를 조회하고, 없으면 "회원 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
            // TODO [2단계] goalId를 사용하여 목표 정보를 조회하고, 없으면 "목표 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
            // TODO [2단계] 조회한 목표가 주어진 회원의 것인지 검증하세요.
            // TODO [2단계] goalRepository에서 해당 목표를 삭제하세요.
            if(memberRepository.findById(memberId).isEmpty()){
                throw new NotFoundException("회원 정보가 없습니다.");
            }
            if(goalRepository.findById(goalId).isEmpty()){
                throw new NotFoundException("목표 정보가 없습니다.");
            }
            if(!Objects.equals((goalRepository.findById(goalId).get()).getMember().getId(), memberId)){
                throw new ForbiddenException("해당 목표에 대한 권한이 없습니다.");
            }
            goalRepository.delete(goalRepository.findById(goalId).get());
        }

    위에 세개의 기능은 수정과 동일하게 구현하였고, goalRepository 안에 있는 delete를 이용하여 삭제 기능을 구현하였다.

     

    <특정 회원의 목표 전체 가져오기>

       public List<GoalResponse> findAllByMemberId(Long memberId) {
            // TODO [2단계] memberId를 사용하여 모든 목표를 조회하세요.
            // TODO [2단계] 조회된 목표 리스트를 GoalResponse 리스트로 변환하여 반환하세요.
            List<Goal> goalList = goalRepository.findAllByMemberId(memberId);
            List<GoalResponse> goalResponseList = new LinkedList<>();
    
            for(int i = 0;i<goalList.size();i++){
                goalResponseList.add(new GoalResponse(goalList.get(i).getId(),goalList.get(i).getName(),goalList.get(i).getColor()));
            }
            return goalResponseList;
        }

    우선 goalRepository 내에 있는 기능으로 해당 멤버의 목표들을 다 가져온다. 

    그 다음 여기서 원하는 형태는 GoalResponse이기 때문에 for문을 이용해서 goalList에 있던 데이터들을 모두 goalResponseList에다가 옮긴 후 리턴하였다.

    이번 기능에서 리스트는 탐색보다는 입출력 기능이 더 많았기 때문에 LinkedList를 사용하였다.

     

    level 2 클리어!

     

    level 3

    Todo

     

    <투두에 대한 권한이 있는지 확인하기>

    public void validateMember(Member member) {
        // TODO [3단계] 이 객체의 goal에 설정된 member가 입력받은 member와 같은지 확인하세요. 같지 않다면 "해당 투두에 대한 권한이 없습니다." 메시지와 함께 ForbiddenException을 발생시키세요.
        if(!Objects.equals(this.goal.getMember().getUsername(), member.getUsername())){
            throw new ForbiddenException("해당 투두에 대한 권한이 없습니다.");
        }
    }

    goal의 멤버의 이름과 파라미터로 받은 멤버의 이름이 같은지 확인하고 같지 않다면 오류를 발생시킨다.

     

    <투두에 있는 내용 수정하기>

    // 투두의 내용과 날짜를 업데이트합니다.
    public void update(String content, LocalDate date) {
        // TODO [3단계] 이 객체의 content를 새로운 content 값으로 설정하세요.
        // TODO [3단계] 이 객체의 date를 새로운 date 값으로 설정하세요.
        this.content = content;
        this.date = date;
    }
    
    // 투두를 완료 상태로 표시합니다.
    public void check() {
        // TODO [3단계] 이 객체의 isCompleted를 true로 설정하세요.
        this.isCompleted = true;
    }
    
    // 투두를 미완료 상태로 표시합니다.
    public void uncheck() {
        // TODO [3단계] 이 객체의 isCompleted를 false로 설정하세요.
        this.isCompleted = false;
    }

    위와 같이 수정 기능을 구현하였다.

     

    TodoRepository

     

    <레파지토리 기본 기능>

    public Todo save(Todo todo) {
        // TODO [3단계] todo의 id를 설정하고, todos 맵에 추가하세요. 그리고 todo를 반환하세요.
        todo.setId(id);
        todos.put(id, todo);
        id++;
        return todo;
    }
    
    // 주어진 id로 Todo를 찾아 Optional로 반환합니다.
    public Optional<Todo> findById(Long id) {
        // TODO [3단계] id를 사용하여 todos 맵에서 Todo를 찾고, 찾은 Todo를 Optional로 감싸서 반환하세요.
        Todo todo = todos.get(id);
        return Optional.ofNullable(todo);
    }
    
    // 모든 Todo를 삭제합니다.
    public void clear() {
        // TODO [3단계] todos 맵의 모든 내용을 제거하세요.
        todos.clear();
    }
    
    // 주어진 Todo를 삭제합니다.
    public void delete(Todo todo) {
        // TODO [3단계] todos 맵에서 주어진 todo의 id를 사용하여 해당 Todo를 삭제하세요.
        todos.remove(todo.getId());
    }

     

    이전과 같은 방식으로 만들었다.

     

    <특정 회원 Id와 특정 날짜에 해당하는 모든 Todo를 찾아 리스트로 반환>

    public List<Todo> findAllByMemberIdAndDate(Long memberId, YearMonth yearMonth) {
        // TODO [3단계] todos 맵에서 memberId와 일치하고, yearMonth에 속하는 모든 Todo를 찾아 리스트로 반환하세요.
        // TODO [3단계] 찾은 Todo 리스트를 날짜 순으로 정렬하세요.
        List<Todo> todoList = new ArrayList<>(todos.values().stream()
                .filter(todo -> Objects.equals(todo.getGoal().getMember().getId(), memberId))
                .filter(todo -> Objects.equals(YearMonth.from(todo.getDate()), yearMonth))
                .toList());
        Collections.sort(todoList,(o1, o2) -> o1.getDate().compareTo(o2.getDate()));
    
        return todoList;
    }

    우선 받은 멤버의 아이디와 일치하는 객체들을 필터링 한 후 YearMonth화 한 date를 비교하여 일치하는 것들만 필터링한다. 

    그 다음 Collections.sort를 사용해서  todoList들을 날짜를 기준으로 오름차순 정렬한다.

     

    TodoService

     

    <todo 저장하기>

    public Long save(Long goalId, Long memberId, String content, LocalDate date) {
        // TODO [3단계] memberId로 회원 정보를 조회하고, 없으면 "회원 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] goalId로 목표 정보를 조회하고, 없으면 "목표 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] 조회한 목표의 멤버가 입력된 멤버와 동일한지 확인하세요.
        // TODO [3단계] Todo 인스턴스를 생성하고 todoRepository에 저장한 후, 저장된 Todo의 ID를 반환하세요.
        if(memberRepository.findById(memberId).isEmpty()){
            throw new NotFoundException("회원 정보가 없습니다.");
        }
        if(goalRepository.findById(goalId).isEmpty()){
            throw new NotFoundException("목표 정보가 없습니다.");
        }
        if(!Objects.equals((goalRepository.findById(goalId).get()).getMember().getId(), memberId)){
            throw new ForbiddenException("해당 목표에 대한 권한이 없습니다.");
        }
    
        Todo newTodo = new Todo(content,date,goalRepository.findById(goalId).get());
        return todoRepository.save(newTodo).getId();
    }

    위 기능은 앞서 구현했던 대로 구현하였다.

     

    <todo 수정하기>

    public void update(Long todoId, Long memberId, String content, LocalDate date) {
        // TODO [3단계] memberId로 회원 정보를 조회하고, 없으면 "회원 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] todoId로 투두 정보를 조회하고, 없으면 "투두 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] 조회한 투두의 멤버가 입력된 멤버와 동일한지 확인하고, 내용 및 날짜를 업데이트하세요.
        if(memberRepository.findById(memberId).isEmpty()){
            throw new NotFoundException("회원 정보가 없습니다.");
        }
        if(todoRepository.findById(todoId).isEmpty()){
            throw new NotFoundException("투두 정보가 없습니다.");
        }
        if(!Objects.equals(todoRepository.findById(todoId).get().getGoal().getMember().getId(), memberId)){
            throw new ForbiddenException("해당 투두에 대한 권한이 없습니다.");
        }
        todoRepository.findById(todoId).get().setContent(content);
        todoRepository.findById(todoId).get().setDate(date);
    }

     

    위와 같이 구현하였다.

     

    <todo 완료상태 만들기>

    public void check(Long todoId, Long memberId) {
        // TODO [3단계] memberId로 회원 정보를 조회하고, 없으면 "회원 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] todoId로 투두 정보를 조회하고, 없으면 "투두 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] 조회한 투두의 멤버가 입력된 멤버와 동일한지 확인하고, 투두를 완료 상태로 표시하세요.
        if(memberRepository.findById(memberId).isEmpty()){
            throw new NotFoundException("회원 정보가 없습니다.");
        }
        if(todoRepository.findById(todoId).isEmpty()){
            throw new NotFoundException("투두 정보가 없습니다.");
        }
        if(!Objects.equals(todoRepository.findById(todoId).get().getGoal().getMember().getId(), memberId)){
            throw new ForbiddenException("해당 투두에 대한 권한이 없습니다.");
        }
        todoRepository.findById(todoId).get().setCompleted(true);
    }

    setter을 사용해서 completed를 true로 변경하였다.

     

    <todo 미완료상태 만들기>

    public void check(Long todoId, Long memberId) {
        // TODO [3단계] memberId로 회원 정보를 조회하고, 없으면 "회원 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] todoId로 투두 정보를 조회하고, 없으면 "투두 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] 조회한 투두의 멤버가 입력된 멤버와 동일한지 확인하고, 투두를 완료 상태로 표시하세요.
        if(memberRepository.findById(memberId).isEmpty()){
            throw new NotFoundException("회원 정보가 없습니다.");
        }
        if(todoRepository.findById(todoId).isEmpty()){
            throw new NotFoundException("투두 정보가 없습니다.");
        }
        if(!Objects.equals(todoRepository.findById(todoId).get().getGoal().getMember().getId(), memberId)){
            throw new ForbiddenException("해당 투두에 대한 권한이 없습니다.");
        }
        todoRepository.findById(todoId).get().setCompleted(false);
    }

    setter을 사용해서 completed를 false로 변경하였다.

     

     

    <todo 삭제하기>

    public void delete(Long todoId, Long memberId) {
        // TODO [3단계] memberId로 회원 정보를 조회하고, 없으면 "회원 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] todoId로 투두 정보를 조회하고, 없으면 "투두 정보가 없습니다." 메시지와 함께 NotFoundException을 발생시키세요.
        // TODO [3단계] 조회한 투두의 멤버가 입력된 멤버와 동일한지 확인하고, 투두를 삭제하세요.
        if(memberRepository.findById(memberId).isEmpty()){
            throw new NotFoundException("회원 정보가 없습니다.");
        }
        if(todoRepository.findById(todoId).isEmpty()){
            throw new NotFoundException("투두 정보가 없습니다.");
        }
        if(!Objects.equals(todoRepository.findById(todoId).get().getGoal().getMember().getId(), memberId)){
            throw new ForbiddenException("해당 투두에 대한 권한이 없습니다.");
        }
        todoRepository.delete(todoRepository.findById(todoId).get());
    }

     

    todoRepository에 있는 delete를 사용해서 구현하였다.

     

    <그룹화해서 리스트로 리턴>

    public List<TodoWithDayResponse> findAllByMemberIdAndDate(Long memberId, YearMonth date) {
        // TODO [3단계] memberId와 date를 사용하여 해당하는 모든 Todo를 조회하세요.
        // TODO [3단계] 조회된 Todo를 날짜별로 그룹화하고, 각 그룹을 TodoWithDayResponse 객체로 변환하여 리스트로 반환하세요.
        List<Todo> todoList = todoRepository.findAllByMemberIdAndDate(memberId,date);
        Map<Integer,List<Todo>> todoMap = todoList.stream().collect(Collectors.groupingBy(todo ->todo.getDate().getDayOfMonth()));
        List<TodoWithDayResponse> returnList = new LinkedList<>();
    
        for(Entry<Integer,List<Todo>> todo : todoMap.entrySet()){
            List<TodoResponse> todoResponses = todo.getValue().stream()
                    .map(it -> new TodoResponse(
                            it.getId(),
                            it.getContent(),
                            it.getGoal().getId(),
                            it.isCompleted()
                    ))
                    .toList();
            returnList.add(new TodoWithDayResponse(todo.getKey(), todoResponses));
        }
        return returnList;
    }

     

    이 부분은 정말 너무 어려웠다. 

    앞서 구현했던 todoRepository로 todoList를 받는다. 

    그 다음 같은 날짜별로 묶는다. 이를 묶어서 hashmap안에 넣기 위해서 .collect(Collectors.groupingBy(todo -> todo.getDate().getDayOfMonth()))를 사용한다.

    그 다음 리턴할 리스트를 선언한 후 entry에 있는 리스트들을 하나씩 꺼내서 map을 통해 자료형을 변환한다. 이를 returnList에 넣어서 최종적으로 리턴한다.

    level 3 클리어!

     

    level 4

    <request 받아서 signup 기능 연결하기>

    public void signup(@RequestBody SignupRequest request) {
        // TODO [4단계] SignupRequest에서 username, password, nickname, profileImageUrl을 추출하여 memberService의 signup 메소드를 호출하세요.
        memberService.signup(request.getUsername(),request.getPassword(),request.getNickname(),request.getProfileImageUrl());
    }

     

    SignupRequest에 getter을 선언하여 구현하였다.

     

    level 4 클리어

     

    정말 오래걸리고 힘들었지만 찬찬히 따라가면서 해보니 막상 할만하기도 했고 배운 점도 많다. 시스템이 전반적으로 어떻게 돌아가는지 이제 좀 알것 같다! 거의 구현에 신경을 많이 써서 좋은 코드를 위한 고민은 많이 못한 것 같다. 다음 미션에서는 좀 더 신경써야겠다.