서버를 돌리지 않아도 웹에서 보았을 때 수정된 내용이 바로 적용되도록 하는 방법 (보통 프로젝트를 생성할 때 미리 다 설정한다.)
- 여기에는 tomcat(+spring)을 실행시킬때의 설정들이 들어있다.
dev.jwkim.mvc.MvcApplication
이 시작점인걸 알려준다.
update classes and resources
두개 추가
- 반환타입을 String으로 써야하지만 보통
ModelAndView
을 많이 사용한다.
- "home/index" 리턴한다.
ModelAndView
을 사용하면 html에서 컨트롤러로 값을 넘기기에 용이하다.
- 참고) body바로 아래 자식에 있는 id는 css에서 사용해도 괜찮다.
id는 하나만 있을 걸 알기 때문에 속도면에서 빠르다.
전송버튼을 누르면 home/index
로 요청이 나가게 된다.
form에 action 속성이 없으면 현재 주소로 요청을 보낸다.
form에 action 속성이 있으면 그 주소로 요청을 보낸다.
form태그에 action을 주지않고 post요청을 하려면 동일한 맵핑메서드를 만들면 된다.
전송버튼 누르고 보여주는 화면은 POST가 보여주는 페이지이다.
새로고침을 해서 다시 보는 화면또한 POST 요청일것이다.
- 새로고침을 했을 때 똑같은 내용이 뜬다는 것을 확인해보자
즉, GET요청이 아니라 POST요청으로 계속 돌아가고 있다는 것을 확인해보자.
새로고침방법이
데이터를 이미가지고있다.
- 현재 html / css 결과
메세지를 배열에 담자.
private String[] messages = new String[] {};
String[] oldMessages = messages;
message = new String[messages.length + 1];
for (int i = 0; i < oldMessages.length; i++) {
message[i] = oldMessages[i];
}
message[message.length -1] = message;
for (String m: messages) {
System.out.println(m);
}
=
messages.forEach(x -> {
System.out.println(x);
});
=
messages.forEach(x -> System.out.println(x));
// 람다(lamda) 표현식
=
messages.forEach(System.out::println);
: 이중 콜론 연산자, 메서드 참조 표현식,
[객체]::[메서드]
목적은 줄이기 위해서 사용한다.
Collector할때도 사용하긴 한다.
[객체]::new
기초타입의 배열이다. 메모리에 쌓이는 방식은 참조타입이랑 같다. - java Stream API
int[] aa = null;
int[] numbers = new int[]{1,2,3,4,5};
int[] evens = Arrays.stream(numbers).filter(x -> x % 2 == 0).toArray();
<html>
태그에xmlns:th="http://www.thymeleaf.org"
라는 내용을 추가하여 해당 HTML 문서에서 Thymeleaf 문법을 사용하겠다 라고 알려준다.
- 실제로 Thymeleaf에 쓴 내용은 클라이언트에게 전달 되지 않는다.
${}
은 Java문법을 사용하고자 할 때 사용한다.
- 소스보기를 하면 8만 뜨는 것을 확인할 수 있다.
- 속성
th:text="내용"
: 해당하는 태그의 내부 내용을 해당 속성 값으로 한다.th:each="item(원소이름) : ${arr}"
: 'arr' 배열 혹은 리스트 등으로 부터 반복할 수 있는 원소인 'item' 에 대해 해당 태그를 반복한다.
제네릭 타입
String 반복이 될 것이다.
새로고침을 해도 남아있다.
둘 다 비정적인 메서드고 속해있는 클래스내에 호출하는데 문제가 없다.
study-database 프로젝트 생성
MVC 패턴으로 코드 작성하면서 데이터베이스를 연결해보자.
- controller의 이름을 붙여주는 것이다.
ModelAndView
를 return 던지기 위해ModelAndView
를 직접 객체화 하지 않아도 매개변수로 적게되면 spring이 알아서 객체화해준다.
< 자바 스크립트 코드 정리 >
내용을 적지 않으면 form 요청이 나가지 않고 warned를 뜨게 한다.
<script>
const main = window.document.getElementById('main');
const form = main.querySelector(':scope > .form');
const writerRegex = new RegExp('^([가-힣]{2,5})$'); // RegExp : Regular Expression(정규식) 타입
form.onsubmit = () => {
if(form['writer'].value === '' || !writerRegex.test(form['writer'].value)) {
form['writer'].classList.add('warned');
form['writer'].focus();
return false;
}
if(form['content'].value === '') {
form['content'].classList.add('warned');
form['content'].focus();
return false; // 내용을 적지 않으면 form 요청이 나가지 않음.
}
return true; // 자바스크립트에서는 안적어줘도 되지만 명시적으로 해준다.(자바에서는 말이 안되기때문에)
};
</srcipt>
입력을 하게 되면 warned를 사라지게 한다.
<script>
form['writer'].addEventListener('keydown', () => { // 입력을 하게 되면 warned 사라진다.
if(form['writer'].classList.contains('warned')) {
form['writer'].classList.remove('warned');
}
});
form['content'].addEventListener('keydown', () => {
if(form['content'].classList.contains('warned')) {
form['content'].classList.remove('warned');
}
});
</srcipt>
return false;
을 하게 되면
: form 요청이 나가지 않는다.
매개변수 e 받아서 e.preventdefault 하고 return 해줘도 된다.
keydown : 키를 누르는 것.
키를 누르면 다시 올라온다. 그때는 keyup이다.
완성 한글 2자~5자까지만 되게 하도록 정규식을 사용해보자!
정규표현식(Regular Expression)은 언어의 종류와 관계 없이 주어진 문자열에 대해 정규화 (Noramlize) 를 하기 위한 공통된 문법
^ : 문자열의 시작 $ : 문자열의 끝 () : 그룹 [] : 표현 범위 {} : 길이
^( )$ ↑ 완성한글 && 2자~5자가 되는지 확인한다.
`홍길동` 이라는 문자열의 시작부터 끝까지를 검사한다.
^([가-힣]{2,5})$
가
가 시작이고 힣
이 가장 마지막이다.여기서 나의 정규식이 올바르게 작동하는지 체크할 수 있다.
String name = "홍길동"; return name.matches("^([가-힣]{2,5})$");
자바에서는 이렇게 사용한다.
- 정규식에 맞게하지 않으면 작성자를 입력해달라고 뜨게 된다.
- 어차피 빈문자열이면 정규식 통과를 못하기 때문에 지워주는 것이 깔끔한 코딩이 된다.
- 현재 값을 입력하고 submit을 하게 되면 오류가 발생한다.
주소는 있는데 그 요청을 받는 맵핑이 없기 때문에 405가 뜬다.
- POST 맵핑 생성.
POST 맵핑으로 값이 두개가 넘어온다. 개발자 도구를 통해서도 확인이 가능하다.
- writer / content 가 Requestparam의 value가 되고
html의 Input의 name이 될 것이다. 이 두개가 일치해야 값이 넘어온다.
- value 와 name이 일치하기에 값이 잘 넘어온다.
넘어온 값을 DB에 담아보자.
root말고 다른 계정을 생성하고 삭제하고 수정하는 방법
CREATE USER `이름`@`호스트` IDENTIFIED BY '비밀번호';
DROP USER `이름`@`호스트`;
ALTER USER `이름`@`호스트` IDENTIFIED BY '새 비밀번호';
GRANT 권한,... ON `스키마`.`테이블` TO `이름`@`호스트`;
-- 특정 스키마/테이블에 대해 명시한 유저에게 명시한 권한을 준다.
REVOKE 권한,... ON `스키마`.`테이블` FROM `이름`@`호스트`;
-- 특정 스키마/테이블에 대해 명시한 유저에게 명시한 권한을 뺏는다.
호스트(Host) 자리가 의미하는 것은 앞에 적어놓은 사용자의 이름으로 접속을 할수 있는 원격 호스트를 제한하는 것이다.
즉, root@localhost라는 계정으로는 그 DB서버가 설치되어있는 컴퓨터로만 접근이 가능하다는 것이다.
예시를 들어보겠다.
본 컴퓨터에서 사용자를 두개 생성을 했다.
CREATE USER `study`@`localhost` IDENTIFIED BY 'test1234';
CREATE USER `study`@`%` IDENTIFIED BY 'test1234';
GRANT ALL ON *.* TO `study`@`localhost`;
FLUSH PRIVILEEGES;
본 컴퓨터에만 SELECT 권한을 주었다.
본 컴퓨터 : study → SELECT ~ → 정상 실행
본 컴퓨터가 DB가 바라본 본 컴퓨터의 접근은 본 컴퓨터에서 하는 것이 맞다라고 한다. study@localhost 로 접속이 된다.
외부 컴퓨터 : study → SELECT → ~SELECT command denied ~ : 거절이 되었다 라고 뜨게 된다.
본 컴퓨터의 DB에서 보았을 때 외부 컴퓨터 (본 컴퓨터의 DBMS가 없는)의 접근은 외부인으로 간주한다.
study@ 192.168.1.254 로 접속이 되는 것이다.
@ 뒤로 부터는 마음대로 지정할 수 없기 때문이다.
터미널로 넘어가자.
mysql -u root -p -P 3306
-> password 입력
CREATE USER `study`@`localhost` IDENTIFIED BY 'test1234'; GRANT ALL ON *.* TO `study`@`localhost`; FLUSH PRIVILEEGES; // 변경 된 권한 정보 업데이트
- 터미널에서 이거 해주면 사용자 생성이 되는 것이다.
- 다 작성후 Test Connection -> Succeeded
문의내용 담는 테이블 생성
테이블이 가지는 열에 대한 1:1 매칭 멤버변수 생성해야한다.
그리고 getter, setter 생성
: 캡슐화
-> entities > InquiryEntity
위의 Entity에서 index랑 created_at은 알아서 값이 정해지기 때문에 나머지 두개에 대해서만 받아주면 된다.
html파일에서는 writer, content의 값이 넘어오게 된다. 컨트롤러 포스트로 넘겨주자.
InquiryEntity inquiry = new InquiryEntity(); inquiry.setWriter(writer); inquiry.setContent(content);
- 웹에서 입력한 값이 객체화된 Entity를 통해 컨트롤러로 넘어오고 정상적으로 출력이 된다.
객체화하고 값을 지정하고 불러오는게 귀찮으면 @RequestParam 대신 Entity를 매개변수 자리에 넣자.
- 어디에도 writer, content에 대한 언급이 없는데 똑같이 값을 잘 출력한다.
왜 이렇게 되는지 알아보자.
작성자를 가지고 있는 html input태그의 name은 writer이다.
작성자 writer -> Writer (파스칼케이싱) -> setWriter메서드를 호출하는 것이다.
문의 내용 content -> Content -> setContent
Entity의 매개변수 또한 input name이 같아야 작동을 한다.
하나는 수동으로 해줘야할 것이 있다.
- InquiryEntity의 Date의 기본값이 현재시각이다 라는 것은 아래에 명시가 되어있지만
inquiry.setCreatedAt(new Date());
을 해주지 않으면 null이 들어가게 됨으로 방지해준다. new Date() : 현재시간
다른 방법 ) 멤버변수에 초기화를 해주면 위처럼 안해도 된다.
Servie를 만들자.
Servie 또한 Controller에서 Controller어노테이션을 달아준것처럼 Servie도 Servie어노테이션을 달아줘야한다. Servie임을 알려주자.
value : 서비스 이름
: 해당 클래스를 서비스(Service)로 사용될 것 임을 스프링 부트에게 알린다.
의존성 주입!!
: HomeController를 이용하려면 스프링 너가 알아서 InquiryService로 객체화해서 집어 넣어놔라 라고 해주는 것이다.
- HomeController의 멤버상수로 만든다
private final InquiryService inquiryService;
멤버상수는 선언만 해놓으면 안되고 선언과 동시에 값을 초기화 하던가 아니면 생성자를 생성자를 통해서 값을 집어넣어야한다.
InquiryService inquiryService = new InquiryService();
생성자를 이용하자.
@Autowired
public HomeController(InquiryService inquiryService) {
this.inquiryService = inquiryService;
}
- InquiryService를 매개변수 타입으로 가지는 생성자 생성.
그리고 @Autowired 어노테이션을 붙여준다!
: 해당 어노테이션이 적용되는 범위 내에 있는 것에 대해 스프링 부트가 알아서 객체화를 하도록 유도한다. 단, 해당되는 타입은 스프링 부트가 인식하고 있어야한다.
- inquiryService라는 매개변수에 전달되어야하는 전달인자를 인식가능한 범위내에서! 스프링 부트가 알아서 InquiryService을 객체화해서 전달해준다.
@Service
어노테이션을 붙임으로써 서비스임을 알려줬기 때문에 스프링이 인식가능한 것이다. 즉,@Service
어노테이션이 없다면 불가능하다.
이런것을 의존성 주입이라고 한다.
: HomeController는 InquiryService에 의존적임
왜냐하면 HomeController의 생성자는 InquiryService 밖에 없기 때문에 InquiryService 없이는 만들어 질 수 없다.
HomeController homeController = new HomeController(...);
이렇게 새로 만들으려면 ...에 InquiryService을 주는 방법밖에 없다.
그렇게 되어도 HomeController는 InquiryService에 의존적이다.
...에 InquiryService을 넣어주지 않으면 HomeController는 객체화 될 수 없다.
Controller < Service < Repository 방향으로 의존적이다.