반응형

REST ( Representational State Trasnfer) 공식표준 스펙은 아닌데

원래 있던 HTTP 통신에서  재사용 하여 데이터 송수신 규칙을 만든것 => CRUD

 

 

CRUD 를 Blazor API 서버를 활용하여 작업 한것 

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SharedData.Models;
using System.Collections.Generic;
using System.Linq;
using WebAPI.Data;

namespace WebAPI.Controllers
{
    //www.google.com 이건 전체 주소
    //www.google.com/api/ranking   이건 세부 라우팅 주소로 전체 주소에서 구분지어 들어갈 때 사용됨
    //그 다음 어떤걸 하는지 지정 할 수 있다 GET, POST, PUT...)
    //Create : POST 방식 api/ranking => body 에 실제 정보를 담는다 , 아이템 생성 요청

    //Read 시에는 보통 Get 을 사용한다
    //GET : api/ranking => 전체 아이템을 가져온다 , api/ranking/1 id가 1인 아이템을 가져온다
    //get /api/ranking  모든 id를 주세요
    //get /api/ranking/1   id 중에서 1전을 주세요 라는 요청 
    //get 은 body 에 정보를 넣지 않고 이 요청 하나로 처리 된다

    //update : put 으로 사용됨
    //PUT : api/ranking  (put은 보안 무제로 일반적인 웹에서 사용되지 않는다)
    //put 은 body 에 정보를 넣어서 요청을 보낸다

    //delete /api/ranking/1   이렇게 삭제 할 수 있는데 보안 문제로 웹에서 쓰지 않는다
    //id=1 번인 아이템 삭제 요청

    //api controller 는 c# 객체를 반환하는 것이 가능하다
    //null 반환하면 클라에서 204 Response (No Content) 를 받는다
    //String을 반호나하면 => text/plain 타입으로 반환한다
    //나머지는 json 형태로 반환한다

    //localhost:portnumber/api/ranking 이 요청이라는 얘기인데 RankingController 이것이 [controller] 부분에서 ranking 으로 변환됨
    [Route("api/[controller]")]
    [ApiController]
    public class RankingController : ControllerBase
    {
        ApplicationDbContext _context;
        public RankingController(ApplicationDbContext context)
        {
                _context = context;

        }

        //create, body 에 보낼때는 [frombody 를 넣어주면 된다]
        [HttpPost]
        public GameResult AddGameResult([FromBody] GameResult gameResult)
        {
            _context.GameResultList.Add(gameResult);
            _context.SaveChanges();
            return gameResult;
        }


        //update
        [HttpPut]
        public bool UpdateGameResult([FromBody] GameResult gameResult)
        {
            var findResult = _context.GameResultList.Where(item => item.Id == gameResult.Id).FirstOrDefault();
            if(findResult == null)
            {
                return false;
            }

            findResult.UserName = gameResult.UserName;
            findResult.Score = gameResult.Score;
            _context.SaveChanges();

            return true;
        }

        //read
        //ranking
        [HttpGet]
        public List<GameResult> GetGameResults()
        {
            List<GameResult> results = _context.GameResultList.OrderByDescending(item=> item.Score).ToList();
            return results;
        }

        //ranking/1
        [HttpGet("{id}")]
        public GameResult GetGameResult(int id)
        {
            GameResult result = _context.GameResultList.Where(item => item.Id == id).FirstOrDefault();
            return result;
        }


        //delete
        [HttpDelete("{id}")]
        public bool DeleteGameResult(int id)
        {
            var findResult = _context.GameResultList.Where(item => item.Id == id).FirstOrDefault();
            if (findResult == null)
            {
                return false;
            }
            _context.GameResultList.Remove(findResult);
            _context.SaveChanges();

            return true;
        }
    }
}

 

아래 예시는 모든 데이터를 json 으로 갖고 오는 예시이다

반응형
반응형

[지연시간]

유선 네트워크에서는 지연시간이 10ms, 많아야 20~30ms 인데

와이파이에서는 100ms 에서 불안정하면 200~300ms 까지도 불안정하게 발생하게 된다

보통 게임에서 30~60 프레임 기준을 많이 사용하는데

60프레임 기준에선 한프레임당 16ms 정도 의 시간이 걸린다

이 시간을 넘어가면 사람들이 인지는 하기 시작하고 

30프레임에선 한 프레임당 33 ms 인데 33ms 가 넘어가면 거의 모든 사람들이 부자연 스럽다고 인지하게 된다 



[예측과 보정]

가지고 있는 정보들을 통해서 미리 예측을 하고 정보를 받았을때 예측과 결과가 다르다면 보정을 해주는 방식 

 

ex ) 타격이 먼저 되고 때리는 모션이 나중에 나가는 경우 

논타겟의 경우 시점 분리가 된다 : 스킬 시전 시점과 스킬 피격 대상 선정 사이에 차이가 발생

만약 스킬 영역에 스킬을 시작하고 스킬이 발생 하는데 까지 시간이 걸리는데 영역안에 있던 대상이 밖으로 도망가면 피격이 되면 안된다, 이때는 피격 당하는 대상의 시점만 미뤄두고 나중에 데미지가 가해지게 하면 되지만 시점분리에 필요한 요구사항이 점점 늘어난다면?

 

ex) 스킬 대상이 넉백 되면서 데미지는 그 도중에 되면 좋겠다

: 1. 시작은 따로 2. 그다음에 넉백.  3 그다음에 데미지  이렇게 3개로 분리가 된다

 

영역은 하나인데 대상을 3번에 걸처서 타격해야 하는 상황이 발생한다

 

더 나아가 쿨 다운은 선딜 후 타격 전에 돌리고 싶다 

내가 스킬 시전하다가 넉백을 당하면 취소가 되야하고

기를 모으는 시간에 따라 효과가 다르게 하고 싶다

돌진하면서 주변에 있는 적들을 차례로 밀치고 싶다

 

등등등.. 요구 사항이 점점 더 많아짐..

 

이때 생각해 볼 수 있는거 스킬 스테이지 방식이다

 

스킬을 여러 단계로 나누어서 구성 하는 것을 말한다

 

그런데 문제는 결정된 시간이 전달이 될때 받는 쪽에서 지연이 되면 내가 원하는 타이밍에 전달되지 않게 된다 




아래처럼 동작하겠지 라고 생각하고 만들었는데



네트워크를 타는 순간 스킬 구성이 달라지게 된다 

 

이 처럼 시간 간격에 딜레이가 생기면서 타격이 늦게 일어나거나 여러 가지 시점이 흔들리는 일들이 벌어지게 된다 

 

그래서 나온게 예측 가능하게 만들자

여러 스테이지 결과를 미리 결정 및 공유하고 

이에 맞춰 각자 서버와 클라가 스킬 스테이지를 진행한다

그래서 이때 미리 계산 할 수 있는 그룹들을 미리 묶어 두어서 처리하게 된다

 

그것이 스킬 스테이지 플로우 도입

 

CapturingTarget 영역 안의 대상 선정 

이나 중요하게 반드시 시점이 나뉘어야 하는 것들만 플로우를 나누는 기준이 되고 나머지 스테이지들은 묶어서 미리 계산을 하도록 처리




이걸 



이렇게 묶음으로 써

원래 4번 전송 되던걸 두번만 전송하게 되고 묶음 안에서는 어색함 없이진행 가능

 

문제점 : 플로우가 길다보면 발생할 수 있는문제가 중간에 취소 하면 취소가 늦게 가는 경우 

피격을 하지 않았는데 피격 연출이 나온다던가 하는 문제

 

해결 방안 => 정상 타격이 완료 되면 데미지 숫자를 붉은색으로, 그렇지 않으면 파란색으로 표시하여 대미지가 안들어간것으로 보이게끔 처리

 

또는 스킬 연출과 별개의 추가 연출 구간을 만들어서 

정상타격 완료 시 : 대미지 플로터로 확정 연출

정상타격 취소로 판정시(늦게 패킷이 오면) : 대미지 플로터를 모래 연출처럼 흩어 버리기

방식으로 처리




 

논 타겟일때 움직이면 맞고 가만히 있으면 맞을때도 있고 안맞을때도 있는데

 

=> 타격자 시점에서는 상대 캐릭터가 스킬 범위에 들어갔는데 피격자에서는 안맞는 문제가 보임 즉 내 캐릭터가 공격을 하는데 상대 캐릭터가 스킬 범위에 들어왔음에도 불구하고
공격이 가해지지 않음


=> 피격자 시점에서는 타격자가 공격 당하는 상대로 스킬을 논타겟으로 썼지만 상대가 스킬 범위에 들어가 있지 않아서 공격을 받지 않은 상황으로 보이는 중

 

 

이건 플레이어 위치가 타격자와 피격자 시점에서 위치가 살짝 다를때 발생 할 수 있다

서버상에서도 피격자의 위치가 실제 달라서 발생 

타격자 시점에서 상대방 위치를 보면 조금 더 왼쪽으로 가 있는 상태














이때 이동 시스템이 어떻게 되어 있는지 알 필요가 있다

 

폴리드 타일기반으로 이동하는 시스템 

셀도 없고  방향도 특정되지 않고 자율롭게 돌아다닐 수 있는 구조

위치를 짧은 시간내에 동기화 할 수 있는 모델은 없었다는 것 MMORPG 라

대규모 전투에서 통신량이 큼으로 유저가 많을 수록 제곱에 비례해서 늘어나기 때문에..



[이동에 관한 히스토리]

  1. 클라가 서버에게 이동 할래 라는 패킷을 날리면 서버가 판단해서 이동을 시키는 구조
  2. 결국 클라이언트에서 먼저 이동하도록 변경




즉시 예상하지 못하는 상황에서 멈춤을 할때 를 생각해 보면
다른 유저, 서버에서의 해당 유저, 내클라이언트에서 다른 유저를 멈출때의 위치가 모두 달라지게 된다

즉 피어들간의 위치 정보의 오차가 커진다

 

멈추는 클라이언트에서는 빨간 위치에서 멈추라고 했지만 서버와 다른 클라에서 모두 다 조금씩 늦게 받게 되어 느려지는 현상이 발생 할 수 있다



순간적으로 변경한 명령이 다음 이동 이다 라고 하면 

이동의 시작 구간에서 보정을 진행하면 된다 이때는 이동을 하는 와중에 스킬을 잘 맞고 위치 오차도 적은 편이다

하지만 바로 멈추라고 하면 다음 이동이 없다, 즉 멈추라고 하면 다음 이동이 없어 보정할 곳이 없다 

=> 만약 원래 멈춰야 하는 곳보다 더 갔을때 캐릭터를 뒤로 당기면 이동이 어색해 지기 때문에 그 자리에 있을 수 밖에 없도록 프라시아에서는 작업을 했었고 다음 이동 명령때 다시 보정을 한다를 컨셉이였었다  => 그러다보니 생각하는 위치들이 다 달랐던 것 

 

해결  : 구간이 없어도 강제로 맞춰줘야 되는 부분이기 때문에 기본적인 해결책 부터 적용 시작

방안 :

  1. 멈추면 그 자리로 그냥 옮기자

 

핑이 안정적일때는 40ms 에서 120ms 까지는 그래도 안정적으로 추가 이동명령으로 위치 보정을 해도 어색하지 않고 추가로 멈춰야 할 위치까지 이동해서 보정이 가능하다

그런데 이미 이동을 멈춰야 하는 위치보다 더 가 있을때 이걸 당기는건 게임을 망치기 때문에 

 그 자리에 멈출 수 밖에 없다 즉 50% 확률이 보정이 되거나 안되거나 하는 상황

 

정확(무보정)의 수치를 200ms 뒤 까지에서 실행되게 해야하는 것이 목표인데

즉 대부분이 보정이 될 수 있게 처리를 해야 하는데

 

이렇게 처리 하기 위해선 내쪽에서

다른 개체의 이동 요청이 왔을때 120에 받았는데 마치 200에 받은 것 처럼 처리해 주면 된다

 

즉 다른 개체의 이동 정보는 한 80ms 늦게 실행 시킨다 

이렇게 하면 보정을 더 과하게 해야하긴 하지만 아예 보정을 할 수 없는 케이스는 거의 사라지게 된다

 

위의 문제는 이동에 관한 갑자기 멈춤에 관한 문제였음으로 상대 캐릭터 이동에 대한걸 80ms 정도 지연시켜 받아서 이동에 대한 보정 처리를 해주면 결과적으로 200ms 내에 이동 보정이 됨으로 타격은 정확한 처리가 된다 => 문제는 해결됐다
수치상 80ms 이긴 한데 상황에 따라 다를 수도 있으니 동적으로 다르게 주는 것도 생각해 볼만한 문제 



그런데 이동하는 중에는 오차가 좀 더 커진 셈이긴 하지만 캐릭터가 스킬을 안맞는 크리티컬한걸 피했다고 볼 수 있다



정리

 

  • 예측을 활용 하는 동기화 방식은 어느정도 활용 중
  • 하지만 모든 정보를 알리는 방식으로 동기화를 할 경우 과다한 네트워크 통신, 느린 반응성등의 문제가 발생 할 수 있다
  • 시간의 흐름에 따른 상태를 계산해 낼 수 있는 일부 상태만 알림을 통해 동기화 처리 한다 => 정해진 규칙에 따라 현재 상태를 도출한다
  • 네트워크를 통하면 외부 요인으로 인해 상태가 변경 되었을 때 이 정보를
    내가 늦게 알게 되는 경우가 자주 발생한다, 
  • 내가 계산해서 예측을 해서 가지고 있는 현재 상태랑 믿을 수 있는 소스 보틍은 서버와 상태가 서로 다를 경우 현재 내가 가지고 있는 상태를 실제 상태로 자연스럽게 맞추는 작업을 보정이라고 한다 

 

만약 더이상 자주 알려주기 힘들다면 고려해볼만 한 것은

이른 시점에 결정적인 상태를 만든다, 먼저 계산해서 알려준다 는것 

 

도출해낸 결과가 정확한게 가끔 많이 틀릴 수 있다면
엄청 많이 틀리는 것을 좀 완화하고 대신에 평소에는 조금 정확성이 떨어지더라도 

워스트 케이스를 없애는 게 더 좋을 수 있다

다만 정확성을 너무 포기하면 보정이 힘들어진다 

구간이 없다면 80ms 정도 늦게 받아서 처리 하는 것처럼 구간을 만들어서 구간을 활용하는 전략들을 고민하면 좋을 것 

 

예측에는 Extrapolation 외삽법이 주로 쓰이고 보간에는 interpolation 이 주로 쓰인다

 

 

 

--------------------

별도 추가 부분

--------------------

 

외삽법 : 외삽법이 뭔지 오래 되서 잘 기억이 안날 수 있음으로 복습차원에서..

검은색이 원래 데이터이고 파란색이 예측하는데이터인데 점3개에 대해서 보간을 하면 원래 검은색과 거의 유사하게 파란색이 나오는데 그 미래에 대한 4번째 점에 대해선 오차가 심해 질 수 있다는 외삽법의 단점이 있다 

고차다항식의 보간법의 위험성 : 항이 많아지고 차수가 높아 질 수록 식이 정교해 질것이라는 기대가 있지만 오히려 않좋은 결과가 나올 수도 있다 

 

점선이 정확한 값인데 점 4개를 갖고 4차 다항식을 만들 수 있고 그림을 그리면 검은 선이 나오는데 형태는 조금 유사해도  오차가 나온다 

이때 더 많은 점을 도입해서 10차 다항식으로 접합을 해보면 

runge 함수를 다항식으로 묘사를 해봤더니 양 끝단에 오차가 상당히 크게 나온걸 알 수 있다

즉 다항식 보간법을 쓸때 고치일 수록 정확하다고 볼 수 없다는 것이 Runge 라는 사람이 만든 Runge 함수이다, 오히려 낮은 차수 일때 더 유사할때가 있다는 것 

 

 

 

 

 

이글의 원문 :

 

● 발표분야: 프로그래밍
● 발표자: 넥슨코리아 정성훈 / Nexon Korea Sunghoon Jung
● 권장 대상: 프로그래머, 시스템 디자이너
● 키워드: #네트워크 #동기화 #연출 #프라시아전기

반응형
반응형

 

서버에는 탈것 에대한 상태만 있고 오브젝트는 없음

크랄에서 탈것의 비주얼을 보여줌 

 



  1. 타기요청패킷 : 일반 상태에서 서버에 부르기를 요청을 시작하면서 타기가 시작됨
    1. 이때 서버는 타기에 필요한 검사를 함
      캐릭터의 이동 방향을 보고 탈것이 캐릭터를 태우러 갈 수 있는지 확인 필요
      이때 네비게이션 메시 상에서도 유효한 경로인지 같이 검사를 한다
  2. 부르기 패킷(만날위치포함) : 준비가 끝나면 문제가 없다 판단 되면, 
    1. 모든 클라에게 탈것에 대한 만날 위치포함하여 부르기 알람 메세지를 보낸다 
      그리고 탈것이 어디로 갈지에 대한 정보도 포함한다
    2. 부르기에 필요한 시간을 기다렸다가 랑데부 알람 메세지를 클라에게 보내기 위해 서버에서 예약한다
  3. 랑데부 패킷 : (랑데부는 만남, 만나는 장소라는 뜻) 
    1. 클라에서 랑데부를 받으면 클라에서 타기지점으로 뛰기 시작한다
    2. 탈것을 스폰하고 캐릭터를 향해 달리게 한다
      캐릭터는 이 지점을 향해 달려 가는 연출을 한다
      이때 캐릭터는 탈것을 부르면서도 계속 달리는 중임으로 부르기 상태에서부터 다른 이동의 간섭을 막고 탈곳과 만나기 위한 곳으로 달려 나간다
      이때 캐릭터와 탈것은 계속 달리는 중이다 
  4. 오르기요청 : 클라가 캐릭터가 탈 준비가 완료 되면 서버에 알려주는 방식
    클라에서 서버로 알리는데 => 캐릭터와 탈것은 계속 달리는 중이기 때문에 서버와 메시지를 주고 받는 동안에도 위치가 계속 바뀌고 있는 중임으로 이때 오차가 발생 할 수 있는데 이를 줄이기 위해  
    1. 캐릭터와 탈것의 위치를 서버로 같이 전달한다
    2. 서버에서는 탈것의 위치가 탈것에 올라타는 캐릭터의 새 위치가 되기 때문에 이 둘의 범위가 약속한 범위 안쪽으로 들어오면 
      모든 클라에게 오르는중패킷을 보낸다
    3. 나 자신은 이미 이전에 오르는 연출을 시작 하기 때문에 오르기요청에 대한 응답 패킷은 무시하지만
      다른 클라는 오르는중 패킷을 받으면 오르는 연출을 시작한다
  5. 오르기 완료 패킷 : 오르는 중이 완료 되는 시간이 정해져 있고 이 시간이 완료 되면 상태와 오르기완료패킷을 클라들에게 보내어 오르기를 완료한다, 

 

위 이미지인데 타는 경로를 아래 처럼 생각 해볼 수 있다

 

캐릭터 오르는 연출

  • 랑데부 에서
    • 캐릭터와 탈것이 각각 어느 위치에 있을때 오르기가 시작되는지를 결정해야한다
      캐릭터가 오르기 시작 애니가 나가고 -> 탈것 기준에서 오르기 시작한 지점과 오르기 완료된 지점까지 완료 되면 이때 약간 더 앞으로 전진하면 원래 캐릭터가 이동하던 직직방향과 만나게 된다, 그리고 오르기 완료시까지 플레이어 입력을 일시적으로 제한한다
      일시적으로 제한되는 사이에 연출이 일어나기 때문에 이때의 시간은 기획자가 알아서 제어하도록 테이블로 빼놓음, 말이나 탈것에 따라 연출이 달라질 수 있기 때문 
      그리고 타는 동안 속력은 기존 그대로 고정한다
  • 오르기 완료 
    • 캐릭터 액터를 탈것 액터에 부착하는 방식
      • 캐릭터와 탈것의 위치와 방향은 다르지만 일단 붙여놓고 애니메이션을 재생 하면서 일정시간동안 위치와 방향의 차이가 0이 되게끔 한다
      • 이 시간은 오르기 애니메이션 시간 중에서 캐릭터가 탈것에 닿을 때까지의 시간 , 이때 위치와 방향은 형태와 상관 없이 선형적으로 처리
      • 이때 카메라는 캐릭터 위치를 따라가게 한다음 타기가 완료되면 원래 카메라를 제어하던 곳으로 반환 시켜준다
  • 모바일에서 인터넷이 일시적으로 안되는 상황을 대비해 현재 탈것 상태를 다시 알려주는 처리를 예방 , 클라는 서버에서 주는 상태대로 클라이언트 상황을 맞춰 적용해줘야함 => 현재 위치에서 다음 목표 지점이 다이렉트로 나오는 방식
  • 길이 좁거나 막혀 있다면 탈것이 올 수가 없으니 이때는 제자리에서 바로 탈것을 타도록 한다 
  • 캐릭터가담당 서버가 바뀌는 지역으로 이동하는 경우 
    • 타려는 와중에 서버 이주가 진행 된다면 서버에서 바로 오르기 완료 상태로 바꾸고 클라이언트에 알려주는 상태로 처리 => 임의의 상태로 바로 전환되게 처리 하는 것이 맞아 떨어짐
  • 클라위치와 탈것이 만나지 못하는 거리로 탈것이 지나간다면 그때는 타지 못하고 지나가고  지나간 이후에 제자리에서 바로 탄 상태가 되도록 처리




  • 내리기 처리 : 빠르게 플레이로 돌아갈 수 있는데 집중
    • 오르기완료 상태라면 내리기 처리를 시작
    • 내리는 중, 내리기 완료 패킷을 서버에서 순차적으로 보내준다
      • 내리기과정은 특별히 진행 될게 없고 서버에서 보내주는대로 처리하기면 하면 됨
      • 내릴때 현재 바라보고 있는 방향으로 달려 나가면서 내리도록 처리하고
        위치도 탈것 상태를 전달하면서 같이 모든 클라에게 전달하여 모든 클라이언트에서 같은 곳이 될 수 있도록 처리
      • 내릴때는 탈때와의 반대 과정으로 처리 : 캐릭터와 탈것은 때어내면서 캐릭터 위치는 고정하고 이쪽으로 캐릭터가 이동 되게 하면서 애니메이션 재생
      • 내리는 중 패킷을 클라가 받은 것이라면 내리기 완료 패킷이 오는것은 확정적이라서 먼저 내려서 걸어가고 조작 될 수있게 처리 하면 자연스러워짐 
        예를 들어 말에서 바로 내리는 순간 공격이 가능하다



  • 곡선이동 자동이동 길잡이
    • 곡선이동 : 
    • 캐릭터의 이동은 기본적으로 직선이동이다, 이동 회전이 있긴 한데 이동 경로가 딱딱하다 , 이동회전은 느낌이 딱딱하다
    • 그래서 먼거리를 갈때는 
      먼저 큰 길을 찾아가고 목적지에 가까운 곳까지 큰길을 통해 이동하는 길 찾기 방식을 사용, 이러면 계속해서 큰길들의 분기점을 지나게 된다

 

 

  • 이 분기점을 지날때 급격히 방향이 꺽이는 경우들이 많다
  • 탈것을 타고 빠르게 지나갈때는 문제가 더 잘보이는데 이때 곡선
    형태로 이동 할 수 있는 것을 추가, 이 지역은 내비메쉬가 깔린 지역이다

  • 이런 점들에선 곡선형태로 분이점을 지나도록 처리
  • 베지어 곡선 사용 2차
    곡선이 항상 중심을 향해 들어오고 중심에서 가도록 하기위해 A1, B1 을 잡는데 각 직선에서 ¼ 지점정도에 만든다
    중간  지점을 만들기 위해 B0 지점을 임의로 만든다
  • 이때 샘플값이 균일한 곡선이 나오진 않는데
  • 이것은 시간이 아닌 이동거리를 통해 곡선의 이동 거리를 찾으면 됨
  • 2차 베지어 곡선은 특정 구간의 거리를 계산식 하나로 구할 수 있는 특성이 있다(이것이 다른 곡선과 달리 거리에 대한 이점이 있는 곡선이라 이걸 선택함) 
  • 거리를 통해 위치를 찾을때는 정확한 지점을 찾긴 어려우니 근사 값으로 찾음(뉴턴랩스방법) : 뉴턴법은 식조작으로 풀지못하는 식의해의 근사 값을 구하는 법으로 접선으로 구한다


  • 멀리서부터 접선을 그리다 보면 빨간점의 해와 접접 가까워지면서 해를 구하는 것 
  • 뉴턴방법 = 뉴턴-랩슨 방법  이라한다
    x0 에 해당하는 fx 의 접선을 구하고 이 접선과 만나는 x 축의 점 x1 에 대해 다시 기울기를 구해 이것을 반복하는 방식 으로 근사 해를 구하는 방식중 가장 빠르다 
  • 이것을 이용해서 거리기반의 위치를 구한다
    spline component는 에르미트 곡선을 사용한다
    에르미트 곡선은 거리에 따른 위치 값을 바로 얻어 올 수 있어서 이걸 쓰면




  • 자동길잡이 : 캐릭터의 이동 경로를 미리 보여주는 기능
    • 자동이동이 시작 되는 지점에서 이동해야 하는 경로가 대부분 결정되기 때문에
      이 경로를 미리 보여주는 것 이건 나한테만 보여지는 것으로 splinecomponent 를
      그대로 이용해서 표현이 가능하다

 

 

 

이글의 원문

프라시아 전기에 멋진 탈것 만들기
멋지게 타고, 멋지게 달리고, 멋지게 내리자

● 발표분야: 프로그래밍
● 발표자: 넥슨코리아 이연석 / Nexon Korea Yeonseok Yi
● 권장 대상: 게임 프로그래머, 클라이언트 프로그래머, UE4 프로그래머

반응형
반응형

3 way handshake

TCP는 장치들 사이에 논리적인 접속을 성립시키기 위해 3 way handshake를 사용한다.

TCP 3 way handshake(이하 3way)는 TCP/IP 프로토콜을 이용해서 통신을 하는 응용프로그램이

데이터를 전송하기 전에 먼저 정확한 전송을 보장하기 위해 상대방 컴퓨터와 사전에 세션을 수립하는 과정을 의미한다.

TCP 통신은 PAR(Positive Acknowledgement with Re-transmission)을 통해 신뢰적인 통신을 제공한다.

PAR을 사용하는 기기는 ack을 받을 때까지 데이터 유닛을 재전송한다.

수신자가 데이터 유닛(세그먼트)이 손상된 것을 확인하면 해당 세그먼트를 없앤다.

그러면 sender는 positive ack이 오지 않은 데이터 유닛을 다시 보내야 한다.

작동방식

HOST P와 HOST Q가 있을 때, HOST P가 클라이언트, HOST Q가 서버 라고 가정해보자.

 
  1. 클라이언트가 서버에게 SYN 패킷을 보냄.Client > Server : TCP SYN
  2. 서버가 SYN(x)을 받고 클라이언트로 받았다는 신호 ACK(x + 1)와 SYN(y)패킷을 보냄.Server > Client : TCP SYN, ACK
  3. 클라이언트는 서버의 응답(ACK(x+1), SYN(y))을 받고, ACK(y+1)를 서버로 보냄.Client > Server : TCP ACK
  • SYN : 'Synchronize sequence numbers', 연결 요청. 세션을 설정하는데 사용되며 초기에 시퀀스 번호를 보냄.
  • ACK : 'Acknowledgement', 보낸 시퀀스 번호에 TCP 계층에서의 길이 또는 양을 더한 것과 같은 값을 ACK에 포함하여 전송.

동기화 요청에 대한 답변: Client의 Sequence Number + 1을 하여 ACK로 돌려준다.

이렇게 3번의 통신이 완료되면 연결이 성립된다.

SYN을 보내면 ACK를 받을 때 까지 재전송한다. ACK가 와야 완료된다는 뜻이다.

이러한 행위로 양쪽 모두 데이터를 전송할 준비가 되었다는 것을 보장하고,

실제로 데이터 전달이 시작하기 전에 한쪽이 다른 쪽이 준비되었다는 것을 알 수 있도록 한다.


4 way handshake

4 way handshake는 연결을 해제하는 과정이다. 여기서는 FIN 플래그를 이용한다.

  • FIN : 세션을 종료시키는데 사용되며, 더 이상 보낸 데이터가 없음을 나타낸다.

Termination의 종류

TCP는 두 가지 연결 해제 방식이 있다.

  1. Graceful connection release(정상적인 연결 해제)

정상적인 연결 해제에서는 양쪽이 커넥션을 모두 닫을 때까지 연결되어 있다.

  1. Abrupt connection release(갑작스런 연결 해제)
    1. 갑자기 한 TCP 엔티티가 연결을 강제로 닫는 경우
    2. 한 사용자가 두 데이터 전송 방향을 모두 닫는 경우

Abrupt

RST(TCP reset) 세그먼트가 전송되면 갑작스러운 연결 해제가 수행되는데, RST 세그먼트는 다음과 같은 경우에 전송된다.

  1. 존재하지 않는 TCP 연결에 대해 비 SYN 세그먼트가 수신된 경우
  2. 열린 커넥션에서 일부 TCP 구현이 잘못된 헤더가 있는 세그먼트가 수신된 경우
    • RST 세그먼트를 보내, 해당 커넥션을 닫아 공격을 방지한다.
  3. 일부 구현에서 기존 TCP 연결을 종료해야 하는 경우

Graceful

연결 종료 요청 역시, 요청을 먼저 시도한 요청자를 Client로, 요청을 받은 수신자를 Server 쪽으로 생각하면 된다.

 
  1. 클라이언트는 서버에게 연결을 종료한다는 FIN 플래그 보냄.
  2. 서버는 FIN을 받고, 확인했다는 ACK를 클라이언트에게 보낸다. 이 때, 모든 데이터를 보내기 위해 CLOSE_WAIT 상태가 된다.
  3. 데이터를 모두 보냈다면, 연결이 종료되었다는 FIN 플래그를 클라이언트에게 보낸다.
  4. 클라이언트는 FIN을 받고, 확인했다는 ACK를 서버에게 보낸다. 이 때 아직 서버로부터 받지 못한 데이터가 있을 수 있으므로 TIME_WAIT을 통해 기다린다.
    1. 서버는 ACK를 받고, 소켓을 닫는다(Closed)
    2. TIME_WAIT 시간이 끝나면 클라이언트도 닫는다 (Closed) => 의도치 않은 에러로 연결이 데드락으로 빠지는 것을 방지한다.

이렇게 4번의 통신이 완료되면 연결이 해제된다.

 

 

ref : https://mirror.xyz/0xA1d9f681B25C14C1eE7B87f1CF102E73cA3ad4d9/l3J2_t_85JfypzGWRdgX-qMYW7nob6XIwi1BkXs4Gm4

반응형
반응형

 

command 패턴으로 서버에서 처리할 명령을 바로 처리하지 않고 우선 처리할 명령을 Queue 에 먾어 넣어 모아두었다가

이 방식은 서버가 처리 할수 있을때 처리 하는 방식으로 직접적으로 명령을 다수의 스레드가 한번에 처리 할때 lock 을 걸어야 하는 상황에서의 부하를 줄일 수 있다


 

설명

이전까지는 클라이언트 세션을 연결되자마자 바로 GameRoom의 List에 넣어주고 있었지만, 이번에는 JobQueue 개념을 이용해 Queue에 일단 GameRoom의 List에 넣어주는 작업을 '예약'해주고 있다.


코드

ServerCore

JobQueue.cs

public interface IJobQueue
    {
        void Push(Action job);
    }

    public class JobQueue : IJobQueue
    {
        Queue<Action> _jobQueue = new Queue<Action>();  
        object _lock = new object();
        bool _flush = false; // 큐에 쌓인걸 '자신이' 실행할건지. 누군가 하고 있으면 자신은 하지 않는다.

        public void Push(Action job)
        {
            bool flush = false;

            lock(_lock)
            {
                _jobQueue.Enqueue(job);
                if (_flush == false)
                    flush = _flush = true;

            }

            if (flush)
                Flush();
        }

        void Flush()
        {
            while(true)
            {
                // 하나씩 꺼내는 와중에도 다른 애가 Push해서 JobQueue에다 넣을 수 있기 때문에 Pop을 할 때 lock을 잡아줘야 한다.
                Action action = Pop();
                if (action == null)
                    return;

                action.Invoke();
            }
        }

        Action Pop()
        {
            lock(_lock)
            {
                if(_jobQueue.Count == 0)
                {
                    _flush = false;
                    return null;
                }
                return _jobQueue.Dequeue(); 
            }
        }
    }
public class Listener
	{
		Socket _listenSocket;
		Func<Session> _sessionFactory;

		public void Init(IPEndPoint endPoint, Func<Session> sessionFactory, int register = 10, int backlog = 10)
		{
			_listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
			_sessionFactory += sessionFactory;

			// 문지기 교육
			_listenSocket.Bind(endPoint);

			// 영업 시작
			// backlog : 최대 대기수
			_listenSocket.Listen(backlog);

			for (int i = 0; i < register; i++)
			{
				SocketAsyncEventArgs args = new SocketAsyncEventArgs();
				args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
				RegisterAccept(args);
			}
		}
        
        ...

Server

GameRoom.cs

 class GameRoom : IJobQueue
    {
        List<ClientSession> _sessions = new List<ClientSession>();
        JobQueue _jobQueue = new JobQueue();

        public void Push(Action job)
        {
            _jobQueue.Push(job);
        }

        public void Broadcast(ClientSession session, string chat)
        {
            // 이 부분은 다른 쓰레드와 공유하고 있지 않음
            S_Chat packet = new S_Chat();
            packet.playerId = session.SessionId;
            packet.chat = $"{chat} 나는 {packet.playerId}";
            ArraySegment<byte> segment = packet.Write();

            foreach (ClientSession s in _sessions)
                s.Send(segment);
            
        }

        public void Enter(ClientSession session)
        {
            _sessions.Add(session);
            session.Room = this;
        }

        public void Leave(ClientSession session)
        {
            _sessions.Remove(session);
        }
    }

ClientSession.cs

class ClientSession : PacketSession
	{
		public int SessionId { get; set; }	
		public GameRoom Room { get; set; }

		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");

			Program.Room.Push(() => Program.Room.Enter(this));
		}

		public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			PacketManager.Instance.OnRecvPacket(this, buffer);
		}

		public override void OnDisconnected(EndPoint endPoint)
		{
			SessionManager.Inst.Remove(this);
			if(Room != null)
            {
				Room.Push(() => Room.Leave(this));
				Room = null;
            }
			Console.WriteLine($"OnDisconnected : {endPoint}");
		}

		public override void OnSend(int numOfBytes)
		{
			Console.WriteLine($"Transferred bytes: {numOfBytes}");
		}
	}

PacketHandler.cs

class PacketHandler
{
	public static void C_ChatHandler(PacketSession session, IPacket packet)
	{
		C_Chat chatPacket = packet as C_Chat;
		ClientSession clientSession = session as ClientSession;

		if (clientSession.Room == null)
			return;

		GameRoom room = clientSession.Room;

		// 할 일을 바로 해주는게 아니라 Push
		room.Push(() => room.Broadcast(clientSession, chatPacket.chat));
	}
}
반응형

'서버(Server) > Server' 카테고리의 다른 글

동기화 처리 : 탈것  (0) 2023.03.18
TCP : 3 way handshake(연결), 4 way handshake(종료)  (0) 2023.03.06
채팅서버  (0) 2023.01.06
Protobuf 로 패킷 보내기  (0) 2023.01.05
PacketSession  (0) 2022.12.29
반응형

설명

Server의 Main에서 Listener를 Init할때 ClientSession 객체를 바로 생성해주는 것이 아닌, SessionManager를 통해 Generate해서 모든 세션을 관리하는 방식을 채택했다.
세션마다 고유 번호를 할당하고 id를 키값으로 Dictionary에 ClientSession을 넣어서 생성, 찾기, 삭제를 할 때마다 lock을 걸어 동시 접근을 차단하는 식으로 구현했다.

서버에는 하나의 GameRoom이 존재하며, 하나의 GameRoom에는 List타입의 _sessions가 존재한다. 그리고 GameRoom에서 어떤 하나의 클라이언트가 메시지를 서버에게 전송하면 서버는 입장 중인 모든 세션 객체에 그 메시지를 Broadcast한다.

하나의 ClientSession이 서버와의 접속이 끊어질 경우, OnDisconnected함수가 호출되어 SessionManager.Remove와 해당 세션이 참여 중인 GameRoom이 있을 경우 GameRoom.Leave() 함수 호출을 통해 퇴장하도록 했다.

10명의 유저가 한 공간에 존재한다고 할때, 한 명의 유저가 메시지를 전송하면, 그 공간 안에 10명의 유저에게 뿌려줘야 한다. 그렇다면 10명의 유저가 동시에 메시지를 하나씩 전송한다면, 10*10=100번의 패킷을 전송해야한다는 말이다. 시간복잡도로 말하자면 O(n^2)가 된다. 그래서 n을 100, 1000으로 늘리면 늘릴수록 서버에 부담이 갈 수 있다.

코드를 그대로 실행해보면 Broadcast 부분에서 수많은 작업자 스레드가 lock에서 대기하고 있는 모습을 볼 수 있다. 하나의 쓰레드가 foreach문을 다 돌 때까지 다른 쓰레드가 대기를 할 수 밖에 없는 상황인데, 최악인 것은 쓰레드가 처리를 못하고 시간을 끌면 작업자 쓰레드를 새로 만들어버리는 악순환이 발생한다는 것이다.

따라서 모든 로직을 lock을 잡아 실행하는 것이 아니라 GameRoom에 Queue를 하나 만들고, 쓰레드들이 일감을 queue에 넣어두고 대기하지 말고 각자 할 일을 하러 가게끔 만들면 된다. 그런 큐를 JobQueue라고 한다.

 

 

 

코드

PDL.xml

<?xml version="1.0" encoding="utf-8" ?>
<PDL>
  <packet name="C_Chat">
	  <string name="chat"/>
  </packet>
  <packet name="S_Chat">
	<int name="playerId"/>
	<string name="chat"/>
  </packet>
</PDL>

Server

GameRoom.cs

 class GameRoom
    {
        List<ClientSession> _sessions = new List<ClientSession>();
        object _lock = new object();

        public void Broadcast(ClientSession session, string chat)
        {
            // 이 부분은 다른 쓰레드와 공유하고 있지 않음
            S_Chat packet = new S_Chat();
            packet.playerId = session.SessionId;
            packet.chat = $"{chat} 나는 {packet.playerId}";
            ArraySegment<byte> segment = packet.Write();

            lock(_lock)
            {
                foreach (ClientSession s in _sessions)
                    s.Send(segment);
            }
        }

        public void Enter(ClientSession session)
        {
            lock (_lock)
            {
                _sessions.Add(session);
                session.Room = this;
            }
        }

        public void Leave(ClientSession session)
        {
            lock (_lock)
            {
                _sessions.Remove(session);
            }
        }
    }

SessionManager.cs

 class SessionManager
    {
        static SessionManager _session = new SessionManager();
        public static SessionManager Inst { get { return _session; } }

        // 세션마다 고유 번호
        int _sessionId = 0;
        Dictionary<int, ClientSession> _sessions = new Dictionary<int, ClientSession> ();
        object _lock = new object ();   

        public ClientSession Generate()
        {
            lock(_lock)
            {
                int sessionId = ++_sessionId;

                ClientSession session = new ClientSession ();
                session.SessionId = sessionId;
                _sessions.Add(sessionId, session);

                Console.WriteLine($"Connected : {sessionId}");

                return session;
            }
        }

        public ClientSession Find(int id)
        {
            lock(_lock)
            {
                ClientSession session = null;
                _sessions.TryGetValue (id, out session);
                return session;
            }
        }

        public void Remove(ClientSession session)
        {
            lock(_lock)
            {
                _sessions.Remove(session.SessionId);
            }
        }
    }

ClientSession.cs

class ClientSession : PacketSession
	{
		public int SessionId { get; set; }	
		public GameRoom Room { get; set; }

		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");

			Program.Room.Enter(this);
		}

		public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			PacketManager.Instance.OnRecvPacket(this, buffer);
		}

		public override void OnDisconnected(EndPoint endPoint)
		{
			SessionManager.Inst.Remove(this);
			if(Room != null)
            {
				Room.Leave(this);
				Room = null;
            }
			Console.WriteLine($"OnDisconnected : {endPoint}");
		}

		public override void OnSend(int numOfBytes)
		{
			Console.WriteLine($"Transferred bytes: {numOfBytes}");
		}
	}

PacketHandler.cs

class PacketHandler
{
	public static void C_ChatHandler(PacketSession session, IPacket packet)
	{
		C_Chat chatPacket = packet as C_Chat;
		ClientSession clientSession = session as ClientSession;

		if (clientSession.Room == null)
			return;

		clientSession.Room.Broadcast(clientSession, chatPacket.chat);
	}
}

DummyClient

Program.cs

class Program 
	{
		static void Main(string[] args)
		{
			// DNS (Domain Name System)
			string host = Dns.GetHostName();
			IPHostEntry ipHost = Dns.GetHostEntry(host);
			IPAddress ipAddr = ipHost.AddressList[0];
			IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

			Connector connector = new Connector();

			connector.Connect(endPoint, () => { return SessionManager.Inst.Generate(); }, 10); // 클라 숫자 10개 접속

			while (true)
			{
				try
				{
					SessionManager.Inst.SendForEach();
				}
				catch (Exception e)
				{
					Console.WriteLine(e.ToString());
				}

				Thread.Sleep(250);
			}
		}
	}

SessionManager.cs

  class SessionManager
    {
        static SessionManager _session = new SessionManager();
        public static SessionManager Inst { get { return _session; } }

        List<ServerSession> _sessions = new List<ServerSession>();
        object _lock = new object();
        public void SendForEach()
        {
            lock (_lock)
            {
                foreach (ServerSession session in _sessions)
                {
                    C_Chat chatPacket = new C_Chat();
                    chatPacket.chat = $"Hello Server!!!";
                    ArraySegment<byte> segment = chatPacket.Write();

                    session.Send(segment);
                }
            }
        }

        public ServerSession Generate()
        {
            lock(_lock)
            {
                ServerSession session = new ServerSession();
                _sessions.Add(session);
                return session;
            }
        }
    }

PacketHandler.cs

class PacketHandler
{
	public static void S_ChatHandler(PacketSession session, IPacket packet)
	{
		S_Chat chatPacket = packet as S_Chat;
		ServerSession serverSession = session as ServerSession;
		
        Console.WriteLine(chatPacket.chat);
	}
}

 

 

ref : https://velog.io/@fere1032/%EC%B1%84%ED%8C%85%ED%85%8C%EC%8A%A4%ED%8A%B8-1

반응형
반응형

아래는 프로토버프에서 구조화된 구조체를 만들기 위한 프로토버프의 문법으로 만든 예시이다

 

위 처럼 패킷을 프로토 버프 문법으로 작성한다음 

protobuf 에서 제공하는 컴파일로 컴파일 하게 되면 직렬화하여 넣을 수 있는 구조가 만들어지고

다음 처럼 C++ 에서 사용 하여 데이터를 넣을 수 있다

 

add_buffs() 는 데이터를 내무적으로 추가하고 포인터를 얻어와 data 포인터를 통해서 데이터를 쓰는 방식이다

 

 

sendbuffer 에 패킷 데이터를 밀어 넣기

->Buffer());  //위 코드 중 짤린 부분

 

S_TEST 패킷을 만들어 데이터를 넣은 모습이다

 

 

실제 패킷 보내기처리

 

 

 

 

다음은 역질렬화방법이다

헤더 만큼은 건너 뛰고 S_TEST::pkt 에 데이터를 담아온다

 

 

 

 

 

 

 

 

 


 

프로토토콜 버퍼는 구글에서 개발하고 오픈소스로 공개한, 직렬화 데이타 구조 (Serialized Data Structure)이다. C++,C#, Go, Java, Python, Object C, Javascript, Ruby 등 다양한 언어를 지원하며 특히 직렬화 속도가 빠르고 직렬화된 파일의 크기도 작아서 Apache Avro 파일 포맷과 함께 많이 사용된다.

(직렬화란 데이타를 파일로 저장하거나 또는 네트워크로 전송하기 위하여 바이너리 스트림 형태로 저장하는 행위이다.)

 

특히 GRPC 라는 네트워크 프로토콜의 경우 HTTP 2.0 을 기반으로 하면서, 메세지를 이 프로토콜 버퍼를 이용하여 직렬화하기 때문에, 프로토콜 버퍼를 이해해놓으면 GRPC를 습득하는 것이 상대적으로 쉽다.

 

프로토콜 버퍼는 하나의 파일에 최대 64M까지 지원할 수 있으며, 재미있는 기능중 하나는 JSON 파일을 프로토콜 버퍼 파일 포맷으로 전환이 가능하고, 반대로 프로토콜 버퍼 파일도 JSON으로 전환이 가능하다.

설치 및 구성

프로토콜 버퍼 개발툴킷은 크게 두가지 부분이 있다. 데이타 포맷 파일을 컴파일 해주는 protoc 와 각 프로그래밍 언어에서 프로토콜 버퍼를 사용하게 해주는 라이브러리 SDK가 있다.

 

protoc 컴파일러와, 각 프로그래밍 언어별 SDK는 https://github.com/google/protobuf/releases  에서 다운 받으면 된다.

 

protoc 는 C++ 소스 코드를 직접 다운 받아서 컴파일하여 설치할 수 도 있고, 아니면 OS 별로 미리 컴파일된 바이너리를 다운받아서 설치할 수 도 있다.  

 

각 프로그래밍 언어용 프로토콜 버퍼 SDK는 맞는 버전을 다운 받아서 사용하면 된다. 파이썬 버전 설치 방법은  https://github.com/google/protobuf/tree/master/python 를 참고한다.

이 글에서는 파이썬 SDK 버전을 기준으로 설명하도록 한다.

구조 및 사용 방법

프로토콜 버퍼를 사용하기 위해서는 저장하기 위한 데이타형을 proto file 이라는 형태로 정의한다. 프로토콜 버퍼는 하나의 프로그래밍 언어가 아니라 여러 프로그래밍 언어를 지원하기 때문에, 특정 언어에 종속성이 없는 형태로 데이타 타입을 정의하게 되는데, 이 파일을 proto file이라고 한다.

이렇게 정의된 데이타 타입을 프로그래밍 언어에서 사용하려면, 해당 언어에 맞는 형태의 데이타 클래스로 생성을 해야 하는데, protoc 컴파일러로 proto file을 컴파일하면, 각 언어에 맞는 형태의 데이타 클래스 파일을 생성해준다.

 

다음은 생성된 데이타 파일을 프로그래밍 언어에서 불러서, 데이타 클래스를 사용하면 된다.

 

 

 

 

 

 

 

 

 

 

ref : https://bcho.tistory.com/1182

반응형

'서버(Server) > Server' 카테고리의 다른 글

채팅서버 JobQueue 방식으로 부하 줄이기(command 패턴)  (0) 2023.01.06
채팅서버  (0) 2023.01.06
PacketSession  (0) 2022.12.29
TCP, UDP 차이  (0) 2022.12.27
send 시 데이터를 취합해 한번에 보내기  (0) 2022.12.25
반응형

 

결과 화면인데 패킷중 첫번째를 size로 받아와 size 바이트 만큼 받아 왔으면 그다음 id 그리고 데이터를 순차적으로 읽어온다, 여기서 패킷은 TCP 로 전송 될경우 데이터 한 덩어리중 일부가 지연으로 나중에 올경우를 대비해 

 

패킷 중 사이즈가 2바이트라면 2바이트를 먼저 받고 해당 사이즈를 먼저 추출한다음 전체 바이트 만큼 모두 도착 할때까지 수신을 반복한다, 그리고 모두다 데이터를 수신 받았다면 이후부터 패킷의 내용들을 까서 데이터로 받는 처리를 하게 된다

 

TCP 는 데이터의 순서가 보장 됨으로 나중에 왔다 하더라고 모두 전송된 데이터의 비트들은 정상적으로 정렬되어 있다

 

 

public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			int pos = 0;

			ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset); 
			pos += 2;
			ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + pos);
			pos += 2;

			// TODO
			switch ((PacketID)id)
			{
				case PacketID.PlayerInfoReq:
					{
						long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + pos);
						pos += 8;
					}
					break;
				case PacketID.PlayerInfoOk:
					{
						int hp = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
						int attack = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
					}
					//Handle_PlayerInfoOk();
					break;
				default:
					break;
			}

			Console.WriteLine($"RecvPacketId: {id}, Size {size}");
		}

 

 

Thread Local Storage(TLS) 

TLS : 스레드 빌딩 블록으로 스레드 마다 고유한 메모리 영역에 할당되게 된다

public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });

 

 

 


코드

ServerCore

Session.cs

namespace ServerCore
{
	public abstract class PacketSession : Session
	{
		public static readonly int HeaderSize = 2;

		// size = size 포함한 전체 패킷 크기
		// [size(2)][packetId(2)][ ... ][size(2)][packetId(2)][ ... ]
		public sealed override int OnRecv(ArraySegment<byte> buffer) // 오버라이드 다시 불가
		{
			int processLen = 0;

			while (true)
			{
				// 최소한 헤더는 파싱할 수 있는지 확인
				if (buffer.Count < HeaderSize)
					break;

				// 패킷이 완전체로 도착했는지 확인
				ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset); // ushort
				if (buffer.Count < dataSize)
					break;

				// 여기까지 왔으면 패킷 조립 가능. new 사용했다고 힙에다 할당해주는게 아니라 스택 복사
				OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize));
				
				processLen += dataSize;
				// [size(2)][packetId(2)][ ... ] 다음 부분으로 위치 변경
				buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
			}

			return processLen;
		}

		public abstract void OnRecvPacket(ArraySegment<byte> buffer);
	}

	public abstract class Session
	{
		Socket _socket;
		int _disconnected = 0;

		RecvBuffer _recvBuffer = new RecvBuffer(1024);

		object _lock = new object();
		Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>();
		List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>();
		SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
		SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs();

		public abstract void OnConnected(EndPoint endPoint);
		public abstract int  OnRecv(ArraySegment<byte> buffer); // 얼마만큼 데이터를 처리했는지 리턴
		public abstract void OnSend(int numOfBytes);
		public abstract void OnDisconnected(EndPoint endPoint);

		public void Start(Socket socket)
		{
			_socket = socket;

			_recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
			_sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

			RegisterRecv();
		}

		public void Send(ArraySegment<byte> sendBuff)
		{
			lock (_lock)
			{
				_sendQueue.Enqueue(sendBuff);
				if (_pendingList.Count == 0)
					RegisterSend();
			}
		}

		public void Disconnect()
		{
			if (Interlocked.Exchange(ref _disconnected, 1) == 1)
				return;

			OnDisconnected(_socket.RemoteEndPoint);
			_socket.Shutdown(SocketShutdown.Both);
			_socket.Close();
		}

		#region 네트워크 통신

		void RegisterSend()
		{
			while (_sendQueue.Count > 0)
			{
				ArraySegment<byte> buff = _sendQueue.Dequeue();
				_pendingList.Add(buff);
			}
			_sendArgs.BufferList = _pendingList;

			bool pending = _socket.SendAsync(_sendArgs);
			if (pending == false)
				OnSendCompleted(null, _sendArgs);
		}

		void OnSendCompleted(object sender, SocketAsyncEventArgs args)
		{
			lock (_lock)
			{
				if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
				{
					try
					{
						_sendArgs.BufferList = null;
						_pendingList.Clear();

						OnSend(_sendArgs.BytesTransferred);

						if (_sendQueue.Count > 0)
							RegisterSend();
					}
					catch (Exception e)
					{
						Console.WriteLine($"OnSendCompleted Failed {e}");
					}
				}
				else
				{
					Disconnect();
				}
			}
		}

		void RegisterRecv()
		{
			_recvBuffer.Clean(); // 커서가 너무 뒤로 가있는 상태 방지
			// 유효한 범위 설정. 다음으로 버퍼를 받을 공간 Set.
			ArraySegment<byte> segment = _recvBuffer.WriteSegment;
			_recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count); // count=freesize, 이만큼 받을 수 있다.

			bool pending = _socket.ReceiveAsync(_recvArgs);
			if (pending == false)
				OnRecvCompleted(null, _recvArgs);
		}

		void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
		{
			if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
			{
				try
				{
					// Write 커서 이동
					if (_recvBuffer.OnWrite(args.BytesTransferred) == false)
					{
						Disconnect();
						return;
					}

					// 컨텐츠 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다
					int processLen = OnRecv(_recvBuffer.ReadSegment);
					if (processLen < 0 || _recvBuffer.DataSize < processLen)
					{
						Disconnect();
						return;
					}

					// Read 커서 이동
					if (_recvBuffer.OnRead(processLen) == false)
					{
						Disconnect();
						return;
					}

					RegisterRecv();
				}
				catch (Exception e)
				{
					Console.WriteLine($"OnRecvCompleted Failed {e}");
				}
			}
			else
			{
				Disconnect();
			}
		}

		#endregion
	}
}

SendBuffer

namespace ServerCore
{
	public class SendBufferHelper
	{
		// 전역이지만 내 쓰레드에서만 사용할 수 있음.
		public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; }); // 처음 만들어질때 null만 리턴하도록

		public static int ChunkSize { get; set; } = 4096 * 100;

		public static ArraySegment<byte> Open(int reserveSize)
		{
			if (CurrentBuffer.Value == null) // SendBuffer 한번도 사용 안한 상태
				CurrentBuffer.Value = new SendBuffer(ChunkSize);

			if (CurrentBuffer.Value.FreeSize < reserveSize)
				CurrentBuffer.Value = new SendBuffer(ChunkSize); // 기존 청크 없앤 후 새롭게 할당

			return CurrentBuffer.Value.Open(reserveSize);
		}

		public static ArraySegment<byte> Close(int usedSize)
		{
			return CurrentBuffer.Value.Close(usedSize);
		}
	}

	public class SendBuffer
	{
		// RecvBuffer처럼 Clean이 없는 이유는 내가 사용이 끝난 부분 이전 부분을 다른 세션에서 Session클래스의 _sendQueue에 넣어논 상태일 수 있기 때문에, 즉 누군가 앞부분을 참조 중인 상태일 수 있기 때문에 재위치 시킬 수 없다.
		// 패킷에 가변인자 들어가면 크기 예측하는게 까다롭기 때문에, 크게 할당 받아놓고 자르는 방식
		// [][][][][][][][][u][] : ChunkSize = 4096 * 100
		byte[] _buffer;
		int _usedSize = 0;

		public int FreeSize { get { return _buffer.Length - _usedSize; } }

		public SendBuffer(int chunkSize)
		{
			_buffer = new byte[chunkSize];
		}

		public ArraySegment<byte> Open(int reserveSize) // 요구할 예약 공간
		{
			if (reserveSize > FreeSize)
				return null;

			// 예약공간이기 때문에 usedSize 이동 x
			return new ArraySegment<byte>(_buffer, _usedSize, reserveSize); // usedSize 위치 포함해서 reserveSize만큼
		}

		public ArraySegment<byte> Close(int usedSize) // 예약 사이즈는 3이라도 실제로 2개 사용되었다면 2만큼 범위 리턴
		{
			ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
			_usedSize += usedSize;
			return segment;
		}
	}
}

Server

ClientSession.cs

namespace Server
{
	class Packet
	{
		public ushort size;
		public ushort packetId;
	}

	class PlayerInfoReq : Packet
	{
		public long playerId;
	}

	class PlayerInfoOk : Packet
	{
		public int hp;
		public int attack;
	}

	public enum PacketID
	{
		PlayerInfoReq = 1,
		PlayerInfoOk = 2,
	}

	class ClientSession : PacketSession
	{
		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");
			Thread.Sleep(5000);
			Disconnect();
		}

		public override void OnRecvPacket(ArraySegment<byte> buffer)
		{
			int pos = 0;

			ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset); 
			pos += 2;
			ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + pos);
			pos += 2;

			// TODO
			switch ((PacketID)id)
			{
				case PacketID.PlayerInfoReq: // required
					{
						long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + pos);
						pos += 8;
					}
					break;
				case PacketID.PlayerInfoOk:
					{
						int hp = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
						int attack = BitConverter.ToInt32(buffer.Array, buffer.Offset + pos);
						pos += 4;
					}
					//Handle_PlayerInfoOk();
					break;
				default:
					break;
			}

			Console.WriteLine($"RecvPacketId: {id}, Size {size}");
		}

		// TEMP
		public void Handle_PlayerInfoOk(ArraySegment<byte> buffer)
		{

		}
        
        ...
}

DummyClient

ServerSession.cs

namespace DummyClient
{
	class Packet
	{
		public ushort size;
		public ushort packetId;
	}

	class PlayerInfoReq : Packet
	{
		public long playerId;
	}

	class PlayerInfoOk : Packet
	{
		public int hp;
		public int attack;
	}

	public enum PacketID
	{
		PlayerInfoReq = 1,
		PlayerInfoOk = 2,
	}

	class ServerSession : Session
	{
		// unsafe : 포인터 조작. 속도가 빠른 장점
		static unsafe void ToBytes(byte[] array, int offset, ulong value) 
		{
			fixed (byte* ptr = &array[offset])
				*(ulong*)ptr = value;
		}

		static unsafe void ToBytes<T>(byte[] array, int offset, T value) where T : unmanaged
		{
			fixed (byte* ptr = &array[offset])
				*(T*)ptr = value;
		}

		public override void OnConnected(EndPoint endPoint)
		{
			Console.WriteLine($"OnConnected : {endPoint}");
			
			// 패킷의 크기는 아래에서 정해짐
			PlayerInfoReq packet = new PlayerInfoReq() { size = 4, packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };


			// 보낸다
			for (int i = 0; i < 5; i++)
			{
				ArraySegment<byte> s = SendBufferHelper.Open(4096);
				//byte[] size = BitConverter.GetBytes(packet.size);
				//byte[] packetId = BitConverter.GetBytes(packet.packetId);
				//byte[] playerId = BitConverter.GetBytes(packet.playerId);

				ushort size = 0; // int로 만들면 TryWriteBytes의 버전 중 int 버전으로 넘겨줌. ToUInt16
				bool success = true;
			
				// destination(Span<byte>)의 공간보다 value의 크기가 크다면 실패
				// offset+size부터 offset-size만큼의 범위
				size += 2;
				success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.packetId);
				size += 2;
				success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset + size, s.Count - size), packet.playerId);
				size += 8;
				success &= BitConverter.TryWriteBytes(new Span<byte>(s.Array, s.Offset, s.Count), size);

				ArraySegment<byte> sendBuff = SendBufferHelper.Close(size); // 실질적으로 보내줄 버퍼.

				if (success)
					Send(sendBuff);
			}
		}
        
        ...
}

 

ref : https://velog.io/@fere1032/SendBuffer-PacketSession

반응형

'서버(Server) > Server' 카테고리의 다른 글

채팅서버  (0) 2023.01.06
Protobuf 로 패킷 보내기  (0) 2023.01.05
TCP, UDP 차이  (0) 2022.12.27
send 시 데이터를 취합해 한번에 보내기  (0) 2022.12.25
멀티스레드를 고려한 SendAsync 보내기  (0) 2022.12.24
반응형

TCP와 UDP 의 차이

중요한 차이점은, TCP는 전송순서가 보장 된다는 것과 받는 측에서 전체 메세지중 일부만 받을 수 있다면 일부만 보내고 나머지 일부는 나중에 보낼 수 있다는 TCP 의 성질이 있다

UDP 는 전송 일부를 보내지 않고 전체다 보내거나 보내지 않거나 하는 특징이 있다

 

릴라이어블 UDP : 

UDP를 사용하는데 TCP와 유사하게 분실이 일어날때 TCP 처럼 처리 하거나 전송순서를 보장해주는 기능이 있는 UDP를 개조한 방식을 말한다

 

UDP 사용 => 캐릭터 위치 : 포지션 위치는 계속 보내주기 때문에 중간에 1,2개 정도 유실 된다 해도 새로운 위치로 바로 갱신되는것이 일반 적임으로

 

MMORPG 는 TCP 를 주료 사용하는데 대체적으로 속도가 엄청 중요한 요소는 아니기 때문

반응에 민감한 FPS 경우에는 UDP를 많이 사용한다

반응형
반응형

_pendingList.Count == 0 이면 보내기를 시작한다는 얘기는 현재 send 를 할때 보낼 데이터중 큐에 있는 것을 모두 뽑아와 한번에 보내는 한번의 send 처리가 이뤄지는 단계라는 것이다

이것으 ArraySegment 를 통해 버퍼의 범위를 지정하여 리스트 혀앹로 담아서

_sendArgs.BufferList = 에 할당해 주면 되고 보낼때는 이 전과 동일하게 SendAsync 를 호출해주면 한번에 보내지게 된다

 

그래서 sendAsync 를 할때 여러 리스트로 구성된 데이터를 한번에 보내게 되고 보내는 처리가 완료되면

OnSendCompleted 가 callback 으로 호출 되게 된다

 

이때 callback 은 별도의 소켓 스레드에 의해서 호출 된 것임으로 sendAsync 를 호출한 후와 OnSendCompleted  이 함수가 호출되기 전 이 사이에서 다른 소켓에 의해 _sendQueue 에 데이터가 채워질수 있기 때문에

 

OnSendCompleted  에서는 _sendQueue 에 데이터가 채워진게 있다면 다시 send 하는 처리 => 여기선 RegisterSend() 를 호출하여 send 하는 처리를 다시 반복시켜준다 : 데이터를 보내야 할 것이 있었던것이기 때문에 보내줘야 함

 

 

 

 

 

 

고려해 봐야 할 점

  • 생각해 봐야 할것은 만약 유저가 엄청 많이 몰려있어서 많은 데이터를 다른 유저간에 서로 보내줘야 할때
    이렇게 요청이 올때마다 보내는 것 보단 일정 패킷을 모아서 다른 유저들에게 한번에 보내주는 것이 더 효과 적일 수도 있다
  • 또는 어떤 유저가 악의적으로 요청(데이터)을 계속 보내서 DDOs 서버에서 이것을 감당하기 힘든 상태가 되는 경우가 발생할 수 있음으로 비정상적으로 많은 데이터를 어떤 클라에서 보내고 있다면 해당 연결을 Disconnect 처리해서 이것을 막아야한다

 

 

 

 

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

PacketSession  (0) 2022.12.29
TCP, UDP 차이  (0) 2022.12.27
멀티스레드를 고려한 SendAsync 보내기  (0) 2022.12.24
_socket.ReceiveAsync 비동기 처리  (0) 2022.12.23
비동기 accept 처리 : _listenSocket.AcceptAsync  (0) 2022.12.23
반응형

send 함수를 멀티스레드로 보내는데 우선 queue 에 담아 놓고

현재 보낼 수 있으면 큐에 있는 내용을 보내고 보낼수 없는 상황이라면 큐에 보낼 내용을 담아 놓기만한다

 

보낼수 있는 상황과 보낼수 없는 상황은

우선 한번 SendAsync 를 할때 동시에 할수는 없고 한번에 하나만 보낼수 있음으로 SendAsync 가 완료 될때까지 다른 스레드가 SendAsync 를 하지 못하게 데이터만 큐에 쌓아 놓게 한뒤 SendAsync 처리가 완료되는 OnSendCompleted 가 호출 되면 SendAsync  처리를 마무리 한다

 

그런데 OnSendCompleted 가 호출 될때 SendAsync 의 한번의 루틴이 끝나기 전에 데이터가 쌓인게 있다면

다시 한번 RegisterSend 를 호출하여 기존에 쌓여 있는것을 다시 send 해주는데

이렇게 하는 이유는 SocketAsyncEventArg 를 여러개 생성하지 않고 재사용하기 위해 이런 처리를 하는건데

이유는 좀금이라도 send 할때의 비용을 아끼기 위해서이다 (유저가 많아질 수록 이 부하도 점점 쌓이기 때문)

 

 

session 코드

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0;

        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        bool _pending = false;
        object _lock = new object();

        public void Start(Socket socket)
        {
            _socket = socket;

            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);


            //receive 할때 데이터를 받는 버퍼를 만들어준다
            recvArgs.SetBuffer(new byte[1024], 0, 1024);        //버퍼를 크게 만들어서 인덱스를 지정하여 분리하여 받아 들일 수도 있다

            //초기에 한번 receive 되도록 등록해 준다
            RegisterRecv(recvArgs);

            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

        }

        
        public void Send(byte[] sendBuff)
        {
            lock(_lock)
            {
                _sendQueue.Enqueue(sendBuff);
                if (_pending == false)
                {
                    RegisterSend();
                }
            }
           
            
        }


        void RegisterSend()
        {
            _pending = true;
            byte[] buff = _sendQueue.Dequeue();
            
            _sendArgs.SetBuffer(buff, 0, buff.Length);
            bool pending = _socket.SendAsync(_sendArgs);
            if (pending == false)
                OnSendCompleted(null, _sendArgs);
            
        }

        //다른 소켓에 의해 OnSendCompleted 호출 될 수 있음으로 lock 을 걸어준다
        private void OnSendCompleted(object value, SocketAsyncEventArgs args)
        {

            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        if(_sendQueue.Count > 0)
                        {
                            RegisterSend();     //send를 보내는 처리 과정 사이에 다른 스레드에 의해서 send 가 보내진게 있다면 다시 send 를 보내도록 한다
                        }
                        else
                        {
                            _pending = false;
                        }
                        
                    }
                    catch (System.Exception ex)
                    {
                        Console.WriteLine($"OnSendCompleted Failed {ex}");
                    }
                }
                else
                {
                    DisConnect();
                }
            }

        }

        void RegisterRecv(SocketAsyncEventArgs args)
        {
            bool pending = _socket.ReceiveAsync(args);

            //pending 이  false 인 경우엔 즉 기다리는것이 없이 바로 처리가 될 경우에는
            //OnRecvCompleted를 직접 호출해 줘야 한다, 그 외는 ReceiveAsync 내부에서 나중에 알아서 OnRecvCompleted 를 호출한다
            if (pending == false)       
            {
                OnRecvCompleted(null, args);
            }
        }


        private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {

            //받은 바이트가 0 바이트 이상이고
            if(args.BytesTransferred > 0 &&  args.SocketError == SocketError.Success)
            {
                try
                {
                    //args.Buffer : recvArgs.SetBuffer 에서 설정한 바이트다
                    //args.BytesTransferred : 몇바이트를 받았는지 바이트 수
                    string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
                    Console.WriteLine($"[From client] {recvData}");
                    RegisterRecv(args);
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecevCompleted Failed {e}");
                }
            }
            else
            {
                DisConnect();
            }
        }

        public void DisConnect()
        {
            //멀티 스레드에서 동시에 disconnect를 처리 할 수 있기 때문에 Interlocked를 사용하여 처리한다
            //Exchange는 _disconnected 값을 1로 바꾼다, 그리고 오리지널 값을 반환한다
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
            {
                //즉 리턴 값이 1 이라는 얘기는 이전에 한번 disconnect 가 됐었다는 얘기 임으로
                //다시 한번 더 disconnect 를 하려고 하면 리턴처리한다 = > 중복 disconnect 방지
                return;
            }
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
    }
}

 

 

 

서버 시작 부분

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{
    class Program
    {
        static Listener _listener = new Listener();

        //클라이언트로부터 접속이 와서 Accept 되었을때 호출 되는 함수
        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                
                Session session = new Session();
                session.Start(clientSocket);        //안에 receive 가 비동기로 처리됨

                //클라이언트로 보내는 처리
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                session.Send(sendBuff);
                
                Thread.Sleep(1000);
                session.DisConnect();
                //session.DisConnect();     //이렇게 두번 처리 해도 멀티 스레드에 안전하게 처리 했음으로 문제가 되지 않는다

            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            
            try
            {

                _listener.Init(endPoint, OnAcceptHandler);
                

                while (true)
                {
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

리슨 부분 ( 이전에 있던 Listen 과 유사하다)

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;

        Action<Socket> _onAcceptHandler;
        
        public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
        {
            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            _listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            _listenSocket.Bind(iPEndPoint);        //소켓에 ip 와 포트 할당


            //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
            //Listen() 메서드는 동시에 여러 클라이언트가 접속되었을 때 큐에 몇 개의 클라이언트가
            //대기할 수 있는지 지정할 수 있는데, 위의 경우는 예시를 위해 10을 넣었다.
            _listenSocket.Listen(10);

            //이건 한번 사용하고 재사용이 가능하다
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            //최초 한번은 등록해준다
            RegisterAccept(args);
            
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //재사용이 됨으로 null 로 처리한다
            args.AcceptSocket = null;

            //SocketAsyncEventArgs 가 하나일때
            //AcceptAsync 는 대기 하고 있는 클라 접속중 하나에 대해서만 receive 처리를 하고 동시에 두개를 하진 않는다,
            //하나하고 그다음 하나 step by step
            bool pending = _listenSocket.AcceptAsync(args);     //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
            if (pending == false)   //false 면 pending 없이 바로 완료 됐다는 얘기임
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                //accept 되어 새로 생성된 소켓을 
                _onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
            }
            else
            {
                Console.WriteLine(args.SocketError.ToString());
            }

            //위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
            //RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다 
            RegisterAccept(args);
        }

        public Socket Accept()
        {

            //return _listenSocket.Accept();    
            //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
            //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
            //return _listenSocket.Accept();    

            //async 는 비동기로 처리 된다
            //return _listenSocket.AcceptAsync()
            return null;
        }
    }
}

 

 

 

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            while (true)
            {
                try
                {
                    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
                    socket.Connect(endPoint);
                    Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


                    //서버로 보낸다
                    for(int i=0;i< 5; ++i)
                    {
                        byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello world! {i}  ");
                        int bytesSent = socket.Send(sendBuff);
                    }

                    //서버에서 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"received : {recvData}");

                    //서버와 통신을 위해 생성했던 소켓 종료 처리
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();

                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex);
                }
                Thread.Sleep(100);
            }
           

        }
    }
}

클라이언트 더미 코드 또한 동일하다 : 좀 더 다양하게 서버로 접속하게 처리해야 하지만 우선 send 에 만 비동기이면서 멀티스레드로 보낼수 있는 구조로 만드는 것에 초점을 맞춘다

 

반응형
반응형

 

서버코어 부분인데 이 부분에서 listen과 receive 를 하기 위한 초기호및 보내기 데이터 설정 등을 한다

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{


    class Program
    {

        

        static Listener _listener = new Listener();


        //클라이언트로부터 접속이 와서 Accept 되었을때 호출 되는 함수
        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                
                Session session = new Session();
                session.Start(clientSocket);        //안에 receive 가 비동기로 처리됨

                //클라이언트로 보내는 처리
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                session.Send(sendBuff);
                
                Thread.Sleep(1000);
                session.DisConnect();
                //session.DisConnect();     //이렇게 두번 처리 해도 멀티 스레드에 안전하게 처리 했음으로 문제가 되지 않는다

            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            
            try
            {

                _listener.Init(endPoint, OnAcceptHandler);
                

                while (true)
                {
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

 

리슨을 담당하는 코드

 

Listen() 메서드는 동시에 여러 클라이언트가 접속되었을 때 큐에 몇 개의 클라이언트가 대기할 수 있는지 지정할 수 있는데, 위의 경우는 예시를 위해 10을 넣었다.

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;

        Action<Socket> _onAcceptHandler;
        
        public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
        {
            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            _listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            _listenSocket.Bind(iPEndPoint);        //소켓에 ip 와 포트 할당


            //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
            _listenSocket.Listen(10);

            //이건 한번 사용하고 재사용이 가능하다
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            //최초 한번은 등록해준다
            RegisterAccept(args);
            
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //재사용이 됨으로 null 로 처리한다
            args.AcceptSocket = null;

            bool pending = _listenSocket.AcceptAsync(args);     //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
            if (pending == false)   //false 면 pending 없이 바로 완료 됐다는 얘기임
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                //accept 되어 새로 생성된 소켓을 
                _onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
            }
            else
            {
                Console.WriteLine(args.SocketError.ToString());
            }

            //위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
            //RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다 
            RegisterAccept(args);
        }

        public Socket Accept()
        {

            //return _listenSocket.Accept();    
            //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
            //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
            //return _listenSocket.Accept();    

            //async 는 비동기로 처리 된다
            //return _listenSocket.AcceptAsync()
            return null;
        }
    }
}

 

 

 

세션코드로 클라로부터 들어온 데이터를 비동기로 받는 로직이다

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0;
        public void Start(Socket socket)
        {
            _socket = socket;

            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);


            //receive 할때 데이터를 받는 버퍼를 만들어준다
            recvArgs.SetBuffer(new byte[1024], 0, 1024);        //버퍼를 크게 만들어서 인덱스를 지정하여 분리하여 받아 들일 수도 있다

            //초기에 한번 receive 되도록 등록해 준다
            RegisterRecv(recvArgs);

        }

        public void Send(byte[] sendBuff)
        {
            _socket.Send(sendBuff);
        }

        void RegisterRecv(SocketAsyncEventArgs args)
        {
            bool pending = _socket.ReceiveAsync(args);

            //pending 이  false 인 경우엔 즉 기다리는것이 없이 바로 처리가 될 경우에는
            //OnRecvCompleted를 직접 호출해 줘야 한다, 그 외는 ReceiveAsync 내부에서 나중에 알아서 OnRecvCompleted 를 호출한다
            if (pending == false)       
            {
                OnRecvCompleted(null, args);
            }
        }

        private void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
            //받은 바이트가 0 바이트 이상이고
            if(args.BytesTransferred > 0 &&  args.SocketError == SocketError.Success)
            {
                try
                {
                    //args.Buffer : recvArgs.SetBuffer 에서 설정한 바이트다
                    //args.BytesTransferred : 몇바이트를 받았는지 바이트 수
                    string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
                    Console.WriteLine($"[From client] {recvData}");
                    RegisterRecv(args);
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecevCompleted Failed {e}");
                }
            }
            else
            {
                
            }
        }

        public void DisConnect()
        {
            //멀티 스레드에서 동시에 disconnect를 처리 할 수 있기 때문에 Interlocked를 사용하여 처리한다
            //Exchange는 _disconnected 값을 1로 바꾼다, 그리고 오리지널 값을 반환한다
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
            {
                //즉 리턴 값이 1 이라는 얘기는 이전에 한번 disconnect 가 됐었다는 얘기 임으로
                //다시 한번 더 disconnect 를 하려고 하면 리턴처리한다 = > 중복 disconnect 방지
                return;
            }
            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }
    }
}

 

 

 

 

 

서버와 클라간의 테스트를 위한 더미 클라이언트 코드

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            while (true)
            {
                try
                {
                    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
                    socket.Connect(endPoint);
                    Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


                    //서버로 보낸다
                    for(int i=0;i< 5; ++i)
                    {
                        byte[] sendBuff = Encoding.UTF8.GetBytes($"Hello world! {i}  ");
                        int bytesSent = socket.Send(sendBuff);
                    }

                    //서버에서 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"received : {recvData}");

                    //서버와 통신을 위해 생성했던 소켓 종료 처리
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();

                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex);
                }
                Thread.Sleep(100);
            }
           

        }
    }
}

 

 

bool pending = _listenSocket.AcceptAsync(args);

SocketAsyncEventArgs 가 하나일때(즉 n 번 선언해 놓고 Register 를 n 번 한게 아닌 n = 1 일때)
AcceptAsync 는 대기 하고 있는 클라 접속중 하나에 대해서만 receive 처리를 하고 동시에 두개를 하진 않는다,
하나하고 그다음 하나 step by step

 

 

클라 접속이 이뤄지더라도 AcceptAsync를 명시적으로 호출해줘야 OnAcceptCompleted가 실행이 된다
즉, AcceptAsync는 일종의 입장 허락의 개념인데 
그 전에 접속을 희망한 클라들은 대기열(큐)에 입장 대기를 하고 있게 되고
OnAcceptCompleted를 호출해서 입장 관련 처리를 다 끝낸 다음,
마지막에 RegisterAccept를 다시 호출해서 다음 입장을 받아주기 때문에
동시에 여러 쓰레드가 OnAcceptCompleted를 실행할 수 없기도 하다



 

Accepting connections asynchronously gives you the ability to send and receive data within a separate execution thread. Before calling the AcceptAsync method, you must call the Listen method to listen for and queue incoming connection requests.

 

 

 

결과 화면 : 더미 클라에서 send 를 연달아 다섯번정도하기 때문에 서버에서 비동기 receive 로 5번이 처리 되는것 것을 볼 수 있다, 콘솔 창에서 보면 알 수 있듯이 receive 가 동시에 멀티 스레드 처럼 5번 처리 되는것이 아니고 한번에 한번만 처리 되는 것이라 볼 수 있는데 현재 Recv 코드는 오직 1번에 1개의 쓰레드만 접근 할 수 있다

 

다시 말해 최초에 RegisterRecv 를 1개만 걸어 놨기 때문에 Register->Completed  .. 과정도 오로지 한번에 1개만 일어난다

 

 

 

ref : https://learn.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.acceptasync?view=netframework-4.7.2&f1url=%3FappId%3DDev16IDEF1%26l%3DEN-US%26k%3Dk(System.Net.Sockets.Socket.AcceptAsync)%3Bk(TargetFrameworkMoniker-.NETFramework%2CVersion%253Dv4.7.2)%3Bk(DevLang-csharp)%26rd%3Dtrue 

 

Socket.AcceptAsync Method (System.Net.Sockets)

Accepts an incoming connection.

learn.microsoft.com

 

반응형
반응형

서버 코드

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{


    class Program
    {

        

        static Listener _listener = new Listener();


        static void OnAcceptHandler(Socket clientSocket)
        {
            try
            {
                //클라이언트로부터 받아오는 처리
                byte[] recvBuff = new byte[1024];
                int recvBytpes = clientSocket.Receive(recvBuff);
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytpes);
                Console.WriteLine($"[From Client] {recvData}");


                //클라이언트로 보내는 처리
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                clientSocket.Send(sendBuff);

                clientSocket.Shutdown(SocketShutdown.Both);     //연결된 소켓 끊기, 듣기와 말하기를 하지 않겠다는 것
                clientSocket.Close();   //클라와 서버간의 연결 끊기
            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            
            try
            {

                _listener.Init(endPoint, OnAcceptHandler);
                

                while (true)
                {
                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

리스터 코드 : 비동기 accept

using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace ServerCore
{
    class Listener
    {
        Socket _listenSocket;

        Action<Socket> _onAcceptHandler;
        
        public void Init(IPEndPoint iPEndPoint, Action<Socket> onAcceptHandler)
        {
            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            _listenSocket = new Socket(iPEndPoint.AddressFamily , SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler;

            _listenSocket.Bind(iPEndPoint);        //소켓에 ip 와 포트 할당


            //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
            _listenSocket.Listen(10);

            //이건 한번 사용하고 재사용이 가능하다
            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            //최초 한번은 등록해준다
            RegisterAccept(args);
            
        }

        void RegisterAccept(SocketAsyncEventArgs args)
        {
            //재사용이 됨으로 null 로 처리한다
            args.AcceptSocket = null;

            bool pending = _listenSocket.AcceptAsync(args);     //비동기 임으로 예약만 하고 넘어간다, accdept 완료는 eventHandler 를 통해서 완료된다
            if (pending == false)   //false 면 pending 없이 바로 완료 됐다는 얘기임
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args)
        {
            if(args.SocketError == SocketError.Success)
            {
                //accept 되어 새로 생성된 소켓을 
                _onAcceptHandler.Invoke(args.AcceptSocket); //넘겨준다
            }
            else
            {
                Console.WriteLine(args.SocketError.ToString());
            }

            //위에 까지 처리가 된것은 accept 가 완료 된것임으로 새로운 accept 를 위해서
            //RegisterAccept 를 다시 호출하여 OnAcceptCompleted 이벤트를 받아 들을 수 있는 상태로 만든다 
            RegisterAccept(args);
        }

        public Socket Accept()
        {

            //return _listenSocket.Accept();    
            //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
            //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
            //return _listenSocket.Accept();    

            //async 는 비동기로 처리 된다
            //return _listenSocket.AcceptAsync()
            return null;
        }
    }
}

 

 

더미 클라이언트 코드

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            while (true)
            {
                try
                {
                    Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

                    //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
                    socket.Connect(endPoint);
                    Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


                    //서버로 보낸다
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Hello world!");
                    int bytesSent = socket.Send(sendBuff);

                    //서버에서 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = socket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
                    Console.WriteLine($"received : {recvData}");

                    //서버와 통신을 위해 생성했던 소켓 종료 처리
                    socket.Shutdown(SocketShutdown.Both);
                    socket.Close();

                }
                catch (System.Exception ex)
                {
                    Console.WriteLine(ex);
                }
                Thread.Sleep(100);
            }
           

        }
    }
}

 

 

AcceptAsync : 비동기 방식으로 클라의 접속을 말한다

AcceptAsync 로 처리하면 서버에서 클라의 접속을 받아 Accept() 함수 처럼 accept 하게 될때 해당 라인에서 무한 대기 하지 않고 클라와 통신하기 위한 전용 socket 을 만들어 리턴하고 서버에선 바로 다음 로직을 처리 할 수 있게 된다

 

bool pending = _listenSocket.AcceptAsync(args);

 

 

결과화면

반응형

'서버(Server) > Server' 카테고리의 다른 글

멀티스레드를 고려한 SendAsync 보내기  (0) 2022.12.24
_socket.ReceiveAsync 비동기 처리  (0) 2022.12.23
기본적인 소켓 프로그래밍  (0) 2022.12.23
소켓 통신  (0) 2022.12.22
통신 모델 OSI 7 계층  (0) 2022.12.22
반응형

 

C# 으로 서버와 클라이언트 기본적인 연결 및 string 데이터 전송 코드이다

 

서버 코드

using System;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text;

namespace ServerCore
{


    class Program
    {
        static void Main(string[] args)
        {
            //DNS : Domain Name System
            // 도메인을 하나 등록해서 해당하는 IP 를 찾아오면 관리가 쉬워짐
            //www.google.com => 

            string host = Dns.GetHostName();
            //host = "google.com";            //ipHost.AddressList[0] == {172.217.161.238}
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            //이렇게 GetHostEntry 로 주소를 얻어오는 건 DNS 서버를 통해서 얻어 올 수 있게 됨

            // ipHost.addressList[0] = IPAddress.Parse("        경우에 따라서 ip 주소는 여러개 일 수도 있다 부하 분산을 위해서 addressList
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     //최종 주소

            //AddressFamily ip version 4,6 에 대한 것 , 위에서 자동으로 만들어줌, 
            //tcp 로 할 경우 stream, tcp 로 설정해준다
            //리슨 하는 자체가 소켓을 하나 만들어야 한다
            Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                listenSocket.Bind(endPoint);        //소켓에 ip 와 포트 할당


                //최대 동시 대기 수, 동시에 들어올대 10명까지만 처리 가능하고 그 위로는 실패가 된다
                listenSocket.Listen(10);


                while (true)
                {
                    Console.WriteLine("Listening .... ");

                    //클라의 접속이 있다면 받아오는 처리, 접속이 있을때까지 계속 대기, 즉 다음으로 넘어가지 않는다
                    //클라로부터 접속이 왔다면 accept 되어 클라와 별도 통실한 socket 이 생성되어 리턴된다
                    Socket clientSocket = listenSocket.Accept();


                    //클라이언트로부터 받아오는 처리
                    byte[] recvBuff = new byte[1024];
                    int recvBytpes = clientSocket.Receive(recvBuff);
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytpes);
                    Console.WriteLine($"[From Client] {recvData}");


                    //클라이언트로 보내는 처리
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to server!");
                    clientSocket.Send(sendBuff);

                    clientSocket.Shutdown(SocketShutdown.Both);     //연결된 소켓 끊기, 듣기와 말하기를 하지 않겠다는 것
                    clientSocket.Close();   //클라와 서버간의 연결 끊기

                }
            }
            catch(Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }
}

 

 

 

클라이언트 코드

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);     
            
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            //소켓으로 서버에 연결한다 , 서버 입장에선 accept 가 된다
            socket.Connect(endPoint);
            Console.WriteLine($"connected to {socket.RemoteEndPoint.ToString()}");


            //서버로 보낸다
            byte[] sendBuff = Encoding.UTF8.GetBytes("Hello world!");
            int bytesSent = socket.Send(sendBuff);

            //서버에서 받는다
            byte[] recvBuff = new byte[1024];
            int recvBytes = socket.Receive(recvBuff);
            string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes);
            Console.WriteLine($"received : {recvData}");

            //서버와 통신을 위해 생성했던 소켓 종료 처리
            socket.Shutdown(SocketShutdown.Both);
            socket.Close();
            

        }
    }
}

 

결과 창

 

서버에서는 클라에서 보낸 데이터를 보여주고

클라에서는 서버로부터 받은 데이터를 뿌려주고 종료한다

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

_socket.ReceiveAsync 비동기 처리  (0) 2022.12.23
비동기 accept 처리 : _listenSocket.AcceptAsync  (0) 2022.12.23
소켓 통신  (0) 2022.12.22
통신 모델 OSI 7 계층  (0) 2022.12.22
javascript : Class  (0) 2018.08.19
반응형

소켓 통신은

Bind 로 ip 와 port 를 설정한 뒤에 

Listen 으로 한번에 몇개의 접속을 받을 수 있을지를 설정하면서 대기하게 하고

Accept 하면서 대기 하고 있다가 클라로부터 접속이 오면 클라와 전용으로 통신할 소켓을 하나 만들어 통신하게 하는 구조다

 

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

비동기 accept 처리 : _listenSocket.AcceptAsync  (0) 2022.12.23
기본적인 소켓 프로그래밍  (0) 2022.12.23
통신 모델 OSI 7 계층  (0) 2022.12.22
javascript : Class  (0) 2018.08.19
Promise [1] catch & all  (0) 2018.08.19
반응형

 

 

어플리케이션 : 유저인터페이스,  HTTP, FPT, DNS 

  • 4단계 : 트랜스포트 TCP/UDP 전송확인/오류해결
  • 3계층 : 네트워크 단계, 라우터 : IP, ipv4, ip6, 라우터 로 경로 설정
  • 2계층 : 데이터 링크 계층 으로  , 스위치, mac address 네트워크 카드에 박혀 있는 주소

 

7 계층 : 도메인 인데 이 도메인을  나중에 IP 로 바꿔야 함

 

 

 

 

 

반응형

'서버(Server) > Server' 카테고리의 다른 글

기본적인 소켓 프로그래밍  (0) 2022.12.23
소켓 통신  (0) 2022.12.22
javascript : Class  (0) 2018.08.19
Promise [1] catch & all  (0) 2018.08.19
Promise [2]  (0) 2018.08.16
반응형


기존엔 함수 형태를 클래스 처럼 (억지로??) 맞추는 형태였기에
구문 문법 자체가 지저분해졌는데 Class 를 사용함으로서 이보다는 좀더 깔끔하게 정의할 수 있다

JavaScript class는 ECMAScript 6을 통해 소개되었으며, 기존 prototype 기반의 상속 보다 명료하게 사용할 수 있습니다. Class 문법은 새로운 객체지향 상속 모델을 제공하는 것은 아닙니다. JavaScript class는 객체를 생성하고 상속을 다루는데 있어 훨씬 더 단순하고 명확한 문법을 제공합니다.

Link to sectionClass 정의

Class는 사실 함수입니다. 함수를 함수 표현식과 함수 선언으로 정의할 수 있듯이 class 문법도 class 표현식과 class 선언 두 가지 방법을 제공합니다.

Link to sectionClass 선언

class를 정의하는 한 가지 방법은 class 선언을 이용하는 것입니다. class를 선언하기 위해서는 클래스의 이름과 함께 class 키워드를 사용해야 합니다.

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

Hoisting

함수 선언과 클래스 선언의 중요한 차이점은 함수 선언의 경우 호이스팅이 일어나지만, 클래스 선언은 그렇지 않다는 것입니다. 클래스를 사용하기 위해서는 클래스를 먼저 선언 해야 하며, 그렇지 않으면, 다음 아래의 코드는 ReferenceError 에러를 던질 것입니다. :

var p = new Polygon(); // ReferenceError

class Polygon {}

Link to sectionClass 표현식

class 표현식은 class를 정의 하는 또 다른 방법입니다. Class 표현식은 이름을 가질 수도 있고, 갖지 않을 수도 있습니다. 기명 class 표현식에 주어진 이름은 클래스의 body에 대해 local scope에 한해 유효합니다.

// unnamed
var Polygon = class {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

// named
var Polygon = class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};

Link to sectionClass body 와 method 정의

Class body는 중괄호 {} 로 묶여 있는 안쪽 부분입니다.. 이곳은 여러분이 method나 constructor와 같은 class members를 정의할 곳입니다.

Link to sectionStrict mode

클래스 선언과 클래스 표현식의 본문(body)은 strict mode 에서 실행됩니다.

Link to sectionConstructor (생성자)

constructor 메소드는 class 로 생성된 객체를 생성하고 초기화하기 위한 특수한 메소드입니다.  "constructor" 라는 이름을 가진 특수한 메소드는 클래스 안에 한 개만 존재할 수 있습니다. 만약 클래스에 한 개를 초과하는 constructor 메소드를 포함한다면, SyntaxError 가 발생할 것입니다.

constructor는 부모 클래스의 constructor 를 호출하기 위해 super 키워드를 사용할 수 있습니다.

Link to sectionPrototype methods

method definitions도 참조해보세요.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Getter
  get area() {
    return this.calcArea();
  }
  // Method
  calcArea() {
    return this.height * this.width;
  }
}

const square = new Rectangle(10, 10);

console.log(square.area); // 100

Link to sectionStatic methods

static 키워드는 클래스를 위한 정적(static) 메소드를 정의합니다. 정적 메소드는 클래스의 인스턴스화(instantiating) 없이 호출되며, 클래스의 인스턴스에서는 호출할 수 없습니다. 정적 메소드는 어플리케이션(application)을 위한 유틸리티(utility) 함수를 생성하는데 주로 사용됩니다.

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    static distance(a, b) {
        const dx = a.x - b.x;
        const dy = a.y - b.y;

        return Math.sqrt(dx*dx + dy*dy);
    }
}

const p1 = new Point(5, 5);
const p2 = new Point(10, 10);

console.log(Point.distance(p1, p2));

 

Link to sectionBoxing with prototype and static methods

정적 메소드나 프로토타입 메소드가 this 값 없이 호출될 때, this 값은 메소드 안에서 undefined가 됩니다. class 문법 안에 있는 코드는 항상 strict mode 로 실행되기 때문에 이동작은 "use strict" 명령어가 없더라도 같습니다.

 

class Animal { 
  speak() {
    return this;
  }
  static eat() {
    return this;
  }
}

let obj = new Animal();
obj.speak(); // Animal {}
let speak = obj.speak;
speak(); // undefined

Animal.eat() // class Animal
let eat = Animal.eat;
eat(); // undefined

If the above is written using traditional function–based syntax, then autoboxing in method calls will happen in non–strict mode based on the initial this value. If the inital value is undefinedthis will be set to the global object.

Autoboxing will not happen in strict mode, the this value remains as passed.

function Animal() { }

Animal.prototype.speak = function() {
  return this;
}

Animal.eat = function() {
  return this;
}

let obj = new Animal();
let speak = obj.speak;
speak(); // global object

let eat = Animal.eat;
eat(); // global object

Link to sectionInstance properties

Instance properties must be defined inside of class methods:

class Rectangle {
  constructor(height, width) {    
    this.height = height;
    this.width = width;
  }
}

Static class-side properties and prototype data properties must be defined outside of the ClassBody declaration:

Rectangle.staticWidth = 20;
Rectangle.prototype.prototypeWidth = 25;

 

 

 

Link to sectionextends를 통한 클래스 상속(sub classing)

extends 키워드는 클래스 선언이나 클래스 표현식에서 다른 클래스의 자식 클래스를 생성하기 위해 사용됩니다.

class Animal { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
  speak() {
    console.log(this.name + ' barks.');
  }
}

subclass에 constructor가 있다면, "this"를 사용하기 전에 가장 먼저 super()를 호출해야 합니다.

또한 es5에서 사용되던 전통적인 함수 기반의 클래스를 통하여 확장할 수도 있습니다.

function Animal (name) {
  this.name = name;  
}
Animal.prototype.speak = function () {
  console.log(this.name + ' makes a noise.');
}

class Dog extends Animal {
  speak() {
    console.log(this.name + ' barks.');
  }
}

var d = new Dog('Mitzie');
d.speak();

클래스는 생성자가 없는 객체(non-constructible)을 확장할 수 없습니다. 만약 기존의 생성자가 없는 객체을 확장하고 싶다면, 이 메소드를 사용하세요. Object.setPrototypeOf():

var Animal = {
  speak() {
    console.log(this.name + ' makes a noise.');
  }
};

class Dog {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log(this.name + ' barks.');
  }
}
Object.setPrototypeOf(Dog.prototype, Animal);

var d = new Dog('Mitzie');
d.speak();

Link to sectionSub classing built-in objects

TBD

Link to sectionSpecies

아마 배열을 상속 받은 MyArray 클래스에서 Array를 반환하고 싶을 것입니다. Species 패턴은 기본 생성자를 덮어쓰도록 해줍니다.

예를 들어, map()과 같은 기본 생성자를 반환하는 메서드를 사용할 때 이 메서드가 MyArray 객체 대신 Array 객체가 반환하도록 하고 싶을 것입니다. Symbol.species 심볼은 이러한 것을 가능하게 해줍니다:

class MyArray extends Array {
  // 부모 Array 생성자로 종류 덮어쓰기
  static get [Symbol.species]() { return Array; }
}
var a = new MyArray(1,2,3);
var mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true

Link to sectionsuper 를 통한 상위 클래스 호출

super 키워드는 객체의 부모가 가지고 있는 함수들을 호출하기 위해 사용됩니다..

class Cat { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}

Link to sectionES5 프로토타입 상속 문법과 ES6 클래스 상속 문법의 비교

ES6 (ES2015):

class Cat { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Lion extends Cat {
  speak() {
    super.speak();
    console.log(this.name + ' roars.');
  }
}

ES5:

function Cat(name) {
  this.name = name;
}

Cat.prototype.speak = function () {
  console.log(this.name + ' makes a noise.');
};

function Lion(name) {
  // `super()` 호출
  Cat.call(this, name);
}

// `Cat` 클래스 상속
Lion.prototype = Object.create(Cat.prototype);
Lion.prototype.constructor = Lion;

// `speak()` 메서드 오버라이드
Lion.prototype.speak = function () {
  Cat.prototype.speak.call(this);
  console.log(this.name + ' roars.');
};

 

 

Link to sectionMix-ins

추상 서브 클래스 또는 믹스-인은 클래스를 위한 템플릿입니다. ECMAScript 클래스는 하나의 단일 슈퍼클래스만을 가질 수 있으며, 예를 들어 툴링 클래스로부터의 다중 상속은 불가능합니다. 기능은 반드시 슈퍼클래스에서 제공되어야 합니다.

슈퍼클래스를 인자로 받고 이 슈퍼클래스를 확장하는 서브클래스를 생성하여 반환하는 함수를 사용하여 ECMAScript에서 믹스-인을 구현할 수 있습니다:

아래는 두 함수는 어떤 클래스를 인자로 받고 그 클래스를 상속한 새로운 익명 클래스를 리턴하는 arrow function 입니다.

var calculatorMixin = Base => class extends Base {
  calc() { }
};

var randomizerMixin = Base => class extends Base {
  randomize() { }
};

위 믹스-인을 사용하는 클래스는 다음과 같이 작성할 수 있습니다:

class Foo { }
class Bar extends calculatorMixin(randomizerMixin(Foo)) { }

ref : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes


반응형

'서버(Server) > Server' 카테고리의 다른 글

소켓 통신  (0) 2022.12.22
통신 모델 OSI 7 계층  (0) 2022.12.22
Promise [1] catch & all  (0) 2018.08.19
Promise [2]  (0) 2018.08.16
[javascript] Singleton 싱글톤  (0) 2018.07.02
반응형


Promise

“A promise is an object that may produce a single value some time in the future”

프로미스는 자바스크립트 비동기 처리에 사용되는 객체입니다. 여기서 자바스크립트의 비동기 처리란 ‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 자바스크립트의 특성’을 의미합니다. 비동기 처리에 대한 이해가 없으시다면 이전 글 ‘자바스크립트 비동기 처리와 콜백 함수’를 읽어보시길 추천드립니다 :)

Promise가 왜 필요한가요?

프로미스는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용합니다. 일반적으로 웹 애플리케이션을 구현할 때 서버에서 데이터를 요청하고 받아오기 위해 아래와 같은 API를 사용합니다.

$.get('url 주소/products/1', function (response) {
  // ...
});

위 API가 실행되면 서버에다가 ‘데이터 하나 보내주세요’ 라는 요청을 보내죠. 그런데 여기서 데이터를 받아오기도 전에 마치 데이터를 다 받아온 것 마냥 화면에 데이터를 표시하려고 하면 오류가 발생하거나 빈 화면이 뜹니다. 이와 같은 문제점을 해결하기 위한 방법 중 하나가 프로미스입니다.

프로미스 코드 - 기초

그럼 프로미스가 어떻게 동작하는지 이해하기 위해 예제 코드를 살펴보겠습니다. 먼저 아래 코드는 간단한 ajax 통신 코드입니다.

function getData(callbackFunc) {
  $.get('url 주소/products/1', function (response) {
    callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
  });
}

getData(function (tableData) {
  console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});

위 코드는 제이쿼리의 ajax 통신을 이용하여 지정한 url에서 1번 상품 데이터를 받아오는 코드입니다. 비동기 처리를 위해 프로미스 대신에 콜백 함수를 이용했죠.

위 코드에 프로미스를 적용하면 아래와 같은 코드가 됩니다.

function getData(callback) {
  // new Promise() 추가
  return new Promise(function (resolve, reject) {
    $.get('url 주소/products/1', function (response) {
      // 데이터를 받으면 resolve() 호출
      resolve(response);
    });
  });
}

// getData()의 실행이 끝나면 호출되는 then()
getData().then(function (tableData) {
  // resolve()의 결과 값이 여기로 전달됨
  console.log(tableData); // $.get()의 reponse 값이 tableData에 전달됨
});

콜백 함수로 처리하던 구조에서 new Promise()resolve()then()와 같은 프로미스 API를 사용한 구조로 바뀌었습니다. 여기서 new Promise()는 좀 이해가 가겠는데 resolve()then()은 뭐 하는 애들일까요? 아래에서 함께 알아보겠습니다.

프로미스의 3가지 상태(states)

프로미스를 사용할 때 알아야 하는 가장 기본적인 개념이 바로 프로미스의 상태(states)입니다. 여기서 말하는 상태란 프로미스의 처리 과정을 의미합니다. new Promise()로 프로미스를 생성하고 종료될 때까지 3가지 상태를 갖습니다.

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

Pending(대기)

먼저 아래와 같이 new Promise() 메서드를 호출하면 Pending(대기) 상태가 됩니다.

new Promise();

이렇게 new Promise() 메서드를 호출할 때 콜백 함수의 인자로 resolve, reject에 접근할 수 있습니다.

new Promise(function (resolve, reject) {
  // ...
});

Fulfilled(이행)

여기서 콜백 함수의 인자 resolve를 아래와 같이 실행하면 Fulfilled(이행) 상태가 됩니다.

new Promise(function (resolve, reject) {
  resolve();
});

그리고 이행 상태가 되면 아래와 같이 then()을 이용하여 처리 결과 값을 받을 수 있습니다.

function getData() {
  return new Promise(function (resolve, reject) {
    var data = 100;
    resolve(data);
  });
}

// resolve()의 결과 값 data를 resolvedData로 받음
getData().then(function (resolvedData) {
  console.log(resolvedData); // 100
});

프로미스의 '이행' 상태를 좀 다르게 표현해보면 '완료' 입니다.

Rejected(실패)

new Promise()로 프로미스 객체를 생성하면 콜백 함수 인자로 resolve와 reject를 사용할 수 있다고 했습니다. 여기서 reject 인자로 reject() 메서드를 실행하면 Rejected(실패) 상태가 됩니다.

new Promise(function (resolve, reject) {
  reject();
});

그리고, 실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있습니다.

function getData() {
  return new Promise(function (resolve, reject) {
    reject(new Error("Request is failed"));
  });
}

// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function (err) {
  console.log(err); // Error: Request is failed
});



프로미스 처리 흐름 - 출처 : MDN

프로미스 코드 - 쉬운 예제

그럼 위에서 배운 내용들을 종합하여 간단한 프로미스 코드를 만들어보겠습니다. 이해하기 쉽게 앞에서 살펴본 ajax 통신 예제 코드에 프로미스를 적용해보겠습니다.

function getData() {
  return new Promise(function (resolve, reject) {
    $.get('url 주소/products/1', function (response) {
      if (response) {
        resolve(response);
      }
      reject(new Error("Request is failed"));
    });
  });
}

// Fulfilled 또는 Rejected의 결과 값 출력
getData().then(function (data) {
  console.log(data); // response 값 출력
}).catch(function (err) {
  console.error(err); // Error 출력
});

위 코드는 서버에서 제대로 응답을 받아오면 resolve() 메서드를 호출하고, 응답이 없으면 reject() 메서드를 호출하는 예제입니다. 호출된 메서드에 따라 then()이나 catch()로 분기하여 데이터 또는 오류를 출력합니다.

여러 개의 프로미스 연결하기 (Promise Chaining)

프로미스의 또 다른 특징은 여러 개의 프로미스를 연결하여 사용할 수 있다는 점입니다. 앞 예제에서 then() 메서드를 호출하고 나면 새로운 프로미스 객체가 반환됩니다. 따라서, 아래와 같이 코딩이 가능합니다.

function getData() {
  return new Promise({
    // ...
  });
}

// then() 으로 여러 개의 프로미스를 연결한 형식
getData()
  .then(function (data) {
    // ...
  })
  .then(function () {
    // ...
  })
  .then(function () {
    // ...
  });

그러면 위의 형식을 참고하여 실제로 돌려볼 수 있는 예제를 살펴보겠습니다. 비동기 처리 예제에서 가장 흔하게 사용되는 setTimeout() API를 사용하였습니다.

new Promise(function(resolve, reject){
  setTimeout(function() {
    resolve(1);
  }, 2000);
})
.then(function(result) {
  console.log(result); // 1
  return result + 10;
})
.then(function(result) {
  console.log(result); // 11
  return result + 20;
})
.then(function(result) {
  console.log(result); // 31
});

위 코드는 프로미스 객체를 하나 생성하고 setTimeout()을 이용해 2초 후에 resolve()를 호출하는 예제입니다.

resolve()가 호출되면 프로미스가 대기 상태에서 이행 상태로 넘어가기 때문에 첫 번째 .then()의 로직으로 넘어갑니다. 첫 번째 .then()에서는 이행된 결과 값 1을 받아서 10을 더한 후 그다음 .then() 으로 넘겨줍니다. 두 번째 .then()에서도 마찬가지로 바로 이전 프로미스의 결과 값 11을 받아서 20을 더하고 다음 .then()으로 넘겨줍니다. 마지막 .then()에서 최종 결과 값 31을 출력합니다.

실무에서 있을 법한 프로미스 연결 사례

실제 웹 서비스에서 있을 법한 사용자 로그인 인증 로직에 프로미스를 여러 개 연결해보겠습니다.

getData(userInfo)
  .then(parseValue)
  .then(auth)
  .then(diaplay);

위 코드는 페이지에 입력된 사용자 정보를 받아와 파싱, 인증 등의 작업을 거치는 코드를 나타내었습니다. 여기서 userInfo는 사용자 정보가 담긴 객체를 의미하고, parseValueauthdisplay는 각각 프로미스를 반환해주는 함수라고 가정했습니다. 아래와 같이 말이죠.

var userInfo = {
  id: 'test@abc.com',
  pw: '****'
};

function parseValue() {
  return new Promise({
    // ...
  });
}
function auth() {
  return new Promise({
    // ...
  });
}
function display() {
  return new Promise({
    // ...
  });
}

이처럼 여러 개의 프로미스를 .then()으로 연결하여 처리할 수 있습니다.

프로미스의 에러 처리 방법

앞에서 살펴본 프로미스 예제는 코드가 항상 정상적으로 동작한다고 가정하고 구현한 예제입니다. 실제 서비스를 구현하다 보면 네트워크 연결, 상태 코드 문제 등으로 인해 오류가 발생할 수 있습니다. 따라서, 프로미스의 에러 처리 방법에 대해서도 알고 있어야 합니다.

에러 처리 방법에는 다음과 같이 2가지 방법이 있습니다. 

1.then()의 두 번째 인자로 에러를 처리하는 방법

getData().then(
  handleSuccess,
  handleError
);

2.catch()를 이용하는 방법

getData().then().catch();

위 2가지 방법 모두 프로미스의 reject() 메서드가 호출되어 실패 상태가 된 경우에 실행됩니다. 간단하게 말해서 프로미스의 로직이 정상적으로 돌아가지 않는 경우 호출되는 거죠. 아래와 같이 말입니다.

function getData() {
  return new Promise(function (resolve, reject) {
    reject('failed');
  });
}

// 1. then()으로 에러를 처리하는 코드
getData().then(function () {
  // ...
}, function (err) {
  console.log(err);
});

// 2. catch()로 에러를 처리하는 코드
getData().then().catch(function (err) {
  console.log(err);
});

프로미스 에러 처리는 가급적 catch()로

앞에서 프로미스 에러 처리 방법 2가지를 살펴봤습니다. 개개인의 코딩 스타일에 따라서 then()의 두 번째 인자로 처리할 수도 있고 catch()로 처리할 수도 있겠지만 가급적 catch()로 에러를 처리하는 게 더 효율적입니다.

그 이유는 아래의 코드를 보시면 알 수 있습니다.

// then()의 두 번째 인자로는 감지하지 못하는 오류
function getData() {
  return new Promise(function (resolve, reject) {
    resolve('hi');
  });
}

getData().then(function (result) {
  console.log(result);
  throw new Error("Error in then()"); // Uncaught (in promise) Error: Error in then()
}, function (err) {
  console.log('then error : ', err);
});

getData() 함수의 프로미스에서 resolve() 메서드를 호출하여 정상적으로 로직을 처리했지만, then()의 첫 번째 콜백 함수 내부에서 오류가 나는 경우 오류를 제대로 잡아내지 못합니다. 따라서 코드를 실행하면 아래와 같은 오류가 납니다.

'에러를 잡지 못했습니다(Uncaught Error)' 로그

하지만 똑같은 오류를 catch()로 처리하면 다른 결과가 나옵니다.

// catch()로 오류를 감지하는 코드
function getData() {
  return new Promise(function (resolve, reject) {
    resolve('hi');
  });
}

getData().then(function (result) {
  console.log(result); // hi
  throw new Error("Error in then()");
}).catch(function (err) {
  console.log('then error : ', err); // then error :  Error: Error in then()
});

위 코드의 처리 결과는 다음과 같습니다.

발생한 에러를 성공적으로 콘솔에 출력한 모습

따라서, 더 많은 예외 처리 상황을 위해 프로미스의 끝에 가급적 catch()를 붙이시기 바랍니다.







Rookie mistake #5: using side effects instead of returning
이 코드에 어떤 문제가 있는 것 같나요?
somePromise().then(function () {
  someOtherPromise();
}).then(function () {
  // Gee, I hope someOtherPromise() has resolved!
  // Spoiler alert: it hasn't.
});

좋습니다. 이 코드를 가지고 당신이 알아야 하는 promise의 모든 것에 대해서 이야기 할 수 있습니다.
진지하게, 이 방법은 꽤나 이상해 보일 수 있지만 한번 이해하고 난 다음에는 우리가 여태 이야기한 모든 에러들을 예방할 수 있는 매우 좋은 방법입니다.

이전에 말했는데 promise가 가진 최대의 이점은 우리에게 소중한 return과 throw를 되돌려준다는 것입니다. 하지만 실제로 이게 어떤 형태일까요?

모든 promise는 우리에게 then() 메서드를 제공합니다. (또는 catch() 메서드나요.) then() 메서드 내부를 보겠습니다.

somePromise().then(function () {
  // I'm inside a then() function!
});

이 함수의 내부에서 우리는 어떤 일을 할 수가 있을까요? 세가지가 가능합니다.
1. return 다른 promise
2. return 발생한 값 이나 undefined
3, throw 에러
이게 다입니다. 이제 이것들을 이해를 하면 여러분은 promise를 이해한 것입니다. 하나씩 살펴보겠습니다.

1. Return another promise
이는 위에서 살펴본 composing promise의 일반적인 패턴입니다.

getUserByName('nolan').then(function (user) {
  return getUserAccountById(user.id);
}).then(function (userAccount) {
  // I got a user account!
});

첫 번째 함수가 getUserAccountById() 함수를 리턴 합니다, 다음 함수로 말이지요. 다음 메서드는 콜백으로 해당 함수의 리턴값을 받습니다. 여기서 return을 하는 것이 매우 중요한데 만약에 return을 하지 않는다면 다음 함수는 아마 undefined를 받게 될 것입니다. 우리는 userAccount가 필요한데 말입니다.

2. Return a synchronous value (or undefined)
undefined를 리턴 하는 일은 종종 발생하는 실수입니다. 하지만 동기적으로 발생한 값을 리턴 하는 것은 동기적인 코드를 promise방식의 코드로 변환하는 굉장한 방법입니다. 예를들어 우리가 사용자 캐시를 in-memory 에 가지고 있다고 하겠습니다. 우리는 아래의 코드처럼 구현할 수 있습니다.

getUserByName('nolan').then(function (user) {
  if (inMemoryCache[user.id]) {
    return inMemoryCache[user.id];    // returning a synchronous value!
  }
  return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
  // I got a user account!
});

굉장하지 않나요? 두 번째 함수는 userAccount가 동기적이나 비동기적으로 가져온 값인지 아닌지는 신경 쓰지 않습니다. 첫 번째 함수는 동기적인 값이나 비 동기적인 값 모두를 마음대로 전달할 수가 있습니다.
불행하게도 값을 리턴하지 않는 함수의 경우에는 자바스크립트는 undefined로 값을 처리해버립니다. 이는 예상치 못한 문제를 일으키게 됩니다.
이러한 이유로 저는 개인적으로는 then() 함수 안에서는 반드시 throw나 return을 통해서 무언가를 다음 함수로 전달을 시킵니다. 
여러분도 이렇게 해야 합니다.

3. Throw a synchronous error
throw는 promise를 더욱 굉장하게 만들어 주는 역할을 합니다. 동기적으로 에러가 발생하면 throw를 통해 에러를 전달합니다. 사용자가 로그아웃이 되었다면 에러를 발생시키는 코드입니다. 

getUserByName('nolan').then(function (user) {
  if (user.isLoggedOut()) {
    throw new Error('user logged out!'); // throwing a synchronous error!
  }
  if (inMemoryCache[user.id]) {
    return inMemoryCache[user.id];       // returning a synchronous value!
  }
  return getUserAccountById(user.id);    // returning a promise!
}).then(function (userAccount) {
  // I got a user account!
}).catch(function (err) {
  // Boo, I got an error!
});

우리의 catch()는 사용자가 로그아웃이 되었다면 동기적으로 에러를 받습니다. 그리고 물론 promise가 reject가 발생하는 경우 비동기적으로 에러를 받습니다. 다시 한번 강조하면 함수는 에러가 동기적이든 비동기적이든 전혀 신경 쓰지 않습니다.

이는 매우 유용한데 왜냐하면 개발 시에 코딩의 에러를 발견하는데 도움을 줍니다. 예를 들어 then() 메서드의 내부에 JSON.parse()를 한다고 가정 하겠습니다. 이 경우에 json에 문제가 있다면 동기적으로 에러를 발생시킬 수 있습니다. callback의 경우에는 에러를 무시하는 일이 발생합니다. 하지만 promise와 함께라면 우리는 간단하게 catch() 내부에서 이모든 일들을 처리할 수 있습니다.


Advanced mistakes
자, 이제 조금 더 깊이 있게 들어가보겠습니다.

이번에 다룰 내용들은 advanced 내용입니다. advanced로 분류가 된 이유는 이미 promise를 이해하고 받아들인 개발자들이 일으키는 실수이기 때문입니다. 하지만 우리는 이것들을 다룰 필요가 있습니다. 만약에 우리가 쉽게 해결할 수 있는 문제였다면 앞으로의 내용들을 beginner 섹션에 다뤘을 것입니다.

Advanced mistake #1: not knowing aboutPromise.resolve()
위의 다양한 예들을 통해 promise는 동기적인 코드를 비 동기적인 코드로 감싸주는 매우 유용한 기능입니다. 구현은 아래와 같이 합니다.

new Promise(function (resolve, reject) {
  resolve(someSynchronousValue);
}).then(/* ... */);

위의 표현을 Promise.resolve()를 통해 훨씬 간결하게 표현하는 것이 가능합니다.
Promise.resolve(someSynchronousValue).then(/* ... */);

이 방법은 어떠한 동기적인 에러를 잡는데 매우 유용합니다. 그래서 저는 대부분의 promise-returning API를 구현할 때 제일 첫 시점에 아래와 같이 형태를 잡아두고 시작을 합니다.

function somePromiseAPI() {
  return Promise.resolve().then(function () {
    doSomethingThatMayThrow();
    return 'foo';
  }).then(/* ... */);
}

그냥 기억하세요. 동기적으로 throw가 발생하는 코드는 코드상의 어딘가에서 에러가 묻혀버려 거의 디버깅이 불가능합니다. 하지만 만약 모든 것들을 Promise.resolve()로 감싼다면 우린 항상 이것들일 catch()를 통해 확인할 수가 있습니다.

이와 유사하게 Promise.reject()가 있는데 이는 당장 promise 를 거부시키는데 사용할수있습니다.
Promise.reject(new Error('some awful error'));


Advanced mistake #2: catch() isn't exactly likethen(null, ...)
저는 위에서 catch()는 매우 유용하다고 말했습니다. 그래서 아래의 두 개의 작은 코드는 동일합니다.
somePromise().catch(function (err) {
  // handle error
});

somePromise().then(null, function (err) {
  // handle error
});


하지만 아래의 두 개의 코드는 동일한 의미는 아닙니다.
somePromise().then(function () {
  return someOtherPromise();
}).catch(function (err) {
  // handle error
});

somePromise().then(function () {
  return someOtherPromise();
}, function (err) {
  // handle error
});

만약에 위의 두 코드가 왜 다른지 궁금하다면 첫번째 코드에서 첫 함수가 에러를 반환한다면 무슨 일이 벌어질지를 생각해보면 됩니다.
somePromise().then(function () {
  throw new Error('oh noes');
}).catch(function (err) {
  // I caught your error! :)
});

somePromise().then(function () {
  throw new Error('oh noes');
}, function (err) {
  // I didn't catch your error! :(
});


보시는 봐와 같이 then(resolveHandler, rejectHandler)의 형태로 구현을 한다면 에러가 resolveHandler를 통해 반환이 된경우라면 rejectHandler가 에러를 잡아내지 못하게 됩니다.

이러한 이유로 저는 절대 두번째 방식으로는 구현을 하지 않습니다. 항상 에러에 대비해서는 catch()를 사용합니다. 단, Mocha 를 통한 비동기적인 테스트 코드 구현은 제외합니다.
it('should throw an error', function () {
  return doSomethingThatThrows().then(function () {
    throw new Error('I expected an error!');
  }, function (err) {
    should.exist(err);
  });
});



Promise.all()


Promise.all(iterable) 메서드는 iterable 인자의 모든 프로미스가 이행하거나 iterable 인자가 비어있는 경우 이행하는 프로미스를 반환합니다. iterable 인자 내 임의의 프로미스가 거부하는 경우 첫 번째로 거절한 프로미스의 이유를 사용해 거절합니다.

구문

Promise.all(iterable);

Link to section매개변수

iterable
Array 또는 String과 같이 순회 가능한 객체.

Link to section반환값

  • 인자가 빈 경우, 이미 이행된 Promise.
  • 인자에 프로미스가 없는 경우, 비동기적으로 이행된 Promise. 단, 구글 크롬 58은 이미 이행된 프로미스를 반환합니다.
  • 다른 모든 경우, 대기 중인 Promise. 반환하는 프로미스는 나중에 비동기적으로(스택이 비는 시점에) , 모든 프로미스가 이행하거나 임의의 프로미스가 거부할 때 이행/거부합니다. "Promise.all 실패-우선 연산" 예제를 참고하세요. 반환하는 프로미스의 이행 값은 인자로 넘어온 프로미스의 순서와 일치하며 완료 순서에 영향을 받지 않습니다.

Link to section설명

이 메서드는 여러 프로미스의 결과를 집계할 때 유용하게 사용할 수 있습니다.

이행:
빈 iterable이 전달되면이 메서드는 이미 해결 된 promise를 동기적으로 반환합니다.
전달 된 promise가 모두 충족(fulfilled)되거나 promise가 아닌 경우 Promise.all에서 반환하는 promise는 비동기 적으로 수행됩니다.
모든 경우에 반환된 promise는 인수로 전달 된 반복 가능한 값의 모든 값 (non-promise value)을 포함하는 배열로 충족(fulfilled)됩니다.

거부:
전달 된 promise중 하나라도 거부하면 Promise.all은 다른 promise가 남아있는지에 여부에 관계없이 다른 모든 Promise를 버리고 문제가 되는 값과 함께 거절(reject)한다.

 

Link to section예제

Link to sectionPromise.all사용하기

Promise.all은 배열 내 모든 값이 결정(resolve)할 때까지 기다립니다.

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, "foo");
}); 

Promise.all([p1, p2, p3]).then(function(values) { 
  console.log(values); // [3, 1337, "foo"] 
});

Link to sectionPromise.all 실패-우선 연산

Promise.all 은 배열 내 요소 중 어느 하나라도 거절(reject)하면 즉시 거절합니다. 즉 Promise.all 은 빠르게 실패합니다: 만약 timeout 이후 결정(resolve)하는 4개의 Promise를 가지고 있고, 그 중 하나가 거절(reject)하면 Promise.all 은 즉시 거절합니다.


all 구문중 실패시하는  Promise 함수가 있다면 해당 실패 promise 함수의 인자가 넘어오게 된다

성공하면 모든 인자가 출력 된다


var p1 = new Promise(function(resolve, reject) { 
  setTimeout(resolve, 1000, "one"); 
}); 
var p2 = new Promise(function(resolve, reject) { 
  setTimeout(resolve, 2000, "two"); 
});
var p3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 3000, "three");
});
var p4 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 4000, "four");
});
var p5 = new Promise(function(resolve, reject) {
  reject("reject");
});

Promise.all([p1, p2, p3, p4, p5]).then(function(value) { 
  console.log(value);
}, function(reason) {
  console.log(reason)
});

//From console:
//"reject"


ref : https://joshua1988.github.io/web-development/javascript/promise-for-beginners
ref  :http://yubylab.tistory.com/entry/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-Promise-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0
ref : 
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all




반응형

'서버(Server) > Server' 카테고리의 다른 글

통신 모델 OSI 7 계층  (0) 2022.12.22
javascript : Class  (0) 2018.08.19
Promise [2]  (0) 2018.08.16
[javascript] Singleton 싱글톤  (0) 2018.07.02
공개/비공개 스태틱 멤버와 클로저를 활용한 객체 생성  (0) 2018.07.01
반응형


Promise



기본적으로 Promise 객체 생성후 Promise 를 통해서 다음 실행 구문을 연달아 수행하는 형태

그러나 처리 되는 상황에 따라 then(then 안의 함수들) or catch(안의 함수) 를 타고 가는 구문에 차이가 생기게 된다

var p = new Promise(function (resolve, reject) {     //이걸 호출하지 않으면 then 으로 넘어가지 않는다     //resolve(2);       //성공 함수로 넘어간다     //이걸 호출하지 않으면 then 으로 넘어가지 않는다     reject(2);        //실패 함수로 넘어가게 된다 }); //then() 은 Primise 를 리턴한다 p.then(function (value) {       //성공함수     console.log(value);     return value + 1; }, function(reject) {           //실패함수     console.log(reject);     return 10; } ).then(function (value) {        //다음 함수들     console.log(value);     return value + 1; }).then(function (value) {        //다음 함수들

    console.log(value);     throw 100; }).catch(function (ff) {            //예외 함수

    console.log(ff); });


결과
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
resolve(2); 일때의 결과
 
2
3
4
100
 
 
 
reject(2); 일때의 결과
 
2
10
11
100
 
cs


Promise 생성하기

Promise 오브젝트는 new 키워드와 생성자를 사용하여 만듭니다. 이 생성자는 인수로 "executor 함수"라는 함수를 사용합니다. 이 함수는 매개 변수로 두 가지 함수를 가져야 합니다. 비동기 작업이 성공적으로 완료되고 결과를 값으로 반환하면 첫 번째 함수(resolve)가 호출됩니다. 두 번째 (reject)는 작업이 실패 할 때 호출되며 보통 오류 객체를 반환합니다.

const myFirstPromise = new Promise((resolve, reject) => {
  // do something asynchronous which eventually calls either:
  //
  //   resolve(someValue); // fulfilled
  // or
  //   reject("failure reason"); // rejected
});

함수가 프로미스를 사용하도록 하려면 단순히 프로미스를 반환하면됩니다.

function myAsyncFunction(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(xhr.statusText);
    xhr.send();
  });
}


예제

Link to section기본 예제

let myFirstPromise = new Promise((resolve, reject) => 
{
  // We call resolve(...) when what we were doing asynchronously was successful, and reject(...) when it failed.
  // In this example, we use setTimeout(...) to simulate async code. 
  // In reality, you will probably be using something like XHR or an HTML5 API.
  setTimeout(function(){
    resolve("Success!"); // Yay! Everything went well!
  }, 3000);
});

myFirstPromise.then((successMessage) => {
  // successMessage is whatever we passed in the resolve(...) function above.
  // It doesn't have to be a string, but if it is only a succeed message, it probably will be.
  console.log("Yay! " + successMessage);
});



결과 3초 후  

"Success!"





Promise.prototype.then()



then() 메서드는 Promise를 리턴하고 두개의 콜백 함수를 인수로 받습니다. 하나는 Promise가 성공(success)했을 때를 위한 콜백 함수이고, 다른 하나는 실패(failure)했을 때를 위한 콜백 함수입니다.



구문

p.then(onFulfilled, onRejected);

p.then(function(value) {
  // 이행(fulfillment)
  }, function(reason) {
  // 거부
});

Link to section매개변수

onFulfilled
Promise가 수행될 때 호출되는 Function 이고, 수행 값(fulfillment value) 하나를 인수로 받습니다.
onRejected
Promise가 거부될 때 호출되는 Function 이고, 거부 이유(rejection reason) 하나를 인수로 받습니다.

Link to section설명

then 과 Promise.prototype.catch() 메서드는 promise 를 리턴하기 때문에, chaining 이 가능합니다. — composition 이라고도 합니다.

Link to section

Link to sectionthen 메서드 사용

var p1 = new Promise(function(resolve, reject) {
  resolve("Success!");
  // 또는
  // reject ("Error!");
});

p1.then(function(value) {
  console.log(value); // 성공!
}, function(reason) {
  console.log(reason); // 오류!
});

Link to sectionChaining

then 메서드는 Promise를 리턴하기 때문에, 이어지는 then 호출들을 손쉽게 chaining 할 수 있습니다. onFulfilled 또는 onRejected 콜백 함수가 리턴하는 값은 자동으로 resolved promise로 wrapping 되기 때문에 다음에 오는 then 이나 catch 메서드로 전달 됩니다.

var p2 = new Promise(function(resolve, reject) {
  resolve(1);
});

p2.then(function(value) {
  console.log(value); // 1
  return value + 1;
}).then(function(value) {
  console.log(value); // 2
});

p2.then(function(value) {
  console.log(value); // 1
});








다음과 같이 catch()를 사용할 수도 있습니다.

get('story.json').then(function(response) {
  console
.log("Success!", response);
}).catch(function(error) {
  console
.log("Failed!", error);
})

catch()에 대한 특별한 것은 없습니다. then(undefined, func)의 보완에 불과하지만 가독성은 훨씬 높습니다. 상기 두 코드 예시는 동일하게 동작하지 않습니다. 후자는 다음과 같습니다.

get('story.json').then(function(response) {
  console
.log("Success!", response);
}).then(undefined, function(error) {
  console
.log("Failed!", error);
})

차이는 미묘하지만 매우 유용합니다. 프라미스 거부는 거부 콜백(또는 동일하게 기능하는 catch())을 사용하여 다음 then()으로 건너뜁니다. then(func1, func2)를 사용하는 경우 func1와 func2 중에 하나만 호출되며, 둘이 동시에 호출되지 않습니다. 그러나 then(func1).catch(func2)를 사용하는 경우 둘은 체인에서 개별적인 단계이므로 func1이 거부하면 둘 다 호출됩니다. 다음을 봅시다.

asyncThing1().then(function() {
 
return asyncThing2();
}).then(function() {
 
return asyncThing3();
}).catch(function(err) {
 
return asyncRecovery1();
}).then(function() {
 
return asyncThing4();
}, function(err) {
 
return asyncRecovery2();
}).catch(function(err) {
  console
.log("Don't worry about it");
}).then(function() {
  console
.log("All done!");
})

위의 흐름은 일반 자바스크립트 try/catch와 매우 유사하며, 'try'를 사용하여 발생하는 오류는 즉시 catch() 블록으로 이동합니다. 다음은 이를 흐름도로 만든 것입니다.



프라미스 처리 시에는 청색선을 따르고 프라미스 거부 시에는 적색선을 따르면 됩니다.






new Promise, Promise.resolve()  차이


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
//new Promise 패턴을 사용하면 아래와 같이 쓸 수 있습니다.
 
function async1 (param) {
    return new Promise(function(resolve, reject) {
        resolve(param*2);
    });
}
function async2 (param) {
    return new Promise(function(resolve, reject) {
        resolve(param*3);
    });
}
function async3 (param) {
    return new Promise(function(resolve, reject) {
        resolve(param*4);
    });
}
 
var start = 1;
async1(start)
    .then(async2)
    .then(async3)
    .then(result => {
        console.log(result); // 24
    });
 
 
 
 
 
//함수 선언 코드가 좀 길어지긴 했지만... 함수 사용 부분은 좀 더 명확해졌습니다.
//같은 내용을 Promise.resolve() 로 사용하면 아래와 같죠.
 
 
function async1 (param) {
    return Promise.resolve(param*2);
}
function async2 (param) {
    return Promise.resolve(param*3);
}
function async3 (param) {
    return Promise.resolve(param*4);
}
 
var start = 1;
async1(start)
    .then(async2)
    .then(async3)
    .then(result => {
        console.log(result); // 24
    });
 
 
 
//그런데 이런 상황이 존재함
 
//(이 예제는 크롬의 cross origin 제약을 해제하고 실행했습니다.)
 
function request (param) {
    return Promise.resolve()
        .then(function () {
            var xhr = new XMLHttpRequest();
 
            xhr.open('GET''http://google.co.kr/'true);
            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    return Promise.resolve(xhr.response);
                }
            };
            xhr.send(null);
        });
}
 
Promise.resolve()
    .then(request)
    .then(result => {
        console.log('ok');
        console.log(result);
    });
 
/*
위 코드는 http://google.co.kr에 GET 요청을 보낸 뒤 받아서 결과를 텍스트로 출력하는 예제입니다.
 Promise 안에서 readyState와 status를 확인해서 Promise.resolve()를 리턴하므로 얼핏 보면 돌아갈 
 것 같습니다만... 제대로 동작하지 않습니다. 콘솔에 찍히는 result는 undefined 입니다.  */
 
 
결과는 
 
ok
undefined
 
 
 
//request() 함수에서 xhr.send() 를 실행하면 해당 컨텍스트에서의 작업은 끝나고 Promise 컨텍스트도 종료됩니다. 
//xhr이 완료되었을 때 반환하는 객체를 받을 타이밍에는 이미 이전 함수가 끝나있기 때문이죠.
 
//이 코드는 new Promise()를 써서 이렇게 바뀌어야 합니다.
 
 
 
function request (param) {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest();
 
        xhr.open('GET''http://google.co.kr/'true);
        xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
                resolve(xhr.response);
            }
        };
        xhr.send(null);
    })
}
 
Promise.resolve()
    .then(request)
    .then(result => {
        console.log('ok');
        console.log(result);
    });
 
 
 
/*
두번째 라인에서 생성한 Promise 객체의 함수 실행 컨텍스트를 유지하기 위해 Promise.resolve() 대신 new Promise()를 사용하고 
최종 콜백 지점인 여덟번째 라인에서 resolve()를 실행해줍니다. 기존의 callback 사용 방식과 비슷하다고 보면 간단합니다.
이렇게 사용하면 Promise 실행 컨텍스트를 벗어나지 않고 원하는 결과를 얻을 수 있습니다.*/
 
 
 
 
 
 

cs



비슷하게 ajax 콜을 요청하거나 DB에서 데이터를 가져올 때와 같이 async 함수를 감싸는 경우는 new Promise()를
사용하면 됩니다.

처음 Promise를 사용할 때는 왜 같은 흐름의 함수가 두 개로 제공될까 이상했는데, 사용하다보니
return Promise.resolve()와 new Promise()는 
사용 방법이 약간 다릅니다. return Promise.resolve()는
sync 로직 흐름에서, new Promise()는 sync는 물론, async 로직 흐름에서도 사용할 수 있습니다.

요새 모듈은 사용할 때 Promise를 지원하는 경우가 종종 있습니다. 좀 더 많은 모듈이 Promise를 지원하기를 바래봅니다


ref : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise
ref : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
ref : https://developers.google.com/web/fundamentals/primers/promises
ref : http://han41858.tistory.com/11


반응형
반응형


[javascript] Singleton 싱글톤




싱글턴의 싱글은 혼자의 싱글이 맞습니다. 

객체를 만들 때, 하나의 생성자로 여러 객체를 만들 수 있었습니다. 

하지만 싱글턴은 필요에 의해 단 하나 객체만을 만들 때 사용합니다. 아래 처럼요.

var obj = {
  a: 'hello',
  b: function() {
    alert(this.a);
  }
};

?? 엄청 간단하네요. 사실 객체 리터럴이 바로 싱글턴 패턴의 대표적인 예입니다. 저 객체는 단 하나밖에 존재하지 않죠. 하지만 모든 속성이 다 공개되어 있다는 단점이 있습니다. 비공개로 만드는 게 바로 제대로 된 싱글턴입니다.

var singleton = (function() {
  var instance;
  var a = 'hello';
  function initiate() {
    return {
      a: a,
      b: function() {
        alert(a);
      }
    };
  }
  return {
    getInstance: function() {
      if (!instance) {
        instance = initiate();
      }
      return instance;
    }
  }
})();
var first = singleton.getInstance();
var second = singleton.getInstance();
console.log(first === second); // true;

코드가 확 길어졌네요. 차근히 살펴보면 쉽습니다. IIFE로 비공개 변수를 가질 수 있게 만들어줍니다. 그리고 그 안에 instance변수와 initiate 함수를 만들어줍니다. initiate 함수 안의 내용이 실제 객체의 내용입니다. 위의 obj 객체와 비교하면 a가 비공개 변수가 되었네요.

IIFE로 즉시 반환되는 부분(return)을 보시죠. getInstance라는 메소드를 가진 객체를 반환하는데, getInstance 함수를 호출하는 순간 내부적으로 initiate 함수가 호출되고, instance에 아까 그 객체의 내용이 저장되고 동시에 반환됩니다. getInstance가 여러 번 호출됐을 경우에는 코드를 보시면 이미 instance 객체가 있는 경우에는 initiate를 거치지 않고 바로 반환하는 것을 알 수 있습니다.

first와 second 변수를 보면 두 번 다 getInstance 함수를 호출했는데요. 결과적으로 두 변수는 같습니다. first 때 initiate된 객체를 second 때도 똑같이 반환받았기 때문이죠. 즉, 아무리 호출해도 기존에 있던 객체는 복사되는 것도 아니고 그냥 그대로 반환됩니다. 싱글턴 패턴은 모듈 패턴을 변형한 디자인 패턴입니다.

싱글턴을 언제 써야할 지 잘 모르겠나요? 처음 네임스페이스를 만들 때 사용하면 됩니다! 예를 들어 게임을 만든다고 치면, 게임 객체를 싱글턴으로 만드는 겁니다. 게임 내의 모든 것을 감싸고 있는 객체를 말이죠. 게임을 실행했을 때 게임은 한 번만 켜져야하기 때문에 싱글턴이 적절합니다.

세 번째는 생성자 패턴입니다. 이것도 이미 배웠죠? 상속을 배울 때 다뤘습니다! 대부분의 객체는 이 패턴으로 만들게 됩니다. 특히 상속이 필요할 때는 제일 많이 쓰이죠. 모듈 패턴과 생성자 패턴을 조합해서 코드를 보기 좋게 만들 수 있습니다.

function Vehicle(name, speed) {
  this.name = name; this.speed = speed;
}
Vehicle.prototype.drive = function () {
  console.log(this.name + ' runs at ' + this.speed)
}; 

function 부분과 prototype 부분으로 따로 떨어져 있는 이 코드를 하나로 묶어줍시다.

var Vehicle = (function() {
  function Vehicle(name, speed) {
    this.name = name; this.speed = speed;
  }
  Vehicle.prototype.drive = function () {
    console.log(this.name + ' runs at ' + this.speed);
  };
  return Vehicle;
})();

생성자와 프로토타입을 모두 Vehicle 변수 안에 넣었습니다. 변수 Vehicle과 생성자 Vehicle 이름이 같아서 걱정하시는 분이 있을 수도 있는데 IIFE라서 바로 변수 Vehicle에 생성자 Vehicle이 덮어씌워집니다.

이외에도 빌더 패턴, 팩토리 패턴, 중재자 패턴, 옵저버 패턴, 메소드 체이닝 패턴 등이 있는데 고급 강좌에서 다뤄보겠습니다. (메소드 체이닝 패턴은 자주 사용됩니다. 28강 턴제 게임 만들기에서 알려드리겠습니다!)

다음 시간에는 자바스크립트 함수형 프로그래밍에 대해 알아보겠습니다.



ref : https://www.zerocho.com/category/JavaScript/post/57541bef7dfff917002c4e86


반응형
반응형

스태틱 멤버

스태틱 프로퍼티와 메서드란 인스턴스에 따라 달라지지 않는 프로퍼티와 메서드 를 말합니다.

클래스 기반 언어에서는 별도의 문법을 통해 스택틱 멤버를 생성하여 클래스 자체의 멤버인 것처럼 사용할 수 있습니다.

예를 들어 MathUtils 클래스에 max() 라는 스태틱 메서드가 있다면MathUtils.max(2,5) 와 같은 식으로 호출할 수 있습니다.

이것은 공개 스태틱 멤버의 예로, 클래스의 인스턴스를 생성하지 않고도 사용할 수 있습니다.

비공개 스태틱 멤버는 클래스 사용자에게는 보이지 않지만 클래스의 인스턴스들은 모두 함께 사용할 수 있습니다.

그럼 자바스크립트에서 공개와 비공개 스태틱 멤버를 구현하는 방법을 살펴보도록 합니다.






공개 스태틱 멤버

자바스크립트에는 스태틱 멤버를 표기하는 별도의 문법이 존재하지 않습니다.

그러나 생성자에 프로퍼티를 추가함으로써 클래스 기반 언어와 동일한 문법을 사용할 수 있습니다.

생성자도 다른 함수와 마찬가지로 객체이고 그 자신의 프로퍼티를 가질 수 있기 때문에 이러한 구현이 가능합니다.

다음 예제는 Gadget 이라는 생성자에 스태틱 메서드인 isShiny() 와 일반적인 인스턴스 메서드인 setPrice() 를 정의한 것입니다.

isShiny() 는 특정 Gadget 객체를 필요로 하지 않기 때문에 스태틱 메서드라 할 수 있습니다.

모든 Gadget 이 빛나는지 알아내는 데는 특정한 하나의 Gadget 이 필요하지 않은 것과 같습니다.

반면 개별 Gadget 들의 가격은 다를 수 있기 때문에 setPrice() 메서드를 쓰려면 객체가 필요합니다.

JAVASCRIPT
// 생성자
var Gadget = function () { };

// 스태틱 메서드
Gadget.isShiny = function () {
	return 'you bet.'
};

// 프로토타입에 일반적인 함수를 추가
Gadget.prototype.setPrice = function (price) {
	this.price = price;
};

이제 이 메서드를 호출해 보도록 합니다.

스태틱 메서드인 isShiny() 는 생성자를 통해 직접 호출되지만, 일반적인 메서드는 인스턴스를 통해 호출됩니다.

JAVASCRIPT
// 스태틱 메서드를 호출하는 방법
console.log(Gadget.isShiny()); // you bet
	
// 인스턴스를 생성한 후 메서드를 호출하기
var iphone = new Gadget();
iphone.setPrice(500);

인스턴스 메서드를 스태틱 메서드와 같은 방법으로 호출하면 동작하지 않습니다.

스태틱 메서드 역시 인스턴스 iphone 객체를 이용해 호출하면 동작하지 않습니다.

JAVASCRIPT
console.log(typeof Gadget.setPrice); // undefined
console.log(typeof iphone.isShiny); // undefined

스태틱 메서드가 인스턴스를 통해 호출했을 때도 동작한다면 편리한 경우가 있을 수 있습니다.

이 경우에는 간단하게 프로토타입에 새로운 메서드를 추가하는 것만으로 쉽게 구현할 수 있습니다.

이 새로운 메서드는 원래의 스태틱 메서드를 가리키는 일종의 퍼사드(facade) 역할을 합니다.

JAVASCRIPT
Gadget.prototype.isShiny = Gadget.isShiny;
console.log(iphone.isShiny());

이런 경우에는 스태틱 메서드 안에서 this 를 사용할 때 주의를 기울여야 합니다.

Gadget.isShiny() 호출했을 때 내부의 this 는 Gadget 생성자를 가리키지만, iphone.isShiny() 를 호출했을 때는 this 는 생성자 함수로 생성된 객체인 iphone 을 가리키게 됩니다.


마지막으로 스태틱한 방법으로도, 스태틱하지 않은 방법으로도 호출될 수 있는 어떤 메서드를 호출 방식에 따라 살짝 다르게 동작하게 하는 예제를 살펴보도록 합니다.

메서드가 어떻게 호출되었는지 판별하기 위해서 instanceof 연산자를 활용해 봅니다.

JAVASCRIPT
// 생성자
var Gadget = function (price) {
	this.price = price;
};

// 스태틱 메서드
Gadget.isShiny = function () {
	
	// 다음은 항상 동작한다.
	var msg = 'you bet';
	
	if (this instanceof Gadget) {
		// 다음은 스태틱하지 않은 방식으로 호출되었을 때만 동작하도록 한다.(인스턴스 멤버를 가리킴)
		msg += ', it costs $' + this.price + '!!';
	}
	return msg;
	
};

// 프로토타입에 일반적인 메서드를 추가한다
Gadget.prototype.isShiny = function () {
	return Gadget.isShiny.call(this); // 이 this 는 생성자 함수로 생성된 인스턴스 객체를 가리키게 된다.
};

스태틱 메서드와 인스턴스를 통해 스태틱하지 않은 방법으로 호출해 보면 다음과 같은 결과가 나타납니다.

JAVASCRIPT
// 스태틱 메서드를 호출
console.log(Gadget.isShiny()); // you bet 이 기록된다.
	
// 인스턴스를 통해 스태틱하지 않은 방법으로 호출
var iphone = new Gadget(345.99);
console.log(iphone.isShiny()); // you bet, it costs $345.99!! 가 기록된다.



비공개 스태틱 멤버

지금까지는 공개 스태틱 멤버를 살펴보았습니다.

이번에는 비공개 스태틱 멤버를 구현하는 방법을 알아봅니다.


비공개 스태틱 멤버란 다음과 같은 의미를 가지고 있습니다.

  • 동일한 생성자 함수로 생성된 객체들이 공유하는 멤버입니다.
  • 생성자 외부에서는 접근할 수 없습니다.


Gadget 생성자 안에 counter 라는 비공개 스태틱 프로퍼티를 구현하는 예제를 살펴보도록 합니다.

비공개 프로퍼티는 먼저 클로저 함수를 만들고, 비공개 멤버를 이 함수로 감싼 후 이 함수를 즉시 실행한 결과로 새로운 함수를 반환하게 됩니다.

반환되는 함수는 Gadget 변수에 할당되어 새로운 생성자가 될 것입니다.

JAVASCRIPT
var Gadget = function () {
	
	// 스태틱 변수/프로퍼티
	var counter = 0;
	
	// 생성자의 새로운 구현 버전을 반환한다.
	return function () {
		console.log(counter += 1);
	};
	
}(); // 즉시 실행한다.

새로운 Gadget 생성자는 단순히 비공개 counter 값을 증가시켜 출력합니다.

몇 개의 인스턴스를 만들어 테스트해보면 실제로 모든 인스턴스가 동일한 counter 값을 공유하고 있음을 확인할 수 있습니다.

JAVASCRIPT
var g1 = new Gadget(); // 1 이 기록된다
var g2 = new Gadget(); // 2 이 기록된다
var g3 = new Gadget(); // 3 이 기록된다

객체당 1씩 counter 를 증가시키고 있기 때문에 이 스태틱 프로퍼티는 Gadget 생성자를 통해 생성된 개별 객체의 유일성을 식별하는 ID 가 될 수 있다.

유일한 식별자는 쓸모가 많으니 특권 메서드로 노출시켜도 좋지 않을까?

앞선 예제에 덧붙여 비공개 스태틱 프로퍼티에 접근할 수 있는 getLastId() 라는 특권 메서드를 추가해보도록 해봅니다.

JAVASCRIPT
// 생성자
var Gadget = function () {
	
	// 스태틱 변수/프로퍼티
	var counter = 0,
	    NewGadget;
	
	// 이 부분이 생성자를 새롭게 구현한 부분이다.
	NewGadget = function () {
		counter += 1;
	};
	
	// 특권 메서드
	NewGadget.prototype.getLastId = function () {
		return counter;
	};
	
	// Gadgt 생성자를 덮어쓴다.
	return NewGadget;
	
}(); // 즉시 실행한다.

새로운 버전을 아래에서 테스트 해봅니다.

JAVASCRIPT
var iphone = new Gadget();
console.log(iphone.getLastId()); // 1 이 기록
	
var ipod = new Gadget();
console.log(ipod.getLastId()); // 2 이 기록

var ipad = new Gadget();
console.log(ipad.getLastId()); // 3 이 기록

공개/비공개 스태틱 프로퍼티는 상당히 편리합니다.

특정 인스턴스에 한정되지 않는 메서드와 데이터를 담을 수 있고 인스턴스별로 매번 재생성되지도 않습니다.




ref : http://webclub.tistory.com/526



반응형

'서버(Server) > Server' 카테고리의 다른 글

Promise [2]  (0) 2018.08.16
[javascript] Singleton 싱글톤  (0) 2018.07.02
JavaScript Closure(자바스크립트 클로저)  (0) 2018.07.01
[Javascript ] 프로토타입 사용 이해하기  (0) 2018.07.01
this 정리  (0) 2018.07.01
반응형

클로저(Closure)

클로저라는 개념은 자바스크립트에 없는 class 의 역할을 대신해 비공개 속성/메서드, 공개 속성/메서드를 구현할 수 있는 근거를 마련할 수 있습니다.

따라서 객체지향적인 특징인 캡슐화(encapsulation)와 정보 은닉(information hiding)을 이해하려면 클로저를 반드시 이해해야 합니다.

클로저는 jQuery 같은 대형 라이브러리에서 채용되는 패턴입니다.




JavaScript Closure

함수를 정의하면 함수 단위의 렉시컬한 변수 스코프가 생성되고 변수 스코프의 체인이 구성됩니다.

즉, 중첩 함수의 경우 코드상에서 함수가 중첩된 그대로의 모습으로 변수 스코프 체인이 구성되어 변수에 대한 접근 권한도 코드의 계층구조 그대로 구성됩니다.


이제 다른 상황을 가정해 봅니다. 

자바스크립트의 함수 역할 중에 함수는 객체로서 다른 함수의 반환값으로 사용될 수 있습니다.

이 경우 반환값으로 사용된 함수의 변수 스코프의 문제를 생각해 보겠습니다.


다음의 코드에서 함수 inner 가 반환값으로 사용되어 다른 영역의 코드에서 실행되는 경우를 가정해 봅니다.

코드는 다음과 같습니다.

JAVASCRIPT
function outer() {
    var x = 0;
    return function () {
        return ++x;
    };
}

// 코드 실행
var x = -1;
var f = outer();
console.log(f()); // 1 반환


위 코드는 클로저가 생성되는 조건에 부합합니다.

  • 내부 함수가 익명함수로 되어 outer의 반환값으로 사용됐다.
  • inner는 outer의 실행 환경에서 실행된다.
  • inner에서 사용되는 변수 x는 outer의 변수 스코프에 있다.


이 프로그램이 실행되면 var f = outer(); 에 의해 파싱 단계에서는 outer의 내부에서 정의됐던 익명 함수가 실행 단계에서는 outer의 외부로 전달되어 실행된다.

앞에서 설명한 상황을 그림으로 나타내면 다음과 같습니다.



            클로저의 렉시컬 환경


실행 환경에 있는 f를 통해 outer가 반환한 익명 함수가 호출되면 return ++x; 에서 사용된 변수 x를 어디에서 검색할까?

원래 정의될 때 생성된 익명 함수의 변수 스코프 객체에서 갬색할까? 아니면 f가 실행되고 있는 영역에서 검색할까?

이는 렉시컬 특성과 유사합니다. 즉, 런터임의 변수는 렉시컬 환경을 기준으로 정의된 변수 스코프 및 체인에서 검색한다는 것입니다.


프로그램 실행 시 변수 검색은 해당 문장(return ++x)이 포함된 함수가 정의된 렉시컬 환경에서의 변수 스코프 체인을 기준으로 한다.


결국 앞의 예제 코드에서 inner를 호출하면 ++x 연산에 의해 1이 반환됩니다.


이제 좀 더 중요한 내용 다뤄보도록 합니다. 

함수 inner 를 계속해서 호출해서 결과를 보면 다음과 같습니다. 

JAVASCRIPT
f(); // 2 반환
f(); // 3 반환
f(); // 4 반환

결과만 봤을 때는 약간 이상하게 보일 수 있습니다.

어떻게 f의 호출이 끝나고 나서도 그 부모의 변수 스코프에 있는 x 값이 유지될 수 있을까?


내부 함수에서 선언된 변수가 아니면서 내부 함수에서 사용하는 outer의 x 같은 변수를 자유 변수(free variable)라고 합니다.

x가 메모리에서 제거되는 시기는 outer가 결정하지 못합니다. 이런 자유 변수는 outer가 실행되고 있는 환경이 "닫는(close)" 역할을 합니다.

즉, x의 경우는 변수 스코프가 outer가 실행되는 환경으로까지 확장됩니다.

외부환경에서 내부 함수에 대한 참조 f를 가지고 이상(즉 f가 메모리에서 사라지지 않는 이상) outer 함수는 "실행 중" 상태에 있습니다.

따라서 자유 변수 x 및 해당 변수 스코프 체인 관계는 메모리에서 계속 유지됩니다.

이처럼 outer 호출이 종료되더라도 outer의 지역 변수 및 변수 스코프 객체의 체인 관계를 유지할 수 있는 구조를 클로저(closure)라고 합니다.


함수 호출이 종료되더라도 그 함수의 지역 변수 및 지역 변수 체인 관계를 유지할 수 있는 구조를 클로저라고 한다.


자유 변수의 경우 그 값은 렉시컬 환경의 영향을 받으면서 그 생명주기는 실행 환경의 영향을 받는다는 것이 결국 클로저를 만들 수 있는 근거가 됩니다.

여기까지 이해하는 것만으로도 많은 것을 배운 것이지만 클로저에 대해 알아야 할 내용은 이뿐만이 아닙니다.


여기서 잠깐!!

다음의 Fucntion 으로 생성한 함수는 클로저를 만들지 못한다는 것을 알아야 합니다. Function 생성자를 이용해 생성한 함수는 렉시컬 영역을 사용하지 않습니다.

그 함수는 항상 전역 영역에서 생성된 것처럼 컴파일 될 것입니다.

JAVASCRIPT
var x = "g";
function f() {
    var x = "1";
    return new Function("return x"); // x를 전역 변수 스코프에서 검색한다.
}
var global = f();
console.log(global()); // g 를 출력한다.

위 코드는 Function 으로 생성된 함수로는 클로저를 구성할 수 없음을 보여주고 있습니다.


그럼 좀더 클로저에 대해 알아보도록 하겠습니다.




클로저 인스턴스

클로저가 생성한 인스턴스라는 의미로서 "클로저 인스턴스"라는 표현을 사용했습니다.

이것은 클로저를 인스턴스를 생성하는 단위로 보겠다는 것입니다.

클로저란 것은 내부 함수를 반환값으로 사용하는 특수한 함수로 볼 수 있습니다.

즉, 클로저를 함수 인스턴스를 만들어내는 특수한 함수로 해석할 수 있다는 것입니다.

일반 객체지향 프로그래밍 언어의 클래스 같은 존재와 비교해 클로저를 "함수를 생성하는 클래스"로 비유적으로 표현할 수 있을 것입니다.


클로저란 호출하면 다른 함수 인스턴스를 생성해내는 특수한 구조의 함수다.


앞에서 본 outer 함수를 클래스로 생각하고 다시 살펴봅니다.

JAVASCRIPT
// outer 정의
function outer() {
    var x = 0;  // 비공개 영역
    return function () { // 외부에서 호출 가능한 영역
        return ++x;  // 공개 영역
    }
}

outer 클로저를 이렇게 비공개 변수를 정의하는 부분과 외부에서 호출이 가능한 공개 영역으로 나눠서 생각해 보면 다른 언어의 클래스와 더욱더 유사해 보일 것입니다.

이제 "outer를 호출" 하는 것을 바로 "함수 객체를 생성"하는 것으로 생각하면 됩니다.

JAVASCRIPT
var f = outer(); // outer 의 인스턴스를 생성


클로저를 호출하는 것은 "클로저 인스턴스를 생성"하는 것이다.


클로저가 반환한 함수 f를 호출하는 것을 outer가 외부에 공개한 메서드를 호출하는 것으로 간주할 수 있습니다.

JAVASCRIPT
f(); // outer 의 공개 함수 사용


클로저 인스턴스를 호출한다는 것은 클로저가 외부에 공개한 멤버를 호출하는 것으로 이해할 수 있다.


"f" 가 사라지지 않는 이상 인스턴스 f가 가지고 있는 변수도 계속 유지됩니다.

이제 다음과 같은 코드를 살펴보겠습니다.

JAVASCRIPT
// outer 정의
function outer() {
    var x = 0;  // 비공개 영역
    return function () {
        return ++x;  // 공개 영역
    }
}

var f = outer();
f(); // 1
f(); // 2
var g = outer();
g(); // 1
g(); // 2

outer()를 호출해서 생성된 함수를 f에 할당했습니다. f를 호출해서 값을 증가시키면 다음에 f를 호출될 때는 이전에 증가된 값이 유지되어 두 번째 호출의 시작값이 됩니다.

함수 f 호출이 종료되더라도 내부 변수 x는 그대로 유지되는 클로저의 속성을 이용하고 있는 것입니다.


중요한 것은 지금부터입니다.

다시 한번 outer()를 호출해서 새로운 인스턴스를 g에 할당한 후 g를 호출해서 결과를 보면 "내부 변수 x가 새롭게 초기화"되었음을 알 수 있습니다.

내부 변수 x가 새롭게 초기화됐다는 것은 이전의 함수 f와 새롭게 생성된 함수 g는 전혀 다른 변수 공간을 사용하는 별도의 존재라는 것입니다.

클래스로 인스턴스를 생성할 때마다 자신만의 닫혀진 공간을 가진 인스턴스가 생성됩니다.

그래서 인스턴스에서는 다른 인스턴스의 닫혀진 공간에 있는 내부 변수에는 직접 접근할 수 없습니다.


클로저를 이렇게 닫혀진 공간을 가진 인스턴스를 생성하는 존재로 생각할 수 있습니다.

클로저를 호출하면 단순히 익명함수가 반환되는 것이 아니라 익명함수와 함께 거기에 연결된 닫혀진 공간이 함께 반환되는 것입니다.

그 닫혀진 공간에 내부 변수가 존재합니다.


클래스와 new를 사용해 여러 개의 닫혀진 공간을 가진 인스턴스를 만들어 내듯이 클로저와 ()를 이용하면 닫혀진 공간을 가지는 인스턴스를 여러 개 반복해서 만들어 낼 수 있습니다.

위의 f와 g는 이제 독립적인 변수 공간을 가지는 인스턴스입니다.

클로저 인스턴스는 단순히 클로저에서 반환된 함수에 대한 참조가 아니라 독립된 변수 공간을 가진 인스턴스를 반환하는 것입니다.

이것이 클로저의 진정한 의미입니다.


클로저란 비공개 내부 변수를 갖는 함수 인스턴스 생성자다. 

그리고 클로저로 생성한 독립된 변수 공간을 가진 인스턴스를 클로저 인스턴스라고 한다.







ref : http://webclub.tistory.com/387

반응형
반응형


[Javascript ] 프로토타입 이해하기

자바스크립트는 프로토타입 기반 언어라고 불립니다. 자바스크립트 개발을 하면 빠질 수 없는 것이 프로토타입인데요. 프로토타입이 거의 자바스크립트 그 자체이기때문에 이해하는 것이 어렵고 개념도 복잡합니다.

하지만 프로토타입이 무엇인지 깨우친 순간 자바스크립트가 재밌어지고, 숙련도가 올라가는 느낌을 팍팍 받을 수 있습니다. 그럼 지금부터 프로토타입을 이해해봅시다.

Prototype vs Class

클래스(Class)라는 것을 한 번쯤은 들어보셨을겁니다. Java, Python, Ruby등 객체지향언어에서 빠질 수 없는 개념이죠. 그런데 중요한 점은 자바스크립트도 객체지향언어라는 것입니다. 이게 왜 중요하냐구요? 자바스크립트에는 클래스라는 개념이 없거든요. 대신 프로토타입(Prototype)이라는 것이 존재합니다. 자바스크립트가 프로토타입 기반 언어라고 불리는 이유이죠.

클래스가 없으니 기본적으로 상속기능도 없습니다. 그래서 보통 프로토타입을 기반으로 상속을 흉내내도록 구현해 사용합니다.

참고로 최근의 ECMA6 표준에서는 Class 문법이 추가되었습니다. 하지만 문법이 추가되었다는 것이지, 자바스크립트가 클래스 기반으로 바뀌었다는 것은 아닙니다.


어디다 쓰나요?

그럼 프로토타입을 언제 쓰는지 알아봅시다.

넌 이미 알고있다

자바스크립트에 클래스는 없지만 함수(function)와 new를 통해 클래스를 비스무리하게 흉내낼 수 있습니다.

function Person() {
this.eyes = 2;
this.nose = 1;
}
var kim  = new Person();
var park = new Person();
console.log(kim.eyes);  // => 2
console.log(kim.nose); // => 1
console.log(park.eyes); // => 2
console.log(park.nose); // => 1

kim과 park은 eyes와 nose를 공통적으로 가지고 있는데, 메모리에는 eyes와 nose가 두 개씩 총 4개 할당됩니다. 객체를100개 만들면 200개의 변수가 메모리에 할당되겠죠?
바로 이런 문제를 프로토타입으로 해결할 수 있습니다.

function Person() {}
Person.prototype.eyes = 2;
Person.prototype.nose = 1;
var kim  = new Person();
var park = new Person():
console.log(kim.eyes); // => 2
...

자바스크립트 개발을 하시는 분이라면 아마 써보진 않았어도 최소한 본 적은 있을겁니다. 간단히 설명하자면 Person.prototype이라는 빈 Object가 어딘가에 존재하고, Person 함수로부터 생성된 객체(kim, park)들은 어딘가에 존재하는 Object에 들어있는 값을 모두 갖다쓸 수 있습니다.
즉, eyes와 nose를 어딘가에 있는 빈 공간에 넣어놓고 kim과 park이 공유해서 사용하는 것이죠. 이해되셨나요?

프로토타입을 깊게 파보면 엄청나게 복잡하지만 개발자가 사용하는 부분만 본다면 이게 거의 전부입니다. 하지만 개발자는 사용법만 알고있는게 아니라 언제나 왜? 를 생각해야합니다.

프로토타입이 왜 이렇게 쓰이는지 조금 더 깊게 알아보도록 하겠습니다.


Prototype Link와 Prototype Object

자바스크립트에는 Prototype Link 와 Prototype Object라는 것이 존재합니다. 그리고 이 둘을 통틀어 Prototype이라고 부릅니다. 프로토타입을 좀 안다는 것은 이 둘을 완벽히 이해하고 갖고 놀 수준이 되었다는 뜻입니다.

제가 프로토타입에 대해 공부하면서 중요하다고 생각되는 포인트가 몇 가지 있었습니다. 그 포인트들을 잘 이해하면서 보시기 바랍니다.

Prototype Object

모든 객체(Object)의 조상은 함수(Function)입니다.

function Person() {} // => 함수
var personObject = new Person(); // => 함수로 객체를 생성

personObject 객체는 Person이라는 함수로부터 파생된 객체입니다. 이렇듯 언제나 객체는 함수로부터 시작됩니다. 여러분이 많이 쓰는 일반적인 객체 생성도 예외는 아닙니다.

var obj = {};

얼핏보면 함수랑 전혀 상관없는 코드로 보이지만 위 코드는 사실 다음 코드와 같습니다.

var obj = new Object();

위 코드에서 Object가 자바스크립트에서 기본적으로 제공하는 함수입니다.

Object도 함수다!

Object와 마찬가지로 Function, Array도 모두 함수로 정의되어 있습니다. 이것이 첫 번째 포인트입니다.

그렇다면 이것이 Prototype Object랑 무슨 상관이있느냐? 함수가 정의될 때는 2가지 일이 동시에 이루어집니다.

1.해당 함수에 Constructor(생성자) 자격 부여

Constructor 자격이 부여되면 new를 통해 객체를 만들어 낼 수 있게 됩니다. 이것이 함수만 new 키워드를 사용할 수 있는 이유입니다.



constructor가 아니면 new를 사용할 수 없다!

2.해당 함수의 Prototype Object 생성 및 연결

함수를 정의하면 함수만 생성되는 것이 아니라 Prototype Object도 같이 생성이 됩니다.



함수를 정의하면 이렇게 됩니다

그리고 생성된 함수는 prototype이라는 속성을 통해 Prototype Object에 접근할 수 있습니다. Prototype Object는 일반적인 객체와 같으며 기본적인 속성으로 constructor와 __proto__를 가지고 있습니다.

prototype 속성으로 Prototype Object에 접근

constructor는 Prototype Object와 같이 생성되었던 함수를 가리키고 있습니다.
__proto__는 Prototype Link입니다. 밑에서 자세히 설명합니다.

이제 위에서 kim과 park이 나왔던 예제를 다시 보겠습니다.

function Person() {}
Person.prototype.eyes = 2;
Person.prototype.nose = 1;
var kim  = new Person();
var park = new Person():
console.log(kim.eyes); // => 2
...

이제 왜 Person.prototype을 사용하는지 눈에 보이시나요?

Person.prototype 객체에 eyes와 nose 속성이 추가되었다!

Prototype Object는 일반적인 객체이므로 속성을 마음대로 추가/삭제 할 수 있습니다. kim과 park은 Person 함수를 통해 생성되었으니 Person.prototype을 참조할 수 있게 됩니다.

Prototype Link를 보기 전에 Prototype Object를 어느 정도 이해하시고 보기 바랍니다. 함수가 정의될 때 이루어지는 일들을 이해하는 것이 두 번째 포인트, Prototype Object를 이해하는 것이 세 번째 포인트입니다.


Prototype Link

kim 객체는 eyes가 없는데 ??

kim에는 eyes라는 속성이 없는데도 kim.eyes를 실행하면 2라는 값을 참조하는 것을 볼 수 있습니다. 위에서 설명했듯이 Prototype Object에 존재하는 eyes 속성을 참조한 것인데요, 이게 어떻게 가능한걸까요??

바로 kim이 가지고 있는 딱 하나의 속성 __proto__가 그것을 가능하게 해주는 열쇠입니다.

prototype 속성은 함수만 가지고 있던 것과는 달리(Person.prototype 기억나시죠?) 
__proto__속성은 모든 객체가 빠짐없이 가지고 있는 속성입니다.

__proto__는 객체가 생성될 때 조상이었던 함수의 Prototype Object를 가리킵니다. kim객체는 Person함수로부터 생성되었으니 Person 함수의 Prototype Object를 가리키고 있는 것이죠.

드디어 __proto__를 공개합니다

__proto__를 까보니 역시 Person 함수의 Prototype Object를 가리키고 있었습니다.

객체, 함수, Prototype Object의 관계

kim객체가 eyes를 직접 가지고 있지 않기 때문에 eyes 속성을 찾을 때 까지 상위 프로토타입을 탐색합니다. 최상위인 Object의 Prototype Object까지 도달했는데도 못찾았을 경우 undefined를 리턴합니다. 이렇게 __proto__속성을 통해 상위 프로토타입과 연결되어있는 형태를 프로토타입 체인(Chain)이라고 합니다.




프로토타입 체인, 최상위는 Object

이런 프로토타입 체인 구조 때문에 모든 객체는 Object의 자식이라고 불리고, Object Prototype Object에 있는 모든 속성을 사용할 수 있습니다. 한 가지 예를 들면 toString함수가 있겠습니다.


Object속성인 toString함수를 kim도 사용가능

__proto__와 프로토타입 체인을 이해하는 것이 네 번째 포인트입니다.





ref : https://medium.com/@bluesh55/javascript-prototype-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0-f8e67c286b67




반응형
반응형








this 총정리


this 란?


this 란?

this 는 일반적으로 메서드가 호출한 객체의 속성(프로퍼티)입니다.

this 는 객체를 생성하는 동시에 자동 생성됩니다.


JAVASCRIPT
function MyClass(){
    this.property1 = "value1";
}

MyClass.prototype.method1 = function(){
    console.log(this.property1);
}

var my1 = new MyClass();

my1.method1();





this 가 만들어지는 경우는 일반적으로 다음과 같습니다.

1) 일반 함수에서의 this

2) 중첩 함수에서의 this

3) 이벤트에서의 this

4) 메서드에서의 this

5) 메서드 내부의 중첩함수에서의 this




일반 함수에서 this

일반 함수에서서의 this 는 무조건 window 를 가리킵니다.


JAVASCRIPT
var data = 10;
// window.data == this.data  형식과 동일하다.

function outer(){
    this.data = 20;
    data = 30;

    console.log("1. data1 = " + data); // 30
    console.log("2. this.data = " + this.data); // 30
    console.log("3. window.data = " + window.data); // 30
}

outer();




일반 중첩함수에서 this

중첩 함수에서의 this 는 window 를 가리킵니다.


JAVASCRIPT
var data = 10;

function outer(){
    // 중첩함수
    function inner(){
        this.data = 20;
        data = 30;
        console.log("1. data1 = " + data); // 30
        console.log("2. this.data = " + this.data); // 30
        console.log("3. window.data = " + window.data); // 30
    }
    inner();
}
outer();



이벤트 리스너에서 this


이벤트에서의 this 는 이벤트를 발생한 객체를 가리킵니다.


JAVASCRIPT
리스너실행

var data = 10;
$(document).ready(function(){

    // 이벤트 리스너 등록    
    $("#myButton").click(function(){
        this.data = 20;
        data = 30;

        this.style.color = '#000';
        console.log("1. data1 = " + data); // 30
        console.log("2. this.data = " + this.data); // 20 (이벤트 핸들러의 객체)
        console.log("3. window.data = " + window.data); // 30
    });
});



메서드에서 this

메서드 내에서의 this 는 메서드를 호출한 객체를 가리킵니다.


JAVASCRIPT
var data = 10;

function MyClass(){
    this.data = 20;
    data = 30;

    console.log("1. data1 = " + data);
    console.log("2. this.data = " + this.data);
    console.log("3. window.data = " + window.data);
}

// 인스턴스 생성
var my1 = new MyClass();
// this 는 메소드를 호출한 객체를 나타낸다.
// 여기선 my1 이 인스턴스

// 일반 함수 호출할 경우
MyClass(); // 30, 30, 30



메서드 내부의 중첩함수에서 this


메서드 내부의 중첩함수에서의 this 는 window 를 가리킵니다.

그리고 메서드 내부의 중첩함수에서 this 를 보존하는 방법은 아래와 같습니다.


JAVASCRIPT
var data = 10;

function MyClass(){

    this.data = 50;

    // 내부 함수내에서 this 를 보존하는 방법
    var objThis = this;
    objThis.data = 40;

    function inner(){

        this.data = 20; // window.data 와 같음.
        data = 30; // window.data 와 같음.

        console.log("1. data1 = " + data); // 30;
        console.log("2. this.data = " + this.data); // 30 중첩(내부)함수, 즉 컨텍스트에 따라 this 가 달라진다.
        console.log("3. window.data = " + window.data);
        console.log("4. objThis = " + objThis.data);
    }

    inner();
}

// 인스턴스 생성
var my1 = new MyClass();



총정리 : this 가 만들어지는 일반적인 경우

1) 일반함수에서 this

    • window

2) 중첩함수에서 this

    • window

3) 이벤트에서 this

    • 이벤트를 발생한 객체

4) 메서드에서 this

    • 메소드를 호출한 객체

5) 메서드 내부의 중첩 함수에서 this

    •  window




ref : http://webclub.tistory.com/140?category=501533



반응형
반응형



Javascript의 객체

1) prototype 기반의 언어

java와 c++은 클래스(class) 기반의 객체지향 언어인 반면, javacript는 prototype기반의 객체지향 언어입니다.

class 기반의 객체지향 언어는 class라는 껍데기를 정의하고 이를 통해 객체를 생성합니다.

그러나 prototype 기반의 객체지향 언어는 class 정의 없이도 객체를 생성할 수 있습니다.

( 생성자 함수를 통해 class를 따라할 수 있으며, ES6부터는 Class가 추가되었습니다. )



2) 캡슐화와 상속 지원

객체지향 프로그래밍에서 중요한 특징 중 하나는 캡슐화와 상속이라는 개념입니다.

javascript는 class가 없어도 캡슐화와 상속을 지원합니다.

캡슐화는 클로저를 통해, 캡슐화는 Prototype을 통해 가능합니다.

( 클로저에 대한 내용은 여기를, Prototype에 대한 내용은 여기를 참고해주세요 ! )



3) 프로퍼티와 값의 모임

javascript에서 객체는 { }라는 울타리 안에서 프로퍼티와 값을 쌍으로 하는 집합의 모임입니다.

이제부터 나올 예제들을 보면서 살펴보도록 하겠습니다.





객체 생성 방법

1) 리터럴

가장 일반적인 방법은 중괄호{ } 를 사용하여 객체를 생성하는 방법입니다.

예제

1
2
3
4
5
var person = {
  name: "victolee",
  email: "asdf@example.com",
  birth: "0225"
}

위와 같이 정의한 person은 name, email, brith 프로퍼티를 갖고 있고, 각 프로퍼티는 값을 갖고 있습니다.

객체를 정의할 때는 중괄호{ } 안에 프로퍼티 : 값의 쌍을 입력하고 쉼표(,)로 프로퍼티를 구분합니다.

프로퍼티와 값의 구분은 콜론 ( : ), 프로퍼티&값 쌍의 구분은 쉼표 ( , )로 합니다.

( 프로퍼티와 값의 구분은 =이 아닙니다 ! )



2) Object() 생성자 함수

new 키워드를 이용하여 Object 생성자 함수를 호출하면 빈 객체를 얻을 수 있습니다.

예제

1
2
3
4
5
6
7
8
9
10
11
var person = new Object();
console.log(person.name)
console.log(person.email)
console.log(person.birth)
 
person.name = "victolee";
person.email = "asdf@example.com";
person.birth = "0225";
console.log(person.name)
console.log(person.email)
console.log(person.birth)

new Object()를 호출하면 비어있는 객체를 생성합니다.

따라서 name, email, birth 프로퍼티를 갖고 있지 않습니다.

비어있는 객체는 의미가 없으므로, new Object()로 비어있는 객체를 생성했으면 프로퍼티를 추가해줘야 합니다.



3) 생성자 함수

생성자 함수를 사용하면 java나 c++의 class 개념처럼 껍데기를 만들 수 있습니다.

기존 함수에 new 연산자를 붙여서 호출하면 해당 함수는 생성자 함수로 동작합니다.

그런데 일반적인 함수인지, 객체를 만들기 위한 목적의 생성자 함수인지 구분하기 위해 생성자 함수의 첫 문자는 대문자로 표기하는 것이 관례입니다.

예제

1
2
3
4
5
6
7
8
9
10
11
function Person(name,email){
  this.name = name;
  this.email = email;
  this.walk = "직립 보행"
}
 
var person1 = new Person("victolee""foo@example.com");
var person2 = new Person("worrr""goo@example.com");
 
console.log(person1.name + " " + person1.email + " " + person1.walk);
console.log(person2.name + " " + person2.email + " " + person2.walk);

리터럴과 Obejct()로 객체를 생성하는 것과 달리, 생성자 함수를 통해 객체를 생성하면 같은 속성을 가진 객체를 여러 개 생성할 수 있습니다.

즉 person1과 person2 객체는 name, email, walk 프로퍼티를 갖게 됩니다.


또한 생성자 함수에서 정의한 this는 생성자 함수로 생성된 인스턴스가 됩니다.

사실 생성자 함수로 인스턴스를 생성하기 전에 먼저 비어있는 객체를 생성합니다.

this는 이 비어있는 객체를 가리키고, 그 객체에 name, email, walk 프로퍼티를 추가한 것입니다.

생성자 함수에 반환 값이 없으면 비어있는 객체에 새로운 프로퍼티를 추가한 this가 반환됩니다.


( 조금 더 구체적인 내용을 원하시면 여기를 참고하셔서 Prototype에 대해 이해해보세요 ! )



일반 함수와 생성자 함수의 차이점은 new 연산자를 붙이느냐의 차이입니다.

생성자 함수인데 new를 붙이지 않는다면 오류를 발생하겠죠.

이러한 경우를 대비해서 생성자 함수를 호출할 경우 새로운 객체를 만들도록 분기문을 작성하곤 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
function foo(arg){
  if(!(this instanceof foo)){
    return new foo(arg);
  }
  this.value = arg ? arg : -99;
}
 
let a = new foo(10);
let b = foo(100);
 
console.log(a);
console.log(b);

변수 b의 경우 생성자 함수 foo를 호출하려 했지만 new 연산자를 붙이지 않은 경우입니다.

즉 foo()함수를 호출했으므로 this는 전역 객체가 될 것이고, 전역 객체는 foo의 인스턴스가 아니므로 생성자 함수를 호출하도록 new foo(arg)를 호출하도록 작성한 코드입니다.





객체의 프로퍼티 작성, 호출 방법

이전 예제들을 보시면서 객체의 프로퍼티를 작성하는 방법과 호출하는 방법에 대해 자연스럽게 배우셨습니다.

객체의 프로퍼티를 정의할 때는 중괄호{ }내에서 프로퍼티를 작성하면 되고, 호출하는 방법은 객체.프로퍼티를 하시면 됩니다.


그런데 객체의 프로퍼티를 작성 할 때의 유의사항과 호출할 때 다른 방식이 있어서 말씀드리려고 합니다.

1) 프로퍼티 작성시 유의사항

프로퍼티를 정의할 때 작성한 이름은 자동으로 문자열로 취급됩니다.

1
2
3
4
5
6
7
8
9
var person = {
  "name" "victolee",
  "email" "foo@example.com"
}
 
var person = {
  name : "victolee",
  email : "foo@example.com"
}

위의 두 객체 선언 방식은 완전히 동일합니다.


javascript에서 정의한 예약어를 프로퍼티로 사용할 수 있습니다.

예약어를 굳이 바꾸는 것은 별로 좋은 방식은 아니지만 " "을 이용하여 프로퍼티를 선언한다면 바꿀 수 있습니다.

그래서 저는 주로 " "을 사용하지 않는 편입니다.



2) 객체의 프로퍼티 이름으로 변수를 사용

1
2
3
4
5
6
7
let foo = "name"
var person = {
  [foo] : "victolee",
  "email" "foo@example.com"
}
 
console.log(person.name)

프로퍼티 이름에 대괄호[ ]로 덮어주면 변수로 프로퍼티 이름을 작성할 수 있습니다.



3) 또 다른 프로퍼티 호출 방법

1
2
3
4
5
6
7
var person = {
  name: "victolee"
}
 
console.log(person.name)
console.log(person["name"])   // victolee
console.log(person[name])     // undefined

보시는 바와 같이 대괄호 [ ]를 사용하여 프로퍼티를 호출 할 수 있습니다.

이 때 대괄호안에는 무조건 문자열이여야 합니다.



그런데 가끔 대괄호에 문자열이 아닌 변수를 넣을 수도 있습니다.

1
2
3
4
5
6
7
8
9
var person = {
  foo : "name",
  name: "victolee"
}
 
console.log(person["name"])
 
var nameVar = person.foo
console.log(person[nameVar])

person.foo는 문자열인 name을 반환합니다.

name이라는 문자열은 변수 nameVar에 할당되고, person의 속성을 호출하는데 사용됩니다.

어떤 프로퍼티를 호출해야 할지 동적으로 결정해야 할 때 변수에 할당하기도 하므로 기억하두셔야 할 부분입니다.




이상으로 javascript에서의 객체에 대해 알아보았습니다.




ref : http://victorydntmd.tistory.com/51

ref : https://blog.naver.com/matzip84/221304426187

반응형
반응형

prototype 기본 사용

 

개체 클래스의 프로토타입에 대한 참조를 반환합니다.


1
objectName.prototype

objectName 인수는 개체의 이름입니다.

  1. prototype 속성은 개체 클래스에 기본적인 함수 집합을 제공하기 위해 사용합니다.  

  2. 개체의 새로운 인스턴스는 해당 개체에 할당된 프로토타입의 동작을 "상속"받습니다.  
    새로운 객체를 prototype 에 대입하면 대입된 객체가 부모가 됨

  1. 예를 들어 가장 큰 배열 요소의 값을 반환하는 Array 개체에 메서드를 추가하려면
    함수를 선언하고 이를 Array.prototype에 추가한 후 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function array_max( )
{
    var i, max = this[0]; //이때 this 는 자기자신 배열의 원소들을 가져오기위한 용도로 사용된다
    for (i = 1; i < this.length; i++)
    {
    if (max < this[i])
     max = this[i];
    }
    return max;
}
Array.prototype.max = array_max;
var myArray = new Array(71311259);
document.write(myArray.max());
 
// 결과 :
// 25










JavaScript는 클래스라는 개념이 없습니다. 그래서 기존의 객체를 복사하여(cloning) 새로운 객체를 생성하는 프로토타입 기반의 언어입니다. 프로토타입 기반 언어는 객체 원형인 프로토타입을 이용하여 새로운 객체를 만들어냅니다. 이렇게 생성된 객체 역시 또 다른 객체의 원형이 될 수 있습니다. 프로토타입은 객체를 확장하고 객체 지향적인 프로그래밍을 할 수 있게 해줍니다. 프로토타입은 크게 두 가지로 해석됩니다. 프로토타입 객체를 참조하는 prototype 속성과 객체 멤버인 proto 속성이 참조하는 숨은 링크가 있습니다. 이 둘의 차이점을 이해하기 위해서는 JavaScript 함수와 객체의 내부적인 구조를 이해 해야합니다. 이번 글에서는 JavaScript의 함수와 객체 내부 구조부터 시작하여 프로토타입에 대해 알아보겠습니다.

1. 함수와 객체의 내부 구조

JavaScript에서는 함수를 정의하고, 파싱단계에 들어가면, 내부적으로 수행되는 작업이 있습니다. 함수 멤버로 prototype 속성이 있습니다. 이 속성은 다른 곳에 생성된 함수이름의 프로토타입 객체를 참조합니다. 프로토타입 객체의 멤버인 constructor 속성은 함수를 참조하는 내부구조를 가집니다. 아래의 그림 1과 같이 표현합니다.

tip : 이미지상 오른쪽을로 '프로토타입 객체' 라고 하는데, '프로토타입 객체'라는 두 단어가 각각 다른것을 의미하는 것이 아니고
       두단어가 합쳐져 프로토타입객체 라고 칭하고있음




[그림 1]
function Person(){}  

[소스 1]

속성이 하나도 없는 Person이라는 함수가 정의되고, 파싱단계에 들어가면, Person 함수 Prototype 속성은 프로토타입 객체를 참조합니다. 프로토타입 객체 멤버인 constructor 속성은 Person 함수를 참조하는 구조를 가집니다. 여기서 알아야 하는 부분은 Person 함수의 prototype 속성이 참조하는 프로토타입 객체는 new라는 연산자와 Person 함수를 통해 생성된 모든 객체의 원형이 되는 객체입니다. 생성된 모든 객체가 참조한다는 것을 기억해야 합니다. 아래의 그림 2와 같이 표현합니다.

[그림 2]
function Person(){}

var joon = new Person();  
var jisoo = new Person();  

[소스 2]

JavaScript에서는 기본 데이터 타입인 boolean, number, string, 그리고 특별한 값인 null, undefined 빼고는 모두 객체입니다. 사용자가 정의한 함수도 객체이고, new라는 연산자를 통해 생성된 것도 객체입니다. 객체 안에는 proto(비표준) 속성이 있습니다. 이 속성은 객체가 만들어지기 위해 사용된 원형인 프로토타입 객체를 숨은 링크로 참조하는 역할을 합니다.


2. 프로토타입 객체란?

함수를 정의하면 다른 곳에 생성되는 프로토타입 객체는 자신이 다른 객체의 원형이 되는 객체입니다. 모든 객체는 프로토타입 객체에 접근할 수 있습니다. 프로토타입 객체도 동적으로 런타임에 멤버를 추가할 수 있습니다. 같은 원형을 복사로 생성된 모든 객체는 추가된 멤버를 사용할 수 있습니다.


[그림 3]
function Person(){}

var joon = new Person();  
var jisoo = new Person();

Person.prototype.getType = function (){  
    return "인간"; 
};

console.log(joon.getType());   // 인간  
console.log(jisoo.getType());  // 인간  

[소스 3]

위 소스 3 6라인은 함수 안의 prototype 속성을 이용하여 멤버를 추가하였습니다. 프로토타입 객체에 getType()이라는 함수를 추가하면 멤버를 추가하기 전에 생성된 객체에서도 추가된 멤버를 사용할 수 있습니다. 같은 프로토타입을 이용하여 생성된 joon과 jisoo 객체는 getType()을 사용할 수 있습니다.

여기서 알아두어야 할 것은 프로토타입 객체에 멤버를 추가, 수정, 삭제할 때는 함수 안의 prototype 속성을 사용해야 합니다. 하지만 프로토타입 멤버를 읽을 때는 함수 안의 prototype 속성 또는 객체 이름으로 접근합니다.

[그림 4]
joon.getType = function (){  
    return "사람"; 
};

console.log(joon.getType());   // 사람  
console.log(jisoo.getType());  // 인간

jisoo.age = 25;

console.log(joon.age);   // undefined  
console.log(jisoo.age);  // 25  

[소스 4]

위 소스 4 1라인은 joon 객체를 이용하여 getType() 리턴 값을 사람으로 수정하였습니다. 그리고 joon과 jisoo에서 각각 getType()을 호출하면 joon 객체를 이용하여 호출한 결과는 사람으로 출력되고, jisoo로 호출한 결과는 인간이 출력됩니다. joon 객체를 사용하여 getType()을 호출하면 프로토타입 객체의 getType()을 호출한 것이 아닙니다. joon 객체에 추가된 getType()을 호출한 것입니다. 프로토타입 객체의 멤버를 수정할 경우는 멤버 추가와 같이 함수의 prototype 속성을 이용하여 수정합니다.

[그림 5]
Person.prototype.getType = function (){  
    return "사람"; 
};

console.log(jisoo.getType());  // 사람  

[소스 5]

소스 5를 보게 되면 함수의 prototype 속성을 이용하여 getType() 리턴 값을 사람으로 수정합니다. 그리고 jisoo 객체를 이용하여 호출한 결과 사람이 나옵니다.

결론을 내리면, 프로토타입 객체는 새로운 객체가 생성되기 위한 원형이 되는 객체입니다. 같은 원형으로 생성된 객체가 공통으로 참조하는 공간입니다. 프로토타입 객체의 멤버를 읽는 경우에는 객체 또는 함수의 prototype 속성을 통해 접근할 수 있습니다. 하지만 추가, 수정, 삭제는 함수의 prototype 속성을 통해 접근해야 합니다.





3. 프로토타입이란?

JavaScript에서 기본 데이터 타입을 제외한 모든 것이 객체입니다. 객체가 만들어지기 위해서는 자신을 만드는 데 사용된 원형인 프로토타입 객체를 이용하여 객체를 만듭니다. 이때 만들어진 객체 안에 __proto__ (비표준) 속성이 자신을 만들어낸 원형을 의미하는 프로토타입 객체를 참조하는 숨겨진 링크가 있습니다. 이 숨겨진 링크를 프로토타입이라고 정의합니다.

[그림 6]
function Person(){}

var joon = new Person();  

[소스 6]

위 그림 6 joon 객체의 멤버인 __proto__ (비표준) 속성이 프로토타입 객체를 가리키는 숨은 링크가 프로토타입이라고 합니다. 프로토타입을 크게 두 가지로 해석된다 했습니다. 

함수의 멤버인 prototype 속성은 프로토타입 객체를 참조하는 속성입니다. 그리고 함수와 new 연산자가 만나 생성한 객체의 프로토타입 객체를 지정해주는 역할을 합니다. 객체 안의 __proto__(비표준) 속성은 자신을 만들어낸 원형인 프로토타입 객체를 참조하는 숨겨진 링크로써 프로토타입을 의미합니다.

JavaScript에서는 숨겨진 링크가 있어 프로토타입 객체 멤버에 접근할 수 있습니다. 그래서 이 프로토타입 링크를 사용자가 정의한 객체에 링크가 참조되도록 설정하면 코드의 재사용과 객체 지향적인 프로그래밍을 할 수 있습니다.




4. 코드의 재사용

코드의 재사용 하면 떠오르는 단어는 바로 상속입니다. 클래스라는 개념이 있는 Java에서는 중복된 코드를 상속받아 코드 재활용을 할 수 있습니다. 하지만 JavaScript에서는 클래스가 없는, 프로토타입 기반 언어입니다. 그래서 프로토타입을 이용하여 코드 재사용을 할 수 있습니다.

이 방법에도 크게 두 가지로 분류할 수 있습니다. classical 방식과 prototypal 방식이 있습니다. classical 방식은 new 연산자를 통해 생성한 객체를 사용하여 코드를 재사용 하는 방법입니다. 마치 Java에서 객체를 생성하는 방법과 유사하여 classical 방식이라고 합니다. prototypal 방식은 리터럴 또는 Object.create()를 이용하여 객체를 생성하고 확장해 가는 방식입니다. 두 가지 방법 중 JavaScript에서는 prototypal 방식을 더 선호합니다. 그 이유는 classical 방식보다 간결하게 구현할 수 있기 때문입니다. 밑의 예제 1 ~ 4번까지는 classical 방식의 코드 재사용 방법이고, 5번은 prototypal 방식인 Object.create()를 사용하여 코드의 재사용을 보여줍니다.

(1) 기본 방법

부모에 해당하는 함수를 이용하여 객체를 생성합니다. 자식에 해당하는 함수의 prototype 속성을 부모 함수를 이용하여 생성한 객체를 참조하는 방법입니다.


[그림 7]
function Person(name) {  
    this.name = name || "혁준"; 
}

Person.prototype.getName = function(){  
    return this.name;
};

function Korean(name){}  
Korean.prototype = new Person();

var kor1 = new Korean();  
console.log(kor1.getName());  // 혁준

var kor2 = new Korean("지수");  
console.log(kor2.getName());  // 혁준  

[소스 7]

위 소스 7을 보면 부모에 해당하는 함수는 Person입니다. 10라인에서 자식 함수인 Korean 함수 안의 prototype 속성을 부모 함수로 생성된 객체로 바꿨습니다. 이제 Korean 함수와 new 연산자를 이용하여 생성된 kor 객체의 __proto__속성이 부모 함수를 이용하여 생성된 객체를 참조합니다. 이 객체가 Korean 함수를 이용하여 생성된 모든 객체의 프로토타입 객체가 됩니다. kor1에는 name과 getName() 이라는 속성이 없지만, 부모에 해당하는 프로토타입객체에 name이 있습니다. 이 프로토타입객체의 부모에 getName()을 가지고 있어 kor1에서 사용할 수 있습니다. 이 방법에도 단점이 있습니다. 부모 객체의 속성과 부모 객체의 프로토타입 속성을 모두 물려받게 됩니다. 대부분의 경우 객체 자신의 속성은 특정 인스턴스에 한정되어 재사용할 수 없어 필요가 없습니다. 또한, 자식 객체를 생성할 때 인자를 넘겨도 부모 객체를 생성할 때 인자를 넘겨주지 못합니다. 그림 7 소스 하단 두 번째 줄에서 kor2 객체를 생성할 때 Korean 함수의 인자로 지수라고 주었습니다. 객체를 생성한 후 getName()을 호출하면 지수라고 출력될 거 같지만, 부모 생성자에 인자를 넘겨주지 않았기 때문에 name에는 default 값인 혁준이 들어있습니다. 객체를 생성할 때마다 부모의 함수를 호출할 수도 있습니다. 하지만 매우 비효율적입니다. 그래서 다음 방법은 이 방법의 문제점을 해결하는 방법을 알아보겠습니다.

(2) 생성자 빌려 쓰기

이 방법은 기본 방법의 문제점인 자식 함수에서 받은 인자를 부모 함수로 인자를 전달하지 못했던 부분을 해결합니다. 부모 함수의 this에 자식 객체를 바인딩하는 방식입니다.

[그림 8]
function Person(name) {  
    this.name = name || "혁준";
}

Person.prototype.getName = function(){  
    return this.name;
};

function Korean(name){  
    Person.apply(this, arguments);
}

var kor1 = new Korean("지수");  
console.log(kor1.name);  // 지수  

[소스 8]

위 소스 8 10라인을 보면 Korean 함수 내부에서 apply 함수를 이용합니다. 

부모객체인 Person 함수 영역의 this를 Korean 함수 안의 this로 바인딩합니다. 
함수를 호출하여 지정된 개체를 함수의 this 값으로 대체하고 지정된 배열을 함수의 인수로 대체합니다.

객체를 생성하고, name을 출력합니다. 객체를 생성할 때 넘겨준 인자를 출력하는 것을 볼 수 있습니다. 기본 방법에서는 부모객체의 멤버를 참조를 통해 물려받았습니다. 하지만 생성자 빌려 쓰기는 부모객체 멤버를 복사하여 자신의 것으로 만들어 버린다는 차이점이 있습니다. 하지만 이 방법은 부모객체의 this로 된 멤버들만 물려받게 되는 단점이 있습니다. 그래서 부모객체의 프로토타입 객체의 멤버들을 물려받지 못합니다. 위 그림 8 그림을 보시면 kor1 객체에서 부모객체의 프로토타입 객체에 대한 링크가 없다는 것을 볼 수 있습니다.


(3) 생성자 빌려 쓰고 프로토타입 지정해주기

이 방법은 방법 1과 방법 2 문제점들을 보완하면서 Java에서 예상할 수 있는 동작 방식과 유사합니다.

[그림 9]
function Person(name) {  
    this.name = name || "혁준"; }

Person.prototype.getName = function(){  
    return this.name;
};

function Korean(name){  
    Person.apply(this, arguments);
}
Korean.prototype = new Person();

var kor1 = new Korean("지수");  
console.log(kor1.getName());  // 지수  

[소스 9]

위 소스 9 9라인에서 부모 함수 this를 자식 함수 this로 바인딩합니다. 11라인에서 자식 함수 prototype 속성을 부모 함수를 사용하여 생성된 객체로 지정했습니다. 부모객체 속성에 대한 참조를 가지는 것이 아닌 복사본을 통해 내 것으로 만듭니다. 동시에 부모객체의 프로토타입 객체에 대한 링크도 참조됩니다. 부모객체의 프로토타입 객체 멤버도 사용할 수 있습니다. 그림 7과 비교했을 때 kor1 객체에 name 멤버가 없는 반면 그림 9에서는 name 멤버를 가지고 있는 것을 확인할 수 있습니다. 그림 8과 비교했을 때는 프로토타입 링크가 부모 함수로 생성한 객체에 대해 참조도 하고 있습니다. 그리고 부모 객체의 프로토타입 객체도 링크로 연결된 것을 볼 수 있습니다. 이 방법에도 문제점이 있습니다. 부모 생성자를 두 번 호출합니다. 생성자 빌려 쓰기 방법과 달리 getName()은 제대로 상속되었지만, name에 대해서는 kor1 객체와 부모 함수를 이용하여 생성한 객체에도 있는 것을 볼 수 있습니다.

(4) 프로토타입공유

이번 방법은 부모 생성자를 한 번도 호출하지 않으면서 프로토타입 객체를 공유하는 방법입니다.

[그림 10]
function Person(name) {  
    this.name = name || "혁준";
}

Person.prototype.getName = function(){  
    return this.name;
};

function Korean(name){  
    this.name = name;
}    
Korean.prototype = Person.prototype;

var kor1 = new Korean("지수");  
console.log(kor1.getName());  // 지수  

[소스 10]

위 소스 10 12라인에서 자식 함수의 prototype 속성을 부모 함수의 prototype 속성이 참조하는 객체로 설정했습니다. 자식 함수를 통해 생성된 객체는 부모 함수를 통해 생성된 객체를 거치지 않고 부모 함수의 프로토타입 객체를 부모로 지정하여 객체를 생성합니다. 부모 함수의 내용을 상속받지 못하므로 상속받으려는 부분을 부모 함수의 프로토타입 객체에 작성해야 사용자가 원하는 결과를 얻게 됩니다. 그림 9와 비교했을 때 중간에 부모 함수로 생성한 객체가 없고 부모 함수의 프로토타입 객체로 링크가 참조되는 것을 볼 수 있습니다.

(5) prototypal한 방식의 재사용

이 방법은 Object.create()를 사용하여 객체를 생성과 동시에 프로토타입객체를 지정합니다. 이 함수는 첫 번째 매개변수는 부모객체로 사용할 객체를 넘겨주고, 두 번째 매개변수는 선택적 매개변수로써 반환되는 자식객체의 속성에 추가되는 부분입니다. 이 함수를 사용함으로 써 객체 생성과 동시에 부모객체를 지정하여 코드의 재활용을 간단하게 구현할 수 있습니다.

var person = {  
    type : "인간",
    getType : function(){
        return this.type;
    },
    getName : function(){
        return this.name;
    }
};

var joon = Object.create(person);  
joon.name = "혁준";

console.log(joon.getType());  // 인간  
console.log(joon.getName());  // 혁준  

[소스 11]

위 소스 1라인에서 부모 객체에 해당하는 person을 객체 리터럴 방식으로 생성했습니다. 그리고 11라인에서 자식 객체 joon은 Object.create() 함수를 이용하여 첫 번째 매개변수로 person을 넘겨받아 joon 객체를 생성하였습니다. 한 줄로 객체를 생성함과 동시에 부모객체의 속성도 모두 물려받았습니다. 위의 1 ~ 4번에 해당하는 classical 방식보다 간단하면서 여러 가지 상황을 생각할 필요도 없습니다. JavaScript에서는 new 연산자와 함수를 통해 생성한 객체를 사용하는 classical 방식보다 prototypal 방식을 더 선호합니다.





자바스크립트의 프로토타입(Prototype) 프로퍼티 란?

모든 함수 객체의 Constructor는 prototype 이란 프로퍼티를 가지고 있다. 이 prototype 프로퍼티는 객체가 생성될 당시 만들어지는 객체 자신의 원형이될 prototype 객체를 가리킨다. 즉 자신을 만든 원형이 아닌 자신을 통해 만들어질 객체들이 원형으로 사용할 객체를 말한다. prototype object는 default로 empty Object 를 가리킨다.

이 말이 매우 어렵게 들릴수도 있다. 하지만 정확히 이해한다면 그리 어려운 말이 아니다. 위에서 분명히 프로토타입은 자기 자신을 생성하게 한 자신의 원형 객체라고 정의했다. 그럼 그 원형객체란 프로토타입은 function A() 함수객체 그 차체일가? 전혀 그렇지 않다.

자바스크립트의 모든 객체는 생성과 동시에 자기자신이 생성될 당시의 정보를 취한 Prototype Object 라는 새로운 객체를 Cloning 하여 만들어낸다. 프로토타입이 객체를 만들어내기위한 원형이라면 이 Prototype Object 는 자기 자신의 분신이며 자신을 원형으로 만들어질 다른 객체가 참조할 프로토타입이 된다. 즉 객체 자신을 이용할 다른 객체들이 프로토타입으로 사용할 객체가 Prototype Object 인 것이다. 즉 위에서 언급한 __proto__라는 prototype 에 대한 link는 상위에서 물려받은 객체의 프로토타입에 대한 정보이며 prototype 프로퍼티는 자신을 원형으로 만들어질 새로운 객체들 즉 하위로 물려줄 연결에 대한 속성이다.

function foo() {}
var foo = new foo();

위 예제코드를 통해 만들어지는 Prototype Link 와 Prototype Property가 가리키는 Prototype Object의 Real Link 에 대한 관계도는 다음과 같다. 관계도1


1
2
3
4
5
6
7
8
9
10
11
12
//#예제 2.
var A = function () { };
A.x=function() {
    console.log('hello');
};
A.prototype.x = function () {
     console.log('world');
};
var B = new A();
var C = new A();
B.x(); // 'world'
C.x(); // 'world'




예제2 에서의 결과가 world 가 되는 이유도 같은 이유다. A.prototype 은 A의 Prototype Object를 참조하는 녀석이기 때문에 A.prototype.x 를 정의한다는 것은 A의 Prototype Object를 직접 이용하게 되는 것이고 그에 따라서 A의 Prototype Object를 프로토타입으로 이용하여 만들어지는 B,C 가 알고 있는 x 는 function () {console.log('world');} 가 되는 것이다.

예제2의 객체 상속 모델을 그림으로 표현하면 아래와 같이 나온다. 




ref : http://www.nextree.co.kr/p7323/

ref : http://insanehong.kr/post/javascript-prototype/

반응형
반응형
객체 생성

객체는 new Object()Object.create() 또는 literal 표기법 (initializer 표기법)을 사용하여 초기화될 수 있습니다. 객체 초기자(object initializer)는 0개 이상인 객체 속성명 및 관련값 쌍 목록입니다, 중괄호({})로 묶인.


1
2
3
4
5
6
7
8
9
10
11
var o = {};
var o = { a: "foo", b: 42, c: {} };
 
var a = "foo", b = 42, c = {};
var o = { a: a, b: b, c: c };
 
var o = {
  property: function ([parameters]) {},
  get property() {},
  set property(value) {},
};

  






설명

객체 초기자는 Object의 초기화를 기술하는 식(expression)입니다. 객체는 속성으로 구성됩니다, 객체를 설명하는 데 사용되는. 객체의 속성값은 primitive 데이터 형 또는 다른 객체를 포함할 수 있습니다.

객체 생성

속성이 없는 빈 객체는 다음과 같이 만들 수 있습니다:

var object = {};

리터럴(literal) 또는 초기자(initializer) 표기법의 이점은, 빠르게 중괄호 내 속성이 있는 객체를 만들 수 있다는 것입니다. 당신은 그저 쉼표로 구분된 키: 값 쌍 목록을 표기합니다. 

다음 코드는 키가 "foo", "age" 및 "baz"인 세 속성이 있는 객체를 만듭니다. 이들 키값은 문자열 "bar", 숫자 42 그리고 세 번째 속성은 그 값으로 다른 객체를 갖습니다.

var object = {
  foo: "bar",
  age: 42,
  baz: { myProp: 12 },
}


javascript : 함수와 객체, new



1
2
3
4
5
6
var n = new Number();
 
var s = 3;
console.log(typeof n); //대문자 Number 는 정수를 담는 객체, typeof 로 확인 되는 Number 타입 문자열은 object
console.log(typeof s); //변수가 숫자로 정의 될때의 타입은 소문자 문자열로 "number"
 


결과


object

number




함수가 생성자가 되는 시점


1
2
3
4
5
6
function Person() {         //Person 함수 이지만 
 
};
 
var p = new Person();       //new 함수명을 쓰게 되면 이때 Person 은 생성자가된다
console.log(p);             //즉 객체를 만든것

//즉 Person 에 인자를 추가 하게 되면 객체 생성시 생성자 인수를 넘길 수 있는 형태가됨
//단 생성자를 통한 초기화가 이뤄짐으로 멤버 변수에 인자 값을 대입하려면
//this.멤버변수 = 매개변수; 의 형태인 this. 이 들어가야한다



일반 함수 앞에 new 를 붙여 생성하면 이 함수가 생성자가 된다

즉 객체를 생성한다는 것이 javascript 에서 custom 형 객체를 생성하는 법인데


javascript 에선 함수 자체에 this 가 들어가 있다는 것 자체가 원래 함수라기 보단 객체라는 점을 내포하고 있다라고 볼 수 있는 부분이기 대문에

함수가 생성자로 취급 되는 대목이 그렇게 생소하게 와닿지는 않는 부분이긴하다








전역객체와 객체지향 프로그래밍







전역 this 는 전역객체 window 와 동일한 객체를 말한다


즉 this.funcThis === window.funcThis 는 true 를 리턴한다




Func() 가 생성자로 호출 될때 funcThis 변수는 o2 에 대한 this 를 담는 것임으로 

funcThis === o2 의 결과는 true 가 된다






그러나 아래의 결과는 false 를 보이는데 전역 왜냐하면 this 와 o2에 대한 this===funcThis 는 서로 다른객체임으로

funcThis === this;

false


이런 결과가 나타남




p.s javascript 가 추구하는 방향을 이해하지 못하는것은 아니지만 그러나 항상 javascript 는 생각 이상으로 이것저것 모아놓은 느낌을 들게한다





일부 ref : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Object_initializer

일부 ref : https://opentutorials.org/module/532/6571

반응형
반응형

Apply 함수


javascript 에서 함수는 일종의 객체인데 이때 apply 라는 내장함수를 포함하고 있어서 apply 함수를 사용 할 수 있음

함수를 apply  로 호출할때 첫번째 인자를 넘겨주게 되는데 이때 넘겨주는 인자는 함수 내에서 this 로 사용되어진다





apply 활용

1
2
3
4
5
6
7
8
9
10
11
12
function sum() {
    var s = 0;
    for (elem in this) { //this 에는 o1 의 내용이 들어 있게 되어 elem 에 하나씩 넘어가게 된다
 
    }
    
    console.log('1');
};
 
o1 = { v1: 10, v2: 20, v3: 30 };
sum.apply(o1); //javascript 에서는 일반 함수에 .apply 가 지원됨
 

 







좀 더 구체적인 예


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
function callMe(arg1, arg2) {
 
    var s = "";
    for (var elem in this) {
        s += this[elem] + ", " ;
    }
 
 
    s += "\n-------------\n";
 
    s += "\nthis value: " + this;
    s += "\n\n";
    s += "\n[[[arguments]]] : \n";
    for (i in callMe.arguments) {
        s += "arguments: " + callMe.arguments[i];
        s += "\n";
    }
 
    return s;
}
 
 
console.log("********************************\n");
 
console.log("Original function: ");
console.log(callMe(12));
 
console.log("\n======================== 1 =============================\n");
 
console.log("Function called with apply: \n");
 
//첫번째 인자는 this 로 사용될 개체 인자
//두번째는 인자는 함수로 넘어갈 인자 집합
 
console.log(callMe.apply(3, [45]));           //정수를 this 로 넘겨주는 형태임으로 for문에서 가져올게 없음
 
 
console.log("\n======================== 2 =============================\n");
 
console.log("객체를 this 로 넘긴다 보면: \n");
var arr = { v1: 10, v2: 20, v3: 30 };
console.log(callMe.apply(arr));
 

  





결과



위 실행에 대한 결과

첫번째 함수에 대한 호출이 callMe안의 this 가 원래의 this 에 관한 내용이 나옴으로 소스코드 형태로 출력결과를 스샷이 아닌 scripter 로 대신함


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
위 소스코드에 대한 결과
 
Debugger listening on ws://127.0.0.1:22561/46f40b18-616e-4019-a9b5-ee13065adf13
For help see https://nodejs.org/en/docs/inspector
Debugger attached.
(node:10212) [DEP0062] DeprecationWarning: `node --inspect --debug-brk` is deprecated. Please use `node --inspect-brk` instead.
 
 
 
********************************
 
줄이 길어서 간격 조절을 함
//함수 자체 this 에 대한 내용
Original function:
[object Object], function () { [native code] }, function () { [native code] }, 
function () { [native code] }, 
function () { [native code] }, function () { [native code] }, function () { [native code] }, 
function () { [native code] }, function () { [native code] }, function () { [native code] }, 
function () { [native code] }, function () { [native code] }, function () { [native code] }, 
              [object global], [object process], 
 
function Buffer(arg, encodingOrOffset, length
{
  doFlaggedDeprecation();
  // Common case.
  if (typeof arg === 'number') {
    if (typeof encodingOrOffset === 'string') {
      throw new Error(
        'If encoding is specified then the first argument must be a string'
      );
    }
  ....
  ....
  ....
},
-------------
 
this value: [object global]
 
 
[[[arguments]]] :
arguments: 1
arguments: 2
 
 
======================== 1 =============================
 
Function called with apply:
 
 
-------------
 
this value: 3
 
 
[[[arguments]]] :
arguments: 4
arguments: 5
 
 
======================== 2 =============================
 
객체를 this 로 넘긴다 보면:
 
102030,
-------------
 
this value: [object Object]
 
 
[[[arguments]]] :
 
Waiting for the debugger to disconnect...
 
 
 
 






apply 메서드(Function)

 

함수를 호출하여 지정된 개체를 함수의 this 값으로 대체하고 지정된 배열을 함수의 인수로 대체합니다.

apply([thisObj[,argArray]])

thisObj

선택 사항입니다. this 개체로 사용될 개체입니다.

argArray

선택 사항입니다.함수에 전달될 인수 집합입니다.

argArray가 올바른 개체가 아니면 "개체가 필요합니다." 오류가 발생합니다.

argArray나 thisObj가 제공되지 않으면 원래 this 개체가 thisObj로 사용되고 인수는 전달되지 않습니다.

다음 코드에서는 apply 메서드를 사용하는 방법을 보여 줍니다

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
function callMe(arg1, arg2){
    var s = "";
 
    s += "this value: " + this;
    s += "<br />";
    for (i in callMe.arguments) {
        s += "arguments: " + callMe.arguments[i];
        s += "<br />";
    }
    return s;
}
 
document.write("Original function: <br/>");
document.write(callMe(12));
document.write("<br/>");
 
document.write("Function called with apply: <br/>");
document.write(callMe.apply(3, [ 45 ]));
 
// Output: 
// Original function: 
// this value: [object Window]
// arguments: 1
// arguments: 2
 
// Function called with apply: 
// this value: 3
// arguments: 4
// arguments: 5









반응형
반응형


   함수명.length : 함수의 매개변수의 개수


   함수명.argument.length == argument.length : 호출하는 쪽의 인자 개수

 






arguments 개체


설명

현재 실행 중인 함수 및 이 함수를 호출한 함수에 대한 인수를 나타내는 개체입니다.

함수

선택 사항입니다.현재 실행 중인 Function 개체의 이름입니다.

n

필수 요소. Function 개체에 전달되는 인수 값에 대한 인덱스(0부터 시작)입니다.

arguments 개체는 명시적으로 만들 수 없습니다. arguments 개체는 함수가 실행을 시작할 때만 사용할 수 있습니다.함수의 arguments 개체는 배열이 아니지만 배열 요소에 액세스하는 방식과 동일하게 각각의 인수에 액세스할 수 있습니다. n 인덱스는 실제로 arguments 개체의 0n 속성 중 하나에 대한 참조입니다.

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
function ArgTest(a, b) {
    var s = "";
 
    s += "Expected Arguments: " + ArgTest.length; // 2 : 함수 인자 개수
    s += "\n";
 
    s += "Expected Function.arguments: " + ArgTest.arguments.length; //4 : 총 매개변수 개수
    s += "\n";
 
    s += "Passed Arguments: " + arguments.length; //4 : 총 매개변수 개수
    s += "\n";
 
    s += "The individual arguments are: "
    for (n = 0; n < arguments.length; n++) {
        s += ArgTest.arguments[n];
        s += " ";
    }
 
    console.log(s);
}
 
ArgTest(12"hello"new Date())
 



[출력 결과]

Expected Arguments: 2

Expected Function.arguments: 4

Passed Arguments: 4

The individual arguments are: 1 2 hello Fri Apr 20 2018 11:23:06 GMT+0900 (대한민국 표준시)





매개변수는 많지만 인자가 적은 경우



 만약 인자의 개수가 1개 즉 ArgTest(1); 로 호출하는 경우  b, c에는 undefined 이 되어 인자의 개수들은

 arguments.length == 1

 ArgTest.arguments.length == 1


즉 함수의 매개변수를 2개 선언했다 할지라도 undefined 은 개수에서 제외 되는 개수를 보이지만


ArgTest.length == 3 의 경우엔 매개변수 개수를 그대로 나타내고 있음을 알 수 있다










반응형
반응형



javascript : 클로저와 private 멤버 유사 처리


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
  
function adds(callback) {
 
 
 
    var ccount = 0;
 
    var state = function() {        //.js 에서는 지역 변수가 fuction 안에 들어 있게 되면 ccount 변수 또한 함께 남게 된다
 
        console.log('=====    ' + ccount);
 
        ccount += 1;
 
    }
 

    state();
 
    callback();
 
    return state;

}
 
 
 
 
 
//rrr 로 받게 되면 인스턴스처럼 메모리에 남게됨
 
var rrr = adds(function () { console.log('inner '); })              //실행 후  ccount = 1 , log 로 찍힌 것은 0
 
 
 
console.log(rrr());              //실행 후  ccount = 2 , log 로 찍힌 것은 1
 
console.log(rrr());              //실행 후  ccount = 3 , log 로 찍힌 것은 2
 
console.log(adds(function () { console.log('inner '); }));
 
console.log(adds(function () { console.log('inner '); }));
 
 
cs



[결과 화면] 


Debugger listening on ws://127.0.0.1:48101/07b14ac8-e1ae-42c5-b370-74ffca833f4d

For help see https://nodejs.org/en/docs/inspector

Debugger attached.

(node:5908) [DEP0062] DeprecationWarning: `node --inspect --debug-brk` is deprecated. Please use `node --inspect-brk` instead.



=====    0

inner

=====    1

undefined

=====    2

undefined

=====    0

inner

[Function: state]

=====    0

inner



[Function: state]

Waiting for the debugger to disconnect...





....  문법이 편리? 하긴 하지만 지역 변수 및 스택 메모리 구조를 이해하고 좀더 OS 적인 체계의 생각이 있는 사람들에게는

지역 변수의 메모리 영역들에서 문법으로 인하여 객체화 된다는 것이 뭔가 억지스러움이 묻어있다고 느낄 수 있는 부분일 수도 있다







클로저의 활용


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
 
//객체를 리턴 하는데 인자로 받은 title 이 캡쳐 됨으로 클로저를 통하여 
//title 은 ghost, matrix 변수에서 각각 독립적으로 남게된다
 
function factory_movie(title) {
    return {
        get_title: function () {
            return title;
        },
        set_title: function (_title) {
            title = _title
        }
    }
}
ghost = factory_movie('first ghost');
matrix = factory_movie('second maxtirx');
 
console.log(ghost.get_title);
console.log(ghost.get_title());
 
console.log(ghost.get_title());
console.log(matrix.get_title());
 
ghost.set_title('ovvvvvvvv');
 
console.log(ghost.get_title());
console.log(matrix.get_title());
 
console.log('\n===========\n');
 
 
//즉시 실행함수 예 
var ssss;
(ssss=function(dd) {
    console.log(dd);
})(10);
 
//즉시 실행함수의 다른 응용 예
ssss(30);
 
console.log('\n===========\n');
 
 
//클로저를 활용한 지역 값 저장 예
var arr = [];
for (var i = 0; i < 5; i++)
{
    arr[i] = function (id) {
                    return function () { return id; }
             }(i);
}
 
for (var index in arr) {
    console.log(arr[index]());
}




결과화면





ghost.get_title() 를 호출하게 되면  ghost private 멤버변수 값인(정확히는 클로저에 묶인 객체안의 함수내의 title 변수)
'first ghost' 을 리턴 받을 수 있게된다






반응형

+ Recent posts