개발자 마다 DB ID / PW가 다르고 각자의 자리에서만 접속이 가능한 환경

Spring profiles 을 local/dev, stage, prod 로 구분 해 놓았지만, application-xxx.properties 나 application-xxx.yml 에 들어 있는 내용을 커밋 하는 경우 나의 DB 비번이 모두에게 공유됨

첫째로, 보안상 문제

둘때로, 다른 개발자가 매우 귀찮아 함.

 

VM 실행시 커맨드라인에서 필요한 변수를 주입 해보자

1. VM 실행시 -D 옵션으로 주입 하는 방법

application-xxx.propertis 예

spring.datasource.username: ${db.user}
spring.datasource.password: ${db.password}

application.xxx.yml 예

spring:
  datasource:
    username: ${db.user}
    password: ${db.password}

통합개발환경 Run/Debug Configurations 의 VM 옵션에 아래 내용을 추가

-Ddb.user=my_db_userid -Ddb.password=my_db_password

IntelliJ 예시

 

2. 스프링 환경 변수를 이용하는 방법

통합개발환경 Run/Debug Configurations 의 VM 옵션에 아래 내용을 추가

-Dspring.application.json="{\"spring.datasource.username\":\"my_db_userid\",\"spring.datasource.password\":\"my_db_password\"}"

application.properties, application.yml 의 설정 보다 우선 순위 이므로 설정파일에 값이 있건 없건 VM 옵션으로 지정한 값으로 주입됨

끝.

취약점 개요 (나무위키)

https://namu.wiki/w/2021%EB%85%84%20%EC%9E%90%EB%B0%94%20%EB%B3%B4%EC%95%88%20%EC%B7%A8%EC%95%BD%EC%A0%90%20%EC%82%AC%ED%83%9C

 

요약

log4j 가 내부적으로 사용하고 있는 JNDI와 LDAP을 기능을 이용해 서버 권한을 탈취할 수 있는 취약점

log4j core v2.0 ~ v2.14.x 까지 해당

 

조치 권고

  • log4j core 모듈 v2.15로 업그레이드
  • 2.0-beta9 ~ 2.10.0 : log4j-core-*.jar 파일에서 org/apache/logging/log4j/core/lookup/JndiLookup.class 클래스를 제거 후 리빌드
  • 2.10 ~ 2.14.1 : log4j2.formatMsgNoLookups 또는 LOG4J_FORMAT_MSG_NO_LOOKUPS 환경 변수를 true로 설정

 

나의 현황

운영중인 Spring Boot 어플리케이션을 점검한 결과 log4j의 상위 버전인 logback + slf4j 로 로깅을 하고 있어서 큰 문제는 없는 것으로 판단

Maven dependency 확인 결과 Spring Boot 프로잭트를 생성하면서 자동으로 추가된 log4j 1.6 이 포함되어 있으나 사용하지 않은 패키지로 배포파일에는 포함되어 있지 않은 것으로 확인됨

 

추가로 검증을 위해 취약점을 확인 할 솔루션을 찾아보니 오픈소스인 로그프레소 제품 제작사에서 스케너를 배포한 것이 있었습니다.

로그프레소 스캐너 관련 기사

http://www.weeklypost.kr/news/articleView.html?idxno=2890 

 

로그프레소, 로그4셸 취약점 대응 스캐너 배포 - 위클리포스트(weeklypost)

로그4셸 보안 취약점이 보고 되면서, 이를 통해 발생할 원격으로 악성코드를 실행하는 문제점이 우려되고 있다. 사실상 거의 모든 자바 서버가 대상인 취약점으로 업계 전문가는 신속하게 취약

www.weeklypost.kr

스캐너 소스 : https://github.com/logpresso/CVE-2021-44228-Scanner


스캐너 검증

실 운영서버에 적용하기 전에 소스 부터 확인 해보았습니다.

https://github.com/logpresso/CVE-2021-44228-Scanner/blob/main/src/main/java/com/logpresso/scanner/Log4j2Scanner.java

 

GitHub - logpresso/CVE-2021-44228-Scanner: Vulnerability scanner and mitigation patch for Log4j2 CVE-2021-44228

Vulnerability scanner and mitigation patch for Log4j2 CVE-2021-44228 - GitHub - logpresso/CVE-2021-44228-Scanner: Vulnerability scanner and mitigation patch for Log4j2 CVE-2021-44228

github.com

내용은 간단해 보였습니다. 지정된 경로의 jar파일을 해제 해서 

org/apache/logging/log4j/core/lookup/JndiLookup.class

클래스가 존재 하는지 체킹하고 실행 파라메터에 추가에 따라 해당 클래스를 삭제해주는 내용이었습니다.

간단해 보이지만 취약점의 원인을 정확히 모르는 우리에겐 이런 것을 만들어서 공개 해주시는 분들께 그저 감사할 따름 입니다.

소스를 살펴 보고 실제 취약점을 찾아 내는지 검증 해보았습니다.

간단한 Spring Boot 프로잭트를 생성후 정상 동작 하는 것을 확인 하고

1. Maven > Update Project

2. Run > Maven Install

3. 커맨드 창에서 스캐너 실행

C:\temp\logpresso-log4j2-scan-1.2.1-win64>log4j2-scan.exe C:\temp\log4j_test\workspace\demo\target

Scanned 26 directories and 16 files
Found 0 vulnerable files
Completed in 0.90 seconds

C:\temp\logpresso-log4j2-scan-1.2.1-win64>

문제 없음

문제가 된 log4j 2.14 버전 추가

pom.xml 추가

<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.0</version>
</dependency>

1. Maven > Update Project

2. Run > Maven Install

3. 커맨드 창에서 스캐너 실행

C:\temp\logpresso-log4j2-scan-1.2.1-win64>log4j2-scan.exe C:\temp\log4j_test\workspace\demo\target
[*] Found CVE-2021-44228 vulnerability in C:\temp\log4j_test\workspace\demo\target\demo-0.0.1-SNAPSHOT.jar (BOOT-INF/lib/log4j-core-2.14.0.jar), log4j 2.14.0

Scanned 26 directories and 16 files
Found 1 vulnerable files
Completed in 0.95 seconds

C:\temp\logpresso-log4j2-scan-1.2.1-win64>

취약점 발견됨

같은 방법으로 업데이트 권고 버전인 log4j 2.15로 변경 후 테스트 결과

C:\temp\logpresso-log4j2-scan-1.2.1-win64>log4j2-scan.exe C:\temp\log4j_test\workspace\demo\target

Scanned 26 directories and 16 files
Found 0 vulnerable files
Completed in 0.90 seconds

C:\temp\logpresso-log4j2-scan-1.2.1-win64>

취약점 발견 안됨

 

같은 방법으로 

서버에서 실행

[myservice@MY_HOST_NAME ~]$ pwd                 <== 경로 확인
/home/myservice/log4j_scan
[myservice@MY_HOST_NAME ~]$ mkdir log4j_scan    <== 스케너 다운로드 경로 확인
[myservice@MY_HOST_NAME ~]$ cd log4j_scan/
[myservice@MY_HOST_NAME log4j_scan]$ wget https://github.com/logpresso/CVE-2021-44228-Scanner/releases/download/v1.2.1/logpresso-log4j2-scan-1.2.1-linux.tar.gz
--2021-12-13 14:39:38--  https://github.com/logpresso/CVE-2021-44228-Scanner/releases/download/v1.2.1/logpresso-log4j2-scan-1.2.1-linux.tar.gz
Resolving github.com (github.com)... 15.164.81.167
Connecting to github.com (github.com)|15.164.81.167|:443... connected.
HTTP request sent, awaiting response... 302 Found
Location: https://objects.githubusercontent.com/github-production-release-asset-2e65be/437261211/2c31e904-1f15-4f6d-93f4-aa6c8b3df5b5?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20211213%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20211213T053815Z&X-Amz-Expires=300&X-Amz-Signature=003efe369b272cca8846a43f4b0b38bd45e47ec82c7613612cdb12c49ba851d3&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=437261211&response-content-disposition=attachment%3B%20filename%3Dlogpresso-log4j2-scan-1.2.1-linux.tar.gz&response-content-type=application%2Foctet-stream [following]
--2021-12-13 14:39:38--  https://objects.githubusercontent.com/github-production-release-asset-2e65be/437261211/2c31e904-1f15-4f6d-93f4-aa6c8b3df5b5?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20211213%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20211213T053815Z&X-Amz-Expires=300&X-Amz-Signature=003efe369b272cca8846a43f4b0b38bd45e47ec82c7613612cdb12c49ba851d3&X-Amz-SignedHeaders=host&actor_id=0&key_id=0&repo_id=437261211&response-content-disposition=attachment%3B%20filename%3Dlogpresso-log4j2-scan-1.2.1-linux.tar.gz&response-content-type=application%2Foctet-stream
Resolving objects.githubusercontent.com (objects.githubusercontent.com)... 185.199.108.133, 185.199.109.133, 185.199.110.133, ...
Connecting to objects.githubusercontent.com (objects.githubusercontent.com)|185.199.108.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2957824 (2.8M) [application/octet-stream]
Saving to: ‘logpresso-log4j2-scan-1.2.1-linux.tar.gz’

100%[================================================================================================================================================================================================================================================>] 2,957,824   11.8MB/s   in 0.2s

2021-12-13 14:39:39 (11.8 MB/s) - ‘logpresso-log4j2-scan-1.2.1-linux.tar.gz’ saved [2957824/2957824]

[myservice@MY_HOST_NAME log4j_scan]$ ls -al    <== 스캐너 다운로드 확인
합계 2892
drwxrwxr-x 2 myservice dev      54 12월 13 14:39 .
drwx------ 5 myservice dev     124 12월 13 14:39 ..
-rw-rw-r-- 1 myservice dev 2957824 12월 13 12:34 logpresso-log4j2-scan-1.2.1-linux.tar.gz
[myservice@MY_HOST_NAME log4j_scan]$ tar zxvf logpresso-log4j2-scan-1.2.1-linux.tar.gz     <== 압축 해제
log4j2-scan
[myservice@MY_HOST_NAME log4j_scan]$ ls
log4j2-scan  logpresso-log4j2-scan-1.2.1-linux.tar.gz
[myservice@MY_HOST_NAME log4j_scan]$ ./log4j2-scan /usr/local/myservice/apiserver     <== jar 파일이 존재하는 경로 지정하여 스캔

Scanned 4 directories and 40 files
Found 0 vulnerable files                 <== 취약점 0 발견
Completed in 6.92 seconds
[myservice@MY_HOST_NAME log4j_scan]$

확인 완료 후 스캐너 파일 삭제함.

소중한 소스코드 공유해 주신 로그프레소 제작사께 감사드립니다.

https://ko.logpresso.com/

1. 생년월일

주민번호 앞 6자리는 호적상 생년월일

2. 성별

뒷 7자리 숫자중 첫자리는 성별

 - 1900년대생 남자:1, 여자:2

 - 2000년대생 남자:3, 여자:4

 - 기타 1800년대생 남/녀도 있고

 - 귀화한 외국인 남/녀도 있다.

아래 표를 참조 하자

태어난 년도 한국인 외국인
남자 여자 남자 여자
1800~1899 9 0    
1900~1999 1 2 5 6
2000~2099 3 4 7 8

언젠가 카드사 홈페이지 회원가입에 주민번호를 입력해도 주민번호를 확인하라고 계속 뜬다고 해서 확인해 보니, 189? 년생 여자분... 기존에 개발 하신 분이 1,2만 체크하도록 만들어 놓으신 것인데, 그때가 100살이 넘은 분이 직접 카드사 홈페이지에 회원가입을 할리는 없고 아들이 본인 신용상의 문제로 어머니(할머니?) 주민번호로 카드를 발급한 상태였음

 

3. 지역코드

성별 다음 4자리는 지역 코드 (주민번호 지역코드 정도로 구글링 하면 많은 정보가 나온다)

4. 순차번호

다음으로 뒷자리 7개 숫자중 끝에서 두번째 자리는 같은 지역에서 같은날 여러명이 태어난 경우를 대비해 만들어 놓은 순차 번호이다.

5. Check Digit

맨마지막 1자리는 앞의 자리가 정상적으로 입력되었는지 확인하는 Check Digit 이다. 예전에 전산을 처음 배울때 Check Digit을 구하는 공식은 국가2급 기밀이라고 했었는데, 요즘 인터넷에 검색해 보면 수도 없이 나온다.

 

실명인증으로 대체

최근에는 개인정보 중요성이 대두되면서 주민번호 뒷자리까지 입력받는 경우는 의료나, 금융쪽밖에 없다. 웬만해선 이름+생년월일+성별로 대부분 식별이 가능하니 구지 주민번호 뒷자리까지 공개하지 않고 있다. 그리고 그것을 이용하여 신용평가회사 등에서 휴대폰 등을 통해 확인을 해주는 방식을 쓰고 있다.

위에 이야기 했던 카드사쇼핑몰에서 일하던 시절... 회원가입하면서 주민번호가 잘못되었다는 메시지가 계속 나온다고해서 확인해 보니 80세 여성고객의 자제분 새로 카드를 만들려고 하는데 주민번호 인증이 안된다고 연락이 왔다... 이분은 1900년대 생으로 주민번호 성별이 잘못된 케이스도 아님... 당시 실명인증을 대행하던 신평사에 문의했더니 진짜 없는 주민번호라고... 어째 그럴수가 있을까?

신용평가사는 금융결재원에서 전국민이 이용한 금융거래 기록에서 이름과 주민번호를 제공받아서 그것으로 장사를 하는 것이다.

그분은 80세가 넘도록 금융거래를 한번도 한적이 없었던 분으로 판명됨.

 

주민번호 중복 가능성

현재의 주민번호 체계는 같은지역(읍/면/동)에서 같은날 출생신고를 하는 동성의 사람이 10명을 초과 하게 되면 주민번호가 중복 될수 밖에 없는 구조이다. 앞의 11자리는 모두 동일할 것이고 순차번호는 0~9까지 10명 밖에 수용하지 못한다. 과거에 인구가 적던 시절에 만들어진 체계 이기 때문에 현실적으로 서울의 큰 동에서는 이를 초과하는 경우가 발생할 수도 있을 것 같다.(실제 있을것 같은데... 어떻게 하는지는 잘 모르겠음)

물론 현재 의 출산율을 봤을 때 인구가 줄어드는 상황으로 큰 문제는 없을 것 같다는 생각이...

2020.02.07 추가 : 최근 뉴스에서 주민번호 지역코드를 전국으로 통합하겠다고 하는 기사를 본것 같다. 아마도 주민번호4자리 + 순차코드 1자리로 대도시의 출생률에 대응하는데 문제가 있어서 아닐까?

 

성별코드 고갈

성별코드 역시 0~9까지 모두 사용한 상태이다. 향후 2100년(...그때 까지 살 수 있을까?) 이후에는 새로운 성별코드가 필요하다. 그때가 되면 현재의 주민번호 체계가 아닌 다른 체계의 인식코드를 사용하거나, 설마... 현재의 주민번호 체계를 그때까지 계속 쓴다면 1800년대 생을 위해 할당되었던 0,9를 재활용하지 않을까... 그때 되면 데이타 마이그레이션 할 일이 많을까? 어쩌면.... 2100년에 호모사피엔스가 지구에 존재하지 않을수도... 

아 쓸데 없는 80년 후의 걱정을 하고 있어... 실명인증이나 얼른 마무리 해야겠다.

 

고전적인 Spring MVC에서

Controller -> Service Interface -> Service Implement -> Dao (또는 Dao Interface -> Dao Implement) -> DB

이런식으로 구현한 소스가 흔했었는데요, 살펴보면 대부분의 경우가 Service Interface 하나에 구현체도 하나, Dao도 그렇게 일대일로 개발해 놓은 경우가 많았습니다.

OOP Polymorphism 사상에서 본다면 Service 구현체가 여러가지 케이스가 있을 수 있기 때문에 그렇게 개발하는 것이라고 생각했지만, 그동안 여러곳의 프로잭트를 경험하면서 구현체가 여러개 였던 적이 한번도 없습니다. 대부분 그때 그때 조건에 따라 if~else if~ else 이러식으로 필요한 부분에서 분기를 해서 로직을 처리 하는 경우가 대부분 이었습니다.

단순한 웹페이지의 경우 하나의 화면을 만들기 위해서

  • Controller 메서드 한개
  • Service 인터페이스 메소드 원형 한개
  • Service 구현체에 매소드 한개, 내용은 Dao 메소드 호출 후 리턴하는 코드 한줄
  • Dao 인터페이스 메소드 원형 한개
  • Dao 구현체에 매소드 한개, 내용은 쿼리맵을 호출해서 결과를 리턴하는 코드 한줄
  • 결과를 보여주는 페이지

거의 모든 페이지가 이렇게 개발되어 있는 홈페이지 소스를 본적이 있는데... 유지보수를 하면서 조회 쿼리에 파라메터를 String에서 콘렉션으로 바꾸려면 콘트롤러에 Service를 호출 하는 부분부터 줄줄이 수정 해야 했습니다.

너무 비합리적인 구조인데 왜 이렇게 만들었을까... 해묵은 논잭이긴 하지만 저랑 비슷한 생각을 하시는 개발자들이 많았었는지 요즘은 서비스나 DAO에 Interface를 만들지 않고 바로 클래스를 호출하는 방식으로 간편하게 많이들 하시는 것 같습니다.

어디서 본 소스에는 Controller에 비즈니스 로직을 모두 구현하고 Controller에서 바로 DAO를 호출 하는 경우도 본적이 있는데... 스프링 MVC 사상에는 어긋나긴 하지만... 예외처리와 Data Rollback만 잘 되게 해놓았다면 문제 없다는 생각입니다.

그런데 조금 아쉬운게... 서비스에서 상황별로 여러가지 분기를 해야 하는 경우 if ~ else if ~ else 이렇게 구현하다 보니 소스도 엄청 복잡해지고 OOP 답지 않게 후져 보이고... 로직이 많이 복잡한경우 if ~ else fi 블럭사이에 수백줄의 코드가 들어가야 하는 경우도 있습니다.

얼마전 그런일이 있어서 이것을 펙토리 패텬으로 바꿔 볼까 해서 시작했는데...

상품을 관리하는 프로그램인데 상품이 딱 정해진게 아니고 계속 늘어나는 상황입니다. 편의상 A상품, B상품, C상품... E상품 이렇게 향후 계속 상품이 늘어나는 상황인데 A상품과 B상품은 로직의 유사도가 높아서 하나의 ProductService 라는 클래스에 담았는데 C상품은 기본적인 내용외에 처리해야 하는 로직이 상당히 다른 형태였습니다. 앞서 말한대로 if ~ else if ~ else 형태로 가기에는 소스가 너무 지저분 해지는 상황이 되었습니다.

ProductController.java

@Autowired 
ProductService productService; 

@RequestMapping("/productInfo") 
@ResponseBody 
public ResponseEntity<Map<String,Object> productInfo ( 
             @RequestParam(value="prodCd", required=true) String prodCd) { 

    return new ResponseEntity(productService.getProductInfo(prodCd), HttpStatus.OK); 
} 

 

ProductService.java

public Map<String,Object> getProductInfo(String prodCd) { 

    Map<String,Object> productInfo = null; 

    // 공통 처리 로직 

    switch (prodCd) { 
        case "A" : 
            // A상품 로직 
            break; 
        case "B" : 
            // B상품 로직 
            break; 
        case "C" : 
            // C상품 로직 
            break; 
         case ... 
    } 

    return productInfo; 
} 

대충 이런 상황이었습니다.

상품별로 처리해야 하는 로직이 늘어나거나, 상품에 대한 공통 로직과 상품개별 조직을 번갈아 가면서 수행해야 하는 경우 중간 중간에 계속 분기문이 나타나서 엄청 지저분하고 가족성이 떨어지는 코드가 되어 버렸습니다.

결국 상품 서비스 인터페이스를 만들고 상품별로 각기 다른 서비스를 구현체로 분리하고 간단하게 Factory Pattern을 적용해서 콘트롤러에서 상품 서비스 인터페이스를 이용하여 호출 하는 방식으로 구현하기로 생각했습니다.

ProductService.java (상품 서비스 인터페이스)

public interface ProductService { 
    public Map<String,Object> getProductInfo(String prodCd); 
} 

 

AProductServiceImpl.java (A상품 서비스 구현체)

@Autowired 
ProductDao productDao; 

public class AProductServiceImpl implements ProductService { 
    @Override 
    public Map<String,Object> getProductInfo(String prodCd) { 

        // 로직 처리 

        Map<String,Object> productInfo = productDao.getProductInfo(prodCd); 

        // 로직 처리 

        return productInfo; 
    } 
} 

 

그리고 이 서비스들을 상품코드에 따라 골라쓸 ProductServiceFactory.java

public ProcuctService getService(String prodCd) { 
    switch (prodCd) { 
        case "A" : return new AProductService(); 
        case "B" : return new BProductService(); 
        case "C" : return new CProductService(); 
    } 
} 

 

콘트롤러의 내용입니다.

ProductService productService; 

@RequestMapping("/productInfo") 
@ResponseBody 
public ResponseEntity<Map<String,Object> productInfo ( 
             @RequestParam(value="prodCd", required=true) String prodCd) { 

    return new ResponseEntity(ServiceFactory.getService(prodCd).getProductInfo(prodCd), HttpStatus.OK); 
} 

대략 이런 식으로 구현했던 것 같은데. 기억을 더듬어 급하게 만든거라 생략된 부분도 많고 실제 동작하는 소스는 아닙니다.

그런데 별로 마음에 안드는게... 상품이 신규로 늘어날 때 마다 상품별 Service Class만 새로 만들어 주면 되는게 아니고, 몇줄 안되기 하지만 ProductServiceFactory.java에 코드를 몇출 추가해햐 합니다. 좋은 방법이 없을까 고민하다가 런타임에 클래스를 동적으로 로딩할 수 있는 Class.forName(...) 이 생각났습니다.

Class.forName(...)은 낮설지가 않은게 Java를 처음 배울 때 JDBC 연결을 처음 구현하면서 대부분의 개발자들이 사용해 보는 방식입니다.

Class.forName("com.mysql.jdbc.Dirver");

이런 코드 생각나시죠?


Service Factory를 바꿨습니다.

public ProcuctService getService(String prodCd) { 
    return Class.forName("mypackage.service."+prodCd+"ProductService"); 
} 

정확한 코드는 기억이 안나고 이렇게 비슷하게... 했던 것 같네요.

그리고 실행을 해보니 아뿔싸... ProductService에 Autowired로 선언되어 있는 prodDao가 null 입니다.

그렇습니다 스프링의 Autowired 어노테이션은 스프링이 처음 실행되면서 콤포넌트 스켄에 지정된 위치의 콤포넌트 들을 메모리에 로드하면서 이 어노페이션을 만나면 해당 프로포티를 자동으로 초기화 시켜주는 역활을 합니다.

디버그 모드로 톰켓을 올리면 우리가 만든 Controller, Service, Dao 들이 메모리에 올라가는 것이 콘솔창에 보입니다.

제가 사용하고자 하는 방식은 스프링 런타임 중에 사용자의 선택에 따라 실시간으로 클래스를 로딩하는 방식을 생각했기 때분에 내부에 Autowired 어노테이션은 작동하지 않은 것입니다.

Autowired 어노테이션을 사용하지 않고 productDao를 new 로 생성해서 사용하는 방법도 있었지만 웬지 후져 보이고 이런 생각을 나만 한것이 아닐 것 이라 생각하고 여러가지 키워드로 구글링을 해보았습니다.

생각보다 원하는 방식의 예제를 찾기가 매우 어려웠습니다. 혹시 스프링에서 다른 더 좋은 깔끔한 방식이 있는 것인지? 아니면 동적으로 클래스를 로드하는 것에 대한 성능이슈가 있는 것인지는 모르겠지만, 상당히 오랜 기간 이런 저런 키워드로 구글링을 한 기억이 납니다. 역시나 Stackoverflow 에서 원하는 답을 찾았습니다.

org.springframework.beans.factory.config.AutowireCapableBeanFactory

이런 놈이 있었네요, 이놈을 가지고 이렇게 만들어 봤습니다.

ProductServiceFactory.java 에 넣으려고 하다가... 클래스의 Full 패키지 네임을 넣으면 해당 빈을 리턴하는 공통용으로 만들어 보자 싶어서 

DynamicAutowiredImplementsService.java

@Service 
public class DynamicAutowiredImplementsService { 

    @Autowired 
    private AutowireCapableBeanFactory beanFactory; 
     
    public Object getBean(String beanFullName) { 
        Object bean = null; 
         
        try { 

            Class<?> beanClass = Class.forName(beanFullName); 
             
            bean = beanFactory.createBean(beanClass, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false); 
            beanFactory.initializeBean(bean, beanFullName); 
             
        } catch (Exception e) { 
            throw new IllegalArgumentException("Couldn't instantiate class " + beanFullName, e); 
        } 
        return bean; 
    } 
}

 

그리코 콘트롤러에서 

@Autowired 
DynamicAutowiredImplementsService dynamicAutowiredImplementsService;

//ProductService productService; 

@RequestMapping("/productInfo") 
@ResponseBody 
public ResponseEntity<Map<String,Object> productInfo ( 
             @RequestParam(value="prodCd", required=true) String prodCd) { 

    ProductService productService = dynamicAutowiredImplementsService("mypackage.service."+prdtCd+"ProductService");
    return new ResponseEntity(productService.getProductInfo(prodCd), HttpStatus.OK); 
} 

이렇게 했습니다.

결과는? 잘~ 동작합니다.

추가 (커뮤니티에 올리니까 바로 문제점을 지적해 주시는 고수 분들이 계시네요, 콘트롤러에서 ProductService 를 전역변수로 사용하면서 경합이 발생할 경우 문제가 될 수 있습니다. 이현재 ProductService 는 매소드 내에 지역변수로 이동 했고 DynamicAutowiredImplementsService 역시 경합이 발생할 경우 문제가 될 수 있습니다. 이것은 어떻게 할 지 고민을 좀 해야봐 할 것 같습니다.)

부하 테스트까지 해보지 않아서... 사실 Autowired로 주입한 것과 어떤 차이가 있는지 검증은 못해 봤습니다. 그리고 스프링에서 Polymophism을 구현하는 방식으로 이렇게 하는 것이 바람직 한 것인지는 짧은 식견으로 좀 더 연구를 해봐야 겠지만... 만들고 있는 어플리케이션이 동시접속자가 많다기 보다는 로직이 매우 복잡하고 다양한 케이스 였기 때문에 상품이 추가 될 때 Service쪽만 새롭게 만들어 주면 되어서 유지보수성이 많이 향상된 것 같습니다.

보시는 분 중에 혹시라도 문제점이나 잘못된 곳이 있다면 지적 부탁드립니다.

읽어주셔서 감사합니다.

서블릿 개발하기 연재목록

#1 개발환경 구축 (https://opensrc.tistory.com/180?category=475522)

#2 프로잭트 생성 (https://opensrc.tistory.com/181?category=475522)

#3 첫번째 JSP 파일 만들기 (https://opensrc.tistory.com/182?category=475522)

#4 첫번째 서블릿 만들기 (https://opensrc.tistory.com/183?category=475522)

#5 포스트 요청을 처리하는 서블릿 만들기 (https://opensrc.tistory.com/203?category=475522)

#6 GET/POST 요청과 함께 파라메터 전달 하기 (https://opensrc.tistory.com/204?category=475522)

#7 Servlet 들여다 보기 (https://opensrc.tistory.com/206?category=475522)

===========================================================

 

이 포스트를 처음 시작한 이유는 요즘 Java Web Application 개발을 가르치는 서적이나 교육기관들을 보면 Java Web Application 의 기본인 Servlet 을 가르치지 않고 처음부터 Spring 같은 잘 만들어진 프레임워크를 이용하여 시작합니다.

실제 현장에서 개발을 할 때도 프레임워크를 사용하지 않고 HttpServlet 을 상속받아서 사용하는 일은 거의 없습니다. 물론 그래도 상관 없을 만큼 국내 Java 기반의 프로잭트의 대부분은 Spring 기반으로 되어 있습니다. 심지어 공공에서 많이 쓰이는 전자정부 프레임워크 조차 Spring기반입니다.

사실 기본적인 Servlet 형태를 모르고 Spring만 잘 해도 개발 하는데 문제가 없겠지만 가끔 Spring Application 은 어떻게 동작 할까... 하는 생각이 들 때가 있을 것 같아서 기본적인 HttpServlet 을 확장해서 Spring MVC Application 과 비슷한 형태로 만들어 보는 것이 목적입니다. 물론 Spring 처럼 수많은 사람들에 의해서 만들어 지고 검증된 프레임워크 처럼 편하고 뛰어난 기능을 모두 갖추기는 어렵지만 Spring은 물론 Java MVC Web Application이 어떻게 동작하는지 이해하는데 도움이 되었으면 좋겠다는 생각에 시작했습니다.

사설이 길었습니다.

 

[서블릿 개발하기] #7 Servlet 들여다 보기

이전 시간까지 우리가 만든 HelloWorldServlet은 javax.servlet.http.HttpServlet을 상속받았습니다.

HttpServlet 의 소스코드는

https://github.com/javaee/servlet-spec/blob/master/src/main/java/javax/servlet/http/HttpServlet.java

 

javaee/servlet-spec

The API and Issue Tracker for the JCP Standard Java Servlet Specification - javaee/servlet-spec

github.com

이곳에서 보실 수 있습니다.

 

HttpServlet 클래스 선언부에 보면 javax.servlet.GenericServlet 을 상속 받은것을 알 수 있습니다.  GenericServlet 의 소스는 이곳에서 살펴 보실 수 있습니다.

https://github.com/javaee/servlet-spec/blob/master/src/main/java/javax/servlet/GenericServlet.java

 

javaee/servlet-spec

The API and Issue Tracker for the JCP Standard Java Servlet Specification - javaee/servlet-spec

github.com

GenericServlet 은 javax.servlet.Servlet, javax.servlet.ServletConfig, java.io.Serializable 의 구현체 이네요 관심있으신 분은 소스를 더 살펴보셔도 좋습니다. 이 포스트에서는 HttpServlet까지만 살펴보겠습니다.

 

 

우리가 만든 HelloWorldServlet 의 GET Request와 POST 요청을 처리하는 goGet(), doPost() 메소드는 상속받은 HttpServlet 의 doGet과 doPost를 각각 재정의(Override)한 것입니다. HttpServlet.java 소스에 각 메소드의 원형을 살펴보면

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException

이렇게 생겼습니다.

사용자로부터 들어오는 요청에 대한 정보는 HttpServletRequest  req 에 담아서 전달 됩니다.

사용자에게 응답해야 하는 정보는 HttpServletResponse resp 에 담아주면 됩니다.

우리가 만든 doGet, doPoat는 이것을 재정의 한 것 입니다. 꼭 있어야 하는 것은 아니지만 우리가 만든 HelloWordServlet.java가 컴파일 될 때 부모 클래스의 기능을 Override 한 것이라고 표시를 해주면 좋을 것 같습니다.

	@Override 
	public void doGet(HttpServletRequest request, HttpServletResponse response)
    	throws ServletException, IOException {

		...

	}
    
 	@Override 
	public void doPost(HttpServletRequest request, HttpServletResponse response)
    	throws ServletException, IOException {

		...

	}  

이렇게 어노테이션을 붙혀주면 더 보기좋네요.

다시 위에 HttpServlet.java 소스를 살펴보면 GET과 POST 외에도 PUT, DELETE 같은 요청을 처리하는 doPut, doDelete 같은 메소드도 있습니다. 전에 말씀드린 대로 POST 요청에 대해 잘 이해를 하신다면 PUT과 DELETE를 사용하시는 데도 문제가 없을 것 같습니다.

다른 것들은 건너 뛰고 좀 더 살펴보면 service 라는 메소드가 보입니다.

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
        ...
    }

이렇게 생겼네요 설명을 보면 

Receives standard HTTP requests from the public service method and dispatches them to the doXXX methods defined in this class...

아마도 doGet, doPost 같은 요청을 이 매소드가 대신 처리 해준다는 소리 같습니다.

소스를 조금 살펴 보면

    protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
        ... 생략...
        	doGet(req, resp);
        ... 생략...
        } else if (method.equals(METHOD_HEAD)) {
        ... 생략...
            doHead(req, resp);
        ... 생략...
        } else if (method.equals(METHOD_POST)) {
        ... 생략...
            doPost(req, resp);
        ... 생략...
        } else if (method.equals(METHOD_PUT)) {
        ... 생략...
            doPut(req, resp);
        ... 생략...
        } else if (method.equals(METHOD_DELETE)) {
        ... 생략...
            doDelete(req, resp);
        ... 생략...
        } else if (method.equals(METHOD_OPTIONS)) {
        ... 생략...
            doOptions(req,resp);
        } else if (method.equals(METHOD_TRACE)) {
        ... 생략...
            doTrace(req,resp);
        ... 생략...
        } else {
        ... 생략...
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }

request 의 메소드를 확인해서 각각을 처리 하는게 보입니다.

정말 그렇게 되는가 우리가 만든 HelloWorldServlet 에 적용해 보도록 하겠습니다.

service 메소드가 작동 하려면 기존에 있던 doGet, doPost 는 삭제 해야 합니다.

package com.tistory.opensrc.basicservlet.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 첫번째 hello, world 서블릿
 * @author cloud
 *
 */
public class HelloWorldServlet extends HttpServlet {

	/**
	 * serial version id
	 */
	private static final long serialVersionUID = 701289870660846795L;

	/**
	 * 사용자 요청을 처리
	 * @param request
	 * @param response
	 * @throws ServletException
	 * @throws IOException
	 */
	@Override
	public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		response.setContentType("text/html"); // text html 형태로 출력 하겠다고 지정함
		PrintWriter out = response.getWriter(); // response로 부터 출력 장치를 확보
		
		String yourName = request.getParameter("your_name"); // request에서 화면으로 부터 넘어온 파라메터의 값을 추출 합니다. 
		
		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world<br/>");
		out.println("method : " + request.getMethod() + "<br/>");
		out.println("You'r name is " + yourName);
		out.println("</html>");
	}
}

이렇게 바꿔봤습니다.

GET Request

POST Request

네 잘 동작 합니다.

특별히 GET, POST 중 어느 한가지만 처리 하려는 목적이 아니라면 service 를 사용하면 둘다 하나의 로직으로 처리 할 수 있어서 로직을 추가 하기 용이하고 혹시 GET이나 POST 같은 특정 요청만 처리 하겠다고 한다면 위의 소스에서 처럼 request.getMethod()를 받아서 분기 처리 해주시면 될 것 같습니다.

다음 시간에는 Java Web Application에서 한글 처리에 대해 이야기 해보도록 하겠습니다.

서블릿 개발하기 연재목록

서블릿 개발하기 연재목록

#1 개발환경 구축 (https://opensrc.tistory.com/180?category=475522)

#2 프로잭트 생성 (https://opensrc.tistory.com/181?category=475522)

#3 첫번째 JSP 파일 만들기 (https://opensrc.tistory.com/182?category=475522)

#4 첫번째 서블릿 만들기 (https://opensrc.tistory.com/183?category=475522)

#5 포스트 요청을 처리하는 서블릿 만들기 (https://opensrc.tistory.com/203?category=475522)

#6 GET/POST 요청과 함께 파라메터 전달 하기 (https://opensrc.tistory.com/204?category=475522)

#7 Servlet 들여다 보기 (https://opensrc.tistory.com/206?category=475522)

===========================================================

 

 

[서블릿 개발하기] #6 GET/POST 요청과 함께 파라메터 전달 하기

우리가 사용하는 웹 어플리케이션은 데이터 입력 없이 자체 만으로 컨텐츠를 표시하는 경우도 있지만 대부분 사용자로 부터 데이터를 입력 받아서 처리를 하고 결과를 보여주는 경우가 대부분입니다.

예를 들어 이 블로그의 특정 게시물을 읽게 되면 https://opensrc.tistory.com/203?category=475522 이렇게 생긴 도메인 주소와 URI, 파라메텨들이 서버로 전송(Request)이 되고 서버는 그것을 받아서 URI 부분을 파싱하고 URI에 해당하는 어플리케이션에 파라메터를 전달하고 어플리케이션은 그 파라메터에 맞는 데이터를 가공하여 다시 브라우저로 응답(Response)되는 구조입니다.

이제 우리가 만들었던 어플리케이션에 사용자가 입력한 값을 받아서 처리 하는 것을 구현해 보도록 하겠습니다.

index.jsp 에 다음 내용을 추가 합니다.

What is your name ? <input type="text" name="my_name"/><br/>

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
hello, world</br>
<form action="/hello" method="POST">
	What is your name ? <input type="text" name="your_name"/><br/>
	<input type="submit" value="Submit"/>
</form>
</body>
</html>

http://localhost 로 접속해 보면 아래와 같은 화면이 나옵니다.

우리가 추가한 입력창 의 이름을 your_name 으로 지정 한것을 기억하고 서버에서 저 값을 받아서 화면에 보여주도록 해야 합니다.

POST Request  파라메터 값 추출

우리가 만든 화면은 아직 POST로 호출 하므로 HelloWorldServlet의 doPost 메서드를 먼저 수정해 보겠습니다. 아래 소스를 잘 보시고 두줄을 추가 합니다.

String yourName = request.getParameter("your_name"); // request에서 화면으로 부터 넘어온 파라메터의 값을 추출 합니다. 

out.println("You'r name is " + yourName);

package com.tistory.opensrc.basicservlet.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 첫번째 hello, world 서블릿
 * @author cloud
 *
 */
public class HelloWorldServlet extends HttpServlet {

	/**
	 * serial version id
	 */
	private static final long serialVersionUID = 701289870660846795L;

	/**
	 * GET 요청을 처리
	 * @param request
	 * @param response
	 * @throws ServletException
	 * @throws IOException
	 */
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		response.setContentType("text/html"); // text html 형태로 출력 하겠다고 지정함
		PrintWriter out = response.getWriter(); // response로 부터 출력 장치를 확보
		
		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world<br/>");
		out.println("method : " + request.getMethod());
		out.println("</html>");
	}
	

	/**
	 * POST 요청을 처리
	 * @param request
	 * @param response
	 * @throws ServletException
	 * @throws IOException
	 */
	public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		response.setContentType("text/html"); // text html 형태로 출력 하겠다고 지정함
		PrintWriter out = response.getWriter(); // response로 부터 출력 장치를 확보
		
		String yourName = request.getParameter("your_name"); // request에서 화면으로 부터 넘어온 파라메터의 값을 추출 합니다. 
		
		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world<br/>");
		out.println("method : " + request.getMethod() + "<br/>");
		out.println("You'r name is " + yourName);
		out.println("</html>");
	}
}

이제 화면에서 이름을 입력해 보겠습니다.

아래와 같이 나왔다면 성공입니다.

주의깊게 살펴 보셔야 할 것은 브라우저 주소창에 localhost/hello 만 표시된다는 것 입니다.

이제 doGet 메서드에도 같은 코드를 추가해 보겠습니다.

package com.tistory.opensrc.basicservlet.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 첫번째 hello, world 서블릿
 * @author cloud
 *
 */
public class HelloWorldServlet extends HttpServlet {

	/**
	 * serial version id
	 */
	private static final long serialVersionUID = 701289870660846795L;

	/**
	 * GET 요청을 처리
	 * @param request
	 * @param response
	 * @throws ServletException
	 * @throws IOException
	 */
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		response.setContentType("text/html"); // text html 형태로 출력 하겠다고 지정함
		PrintWriter out = response.getWriter(); // response로 부터 출력 장치를 확보

		String yourName = request.getParameter("your_name"); // request에서 화면으로 부터 넘어온 파라메터의 값을 추출 합니다. 
		
		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world<br/>");
		out.println("method : " + request.getMethod());
		out.println("You'r name is " + yourName);
		out.println("</html>");
	}
	

	/**
	 * POST 요청을 처리
	 * @param request
	 * @param response
	 * @throws ServletException
	 * @throws IOException
	 */
	public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		response.setContentType("text/html"); // text html 형태로 출력 하겠다고 지정함
		PrintWriter out = response.getWriter(); // response로 부터 출력 장치를 확보
		
		String yourName = request.getParameter("your_name"); // request에서 화면으로 부터 넘어온 파라메터의 값을 추출 합니다. 
		
		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world<br/>");
		out.println("method : " + request.getMethod() + "<br/>");
		out.println("You'r name is " + yourName);
		out.println("</html>");
	}
}

 

그리고 사용자 요청을 GET으로 변경합니다.

index.jsp 에 <form action="/hello" method="POST"> 부분을 GET으로 변경합니다.

그리고 주소창에 http://localhost/ 를 다시 입력하고 이름을 입력하고 Submit 을 클릭해 봅니다.

POST 요청에서는 주소창에 localhost/hello 만 표시되었던 반면, GET 요청에서는 localhost/hello?your_name=CloudNine 라고 ? 뒷 부분에 파라메터 명과 값이 표시되어 있습니다. F5 키를 눌러서 새로고침 하거나 다른 브라우저 창을 열고 복사해서 붙혀 넣어도 똑같은 화면이 출력 됩니다.

그럼 다시 index.jsp의 <form ... method="GET"/> 부분을 POST 로 바꾸고 http://localhost/ 를 호출해서 값을 전송해 보겠습니다. 그리고 GET요청 화면에서 했던 것 처럼 F5 키를 눌러보면

이런 화면을 만나게 됩니다 '계속' 버튼을 누르면 이전 요청에 값들을 이용해서 다시 처리 하겠지만. GET요청 처럼 바로 실행되지는 않습니다. 그리고 주소창의 내용을 복사해서 다른 브라우저 창에 붙혀 넣으면

GET 요청에 You;'s name is null 로 표시됩니다.

브라우저 주소창에 주소를 직접 입력하는 것은 GET 요청으로 간주 된다는 것을 기억하시기 바랍니다.

 

 

여기까지 간단한 서블릿 개발에 대해 알아봤습니다. 지금 까지 한 것을 조금 복잡하게 이야기 하면 JSP Model 1 방식의 Java Application 이라고 할 수 있습니다.

 

6번의 연재에서 기초적인 것은 제외 하고 

  • JSP는 수정후 최초 실행시 Servlet 형태로 변환된후 컴파일 되어 (Tomcat 같은)Application Server에 적재 되어 실행 된다는 점.
  • 모든 Servlet 은 javax.servlet.http.HttpServlet 을 상속 받는다는 점
  • GET, POST 요청의 차이점과 그 목적이 GET은 읽기 요청에 사용되고 POST는 쓰기/수정/삭제 등의 특성에 맞게 고안되어 있다는 것

이정도 이해 하시고 기억하시면 될 것 같습니다.

여기까지 서블릿 개발하기 기초 편을 마무리하고

차후에 시간이 되는 대로 JSP Model 1 에서 확장해서 JSP Model 2 Java Web Application, 우리가 흔히 말하는 Java MVC 방식에 대해 알아보도록 하겠습니다.

 

감사합니다.

서블릿 개발하기 연재목록

#1 개발환경 구축 (https://opensrc.tistory.com/180?category=475522)

#2 프로잭트 생성 (https://opensrc.tistory.com/181?category=475522)

#3 첫번째 JSP 파일 만들기 (https://opensrc.tistory.com/182?category=475522)

#4 첫번째 서블릿 만들기 (https://opensrc.tistory.com/183?category=475522)

#5 포스트 요청을 처리하는 서블릿 만들기 (https://opensrc.tistory.com/203?category=475522)

#6 GET/POST 요청과 함께 파라메터 전달 하기 (https://opensrc.tistory.com/204?category=475522)

#7 Servlet 들여다 보기 (https://opensrc.tistory.com/206?category=475522)

===========================================================

 

너무 오랫만에 포스팅을 하게 되었습니다.

스프링 같은 Java Web Application개발용 프레임워크를 이용하지 않고 옛날 방식의 Servlet 개발 방식을 살펴보고 있습니다. 물론 요즘에는 거의 사용되고 있지 않지만 스트러츠나 스프링 같은 고도화된 프래임워크의 작동 방식을 이해하는데 꼭 필요하다는 생각에서 스프링 개발을 진행하기 전에 또는 스프링 기반의 개발 방식에 익숙한 분들께 도움이 될까... 하는 생각에 시작한 연재입니다.

 

HTTP Method 이해

일반적으로 우리가 브라우저 창에 주소를 치거나 웹페이지에서 링크를 클릭해서 화면을 표시하는 방식을 GET 방식이라고 합니다.

다른 페이지에서 우리가 만든 페이지를 찾아오기 쉽게 주소를 기술해 놓은 것을 브라우저 주소창에 붙혀넣거나 클릭하는 것 만으로 해당페이지에 접근 할 수 있는 방식입니다.

이미 모든 분들이 누군가에게 어느 사이트의 주소를 알려줄 때 많이 사용해 보았을 것입니다.

HTTP 프로토콜에는 GET 외에도 POST, PUT, DELETE, HEAD, OPEIONS... 등등 몇가지 방식이 있습니다. 실질적으로 어느 방식을 쓰든 클라이언트(브라주저)에서 서버쪽으로 요청을 보내는 것은 동일하지만 전달하는 방식은 약간 차이가 있습니다.

그 차이는 차차 알아가시면 되고 여기서 설명 할 것은 GET과 POST의 차이만을 설명하도록 하겠습니다.

우선 GET방식의 요청은 브라우저 주소창에 해당 컨텐츠의 주소와 파라메터 까지 그대로 기술 됩니다. 그 주소 만으로 해당 컨텐츠가 온전하게 표시 될 수 있어야 합니다.

반면 POST 방식은 해당 컨텐츠의 주소와 URI 까지만 표시를 하고 파라메터와 그 값은 주소창에 표시 되지 않습니다.

이런 특성을 잘 이용해서 개발을 한다면 GET방식은 읽기에 유용하고 POST는 서버로 데이터를 전송해서 저장, 삭제 하는 기능에 유용합니다.

만일 어느 게시판의 글을 읽는 기능을  GET 방식으로 http://somesite.co.kr/read?artecleId?=10  와 같이 구현 했다면 이것만 복사 해서 카카오톡에도 붙혀넣고 블로그에도 붙혀넣고 누구든 (글을 읽을 수 있는 권한이 있는 경우) 저 주소만 클릭하면 글을 볼수가 있습니다. 

만일 삭제하는 기능도 역시 GET방식으로 http://somesite.co.kr/delete?artecleId?=10 이렇게 구현 했다면 저 주소를 브라우저 창에 입력해서 실행 하는 것 만으로 글 삭제를 시도 할 것입니다.(물론 삭제 할 수 있는 권한이 있는 경우에 그렇게 되겠죠)

그래서 주로 GET은 읽기 엑션에 적당하고 POST, PUT 같은것은 저장/수정, DELETE는 삭제 액션에 적당한 방식입니다.

앞서도 말했지만 여기서는 GET과 POST만을 다루겠습니다. POST를 이해하시면 PUT, DELETE를 사용하시는데 문제가 없습니다.

 

Request Method 확인 하기

이전 포스팅에서 우리가 만들었던 hello world 서블릿은 사용자로부터 GET 방식의 요청을 처리하는 매서드만 가지고 있습니다. 이제 사용자 요청이 POST로 들어오는 것을 처리할 매서드를 추가해 보도록 하겠습니다.

우선 우리가 만들었던 HelloWorldServlet.java 의 doGet(...) 메서드가 정말 GET방식의 요청인지 확인해보도록 하겠습니다.

HelloWorldServlet.java

package com.tistory.opensrc.basicservlet.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 첫번째 hello, world 서블릿
 * @author cloud
 */
public class HelloWorldServlet extends HttpServlet {
	
	/**
	 * serial version id
	 */
	private static final long serialVersionUID = 701289870660846795L;

	/**
	 * GET 요청을 처리
	 * @param request
	 * @param response
	 * @throws ServletException
	 * @throws IOException
	 */
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		
		response.setContentType("text/html"); // text html 형태로 출력 하겠다고 지정함
		PrintWriter out = response.getWriter(); // response로 부터 출력 장치를 확보
		
		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world");
		out.println("method : " + request.getMethod()); // 요청 메서드 확인
		out.println("</html>");
	}
}

out.println("out.println("method : " + request.getMethod()); // 요청 매서드 확인

코드를 한줄 추가 합니다.

HttpServletRequest request 의 getMethod() 는 Request 헤더를 파싱해서 사용자 요청의 Method를 확인합니다.

이제 서블릿을 호출 해 봅니다.

위와 같은 결과가 나왔으면 정상적으로 처리 된 것입니다.

위와 같이 나오지 않는다면 [서블릿 개발하기] #1 개발환경 구축 편의 4. 서버 실행 파트를 참고 해서 서버를 실행 하셔야 합니다.

POST 로 요청(Request) 하기

이제 우리가 만든 서블릿을 POST로 호출 하도록 준비를 해보겠습니다. POST로 호출하기 위해서는 간단한 페이지를 만들어야 합니다. [서블릿 개발하기] #3 첫번째 JSP 파일 만들기에서 처음 만들었던 hello, world 를 출력하는 index.jsp 파일을 수정해서 만들어 보겠습니다.

<!@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
hello, world</br>
<form action="/hello" method="POST">
	<input type="submit" value="Submit"/>
</form>
</body>
</html>

우리가 출력했던 hello, world 아래에 아무것도 없는 폼 엘리먼트를 하나 추가 하고 액션은 /hello 매서드는 POST로 지정했습니다. Submit 버튼을 클릭하면 우리가 만든 HelloWorldServlet POST Request를 호출 하겠다는 이야기 입니다.

action="/hello" 가 어떻게 HelloWorkdServlet 과 연결이 되는지 이해가 안가시는 분은 [서블릿 개발하기] #4 첫번째 서블릿 만들기 를 다시 한번 보시기 바랍니다.

 

실행을 하면 이런 결과가 나옵니다.

Submit 버튼을 클릭해 봅니다.

우리가 만든 HelloWorldServlet에는 GET 요청만을 처리하는 doGet() 메서드만 추가되어 있기 때문에 아래와 같이 

HTTP Status 405 - HTTP method POST is not supported by this URL

에러가 나오고 있습니다.

이제 HelloWorldServlet에 POST Method Request 에 대응하는 메서드를 추가하도록 하겠습니다.

아래 내용처럼 HelloWorldServlet.java 에 doGet 메서드를 복사해서 doPost 메서드를 추가 합니다.

package com.tistory.opensrc.basicservlet.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 첫번째 hello, world 서블릿
 * @author cloud
 *
 */
public class HelloWorldServlet extends HttpServlet {

	/**
	 * serial version id
	 */
	private static final long serialVersionUID = 701289870660846795L;

	/**
	 * GET 요청을 처리
	 * @param request
	 * @param response
	 * @throws ServletException
	 * @throws IOException
	 */
	public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		response.setContentType("text/html"); // text html 형태로 출력 하겠다고 지정함
		PrintWriter out = response.getWriter(); // response로 부터 출력 장치를 확보
		
		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world<br/>");
		out.println("method : " + request.getMethod());
		out.println("</html>");
	}
	

	/**
	 * POST 요청을 처리
	 * @param request
	 * @param response
	 * @throws ServletException
	 * @throws IOException
	 */
	public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		response.setContentType("text/html"); // text html 형태로 출력 하겠다고 지정함
		PrintWriter out = response.getWriter(); // response로 부터 출력 장치를 확보
		
		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world<br/>");
		out.println("method : " + request.getMethod());
		out.println("</html>");
	}
}

 

이제 톰켓 서버를 재시작 하고

앞의 http://localhost 화면에서 Submit 버튼을 클릭해 봅니다.

정상적으로 POST 요청에 대한 응답이 성공하였습니다.

다음 편에서는 GET/POST 요청에 변수(Parameter)를 전송하는 방법을 알아보겠습니다.

 

서블릿 개발하기 연재목록

#1 개발환경 구축 (https://opensrc.tistory.com/180?category=475522)

#2 프로잭트 생성 (https://opensrc.tistory.com/181?category=475522)

#3 첫번째 JSP 파일 만들기 (https://opensrc.tistory.com/182?category=475522)

#4 첫번째 서블릿 만들기 (https://opensrc.tistory.com/183?category=475522)

#5 포스트 요청을 처리하는 서블릿 만들기 (https://opensrc.tistory.com/203?category=475522)

#6 GET/POST 요청과 함께 파라메터 전달 하기 (https://opensrc.tistory.com/204?category=475522)

#7 Servlet 들여다 보기 (https://opensrc.tistory.com/206?category=475522)

===========================================================




이제 첫번째 서블릿을 만들어 보겠습니다.

좀 식상하긴 하지만 화면에 hello, world 라고 출력하는 서블릿을 만들어 보도록 하겠습니다.

많은 프로그래밍 서적에 처음 작성하는 프로그램의 단골로 등장하는 프로그램입니다.


hello, world 는 UNIX OS를 만든 Dennis M. Ritchie가 UNIX를 만들기 위해 만든 C언어를 대중에 소개하기 위해 1978년에 쓴 The C PROGRAMMING LANGUAGE 라는 책의 1장에 나오는 프로그램입니다. 

#include <studio.h>
main()
{
    printf("hello, world\n");
}

마치 이후로 40년이 넘는 세월 동안 수 많은 시스템을 만드는 언어가 될 것을 예고라도 하듯이 '세상아 안녕' 하는 느낌입니다. 


이것을 순수하게 현대의 자바 코드로 바꾼다면

class HelloWorld
{
    public static void main(String[] args)
    {
        System.out.print("hello, world\n");
    }
}

이정도 코드로 만들면 될 것 같습니다. 우리는 이것을 HelloWorldServlet.java 라는 이름의 서블릿 프로그램으로 작성해 보도록 하겠습니다.

Java 코드를 먼저 보여드린 이유는 서블릿은 많은 플렛폼과 통신을 해야 하는 프로그램이기 때문에 Java 코드 몇 줄로 뚝딱 나오지 않는 다는 것을 비교하기 위해서 입니다.


각설 하고 이제 HelloWorldServlet.java 를 생성해 보도록 하겠습니다.

먼저 서블릿을 담을 패키지를 먼저 생성하겠습니다. 저는 이 블로그 도메인을 기준으로 com.tistory.opensrc.basicservlet.servlet 으로 하겠습니다.

이클립스 Project Explorer에서 지난시간에 생성한 프로잭트에 오른쪽 마우스 클릭하여 New > Package 메뉴를 를 선택합니다.

패키지명에 원하시는 패키지명을 입력하세요.


패키지가 생성되었으면 서블릿을 만들 겠습니다.

생성된 패키지 오른쪽 마우스 클릭 New > class 메뉴 선택

아래 화면처럼 Name : HelloWorldServlet 라고 입력 합니다.

부모 클래스를 지정해 주도록 하겠습니다.

Superclass : 오른쪽으 [Browse...] 버튼을 클릭합니다.


Choose a type : HttpServlet 입력하면

Matching items 에 아래처럼 목록이 나옵니다.

HttpServlet - javax.servlet.http 선택 후 [OK] 클릭


[Finish]를 클릭 하면 아래와 같은 모양의 첫번째 서블릿이 생성됩니다.



자동으로 생성된 코드인데도 이상하게 HelloWorldServlet 에 warning 표시인 노란색 및줄과 라인번호 앞에 느낌표가 켜져있습니다.

내용을 확인해 보면... 시리얼 버전 ID를 입력하라는 경고입니다.

이것은 java.io.Serializable 인터페이스를 구현(implements)하는 클래스에 나타나는 특성입니다.

API를 확인해 보면 HttpServlet 이 java.io.Serializable 를 implemented 되었음을 알 수 있습니다.

(https://docs.oracle.com/javaee/7/api/javax/servlet/http/HttpServlet.html)


위 메뉴에서 Add generated serial version ID 를 클릭해서 ID를 생성해도 되고 그냥 실행하면 JVM이 자동 생성해서 붙혀준답니다.

자세한 내용은 http://hyeonstorage.tistory.com/253 이것에 잘 설명이 되어 있으니 참고 하시길 바랍니다.



HelloWorldServlet.java

package com.tistory.opensrc.basicservlet.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 첫번째 hello, world 서블릿
 * @author cloud
 */
public class HelloWorldServlet extends HttpServlet {
	
	/**
	 * serial version id
	 */
	private static final long serialVersionUID = 701289870660846795L;

	/**
	 * GET 요청을 처리
	 * @param request
	 * @param response
	 * @throws ServletException
	 * @throws IOException
	 */
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		
		response.setContentType("text/html"); // text html 형태로 출력 하겠다고 지정함
		PrintWriter out = response.getWriter(); // response로 부터 출력 장치를 확보
		
		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world");
		out.println("</html>");
	}
}

URL을 호출 하면 응답 하도록 GET 메소드에 응답하는 doGet 메서드를 생성 했습니다.

doGet 메서드의 원형은 사용자로 부터 요청을 담고 있는 HttpServletRequest 와 응답을 할 HttpServletResponse 객체를 인자로 가지고 ServletException 과 IOException 을 발생 할 수 있습니다.

이외 소스 내용은 주석을 참고 하시기 바랍니다.


이제 우리가 HelloWorldServlet 이 사용자 요청을 받아서 실행 될 수 있도록 설정을 해야 합니다.

간단하게 어노테이션을 이용할 수 도 있지만. 원래의 목적대로 서블릿의 근본적인 부분을 알아가기 위해  web.xml 에 직접 세팅을 해보도록 하겠습니다.


WebContent\WEB-INF\web.xml

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"> <display-name>BasicServlet</display-name> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> <welcome-file>default.html</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>HelloWorld</servlet-name><!-- 시스템 에서 사용할 서블릿의 이름 --> <servlet-class>com.tistory.opensrc.basicservlet.servlet.HelloWorldServlet</servlet-class><!-- 서블릿의 패키지명 포함 클래스명 --> </servlet> <servlet-mapping> <servlet-name>HelloWorld</servlet-name><!-- 위에서 선언한 서블릿 이름 --> <url-pattern>/hello</url-pattern><!-- 대응할 URL --> </servlet-mapping> </web-app>

<servlet>~</servlet>, <servlet-mapping>~</servlet-mapping> 내용을 입력합니다.

간단히 설명하면  윗쪽 <servlet>의 두줄은 com.tistory.opensrc.basicservlet.servet.HelloWorldServlet 을 HelloWorld로 서블릿의 이름을 지정하는 것이고

아래 <servlet-mapping> 사용자로 부터 /hello URI로 호출이 들어오면 위에서 지정한 HelloWorld 서블릿을 호출 하도록 지정 하는 것입니다.

결국 사용자가 주소창에 http://serveraddress/hello 라고 입력하면 위에서 작성한 HelloWorldServlet.java 가 실행되어서 화면에 hello, world 라고 출력 하도록 설정을 하는 것입니다.


모두 이상 없이 입력 되었으면

지난 시간에 설정한 Apache Tomcat v8.0 서버를 실행한 후 브라우져를 띄워서 주소창에 http://localhost/hello 라고 입력합니다.

지금까지 스텝을 잘 읽고 따라 했다면 아래와 같은 결과 화면을 만나게 될 것입니다.


화면 바탕에 마우스 오른쪽 버튼을 누르고 페이지 소스보기를 해보면


이것은 우리가 HelloWorldServlet.java 의 doGet 메서드 안에 기술 했던

		// html 내용을 출력
		out.println("<html>");
		out.println("hello, world");
		out.println("</html>");

이부분이 브라우저에 출력된 것임을 알 수 있다.


다음 시간에는 POST 메서드에 대응하는 doPost 메서드에 대해서 알아보도록 하겠습니다.

서블릿 개발하기 연재목록

#1 개발환경 구축 (https://opensrc.tistory.com/180?category=475522)

#2 프로잭트 생성 (https://opensrc.tistory.com/181?category=475522)

#3 첫번째 JSP 파일 만들기 (https://opensrc.tistory.com/182?category=475522)

#4 첫번째 서블릿 만들기 (https://opensrc.tistory.com/183?category=475522)

#5 포스트 요청을 처리하는 서블릿 만들기 (https://opensrc.tistory.com/203?category=475522)

#6 GET/POST 요청과 함께 파라메터 전달 하기 (https://opensrc.tistory.com/204?category=475522)

#7 Servlet 들여다 보기 (https://opensrc.tistory.com/206?category=475522)

===========================================================




이제 간단한 JSP 파일을 하나 만들어 보겠습니다.

Project Explorer 에서 WebContent 디렉토리를 마우스 오른쪽 버튼으로 클릭

New > Other 클릭

Web > JSP File 선택 후 [Next >]

File Name 에 index.jsp 입력 후 [Finish] 클릭

body 와 body사이에 hello, world 라고 입력 후 저장 (Alt + S)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
hello, world
</body>
</html>


브라우저에서 첫번째 웹페이지를 확인합니다.

여기까지 잘 오셨다면 절반은 성공하신 겁니다.

시작이 반이라는 옛말에 의하면 말입니다.


심화 : JSP 라이프 사이클

지금 부터 설명하는 부분은 잘 이해가 안가시는 분은 한가지만 기억하고 건너뛰셔도 됩니다.

'JSP 파일은 실제 서버에서 실행되기 전에 Servlet 변환된 후 실행 된다.'

요것 만 이해하면 아래 내용은 건너 뛰셔도 관계 없습니다.


자 이제부터 JSP가 서블릿에서 실행되기 위해 어떤 과정을 거치는지 살펴 보겠습니다.

서버의 설정에 따라 조금 다르지만 일반적으로 JSP 파일을 새로 만들거나 파일의 내용을 수정한 후, 사용자로 부터 해당 JSP로 요청이 들어오면 서블릿 컨테이너가 JSP파일이 변경되는지 감시하고 있다가 새로 생성된 JSP 파일인 경우 해당하는 Servlet의 클래스가 존재 하지 않거나, class 파일의 생성 시간이 JSP파일의 최종 변경된 시간 보다 오래 되었다면 서블릿 컨테이너(WAS)는 JSP를 Servlet 코드로 변환한뒤 .class로 컴파일 하고 메모리로 로딩한 후에 비로서 실행합니다.

진짜 그런지 우리가 만든 JSP 파일을 살펴보도록 하겠습니다.


Server 텝에서 현재 가동하고 있는 서버를 더블클릭

Open launch configuration 클릭

Arguments 텝 클릭

VM arguments 부분에

-Dcatalina.base="..." 에 해당하는 문자열 복사

탐색기 주소창에 붙혀넣기 후 엔터

이후 다음 경로대로 하위 디렉토리를 탐색한다.

work\Catalina\localhost\ROOT\org\apache\jsp

아래 그림처럼 우리가 만든 JSP가 서블릿으로 변환된 java 파일과 그 서블릿이 컴파일된 class 파일 두개가 생성되어 있습니다.


index_jsp.java 파일의 내용을 구경하겠습니다.

/*
 * Generated by the Jasper component of Apache Tomcat
 * Version: Apache Tomcat/8.0.36
 * Generated at: 2017-12-25 14:51:46 UTC
 * Note: The last modified time of this file was set to
 *       the last modified time of the source file after
 *       generation to assist with modification tracking.
 */
package org.apache.jsp;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;

public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
                 org.apache.jasper.runtime.JspSourceImports {

  private static final javax.servlet.jsp.JspFactory _jspxFactory =
          javax.servlet.jsp.JspFactory.getDefaultFactory();

  private static java.util.Map _jspx_dependants;

  private static final java.util.Set _jspx_imports_packages;

  private static final java.util.Set _jspx_imports_classes;

  static {
    _jspx_imports_packages = new java.util.HashSet<>();
    _jspx_imports_packages.add("javax.servlet");
    _jspx_imports_packages.add("javax.servlet.http");
    _jspx_imports_packages.add("javax.servlet.jsp");
    _jspx_imports_classes = null;
  }

  private volatile javax.el.ExpressionFactory _el_expressionfactory;
  private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;

  public java.util.Map getDependants() {
    return _jspx_dependants;
  }

  public java.util.Set getPackageImports() {
    return _jspx_imports_packages;
  }

  public java.util.Set getClassImports() {
    return _jspx_imports_classes;
  }

  public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
    if (_el_expressionfactory == null) {
      synchronized (this) {
        if (_el_expressionfactory == null) {
          _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
        }
      }
    }
    return _el_expressionfactory;
  }

  public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
    if (_jsp_instancemanager == null) {
      synchronized (this) {
        if (_jsp_instancemanager == null) {
          _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
        }
      }
    }
    return _jsp_instancemanager;
  }

  public void _jspInit() {
  }

  public void _jspDestroy() {
  }

  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
        throws java.io.IOException, javax.servlet.ServletException {

final java.lang.String _jspx_method = request.getMethod();
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method) && !javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET POST or HEAD");
return;
}

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;


    try {
      response.setContentType("text/html; charset=UTF-8");
      pageContext = _jspxFactory.getPageContext(this, request, response,
      			null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;

      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("Insert title here\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("hello, world\r\n");
      out.write("\r\n");
      out.write("\r\n");
    } catch (java.lang.Throwable t) {
      if (!(t instanceof javax.servlet.jsp.SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          try {
            if (response.isCommitted()) {
              out.flush();
            } else {
              out.clearBuffer();
            }
          } catch (java.io.IOException e) {}
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
        else throw new ServletException(t);
      }
    } finally {
      _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }
}

간단한 JSP 코드가 많은 내용이 더해진 채로 변환되어 있습니다.


중요한 부분 몇가지만 확인하면... 

public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase

해당 클래스는 org.apache.jasper.runtime.HttpJspBase를 상속 받았다는 것을 알 수 습니다.

HttpJspBase 의 API 문서는 

https://tomcat.apache.org/tomcat-4.0-doc/jasper/docs/api/org/apache/jasper/runtime/HttpJspBase.html

이곳에서 볼 수 있습니다.

HttpJspBase는 javax.servlet.http.HttpServlet 을 상속 받은 서블릿이라는 것을 추측 할 수 있습니다.

소스의 자세한 부분은 본 문서의 범위를 벗어날 수 있으므로 흥미가 있는 분들만 확인하도록 하고

73라인의 _jspService 메서드에 대해서만 간단히 보겠습니다.

(실제 소스에서는 73라인 이지만 SyntaxHighlighter 설정이 잘 못되어 라인넘버가 밀려서 나옵니다.)

_jspService 메서드는 리턴값이 없으며(void)

인자로 javax.servlet.http.HttpServletRequest 와 javax.servlet.http.HttpServletResponse를 받습니다.

그리고 java.io.IOException, javax.servlet.ServletException 을 throws 할 수 있습니다.

하는 일은

response.setContentType("text/html; charset=UTF-8"); 로 페이지 케릭터 셋을 지정하고

pageContext로 부터 PrintWriter out 을 생성하고 out을 통해 hello, workd 를 출력하고 종료합니다.


이것은 http://localhost 라고 호출 하면 web.xml에 기술되어 있는 welcome-file 에 기술되어 있는 파일중 index.jsp가 호출 되어서 tomcat 내부적으로는 index.jsp에 해당하는 index_jsp.java 서블릿이 실행됩니다.

index_jsp.java 에서는 HTTP GET 메서드에 해당하는 _jspService(...) 메서드가 호출 되면서 파라미터로 사용자의 요청을 담은 HttpServletRequest, 사용자에게 응답할  객체인 HttpServletResponse가 전달 되어 실행 되는 것입니다.

소스한번 살펴 보시고 잘 이해가 안가셔도 한가지만 기억하시면 됩니다.

JSP는 그 자체가 실행되는 것이 아니고 서블릿으로 변환 된 후 실행된다는 것!

이것만 잊지 않으시면 됩니다.


다음시간에는 이것을 참고 해서 hello, world 를 출력하는 서블릿을 작성해 보도록 하겠습니다.

서블릿 개발하기 연재목록

#1 개발환경 구축 (https://opensrc.tistory.com/180?category=475522)

#2 프로잭트 생성 (https://opensrc.tistory.com/181?category=475522)

#3 첫번째 JSP 파일 만들기 (https://opensrc.tistory.com/182?category=475522)

#4 첫번째 서블릿 만들기 (https://opensrc.tistory.com/183?category=475522)

#5 포스트 요청을 처리하는 서블릿 만들기 (https://opensrc.tistory.com/203?category=475522)

#6 GET/POST 요청과 함께 파라메터 전달 하기 (https://opensrc.tistory.com/204?category=475522)

#7 Servlet 들여다 보기 (https://opensrc.tistory.com/206?category=475522)

===========================================================




지난편에서 서블릿 개발을 위한 이클립스 + 톰켓 기반의 개발 환경 설정을 살펴보았습니다.

이번에는 설치한 개발환경에 프로잭트를 생성하고 간단한 서블릿을 만들어서 서버에 올려서 실행하는 것까지 진행 해 보도록 하겠습니다.


1. Dynamic Web Project 생성

이클립스 메뉴 File > New > Other... 선택

[Next >] 클릭


원하는 이름으로 Project name을 입력합니다.

Dynamic web module version이 3.1 인것을 기억하세요.

[Next >] 클릭


Default out folder 를 build\classes => WebContent\WEB-INF\classes 로 변경합니다.

모든 WAS(Web Application Server)에서 Java 소스가 컴파일 되어 배치되야 하는 곳은 Context Root 아래에 WEB-INF\classes 입니다.

아직 생성되지는 않았지만 미리 경로를 잡아주도록 하겠습니다.

[Next >] 클릭


Context root 를 / 로 변경하고, 기본 web.xml 을 생성 하도록 'Generate web.xml deployment descriptor' 를 체크 한다.

하나의 WAS 인스턴스에서 여러개의 Context를 실행하는 경우 Context root 를 각각 다르게 주어야 하지만 우리는 한개만 실행 할 것이르모 / 변경했습니다.

Context root를 특별한 경로로 지정한 경우 브라우져로 접근 할 때 해당 경로를 입력해야 접근 가능합니다.

[Finish] 클릭


2. 프로잭트 Encodeing 변경

전편에서도 언급했지만 최근 Java Web Application은 인코딩이 UTF-8이 기본으로 자리잡은지 오래되었습니다.

이상하게도 윈도우용 이클립스는 최신버전도 2byte 한글을 디폴트로 설정합니다. 나중에 변경해도 큰 문제는 없지만 미리 확인을 하고 넘어가는게 좋습니다.

Project Explorer에서 생성한 프로잭트를 오른쪽 마우스 클릭 > Properties 클릭

좌측에서 Resource 선택, Text file encoding 아래에 Inherited from container (MS949) 에 선택되어 있는 것을 Other로 변경 하고 우측 콤보박스를 UTF-8로 변경

[Apply] 클릭

나머지 내용등은 지금 부터 들여다 볼 필요가 없다고 생각되어 차자 필요할 때 다시 확인하거나 설명 드리도록 하도록 하겠습니다.

2018년 01월 17일 추가

기본 한글이 MS949로 잡히는 이유를 추측해보면... 

윈도우 커맨드창에서 java -XshowSettings:properties -version 명령을 쳐보면

sun.jnu.encoding = MS949

이런 설정이 나옵니다.


UNIX 계열에서 같은 명령을 쳐보면

sun.jnu.encoding = UTF8

이렇게 나옵니다.


저걸 바꿀 수 있는지는 나중에 좀 더 알아 보도록 하겠습니다.


2. 프로잭트 구조 살펴보기

프로잭트가 생성되면 왼쪽 Project Explorer 에서 아래 그림과 같은 구조가 보입니다.

다른 것들은 차차 설명하기로 하고

중요한 부분은

Java Resources > src : Java 클래스의 Source가 패키지 구조에 마춰 배치 됩니다.

Java Resources > Libraries : 추가된 라이브러리들이 표시됩니다.

WebContent : 위 설정에서 Context root 와 매핑해준 WebContent 입니다.

외부에서 http://localhost/~ 로 접근 되는 위치입니다.

WebContent\WEB-INF : Java Web Applicatin 에서 꼭 필요한 디렉토리 입니다. 여기에 web.xml도 있고 컴파일된 java 클래스들이 배치되는 classes 디렉토리도 있고 외부 라이브러리들이 배치되는 lib 디렉토리도 여기에 존재 합니다.

WebContent\WEB-INF\web.xml : 향후 우리가 친숙해져야 할 파일입니다. Java 기반의 Web Applicatiaon의 기본적인 특성을 여기에 모두 기술 합니다. 새로 생성하는 서블릿역시 여기에 등록 하고 외부에서 접근할 URI를 매칭해줘야 합니다.

프로잭트를 생성 할 때 마지막에 Generate web.xml deployment descriptor를 체크 해주었기 때문에 기본적인 내용이 기술된 web.xml이 생성되어 있습니다.

현재는 Servlet 3.1 스펙을 이용하겠다는 표시로 http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd 내용 정도만 확인하시면 될 것 같습니다.


이정도만 이해하시고 나머지는 차자 기회가 되면 설명하도록 하겠습니다.


3. 서버 설정

Java Web Application을 실행하기 위해서는 Java Web Application Server가 필요합니다. 줄여서 WAS 라고 부르기도 하고 톰켓같은 경량서버는 Servlet Container라고 부르기도 합니다.

우리는 이미 전 회차에서 Apache Tomcat v8.0을 사용하기 위해 다운로드 받아놓았습니다.

그것을 우리가 만든 프로잭트에서 사용할 수 있도록 설정을 해보겠습니다.

이클립스 메뉴 Window > Show view > Other... 클릭

Server > Servers 선택

[Open] 클릭


하단의 Servers 텝에서

No Server are available. Click this link to create a new server... 를 클릭합니다.


이전 회자에서 다운받은 Tomcat 8.0이 기본적으로 선택되어 표시됩니다.

[Next >] 클릭


우리가 생성한 프로잭트가 좌측에 표시됩니다.

더블클릭 하거나 [Add >] 버튼을 클릭하면 아래 화면처럼 우측으로 이동합니다.

[Finish] 클릭


아래 그림처럼 하단의 Servers 텝에 Tomcat 서버가 사용할 수 있는 상태로 활성화 되었습니다.

Project Explorer 에도 우리가 추가한 프로잭트 외에 Servers 트리가 하나 생겼습니다.


서버 설정을 변경하기 위해서 Servers 텝에서 추가한 서버를 더블클릭 하여 서버 설정을 확인합니다.


General Information 섹션에 Open launch configuration 클릭


맨마지막 Common 텝을 선택하고

Encoding을 Default - inherited (MS949) 에서

Other 로변경하고 UTF-8로 선택합니다.

[Apply] , [OK] 클릭


아래 그림처럼

① HTTP/1.1 Port를 : 8080 -> 80으로 변경한 후 

② Save (Alt + S)

③ Close

후에 콘솔을 확인하기 위해 아래처럼 Servers 텝을 Project Explorer 텝 아래로 드래그 해서 옮기면 편하게 사용 할 수 있습니다.(선택사항)


4. 서버 실행

톰켓 디버그 모드로 실행 버튼

톰켓 실행 버튼

개발 할 때는 디버그 모드로 사용하는게 편리합니다.

디버그 모드로 실행하도록 하겠습니다.


하단에 console 텝이 생기면서

붉은색 로그들이 주르륵 나옵니다.

맨마지막에 

정보: Server startup in 3340 ms

이 메시지가 나오면 정상적으로 톰켓이 가동된 것입니다.


아직 아무것도 만들지 않았기 때문에

브라우저에서 http://localhost 라고 입력하면 404 Not Found 에러가 발생합니다.

Stop 버튼을 눌러서 종료 하거나 위에서 설명한 디버그, 시작 버튼을 눌러 재시작 할 수 있습니다.



다음 회에서는 간단한 jsp 파일과 servlet 파일을 만들어서 호출해 보도록 하겠습니다.

+ Recent posts