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