오늘은 하루종일 1시간짜리 강의를 씹고 뜯고 맛보고 즐기면서 6시간정도 면밀히 분석하고 설명하여 아예 강의를 보지않고 정리한 글만 봐도 내가 직접 프로그램을 작성할 수 있는 수준으로 메모를 해보았다. 메모작성에 걸린시간은 약 6시간 이지만.. 이걸 연습해보고 흐름을 익혀가며 아예 글도 안보고 튜터님 처럼 직접 작성할 수 있는 레벨까지 되기를 바란다. 지금부터 정리한 내용을 글로만 서술해 보겠다. (사실상 동영상을 글로옮겨놓은 메뉴얼이나 다름없지만, 글로봐도 흐름을 이해하고 작성할 수 있으면 코드를 눈으로보고 따라하는게 아니라는것에 의의를 두려고 한다.)
1. build.gradle 에 추가될 의존성 롬복, 스프링웹, sql 커넥터, jdbc
2. 컨트롤러를 먼저 만들기 위해 패키지를 작성한다.
3. 패키지는 특정 도메인들이 담길 도메인 패키지, 그리고 그 안에 todo 라는 패키지가 들어가고 todo 패키지안엔 컨트롤러, 서비스, 레포지토리 ,dto 패키지가 들어간다.
4. TodoController 클래스를 만들고, TodoService 클래스, TodoRepository 클래스를 만든다.
5. 컨트롤러 클래스 위에 json 데이터를 사용하기 위해 @RestController 라고 명시해준다(RestFul 웹서비스). 그리고 API의 공통적인 주소가 /api 가 들어가기 때문에 @RequestMapping("/api/todo") 를 컨트롤러 클래스 위에 명시해준다.
6. 컨트롤러 클래스안에 일정 생성 에 필요한 Json 코드를 가져온다. 이때 API 명세서를 참고하여 가져온다.
내용은 다음과 같다.
json
{
"username":"testuser","title":"할 일 제목",
"password":"securePassword123",
"description":이 할 일에 대한 설명입니다.",
"createdAt":"2024-10-03"
}
7. 일정 생성 기능은 Post method 를 사용한다고 API 명세에 명시해두었기 때문에, @PostMapping 을 사용하여
public void createTodo(@RequestBody TodoRequestDto todoRequestDto){
}
를 작성하고, 반환타입은 우선 뭘 반환할지 모르기 때문에 void로 지정한 것이다.
그리고 요청값에 대한 처리를 해줘야하고, 그 내용은 6번의 Json코드에서 약식으로 작성되어 있다.
이 처리를 위해서 @RequestBody 로 작성하고 TodoRequestDto todoRequestDto 을 입력받는 매개변수로 작성하고, 현재 TodoRequestDto 라는 인스턴스가 없기 때문에 빨간줄이 그이지만 생성을 눌러서 만들어준다. 이때 생성은 dto 패키지안에 TodoRequestDto 클래스르 만든다.
8. 7번에 작성한 일정생성이라는 API 안에 들어갈 코드는 당연히 일정 생성에 관련된 기능이어야 한다. 그리고
일정 생성을 하면 어떤 일정이 생성되었는지 return 하는 내용이 들어가야 한다. 그래서 7번의 기본틀을 void가
아니라 TodoResponseDto 라는 타입으로 수정해 준다.
public TodoResponseDto createTodo(@RequestBody TodoRequestDto todoRequestDto){
}
이때 TodoResponseDto 에 나오는 빨간줄도 dto 패키지안에 새로운 클래스를 생성하여 처리한다.
9. TodoResponseDto 타입을 리턴하는 방식으로 사용해도 되지만, 이것을 ResponseEntity라는 것으로 묶어서 리턴하는 것이 좋다. 이때 ResponseEntity 라는 기능은 값만 딱 반환하는게 아니라, http 의 상태까지 포함해서 반환할수 있는 Spring에서 기본제공하는 클래스이다.
10. TodoRequestDto를 완성한다. 이때 완성하는 방식은 6번의 Json 코드에 있는 내용들을 작성한다.
private String username;
private String title;
private String password;
private String desciption;
private String createdAt;
private String updatedAt;
그리고 이 RequestDto 에는 롬복으로 @Getter 만 붙여준다. 게터만 있어도 RequestBody가 매핑이 된다.
11. ResponseDto 도 무엇을 저장했는지, 그 값을 그대로 돌려주는 역할을 한다. 그래서 TodoResponseDto는 내부에 Long 타입의 id라는 값을 하나더 추가해서 10번과 마찬가지로 작성해 준다. 왜냐하면 DB에 작성한 작성글의 내용들이 추가 되었다면, 그 내용들의 PK가 추가되어야 하기 때문이다.
private Long id; //pk를 위해
private String username;
private String title;
private String desciption;
private String createdAt;
private String updatedAt;
비밀번호는 유저에게 반환할 필요가없어서 제외한다. 그리고 마찬가지로 ResponseDto에도 @Getter 롬복을 달아준다.
12. 지금까지 이런방식으로(Controller 와 DTO 먼저 작성하는방식) 코드를 작성한 이유는, 나중에 스웨거라는 것을 사용하면 지금까지의 과정들을 이용한 API 명세서를 만들어 낼 수 있다.
13. 지금까지 만든 내용은 컨트롤러이기 때문에, 서비스(비즈니스 로직)과 연결을 해주어야 한다.
private final TodoService todoService;
를 컨트롤러 안에 선언해주고, 이 부분을 생성해주어야 해서 생성자
public TodoController(TodoService todoService){
this.todoService=todoService;
}
를 밑에 작성해준다.
이 생성자의 롬복 버전이 @RequiredArgsConstructor 라는것을 적으면 위에적은 생성자를 적지않아도 된다.
14. 여기까지 했을때 위의 todoService라는 인스턴스에 빨간줄이 생길텐데 이런 이유는 TodoService 라는 서비스 클래스에 Bean 이 등록되어 있지 않기때문이다. 이부분을 Bean 을 달아주려면 @Component 나 @Service 어노테이션을 달아주면 된다. 여기까지하면 컨트롤러와 서비스가 연결이 되었다.
15. 컨트롤러의 createTodo API에서 선언한 todoService를 이용하여 createTodo 라는 행위를 해야한다. 그래서 일정생성 API 내부에 todoService.createTodo() 라는 행위를 한다고 작성하는데 createTodo()메서드 안에 todoRequestDto를 넘겨준다는 의미로 todoService.createTodo(todoRequestDto); 라고 작성해서 Service에 있는 createTodo 라는 메서드가 todoRequestDto 라는 녀석을 받으면 알아서 처리하도록 작성한다.
16. 그리고 이렇게 서비스의 메서드로 값을 보내면 결국 이 값을 TodoResponseDto 로 나와서 반환을 해야 API로서 기능을 하게 되기 때문에,
TodoResponseDto dto = todoService.createTodo(todoRequestDto);
이렇게 작성을 한다.
17. 이까지 작성하면, createTodo 라는 메서드를 서비스에서 만들어주기 위해 메서드 자동생성을 하면 TodoService 클래스에 createTodo 라는 메서드가 기본적으로 생성이 된다.
18. 서비스에선 어떤것을 해주어야할까? 라고 생각하면, 컨트롤러에서 받은 todoRequestDto 에서 DB로 값을 전달을 해주어야 한다. 그런데 Dto를 DB로 값을 그대로 넣기 보다는 DTO와 DB 사이에 어떠한 전달해주는 객체가 있으면 어떨까? 라는 생각을 해보아야한다. DTO <-> ???(객체) <->DB
왜냐하면 DTO 는 데이터를 통신하는 객체일 뿐이고 실제로 DB에 접근하는 객체는 아니기 때문이다. 그래서 이 사이의 객체는 DAO 라는 객체가 있다. 그래서 DTO와 DB 사이에 있는 객체를 만들어 보자는 것을 목표로 한다.
19. 그래서 domain.todo 안에 Entity 라는 패키지를 하나더 만들어주고, Todo 라는 Entity 객체를 하나더 만들어준다. 그리고 이 Entity 객체가 DAO라는 연결고리 객체가 되어줄 것이다. 그래서 이 Entity 에 만든 클래스는 DB의 스펙과 동일해야한다. 이녀석이 DB와 통신을 할것이기 때문이다. 같지 않으면 컬럼명 불일치 오류등 이 발생할 수 있다. 그리고 이 Todo 라는 Entity 가 DB에 값을 넣고싶을때도, 꺼내고싶을때도 사용된다.
Todo 안에는 DB 스펙과 동일하게
private Long id;
private String username;
private String title;
private String password;
private String desciption;
private String createdAt;
private String updatedAt;
필드들을 넣어준다. 그리고 @Getter를 넣어준다.
20. 다시 Service 로 돌아와서 createTodo라는 메서드는 처음할일은, 받은 RequestDto 를 DAO 로 바꿔주어야한다. 그리고 생성된 Todo(Entity)를 DB에 집어 넣어야 한다.
Request -> Todo 로 바꾸는 과정
은 Todo todo = new Todo(); 가 가장 간단한 방법이다. 그리고 이 todo에
todo.setUserName(todoRequestDto.getUsername()); //이렇게해서
todo.setUserName(todoRequestDto.getUsername()); //스펙에 맞게
todo.setUserName(todoRequestDto.getUsername()); //한땀한땀 넣어줘야
todo.setUserName(todoRequestDto.getUsername()); //한다.
todo.setUserName(todoRequestDto.getUsername());
todo.setUserName(todoRequestDto.getUsername());
이런 방법도 있겠지만, Todo Entity 는 DB와 스펙이 동일하고 도메인의 핵심이기 때문에 보호해야 한다.
보호를 하려면 Todo 엔터티에 @NoArgsContructor를 선언하고, @NoArgsContructor(access = AccessLevel.PRIVATE)
으로 설정하면 위처럼 Todo todo = new Todo() 이렇게 선언해서 하는 방식은 불가능하다.(외부에서 기본생성자로 생성을 못함)
21. 그러면 생성은 어디서 담당을 하는가? -> Todo Entity 클래스 내부에서 할것이다.
entity의 Todo 내부에, public static 으로 접근제한자를 설정하고 리턴타입은 Todo로 한다. 객체를 생성해서 DB로 돌려주든지 할것이기 때문에 그리고 이름은 from이라고 적는다. 왜냐하면 인자로 받는 것이 TodoRequestDto로 부터 데이터를 받아서 생성하는 객체이기 때문이다. 그리고 이 안에 Todo todo = new Todo(); 라는 식을 넣어서 생성해준다. 그리고 return todo 로 생성을 마무리한다. 이것을 코드로 요약하면
public static Todo from(TodoRequestDto requestDto){
Todo todo = new Todo();
return todo;
}
이다.
그리고
Todo todo = new Todo();
return todo;
이 두줄사이에 Request에서 Todo로 바꿔주는 과정이 필요하다.
22. 이러기 위해서는 생성자 말고 새로운 메서드를 만들어 줄것인데, public void 제한의 init()이라는 메서드를 만들어 주는데, 이때 init() 메서드는 인자에 TodoRequestDto의 requestDto로 부터 데이터를 받고 this.username=todoRequestDto.getUsername(); 이런 행동을 하는 것이 init() 메서드이다.
이것을 요약하면
public void init(TodoRequestDto requestDto){
this.username=requestDto.getUsername();
this.title=requestDto.getTitle();
this.password=requestDto.getpassword();
this.description=requestDto.getDescription();
this.createdAt=requestDto.getCreatedAt();
this.updatedAt=requestDto.getUpdatedAt();
}
이렇게 된다.
23. 22에서 만든것을 21에서의 2줄 사이에 넣어준다
public static Todo from(TodoRequestDto requestDto){
Todo todo = new Todo();
todo.init(requestDto);
return todo;
}
24. 이렇게 내부적으로 생성하는 방식으로 설계를 마친뒤, Service로 돌아와서 아까 Todo 라는 static 생성자를 만들었기 때문에 Service 내부에서 Todo.from(todoRequestDto); 를 작성하면 생성과 데이터를 넣고 엔터티에서 변경까지 다해서 돌려준다. 그리고 이 과정까지가 RequestDto 에서 Todo 엔터티로 바꿔주는 과정이다.
그래서 이 과정설계가 끝나면 Service 의 createTodo 라는 메서드 내부에 Toto todo 라는 녀석에 넣어주면된다. 즉,
Todo todo = Todo.from(todoRequestDto);
를 작성한다.
25. createTodo라는 메서드는 아직 만든 엔터티 Todo->DB에 담아주는 과정이 필요하다. 이 과정은 Repository에서 이 역할을 할 것이다. 이렇게 하려면 아까 컨트롤러와 서비스를 연결한 것 처럼, 서비스와 레포지랑 연결하는 과정이 필요하다.
26. private final 제한의 TodoRepository todoRepository; 를 선언해주고, 빨간줄이 뜨는것은 어노테이션으로@RequiredArgsConstructor 를 선언해서 생성자 주입을 해준다. 그럼 아직 todoRepository 에 빨간줄이 뜰텐데
이것은 아직 Repository가 bean 등록이 되어있지 않기 때문이다. 그러면 일단 Service의 Repository와 연결한 부분에선
빨간줄이 사라진다.
27. 여기까지의 과정이 컨트롤러, 서비스, 레포지토리가 한줄로 이어지는 구조(3LA)로 완성이 되긴했다. 그리고 Repository 에서는 JdbcTemplate 을 사용하기 때문에,
public final JdbcTemplate jdbcTemplate;
을 선언하고 빨간줄이 뜨면, 위와 똑같이 어노테이션으로 @RequiredArgsConstructor 를 선언하면 해결된다.
28. 다시 서비스로 돌아가서 Todo를 저장을 해야한다. 이 저장이라는 행동은 todoRepository.save() 라는 메서드가 했으면 좋겠다고 생각하고 작성한다. 이때 save라는 메서드는 입력인자로 아까 변환했던 entity todo를 받는다.
즉, 25번의 Todo->DB 과정은 todoRepository.save(todo); 를 작성해서 한다.
29. 이때 save() 라는 메서드는 Repository에 없기때문에 메서드 생성하기를 하고, 다시 서비스에서 createTodo 라는 메서드는 TodoResponseDto를 반환타입으로 하는데, 이때의 리턴 타입이 TodoResponseDto 로 바로 나와도 되긴 하지만 한번 더 변환해서 반환하는 과정을 넣도록 해야한다. 그리고 save라는 Repository의 메서드는 void가 아니라 Todo 가 리턴타입으로 들어가야한다.
30. 그리고 Todo todo= Todo.from(todoRequestDto); 와 todoRepository.save(todo); 이렇게 되있던 부분을 todoRepository.save(Todo.from(todoRequestDto)); 로 한줄로 변경가능하다.
31. 이제 레포지에서 save라는 메서드를 완성해야하는데 entity 에 있는 필드들을 다 집어넣어야 한다.
String sql = "INSERT INTO todo (username, title, password, description, createdAt) VALUES (?,?,?,?,?)";
이렇게 작성하고, 이때 처음 일정을 생성할때는 updatedAt이 필요없어서 제외한다.
밑에 나머지 코드들은 일단 키홀더를 생성하는데, 이것은 DB의 쿼리문에서 기본적으로 데이터가 들어갈때 오토 인크리먼트로 키값이 1씩 상승하면서 들어가지게 되기때문에 이걸 다룰 키홀더를 선언한다.
KeyHolder keyholder = new GeneratedKeyHolder(); //키홀더를 먼저 선언하고, 위의 쿼리문을 넣고, jdbcTemplate을
업데이트 해준다. (아래)
jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, todo.getUsername());
preparedStatement.setString(2, todo,getTitle());
preparedStatement.setString(3, todo.getPassword());
preparedStatement.setString(4, todo.getDescription());
preparedStatement.setString(5, todo.getCreatedAt());
return preparedStatement;
},
keyHolder);
최종 save() 메서드의코드는
KeyHolder keyholder = new GeneratedKeyHolder();
String sql = "INSERT INTO todo (username, title, password, description, createdAt) VALUES (?,?,?,?,?)";
jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, todo.getUsername());
preparedStatement.setString(2, todo,getTitle());
preparedStatement.setString(3, todo.getPassword());
preparedStatement.setString(4, todo.getDescription());
preparedStatement.setString(5, todo.getCreatedAt());
return preparedStatement;
},
keyHolder);
Long id = keyHolder.getKey().longValue();
todo setId(id);
return todo;
이렇게 DB 에 Insert를 하고 받아온 기본키를 확인하는 코드를 추가로 작성한다.
이렇게 해서 save() 메서드의 내부 코드를 완성한다. (이때 바로위에 setId() 메서드가 안될텐데, 이것은 Todo에 세터가
없어서 그렇기 때문에 Todo 엔터티에 들어가서 Todo 클래스 내부에 어노테이션으로 id만 따로 세터를 달아준다.)
32. 이 까지 해서 레포지토리에서 DB로 데이터 넣는 흐름까지 완성했기 때문에 다시 되돌아 가면서 빼먹은게 있는지 체크를 해본다. 이때 Service의 createTodo 라는 메서드에 Todo 가 리턴이되지만 리턴되는게 없기때문에
Todo todo = todoRepository.save(Todo.from(todoRequestDto));
이렇게 선언하고 return todo.to(); 이렇게 반환해준다. 이때 to() 라는 메서드를 붙여주는 이유는 이 todo를 다시 Response 로 바꿔줘야하기 때문이다.
33. 이 to() 메서드를 만들기 위해서 다시 엔터티로 돌아가서 public TodoResponseDto 라는 타입을 리턴하는 to()
라는 메서드를 생성해준다. 근데 이녀석은 TodoResponseDto 를 반환하는 녀석이기 때문에 그냥 간단하게
TodoResponseDto 라는 Dto 클래스에 들어가서 어노테이션 @AllArgsConstructor 를 선언한다. 이건 모든 필드
변수가 포함된 생성자를 만들어주고 이 생성자를 이용하여 to() 메서드 안에
return new TodoResponseDto(
this.id,
this.username,
this.title,
this.description,
this.createdAt,
this.updatedAt
);
이걸 넣어주면 이 to()라는 메서드는 다시 ResponseDto 로 변환해주는 메서드가 된다.
34. 여기까지의 과정은 Service beab 클래스에서, 처음에 Request로 부터 Todo(DAO)를 만들고, Todo로부터 다시 Response를 만들어서 return 하는 과정이다.
35. 다시 이제 역으로 컨트롤러로 돌아가보면, 반환타입이 ResponseEntity<TodoResponseDto> 로 되어있는 부분이
있는데, 이 ResponseEntity를 사용해서 리턴하는 방법은,
return ResponseEntity
.status(HttpStatus.CREATED)
.body(dto);
이렇게 작성해주면 된다. 근데 이걸 축약하려면 원래 적혀있던
TodoResponseDto dto =
return ResponseEntity
.status(HttpStatus.CREATED)
.body(dto);
이 부분을 이렇게 하지않고
return ResponseEntity
.status(HttpStatus.CREATED)
.body(todoService.createTodo(todoRequestDto));
이렇게 하는것도 가능하다.
36. 이렇게 까지 하면 일정생성하는 기능까지는 끝까지 구현되어 있다. 다른 API의 도메인에 따라서 부수적인것들이 더 생겨날 수는 있지만, create 를 만드는 과정에서 기본적인 틀이 다 완성되게 된다.
37. 전체일정조회 API는 컨트롤러에 아래와 같이 만들어져있다. 생성과 거의 동일한 구조이다.
public ResponseEntity<List<TodoResponseDto>> getTodoList(){
return ResponseEntity
.status(HttpStatus.OK)
.body(todoService.getTodoList());
}
ResponseEntity 를 사용한것은 동일하지만, 일정이 리스트로 들어올것이기 때문에 리스트타입을 선언해놓았다. 이것은 원래 리턴타입을 List<TodoResponseDto> 라는 반환타입만 적을것을 겉에 ResponseEntity<> 만 씌워놓았다 라고 생각하면 된다.
38. 위의 전체일정조회 API 에 필요한 getTodoList() 메서드도 빨간줄이 그여있기때문에 서비스 bean 객체에 생성을 해주어야 한다. 서비스 클래스에 자동생성으로 생성하고 나면 리턴타입을 List<TodoResponseDto> 로 바꿔준다. 그리고 getTodoList() 라는 메서드의 내부 코드는 그렇게 길지는 않고 바로 리턴하는 방식인데, return todoRepository.findAll(); 만 작성하면되고 이제 이 findAll이라는 메서드를 Repository 에서 만들어준다.
39. 레포지에서 public List<TodoResponseDto> findAll() 이렇게 선언해주고 내부의 DB에 요청하는 쿼리부분은
String sql = "SELECT * FROM todo";
를 작성해서 그냥 테이블의 모든 컬럼의 데이터를 다가져오라는 요청을 한다.
40. 그리고 findAll() 메서드에 내부 자세한로직을 추가하는데, 아까 save() 메서드는 Todo를 바로 리턴하지만 이번에는 DTO를 리턴하도록 해보자 라는 생각으로 DTO를 리턴하는 코드를 작성한다.(findAll 이라는 메서드의 반환타입이 List<TodoResponseDto> 로 선언되어 있기때문에 DTO 로 리턴하는 코드인 것이다.)
41. 이런 코드의 내용은 위의 String sql = "SELECT * FROM todo"; 다음부분에
return jdbcTemplate.query(sql, (rs, rowNum)->{
Long id = rs.getLong("id");
String username =rs.getString("username");
String title =rs.getString("title");
String description =rs.getString("description");
String createdAt =rs.getString("created_At");
String updatedAt =rs.getString("updated_At");
return new TodoResponseDto(
id,
username,
title,
description,
createdAt,
updatedAt
);
});
을 넣는다. 위에 rs.get~ 이부분은 각각의 변수에 가져온 데이터를 각각 다 담아주는데 이게 가능한 이유는 rowNum 덕분에 반복문 처럼 전부 넣는 방식으로 담아주게 된다. 이렇게 다 담아준것을 TodoResponseDto에 담고 리턴한다. 이렇게 하면 전체 일정 조회하는 기능도 완성된다.
42. 추가적인 요구사항(작성자 기준으로 조회, 수정일 기준으로 조회)같은것들은 처음부터 RequestParam 으로 받아온다고 가정하면 컨트롤러에 선언해 놓은 getTodoList API 에 괄호안 받아오는 인자에 @RequestParam 을 적고 @RequestParam String userName 이런식으로 적게되면, 유저네임을 받아왔다 가정하고 아래 리턴부분의 .body(todoService.getTodoList()); 부분에 getTodoList()의 괄호안 넘겨주는것에 userName을 넘겨준다.
즉, .body(todoService.getTodoList(userName)); 이렇게 하면 된다.
이렇게 수정하면 서비스에서 선언해놓은 getTodoList 메서드에서도 매개변수에 String userName 을 넣어주고 서비스 안에 있는 getTodoList 메서드의 리턴값인 findAll() 이라는 메서드의 빈칸안에 매개변수로 userName을 넘겨주고 Repository까지 전달하면 된다.
Repository 에서도 findAll()이라는 메서드 매개변수로 String userName을 넣고, 유저네임을 토대로 전체데이터를 조회하는거니까 아까의 String sql = ""; 하는 쿼리문에도 원래 내부에
"SELECT * FROM todo" 만 적혀있었는데 여기에 WHERE username = ? 를 추가하고 return jdbcTemplate.query
부분의 끝에 query() 의 소괄호 안에 , username 을 넣어주면 물음표에 매핑이 되면서 유저네임에 따라 필터가 된다.
return jdbcTemplate.query(sql, (rs, rowNum)->{
Long id = rs.getLong("id");
String username =rs.getString("username");
String title =rs.getString("title");
String description =rs.getString("description");
String createdAt =rs.getString("created_At");
String updatedAt =rs.getString("updated_At");
return new TodoResponseDto(
id,
username,
title,
description,
createdAt,
updatedAt
);
},userName);
이렇게 한다.
43. 선택일정 조회기능은 어떻게 구현되는지 생각해 보면, 게시판을 떠올려보고, 게시판의 3번째 글을 누르면 그 3번째 글이 조회가 되어야 하는데, 그 "3번째"라는 데이터를 어떻게 주고받을지 생각해보면 된다.
44. 그 "3번째" 라는 것은 Todo 가 가지고 있는 id 라는 데이터를 이용해서 조회할 것이다. 기본적인 설계는 비슷하다.
@GetMapping("/{todoId}")
public ResponseEntity<TodoResponseDto>getTodo(@PathVariable Long todoId){
return ResponseEntity
.status(HttpStatus.OK)
.body(todoService.getTodo(todoId));
}
이렇게 방식은 비슷하지만, todoId 를 추가로받아와서 하는 방식이 다르다고 볼수있다. 이제 getTodo메서드를 만들면
된다.
45. 서비스 클래스에 메서드를 추가하는데 public TodoResponseDto 를 리턴타입으로 해주고 이름과 매개변수를 getTodo(Long todoId) 이렇게 해준다.
그리고 내부코드 return todoRepository.findById(todoId); 이렇게 작성해주고 Repository 로 다시 넘어가서 findById() 메서드를 만들어주는데 이것도 역시 public TodoResponseDto 를 리턴타입으로 해주고 메서드명 매개변수 명 을 findById(Long todoId) 를 작성하고 내부 코드를 작성해야한다.
46. 근데 이번에는 다시 서비스클래스로 돌아가서 getTodo 메서드의 return을 조금 바꿔서 바로 그냥 리턴하는것이 아니라 Todo todo에 받아와서 바꿔주고 리턴해주는 방식으로 해볼것이다. 그럴려면 다시 레포지토리에 반환타입을 TodoResponseDto로 해두었는데 이걸 Todo로 바꿔준다. 그리고 findById 의 내부 코드는 아래와 같다. 쿼리문 부터 시작한다.
String sql = "SELECT * FROM todo WHERE id = ?";
이것은 위의 전체일정 조회에서 유저이름으로 조회하는 기능을 추가했을때를 가정하여 작성한 코드와 유사하지만 이번에는 id로 조회하는 차이가 있다.
그리고 쿼리문 밑에 바로 리턴을 하는데,
return jdbcTemplate.query(sql, rs -> {
if(re.next()){
return Todo.from(rs);
} else {
return null;
}
},todoId);
이렇게 리턴해준다. 이 내용은 기본적으로 변수 rs라는 곳에 값이 담기게 될텐데, 이전에는 rs에 담겨있던 변수를 ResponseDto로 바꿔서 리턴했다면(전체일정조회에서) 이번에는 바로 Todo로 바꿔보자 라는 방식이다.
즉, rs -> Todo(DAO) 이런방식이다. 근데 이런방식에서 사용되는 from 이라는 메서드는 아직없기때문에 이것을
만들어 주어야 한다.
47. 이번엔 Todo 엔터티로 돌아와서 public static Todo from 을 만들어주는데 매개변수로 (ResultSet rs) 를 입력
받는다. 이렇게 from 이 두갠데 동명이인메서드가 가능한 자바 스킬이 있는데 이 스킬이름을 오버로드 라고 한다. 오버라이딩은 상속관계에서 부모의 것을 쓸때 하는것.
48. 이 rs를 매개변수로 받는 from 메서드에도 아까의 from 메서드의 내용과 동일하게 들어가기는한다.
Todo todo = new Todo();
todo.init(rs);
return todo;
이렇게 세줄이 들어가는데, 이때 init 이라는 메서드도 다시 오버로딩이 되서 비슷한 메서드가 하나더 만들어져야 한다.
동일하게 엔터티클래스 내부의 바로 아래에
private voic init(ResultSet rs){
this.id=rs.getLong("id");
this.username=rs.getString("username");
this.title=rs.getString("title");
this.password=rs.getString("password");
this.description=rs.getString("description");
this.createdAt=rs.getString("created_at");
this.updatedAt=rs.getString("updated_at");
}
이렇게 작성하고 방금 오버로딩한 from 과 init 는 throws SQLException 을 메서드이름 옆에 추가해주면된다.
49. 이렇게 하면 다시 레포지로 돌아갔을때 rs to DAO 의 메서드가 완성된다. 그리고 다시 서비스로 돌아오면 getTodo 메서드에 return todo.to(); 이렇게 리턴하면 바로 ResponseDto로 변환해서 넣어주게 된다. 이부분을 한줄코드로 작성한다면
public TodoResponseDto getTodo(Long todoId) {
return todoRepository.findById(todoId).to();
}
이렇게 수정가능하다. 하지만 읽기 편한방법은 아무래도
public TodoResponseDto getTodo(Long todoId) {
Todo todo = todoRepository.findById(todoId);
return todo.to();
}
이거라고 나도 튜터님과 동일하게 생각한다.
50. 일정 수정과 삭제 기능이 남아있다. 이때 수정과 삭제도 선택한 id 를 통해서 처리가 되는 부분이다.
선택일정 수정의 로직은
@PutMapping("/{todoId}")
public ResponseEntity<Void> updateTodo(
@PathVariable Long todoId,
@RequestBody TodoRequestDto requestDto
){
todoService.updateTodo(todoId, requestDto);
return ResponseEntity
.status(HttpStatus.NO_CONTENT)
.build();
}
이렇게 설계 가능하다. 설명을 추가하자면, "선택한"일정이기 때문에 매개변수로 처음에 @PathVariable로 id를 받고, RequestDto로는 수정을 요청했으면 데이터를 바꿔야하는 부분이 있어야 하기때문에 그 데이터를 Request로 받기로 한다. 그리고 이 API 는 반환타입이 없다. 이 부분은 프론트엔드와 프로젝트에 따라 달라진다.
51. 서비스 클래스에 동일한 방식으로 updateTodo 라는 메서드를 자동생성하고, 내용에는 패스워드가 맞는지 먼저 확인한다. Todo todo 라는 Todo 타입 변수 todo에 todoRepository.findById(todoId)메서드를 이용하여
id를 먼저 꺼내온다.
즉, Todo todo = todoRepository.findById(todoId); 라고 작성하여 비밀번호를 확인하는 준비를하고,
비밀번호가 맞는지 확인한다.
비밀번호가 requestDto 에 숨어있다고 생각하기는 하지만, "진짜 비밀번호"는 DB에 들어있는 것이다. 사용자가 입력한 패스워드와 DB에 있는 패스워드를 비교하는 로직을 작성하면 되는데,
public void updateTodo(Long todoId, TodoRequestDto requestDto){
Todo todo = todoRepository.findById(todoId);
}
이 세줄에서 매개변수에 입력받은 requestDto 이부분이 사용자의 입력이고, todo에 들어있는 비밀번호가 DB의 비밀번호인것을 인지해야 한다.
내부에 다음과 같은 비밀번호 비교 로직을 작성한다.
if(todo==null){
throw new IllegalArgumentException("해당 id를 찾을 수 없음");
} //todo가 존재하는지 먼저 확인
if(!Objects.equals(todo.getPassword(), requestDto.getPassword())){
throw new IllegalArgumentException("패스워드가 틀립니다.");
} //비밀번호가 맞는지
//위의 예외처리가 다 통과되면 아래의 로직까지 실행된다.
todoRepository.update(todoId,requestDto);
52. Repository 로 넘어와서 쿼리문은 아래와같이 작성한다.
public void update(Long todoId, TodoRequestDto requestDto) {
String sql = "UPDATE todo SET description = ?, username = ?, updated_at = ? WHERE id = ?";
jdbcTemplate.update(sql, requestDto.getDescription(), requestDto.getUsername(), requestDto.getUpdatedAt(), id);
}
jdbcTemplate 은 return 타입이 없기때문에 비교적 간단하게 끝난다. 이렇게 업데이트까지 설계가 되었다.
53. 삭제하는 로직은, 다음과 같다(수정과 유사하지만 어노테이션이 @DeleteMapping 인부분이 다름)
@DeleteMapping("/{todoId}")
public ResponseEntity<Void> deleteTodo(
@PathVariable Long todoId,
@RequestBody TodoRequestDto requestDto //여기서 필요한 데이터는 비밀번호만 이다.
){
todoService.deleteTodo(todoId, requestDto);
return ResponseEntity
.status(HttpStatus.NO_CONTENT)
.build();
}
RequestBody 에 비밀번호를 담을지, PathVariable 로 해서 URL에 담을지 고민을 해볼 수 있는데, 지금같이 작성한 이유는, URL 에 비밀번호를 담는것 보다는, RequestBody 에 담아서 데이터를 주고받는게 안전하다 라는 판단이 있어서 한 것이다.
54. 서비스에 deleteTodo 라는 메서드를 만들어주는데, 이것또한 확인하는 방식이 수정과 동일하다.
내부에
if(todo==null){
throw new IllegalArgumentException("해당 id를 찾을 수 없음");
} //todo가 존재하는지 먼저 확인
if(!Objects.equals(todo.getPassword(), requestDto.getPassword())){
throw new IllegalArgumentException("패스워드가 틀립니다.");
} //비밀번호가 맞는지 이것을 확인하고,
todoRepository.delete(todoId);
이렇게 까지 작성하면 서비스 부분은 완성이다.
55. Repository 의 쿼리쪽으로 오면,
public void delete(Long todoId) {
String sql = "DELETE FROM todo WHERE id = ?";
jdbcTemplate.update(sql,id);
}
이렇게 하면 CRUD 부분은 전부 끝이났다.
56. 지금부턴 API 테스트 하는 방식에 대한 설명이다
튜터님의 깃허브를 보면, src-main-resources-api와 sql 을 보면 erd에 따른 쿼리문과 포스트맨에 사용할 수 있는
내용을 모두 적어놓았다.
api 에는 api 를 테스트 할 수 있는 포스트맨 컬렉션을 넣어놓았다. 이걸 포스트맨에 import 눌러서 api에 들어있는 컬렉션을 임포트하면 테스트 api 부분을 모두 사용할 수 있다. 일정생성 부분을 켜서, raw 를 고르고 JSON 을 선택하면 Body 에 JSON 이 들어가게 된다.
57. 그리고 지금까지 작성한 내용을 토대로 서버를 켜고 postman에서 샌드를 해본다. 그리고 에러가 발생하면 에러를 분석한다.
에러의 첫번째줄은 에러가 발생한 이유와 명칭이고, 밑에나오는 부분은 에러때문에 아래의 내용모두가 전부 동작을 하지않는다는것을 명시한다.
튜터님 영상에서 에러가 발생한 이유는, DB에 들어 있는 내용이, 도전과제까지 작동하게설계되어있어서 컬럼등의 명칭이 달라서 오류가 났다.
58. DB를 과제에 맞게 똑같이 잘 설계한다음 SEND를 보내면 할일이 정상적으로 들어가면서 ID 카운트가 1씩 증가하는 것을 확인할 수 있다. 전체일정조회도 같은 방식으로 해보면 에러가 뜰수도 잇는데, 이부분은 아까 where 테스트 한다고 컨트롤러에 username 이런 항목을 넣어놔서 발생한 에러인것이다. 이때 위의 작성에 username이 추가된 부분을 지워주는 방법도 있고, 아니면 요청하는 주소 localhost:8080/api/todo?userName=123 이런식으로 추가해서 요청에 따라주는 방법도 있다. 하지만 이러면 더 수정해야되는부분이 많아서 추가된부분을 제거하고하는게 편하다.
59. postman 을통해서 비밀번호를 틀리게 입력한다던지, id를 잘못입력해본다던지 해서 콘솔창에 에러를 보고 API가 예상대로 잘 작동하고있는지 확인하면 된다.
60. 지금부터는 도전과제의 내용을 따라 만들어본다.
깃허브의 sql 부분을 보면, 일반버전 쿼리문과, 도전과제쿼리문이 있는데, 두개의 차이를 보면 username 대신에 member_id 라고 되어있는게 추가된것을 볼 수 있다. 그리고 이 member_id는 FK로 외래키를 설정하여 member라는 다른 테이블과 연결하여 데이터를 연결해서 사용할 수 있다.
61. 도전과제 쿼리문을 실행시켜서 DB를 교체해준다. 깃에 도전과제는 challenge 라는 브랜치를 분리해놓아서 그쪽을 보면 코드를 볼수있다. 그리고 도전과제로 과제를 업그레이드를 하려면 수정되야할 부분이 많은데, domain 패키지에서 todo 이외에 member라는 패키지를 따로 하나 만들어준다. 이 member 라는 패키지는 내부에 entity랑 repository 패키지 만 만들어준다.
62. entity 는 member 라는 클래스가 될것이고, 레포지는 memberRepository 가 될것이다. 여기서 부터 username라고 되있던것을 모두 memberId 로 바꿔야한다. 가장 간단한 방법은 todo에 있는 entity에 있는 username을 Long 타입 memberId 로 변경한다. 이때 entity 중심으로 개발했기 때문에, entity의 변수타입과 변수명을 바꾸면 그부분만 에러가 표시된다.
63. entity 내부부터 모두 하나씩 수정해준다. getMemberId 이렇게 Getter 로 받아오는 부분에 이름을 바꿔도 안되면 Dto로 들어가서 username 부분을 memberId로 바꿔주면 된다. 바꿀때 항상 타입도 같이 보고 변경한다. 도전과제 4단계를 진행하면 이렇게 수정하는 과정이 반드시 필요하다.
64. ctrl shift f 를 누르면 검색해서 변경하는것이 가능하다.
65. Entity에 Member 엔티티에는
private Long id;
private String username;
private String email;
private String createdAt;
private String updatedAt;
이 들어간다. 이것은 sql 테이블 컬럼에 있는 데이터와 완전히 일치한다.
66. 이 entity도 클래스이름 바로 위에 @Getter 선언해주고 데이터 보호를 위해서 @NoArgsContructor(access = AccessLevel.PRIVATE) 도 작성해준다. 그리고 todo 서비스를 보면 패스워드로 수정이나 삭제를 진행하는 방식도 바뀌는것이 좋을것 같고, 조회할때도 멤버 정보가 보이게 변경되면 좋을것이다.
67. findAll() 메서드의 쿼리문을 변경하여 member에 해당하는 데이터도 조회하고 싶다.
String sql = "SELECT * FROM todo " +
"left join member m on todo.member_id = m.id";
이렇게 쿼리문을 수정해준다.
68. DB를 알맞게 세팅하거나 연결해준다음, SQL 콘솔창에서도 바로 쿼리를 작성해서 테이블에 데이터를 넣어줄 수
있기 때문에,
INSERT INTO member (username, email, created_at, updated_at)
VALUES ('박유저', 'user@example.com', '2024-10-03', '2024-10-04';
이런식으로 데이터를 넣어줘버릴 수 있다.
69. 다시 컨트롤러로 돌아와서, 일정생성부터 다시확인한다. postman 으로 도전과제에 대한 API를 참고해서 서버를 재시작한다음 raw JSON 세팅 그대로 다시 연결한다. 이때 member_id 라는 값은 사실 사용자가 가지고있어야 하는지, 서버가 가지고있어야 하는지 애매한 부분이지만, 결국 나중에 로그인 기능을 구현하게 되면 알게된다.
70. 68번의 방식대로 김유저도 추가하고 비교를 해볼 수있다.
SELECT * FROM todo
left join member m on todo.member_id=m.id
where m.id =1;
이렇게하면 유저에 따라 확인이 가능하다.
71. todoService에서 패스워드로 수정, 삭제 확인하던 부분을 패스워드가 아니라 멤버아이디로 수정삭제를 확인하게 한다(로그인하고 수정삭제하는 개념)
그럴려면 MemberRepository memberRepository 를 선언하고 MemberRepository 로 이동하고 @Repository 와 @RequiredArgsConstructor 를 선언해놓으면된다.
72. 그리고 다시 todoService 에서 수정하는 부분에, 패스워드 부분의 로직을 없애고
Member member = memberRepository.findById(todo.getMemberId()); 이렇게 해서 member 객체에
memberId만 가져온다. 그다음 로직으로
if(!Objects.equals(member.getId(),requestDto.getMemberId())){
throw new IllegalArgumentException("작성자만 수정 가능합니다.");
}
이것을 추가한다.
73. MemberRepository 클래스에 findById 메서드를 추가해준다. 매개변수는 Long id 이고 내부의 코드는
String sql = "SELECT * FROM member where id = ?";
return jdbcTemplate.query(sql, rs -> {
if (rs.next()){
return Member.from(rs);
} else {
return null;
}
}, id);
를 넣어준다.
이때 jdbcTemplate과 from 메서드에 빨간줄 오류가 발생한다. jdbctemplate은 늘하던대로 추가해주고, from 메서드는 Member entity에 메서드 생성으로생성해주고, 생성자를 DAO 에서 억세스레벨을 조정하는방식으로 하고, entity에 아까했던것과 같은방식으로
public static Member from(ResultSet rs) throws SQLException {
Member member = new Member();
member.init(rs);
return member;
}
을 추가하고,
private void init(ResultSet rs) throws SQLException {
this.id=rs.getLong("id");
this.username=rs.getString("username");
this.email=rs.getString("email");
this.createdAt=rs.getString("created_at");
this.updatedAt=rs.getString("updated_at");
}
이렇게 추가한다. 이 과정은 생성자를 내부에서 생성하기 위함이다.
74. service 의 일정 수정 부분에 결국 바뀌는 부분은
Todo todo= todoRepository.findById(todoId); //부분을 놔두고,
Member member = memberRepository.findById(todo.getMemberId());
if(!Objects.equals(member.getId(),requestDto.getMemberId())){
throw new IllegalArgumentException("작성자만 수정 가능합니다.");
}
todoRepository.update(todoId,requestDto);
로 내부로직은 끝난다.
75. 그리고 삭제 로직도
Member member = memberRepository.findById(todo.getMemberId());
if(!Objects.equals(member.getId(),requestDto.getMemberId())){
throw new IllegalArgumentException("작성자만 삭제 가능합니다.");
}
이부분을 똑같이 추가해서 Exception 부분의 멘트만 삭제로 변경해서 코드를 수정해준다.
76. 이렇게 수정후에 프로그램을 재실행해주고 postman의 도전과제에서 수정된것을 확인할 수 있다. 수정삭제같은 경우는 return 타입이 따로 없어서 DB 테이블을 보고 잘 되고있는지 직접확인해야한다.
77. 마지막 도전과제 기능은 페이지네이션이다. 이걸 DB에서 처리하는 방법이 따로 있고, limit 이나 offset 이런기능을 사용하면 된다.
78. 쿼리문 예시는 다음과 같다.
SELECT * FROM todo
LEFT JOIN member m on todo.member_id = m.id
ORDER BY todo.id LIMIT 10개씩 줘(size) OFFSET 몇번째페이지 = size
이러한 방식이 있다. 이것은 따로 공부해보면서 익히면 된다.
79. 위의 공식대로 controller 부터 수정시작한다. Controller 의 List API 부분의 getTodoList의 매개변수로
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size)
을 넣어준다. 그리고 Service 부분으로 이동하여 원래 전체페이지를 조회하는 List<TodoResponseDto> getTodoList() 라는 메서드가 존재하는데 이것 외에,
public List<TodoMemberDto> getTodoListWithPaging(int page, int size) {
return todoRepository.findAllWithPaging(page,size);
}
라는 메서드를 새로 추가한다.
80. 그렇게하면 List의 타입으로있는 TodoMemberDto 가 아직 없는데, 이것을 DTO패키지에
새로 생성한다. 그리고 이 DTO 내부에는
private Long id;
private Long memberId;
private String title;
private String username;
private String email;
private String description;
private String createdAt;
private String updatedAt;
필드들이 들어간다. 그리고 이 클래스도 역시 @Getter 와 @AllArgsContructor 를 명시해준다.
81. 다시 서비스로 돌아와보면 findAllWithPaging 이라는 메서드가 없어서 이녀석을 다시 Repository에 만들어줘야 한다.
public List<TodoMemberDto> findAllWithPaging(int page, int size) {
String sql = "SELECT * FROM todo " +
"LEFT JOIN member m on todo.member_id = m.id " +
"ORDER BY todo.id LIMIT ? OFFSET ?";
return jdbcTemplate.query(sql, (rs,rowNum) -> {
Long id = rs.getLong("id");
Long member_id = rs.getLong("member_id");
String username =rs.getString("username");
String title =rs.getString("title");
String email = rs.getString("email");
String description =rs.getString("description");
String createdAt =rs.getString("created_At");
String updatedAt =rs.getString("updated_At");
return new TodoMemberDto(
id,
member_id,
username,
title,
email,
description,
createdAt,
updatedAt
);
}, size, page*size);
}
이 결과는결국 List로 잘 담겨서 나올것이다.
82. 이 메서드를 사용해보려면 Controller 에서 전체일정조회의 리턴부분에, todoService.getTodoListWIthPaging(page,size)); 로 바꿔주고 List<>의 반환타입부분에 TodoMemberDto 로 바꿔준다 그리고 코드를 재실행 시키면 된다.
83. postman 도전과제 일정 조회 하면 된다. 일정을 충분히 10개이상 넣어줘 보고, 페이지별로 들어간 것을 확인해본다.
84. 페이지를 나누는방법은 이렇게 나누는 방법도 있고 무한스크롤로 커서방식으로 페이징을 하는 방법이 있다.
'Project' 카테고리의 다른 글
[팀 프로젝트 KPT+F] Spring JPA & JWT & Github (0) | 2024.10.24 |
---|---|
[개인과제] 일정 관리 앱 Develop - 댓글 관리 및 트러블 슈팅 (0) | 2024.10.17 |
[개인과제] 일정 관리 앱 Develop - 개발 과정 및 트러블 슈팅 (0) | 2024.10.17 |
[개인과제] 일정 관리 어플리케이션 (1) | 2024.10.01 |
일정 관리 어플리케이션 설계 (2) | 2024.09.27 |