2017년 10월 29일 일요일

토비의 스프링 공부/정리 1장


회사에서 토비의 스프링 스터디를 시작하였다.
새로운 기분으로 정독하고 있으며, 정리한 내용을 학습목적으로 git에 올리고 있다.
이해가 가지 않는 내용도 있지만, 일단 1회독을 마치는 것을 목표로 진행하자!

https://github.com/cotgyu/toby_springstudy


1. 오브젝트와 의존관계

스프링 프레임워크에서 가장 중요한 것은 오브젝트(생성, 다른 오브젝트와의 관계, 사용, 소멸)이라 설명하면서, 스프링은 오브젝트를 어떻게 효과적으로 설계하고 구현, 사용 , 어떻게 개선해야하는 가의 기준을 마련해준다고 한다.
객체지향 기술, 설계, 구현, 검증된 프렉티스를 프레임워크로 제공

어떠한 변경이 일어날 때, 필요한 작업을 최소화하고 다른 곳에 영향이 가지 않게 하는 것이 중요한데, 이를 분리와 확장을 고려한 설계라 한다. 이 분리의 개념 중 하나인 관심사의 분리를 책에서 단계별 예제를 통해 소개하고 있다.
관심사의 분리 - 관심이 같은 것은 하나의 객체 안으로, 관심이 다른 것은 가능한 떨어뜨려 서로 영향이 가지 않게 분리

책에서는 간단한 사용자를 등록하는 UserDao를 제시하였는데, 여기에는 3가지 관심사가 있다.
  1. DB연결을 위한 커넥션
  2. SQL을 실행하는 statement
  3. statement, 커넥션 닫기
1장에서는 1번을 분리와 확장을 고려하여 개선하는 작업을 수행하였다.
첫번째로 이 커넥션 부분을 getConnention 이라는 함수로 빼내었고, 이를 통해 분리하였다. (메소드 추출 기법)
두번째로는 커넥션 확장을 위해 상속을 사용하였다. UserDao를 상속하고 서브클래스에서 추상메소드인 getConnention을 구현하여 DB커넥션 연결이라는 관심을 서브클래스로 분리하여 각자 원하는 구성의 getConnention을 사용하는 소개하고있다.
템플릿 메소드 패턴 - 슈퍼클래스의 기본적인 로직흐름을 만들고, 그 기능의 일부를 추상메소드나 오버라이딩이 가능한 메소드 등 
으로 만든 뒤 서브클래스에서 필요에 맞게 구현, 사용하는 방법

팩토리 메소드 패턴 - 상속을 통해 기능을 확장하는 패턴으로 서브클래스에서 구체적인 오브젝트 생성방법을 결정한다.
하지만 이 상속을 이용한 방법도 슈퍼클래스 내부의 변경이 있을 때 모든 서브클래스를 함게 수정하거나 다시 개발할 수 있는 단점이 있음.(생각보다 밀접한 관계)
그래서 책에서는 세번째로 상속이 아닌 인터페이스를 통해 클래스를 분리하는 것을 소개한다. 이를 통해 UserDao는 인터페이스의 메소드를 통해 알 수 있는 기능에만 관심을 가지면 되지, 그 기능을 어떻게 구현했는지에는 관심을 둘 필요가 없어진다. (최종족으로 UserDao는 ConnentionMaker라는 인터페이스 외에는 어떤 클래스와 관계를 갖지 않게 개선하였다.)

책에서는 이 UserDao를 개선해온 결과를 객체지향 기술의 여러가지 이론을 통해 설명하였다.
개방 폐쇄 원칙(OCP) - 클래스나 모듈은 확장에는 열려 있어야하고 변경에는 닫혀있어야한다. (객체지향설계원칙)

높은 응집도 - 하나의 모듈이나 클래스가 하나의 책임 또는 관심사에 집중되어 있다.
낮은 응집도 - 하나의 오브젝트가 변경이 발생할 때, 관계를 맺고있는 다른 오브젝트에 변경을 최소화한다.

전략패턴 - 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 
클래스를 팔요에 따라 바꿔서 사용할 수 있게 하는 디자인 패턴  
ex) DB연결방식(ConnentionMaker) - 알고리즘
스프링은 이러한 객체지향적 설계원칙과 디자인 패턴에 나타난 장점을 자연스럽게 개발자들이 활용할 수 있게 해주는 프레임워크라고 소개하고 있다.

책에서는 어떤 ConnectioNmaker를 사용할지 결정하는 기능을 UserDaoTest가 맡고 있는데, 이를 분리하면서 팩토리의 개념을 설명하고 있다.
(DaoFactory를 통해 UserDao타입의 오브젝트를 어떻게 만들고, 어떻게 준비하는지를 결정하도록 개선하였다.)
팩토리 - 객체의 생성방법을 결정하고 만들어진 오브젝트를 돌려줌.

이를 통해 책에서는 기존의 Dao는 모든 종류의 작업을 사용하는 쪽에서 제어하는 반면, 개선된 Dao는 ConnectionMaker의 구현 클래스 결정과 오브젝트의 생성의 제어권이 DaoFactory에 있음을 비교하며 IoC를 설명하고 있다.
 IoC(제어의 역전) - 객체에 대한 제어권이 개발자로 부터 컨테이너에 넘어가면서 객체의 생성부터 생명주기 관리까지의 
 모든 것을 컨테이너가 맡아서 함
스프링은 IoC를 극적으로 적용하고 있는 프레임워크

스프링의 IoC
빈(bean): 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트
(스프링 빈 - 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트)

빈 팩토리: 빈의 생성과 관계설정 같은 제어를 담당하는 IoC오브젝트
(어플리케이션 컨텍스트 - 별도의 정보를 참고해 빈의 생성, 관계 설정 등 제어작업 총괄)

어노테이션
@Configuration - 스프링이 오브젝트 설정을 담당하는 클래스로 인식
(어플리케이션 컨텍스트가 사용할 설정 정보)
@bean: 오브젝트 생성 담당

Application Context: 어플리케이션에서 IoC를 적용해서 관리할 모든 오브젝트에 대한 생성과 관계설정을 담당
- 오브젝트 팩토리가 아무리 많아도 이를 알아야하거나 직접 사용X
- 종합 IoC서비스를 제공한다.
- 빈을 검색하는 다양한 방법을 제공한다. (이름, 타입, 어노테이션)

책에서는 DaoFactory와 어플리케이션컨텍스트를 사용해서 UserDao를 호출 시 차이점을 보여주면서 싱글톤을 설명하고 있다.
DaoFactory를 직접 사용해서 UserDao를 여러번 호출 시 다른 오브젝트가 생성되지만, 애플리케이션 컨텍스트를 이용하면 
동일한 오브젝트가 생성되는 것을 볼 수 있는데(싱글톤 레지스트리),
이는 스프링이 주로 적용되는 대상이 자바 엔터프라이즈 기술을 사용하는 서버환경이기 때문이라고 설명하고 있다.
(서버 환경에서는 싱글토의 사용이 권장됨)

또한 자바의 기본 싱글톤 패턴 구현은 여러 단점이 있기 때문에 스프링은 직접 싱글톤 형태의 오브젝트를 관리하는 기능을 제공한다.

스프링 빈 스코프: 빈이 생성되고 존재하고, 적용되는 범위
 - 기본은 싱글톤(컨테이너 내 한개, 컨테이너가 존재하는 한 유지)
 - 그 외 프로토 타입, 요청, 세션 등 있음(10장에서 자세한 설명)

IoC의 대표적인 동작원리 - DI
DI(의존관계 주입)
- 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘겨 자신은 수동적으로 주입받은 오브젝트를 사용
- 어떤 오브젝트가 이용할 오브젝트를 프로퍼티에 설정
- 객체의 실체를 외부환경설정(xml, 어노테이션 등)에서 컨트롤 할 수 있도록 하는 것
DI(의존관계 주입)를 통해 모듈간 결합도를 낮춰서 유연한 변경이 가능해진다.

스프링은 DaoFactory와 같은 자바클래스 외에도 다양한 방법들을 통해 DI의존관계 설정 정보를 만들 수 있는데, 책에서는 대표적으로 XML을 이용한 설정을 소개하고 있다.
장점
- XML은 텍스트파일이기 때문에 다루기 쉽다.
- 쉽게 이해 할 수 있다.
- 컴파일과 같은 별도의 빌드 작업이 필요 없다.
- 스키마나 DTD를 이용해 정해진 포멧에 따라 작성되었는지 확인 가능하다.

@Configuration -> <beans>
@bean -> <bean>
스프링이란 '어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크' 라는 사실을 기억하자.
스프링의 관심은 오브젝트와 그 관계다.






//////////////////////////////////////////////////////////////////////////////////////////////






도서관에서 책을 빌렸다.
차근차근 정리해보자!!


1. 오브젝트와 의존관계

1장에서는 스프링이 어떤 것이고, 무엇이을 제공하는지보다는 오브젝트의 설계과 구현, 동작원리에 더 집중해보도록 하자.
스프링을 이해하려면 오브젝트에 깊은 관심을 가져야한다.  이러한 오브젝트에 대한 관심은 오브젝트의 기술적인 특징과 사용 방법을 넘어서 오브젝트의 설계로 발전한다. 

**책에서는 간단하고 문제가 많은 DAO코드를 예시를 주고 이를 계속 개선, 수정하면서 그에 해당하는 개념을 설명하는 방식으로 설명하는 듯 싶음

1.1 초난감 DAO

DAO(Data Access Object)란?

DB를 사용해 데이터를 조회하거나 조작하는 기능을 전담하도록 만든 오브젝트

자바빈(JavaBean)이란?

자바빈은 원래 비주얼 툴에서 조작 가능한 컴포넌트를 말한다. 자바의 주력 개발 플랫폼이 웹 기반의 엔터프라이즈 방식으로 바뀌면서 비주얼 컴포넌트로서 자바빈은 인기를 잃어갔지만 자바빈의 몇 가지 코딩 관례는 JSP 빈, EJB와 같은 표준 기술과 자바빈 스타일의 오브젝트를 사용하는 오픈소스 기술을 통해 게속 이어져 왔다. 이제는 자바빈이라고 말하면 비주얼 컴포넌트라기 보다는 다음 두 가지 관례를 따라 만들어진 오브젝트를 가리킨다. 간단히 빈이라고 부르기도 한다.
-디폴트 생성자: 자바빈은 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다. 툴이나 프레임워크에서 리플렉션을 이용해 오브젝트를 생성하기 때문에 필요하다.
-프로퍼티: 자바빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. 프로퍼티는 set으로 시작하는 수정자 메소드(setter)와 get으로 시작하는 접근자 메소드(getter)를 이용해 수정 또는 조회할 수있다. 

1.2 DAO의 분리

프로그래밍의 기초 개념 중에 관심사의 분리(Separation of Concerns)이라는 게 있다. 이를 객체지향에 적용해보면, 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모이게하고, 관심이 다른 것은 가능한 한 따로 떨어져서 서로 영향을 주지 않도록 분리하여는 것이라 생각할 수 있다. 

리펙토링이란?

기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술을 말한다. 리펙토링을 하면 코드 내부의 설계가 개선되어 코드를 이해하기가 더 편해지고, 변화에 효율적으로 대응할 수 있다. 결국 생산성은 오라가고, 코드의 품질은 높아지며, 유지보수하기 용이해지고, 견고하면서도 유연한 제품을 개발할 수 있다. 대표적으로 중복된 코드 제거가 있다.

템플릿 메소드 패턴이란?

상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다. 변하지 않는 기능을 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 한다. 슈퍼클래스에서는 미리 추상 메소드 또는 오버라이드 가능한 메소드를 정의해두고 이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메소드를 만든다. 슈퍼클래스에서 디폴드 키능을 정의해두거나 비워뒀다가 서브클래스에서 선택적으로 오버라이드할 수 있도록 만들어둔 메소드를 훅 메소드라고 한다. 서브클래스에서는 추상 메소드를 구현하고나, 훅 메소드를 우버라이드하는 방법을 이용해 기능의 일부를 확장한다. 


펙토리 메소드 패턴이란?

펙토리 메소드 패턴도 템플릿메소드 패턴과 마찬가지로 상속을 통해 기능을 확장하게 하는 패턴이다. 그래서 구조도 비슷하다. 슈퍼클래스 코드에서도 서브클래스에서 구현할 메소드를 호출해서 필요한 타입의 오브젝트를 가져와 사용한다. 이 메소드는 주로 인터페이스 타입으로 오브젝트를 리턴하므로 서브클래스에서 정호가히 어떤 클래스의 오브젝트를 만들어 리턴할지는 슈퍼크래스에서는 알지 못한다. 사실 관심도 없다. 서브 클래스는 다양한 방법으로 오브젝트를 생성할 수 있도록 미리 정의해둔 메소드를 펙토리 메소드라고 하고, 이 방식을 통해 오브젝트 생성 방법을 나머지 로직, 즉 슈퍼클래스의 기본 코드에서 독립시키는 방법을 펙토리 메소드 패턴이라고한다, 자바에서는 종종 오브젝트를 생성하는 기능을 가진 메소드를 일반적으로 펙토리 메소드라고 부르기도 한다, 이때 말하는 펙토리 메소드와 펙토리 메소드 패턴의 펙토리 메소드는 의미가 다르므로 혼동하지 않도록 주의해야 한다. 
//이 부분 다시 정리 할 것 

>>이 템플릿 메소드 패턴이나 팩토리 메소드 패턴으로 관심사사힝 다른 코드를 분리하고 서로 독립적으로 변경, 확장할 수 있도록 만드는 것은 효과적이지만 상속을 사용했다는 단점이 있다.
>>상속 자체는 간단해 보이고 사용하기도 편리하게 느껴지지만 사실 많은 한계점이 있다.

>>자바는 다중상속을 지원하지 않기 때문에 상속 후 다른 목적으로 상속을 적용하기 힘들기 때문??


1.3 DAO의 확장

독립적인 클래스를 만들어 클래스를 분리하여 확장할 수 있다. 하지만 이런식으로 사용하면 다른 환경에 맞게 구현해서 쓸 수 있는 자유로운 확장이 불가능 하다. 
그렇기 때문에 두 개의 클래스가 서로 긴밀하게 연결되어 있지 않도록 중간에 추상적인 연결고리를 만들어주는 방법으로 해결할 수 있다. >> 인터페이스  

개방 폐쇄의 원칙이란?

깔끔한 설게를 위한 적용 가능한 객체지향 설계 원칙 중 하나로, '클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다.' 라는 의미이다. 잘 설계된 객체 지향 클래스의 구조를 살펴보면 이 원칙을 잘 지키고 있다.(인터페이스를 사용해 확장 기능을 정의한 대부분의 API)
>> 개방 폐쇄 원칙은 높은 응집도와 낮은 결합도라는 원리로도 설명 가능함.
높은 응집도란, 변경이 일어날 때 모듈의 많은 부분이 함께 바뀐다는 것이다. 만약 모듈의 일부분에만 변경이 일어나도 된다면, 모듈 전체에서 어떤 부분이 바뀌어야 하는지 파악해야 하고, 또 그변경으로 인해 바뀌지 않은 부분에는 다른 영향을 미치지 않는지 확인해야 하는 이중의 부담이 생긴다. 

낮은 결합도란, 관계를 유지하는 데 꼭 필요한 최소한의 방법만 간접적인 형태로 제공하고, 나머지는 서로 독립적이고 알 필요도 없게 만들어주는 것이다. 결합도가 낮아지면 변화에 대응하는 속도가 높아지고, 구성이 깔끔해진다. 또한 확장하기 편해짐
>>결국 하나의 변경이 발생할 때 모듈과 객체로 변경에 대한 요구가 전파되지 않은 상태

객체지향 설계 원칙(SOLID)이란?

오랜 시간 동안 객체지향 설계에 관한 여러가지 원리와 원칙을 체계적으로 잘 정리한 것으로, 로버트 마틴이 정리하였다. SOLID는 
SRP(The Single Responsibility Principle): 단일 책임 원칙
OCP(The Open Closed Principle): 개방 폐쇄 원칙
LSP(The Liskov Substitution Principle): 리스코프 치환 원칙
ISP(The Interface Segregation Principle): 인터페이스 분리 원칙
DIP(The Dependency Inversion Principle): 의존관계 역전 원칙

전락패턴이란?

자신의 기능 맥락에서, 필요에 따라 변경이 필요한 알고리즘을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘을 클래스를 필요에 따라 바꿔서 사용할 수 있게하는 디자인 패턴
>> 스프링은 지금까지 언급한 객체지향적 설계 원칙과 디자인 패턴에 나타난 장점을 자연스럽게 개발자들이 활용할 수 있게 해주는 프레임워크이다. 

1.4 제어와 역전(IoC, Inversion of Control)

IoC는 프로그램의 제어 흐름 구조가 뒤바뀌는 것이라고 설명할 수 있다.

//추가정리 필요


1.5 스프링의 IoC

빈(bean): 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트

빈펙토리(bean factory): 빈의 생성과 관계 설정과 같은 제어를 담당하는 IoC 오브젝트 보통 빈 팩토리보다는 이를 좀 더 확장한 애플리케이션 컨텍스트를 주로 사용함
(빈 팩토리라고 할때는 빈을 생성하고 관게를 설정하는 IoC의 기본 기능에 초점을 맞춘 것이고, 에플레케이션 컨텍스트라고 말할 때는 애플리케이션 전반에 걸쳐 모든 구성요소의 제어 작업을 담당하는 IoC 엔진이라는 의미가 좀 더 부각된다 보면 된다.)

애플리케이션 컨텍스트: 빈의 생성, 관계설정 등의 제어 작업을 총괄(IoC컨테이너라고도 하고, 스프링컨테이너, 빈 팩토리라고도 부름?)>>스프링의 가장 대표적인 오브젝트


애플리케이션 컨텍스트를 사용했을 때 얻을 수 있는 장점

클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다
>>오브젝트 텍토리가 아무리 많아져도 이를 알아야하거나 직접 사용할 필요 없고, 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.

애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.
>>오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수도 있고, 부가적으로 자동생성, 오브젝트에 대한 후처리, 정보의 조합, 설장방식의 다변화 ,인터셉팅 등 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다. 또한 빈이 사용할 수 있는 기반기술 서비스나 외부 시스템과의 연동 등을 컨테이너 차원에서 제공해주기도 한다.

애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공해준다.
>>getBean()메소드는 빈의 이름을 이용해 빈을 찾아준다. 타입믄으로 빈을 검색하거나 특별한 어노테이션 설정이 되어 있는 빈을 찾을 수도 있다.


스프링 IoC의 용어 정리

빈: 스프링이 IoC방식으로 관리하는 오브젝트. 주의할 점은 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 다 빈은 아니라는 사실이다. 그중에서 스프링이 직접 생성과 제어를 담당하는 오브젝트만을 빈이라 부른다.

빈 팩토리: 스프링의 IoC를 담당하는 핵심 컨테이너. 빈을 등록하고, 생성하고, 조회하고 돌려주고, 그 외 부가적인 빈을 관리하는 기능을 담당. 보통 바로 사용하지 않고 이를 확장한 애플리케이션 컨텍스트를 이용함. BeanFactory라고 붙여쓰면 빈 팩토리가 구현하고 있는 가장 기본적인 인터페이스의 이름이 됨. getBean()과 같은 메소드가 정의되어 있다.

애플리케이션 컨텍스트: 빈 팩토리를 확장한 IoC컨테이너. 빈을 등록하고 관리하는 기본적인 기능은 빈 팩토리와 동일. 여기에 스프링이 제공하는 각종 부가서비스를 추가로 제공함

설정정보/설정 메타정보(configuration metadata): 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타 정보를 말함. 실제로 스프링의 설정 정보는 컨테이너에 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만, 그보다는 IoC컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용됨. 애플리케이션 형상 정보라고 부르기도 함.

컨테이너 또는 IoC컨테이너: IoC방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC컨테이너 라고도 함.

스프링 프레임워크: IoC컨테이너, 애플리케이션 컨텍스트를 포함해서 스프링이 제공하는 모든 기능을 통틀어 말할 떄 주로 사용함.


1.6 싱글톤 레지스트리와 오브젝트 스코프 

오브젝트의 동일성과 동등성

동일성 ==, 하나의 오브젝트만 존재하는 것이고 두 개의 오브젝트 레퍼런스 변수를 가짐

동등성 equals(), 두개의 각기 다른 오브젝트가 메모리상에 존재, 오브젝트 동등성 기준에 따라 두 오브젝트의 정보가 동등하다고 판단되는 것


싱글톤 패턴(Singleton Pattern)이란?

GoF가 소개한 디자인 패턴중 하나로 가장 자주 활용되는 패턴이기도 하지만 가장 많은 비판을 받는 패턴이기도 함.
어떤클래스를 애플리케이션 내에서 제한된 인스턴스 개수, 이름처럼 주로 하나만 존재하도록 강제하는 패턴. 이렇게 하나만 만들어지는 클래스의 오브젝트는 애플리케이션 내에이서 전역적으로 접근이 가능함. 단일 오브젝트만 존재해야하고, 이를 애플리케이션의 여러 곳에서 공유하는 경우에 주로 사용함.


자바에서 싱글톤을 구현하는 방법

-클래스 밖에서는 오브젝트를 생성하지 못하도록 생성자를 private으로 만든다.
-생성된 싱글톤 오브젝트를 저장할 수 있는 자신과 같은 타입의 스태틱필드를 정의한다.
-스태틱 펙토리 메소드인 getInstence()를 만들고 이 메소드가 최초로 호출되는 시점에서 한 번만 오브젝트가 만들어지게 한다
-한번 오브젝트(싱글톤)가 만들어지고 난 후에는 getInstence() 메소드를 통해 이미 만들어져 스태틱 필드에 저장해둔 오브젝트를 넘겨준다.


싱글톤 패턴 구현방식의 문제점

-private 생성자를 갖고 있기 떄문에 상속할 수 없음.
-싱글톤은 테스트하기가 힘들다
-서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
(자바 언어를 이용한 싱글톤 패턴 기법은 서버환경에서는 싱글톤이 꼭 보장된다고 볼 수 없음)
-싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.


싱글톤 레지스트리

자바의 기본적인 싱글톤 패턴의 구현 방시기은 여러가지 단점이 있기 때문에, 스프링은 직접 싱글톤 형태의 오브젝트를 만들어 관리하는 기능을 제공

-장점은 스태틱 메소드와 private 생성자를 사용해야하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다는 점.

-싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있기 때문에 조심해야 함. 상태정보를 내부에 갖고 있지 않는 무정부(stateless)방식으로 만들어져야 함.

-다중 사용자의 요청을 한꺼번에 처리하는 스레드들이 동시에 싱글톤 오브젝트의 인스턴스 변수를 수정하는 것은 매우 위험함.>> 저장할 공간이 하나뿐이니 서로 값을 덮어쓰고 자신이 저자앟지 않은 값을 읽어올 수 있기 때문
따라서 싱글톤은 기본적으로 인스턴스 필드의 값을 변경하고 유지하는 상태유지(stateful) 방식으로 만들지 않는다.


스프링 빈의 스코프

스코프: 스프링이 관리하고 오브젝트, 즉 빈이 생성된고, 존재하고 적용되는 범위
스코프의 종류
-싱글톤 스코프: 컨테이너 내에 한 개의 오브젝트만 만들어져서, 강제로 제거하지 않는 한 스프링 컨테이너가 존재하는 동안 계속 유지됨. 스프링에서 만들어지는 대부분의 빈은 싱글톤스코프임.

-프로토타입 스코프: 싱글톤과 달리 컨테이너에 빈을 요청할 때마다 매번 새로운 오브젝트를 만들어줌.

-요청(request) 스코프: 웹을 통해 새로운 HTTP 요청이 생길때마다 생성

-세션(session) 스코프: 웹의 세션과 유사


1.7 의존관계 주입(DI)

의존관계 주입, 의존성 주입, 의존 오브젝트 주입?

Dependency Injection는 오브젝트 레퍼런스를 외부로부터 제공(주입)받고 이를 통해 여타 오브젝트와 다이내믹하게 의존관계가 만들어지는 것이 핵심이다. 그렇기에 책에선 의존관계 주입이라 사용한다.
>>의존관계를 런타임시에 연결해주는 작업

의존 관계주입이란 다음 세가지 조건을 충족하는 작업

-클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.

-런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.

-의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.

의존관계 검색(dependency lookup)

의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용
getBean()메소드를 통해 객체를 검색하여 받아옴



1.8 XML을 이용한 설정

스프링은 자바클래스를 이용하는 것 외에도, 다양한 방법을 통해 DI의존관계 설정정보를 만들 수 있는데 대표적인 것이 XML이다. XML은 텍스트파일이기 때문에 다루기 쉽고, 컴파일과 같은 별도의 빌드작업이 없다는 것이 장점이다.


XML설정

스프링 애플리케이션 컨텍스트는 XML에 담긴 DI정보를 활용할 수 있다. DI정보가 담긴 XML파일은 <beans>를 루트 엘리먼트로 사용한다.
빈의 DI정보
-빈의 이름
-빈의 클래스: 빈 오브젝트를 어떤 클래스를 이용해서 만들지를 정의
-빈의 의존 오브젝트: 빈의 생성자나 수정자 메소드를 통해 의존 오브젝트를 넣어줌.

클래스 설정과 XML 설정의 대응항목
                         자바                        XML 설정 정보
빈 설정파일    @Configuration                 <beans>
빈의 이름       @Bean methodNames         <bean id="methodName"
빈의 클래스     return new BeanClass()       class-"a.b.c...BeanClass">

XML에서는 <property>태그를 사용해 의존 오브젝트와의 관계 정의한다.


DTD와 스키마

XML 문서는 미리 정해진 구조를 따라서 작성됐는지 검사할 수 있다. 구조를 정의하는 방법에는 DTD와 스키마(schema)가 있는데, 스프링 XML 설정파일은 두 가지 방식 모두 지원한다.
-DTD를 사용할 경우
<beans>엘리먼트 앞에 다음와 같은 선언을 넣어준다.
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">

-스키마는 DI를 위한 기본태그 <beans>, <bean> 외에도 특별한 목적을 위해 별도의 태그를 사용할 수 있는 방법을 제공한다. 특별한 이유가 없으면 스키마를 사용하는 것이 바람직함.
ex)내가 쓴 applicationcontext.xml
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?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:context="http://www.springframework.org/schema/context"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
    http://www.springframework.org/schema/jdbc  
    http://www.springframework.org/schema/jdbc/spring-jdbc-3.0.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    <context:property-placeholder location="classpath*:application-properties.xml"/>
    
    <context:annotation-config />
    
    <!-- 데이터 부분 -->
     <bean id="dataSource"
        class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
            <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
            <property name="url" value="jdbc:mysql://localhost:3306/cot_pr1"></property>
            <property name="username" value="root"></property>
            <property name="password" value="123321"></property>
    </bean>    
    
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:mybatis-config.xml" />
    </bean>
    
    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="sqlSessionFactory"></constructor-arg>
    </bean>
   
    <bean id="UserDao" class="net.cot_pr1.dao.UserDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
            
    <bean id="webBoardDao" class="net.cot_pr1.dao.WebBoardDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
     
    <bean id="webReplyDao" class="net.cot_pr1.dao.WebReplyDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
        
    <bean id="galleryDao" class="net.cot_pr1.dao.GalleryDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
    <bean id="freeboardDao" class="net.cot_pr1.dao.FreeBoardDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
        
    <bean id="freereplyDao" class="net.cot_pr1.dao.FreeReplyDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
        
    <bean id="noticeDao" class="net.cot_pr1.dao.NoticeDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
        
    <bean id="noticereplyDao" class="net.cot_pr1.dao.NoticeReplyDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
    
    <bean id="qnaDao" class="net.cot_pr1.dao.QnADao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
        
    <bean id="qnareplyDao" class="net.cot_pr1.dao.QnAReplyDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
    <bean id="adminDao" class="net.cot_pr1.dao.AdminDao"
        p:sqlSession-ref="sqlSession"
        p:dataSource-ref="dataSource" />
        
        
    <!-- 파일업로드를 위한 디렉토리 설정 -->
    <!-- String uploadPath = new String("업로드 경로") -->
    <bean id="uploadPath" class="java.lang.String">
        <!-- 파일업로드 디렉토리 -->
        <constructor-arg value="C:\Users\SK\git\Cot_pr1\Cot_Pr1\src\main\webapp\resources\uploads" />
    </bean>
    <bean id="uploadPath2" class="java.lang.String">
        <!-- 파일업로드 디렉토리 -->
        <constructor-arg value="C:\Users\SK\git\Cot_pr1\Cot_Pr1\src\main\webapp\resources\profile" />
    </bean>
    
     <!-- 메일보내기 -->
   <bean id="mailSender" class = "org.springframework.mail.javamail.JavaMailSenderImpl">
      <property name="host" value="smtp.gmail.com" />
      <property name="port" value="587" />
      <property name="username" value="--@gmail.com" />
      <property name="password" value="--" />
      <property name="javaMailProperties">
         <props>
               <prop key="mail.transport.protocol">smtp</prop>
               <prop key="mail.smtp.auth">true</prop>
               <prop key="mail.smtp.starttls.enable">true</prop>
               <prop key="mail.debug">true</prop>
         </props>
      </property>
   </bean>
</beans>
cs

1장을 끝내기 전에, 스프링이란 '어떻게 오브젝트가 설계되고, 만들어지고, 어떻게 관계를 맺고 사용되는지에 관심을 갖는 프레임워크' 라는 사실을 기억하자. 스프링의 관심은 오브젝트와 그 관계다. 



//////////////////////////////////
ㅜㅜㅜ어렵다 책 1회독으로 안될듯 장기간적으로 봐야하나..ㅠㅠ 

2017년 10월 28일 토요일

Node.js 공부[5]

5챕터-웹 서버 만들기

노드에서는 웹 서버를 만들 때 필요한 http 모듈이 있는데, 이 모듈을 사용하면 HTTP프로토콜로 요청하는 내용과 응답을 모두 처리할 수 있다. 
그러나 쉽고 빠르게 웹 서버를 구성하려면 익스프레스(Express)를 사용하는 것이 좋다.

간단한 웹 서버 만들기

1
2
3
4
5
6
7
8
9
10
11
var http = require('http');
//웹서버 객체 만들기
var server = http.createServer();
//웹서버를 시작하여 192.168.0.5 IP와 3000번 포트에서 대기
var host = '192.168.0.2';
var port =3000;
server.listen(port, host, '50000',function(){
    console.log('웹 서버가 시작되었습니다. : %s, %d',host, port);
});
cs

웹브라우저에 요청할 때 어떤 이벤트가 발생하는 지 예제를 통해 볼 수 있다.
on()메소드는 이벤트를 처리할 때 가장 기본적인 메소드이다. 이 메소드로 connection, request, close 이벤트를 처리할 수 있는 콜백 함수를 각각 등록해 두면 상황에 맞게 호출된다.
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
var http = require('http');
var server = http.createServer();
var port = 3000
server.listen(port, function(){
    console.log('웹 서버가 시작되었습니다. : %d', port);
});
//클라이언트 연결 이벤트 처리
server.on('connection'function(socket){
    var addr = socket.address();
    console.log('클라이언트가 접속했습니다.: %s, %d', addr.address, addr.port);
});
//클라이언트 요청 이벤트 처리
server.on('request'function(req, res){
    console.log('클라이언트 요청이 들어왔습니다.');
   
    //클라이언트 요청시 페이지로 응답 보냄.
    res.writeHead(200, {"Content-Type""text/html; charset=utf-8"});
    res.write("<html>");
    res.write("<head>");
    res.write("<title>응답 페이지</title>");
    res.write("</head>");
    res.write("<body>");
    res.write("<h1>node 응답 페이지</h1>");
    res.write("</body>");
    res.write("</html>");
    //end는 응답을 모두 보냈다는 것을 의미하며 일반적으로는 end()가 호출될 때 클라이언트로 응답을 전송한다.
    res.end();  
    
});
//서버 종료 이벤트 처리
server.on('close'function(){
    console.log('서버가 종료 됩니다.');
});
cs

파일을 스트림으로 읽어 응답 보내기
파일은 스트림 객체로 읽어 들일 수 있고 웹서버의 응답 객체도 스트림으로 데이터를 전송할 수 있기 때문에 두 개의 스트림은 파이프로 서로 연결할 수 있다.
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
var http = require('http');
var fs = require('fs');
var server = http.createServer();
var port = 3000
server.listen(port, function(){
    console.log('웹 서버가 시작되었습니다. : %d', port);
});
//클라이언트 연결 이벤트 처리
server.on('connection'function(socket){
    var addr = socket.address();
    console.log('클라이언트가 접속했습니다.: %s, %d', addr.address, addr.port);
});
//클라이언트 요청 이벤트 처리
server.on('request'function(req, res){
    console.log('클라이언트 요청이 들어왔습니다.');
   
    var filename = 'aa.jpg';
    var infile = fs.createReadStream(filename, {flage: 'r'});
    
    //파이프로 연결하여 알아서 처리하도록  설정
    infile.pipe(res);
    
});
//서버 종료 이벤트 처리
server.on('close'function(){
    console.log('서버가 종료 됩니다.');
});
cs

서버에서 다른 웹사이트의 데이터를 가져와 응답하기
http모듈을 사용해 GET방식으로 다른 사이트에 데이터를 요청하는 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var http = require('http');
var options = {
    host: 'www.google.com',
    port:80,
    path: '/'
};
var req = http.get(options, function(res){
    //응답처리
    resData = '';        
    res.on('data'function(chunk){
        resData += chunk;
    });
    
    res.on('end'function(){
        console.log(resData);
    });
});
req.on('error'function(err){
       console.log("오류 발생: " + err.message);
});
cs

POST방식으로 데이터를 요청하는 코드이다.
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
var http = require('http');
var opts = {
    host: 'www.google.com',
    port: 80,
    method: 'POST',
    path: '/',
    headers: {}
};
var resData ='';
var req = http.request(opts, function(res){
    //응답처리
    res.on('data'function(chunk){
        resData += chunk;
    });
    
    res.on('end'function(){
        console.log(resData);
    });
});
opts.headers['Content-Type'= 'application/x-www-form-unlencoded';
req.data = "q=actor";
opts.headers['Content-Lenth'= req.data.length;
req.on('error'function(err){
    console.log("오류발생:"+ err.message);
});
//요청 전송
req.write(req.data);
req.end();
cs

익스프레스로 웹 서버 만들기

http모듈만 사용해 웹 서버를 구성하면 많은 것들을 직접 만들어야 한다. 하지만 express모듈을 사용하면 간단한 코드로 웹서버의 기능을 구현할 수 있다. 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//여기서 express모듈은 웹 서버를 위해 만들어진 것이므로 http모듈 위에서 동작한다. 따라서 함께 불러줘야함.
var express = require('express')
, http = require('http');
//익스프레스 객체 생성
var app = express();
//기본 포트를 app객체에 속성으로 지정
app.set('port', process.env.PORT || 3000);
//Express 서버 시작
http.createServer(app).listen(app.get('port'), function(){
    console.log('익스프레스 서버를 시작했습니다: ' +app.get('port'));
});
cs
전 단계에서 http모듈로 웹 서버를 만들 때 createServer() 메소드로 웹 서버 객체를 만들고, listen() 메소드를 호출하여 클라이언트의 요청을 대기하도록 설정하였다. 익스프레스를 사용한 것도 같지만 차이점으로 createServer()메소드에 전달되는 파리미터로 app객체를 전달하는 것이 있다. app객체는 익스프레스 서버 객체이다.
이 객체의 주요 메소드로는
set(name, value) : 서버 설정을 위한 속성 지정, set()메소드로 지정한 속성은 get()으로 꺼내어 확인 할 수 있음
get(name) : 서버 설정을 위해 지정한 속성을 꺼내온다.
use([path,] function[, function...]) : 미들웨어 함수를 사용한다,
get([path,] function) : 특정 패스로 요청된 정보를 처리한다.


미들웨어로 클라이언트에 응답보내기
노드에서는 미들웨어를 사용하여 필요한 기능을 순차적으로 실행할 수 있다. 
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
var express = require('express')
, http = require('http')
var app = express();
app.use(function(req, res, next){
    console.log('첫 번째 미들웨어에서 요청을 처리함.');
    
    req.user = 'mike';
    
    next();
});
app.use(function(req, res, next){
    console.log('두 번째 미들웨어에서 요청을 처리함.');
    
    res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
    res.end('<h1>Express 서버에서' +req.user+ '가응답한 결과입니다.</h1>');
});
http.createServer(app).listen(3000function(){
    console.log('Express 서버가 3000번 포트에서 시작됨');
})
cs
>>각각의 미들웨어 안에서 마지막에 next() 메소드를 호출하여 다음 미들웨어로 처리 결과를 넘겨준다. 

또한 익스프레스에는 send(), redirect() 등 응답객체 메소드가 있다.
1
2
3
4
5
6
7
8
9
10
app.use(function(req, res, next){
    //send()로 JSON데이터 전송하기
    res.send({name:'소녀시대', age:20});
});
app.use(function(req, res, next){
    //redirect()로 페이지 이동하기
    res.redirect('http://google.co.kr');
});
cs

익스프레스에서 요청 객체에 추가한 헤더와 파라미터 알아보기
클라이언트에서는 요청 파라미터를 함께 보낼 수 있다. 이때 GET방식으로 요청했다면 요청파라미터들은 요청 객체의 query 객체 안에 들어간다. 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var express = require('express')
, http = require('http')
var app = express();
app.use(function(req, res, next){
    console.log('첫 번째 미들웨어에서 요청을 처리함.');
    
    var userAgent = req.header('User-Agent');
    var paramName = req.query.name;
    
    res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
    res.write('<h1>Express서버에서 응답한결과입니다. </h1>');
    res.write('<div><p>User-Agent : ' + userAgent + '</p></div>');
    res.write('<div><p>Param name: '+ paramName + '</p></div>');
    res.end();
});
http.createServer(app).listen(3000function(){
    console.log('Express 서버가 3000번 포트에서 시작됨');
})
cs
요청 파라미터를 주소에 입력해주면 
http://localhost:3000/?name=mike
이러한 창이 뜬다. 

미들웨어 사용하기

지금까지 use()메소드로 설정하는 미들웨어 함수 안에 코드를 직접 넣어 클라이언트로 응답을 전달했다. 하지만 모든 기능을 직접 만들어야한다면 쉽지 않으므로, 익스프레스에는 미리 만들어 둔 여러 미들웨어를 제공한다.

static 미들웨어: 특정 폴더의 파일들을 특정 패스로 접근할 수 있도록 만들어 줌
1
2
3
var static = require('serve-static');
...
app.use(static(path.join(__dirname, 'public')));

cs
이런식으로 지정해주면 public 폴더안의 파일들을  바로 접근할 수 있는 듯
public/index.html
ex) http://localhost:3000/index.html 와 같은 주소로 바로 접근 가능

body-parser 미들웨어: POST로 요청했을 때 요청 파라미터를 확인할 수 있음.
>>클라이언트가 POST방식으로 요청할 때 본문 영역에 들어있는 요청 파라미터들을 파싱하여 요청 객체의 body속성에 넣어준다.
public/login.html
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
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>로그인 테스트</title>
    </head>
    
<body>
    <h1>로그인</h1>
    <br>
    <form method="POST">
        <table>
            <tr>
                <td><label>아이디</label></td>
                <td><input type="text" name="id"></td>
            </tr>
            
            <tr>
                <td><label>비밀번호</label></td>
                <td><input type="password" name="password"></td>
            </tr>
        </table>
        <input type="submit" value="전송" />
    </form>
</body>
</html>
cs
app.js
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
//Express 기본 모듈
var express = require('express')
, http = require('http')
, path = require('path');
//Express 미들 웨어
var bodyParser = require('body-parser')
, static = require('serve-static');
//익스프레스 객체 생성
var app = express();
//기본 속성 설정
app.set('port', process.env.PORT || 3000);
//body-parser를 사용해 application/x-www-form-urlencoded 파싱
app.use(bodyParser.urlencoded({extended: false}));
//body-parser를 사용해 application.json 파싱
app.use(bodyParser.json());
app.use(static(path.join(__dirname, 'public')));
//미들웨어에서 파라미터 확인
app.use(function(req, res, next){
    console.log('첫 번째 미들웨어에서 요청을 처리함.');
    
    var paramId = req.body.id || req.query.id;
    var paramPassword = req.body.password || req.query.password;
    
    res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
    res.write('<h1>Express서버에서 응답한결과입니다. </h1>');
    res.write('<div><p>paramId : ' + paramId + '</p></div>');
    res.write('<div><p>Param Password: '+ paramPassword + '</p></div>');
    res.end();
});
http.createServer(app).listen(3000function(){
    console.log('Express 서버가 3000번 포트에서 시작됨');
})
cs
>>예제를 실행해 보면 login창에서 전송버튼을 누르면 입력한 파라미터들이 다음 창에서 표시된다. 

요청 라우팅하기

다른 요청이 들어왔을 때도 use()메소드로 설정한 미들웨어 함수가 항상 호출되기 때문에 요청 url이 무엇인지 일일이 확인해야 하는 번거로움이 생긴다. 이 문제를 해결하는 것이 라우터 미들웨어(router middleware)라고 한다.
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
//Express 기본 모듈
var express = require('express')
, http = require('http')
, path = require('path');
//Express 미들 웨어
var bodyParser = require('body-parser')
, static = require('serve-static');
//익스프레스 객체 생성
var app = express();
//기본 속성 설정
app.set('port', process.env.PORT || 3000);
//body-parser를 사용해 application/x-www-form-urlencoded 파싱
app.use(bodyParser.urlencoded({extended: false}));
//body-parser를 사용해 application.json 파싱
app.use(bodyParser.json());
app.use(static(path.join(__dirname, 'public')));
//라우터 객체 참조
var router = express.Router();
//라우팅 함수 등록
router.route('/process/login').post(function(req, res){
    console.log('/process/login 처리함');
    
   var paramId = req.body.id || req.query.id;
    var paramPassword = req.body.password || req.query.password;
    
    res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
    res.write('<h1>Express서버에서 응답한결과입니다. </h1>');
    res.write('<div><p>paramId : ' + paramId + '</p></div>');
    res.write('<div><p>Param Password: '+ paramPassword + '</p></div>');
    res.write("<br><a href='/login2.html'>로그인 페이지로 돌아가기</a>")
    res.end();
})
// 라우터 객체를 app객체에 등록
app.use('/', router);
// 등록되지 않은 패스에 대해 페이지 오류 응답
app.all('*'function(req, res) {
    res.status(404).send('<h1>ERROR - 페이지를 찾을 수 없습니다.</h1>');
});
http.createServer(app).listen(3000function(){
    console.log('Express 서버가 3000번 포트에서 시작됨');
})
cs


오류를 처리할 수 있는 미들웨어도 있다.
epress-error-handler 미들웨어 이다. 
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
//Express 기본 모듈
var express = require('express')
, http = require('http')
, path = require('path');
//Express 미들 웨어
var bodyParser = require('body-parser')
, static = require('serve-static');
//오류 핸들러 사용
var expressErrorHandler = require('express-error-handler');
//익스프레스 객체 생성
var app = express();
//기본 속성 설정
app.set('port', process.env.PORT || 3000);
//body-parser를 사용해 application/x-www-form-urlencoded 파싱
app.use(bodyParser.urlencoded({extended: false}));
//body-parser를 사용해 application.json 파싱
app.use(bodyParser.json());
app.use(static(path.join(__dirname, 'public')));
//라우터 객체 참조
var router = express.Router();
//라우팅 함수 등록
router.route('/process/login').post(function(req, res){
    console.log('/process/login 처리함');
    
   var paramId = req.body.id || req.query.id;
    var paramPassword = req.body.password || req.query.password;
    
    res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
    res.write('<h1>Express서버에서 응답한결과입니다. </h1>');
    res.write('<div><p>paramId : ' + paramId + '</p></div>');
    res.write('<div><p>Param Password: '+ paramPassword + '</p></div>');
    res.write("<br><a href='/login2.html'>로그인 페이지로 돌아가기</a>")
    res.end();
})
// 라우터 객체를 app객체에 등록
app.use('/', router);
// 404 에러 페이지 처리
var errorHandler = expressErrorHandler({
    static: {
      '404''./public/404.html'
    }
});
app.use( expressErrorHandler.httpError(404) );
app.use( errorHandler );
http.createServer(app).listen(3000function(){
    console.log('Express 서버가 3000번 포트에서 시작됨');
})
cs


토큰과 함께 요청한 정보 처리하기
/get()메소드를 호출하면서 동시에 /process/users/:id 패스를 처리하는 예제이다. http://localhost:3000/process/users/2 주소를 입력하면 users 뒤에있는 2가 아이디로 접근한다.
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
// Express 기본 모듈 불러오기
var express = require('express')
  , http = require('http')
  , path = require('path');
// Express의 미들웨어 불러오기
var bodyParser = require('body-parser')
  , static = require('serve-static');
// 익스프레스 객체 생성
var app = express();
// 기본 속성 설정
app.set('port', process.env.PORT || 3000);
// body-parser를 이용해 application/x-www-form-urlencoded 파싱
app.use(bodyParser.urlencoded({ extended: false }))
// body-parser를 이용해 application/json 파싱
app.use(bodyParser.json())
app.use('/public', static(path.join(__dirname, 'public')));
// 라우터 사용하여 라우팅 함수 등록
var router = express.Router();
router.route('/process/users/:id').get(function(req, res) {
    console.log('/process/users/:id 처리함.');
    // URL 파라미터 확인
    var paramId = req.params.id;
    
    console.log('/process/users와 토큰 %s를 이용해 처리함.', paramId);
    res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
    res.write('<h1>Express 서버에서 응답한 결과입니다.</h1>');
    res.write('<div><p>Param id : ' + paramId + '</p></div>');
    res.end();
});
app.use('/', router);
// 등록되지 않은 패스에 대해 페이지 오류 응답
app.all('*'function(req, res) {
    res.status(404).send('<h1>ERROR - 페이지를 찾을 수 없습니다.</h1>');
});
// Express 서버 시작
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});
cs

쿠키와 세션 관리하기

사용자가 로그인 상태인지 아닌지 확인하고 싶을 때에는 쿠키나 세션을 사용한다.  쿠키는 클라이언트 웹 브라우저에 저장되는 정보이며, 세션은 웹 서버에 저장되는 정보이다.
익스프레스에서는 cookie-parser 미들웨어를 사용하면 쿠키를 설정하거나 확인할 수 있다.
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
62
63
// Express 기본 모듈 불러오기
var express = require('express')
  , http = require('http')
  , path = require('path');
// Express의 미들웨어 불러오기
var bodyParser = require('body-parser')
  , static = require('serve-static');
var cookieParser = require('cookie-parser')
// 익스프레스 객체 생성
var app = express();
// 기본 속성 설정
app.set('port', process.env.PORT || 3000);
// body-parser를 이용해 application/x-www-form-urlencoded 파싱
app.use(bodyParser.urlencoded({ extended: false }))
// body-parser를 이용해 application/json 파싱
app.use(bodyParser.json())
app.use('/public', static(path.join(__dirname, 'public')));
// 라우터 사용하여 라우팅 함수 등록
var router = express.Router();
router.route('/process/showCookie').get(function(req, res) {
    console.log('/process/showCookie 호출됨');
    
    res.send(req.cookies);
});
router.route('/process/setUserCookie').get(function(req, res) {
    console.log('/process/setUserCookie 호출됨');
    
    //쿠키 설정
    res.cookie('user', {
        id: 'mike',
        name'소녀시대',
        authorized: true
    });
    
    //redirect로 응답
    res.redirect('/process/showCookie');
});
app.use('/', router);
// 등록되지 않은 패스에 대해 페이지 오류 응답
app.all('*'function(req, res) {
    res.status(404).send('<h1>ERROR - 페이지를 찾을 수 없습니다.</h1>');
});
// Express 서버 시작
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});
cs
크롬의 개발자 도구에서 어플리케시연 - cookies 항목을 클릭하면 쿠기 정보가 보인다.

이번엔 세션이다. 세션도 상태정보를 저장하는 역할을 하지만 쿠키와 달리 서버쪽에 저장된다. 대표적인 예로는 로그인했을때 저장되는 세션을 들 수 있다.
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// Express 기본 모듈 불러오기
var express = require('express')
  , http = require('http')
  , path = require('path');
// Express의 미들웨어 불러오기
var bodyParser = require('body-parser')
  , cookieParser = require('cookie-parser')
  , static = require('serve-static')
  , errorHandler = require('errorhandler');
// 에러 핸들러 모듈 사용
var expressErrorHandler = require('express-error-handler');
// Session 미들웨어 불러오기
var expressSession = require('express-session');
// 익스프레스 객체 생성
var app = express();
// 기본 속성 설정
app.set('port', process.env.PORT || 3000);
// body-parser를 이용해 application/x-www-form-urlencoded 파싱
app.use(bodyParser.urlencoded({ extended: false }))
// body-parser를 이용해 application/json 파싱
app.use(bodyParser.json())
app.use('/public', static(path.join(__dirname, 'public')));
// cookie-parser 설정
app.use(cookieParser());
// 세션 설정
app.use(expressSession({
    secret:'my key',
    resave:true,
    saveUninitialized:true
}));
// 라우터 사용하여 라우팅 함수 등록
var router = express.Router();
// 로그인 라우팅 함수 - 로그인 후 세션 저장함
router.route('/process/login').post(function(req, res) {
    console.log('/process/login 호출됨.');
    var paramId = req.body.id || req.query.id;
    var paramPassword = req.body.password || req.query.password;
    
    if (req.session.user) {
        // 이미 로그인된 상태
        console.log('이미 로그인되어 상품 페이지로 이동합니다.');
        
        res.redirect('/public/product.html');
    } else {
        // 세션 저장
        req.session.user = {
            id: paramId,
            name: '소녀시대',
            authorized: true
        };
        
        res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
        res.write('<h1>로그인 성공</h1>');
        res.write('<div><p>Param id : ' + paramId + '</p></div>');
        res.write('<div><p>Param password : ' + paramPassword + '</p></div>');
        res.write("<br><br><a href='/process/product'>상품 페이지로 이동하기</a>");
        res.end();
    }
});
// 로그아웃 라우팅 함수 - 로그아웃 후 세션 삭제함
router.route('/process/logout').get(function(req, res) {
    console.log('/process/logout 호출됨.');
    
    if (req.session.user) {
        // 로그인된 상태
        console.log('로그아웃합니다.');
        
        req.session.destroy(function(err) {
            if (err) {throw err;}
            
            console.log('세션을 삭제하고 로그아웃되었습니다.');
            res.redirect('/public/login2.html');
        });
    } else {
        // 로그인 안된 상태
        console.log('아직 로그인되어있지 않습니다.');
        
        res.redirect('/public/login2.html');
    }
});
// 상품정보 라우팅 함수
router.route('/process/product').get(function(req, res) {
    console.log('/process/product 호출됨.');
    
    if (req.session.user) {
        res.redirect('/public/product.html');
    } else {
        res.redirect('/public/login2.html');
    }
});
app.use('/', router);
// 404 에러 페이지 처리
var errorHandler = expressErrorHandler({
    static: {
      '404': './public/404.html'
    }
});
app.use( expressErrorHandler.httpError(404) );
app.use( errorHandler );
// Express 서버 시작
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});
cs
세션이 만들어지면 connect.sid 쿠기가 브라우저에 저장된다.

파일 업로드 기능 만들기

파일을 업로드할 때는 멀티파트(multipart)포맷으로 된 파일 업로드 기능을 사용한다.

여기에선 multer 미들웨어로 파일을 업로드하는 방법을 알아본다. 
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// Express 기본 모듈 불러오기
var express = require('express')
  , http = require('http')
  , path = require('path');
// Express의 미들웨어 불러오기
var bodyParser = require('body-parser')
  , cookieParser = require('cookie-parser')
  , static = require('serve-static')
  , errorHandler = require('errorhandler');
// 에러 핸들러 모듈 사용
var expressErrorHandler = require('express-error-handler');
// Session 미들웨어 불러오기
var expressSession = require('express-session');
// 파일 업로드용 미들웨어
var multer = require('multer');
var fs = require('fs');
// 클라이언트에서 ajax로 요청했을 때 CORS(다중 서버 접속) 지원
var cors = require('cors');
// 익스프레스 객체 생성
var app = express();
// 기본 속성 설정
app.set('port', process.env.PORT || 3000);
// body-parser를 이용해 application/x-www-form-urlencoded 파싱
app.use(bodyParser.urlencoded({ extended: false }))
// body-parser를 이용해 application/json 파싱
app.use(bodyParser.json())
// public, uploads 폴더 오픈
app.use('/public', static(path.join(__dirname, 'public')));
app.use('/uploads', static(path.join(__dirname, 'uploads')));
        
// cookie-parser 설정
app.use(cookieParser());
// 세션 설정
app.use(expressSession({
    secret:'my key',
    resave:true,
    saveUninitialized:true
}));
// 클라이언트에서 ajax로 요청했을 때 CORS(다중 서버 접속) 지원
app.use(cors());
//multer 미들웨어 사용: 미들 웨어 사용 순서 중요 body-parser > multer > router
// 파일제한: 10개, 1G
var storage = multer.diskStorage({
    //destination: 업로드한 파일이 저장될 폴더를 지정
    destination: function(req, file, callback){
        callback(null'uploads')    
    },
    filename: function(rqe, file, callback){
        callback(null, file.originalname + Date.now());
    }
});
var upload = multer({
    storage: storage,
    limits: {
        files:10,
        fileSize: 1024 * 1024 * 1024
    }
});
// 라우터 사용하여 라우팅 함수 등록
var router = express.Router();
router.route('/process/photo').post(upload.array('photo',1),function(req,res){
    console.log('photo 호출됨');
    
    try{
        var files = req.files;
        
        console.dir('#=== 업로드된 첫번째 파일 정보=====#')
        console.dir(req.files[0]);
        console.dir('#======#')
        
        //현재의 파일 정보를 저장할 변수 선언
        var originalname = '',
            filename = '',
            mimetype ='',
            size =0;
        
            if(Array.isArray(files)){ //배열에 들어가 있는 경우(설정에서 1개의 파일도 배열에 넣게 했음)
                console.log("배열에 들어있는 파일 갯수 :%d", files.length);
                
                for(var index =0; index < files.length; index++){
                    originalname = files[index].originalname;
                    filename = files[index].filename;
                    mimetype = files[index].mimetype;
                    size = files[index].size;
                    
                }
            }else//배열에 들어가 있지 않는 경우(현재 설정에서는 해당 없음)
                console.log("파일 갯수: 1");
                
                originalname = files[index].originalname;
                filename = files[index].filename;
                mimetype = files[index].mimetype;
                size = files[index].size;
            }
        
        console.log('현재 파일 정보:'+originalname+','+ filename+','+mimetype+',' +size);
        
        //클라이언트에 응답 전송
        res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
        res.write('<h1>파일 업로드  성공</h1>');
        res.write('<hr/>');
        res.write('<p>원본 파일이름:' +originalname+'-> 저장 파일명:'+filename +'</p>');
        res.write('<p>MIME TYPE:' +mimetype+ '</p>');
        res.write('<p>파일 크기:' +size+ '</p>');
        res.end();
        
    }catch(err){
        console.dir(err.stack);
    }
});
app.use('/', router);
// 404 에러 페이지 처리
var errorHandler = expressErrorHandler({
    static: {
      '404''./public/404.html'
    }
});
app.use( expressErrorHandler.httpError(404) );
app.use( errorHandler );
// Express 서버 시작
http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

cs
파일을 업로드했을 때 업로드한 파일의 정보는 배열 객체로 저장된다. 여기서는 for문으로 배열 객체의 요소들인 파일 이름이나 크기를 하나씩 확인한다. 

업로드버튼을 눌러 웹서버로 요청하면 업로드 기능을 담당하는 multer 미들웨어를 거쳐 라우팅 함수쪽으로 전달된다. 요기선 파일이름을 변경하여 uploads폴도어 저장된다. 이 과정에서 확인된 정보는 응답으로 보내저 업로드 결과를 볼 수 있다.