Back-end/멋쟁이사자처럼

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

잔디🌿 2024. 3. 12. 23:59

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 클리어

 

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