반응형

자바스크립트 실행 환경

모든 브라우저와 Node.js에서 ECMAScript를 실행할 수 있다.
하지만 ECMAScript 외의 기능들은 호환되지 않는다.

예) 브라우저에서 활용하는 자바스크립트 DOM API

// ECMAScript 코드
const button = document.getElementById('myButton'); // DOM API 사용

// ECMAScript 함수 정의
button.addEventListener('click', () => { // DOM API 사용
  alert('Button clicked!'); // ECMAScript 내장 함수 사용
});

Node.js가 DOM API를 활용하지 않는 이유는 Node.js역할과 관련있다.
Node.js는 브라우저 외부 환경에서 동작하는 백단의 구현을 위한 실행 환경 이기에 Core 언어로 제공하지 않으며, 필요한 경우 라이브러리를 사용해서 HTML문서를 가공할 수 있다.

백단에서 사용하는 우리가 아는 언어들은 모두 파일 시스템을 기본적으로 제공하는 것처럼 Node.js환경에서는 이를 동일하게 제공한다(Javascript범주). 하지만, 브라우저에서는 이런 파일 시스템을 기본 제공하지 않는다.(FileReader로 읽기동작은 가능)

제공하지 않는 이유 : 브라우저에서 동작하는 자바스크립트가 사용자의 파일을 임의로 다룰 수 있다면 사용자의 파일을 보안 공격으로 쉽게 공격할 수 있음

위에서 이야기한 내용을 정리하면

  • 브라우저
    • ECMAScript
    • DOM
    • BOM
    • Canvas
    • XMLHttpRequest
    • fetch
    • ….
  • Node.js
    • ECMAScript
    • 클라이언트 사이드 API(DOM …)를 미지원


Node.js와 NPM

  • Node.js : 크롬 V8 자바스크립트 엔진으로 빌드된 자바스크립트 런타임 환경
    ⇒ 서버 사이드 개발이 가능
  • NPM(Node Package Manager) : 자바스크립트 패키지 매니저로 Node.js에서 사용할 수 있는 모듈을 패키지화해서 모아둔 저장소 역할, 패키지 설치 및 관리를 위한 CLI를 제공
반응형
반응형

자바스크립트의 표준화

자바스크립트가 나온 초창기에는 자사 브라우저의 시장 점유율을 높이기 위해 자사 브라우저에만 동작하는 기능들을 추가했는데, 이로 인해 브라우저에 따라 웹페이지가 정상적으로 동작하지 않는 크로스 브라우징 이슈가 발생

이로 인해 비영리 표준화 기구인 ECMA 인터네셔널에서 자바스크립트 표준화 진행되어, 표준화된 자바스크립트 버전인 ECMAScript의 최초버전인 ES1이 등장

이후 ES2 버전에서는 ISO/IEC 16262 국제 표준과 동일한 규격을 적용하며 계속 버전업 되어왔음

자바스크립트 버전별 변천사

  • ES3(1999) → 정규 표현식, try catch
  • ES5(2009) → HTML5와 함께 등장 JSON, 배열조작(forEach, map 등), map, set
  • ES6(2015) → let/const, 클래스, 화살표 함수, 프로미스
  • ES7(2016) → prototype, **연산자
  • ES8(2017) → async/await, Object 정적 메서드
  • ES9(2018) → for await …of, async generator
  • ES10(2019) → Object.fromEntries, Array.prototype.flatMap
  • ES11(2020) → String. prototype. matchAll, BigInt, 옵셔널 체이닝 연산자 등

자바스크립트 성장의 역사

첫 자바스크립트는 웹 페이지의 보조기능을 수행하는 용도였고, 대부분의 로직이 웹 서버에서 실행되며 서버측에서 렌더링(Server Side Rendering)이 진행됨

Ajax

비동기(Asynchronous )방식으로 데이터를 교환할 수 있는 Ajax가 등장하며 렌더링을 Client측에서 직접 렌더링하기 시작했다

서버 측에서의 렌더링은 매번 데이터를 변경할 때마다 새로 페이지를 불러와 렌더링 해야하기에 성능상 문제가 있었고 사용자 입장에서도 좋지않은 경험이였다.

Ajax의 등장으로 변경되는 데이터만 특정하여 값을 변경해 부드러운 화면 전환 효과를 보게 되었다.

jQuery

jQuery는 자바스크립트의 까다롭고 복잡한 문제를 해결해주었고, 직관적인 jQuery로 인해 자바스크립트 환경이 더욱 개선되었다.

V8 자바스크립트 엔진

구글에서 개발한 V8엔진은 빠른 성능을 보여주었고, 데스크톱 애플리케이션과 유사한 사용자 경험을 제공할 수 있게 되었다.

이 이후 클라이언트 사이드 렌더링이 본격적으로 활성화되었고, 프론트엔드 영역이 주목받기 시작했다.

Node.js

브라우저에서만 동작하는 자바스크립트 엔진을 브라우저 이외의 환경에서도 동작할 수 있도록 브라우저에서 독립시킨 자바스크립트 실행 환경

Node.js가 생기며 프론트, 백 모두 자바스크립트를 사용해 개발할 수 있다는 동형성(동일한환경)이라는 *_장점이 있으며, *_비동기I/O, 단일스레드 이벤트 루프 기반으로 동작하기에 요청 처리성능이 좋으며 실시간으로 데이터를 처리하기 위해 I/O가 빈번하게 발생하는 SPA(Single Page Application)에 적합하다.

SPA 프레임워크

복잡해진 개발환경에서는 이전의 자바스크립트 환경에서는 수행하긴 하지만 변경에 유연해지며 CBD 방법론을 기반으로 하는 SPA(Single Page Application)가 대중화되었다.

SPA를 돕는 프레임워크/라이브러리로는 Angular, Reach, Vue.js, Svelte 등이 있다

자바스크립트와 ECMAScript

각 브라우저 제조사는 ECMAScript 사양을 준수해서 브라우저에 내장되는 자바스크립트 엔진을 구현한다.

자바스크립트는 큰 범주로 JavaScript내에 ECMAScript가 들어있다.

ECMAScript는 자바스크립트에서 구현할 때의 뼈대라고 생각해야 한다. 이 뼈대는 문법과 기본 기능을 정의하며 ECMAScript를 기반으로 브라우저에서 자바스크립트가 동작하는 엔진을 구현하여 자바스크립트 실행 환경을 제공한다.

자바스크립트는 브라우저 엔진을 통해 만들어져 실제 웹 페이지와 상호작용하기 위해 사용되는 프로그래밍 언어이다.

자바스크립트의 특징

  • 웹 브라우저에서 동작하는 유일한 프로그래밍 언어이다.
  • 개발자가 별도의 컴파일 작업을 수행하지 않는 인터프리터 언어이다.
  • 인터프리터 언어의 단점인 실행시간을 최적화하기 위해
    JIT(Just-In-Time) 컴파일 기술이 도입되어 자주 실행되는 코드를 컴파일해둔다
  • 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어이다.

스터디 회고

자바스크립트가 ECMAScript를 아우른다는 것 추가설명

이 부분이 다음 주 주제와도 연결이 되다보니 이해가 중요한 부분이였는데, 제가 설명을 자세히 못 적었다고 생각이 드네요..!

다음번엔 이해하기 쉽도록 더 잘 정리해보겠습니다 ㅜㅜ

제가 이해할 때 도움됐던 부분을 조금 더 정보를 꺼내 부연 설명 드리자면, 크롬의 경우 우리가 자주 사용하는 확장프로그램을 예시로 들 수 있습니다.

확장프로그램은 ECMAScript 표준을 기반으로 작성되며, 브라우저 자체에서 제공하는 고유API와 모듈을 사용해 다른 기능과 상호작용합니다.

크롬 자체에서만 지원하는 API를 활용하기에 이는 JavaScript 라고 이해했었습니다!

  if (request.message === 'Tab updated') {
    console.log('Tab was updated');
  }
});

🤔 질문

자바스크립트에 대해 알게 된 좋은 글이었습니다! 감사합니다

올려주신 글 부분에서 Node.js 설명 중 궁금한 부분이 있습니다.
Node.js에서 단일 스레드 이벤트 루프 기반이라는데, 단일 스레드인 경우에는 여러 요청들이 몰리면 성능에 이슈가 생기는 것은 아닌가요?
단일 스레드라 요청 처리 성능이 좋다고 되어 있어서 궁금해져 여쭤봅니다!..

🖥️ 답변

저도 처음에 보고 당황해서 따로 검색을 했었는데, 명확히 이해하지 못했다 생각해 올리지는 않았었습니다..

제가 이해가 가능했던 부분까지 설명을 해보자면, 이벤트 루프에 대한 이해가 필요한데요.

간단하게 한 문장으로 표현하자면, 싱글스레드로 이벤트 루프 기능이 동작하고 있고, 상세 기능은 내부적으로 비동기요청들을 멀티스레드 처리생성해 로직 처리 후 콜백하는 형태입니다.

노드에서는 멀티스레드 처리하는 라이브러리를 libuv라고 부르며, 이 라이브러리가 스레드풀에 멀티스레드를 담은 후 요청들에 대한 처리를 진행 한다고 합니다.

때문에 싱글 스레드로 동작을 모니터링 후 동작 처리는 멀티스레드 라이브러리(libuv)에 비동기 요청으로 맡기는 식으로 동작한다고 합니다.

저도 완전한 이해를 기반으로 답변드리지 못하여 아쉽지만 생각보다 단번에 이해하기 어려워 큰 틀에서만 이해해둔 상태입니다..!

이는 이후 책 진도가 나가면 한번 더 깊게 다루도록 하겠습니다!!

반응형
반응형

우리가 알고있는 HTTP통신은 OSI 7계층의 응용계층에 속한다. 이 통신을 하기 위해 어떤 동작이 이루어지는지 키워드를 갖고 간단하게 다뤄보자

TCP(Transmission Controll Protocol)

TCP는 클라이언트와 서버간의 데이터 통신을 위해 사용하는 4계층에 속하는 프로토콜이다.
HTTP는 기본적으로 통신을 하기 위해 TCP를 활용해 3-way-handshake 과정을 거쳐 TCP 소켓 연결을 하고 데이터를 전송한다.이후 소켓 연결을 해제할 때는 4-way-handshake 과정을 거쳐 연결을 해제한다.

3-way-handshake

TCP통신은 기본적으로 3방향 핸드셰이크라는 방식으로 네트워크 소켓연결을 시도하는데
SYN → SYN-ACK → ACK 의 순서로 진행된다.

  1. SYN(SYNchronize) : 클라이언트가 TCP SYN FLAG패킷을 서버로 송신
  2. SYN-ACK(Synchronize-ACKnowledgement) : 서버는 SYN을 수신해 클라이언트에게 SYN-ACK를 송신
  3. ACK(ACKnowledge) : 클라이언트에서 SYN-ACK를 수신하면 ACK를 서버에 송신

서버는 위의 과정을 거친 최종ACK를 수신받았을 때 TCP 소켓연결이 설정된다.

4-way-handshake

소켓연결 후 소켓을 끊을 때는 4-way-handshake 과정을 진행한다. 여기서 사용하는 주요 FLAG는 FIN, ACK이 있다.

해당 연결해제 절차는 클라이언트 측과 서버 측 모두 사용 가능하기에 A, B로 설명하자면,

  1. A(FIN) → B
  2. B(ACK) → A
  3. B(FIN) → A
  4. A(ACK) → B

위의 순서를 통해 A와 B의 소켓이 해제되는데 최초 (1)FIN요청을 받은 B는 A에게 (2)ACK신호를 줌과 동시에 연결을 끊는 작업을 시행하고 연결을 종료할 수 있는 준비작업이 완료되면 B가 A에게 최종종료되었다는 (3)FIN FLAG를 넘겨준다. 이를 받은 A는 최종확인 (4)ACK FLAG를 B에게 보냄으로써 소켓은 서로 닫히게 된다.

HTTP 통신

HTTP통신으로 호출하기 위한 흐름은 기본적으로 TCP 소켓 연결을 기본으로 소켓연결이 된 후 Request & Response작업이 진행된다.

때문에 순서를 다시한번 적자면 이렇게 된다

3-way-handshake → Request → Response → 4-way-handshake의 순서로 통신 흐름을 이해할 수 있다.

HTTPS 통신

우리가 평소에 사용하는 사이트들은 모두 HTTP가 아닌 HTTPS를 사용하고있다.
HTTPS는 Http에 S(Secure)가 붙어서 보안을 강화시킨 통신방식인데, 이 통신을 시행하면 중간에 데이터를 가로채가도 데이터는 암호화되어있기에 어떤 데이터를 가로챈건지 알 수 없게 되는 보안프로토콜이다.

이 통신방식은 Secure Sockets Layer(SSL)/Transport Layer Security (TLS) 등으로 불리우는 암호화 프로토콜통신(SSL Handshake, TLS Negotiation)을 HTTP통신의 데이터 교환에 앞서서 시행한다.

SSL Handshake, TLS Negotiation

이 통신을 SSL Handshake/TLS Negotiation이라고도 불리며, 기존의 소켓연결인 3-way-handshake 직후에 시행하는 통신작업인데, 순서는 다음과 같다.

  1. Client → ClientHello → Server
  2. Server → ServerHello → Client
  3. Client → PremasterSecret → Server
  4. Server → PremasterSecret(decrypt) → Client
  5. HTTPS 통신가능

위의 과정을 거쳐 진행되고 전체 진행순서를 총 정리하면

3-way-handshake → SSL Handshake/TLS Negotiation(소켓 연결 완료) → Request → Response → 4-way-handshake(연결 해제)

의 순서가 된다.

출처 : https://sh-safer.tistory.com/146

https://velog.io/@dyunge_100/Network-TCPIP-4계층에-대하여

https://velog.io/@tess/아니-왜-http를-써요-tcp를-쓰지

https://brunch.co.kr/@sangjinkang/38

https://sooolog.dev/HTTP-통신과-TCP-통신-그리고-웹-소켓에-대한-기본-개념-정리/

반응형
반응형

BoardsService Test

라라벨 테스트 assert를 통해 테스트코드 작성 실습

테스트코드를 루트\Tests\Unit 폴더에 단위테스트 코드를 작성한 후 아래의 명령어를 터미널에서 실행하면 테스트가 가능

php .\\vendor\\bin\\phpunit

Mocking을 하고싶다면 composer를 통해 설치

composer require --dev mockery/mockery
<?php

namespace Tests\\Unit;

use Tests\\TestCase;
use Illuminate\\Foundation\\Testing\\RefreshDatabase;
use Illuminate\\Support\\Facades\\DB;
use Mockery;
use App\\Http\\Services\\BoardsService;
use stdClass;
use App\\Http\\Dtos\\BoardCommentAddDto;
use App\\Models\\Comment;
use Illuminate\\Database\\Eloquent\\Model;
use Illuminate\\Support\\Collection;

class UserTest extends TestCase
{

    private $boardsService;

    function setUp(): void
    {
        parent::setUp(); // TestCase의 기본 설정을 불러오기
        $this->boardsService = new BoardsService();
    }

    public function testShow()
    {
        $mock = Mockery::mock('Illuminate\\Database\\Query\\Builder');
        $mock->shouldReceive('join')
        ->once()
        ->with('users', 'boards.user_id', '=', 'users.id')
        ->andReturnSelf();  // 메서드 체이닝을 계속하기 위해 자기 자신을 반환

        $mock->shouldReceive('where')
            ->once()
            ->with('boards.id', 1)
            ->andReturnSelf();  // 메서드 체이닝을 계속하기 위해 자기 자신을 반환

        $mock->shouldReceive('select')
            ->once()
            ->with('boards.*', 'users.name as username')
            ->andReturnSelf();  // 메서드 체이닝을 계속하기 위해 자기 자신을 반환

        $mock->shouldReceive('first')
            ->once()
            ->andReturn((object)['id' => 1, 'username' => 'Test User', 'content' => '글입니다']);  // 가짜 데이터 반환

        DB::shouldReceive('table')
            ->once()
            ->with('boards')
            ->andReturn($mock);

        $result = $this->boardsService->show(1);

        // 결과 검증
        $this->assertInstanceOf(stdClass::class, $result);
        $this->assertEquals(1, $result->id);
        $this->assertEquals('Test User', $result->username);
        $this->assertEquals('글입니다', $result->content);
    }

    public function testCommentAdd()
    {
        $boardCommentAddDto = new BoardCommentAddDto('내용', 1);

        $comment = $this->boardsService->commentAdd($boardCommentAddDto);
        
        $mockComment = new Comment;
        $mockComment->content = '내용';
        $mockComment->board_id = 1;

        $this->assertInstanceOf(Model::class, $comment);
        $this->assertEquals($comment->content, $mockComment->content);
        $this->assertEquals($comment->board_id, $mockComment->board_id);
    }

    public function testCommentsShow()
    {
        $mock = Mockery::mock('Illuminate\\Database\\Query\\Builder');

        $mock->shouldReceive('where')
            ->once()
            ->with('board_id', 1)
            ->andReturnSelf();

        $mock->shouldReceive('select')
            ->once()
            ->with('comments.*')
            ->andReturnSelf();

        $mock->shouldReceive('get')
        ->once()
        ->andReturn(collect([
            (object)['id' => 1, 'content' => '내용', 'board_id' => 1],
            (object)['id' => 2, 'content' => '내용2', 'board_id' => 1]
        ]));

        DB::shouldReceive('table')
            ->once()
            ->with('comments')
            ->andReturn($mock);

        $result = $this->boardsService->commentsShow(1);
        
        $this->assertInstanceOf(Collection::class, $result);
        // $this->assertIsArray($result);
        $this->assertEquals(1, $result[0]->id);
        $this->assertEquals(2, $result[1]->id);
        $this->assertEquals('내용', $result[0]->content);
        $this->assertEquals('내용2', $result[1]->content);
        $this->assertEquals(1, $result[0]->board_id);
        $this->assertEquals(1, $result[1]->board_id);
    }
}

한 메서드안의 assert갯수가 assertions 메서드 각각 tests에 포함

반응형

'공부 > Laravel' 카테고리의 다른 글

Pusher를 활용한 Laravel5.8 + Javascript 소켓통신  (0) 2024.06.20
반응형

라라벨의 브로드캐스팅 기능을 활용해 소켓통신을 실습해보았다

blade템플릿을 사용하면 더 편할텐데 javascript만 사용하는 회사이기에 바닐라js로 시도

적용하는 과정 자체는 몇 단계 없는데 적용하는데 시간을 많이 썼다

csrf토큰을 받아온 후 listen하는 등의 작업을 통해 소켓연결 완료

config/app.php, .env, channels.php, Events/CommentPosted.php, config/broadcasting.php 까지 설정을 완료 후 적절하게 활용하면 된다.

pusher를 composer로 먼저 받기

composer require pusher/pusher-php-server

으로 라라벨에 설치해준 후

위에서 언급한 방식대로 설정해주면 되는데,

app.php

...
'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\\Auth\\AuthServiceProvider::class,
        Illuminate\\Broadcasting\\BroadcastServiceProvider::class, //<<----이친구를 주석해제
        Illuminate\\Bus\\BusServiceProvider::class,
        Illuminate\\Cache\\CacheServiceProvider::class,
        Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider::class,
 ....

.env

BROADCAST_DRIVER=pusher

...

PUSHER_APP_ID=xxxxxxxx
PUSHER_APP_KEY=xxxxxxx
PUSHER_APP_SECRET=xxxxxxxx
PUSHER_APP_CLUSTER=xxxxxx

pusher가입 후

https://dashboard.pusher.com/

Channels에 있는 앱으로 생성 후 나온 값들을 env에 넣어줘야함

channels.php

Broadcast::channel('comment.{boardId}', function ($user, $boardId) {
    // 게시판의 모든 사용자가 이 채널을 들을 수 있도록 설정
    $board = \\App\\Models\\Board::find($boardId);
    if (!$board) return false;

    // 이 예제는 간단하게 모든 사용자에게 접근을 허용합니다.
    return true;
});

구독채널 설정

Events/CommentPosted.php

<?php

namespace App\\Events;

use Illuminate\\Broadcasting\\Channel;
use Illuminate\\Queue\\SerializesModels;
use Illuminate\\Broadcasting\\PrivateChannel;
use Illuminate\\Broadcasting\\PresenceChannel;
use Illuminate\\Foundation\\Events\\Dispatchable;
use Illuminate\\Broadcasting\\InteractsWithSockets;
use Illuminate\\Contracts\\Broadcasting\\ShouldBroadcast;
use App\\Models\\Comment;

class CommentPosted implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $comment;

    public function __construct(Comment $comment)
    {
        $this->comment = $comment;
    }
    
    public function broadcastOn()
    {
        return new PrivateChannel('comment.' . $this->comment->board_id);
    }    

}

ShouldBroadcast를 상속받은 구현체를 만들어서 채널명과 구독방식 등을 정해둘 수 있다.

config/broadcasting.php

...

'connections' => [

        'pusher' => [
            'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                'useTLS' => false,
            ],
        ],
        
...

pusher를 활용해 설정할 때 env()에 담긴 이름은 .env에 지정해둔 설정을 따라가기 때문에 변경할 필요가 없고 통신방식을 확인할 때 useTLS정도만 확인하면 될 것이다. 이번엔 실습을 위해 http일반통신을 하기 위해 false설정

이후 js설정에서

<script src="https://cdn.jsdelivr.net/npm/laravel-echo/dist/echo.iife.min.js"></script>
    <script src="https://js.pusher.com/8.2.0/pusher.min.js"></script>
    
    ...

$.ajax({
                url: 'http://localhost:8000/csrf-token',
                type: 'get',
                success: function(data){
                    csrfToken = data.csrfToken;

                    // Pusher를 사용하는 경우의 설정 예제
                    Pusher.logToConsole = true;

                    var pusher = new Pusher('xxxxxxxx', { //key입력
                        cluster: 'xxxxxxx',
                        encrypted: false,
                        auth: {
                            headers: {
                                'X-CSRF-Token': data.csrfToken
                            }
                        }
                    });

                    var echo = new Echo({
                        broadcaster: 'pusher',
                        key: 'xxxxxx',
                        cluster: 'xxxxxxxx',
                        encrypted: false,  // 보안을 위해 true로 설정
                        auth: {
                            headers: {
                                'X-CSRF-TOKEN': data.csrfToken,  // CSRF 토큰 추가
                                'Authorization': 'Bearer ' + localStorage.getItem('accessToken')  // 사용자 토큰 추가
                            }
                        }
                    });
                    // Echo를 사용하여 댓글 채널 구독
                    echo.private('comment.' + globalBoardId)
                        .listen('CommentPosted', (event) => {
                            alert('댓글추가됨');
                    });
                },
                error: function(xhr){

                }
            });

두 Script 라이브러리를 import후에

csrf토큰을 헤더에 추가하여 pusher를 구독 하면 완료!

추가로 csrf토큰을 담지못하면 해당 auth에 문제가 생겨서 구독이 불가능하니 csrf토큰을 발급해서 헤더에 꼭 추가하기!

저는 간단하게 해당 게시글을 구독 후 댓글이 작성될 때마다 alert를 띄우도록 설정했습니다

 

한쪽에서 댓글을 추가 시 양쪽 페이지 모두 listen되어 alert창 팝업

 

Reference : https://laravel.kr/docs/5.8/broadcasting

 
반응형

'공부 > Laravel' 카테고리의 다른 글

Laravel 5.8 TestCode  (0) 2024.06.21
반응형

https://school.programmers.co.kr/learn/courses/30/lessons/131123

p131123 Level.3 GROUP BY

내가 시도한 방식

SELECT FOOD_TYPE, REST_ID, REST_NAME, MAX(FAVORITES)
FROM REST_INFO
GROUP BY FOOD_TYPE
ORDER BY FOOD_TYPE DESC;

정답

SELECT FOOD_TYPE, REST_ID, REST_NAME, FAVORITES
FROM REST_INFO
WHERE (FOOD_TYPE, FAVORITES) IN 
       (SELECT FOOD_TYPE, MAX(FAVORITES)
        FROM REST_INFO
        GROUP BY FOOD_TYPE)
ORDER BY FOOD_TYPE DESC

서브쿼리를 넣어야 하는 이유 : 두 컬럼의 조건이 일치해야 한다

처음 넣었던 MAX함수만으로 뽑게되면 식당 명과 일치하지 않게 되는 문제가 발생한다.

반응형
반응형

애플리케이션 수준 보안을 지원하는 스프링 시큐리티

스프링 시큐리티는 인프라적으로 가장 최상층에 위치한 애플리케이션 수준 보안을 구현한다

웹 애플리케이션의 일반적인 보안 취약성

  • 인증 취약성
  • 세션 고정
  • XSS(교차 사이트 스크립팅)
  • CSRF(사이트 간 요청 위조)
  • 주입
  • 기밀 데이터 노출
  • 메서드 접근 제어 부족
  • 알려진 취약성이 있는 종속성 이용

인증과 권한 부여의 취약성

  • 인증(Authentication)
    • 애플리케이션을 이용하려는 사람을 식별하는 프로세스
    • 사용자의 ID를 확인하고 나면 권한을 부여하는 프로세스가 시작
  • 권한 부여(Authorization)
    • 인증된 호출자가 특정 기능과 데이터에 대한 이용 권리가 있는지 확인하는 프로세스
    • ex) 대부분의 인증된 사용자는 본인의 계좌에서만 이체가 가능하다
  • 세션 고정(Session fixation)
    • 웹 애플리케이션의 더 구체적이고 심각한 약점
    • 이미 생성된 세션 ID를 재이용해 유효한 사용자를 가장할 수 있다.
    • 취약성 발생이유 : 인증 프로세스 중 고유한 세션ID를 할장하지 않아 기존 세션ID가 재사용될 가능성이 있을 때 발생
    • 공격방식 : 세션ID가 URL에 들어있는 방식으로 세션이 구현되어있을 때 악성 링크를 클릭하도록 유인할 수 있다. 이 때 클릭했을 때 세션 ID가 탈취된다.
      세션 값을 쿠키에 저장하는 경우 피해자의 브라우저가 스크립트를 실행하도록 할 수도 있다.
  • XSS(교차 사이트 스크립팅, Cross Site Scripting)
    • 클라이언트 쪽 스크립트를 주입해 다른 사용자가 이를 실행하도록 한다.
    • 소독 하는 과정을 통해 해결
    • 이 취약성으로 인해 발생되는 문제 : 계정 가장(세션 고정과 결합), DDOS
  • CSRF(사이트 간 요청 위조, Cross-Site Request Forger)
    • 서버가 요청의 출처를 확인하지 않을 때 발생하는 문제(모든 곳에서 요청이 실행될 수 있음)
    • 일반적으로 공격자는 CSRF를 이용해 시스템의 데이터를 변경하는 동작을 실행
  • 웹 애플리케이션의 주입(Injection) 취약성 이해
    • 주입 공격은 광범위한 공격 방식.
    • 공격자는 시스템에 특정 데이터를 주입하는 취약성을 이용
    • 공격의 목표 : 시스템의 데이터 변경, 삭제, 무단 이용을 유발
    • 주입 공격의 유형 : XSS Injection, SQL Injection, XPath Injection, OS Command Injection, LDAP Injection 등
  • 민감한 데이터의 노출 처리하기
    • 공개정보가 아닌 것은 절대 로그에 기록하지 말아야 한다
    • 로그에 기록하지 말아야 하는 예시
      • [오류] 요청의 서명이 잘못되었습니다. 사용할 올바른 키는 X입니다.
      • [경고] 사용자 이름 X와 암호 Y를 이용하여 로그인하지 못했습니다.
      • [정보] 사용자 X가 올바른 암호 Y를 이용하여 로그인했습니다.
    • 애플리케이션에 예외가 발생했을 때 서버가 클라이언트에 반환하는 정보를 주의
    • 로그인의 예시
      • 사용자의 이름이 올바르지 않음, 사용자의 암호가 올바르지 않음
        보다 사용자 이름 또는 암호가 올바르지 않음을 통해 처리하는 것이 정보를 제공하지 않기에 더 바람직함
  • 메서드 접근 제어 부족
    • 컨트롤러 단에서 구성했을 때 나중에 다른 컨트롤러에서 레포지토리 참조시 권한부여가 제대로 안될 수도 있다.
  • 알려진 취약성이 있는 종속성 이용
    • 개발하는 애플리케이션이 아닌 기능을 만들기 위해 이용하는 라이브러리나 프레임워크 같은 종속성에 취약성이 있을 수 있다.
    • 메이븐 또는 그레이들 구성에 플러그인을 추가하면 정적 분석을 수행할 수 있음

OAuth 2 흐름 이해

인증 권한 부여 방식

  • 사용자가 애플리케이션접근 시 백엔드 리소스 호출
  • 권한 부여 서버를 호출해서 토큰을 획득, 자격증명, 갱신 토큰으로 토큰 획득
  • 자격 증명, 갱신 토큰이 올바를 때 새로운 액세스 토큰을 클라이언트로 반환
  • 리소스 호출 시 서버에 요청할 때 헤더에 액세스 토큰을 담아 서비스 이용

토큰 형태로 애플리케이션이 구현되었을 때 이점

  • 클라이언트는 사용자 자격 증명을 저장할 필요 없이 액세스 토큰과 갱신 토큰만 저장하면 된다.
  • 애플리케이션은 사용자 자격 증명을 노출하지 않는다.
  • 토큰을 가로챘을 때 사용자 자격 토큰을 실격시킬 수 있다.
  • 토큰을 사용했을 때는 제삼자가 사용자를 가장하지 않고도 사용자 대신 리소스에 접근할 수 있다. 토큰의 수명은 짧기에 취약성을 악용할 기한이 제한된다.

API 키, 암호화 서명, IP 검증을 이용해 요청 보안

교환되는 메시지를 아무도 변경하지 못하게 하기 위해 백엔드 구성요소간 보안을 거는 방식이 있다.

  • 요청 및 응답 헤더에 정적 키 이용
  • 암호화 서명으로 요청 및 응답 서명
  • IP 주소에 검증 적용

통신의 신뢰성을 테스트하는 제일 좋은 방법은 암호화 서명을 이용하는 것

이 책에서 배울 내용

스프링 시큐리티를 배우는 실용적 접근법 소개

  • 스프링 시큐리티의 아키텍처와 기본 구성 요소 및 이를 이용해 애플리케이션을 보호하는 방법
  • OAuth 2 및 OpenID Connect 흐름과 시큐리티로 인증과 권한 부여를 구현하는 방법
  • 애플리케이션의 다양한 계층에서 스프링 시큐리티로 보안을 구현하는 방법
  • 다양한 구성 스타일과 프로젝트에 맞는 모범 사례
  • 리액티브 애플리케이션에 스프링 시큐리티 이용
  • 보안 구현 테스트

요약

  • 스프링 시큐리티는 스프링 애플리케이션을 보호하기 위한 가장 인기 있는 선택이며 다양한 스타일과 아키텍처에 적용할 수 있는 갖가지 대안을 제공한다
  • 시스템의 계층별로 보안을 적용해야 하며 계층별로 다른 관행을 이용해야 한다.
  • 보안은 소프트웨어 프로젝트를 시작할 때부터 고려해야 하는 공통 관심사다
  • 일반적으로 취약성을 예방하는 투자 비용보다 공격의 대가가 훨씬 크다.
  • 오픈 웹 애플리케이션 보안 프로젝트(OWASP)는 취약성과 보안 관련 사항을 참고할 수 있는 훌륭한 장소다.
  • 종종 아주 작은 실수 때문에 큰 피해가 발생한다.( 로그나 오류 메시지에 민감한 데이터 노출)
반응형
반응형

Chapter01 객체, 설계

step01. 구현

  • 수동적이지 않은 객체
    • 객체의 메서드가 모두 열려있음 - public
    • 하나의 객체(Theater)가 모든 것을 조종함 - Processing(절차지향적)
  • 변경에 취약한 코드
    • 요구사항이 변경 될 가능성
    • 구현된 클래스가 다른 클래스에 의존되어 구현된다.
    • 결합도(coupling)가 높음
public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

public void enter(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

Step02. 설계 개선

  • 자율적인 존재로 만들기
    • 기존 : 티켓 판매를 티켓 판매원이 아닌 Theater가 담당함
    • 변경 : 티켓 판매 로직을 티켓 판매원에게 양도
    • TicketSeller{ ... public void sellTo(Audience audience){ if(audienct.getBag().hasInvitation()){ Ticket ticket = ticketOffice.getTicket(); audience.getBag().setTicket(ticket); }else{ Ticket ticket = ticketOffice.getTicket(); audience.getBag().minusAmount(ticket.getFee()); ticketOffice.plusAmount(ticket.getFee90); audience.getBag().setTicket(ticket); } } ... } -> public class Theater { ... public void enter(Audience audience){ ticketSeller.sellTo(audience); } ... }
  • 기존 : bag을 getter로 불러옴
  • 변경 : bag을 private화 + getter삭제 + 공용 인터페이스인 buy()를 bag에 선언
  • public class Audience{ public Bag bag; -> private Bag bag; public Bag getBag() { return bag; } -> public Long buy(Ticket ticket){ if( bag.hasInvitation()) { bag.setTicket(ticket); return 0L; } else { bag.setTicket(ticket); bag.minusAmount(ticket.getFee()); return ticket.getFee(); } }
  • 변화점 : 각 객체들이 맡은 책임에 맞게 일을 행하도록 구현되었다.
  • Theater의 경우를 예로 들었을 때 객체는 다른 객체에게 이 일을 행해달라고 전달했을 뿐이다.

캡슐화와 응집도

  • 객체지향의 핵심은 객체 내부의 상태를 캡슐화하고 객체 간에 오직 공용 인터페이스(메시지)를 통해서만 상호작용하도록 만드는 것
  • 객체가 밀접하게 연관된 작업만을 수행하고 연관성이 없는 작업은 다른 객체에게 위임하는 것이 응집도를 높이는 방법이다.
  • 자신의 데이터를 책임지며 자신의 데이터를 가공하는 것이 객체의 응집도를 높이는 핵심

절차지향과 객체지향

  • 절차적 프로그래밍
    Step01에서는 Theater가 절차지향적인 코드였으며, Process역할을 했었다. 이렇게 Step01의 Theater처럼 별도 모듈에 위치시키는 방식
  • 객체지향 프로그래밍
    Step02에서는 프로세스가 각 객체 안에서 동작하도록 구성했다. ex)class Audience
  • 책임의 이동(shift of responsibility)
    두 방식의 근본적인 차이는 기능을 처리할 때 Theater가 직접 처리를 하는 방식과, Theater가 말을하면 Audience와 TicketSeller가 스스로 처리를 하는 방식으로 구분을 할 수 있다.
    • 책임 집중 : Theater
    • 책임 분산 : buy → Audience, SellTo → TicketSeller

Step03. 추가 개선

TicketOffice

  • 기존 : getTicket, getFee를 통해 티켓 자체를 Office에서 꺼내옴
  • 변경 : sellTicketTo를 통해 기존의 티켓을 꺼내오는 방식 변경
  • class TicketOffice{ ... public Ticket getTicket(){ ... } public void plusAmount(){ ... } ... } -> class TicketOffice{ ... public void sellTicketTo(Audience audience){ plusAmount(audience.buy(getTicket())); } private Ticket getTicket(){ ... } private void plusAmount(){ ... } ... } public TicketOffice getTicketOffice() { return ticketOffice; } -> public class TicketSeller{ public void sellTo(Audience audience){ ticketOffice.sellTicketTo(audience); } }
  • 인터페이스에만 의존하게하는 형태로 변경됨
  • but TicketSeller는 audience를 넘겨주며 서로 의존하게 만듦

트레이드오프

  • 인터페이스에만 의존하는 Audience결합도가 중요한지, audience를 넘겨주며 의존성이 생겨난 것이 더 좋을지 개발자는 트레이드 오프를 해야 할 순간이 온다.
  • 두가지 이슈를 통해 (책에서의)개발팀은 TicketOffice의 자율성을 지켜 의존성 제거를 택했다.

의인화

Theater, Bag, TicketOffice는 실세계에서 자율적인 존재가 아니다. 하지만 객체지향 패러다임에서는 생물과 동일하게 객체로 다뤘다. 이렇게 모든 객체는 능동적이고 자율적인 존재로 바뀌도록 설계하는 원칙을 의인화라고 부른다

설계

설계란 코드를 배치하는 것이다 - Metz12 -

  • 요구사항은 항상 변경된다. → 코드는 항상 변경된다.
  • 설계는 코드를 잘 배치하는 것 → 변경될 때 코드의 변경을 최소화하는 것
  • 코드의 변경을 최소화하려면 → 객체 사이의 의존성을 적절하게 관리하는 것
반응형
반응형

EnumType을 엔티티 컬럼으로 생성할 때 반드시 @Enumerated(EnumType.STRING)을 사용해야 한다.

  • 이넘 타입을 그대로 저장하게되면 ordinal() 형태로 저장된다.
  • ordinal 형태로 저장하면 데이터는 조금 아낄 수 있다. but 혹시나 나중에 추가될 enum타입이 생긴다면, 0, 1, 2, 등 숫자로 파악해야 하므로 타입 데이터가 꼬이게 된다.
  • 때문에 반드시 확실한 매핑을 할 수 있는 @Enumerated(EnumType.STRING) 스트링 형태로 컬럼을 생성하자
반응형
반응형

Chapter07 병렬 데이터 처리와 성능

자바의 병렬

자바 7 이전의 버전에서는 병렬의 처리가 어려웠다.
분할을 위한 스레드 할당 → 동기화 추가 → 결과 합치기의 과정을 통해 병렬 처리가 실행되는데, 각 병렬처리를 한 후에 스레드를 합칠 때 동기화를 적절히 이뤄줘야 교착 상태를 피할 수 있었다.
7버전 부터 지원하기 시작한 Fork/Join 프레임워크를 활용하면 해당 문제를 쉽고 효율적으로 해결할 수 있다. 이 Fork/Join 프레임워크를 활용한 스트림 API와 병렬 처리에 대해서 배워보자.


병렬 스트림

  • 각각의 스레드에서 처리할 수 있도록 스트림 요소를 여러 청크로 분할한 스트림
  • 멀티코어 프로세서가 각각의 청크를 처리하도록 한다
  • Collections.parallelStream() or Arrays.stream().parallel()
    • Pipeline에있는 parallel 상태에 true가 저장된다.
    • 이후 파이프라인 처리할 때 처리할 때 parallel 체크를 통해 해결한다.
  • 병렬 처리를 고려할 때는 성능 벤치마킹을 하는 것을 권장
  • 성능 최적화
    • 기본형(primitive) 타입의 경우 기본형 특화 스트림 권장 - 오토박싱의 이유
    • 일반적으로 순차적인 처리가 필요한 스트림의 경우 병렬 비용이 더욱 비싸 느릴 가능성이 높음
    • 순차 처리가 필요없는 경우 findAny같은 순서에 상관 없는 쇼트 서킷, unordered 같은 메서드 사용
    • 하나의 요소를 처리하는 데 드는 비용이 비싸다면 고려
    • 스트림 자료구조의 특성을 확인 ex) LinkedList vs ArrayList 일반적으로 좋음
    • 병합 과정의 비용을 생각해야 한다
    | 소스 | 분해성 |
    | --- | --- |
    | ArrayList | Excellent |
    | LinkedList | Bad |
    | IntStream.range | Excellent |
    | Stream.iterate | Bad |
    | HashSet | Good |
    | TreeSet | Good |
    | Stream.generate | iterate보단 낫다 |

parallel처리 관련 이미지

Untitled

Untitled

Untitled

포크/조인 프레임워크

  • 병렬 작업할 때 사용

  • 재귀적으로 큰 작업을 작은 작업으로 분할한 후 서브태스크의 결과를 합쳐서 전체 결과를 만듦

  • 스레드 풀(ForkJoinPool)의 작업자 스레드에 분산 할당하는 ExecutorService

    • ExecutorService를 왜 언급하는가?

      RecursiveTask를 실제로 활용했을 때 해당 추상 클래스의 부모인 ForkJoinTask에서 ForkJoinPool을 활용해서 실제 활용하고있기 때문

      RecursiveTask → ForkJoinTask에서 사용하는 ForkJoinPoolExcutorService

      Untitled

      Untitled

  • 분할 정복(divide-and-conquer) 알고리즘의 병렬화

  • compute() 메서드 오버라이딩 해서 구현

  • 병렬처리 시 주의점

    • join 호출은 두 서브태스크가 모두 시작된 다음에 해야한다.
    • ForkJoinPool의 invoke메서드는 병렬 계산 시작하는 시점에서 한번만 사용
    • 한쪽은 fork() 한쪽은 compute() ⇒ 같은 스레드 재사용 피하기 위함
    • 디버깅이 어렵다 ⇒ 스택이 아닌 다른 스레드 이기 때문
    • 병렬 처리가 무조건 빠르지 않다.
  • 작업 훔치기(work stealing) 특성을 갖고있음

  • ForkJoin을 활용한 RecursiveTask 예시

import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;

public class ForkJoinSumCalculator extends RecursiveTask<Long> {

    //THRESHOLD 이상의 값 까지만 분해
  public static final long THRESHOLD = 10_000;

  private final long[] numbers;
  private final int start;
  private final int end;

  public ForkJoinSumCalculator(long[] numbers) {
    this(numbers, 0, numbers.length);
  }

  private ForkJoinSumCalculator(long[] numbers, int start, int end) {
    this.numbers = numbers;
    this.start = start;
    this.end = end;
  }

  @Override
  protected Long compute() {
    int length = end - start; //해당 태스크에서 더할 배열의 길이
    if (length <= THRESHOLD) {
      return computeSequentially();
    }
        //각 태스크 분리 left, right
    ForkJoinSumCalculator leftTask =
        new ForkJoinSumCalculator(numbers, start, start + length / 2);
        //생성한 태스크 비동기 실행
    leftTask.fork();
    ForkJoinSumCalculator rightTask =
        new ForkJoinSumCalculator(numbers, start + length / 2, end);
        //두번째 태스크 동기 실행
    Long rightResult = rightTask.compute();
        //비동기 실행했던 left의 결과를 읽거나 처리완료 후 읽기까지 대기
    Long leftResult = leftTask.join();
    return leftResult + rightResult;
  }

    //가장 작은 단위일 때 작은 단위로 쪼갠 태스크의 결과를 계산
  private long computeSequentially() {
    long sum = 0;
    for (int i = start; i < end; i++) {
      sum += numbers[i];
    }
    return sum;
  }

    public static long forkJoinSum(long n) {
    long[] numbers = LongStream.rangeClosed(1, n).toArray();
    ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers);
    return FORK_JOIN_POOL.invoke(task);
  }
}

//호출 방법
ForkJoinSumCalculator.forkJoinSum(long n));

Spliterator 인터페이스

  • 구성 요소
    • booleaen tryAdvance(Consumer<? super T> action)
      • 요소를 하나씩 순차적으로 돌며 요소가 남아있는지 확인
    • Spliterator trySplit()
      • 일부 요소를 분할해서 새로운 Spliterator 생성
    • long estimateSize()
      • 탐색해야 할 요소 수 정보 제공(RecursiveTask의 THRESHOLD 역할로 보임)
    • int characteristics()
      • 다양한 특성 집합을 포함(ORDERED, DISTINCT 그 외 다수)

7장을 마치며

  • 내부 반복을 통해 명시적으로 스트림 병렬처리
  • 병렬처리가 무조건 빠르지 않음 특성과 병렬처리 후의 결과 등을 잘 확인하여 고려할 것
  • 병렬 처리에는 포크/조인 프레임워크를 활용한다
  • Spliterator를 통해 병렬 처리를 직접 정의할 수 있다.
반응형

+ Recent posts