웹 사이트를 제작 할 때, float: left로 둔 속성들에 대해 빈공간이 생기는 걸 볼 수 있다.

HTML을 처음 시도한다면 아래와 같은 코드를 입력했을 때, 빈공간이 없어야 한다고 생각할 수 있다.

#p-wrapper{
	float: left;
	background-color: #f1cc82;
}

.p-tag{
	display: inline-block;
	background-color: #ccc;
}
<div id="p-wrapper">
  <p class="p-tag">너</p>
  <p class="p-tag">와</p>
  <p class="p-tag">나</p>
</div>

빈공간이 보인다

그럼에도 불구하고 위 사진처럼 너 와 나 사이에 공간이 생기는걸 볼 수 있는데, 이는 버그가 아니라 HTML 문서에서 줄바꿈이 인식되어 공간이 발생한 것이다. 즉 이를 해결하기 위해서는 빈공간에 대한 사이즈를 줄여야 하는데, 다음과 같은 방법이 있다.

1. <p> 태그를 작성하면서 엔터(줄바꿈)을 하지 않는다.
2. p-wrapper내부에 있는 공간을 인식하는 것 이기 때문에, p-wrapper내의 글자 크기를 줄인다.
3. <p>태그의 닫힘 문자를 건너뛴다. (</p>를 쓰지 않는다. 이는 문제가 될 수 있어서 비추천...)

1번은 다음과 같다.

<div id="p-wrapper">
<p class="p-tag">너</p><p class="p-tag">와</p><p class="p-tag">나</p>
</div>

그러나 이 역시 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

이후에 내려받은 폴더로 이용하여 certbot-auto를 실행시키면 된다.

certbot-auto certonly --manual --preferred-challenges=dns --email [이메일] --server https://acme-v02.api.letsencrypt.org/directory -d [SSL을 인증하고자 하는 도메인]

여러 도메인을 등록하고자 하는 경우 -d DOMAN1 -d DOMAN2 와 같이 -d를 이용해 추가해주면 된다.

이후에 certbot에서 시키는대로 하면 된다. 위 명령어가 동작하지 않는 사람들은 아래 명령을 이용하면 된다. 그러나 아래 방식의 경우 standalone이기 때문에, 위 명령어를 추천한다. 

cerbot-auto certonly --standalone -d [SSL을 인증하고자 하는 도메인]

 


사용 예시

$ ./certbot-auto certonly --manual --preferred-challenges=dns --email dev.whoan@gmail.com --server https://acme-v02.api.letsencrypt.org/directory -d *.dev-whoan.xyz

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에서 추가했고, 추가된 코드는 다음과 같다.

  <dependencies>
  	<dependency>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-context</artifactId>
  		<version>5.3.3</version>
  	</dependency>
  </dependencies>

이후에 src폴더에 xml만 모아놓은 폴더여도 좋고, DI를 이용하는 소스가 있는 폴더에 넣어도 좋다. 개인적으로는 DI xml을 넣는 폴더를 따로 만들어 관리하기를 추천한다.

이클립스에서 새로 추가하기 -> Other -> Spring -> Spring Bean Configuration File을 추가해 주고 이름을 적절히 설정하면 name.xml파일이 생성된다.

나는 myCar.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을 이용하였을 때의 결과를 보여주겠다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

	<bean id="myCar" class="spring.di.entity.Car"  >
		<constructor-arg name="name" value="SUV" />
		<constructor-arg name="wheels" value="4" />
	</bean> 
    
    <!-- myCar는 Hyundai가 된다. -->
	<bean id="console" class="spring.di.ui.Hyundai">
		<property name="car" ref="myCar" /> 
	</bean>

</beans>
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import spring.di.entity.Car;
import spring.di.ui.CarUI;

public class MyOwnCar {

	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("spring/di/myCar.xml");
		Car myCar = context.getBean(Car.class);

		CarUI console = (CarUI) context.getBean("console");
		console.print();
	}
}

실행 결과. Hyundai의 print가 실행됐다.

 

'웹서버 > Spring' 카테고리의 다른 글

[설치] Spring Framework 이클립스에 설치하기  (0) 2021.01.19

Spring Framework를 이클립스에 설치하는 방법을 알아보려고 한다.

웹서버 프레임워크를 만들면서 웹서버의 기초를 공부하고 있고, 실제 환경(프레임워크 등)에서는 어떻게 동작하는지, 어떤식으로 문제를 해결했는지 공부하기 위해 스프링 프레임워크에 대해 알아보려고 한다.

Spring Framework는 어떤 기업을 가든 안쓰는곳이 없을 정도로 널리 쓰이는 프레임워크이다.

스프링 프레임워크가 뭐고 어쩌고 하는거는 따로 다루지 않겠다.

다만 Spring Framework가 왜 잘 쓰이는지, 어떤 장점을 갖는지는 간단하게 적어보려고 한다.

MVC, Dependency Injection, Transaction (AOP) 등.

MVC: Model, View, Controller로 Model <-> Controller <-> View와 같은 디자인 패턴이다.

Dependency Injection: xml, annotation과 같은 데이터를 활용하여 의존성을 낮출 수 있다.

Transaction (AOP): 핵심코드와 부가기능 분리


Spring Framework 설치

Spring Framework는 http://spring.io 에서 다운로드를 하여 이클립스에서 사용하거나,

 

Spring makes Java simple.

Level up your Java code and explore what Spring can do for you.

spring.io

이클립스 마켓플레이스에서 Spring Tools Suite (STS)를 검색함으로써 설치할 수 있다.

마켓플레이스에서 STS를 검색하고, 빨간줄로 표시된 것과 같은것을 설치하면 된다.

성공적으로 설치가 되면, 이클립스에서 새로운 프로젝트를 생성할 때 Spring 항목이 있어야 한다.

따로 링크에서 설치를 하더라도, 똑같이 떠야한다.

'웹서버 > Spring' 카테고리의 다른 글

[Dependency] Spring Dependency / 스프링 의존성  (0) 2021.01.19

JSP 혹은 서블릿을 이용해 웹 서버를 개발할 때 파일 입출력 관련 프로그래밍을 해야하는 경우가 있다. 이 때, 출력 파일의 권한을 수정할 때, 수정되지 않는 경우가 있다.
이번 게시글에 작성할 문제의 경우, 파일을 생성할 때는 정상적으로 권한이 변경됐으나, 폴더를 생성할 때는 권한이 정상적으로 변경되지 않았다.
아무리 생각해봐도 톰캣의 권한 문제라면 생성되는 파일또한 권한이 정상적으로 변경되지 않아야 했으나, 정상적으로 출력되는 바람에 꽤 많이 헤매었다.


2021.11.23 추가

만약에, 아래 문단의 방법으로 안될 경우 아래 명령어를 통해 ReadWritePaths가 정상적으로 수정되었는지 확인해보자. 아직까지 왜 파일이 설정한대로 안바뀌는지 이유는 찾지 못했다.
이유를 찾으면 게시글을 업데이트 하겠당.

$ systemctl edit --full tomcat#.service

...
ReadWritePaths=대상폴더
...

이 명령어를 통해 어떤 폴더에 권한을 줄 경우, 해당 폴더는 미리 생성되어 있어야 한다. 뿐만 아니라, 아래 방식의 tomcat9.service에도 똑같은 내용을 적어놓는 것을 추천한다.


Tomcat 버전 9 이상의 경우, 톰캣이 실행할 웹앱 디렉토리의 소유자에 대하여 tomcat 계정, 혹은 tomcat이 추가된 그룹으로 바꾸고, 아래 루트로 이동하여 ReadWritePaths에 해당 웹앱의 ROOT 디렉토리를 추가한다.

/etc/systemd/system/multi-user.target.wants/tomcat#.service
User=root

또한 Tomcat의 쓰기 권한을 바꿔줘야 하는데, 다음처럼 하면 된다.

/usr/share/tomcat#/bin/catalina.sh

텍스트 편집기로 catalina.sh를 연 다음에, umask를 찾아서 0022로 변경해주면 된다.

UMASK="0022"

정의된 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를 설정한 만큼만 출력하게 되어 있었다. 따라서 실제 출력하고자 하는 내용이 모두 출력되지 않는 버그가 간간히 있었다.

따라서 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를 읽어들이면 된다.

StringBuilder stringBuilder = new StringBuilder(); // String Builder BufferedReader bufferedReader = null; try(InputStream inputStream = request.getInputStream()){ if(inputStream != null){ bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); char[] charBuffer = new char[256]; int bytesRead = -1; while ((bytesRead = bufferedReader.read(charBuffer)) > 0){ stringBuilder.append(charBuffer, 0, bytesRead); } } } catch (IOException e){ throw e; } finally { if(bufferedReader != null){ try{ bufferedReader.close(); }catch(IOException e){ throw e; } } }

이는 URL encoding 된 데이터를 가져오기 때문에 URL decoding 후 데이터를 사용하거나, 있는 그대로 사용해도 된다.


그러나 위 방식으로 parameter를 읽어올 경우, inputStream이 초기화되는 문제가 발생한다. (Tomcat은 inputStream을 읽는 순간 초기화된다.)

따라서 getInputStream으로 body는 모든 작업이 끝난 후 (적어도 내가 필요한 정보를 모두 확인했으나, body를 직접 읽어야 하는 경우)읽는게 좋다.

만약 request.getParameter()로 정상적인 parameter 접근이 가능하지만, request.getInputStream()으로 모두 읽어들인 후 request.getParameter()를 하면 null이 발생한다.


중간에 charBuffer 크기를 얼마나 할지 의문이 들 수 있는데, 넉넉하게 2^7 이상을 주거나, character형이 아닌 String형으로 한 line씩 읽어와도 된다.

... bufferedReader = new Bufferedreader(new InputStreamReader(inputStream)); String line; while((line = bufferedReader.readLine()) != null){ stringBuilder.append(line); } ...

+ Recent posts