톰캣 8.5 버전 이상부터 톰캣 컨텍스트 애플리케이션에서 파일 쓰기 작업을 할 때 정상적으로 안되는 경우가 있다.
톰캣 버전이 올라가면서 시스템 보안을 위한 정책이 추가된건데, 기본적으로 톰캣이 포함된 webapps와 logs 파일에 읽고 쓰기를 지원하고, 다른 경로는 설정을 통해 읽고 쓰기가 가능하게끔 해야한다.
이러한 방법에는 다음과 같은 두 가지 방법을 생각할 수 있다.
1. 톰캣유저 자체의 권한 상승
2. 톰캣 서비스 수정을 통한 IO 경로 추가
1번의 경우 보안상 이유로 패치된것 이기 때문에 추천하지 않고, 따라서 2번을 추천하다.
$ cd /etc/systemd/system/multi-user.target.wants
$ vi /tomcat9.service
# 여기부터 tomcat9.service File
...
#Security
#제일 하단에 다음과 같이 추가
ReadWritePaths=IO작업을 수행할 경로
ReadWritePaths 옵션을 보면 알겠지만, 톰캣이 파일 IO작업을 할 수 있도록 허용하는 경로를 추가하는 것이다.
모든 데이터 및 알고리즘을 List 구조만 이용해서 최적화된 프로그래밍을 할 수 있으면 얼마나좋을까?
안타깝게도 그렇지 않기 때문에 세상에는 List를 제외한 많은 자료구조가 존재한다.
이번에는 Stack과 관련된 구현을 할 예정이다
Stack은 Last In First Out 구조를 갖는다. 이 LIFO 구조를 갖는 가장 흔한 예를 들어보면 많이 원기둥으로 된 초콜릿 통을 얘기한다. 하지만 우리 프로그래밍 세계에는 초콜릿 통만 만들지 않으니, 다른 예를 알아볼 필요도 있다.
이런 예는 어떨까?
구조선에 총 500kg의 인원을 태울 수 있다. 구조를 기다리는 사람들은 차례로 줄 서서 탑승을 기다리고 있다. 구조선이 도착지에 도착하면, 탑승구에 있는 사람부터 내린다. 구조선은 총 탑승 중량이 500kg을 넘어가면, 출발한지 조금 지나 침몰한다. 탑승할 때, 중량을 나타내는 전자저울이 있다.
해당 문제는 도착지에 도착하건, 중량초과를 하건, 제일 마지막에 탑승한 사람부터 내려야 한다. 이러한 경우 Stack을 이용해 구현할 수 있겠다.
Stack은 Last In First Out
Stack에 대해 이미지를 연상해보면 다음과 같이 생각하면 편하다.
즉, 제일 위에서(top) 데이터를 삽입하거나, 데이터를 반환한다.
stack은 리스트처럼 1. 크기가 고정(배열) 2. 크기가 동적(ArrayList)의 성격을 가질 수 있고, 구현 방법은 그 성격과 같다.
크기를 고정시키고 싶다면 배열로 구현하고, 크기를 동적으로 변경하고 싶다면 ArrayList로 구현하면 된다. 오늘 게시글에서는 크기가 고정된 Stack을 구현해 보겠다.
Stack에는 어떤게 필요할까?
Stack은 어떤 기능이 필요할까? 기본적인 목적은 제일 위에서 데이터를 삽입, 반환할 수 있어야 한다. 부가적으로, 현재 Stack의 사이즈와 크기, 초기화, 그리고 비어있거나 꽉찼는지를 구할 수 있으면 된다.
따라서 제일 위를 구하는 top, 크기를 담는 capacity를 가지면 된다.
Stack에서는 ArrayList와 달리 add, remove라는 함수 이름을 사용하지 않고 각각 push, pop이라는 이름의 함수를 사용한다.
Stack 구현
따라서 Stack의 기본은 다음과 같다.
public class Stack<T> {
int top;
int capacity = -1;
T[] stack;
Stack(int capacity){
this.capacity = capacity;
stack = (T[]) (new Object[capacity]);
top = -1;
}
}
top을 -1로 초기화 하는 이유는 개발의 간편함을 위함이다. 따라서 스택이 비어있을때 top은 -1이 된다.
고정된 크기를 갖는 Stack을 구현하기 생성자는 capacity값을 전달해야 한다.
데이터를 삽입할 때는 Stack이 가득찼는지 확인해야 한다. 그리고 공간이 남아있다면 데이터를 삽입하면 된다.
public class Stack<T> {
...
public void push(T element) {
if(isFull()) {
System.out.println("Stack이 가득 찼습니다.");
return;
}
stack[++top] = element;
}
public boolean isFull() { return (this.top == this.capacity-1); }
...
}
isFull(): 스택이 가득찼는지 확인한다.
데이터를 반환할 때는 삽입할 때와 반대로 Stack이 비어있는지 확인해야 한다. 그리고 비어있지 않다면 데이터를 반환하면 된다.
public class Stack<T> {
...
public T pop() {
if(isEmpty()) {
System.out.println("Stack이 비어있습니다.");
return null;
}
return stack[top--];
}
public boolean isEmpty() { return (this.top == -1); }
...
}
isEmpty(): 스택이 비어있는지 확인한다.
그런데 우리는 반환하지 않고 데이터를 확인만 하고 싶을수도 있다. 이럴 때를 위해 peek()라는 함수가 존재한다. 이는 stack에서 데이터를 삭제하지 않고, top에 어떤 data가 존재하는지 확인을 가능하게 한다.
public class Stack<T> {
...
public T peek() {
if(isEmpty()) {
System.out.println("Stack이 비어있습니다.");
return null;
}
return stack[top];
}
public boolean isEmpty() { return (this.top == -1); }
...
}
pop과의 차이점은 return (T) stack[top--]; 과 return (T) stack[top]; 이다.
pop의 경우 top을 하나 감소시켜 다음 데이터가 삽입될때, 반환한 데이터 위치에 새로운 데이터가 덮어씌워지지만, peek의 경우 top의 데이터 조작을 일체 하지 않음으로써 다음 pop 혹은 push를 수행할 때 기존과 같은 역활을 하게 하는 것이다.
이제 나머지 부가기능인 stack의 크기와 초기화 하는 함수를 만들어주면 된다.
public class Stack<T> {
...
public void clear(){
if(isEmpty()){
System.out.println("Stack은 이미 비어있습니다.");
return;
}
top = -1;
stack = (T[]) (new Object[capacity]);
System.out.println("Stack 초기화 완료!");
}
public int size(){
return (top+1);
}
}
전체코드와 실행 결과
public class mainStack {
public static void main(String[] args) {
System.out.println("=====짧은머리 개발자=====");
Stack<Integer> stack = new Stack<>(5);
for(int i = 0; i < 5; i++) {
stack.push((i+1));
System.out.println(i + " 번째 peek: " + stack.peek());
}
System.out.println("===Pop===");
for(int i = stack.size(); i > 0; i--) {
System.out.print(i + " 번째 : " + stack.pop() + " | " );
}
}
}
public class Stack<T> {
int top;
int capacity = -1;
T[] stack;
public Stack(int capacity){
this.capacity = capacity;
stack = (T[]) (new Object[capacity]);
System.out.println("size : " + capacity);
top = -1;
}
public void push(T element) {
if(isFull()) {
System.out.println("Stack이 가득 찼습니다.");
return;
}
stack[++top] = element;
}
public T pop() {
if(isEmpty()) {
System.out.println("Stack이 비어있습니다.");
return null;
}
return stack[top--];
}
public T peek() {
if(isEmpty()) {
System.out.println("Stack이 비어있습니다.");
return null;
}
return stack[top];
}
public void clear(){
if(isEmpty()){
System.out.println("Stack은 이미 비어있습니다.");
return;
}
top = 0;
stack = (T[]) (new Object[capacity]);
System.out.println("Stack 초기화 완료!");
}
public int size(){
return (top+1);
}
public boolean isEmpty() { return (this.top == -1); }
public boolean isFull() { return (this.top == this.capacity-1); }
}
배열(Array)와의 차이점으로 배열은 초기화할 때 크기를 지정해야 하지만, ArrayList는 그러지 않아도 된다.
즉 배열은 고정값의 크기를 가지게 되고, 이후에 크기를 늘리는 행위를 하려면 새로운 배열을 생성하여 내용을 복사해야 하는 반면에, ArrayList는 그냥 추가해 주면 된다.
그렇다면 동적 크기를 갖는 ArrayList를 구현하려면 어떻게 해야할까?
다른 블로그 포스트들을 보면 똑같은 코드를 바탕으로 똑같은 설명을 반복하고 있다. 해당 내용을 보면 심화된 ArrayList를 구현할 수 있기 때문에 나는 기초 구현을 하겠다.
들어가기에 앞서서, 제네릭이라는 기술이 있다. 제네릭은 쉽게 말하면 하나의 구조에 대해 여러 자료형을 사용할 수 있도록 하는 것이다. 우리가 String name이라는 변수를 통해 홍길동, 김철수, 김영희 등을 지정하는 것 처럼 name을 String뿐만 아니라 Integer, Float, Boolean, 등 여러개의 자료형을 사용할 수 있도록 해주는 것이다.
이러한 제네릭을 사용하기 위해서는 제네릭 클래스를 만들어야하는데, 간혹 어떤 클래스를 볼 때 다음과 같은 구조를 본 적 있을 것이다.
class Point<T>{ ... }
여기서 <T>부분이 제네릭 부분이며, 내가 자료형을 T로 표현한 부분은 모두 해당 클래스를 사용할 때 지정한 자료형임을 나타내는것이다. 따라서 간략히 Point 내부에는 int x, y 혹은 Float x, y 대신 T x, y를 사용함으로써 Integer를 갖는 Point와 Float을 갖는 Point를 따로 만들지 않고 하나의 Point<T>를 통해 두가지를 모두 구현할 수 있게 되는 것이다.
Array List
이름을 보면 정답이 나온다.
Array List, 즉 Array를 이용하여 List를 만드는 것이다. Array는 고정 크기를 갖는데 어떻게 동적 크기를 만든다는거지?
본 게시글의 시작 부분에서 " ... 배열은 고정값의 크기를 가지게 되고, 이후에 크기를 늘리는 행위를 하려면 새로운 배열을 생성하여 내용을 복사 ... "
즉, ArrayList는 내부에 어떤 고정 크기를 갖는 배열을 가지고 있고, 어떤 요소를 추가할 때 그 배열의 크기를 넘어가게되면 배열의 크기를 늘려주면 된다.
ArrayList를 만들기에 앞서 목적이 무엇인지 생각해보자.
1. 어떠한 Data를 보관한다.
2. 보관된 Data를 반환한다.
1. Data를 보관할 때 조금 옵션을 추가해 보자면 제일 앞에, 제일 뒤에, 특정한 위치에 이렇게 3가지가 있을 수 있다. (제일 뒤는 가장 마지막으로 추가한 Data의 뒤를 의미)
2. Data를 반환할 때 마찬가지로 옵션을 추가해 보면 제일 앞에, 제일 뒤에, 특정한 위치에 이렇게 3가지가 있다.
위 목적을 바탕으로 ArrayList가 갖는 요소를 생각해 보면 Array, index(혹은 iterator)면 충분하다.
이제 ArrayList를 만들기 위한 준비는 끝났다.
Class ArrayList
class ArrayList{
Object[] arr = null;
int capacity = 0;
int size = 0;
ArrayList(int capacity){
this.capacity = capacity;
arr = new Object[capacity];
size = 0;
}
ArrayList(){
capacity = 1;
arr = new Object[capacity];
size = 0;
}
}
시작은 위와 같다. ArrayList class에 대하여 array, arr의 크기를 알려주는 capacity, 그리고 현재 arr의 size를 반환해주는 size,를 만들었다. (size는 arr이 지금까지 사용하고 있는 크기, capacity는 arr의 length를 알려주는 변수이다.)
그리고 ArrayList를 생성함과 동시에 arr을 전달받은 size로 크기를 초기화하거나, 그렇지 않은 경우 배열의 크기를 1로 설정했다.(1 이상으로 해도 된다. 다만 0으로 할 경우, ArrayList 생성 후에 삽입을 한다면 에러가 발생하니 주의하자)
Data 보관
이제 데이터 보관을 구현할 차례다. 이 때 주의해야 할 점은 다음과 같다.
1. arr이 꽉찼다면, arr의 크기를 바꾸고 내용을 복사한다.
2. 삽입하는 위치가 특정 위치일 경우, 해당 위치부터 요소들을 뒤로 한칸씩 민다.
1번의 경우, 그림으로 표현하면 다음과 같다.
arr이 가득 찬 상태에서 새로운 요소를 추가한다면, 새로운 배열을 만들고, 해당 배열의 크기를 기존의 2배로 만들면 된다. 이후에 새로운 배열에 기존 배열을 복사 하고, 새로운 요소를 추가해주면 된다.
2번의 경우, 그림으로 표현하면 다음과 같다.
그림에서 보면 새로운 요소 5를 두번째에 추가하려고 한다.
하지만 이미 두번째부터 2, 3, 4라는 요소가 삽입되어 있다. 이럴 경우, 둘, 셋, 네 번째 위치의 요소들을 뒤로 한칸씩 밀어주고, 내가 원하는 위치에 데이터를 삽입하면 된다. 이를 응용하여, 제일 앞에 요소를 추가한다면, 모든 요소를 뒤로 한칸씩 밀어주면 된다.
이를 코드로 표현하면 다음과 같다.
class ArrayList{
...
public void add(Object element){
if(size == capacity){
expandArray();
}
arr[size++] = element;
}
public void addFirst(Object element){
add(0, element);
}
public void add(int index, Object element){
if(size == capacity){
expandArray();
}
for(int i = size; i >= index; i--)
arr[i] = arr[i-1];
arr[index] = element;
size++;
}
private void expandArray(){
capacity *= 2;
Object[] tempArr = new Object[capacity];
copyArr(tempArr, arr);
arr = new Object[capacity];
copyArr(arr, tempArr);
}
private void copyArr(Object[] arr1, Object[] arr2){
for(int i = 0; i < arr2.length; i++){
arr1[i] = arr2[i];
}
}
...
}
요소를 추가할 때 마다 size는 증가하게되고 따라서 arr이 가득 차게 되면 size는 capacity와 값이 같아지게된다. 이를 조건으로 사용하여 arr이 가득 찼다면 expandArray함수를 호출함으로써 arr을 크기를 확장힌다.
기본적으로 add 함수는 배열의 가장 뒤에 요소를 추가하며, 추가하고자 하는 위치가 있을 경우 해당 index로 데이터를 삽입한다.
Data 반환
데이터를 반환하는 코드는 비교적 단순하다.
원하는 위치에 대해 Object를 반환해주면 끝이다.
class ArrayList{
...
public Object get(int index){
if(size <= 0){
System.out.println("배열이 비어있습니다.");
return null;
}
return arr[index];
}
...
}
이 때 주의해야 할 점은 내가 구하고자 하는 위치가 배열의 크기를 넘어설 수 있다. 이를 따로 조건문으로 처리해주어도 되지만, 그냥 반환함으로써 Out Of Bounds 예외를 출력해주어도 되기 때문에 따로 작업하지 않았다.
추가적인 동작들
추가적으로 배열 초기화, 삭제, Size 반환, Capacity 반환 등이 있을 수 있다.
삭제의 경우, 삭제하고자 하는 위치가 배열의 크기를 넘어가는지, 이미 비어있는지 등을 확인하고 삭제, 반환해주면 된다. 나는 따로 조건을 추가하지 않았다.
초기화는 현재의 capacity로 arr를 초기화 시켜주면 된다.
class ArrayList{
...
public Object remove(int index){
/*
크기 초과, 이미 비어있는지 등 조건문은 원하는대로 추가해주면 된다.
*/
Object result = arr[index];
arr[index] = null;
return result;
}
public void reset(){
arr = new Object[capacity];
}
public int size(){
return this.size;
}
public int getCapacity() {
return this.capacity;
}
}
전체 코드와 실행 결과
package DataStructure;
public class ArrayList<T> {
Object[] arr = null;
int capacity = 0;
int size = 0;
public ArrayList(int capacity){
this.capacity = capacity;
arr = new Object[capacity];
size = 0;
}
public ArrayList(){
capacity = 1;
arr = new Object[capacity];
size = 0;
}
public void add(T element){
if(size == capacity){
expandArray();
}
arr[size++] = element;
}
public void addFirst(T element){
add(0, element);
}
public void add(int index, T element){
if(size == capacity){
expandArray();
}
for(int i = size; i >= index; i--)
arr[i] = arr[i-1];
arr[index] = element;
size++;
}
private void expandArray(){
capacity *= 2;
Object[] tempArr = new Object[capacity];
copyArr(tempArr, arr);
arr = new Object[capacity];
copyArr(arr, tempArr);
}
private void copyArr(Object[] arr1, Object[] arr2){
for(int i = 0; i < arr2.length; i++){
arr1[i] = arr2[i];
}
}
public T get(int index){
if(size <= 0){
System.out.println("배열이 비어있습니다.");
return null;
}
return (T) arr[index];
}
public T remove(int index){
/*
크기 초과, 이미 비어있는지 등 조건문은 원하는대로 추가해주면 된다.
*/
T result = (T) arr[index];
arr[index] = null;
size--;
if(size > 0) {
/*
삭제한 이후부터 앞으로 한칸씩 땡긴다.
*/
for(int i = index; i < size; i++) {
arr[i] = arr[i+1];
}
}
return result;
}
public void reset(){
arr = new Object[capacity];
size = 0;
}
public int size() {
return this.size;
}
public int getCapacity() {
return this.capacity;
}
}
package Main;
import DataStructure.ArrayList;
public class mainTask {
public static void main(String[] args) {
System.out.println("=====짧은머리 개발자=====");
ArrayList<Integer> arr = new ArrayList<Integer>();
System.out.println("배열 크기 : " + arr.getCapacity());
System.out.println("데이터 삽입 1~5");
for(int i = 0; i < 5; i++) {
arr.add((i+1));
}
//출력
System.out.println("\n==출력==");
for(int i = 0; i < arr.size(); i++) {
System.out.print(i + "번째 : " + arr.get(i) + " | ");
}
System.out.println("\n배열 크기 : " + arr.getCapacity());
arr.add(1, 6);
arr.add(4, 7);
System.out.println("\n==출력2==");
for(int i = 0; i < arr.size(); i++) {
System.out.print(i + "번째 : " + arr.get(i) + " | ");
}
System.out.println("\n배열 크기 : " + arr.getCapacity());
System.out.println("1번째 요소 삭제 : " + arr.remove(1));
System.out.println("\n==출력3==");
for(int i = 0; i < arr.size(); i++) {
System.out.print(i + "번째 : " + arr.get(i) + " | ");
}
System.out.println("\n배열 크기 : " + arr.getCapacity());
System.out.println("\n==출력4==");
while(arr.size() > 0) {
System.out.println("0번째 삭제: " + arr.remove(0));
}
System.out.println("\n배열 크기 : " + arr.getCapacity());
System.out.println("데이터 삽입 1~5");
for(int i = 0; i < 5; i++) {
arr.add((i*2));
}
//출력
System.out.println("\n==출력6==");
for(int i = 0; i < arr.size(); i++) {
System.out.print(i + "번째 : " + arr.get(i) + " | ");
}
System.out.println("\n배열 크기 : " + arr.getCapacity());
System.out.println("초기화");
arr.reset();
System.out.println("\n==출력7==");
for(int i = 0; i < arr.size(); i++) {
System.out.print(i + "번째 : " + arr.get(i) + " | ");
}
System.out.println("\n배열 크기 : " + arr.getCapacity());
}
}
이 때, 위에 포트1과 포트2로 나뉘어져 있는걸 볼 수 있는데, 다음과 같은 규칙을 가지면 좋다.
포트1=800X | 포트2=801X
이후 tomcat01이 정상적으로 동작하는지 보면 된다.
Apache2 설정
Tomcat01이 정상적으로 동작한다면 Apache2의 설정을 진행하면 된다.
$ vi /etc/apache2/envvars
# /etc/apache2/envvars
export APACHE_RUN_USER=tomcat9 #혹은 자신이 설치한 tomcat (복제본 아님)
export APACHE_RUN_GROUP=tomcat9 #혹은 자신이 설치한 tomcat (복제본 아님)
$ vi /etc/apache2/conf-enabled/other-vhosts-acces-log.conf
# /etc/apache2/conf-enabled/other-vhosts-acces-log.conf
# CustomLog ~ # 주석 처리
$ cd /etc/apache2/sites-available
$ vi 본인이 JSP로 연결할 사이트.conf
# vi 본인이 JSP로 연결할 사이트.conf
<VirtualHost *:80>
...
JkMountCopy On
JkMount /* 워커이름
...
</VirtualHost>
$ apt-get install libapache2-mod-jk
$ vi /etc/apache2/mods-available/jk.conf
# /etc/apache2/mods-available/jk.conf
JkLogLevel Error
# 아래 내용 주석 처리
# <Location /jk-status>
# # Inside Location we can omit the URL in JkMount
# JkMount jk-status
# Order deny,allow
# Deny from all
# Allow from 127.0.0.1
# </Location>
# <Location /jk-manager>
# Inside Location we can omit the URL in JkMount
# JkMount jk-manager
# Order deny,allow
# Deny from all
# Allow from 127.0.0.1
# </Location>
$ vi /etc/libapache2-mod-jk/workers.properties
worker.list=워커이름
worker.톰캣복사본이름=port=포트2
worker.톰캣복사본이름.host=도메인주소 #특별하지 않으면 localhost를 사용하자
worker.톰캣복사본이름.type=ajp13 # HTTP로 설정할 경우 오류가 발생한다.
worker.톰캣복사본이름.lbfactor=1
worker.워커이름.type=lb
worker.워커이름.balance_workers=톰캣복사본이름
톰캣2에서 환경변수 설정을 통해 아파치 실행 유저를 톰캣 유저로 설정하는건 안해도 되지만, 나는 JSP서버만 이용할것이기 때문에 tomcat9유저로 통일했다.
JSP 서버를 실행하기 위해서는 mod-jk를 사용해야하는데, mod-jk는 아파치 프로토콜을 사용하여 Tomcat 서블릿 컨테이너를 연결하는 모듈이다.
따라서 내가 연결할 사이트에 JkMount를 설정해줘야한다.
mod-jk를 설정하고, 워커 (loadbalancer)를 설정해줘야 하는데, 이는 내 서버의 아이피 주소로 들어오는 연결을 톰캣으로 보내기 위함이다.
따라서 워커가 갖는 host과 port는 tomcat01(톰캣복사본) server.xml에서 설정한 포트와 hostname이 된다.
type의 경우도 server.xml에서 설정한 protocol이 되는데, 아파치와 톰캣은 AJP를 이용해 통신하므로 ajp13으로 해준다. (이 때문에 tomcat복사본의 server.xml의 protocol도 AJP 1.3이다.)
윈도우에서 흔히 사용하는 환경변수를 리눅스에서 설정해줄 수 있지만, 안해도 웹서버 유지에 큰 문제는 되지 않는다.
$ vi /etc/profile
# /etc/profile
JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 # 혹은 설치한 자바 경로
export JAVA_HOME
PATH=$PATH:$JAVA_HOME/bin
CLASSPATH=$CLASSPATH:$JAVA_HOME/lib
이후 톰캣 설정해야하는데, 웹앱 디렉토리 설정은 본 게시글의 하단에서 설명하겠다.
우선 톰캣의 사용 유저와 그룹을 설정하여 해당 유저와 그룹을 통해 프로세스를 관리할 수 있도록 하고, 메모리를 설정해 주자.
$ vi /etc/default/tomcat9
# /etc/default/tomcat9
TOMCAT8_USER=root
TOMCAT8_GROUP=tomcat9
JAVA_OPTS="-Djava.awt.headless=true -Xmx1024m -Djava.security.egd=file:/dev/./urandom"
JAVA_OPTS를 설명하자면,
Djava.awt.headless=true
- 리눅스 환경에서 GUI 클래스를 사용할 수 있게 한다.
Xmx1024m
- 최대 JVM이 1024Mb를 가질 수 있다. (Xms는 최소)
Djava.security.egd
- 자바 응용 프로그램은 시작시 난수를 이용해 임의의 값(세션 아이디 같은)을 생성하는데, 리눅스 시스템에서는 난수 생성을 위해서 /dev/urandom을 사용한다. 따라서 해당 경로를 설정해주는 것이다.
/etc/tomcat9/server.xml
이후 웹앱에 대한 설정을 해야한다. server.xml파일을 통해 내가 등록할 웹 앱들을 관리할 수 있다. 많은 옵션들이 존재하지만, 별도로 설정하지 않는다면 다음 설정을 따르길 추천한다.
웹 서버 공부를 시작할 때 아파치를 빼놓을 수 없고, 흔히 아파치는 PHP, JSP등 웹 애플리케이션을 동작하기 위한 기초적인 웹 서버이다.
Nodejs와도 연동할 수 있는 만큼 웹 개발자로 나아가는 사람이라면 설치는 기본적으로 할 줄 알아야 한다.
아파치를 설치하는 방법은 쉽다.
$ apt-get install apache2
apt를 이용해 install 명령을 날려주면 설치 된다.
문제는 설치 이후에 어떻게 해야 내 컴퓨터로 사람들이 접속할 수 있는지, 그리고 내 도메인을 연결할 수 있는지다.
아파치에서는 해당 아이피에 대해 어떤 webapps 디렉토리로 연결될 지 설정해 주어야 한다. 즉 도메인에 대해 웹 디렉토리를 할당하여, 외부 혹은 내부 사용자가 도메인에 접속을 요청 했을 때, 해당 디렉토리의 webapp 내용을 보여줄 수 있어야 한다.
웹 앱 디렉토리 설정을 들어가기 전에, 내가 사용하고있는 아파치 설정은 다음과 같다.
# /etc/apache2/apache2.conf Timeout 60 KeepAlive On MaxKeepAliveRequests 100
# Timeout : 몇초가 지나면 Timeout을 보낼것인가 ( 초 단위 ) # KeepAlive : Connection에 대해 동일한 연결을 재사용 할 것인지 설정한다. KeepAlive가 꺼져있으면 동일한 요청에 대해 여러 접속이 발생하고, 켜져있으면 한 요청으로 처리하게된다. 하지만 메모리 사용량이 증가하므로(Connection을 갖고 있어야 하기 때문에) 사용 환경에 따라 On Off 하면 될 것 같다. # MaxKeepAliveRequests : 위 KeepAlive를 몇개까지 허용할 것인지 #KeepAliveTimeout : 얼마만큼의 시간동안 위 KeepAlive를 갖고 있을 것인지 ( 초 단위 )
웹 디렉토리를 설정할 때 내가 사용하는 방법은 다음과 같다. 1. /etc/apache2/apach2.conf 파일 수정 2. /etc/apache2/sites-available 내에 파일 추가 추천하는 방법은 2번이다. 1번에서는 아파치의 기본 설정을 하고, 웹 서버 추가는 2번을 통해 sites를 추가하고, 아파치에 등록함으로써 webapp을 동작시키는 것이다.
/etc/apache2/apache2.conf 파일 수정
위 파일을 열고, "Directory /var/www"를 검색하면 다음과 같은 코드가 있다.
<Directory /var/www/> Options FollowSymLinks AllowOverride All Require all granted </Directory>
아파치를 처음 깔았을 때 보이는 페이지를 해당 코드를 통해 웹서버 등록을 해 주고 있는 것이다. 즉 /var/www로 디렉토리를 변경하면 아파치 설치후 localhost에 접속했을 때 나오는 화면을 볼 수 있다. 우리가 웹 서버를 등록하기 위해서는 위 설정을 따라주면 된다. Directory 태그를 통해 내가 등록하고자 하는 웹앱을 설정해주면 된다.
<Directory 웹앱경로> DirectoryIndex index.html # 기본 페이지 Options FollowSymLinks # 디렉토리 목록에 대한 설정 AllowOverride All # 접근방식 Require all granted # 접근 권한 </Directory>
웹앱경로: 내가 설정하고자 하는 웹앱의 절대경로이다. DirectoryIndex: 기본 페이지를 설정한다. - index.html, index.php, index.jsp 등 Options: 접근제어를 설정할 수 있다. 사용자에게 디렉토리 목록을 보여줄지 말지, SSI를 설정할지 말지 등을 설정한다. - None, ALL, Indexes, Includes, IncludeNOEXEC, FollowSymLinks, ExecCGI, MultiVIews. AllowOverride: 접근에 대한 인증을 어떻게 허용할 것인지 설정한다. 즉 연결에 대한 인증을 계속 사용할지, 새로운 연결은 새로운 인증을 할지 등을 설정한다. - All, AuthConfig, FileInfo, Indexes, Limit, Options 등 Require: 접근 권한을 설정할 수 있다. 모든 접근 허용/거부, 특정 아이피 허용/거부 등을 설정한다. - all granted, all denied, ip ipv4, not ip ipv4, host hostname, not host hostname
/etc/apache2/sites-available 사이트 추가
1번과는 다르게 2번은 가상호스트를 사용하는 방법이다. 따라서 설정 양식이 다른데, 다음과 같다.
<VirtualHost *:80> ServerName 도메인 주소 DocumentRoot 웹앱 경로 ErrorLog 에러로그 경로 </VirtualHost>
우슨 태그가 VirtualHost이고, 포트도 설정해줘야 한다. <VirtualHost *:80> : 컴퓨터가 갖고있는 아이피(*)의 80포트 접속을 웹앱으로 설정한다. ServerName : 아이피(*)중 어떤 도메인 주소 연결을 해당 웹앱으로 받을지 설정한다. 외부 아이피를 줘도 되고, (*)에 적은 아이피를 똑같이 적어줘도 된다. DocumentRoot : 웹앱 경로 ErrorLog : 에러로그 경로 나는 톰캣 서버를 사용하기 때문에 추가적인 다음 옵션을 사용한다 JkMount /* loadballancer JkMountCopy On SSL설정도 사용하고 있어서 http 접속을 https로 Redirect 시켜준다. Redirect permanent / https://~
이로써 아파치의 기본 설정은 모두 끝났다.
내가 설정한 아이피 주소(도메인)으로 내가 등록해놓은 웹앱이 돌아가는것을 확인할 수 있다. SSL의 경우 추가적인 설정이 필요하기 때문에 나중에 게시할 예정이다. JSP사용자의 경우 마찬가지로 JkMount, loadbalancer 설정 등이 필요하기 때문에 이 글을 통해 html과 같이 기본적인 페이지만 확인할 수 있다. (따로 플러그인을 사용한다면 그에 맞는 설정만 추가적으로 하면 된다는 말)
한가지 확인해야 할 것은 도메인을 사용중이라면, 도메인 제공자 사이트에서(가비아 등) 해당 도메인에 대해 내 웹서버의 아이피를 설정해줘야 한다.
리눅스 커맨드를 통해 어떻게 하면 되는지 정리하겠다.
$ apt-get install apache2 $ vi /etc/apache2/apache2.conf #여기부터 apache2.conf 파일 KeepAlive On MaxKeepAliveRequests 100 KeepAliveTimeout 5 LogLevel error <Location /WEB-INF> SetHandler WEB-INF Order deny,allow Deny from all </Location> #커맨드 $ vi /etc/apache2/conf-enabled/charset.conf #charset.conf AddDefaultCharset UTF-8 # 아파치의 기본 언어셋을 UTF-8로 설정 #커맨드 $ vi /etc/apache2/sites-available $ vi mywebapp.conf #mywebapp.conf <VirtualHost *:80> ServerName localhost DocumentRoot /myweb/webapps/ROOT ErrorLog /myweb/log/error.log </VirtualHost> #커맨드 $ a2ensite mywebapp $ systemctl restart apache2