그럼에도 불구하고 위 사진처럼 너 와 나 사이에 공간이 생기는걸 볼 수 있는데, 이는 버그가 아니라 HTML 문서에서 줄바꿈이 인식되어 공간이 발생한 것이다. 즉 이를 해결하기 위해서는 빈공간에 대한 사이즈를 줄여야 하는데, 다음과 같은 방법이 있다.
1. <p> 태그를 작성하면서 엔터(줄바꿈)을 하지 않는다. 2. p-wrapper내부에 있는 공간을 인식하는 것 이기 때문에, p-wrapper내의 글자 크기를 줄인다. 3. <p>태그의 닫힘 문자를 건너뛴다. (</p>를 쓰지 않는다. 이는 문제가 될 수 있어서 비추천...)
그러나 이 역시 p태그의 요소와 내용이 간결하기 때문에 보기 쉽지만, 클래스가 여러 개 들어가거나, 내용이 길어질 경우 꽤 tricky 해질 수 있기 때문에 비추천 한다.
그래서 내가 주로 사용하는 방법은 2번 방법이다. css에서 p-wrapper의 글자 크기(줄바꿈을 없애기 위해)를 0으로 설정하고, p-wrapper내에 직접적인 글자를 작성하지 않는 것이다.
#p-wrapper{
float: left;
background-color: #f1cc82;
font-size: 0; /* 여기서 p-wrapper의 글자를 지우고 */
}
.p-tag{
display: inline-block;
background-color: #ccc;
font-size: 16px; /* 여기서 p tag의 글자 크기를 설정한다. */
}
만약 p-tag에서 font-size를 지정해주지 않으면, 다음과 같은 문제가 발생할 수 있으니, 꼭 p-wrapper 내부의 어떤 요소에 글자를 작성한다면, 해당 요소의 css에서 글자 크기를반드시지정해 줘야 한다.
정상적으로 모든 요소에 글자 크기 설정을 진행하고 나면, 아래와 같이 빈 공간이 사라진 채로 요소를 붙일 수 있다.
내가 만든 웹 서버가 HTTP가 아닌 HTTPS에서 통신하게 하려면 SSL이라는 인증서가 필요하다. Secure Socket Layer, 전송 계층 보안은 보안된 환경에서의 통신을 제공하기 위한 규약이다. 조금 왜곡을 보태 간단하게 말하면 HTTP의 취약한 해킹을 보안하기 위해 SSL을 이용함으로써 해킹으로부터 안전할 수 있는 것이다.
뿐만 아니라 간혹 웹서버 개발 중에 CORS 관련 오류나, 오류 내용에서 HTTPS에서만 가능하다. 는 문구를 확인할 수 있는데, 이를 해결하기 위해서는 HTTPS가 필요하다.
유료 HTTPS의 경우 비싼 돈이 들기 때문에 나같은 학생 개발자나 간단한 개발에서는 굳이 돈을 내고 쓸 필요가 없는데, 이 때 LetsEncrypt를 이용해 SSL 인증서를 무료로 발급받을 수 있다. (LetsEncrypt! 누르면 홈페이지로 이동)
LetsEncrypt를 우선 받아야 하는데, Github에 올라온걸 clone 해주면 된다.
git clone https://github.com/letsencrypt/letsencrypt [PATH/TO/DOWNLOAD] # 나같은 경우 /opt/letsencrypt
Requesting to rerun ./certbot-auto with root privileges... Your system is not supported by certbot-auto anymore. certbot-auto and its Certbot installation will no longer receive updates. You will not receive any bug fixes including those fixing server compatibility or security problems. Please visit https://certbot.eff.org/ to check for other alternatives. Saving debug log to /var/log/letsencrypt/letsencrypt.log Plugins selected: Authenticator manual, Installer None Obtaining a new certificate Performing the following challenges: dns-01 challenge for dev-whoan.xyz - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NOTE: The IP of this machine will be publicly logged as having requested this certificate. If you're running certbot in manual mode on a machine that is not your server, please ensure you're okay with that. Are you OK with your IP being logged? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y
위에서 *.dev-whoan.xyz에 대한 와일드카드 SSL 인증서 발급을 요청했고, 이후에 설명이 나온다. IP 로그에 대해 거절(N)하면, SSL 발급이 취소된다. Y를 입력하면 다음과 같이 DNS TXT 레코드를 입력하라고 하는데(도메인 주인 확인을 위한), 사용중인 도메인의 TXT 레코드를 추가해주면 된다.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please deploy a DNS TXT record under the name _acme-challenge.dev-whoan.xyz with the following value: 입력해야할 TXT 값 Before continuing, verify the record is deployed. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Press Enter to Continue
이 때, TXT 레코드를 추가해 줄 때 _acme-challenge만 TXT name으로 입력해주면 된다. 뒤의 dev-whoan.xyz까지 모두 입력하면 안된다. (모두 입력하면 txt는 _acme-challenge.dev-whoan.xyz.dev-whoan.xyz에 기록되기 때문)
이후에 다음과 같은 글이 뜨면 성공!
Waiting for verification... Cleaning up challenges Subscribe to the EFF mailing list (email: dev.whoan@gmail.com). IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/dev-whoan.xyz/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/dev-whoan.xyz/privkey.pem Your cert will expire on 2021-05-04. To obtain a new or tweaked version of this certificate in the future, simply run certbot-auto again. To non-interactively renew *all* of your certificates, run "certbot-auto renew" - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le
이 외의 글이 뜨면 오류도 함께 출력되니, 그에 맞게 해결하면 된다. (대게 와일드 카드 혹은 TXT 레코드를 이용한 도메인 주소 검증에서 실패하는 경우, _acme-challenge를 잘못 설정해서 그런 경우다.)
하루에 10번 이상인가? 실패하면 24시간 이후에 인증서 발급이 가능하니 주의하도록 하자.
프로그래밍에서 의존성이라고 하면, 예를 들어 A라는 객체를 만들 때 A의 원형태라고 생각하면 쉽다. 즉, A 객체는 A 클래스에 의존하여 생성되는 것이다.
Car myCar = new Hyndai();
위 코드에서는 myCar가 Hyndai에 의존한다는 것을 알 수 있다.
프로그램을 만들고, 이후에 코드를 업데이트 해야하는 경우가 발생하면, A와 연관된 클래스를 모두 수정해야 하는 일이 발생한다. 즉 의존성을 일일이 모두 변경해줘야 하는 것이다.
만약 내가 현대차에서 Kia차로 바꾼다면 어떻게 해야할 까? myCar가 사용되는 모든 곳에 대해 Hyundai를 Kia로 바꿔줘야 하는 상황이 발생하는 것이다.
Car myCar = new Kia();
Car myCar = new Hyundai();
...
class Hyundai extends Car{
...
}
class Car implements Vehicle{
...
}
interface Vehicle{
...
}
기존에는 위와 같이 코드를 작성한다 할 때, myCar를 만드는 순서는 다음과 같다.
Hyundai -> Car -> Vehicle
Class Diagram으로 설명하면, 위 코드는 다음과 같은 그림을 갖는다.
즉 myCar를 만들기 위해 Hyundai를 만들고, Hyundai를 만들기 위해 Car, Car를 위해 Vehicle이 만들어 지는 것이다.
위처럼 Car myCar = new Hyundai()처럼 직접 객체를 만들고 의존하는 방식을 Composition has a 라고 하는데, 우리는 연결형, 즉 Association has a를 만들려고 한다.
Composition has a는 위에서 언급 했듯이, Kia차로 차를 바꾼다면 모든 myCar를 Kia로 바꿔줘야 하기 때문이다.
Association has a를 한 코드로 예를 들면, 다음과 같다.
...
class Car{
Car car;
...
void setCar(Car car){
this.myCar = car;
}
}
...
Hyundai hyundai = new Hyundai();
Car myCar;
myCar.setCar(hyundai);
...
Car를 연결하는 Hyundai를 따로 만들고, 이를 setter 등을 이용하여 myCar의 Car에 연결하는 것이다. 이렇게 되면 myCar를 바꿀 필요 없이 Hyundai의 선언만 Kia로 바꿔주면 된다.
하지만 스프링은 이를 xml 등을 이용하여 소스를 직접 수정하지않고, 구성 파일만 수정함으로써 의존성을 변경할 수 있게 해준다.
이를 의존성을 주입한다고 하는데, 영어로는 Dependency Injection다.
Inversion of Control ?
갑자기 Inversion of Control이 나와서 이게 뭘까 싶을 수 있다. IoC는 자바의 DI를 이해하기 위해서 알아야 하는 속성인데, 구글 번역기를 돌려보면 "제어 반전"이라는 해석이 나온다. 제어??? 반전??? 도대체 무슨말이야??
위에 Class Diagram으로 Composition has a의 순서와 반대로 생성되는 것이다.
Vehicle -> Car -> Hyundai
이 순서로 생성되는 것인데, 이는 이미 스프링의 어떤 xml 파일에서 beans로 정의되어 있기 때문에 가능한 것이다.
xml에 정의되어있는 어떤 컨트롤을 내 소스에 주입 하는 것인데, 쉽게 말하면 만들어진 Hyundai, Kia를 내 myCar.setCar(hyundai) 혹은 myCar.setCar(kia)와 같은 방식으로 자동으로 설정되게 하는 것이다.
이를 이용하면 소스파일의 수정 없이 의존성을 주입할 수 있다.
직접 주입해보자!
이러한 스프링 xml파일을 만들기 위해서는 springframework의 spring-context를 이용하여야 한다. 이를 pom.xml에 넣어주자. 이는 maven을 이용한다면, maven에서 dependency를 추가해주면되고, 그렇지 않다면 직접 코드를 추가해주면 된다. 나의 경우 maven repository에서 추가했고, 추가된 코드는 다음과 같다.
이후에 src폴더에 xml만 모아놓은 폴더여도 좋고, DI를 이용하는 소스가 있는 폴더에 넣어도 좋다. 개인적으로는 DI xml을 넣는 폴더를 따로 만들어 관리하기를 추천한다.
이클립스에서 새로 추가하기 -> Other -> Spring -> Spring Bean Configuration File을 추가해 주고 이름을 적절히 설정하면 name.xml파일이 생성된다.
우선, Car 클래스와 관련된 소스는 아래와 같다.
public interface Vehicle {
int getWheels();
String getNames();
}
public class Car implements Vehicle {
String name;
int wheels;
public Car(String name, int wheels) {
this.name = name;
this.wheels = wheels;
}
@Override
public int getWheels() { return wheels; }
@Override
public String getNames() { return this.name; }
public void setWheels(int wheels) { this.wheels = wheels; }
public void setName(String name) { this.name = name; }
@Override
public String toString() {
return "Car [name=" + name + ", wheels=" + wheels + "]";
}
}
public class Hyundai implements CarUI {
private Car car;
public Hyundai() {
}
@Override
public void print() {
System.out.println("*************");
System.out.println("HYUNDAI " + car.toString());
System.out.println("*************");
}
public void setCar(Car car) {
this.car = car;
}
}
public class Kia implements CarUI {
private Car car;
public Kia() {
}
@Override
public void print() {
System.out.println("=============");
System.out.println("KIA " + car.toString());
System.out.println("=============");
}
public void setCar(Car car) {
this.car = car;
}
}
public interface CarUI {
void print();
}
Spring을 사용하지 않을때 myCar를 설정한다면, 다음과 같이 해야 했다.
public class MyOwnCar {
public static void main(String[] args) {
Car hyundai = new Car("SUV", 4);
Hyundai myCar = new Hyundai();
myCar.setCar(hyundai);
myCar.print();
}
}
즉, 내 차가 Kia차로 바뀐다면, 아래와 같이 바뀌어야 했다.
public class MyOwnCar {
public static void main(String[] args) {
/*
Car hyundai = new Car("SUV", 4);
Hyundai myCar = new Hyundai();
*/
Car kia = new Car("SUV", 4);
Kia myCar = new Kia();
myCar.setCar(kia);
myCar.print();
}
}
이제는 myCar.xml을 통해 이러한 행동을 하지 않으려고 한다.
스프링에서는 xml과 같은 DI 지시서를 이용하기 위해서는 Spring context를 이용하여야 하는데, 코드로 다음과 같이 작성한다.
ApplicationContext context = new ClassPathXmlApplicationContext("spring/di/myCar.xml");
ApplicationContext를 xml로부터 가져오는데, "spring/di/myCar.xml"이 내가 불러오고자 하는 DI 지시서의 경로인 것이다.
이후 context.getBean을 통해 해당 DI 지시서에 등록되어있는 beans를 이용할 수 있다.
bean을 이용하는 방법은 다음과 같다.
<bean id="의존성 주입할 객체의 변수명" class="해당 객체의 형태를 나타내는 class경로" />
즉, 내 myCar의 경우에는
<bean id="myCar" class="spring.di.entity.Car" />
이 된다.
만약 의존성을 주입할 때 그냥 설정만 하는거라면 우리가 일일이 변수를 다 변경해줘야 하고, 그렇다면 스프링은 지금처럼 많이 쓰이지 않았을 것이다. 한마디로 스프링은 변수와 같은 속성 설정도 지원하는데, 다음과 같은 속성을 사용할 수 있다.
1. index : n번째 index를 설정한다. 2. name : name과 동일한 이름을 갖는 변수를 설정한다. 3. p : p:"NAME" 에서 "NAME"과 동일한 변수를 설정한다. 이 때, xmlns:p="http://www.springframework.org/schema/p" 를 추가하여야 한다. 4. setter를 직접 사용한다.
각각 사용하는 방법은 다음과 같다.
<bean id="myCar" class="spring.di.entity.Car">
<!-- 1번. index를 이용하는 경우 -->
<constructor-arg index="0" value="SUV" />
<constructor-arg index="1" value="4" />
<!--
만약 생성자가 2개 이상이고, 자료형만 다른 parameter를 전달 받는다면 type을 이용해 구분할 수 있다.
<constructor-arg index="0" type="int" value="4" />
<constructor-arg index="0" type="String" value="SUV" />
-->
<!-- 2번 name을 이용하는 경우 -->
<property name="name" value="SUV" />
<property name="wheels" value="4" />
<!-- 3번 p 속성을 이용하는 경우 -->
<bean id="myCar" class="spring.di.entity.Car" p:name="SUV" p:wheels="4" />
<!-- 4번. 직접 setter를 이용하는 경우 -->
myCar.setName("SUV");
</bean>
나머지 경우는 모두 직접 해보시길 바라고, 나는 2번 name을 이용하였을 때의 결과를 보여주겠다.
JSP 혹은 서블릿을 이용해 웹 서버를 개발할 때 파일 입출력 관련 프로그래밍을 해야하는 경우가 있다. 이 때, 출력 파일의 권한을 수정할 때, 수정되지 않는 경우가 있다. 이번 게시글에 작성할 문제의 경우, 파일을 생성할 때는 정상적으로 권한이 변경됐으나, 폴더를 생성할 때는 권한이 정상적으로 변경되지 않았다. 아무리 생각해봐도 톰캣의 권한 문제라면 생성되는 파일또한 권한이 정상적으로 변경되지 않아야 했으나, 정상적으로 출력되는 바람에 꽤 많이 헤매었다.
2021.11.23 추가
만약에, 아래 문단의 방법으로 안될 경우 아래 명령어를 통해 ReadWritePaths가 정상적으로 수정되었는지 확인해보자. 아직까지 왜 파일이 설정한대로 안바뀌는지 이유는 찾지 못했다. 이유를 찾으면 게시글을 업데이트 하겠당.
$ systemctl edit --full tomcat#.service
...
ReadWritePaths=대상폴더 ...
이 명령어를 통해 어떤 폴더에 권한을 줄 경우, 해당 폴더는미리 생성되어 있어야 한다. 뿐만 아니라, 아래 방식의 tomcat9.service에도 똑같은 내용을 적어놓는 것을 추천한다.
Tomcat 버전 9 이상의 경우, 톰캣이 실행할 웹앱 디렉토리의 소유자에 대하여 tomcat 계정, 혹은 tomcat이 추가된 그룹으로 바꾸고, 아래 루트로 이동하여 ReadWritePaths에 해당 웹앱의 ROOT 디렉토리를 추가한다.
정의된 RESTful API를 구현하는 도중 Client측에서 요청한 정보에 대해 결과를 모두 반환하지 않고 중간에 글자가 잘리는 버그가 있었다.
해당 버그를 해결하지 않은 시점에서 본 게시글을 작성하기 시작했는데, 버그는 쉽게 해결됐다.
버그의 발생은 라즈베리파이에서 MkWeb의 RESTful API를 사용할 경우 모두 출력되지 않고 종료되는 (성능상의 이슈로 인해) 문제가 있었는데, 해당 문제를 해결하기 위해 PrintWriter의 버퍼를 일정 주기를 내가 강제하여 flush 하였더니, 기존에 잘 동작하던 플랫폼에서도 해당 이슈가 발생하였다.
flush를 지워봐도 해당 문제가 지속되어, flush를 내장하고 있는 PrintWriter의 close() 메소드를 사용해보니 이유는 모르겠지만 기존 플랫폼에서는 버그가 해결됐다.
아무리 생각해봐도 flush를 강제적으로 해주면, 성능 저하의 이슈가 발생할지라도 정상적으로 출력돼야 하는데 잘 모르겠다.
해당 문제의 원인을 알아내면 이어쓰도록 하겠다.
2021/01/07 21:32분 수정
오류의 원인을 발견하였다. HttpServletResponse 객체의 헤더 중 Content-Length를 직접 설정해 주었는데, 이로 인해 response의 PrintWriter 객체가 내가 Content-Length를 설정한 만큼만 출력하게 되어 있었다. 따라서 실제 출력하고자 하는 내용이 모두 출력되지 않는 버그가 간간히 있었다.
RESTful API PUT method를 어떻게 구현할지 많이 고민 했다. RESTful API에 대한 정보를 찾아봐도, 특성만 나타나 있지 실제로 어떤식으로 구현해야할지는 없었다.
MkWeb의 초기 버전은 RESTful API의 성능을 떠나서 "동작하는 기능"으로써의 구현을 진행하고 있기 때문에, 개발 및 테스트 하면서 원리를 이해하고, 앞으로의 수정방향을 잡고 있기 때문에 일단 구현해 봤다.
MkWeb에서 PUT Method는 다음과 같이 정의 하였다.
삽입 / 수정, 즉 UPDATE를 담당하는 METHOD
PUT Method에 관한 응답 및 처리를 찾아 봤을 때, 어떤 글은 GET과 같은 조회와 POST의 삽입의 기능을 담당한다.
또 어떤 글은 POST의 삽입 기능과 SQL의 UPDATE문을 담당한다는 등 크게 두 파로 나뉘어 있었다.
그래서 사실 조회의 경우 URL을 통해 쉽게 할 수 있으니, 삽입과 수정의 기능을 바탕으로 만들었다.
그리고 수정의 경우, PATCH로써 동작하게끔 했다. ( PATCH 를 넣으니, 톰캣의 버전 때문인지 정상적인 method가 아니라며 실행되지 않았기 때문. 후에 가능하다면 MKWeb을 업데이트 할 때 PATCH를 새로 만들 예정이다.)
조회와 삽입을 구분하는 기준은 다음과 같이 정했다.
URI에 수정하고자 하는 대상의 조건을 보내고, Body Parameter에 수정 내용을 입력한다. 이 때, 대상이 존재하지 않으면 삽입, 존재하면 데이터를 수정한다.
이렇게 정의하고 나니 한결 쉬워졌는데, 데이터 입력 및 수정 할 때, 삽입/수정하고자 하는 column이 Not Null이고 Deafult 값이 없거나 하는 경우에는 오류가 발생해서 난감했다.
사실 이 문제는 RESTful API의 본질적인 기능을 잊고, 기능 구현에 집중하다보니 발생한 문제였는데, message와 error code를 보내주는걸로 해결했다.
사용 예는 다음과 같다.
URI : /users/name/dev.whoan body parameter: email=dev.whoan@gmail.com
users Data Set에서 name column이 dev.whoan인 사람의 email을 dev.whoan@gmail.com으로 수정한다. 여기에 입력되지 않은 column은 기존 값을 유지한다.
삽입 URI: /users/name/whoan body parameter: email=dev.whoan@gmail.com, name=whoan
users Data Set에 name=whoan이라는 사람이 존재하지 않으면, email=dev.whoan@gmail.com, name=whoan으로 데이터를 삽입한다. 이 때, 모든 column이 입력되어야 한다. 모든 column이 없으면, 400 Bad Request와 모두 입력하라는 message를 보내주었다.
구현할 때 문제점이 있었는데, MkWeb의 SQL을 정의하는 구간에서 각 data를 짜집어 SQL을 미리 정의해두는게 있었는데, 이 때 이미 SQL이 설정되어 있어서 따로 수정할 수 없는게 문제였다. 이는 따로 RESTful API전용 SQL Creator를 만들어 고쳤다. (여기서 중요한건 이게 아니니 패스)
Data의 유/무를 확인하기 위해 GET Method를 통해 PUT Method 요청시 특정된 데이터가 존재하는지 확인했고, 특정된 데이터가 있다면 update, 없으면 insert문을 수행하게 했다. 사실 insert문의 경우 POST로 대체할 수 있지 않을까 싶었는데, PATCH의 기능을 수행하게 하였기 때문에 column의 값들이 문제가 되어서 put에서 수행하게 했다.
그리고 PUT method 안에서 INSERT와 UPDATE 중 하나가 발생하기 때문에(두 메소드 모두 사용되기 때문에), 해당 작업 수행 후 MkRestApiResponse에 응답 코드를 담아서 반환해주었다.
프론트로부터 request.getParameter를 통해 데이터를 받을 때, 데이터가 없는 경우가 있다. 이 때는 Form Data나 Body Message에 데이터가 담겨온 것인데, 이럴 경우 Stream을 이용해 parameter를 받아와야 한다. 보통 프론트 사이드에서 HttpRequest에 Parameter를 전송할 경우 Servlet에서 다음 함수로 받을 수 있다.
파라미터명을 아는 경우 request.grtParamrter(String param); 파라미터명을 모르는 경우 request.getParameterNames()
위 방식은 쿼리 스트링, 혹은 Form Data의 경우 정상적으로 받아지지만 Content-Type이 변하여(application/json 등) Body Content에 parameter가 담겨올 경우, 위 방식으로 Parameter를 찾아보면 null 값이 나온다.
이럴 경우 body content를 읽어들여야 하는데, request.getInputStream()을 이용해 buffer를 읽어들이면 된다.