<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Joon's Web Backend World</title>
        <link>https://velog.io/</link>
        <description>A web backend developer, let's share information and problem solving!</description>
        <lastBuildDate>Sat, 06 Jan 2024 08:51:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Joon's Web Backend World</title>
            <url>https://velog.velcdn.com/images/who_doctor/profile/ee71caa8-1b06-44c8-b466-cd47c976a5f7/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Joon's Web Backend World. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/who_doctor" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Spring Boot] Using JIB to speed up the build time!]]></title>
            <link>https://velog.io/@who_doctor/Spring-Boot-Using-JIB-to-speed-up-the-build-time</link>
            <guid>https://velog.io/@who_doctor/Spring-Boot-Using-JIB-to-speed-up-the-build-time</guid>
            <pubDate>Sat, 06 Jan 2024 08:51:38 GMT</pubDate>
            <description><![CDATA[<p> I deployed a Spring Boot project with Docker, enforced Continuous Delivery, and ran into a big problem. <strong>The speed and performance of Oracle&#39;s VMs were taking too much time to compile</strong> and build the spring boot, causing the build to fail in the Github flow.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/who_doctor/post/3369745a-36ee-4fe5-ba7d-96dedfa5acfb/image.png" alt=""> 2024/01/03 15:09:04 Run Command Timeout</p>
</blockquote>
<p>While debugging to troubleshoot an issue that was working well locally, but only in a VM deployment, I started to get frustrated with the slow builds, and even though I tried multi-staging and moderately utilizing Docker&#39;s cache features, the builds themselves were too slow for a quick fix. 
<br></p>
<h3 id="my-first-idea">My first idea</h3>
<ul>
<li>was to build directly outside of docker, i.e. in a vm environment. <br> So, even though it was a docker environment, I installed the JDK on a VM machine to build. Unfortunately, this didn&#39;t work out and I found another way.</li>
</ul>
<ol>
<li>Caching build images via Kaniko</li>
<li>using JIB to generate images from Github Workflow and importing them from docker-compose.<br>
#### **build.gradle.kts**
```js
jib {
 from {
     image = "azul/zulu-openjdk-alpine:21-jre-headless-latest"
 }
 to {
     image = "hojunsong/tripsketch:latest"
 }
 container {
     entrypoint = listOf("java", "-cp", "/app/resources:/app/classes:/app/libs/*", "kr.kro.ApplicationKt")
 }
}
```
_The listof syntax is used in Kotlin, so you'll need to use "[]" in Java._
<br>
#### **cd.yml**
```js
   - name: Docker Login
     uses: docker/login-action@v3
     with:
       username: ${{ secrets.DOCKER_HUB_USERNAME }}
       password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
```

</li>
</ol>
<p><em>Sign up for Docker, get an access token, and log in with secret.</em></p>
<h4 id="cdyml"><strong>cd.yml</strong></h4>
<pre><code class="language-js">      - name: Build and push Docker image with Jib
        run: |
          ./gradlew jib</code></pre>
<p><img src="https://velog.velcdn.com/images/who_doctor/post/bb07cefa-43a1-405e-873f-6ac7eb51e8c6/image.png" alt=""><em>Having the github workflow do the building for me has also been beneficial in terms of resources.</em>
<em><a href="https://hub.docker.com/r/hojunsong/tripsketch">https://hub.docker.com/r/hojunsong/tripsketch</a></em>
<br></p>
<pre><code class="language-js">  tripsketch:
    build:
      context: .
      dockerfile: tripsketch.dockerfile</code></pre>
<p>** - &gt; After the change **</p>
<pre><code class="language-js">  tripsketch:
    image: hojunsong/tripsketch:latest</code></pre>
<p>Reduced build time from 10+ minutes to less than a minute.</p>
<br>
<br>

<h3 id="why-jibs-build-speed-is-fast">Why Jib&#39;s Build Speed is Fast</h3>
<p><img src="https://velog.velcdn.com/images/who_doctor/post/4b382330-9e2b-407a-aeaf-c5884093001f/image.png" alt=""></p>
<ol>
<li><p><strong>Layer Separation</strong>: Jib divides an application into multiple layers. For example, dependencies, resources, and classes are placed in different layers. By doing this, parts that have not changed do not need to be rebuilt, reducing build time.</p>
</li>
<li><p><strong>Caching Mechanism</strong>: Jib stores layers used in previous builds in a cache. If a certain layer has not changed in a new build, Jib reuses the cached layer, shortening the build time.</p>
</li>
<li><p><strong>Container Registry Optimization</strong>: Jib can directly push the built image to a container registry. In this process, only the necessary layers are pushed, and existing layers are skipped. This reduces data transfer time.</p>
</li>
<li><p><strong>No Need for Docker Daemon</strong>: Jib can operate without a Docker daemon. This simplifies the build process and easily integrates into the build pipeline.</p>
</li>
<li><p><strong>Plugin Integration</strong>: Integrated with build tools like Maven or Gradle, Jib works well with the existing Java build process and can be used without additional settings or scripts.</p>
</li>
</ol>
<BR>

<p><strong>I can skip the dockerfile below and save resources on VM.</strong></p>
<pre><code class="language-js">FROM azul/zulu-openjdk-alpine:21 as build

RUN apk add git &amp;&amp; \
    mkdir /tripsketch &amp;&amp; \
    git clone --branch main https://github.com/trip-sketch/tripsketch /tripsketch

WORKDIR /tripsketch
RUN ./gradlew build -x test --no-daemon --parallel</code></pre>
<br>


<blockquote>
<ul>
<li>Google. “Jib: Build Container Images for Your Java Applications.” <a href="https://cloud.google.com/java/getting-started/jib?hl=en">https://cloud.google.com/java/getting-started/jib?hl=en</a>.</li>
</ul>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kakao] Oauth ip mismatched 해결 방법]]></title>
            <link>https://velog.io/@who_doctor/%EC%B9%B4%EC%B9%B4%EC%98%A4-Oauth-ip-mismatched-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@who_doctor/%EC%B9%B4%EC%B9%B4%EC%98%A4-Oauth-ip-mismatched-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Wed, 03 Jan 2024 16:30:15 GMT</pubDate>
            <description><![CDATA[<pre><code>{“msg”:”ip mismatched! callerIp= check out registered ips.”,”code”:-401}</code></pre><p>결국 로깅과 로깅을 거듭한 끝에 발견했다.</p>
<p>분명히 0.0.0.0/0으로 로그인 제한을 풀었다고 생각한 나의 실수였다.
국내서버에서 해외리전 VM으로 이동하면서, 잘 운영되던 서버가 되지 않던게 당황스러웠다.</p>
<p>docker 환경설정 등 삽질을 하다가 다시 코드에 logger를 
붙여서 디버깅을 하다가, 특정 API 요청에 401이 뜨는 것을 발견했다.</p>
<p>해외 리전 VM, 즉 해외 IP는 허용 서버로 등록해야한다.
<img src="https://velog.velcdn.com/images/who_doctor/post/8d3321e5-3a25-4db9-bdb2-e7fde8d45f07/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Exrpess]  Using TSOA, Crafting Controllers over Traditional Routing]]></title>
            <link>https://velog.io/@who_doctor/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-1%EB%B6%80-TSOA%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-Express%EC%97%90%EC%84%9C%EB%8F%84-%EA%B9%94%EB%81%94%ED%95%98%EA%B2%8C-Controller-%EC%9E%91%EC%84%B1%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@who_doctor/%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81-1%EB%B6%80-TSOA%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-Express%EC%97%90%EC%84%9C%EB%8F%84-%EA%B9%94%EB%81%94%ED%95%98%EA%B2%8C-Controller-%EC%9E%91%EC%84%B1%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 06 Dec 2023 09:20:49 GMT</pubDate>
            <description><![CDATA[<p>내 생애 첫 프로젝트를 리팩토링을 시작했다. 2주만에 5명이서 만든 이 프로젝트를 다시 코드를 뜯어보니, 처음 웹을 개발한 우리 팀원 모두가 자랑스러울 정도로, 코드는 투박하지만 프로젝트는 완성도가 높았다. 물론 프론트는 지금 봐도 잘한 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/who_doctor/post/4d5348f6-f14e-4853-931d-f6184c4b3516/image.png" alt=""></p>
<p>2주 동안, 내 백엔드 팀원과 새벽 4시, 때론 아침 10시에 회의에 들어가고 날을 세워가며 개발했던 애증의 코드. 판도라의 상자를 열었다. Express 특유의 non-opinionated한 프레임워크에 인증에 대한 개념 숙지를 못한 내가, 사실상 세션도 쓰고 토큰도 사용하는 기괴한 인증방식이 눈에 보였다. </p>
<p>디자인 출신과 디자인 감각이 넘친 우리 팀원들 덕분에, 나의 가난하고 투박한 코드를 프론트엔드가 빛내주었다. 프론트를 바닐라 자바스크립트를 사용해서, 프론트 개념도 다시 한번 익힐겸 리팩토링을 진행하고자 했다.</p>
<p><img src="https://velog.velcdn.com/images/who_doctor/post/650d792c-16bd-44ef-abdf-a9624afafcdf/image.png" alt=""></p>
<blockquote>
<p>다시 살아난 리프레시 북스토어</p>
</blockquote>
<p>기존의 컨트롤러는 이렇게 되어있다. 기본적인 commonjs 문법이며, 함수형이다.</p>
<pre><code class="language-js">exports.createCategory = async (req, res, next) =&gt; {
  try {
    const categoryName = req.body.name;
    const result = await categoryService.createCategory(categoryName);

    res.status(200).json({
      message: &quot;카테고리 생성이 완료되었습니다.&quot;,
      data: result,
    });
  } catch (error) {
    res.status(error.status || 500).json({
      message: error.message || &quot;서버 오류가 발생했습니다.&quot;,
    });
  }
};</code></pre>
<p>지금보니, 이렇게 막강의 자유도는 잘 정리되지 못한 느낌을 주었다.
리팩토링을 어떻게 해야하나 고민이 많았다.</p>
<p>마침 타입스크립트 진영에서는 Typescript-rest와 TSOA라는 라이브러리를 통해, nest까지는 아니지만, router를 분리해서 app 등록하는 수고스러운 일까지 덜어주는 멋진 라이브러리를 발견했다. 이것을 통해, 대략 이렇게 카테고리를 변경할 수 있었다. </p>
<pre><code class="language-js">  @Post(&quot;user-admin/category&quot;)
  @Security(&quot;sessionAuth&quot;, [&quot;isAdmin&quot;])
  public async createCategory(
    @Body() body: CreateUpdateCategoryDto
  ): Promise&lt;any&gt; {
    try {
      const result = await this.categoryService.createCategory(body.name);
      console.log(body.categoryId);
      return { message: &quot;카테고리 생성이 완료되었습니다.&quot;, data: result };
    } catch (error) {
      this.setStatus(error.status || 500);
      return { message: error.message || &quot;서버 오류가 발생했습니다.&quot; };
    }
  }</code></pre>
<p>  앞으로 이어질 내용
  기존 미들웨어 방식을 Security 데코레이터로 전환한 경험을 기록해보고자 한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JWT] 프레임워크 별 JWT 인증 방식]]></title>
            <link>https://velog.io/@who_doctor/wanted-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EB%B3%84-JWT-%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@who_doctor/wanted-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EB%B3%84-JWT-%EC%9D%B8%EC%A6%9D-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Fri, 27 Oct 2023 08:06:13 GMT</pubDate>
            <description><![CDATA[<p>10-27 Wanted Today I learned. </p>
<p>스프링을 처음 배울 때 당황했던 것은 토큰 인증 방식을 위한 filter나 intercepter로, 맨 처음 Express와 FastAPI를 접했던 나에게는 상당히 이질적인 존재였다.</p>
<p>스프링을 3개월 쓰다가, 다시 Node의 NestJS로 돌아오니, 역으로 다시 적응이 안되는 시간이 다가왔다. Nest는 다양한 전략을 사용할 수 있었고, 스프링 사용자라면 익숙한 인터셉터 방식으로도 구현이 가능하다. 다음은 두 프레임워크별로 jwt인증 방식을 간략하게 비교해보고자 한다. 바로 아래는 jjwt 라이브러리를 사용해서, 간단한 jwt토큰을 발급하는 스프링 부트의 예제이다.</p>
<pre><code class="language-ts">@Service
class AuthService {

    companion object {
        private val users = listOf(
            User(&quot;1&quot;, &quot;John&quot;),
            User(&quot;2&quot;, &quot;Maria&quot;)
        )
    }

    fun validateUser(userId: String): User? {
        return users.find { it.userId == userId }
    }

    fun login(userId: String): String? {
        val user = validateUser(userId) ?: return null

        return Jwts.builder()
            .setSubject(user.userId)
            .claim(&quot;username&quot;, user.username)
            .signWith(SignatureAlgorithm.HS256, System.getenv(&quot;JWT_SECRET&quot;))
            .setExpiration(Date(System.currentTimeMillis() + 3600000))  // 1 hour
            .compact()
    }
}

class JwtFilter : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val authorizationHeader = request.getHeader(&quot;Authorization&quot;)
        if (authorizationHeader != null &amp;&amp; authorizationHeader.startsWith(&quot;Bearer &quot;)) {
            val jwt = authorizationHeader.substring(7)
            val claims = Jwts.parser()
                .setSigningKey(System.getenv(&quot;JWT_SECRET&quot;))
                .parseClaimsJws(jwt)
                .body
            val userId = claims.subject
            // ... set authentication in context ...
        }
        filterChain.doFilter(request, response)
    }
}</code></pre>
<p>이렇게 filter로 처리하면, 토큰이 필요하지 않은 엔드포인트를 역으로 한곳에서 관리해서 처리했었다. 토큰에 특정 값을 저장하면, 컨트롤러에서 httpServeletRequest에 등록된 값을 가져올 수 있었다.</p>
<p>다음은 내가 만들었던 컨트롤러의 예시이다.</p>
<pre><code class="language-ts">@ApiResponse(responseCode = &quot;200&quot;, description = &quot;모든 사용자의 정보를 성공적으로 반환합니다.&quot;)
    fun getAllUsers(req: HttpServletRequest, pageable: Pageable): ResponseEntity&lt;Page&lt;UserDto&gt;&gt; {
        val users = userService.getAllUsers(pageable)
        return ResponseEntity.ok(users.map { userService.toDto(it) })
</code></pre>
<p>어떤 유저인지 파악하려면, 토큰값에 담아둔 memberId를 컨트롤러에서 조회했다.</p>
<pre><code class="language-ts">val memberId = req.getAttribute(&quot;memberId&quot;) as Long?</code></pre>
<p>Nest의 경우에는 filter와 middleware가 아닌 Guard라는 개념을 배웠는데, 이 개념이 흥미롭다. 스프링의 경우, 나는 특정 엔드포인트를 아래와 같이 예외처리를 했다.</p>
<pre><code class="language-ts">override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(jwtTokenInterceptor)
            .addPathPatterns(&quot;/**&quot;)
            .excludePathPatterns(
                &quot;/swagger-ui/**&quot;,
                &quot;/v3/**&quot;,
                &quot;/api/user/nickname/guest&quot;,
                &quot;/api/follow/guest/followings&quot;,
                &quot;/api/follow/guest/followers&quot;,
                &quot;/api/oauth/kakao/**&quot;,
                &quot;/api/comment/guest/**&quot;,
                &quot;/api/trip/nickname/**&quot;,
                &quot;/api/trip/guest/**&quot;,
                &quot;/api/trip/like/user/**&quot;,
                &quot;/favicon.ico&quot;,
            )
    }</code></pre>
<p>nest에서는 간단하게</p>
<pre><code class="language-ts">@Controller(&#39;your-path&#39;)
@UseGuards(YourGuard)
export class YourController {
    // ...
}</code></pre>
<p>이렇게 데코레이터를 붙여주기만 하면 된다. 
Authguard에는 다음과 같이 토큰에 값을 설정할 수 있다.</p>
<pre><code class="language-ts">import { Injectable, ExecutionContext } from &#39;@nestjs/common&#39;;
import { AuthGuard } from &#39;@nestjs/passport&#39;;

@Injectable()
export class JwtAuthGuard extends AuthGuard(&#39;jwt&#39;) {
  async canActivate(context: ExecutionContext): Promise&lt;boolean&gt; {
    const canActivate = (await super.canActivate(context)) as boolean;
    if (!canActivate) {
      return false;
    }

    const request = context.switchToHttp().getRequest();
    const user = await this.getUserFromToken(request);
    request.user = user;

    return true;
  }

  async getUserFromToken(request): Promise&lt;any&gt; {
    // Implement your logic to extract user information from the token.
    // For example:
    const token = request.headers.authorization.split(&#39; &#39;)[1];
    return { userId: token.sub };  // Assuming &#39;sub&#39; contains the user ID.
  }
}</code></pre>
<p>그럼 이렇게 스프링과 비슷하게 컨트롤러에서도 꺼내쓸 수 있다.</p>
<pre><code class="language-ts">@Controller(&#39;your-path&#39;)
@UseGuards(JwtAuthGuard)
export class YourController {

  @Get(&#39;profile&#39;)
  getProfile(@Request() req) {
    const memberId = req.user.userId;  // Accessing the user ID from the request object.
    return { memberId };
  }

}</code></pre>
<p>하지만 이 방식들이 옳은건지는 조금 더 고민을 해봐야겠다. 익숙치 않은 프레임워크로 개발을 하려고 하니, 새로운 것들이 적지 않아 당황스러울 때가 많다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[wanted] NoSQL의 Normalized Data Model]]></title>
            <link>https://velog.io/@who_doctor/wanted-NoSQL%EC%9D%98-Normalized-Data-Model</link>
            <guid>https://velog.io/@who_doctor/wanted-NoSQL%EC%9D%98-Normalized-Data-Model</guid>
            <pubDate>Wed, 25 Oct 2023 07:39:53 GMT</pubDate>
            <description><![CDATA[<p> 팀원과 회의 중 NoSQL과 RDBMS의 선택 과정에 있어서, 내가 사용했던 기술이 무엇인지 되짚어보았다. <a href="https://github.com/hojoonSong/tripsketch">이전 프로젝트</a>에서는 유저의 팔로잉기능을 제작하면서, 이런 테이블을 구성해서 만든 적이 있다. </p>
<pre><code class="language-ts">@Document(collection = &quot;follows&quot;)
data class Follow(
    @Id val id: String? = null,

    @field:NotBlank(message = &quot;비워둘 수 없습니다.&quot;)
    val follower: String,

    @field:NotBlank(message = &quot;비워둘 수 없습니다.&quot;)
    val following: String,
)</code></pre>
<p>이런 전략을 그냥 모르고 개발했는데, 이것을 NoSQL의 &quot;Normalized Data Model&quot;이라고 부른다고 한다. 한국말로 풀이하면, 정규화 데이터 모델이다.</p>
<h3 id="rdbms-nestjs-with-typeorm">RDBMS (NestJS with TypeORM):</h3>
<p>RDBMS에서는 테이블을 분리하여 중복을 최소화하고 무결성을 유지한다. 아래는 Normalized 사례이다. 사실 관계형 데이터베이스에서는 이 모델링이 주를 이루는 것 같다.</p>
<ul>
<li>Normalized Data Model</li>
</ul>
<pre><code class="language-ts">@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  nickname: string;

  // other fields...
}

@Entity()
export class Follow {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() =&gt; User, user =&gt; user.following)
  follower: User;

  @ManyToOne(() =&gt; User, user =&gt; user.followers)
  following: User;
}</code></pre>
<h4 id="반면-denormalized-data-model은-단일-테이블에-모든-데이터를-저장하여-쿼리의-복잡성을-줄인다">반면 Denormalized Data Model은 단일 테이블에 모든 데이터를 저장하여 쿼리의 복잡성을 줄인다.</h4>
<ul>
<li>Denormalized Data Model</li>
</ul>
<pre><code class="language-ts">@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  nickname: string;

  @Column(&quot;simple-array&quot;)
  following: number[];

  @Column(&quot;simple-array&quot;)
  followers: number[];

  // other fields...
}</code></pre>
<h3 id="nosql-nestjs-with-mongoose">NoSQL (NestJS with Mongoose)</h3>
<ul>
<li><p>Normalized Data Model </p>
<pre><code class="language-ts">@Schema()
export class User extends Document {
@Prop()
nickname: string;

// other fields...
}
</code></pre>
</li>
</ul>
<p>export const UserSchema = SchemaFactory.createForClass(User);</p>
<p>@Schema()
export class Follow extends Document {
  @Prop({ type: Types.ObjectId, ref: &#39;User&#39; })
  follower: Types.ObjectId;</p>
<p>  @Prop({ type: Types.ObjectId, ref: &#39;User&#39; })
  following: Types.ObjectId;
}</p>
<p>export const FollowSchema = SchemaFactory.createForClass(Follow);</p>
<pre><code>
- Denormalized Data Model

```ts 
import { Prop, Schema, SchemaFactory } from &#39;@nestjs/mongoose&#39;;
import { Document, Types } from &#39;mongoose&#39;;

@Schema()
export class User extends Document {
  @Prop()
  nickname: string;

  @Prop([{ type: Types.ObjectId, ref: &#39;User&#39; }])
  following: Types.ObjectId[];

  @Prop([{ type: Types.ObjectId, ref: &#39;User&#39; }])
  followers: Types.ObjectId[];

  // other fields...
}

export const UserSchema = SchemaFactory.createForClass(User);</code></pre><p>역으로 Nosql에서는 비정규화 데이터 모델이 주를 이루는 것 같다. 아마 이전프로젝트에서는 팔로우 취소와 팔로우 기능이 빠르게 동작해야했기 때문에, 정규화를 통해 속도향상을 기대하는게 맞았던 것 같다.</p>
<h3 id="nosql-데이터베이스에서-정규화normalization와-비정규화denormalization">NoSQL 데이터베이스에서 정규화(Normalization)와 비정규화(Denormalization)</h3>
<p>NoSQL 데이터베이스에서의 데이터 구조와 성능에 영향을 미치는 중요한 개념인 정규화와 비정규화에 대해 알아보겠습니다.</p>
<h3 id="정규화-normalization">정규화 (Normalization)</h3>
<h4 id="정의">정의:</h4>
<p>정규화는 데이터를 여러 컬렉션에 나누어 저장하고, 이들 사이의 참조를 통해 데이터를 정의하는 방식입니다 (<a href="https://blog.usu.com/schema-design-and-relationship-in-nosql-document-based-databases">blog.usu.com</a>).</p>
<h4 id="장점">장점:</h4>
<ul>
<li>데이터의 중복을 방지하며, 데이터 무결성과 일관성을 유지할 수 있습니다 (<a href="https://www.tutorialspoint.com/difference-between-normalization-and-denormalization">tutorialspoint</a>).</li>
<li>데이터 업데이트가 쉽습니다 (<a href="https://blog.usu.com/schema-design-and-relationship-in-nosql-document-based-databases">blog.usu.com</a>).</li>
</ul>
<h4 id="단점">단점:</h4>
<ul>
<li>데이터를 읽으려면 여러 컬렉션에서 여러 쿼리를 수행해야 하므로 읽기 프로세스가 느려질 수 있습니다 (<a href="https://blog.usu.com/schema-design-and-relationship-in-nosql-document-based-databases">blog.usu.com</a>).</li>
</ul>
<h3 id="비정규화-denormalization">비정규화 (Denormalization)</h3>
<h4 id="정의-1">정의:</h4>
<p>비정규화는 하나의 문서에 대량의 중첩 데이터를 저장하는 방식으로, 이 모델은 읽기 작업을 더 빠르게 수행하지만 삽입 및 업데이트 작업은 느리게 만듭니다 (<a href="https://blog.usu.com/schema-design-and-relationship-in-nosql-document-based-databases">blog.usu.com</a>).</p>
<h4 id="장점-1">장점:</h4>
<ul>
<li>한 번의 쿼리로 모든 관련 정보를 검색할 수 있습니다.</li>
<li>응용 프로그램 코드에서 조인을 구현하거나 populate/lookup (Join)을 사용할 필요가 없습니다.</li>
<li>관련 정보를 단일 원자 작업으로 업데이트할 수 있습니다 (<a href="https://blog.usu.com/schema-design-and-relationship-in-nosql-document-based-databases">blog.usu.com</a>).</li>
</ul>
<h4 id="단점-1">단점:</h4>
<ul>
<li>문서 기반 데이터베이스에는 문서 크기 제한이 있으며, 예를 들어 MongoDB는 단일 문서 항목에 대한 16MB의 제한을 가집니다. 또한 서브 문서의 임베딩 수준도 고려해야 하는 이슈입니다 (<a href="https://blog.usu.com/schema-design-and-relationship-in-nosql-document-based-databases">blog.usu.com</a>).</li>
</ul>
<p>NoSQL에서는 비정규화 방식이 일반적으로 선호되며, 이는 읽기 작업의 성능을 향상시키고 단일 쿼리로 더 많은 데이터를 빠르게 가져오기 때문입니다 (<a href="https://www.techtarget.com/what-is-denormalization-and-how-does-it-work">TechTarget</a>)(<a href="https://www.tutorialspoint.com/difference-between-normalization-and-denormalization">tutorialspoint</a>). 그러나 정규화된 모델은 데이터의 무결성과 일관성을 유지하는 데 더 유리하며, 특히 데이터를 업데이트하거나 수정할 때 유용할 수 있습니다 (<a href="https://blog.usu.com/schema-design-and-relationship-in-nosql-document-based-databases">blog.usu.com</a>).</p>
<h3 id="출처">출처</h3>
<ul>
<li><a href="https://blog.usu.com/schema-design-and-relationship-in-nosql-document-based-databases">https://blog.usu.com/schema-design-and-relationship-in-nosql-document-based-databases</a></li>
<li><a href="https://www.tutorialspoint.com/difference-between-normalization-and-denormalization">https://www.tutorialspoint.com/difference-between-normalization-and-denormalization</a></li>
<li><a href="https://www.techtarget.com/what-is-denormalization-and-how-does-it-work">https://www.techtarget.com/what-is-denormalization-and-how-does-it-work</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[wanted] 원티드 프리온보딩
"Today I learned... 10-25"]]></title>
            <link>https://velog.io/@who_doctor/wanted-%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9Today-I-learned...-10-25</link>
            <guid>https://velog.io/@who_doctor/wanted-%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9Today-I-learned...-10-25</guid>
            <pubDate>Wed, 25 Oct 2023 07:13:03 GMT</pubDate>
            <description><![CDATA[<p>　과제를 받은 오늘 아침, 프로젝트를 시작하기 전에 팀원들과 첫 소통을 했다. 이전에 엘리스 트랙에서 만났던 사람들은 대부분 프론트엔드를 선호했고, 백엔드는 누구도 하고 싶어하지 않는 포지션이었다. </p>
<p>　하지만 여기서는 상황이 달랐다. 나는 지금까지는 백엔드를 적극적으로 담당하고, 잘 모르지만 매번 팀장을 맡으며 리딩해왔지만, 여기선 현 팀원들의 지식과 의견을 듣고 나의 부족함을 많이 느꼈다.  무엇을 설명하려고 해도, 정확한지 확신이 없었고, 백엔드에 열정을 쏟아온 팀원들의 시선이 내게 부끄러움을 주기도 했다. 더군다나 Git 컨벤션등을 지키려고 하는 모습에, 지난 프로젝트를 중구난방으로 커밋했던 내 자신이 그려졌다. 난 그리고 완성된 프로젝트로 기준으로 전부 NoSQL로, RDBMS에는 너무 약했다. 개발 이야기와는 번외로 내게 귀가 두 개가 있고 입이 하나인 이유를 다시 한번 새기면서, 항상 경청해야함을 다시 마음에 새기고 블로그에 포스팅하는 마음을 가진다. </p>
<h4 id="궁금증이-가시지-않는-nosql이-적합할지-rdbms를-써야되는지-gpt는-이렇게-분석했다">궁금증이 가시지 않는 NoSQL이 적합할지, RDBMS를 써야되는지, GPT는 이렇게 분석했다.</h4>
<h3 id="rdbms관계형-데이터베이스-관리-시스템-vs-nosql">RDBMS(관계형 데이터베이스 관리 시스템) vs NoSQL:</h3>
<blockquote>
<p>서비스의 특성을 고려할 때, RDBMS(관계형 데이터베이스 관리 시스템)와 NoSQL 중 어떤 데이터베이스 시스템을 선택할지 결정할 수 있습니다. 아래는 각 시스템의 특징과 서비스 요구사항에 대한 분석을 통해 어떤 시스템이 더 적합할지 제안합니다:</p>
</blockquote>
<h4 id="rdbms">RDBMS:</h4>
<ol>
<li><p><strong>스키마 정의</strong>: </p>
<ul>
<li>RDBMS는 미리 정의된 스키마를 기반으로 데이터를 저장하며, 각 필드의 데이터 타입을 정의해야 합니다. </li>
<li>이는 게시물의 구조가 정해져 있고, 이메일 인증, 로그인, 통계 등의 기능에서 일관된 데이터 처리를 요구할 때 유용합니다.</li>
</ul>
</li>
<li><p><strong>트랜잭션 처리</strong>: </p>
<ul>
<li>RDBMS는 ACID(Atomicity, Consistency, Isolation, Durability) 속성을 지원하여 트랜잭션 처리에 강점이 있습니다. </li>
<li>이는 좋아요나 공유와 같은 동작을 처리할 때 데이터의 일관성을 보장하는데 도움이 됩니다.</li>
</ul>
</li>
<li><p><strong>조인 연산</strong>: </p>
<ul>
<li>RDBMS는 테이블 간의 관계를 통해 조인 연산을 지원합니다. </li>
<li>이는 통계 데이터를 생성하거나 다양한 SNS 플랫폼 간의 데이터를 관리할 때 유용할 수 있습니다.</li>
</ul>
</li>
</ol>
<h4 id="nosql">NoSQL:</h4>
<ol>
<li><p><strong>스케일 아웃</strong>: </p>
<ul>
<li>NoSQL 데이터베이스는 높은 트래픽과 데이터의 빠른 증가를 처리하기 위해 스케일 아웃이 가능합니다. </li>
<li>이는 복수의 SNS 플랫폼에서 큰 데이터를 빠르게 처리할 필요가 있을 때 유용할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>유연한 스키마</strong>: </p>
<ul>
<li>NoSQL은 유연한 스키마를 제공하여, 다양한 데이터 구조를 쉽게 저장하고 관리할 수 있습니다. </li>
<li>이는 다양한 SNS 플랫폼의 데이터 구조를 하나의 시스템에서 처리할 때 유용할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>빠른 읽기/쓰기</strong>: </p>
<ul>
<li>NoSQL은 빠른 읽기 및 쓰기 성능을 제공하며, 대용량 데이터를 효율적으로 처리할 수 있습니다.</li>
</ul>
</li>
</ol>
<h4 id="제안">제안:</h4>
<ul>
<li><strong>해당 어플리케이션의 경우</strong>, 다양한 SNS 플랫폼에서 가져온 데이터를 유연하게 처리할 필요가 있을 것으로 보입니다. 이를 위해 NoSQL 데이터베이스를 사용하는 것이 더 적합할 수 있습니다. 특히, 각 SNS 플랫폼의 데이터 구조가 서로 다를 수 있으며, 빠른 읽기 및 쓰기 성능이 요구될 것으로 예상됩니다.</li>
<li>또한, 서비스가 확장될 경우를 대비하여, NoSQL의 스케일 아웃 기능을 활용하여 높은 트래픽과 데이터의 빠른 증가를 효율적으로 처리할 수 있을 것입니다.</li>
<li>그러나, 통계와 같은 기능에서는 정확한 데이터 처리와 트랜잭션의 일관성이 중요할 수 있으므로, 이러한 부분을 처리하기 위해 RDBMS를 함께 사용하는 혼합 모델을 고려할 수도 있습니다.</li>
</ul>
<blockquote>
<p>최종적으로는 서비스의 요구사항과 팀의 기술 스택, 그리고 미래의 확장성을 고려하여 더 적합한 데이터베이스 시스템을 선택해야 합니다.</p>
</blockquote>
<p>　물론, GPT의 한계로 정확하지 않을 수 있지만, 해당 제안서를 보고 관계가 필요한지, 독립적으로 테이블이 구성되는지 파악할 수 없다는 것은 내게 큰 약점이라는 생각이 들었다. 일단 Nest가 내겐 사실상 처음인 프레임워크이다보니, 이번 업무에서도 유저 인증을 선택했다. 지겨울만도 하지만, 프레임워크가 달라지면 나름 신선하게 다가오기도 해서, 다만, 매번 사용자 인증 관련을 나혼자 개발해왔지만, 이번엔 팀원 한명과 함께 해야하기 때문에 소통하고 소통해야겠다고 다짐했다. 이젠 데이터모델링과 Validate하는 예시를 먼저 검색해보았다.</p>
<h3 id="데이터모델링-및-validate-예시">데이터모델링 및 Validate 예시</h3>
<h4 id="nestjs">NestJS:</h4>
<pre><code class="language-ts">import { Prop, Schema, SchemaFactory } from &#39;@nestjs/mongoose&#39;;
import { Document } from &#39;mongoose&#39;;
import { Validate, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from &#39;class-validator&#39;;

@Schema()
export class User extends Document {
    @Prop({ unique: true })
    account: string;  // Username managed as hashtag

    @Validate(PasswordConstraint)
    @Prop()
    password: string;

    @Prop({ unique: true })
    email: string;
}

export const UserSchema = SchemaFactory.createForClass(User);

@ValidatorConstraint({ name: &#39;ValidPassword&#39;, async: false })
export class PasswordConstraint implements ValidatorConstraintInterface {
    validate(password: string, args: ValidationArguments) {
        if (password.length &lt; 10) return false;

        const commonPasswords = [&#39;123456&#39;, &#39;password&#39;, &#39;123456789&#39;];
        if (commonPasswords.includes(password)) return false;

        if (/^\d+$/.test(password)) return false;

        const types = [/\d/.test(password), /[a-zA-Z]/.test(password), /\W/.test(password)];
        if (types.filter(Boolean).length &lt; 2) return false;

        if (/([a-zA-Z\d\s])\1{2,}/.test(password)) return false;

        return true;
    }

    defaultMessage(args: ValidationArguments) {
        return &#39;Password does not meet complexity requirements!&#39;;
    }
}
</code></pre>
<h4 id="spring-bootkotlin">Spring Boot(Kotlin):</h4>
<pre><code class="language-ts">import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.mongodb.core.mapping.Document

@Document
data class User(
    @Id val id: String? = null,

    @Indexed(unique = true)
    val account: String,  // Username managed as hashtag

    @ValidPassword
    val password: String,

    @Indexed(unique = true)
    val email: String,
    val oldPassword: String? = null
)


@Constraint(validatedBy = [PasswordValidator::class])
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class ValidPassword(
    val message: String = &quot;Invalid password&quot;,
    val groups: Array&lt;KClass&lt;*&gt;&gt; = [],
    val payload: Array&lt;KClass&lt;out Payload&gt;&gt; = []
)

class PasswordValidator : ConstraintValidator&lt;ValidPassword, String&gt; {
    override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
        if (value == null) return false

        if (value.length &lt; 10) return false

        val commonPasswords = listOf(&quot;123456&quot;, &quot;password&quot;, &quot;123456789&quot;)
        if (commonPasswords.contains(value)) return false
s
        if (value.all { it.isDigit() }) return false

        val types = listOf(value.any { it.isDigit() }, value.any { it.isLetter() }, value.any { !it.isLetterOrDigit() })
        if (types.count { it } &lt; 2) return false

        if (value.contains(Regex(&quot;&quot;&quot;(.)\1{2,}&quot;&quot;&quot;))) return false

        return true
    }
}</code></pre>
<p> Express에서도 타입스크립트를 사용하면, 저 데코레이터를 쓸 수 있는 것으로 알고 있다. 내게 스프링에서 어노테이션과 Nest의 데코레이터를 봤을 때, 스프링과 NestJS의 철학을 공유하고 있지 않나, 라는 생각을 갖게 해줬다. </p>
<p><a href="https://github.com/trip-sketch/tripsketch">지난 프로젝트 </a>에서는 스프링 시큐리티 도입을 할까 하다가, 간단한 인터셉터 방식으로 개발을 진행했다. Express와 FastAPI와는 다르게 미들웨어 개념을 쓰지 않아서 처음으로 회원가입을 구현하는데 애를 먹었던 기억이 났다.</p>
<pre><code class="language-ts">@Configuration
class WebMvcConfig(
    private val jwtTokenInterceptor: JwtTokenInterceptor,
) : WebMvcConfigurer {
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(jwtTokenInterceptor)
            .addPathPatterns(&quot;/**&quot;)
            .excludePathPatterns(
                &quot;/swagger-ui/**&quot;,
                &quot;/v3/**&quot;,
                &quot;/api/user/nickname/guest&quot;,
                &quot;/api/follow/guest/followings&quot;,
                &quot;/api/follow/guest/followers&quot;,
                &quot;/api/oauth/kakao/**&quot;,
                &quot;/api/comment/guest/**&quot;,
                &quot;/api/trip/nickname/**&quot;,
                &quot;/api/trip/guest/**&quot;,
                &quot;/api/trip/like/user/**&quot;,
                &quot;/favicon.ico&quot;,
            )
    }
}


@Component
class JwtTokenInterceptor(
    private val jwtService: JwtService,
    @Value(&quot;\${admin.ids}&quot;) private val adminIdsConfig: String,
) : HandlerInterceptor {

    private val adminIds: List&lt;Long&gt; by lazy {
        adminIdsConfig.split(&quot;,&quot;).mapNotNull { it.trim().toLongOrNull() }
    }

    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
        val authorization = request.getHeader(&quot;Authorization&quot;) ?: throw UnauthorizedException(&quot;Authorization 헤더가 없습니다.&quot;)

        val token = authorization.removePrefix(&quot;Bearer &quot;).trim()
        if (!jwtService.validateToken(token)) {
            throw UnauthorizedException(&quot;유효하지 않은 토큰입니다.&quot;)
        }

        val memberId: Long = jwtService.getMemberIdFromToken(token)
        request.setAttribute(&quot;memberId&quot;, memberId)

        if (request.requestURI.contains(&quot;/admin/&quot;)) {
            if (!adminIds.contains(memberId)) {
                throw ForbiddenException(&quot;관리자만 접근 가능합니다.&quot;)
            }
        }
        return true
    }
}</code></pre>
<p>그래서, 미들웨어와는 다르게 오히려 로그인이 필요없는 엔드포인트를 모아 excludePathPatterns에 넣어서, 모든 신호에 토큰 여부를 확인하는 방식으로 구현했다. </p>
<p>Nest에서는 주로 Passport.js와 함께 <a href="https://blog.logrocket.com/how-to-implement-jwt-authentication-nestjs/#:~:text=Now%2C%20let%E2%80%99s%20implement%20a%20JSON,authenticate%20the%20JSON%20web%20token">JWT 인증</a>을 구현을 하는 정보를 습득했다. 이 방식에서는 @nestjs/jwt 및 passport-jwt 패키지를 사용하여 JWT 인증을 처리하고, Passport의 전략을 따르는 것으로 <a href="https://medium.com/@hsndmr/implementing-jwt-authentication-in-nestjs-a-step-by-step-guide-847c1e97e490#:~:text=JWTs%20are%20commonly%20used%20for,The%20server">많은 검색 결과</a>가 나왔다.</p>
<pre><code class="language-ts">@Module({
  imports: [UserModule, PassportModule, JwtModule.register({
    secret: &#39;secretKey&#39;,
    signOptions: { expiresIn: &#39;60s&#39; },
  }), MongooseModule.forFeature([{ name: &quot;user&quot;, schema: UserSchema }])],
  providers: [AuthService, UsersService, LocalStrategy],
  controllers: [AuthController],
})
export class AuthModule { }</code></pre>
<p>Nest의 Module이 내게는 크게 익숙하게 다가오지 않은데, 개발을 하면서 익숙해져야할 포인트처럼 느껴진다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[wanted] Django, MVT패턴을 이용하여 프로젝트 생성]]></title>
            <link>https://velog.io/@who_doctor/wanted-Django-MVT%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@who_doctor/wanted-Django-MVT%ED%8C%A8%ED%84%B4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Sat, 30 Sep 2023 04:28:06 GMT</pubDate>
            <description><![CDATA[<p>Django가 처음이다 보니, 구글링도 하고 GPT도 사용하여 처음으로 프로젝트를 생성해봤다. 보일러 플레이트 원칙을 적용한 Django라고 하지만, 생각보다 해줄 일이 너무 많아서 해매고 당황스러웠다.</p>
<p>MVC패턴을 가진 다른 프레임워크와 다르게 MVT패턴을 통해서 프로젝트를 구성해봤다. 간단하게 말해서 View는 MVC의 View가 아니라 Controller의 위치를 맡고 있다고 한다.</p>
<p>또한 스프링에서 자주 쓰이는 DTO개념은 serializer라는 개념을 통해 쓰고 있었다.</p>
<p>다음은 내가 구성한 프로젝트 Tree이다.</p>
<pre><code class="language-└──">    ├── __init__.py
    ├── asgi.py
    ├── jobpostings
    │   ├── __init__.py
    │   ├── admin.py
    │   ├── apps.py
    │   ├── dao
    │   │   ├── __init__.py
    │   │   ├── applicant_repository.py
    │   │   ├── company_repository.py
    │   │   └── job_posting_repository.py
    │   ├── management
    │   │   └── commands
    │   │       ├── __init__.py
    │   │       └── add_companies.py
    │   ├── migrations
    │   │   ├── 0001_initial.py
    │   │   └── __init__.py
    │   ├── models
    │   │   ├── __init__.py
    │   │   ├── applicant.py
    │   │   ├── company.py
    │   │   └── job_posting.py
    │   ├── serializers
    │   │   ├── __init__.py
    │   │   └── job_posting.py
    │   ├── services
    │   │   ├── __init__.py
    │   │   ├── applicant_service.py
    │   │   ├── company_service.py
    │   │   └── job_posting_service.py
    │   ├── tests
    │   ├── tests.py
    │   ├── urls.py
    │   └── views
    │       ├── __init__.py
    │       └── job_posting.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[wanted] 원티드 프리온보딩 인턴십 백엔드 프레임워크 선택 ]]></title>
            <link>https://velog.io/@who_doctor/wanted-%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%EC%9D%B8%ED%84%B4%EC%8B%AD-%EB%B0%B1%EC%97%94%EB%93%9C-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%84%A0%ED%83%9D</link>
            <guid>https://velog.io/@who_doctor/wanted-%EC%9B%90%ED%8B%B0%EB%93%9C-%ED%94%84%EB%A6%AC%EC%98%A8%EB%B3%B4%EB%94%A9-%EC%9D%B8%ED%84%B4%EC%8B%AD-%EB%B0%B1%EC%97%94%EB%93%9C-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%84%A0%ED%83%9D</guid>
            <pubDate>Sat, 30 Sep 2023 04:16:22 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>사용 가능  언어 및 프레임워크: </p>
</blockquote>
<ul>
<li>Javascript &amp; Node.js</li>
<li>Python &amp; Django</li>
<li>Java &amp; Spring</li>
<li>중 택 1</li>
</ul>
<blockquote>
<p>※ 본 과제 수행 프레임워크는 추후 코스 팀 구성에 활용 됩니다. 참고하시고 코스수행을 희망하는 프레임 워크 선정 바랍니다.</p>
</blockquote>
<p>참가 여부와 상관없이 원티드 10월 프리온보딩 과제를 일단 해보기로 결심했다. 일단 난 Spring은 Kotlin을 통해서 경험해서 프로젝트를 완성시켜본 경험이 있다. 그리고 Express.js도 경험이 있다. 파이썬은, FastAPI를 통해서 개발한 경험이 있다.</p>
<p>사이드프로젝트 중 포기했던 프레임워크 중 선택지 하나가 바로 Django였는데, 아쉽게도 MongoDB하고는 극악의 호환성 (ORM) 때문인 것 같아. 포기했던 기억이 났다.</p>
<p>또 내가 익숙한 프레임워크보다, 포트폴리오에는 없는 프레임워크를 선택하고 싶었다. 이 선택지 중 나에겐 Django 혹은 NestJS였는데, Node.js를 선택할 경우 팀 구성에 있어서 NestJS가 되지 않고 다른 프레임워크(Express.js)가 될 확률도 있었고 그래서 과감하게 Django를 경험해보고 싶어서, Django를 선택했다. MVC패턴이 아닌 프레임워크와 Django의 Admin 등도 워낙 칭찬을 많이 받았어서...!</p>
<p>Django로 간단한 과제를 수행하는 일지를 작성해보고자 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Go] 특별한 할당연산자]]></title>
            <link>https://velog.io/@who_doctor/Go-%ED%8A%B9%EB%B3%84%ED%95%9C-%ED%95%A0%EB%8B%B9%EC%97%B0%EC%82%B0%EC%9E%90</link>
            <guid>https://velog.io/@who_doctor/Go-%ED%8A%B9%EB%B3%84%ED%95%9C-%ED%95%A0%EB%8B%B9%EC%97%B0%EC%82%B0%EC%9E%90</guid>
            <pubDate>Thu, 21 Sep 2023 09:19:33 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-go">x := 10  // x는 int 타입으로 10을 가지게 된다.
y, z := &quot;hello&quot;, 20.5  // y는 string, z는 float64로 각각 할당된다.</code></pre>
<p>:= 짧은 변수 선언 및 할당. 변수를 선언할 때 초기값과 함께 사용하며, 함수 내에서만 사용 가능하다. 이 연산자를 사용하면 Go의 타입 추론 기능을 활용하여 명시적인 타입 선언 없이 변수를 선언하고 초기화할 수 있다.</p>
<p>예제</p>
<pre><code class="language-go">func Solve0921() {
    var str string
    var n int

    // 문자열과 정수를 입력받습니다.
    fmt.Scan(&amp;str, &amp;n)

    // 결과 문자열을 저장할 변수를 선언합니다.
    var result string

    // n만큼 반복하여 result에 str을 추가합니다.
    for i := 0; i &lt; n; i++ {
        result += str
    }

    // 결과를 출력합니다.
    fmt.Println(result)
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Go] fmt vs bufio in Go]]></title>
            <link>https://velog.io/@who_doctor/Go-fmt-vs-bufio-in-Go</link>
            <guid>https://velog.io/@who_doctor/Go-fmt-vs-bufio-in-Go</guid>
            <pubDate>Tue, 19 Sep 2023 10:22:59 GMT</pubDate>
            <description><![CDATA[<h1 id="fmt-vs-bufio-in-go">fmt vs bufio in Go</h1>
<p>Go의 표준 라이브러리 내의 <code>fmt</code>와 <code>bufio</code>는 I/O 작업을 지원하는 패키지이다. 두 패키지는 각각 다음과 같은 특징과 사용 사례를 가집니다.</p>
<h2 id="fmt">fmt</h2>
<ul>
<li><p><strong>기본적인 I/O</strong></p>
<ul>
<li><code>fmt</code>는 기본적인 입출력 작업을 위한 함수들을 제공합니다.</li>
<li>특히 문자열 포매팅에 초점을 맞추고 있습니다.</li>
</ul>
</li>
<li><p><strong>간단한 사용</strong></p>
<ul>
<li><code>fmt.Println()</code>, <code>fmt.Print()</code>, <code>fmt.Printf()</code>, <code>fmt.Scan()</code>, <code>fmt.Scanln()</code> 등의 함수를 포함하여 간단한 I/O 작업을 수행합니다.</li>
</ul>
</li>
<li><p><strong>포맷팅</strong></p>
<ul>
<li><code>Printf</code> 및 <code>Sprintf</code>와 같은 함수들로 문자열 포맷팅을 제공합니다.</li>
</ul>
</li>
</ul>
<h2 id="bufio">bufio</h2>
<ul>
<li><p><strong>버퍼링된 I/O</strong></p>
<ul>
<li><code>bufio</code>는 버퍼링된 입출력을 제공합니다.</li>
<li>큰 데이터 스트림을 효율적으로 처리할 때 유용합니다.</li>
</ul>
</li>
<li><p><strong>텍스트 스캔</strong></p>
<ul>
<li><code>Scanner</code>는 텍스트를 토큰으로 분리하는데 유용하게 사용됩니다.</li>
<li>기본적으로 줄 단위로 입력을 읽지만, 사용자 정의 분리 함수를 제공하여 다른 방식으로 텍스트를 스캔할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>버퍼링된 Reader와 Writer</strong></p>
<ul>
<li><code>bufio.Reader</code>와 <code>bufio.Writer</code>는 버퍼링된 I/O 작업을 위한 여러 메서드를 제공합니다.</li>
<li>예를 들면, <code>ReadBytes</code>, <code>ReadString</code>, <code>WriteString</code> 등이 있습니다.</li>
</ul>
</li>
</ul>
<h3 id="요약">요약</h3>
<p><code>fmt</code>는 기본적이며 포맷팅에 중점을 둔 입출력 작업에 적합합니다. 반면, <code>bufio</code>는 효율적인 버퍼링된 I/O 작업과 텍스트 스캔 기능에 초점을 맞추고 있습니다. 어떤 패키지를 사용할지는 작업의 요구 사항과 성격에 따라 결정됩니다.</p>
<h2 id="프로그래머스-0단계">프로그래머스 0단계</h2>
<h3 id="문자열-출력하기">문자열 출력하기</h3>
<h4 id="fmt만-사용한-경우">fmt만 사용한 경우</h4>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
)

func main() {
    var str string
    fmt.Scan(&amp;str)
    fmt.Println(str)</code></pre>
<h4 id="fmt-os-bufio를-사용한-경우">fmt, os, bufio를 사용한 경우</h4>
<pre><code class="language-go">package main

import (
    &quot;bufio&quot;
    &quot;fmt&quot;
    &quot;os&quot;
)

func main() {
    reader := bufio.NewReader(os.Stdin)
    str, _ := reader.ReadString(&#39;\n&#39;)
    fmt.Print(str)
}</code></pre>
<h3 id="fmt와-os만을-사용한-경우">fmt와 os만을 사용한 경우</h3>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;os&quot;
)

func main() {
    // 버퍼를 생성합니다. 최대 1,000,000 크기를 가정하였습니다.
    buf := make([]byte, 1_000_000)
    n, _ := os.Stdin.Read(buf)

    // 실제로 읽은 바이트만큼 슬라이스를 조절합니다.
    str := string(buf[:n])

    fmt.Print(str)
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Python] Django vs FastAPI]]></title>
            <link>https://velog.io/@who_doctor/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%E3%85%87%E3%85%8B%E3%84%B7%E3%84%B11-%EC%A0%81%EC%A0%88%ED%95%9C-%EC%9B%B9-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-Django-vs-FastAPI</link>
            <guid>https://velog.io/@who_doctor/%EA%B0%9C%EB%B0%9C%EC%9D%BC%EC%A7%80-%E3%85%87%E3%85%8B%E3%84%B7%E3%84%B11-%EC%A0%81%EC%A0%88%ED%95%9C-%EC%9B%B9-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-Django-vs-FastAPI</guid>
            <pubDate>Sat, 26 Aug 2023 07:20:13 GMT</pubDate>
            <description><![CDATA[<p>새로운 프로젝트에 합류하게 되었다. 실제 서비스를 목표로 팀이 구성되었고, 나는 백앤드 개발자로 합류하게 되었다. 나는 고작 프로젝트 3-4개 정도 해본 정도의 초초초 주니어 백엔드 개발자이기에 부담감이 많이 컸지만, 취직을 해야지라는 목표보다, 어떤 서비스라도 만들어 내는 백앤드 개발자가 목표를 잡았기 때문에, 내 역량으로 충분히 가능하다는 것을 보여주고 싶었다.</p>
<p>창업, 즉 스타트업이기에 백앤드 웹 프레임워크를 정하기에 고민이 들었다.</p>
<ol>
<li><p>어떤 언어를 선택할 것인가?</p>
</li>
<li><p>Full vs Micro</p>
</li>
</ol>
<p><a href="https://github.com/nea04184/tripsketch">트립스케치라는 프로젝트</a> 에서 코틀린 JVM의 스프링 부트를 이용한 웹 개발을 진행한 적이 있었는데, 처음으로 사용한 풀스택 웹프레임워크였지만, 개발에 유연한 MongoDB, 그리고 자바가 아닌 코틀린으로 개발해서 그런지, 오히려 마이크로한 웹프레임워크를 사용했을 때의 경험보다 생산성이 낮다고 판단하지 않았다.</p>
<p>그럼에도 JVM 언어가 가지고 있는 한계, 여전히 다른언어에 비해서 불편한 라이브러리 툴 Gradle, JVM 특유의 메모리 점유율 등, 다른 풀 스택 프레임워크는 어떤 느낌일까라는 궁금증이 떠올랐다.</p>
<p>일전에, FastAPI를 통해, <a href="https://github.com/nea04184?tab=repositories">백앤드 서버를 구축한 경험</a>이 있다. </p>
<p>FastAPI는 다음과 같은 장점을 가지고 있었던 것 같다.</p>
<ol>
<li><p>파이썬 기반의 웹 프레임워크지만, 비동기를 지원하는 등 빠른 서버를 제공하는 것 같았다.</p>
</li>
<li><p>문서 자동화 기능은 충분히 매력적이다. 주석 등이 거의 필요하지 않고 대부분을 만들고 테스트할 수 있었다.</p>
</li>
<li><p>파이썬은 다른 언어에 비해 생산성이 높은 언어라고 생각된다. 어떤 언어라도 깊게 파고 들면 들수록 어렵지만, 그래도 파이썬이 가지고 있는 낮은 학습곡선은 큰 장점이라고 생각한다</p>
</li>
</ol>
<p>단점은 다음과 같다.</p>
<ol>
<li><p>비교적 신생 웹프레임워크라 공식문서나 자료가 적다.</p>
</li>
<li><p>나름 풀스택과 마이크로의 중간위치를 점하려고 했다는 생각이 들었지만, 이것도 저것도 아니라는 포지션이 스프링과 Express.js를 경험해본 나한텐, 애매하다라는 생각이 들었다.</p>
</li>
</ol>
<p>Python의 대표적인 풀 스택 프레임워크인 Django는 내가 파악하기로 다음과 같은 장점이 있었다.</p>
<ol>
<li><p>보일러 플레이트라는 원칙에 다양한 기능을 이미 프레임워크 단에서 제공</p>
</li>
<li><p>Admin 페이지의 제공해서 생산성 향상</p>
</li>
</ol>
<p>단점은 다음과 같다.</p>
<ol>
<li><p>파이썬 특유의 느린 속도</p>
</li>
<li><p>또한 우리 서비스는 채팅서비스를 제공해야하는데, 비동기 기능이 완벽하지 않다.</p>
</li>
</ol>
<p>다음 편은, 결국 어떤 프레임워크를 선택하게 되었고 그 이유와 과정을 이야기하는 시간이 되었으면 좋겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코틀린] 엘비스 연산자]]></title>
            <link>https://velog.io/@who_doctor/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%97%98%EB%B9%84%EC%8A%A4-%EC%97%B0%EC%82%B0%EC%9E%90</link>
            <guid>https://velog.io/@who_doctor/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%97%98%EB%B9%84%EC%8A%A4-%EC%97%B0%EC%82%B0%EC%9E%90</guid>
            <pubDate>Thu, 27 Jul 2023 06:11:30 GMT</pubDate>
            <description><![CDATA[<p>&quot;엘비스(Elvis)&quot; 연산자는 코틀린에서 사용되는 특별한 연산자로, null 체크를 간편하게 수행하는데 사용됩니다. 느낌표와 콜론 기호(?:)를 조합하여 표현합니다. 이 연산자는 주로 null인지 아닌지를 확인하고, null인 경우 대체값을 지정하는 용도로 활용됩니다.</p>
<p>엘비스 연산자의 구문은 다음과 같습니다:</p>
<pre><code class="language-kotlin">val result = nullableValue ?: defaultValue</code></pre>
<p>여기서 nullableValue는 null일 수 있는 변수나 표현식을 나타내고, defaultValue는 nullableValue가 null인 경우 사용할 기본값을 의미합니다.</p>
<p>예를 들어, 아래와 같은 코드는 name 변수가 null인지 아닌지를 확인하고, null인 경우 기본값으로 &quot;Unknown&quot;을 사용하는 예제입니다:</p>
<pre><code class="language-kotlin">fun main() {
    val name: String? = null

    // 엘비스 연산자를 사용하여 null 체크와 대체값 지정
    val result = name ?: &quot;Unknown&quot;

    // result 변수는 name이 null이면 &quot;Unknown&quot;이 할당되고, 그렇지 않으면 name의 값이 할당됨
    println(result)
}</code></pre>
<pre><code class="language-kotlin">// 놀이터에 온 친구의 나이를 저장하는 변수야.
// 친구가 아직 나이를 말하지 않았으면 null로 표시할 거야.
var 친구나이: Int? = null

// 만약 친구가 나이를 말해주었다면 그 값을 출력하는 함수야.
// 나이를 모르면 대신 &quot;나이를 모르겠어요!&quot;를 출력할 거야.
fun 나이출력() {
    // 엘비스 연산자를 사용해 나이가 null인지 아닌지를 확인해볼게.
    // 만약 나이가 null이면 &quot;나이를 모르겠어요!&quot;를 출력하고, null이 아니면 친구의 나이를 출력할 거야.
    val 나이결과 = 친구나이 ?: &quot;나이를 모르겠어요!&quot;

    println(나이결과)
}

fun main() {
    // 놀이터에 온 친구가 나이를 말해주었어요.
    친구나이 = 8

    // 나이를 출력하는 함수를 호출할게요.
    // 친구가 나이를 말해줬으므로 친구의 나이인 8이 출력될 거야.
    나이출력()

    // 이번에는 놀이터에 온 친구가 나이를 말하지 않았어요.
    // 나이를 모르는 상태에서 나이 출력 함수를 호출해볼게요.
    // 이제 나이를 모르기 때문에 &quot;나이를 모르겠어요!&quot;가 출력될 거야.
    친구나이 = null
    나이출력()
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[코틀린] lateinit 개념]]></title>
            <link>https://velog.io/@who_doctor/%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%98-lateinit-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@who_doctor/%EC%BD%94%ED%8B%80%EB%A6%B0%EC%9D%98-lateinit-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Thu, 27 Jul 2023 04:07:44 GMT</pubDate>
            <description><![CDATA[<ol>
<li><p>lateinit으로 선언된 프로퍼티는 기본 타입(Int, Long, Double 등)으로 선언할 수 없습니다. 오직 클래스 타입 프로퍼티만 늦은 초기화가 가능합니다.</p>
</li>
<li><p>lateinit 프로퍼티는 var로 선언되어야 합니다. 즉, 변경 가능한 변수로 선언되어야 하며 val로는 사용할 수 없습니다.</p>
</li>
<li><p>lateinit 프로퍼티를 사용하기 전에 반드시 초기화가 되어야 합니다. 그렇지 않으면 &quot;lateinit property has not been initialized&quot;과 같은 예외가 발생합니다.</p>
</li>
<li><p>초기화 전에 프로퍼티에 접근하려면 프로퍼티 타입에 대한 기본값이 아니어야 합니다. 기본값으로 초기화하는 것이 필요한 경우 lateinit을 사용할 수 없습니다</p>
</li>
</ol>
<pre><code class="language-kotlin">// 코틀린 코드를 이해하기 쉽게 설명해줄게!
// 이 클래스는 &quot;늦은 초기화&quot;를 사용하는 놀이터 예제야.

class 놀이터 {
    // 놀이터에 있는 친구의 이름을 저장하는 변수야.
    // 우리는 처음에 이 친구의 이름을 모르지만, 나중에 놀이터에서 만나서 이름을 알게 될 거야!
    lateinit var 친구이름: String

    // 친구의 이름을 초기화하는 함수야.
    fun 이름설정() {
        // 이 함수에서 우리는 친구의 이름을 &quot;존 도우&quot;로 설정해줄 거야.
        친구이름 = &quot;존 도우&quot;
    }

    // 친구의 이름을 출력하는 함수야.
    fun 이름출력() {
        // 친구의 이름이 초기화되었는지 확인하는 거야.
        // 만약 초기화가 안 되어있으면 &quot;아직 이름을 모르겠어요!&quot;라고 출력할 거야.
        if (::친구이름.isInitialized) {
            // 초기화가 되어있으면 친구의 이름을 출력해줄 거야.
            println(친구이름)
        } else {
            println(&quot;아직 이름을 모르겠어요!&quot;)
        }
    }
}

fun main() {
    // 놀이터 클래스를 사용해서 놀이터 객체를 만들어줄 거야.
    val 놀이터객체 = 놀이터()

    // 아직 우리는 놀이터 친구의 이름을 모르지만, 이름을 설정하는 함수를 호출해서 &quot;존 도우&quot;로 설정해줄 거야.
    놀이터객체.이름설정()

    // 이제 우리는 놀이터 친구의 이름을 알게 되었어요!
    // 그래서 이름을 출력하는 함수를 호출하면 &quot;존 도우&quot;가 출력될 거야.
    놀이터객체.이름출력()
}
</code></pre>
]]></description>
        </item>
    </channel>
</rss>