to-do 애플리케이션 개발
1. 기본 설정
2. 모델-엔티티 클래스 구현
3.DTO 구현
*DTO(Data Transfer Object)
: 데이터를 전달하는 데 사용하는 오브젝트.
사용 목적
-비즈니스 로직의 캡슐화 : 외부 사용자에게서 서비스 내부의 로직, DB 구조 등을 숨기기 위해(모델은 DB 테이블 구조와 매우 유사하며, 모델이 갖고 있는 필드는 테이블의 스키마와 비슷할 확률이 높아 외부인이 이를 알 수 있는 가능성이 있음)
-클라이언트가 필요한 정보를 모델이 전부 포함하지 않는 경우가 많음 : 대표적인 예시로 에러 메시지가 있으며, 서비스 실행 도중 사용자 에러가 날 경우 이 에러 메시지 필드를 DTO 에 선언 후 포함하면 됨.
*자바 Generic :
사용 목적 : 다른 모델의 DTO가 이를 이용해 리턴할 수 있도록 Generic 이용
4. 컨트롤러 레이어 : REST API 컨트롤러
+)매개변수 넘겨받기
1.@PathVariable 사용하기
2.@RequestParam 사용하기
3.@RequestBody 사용하기
4.@ResponseBody 사용하기
-오브젝트 리턴 시 사용!
5.@ResponseEntity 사용하기
-> 리턴된 body 외에 큰 차이점이 보이지 않지만, 헤더와 HTTP Status를 조작할 수 있는 쪽은 Entity쪽임!!
5. 서비스 레이어 : 비즈니스 로직
: 컨트롤러-퍼시스턴스 사이에서 비즈니스 로직을 수행하는 역할. 컨트롤러와 퍼시스턴스에서 분리되어 있음.
6. 퍼시스턴스 레이어 : JPA
*JPA
: 스펙(Specification) - 구현을 위해 작성해야 하는 기능을 알려주는 지침이 되는 문서. 자바에서 DB 접근, 저장, 관리에 필요한 스펙으로, 이 스펙을 구현하는 구현자를 JPA Provider라고 부르며 그 중 대표적인 것이 Hibernate임
: 반복해서 DB 쿼리를 보내 ResultSet을 파싱해야하는 과정을 줄여줌!
*스프링 데이터 JPA
: JPA+@의 개념. JPA를 더 사용하기 쉽게 도와주는 스프링의 프로젝트인데, 기술적으로는 이를 추상화(=사용하기 쉬운 인터페이스를 제공함)한다고 함.
*H2 DB
: In-Memory 데이터베이스. 로컬 환경에서 메모리상에 DB를 구축해줌. 사용 시 따로 DB 서버를 구축하는 데에 시간을 할애할 필요가 없어, 초기 개발 시 많이 사용. In-Memory 로 실행할 경우 애플리케이션 실행 시 테이블이 생성되고, 애플리케이션 종료 시에는 테이블이 함께 소멸됨
보통 DB 테이블마다 그에 상응하는 엔티티 클래스가 존재하는데, 엔티티 클래스는 클래스 그 자체가 테이블을 정의해야 함! 즉, ORM이 엔티티를 보고 어떤 테이블의 어떤 필드에 매핑해야 하는지 알 수 있어야 하며 어떤 필드가 기본키 / 외래키인지 구분이 가능해야 함. 이런 DB 테이블 스키마에 관한 정보는 javax.persistence가 제공하는 JPA관련 어노테이션을 이용해 정의함.
*자바 클래스를 엔티티로 정의할 때 주의해야할 점
-클래스에는 매개변수가 없는 생성자(=NoArgsConstructor)가 필요함
-Getter/Setter가 필요함
-기본키 지정 필요
문자열 형태의 UUID의 사용을 위해 커스텀 Generator를 만들기 위해 @GenericGenerator를 이용
→매개변수 strategy로 uuid를 넘기고, 이렇게 uuid를 사용하는 system-uuid 라는 이름의 GenericGenerator를 만들었고 이를 @GeneratedValue가 참조해서 사용함
*JpaRepository<T, ID>
-T ; 테이블에 매핑될 엔티티 클래스
-ID ; 엔티티의 기본 키 타입
→ 테이핑에 매핑될 엔티티 클래스인 TodoEntity, 기본키 타입인 String을 넣어줌
*AOP(Aspect Oriented Programming)
:
기본적인 쿼리인 경우 JpaRepository를 상속하고, JpaRepository가 기본적인 DB 오퍼레이션 인터페이스를 제공한다. 이 인터페이스를 스프링 데이터 JPA가 실행 시 구현해주기 때문에 SQL쿼리를 따로 작성할 필요가 없음
기본적인 쿼리가 아닌 경우) 메서드를 작성하면, 메서드 이름은 쿼리 / 매개변수는 쿼리의 where문에 들어갈 값을 의미하게 됨. 더 복잡한 쿼리의 경우는 아래와 같이 @Query 어노테이션을 사용해 지정할 수 있음
일단 따로 지정이 아닌 처음 작성한 findByUserId로 간다.
아래의 공식 사이트의 레퍼런스를 참고하여 메서드의 이름 작성 방법 및 예제를 확인할 수 있음!
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
1. 본격적으로 개발을 해 보자
퍼시스턴스 > 서비스 > 컨트롤러 순으로 구현
*@Sl4fj ; 로그 라이브러리~
1)Create Todo 구현 - Todo 생성
-퍼시스턴스 구현 ; save 메서드(엔티티 저장) / findByUserId 메서드(새 Todo 리스트 반환) > 모두 구현 완료
-서비스 구현 ;
validation 부분은 계속해서 사용하므로 따로 빼내어 리팩토링 해주기
-컨트롤러 구현 ;
*메서드 및 설명...
*여기서 실행하려고 했더니 오류 발생... ( https://sum-milkyway.tistory.com/51 )
2)Retrieve Todo 구현 - Todo 검색
-퍼시스턴스 구현 ; TodoRepository 그대로 사용
-서비스 구현 ; retrieve() 메서드 작성
-컨트롤러 구현 ; 컨트롤러의 @getmapping 이용, retrieve() 메서드 작성
여기까지 작성하고 테스팅하기
-테스팅 ; postman 이용(H2 DB 사용으로 프로그램 재시작시 이전에 생성한 아이템은 삭제되므로, 다시 바로 아래에 있는 코드 작성-실행(POST)후 테스팅 코드 실행해줌
3)Update Todo 구현 - Todo 검색
-퍼시스턴스 구현 ; TodoRepository 그대로 사용(save(), findByUserId() 사용)
-서비스 구현 ; update() 메서드 작성
-컨트롤러 구현 ; 컨트롤러의 @putmapping 이용(업데이트니까!!), updateTodo() 메서드 작성
-테스팅 ; postman 이용(H2 DB 사용으로 프로그램 재시작시 이전에 생성한 아이템은 삭제되므로, retrievee()때와 같이 새로 생성 후 테스팅 코드 실행해줌
4)Delete Todo 구현 - Todo 삭제
-퍼시스턴스 구현 ; TodoRepository 그대로 사용(delete(), findByUserId() 사용)
-서비스 구현 ; delete() 메서드 작성
-컨트롤러 구현 ; 컨트롤러의 @deletemapping 이용(delete니까!!), deleteTodo() 메서드 작성
-테스팅 ; postman 이용(H2 DB 사용으로 프로그램 재시작시 이전에 생성한 아이템은 삭제되므로, retrievee()때와 같이 새로 생성 후 테스팅 코드 실행해줌
프론트엔드
를 만져보자
0. 기본 이론
-Node.js ; 자바스크립트 런타임 환경. 브라우저 바깥에서도 실행할 수 있도록 함. 브라우저 밖에서도 실행 가능하다는 것은, 자바스크립트로 서버를 만들 수 있다는 것.
-NPM ; Node.js의 패키지 관리 시스템.
-브라우저의 동작 원리 ;
1. 애플리케이션 생성/필요한 패키지 설치
글고 material-ui 패키지를 설치(사이트 참고)
https://mui.com/material-ui/getting-started/installation/
2. 리액트 컴포넌트 작성
지금은 이렇게 임의로 지정한 이름이 정해진 똑같은 컴포넌트를 사용하고 있지만 이제는. 임의의 Todo 리스트+다른 타이틀을 가지고 있는 컴포넌트를 만들어 보기다.
; 필요한 매개변수를 생성자-constructor-를 통해 넘겨야줘야 하는데, 여기서는 constructor(props)~ 부분.
->super를 이용해 props 오브젝트를 초기화시키고, this.state*를 item변수와 props.item으로 초기화
*state = 리액트가 관리하는 오브젝트. 추후에 변경할 수 있는 변수를 이 오브젝트에서 관리함(js 내에서 변경한 변수의 값을 HTML에 다시 렌더링하기 위해)
이제 props에 item 을 넘겨주기 위해 코드 작성해줌
이렇게 작성하면 생성자를 통해 props를 넘겨받고, this.state에서 item을 초기화 > this.state.item을 이용해 item 오브젝트에 접근 > Todo에 item={<변수>}를 이용해 props로 매개변수를 넘겨줌
암튼 지금은
-id = 0
-title : Hello world1
-done : true(checked)
상태라, 실제로 실행해보면 이렇게 나옴
연습time~
1. Todo를 하나 더 만들고 상태값은 아래와 같음
-id = 0
-title : Hello world2
-done : false(unchecked)
2. todo를 배열 및 반복문을 통해 생성하고, 이 컴포넌트들을 넘기기

1) 일단 추가할 item을 items, 배열로 만들어 생성(초기화)

2)얘네를 배열 반복 해서 컴포넌트를 생성해줌

3)이제 이걸 { } 에 넣어서 생성된 컴포넌트를 리턴시키기


이제 이렇게 만들었으니... ui를 다듬어봅시다
material ui를 이용해서~~~!
Todo를 요렇게 수정해주고
app.js 도 그에 맞춰 수정해줌
실행하면 이런 화면이!
이번에는 새로운 todo 아이템을 추가하고, 관련 화면을 만들어봄
UI부분이 되어줄 새 컴포넌트를 작성해주고
return 부분에 새로 작성한 AddTodo 컴포넌트를 삽입!
여기까지 마치고 실행하면 이렇게 UI가 완성되어있음!
이제 이벤트 핸들러를 추가해서 기능을 넣어주면 됨...!!
UI만 갖추어진 상태라서 저기 + 버튼을 눌러도 아무런 일이 일어나지 않음~
추가해야 할 이벤트는 3가지.
1)onInputChange : 사용자가 input 필드에 키를 입력할 때마다 실행되고, 필드에 담긴 문자열을 js 오브젝트에 저장
2)onButtonClick : 사용자가 +버튼을 클릭할 때마다 실행되고, 1) 이벤트에서 저장하고 있던 문자열을 아래 리스트에 추가
3)enterKeyEventHandler : 사용자가 input 필드에서 엔터/리턴키를 눌렀을 때 실행되고, 기능은 2)와 같음(엔터키 사용으로 추가될 수 있도록 함)
이벤트1) 작성
1)onInputChange : 사용자가 input 필드에 키를 입력할 때마다 실행되고, 필드에 담긴 문자열을 js 오브젝트에 저장

일단 onInputChange 함수 작성 후, TextField 쪽에 함수를 연결해줌

그리고 실행하면, 이렇게 console 쪽에 textfield에 문자가 입력될 때마다 찍히는게 보임 > onInputChange() 가 정상 작동!
이벤트 2) 작성
2)onButtonClick : 사용자가 +버튼을 클릭할 때마다 실행되고, 1) 이벤트에서 저장하고 있던 문자열을 아래 리스트에 추가

app.js에 add 함수를 추가하고, 이 함수를 addTodo에 연결시켜줌

윗쪽 생성자에 우선 넘겨받은 props를 this.add에 연결시켜주고

그런 후에... onButtonClick 함수를 작성해서 넘겨받은 add함수를 사용하는 이벤트를 만들고, 실질적으로 이 이벤트가 동작하는 button에 이벤트를 연결시켜줌

그리고 실행하면, todo 아이템을 리스트에 추가시킬 수 있다!
이벤트 3) 작성
3)enterKeyEventHandler : 사용자가 input 필드에서 엔터/리턴키를 눌렀을 때 실행되고, 기능은 2)와 같음(엔터키 사용으로 추가될 수 있도록 함)

enterKeyEventHandler 이벤트를 추가해주고, 이걸 onkeydown을 통해 textfield에 넣어 textfield에서 키가 눌릴때+enter키가 눌릴 때 실행되도록 함.
*책에서는 onKeyPress()를 사용하도록 안내하나, 지금은 deprecated되었으므로 다른 방법으로 대체함!
>>onkeydown을 통해 변경

지금은 아무런 유효성 검사도 없지만... validation이 걸린다면 빈 title일 때 저장되는 걸 막을 수 있을 듯하다!
이번에는 삭제 기능 구현.
-리스트의 요소마다 삭제 아이콘 추가
-삭제 아이콘 클릭 시 아이템 삭제 기능
(*disableRipple 관련; https://www.daleseo.com/material-ui-buttons/ )
먼저 listItemSecondaryAction 태그 작성하고,
실행해보면 이렇게 요소마다 휴지통 아이콘이 붙어있는걸 볼 수 있음!
그러고 나서... 이제 새로운 delete 함수를 작성하고, 이를 Todo에 연결시켜줌
이제 생성자 쪽에 delete 추가해주고, deleteEventHandler 로 delete 함수까지 추가!
그런 다음, 아이콘이 눌릴 때마다 delete 함수가 실행되어야 하므로, icon button에 onclick을 통해 함수를 연결시켜줌
실행 후 아이콘 클릭을 통해 todo 아이템이 삭제됨!
마지막으로 수정부분...!!
타이틀 / 체크 박스 선택-해제 이렇게 두 개의 수정이 가능하게끔 기능을 추가할 것임
1)체크 박스 : item.done 부분을 true / false로 변경시켜주면 됨

1)checkbox의 상태를 true-false 변환할 수 있는 함수 checkboxEventHandler() 를 작성하고,
2)이걸 checkbox의 속성에 onclick / onchange를 통해 걸어줌

그럼 이렇게 정상적으로 잘~ 작동하는 걸 확인할 수 있음!
2)타이틀 수정 : title 부분을 클릭하면 수정이 가능하고, enter 키를 누르면 수정 내용이 저장됨

1)우선 readOnly라는 boolean형 상태 변수를 생성자에 추가하고, readonly상태를 변화시키는 함수를 추가

2) 그 후 readOnly 상태를 연결하고, onclick을 통해 위에서 작성한 readOnly상태를 변화시키는 함수를 연결
>inputBase에는 inputProps라는 props가 있고, 거기에 readOnly가 있음(여기에는 state.readOnly 값이 들어감)
>타이틀이 클릭될 때마다 readOnly가 fasle로 바뀌어 수정이 가능한 상태로 변경되어야 하므로, onClick을 통해 readOnly 상태를 false로 변화시켜주는 offReadOnlyMode 함수가 실행되도록 걸어줌!
이 상태로 실행시켰을 경우

타이틀을 클릭하면 커서가 뜨며 깜빡이는 것을 확인할 수 있음

3) 이번에는 ReadOnly를 true로 만드는, Enter키를 눌렀을 때 readOnly를 true로 바꾸는 함수를 작성
-새로운 함수 eventKeyHandler() 를 작성하고, 이를 input 필드의 onKeyDown 속성을 통해 작성

정상작동을 확인하기 위해, console에 enter~handler 작동 시 true complete가 출력되도록 추가.
엔터키를 눌렀을 때, 콘솔창에서 정상 작동되는 것을 확인

4) 이제 변경된 값을 저장해 줄 새로운 함수 edit~을 작성해주고, 작성한 값이 바뀔때마다 이 함수가 실행되어 변경된 값이 바로바로 변경될 수 있도록 onChange에 함수를 걸어줌


작성한 값을 수정하고, 마찬가지로 enter 키를 눌러 저장. enter 키를 누를 때 콘솔 창에 true complete가 뜨는 것을 확인 가능.
여기까지 하면 title 수정 완료!
여기까지 하면 우선 프론트엔드 애플리케이션은 작성 완료. 했다
이제... 서비스 통합의 시간.
작성한 백엔드 / 프론트엔드 애플리케이션을 통합하여 하나의 웹 애플리케이션으로 만들어주자...
1)Todo 아이템을 불러오는 부분을 먼저 구현 - 도메인 접속 시 Todo 아이템이 리스트에 보이게끔!
→렌더링 직후 백엔드 API콜 > 결과를 리스트로 보여주기 위해 API 콜 부분을 componentDidMount 함수에 구현*
(*자세한 사항 https://sum-milkyway.tistory.com/61 참고)

1. 먼저 componentDidMount() 를 추가 - 콘솔창을 확인해보면

이렇게 에러가 잔뜩 나와있는데...
이는 CORS* 헤더 policy를 위반했기 때문. (*https://sum-milkyway.tistory.com/64)
CORS가 가능하게끔 백엔드에서 방침 설정을 해주겠음!
2. 스프링으로 돌아가서,

새로운 패키지에 새로운 클래스를 생성하고, 이렇게 코드 작성
코드는 결국 모든 경로에 대해 Origin이 http~3000인 경우, get~등과 같은 메서드를 이용한 요청을 허용하며, 모든 헤더와 인증에 관한 정보도 허용한다는 뜻.
이렇게 작성-재시작 후 브라우저 새고하면

아까와는 다르게 정상적으로 접근(200)하여 연결된 것을 확인할 수 있음.
요청은 8080/todo로 갔고, 전과 같이 도메인이 다르나(localhost:3000) 잘 연결됨(200)
2) 이제 fetch를 이용해서, 애플리케이션이 사용할 백엔드 URI를 가져오도록 구현(도메인 변경 대비)

1. app-config.js 파일 생성 후, 다음과 같이 코드 작성
> 백엔드 서비스 주소 http://localhost:8000을 변수에 담고, 현재 브라우저의 도메인이 localhost인 경우 로컬 호스트에서 동작하는 백엔드 애플리케이션 사용.
*여기서 url을 잘못 설정하거나..오타를 낸다면 콘솔 창에서 해당 오류가 발생할 수도 있음(

(관련 게시글 참고 : https://sum-milkyway.tistory.com/67 )

2. src-service 디렉터리 생성, 그 아래에 ApiService.js 생성 후 백엔드로 요청을 보낼 때 사용할 유틸리티 함수 작성
(이렇게 작성하지 않으면 계속해서 콜 메서드를 매번 작성-실행해야 함)

3. ApiService.js에서 작성한 call 메서드로 기존 코드를 수정해줌
수정 후 실행 > Todo 아이템을 추가한 후 새로고침 / 아예 FE 애플리케이션을 껐다 켜도 을 해도 아이템은 사라지지 않음!
→백엔드의 DB에서 리스트를 가지고 오기 때문!
2)Todo 아이템 수정 기능 > API를 이용해 update가 되도록, 서버 데이터 업데이트 후 변경된 내용을 화면에 재출력하는 과정 필요



1. update() 함수 작성, 연결
; 지금까지는 직접 eventHandler를 지정-추가했기 때문에 App.js에서 함수를 따로 추가하지 않아도 작동했으나, 이제 백엔드와 연결해야 하므로 이 과정이 필요함
; call() 을 통해 백엔드 API와 연결-기능 추가 후, update()를 새로 추가하여 백엔드와 연결.

새로고침 후 수정 기능을 사용해보면... 요청 메서드가 PUT(정상 작동 200)이 되었음을 확인할 수 있음
이제 인증 절차의 시간....
백엔드 인증 절차를 먼저 진행함.
(관련 이론 : https://sum-milkyway.tistory.com/68 )
1. User 레이어 구현

1. UserEntity 작성

2. UserRepository 작성

3. UserService 작성



4. UserController 작성
여기까지 작성한 후 포스트맨을 통해 테스팅해봄!


잘~ 동작함을 확인할 수 있음.
1. User 레이어 구현까지 완료하면 회원가입-로그인까지 잘 동작함을 알 수 있음.
단, 지금까지의 과정에서는
1) 로그인 상태 유지 불가 - 로그인 구현까진 완료되었지만, 다른 api는 사용자의 로그인 확인 불가(REST API는 상태가 없으므로 로그인 상태를 기억하지 않음)
2) 지금까지 작성한 API는 사용자의 로그인 여부 자체를 확인하지 않음 - 임의로 사용자 아이디를 작성해주었기 때문에, 지금 상태에서는 로그인 API를 사용하지 않음!
3) 패스워드를 암호화하지 않음
2. JWT 생성 및 반환 구현 > 지속적인 인증과 로그인 상태의 유지를 위해 구현
1. JWT 생성 및 반환 부분 구현

1) 일단 JWT 관련 라이브러리-jjwt- 디펜던시에 추가( https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt/0.9.1 )


2) 인증과 인가를 위한 모든 클래스를 관리하는 security 패키지 생성,
이후 TokenProvider 클래스 생성 > 사용자 정보를 받아 JWT를 생성하는 클래스


3) 이제 이렇게~ User 레이어 구현 시 작성했던 코드 중 token 생성 및 반환과 관련된 부분에 security-부분의 메소드를 이용해 리펙토링 진행
*테스팅 도중 오류가 발생하여 dependencies 에 아래 코드 추가 - gradle refresh & restart 해주기
자세한 포스팅은 > https://sum-milkyway.tistory.com/69

여기까지 하고 테스팅해보면,


짠~로그인 부분도 정상 작동됐고 토큰을 발행-로그인 시 반환되는 부분까지 구현 완료됨!
3. JWT를 이용한 인증 구현 > 스프링 시큐리티 이용

1) 스프링 시큐리티 디펜던시 추가 > gradle refersh 해주기



2) 서블릿 필터 구현 > 토큰을 이용해 인증 - 인증정보 저장 - 인증된 사용자 저장
* OncePerRequestFilter ; 한 요청당 반드시 한 번만 실행됨
* 스프링 시큐리티의 SecurityContext ; createEmptyContext() 메서드 이용해 생성 - context에 인증정보인 authentication을 넣고 다시 securitycontextholder**에 context로 등록.
**기본적으로 ThreadLocal에 저장되며, Thread마다 하나의 context를 관리할 수 있으며 같은 Thread 내라면 어디에서든 접근 가능함
3) 서블릿 컨테이너에서 이 필터를 사용하라고 알려줌
ㅎㅎ
ㅎㅎ