일반적인 프로그래밍에서 하지 말아야할 것

먼저 일반적인 프로그래밍에서 하지 말아야할 것에 대해 말하기 전에 소개하고 싶은 것이 있다.

이 글은 이펙티브 자바 3판을 읽으면서 처음쓰는 글이다.

그런데 포스트의 내용은 첫 장에 나오는 내용이 아니라 중간에 나오는 내용이다.

왜 이렇게 글을 쓰냐면 이 책은 스터디 방법이 좀 달라야한다고 생각하기 때문이다.

이 책을 한 번 읽고 완전히 이해하고 적용하는 사람은 없다고 생각한다.

그냥 쭉 읽고 이해한 부분을 정리하고 적용할 수 있는 건 해보면 그걸로 충분한 것 같다.

이해하지 못한 부분은 또 읽으면 되니까. 좀 더 레벨업하고 와서 보면 또 다르게 이해가 되는 것도 있겠지 하는 마음의 스터디 방법이다. (추천합니다.)

각설하고 간단하게 정리하며 한 번 더 공부해본다.

1. 지역 변수 범위를 최소화 하기

- 지역 변수 범위를 최소화하면 유지보수성이 높아지고 오류를 낼 가능성은 낮아진다.

지역 변수 범위를 최소화하기 위한 기법 3 가지

1) 가장 처음 쓰일 때 선언하기

  이제는 C언어에서도 코드 상단이 아닌 사용해야할 위치에서 변수를 선언한다. 가독성도 좋아진다.

2) 선언과 동시에 초기화하기 

  초기화에 필요한 정보다 부족하면 선언은 미뤄야 한다.

  try-catch는 try안에서 선언하는 것이 예외를 멀리 퍼지지 않게해서 좋다. 부득이하게 try-catch문 외부에서도 지역 변수를 사용해야한다면 try문 앞에서 선언해야한다.

3) 메서드를 최대한 작게 유지하고 그 메서드는 1개의 기능에만 집중하기

  만약 메서드에서 여러 기능을 수행하는데 지역 변수가 살아있다면 괜히 다른 기능에서 접근이 가능해져서 문제를 일으킬 수도 있다.

 

2. for문 보다는 for-each 문을 사용하기

- 배열이냐 컬렉이냐에 따라 문법 형태가 바뀌지 않고 변수가 조잡하지 않아 실수할 일이 적다.

for(int i=0;i<a.length;i++), Iterator, ... 뭐 이런게 안나와서 가독성도 좋고 성능도 떨어지지 않는다.

* for-each문을 사용할 수 없는 경우 (이럴 때만 기존 for문을 사용하자.)

1) 컬렉션을 순회하면서 선택된 원소를 제거하는 경우

  이럴 때는 반복자의 remove() 메서드를 사용해야한다. 물론 컬렉션은 removeIf()를 사용하면 for-each도 사용할 수 있다.

2) 배열을 순회하면서 원소의 값을 변경하는 경우

  일부나 전체 값을 변경하는 경우에는 배열의 인덱스를 사용해야한다.

3) 병렬 순회를 사용해야하는 경우

컬렉션을 스트림으로 병렬 순회하는 경우 인덱스를 사용하여 엄격하게 처리해야하므로 일반 for문을 사용해야한다.

참고로 for-each문을 사용하려면 Iterable<E> 인터페이스를 구현해야한다.

 

3. 라이브러리 활용하기

- 아주 기본적인 이야기지만 자기보다 훨씬 고수인 개발자가 심혈을 기울여서 만들어 놓은 표준라이브러리를 적극 사용하자.

구글의 구아바(guava), java.lang, java.util 의 하위패키지는 꼭 한 번씩 사용해보도록 하자. 아주 기본기가 충실한 개발자가 될 수 있을 것이다.

 

4. 정확한 값이 필요할 때는 Float, Double을 쓰지말자

- 금융과 관련된 계산처럼 정밀한 값을 표현할 때는 Float, Double을 쓰면 안된다.

Float, Double은 0.1, 0.01등을 표현할 수 없기 때문이다.

대신 BigDecimal, int, long을 사용하도록 하자.

 

5. 박싱된 기본 타입(Integer, Double, Boolean)보다 기본 타입(int, double, boolean)을 사용하자.

- 엄연히 박싱된 기본 타입은 클래스고 내부적으로 식별성 속성을 갖는다.

기본 타입은 언제나 유효하지만 박싱된 기본 타입은 객체기 때문에 null을 가질 수 있다.

박싱된 타입은 '==' 연산을 할 때 문제를 일으킬 수 있다. 고유 식별성 속성이 같은지를 검사해버리기 때문이다.

naturalOrder.compare(new Integer(42), new Integer(42)); 의 값은 1을 출력한다. (같으면 0)

연산 같은 곳에 잘못하면 오토 언박싱/박싱이 일어나면서 오버헤드가 엄청나게 발생할 수도 있다.

 

6. 다른 타입이 더 적절하다면 문자열 타입을 피하자

- 문자열이 여러 메서드도 제공해주고 사용하기 편리하다고 느껴 자주 사용되지만 더 적합한 타입이 있으면 boolean, enum, float등을 쓰는게 더 좋다.

 

7. 문자열 연결은 느리니 주의해서 사용하라

- 문자열은 불변클래스라 두 문자열을 연결할 때 복사본을 만들어서 연결하고 새 문자열을 만든다.

성능이 떨어질 수 밖에 없다. 다른 포스트에서 언급했듯이 stringbuilder, stringbuffer를 사용하도록 한다.

 

8. 객체는 인터페이스를 사용해 참조하라

- 예를들면 Map<String, Object> map = new HashMap<>(); 이런식으로 앞에 선언에서 인터페이스를 사용해 참조하라는 얘기다.

이렇게 하면 HashMap서 다른 더 효율적인 Map을 사용하려고 할 때 변경에서 자유롭다.

 

9. 리플렉션보다는 인터페이스를 사용하라

- 리플렉션은 컴파일타임에 타입검사를 할 수 없기때문에 없는 메서드를 호출하려한다든지 하면 에러가 발생할 수 있는 단점이 있다.

또한 리플렉션을 이용하면 코드가 지저분해지고 성능이 떨어진다.

상식적으로 생각해도 그냥 메서드를 호출하는 것보다 클래스를 찾아서 메서드 리스트를 받아서 찾아서 호출하는 식이 느리다.

리플렉션은 딱 인스턴스 생성에만 쓰고, 그렇게 생성된 인스턴스는 인터페이스나 상위클래스로 참조해 사용하면 좋다.

막상 예제를 테스트해보니 잘 안된다.

느낌만 이해하고 그것을 설명하면, 리플렉션으로 런타임중에 사용되는 어떤 클래스를 Class.forName("java.lang.String"); 처럼 클래스 이름으로 찾고, Class 객체의 getDeclaredConstructor()로 생성자를 찾아 .newInstance()로 인스턴스까지 생성을 한 후, 그 인스턴스를 가리킬 수있는 super 클래스 혹은 인터페이스로 참조하고 해당 기능을 직접사용하는 것이다.

 

10. 네이티브 메서드는 신중히 사용하라(아니 사용하지 말라)

- 네이티브 메서드는 잘 사용하기 아주 어렵다. 사용하지 말자.

GC가 자동으로 메모리 수거도 못하고 자바코드랑 네이티브메서드랑 접착 코드를 작성해야하는데 엄청 어렵고 자바의 장점인 플랫폼을 타지 않는다는 것이 사라지고 이식성이 떨어지는 단점들이 존재하니 일반적인 개발자는 사용하지 말자.

 

11. 최적화는 신중히 하라

- 최적화, 성능, 효율성을 강조하기 위해 설계를 바꾸지 말자

병목현상이 일어나는 부분은 분명히 해결해야할 숙제고 이런것들을 하지 말라는 게 아니다.

여기서 하지말라는 것은 성능때문에 기존의 좋은 프로그램 설계를 깨는 것이 오히려 위험하고, 성능이 좋지 않을 수 있다는 것이다.

또한 각각의 성능 최적화 시도를 했으면 전후로 성능을 측정해서 확실히 하는 버릇을 들이라는 내용이다.

 

12. 일반적으로 통용되는 명명규칙을 따르라

- 개발하면서 이게 제일 어려운것 같다.

1) min, max처럼 통용되는 약어는 약어로 쓰되 단어를 임의의로 줄이지 않는다.

2) 클래스나 인터페이스 첫 글자는 대문자로 쓴다. ex) String

3) 메서드와 필드는 첫 글자를 소문자로 쓴다. ex) getName()

4) 상수는 전체 대문자와 띄어쓰기는 언더바로 한다. ex) NEGATIVE_INFINITY

5) 타입매개변수는 한 문자로 표현하고 임의의 타입은 T, 컬렉션의 원소는 E, Map의 키와 값은 K, V, 예외는 X, 메서드의 리턴 타입은 R을 사용한다.

6) 멤버 속성을 반환하는 메서드는 get으로 시작하는 동사구로 짓는다. ex) getSize()

7) boolean 값을 반환하면 has나 is로 시작하는 메서드를 짓는다.

9) 다른 타입으로 변환할 때는 toArray등의 형태로 짓는다. ex) toJson()

10) 의미가 잘 드러날 수 있는 단어를 조합해 사용한다.

참고로 여기 변수명을 지어주는 사이트가 있다. 아주 유용하다.(https://www.curioustore.com/#!/)

 

참고 자료

도서 : 이펙티브 자바(effective java) 3판



출처: https://jeong-pro.tistory.com/176 [기본기를 쌓는 정아마추어 코딩블로그]

어떻게 해야 메서드 잘 만들었다고 소문이 날까?

개발을 하면서 가장 많이 하는 일이면서 가장 난해한 것이 메서드 작성이 아닐까 싶다.

"하나의 메서드에서는 하나의 작업만 한다!" 라는 기본 원칙을 지키려고 하면서도 잘 안된다.

사소하게는 private으로 할지 public으로 해야 할지등 정해야할 것들이 너무도 많은 것이 메서드다.

역시나 한 번에 제대로 작성하려고하면 어렵다. 대신 유념하면서 고치고 또 고치면서 배우는게 코딩아닐까 싶다.

회사를 다닌다면 코드 리뷰를 하는 이유도 올바른, 좋은 메서드를 개발하기 위함이 아닐까 한다.

결국 좋은 메서드를 만드려면 기본 원칙을 잘 알고 시도하고 리팩토링을 거쳐봐야 한다. 그래서 아래에 기본 원칙을 소개하려고 한다.

메서드 작성 기본 원칙

- 파라미터가 유효한지 검사하라

메서드를 작성할 때 파라미터가 유효한지 제일 앞에서 검사해야한다.

메서드의 파라미터가 당연히 유효할 것이라는 생각을 버리고 public, protected 접근지정자가 붙으면 특히나 유념해서 null 체크나 음수, 양수등의 조건 필터를 반드시 넣어야한다.

Objects 클래스에서 requireNonNull로 null 체크를 할 수 있고 예외 메시지도 넣을 수 있다.

이 작업을 앞에서 하지 않고 중간에 확인하면 의도치 않은 에러가 발생할 확률이 올라가기 때문에 파라미터를 제일 앞에서 바로 검사하는 것이 좋다.

- 적시에 방어적 복사본을 만들어라

약간 과장하면 클라이언트가 내가 만든 불변식을 깨뜨리려고 하는 사람이라고 생각하고 프로그래밍 해야한다.

그래서 방어적 복사라는 말이 나온 것이다.

방어적 복사란 어떤 클래스의 메서드에서 클래스의 멤버 변수(객체)에 값을 쓰거나 가져올 때 복사본을 만들어서 get/set 을 사용하는 것이다. (예를 들어서 get/set 메서드일 뿐 다른 메서드도 멤버 변수에 접근한다면 복사본을 만들 필요가 있다.)

* 중요한 것은 파라미터의 유효성을 검사하기 전에 방어적 복사본을 만들고 이 복사본으로 유효성을 검사해야한다. 

방어적 복사를 할 때는 clone 메서드를 사용해서는 안된다.

생성자에서 받은 매개변수 값을 각각 방어적 복사해서 사용해야한다.

성능이 중요할 때는 이런 과정이 영향을 끼칠 수 있다. 따라서 이러한 경우에는 방어적 복사를 하지 않고 문제가 생길 수 있음을 주석과 문서에 명시하는 것으로 대체하면 된다.

- 메서드 설계를 잘하자

시그니처를 신중히 설계하라고 되어있지만 결국은 메서드 설계를 잘하자는 내용이다.

1) 메서드 이름을 잘 짓자.

메서드의 이름은 표준 명명 규칙에 따라서 짓고 같은 패키지에 속한 이름과 일관되게 짓는 것이 좋다.

2) 편의 메서드를 너무 많이 만들지 말자.

하나의 클래스에 너무 많은 메서드가 있으면 다 익히기도 어렵고 구현이 엉켜서 에러를 만들 수 있다.

3) 파라미터는 4개 이하로 만들자.

파라미터를 단순히 필요하다고 5개 이상을 받도록 만들면 메서드를 사용하는 과정에서 헷갈리고 이상한 매개변수를 넣을 확률이 높아진다.

파라미터 개수를 줄이는 방법으로는 여러 메서드로 쪼개는 방법도 있고 파라미터 여러 개를 묶어주는 헬퍼 클래스를 만드는 방법도 있다. 또한 빌더 패턴을 메서드 사용에 호출하는 방법도 있다. (객체 생성에서 빌더 패턴은 다른 포스트에서 작성할 것이다)

4) 파라미터의 타입으로는 클래스보다 인터페이스가 낫다.

구체적으로 HashMap, HashSet이 아니라 Map, Set 인터페이스로 받는 것이 더 유연하다는 얘기다.

5) boolean 보다는 원소 2개짜리 enum이 낫다.

enum을 쓰게되면 코드를 읽고 쓰는게 더 용이하다.

- 다중 정의는 웬만하면 하지 마라

메서드를 같은 이름으로 파라미터 타입만 다르게 정의하는 것이 다중 정의다.

다중 정의가 혼동을 일으킬 우려가 있으니 그냥 사용하지 말자 대신 메서드 이름을 다르게 지어주는 것으로 대체하자.

parseInt, parseDouble, ... 이런식으로 지어주는 것이 헷갈리지 않고 더 좋다.

생성자의 경우에는 이름을 다르게 짓는 것이 불가능하므로 헷갈릴만한 파라미터가 나오면 그냥 instanceof 로 정확한 타입을 찾아서 형변환(캐스팅)해주는 것으로 대체한다.

- 가변 인수는 꼭 필요할 때만 쓰자

가변 인수가 뭐냐면 메서드에 들어올 파라미터 수가 여러 개로 올 수 있게 할 때 쓰는 문법이다.

1
2
3
4
5
6
public int sum(int... args){
    int sum = 0;
    for(int arg : args)
        sum += arg;
    return sum;
}
cs

위의 예제코드처럼 (int...)으로 int 타입의 파라미터가 여러개 올 수 있는 것을 가변 인수라고 한다.

가변 인수는 메서드가 호출될 때마다 배열이 복사되기 때문에 성능에 큰 영향을 미칠 수 있어 자주 호출된다면 사용하지 말아야 한다.

가변 인수대신 사용할 것은 아까하지말라던 다중정의를 쓰는 것이다.

만약 sum 메서드 호출의 대부분이 args가 3개 이하라면 아래와 같이 구현해놓는 것이다.

1
2
3
4
5
public void sum(){}
public void sum(int a1){}
public void sum(int a1, int a2){}
public void sum(int a1, int a2, int a3){}
public void sum(int a1, int a2, int a3, int ...rest){}
cs

이렇게 구현하면 3개 이하를 사용하는 코드에서는 가변 인수를 사용하지 않다가 소수로 몇 번 사용하는 경우에만 가변 인수가 있는 메서드를 호출할 것이다.

가변 인수를 꼭 써야만 하는 곳이라면 써라

- null이 아닌 빈 컬렉션이나 배열을 반환하라

그냥 null을 반환해버리면 반환된 객체가 null이 아닌지도 체크를 매번해줘야한다.

그런데 빈 컬렉션이나 배열을 반환하면 size()라든지 contains(...) 메서드로 비어있는지 사용가능한지 알 수 있다.

- Optional 반환은 신중히 하라

Optional에 대해서 포스팅도 이미 있다. Optional은 특정 조건에서 반환할 값이 없을 때 null 대신 래퍼로 감싸는 타입이다.

Optional은 nullPointerException으로부터 조금 더 자유로워(?)지기 위해서 나온 타입이다.

따라서 Optional을 반환하는 메서드를 만드려면 절대 null을 반환하는 Optional을 리턴해서는 안된다.

그리고 Optional은 아무래도 래퍼하고 그걸 다시 풀고, 값이 없을 때 대체하는 값을 넣고 하는 등의 오버헤드가 있으니 성능 저하는 반드시 동반한다.

컬렉션, 스트림, 배열, 옵셔널같은 어떤 객체를 담을 수 있는 컨테이너타입은 절대 optional로 감싸면 안된다.

그럴바엔 빈 컬렉션, 배열을 반환하는 것이 훨씬 좋다.

박싱된 기본 타입(Integer, Boolean, Double, ...)을 담은 옵셔널을 반환하지 말라

이미 OptionalInt, OptionalLong, OptionalDouble이 있다.

기본타입이 들어있는 것을 반환할 때는 위의 메서드를 사용하도록 하자. 그러면 성능저하가 덜하다.

Optional을 컬렉션의 key, value, 배열의 원소로 사용하는게 적절한 상황은 없다. 사용하지 말라.

그러면 언제 써야할까?

딱 1개의 상황이다.

메서드의 결과를 알 수 없으며, 클라이언트가 이 상황을 특별하게 처리해야 할때 Optional<T>를 반환하게 하면 된다.

 

위와 같이 메서드를 작성할 수 있도록 유념하고 잘못한 부분이 있다면 다시 고치면 된다.



출처: https://jeong-pro.tistory.com/177 [기본기를 쌓는 정아마추어 코딩블로그]

HTTP Header 정리를 하는 이유

KOCW의 "컴퓨터 네트워크 - 한양대학교 이석복 교수님" 강의 중에 이런 표현이 있다.

"TCP를 이해하려면 TCP Header를 이해하면 된다."

"UDP를 이해하려면 UDP Header를 이해하면 된다."

실제로 토씨 하나 안 틀리고 위와 같이 말씀하신 것은 아니지만 비슷한 문장(맥락)이었다.

결국, 어떤 프로토콜을 이해하려면 프로토콜의 헤더만 알면 된다는 얘기었다.

왜 그럴까?

특정 프로토콜의 헤더의 내용은 특정 프로토콜의 기능을 제공하기 위해 담고 있는 최소한의 정보기 때문이다.

헤더에 그 프로토콜에 불필요한 내용을 담으면 네트워크로 전송되는 데이터의 크기가 커져서 빠른 전송이 불가능하기 때문에 프로토콜을 설계할 때부터 꼭 필요한 내용만 담아야 하고, 모든 기능이 표현되어야 한다.

우리가 주로 사용하는 TCP에 대한 이해가 충분히 있어야 하지만, 먼저 다가가기 쉬운 HTTP를 정리해보고자 한다. (HTTP 헤더 정리 = HTTP 이해)

* 여러 블로그를 돌아다니면서 최대한 내용을 많이 모으고, 이해하고 검증하려고 노력했으나 실제로 HTTP 헤더를 열어보았을 때 아래에 기재된 내용이 무조건 들어있지 않기 때문에 포맷이나 설명이 실제와 다를 수 있습니다.

* 잘못된 정보가 있다면 지적해주시면 감사하겠습니다. 그러면 고쳐서 제대로된 정보가 공유되도록 하겠습니다.

HTTP Header

- 공통 헤더

Date : 현재시간 (Sat, 23 Mat 2019 GMT)

Pragma : 캐시제어 (no-cache), HTTP/1.0에서 쓰던 것으로 HTTP/1.1에서는 Cache-Control이 쓰인다.

Cache-Control : 캐시 제어

+ no-store : 캐시를 저장하지 않겠다.

+ no-cache : 모든 캐시를 쓰기 전에 서버에 해당 캐시를 사용해도 되는지 확인하겠다.

+ must-revalidate : 만료된 캐시만 서버에 확인하겠다.

+ public : 공유 캐시에 저장해도 된다.

+ private : '브라우저' 같은 특정 사용자 환경에만 저장하겠다.

+ max-age : 캐시의 유효시간을 명시하겠다.

Transfer-Encoding : body 내용 자체 압축 방식 지정

'chunked'면 본문의 내용이 동적으로 생성되어 길이를 모르기 때문에 나눠서 보낸다는 의미다.

본문에 데이터 길이가 나와서 야금야금 브라우저가 해석해서 화면에 뿌려줄 때 이 기능을 사용한다.

Upgrade : 프로토콜 변경시 사용 ex) HTTP/2.0

Via : 중계(프록시)서버의 이름, 버전, 호스트명

Content-Encoding : 본문의 리소스 압축 방식 (transfer-encoding은 body 자체이므로 다름)

Content-type : 본문의 미디어 타입(MIME) ex) application/json, text/html

Content-Length : 본문의 길이

Content-language : 본문을 이해하는데 가장 적절한 언어 ex) ko

한국사이트여도 본문을 이해하는데 영어가 제일 적절하면 영어로 지정된다.

Expires : 자원의 만료 일자

Allow : 사용이 가능한 HTTP 메소드 방식 ex) GET, HEAD, POST

Last-Modified : 최근에 수정된 날짜

ETag : 캐시 업데이트 정보를 위한 임의의 식별 숫자

Connection : 클라이언트와 서버의 연결 방식 설정 HTTP/1.1은 kepp-alive 로 연결 유지하는게 디폴트.


- 요청 헤더

Host : 요청하려는 서버 호스트 이름과 포트번호

User-agent : 클라이언트 프로그램 정보 ex) Mozilla/4.0, Windows NT5.1

이 정보를 통해서 서버는 클라이언트 프로그램(브라우저)에 맞는 최적의 데이터를 보내줄 수 있다.

Referer : 바로 직전에 머물렀던 웹 링크 주소(해당 요청을 할 수 있게된 페이지)

Accept : 클라이언트가 처리 가능한 미디어 타입 종류 나열 ex) */* - 모든 타입 처리 가능, application/json - json데이터 처리 가능.

Accept-charset : 클라이언트가 지원가능한 문자열 인코딩 방식

Accept-language : 클라이언트가 지원가능한 언어 나열

Accept-encoding : 클라이언트가 해석가능한 압축 방식 지정 ex) gzip, deflate

압축이 되어있다면 content-length와 content-encoding으로 압축을 해제한다.

Content-location : 해당 개체의 실제 위치

Content-disposition : 응답 메세지를 브라우저가 어떻게 처리할지 알려줌. ex) inline, attachment; filename='jeong-pro.xlsx'

Content-Security-Policy : 다른 외부 파일을 불러오는 경우 차단할 리소스와 불러올 리소스 명시

ex) default-src https -> https로만 파일을 가져옴

ex) default-src 'self' -> 자기 도메인에서만 가져옴

ex) default-src 'none' -> 외부파일은 가져올 수 없음

If-Modified-Since : 여기에 쓰여진 시간 이후로 변경된 리소스 취득. 페이지가 수정되었으면 최신 페이지로 교체하기 위해 사용된다.

Authorization : 인증 토큰을 서버로 보낼 때 쓰이는 헤더

Origin : 서버로 Post 요청을 보낼 때 요청이 어느 주소에서 시작되었는지 나타내는 값

이 값으로 요청을 보낸 주소와 받는 주소가 다르면 CORS 에러가 난다.

Cookie : 쿠기 값 key-value로 표현된다. ex) attr1=value1; attr2=value2


- 응답 헤더

Location : 301, 302 상태코드일 떄만 볼 수 있는 헤더로 서버의 응답이 다른 곳에 있다고 알려주면서 해당 위치(URI)를 지정한다.

Server : 웹서버의 종류 ex) nginx

Age : max-age 시간내에서 얼마나 흘렀는지 초 단위로 알려주는 값

Referrer-policy : 서버 referrer 정책을 알려주는 값 ex) origin, no-referrer, unsafe-url

WWW-Authenticate : 사용자 인증이 필요한 자원을 요구할 시, 서버가 제공하는 인증 방식

Proxy-Authenticate : 요청한 서버가 프록시 서버인 경우 유저 인증을 위한 값

 

* 자료의 출처는 아래 참고 사이트입니다.

 

참고 사이트

https://www.slideshare.net/skytear7/http-28415549

http://rangken.github.io/blog/2015/http-headers/

https://gmlwjd9405.github.io/2019/01/28/http-header-types.html



출처: https://jeong-pro.tistory.com/181 [기본기를 쌓는 정아마추어 코딩블로그]

급하게 알아보는 스프링 기반 기술

이름은 아주 거창하게 "스프링 기반 기술" 이라고 지어봤습니다.

배경

최근에 첫 이직을 했습니다. 낯설기도하고 약간의 긴장감과 약간의 두려움이 합쳐져서 우당탕탕(?)하고 있습니다.

그러다가 이제 회사 프로젝트(소스 코드)를 좀 보려고하니, 스프링(Spring)이었습니다.

스프링부트(Springboot)를 써왔어서 큰 거부감은 없었기때문에 볼 만 하겠지 했는데... 음?

Servlet, ServletContext, ApplicationContext, ContextLoaderListener, ... 다양한 ~Context의 향연이 펼쳐지며 아차 싶었습니다.

스프링부트에서도 자바 소스(Java Config)로 설정을 했었지만 Servlet으로 자바 소스 설정을 하니까 기본기(Servlet에 대한 이해)가 부족한 저는 급하게 기반 기술에 대한 이해가 필요해져서 급하게 정리했습니다.

틀린 부분이 상당히 많이 있을 수 있으니 양해바랍니다...🙏🏼

Servlet

public abstract class HttpServlet extends GenericServlet { //... protected void doGet(HttpServletRequest req, HttpServletResponse resp){...} protected void doPost(HttpServletRequest req, HttpServletResponse resp){...} //... }

  • 자바를 사용하여 웹 페이지를 동적으로 생성하는 서버측 프로그램 혹은 그 사양입니다.
    • 쉽게 말해, "웹 서버 프로그래밍을 하기 위한 사양을 갖춘 자바 코드" 라고 할 수 있습니다.
  • HttpServlet 클래스를 상속한 클래스
  • Servlet은 Servlet Container에 의해 관리, 실행됩니다. HTTP Server + Servlet Container가 웹 서버 역할에 필요한 대부분을 구현해두었고, 개발자는 Servlet을 만들어 HTTP 요청을 받아 처리하는 부분을 구현하는 것입니다.
  • 메서드를 참고하면 알 수 있듯 요청(Request)과 응답(Response) 즉, Http 웹 서버 기능 동작이 가능합니다.

톰캣(Tomcat)

  • 웹 애플리케이션 서버(WAS)중 하나로 Servlet Container, Servlet Engine이라고 표현할 수 있으며 자바 웹 프로그래머가 작성한 Servlet을 관리합니다.
    • Servlet을 관리한다는 말은 클라이언트가 어떤 요청(Request)을 했을 때, 어떤 Servlet을 실행할 것인지 제어해준다는 것입니다.
    • 톰캣은 Servlet을 관리해주는 주체이기 때문에 아무 클래스가 아니라 Servlet(HttpServlet 클래스를 상속한 클래스)이어야 합니다.

web.xml

  • WAS(e.g. tomcat)는 Servlet을 생성하고 어떤 Servlet이 어떤 요청을 담당할 것인지(mapping), 어떤 요청이 인증과정을 거칠 것인지 등의 제어 기능을 지원해줍니다. 그러려면 WAS에게 Servlet에 대한 정보를 줘야하는데 이때 쓰이는 파일이 web.xml(Deployment Descriptor)입니다.
    • servlet3.0부터는 web.xml에서만 Servlet에 대해 정의하지 않고, 자바 소스 설정(java config)으로도 가능합니다. 요즘은 학습할 때 빼고는 대부분 자바 소스 설정으로 하는 것으로 추정됩니다.

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <servlet> <servlet-name>appServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>appServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>

DispatcherServlet

  • dispatch는 '급파하다', '신속히 보내다', '특파하다', ... 뭐 이런 뜻이 있습니다. dispatcher는 '항공기 운항 관리자(관제사)', '배차 담당자', ... 이런 뜻이 있습니다.
  • Servlet에 대입해보면, "Servlet Container(e.g. tomcat)으로부터 들어오는 요청을 관제하는 컨트롤러다" 라고 할 수 있습니다. Spring MVC에서 요청을 받는 부분이라고 할 수 있습니다.
  • Servlet Container(e.g. tomcat)에 여러 매핑 정보를 가진 여러 Servlet을 생성하고 관리할 수도 있지만 일반적인 경우(?)에는 Servlet Container에는 DispatcherServlet만 등록해놓고 DispatcherServlet이 HandlerMapping을 통해 적절한(개발자가 만든) Controller로 매핑하도록 하는 것으로 알고 있습니다.

Servlet Filter

  • Servlet 실행 전, 후에 어떤 작업을 하고자할 때 Servlet Filter 를 사용한다.
    • Interceptor를 사용할 수 있겠지만 차이점은 실행시점(handler전, 후)에 차이가 있습니다.
    • Filter 는 Servlet Container에 등록하고 Interceptor는 스프링 컨테이너에 등록합니다.
    • javax.servlet.Filter 인터페이스의 구현체입니다.

Servlet Context

  • Servlet 단위로 생성되는 Context입니다.
  • Servlet Container(e.g. tomcat)에 DispatcherServlet과 같은 servlet을 등록하면 해당 servlet이 갖는 하나의 작은 컨테이너 역할을 하는 객체입니다.
  • 스프링을 이용하는 경우, 스프링 컨테이너(Application Context)를 부모 Context로 사용합니다.
  • Application Context와 Servlet Context에 같은 id로 된 Bean이 있으면 ServletContext에 있는 Bean을 우선 사용합니다. (Bean을 찾는 순서가 Servlet에서 ServletContext를 확인한 후에 부모인 ApplicationContext를 확인하기 때문입니다.

Application Context

  • Root Context이자 스프링에 의해 생성되는 Bean에 대한 Spring IoC Container입니다.
  • BeanFactory를 상속받는 Context
  • 여러 Servlet에서 공통으로 사용할 Bean을 등록하는 Context입니다.
  • @Transactional 으로 트랜잭션을 이용해야할 때 ApplicationContext에 있는 Service에서만 트랜잭션이 정상 작동합니다.

WebApplicationInitializer

public interface WebApplicationInitializer{ void onStartup(ServletContext servletContext) throws ServletException; //onStartup()메소드의 파라미터인 ServletContext는 Servlet Container를 가리킵니다. }

  • Servlet Context를 프로그래밍적으로 설정하기 위한 인터페이스입니다. (web.xml을 대체하기 위함)
  • 스프링에 ServletContainerInitializer 를 구현한 클래스(SpringServletContainerInitializer)가 있고 그 클래스가 WebApplicationInitializer 인터페이스를 구현한 클래스를 찾아 초기화 작업을 위임하도록 구현해놨습니다.
  • 설정하는 방식마다 차이가 있어서 좀 눈여겨봐야할 필요가 있고, 추가되는 내용을 정리해야할 필요가 있습니다...

ContextLoaderListener

  • Servlet Container(e.g. tomcat)에 루트 웹 애플리케이션 컨텍스트(Application Context)를 등록하는 방법을 제공합니다.
    • Servlet Container의 시작과 종료 시에 발생하는 이벤트를 처리하는 리스너를 등록하기 위해 ServletContextListener 인터페이스를 구현한 리스너를 사용하는데 그 구현체가 바로 ContextLoaderListener 입니다.
  • Application Context에 대한 실제 초기화 작업을 수행합니다.
  • 이 리스너(Listener)만 등록하면 자동으로 디폴트 루트 애플리케이션 컨텍스트를 생성해줍니다.
    • 디폴트 설정
      • XmlWebApplicationContext
      • XML 설정파일 위치 : /WEB-INF/applicationContext.xml

RequestContextListener

  • 현재 스레드에 요청(Request)을 노출하는 Servlet Listener입니다.
  • RequestContextListener 를 등록하면 LocaleContextHolder, RequestContextHolder를 통해서 HttpServletRequest에 접근할 수 있게 합니다.

AnnotationConfigWepApplicationContext

  • Component 클래스를 입력값으로 받는 WebApplicationContext 인터페이스의 구현체입니다.
    • Component 클래스는 @Configuration, @Component, @Inject annotation을 사용하는 클래스를 포함합니다.
  • 패키지 경로를 스캔하여 컴포넌트를 Context에 등록합니다.
  • AnnotationConfigWebApplicationContext 인스턴스는 스프링에서 DispatcherServlet이나 ContextLoaderListener에 주입되는 경우에 많이 사용합니다. (web.xml을 대체하여 WebApplicationInitializer 를 이용한 자바 코드 기반 설정을 할 때)
  • XmlWebApplicationContext와 달리 디폴트로 ConfigurationLocation을 지정하지 않습니다. 따라서 ContextLoader에 대한 context-param= 'contextConfigLocation' 또는 Servlet에 대한 init-param = 'contextConfigLocation'을 반드시 설정해야합니다.

참고사이트

https://victorydntmd.tistory.com/148

https://jaehun2841.github.io/2018/10/21/2018-10-21-spring-context/

https://www.baeldung.com/spring-web-contexts



출처: https://jeong-pro.tistory.com/222 [기본기를 쌓는 정아마추어 코딩블로그]

DispatcherServlet

스프링 MVC는 모든 요청(Request)을 받아 실제 작업은 다른 컴포넌트로 위임하는 DispatcherServlet 을 두어 프론트 컨트롤러 패턴으로 디자인되었습니다.

DispatcherServlet  Servlet 사양에 맞게 선언되어야 하고 매핑되어야 합니다.

스프링에서는 web.xml 파일에 정의하고, 요새는 스프링과 스프링부트에서는 자바 설정을 사용해서 정의합니다.

결과적으로, DispatcherServlet 은 스프링 설정을 사용하여 위임할 컴포넌트를 찾습니다. (해당 컴포넌트는 Request Mapping, View Resolution, Exception handling, ...의 작업을 합니다.)

아래 코드는 자바 스프링 설정을 이용한 DispatcherServlet 의 생성과정입니다.

public class MyWebApplicationInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletCxt) { // Load Spring web application configuration AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext(); ac.register(AppConfig.class); ac.refresh(); // Create and register the DispatcherServlet DispatcherServlet servlet = new DispatcherServlet(ac); ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet); registration.setLoadOnStartup(1); registration.addMapping("/app/*"); } }

아래 코드는 web.xml 을 이용한 DispatcherServlet 의 생성 과정입니다.

<web-app> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/app-context.xml</param-value> </context-param> <servlet> <servlet-name>app</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value></param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>app</servlet-name> <url-pattern>/app/*</url-pattern> </servlet-mapping> </web-app>

스프링부트는 스프링과 다른 초기화 과정을 거칩니다.

스프링에서는 위와 같이 Servlet Container 의 생명 주기에 listener로 연결하고, 스프링부트는 스프링 설정을 이용하여 스프링 애플리케이션과 내장된 Servlet Container 를 실행시킵니다.

DispatcherServlet 에 대한 간단한 Overview를 마칩니다.

Context 계층 구조

DispatcherServlet 은 일반적으로 계층 구조를 갖습니다.

많은 애플리케이션에는 단일 DispatcherServlet, 단일 WebApplicationContext 를 갖는 간단한 스타일로 만듭니다.

WebApplicationContext 를 루트 컨텍스트(부모 컨텍스트)라고 부르고 DispatcherServlet 은 자식 컨텍스트 구조를 갖습니다.

DispatcherServlet 은 요청에 대응할 수 있는 Controller, ViewResolver, HandlerMapping 과 같은 스프링 빈(Beans)을 구성하고, WebApplicationContext 에는 모든 서블릿이 공유할 수 있는 Service, Repository 와 같은 스프링 빈을 구성합니다.

특별한 빈 타입

  • HandlerMapping
    • 요청(Request)을 handler(=controller)로 매핑합니다. 전/후 처리를 위한 interceptor 리스트를 포함합니다.
    • 매핑은 몇 가지 기준을 기반으로 합니다.
    • 두 가지 핵심 구현체가 있습니다. RequestMappingHandlerMapping  @RequestMapping 애노테이션을 지원하고, SimpleUrlHandlerMapping 은 URI 경로 패턴으로 명시적인 핸들러 등록 기능을 지원합니다.
  • HandlerAdapter
    • DispatcherServlet 이 요청에 매핑된 handler를 호출할 수 있도록 도와줍니다. 실질적인 컨트롤러 호출 방식은 DispatcherServlet 이 몰라도 되게 해줍니다.
  • HandlerExceptionResolver
    • Exception을 해결하기 위한 전략으로 예외가 발생했을 때, 컨트롤러나 HTML Error view 또는 다른 것으로 결정해줍니다.
  • ViewResolver
    • 컨트롤러에서 리턴된 문자열 기반의 View 이름을 기준으로 실제로 렌더링할 뷰 객체를 결정해줍니다.
  • LocaleResolver
    • 국제화된 View를 제공하기위해서 클라이언트의 타임존과 Locale 을 결정해줍니다.
  • ThemeResolver
    • 웹 애플리케이션에서 사용할 수 있는 테마를 결정해줍니다.
  • MultipartResolver
    • 멀티파트 파싱 라이브러리를 이용하여 멀티파트 요청(파일업로드와 같은 요청)을 파싱하기위한 추상화
  • FlashMapManager
    • 한 요청에서 다른 요청(redirect)으로 속성을 전달하는데 사용할 수 있는 Input, Output 을 FlashMap에 저장하고 검색합니다.

Web MVC Config

위에서 설명한 요청을 처리하기 위해 필요한 특별한 빈 타입 리스트에 애플리케이션은 인프라 Bean을 선언할 수 있습니다.

DispatcherServlet 은 각 특별한 빈에 대하여 WebApplicationContext 를 검사합니다. 만약 매칭되는 빈이 없으면 DispatcherServlet.properties 파일에 나열된 디폴트 타입으로 대체됩니다.

스프링부트는 MVC 자바 설정에 의존하여 Spring MVC를 구성하고 많은 편리한 옵션을 제공합니다.

→ 추후 다른 포스트에서 설명

Processing

DispatcherServlet 에서 진행되는 과정

  • WebApplicationContext 를 컨트롤러 같은 프로세스의 다른 요소가 사용할 수 있는 속성으로 요청에 바인딩합니다. 기본적으로 DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE 라는 키에 바인딩됩니다. (request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext())
  • locale resolver 는 프로세스의 요소가 뷰 렌더링, 데이터 준비, ... 등을 위한 요청을 처리할 때 사용할 locale(지역 정보)을 결정할 수 있도록 요청에 바인딩됩니다. (request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver))
  • theme resolver 는 사용할 테마를 결정할 수 있도록 요청에 바인딩됩니다. (request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver))
  • 요청이 멀티파트인지 검사하고 멀티파트 요청이라면 프로세스의 다른 요소에 의해 추가 처리가 될 수 있도록 MultipartHttpServletRequest 로 래핑됩니다.
  • 앞선 과정이 끝나면, 요청을 처리할 적절한 컨트롤러(핸들러) 검색합니다. 핸들러를 찾으면 이 핸들러에게 요청을 전달하기 위해 적당한 HandlerAdapter 를 가져와서 실행시킵니다. 물론 전/후처리기가 있으면 요청을 컨트롤러로 위임하기 전/후에 실행됩니다.
  • 적당한 뷰를 찾고 model에 있는 데이터를 매핑합니다.
  • 결과를 응답(Response)에 담아 넣습니다.

WebApplicationContext에 선언된 HandlerExceptionResolver 빈은 요청이 처리되는 동안 발생한 예외를 결정하는데 사용됩니다.

DispatcherServlet 은 Servlet API에 지정된대로 last-modification-date 리턴도 지원합니다.

특수 요청에 대한 마지막 수정 날짜를 결정하는 과정은 다음과 같이 간단합니다.

 DispatcherServlet 은 적절한 핸들러 매핑을 검색하고 발견된 핸들러가 LastModified 인터페이스를 구현하는지 테스트합니다. LastModified 인터페이스의 getLastModified(request) 메서드의 결과같이 클라이언트로 리턴됩니다.

DispatcherServlet 은 초기화 파라미터(init-param)를 이용하여 입맛에 맞게 커스터마이징할 수도 있습니다.

Interception

모든 HandlerMapping 의 구현체는 특정 요청에 특정 기능을 적용하기 좋은 핸들러 인터셉터를 지원합니다.

인터셉터는 org.springframework.web.servlet 패키지에있는 HandlerInterceptor 인터페이스를 구현해야 합니다. (전/후처리에 좋은 세 가지 메서드가 있음)

  • preHandle(...) : 컨트롤러(=핸들러)를 실행하기 전에 실행
  • postHandle(...) : 컨트롤러를 실행하고난 후에 실행
  • afterCompletion(...) : 온전하게 요청이 끝난 후에 실행

preHandle(...) 메소드는 boolean 값을 리턴합니다. 이것을 사용해서 실행체인을 그만둘지, 계속할지를 결정할 수 있습니다.

만약 true 를 리턴한다면, 실행체인은 계속됩니다. 만약 false 를 리턴한다면, DispatcherServlet 은 인터셉터가 스스로 요청을 잘 처리했다고 여기고 실제 실행 체인에서 다른 인터셉터와 실제 컨트롤러를 계속 실행하지 않습니다.

postHandle(...) 메소드는 실제 컨트롤러에서 이미 응답(Response)가 작성되기 때문에 상대적으로 덜 유용합니다. (헤더 추가하는 등의 작업을 하기엔 늦음)

그렇기 때문에 그런 부분을 해결하려면 ResponseBodyAdvice 를 구현하고 이를 ControllerAdvice 빈으로 선언하거나 RequestMappingHandlerAdapter 에서 직접 구성하면 된다.

Exceptions

요청을 매핑하는 중에 예외가 발생하거나 실제 컨트롤러로부터 예외가 발생하면 DispatcherServlet  HandlerExceptionResolver 빈의 체인에 예외를 처리를 위임한다.

사용할 수 있는 HandlerExceptionResolver 구현체는 다음과 같습니다.

(체인 순서 : ExceptionHandlerExceptionResolver → ResponseStatusExceptionResolver → DefaultHandlerExceptionResolver)

  • SimpleMappingExceptionResolver : 예외 클래스 명과 에러 뷰 이름을 매핑합니다. 브라우저에서 에러페이지를 렌더링할 때 유용합니다. (Web.xml 같은데다가 error-page하고 지정한 것을 얘가 처리해준다. 예외 이름과 뷰 이름을 하나의 쌍으로 정의하고 예외 발생시 처리한다.)
  • ExceptionHandlerExceptionResolver : @Controller 또는 @ControllerAdvice 안에 있는 @ExceptionHandler 애노테이션이 적용된 메소드에 의해 예외가 처리됩니다.
  • ResponseStatusExceptionResolver : @ResponseStatus 애노테이션으로 예외를 처리하고 값을 기반으로 HTTP 상태 코드에 매핑합니다. (@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "Permission Denied") 이런 적용을 해봤을 것이다. 예외발생시에 이 애노테이션이 적용된 메소드로 예외를 처리한다.)
  • DefaultHandlerExceptionResolver : Spring MVC가 발생한 예외를 해결하고 이것을 HTTP 상태 코드에 매핑합니다. 그러니까 스프링에서 최소한의 예외처리는 해주는 객체로 만들어 놓은 것이다. (페이지가 없으면 뜨는 404 Not Found 예외같은 것을 내가 정의한 적 없는데 스프링 애플리케이션에서 페이지를 못 찾으면 뜬다. 이것을 DefaultHandlerExceptionResolver가 처리해준다.)

예외 처리를 커스터마이징하고 싶으면 위의 클래스들 처럼 HandlerExceptionResolver를 구현한 빈을 생성하고 체인의 순서를 정해주면 된다. 우선순위가 높을수록 나중에 위치합니다.

public interface HandlerExceptionResolver { @Nullable ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex); }

  • ModelAndView 는 에러 뷰를 가리킵니다.
  • 해당 ExceptionResolver에서 예외가 처리되었다면 ModelAndView 는 비어있게됩니다.
  • 예외가 처리되지 못하고 아직 남아있다면 ModelAndView  null 입니다. 다음 ExceptionResolver까지 계속 타고가다가 끝까지 처리가 안되면 결국에는 Servlet Container 까지 예외가 전파될 수 있습니다.

View Resolution

Spring MVC는 특정 뷰 기술에 의존하지 않고 브라우저에서 모델을 렌더링할 수 있는 ViewResolver  View 인터페이스를 정의합니다.

ViewResolver 는 뷰 이름과 실제 뷰와의 매핑을 제공합니다.

마찬가지로 ViewResolver 구현체를 살펴봅니다.

  • AbstractCachingViewResolver : AbstractCachingViewResolver 의 하위 클래스는 결정하는 View 객체 인스턴스를 캐시합니다. 뷰를 캐싱하여 성능을 끌어올리는 역할을 하며 필요하면 캐시를 활성화하지 않을 수 있습니다. 대부분의 뷰리졸버들이 상속합니다.
  • XmlViewResolver : DTD같은 XML로 쓰여진 설정 파일을 통한 뷰를 결정합니다.
  • ResourceBundleViewResolver : ResourceBundle 에 bean definitions를 사용하여 결정합니다. 기본적으로 views.properties 파일을 사용합니다. 결정해야하는 각 뷰에 대해 [viewname].class 속성 값을 뷰 클래스로 사용하고 [viewname].url의 값을 뷰의 URL로 사용합니다.
  • UrlBasedViewResolver : 명시적인 정의없이 URL에 대한 논리적 뷰 이름으로 결정합니다.
  • InternalResourceViewResolver : Servlet, JSP같은 InternalResourceView 를 지원합니다.
  • FreeMarkerViewResolver : FreeMarkerView 를 지원합니다.
  • ContentNegotiatingViewResolver : 요청 파일 이름이나 Accept 헤더를 기준으로 뷰를 결정합니다.

Handling

더 다양한 뷰 리졸버 빈을 선언하여 뷰 리졸버들을 체인으로 엮을 수 있습니다.

적당한 뷰를 찾을 수 없음을 나타내기 위해 null 을 리턴할 수 있습니다.

Redirecting

redirect: prefix를 뷰 이름에 붙이면 리다이렉트를 실행시켜줍니다. UrlBasedViewResolver와 이를 상속한 하위 클래스는 prefix를 붙이면 리다이렉트가 필요하다는 명령으로 인식합니다.

@ResponseStatus 애노테이션을 달면 RedirectView 에서 설정한 응답 상태보다 우선하는 것을 주의해야합니다.

Forwarding

forward: prefix를 사용하면 궁극적으로 UrlBasedViewResolver 와 그 하위클래스에 의해 뷰가 결정됩니다.

forward를 사용하면 InternalResourceView 를 생성합니다.

Content Negotiation

ContentNegotiationViewResolver 는 뷰를 결정하지 않습니다. 다른 뷰리졸버에게 위임하고 클라이언트 요청에서 Accept 헤더 또는 쿼리 파라미터로부터 결정할 수 있습니다.

미디어 타입(Content-Type)을 비교하여 적절한 뷰를 찾습니다.

Locale

Spring MVC에서도 국제화를 지원합니다.

DispatcherServlet 은 클라이언트의 locale을 사용하여 자동으로 메시지를 결정합니다. (LocaleResolver)

클라이언트로부터 요청이 오면, DispatcherServlet 은 locale resolver를 찾습니다. 찾으면 locale을 설정하려고 시도합니다.

RequestContext.getLocale() 메서드를 사용해서 locale resolver가 결정한 locale에 항상 접근할 수 있습니다.

locale resolver와 인터셉터는 `org.springframework.web.servlet.i18n 패키지에 정의되어 있습니다. 일반적인 방식으로 application context에 구성됩니다.

  • Time Zone
    • LocaleContextResolver 인터페이스는 표준 시간대 정보를 포함할 수 있는 더 풍부한 LocaleContext를 제공할 수 있는 확장을 제공합니다. RequestContext.getTimeZone() 메서드를 사용하여 사용자의 TimeZone을 가져올 수 있습니다.
  • AcceptHeaderLocaleResolver
    • locale resolver는 Request에서 Accept-language 헤더를 분석합니다. 보통 헤더 필드에는 클라이언트의 운영체제의 locale이 포함되어있습니다. 대신 이 리졸버는 표준시간대정보를 지원하지 않습니다.
  • CookieLocaleResolver
    • Cookie 를 조사해서 Locale 이나 TimeZone 이 명시되어 있으면 그것을 사용합니다. 최초에는 한 번 setLocale() 메서드를 통해서 쿠키에 값을 저장해야합니다. 쿠키에 값이 존재하지 않으면 디폴트 설정을 따라가고 그 마저 안되면 Accept-language 를 따라갑니다.
  • SessionLocaleResolver
    • SessionLocaleResolver 는 연관된 유저의 요청으로부터 얻은 세션에서 Locale  TimeZone 을 조회합니다. 마찬가지로 최초에 setLocale() 메서드를 통해서 세션에 Locale을 저장해야합니다. 세션에 값이 존재하지 않으면 디폴트 설정을 따라가고 그 마저 안되면 Accept-language 를 따라갑니다.
  • LocaleChangeInterceptor
    • LocaleChangeInterceptor 를 이용하여 Locale 변경을 할 수 있습니다. 요청(Request)의 파라미터를 찾고 locale을 변경합니다.

Themes

스타일시트나 이미지같은 정적인 자원의 모음을 테마라고 합니다. ThemeResolver와 ThemeSource 를 적용하여 애플리케이션의 외적인 스타일을 꾸밀 수 있습니다.

실질적으로 테마에 대한 처리는 ResourceBundleThemeSource 로 위임하고 이것은 프로퍼티 파일을 참고하여 테마를 적용시켜줍니다. 프로퍼티 파일 내부는 다음과 같습니다.

styleSheet=/themes/cool/style.css background=/themes/cool/img/coolBg.jpg

이렇게 프로퍼티 파일을 정의하고 난 후에 DispatcherServlet  themeResolver 라는 이름을 갖는 빈을 찾습니다. 그리고 그 themeResolver가 요청에 대하여 테마를 적용해줍니다. (잘 안 쓰이니 간단히 그렇구나하고 넘어갑니다.)

Multipart Resolver

MultipartResolver 는 파일업로드가 포함된 멀티파트 요청을 파싱하는 것에 대한 전략입니다.

멀티파트 요청을 처리하기 위해서 MultipartResolver 라는 이름을 갖는 MultipartResolver 빈이 필요합니다.

DispatcherServlet 은 HTTP POST요청에 content-type  multipart/form-data 인 요청을 받았을 때 MultipartResolver 를 찾아 요청을 처리한다.

MultipartResolver 는 내용을 파싱하고 현재 요청(HttpServletRequest)에 래핑하여 해당 멀티파트 내용을 사용할 수 있도록 합니다.

정리

Spring MVC 에서 핵심적인 역할을 담당하는 DispatcherServlet 에 대해서 조금 자세하게(?) 살펴봤습니다.

내용을 제대로 이해하기 위해서 DispatcherServlet.class 코드를 같이 보면서 이해하면 더 좋을 것 같습니다.

어떤 동작, 어떤 기능을 하는지도 중요하지만, Spring 특유의 인터페이스를 통한 확장 포인트와 아키텍처에 대해서 생각해보는 게 더 중요하다는 느낌을 받았습니다.

참고 사이트

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/web.html#mvc-servlet-sequence

https://sabarada.tistory.com/16

https://jeong-pro.tistory.com/96



출처: https://jeong-pro.tistory.com/225 [기본기를 쌓는 정아마추어 코딩블로그]

REST가 뭐에요? RESTful에 대해서 설명해주세요.

REST가 무엇인가?

REST는 분산 시스템 설계를 위한 아키텍처 스타일이다.

아키텍처 스타일이라는건 쉽게 말하면 제약 조건의 집합이라고 보면 된다.

RESTful은 무엇인가?

RESTful은 위의 제약 조건의 집합(아키텍처 스타일, 아키텍처 원칙)을 모두 만족하는 것을 의미한다.

REST라는 아키텍처 스타일이 있는거고 RESTful API라는 말은 REST 아키텍처 원칙을 모두 만족하는 API라는 뜻이다.

우리가 REST와 RESTful을 동일한 의미로 사용하곤 하는데 엄격하게는 다르다는 것을 알 수 있다.

->이로써 REST와 RESTful, RESTful API가 무엇인지, 어떻게 다른지를 말할 수 있게 되었다.

REST가 필요한 이유는 뭘까?

1. 위에서 말한 것과 같이 분산 시스템을 위해서다.

거대한 애플리케이션을 모듈, 기능별로 분리하기 쉬워졌다. RESTful API를 서비스하기만 하면 어떤 다른 모듈 또는 애플리케이션들이라도 RESTful API를 통해 상호간에 통신을 할 수 있기 때문이다.

2. WEB브라우저 외의 클라이언트를 위해서다. (멀티 플랫폼)

웹 페이지를 위한 HTML 및 이미지등을 보내던 것과 달리 이제는 데이터만 보내면 여러 클라이언트에서 해당 데이터를 적절히 보여주기만 하면 된다.

예를 들어 모바일 애플리케이션으로 html같은 파일을 보내는 것은 무겁고 브라우저가 모든 앱에 있는 것은 아니기 때문에 알맞지 않았는데 RESTful API를 사용하면서 데이터만 주고 받기 때문에 여러 클라이언트가 자유롭고 부담없이 데이터를 이용할 수 있다.

서버도 요청한 데이터만 깔끔하게 보내주면되기 때문에 가벼워지고 유지보수성도 좋아졌다.

REST의 구성 요소

HTTP URI = 자원

HTTP Method = 행위

MIME Type = 표현 방식

1
2
GET /100 HTTP/1.1
Host : jeong-pro.tistory.com
cs

위와 같은 Request 메세지가 있으면 URI자원은 "/100" 이고, HTTP Method는 "GET" 이다.

MIME 타입은 보통 Response Http header 메세지에 Content-type으로 쓰인다. 여기서는 없다.

그러면 이해하기를 jeong-pro.tistory.com 서버에 /100 이라는 자원을 GET(조회)하고 싶다는 요청으로 해석이 가능하다. 이게 REST 방식을 이용한 Request 예시다. (참고로 이것은 이해를 위한 것일 뿐 RESTful 하다고는 못한다.) 

1
2
3
4
HTTP/1.1 200 OK
Content-Type : application/json-patch+json


[{"title""helloworld""author""jeong-pro"}]

이런 Response가 왔다고 해보자.

그러면 Content-Type을 보고 클라이언트는 IANA라는 타입들의 명세를 모아놓은 사이트에 가서 application/json-patch+json 이라는 타입의 명세를 확인하고 아래 Body의 내용이 json타입이구나를 알 수 있는 것이다.

 

REST는 알겠고 그러면 그 제약 조건이 뭔데요?

1. Client/Server

2. Stateless : 각 요청에 클라이언트의 context가 서버에 저장되어서는 안된다.

3. Cacheable : 클라이언트는 응답을 캐싱할 수 있어야 한다.

4. Layered System : 클라이언트는 서버에 직접 연결되었는지 미들웨어에 연결되었는지 알 필요가 없어야 한다.

5. Code on demand(option) : 서버에서 코드를 클라이언트에게 보내서 실행하게 할 수 있어야 한다.

6. uniform interface : 자원은 유일하게 식별가능해야하고, HTTP Method로 표현을 담아야 하고, 메세지는 스스로를 설명(self-descriptive)해야하고, 하이퍼링크를 통해서 애플리케이션의 상태가 전이(HATEOAS)되어야 한다.

왜 uniform interface에 강조가 되어있냐면, 1~5번의 제약 조건은 HTTP를 기반으로하는 REST는 HTTP에서 이미 충분히 지켜지고 있는 부분이라서 비교적 덜 주의를 기울여도 된다.

RESTful하려면 저 uniform interface를 잘 지켜야 한다.

그 중에서도 HATEOAS self-descriptive를 잘 지켜야 한다.

필자가 주로 쓰는 Spring에는 spring-data-rest, spring hateoas, spring-rest-doc으로 두 제약을 지키기위해 사용할 수 있는 라이브러리가 있다. (이 포스트는 면접을 위한 포스트일 뿐 사용법과 테스트는 다른 포스트에서 한다.)

HATEOAS는 Link 라는 HTTP 헤더에 다른 리소스를 가리켜 주는 값을 넣는 방법으로 해결한다.

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Content-Type : application/json
Link : </spring/1>; rel="previous",
        </spring/3>; rel="next";
{
    "title" : "spring의 모든 것"
    "author" : "jeong-pro"
}
Colored by Color Scripter
cs

위와 같이 해당 정보에서 다른 정보로 넘어갈 수 있는 하이퍼링크를 명시해야 한다는 것이다.

완벽한 REST는 무엇일까? WEB이다.

어떤 Application이 생겼다고 브라우저는 버전을 업데이트할 필요가 없고, 브라우저가 해당 application으로 어떻게 요청하는지를 알게 해야할 필요가 없다.

* 장점

- 메세지를 단순하게 표현할 수 있고 WEB의 원칙인 확장에 유연하다. (멀티플랫폼)

- 별도의 장비나 프로토콜이 필요없이 기존의 HTTP 인프라를 이용할 수 있다. (사용이 용이함)

- server, client를 완전히 독립적으로 구현할 수 있다.

* 단점

- 표준, 스키마가 없다. 결국은 API 문서가 만들어지는 이유다.

- 행위에 대한 메소드가 제한적이다. (GET, POST, PUT, DELETE, HEAD, ...)

 

* REST는 분산 시스템 설계를 위한 이키텍처 스타일이라고 했다.

마이크로서비스라는 말을 들어보았을 것이다. 이 쪽으로 질문이 연계될 수 있다.

RESTful API를 이용해서 하나의 큰 서비스 애플리케이션을 여러 모듈화된 작은 서비스 애플리케이션(마이크로 서비스)들로 나눌 수 있게 됐기 때문이다.

 

* REST를 공부하니까 URI와 URL의 차이점에 대해서도 이해할 수 있게되었다.

Uniform Resource Identifier, Uniform Resource Locator

REST에서는 모든 것을 Resource로 표현한다. 그리고 그 자원은 유일한 것을 나타낸다. Identifier, 식별자라는 것이다.

반면에 과거의 웹에서는 Identifier의 개념이 따로 필요없었다. html같은 파일들을 주고 받았기 때문에 파일의 위치를 가리키는 Locator를 썼다고 이해하면 된다.

URI가 파일뿐만 아니라 여러 자원들 까지도 포함하는 개념으로 이해할 수 있다.

 

* 자세한 명세를 알고 싶은 사람은 마이크로소프트에서 발표한 REST 가이드라인을 보면 좋을 것이다.

https://github.com/Microsoft/api-guidelines/blob/vNext/Guidelines.md

참고 사이트

https://www.youtube.com/watch?v=RP_f5dMoHFc

https://spring.io/understanding/REST



출처: https://jeong-pro.tistory.com/180?category=793347 [기본기를 쌓는 정아마추어 코딩블로그]

제네릭(Generic)

제네릭 타입을 이용해서 컴파일 과정에서 타입 체크를 할 수 있다.

제네릭은 클래스와 인터페이스, 메소드를 정의할 때 타입 파라미터로 사용한다.

* 제네릭을 사용하는 이유(=장점)

1. 컴파일할 때 타입을 체크해서 에러를 사전에 잡을 수 있다.

2. 컴파일러가 타입캐스팅을 해주기 때문에 개발자가 편리하다.

3. 타입만 다르고 코드의 내용이 대부분 일치할 때, 코드의 재사용성이 좋아진다.


1
2
3
public class className<T>{...}


public interface interfaceName<T>{...}

* 클래스, 인터페이스에서 제네릭 타입파라미터 사용법

실제 사용할 시에는 타입 파라미터에 구체적인 타입을 지정해야 함.

1 ArrayList<String> list = new ArrayList<String>();

ArrayList에 String 값만 저장할 수 있도록 구체적인 타입을 지정했음.

* 멀티 타입 파라미터

1 HashMap<String,Object> map = new HashMap<String,Object>();

제네릭 타입을 2개 이상 만들 수 있음.

해시맵도 <K,V>로 제네릭 타입을 사용하고 있음.

* 제네릭 메소드

매개변수 타입과 리턴 타입으로 타입파라미터를 갖는 메소드.

제네릭 클래스가 아니여도 메소드만 제네릭 메소드일 수 있음.

1 public <T> Product<T> boxing(T t){...}

* 제한된 타입 파라미터

<T extends 타입>

타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 있을 때가 있다.

ex) 숫자 연산 제네릭 메소드는 Byte, Short, Integer, Long, Double 인스턴스만 가져야 한다. 이럴 때 Number타입으로 제한한다. <T extends Number>

Number타입 또는 Number타입의 하위 클래스 타입만 올 수 있음.

<T super 타입> 으로 상위클래스도 표현가능하지만 <T implements 타입>같은 표현은 없음.

만약 인터페이스로 제한하고 싶다할지라도 "extends" 를 사용해야 함.



출처: https://jeong-pro.tistory.com/100?category=793347 [기본기를 쌓는 정아마추어 코딩블로그]

자바 컬렉션(Java Collections)

JAVA에서 대용량의 데이터를 추가/삭제하면서 처리가 필요할 때 자바 컬렉션을 사용한다.

Vector : java 1.0부터 이어져온 List객체, ArrayList가 상위호환(?)이라 잘 안쓴다. 특히 쓰레드의 개수와 상관없이 동기화(synchronize) 처리를 하므로 Thread-safe 하지만 싱글쓰레드 환경이어도 동기화처리를 하므로 성능이 좋지 않아 쓰이지 않는다.

ArrayList : Vector와 같은 추가/삭제 기능을 가지고 있고 자동 동기화처리가 되지 않기 때문에 빠르게 처리 가능,

대신 내부적으로 배열(array) 구조를 이용하기때문에 데이터 추가/제거를 배열을 복사하는 방법으로 처리하기 때문에 추가/제거가 많을 경우 오버헤드가 많이 발생함. 특히 중간에 삽입될 때 데이터들이 뒤로 밀리면서 성능저하가 큼.

대신! 인덱스를 가지로 있어서 조회할 때 한 번에 접근이 가능하기 때문에 대용량 데이터를 한 번에 가져와서 여러번 참조해 사용할 때 최상의 성능을 내는 객체다. (+크기 조절이 마음대로..)

Collections.synchronizedList, Collections.synchronizedMap, Collections.synchronizedCollection 등으로 동기화처리가 되는 컬렉션 생성하면 멀티쓰레드환경에서 쓸 수 있음.

LinkedList : C언어의 연결리스트처럼 다음 자료의 정보만 가지고 있고 내부적으로 인덱스는 없는 컬렉션

(양쪽으로 연결된 리스트를 만들 수도 있다)

연결된 자료의 정보만 담고 있기 때문에 중간 노드에 추가/삭제시 다른 데이터의 위치를 변경시킬 필요없이 간단하게 추가/삭제할 수 있다.

따라서 추가/삭제가 빈번하게 일어나는 대용량 데이터 처리가 필요할 때 사용하면 성능이 좋다.

대신 어디에 어떤 데이터가 있는지는 첫 노드부터 정보를 타고가면서 찾아야 하므로 검색에 있어서 성능이 좋지 않다. (ArrayList와 장단점이 반대다.)

스택, 큐, 양방향 큐등을 만들 때 사용한다.

여기서 질문!

-> 랜덤액세스할 때 ArrayList와 LinkedList 중에 어떤 것이 유리한가?

이에 대답은 ArrayList다. 랜덤 액세스 말 그대로 임의의 값에 접근하는데 ArrayList는 인덱스(0,1,2,...)로 한 번에 접근하기 때문에 유리하다. 반면 노드를 head나 tail부터 이동하며 접근하는 LinkedList는 성능상 불리하다.

-> 만약 추가/삭제도 많고 조회도 많으면 어떤 자료를 써야하나요?

여기서는 정답은 필자도 모른다.

다만 필자의 의견은 둘 중에 굳이 고르자면 ArrayList를 고른다 또는 다른 자료구조(ex. HashMap)을 사용하는 것이다.

첫 번째 근거는 LinkedList가 삽입/삭제에서 이 점을 갖는다고 했는데 결국은 어떤 값을 삭제할 때 존재하는지 확인해야한다는 것이다.

즉, 조회를 해야 삭제가 가능한 것이 아닌가 하는 의문이다. 정확하게 소스를 까서 보면 알 수 있겠지만 정확한 데이터는 아니니까 오해는 안했으면 한다. (개인 의견)

두 번째 근거는 테스트에 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
ArrayList<Integer> arrayList = new ArrayList<Integer>();
LinkedList<Integer> linkedList = new LinkedList<Integer>();


// ArrayList add
long startTime = System.nanoTime();


for (int i = 0; i < 100000; i++) {
    arrayList.add(i); 
}
long endTime = System.nanoTime();
long duration = endTime - startTime;
System.out.println("ArrayList add:  " + duration);


// LinkedList add
startTime = System.nanoTime();


for (int i = 0; i < 100000; i++) {
    linkedList.add(i);
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("LinkedList add: " + duration);


// ArrayList get
startTime = System.nanoTime();


for (int i = 0; i < 10000; i++) {
    arrayList.get(i);
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("ArrayList get:  " + duration);


// LinkedList get
startTime = System.nanoTime();


for (int i = 0; i < 10000; i++) {
    linkedList.get(i);
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("LinkedList get: " + duration);
// ArrayList remove
startTime = System.nanoTime();


for (int i = 9999; i >=0; i--) {
    arrayList.remove(i);
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("ArrayList remove:  " + duration);


// LinkedList remove
startTime = System.nanoTime();


for (int i = 9999; i >=0; i--) {
    linkedList.remove(i);
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("LinkedList remove: " + duration);
cs

위의 테스트소스 출처는 https://www.programcreek.com/2013/03/arraylist-vs-linkedlist-vs-vector/ 이다.

결과는 아래와 같다! (openjdk11, 메모리8GB, i5-6200U, win10 기준)

나노초로 찍었으니까 밀리로 환산하면 아래와 같다.

ArrayList add = 약 6ms , LinkedList add = 약 3ms

ArrayList get = 약 0.01ms , LinkedList get = 약 157ms

ArrayList remove = 약 135ms, LinkedList remove = 약 126ms

결과적으로 조회에서는 ArrayList가 압도하지만 취약하다던 add, remove에서는 크게 차이나지 않는다!

정리

위의 Vector, ArrayList, LinkedList는 List인터페이스의 구현 클래스다.

공통점 : 순서가 있는 데이터 집합이고 데이터 중복을 허용한다.

Vector : 과거에 사용하던 대용량 처리 컬렉션, 잘 사용 안하고 동기화처리가 자동적으로 내부에서 일어나므로 비교적 성능이 좋지 않고 무겁다.

ArrayList : 배열의 복사에 의한 데이터 저장처리를 내부적으로 수행하므로 많은 데이터의 추가/삭제시에는 성능이 떨어지나 각 데이터에 대한 인덱스를 가지고 있기 때문에 조회(검색)에 있어서 빠르다. (성능이 좋다)

특히 순차 접근이 필요할 때 인덱스가 있으므로 유용하다!

LinkedList : 다음 자료의 위치 정보를 가지며, 인덱스는 가지고 있지 않다. 데이터의 추가/삭제는 다음 데이터 위치정보만 수정하면 되므로 많은 정보의 추가/삭제가 일어날 때 유용하다. 대신 데이터 조회(검색)시 처음 자료부터 찾아 나가야 하므로 느려지는 단점이 있다.

Set

순서가 없는 데이터 집합이고 데이터 중복을 허용하지 않는다.

Set은 ArrayList처럼 인덱스가 따로 존재하지 않아 iterator(반복자)를 사용해야한다.

LinkedList도 순서는 있지만 인덱스가 없으므로 iterator를 사용해야 함.

1
2
3
Set<String> set = ...;


Iterator<String> iterator = set.iterator();

HashSet : 빠른 접근속도를 가지고 있음 단, 순서를 알 수 없음

LinkedHashSet : 추가된 순서대로 접근 가능

TreeSet : 정렬방법을 지정할 수 있음

Map

키(key), 값(value)으로 구성된 데이터 집합으로 key는 중복이 불가하지만 value는 중복 허용.

key를 알고 있다면 get메서드로 값을 가져올 수 있으나 저장된 객체 모두를 가져오고 싶다면 keySet()으로 모든 키를 Set으로 가져온 후 iterator를 통해 get메서드를 부르는 방법이 있음

1
2
3
4
5
6
7
8
9
Map<K, V> map = ...;


Set<K> keySet = map.keySet();
Iterator<K> it = keySet.iterator();


while (it.hasNext()) {
    K key = it.next();
    V value = map.get(key);
}
Colored by Color Scripter

HashMap : 중복X, 순서X, null허용

HashTable : HashMap보다 느리지만 동기화 지원, null 불가

TreeMap : 정렬된 순서대로 저장되어 검색은 빠르지만, 요소 추가/삭제시 성능이 좋지 않음



출처: https://jeong-pro.tistory.com/87?category=793347 [기본기를 쌓는 정아마추어 코딩블로그]

웹에서 쿠키와 세션

쿠키와 세션을 사용하는 이유

→ HTTP 프로토콜의 특징이자 약점을 보완하기 위해서 사용한다.

HTTP 프로토콜의 특징

  1. 비연결지향(Connectionless)
    • HTTP는 클라이언트가 요청(Request)을 서버에 보내고, 서버는 클라이언트에게 적절한 응답(Response)을 주고 연결(Connection)을 끊는 특성이 있다.
      • HTTP1.1 버전에서는 커넥션을 계속 유지하고 요청(Request)에 재활용하는 기능이 추가되었다. (HTTP Header에 keep-alive 옵션을 주어 커넥션을 재활용하게 한다. HTTP1.1 버전에서는 디폴트(default)옵션이다.
    • HTTP가 TCP위에서 구현되었기 때문에(TCP는 연결지향, UDP는 비연결지향) 연결지향적이라고 할 수 있다는 얘기가 있어 논란이 있지만, 아직까지는 네트워크 관점에서 keep-alive는 옵션으로 두고, 서버측에서 비연결지향적인 특성으로 커넥션 관리에 대한 비용을 줄이는 것이 명확한 장점으로 보기 때문에 비연결지향으로 알아두었다.)
  2. 상태없음(Stateless)
    • 커넥션을 끊는 순간 클라이언트와 서버의 통신이 끝나며 상태 정보는 유지하지 않는 특성이 있다.

HTTP는 이 두 가지 특성을 보완하기 위해서 쿠키  세션 을 사용하게 되었다.

비연결지향이라는 특성 덕분에 계속해서 커넥션을 유지하지 않기 때문에 서버 리소스 낭비가 줄어드는 것은 아주 큰 장점이지만, 통신할 때마다 새로 커넥션을 만들기 때문에 클라이언트 측면에서는 상태를 유지(ex. 인증에 쓰이는 상태, ...)를 위해 통신할 때마다 어떤 절차를 가져야하는 단점이 생긴다.

심각하게 말하면 만약 쿠키와 세션이 없다면 어떤 페이지에서 다른 어떤 페이지로 넘어갈 때마다 인증을 다시 받아야하는 것이다.

쿠키(Cookie)

쿠키는 클라이언트 로컬(local)에 저장되는 키와 값(key, value)이 들어있는 작은 데이터 파일이다.

쿠키는 서버에서 HTTP Response Header에 Set-Cookie 속성을 이용하여 클라이언트에 쿠키를 제공한다.

쿠키에는 이름, 값, 만료 날짜/시간(쿠키 저장기간), 경로 정보등이 들어있다.

쿠키는 클라이언트의 상태 정보를 로컬에 저장했다가 요청(Request)할 때 참조된다.

쿠키는 서버측에서 만료 날짜/시간을 지정하여 정해진 시간동안 데이터(상태정보)를 유지할 수 있다. (로그인 상태 유지에 활용된다)

세션쿠키(Session Cookie)와 지속 쿠키(Persistent Cookie)

쿠키는 세션 쿠키(Session Cookie)  지속 쿠키(Persistent Cookie) 로 나뉜다.

만료 날짜/시간을 지정하지 않으면 항상 유지하라는 것으로 판단하고 지속 쿠키에 저장되고, 만료 날짜/시간을 지정하면 세션 쿠키로 저장된다.

세션 쿠키는 브라우저 메모리에 저장되므로 브라우저가 종료되면 쿠키는 사라지게 된다.

지속 쿠키는 파일로 저장되므로 브라우저가 종료되어도 쿠키는 남아있게 된다.

→ 참고로 세션 쿠키의 값은 보안상 꽤나 안전한 브라우저(ex. 구글 크롬)의 메모리에 저장되기 때문에 보안에 유리하지만 파일로 저장되는 지속 쿠키의 경우 비교적으로 보안에 취약하다.

  • 쿠키 프로세스
    1. 브라우저에서 웹페이지에 접속한다.
    2. 클라이언트가 요청한 웹페이지를 응답으로 받으면서 HTTP 헤더를 통해 해당 서버에서 제공하는 쿠키 값을 응답으로 준다. (이러면 클라이언트는 해당 쿠키를 저장한다.)
    3. 클라이언트가 웹페이지를 요청한 서버에 재 요청시 받았던 쿠키 정보도 같이 HTTP 헤더에 담아서 요청한다.
    4. 서버는 클라이언트의 요청(Request)에서 쿠키 값을 참고하여 비즈니스 로직을 수행한다. (ex 로그인 상태 유지, ...)
  • 쿠키 사용 사례
    • 자동로그인, 팝업에서 "오늘 더 이상 이 창을 보지 않음" 체크, 쇼핑몰의 장바구니, ...
  • 쿠키의 한계
    • 클라이언트에 최대 300개 까지 쿠키를 저장할 수 있다.
    • 서버 도메인 하나당 최대 20개의 쿠키를 저장할 수 있다.
    • 하나의 쿠키 값은 최대 4KB까지 저장할 수 있다.

→ 쿠키는 사용자가 별도로 요청하지 않아도 브라우저(Client)에서 서버에 요청(Request) 시에 Request Header에 쿠키 값을 넣어 요청한다. (=자동이다.)

그렇다고 그 많은 쿠키 값을 굳이 모든 요청에 넣어서 비효율적으로 동작하지는 않는다. 도메인 설정을 통해서 지정한 도메인으로 요청할 때만 쿠키 값이 제공되도록 할 수도 있다.

세션(Session)

서버(Server)에 클라이언트의 상태 정보를 저장하는 기술로 논리적인 연결을 세션이라고 한다.

웹 서버에 클라이언트에 대한 정보를 저장하고 클라이언트에게는 클라이언트를 구분할 수 있는 ID를 부여하는데 이것을 세션아이디라 한다.

  • 세션 프로세스
    1. 클라이언트가 서버에 요청했을 때, 필요에 따라 세션에 클라이언트에 대한 데이터를 저장하고 세션 아이디를 응답을 통해 발급해준다. (브라우저 단에서 관리될 수 있도록 쿠키로 발급하는게 일반적인 구조)
    2. 클라이언트는 발급받은 세션 아이디를 쿠키로 저장한다. (ex. JSESSIONID)
    3. 클라이언트는 다시 서버에 요청할 때, 세션 아이디를 서버에 전달하여 상태 정보를 서버가 활용할 수 있도록 해준다.

결과적으로 세션을 통해 클라이언트의 정보는 서버에 두고, 세션 아이디를 이용해서 인증받고 정보를 이용하는 방식이다.

  • 세션 사용 사례
    • 로그인 정보 유지

쿠키와 세션의 차이

  • 저장 위치
    • 쿠키는 클라이언트(브라우저)에 메모리 또는 파일에 저장하고, 세션은 서버 메모리에 저장된다.
  • 보안
    • 쿠키는 클라이언트 로컬(local)에 저장되기도 하고 특히 파일로 저장되는 경우 탈취, 변조될 위험이 있고, Request/Response에서 스나이핑 당할 위험이 있어 보안이 비교적 취약하다. 반대로 Session은 클라이언트 정보 자체는 서버에 저장되어 있으므로 비교적 안전하다.
  • 라이프 사이클
    • 쿠키는 앞서 설명한 지속 쿠키의 경우에 브라우저를 종료하더라도 저장되어 있을 수 있는 반면에 세션은 서버에서 만료시간/날짜를 정해서 지워버릴 수 있기도 하고 세션 쿠키에 세션 아이디를 정한 경우, 브라우저 종료시 세션아이디가 날아갈 수 있다.
  • 속도
    • 쿠키에 정보가 있기 때문에 쿠키에 정보가 있기 때문에 서버에 요청시 헤더를 바로 참조하면 되므로 속도에서 유리하지만, 세션은 제공받은 세션아이디(Key)를 이용해서 서버에서 다시 데이터를 참조해야하므로 속도가 비교적 느릴 수 있다.

세션을 주로 사용하면 좋은데 왜 굳이 쿠키를 사용할까?

→ 세션은 서버에 데이터를 저장 즉, 서버의 자원을 사용하기 때문에 서버 자원에 한계가 있고 메모리를 사용하다보면 속도 저하도 올 수 있기 때문이다.

쿠키, 세션은 캐시와 엄연히 다르다

브라우저 캐시는 이미지(.png, .jpg, ...)나 .css, .js파일 등이 사용자의 브라우저에 저장이 되는 것이다.

이를 이용해 같은 자원을 로드(load)해야할 때, 해당 자원을 다시 불러오지 않고 캐시되어 있는 자원을 써서 클라이언트 자원을 아끼는 것이다.

해당 자원이 한 번 브라우저 캐시에 저장되면 다음 필요시에도 브라우저에 있는 걸 재사용하기 때문에 경우에 따라 해당 자원이 변경되어도 변경된 자원을 참조할 수 없는 경우가 생길 수 있다.

따라서 사용자는 브라우저 캐시를 지워주거나 서버에서 클라이언트로 응답을 보낼 때 header에 자원 캐시 만료시간을 명시하는 방법등을 이용하여 캐시를 회피할 수 있다.

보통 쿠키와 세션의 차이를 물어볼 때, 저장위치나 보안에 대해서는 대체적으로 대답을 잘 한다.

그런데 라이프 사이클에 대해서 얘기하지 않는 경우는 많은데 이런 것까지 디테일하게 대답해주는게 좋지 않을까한다.

추가적으로 이론적인 내용뿐만 아니라 실제 사용해보고 사례를 언급하면 더 좋지 않을까한다.

참고 사이트

http://88240.tistory.com/190

http://interconnection.tistory.com/74



출처: https://jeong-pro.tistory.com/80?category=793347 [기본기를 쌓는 정아마추어 코딩블로그]

객체 지향 프로그래밍(Object Oriented Programming)

여러 SW기업 신입사원 기술면접에서 워밍업느낌으로 면접자들 긴장을 풀어줄 겸 처음으로 자주 나오는 질문이다.

"객체 지향 프로그래밍에 대해 설명 한번 해주세요" 

가장 기본이면서 이것마저 대답을 명확하게 못하면 첫인상이 안 좋아지는(?) 결과를 만드는 질문이다.

그리고 워밍업같지만 꼬리에 꼬리를 무는 모든 질문의 시작(?)이다.


객체 지향 프로그래밍(OOP)이 뭐에요?

객체 지향 프로그래밍은 컴퓨터 프로그래밍 패러다임중 하나로, 프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법이다.

이러면 이제 아까 말했던 꼬리에 꼬리를 무는 질문이 시작된다.

"객체 지향 프로그래밍을 했을 때 장점이 뭐에요?"

"객체 지향 프로그래밍의 특징을 말씀해주세요"

"객체(또는 클래스)가 뭐에요?"

=> 결국 객체 지향 키워드 5가지와 관련된 내용과 장단점을 알고 있는지에 대한 질문이다.

(객체지향의 5원칙(SOLID)은 아님)

객체 지향 프로그래밍의 장, 단점 간단하게 설명해주세요

- 장점

코드 재사용이 용이

남이 만든 클래스를 가져와서 이용할 수 있고 상속을 통해 확장해서 사용할 수 있음.

유지보수가 쉬움

절차 지향 프로그래밍에서는 코드를 수정해야할 때 일일이 찾아 수정해야하는 반면 객체 지향 프로그래밍에서는 수정해야 할 부분이 클래스 내부에 멤버 변수혹은 메서드로 있기 때문에 해당 부분만 수정하면 됨. 

대형 프로젝트에 적합

클래스단위로 모듈화시켜서 개발할 수 있으므로 대형 프로젝트처럼 여러명, 여러회사에서 개발이 필요할 시 업무 분담하기 쉽다.

 

- 단점

처리속도가 상대적으로 느림

객체가 많으면 용량이 커질 수 있음

설계시 많은 시간과 노력이 필요

객체 지향 프로그래밍 키워드 5가지

1) 클래스 + 인스턴스(객체)

2) 추상화

3) 캡슐화

4) 상속

5) 다형성


클래스와 인스턴스(객체)는 무엇인지 설명해주세요.

클래스 : 어떤 문제를 해결하기 위한 데이터를 만들기기 위해 추상화를 거쳐 집단에 속하는 속성(attribute)과 행위(behavior)를 변수 메서드로 정의한 것

인스턴스(객체) : 클래스에서 정의한 것을 토대로 실제 메모리상에 할당된 것으로 실제 프로그램에서 사용되는 데이터

객체지향프로그래밍에서 추상화 (자료의 추상화)

불필요한 정보는 숨기고 중요한 정보만을 표현함으로써 공통의 속성이나 기능을 묶어 이름을 붙이는 것이다.

(= 객체지향 관점에서 클래스를 정의하는 것.)

(!= abstract 클래스, abstract 메서드와는 다른 이야기임)

캡슐화가 무엇인가요?

캡슐화의 목적 : 코드를 재수정 없이 재활용하는 것.

프로그램 코드에서 변수와 함수를 재활용하기에는 분산되어 있기 때문에 재활용이 어려웠으나 캡슐화를 통해 관련된 기능과 특성을 한 곳에 모으고 분류하기 때문에 재활용이 원활해졌다.

객체 지향 프로그래밍에서 기능과 특성의 모음을 "클래스"라는 "캡슐"에 분류해서 넣는것이 캡슐화다.

객체가 맡은 역할을 수행하기 위한 하나의 목적을 한데 묶는다.

상속은 무엇인가요?

절자 지향 프로그래밍에서도 "라이브러리"를 통해서 남이 짜놓은 소스 코드를 가져와 사용할 수 있었다.

하지만 내 의도에 맞게 수정하게되면 다른 라이브러리가 되어 버전에 따라 동작하지 않을 수 있고 불필요한 코드의 수정작업을 해야한다는 것이다.

이런 문제를 해결하기 위해 [상속]이라는 것을 도입하였다.

상속 부모클래스의 속성과 기능을 그대로 이어받아 사용할 수 있게하고 기능의 일부분을 변경해야 할 경우 상속받은 자식클래스에서 해당 기능만 다시 수정(정의)하여 사용할 수 있게 하는 것이다.

* 다중상속은 불가하다. (클래스의 상속 관계에서 혼란을 줄 수 있기 때문에 상속은 반드시 하나만 가능하고 필요에 따라 인터페이스를 사용할 수 있게 했다. 자세한 내용은 이전 포스트에 있음.)


다형성은 무엇인가요?

하나의 변수명, 함수명 등이 상황에 따라 다른 의미로 해석될 수 있는 것이다.

즉 오버라이딩(Overriding), 오버로딩(Overloading)이 가능하다는 얘기다.

오버라이딩 : 부모클래스의 메서드와 같은 이름, 매개변수를 재정의 하는것.

오버로딩 : 같은 이름의 함수를 여러개 정의하고, 매개변수의 타입과 개수를 다르게 하여 매개변수에 따라 다르게 호출할 수 있게 하는 것.

getter, setter 를 사용하는 이유는 무엇인가요?

멤버변수에 직접접근하지 못하게 private으로 접근지정자를 설정하고 public으로 getter, setter 메서드를 만드는 것을 많이 해왔다.

그러면서 이럴꺼면 어차피 아무나 접근가능한데 왜 private을 할까? 라고 생각했었다.

결론부터 말하면 getter, setter를 사용하면 메서드를 통해서 접근하기 때문에, 메서드 안에서 매개변수같이 어떤 올바르지 않은 입력에 대해 사전에 처리할 수 있게 제한하거나 조절할 수 있기 때문이다.

예를들면 setter에서 유효범위를 넘은 정수가 들어왔을 때의 처리를 하고나서 set하거나 예외처리를 해버릴 수 있는 것이다.

getter도 마찬가지로 굳이 예를들자면 자료에 무언가 더하거나 빼고 준다든지가 가능하다.



출처: https://jeong-pro.tistory.com/95?category=793347 [기본기를 쌓는 정아마추어 코딩블로그]

+ Recent posts