추상화

일반적인 추상화란 중요한 특징을 찾아낸 후 간단하게 표현하는 것이다. 추상화는 여러가지 요소를 하나로 통합하는 방향성을 가지고 있다. 이는 문제를 여가지로 쪼개서 나눠보는 '문제 분할'과는 별개이다. 추상화를 이용하여 핵심적인 것만 남기게 되면 복잡한 내용도 한 눈에 알아볼 수 있어 이해하기 쉽다는 장점이 있다.

컴퓨터공학에서 추상화란 복잡한 자료, 모듈, 시스템에서 핵심적인 개념, 기능등을 간추려내는 것이다. 운영체제는 하드디스크는 파일, 네트워크에 대해 포트, 메모리에 대해 주소, CPU에 대해 프로세스라는 추상화된 방법을 제공한다.

객체지향에서 클래스 관련하여 추상화 요소를 찾아보자. 첫번째로 어떤 관념을 클래스를 정의하는 것 자체부터 추상화이다. 또한 여러 클래스간의 공통 속성/기능을 묶어 새로운 클래스를 정의하는 것도 추상화라고 볼 수있다. 예를 들어 개라는 관념을 Dog라는 클래스로 정의하는 것도 추상화이고, Dog 클래스와 Cat 클래스에서 공통된 기능을 묶어 Animal 클래스를 정의하는 것도 추상화이다. 즉 모든 클래스는 추상화를 통해 만들어진다. 인터페이스, 부모클래스를 정의하는 것은 클래스들을 추상화한 결과로 볼 수 있다.

캡슐화

연관 있는 것들을 하나로 묶어주는 것이 캡슐화이다. 연관있는 변수와 함수를 클래스로 묶고 외부에 감출내용은 감춘다. 잘 캡슐화(모듈화)되었다는 말은 연관있는 것들이 잘 응집되었다는 것이고 다른 모듈(객체, 클래스)와는 결합도가 낮아야 한다. 관심사의 분리, 낮은 결합도를 갖게하는 것이 목적이다.

상속

부모 클래스의 속성/기능은 자식 클래스에게 상속된다. 자식 클래스에서 따로 정의하지 않아도 부모 클래스에서 정의된 것들을 자동으로 상속받는 것이다. 상속을 통해 코드의 재사용성이 증가하여 기능을 확장하기 좋다.

다형성

서로 다른 방식을 하나의 표현으로 사용 할 수 있는 것이 다형성이다. 서로 다른 객체를 하나의 일반화된(추상화된) 클래스 혹은 메소드로 처리할 수 있는 것이다. 어떤 추상화된 객체를 사용하는 입장에서 그 객체가 실제로 무엇인지 몰라도 사용할 수 있어야 한다. 예를 들어 어떤 인터페이스를 사용한다고 할 때 그 인터페이스의 구현체가 무엇인지는 모른채로 사용할 수 있는 것이다. 객체간의 결합도를 낮추어 생산성과 유지보수성이 향상된다.

결론은 '관심사의 분리, 낮은 결합도, 코드의 재사용성'를 갖게하는 원리라고 볼 수 있다.

참고자료

https://terms.naver.com/entry.nhn?docId=3607505&cid=58598&categoryId=59316
https://ko.wikipedia.org/wiki/추상화_(컴퓨터_과학)

호이스팅이란

function test() {
  console.log(test); // undefined 출력
  var test = 'hello';
}

위 코드에서 test가 선언되기 전에 호출을 했음에도 오류가 나지 않는다. 다만 undefined 가 출력될 뿐이다. 이는 자바스크립트에서 함수 내에 선언된 변수/함수등을 최상단으로 끌어올려 선언하기 때문이다. 이를 호이스팅이라 한다.

즉 호이스팅이 일어나서 다음 코드와 동일하게 동작한다.

function test() {
  var test;
  console.log(test);
  var test = 'hello';
}

function안의 변수들은 모두 선언 위치와 상관없이 function 내에서 scope를 유지하게 된다. 이를 function scope를 가진다고 한다.

처리방식

  • 자바스크립트 Parser가 함수 실행전에 함수를 한번 훑어서 필요한 변수/함수 정보를 기억하고 있다가 실행한다.
  • 코드 자체가 끌어올려지는 것은 아니고 Parser가 내부적으로 그렇게 처리하는 것이다.
  • 때문에 실제 메모리에 변화는 없다.

호이스팅 대상

  • var로 선언된 변수/함수만 끌어올려지며 할당은 끌어올려지지 않는다. 즉 undefined 인 상태로 올려진다.
  • let/const, 함수표현식에선 호이스팅이 발생하지 않는다.
// 함수 선언문 - 호이스팅O (오류 안남, 콘솔로그 'myFunc' 출력됨)
function test() {
    myFunc();
    function myFunc() { 
     console.log('myFunc');
    }
}   

// 함수 표현식 - 호이스팅X (오류 발생 Uncaught TypeError: myFunc is not a function)  
// myFunc은 undefined로 지정됨.  
function test() {  
  myFunc();  
  var myFunc() = function() {  
  }  
}

호이스팅 순서

동일 이름의 함수/변수가 있을 땐 '변수 선언 - 함수 선언 - 변수 할당' 순으로 진행된다.

function test() {  
  function hello() {  
  };  
  var hello = 'hello';  
  console.log(typeof hello);  
}

호이스팅이 되면 다음 코드처럼 동작한다

function test() {  
  var hello;  
  function hello() {  
  };  
  hello = 'hello';  
  console.log(typeof hello); // 'string' 출력  
}

중간에 변수 할당이 안일어 났다면

function test() {  
  var hello;  
  function hello() {  
  };  
  // hello = 'hello';  
  console.log(typeof hello); // 'function' 출력  
}

알아두기

코드의 가독성과 유지보수를 위해 왠만하면 호이스팅이 안일어나게 하는게 좋다.

  • var을 사용하는 대신 ES6의 let/const를 사용한다.
    • var- function scope(호이스팅O), let/const- block scope(호이스팅X)
    • var- 중복 선언 가능, let/const- 중복선언 불가(에러발생)
  • 함수/변수를 최상단에 선언하여 호이스팅으로 인한 스코프 꼬임이 발생하지 않도록 한다.

참고 사이트

'개발 > 프론트' 카테고리의 다른 글

[CSS] flex를 이용한 그리드 배열 만들기  (0) 2020.05.20

카프카의 특징

  • Pub/Sub 동작 방식이다.
    • Publisher와 Subscriber가 독립적으로 데이터를 생산하고 소비한다. 프로듀서와 컴포넌트를 디커플링하기 위한 좋은 수단이다.
    • 이런 느슨한 결합을 통해 둘 중 하나가 죽어도 서로간에 의존성이 없으므로 안정적으로 데이터를 처리할 수 있다.
      • 리시버와 컬렉터의 통신을 API로 한다고 가정해보자.
        • 컬렉터는 리시버가 요청한 것을 바로 처리해줘야 한다. (처리를 미룰 수 없다)
        • 실패가 났을 때 부분 재처리가 불가능하다.
        • 처리속도가 느린 컴포넌트 기준으로 처리량이 결정된다. 그러므로 트래픽이 몰리는 상황을 대비해 전체 파이프라인을 넉넉하게 산정해야 한다.
      • 중간에 카프카를 두게 되면 이런게 좋아진다.
        • 컬렉터는 리시버가 요청한 작업을 쌓아두고 나중에 처리할 수 있다.
        • 실패가 나도 컬렉터만 따로 재처리 할 수 있다.
        • 일시적으로 트래픽이 몰리는 상황을 대비해서 제일 프론트의 서버만 넉넉하게 준비해두면 된다. 물론 장기적이 되면 전체 파이프라인을 증설해야 겠지만.
    • 또한 설정 역시 간단해진다.
      • Publisher 따로, Consume 따로 독립적으로 설정할 수 있다.
  • 디스크 순차 저장 및 처리
    • 메시지를 메모리큐에 적재하면 데이터 손실 가능성이 있지만 카프카는 디스크에 쓰므로 데이터 손실 걱정이 없다.
    • 디스크에 쓰므로 상대적으로 속도가 느리지만, 순차처리 방식으로 디스크 I/O를 줄여 그렇게 느리지도 않다. 엄청 반응성이 높은 서비스가 아니라면 이 속도가 문제되진 않는다.

카프카 아키텍처 구성요소

  • 토픽
    • 카프카 안에는 여러 레코드 스트림이 있을 수 있고 각 스트림을 토픽이라고 부른다.
    • 하나의 토픽에 대해 여러 Subscriber가 붙을 수 있다.
  • 파티션
    • 각 토픽마다 데이터를 여러개의 파티션에 나누어서 저장/처리한다.
    • 토픽 사이즈가 커질 경우 파티션을 늘려서 스케일아웃을 할 수 있다.
    • 일반적으로 브로커 하나당 파티션 하나다.
    • 컨슈머 인스턴스 수는 파티션 갯수를 넘을 수 없다. 그러므로 병렬처리의 수준은 파티션 수에 의해 결정된다. 즉 파티션이 많을 수록 병렬처리 정도가 높아진다.
    • 각 파티션마다 Publish되는 레코드에 고유 오프셋을 부여한다. 때문에 레코드는 파티션 내에서는 유니크하게 식별된다. 하지만 파티션간에는 순서를 보장하지 않는다.
    • 전체 순서를 보장하고 싶으면 파티션을 하나만 두는 수 밖에 없는데, 이러면 데이터량이 많아져도 스케일아웃이 안되고, 컨슈머 인스턴스도 하나만 둘 수 있으므로 병렬 컨슈밍도 안된다.
  • 데이터 보관기간
    • 컨슘하고는 상관없다. 보관기간(Retention) 정책에 따른다.
    • 데이터 사이즈가 늘어날지라도 성능은 일정하게 유지된다.
  • 오프셋
    • 일반적으로 컨슈머가 오프셋을 순차적으로 증가하며 컨슘해하지만, 원한다면 컨슈머 마음대로 조정할 수 있다. 재처리가 필요한 경우 오프셋을 이전으로 돌릴 수도 있고 가장 최근 레코드 부터 처리할 수도 있다.
    • 컨슈밍 비용이 저렴하기 때문에 커맨드라인 컨슈머로 데이터를 "tail"하는 작업도 다른 컨슈머에 별 영향을 끼치지 않는다.
  • 프로듀서
    • 레코드를 프로듀스할 때 어느 토픽의 어느 파티션에 할당할 지를 결정한다.
    • 일반적으로 라운드로빈 혹은 원하는 대로 할당방식을 지정할 수 도 있다.
  • 컨슈머 그룹
    • 컨슈머 그룹 마다 독립적인 컨슘 오프셋을 가진다.
    • 컨슈머 그룹 내에서 처리해야할 파티션이 분배된다. 즉 하나의 파티션은 하나의 서버가 처리한다. 그룹에 서버가 추가되면 카프카 프로토콜에 의해 동적으로 파티션이 재분배 된다.
    • 하나의 토픽 레코드를 분산처리하는 구조라면 동일 컨슈머 그룹을 가지게 해야 한다.
    • 하나의 토픽 레코드에 각각 별도의 처리를 하는 다른 파이프라인이라면 서로 다른 컨슈머 그룹을 가지게 해야 한다.
  • 큐(Queue)와 Pub/Sub의 혼합 모델
    • 컨슈머그룹 내에서는 큐처럼 동작하고 컨슈머그룹간에는 Pub/Sub처럼 동작한다. 컨슈머그룹을 어떻게 두냐에 따라 두 방식 다 입맛에 맞게 선택할 수 있다.
  • 데이터 순서 보장
    • 일반적인 큐 모델에서 순차적으로 컨슘해간다고 해도 각 처리기에 도달하는 시간이 다르므로 순서가 보장되지 않는다.
    • 그렇다고 처리기를 1개만 두면 병렬성이 떨어진다.
    • 카프카는 토픽의 전체 순서는 보장하지 않지만 파티션별로는 처리 순서를 보장한다. (파티션은 하나의 컨슈머 인스턴스에서만 처리되므로). 즉 전체 순서 보장을 포기하고 부분 순서 보장을 취한 것이다. 대신 병렬성을 유지할 수 있다. 다르게 말하면 파티션당 컨슈머를 하나만 가져야하는 이유는 파티션 내에서 처리순서를 보장하기 위함이다.

데이터 복제

  • 각 파티션을 여러대의 서버에 복제해둔다. 이는 설정값을 따른다. 크래시리포트는 ?개의 리플케이션을 두었다.
  • 각 파티션 마다 특정 서버 한대를 리더로 선정하고 나머지 복제 파티션은 팔로워가 된다.
  • 리더가 모든 읽기/쓰기를 처리한다. 리더는 요청을 패시브하게 팔로워들에게 전파한다.
  • 각 서버는 하나의 리더 파티션을 갖는다. 때문에 트래픽은 각 서버로 밸런싱이 되는 셈이다.
  • 복제 수가 N개라면 N-1개까지 죽어도 실패복구가 가능하다.

주키퍼의 역할

  • 컨트롤러를 선출한다. 컨트롤러는 전체 파티션에 대한 리더/팔로워를 관리한다. 컨트롤러의 역할은 노드가 죽으면 다른 리플리카에게 리더가 되라고 명령을 내리는 역할을 한다.
  • 어떤 브로커가 살아있는지를 체크한다.
  • 어떤 토픽이 있고, 토픽에 파티션이 몇개 있고, 리플리카는 어디있고, 누가 리더가 될만한지, 각 토픽에 어떤 설정이 되어있는지를 관리한다.

오프셋 관리

  • 카프카에서 관리하는 오프셋은 두 종류가 있다.
    • Current Offset : 컨슈머에게 전송된 마지막 레코드의 포인터
    • Committed Offset : 컨슈머가 성공적으로 처리한 마지막 레코드의 포인터.
  • 오프셋 커밋 방법
    • 카프카에서 관리되는 방식 : _consumer_offsets 토픽에 오프셋이 저장된다.
      • 오토 커밋 : 일정 주기를 가지고 자동으로 커밋한다. 작업이 끝났든 안끝났든 주기만 되면 커밋한다.
        • 커밋되기 전에 리밸런싱이되면 중복처리가 발생한다.
        • 처리 완료되기 전에 커밋 후 프로세스가 죽으면 데이터 손실이 발생한다. (at-least-once 보장X)
      • 수동 커밋 : 수동으로 커밋 API를 호출한다. 정확히 작업이 끝나고 커밋하는 걸 보증하기 위해 사용한다.
        • 커밋되기 전에 리밸런싱되면 중복처리가 발생한다.
    • 자체적으로 관리하는 방식 : 별도 DB등에 자체적으로 오프셋을 관리한다.
  • 메시지 보증 전략
    • 일반적인 보증 전략은 다음과 같고 카프카는 at-least-once로 보증한다.
      • at-most-once : 중복X, 유실O
      • exactly-once : 중복X, 유실X, 구현하기 어렵다.(비용이 비싸다)
      • at-least-once : 중복O, 유실X
    • 데이터가 중복되는 경우는 다음과 같다. 어떤 데이터가 poll됐으나 commit되지 않은 시점에 리밸런싱 작업이 일어나면 이미 poll되어서 처리중인 데이터를 다른 컨슈머가 또 poll갈 수 있다.
    • 수동 커밋에서도 마찬가지다. 커밋 전에 리밸런싱이 되는 경우 중복으로 처리될 수 있다.
    • 가장 좋은건 exactly-once지만 비용이 비싸므로 적당한 타협지점은 at-least-once이다.중복되는 메시지는 메시지 ID나 시간등으로 어느정도 보정이 가능하기 때문이다.
    • 카프카 0.11 버전 부터 idempotent producer와 tracsaction producer의 등장으로 exactly-once를 보장할 수 있다고 한다.
      • 내부적으론 레코드의유니크ID, 트랜잭션ID, 프로듀서ID등을 조합해서 처리한다고 한다.
  • 크래시리포트의 오프셋 관리
    • 스파크 클러스터만 수동커밋을 하고 있다. 스파크는 보통 poll에서 프로세싱 완료까지 시간이 오래걸릴 것으로 예상되므로 오토커밋을 할 경우 데이터 손실 가능성이 커서 수동으로 커밋하는 걸로 보인다.
    • 오프셋을 카프카가 아닌 별도의 DB에 저장했다. commitAsync()에서 fail이 발생했던 거 같은데 정확한 원인은 모르겠다.

다른 미들웨어와 비교해보자.

  • RabbitMQ
    • 장점
      • 다양한 기능, 높은 성숙도
      • 20k/sec 처리 보장
  • Kafka
    • 장점
      • 고성능 고가용성
      • 분산처리에 효과적으로 설계됨
      • 100k/sec 처리 보장

겪었던 이슈들

ERROR [Thread-20] [Consumer clientId=consumer-13, groupId=xxx-collector] Offset commit failed on partition zxx.topic-1 at offset 199: The request timed out. 

원인은 글쎄.... 뭐였지...

Java8에서 도입된 람다표현식은 추상메서드가 1개인 인터페이스를 사용할 때 익명클래스를 사용하지 않고 코드의 구현부만 넘길 수 있개 해주는 문법으로써 코드의 간결성을 높일 수 있다.

다음 상황을 가정해보자.

1. 유저 리스트를 정렬하고 싶다.

2. 나이순으로, 이름순으로 각각 한번씩!

코드로 이렇게 작성할 수 있을 거 같다.

List<User> users = ...

Collections.sort(users, new Comparator<User>() {
    @Override
    public int compare(User o1, User o2) {
        return o1.getName().compareTo(o2.getName());
    }
});

// Do something...

Collections.sort(users, new Comparator<User>() {
    @Override
    public int compare(User o1, User o2) {
        return Integer.compare(o1.getAge(), o2.getAge());
    }
});

코드에서 Comparator 인터페이스의 익명클래스 두 개를 만들어 인스턴스를 생성했다.

그런데 구현해야할 추상 메서드가 오직 하나인 인터페이스를 사용하는데에는 이 정도도 과해보인다. 정렬 기준이 추가될때 마다 저 코드가 반복된다고 생각하면 더 그렇다. 람다는 이런 번거로운 작업을 좀 더 간소화해준다. 람다를 사용하면 익명클래스를 사용하지 않고도 함수(Function)를 파라미터로 넘길 수 있다. 클래스를 사용하지 않기 때문에 메서드가 아닌 함수라는 용어를 사용한다.

람다를 사용한 코드는 다음과 같은 형태이다.

List<User> users = ...

Collections.sort(users, (o1, o2)->{
    return o1.getName().compareTo(o2.getName());
});

// Do something...

Collections.sort(users, (o1, o2)->{
    return Integer.compare(o1.getAge(), o2.getAge());
});

(파라미터) -> {구현부}의 형태를 띄고 있다.

그럼 람다로 넘길 수 있는 지 아닌지는 어디서 결정되는 걸까?

Comparator 인터페이스를 살펴보면 알 수 있다.

@FunctionalInterface
public interface Comparator<T> {
  int compare(T o1, T o2);
  // Others...
 }

구현해야 할 추상메서드가 1개인 인터페이스에 @FunctionalInterface 어노테이션을 붙여주면 람다식으로 사용할 수 있다. 추상메서드는 1개여야 하지만 default나 static 메서드에는 제한이 없다.

참고로 Comparator 인터페이스를 다시보면 헷갈릴 수 있는 부분이 있는데

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }
    // Others...
}

default, static이 붙은 걸 제외해도 equals 메서드를 추상메서드로 제공하고 있다는 점이다. 즉 2개의 추상메서드가 있다. 그렇다면 내가 넘긴 람다식이 compare에 매칭되는지 equals에 매칭되는지 어떻게 알겠는가? 여기에 대해서는 다음과 같이 말하고 있다.

Functional Interface의 정의에서 Object 클래스의 public 메서드는 제외한다.

equals는 이미 모든클래스의 조상인 Object클래스에 이미 구현되어 있으므로 이런 경우에는 추상메서드로써 남겨놔도 FunctionalInterface로 사용할 수 있게끔 해서 편의성을 가져가는 것이 아닐까 한다.

Anoymous Inner Class와의 차이

public class Main {
    public static void main(String[] args) {
        List<Integer> iList = new ArrayList<>();
        iList.sort(new Comparator<Integer>() {
            @Override
            public int compare(Integer integer, Integer t1) {
                return 0;
            }
        });
    }
}

위의 코드를 컴파일하면 Main$1.class, Main.class 두 개의 class 파일이 생성된다.

public class Main {
    public static void main(String[] args) {
        List<Integer> iList = new ArrayList<>();
        iList.sort((i1, i2) -> i2 - i1);
    }
}

람다로 작성 후 컴파일하면 Main.class만 생성된다. 별도의 클래스를 생성하지 않고 outer class의 private static 메서드로 컴파일된다.

 

어떻게 컴파일되는지 좀 더 자세하게 알아보고 싶다면 아래 글을 읽어보는 것도 좋을 것이다.
https://dzone.com/articles/how-lambdas-and-anonymous-inner-classesaic-work

 

람다에 대한 딥다이브를 원한다면 참고할 수 있는 오라클 강의도 있다.

https://www.infoq.com/presentations/lambda-invokedynamic/

 

하이퍼링크를 통해서 파일을 다운로드할 때 파일이름을 바꾸고 싶으면

<a href="/test.png" donwload="newname.png"></a>

으로 하면 newname.png으로 다운로드가 되지만,

<a href="http//blahblah.com/file/test.png" donwload="newname.png"></a>

cross-origin 요청에 대해서는 더 이상 download attribute를 지원하지 않는다.

 

결국 request에 파일명을 쿼리스트링으로 전달한다음 nginx 자체에서 처리하도록 바꿨다.

location /file {
  autoindex      on;
  alias /data01/file/;
  # 이 설정을 추가해준다. 
  add_header Content-Disposition 'attachment;filename=$arg_filename'; 
}
<a href="http//blahblah.com/file/test.png?filename="newname.png"></a>

'개발 > 기타' 카테고리의 다른 글

[PHP] Call to undefined function dl()  (0) 2020.02.10
카프카(Kafka)  (0) 2020.01.30
Vue.js에서 Props가 안넘어가는 문제 (undefined)  (0) 2020.01.08
[Java] 원격 디버깅(Remote debugging)  (0) 2019.12.18
[Nginx] 'Server' 헤더 제거  (0) 2019.02.07
<report-list :reports="reports" :fields="fields" :clickHandler="moveToReport"></report-list>

var ReportList = {
	Props: ['clickHandler']
};

new Vue({
  .. 
  methods:  {
    moveToReport: function() {
       ....
    }
  }
});

 

clickHandler를 넘기고 싶은데, report-list에서 props 를 찍어보면 계속 undefined로 떠서 도대체 왜 이러나 싶었다.

<report-list :reports="reports" :fields="fields" :handler="moveToReport"></report-list>

var ReportList = {
	Props: ['handler']
};

new Vue({
  .. 
  methods:  {
    moveToReport: function() {
       ....
    }
  }
});

대소문자를 섞어쓰지 않고 소문자로만 props명을 만드니 잘 작동했다.

 

관련해서 가이드가 있었다.

https://vuejs.org/v2/guide/components-props.html#Prop-Casing-camelCase-vs-kebab-case

 

Props — Vue.js

Vue.js - The Progressive JavaScript Framework

vuejs.org

HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you’re using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents:

DOM 템플릿에서는 camelCase->kebab-case로 변환해서 사용해줘야 한다. 

<report-list :reports="reports" :fields="fields" :click-handler="moveToReport"></report-list>

var ReportList = {
	Props: ['clickHandler']
};

new Vue({
  .. 
  methods:  {
    moveToReport: function() {
       ....
    }
  }
});

Spring에서 서블릿을 등록하려면 아래와 같이 해야한다.

@WebServlet(name = "myServlet", urlPatterns = "/app2")
public class MyServlet extends HttpServlet {

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

      System.out.println("-- In MyServlet --");
      PrintWriter writer = resp.getWriter();
      writer.println("dummy response from MyServlet");
  }
}

GET /app2 로 들어오는 요청을 처리할 서블릿을 정의하는 코드다.
Java EE에서 Http 요청을 처리하기 위해서는 다음과 같이 작업해야 한다.

  1. HttpServlet 클래스를 상속받아 요청을 처리할 로직을 작성한다.
  2. 작성한 클래스를 서블릿컨테이너에 등록한다.

하지만 SpringMVC를 사용하다보면 서블릿을 등록할 일이 없다.
대신 다음과 같은 코드를 작성하게 된다.

@Controller
public class GreetingController {

    @GetMapping("/greeting")
    public String greeting(@RequestParam(name="name", required=false, defaultValue="World") String name, Model model) {
        model.addAttribute("name", name);
        return "greeting";
    }

}

이건 서블릿은 아니지만 서블릿 처럼 요청에 대한 핸들러 역할을 한다.
SpringMVC는 모든 요청을 하나의 서블릿, DispatcherServlet이 받는다.
DispatcherServlet이 받아서 우리가 정의한 컨트롤러 맵핑에 맞게 요청을 넘겨준다.

바햐흐로..

바야흐로 그 옛날엔 웹 페이지가 정적 컨텐츠로만 구성된 시절이 있었을 것이다. 요청에 매칭되는 파일을 찾아 응답을 내려주면 된다.

그러나 시간이 흐르면서 정적 컨텐츠만으론 충분하지 않음을 느꼈을 것이다. DB에 저장된 데이터을 읽어서 HTML 포함 시키고 싶다고 하면 어떻게 해야할까? 모든 데이터에 맞게 HTML을 미리 생성해 놓을 순 없는 노릇이다. 이러한 이유로 동적으로 HTML을 생성하고 싶은 요구가 생겼고 Java에서는 이를 서블릿과 서블릿컨테이너를 통해 이를 지원하게 된다.

간략히 프로세스를 설명하면 다음과 같다.

  1. 서블릿 클래스를 상속받아 요청에 대해 어떤 응답을 줄 지에 대한 코드를 작성한다.
  2. 컨테이너의 설정파일인 web.xml에 어떤 uri에 어떤 서블릿이 매칭되는지를 작성한다.
  3. 내 어플리케이션을 컨테이너에게 제출한다(Submit).
  4. 컨테이너가 실행되어 Request를 받으면...
    1. web.xml을 참고하여 Request를 처리할 서블릿 클래스를 찾는다.
    2. 찾은 클래스의 인스턴스를 생성한다.
    3. 그 인스턴스에 구현된 메소드를 호출하여 응답값을 돌려받는다.

즉 서블릿은 우리가 원하는 로직을 작성할 수 있는 클래스이고, 서블릿 인터페이스에 맞게 작성하여 컨테이너에 등록해두면, 컨테이너가 이를 가져다 사용하는 식이다.

사실 SrpingMVC를 주로 사용하는 입장에선 서블릿을 직접 작성할 케이스가 많지 않다.

참고자료

https://javaee.github.io/servlet-spec/downloads/servlet-4.0/servlet-4_0_FINAL.pdf

https://engkimbs.tistory.com/755

http://www.pearsonitcertification.com/articles/article.aspx?p=29786&seqNum=3

1. Java 실행 옵션에 다음 추가

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000

ex)

java -Dsun.misc.URLClassPath.disableJarChecking=true -server -Xms2g -Xmx2g -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 -Dfile.encoding=UTF-8 -Dconsole=true -jar test.jar --spring.profiles.active=alpha

2. IDE로 디버깅

Eclipse

Eclipse -> Run -> Debug Configuration -> Remote Java Application -> Build 에서
Host : 원격 서버 ip
Port : address로 넣어준 port (여기선 8000)
입력 후 Debug 시작

Intellij

Edit configuration -> Add new configuration -> remote
Host : 원격 서버 ip
Port : address로 넣어준 port (여기선 8000)
입력 후 Debug 시작

+ Recent posts