쾌락코딩

sequelize belongsToMany vs hasMany

|

sequelize belongsTo vs hasOne 포스트에서 1대1 관계에 대해서 알아보았다. 이번에는 hasMany와 belongsToMany가 어떤 차이점이 있고 언제 쓰이는지에 대해서 알아보았다. 물론 이것 역시 공식문서를 번역한 것이나 다름없다.

One-To-Many associations (hasMany)

hasMany의 경우 하나의 source 모델을 여러개의 Target 모델과 연결시킨다. 그리고 Target의 경우 단 하나의 soucce모델을 참조 할 수 있게 된다. 예를 들자면 여러개의 City(도시)를 가지는 Country(국가)를 들 수 있다. 각 모델을 생성하기 위해 간단히 아래와 같이 define했다고 가정하자.

const City = sequelize.define('city', { countryCode: Sequelize.STRING });
const Country = sequelize.define('country', { isoCode: Sequelize.STRING });

Country는 여러개의 City를 가지므로 Country hasMany City는 자연스럽다. 반대로 City는 단 하나의 Country만을 가지는 것이 자연스러운 그런 관계이다. 따라서 관계를 아래와 같이 설정할 수 있다.

Country.hasMany(City, {as : "Cities"});

위와 같이 설정하면 Country는 두가지 메서드를 가지게 된다. getCities와 setCities이다. Country가 여러 cities들을 가져오는 것은 자연스러운 일이다. 한편 반대로 City는 한개의 Country를 가지므로, City모델에는 Country를 참조하는 countryId 또는 country_id라는 attribute가 생긴다.

꼭 country_id를 가지고 관계를 설정하지 않아도 된다. default는 id지만 sourceKey 옵션을 통해 isoCode를 가지고 관계를 설정하게 할 수도 있다.

// Here we can connect countries and cities base on country code
Country.hasMany(City, {foreignKey: 'countryCode', sourceKey: 'isoCode'});
City.belongsTo(Country, {foreignKey: 'countryCode', targetKey: 'isoCode'});

Belongs-To-Many associations (belongsToMany)

n:m 관계 설정시 사용한다.

한 User가 여러개의 Project에 참여할 수 있고, 또한 하나의 Project에 여러명의 User들이 참여하는 상황이라면 n:m 이라고 할 수 있다. 그러나 이런 관계는 데이터베이스에서 직접 구현 할 수 없다. 대신, 이러한 관계를 두 개의 일대다 관계로 분리해야한다. 즉, 또 다른 하나의 table, 즉 연결 테이블이라 불리는 또 다른 모델이 필요하다(예를 들어 UserProject 테이블). 코드를 보자면 아래와 같다.

Project.belongsToMany(User, {through: 'UserProject'});
User.belongsToMany(Project, {through: 'UserProject'});

이렇게 관계를 설정해 준다면, UserProject라는 테이블이 하나 자동으로 생성되고, 이 테이블(모델)에는 projectId와 userId가 존재하게 된다. 즉, 관계를 중계해주는 하나의 테이블(모델)이 생성되는 것이다. 이때 테이블의 이름을 정하는 through 속성은 필수적이다.

이렇게 설정할 경우 좋은 장점은, UserProject를 생성했지만 Project와 User테이블 각각에 메서드가 추가된다는 점이다. 예를 들어 Project모델에 getUsers, setUsers, addUser, addUsers 등의 메서드가 추가되고, User모델에 getProjects, setProjects, addProject, addProjects 등의 메서드가 추가된다.

또한 as옵션, foreignKey옵션을 사용할 수 있다.

User.belongsToMany(Project, { as: 'Tasks', through: 'worker_tasks', foreignKey: 'userId' })
Project.belongsToMany(User, { as: 'Workers', through: 'worker_tasks', foreignKey: 'projectId' })

위의 예시처럼, User를 Workers라는 별칭으로 지정할 수 있고, worker_tasks라는 join table에 userId라는 이름으로 attributes를 직접 지정할 수 있다. 이렇게 하면 Project 모델에서 getWorkers()나 addWorkers()처럼 사용할 수 있다. 마찬가지로 Project를 Tasks라는 이름으로 사용할 수 있게 된다.

그러나 이렇게만 한다면 worker_tasks 테이블, UserProject 테이블에는 userId와 projectId라는 속성밖에 없다. 그래서 UserProject 테이블에 추가적인 attributes를 추가할 수 있는 방법이 있다.

const User = sequelize.define('user', {})
const Project = sequelize.define('project', {})
const UserProjects = sequelize.define('userProjects', {
    status: DataTypes.STRING
})

User.belongsToMany(Project, { through: UserProjects })
Project.belongsToMany(User, { through: UserProjects })

User에 새로운 Project를 추가하고 싶은 경우, options.through를 setter에 추가한다.

user.addProject(project, { through: { status: 'started' }})

위와 같은 n:m관계에서 Query문을 날릴 땐, through와 구체적인 attributes를 명시해줘야 한다.

User.findAll({
  include: [{
    model: Project,
    through: {
      attributes: ['createdAt', 'startedAt', 'finishedAt'],
      where: {completed: true}
    }
  }]
});

req.params vs req.query (axios, express)

|

사이드 프로젝트중에 axios로 delete메서드를 호출해야 상황이 있었다. 특정 post를 삭제하려는 목적이었다. axios에서 특정 post의 id를 넘겨주는데도 불구하고 서버에서는 이 id값을 인식하지 못하는 문제점이 있었다. 원인은 바로 내가 req.params와 req.query를 정확히 구분 하지 못하고 있었다는 것이다. 나의 지식 부족을 다시 한번 느끼며 이를 정리해 보았다.

req.params vs req.query

서버에서 req.params로 받을 수 있는 것은, 이미 예약된(?) 값이라고 생각 할 수 있다. 예를 들어 서버의 routing코드가 아래와 같다고 하자.

post.get("/:id/:name", function1);

그리고 데이터를 요청하는 클라이언트 측의 axios가 아래와 같다고 하자.

await axios({
  method: "get",
  url: `www.example.com/post/1/jun`,
  params: { title: 'hello!' },
})

이 경우 전송되는 url은 ‘www.example.com/post/1/jun?title=hello!’ 이다.

이럴 경우 서버에서 req.params와 req.query를 출력하면 결과값은 어떻게 나올까? 위에서 조금 애매하게 말하긴 했지만, req.params는 예약된 값이라고 했다. routing을 보면 id와 name이 예약되어 있음을 알고있다. 즉 서버에서 id라는 변수로 어떤 값이 들어올 것을 알고, name이라는 변수에 어떤 값이 들어올 것임을 알고 대기하고 있다. req.params는 url을 분석하여 id와 name자리에 있는 값을 낚아챈다. 따라서 req.params를 출력해보면 아래와 같다.

console.log(req.params); // { id: '1', name: 'jun' }

한편 req.query를 출력하면 아래와 같다.

console.log(req.query); // { title : 'hello!' }

즉, url에서 ?뒤에 입력되는 query문을 req.query로 받아오는 것이다.

req.body는?

req.body는 XML, JSON, Multi Form 등의 데이터를 담는다. 주소에선 확인할 수 없고 post method를 사용할 때 쓴다. delete나 get을 사용할 때, axios에 data : { ~ ~ } 이렇게 data를 사용하면 서버에서 받아올 수 가 없다.

결론

axios에서 추가 데이터를 보내는 방법에는 data와 params가 있다. data는 post요청을 보낼 때 사용되는 객체이며, params는 위의 예시처럼 url에 포함되는 데이터를 넣어주는 것이다. 주의해야 할 점은, 서버에서는 req.params를 사용하면 예상된 변수(?)값을 받아오는 의미이지만, axios에서는 params가 ?뒤에오는 쿼리문을 뜻한다. 헷갈리지 말자!

추가적으로 XML, JSON, Multi Form등을 사용해야 할 때는 post 요청을 보낼 때다. delete, get을 사용할 때는, data로 넘기지말고 params로 간단히 id를 넘겨야 한다.

Typescipt, React 에서 match사용하기

|

매치(match) 가 필요한 경우

react를 사용하여 라우팅을 할 때, 주소 값뒤에 :id, :name과 같이 추가적인 정보가 필요한 경우가 있다. 예를 App.js파일에서 라우팅 설정을 한다면 아래와 같을 수 있다.

App.tsx

import { BrowserRouter, Route } from "react-router-dom";
...

class App extends React.Component<Props, IState> {

  ...

  render () {
    return (
      <BrowserRouter>
        <React.Fragment>
          <Route exact={true} path="/" component={IntroPage} />
          <Route exact={true} path="/signIn" component={SignInPage} />
          <Route exact={true} path="/signUp" component={SignUpPage} />
          ...
          <Route exact={true} path="/post/:postId" component={PostPage} />
        </React.Fragment>
      </BrowserRouter>
    );
  }
}

만약 유저가 /post/1 이라는 주소로 들어오게 되고 postId로 무언가 하고싶다면(여기서는 단순 log를 찍어보는 것으로 하자), 타입스크립트를 적용하지 않은 React인 경우 PostPage컴포넌트에서 this.props.match.postId를 해주면 그만임을 알고 있다. (PostPage는 Functional Component로 하겠다.)

PostPage.js (pure React.js)

const PostPage = ({match}) => {
  console.dir(match); // params, isExact, path, url 속성들...
  console.log(match.params.postId); // 1
  return <div>blablabla</div>
}

그러나 타입스크립트를 사용하게 되면, 늘 해당 컴포넌트가 받을 Props를 정의해 주었듯이, match를 받을 것이므로 match에 대한 Props 타입이 필요하다. 그렇다면 match의 속성인 params, isExact, paht, url 등의 타입을 일일히 만들어야 할까? 그렇지 않다. 만약 그렇다면 match 뿐만 아니라 location, history등의 객체도 있는기 때문에 상당히 복잡해질 것이다.

react-router를 사용하면 된다.

흔히 stateless component에서 Props를 설정할 때, 아래와 같이 interface를 작성하는 경우가 많다.

stateless component에서 Props를 설정하는 간단한 예시

interface Props{
  name: string;
  age: number;
}

const PersonPage:React.SFC<Props> = ({name, age}) => (
  <p>{name}, {age}</p>
)

match를 받을 때도 다를 것이 없다. react-router가 제공하는 RouteComponentProps 인터페이스를 사용하면 된다. 동작하는 코드를 보기전에 RouteComponentProps가 어떻게 되어있는지 알면 이해하기 쉬울 것 같다.

RouteComponentProps

export interface match<P> {
  params: P;
  isExact: boolean;
  path: string;
  url: string;
}

...

export interface RouteComponentProps<P, C extends StaticContext = StaticContext, S = H.LocationState> {
  history: H.History;
  location: H.Location<S>;
  match: match<P>;
  staticContext?: C;
}

위의 코드를 보면 RouteComponentProps가 interface임을 알수 있고, match 뿐만 아니라 history, locaion 타입이 정의되어 있음을 알 수 있다. 그런데 제네릭타입 P를 보면 디폴트 값이 없기 때문에 우리가 직접 입력해 줘야 한다. 즉, P값으로 우리가 어떤 interface 타입을 부여하면, match객체의 params는 P라는 인터페이스 타입을 가지는 것이다. 따라서 아래와 같이 인터페이스를 정의하고 P값으로 부여하면, match.params 객체에서 postId를 받아와 출력할 수 있다.

import * as React from "react";
import { withRouter, RouteComponentProps } from "react-router";

interface MatchParams {
  postId: string;
}

const PostPage: React.SFC<RouteComponentProps<MatchParams>> = ({ match }) => {
  console.log(match.params.postId);
  return (
    <p>{match.params.postId}</p>
  )
};

export default withRouter(UserPage);

참고자료

쿠키 vs 세션

|

쿠키와 세션은 왜 필요한가?

쿠키와 세션이 왜 필요한지, 무엇 때문에 등장했는 지를 알기 위해선 우리가 사용하는 웹, http 프로토콜이 비연결성을 띄며 Stateless하다는 것을 이해해야 한다. 이것들의 단점을 보완하기 위해 쿠키와 세션이 등장했기 때문이다.

비연결성

http는 요청과 응답을 주고 받을 때 지속적으로 연결 되어있지 않다. 즉 클라이언트가 서버로 request를 날리고, 서버가 클라이언트에게 response로 응답을 주면 이후 그 둘은 서로 모르는 사이가 된다. 서로 자기가 할 역할에만 충실할 뿐 서로에게 관심이 없다는 뜻이다. cookie1 이 과정이 한차례 지나가면, cookie2 이렇게 서로 모르는 사이, 서로 접속이 끊어진 사이가 되는 것이다.

stateless

사실 statelss한 것은 비연결성과 밀접한 관계이다. 클라이언트와 서버의 통신이 한 번 이루어지고 끝이나면 서버는 데이터를 저장하지 않는다는 것을 의미한다. 상태가 없다는 뜻에서 stateless라고 한다. HTTP 프로토콜은 요청 간 사용자 데이터를 저장하는 수단을 제공하지 않는다.

그러나 상태를 저장하지 않는다고 해서 매번 클라이언트가 요청 할 때 마다 새로 로그인을 하는 건 너무 비효율적인 일이 될 것이다. 그것을 해결하기 위해 쿠키와 세션이 등장했다.

결론부터 말하자면 쿠키는 클라이언트에서 저장되는 데이터인데, 쿠키에 로그인 정보를 저장해 놓으면 매번 request요청시 쿠키도 자동으로 전송되어 로그인이 끊기지 않은 것 처럼 느낄 수 있다. 세션은 뒤에서 더 자세히 알아보겠다.

쿠키(COOKIE)

쿠키는 클라이언트 로컬에 저장되는 데이터 문자열이다. 이 쿠키 데이터는 아주 작은 데이터로 파일에 저장되는데 브라우저별로 저장되는 위치가 다르다. 위치는 여기에서 참조했다. cookie3

위와 같은 경로에 쿠키가 저장되는데 쿠키에 저장되는 정보는 대표적으로 쿠키 이름, 값, 만료 날짜(쿠키 저장 기간), Domain, 경로 정보 등이 있고, 몇 가지 사례를 들자면 특정 웹사이트나 웹페이지에 얼마나 자주 또는 몇 번 방문했는지, 또는 팝업에서 “오늘 더 이상 이 창을 보지 않음” 체크 상태 등 서버가 어떤 주기동안 클라이언트의 상태를 알아줬으면 하는 데이터를 저장한다.

위에서 domian은 굵은 글씨로 작성했는데, 도메인에 적힌 곳에 반복 요청을 할 경우 자동으로 쿠키가 전송된다. 만약 그렇지 않다면 naver에 request를 보낼 때도, 구글에 어떤 requeset를 보낼 때도 같은 로그인 정보를 전송하게 될테니 domain은 꼭 필요한 것이다.

쿠키는 Request요청시 COOKIE라는 Header에 값이 설정되어 함께 보내진다.

쿠키의 단점

쿠키는 브라우저에 저장되는 값이기 때문에 보안에 취약하다. 또한 하나의 도메인에서 설정한 쿠키 값이 20개를 초과하면 가장 적게 사용된 쿠키부터 지워진다.

세션(SESSION)

흔히들 쿠키는 클라이언트에, 세션은 서버에 저장되는 데이터라고 말한다. 그러나 enterkey님의 블로그에 따르면 특정 프레임워크에서는 세션을 쿠키에 저장한다고도 한다. 그래서 서버에 데이터를 저장한다기 보다 서버에서 클라이언트의 상태를 유지하는 수단이라는 말이 좀 더 맞다고 이야기한다(세션을 쿠키에 저장하는 것의 장점은 잘 모르겠다..). 아무튼 나는 “일정 시간동안 같은 사용자(같은 브라우저)로부터 들어오는 일련의 요구를 하나의 상태로 보고, 그 상태를 서버에 유지시키는 기술” 이라는 정의를 좋아한다.

클라이언트가 Request를 보내면, 해당 서버의 엔진이 클라이언트마다 고유한 id를 부여하고 저장하는데, 이 id가 세션 id이다. 이렇게 하면, 클라이언트의 민감한 데이터를 굳이 쿠키에 저장할 필요가 없이, 쿠키에는 세션id만 등록해놓고, request시 세션id를 서버로 보내면 서버가 세션 id를 기준으로 데이터를 찾아서 읽으면 그만이다. 결국 비연결성, statless한 http의 단점을 보완하고자 하는 목적에서 쿠키와 세션의 역할을 동일하다고 볼 수 있다.

세션의 저장은?

쿠키는 브라우저별로 파일을 만들어 저장했다. 그럼 세션은 어떻게 저장할까? 그것은 서버 엔지니어의 마음대로이다. 메모리에 저장할 수도, 파일에 저장할 수도, db에 저장할 수도 있다. 그러나 부하분산을 위해 한 도메인에 여러대의 서버를 운영하고 있다면, 메모리나 파일을 사용하면 안되고 데이터베이스나 쿠키를 사용해야 한다(개인적으로 데이터베이스 추천! 쿠키는 보안에 취약하니깐).

그렇다면 쿠키의 만료기간은 쿠키에 명시되어 있는데, 세션의 만료기간은 어떻게 될까? 세션을 db에 저장한다면, DB_SESSION관련 라이브러리를 사용할 경우 해당 라이브러리가 알아서 관리해주고 그렇지 않다면 서버 개발자가 직접 설정해주게 된다.

세션에서 쿠키로 id 전송

위에서 설명했듯이 쿠키에는 session id만 저장하면 된다. 그러기 위해서는 우선 서버에서 세션을 생성한 후 세션 id를 쿠키에 알려줘야 한다. 이때는 Set-Cookie헤더에 seesion_id를 담아서 response와 함께 전달한다.

세션의 단점

세션은 서버의 자원을 사용하기 때문에 미세하더라도 서버에 부하를 준다. 때문에 속도가 느려질 수도 있다.

참고

퀵 정렬

|

퀵 정렬이란?

말 그대로 빠른(quick) 정렬이다. 분할 정복법을 사용하는 알고리즘의 하나로 병합 정렬과 비슷하지만, 병합 정렬과는 다르게 리스트를 비균등하게 분할한다는 점이 다르다. 오름차순 정렬을 할 경우, 리스트에서 각자의 기준대로 pivot(기준 값)을 하나 선택하고, 그 기준 값보다 작은 요소들은 모두 pivot 왼쪽에, 더 큰 값은 모두 pivot의 오른쪽에 배치하는 방법을 재귀적으로 계속 호출하는 방법이다.

과정

우선 매번 pivot값을 정하고, 그 값을 기준으로 데이터를 분류해야 한다. pivot값을 어떻게 설정하는 지에 따라 퀵 정렬의 성능은 달라지는데, 여기서는 항상 가장 왼쪽에 있는 값을 pivot값으로 정하겠다.

아래와 같이 정렬되지 않은 배열과 pivot값이 있다. quick3

이제 이 pivot값을 제외한 배열에서 첫 번째 배열을 left로, 마지막 배열을 right로 잡고 분류 과정을 진행해 나간다. quick2

이제부터 left를 오른쪽으로 계속 이동시킬건데, 결과적으로 left보다 왼쪽에 있는 값은 pivot값인 5 보다 작아야 한다. 즉 값을 하나씩 오른쪽으로 이동하면서, pivot 값인 5 보다 큰 숫자가 있다면 거기서 더이상 움직이지 않고 stop한다.

동시에 right도 왼쪽으로 움직이는데, left와는 반대로 pivot값보다 작은 숫자를 만나면 stop한다. quick3

둘이 멈춘 상태라면 이 둘의 위치를 바꿔준다. 둘의 위치를 바꾼 아래 그림을 보면, pivot값인 5보다 작은 값들이 점점 왼쪽으로 모이고 있음을 알 수 있다. 이 과정을 계속 진행하다보면 정렬이 될것 같은 느낌이든다(즉, 5보다 큰 숫자는 배열의 뒷 쪽 쯤에, 5보다 작은 숫자는 배열의 앞쪽에 배치되게 된다). quick4

한번더 진행해 보자. left를 오른쪽으로, right를 왼쪽으로 진행시키면서 아까와 같은 행위를 반복한다. quick5 그런데 이번엔 조금 주의해야한다. right와 left의 상대적 위치가 바뀌었다. left가 왼쪽에, right가 오른쪽에 있어야 하는데 서로 자리가 뒤바껴있다. 바로 이 때가 한번의 분류를 끝마칠 때이다. right과 left가 있는 1 과 7 사이에 딱 5가 들어간다면? 그렇다면 5의 왼쪽에 있는 수는 모두 5보다 작을 것이며, 5의 오른쪽에는 모두 5보다 클것이다. 따라서 right 자리에 pivot을 넣고, 원래의 피봇자리인 첫 번째 자리에 right가 가르키고 있는 값 1을 주면 된다.

quick6

이로써 한번의 분류가 끝났다. 이제는 두개의 정렬되지 않은 배열이 생긴 셈이다. 즉, quick7 그리고 quick8 이렇게 두개의 배열이 있는 것이고, 각각 독립적으로 위에서 했던 방법 그대로 적용하면 된다.

코드

def swap(x, i, j):
    x[i], x[j] = x[j], x[i]

def pivotFirst(x, lmark, rmark):
    pivot_val = x[lmark]
    pivot_idx = lmark
    while lmark <= rmark:
        while lmark <= rmark and x[lmark] <= pivot_val:
            lmark += 1
        while lmark <= rmark and x[rmark] >= pivot_val:
            rmark -= 1
        if lmark <= rmark:
            swap(x, lmark, rmark)
            lmark += 1
            rmark -= 1
    swap(x, pivot_idx, rmark)
    return rmark

def quickSort(x, pivotMethod=pivotFirst):
    def _qsort(x, first, last):
        if first < last:
            splitpoint = pivotMethod(x, first, last)
            _qsort(x, first, splitpoint-1)
            _qsort(x, splitpoint+1, last)
    _qsort(x, 0, len(x)-1)

여기 참조!

시간 복잡도

  • 평균 : O(nlogn)
  • 최악 : O(n^2)